session-orchestrator 3.20.0 → 3.22.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.
Files changed (202) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/000-session-orchestrator.mdc +3 -2
  5. package/.cursor/rules/030-wave-execution.mdc +10 -8
  6. package/.cursor/rules/040-discovery.mdc +6 -6
  7. package/.cursor/rules/050-plan.mdc +8 -8
  8. package/CHANGELOG.md +515 -0
  9. package/README.md +16 -11
  10. package/agents/analyst.md +1 -1
  11. package/agents/architect-reviewer.md +1 -1
  12. package/agents/code-implementer.md +4 -2
  13. package/agents/db-specialist.md +1 -1
  14. package/agents/dialectic-deriver.md +1 -1
  15. package/agents/docs-writer.md +1 -1
  16. package/agents/memory-proposal-collector.md +7 -5
  17. package/agents/qa-strategist.md +1 -1
  18. package/agents/security-reviewer.md +1 -1
  19. package/agents/session-reviewer.md +42 -1
  20. package/agents/skill-applied-judge.md +1 -1
  21. package/agents/test-writer.md +1 -1
  22. package/agents/ui-developer.md +1 -1
  23. package/agents/ux-evaluator.md +1 -1
  24. package/commands/eli5.md +33 -0
  25. package/commands/release.md +62 -0
  26. package/commands/test.md +2 -2
  27. package/docs/components.md +6 -5
  28. package/docs/migration-v3.md +9 -6
  29. package/docs/persona-panel.md +3 -1
  30. package/docs/scope-collision-guard.md +167 -0
  31. package/docs/session-config-reference.md +31 -8
  32. package/hooks/_lib/lock-bootstrap.mjs +19 -13
  33. package/hooks/enforce-scope.mjs +103 -3
  34. package/hooks/hooks-codex.json +1 -1
  35. package/hooks/hooks.json +21 -1
  36. package/hooks/on-session-end.mjs +76 -97
  37. package/hooks/on-session-start.mjs +195 -104
  38. package/hooks/on-stop.mjs +127 -12
  39. package/hooks/post-bash-write-verify.mjs +8 -32
  40. package/hooks/pre-auq-clarity.mjs +787 -0
  41. package/hooks/pre-bash-issue-budget.mjs +17 -18
  42. package/hooks/pre-task-scope-disjoint.mjs +1042 -0
  43. package/package.json +3 -1
  44. package/pi/prompts/eli5.md +12 -0
  45. package/pi/prompts/release.md +12 -0
  46. package/scripts/auq-audit.mjs +825 -0
  47. package/scripts/autopilot.mjs +10 -9
  48. package/scripts/emit-session.mjs +42 -0
  49. package/scripts/export-hw-learnings.mjs +61 -2
  50. package/scripts/lib/auq/clarity.mjs +1314 -0
  51. package/scripts/lib/auq/parse.mjs +1006 -0
  52. package/scripts/lib/auq/schema.mjs +1457 -0
  53. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  54. package/scripts/lib/backlog-scan.mjs +106 -15
  55. package/scripts/lib/build-live-signals.mjs +7 -3
  56. package/scripts/lib/ci-status-banner.mjs +267 -77
  57. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
  58. package/scripts/lib/config/vault-integration.mjs +12 -1
  59. package/scripts/lib/dispatcher/rank.mjs +4 -7
  60. package/scripts/lib/gates/gate-full.mjs +3 -3
  61. package/scripts/lib/gates/gate-helpers.mjs +17 -6
  62. package/scripts/lib/git-config-drift.mjs +471 -0
  63. package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
  64. package/scripts/lib/io.mjs +432 -7
  65. package/scripts/lib/issue-budget.mjs +63 -9
  66. package/scripts/lib/learnings/select.mjs +157 -3
  67. package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
  68. package/scripts/lib/mirror-issues-banner.mjs +266 -0
  69. package/scripts/lib/named-vault-resolver.mjs +105 -16
  70. package/scripts/lib/owner-interview.mjs +78 -32
  71. package/scripts/lib/peer-cards/schema.mjs +6 -2
  72. package/scripts/lib/peer-discovery.mjs +73 -22
  73. package/scripts/lib/project-hygiene.mjs +64 -4
  74. package/scripts/lib/reconcile/renderer.mjs +17 -4
  75. package/scripts/lib/reconcile/writer.mjs +69 -30
  76. package/scripts/lib/redact-spans.mjs +89 -0
  77. package/scripts/lib/resource-probe/evaluate.mjs +330 -149
  78. package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
  79. package/scripts/lib/resource-probe.mjs +18 -2
  80. package/scripts/lib/scope-baseline.mjs +77 -17
  81. package/scripts/lib/scope-gate.mjs +658 -0
  82. package/scripts/lib/secret-masker.mjs +262 -0
  83. package/scripts/lib/session-lock.mjs +34 -10
  84. package/scripts/lib/session-registry.mjs +9 -1
  85. package/scripts/lib/spiral-carryover.mjs +23 -2
  86. package/scripts/lib/state-md/mission-status.mjs +164 -58
  87. package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
  88. package/scripts/lib/validate/check-agents.mjs +77 -5
  89. package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
  90. package/scripts/lib/validate/check-commands.mjs +2 -20
  91. package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
  92. package/scripts/lib/validate/check-hooks-symmetry.mjs +48 -0
  93. package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
  94. package/scripts/lib/validate/check-rules.mjs +153 -9
  95. package/scripts/lib/validate/check-skills.mjs +191 -0
  96. package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
  97. package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
  98. package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
  99. package/scripts/lib/validate/check-unwired-features.mjs +219 -11
  100. package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
  101. package/scripts/lib/validate/frontmatter-block.mjs +61 -0
  102. package/scripts/lib/validate/tier-inference.mjs +46 -8
  103. package/scripts/lib/vault-backfill/glab.mjs +91 -58
  104. package/scripts/lib/vault-backfill/manifest.mjs +28 -8
  105. package/scripts/lib/vault-mirror/namespace.mjs +146 -1
  106. package/scripts/lib/vault-mirror/process.mjs +264 -31
  107. package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
  108. package/scripts/lib/vault-status/board-writer.mjs +300 -56
  109. package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
  110. package/scripts/lib/vcs-repo-spec.mjs +680 -30
  111. package/scripts/lib/wave-resource-gate.mjs +67 -73
  112. package/scripts/materialize-wave-scope.mjs +281 -0
  113. package/scripts/print-learnings-index.mjs +30 -3
  114. package/scripts/release.mjs +983 -107
  115. package/scripts/run-quality-gate.mjs +14 -0
  116. package/scripts/site-numbers.mjs +1049 -0
  117. package/scripts/validate-plugin.mjs +64 -0
  118. package/scripts/validate-wave-scope.mjs +286 -12
  119. package/scripts/vault-backfill.mjs +32 -5
  120. package/scripts/vault-mirror.mjs +26 -1
  121. package/skills/_shared/monitor-patterns.md +24 -4
  122. package/skills/_shared/parallel-aware-auq.md +30 -24
  123. package/skills/_shared/parallel-aware-preamble.md +31 -2
  124. package/skills/_shared/state-ownership.md +49 -6
  125. package/skills/bootstrap/SKILL.md +2 -1
  126. package/skills/brainstorm/SKILL.md +18 -18
  127. package/skills/brainstorm/soul.md +12 -0
  128. package/skills/claude-md-drift-check/SKILL.md +9 -1
  129. package/skills/debug/SKILL.md +4 -1
  130. package/skills/discovery/SKILL.md +28 -24
  131. package/skills/discovery/issue-templates.md +4 -4
  132. package/skills/discovery/probes-code.md +2 -2
  133. package/skills/discovery/probes-feature.md +6 -6
  134. package/skills/discovery/probes-infra.md +2 -2
  135. package/skills/discovery/probes-session.md +5 -5
  136. package/skills/dispatcher/SKILL.md +10 -1
  137. package/skills/eli5/SKILL.md +43 -0
  138. package/skills/evolve/SKILL.md +8 -9
  139. package/skills/frontmatter-guard/SKILL.md +9 -1
  140. package/skills/gitlab-ops/SKILL.md +73 -59
  141. package/skills/gitlab-portfolio/SKILL.md +10 -1
  142. package/skills/grill/SKILL.md +6 -6
  143. package/skills/grill/soul.md +16 -0
  144. package/skills/memory-cleanup/SKILL.md +20 -7
  145. package/skills/npm-publish/SKILL.md +23 -51
  146. package/skills/peekaboo-driver/SKILL.md +3 -3
  147. package/skills/persona-panel/SKILL.md +3 -1
  148. package/skills/plan/SKILL.md +18 -16
  149. package/skills/plan/mode-feature.md +1 -1
  150. package/skills/plan/mode-new.md +42 -12
  151. package/skills/plan/soul.md +12 -0
  152. package/skills/reconcile/SKILL.md +3 -3
  153. package/skills/repo-audit/SKILL.md +10 -1
  154. package/skills/session-end/SKILL.md +97 -22
  155. package/skills/session-end/metrics-collection.md +1 -1
  156. package/skills/session-end/phase-3-6-tail.md +37 -2
  157. package/skills/session-end/session-metrics-write.md +4 -10
  158. package/skills/session-plan/SKILL.md +2 -2
  159. package/skills/session-plan/wave-template.md +1 -1
  160. package/skills/session-start/SKILL.md +82 -36
  161. package/skills/session-start/phase-2-5-docs-planning.md +8 -8
  162. package/skills/session-start/phase-4-5-resource-health.md +82 -19
  163. package/skills/session-start/soul.md +110 -0
  164. package/skills/spinout/SKILL.md +5 -1
  165. package/skills/sunset-review/SKILL.md +11 -1
  166. package/skills/test-runner/SKILL.md +2 -2
  167. package/skills/tmux-layout/SKILL.md +7 -2
  168. package/skills/using-orchestrator/SKILL.md +1 -1
  169. package/skills/vault-mirror/SKILL.md +10 -1
  170. package/skills/vault-sync/SKILL.md +10 -1
  171. package/skills/vault-sync/validator.mjs +55 -6
  172. package/skills/wave-executor/wave-loop.md +64 -12
  173. package/skills/write-executable-plan/SKILL.md +6 -6
  174. package/scripts/lib/mission-status-schema.mjs +0 -114
  175. package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
  176. package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
  177. package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
  178. package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
  179. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
  180. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
  181. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
  182. package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
  183. package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
  184. package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
  185. package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
  186. package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
  187. package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
  188. package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
  189. package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
  190. package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
  191. package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
  192. package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
  193. package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
  194. package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
  195. package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
  196. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
  197. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
  198. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
  199. package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
  200. package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
  201. package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
  202. package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
@@ -42,7 +42,7 @@ This runs BEFORE the local session-lock acquire in Phase 1.2 — the preamble's
42
42
  **Outcome handling:**
43
43
  - `PASS_THROUGH` → continue to Phase 1
44
44
  - `EXCLUSIVE_BLOCKED` → exit Phase 0 cleanly per the AUQ outcome (`Warten` / `Andere Session beenden` / `Abbrechen` — all three return without initializing STATE.md)
45
- - `PROMOTION_OFFER` with user picking "Worktree anlegen + starten" → call `enterWorktree({ basePath, sessionId, branch, repoRoot })` from `scripts/lib/autopilot/worktree-pipeline.mjs`. Compute params: `basePath = path.dirname(repoRoot)`, `sessionId` from resolveSemanticSessionId(), `branch` from current HEAD, `repoRoot = process.cwd()`. On success, exit Phase 0 immediately — the new worktree's own session-start runs from scratch (Phase 1 onwards), Phase 1.2 session-lock-acquire is the new worktree's responsibility. On enterWorktree failure (`WorktreeBoundaryError` or `git worktree add` non-zero exit), emit stderr WARN `parallel-aware: enterWorktree failed: <err>; falling back to Manuell` and proceed via the Manuell path.
45
+ - `PROMOTION_OFFER` with user picking "Worktree anlegen + starten" → call `enterWorktree({ basePath, sessionId, branch, repoRoot })` from `scripts/lib/autopilot/worktree-pipeline.mjs`. Compute params: `basePath = path.dirname(repoRoot)`, `sessionId` from resolveSemanticSessionId() **for the worktree-name attribution label only**, `branch` from current HEAD, `repoRoot = process.cwd()`. It is not a lock/registry ownership key; the new worktree's Phase 1.2 obtains its own physical raw `session_id`. On success, exit Phase 0 immediately — the new worktree's own session-start runs from scratch (Phase 1 onwards), Phase 1.2 session-lock-acquire is the new worktree's responsibility. On enterWorktree failure (`WorktreeBoundaryError` or `git worktree add` non-zero exit), emit stderr WARN `parallel-aware: enterWorktree failed: <err>; falling back to Manuell` and proceed via the Manuell path.
46
46
  - `PROMOTION_OFFER` with user picking "Manuell — in-place daneben" → append Deviation, continue to Phase 1
47
47
  - `PROMOTION_OFFER` with user picking "Abbrechen" → exit cleanly
48
48
 
@@ -112,14 +112,14 @@ if (content && !isDispatcherAutonomyBlockPresent(content)) {
112
112
 
113
113
  Acquire a distributed session-lock to detect parallel sessions in the same repo before initializing STATE.md. This prevents two concurrent Claude/Codex sessions from stomping each other's wave state and metrics writes.
114
114
 
115
- **Mechanical wiring (Epic #583, 2026-05-27):** The SessionStart hook (`hooks/on-session-start.mjs` → `hooks/_lib/lock-bootstrap.mjs`) now writes `.orchestrator/session.lock` mechanically BEFORE this skill's prose runs. The prose Phase 1.2 becomes confirmatory — it verifies the lock exists with the expected shape via `readLock({ repoRoot: process.cwd() })`. Re-call `acquire()` only if `readLock()` returns `null` (mechanical hook failed) OR the existing lock's `session_id` does not match the current session's id (a rare divergence — surface via AUQ before overwriting). The decision flow below still applies to all three outcomes (active / stale / fs-error) when the prose path needs to acquire.
115
+ **Mechanical wiring (Epic #583, 2026-05-27):** The SessionStart hook (`hooks/on-session-start.mjs` → `hooks/_lib/lock-bootstrap.mjs`) now writes `.orchestrator/session.lock` mechanically BEFORE this skill's prose runs. The prose Phase 1.2 becomes confirmatory — it verifies the lock exists with the expected shape via `readLock({ repoRoot: process.cwd() })`. Re-call `acquire()` only if `readLock()` returns `null` (mechanical hook failed) OR the existing lock's raw `session_id` does not exactly match the current session's raw id (a rare divergence — surface via AUQ before overwriting). A matching `semantic_session_id`, STATE.md `session`, or owner proof cannot repair that mismatch. The decision flow below still applies to all three outcomes (active / stale / fs-error) when the prose path needs to acquire.
116
116
 
117
117
  ```javascript
118
118
  import { acquire, forceAcquire } from 'scripts/lib/session-lock.mjs';
119
119
  const result = acquire({ sessionId, mode: sessionType, ttlHours: 4, repoRoot: process.cwd() });
120
120
  ```
121
121
 
122
- Where `sessionId` is the session identifier derived from the session type and timestamp (e.g. `main-2026-05-08-deep-1`), and `sessionType` is the session mode (`housekeeping`, `feature`, or `deep`).
122
+ Where `sessionId` is the physical raw identity for this invocation: the native harness-provided raw id, or a generated UUID when no trustworthy raw id exists. It is the only value passed to `acquire()` and the only live lock/registry ownership key. `semanticSessionId` may be recorded separately as an attribution/history label and may populate STATE.md `session`; neither label is a substitute for `sessionId`. `sessionType` is the session mode (`housekeeping`, `feature`, or `deep`).
123
123
 
124
124
  ### Decision flow
125
125
 
@@ -131,21 +131,21 @@ Where `sessionId` is the session identifier derived from the session type and ti
131
131
  ```js
132
132
  AskUserQuestion({
133
133
  questions: [{
134
- question: `Another session lock is active in this repo (started ${ageHours}h ago, mode=${existingLock.mode}, host=${existingLock.host}, pid=${existingLock.pid}). How should I proceed?`,
135
- header: "Session Lock Conflict",
134
+ question: `Another session holds the lock here started ${ageHours}h ago, mode=${existingLock.mode}, host=${existingLock.host}, pid=${existingLock.pid}. Wait, or take the lock?`,
135
+ header: "Session lock",
136
136
  multiSelect: false,
137
137
  options: [
138
- { label: "Abort (Recommended)", description: "Let the other session finish. Safe default prevents metrics and wave-state corruption." },
139
- { label: "Force-take the lock", description: "Overwrites the active lock. ONLY use if you are certain the other session is no longer running." },
138
+ { label: "Abort (Recommended)", description: "Stop here and let the other session finish, then start again. Nothing is written until it releases the lock, and two sessions sharing one wave state overwrite each other's metrics." },
139
+ { label: "Force-take the lock", description: "Overwrites the active lock and starts anyway. Only when that session is certainly gone — otherwise both keep writing the same wave state and one of them loses everything." },
140
140
  ],
141
141
  }],
142
142
  });
143
143
  ```
144
144
  - **Codex CLI / Cursor IDE fallback (numbered Markdown list):**
145
145
  ```
146
- Session lock conflict active lock detected (started <ageHours>h ago, mode=<mode>, host=<host>, pid=<pid>).
147
- 1. Abort (Recommended) — let the other session finish.
148
- 2. Force-take the lock — ONLY if the other session is known dead.
146
+ Another session holds the lock here started <ageHours>h ago, mode=<mode>, host=<host>, pid=<pid>. Wait, or take the lock?
147
+ 1. Abort (Recommended) — stop here and let the other session finish, then start again; nothing is written until it releases the lock.
148
+ 2. Force-take the lock — overwrites the active lock. Only when that session is certainly gone, otherwise both keep writing the same wave state and one loses everything.
149
149
  Reply with the number of your choice.
150
150
  ```
151
151
  - On **Abort**: exit session-start cleanly with a brief stderr note (`session-lock: aborted — active lock held by session_id=<id>`). Do NOT initialize STATE.md.
@@ -158,21 +158,21 @@ Where `sessionId` is the session identifier derived from the session type and ti
158
158
  ```js
159
159
  AskUserQuestion({
160
160
  questions: [{
161
- question: `Stale session lock found (started ${ageHours}h ago, ttl=${existingLock.ttl_hours}h). Process pid=${existingLock.pid} on host=${existingLock.host} is ${reason === 'stale-pid-dead' ? 'confirmed dead' : 'still running or status unknown'}. Reclaim the lock?`,
162
- header: "Stale Session Lock",
161
+ question: `A stale session lock is in the way — started ${ageHours}h ago, its ttl=${existingLock.ttl_hours}h has expired, and pid=${existingLock.pid} on host=${existingLock.host} is ${reason === 'stale-pid-dead' ? 'confirmed dead' : 'still running or status unknown'}. Reclaim it?`,
162
+ header: "Stale lock",
163
163
  multiSelect: false,
164
164
  options: [
165
- { label: "Reclaim (Recommended)", description: "Overwrite the stale lock and continue. Safe when the previous session is no longer active." },
166
- { label: "Abort — investigate manually", description: "Stop here. Inspect .orchestrator/session.lock before proceeding." },
165
+ { label: "Reclaim (Recommended)", description: "Overwrites the stale lock and continues, because its time-to-live has run out. When that process is really dead, nothing of the old session is lost." },
166
+ { label: "Abort — investigate manually", description: "Stops here and writes nothing. The lock file `.orchestrator/session.lock` (it names the process that wrote it) tells you whether that session is still alive." },
167
167
  ],
168
168
  }],
169
169
  });
170
170
  ```
171
171
  - **Codex CLI / Cursor IDE fallback (numbered Markdown list):**
172
172
  ```
173
- Stale session lock found (started <ageHours>h ago, ttl=<ttlHours>h, pid=<pid> on <host>).
174
- 1. Reclaim (Recommended) — overwrite stale lock and continue.
175
- 2. Abort — investigate .orchestrator/session.lock manually.
173
+ A stale session lock is in the way — started <ageHours>h ago, ttl=<ttlHours>h expired, pid=<pid> on <host>. Reclaim it?
174
+ 1. Reclaim (Recommended) — overwrites the stale lock and continues, because its time-to-live has run out and that process is no longer holding anything.
175
+ 2. Abort — stops here and writes nothing. The lock file `.orchestrator/session.lock` (it names the process that wrote it) tells you whether that session is still alive.
176
176
  Reply with the number of your choice.
177
177
  ```
178
178
  - On **Reclaim**: call `forceAcquire({ sessionId, mode: sessionType, ttlHours: 4, repoRoot: process.cwd() })`. After Phase 1.5 initializes STATE.md, append a deviation:
@@ -196,11 +196,14 @@ When `existingLock.host !== os.hostname()`, PID liveness cannot be checked (`pid
196
196
 
197
197
  > Skip this phase if `persistence` config is `false`.
198
198
 
199
- After Phase 1.2 acquires (or confirms) the lock, call `checkPeerStateMd(repoRoot, sessionId)` from `scripts/lib/state-md-peer-guard.mjs`. This catches the rare case where lock-based detection missed an active peer (e.g., the peer's `session.lock` was force-deleted by an out-of-band sweep but STATE.md is still `status: active`, OR the peer's registry write succeeded but the lock-bootstrap hook crashed before the lock landed).
199
+ After Phase 1.2 acquires (or confirms) the lock, use `findPeers(repoRoot, { mySessionId: callerSessionHint })` for the STATE.md peer guard. `callerSessionHint` is the original semantic attribution label when one exists, otherwise the raw `sessionId`: `findPeers` may translate the semantic hint for the discovered lock/registry surface only after the exact raw binding check in `parallel-aware-preamble.md`, while keeping the original hint for STATE.md. This catches the rare case where lock-based detection missed an active peer (e.g., the peer's `session.lock` was force-deleted by an out-of-band sweep but STATE.md is still `status: active`, OR the peer's registry write succeeded but the lock-bootstrap hook crashed before the lock landed).
200
200
 
201
201
  ```javascript
202
202
  import { findPeers } from '$PLUGIN_ROOT/scripts/lib/peer-discovery.mjs';
203
- const { peers } = await findPeers(process.cwd(), { mySessionId: sessionId });
203
+ // Keep the STATE.md comparison in its original attribution-label space.
204
+ // findPeers performs the guarded semantic→raw translation only for discovered peers.
205
+ const callerSessionHint = semanticSessionId ?? sessionId;
206
+ const { peers } = await findPeers(process.cwd(), { mySessionId: callerSessionHint });
204
207
  const peer = peers.find((p) => p.source === 'state-md') ?? null;
205
208
  // Phase 1.2.1 consumes only the 'state-md' subset (STATE.md surface only).
206
209
  if (peer) {
@@ -367,29 +370,60 @@ If `snaps.length >= 1` → present the following choice:
367
370
 
368
371
  **Claude Code (AskUserQuestion):**
369
372
 
373
+ Before asking, read what "Recover" would actually put back — the operator decides on that diff, not on the word:
374
+
375
+ ```js
376
+ import { execFileSync } from 'node:child_process';
377
+
378
+ // Read-only: `git stash show` prints a diffstat and never touches the working tree.
379
+ // Capped at 12 lines so the preview box stays shorter than the option list beside it.
380
+ const stat = execFileSync('git', ['stash', 'show', '--stat', snaps[0].sha], { encoding: 'utf8' })
381
+ .split('\n').slice(0, 12).join('\n');
382
+ const refs = snaps.map((s) => s.ref).join('\n');
383
+ ```
384
+
370
385
  ```js
371
386
  AskUserQuestion({
372
387
  questions: [{
373
- question: `Found ${snaps.length} coordinator snapshot(s) from the resumed session (latest from ${humanAgeOf(snaps[0].createdAt)}). Recover, keep as backup, or discard?`,
388
+ question: `${snaps.length} snapshot(s) from the resumed session, newest ${humanAgeOf(snaps[0].createdAt)}. Recover, keep, discard?`,
374
389
  header: "Snapshot",
375
390
  multiSelect: false,
376
391
  options: [
377
- { label: "Recover (diff vs current tree) (Recommended)", description: "Apply the latest snapshot back onto the working tree. You will see a diff and can unstage unwanted changes before committing." },
378
- { label: "Keep as backup", description: "Leave refs/so-snapshots/* in place untouched. You can recover manually later via `git stash apply $(git rev-parse <ref>)`." },
379
- { label: "Discard all", description: "Delete all refs/so-snapshots/<sessionId>/* immediately via deleteSnapshot." },
392
+ {
393
+ label: "Recover (Recommended)",
394
+ description: "Puts the newest saved state back into your working tree and commits nothing. You can drop any of those changes afterwards.",
395
+ preview: `These files come back:\n\n\`\`\`\n${stat}\n\`\`\``,
396
+ },
397
+ {
398
+ label: "Keep as backup",
399
+ description: "Nothing happens now: `refs/so-snapshots/*` (the saved states) stay, and `git stash apply $(git rev-parse <ref>)` (this puts one back) works later.",
400
+ },
401
+ {
402
+ label: "Discard all",
403
+ description: "Deletes every saved state of this session for good: `refs/so-snapshots/<sessionId>/*` (all of them) is gone, and there is no second copy.",
404
+ preview: `Deleted for good:\n\n\`\`\`\n${refs}\n\`\`\``,
405
+ },
380
406
  ],
381
407
  }],
382
408
  });
383
409
  ```
384
410
 
411
+ `preview` renders beside the option list and only works with `multiSelect: false`. It is used here because the answer decides which literal text lands in the working tree — "Recover" is a diff, "Discard all" is a list of refs that stop existing. "Keep as backup" carries none: keeping is exactly the state the operator already sees.
412
+
385
413
  **Codex CLI / Cursor IDE fallback (numbered Markdown list):**
386
414
 
415
+ These harnesses have no preview box, so the same diffstat is printed inline — it is the only place the operator ever sees it:
416
+
387
417
  ```markdown
388
- Snapshot recovery options:
418
+ "Recover" would put these files back:
419
+
420
+ <git stash show --stat <snaps[0].sha>, capped at 12 lines>
389
421
 
390
- 1. **Recover (Recommended)** Apply the latest snapshot back onto the working tree. You will see a diff and can unstage unwanted changes before committing.
391
- 2. **Keep as backup** — Leave the refs in place untouched. You can recover manually later.
392
- 3. **Discard all** — Delete all refs/so-snapshots/<sessionId>/* immediately.
422
+ <N> snapshot(s) from the resumed session, newest <age>. Recover, keep, discard?
423
+
424
+ 1. **Recover (Recommended)** — puts the newest saved state back into your working tree and commits nothing. You can drop any of those changes afterwards.
425
+ 2. **Keep as backup** — nothing happens now: `refs/so-snapshots/*` (the saved states) stay, and `git stash apply $(git rev-parse <ref>)` (this puts one back) works later.
426
+ 3. **Discard all** — deletes every saved state of this session for good: `refs/so-snapshots/<sessionId>/*` (all of them) is gone, and there is no second copy.
393
427
 
394
428
  Reply with the number of your choice.
395
429
  ```
@@ -471,7 +505,7 @@ await sweepBoard({
471
505
 
472
506
  This single call does three things:
473
507
 
474
- 1. **Sets THIS repo's board row to `in-progress`** with the current semantic-session-id, branch, mode, and heartbeat (read off this repo's `session.lock` v2 lease + the host-wide registry — both already written by Phase 1.2's `acquire()`).
508
+ 1. **Sets THIS repo's board row to `in-progress`** with the current semantic-session-id **attribution label** (never a lock/registry ownership key), branch, mode, and heartbeat (read off this repo's `session.lock` v2 lease + the host-wide registry — both already written by Phase 1.2's `acquire()`).
475
509
  2. **Re-derives THIS repo's status from its live lease**, so a stale lease left by a prior crashed session in this same repo renders as `force-closed` (heartbeat older than the v2 ttl, default 4h — `DEFAULT_TTL_HOURS` in `scripts/lib/session-lock.mjs`, evaluated via `isLockLive`) and is **never silently dropped** — its fields are read straight off the dead lock.
476
510
  3. **Re-derives every OTHER busy repo's status host-wide** via `enumerateCandidates` — a dead lease in repo B renders `force-closed` on the board the next time ANY repo's session-start runs `sweepBoard`, closing the #676→#716 gap. `frei` (lock-less) repos are excluded from re-derivation to avoid board noise; their prior rows, and the prior rows of any repo `enumerateCandidates` did not surface, are preserved unchanged via the idempotent merge — never dropped.
477
511
 
@@ -785,7 +819,19 @@ Group issues by:
785
819
 
786
820
  Non-blocking. Cross-reference: `scripts/lib/ci-status-banner.mjs` (the sibling project-facing probe) and `.claude/rules/test-value.md` § TV-005 (why structural gates beat unit-test volume).
787
821
 
788
- All banners are non-blocking display in the Session Overview, do not halt the session. If `bootstrap-lock-freshness.mjs` is absent (pre-#186 plugin install) or `peer-cards/staleness-banner.mjs` is absent (pre-#503 plugin install) or `loop-readiness-banner.mjs` is absent (pre-#633 plugin install) or `instruction-budget-guard.mjs` is absent (pre-#687 plugin install) or `reconcile-nudge-banner.mjs` is absent (pre-#723 plugin install) or `sessions-staleness-banner.mjs` is absent (pre-#724 plugin install) or `sessions-integrity-banner.mjs` is absent (pre-#958 plugin install) or `owner-config-banner.mjs` is absent (pre-#820 plugin install) or `moc-staleness-banner.mjs` / `context-coverage-banner.mjs` are absent (pre-#831 plugin install) or `claude-md-budget-lint.mjs` is absent (pre-#878 plugin install), skip silently.
822
+ Additionally, invoke the mirror-issues probe (`scripts/lib/mirror-issues-banner.mjs`) via `await checkMirrorIssues({ repoRoot })`. This is the only probe that deliberately queries the platform the session did NOT auto-detect. `skills/gitlab-ops/SKILL.md` § VCS Auto-Detection selects exactly one platform via if/else, so in a repo whose `origin` is GitLab and whose `github` remote is a public mirror, no code path ever reads the mirror's issue tracker — issues filed there by external reporters are structurally invisible to every session. The VCS family is therefore hard-pinned to `'github'` inside the module rather than auto-detected. It takes no Session Config key: `resolveRepoSpec({ repoRoot, vcs: 'github' })` derives the `gh -R` spec from `git remote`, which makes the probe self-disabling a repo with no GitHub mirror resolves to `undefined`, returns `null`, and spawns no subprocess.
823
+
824
+ The return contract has THREE states, not the usual two, and the third is the point: `null` means either "no mirror remote" or "queried successfully, zero open issues"; `{ severity, message, count, repoSpec, issues }` means N > 0; and `{ severity, message, repoSpec, degraded }` means the query did NOT succeed, where `degraded` is one of `cli-missing | timeout | parse-error | auth-error | query-failed`. Render `result.message` verbatim in either non-null case. A `degraded` result must be read as *"the mirror's state is unknown"* — never as clean. `scripts/lib/ci-status-banner.mjs` collapses all three of missing-CLI, unparseable output and absent-remote onto `null`, which in the banner contract reads as "all clear"; that collapse is why this gap survived unseen. Do not reproduce it.
825
+
826
+ Additionally, invoke the git-config-drift probe (`scripts/lib/git-config-drift.mjs`) via `checkGitConfigDrift({ repoRoot })` (synchronous — no await; `env` defaults to `process.env`). It reads `git config --local --list` with a FILTERED environment, so an ambient `GIT_DIR` cannot redirect the probe itself at a foreign repository and let it call this one clean. **Three states, not two:** `null` = read and clean; `{ severity: 'warn', message, findings }` = at least one unexpected entry (a local identity override, a local `commit.gpgsign`, a remote on a reserved fixture host, a `core.hooksPath` not pointing at `.husky/_`, or `GIT_DIR`/`GIT_WORK_TREE` set in the environment); `{ …, degraded }` = the config could NOT be read — **never render that as clean.** Render `result.message` alongside the other banners.
827
+
828
+ This is the only probe that inspects `.git/config`, and that is the whole point: `git status` cannot see that file. On 2026-08-19 a coordinator diagnostic exported `GIT_DIR` at this repository while the suite ran; test fixtures wrote a foreign remote and their own `user.email`/`user.name` into the local config, and the identity then authored two commits that reached both remotes. A recovery pass checked HEAD, the index and all 1614 tracked files, found everything clean, and missed it — because none of those surfaces show `.git/config`. It surfaced two hours later, from an agent measuring something else.
829
+
830
+ The complementary halves live elsewhere and are not duplicates of this probe: `tests/setup/scrub-git-env.mjs` (wired via `setupFiles` in `vitest.config.mjs`) removes the redirecting variables before any test runs, and `scripts/lib/validate/check-test-git-config-target.mjs` censuses untargeted state-mutating git calls in `tests/**`. The census is WARN-only by measurement — its first cut was 11 hits, all false positives — and it explicitly reports `gitDirInheritable`, the population it cannot close, because the incident's own call sites passed a correct `cwd` and were redirected anyway.
831
+
832
+ Non-blocking. Cross-reference: `scripts/lib/vcs-repo-spec.mjs` (`isQueryFailure` — the same absence-vs-query-failure split this probe's `degraded` state implements).
833
+
834
+ All banners are non-blocking — display in the Session Overview, do not halt the session. If `bootstrap-lock-freshness.mjs` is absent (pre-#186 plugin install) or `peer-cards/staleness-banner.mjs` is absent (pre-#503 plugin install) or `loop-readiness-banner.mjs` is absent (pre-#633 plugin install) or `instruction-budget-guard.mjs` is absent (pre-#687 plugin install) or `reconcile-nudge-banner.mjs` is absent (pre-#723 plugin install) or `sessions-staleness-banner.mjs` is absent (pre-#724 plugin install) or `sessions-integrity-banner.mjs` is absent (pre-#958 plugin install) or `owner-config-banner.mjs` is absent (pre-#820 plugin install) or `moc-staleness-banner.mjs` / `context-coverage-banner.mjs` are absent (pre-#831 plugin install) or `claude-md-budget-lint.mjs` is absent (pre-#878 plugin install) or `mirror-issues-banner.mjs` is absent (pre-#1022 plugin install), skip silently.
789
835
 
790
836
  ## Phase 4.5: Resource Health (v3.1.0)
791
837
 
@@ -1037,12 +1083,12 @@ if (!c.prompt) {
1037
1083
  ```js
1038
1084
  AskUserQuestion({
1039
1085
  questions: [{
1040
- question: "Anonyme Usage-Telemetrie aktivieren? Strikt opt-in, whitelist-projiziert (keine Repo-Namen/Pfade/Prompts), jederzeit abschaltbar — Details: docs/telemetry.md",
1041
- header: "Usage Telemetry",
1086
+ question: "Anonyme Usage-Telemetrie aktivieren? Strikt opt-in, jederzeit abschaltbar; was genau gesendet wird: docs/telemetry.md",
1087
+ header: "Telemetrie",
1042
1088
  multiSelect: false,
1043
1089
  options: [
1044
- { label: "Ja, aktivieren", description: "Anonymer Zähl-/Struktur-Datensatz (Skill-/Phasen-Nutzung, Erfolg/Abbruch) whitelist-projiziert, keine Pfade/Prompts/Repo-Namen. Details: docs/telemetry.md" },
1045
- { label: "Nein", description: "Keine Telemetrie senden. Jederzeit später aktivierbar via node scripts/telemetry.mjs." },
1090
+ { label: "Ja, aktivieren", description: "Sendet anonyme Zähl- und Strukturdaten (welche Phase lief, Erfolg oder Abbruch), whitelist-projiziert: keine Pfade, keine Prompts, keine Repo-Namen." },
1091
+ { label: "Nein", description: "Sendet nichts; die Frage kommt hier nicht wieder. Einschalten geht später mit `node scripts/telemetry.mjs` (das ist der Befehl dafür)." },
1046
1092
  ],
1047
1093
  }],
1048
1094
  });
@@ -1052,9 +1098,9 @@ AskUserQuestion({
1052
1098
 
1053
1099
  - **Codex CLI / Cursor IDE fallback (numbered Markdown list — AUQ-004 exception 1):**
1054
1100
  ```
1055
- Anonyme Usage-Telemetrie aktivieren? Strikt opt-in, whitelist-projiziert (keine Repo-Namen/Pfade/Prompts), jederzeit abschaltbar — Details: docs/telemetry.md
1056
- 1. Ja, aktivieren — anonymer Zähl-/Struktur-Datensatz, keine Pfade/Prompts/Repo-Namen.
1057
- 2. Nein — keine Telemetrie senden.
1101
+ Anonyme Usage-Telemetrie aktivieren? Strikt opt-in, jederzeit abschaltbar; was genau gesendet wird: docs/telemetry.md
1102
+ 1. Ja, aktivieren — sendet anonyme Zähl- und Strukturdaten (welche Phase lief, Erfolg oder Abbruch), whitelist-projiziert: keine Pfade, keine Prompts, keine Repo-Namen.
1103
+ 2. Nein — sendet nichts; die Frage kommt hier nicht wieder. Einschalten geht später mit `node scripts/telemetry.mjs` (das ist der Befehl dafür).
1058
1104
  Reply with the number of your choice. (No option is pre-recommended — the choice is yours.)
1059
1105
  ```
1060
1106
 
@@ -58,15 +58,15 @@ AskUserQuestion({
58
58
  options: [
59
59
  {
60
60
  label: "Dev (Recommended)", // add "(Recommended)" to each detected audience
61
- description: "Architektur-, Modul- oder Refactoring-Änderungen aktualisiert CLAUDE.md (oder AGENTS.md auf Codex CLI), docs/dev/**, docs/adr/**."
61
+ description: "Architektur-, Modul-, Refactoring-Änderungen. Dann ändern sich CLAUDE.md bzw. AGENTS.md und `docs/dev/**, docs/adr/**` (Handbuch und Entscheidungen)."
62
62
  },
63
63
  {
64
64
  label: "User",
65
- description: "Öffentlich sichtbare Änderungen aktualisiert README.md, docs/user/**, examples/**."
65
+ description: "Öffentlich sichtbare Änderungen. Dann werden README.md und `docs/user/**, examples/**` (was Nutzer davon lesen) nachgezogen."
66
66
  },
67
67
  {
68
68
  label: "Vault",
69
- description: "Strategische oder Status-Änderungen aktualisiert <vault>/01-projects/<slug>/context.md, decisions.md, people.md."
69
+ description: "Strategische oder Status-Änderungen. Dann wird `<vault>/01-projects/<slug>/context.md, decisions.md, people.md` (die Projektakte dazu) nachgezogen."
70
70
  }
71
71
  ]
72
72
  }]
@@ -76,13 +76,13 @@ AskUserQuestion({
76
76
  **Codex CLI / Cursor IDE fallback (numbered Markdown list):**
77
77
 
78
78
  ```markdown
79
- Welche Audiences berührt dieser Scope? (Mehrfachauswahl möglich)
80
-
81
79
  Auto-detected: [dev] ← list detected audiences here, or "none" if empty intersection
82
80
 
83
- 1. **Dev (Recommended)** Architektur-, Modul- oder Refactoring-Änderungen. Targets: CLAUDE.md (oder AGENTS.md auf Codex CLI), docs/dev/**, docs/adr/**.
84
- 2. **User** — Öffentlich sichtbare Änderungen. Targets: README.md, docs/user/**, examples/**.
85
- 3. **Vault** — Strategische oder Status-Änderungen. Targets: <vault>/01-projects/<slug>/context.md, decisions.md, people.md.
81
+ Welche Audiences berührt dieser Scope? Mehrfachauswahl möglich.
82
+
83
+ 1. **Dev (Recommended)** — Architektur-, Modul-, Refactoring-Änderungen. Dann ändern sich CLAUDE.md bzw. AGENTS.md und `docs/dev/**, docs/adr/**` (Handbuch und Entscheidungen).
84
+ 2. **User** — öffentlich sichtbare Änderungen. Dann werden README.md und `docs/user/**, examples/**` (was Nutzer davon lesen) nachgezogen.
85
+ 3. **Vault** — strategische oder Status-Änderungen. Dann wird `<vault>/01-projects/<slug>/context.md, decisions.md, people.md` (die Projektakte dazu) nachgezogen.
86
86
 
87
87
  Enter one or more numbers (comma-separated), or press Enter to accept the recommended default.
88
88
  ```
@@ -16,31 +16,88 @@ const verdict = evaluate(snapshot, config['resource-thresholds'], {
16
16
  });
17
17
  ```
18
18
 
19
- The `evaluate()` result has three fields:
20
- - `verdict`: `green` | `warn` | `critical`
21
- - `reasons`: array of human-readable explanations
19
+ The `evaluate()` result has four fields:
20
+ - `verdict`: `green` | `warn` | `critical` (the `degraded` tier is no longer produced)
21
+ - `reasons`: array of human-readable explanations, including `info:`-prefixed
22
+ lines for signals that were seen but deliberately not acted on
22
23
  - `recommended_agents_per_wave_cap`: integer cap (0 = coordinator-direct) or null
24
+ - `signals`: `{ hard: string[], soft: string[] }` — which axes fired (#1089)
23
25
 
24
26
  The third `options` argument is optional (HR-003/HR-004, baseline #60) — when `config['heavy-repo']` is `true`, the cap is forced to at most `config['agents-per-wave']` regardless of the live verdict (static preflight ceiling; more-restrictive-wins against whatever the resource signals already computed). Omitting `options` entirely preserves pre-#60 behaviour.
25
27
 
26
- ## Adaptive Rules (default thresholds; configurable via `resource-thresholds`)
27
-
28
- | Signal | Threshold | Action |
29
- |--------|-----------|--------|
30
- | RAM free below `ram-free-min-gb` (default 4) | warn | Cap `agents-per-wave` at 2 |
31
- | RAM free below `ram-free-critical-gb` (default 2) | critical | Recommend coordinator-direct (0 agents) |
32
- | CPU load above `cpu-load-max-pct` (default 80) sustained judged on **min(1m, 5m)** load average (#943) | warn | Cap `agents-per-wave` at 2 |
33
- | Claude processes ≥ `concurrent-sessions-warn` (default 5) | warn | Warn; suggest sequencing or waiting |
34
- | SSH session detected AND `ssh-no-docker: true` | info | Append note: host is SSH-attached, Docker-dependent steps should run on a local dev host |
35
-
36
- **CPU methodology (#943):** the gate/probe runs right after the coordinator's own CPU-saturating quality-gate run by construction, so the 1-minute load average systematically carries that decaying tail (observed: 96% 75% within 36s). `probe()` therefore also emits `cpu_load_5m` / `cpu_load_5m_pct`, and `evaluate()` + `evaluateWaveResourceGate()` judge the CPU threshold on **min(1m, 5m)**: only-1m-high is reported as an informational "decaying transient" reason without capping; both-high (genuine sustained load) still caps. When `cpu_load_5m_pct` is `null` (Windows, zero-load), judging falls back to the 1m-derived `cpu_load_pct` alone.
28
+ ## Adaptive Rules (rebuilt in #1089 see `.claude/rules/host-resources.md`)
29
+
30
+ The rule set is **signal precedence + the two-signal rule**, not a list of
31
+ independent thresholds ORed together. Measured 2026-08-21 over 1477
32
+ `orchestrator.session.started` events across 18 repos, the previous OR-of-three
33
+ produced warn-or-worse on **99.0%** of session starts a warning that fires
34
+ almost always changes no decision except how fast it gets ignored.
35
+
36
+ **Memory judge on the best signal present, never on a worse one:**
37
+
38
+ | Precedence | Signal | Hard (→ critical) | Soft |
39
+ |---|---|---|---|
40
+ | 1 | `memory_pressure_pct_free` (macOS) | `< 15%` | `< 30%` |
41
+ | 2 | `ram_available_gb` (macOS, vm_stat) | `< ram-free-critical-gb` | `< ram-free-min-gb` |
42
+ | 3 | `ram_free_gb` (`os.freemem`) | same | same |
43
+
44
+ Level 3 is reached only when neither better signal exists — i.e. on
45
+ Linux/Windows, where `os.freemem()` is accurate. On Darwin it reports `Pages
46
+ free` only (median **0.4 GB** across the corpus, on 24-128 GB hosts), so gating
47
+ on it there fired the *critical* threshold on 84.0% of starts.
48
+
49
+ **Other axes:**
50
+
51
+ | Signal | Threshold | Class |
52
+ |---|---|---|
53
+ | CPU, judged on **min(1m, 5m)** (#943) | above `cpu-load-max-pct` (default 90) | soft |
54
+ | Live peer **sessions** from the registry | ≥ `concurrent-sessions-warn` (default 5) | soft |
55
+ | Claude **processes** (fallback only, registry unreadable) | ≥ threshold × 6 | soft |
56
+ | Swap, **only while memory is unhealthy** | `> 3072 MB` hard / `> 1024 MB` soft | both |
57
+ | Zombie processes with a live peer/process context | ≥ 1 | **info** (reported, never counted — see HR-104) |
58
+ | SSH detected AND `ssh-no-docker: true` | — | info note |
59
+
60
+ **Verdict composition:**
61
+
62
+ - any **hard** signal → `critical`, recommend coordinator-direct (0 agents)
63
+ - **two or more independent soft** signals → `warn`, cap agents-per-wave at 2
64
+ - exactly **one soft** signal → `green`, reported in `reasons`, **no cap**
65
+ - none → `green`
66
+
67
+ `evaluate()` additionally returns `signals: { hard: [...], soft: [...] }` so a
68
+ caller can log which axes fired rather than re-deriving them from prose.
69
+
70
+ **Unit note (#1089):** `concurrent-sessions-warn` is denominated in SESSIONS. It
71
+ was compared against `claude_processes_count` until this rebuild — a measured 6x
72
+ unit error (median processes:sessions = 6.0 over 1461 paired samples) that made
73
+ the threshold fire on 93.6% of starts instead of 4.2%. `probe()` now supplies
74
+ `peer_sessions_count` from the session registry (`detectPeers()`, self excluded,
75
+ heartbeat-fresh); the rescaled process count is a fallback for hosts where the
76
+ registry is unreadable.
77
+
78
+ **CPU methodology (#943):** the gate/probe runs right after the coordinator's own
79
+ CPU-saturating quality-gate run by construction, so the 1-minute load average
80
+ systematically carries that decaying tail (observed: 96% → 75% within 36s).
81
+ `probe()` therefore also emits `cpu_load_5m` / `cpu_load_5m_pct`, and both
82
+ `evaluate()` and `evaluateWaveResourceGate()` judge CPU on **min(1m, 5m)**:
83
+ only-1m-high is reported as an informational "decaying transient" and is NOT
84
+ counted as a signal at all — so it cannot become the second signal that triggers
85
+ a cap. When `cpu_load_5m_pct` is `null` (Windows, zero-load), judging falls back
86
+ to `cpu_load_pct` alone.
37
87
 
38
88
  ## Presentation
39
89
 
40
90
  Print a one-line Resource Health verdict immediately after Phase 4's output:
41
91
 
42
92
  ```
43
- Resource Health: ⚠ warn — RAM free 3.1 GB below threshold 4 GB; capping agents-per-wave at 2.
93
+ Resource Health: ⚠ warn — two signals agree (cpu + concurrency); capping agents-per-wave at 2.
94
+ ```
95
+
96
+ On `green` with a reported signal, print the line but say plainly that nothing
97
+ was capped — a bare signal with no consequence reads as a suppressed warning:
98
+
99
+ ```
100
+ Resource Health: ✓ green — 6884 MB swap present but memory_pressure healthy (53% free); no cap.
44
101
  ```
45
102
 
46
103
  When `config['heavy-repo']` is `true` and the HR-004 preflight ceiling actually reduces `recommended_agents_per_wave_cap` below what the live verdict alone would have produced, print an additional banner line right after the verdict line:
@@ -49,10 +106,16 @@ When `config['heavy-repo']` is `true` and the HR-004 preflight ceiling actually
49
106
  ⚠ Heavy-repo mode active — agents-per-wave capped to 4 (Session Config heavy-repo: true)
50
107
  ```
51
108
 
52
- When verdict is `warn` or `critical`, use the AskUserQuestion tool to present:
53
- 1. **Proceed as recommended** (apply the cap) Recommended
54
- 2. **Proceed as originally planned** (user accepts the risk)
55
- 3. **Abort** (no wave planning runs; user closes or investigates)
109
+ **AUQ only on `critical`** (#1089). A `warn` applies its cap and reports it in
110
+ one line it does not interrupt. Under the previous rule set warn-or-worse was
111
+ the verdict on 99.0% of session starts, so an AUQ here was an operator interrupt
112
+ on essentially every session, which `.claude/rules/ask-via-tool.md` AUQ-005
113
+ names outright ("an AUQ that blocks nothing"). `critical` means coordinator-direct
114
+ — zero agents — which genuinely changes the plan, so it earns the prompt:
115
+
116
+ 1. **Proceed coordinator-direct** (0 agents) — Recommended
117
+ 2. **Proceed as originally planned** (operator accepts the risk)
118
+ 3. **Abort** (no wave planning runs; operator closes or investigates)
56
119
 
57
120
  When SSH is detected and the session type is `deep`, auto-append this note to the plan handoff to session-plan (no user prompt needed):
58
121
  > Host is SSH-attached — Docker-dependent wave steps should run on a local dev host.
@@ -64,6 +64,116 @@ The active level is `efficiency.output-level` in `~/.config/session-orchestrator
64
64
  - Shape: explain the WHY behind each recommendation, name the alternatives you rejected and why, spell out unfamiliar terms on first use.
65
65
  - Escalation: `expand <topic>` — see § Escalation above.
66
66
 
67
+ ### Register — how a sentence reads
68
+
69
+ The budgets above set *how much* you say. This sets *how*. It binds at every
70
+ level and is not itself a budget: applying it changes word order and word
71
+ choice, not line count. It is the canonical statement for this repo — the
72
+ other three souls (`plan`, `brainstorm`, `grill`) point here rather than
73
+ copying it.
74
+
75
+ **Write for someone who knows this project but has not seen what you just saw.**
76
+ What he needs to decide stands in the text, not in the file it points at — in
77
+ the AUQ payload and in every finding you post.
78
+
79
+ This is not "explain it like he is five". The operator owns this repo. He is
80
+ not missing knowledge, he is missing **observation** — he did not watch the
81
+ command you just ran or read the file you just opened. A knowledge framing
82
+ would be factually wrong and condescending at the same time. Write across, not
83
+ down: same expertise as yours, minus your last ten minutes.
84
+
85
+ #### Plain words, real things
86
+
87
+ > **Say more simply what actually happens — and introduce nothing that does not exist.**
88
+ >
89
+ > **The test:** delete every noun the system does not contain. If the sentence
90
+ > is still true and complete, it was no analogy. If it collapses, the analogy
91
+ > was load-bearing — replace it with a description of what actually happens.
92
+
93
+ Five worked cases, in rising difficulty:
94
+
95
+ 1. "Waiting means the other session finishes first." — **allowed.** Sessions
96
+ and waiting both exist; nothing foreign was introduced.
97
+ 2. "Think of the session as a level crossing." — **forbidden.** Delete "level
98
+ crossing" and nothing is left. Say what happens instead: one session holds
99
+ `.orchestrator/session.lock`, the other waits for it.
100
+ 3. "The token budget is used up." — **allowed.** `TOKEN_BUDGET_EXCEEDED` is a
101
+ real identifier and "budget" is the system's own word. Adding "…like a tank
102
+ of fuel" would be forbidden — the tank does not exist.
103
+ 4. "Think of the kill-switches as a fuse box." — **forbidden, and wrong on the
104
+ facts.** Fuses trip on overload; the kill-switches also test elapsed time and
105
+ confidence. The image sounds helpful and is not. A wrong picture costs more
106
+ than no picture, because the operator reasons from it.
107
+ 5. **Dead metaphors.** A proper name may itself be a metaphor —
108
+ `pre-bash-destructive-guard` is called a guard — and you use the name as
109
+ given. Reviving the image is the violation: "the guard will not let it
110
+ through" invites the operator to picture a guard and then reason from the
111
+ picture instead of from the hook. Name the identifier, then say what it
112
+ does: the hook denies the Bash call.
113
+
114
+ #### Precedence over § "Never traded for brevity"
115
+
116
+ There is a real collision above: "say it more simply" can water down a precise
117
+ error message. Resolve it in three steps.
118
+
119
+ 1. **Simplifying removes words, never facts.** If a path, a number, an error
120
+ code, an identifier, or an instruction to act disappears, that is data loss,
121
+ not simplification — and § "Never traded for brevity" already forbids it.
122
+ 2. **When both will not fit in one sentence: precision in the sentence,
123
+ plainness in the one beside it.** The exact term is never replaced, only
124
+ accompanied. It is what the operator greps, quotes, and pastes into an issue.
125
+ 3. **The mechanical tie-breaker:** could the token you are about to cut ever
126
+ appear in a `grep`? Then it stays.
127
+
128
+ Measured 2026-08-22 at `a4f93cf`: of 191 option descriptions in this repo, the
129
+ 20 that match the safety lexicon (`SAFETY_PATTERN` in
130
+ `scripts/lib/auq/schema.mjs`) run 26–108 codepoints — all of them under both K6
131
+ length thresholds (`descriptionCharsWarn` 120, `descriptionCharsFail` 150). The
132
+ collision therefore does not occur today. This precedence rule is a precaution,
133
+ not a repair.
134
+
135
+ #### Worked example — an operator-visible message
136
+
137
+ `formatBlockReason()` in `scripts/lib/issue-budget.mjs` is what the operator
138
+ sees when the issue cap blocks a creation. Rendered with the collector-issue
139
+ sink, before:
140
+
141
+ ```
142
+ issue-budget: session cap reached — 12/12 issues already created.
143
+ This request was NOT created. It is parked as overflow entry #3 in:
144
+ .orchestrator/runtime/issue-budget-overflow.jsonl
145
+ session-end Phase 5 will fold all overflow entries into ONE collector issue `[Backlog-Sammel] <session-id>, N zurückgestellte Punkte`. Nothing is lost.
146
+ Exempt from the cap: priority::critical, the carryover class (SPIRAL/FAILED, [Carryover]),
147
+ and broken-window closure issues — those are never deferred.
148
+ To raise the cap for this repo, edit `issue-budget.max-per-session` in the Session Config;
149
+ `mode: warn` reports without blocking, `mode: off` disables the gate.
150
+ ```
151
+
152
+ After:
153
+
154
+ ```
155
+ Nothing is lost — the issue is parked, and nothing needs doing right now.
156
+ issue-budget: session cap reached — 12/12 issues already created, so this one was NOT created.
157
+ It is parked as overflow entry #3 in:
158
+ .orchestrator/runtime/issue-budget-overflow.jsonl
159
+ session-end Phase 5 folds all overflow entries into ONE collector issue `[Backlog-Sammel] <session-id>, N zurückgestellte Punkte`.
160
+ Exempt from the cap: priority::critical, the carryover class (SPIRAL/FAILED, [Carryover]),
161
+ and broken-window closure issues — those are never deferred.
162
+ To raise the cap for this repo, edit `issue-budget.max-per-session` in the Session Config; `mode: warn` reports without blocking, `mode: off` disables the gate.
163
+ ```
164
+
165
+ Three changes, and only these three: the operator's own question — *must I do
166
+ something?* — moved to line 1, carrying "Nothing is lost" up from line 4 where
167
+ he used to reach it last; `will fold` became the active `folds`; and the first
168
+ two lines merged on a causal `so`, which is why "This request" is now "this
169
+ one" — the same subject, already named in the sentence.
170
+
171
+ The whole word-level diff is four dropped tokens: `This`, `request`, `will`,
172
+ `fold`. Not one of them is a path, a count, a label, a config key, or a mode
173
+ value; every one of those survives character for character. Eight lines before,
174
+ eight lines after — **this register is not a diet.** It is the same facts, in
175
+ the order the reader needs them.
176
+
67
177
  ### Companion dials
68
178
 
69
179
  Same file, same lookup, same fallback-to-default rule:
@@ -2,7 +2,11 @@
2
2
  name: spinout
3
3
  user-invocable: true
4
4
  model: sonnet
5
- description: Use when extracting a project into its own repo — a venture spinout (e.g. a product leaving its incubator repo) or a sanitized content-snapshot fork. Guided 5-step runbook: target sphere + path, confidentiality/sanitize check, copy + fresh git init, SNAPSHOT-FREEZE marker in the source repo, remotes + registration. Trigger on 'spin out X', 'extract this into its own repo', 'fork X sanitized'.
5
+ description: >
6
+ Use when extracting a project into its own repo — a venture spinout (e.g. a product leaving its
7
+ incubator repo) or a sanitized content-snapshot fork. Guided 5-step runbook: target sphere + path,
8
+ confidentiality/sanitize check, copy + fresh git init, SNAPSHOT-FREEZE marker in the source repo,
9
+ remotes + registration. Trigger on 'spin out X', 'extract this into its own repo', 'fork X sanitized'.
6
10
  ---
7
11
 
8
12
  # spinout — Guided Project-Extraction Runbook
@@ -1,6 +1,16 @@
1
1
  ---
2
2
  name: sunset-review
3
- description: Use this skill when the user wants to identify unused, near-zero-use, or stale skills/agents/commands in the plugin surface so they can be demoted or retired. Combines agent-dispatch telemetry (start-events only) with static reference scanning, classifies every surface item into Active / Investigate / Demote / Retire, and emits a Markdown report plus JSON sidecar. NEVER auto-deletes — surfaces candidates for human decision. Quarterly cadence. <example>Context: The plugin surface has grown and the maintainer wants to prune dead weight. user: "/sunset-review" assistant: "Running the sunset walk — classifying skills, agents, and commands by usage telemetry + static refs, grouped by Retire / Demote / Investigate / Active. No item is deleted automatically; I'll surface Retire/Demote candidates for your decision." <commentary>The user wants a usage-driven prune candidate list; this skill runs the read-only walker, presents grouped verdicts, and writes a sidecar — it never deletes.</commentary></example>
3
+ description: >
4
+ Use this skill when the user wants to identify unused, near-zero-use, or stale skills/agents/commands in
5
+ the plugin surface so they can be demoted or retired. Combines agent-dispatch telemetry (start-events
6
+ only) with static reference scanning, classifies every surface item into Active / Investigate / Demote /
7
+ Retire, and emits a Markdown report plus JSON sidecar. NEVER auto-deletes — surfaces candidates for
8
+ human decision. Quarterly cadence. <example>Context: The plugin surface has grown and the maintainer
9
+ wants to prune dead weight. user: "/sunset-review" assistant: "Running the sunset walk — classifying
10
+ skills, agents, and commands by usage telemetry + static refs, grouped by Retire / Demote / Investigate
11
+ / Active. No item is deleted automatically; I'll surface Retire/Demote candidates for your decision."
12
+ <commentary>The user wants a usage-driven prune candidate list; this skill runs the read-only walker,
13
+ presents grouped verdicts, and writes a sidecar — it never deletes.</commentary></example>
4
14
  model: inherit
5
15
  color: amber
6
16
  ---
@@ -216,11 +216,11 @@ Group `medium` and `low` findings and present via a single `AskUserQuestion` cal
216
216
  AskUserQuestion({
217
217
  questions: [{
218
218
  question: `<N> medium/low findings to triage. How to handle?`,
219
- header: "Test-runner triage",
219
+ header: "Triage",
220
220
  options: [
221
221
  {
222
222
  label: "Create all (Recommended)",
223
- description: "File <N> new issues, all with label from:test-runner"
223
+ description: "Files <N> new issues at once, all with label from:test-runner — fastest, and you can still close any of them afterwards."
224
224
  },
225
225
  {
226
226
  label: "Review each",
@@ -1,6 +1,11 @@
1
1
  ---
2
2
  name: tmux-layout
3
- description: Use this skill when the operator wants a prepared tmux visualization layout for the session's side-channels (STATE.md tail, CI-watch, events.jsonl tail). Renders a 4-pane default layout or debug layout. Read-only side-channel observability — the coordinator chat stays in the operator's original terminal. Trigger phrases: "tmux layout", "split panes for ci watch", "visualize session side-channels", "show me state-md tail and ci".
3
+ description: >
4
+ Use this skill when the operator wants a prepared tmux visualization layout for the session's
5
+ side-channels (STATE.md tail, CI-watch, events.jsonl tail). Renders a 4-pane default layout or debug
6
+ layout. Read-only side-channel observability — the coordinator chat stays in the operator's original
7
+ terminal. Trigger phrases: "tmux layout", "split panes for ci watch", "visualize session side-channels",
8
+ "show me state-md tail and ci".
4
9
  model: inherit
5
10
  color: cyan
6
11
  tools: Read, Bash, Grep, Glob
@@ -47,7 +52,7 @@ The skill prints a one-line tmux command. Paste it into a SECOND terminal (do no
47
52
  |---|---|---|
48
53
  | 1 | **Shell** (operator scratch — NOT claude) | `bash` (interactive) |
49
54
  | 2 | STATE.md tail | `tail -F <state-dir>/STATE.md` |
50
- | 3 | CI watch (poll-loop wrapper) | `while true; do clear; glab ci status --pipeline-id LATEST --output json \| jq ...; sleep 15; done` |
55
+ | 3 | CI watch (poll-loop wrapper) | `while true; do clear; glab ci status -R <spec> --output json \| jq -r '.jobs[] \| ...'; sleep 15; done` |
51
56
  | 4 | events.jsonl wave/gate filter | `tail -F .orchestrator/metrics/events.jsonl \| jq --unbuffered 'select(.event \| test("wave\|gate\|spiral"))'` |
52
57
  | 5 | agent-status telemetry (#565, only with `--with-status-pane`) | `while true; do clear; jq . .orchestrator/runtime/agent-status-current.json 2>/dev/null \|\| echo ...; sleep 2; done` |
53
58