session-orchestrator 3.22.0 → 3.24.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 (316) 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/commands/autopilot-multi.md +14 -0
  5. package/.cursor/commands/autopilot.md +14 -0
  6. package/.cursor/commands/bootstrap.md +14 -0
  7. package/.cursor/commands/brainstorm.md +14 -0
  8. package/.cursor/commands/close.md +13 -0
  9. package/.cursor/commands/contract-version-bump.md +14 -0
  10. package/.cursor/commands/debug.md +14 -0
  11. package/.cursor/commands/discovery.md +14 -0
  12. package/.cursor/commands/dispatcher.md +14 -0
  13. package/.cursor/commands/eli5.md +14 -0
  14. package/.cursor/commands/eval.md +14 -0
  15. package/.cursor/commands/evolve.md +14 -0
  16. package/.cursor/commands/go.md +14 -0
  17. package/.cursor/commands/grill.md +14 -0
  18. package/.cursor/commands/harness-audit.md +13 -0
  19. package/.cursor/commands/journey-audit.md +14 -0
  20. package/.cursor/commands/memory-cleanup.md +14 -0
  21. package/.cursor/commands/persona-panel.md +14 -0
  22. package/.cursor/commands/plan.md +14 -0
  23. package/.cursor/commands/portfolio.md +14 -0
  24. package/.cursor/commands/reconcile.md +14 -0
  25. package/.cursor/commands/release.md +14 -0
  26. package/.cursor/commands/repo-audit.md +13 -0
  27. package/.cursor/commands/session.md +14 -0
  28. package/.cursor/commands/spinout.md +14 -0
  29. package/.cursor/commands/sunset-review.md +14 -0
  30. package/.cursor/commands/templates-ack.md +14 -0
  31. package/.cursor/commands/test.md +14 -0
  32. package/.cursor/hooks.json +60 -0
  33. package/.cursor/rules/000-session-orchestrator.mdc +8 -0
  34. package/.cursor/rules/010-session-workflow.mdc +9 -1
  35. package/.cursor/rules/020-quality-gates.mdc +1 -1
  36. package/.cursor/rules/030-wave-execution.mdc +1 -1
  37. package/.cursor/rules/050-plan.mdc +2 -2
  38. package/.cursor/rules/070-gitlab-ops.mdc +73 -57
  39. package/.cursor/rules/080-ecosystem-health.mdc +7 -7
  40. package/.cursor/skills/architecture/SKILL.md +13 -0
  41. package/.cursor/skills/autopilot/SKILL.md +12 -0
  42. package/.cursor/skills/bootstrap/SKILL.md +12 -0
  43. package/.cursor/skills/brainstorm/SKILL.md +13 -0
  44. package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
  45. package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
  46. package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
  47. package/.cursor/skills/daily/SKILL.md +12 -0
  48. package/.cursor/skills/debug/SKILL.md +13 -0
  49. package/.cursor/skills/discovery/SKILL.md +13 -0
  50. package/.cursor/skills/dispatcher/SKILL.md +13 -0
  51. package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
  52. package/.cursor/skills/domain-model/SKILL.md +13 -0
  53. package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
  54. package/.cursor/skills/eli5/SKILL.md +13 -0
  55. package/.cursor/skills/eval/SKILL.md +12 -0
  56. package/.cursor/skills/evolve/SKILL.md +13 -0
  57. package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
  58. package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
  59. package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
  60. package/.cursor/skills/grill/SKILL.md +13 -0
  61. package/.cursor/skills/hook-development/SKILL.md +13 -0
  62. package/.cursor/skills/journey-audit/SKILL.md +13 -0
  63. package/.cursor/skills/mcp-builder/SKILL.md +13 -0
  64. package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
  65. package/.cursor/skills/mode-selector/SKILL.md +13 -0
  66. package/.cursor/skills/npm-publish/SKILL.md +12 -0
  67. package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
  68. package/.cursor/skills/persona-panel/SKILL.md +12 -0
  69. package/.cursor/skills/plan/SKILL.md +13 -0
  70. package/.cursor/skills/playwright-driver/SKILL.md +13 -0
  71. package/.cursor/skills/quality-gates/SKILL.md +13 -0
  72. package/.cursor/skills/reconcile/SKILL.md +12 -0
  73. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  74. package/.cursor/skills/repo-audit/SKILL.md +13 -0
  75. package/.cursor/skills/session-end/SKILL.md +13 -0
  76. package/.cursor/skills/session-plan/SKILL.md +13 -0
  77. package/.cursor/skills/session-start/SKILL.md +13 -0
  78. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  79. package/.cursor/skills/spinout/SKILL.md +12 -0
  80. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  81. package/.cursor/skills/test-runner/SKILL.md +13 -0
  82. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  83. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  84. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  85. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  86. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  87. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  88. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  89. package/.mcp.json +4 -1
  90. package/CHANGELOG.md +446 -0
  91. package/README.md +22 -17
  92. package/agents/AGENTS.md +23 -4
  93. package/agents/code-implementer.md +2 -1
  94. package/agents/db-specialist.md +2 -2
  95. package/agents/docs-writer.md +3 -1
  96. package/agents/eval-judge.md +1 -1
  97. package/agents/session-reviewer.md +7 -1
  98. package/agents/test-writer.md +2 -1
  99. package/agents/ui-developer.md +2 -1
  100. package/commands/bootstrap.md +2 -2
  101. package/commands/close.md +3 -1
  102. package/commands/go.md +1 -1
  103. package/commands/journey-audit.md +43 -0
  104. package/docs/USER-GUIDE.md +2 -2
  105. package/docs/ci-setup.md +194 -25
  106. package/docs/codex-setup.md +64 -0
  107. package/docs/components.md +7 -7
  108. package/docs/cursor-setup.md +26 -47
  109. package/docs/events-schema.md +120 -10
  110. package/docs/github-mirror-protection.md +197 -0
  111. package/docs/pi-setup.md +2 -0
  112. package/docs/rule-authoring.md +3 -1
  113. package/docs/scope-collision-guard.md +49 -2
  114. package/docs/session-config-reference.md +89 -9
  115. package/docs/session-config-template.md +38 -7
  116. package/docs/telemetry/telemetry-claims.md +11 -10
  117. package/docs/telemetry.md +52 -1
  118. package/hooks/_lib/atomic-json.mjs +111 -0
  119. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  120. package/hooks/_lib/subagent-paths.mjs +143 -0
  121. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  122. package/hooks/cwd-change-restore.mjs +9 -29
  123. package/hooks/enforce-scope.mjs +93 -0
  124. package/hooks/hooks-codex.json +1 -1
  125. package/hooks/hooks-cursor.json +201 -20
  126. package/hooks/hooks-pi.json +1 -1
  127. package/hooks/hooks.json +2 -2
  128. package/hooks/on-session-end.mjs +486 -19
  129. package/hooks/on-session-start.mjs +263 -12
  130. package/hooks/on-stop.mjs +392 -24
  131. package/hooks/post-bash-write-verify.mjs +104 -4
  132. package/hooks/post-subagent-discovery-validator.mjs +182 -21
  133. package/hooks/post-tool-batch-wave-signal.mjs +165 -42
  134. package/hooks/post-tool-failure-corrective-context.mjs +9 -32
  135. package/hooks/pre-bash-issue-budget.mjs +117 -4
  136. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  137. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  138. package/hooks/pre-bash-staging-fence.mjs +4 -0
  139. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  140. package/hooks/skill-invocation-telemetry.mjs +21 -10
  141. package/hooks/subagent-telemetry.mjs +11 -26
  142. package/monitors/monitors.json +6 -0
  143. package/package.json +1 -1
  144. package/pi/prompts/journey-audit.md +12 -0
  145. package/rules/_index.md +9 -1
  146. package/rules/always-on/ask-via-tool.md +62 -0
  147. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  148. package/rules/always-on/build-value.md +47 -0
  149. package/rules/always-on/cross-session-messaging.md +59 -0
  150. package/rules/always-on/loop-and-monitor.md +221 -0
  151. package/rules/always-on/parallel-sessions.md +142 -12
  152. package/rules/always-on/receiving-review.md +108 -0
  153. package/rules/always-on/test-value.md +40 -0
  154. package/rules/always-on/verification-before-completion.md +77 -0
  155. package/scripts/archive-closed-prds.mjs +258 -18
  156. package/scripts/autopilot.mjs +31 -12
  157. package/scripts/backfill-abandoned-sessions.mjs +80 -11
  158. package/scripts/backfill-evidence-digest.mjs +376 -0
  159. package/scripts/cursor-install.mjs +89 -48
  160. package/scripts/emit-event.mjs +10 -2
  161. package/scripts/export-hw-learnings.mjs +143 -2
  162. package/scripts/express-path.mjs +299 -0
  163. package/scripts/generate-cursor-adapter.mjs +253 -0
  164. package/scripts/github-protection-audit.mjs +358 -0
  165. package/scripts/lib/auq/parse.mjs +5 -29
  166. package/scripts/lib/auto-dialectic.mjs +68 -0
  167. package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
  168. package/scripts/lib/build-live-signals.mjs +49 -27
  169. package/scripts/lib/ci-status-banner.mjs +158 -11
  170. package/scripts/lib/cold-start-detector.mjs +23 -14
  171. package/scripts/lib/command-blocker.mjs +70 -0
  172. package/scripts/lib/config/block-header.mjs +55 -0
  173. package/scripts/lib/config/discovery-validator.mjs +7 -2
  174. package/scripts/lib/config/health-endpoints.mjs +383 -0
  175. package/scripts/lib/config/reconcile.mjs +79 -4
  176. package/scripts/lib/config/remote-hosts.mjs +233 -0
  177. package/scripts/lib/config/section-extractor.mjs +235 -36
  178. package/scripts/lib/config-schema.mjs +9 -1
  179. package/scripts/lib/config.mjs +87 -8
  180. package/scripts/lib/convergence-monitor.mjs +13 -2
  181. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  182. package/scripts/lib/dispatcher/cli.mjs +2 -2
  183. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  184. package/scripts/lib/events-schema.mjs +48 -0
  185. package/scripts/lib/events.mjs +238 -5
  186. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  187. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  188. package/scripts/lib/express-path.mjs +327 -0
  189. package/scripts/lib/file-lock.mjs +22 -4
  190. package/scripts/lib/gates/gate-full.mjs +81 -8
  191. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  192. package/scripts/lib/git-config-drift.mjs +134 -5
  193. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  194. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  195. package/scripts/lib/host-identity.mjs +247 -2
  196. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  197. package/scripts/lib/issue-budget.mjs +229 -30
  198. package/scripts/lib/learnings/io.mjs +55 -10
  199. package/scripts/lib/learnings/schema.mjs +95 -28
  200. package/scripts/lib/lock-reaper.mjs +7 -1
  201. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  202. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  203. package/scripts/lib/memory-banner.mjs +25 -10
  204. package/scripts/lib/memory-paths.mjs +15 -6
  205. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  206. package/scripts/lib/peer-discovery.mjs +20 -2
  207. package/scripts/lib/platform.mjs +72 -9
  208. package/scripts/lib/plugin-root.mjs +143 -19
  209. package/scripts/lib/project-hygiene.mjs +43 -3
  210. package/scripts/lib/quality-gate.mjs +271 -13
  211. package/scripts/lib/reconcile/emitter.mjs +87 -19
  212. package/scripts/lib/reconcile/engine.mjs +517 -18
  213. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  214. package/scripts/lib/reconcile/renderer.mjs +148 -3
  215. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  216. package/scripts/lib/reconcile/writer.mjs +415 -84
  217. package/scripts/lib/rule-loader.mjs +37 -2
  218. package/scripts/lib/rules-sync.mjs +51 -8
  219. package/scripts/lib/scope-gate.mjs +126 -0
  220. package/scripts/lib/session-close-backfill.mjs +427 -37
  221. package/scripts/lib/session-discovery.mjs +69 -5
  222. package/scripts/lib/session-end/phase-skip.mjs +38 -5
  223. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  224. package/scripts/lib/session-id.mjs +30 -14
  225. package/scripts/lib/session-identity/own-session.mjs +220 -0
  226. package/scripts/lib/session-lock.mjs +85 -30
  227. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  228. package/scripts/lib/session-schema/validator.mjs +40 -0
  229. package/scripts/lib/session-start-probes.mjs +608 -0
  230. package/scripts/lib/session-transition.mjs +277 -0
  231. package/scripts/lib/sessions-canonical.mjs +446 -0
  232. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  233. package/scripts/lib/spiral-carryover.mjs +90 -9
  234. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  235. package/scripts/lib/state-md/mission-status.mjs +350 -52
  236. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  237. package/scripts/lib/state-md.mjs +12 -2
  238. package/scripts/lib/telemetry/schema.mjs +74 -8
  239. package/scripts/lib/telemetry/sync.mjs +91 -16
  240. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  241. package/scripts/lib/validate/check-agents.mjs +66 -0
  242. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  243. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  244. package/scripts/lib/validate/check-doc-cli-commands.mjs +25 -65
  245. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  246. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  247. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  248. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  249. package/scripts/lib/validate/check-skill-script-paths.mjs +436 -0
  250. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  251. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  252. package/scripts/lib/validate/check-unwired-features.mjs +333 -32
  253. package/scripts/lib/validate/check-validator-registration.mjs +248 -0
  254. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  255. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  256. package/scripts/lib/validate/repo-files.mjs +275 -0
  257. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  258. package/scripts/lib/vault-mirror/process.mjs +99 -43
  259. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  260. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  261. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  262. package/scripts/lib/vault-status/board-writer.mjs +381 -141
  263. package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
  264. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  265. package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
  266. package/scripts/lib/wave-resource-gate.mjs +127 -7
  267. package/scripts/lib/wave-transcript-tail.mjs +889 -0
  268. package/scripts/materialize-wave-scope.mjs +228 -15
  269. package/scripts/mcp-server.sh +11 -2
  270. package/scripts/memory-propose.mjs +132 -8
  271. package/scripts/parse-config.mjs +65 -0
  272. package/scripts/promote-vault-strict.mjs +4 -15
  273. package/scripts/site-numbers.mjs +36 -4
  274. package/scripts/token-audit.sh +9 -2
  275. package/scripts/validate-plugin.mjs +29 -0
  276. package/scripts/validate-wave-scope.mjs +67 -0
  277. package/scripts/vault-consolidate.mjs +3 -11
  278. package/scripts/vault-integration-watcher.mjs +2 -4
  279. package/scripts/vault-mirror.mjs +305 -51
  280. package/skills/_shared/monitor-patterns.md +31 -5
  281. package/skills/_shared/parallel-aware-auq.md +31 -2
  282. package/skills/_shared/parallel-aware-preamble.md +19 -4
  283. package/skills/_shared/platform-tools.md +11 -5
  284. package/skills/_shared/state-ownership.md +29 -2
  285. package/skills/autopilot/SKILL.md +5 -1
  286. package/skills/bootstrap/SKILL.md +3 -3
  287. package/skills/bootstrap/_shared-template.md +18 -10
  288. package/skills/bootstrap/deep-template.md +10 -6
  289. package/skills/bootstrap/fast-template.md +15 -8
  290. package/skills/bootstrap/standard-template.md +10 -6
  291. package/skills/claude-md-drift-check/checker.mjs +39 -11
  292. package/skills/contract-version-bump/SKILL.md +1 -1
  293. package/skills/dispatcher/SKILL.md +1 -1
  294. package/skills/ecosystem-health/SKILL.md +4 -1
  295. package/skills/ecosystem-health/wizard.md +5 -0
  296. package/skills/evolve/SKILL.md +38 -1
  297. package/skills/journey-audit/SKILL.md +270 -0
  298. package/skills/peekaboo-driver/SKILL.md +15 -3
  299. package/skills/persona-panel/SKILL.md +1 -1
  300. package/skills/reconcile/SKILL.md +46 -3
  301. package/skills/remote-offload/SKILL.md +89 -0
  302. package/skills/session-end/SKILL.md +17 -4
  303. package/skills/session-end/metrics-collection.md +7 -4
  304. package/skills/session-end/phase-3-6-tail.md +20 -9
  305. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  306. package/skills/session-plan/SKILL.md +6 -1
  307. package/skills/session-plan/wave-template.md +1 -0
  308. package/skills/session-start/SKILL.md +54 -17
  309. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  310. package/skills/session-start/phase-8-5-express-path.md +77 -12
  311. package/skills/vault-sync/validator.mjs +31 -0
  312. package/skills/wave-executor/SKILL.md +5 -3
  313. package/skills/wave-executor/circuit-breaker.md +34 -9
  314. package/skills/wave-executor/wave-loop.md +143 -22
  315. package/templates/_shared/journey-manifest.md +110 -0
  316. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -0,0 +1,270 @@
1
+ ---
2
+ name: journey-audit
3
+ description: >
4
+ Use this skill when the user wants the PRODUCT audited from the outside in — what the website
5
+ promises vs what the code does vs what a real user experiences vs what actually arrives by mail
6
+ vs what the data says is used. Runs a 7-role read-only wave (R5 writes only under an explicit
7
+ SAFETY block) against a per-repo `.orchestrator/journey-manifest.md` and writes a dossier to
8
+ docs/audits/. Distinct from /discovery, which checks code quality inside-out — this checks
9
+ product truth outside-in; they complement, neither replaces the other. Triggers "journey audit",
10
+ "Nutzersicht-Audit", "audit the product from the user's side", "/journey-audit".
11
+ model: inherit
12
+ color: purple
13
+ tools: Read, Grep, Glob, Bash, Write, Task
14
+ ---
15
+
16
+ # Journey Audit
17
+
18
+ > Outside-in product audit as a repeatable deep session. Code review checks code against code;
19
+ > this checks the product against the user's experience. Quarterly, never a CI component.
20
+
21
+ ## Purpose
22
+
23
+ The most expensive defects of a consumer product's first run (session id redacted) — a core
24
+ feature with no entry point anywhere in the UI, burned free quota, a plan gate that could be
25
+ bypassed, a claim-vs-code contradiction, EUR copy in a USD context — are **invisible to every
26
+ code-centric check**. They are not bugs in a function; they are gaps between five surfaces —
27
+ marketing copy, code constants, live UI, outbound mail, real usage data — that no single
28
+ surface can reveal.
29
+
30
+ This skill dispatches seven roles that each own one surface, then makes the coordinator personally
31
+ re-verify every P0 before it is written down. The re-verification is the value, not the fan-out:
32
+ the first run caught 2 agent errors that way and confirmed 3 real P0s.
33
+
34
+ ## Phase 0: Gates
35
+
36
+ ### 0.1 Bootstrap Gate
37
+
38
+ Read `skills/_shared/bootstrap-gate.md` and execute the gate check. GATE_CLOSED → invoke
39
+ `skills/bootstrap/SKILL.md` first. GATE_OPEN → continue.
40
+
41
+ ### 0.2 Manifest HARD-GATE
42
+
43
+ <HARD-GATE>
44
+ Read `.orchestrator/journey-manifest.md` in the target repo (or the path passed as `$ARGUMENTS`).
45
+
46
+ **A — File missing or empty → REFUSE the whole run.** Say exactly this, then stop:
47
+
48
+ > `/journey-audit` needs `.orchestrator/journey-manifest.md` in this repo and it is not there.
49
+ > The audit is repo-specific: without personas, entry points, truth-SSOTs, the chat-interview key
50
+ > and the SAFETY block, seven agents would invent their own definition of "correct" and every
51
+ > finding would be unfalsifiable.
52
+ > Copy the template — `templates/_shared/journey-manifest.md` in the session-orchestrator plugin —
53
+ > to `.orchestrator/journey-manifest.md`, fill it in, then re-run `/journey-audit`.
54
+
55
+ **B — Manifest present, `## SAFETY` heading absent or its body empty → REFUSE R5 ONLY.** R5 is the
56
+ only role that touches production with a real account. Say:
57
+
58
+ > The manifest has no filled `## SAFETY` block, so R5 (real end-to-end run against production) is
59
+ > NOT dispatched. Running as dry-run: R1–R4 + R7 (R6 additionally needs the read-only DB
60
+ > credentials named in the manifest). Fill in allowed accounts/events, no-go actions, the checkout
61
+ > limit and the cleanup rule to enable R5.
62
+
63
+ Do NOT infer a SAFETY block from other sections, do NOT ask the operator to dictate one into chat,
64
+ do NOT dispatch R5 "read-only, just this once". A production write without a written, committed
65
+ safety envelope is the one thing this skill never does.
66
+ </HARD-GATE>
67
+
68
+ Both gates are cheap and mechanical:
69
+
70
+ ```bash
71
+ MANIFEST="${1:-.orchestrator/journey-manifest.md}"
72
+ [ -s "$MANIFEST" ] || { echo "GATE-A: no manifest"; exit 1; }
73
+ awk '/^## SAFETY/{f=1;next} /^## /{f=0} f&&NF{c++} END{exit c>0?0:1}' "$MANIFEST" \
74
+ || echo "GATE-B: SAFETY block absent or empty → R5 disabled"
75
+ ```
76
+
77
+ ### 0.3 Peer-session check (before dispatch, not after)
78
+
79
+ Run `ListAgents` / read `.orchestrator/` session locks and check for peer sessions in this working
80
+ copy per `.claude/rules/parallel-sessions.md`. A journey audit reads wide and R5 writes to
81
+ production — a peer holding the same files must know before the wave starts, not from the diff.
82
+ Announce the audit and the file scope you will touch; the audit itself claims almost no write
83
+ scope, which is exactly why it is easy to forget.
84
+
85
+ ## Phase 1: Read the manifest
86
+
87
+ Parse the manifest into `$MANIFEST` and derive per-role inputs. Every role prompt below is filled
88
+ from these fields — an empty field is reported to the operator, never guessed:
89
+
90
+ `personas` · `entry_points` · `truth_ssots` · `touchpoints` (template dir, send path, cron sources,
91
+ render idiom) · `chat_interview` (question → expected answer → SSOT) · `safety` · `credentials`
92
+ (env-var NAMES) · `realdata_queries` · `platform_expectation` · `known_exceptions`.
93
+
94
+ ## Phase 2: The wave — R1–R7
95
+
96
+ All roles are **read-only** except R5. All run in parallel in one wave; none depends on another's
97
+ output (the coordinator, not an agent, joins their findings).
98
+
99
+ | R | Rolle | Generisch | Aus dem Manifest |
100
+ |---|-------|-----------|------------------|
101
+ | R1 | Flow-Zensus | Trigger→Empfänger→Zeitpunkt→Dedupe map of every outbound touchpoint (mail/push/webhook) | Template dir, send path, cron sources |
102
+ | R2 | Artefakt-Rendering | Render templates to HTML, screenshots desktop/mobile/dark, consistency matrix | Render idiom, example props |
103
+ | R3 | Claim-vs-Code-Matrix | Every marketing/FAQ/chat claim against the SSOT constants; feature inventory × surfaces | SSOT files, i18n namespaces, chat fact source |
104
+ | R4 | Anonymer Live-Rundgang | agent-browser desktop+mobile, chat interview against the truth key, dead links, console | Route list, chat questions + expected answers |
105
+ | R5 | Echter E2E-Durchstich | Prod, real account, actually execute each core flow, DB counter-check, mail-log reconciliation | **SAFETY block (mandatory)** |
106
+ | R6 | Realdaten-Funnel | Usage funnel, never-fired flows, delivery defects, name what is not measurable | Read-only DB access, identity rules, mail-provider API |
107
+ | R7 | Plattform-Ausnutzung | Self-built vs platform matrix (hosting/DB/realtime/queues/CDN/WAF), limits at 10× | CLI logins, expected plan, open perf issues |
108
+
109
+ ### Shared prompt preamble (prepend to every role)
110
+
111
+ > You are role `<R#>` of a journey audit of `<repo>`. Manifest: `.orchestrator/journey-manifest.md`
112
+ > — read it first; it is your only definition of "correct". You are **read-only**: no `Edit`, no
113
+ > `Write` outside your own report, no git write operations (PSA-007), no production writes.
114
+ > Every distributional claim ("all N routes", "no template does X") carries the executed command
115
+ > and its output (PSA-006). A finding you cannot reproduce with a quoted command is a suspicion —
116
+ > label it as one. Findings listed under `Bekannte Ausnahmen` in the manifest are reported as
117
+ > `known-exception`, never as new. Severity: **P0** = money, data loss, or a core flow unreachable ·
118
+ > **P1** = a broken flow or a UX dead end · **P2** = content/platform. Return the FULL report as
119
+ > your last message.
120
+
121
+ ### Role prompt skeletons
122
+
123
+ Each is self-contained; fill the `<…>` from `$MANIFEST`.
124
+
125
+ **R1 — Flow-Zensus.** Inputs: template dir `<…>`, send path `<…>`, cron sources `<…>`.
126
+ Task: enumerate every outbound touchpoint and produce one row per flow: trigger (code location) →
127
+ recipient (which persona) → timing → dedupe/idempotency → is it reachable at all. Grep the send
128
+ path for callers; a template with zero callers is a P1 finding, a caller with no dedupe on a
129
+ money-relevant flow is P0. Output: Markdown table + a list of `never-fired` candidates with the
130
+ grep that proves zero callers.
131
+
132
+ **R2 — Artefakt-Rendering.** Inputs: render idiom `<…>`, example props `<…>`.
133
+ Task: render every template found by R1's directory to HTML, screenshot desktop + mobile + dark,
134
+ and build a consistency matrix (logo, sender, footer, legal block, CTA, language, currency).
135
+ Output: matrix table + the artifact paths under `.orchestrator/journey-audit/<date>/`, plus every
136
+ divergence as a finding. Do not fix a template; report it.
137
+
138
+ **R3 — Claim-vs-Code-Matrix.** Inputs: SSOT files `<…>`, i18n namespaces `<…>`, chat fact source `<…>`.
139
+ Task: extract every quantitative or capability claim from marketing pages, FAQ, pricing, chat facts
140
+ and mail copy; compare each against the SSOT constant. One row: claim · where it is said · SSOT
141
+ value · verdict (`match` / `drift` / `unbacked`). Then a feature inventory × surfaces grid: a
142
+ feature that exists in code but appears on no surface is a P0 candidate ("no entry point").
143
+ **i18n files are owned by R3 alone** — no other role opens them.
144
+ Output: two tables + findings.
145
+
146
+ **R4 — Anonymer Live-Rundgang.** Inputs: route list `<…>`, chat questions + expected answers `<…>`.
147
+ Task: `agent-browser`, logged OUT, desktop and mobile viewport. Walk every route: dead links,
148
+ console errors, layout breaks, CTA that goes nowhere. Then run the chat interview verbatim and
149
+ score each answer against the expected answer from the manifest (`match` / `drift` / `refused` /
150
+ `hallucinated`). Output: route table (status, console, findings), interview table, screenshots.
151
+ Read-only: never submit a form that creates data, never sign up.
152
+
153
+ **R5 — Echter E2E-Durchstich.** Inputs: SAFETY block `<…>`, credentials by env-var NAME `<…>`.
154
+ Task: with the allowed account only, actually execute each core flow end to end in production;
155
+ counter-check in the DB read-only that the expected records exist; reconcile against the mail log.
156
+ **Before every step, re-read the SAFETY block and state which rule permits this step.** Stop at the
157
+ first no-go, do not improvise around a block, honour the checkout limit exactly, and run the
158
+ cleanup rule at the end and prove it ran. Output: per-flow trace (step → observed → expected →
159
+ verdict), the DB counter-check output, the cleanup proof.
160
+
161
+ **R6 — Realdaten-Funnel.** Inputs: read-only queries `<…>`, identity rules `<…>`, mail-provider API `<…>`.
162
+ Task: run the manifest's `SELECT`s only. Build the funnel per persona, list flows that never fired
163
+ in production, and pull delivery defects from the mail provider (bounces, suppressions, a
164
+ permanently silenced owner address). Then name explicitly what the data CANNOT answer — an unnamed
165
+ blind spot reads as a zero. Output: funnel table, never-fired list, delivery-defect list,
166
+ `not-measurable` list. No writes, no schema changes, no query outside the manifest.
167
+
168
+ **R7 — Plattform-Ausnutzung.** Inputs: CLI logins `<…>`, expected plan/tier `<…>`, open perf issues `<…>`.
169
+ Task: self-built vs platform matrix across hosting, DB, realtime, queues, CDN, WAF, cron, mail:
170
+ what does the repo hand-roll that the paid platform already provides, and what breaks at 10× today's
171
+ load (quote the actual limit from the provider CLI/API, not from memory). Output: matrix
172
+ (capability · self-built? · platform feature · effort to switch · limit at 10×) + findings.
173
+
174
+ ## Phase 3: Koordinator-Disziplin
175
+
176
+ Numbered MUSTs. This phase is the skill; the wave is only its input.
177
+
178
+ 1. **Every P0 claim is re-verified by the coordinator, individually, with its OWN grep/curl/DB read,
179
+ before it enters the dossier.** Not "the agent quoted a command" — you run one yourself, and the
180
+ dossier carries YOUR command and output. An unreproduced P0 is downgraded to P1 with the note
181
+ `agent claim, not reproduced`, or dropped. First run: 2 of 5 P0 claims were agent errors
182
+ ("instantly unreachable" — it was selectable in the UI; "USD on prod" — a session artifact).
183
+ 2. **A finding is attributed.** Every dossier row names the role that found it and the command that
184
+ proves it — the reader must be able to re-run it without asking anyone.
185
+ 3. **i18n files are owned by exactly ONE agent (R3).** They were the known contention point in the
186
+ first run: many roles want to quote them, and a second reader turns into a second writer the
187
+ moment a fix is suggested. Same rule for any other file two roles both want.
188
+ 4. **Peer-session check before the wave starts** (Phase 0.3), not after the diff.
189
+ 5. **The coordinator never lets an agent write the dossier.** Agents return reports; the joining,
190
+ de-duplication and severity decision are the coordinator's, because contradictions between two
191
+ agents are only visible in one place.
192
+ 6. **Contradictions are reported as contradictions.** Two roles disagreeing is a finding about the
193
+ product, not a merge conflict to smooth over.
194
+ 7. **No fixes during the audit.** Findings only. The fix wave is Phase 5 and is a separate decision.
195
+
196
+ ## Phase 4: Dossier
197
+
198
+ Write `docs/audits/YYYY-MM-DD-user-journey-audit.md` with this fixed section order:
199
+
200
+ 1. `## P0 — Geld & Kern-Flows` (each: symptom · evidence (coordinator's own command + output) ·
201
+ impact · suggested fix · role)
202
+ 2. `## P1 — Flows`
203
+ 3. `## P1 — UX`
204
+ 4. `## P2 — Inhalt`
205
+ 5. `## P2 — Plattform`
206
+ 6. `## Realdaten` (funnel, never-fired flows, delivery defects, explicitly not-measurable)
207
+ 7. `## Marketing-Hebel` (what the product does well and says nowhere)
208
+
209
+ Header carries: date, repo, HEAD SHA, roles dispatched (and which were skipped, with the reason —
210
+ "R5 skipped: no SAFETY block" is a result, not a gap), manifest path, artifact directory.
211
+
212
+ ## Phase 5: Closing AskUserQuestion
213
+
214
+ One `AskUserQuestion` call, per `.claude/rules/ask-via-tool.md` (option 1 `(Recommended)`, every
215
+ description carries reason + cost + consequence, `header` ≤ 12 codepoints):
216
+
217
+ ```
218
+ AskUserQuestion({ questions: [
219
+ { question: "Which fix packages should the follow-up wave carry?",
220
+ header: "Fix-Wellen", multiSelect: true,
221
+ options: [
222
+ { label: "P0 money+core (Recommended)", description: "The <N> P0s I re-verified myself. Blocks revenue/core flow today; ~<X>h; freezes nothing else." },
223
+ { label: "P1 flows", description: "<N> broken/undedupliced flows. ~<X>h; needs the R1 map as input." },
224
+ { label: "P1 UX", description: "<N> dead ends. Cheap individually, touches i18n — one owner." },
225
+ { label: "P2 content+platform", description: "<N> items. No user impact today; do in cooldown." } ] },
226
+ { question: "Create issues for the selected packages?",
227
+ header: "Issues", multiSelect: false,
228
+ options: [
229
+ { label: "Yes, batch now (Recommended)", description: "One issue per finding via gitlab-ops; labels priority::critical|high|medium + area:* + type:bug/feature. ~<N> issues; respects issue-budget." },
230
+ { label: "Dossier only", description: "No issues; the dossier stays the record. Choose when the backlog is already over budget." } ] } ] })
231
+ ```
232
+
233
+ Issue creation follows `skills/gitlab-ops/SKILL.md` § Label Taxonomy: `priority::critical` for P0,
234
+ `priority::high` for P1, `priority::medium` for P2, plus the repo's `area:` and `type:` axes. Each
235
+ issue links back to the dossier section that produced it.
236
+
237
+ ## Offload note (R6/R7)
238
+
239
+ R6 and R7 are pure CLI/API roles — no browser, no repo writes — so they can run headless on a
240
+ second machine (`claude -p "<prompt>"`, see the `m5-offload` skill). Two rules from the first run,
241
+ both learned the hard way:
242
+
243
+ - **`claude -p` prints only the LAST message.** The prompt MUST end with: *"Return the complete
244
+ report in your last message — not a summary, not a pointer to a file. Nothing you say before the
245
+ last message will be read."*
246
+ - **No background processes.** The prompt MUST forbid `&`, `run_in_background`, and any "I'll keep
247
+ it running" pattern: the process is killed when `-p` returns, and a half-finished role looks
248
+ identical to a clean one.
249
+
250
+ ## Cadence & Abgrenzung
251
+
252
+ - **Quarterly, or after a large feature drop.** Deliberately **not a CI component** — it is
253
+ expensive and judgment-heavy, and a judgment-heavy gate that runs on every pipeline becomes a
254
+ rubber stamp.
255
+ - **Optional monthly light variant:** R3 + R4 only, as a scheduled cloud session.
256
+ - **vs `/discovery`:** discovery checks code quality inside-out (probes over the tree);
257
+ journey-audit checks product truth outside-in (five user-facing surfaces against each other).
258
+ Neither replaces the other; a repo needs both.
259
+ - **Follow-through:** after each run, freeze the mechanizable parts per repo — a `report:*` script,
260
+ a claim-drift test pinned to the SSOT constant — so the next run is cheaper. The audit's job is
261
+ to find what no test knows how to look for yet.
262
+
263
+ ## Anti-Patterns
264
+
265
+ - Dispatching R5 without a SAFETY block "because the operator said it's fine in chat" — the block
266
+ is a committed artifact for a reason.
267
+ - Copying an agent's P0 into the dossier because its command looked convincing (Phase 3.1).
268
+ - Two roles both editing/owning the i18n files (Phase 3.3).
269
+ - Running the audit as a CI job to "keep it honest" — it turns into a stamp nobody reads.
270
+ - Fixing findings mid-audit, so the later roles measure a moving product.
@@ -156,11 +156,15 @@ For SwiftUI 26+ targets (projects with `Package.swift` declaring `.iOS("26")` or
156
156
  ```bash
157
157
  # Gate: glass-modifiers emit is opt-in per the active profile's rubric_features flag (v1 rubric does not consume).
158
158
  PROFILES_FILE=".orchestrator/policy/test-profiles.json"
159
- HAS_GLASS_V2="false"
159
+ HAS_GLASS_V2="false" # preconditions unmet (no PROFILE / no file / no jq) = feature not requested
160
160
  if [ -n "${PROFILE:-}" ] && [ -f "$PROFILES_FILE" ] && command -v jq >/dev/null 2>&1; then
161
- HAS_GLASS_V2=$(jq -r --arg p "$PROFILE" '(.[$p].rubric_features // []) | contains(["glass-v2"])' "$PROFILES_FILE" 2>/dev/null || echo false)
161
+ # Fallback is `unknown`, NOT `false`: false is jq's legitimate "feature off" answer,
162
+ # so a jq crash on a malformed profiles file would read as a deliberate opt-out.
163
+ HAS_GLASS_V2=$(jq -r --arg p "$PROFILE" '(.[$p].rubric_features // []) | contains(["glass-v2"])' "$PROFILES_FILE" 2>/dev/null || echo unknown)
162
164
  fi
163
- if [ "$HAS_GLASS_V2" = "true" ]; then
165
+ if [ "$HAS_GLASS_V2" = "unknown" ]; then
166
+ echo "WARN: glass-v2 probe failed (jq could not read $PROFILES_FILE) — no conformance artifact emitted" >&2
167
+ elif [ "$HAS_GLASS_V2" = "true" ]; then
164
168
  cat > "${RUN_DIR}/ax-snapshots/glass-modifiers-$(date +%s%3N).json" <<EOF
165
169
  {
166
170
  "schema_version": "v1",
@@ -175,6 +179,14 @@ EOF
175
179
  fi
176
180
  ```
177
181
 
182
+ `HAS_GLASS_V2` has **three** states, and the third is the reason the fallback is not `false`:
183
+
184
+ | Value | Meaning | Driver action |
185
+ |---|---|---|
186
+ | `true` | The active profile lists `glass-v2` in `rubric_features` | Emit the conformance artifact |
187
+ | `false` | The profile does not list it — a real answer from a readable file, or preconditions unmet (no `PROFILE`, no profiles file, no `jq`) | Skip the artifact silently |
188
+ | `unknown` | The probe itself failed — `jq` could not read/parse `$PROFILES_FILE` | WARN on stderr, skip the artifact. **Never** collapsed into `false`: a crashed probe is not an opt-out, and ux-evaluator Check 4 would otherwise report screenshot-only fallback as if the profile had chosen it |
189
+
178
190
  The ux-evaluator Check 4 reads this file. `glassEffect_frames` = compliant (uses `.glassEffect()`). `legacy_material_frames` = non-compliant (uses `.background(.thinMaterial)` etc.). `blur_modifier_frames` = non-compliant (uses `.blur(radius:)` as background). In v1 the arrays are always empty — the evaluator falls back to screenshot-only analysis. Do NOT emit a bare `{}` — use the full schema structure with empty arrays.
179
191
 
180
192
  ## Composability Contract
@@ -334,7 +334,7 @@ result. If `final_verdict == "warn"`: exit 0 with a warning line on stderr. If
334
334
  - **NEVER** dispatch more than 20 personas per panel (security M2 cap).
335
335
  - **NEVER** write a sidecar that fails schema validation — validate BEFORE write (security M3).
336
336
  - **NEVER** skip `validatePathInsideProject` for the target path OR the sidecar output path (H1).
337
- - **NEVER** use `run_in_background: true` for persona agents — lose coordination ability.
337
+ - **NEVER** use `run_in_background: true` for persona agents — the panel synthesis needs every reviewer's findings before Phase 4; background dispatch would add turn-juggling with no consumer for early results.
338
338
  - **ALWAYS** validate `model:` fields from the catalog against `MODEL_ID_RE` + aliases (H2).
339
339
  - **ALWAYS** run `output_contract` structural pre-check before `ajv.compile()` (H3).
340
340
  - **ALWAYS** emit the `aggregator_warning` to BOTH stderr and sidecar when using
@@ -122,12 +122,43 @@ fi
122
122
  Resolve `$PLUGIN_ROOT` per `skills/_shared/config-reading.md` (the standard resolution chain:
123
123
  `$CLAUDE_PLUGIN_ROOT` → `$CODEX_PLUGIN_ROOT` → `$CURSOR_RULES_DIR` → common install locations).
124
124
 
125
- ### 2.2 Invoke `runReconcile`
125
+ ### 2.2 Resolve the Effective Write-Targets
126
+
127
+ `reconcile.targets` says WHERE approved rules land. Resolve it BEFORE surfacing
128
+ the approval AUQ — the operator must never be asked to approve a write to a
129
+ destination that cannot exist:
126
130
 
127
131
  ```javascript
128
- import { runReconcile } from '$PLUGIN_ROOT/scripts/lib/reconcile/engine.mjs';
132
+ import { resolveEffectiveTargets } from '$PLUGIN_ROOT/scripts/lib/reconcile/engine.mjs';
133
+
134
+ const { targets, baselineRoot, dropped, reason } = resolveEffectiveTargets({
135
+ targets: CONFIG.reconcile?.targets, // ['repo-local'] | ['baseline'] | both
136
+ baselineRoot: CONFIG['plan-baseline-path'], // already 3-tier-resolved by config.mjs
137
+ });
138
+ ```
139
+
140
+ | Target | Writes to | Root |
141
+ |---|---|---|
142
+ | `repo-local` (default) | `<repoRoot>/.claude/rules/<slug>.md` | `repoRoot` |
143
+ | `baseline` (#1099) | `<baselineRoot>/proposals/<slug>.md` | `plan-baseline-path`, resolved `SO_BASELINE_PATH` env > `owner.yaml` `paths.baseline-path` > committed |
144
+
145
+ `baseline` is DROPPED (with one stderr WARN, and `dropped: ['baseline']` in the
146
+ return) when the root is unresolvable on all three tiers, is still the committed
147
+ `OVERRIDE-IN-…` placeholder, or is not absolute. A dropped target means: do not
148
+ surface its proposals in the AUQ at all. If `targets` comes back EMPTY, stop
149
+ here and report the `reason` — there is nowhere to write.
129
150
 
130
- const { proposals, rejected, summary, error } = await runReconcile({
151
+ Writing to `baseline` is still advisory and AUQ-gated exactly like `repo-local`:
152
+ files land under `proposals/` in the baseline checkout, nothing is committed
153
+ there, and no branch is touched. The operator reviews and commits in that repo
154
+ himself.
155
+
156
+ ### 2.3 Invoke `runReconcile`
157
+
158
+ ```javascript
159
+ import { runReconcileFromSkill } from '$PLUGIN_ROOT/scripts/lib/reconcile/engine.mjs';
160
+
161
+ const { proposals, rejected, summary, error } = await runReconcileFromSkill({
131
162
  repoRoot, // absolute path from git rev-parse --show-toplevel
132
163
  ruleExpiryDays: RULE_EXPIRY_DAYS, // empty → undefined → engine per-type TTL
133
164
  minRuleDays: MIN_RULE_DAYS, // default 7 — floors a near-dead expires-at
@@ -135,6 +166,9 @@ const { proposals, rejected, summary, error } = await runReconcile({
135
166
  maxProposalsPerRun: MAX_PROPOSALS_PER_RUN, // default 10 — volume brake (issue #900 D)
136
167
  now: new Date(),
137
168
  dryRun: DRY_RUN, // true → engine touches no disk (no idempotency sidecar write)
169
+ // trigger is pinned to 'skill' IN CODE by runReconcileFromSkill (#1201 Part A) —
170
+ // this prose block no longer sets it.
171
+ targets, // from resolveEffectiveTargets above; recorded when non-empty, omitted otherwise
138
172
  });
139
173
 
140
174
  // The engine does NOT apply a confidence floor — it proposes every eligible
@@ -282,12 +316,21 @@ const { written, archived, errors } = await writeApprovedRules({
282
316
  approved: approved, // proposals the operator approved
283
317
  rejected: rejected_by_operator, // proposals the operator declined
284
318
  repoRoot,
319
+ baselineRoot, // from Phase 2.2; omit/undefined ⇒ baseline is a no-op
320
+ targets, // from Phase 2.2; omitted ⇒ ['repo-local']
285
321
  sessionId: currentSessionId, // informational; from STATE.md or 'manual'
286
322
  });
287
323
  ```
288
324
 
289
325
  `writeApprovedRules` NEVER throws — per-item failures are collected in `errors[]`.
290
326
 
327
+ `written` is a FILE count, not a proposal count: one proposal approved with both
328
+ targets in effect writes two files and counts 2, while stamping the idempotency
329
+ sidecar exactly once. A baseline root that does not exist on disk (the
330
+ fresh-clone / CI case) skips that target with an `errors[]` entry — it is NEVER
331
+ created, because a typo'd path that silently mints a directory tree looks
332
+ exactly like a successful write.
333
+
291
334
  ### 6.2 Handle Errors
292
335
 
293
336
  If `errors.length > 0`, surface each error to the operator:
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: remote-offload
3
+ user-invocable: false
4
+ tags: [reference, remote, offload, wave-executor, resource-gate]
5
+ model: haiku
6
+ model-preference: sonnet
7
+ model-preference-codex: gpt-5.4-mini
8
+ model-preference-cursor: claude-sonnet-4-6
9
+ description: Use when local resource pressure would shrink or coordinator-direct a wave, a wave plan carries heavy build/test/audit roles (test, ui, perf), or the operator says offload, remote host, or auslagern — reference for routing that wave role to a declared SSH-reachable host instead of reducing agent count
10
+ ---
11
+
12
+ # Remote Offload — routing a wave role to a declared host instead of shrinking it
13
+
14
+ Repo-side half of #1160: `remote-hosts:` declares hosts, `wave-resource-gate.mjs` places work on them, `remote-dispatch.mjs` runs it. The host-side `offload` CLI (a separate baseline repo owns its SSOT) is invoked as a subprocess; this skill covers what the repo itself knows about it.
15
+
16
+ ## Quick reference
17
+
18
+ | Task | Command |
19
+ |---|---|
20
+ | Host readiness | `offload doctor -H <alias> --brief` |
21
+ | Gate (typecheck/lint/test) | `offload gate <repo> -H <alias>` |
22
+ | Run a command | `offload run <repo> -H <alias> -- <cmd...>` |
23
+ | Read-only analysis | `offload claude <repo> -H <alias> --model <name> < prompt.txt` |
24
+ | Implementation + patch | `offload claude <repo> -H <alias> --write --patch <path> < prompt.txt` |
25
+ | Remove finished jobs | `offload clean -H <alias> --older-than <hours>` |
26
+
27
+ Exit codes (`offload --help`, measured 2026-09-02): `0` ok · `1` usage/config · `2` host unreachable/not ready · `3` remote command failed · `4` sync failed · `5` timeout · `6` empty diff on a `--write` run · `7` account quota exhausted (429) · `8` write lock held. Same map as `OFFLOAD_EXIT_REASONS` in `scripts/lib/wave-executor/remote-dispatch.mjs`.
28
+
29
+ ## 1. Decision rule — offload vs reduce
30
+
31
+ The gate decides, not the coordinator. `applyOffloadDecision()` in `scripts/lib/wave-resource-gate.mjs` only fires when the resource verdict is already `reduce` or `coordinator-direct`, and only AFTER the HR-004 heavy-repo cap — a capped wave that offloads still respects the cap. It never probes the network; the coordinator supplies a readiness WITNESS:
32
+
33
+ - `opts.remoteReady` — `{ [alias]: boolean }`, built from the SessionStart banner line `Offload <alias>: ready=yes …`, or
34
+ - `opts.probeFn` — an async `(alias) => boolean` fallback, consulted only for aliases `remoteReady` doesn't answer for (backed by `remoteDoctor()`, i.e. `offload doctor -H <alias> --brief` parsed by `parseDoctorLine()`).
35
+
36
+ With neither supplied, no host counts as ready and the wave stays local — the gate fails toward local, never toward an unverified host. A role in `NEVER_FOREIGN_ROLES` (`scripts/lib/wave-executor/foreign-dispatch.mjs`: `impl-core`, `security-review`, `migration`, `release`, `secrets`) is never offloaded regardless of readiness.
37
+
38
+ ## 2. What is declared where
39
+
40
+ `remote-hosts:` in Session Config (`docs/session-config-reference.md` § Remote Hosts) declares the hosts, in preference order — the gate takes the FIRST host whose `roles-allowed` accepts the wave role and is witnessed ready:
41
+
42
+ ```yaml
43
+ remote-hosts:
44
+ - alias: <ssh-alias> # required, SAFE slug; reaches argv as `-H <alias>`
45
+ roles-allowed: [test, ui, perf] # subset of test|ui|perf (default: all three)
46
+ repo-path: ~/path/on/host # optional; SAFE path; default null
47
+ claude-path: ~/.local/bin/claude # optional; SAFE path; default null
48
+ ```
49
+
50
+ Two enums meet here and must not be conflated: `roles-allowed` holds `agent-mapping` roles (`test`/`ui`/`perf`), not wave roles (`Impl-Core`/`Quality`/…). The translation table is `OFFLOADABLE_WAVE_ROLES` in `wave-resource-gate.mjs` (`quality`→`test`, `test`→`test`, `ui`→`ui`, `perf`→`perf`; a wave role absent from that map stays local by default).
51
+
52
+ An `agent-mapping` entry of the form `<role>: ssh:<alias>` routes that wave role to Claude running ON the declared host instead of shrinking the wave; the alias must already exist under `remote-hosts`, or the config parse throws.
53
+
54
+ ## 3. Three channels of work
55
+
56
+ | Channel | Command | Verdict rule |
57
+ |---|---|---|
58
+ | Gate / arbitrary command | `offload gate` / `offload run` | Exit code decides — use the table above, never the prose in the run's own output |
59
+ | Implementation, patch back | `offload claude --write --patch <file>` via `dispatchRemote()` | Empty patch (exit `6`) is a FAILURE regardless of what the run reports; the coordinator READS the patch, then applies it with its own `git apply` — the remote job never touches the repo the coordinator commits from |
60
+ | Read-only analysis | `offload claude` (no `--write`) | No patch is produced; treat the transcript as advisory input, same skepticism as any reviewer output (`receiving-review.md`) |
61
+
62
+ `dispatchRemote()` (`scripts/lib/wave-executor/remote-dispatch.mjs`) is the wave-executor caller for the second and third channels; it emits `orchestrator.remote_dispatch.completed` once per call (`docs/events-schema.md`) — the only ledger record a remote dispatch produces, since a Bash-spawned `offload` child fires no `SubagentStop` hook. Payload: `host`, `role`, `run_id`, `ok`, `exit_code`, `duration_ms`, `patch_files`, `patch_bytes`, `reason` (present on every refusal and every failure class — absence means success). Deliberately excluded from the payload: prompt text, patch body, `patch_path`.
63
+
64
+ ## 4. Rules
65
+
66
+ - **Supervised, not blind.** Read the gate log or the patch before treating it as a result — completed and correct are not the same claim.
67
+ - **Prompt travels on stdin, never argv** (`offload --help`: "prompts travel by file (mode 600), never argv"; argv is visible to every process on the host).
68
+ - **The patch is READ, then applied by the coordinator** — never inside the offloaded job.
69
+ - **`never_foreign` roles are never offloaded** — checked first in `dispatchRemote()`, before any spawn or side effect.
70
+ - **One `--job` per concurrent run.** A job holds ONE set of run artefacts; two parallel runs sharing a job collided until per-run ids were introduced.
71
+ - **Rate-limit (exit `7`) carries the reset time in the message** — do not retry blind.
72
+ - **Secrets never in output** (SEC-008) — `offload` does not print credentials, and the module deliberately excludes prompt text and patch body from telemetry.
73
+ - **Never "clean up" another checkout on the host.** `offload clean` only removes the offload tool's OWN finished job worktrees, never a host's other active checkouts.
74
+
75
+ ## 5. Host readiness checklist
76
+
77
+ - SSH alias configured with key auth (no password/interactive prompt on connect).
78
+ - `tmux` available on the host (for an interactive `offload session`).
79
+ - Claude authenticated ON the host — never copy OAuth credentials between machines (refresh-token rotation invalidates the source copy); log in fresh with `/login` there instead.
80
+ - Repo cloned on the host with headless git credentials configured (no interactive auth prompt on push/pull).
81
+ - Node version matching this repo's `.nvmrc`.
82
+ - Toolchain parity with the local checkout (same package manager, same lockfile).
83
+
84
+ ## 6. Pitfalls measured in this repo
85
+
86
+ - **The pre-push quality gate used to fire on the sync push.** `.husky/pre-push` (#C10) detects a SCRATCH push — an unconfigured remote URL, e.g. the offload tool's own SSH sync target — and skips the gate for it; publish remotes (`origin`, `github`) stay gated regardless. The offload tool has since been fixed upstream to push with `--no-verify` itself, so this repo-side detection is defense-in-depth, not the primary fix.
87
+ - **The host installer must follow this repo's committed lockfile.** `package-lock.json` is tracked here (npm-canonical — `.claude/rules/development.md` § Package Management); install with `npm ci`, never a different package manager's install command, or the host checkout's `node_modules` layout diverges from CI's.
88
+ - **A linked worktree makes `.git` a file, not a directory.** 13 tracked files used to be flagged as "not in repository" in an unmodified worktree at the same layout, because the file form of `.git` was read as an untracked candidate rather than the repository marker — wave 3 fixed `scripts/lib/validate/check-untracked-test-deps.mjs` to treat a `.git` FILE as the repository marker in a linked worktree; the remote gate then ran 15,829/0.
89
+ - **Keychain-route auth shares the host account's usage window with that account's other interactive sessions**, not a dedicated quota — a token-slot profile (`--via slot`) avoids the sharing where a fixed quota matters.
@@ -843,7 +843,12 @@ Auto-promoted sibling worktrees are created by `enterWorktree()` during the Phas
843
843
 
844
844
  > **Authoritative impl:** `scripts/lib/session-end/worktree-cleanup.mjs` — `detectAutoPromotedWorktree(repoRoot, sessionId, opts)`. Import and call; do NOT re-implement from this doc.
845
845
  >
846
- > Algorithm: parse `sessionId` via `parseSessionId()`; return `null` immediately for UUID-format sessions (never auto-promoted). Derive the MAIN checkout root from the first `worktree ` entry of `git worktree list --porcelain` (NOT `path.basename(repoRoot)` — the promoted worktree's basename IS the comparison target). If `repoRoot` resolves to the main checkout, return `null`. Otherwise compare `path.basename(repoRoot)` against `<main-repo-name>-<sessionId>`; on match return `{ wtPath, sessionId, branch }`, else `null`. All git invocation is via the injection-safe `opts.execFileFn` (default `execFileSync` with an args array — #577 HARDEN-001).
846
+ > Two keys, tried in this order:
847
+ >
848
+ > 1. **Marker (primary).** Reads `<repoRoot>/.orchestrator/promoted-from.json` (`PROMOTION_MARKER_RELPATH`), written by `enterWorktree()` at creation time with `branch`, `source_session_id`, `source_root_hash`, `source_root_basename`, `promoted_at`. Accepted when the file parses, carries a non-empty `branch` + `source_session_id`, and the worktree's current branch (`git branch --show-current`) either matches the recorded one or cannot be read at all — an unverifiable branch never triggers auto-removal by itself, since that is gated separately by `isWorktreeClean()`, which fails closed on any git error. On match returns `{ wtPath, sessionId: marker.source_session_id, branch: marker.branch, source: 'marker' }`. This is the only key that survives the #1069 process boundary: since #1069 the session that RUNS in the promoted worktree is a brand-new session with its own id (see ADR-0013), so the current session's id appears in neither the worktree's directory name nor its branch — key 2 below can never match a #1069-promoted worktree.
849
+ > 2. **Basename (legacy fallback).** Parse `sessionId` via `parseSessionId()`; return `null` immediately for UUID-format sessions (never auto-promoted). Derive the MAIN checkout root from the first `worktree ` entry of `git worktree list --porcelain` (NOT `path.basename(repoRoot)` — the promoted worktree's basename IS the comparison target). If `repoRoot` resolves to the main checkout, return `null`. Otherwise compare `path.basename(repoRoot)` against `<main-repo-name>-<sessionId>` (the CURRENT session id); on match return `{ wtPath, sessionId, branch: parsed.branch, source: 'basename' }` — still correct for worktrees created before the marker existed. Returns `null` on no match.
850
+ >
851
+ > All git invocation is via the injection-safe `opts.execFileFn` (default `execFileSync` with an args array — #577 HARDEN-001).
847
852
 
848
853
  ### Clean-check
849
854
 
@@ -855,7 +860,7 @@ A worktree is clean iff ALL three conditions hold:
855
860
 
856
861
  > **Authoritative impl:** `scripts/lib/session-end/worktree-cleanup.mjs` — `isWorktreeClean(wtPath, opts)`. Import and call; do NOT re-implement from this doc.
857
862
  >
858
- > Algorithm: run `git status --porcelain`; if non-empty → dirty (`false`). Else run `git status --short --branch`; if it matches `/\bahead\b/` → unpushed (`false`). Otherwise `true`. On ANY git error → `false` (conservative PSA-003 default: never auto-remove a worktree we could not verify). Git invocation is via the injection-safe `opts.execFileFn` (default `execFileSync` with an args array — #577 HARDEN-001).
863
+ > Algorithm: run `git status --porcelain`; filter blank lines, then discount EXACTLY the one untracked line the promotion marker itself produces (`?? .orchestrator/promoted-from.json` — in a repo where `.orchestrator/` is only partly gitignored, or on a worktree whose branch predates the ignore line, the marker `enterWorktree()` writes would otherwise make every promoted worktree read "dirty"; a modified/staged/renamed/conflicted marker still counts as dirty). If any lines remain → dirty (`false`). Else run `git status --short --branch`; if it matches `/\bahead\b/` → unpushed (`false`). Otherwise `true`. On ANY git error → `false` (conservative PSA-003 default: never auto-remove a worktree we could not verify). Git invocation is via the injection-safe `opts.execFileFn` (default `execFileSync` with an args array — #577 HARDEN-001).
859
864
 
860
865
  ### Clean path: auto-remove + WARN (PRD §3 P3 Gherkin row 2)
861
866
 
@@ -1032,7 +1037,7 @@ if (sweep) {
1032
1037
 
1033
1038
  Fail-open: a `markOpenQuestionAnsweredOnDisk` failure is non-fatal — log a WARN and proceed with the close; the question simply stays `- [ ]` and roundtrips to the next session.
1034
1039
 
1035
- 3b. **Drain the issue-budget overflow — exactly ONE collector artefact (issue-budget):** when `.orchestrator/runtime/issue-budget.json` has a non-empty `overflow[]`, the session hit its `issue-budget.max-per-session` cap and every over-cap creation was PARKED rather than filed. Fold the whole list into a single artefact so nothing is silently dropped.
1040
+ 3b. **Drain the issue-budget overflow — exactly ONE collector artefact (issue-budget):** when this session's budget file (`budgetStatePath(repoRoot, accountingSessionId)` → `.orchestrator/runtime/issue-budget/<hash>.json`, #1141) has a non-empty `overflow[]`, the session hit its `issue-budget.max-per-session` cap and every over-cap creation was PARKED rather than filed. Fold the whole list into a single artefact so nothing is silently dropped.
1036
1041
 
1037
1042
  **Ordering (load-bearing):** run this as the LAST issue-creating action of Phase 5 — after step 3, after "Discovery Issue Creation", after step 4 — and re-read the counter file at that moment. Those steps can themselves push new entries into `overflow[]`; draining early would leave them unfiled.
1038
1043
 
@@ -1072,6 +1077,14 @@ if (sweep) {
1072
1077
  - After the artefact exists, reset `overflow` to `[]` in the counter file and record the collector issue ID / note path in the Phase 6 Final Report under `### Zurückgestellt (issue-budget)`.
1073
1078
  - **Never exempt-by-accident:** the cap never applied to `priority::critical`, the carryover class (`[Carryover]`, SPIRAL/FAILED, `type::carryover`), or `broken-window` closure issues, so nothing on the Phase 1.65 carry-list can ever appear in `overflow[]`. The promises at Phase 1.8 ("SPIRAL / FAILED agent carryover … non-deselectable") and the Critical Rule "ALWAYS create issues for unfinished PLANNED work" stay intact by construction.
1074
1079
  - Fail-open: a missing or malformed counter file means "no overflow" — log a WARN and continue the close.
1080
+ - **Then reap stale counter files (#1151):** the per-session split (#1141) writes one file per accounting session and nothing ever deleted them, so `.orchestrator/runtime/issue-budget/` grew without bound in every working copy. After the drain, sweep files older than 14 days; THIS session's file is exempt regardless of age, and the call is best-effort (it never throws, so it can never abort the close).
1081
+
1082
+ ```js
1083
+ import { reapStaleBudgetFiles } from '${PLUGIN_ROOT}/scripts/lib/issue-budget.mjs';
1084
+
1085
+ const { removed } = reapStaleBudgetFiles({ repoRoot, sessionId: accountingSessionId });
1086
+ if (removed.length) console.log(`issue-budget: reaped ${removed.length} stale counter file(s) (> 14 d)`);
1087
+ ```
1075
1088
 
1076
1089
  #### Discovery Issue Creation (if discovery ran in Phase 1.5)
1077
1090
 
@@ -1161,7 +1174,7 @@ Present to the user:
1161
1174
  | `learning-patterns.md` | Phases 3.5a + 3.6 extraction heuristics, confidence updates, passive decay, and JSONL write procedure |
1162
1175
  | `phase-3-6-tail.md` | Phase 3.6.x tail — full unabridged detail procedures for all six tail phases: 3.6.3 Memory-Proposals Collection (`collectProposals` + AUQ multiSelect + `promoteAndClear`, composing `writeApproved` + `clearProposalsJsonl` behind a mechanical write-before-clear guard, #828), 3.6.4 Expired-Learnings Sweep (Epic #723 B4), 3.6.5 Auto-Dream nudge (`shouldDispatchAutoDream`, #614), 3.6.6 Skill-Applied Judge (#645 L3 — `runSkillJudge`, coordinator-writes), 3.6.7 Auto-Dialectic nudge (`shouldDispatchAutoDialectic`, #614), 3.6.8 Reconciliation Rule Proposals (#696 FA3 — `runReconcile` + AUQ + `writeApprovedRules`). Loaded on demand by the SKILL.md skip-plan dispatcher (#724) — only phases with `run: true` in the `planTailPhases()` plan execute |
1163
1176
  | `scripts/lib/session-end/phase-skip.mjs` | Phase 3.6.x tail skip-plan aggregator (#724) — `planTailPhases({repoRoot, config, sessionId, platform})` → `{plan, skippedReport}`; side-effect-free (reconcile/sweep via dry-run — no writes), never-throws (per-phase probe error fail-opens to `run: true`); wraps the six existing signal helpers with config gates first, then input detection |
1164
- | (inline) Phase 3.45 | Telemetry Flush (advisory, #844) — `flush()` from `scripts/lib/telemetry/sync.mjs` drains the host-local send-queue fire-and-forget; no config key (send-gate is `resolveConsent()` inside the module, fail-closed); skip when `persistence: false`; never-throw + ~3s-bounded, offline → bounded oldest-dropped queue, optional `Telemetry: sent/queued/gated` close-summary line, NEVER an error banner; runs late in the close after Phase 3.7 |
1177
+ | (inline) Phase 3.45 | Telemetry Flush (advisory, #844; MECHANICAL since #1138 — `hooks/on-session-end.mjs` calls `flush()` itself at the end of every teardown and emits an `orchestrator.telemetry.flush` breadcrumb, so this phase is the DESCRIPTION and the fallback, never the trigger; a coordinator that skips it changes nothing) — `flush()` from `scripts/lib/telemetry/sync.mjs` drains the host-local send-queue fire-and-forget; no config key (send-gate is `resolveConsent()` inside the module, fail-closed); skip when `persistence: false`; never-throw + ~3s-bounded, offline → bounded oldest-dropped queue, optional `Telemetry: sent/queued/gated` close-summary line, NEVER an error banner; runs late in the close after Phase 3.7 |
1165
1178
  | `session-metrics-write.md` | Phase 3.7 JSONL append, vault-mirror invocation, durable narrative mirror (`mirrorNarrative`, #675), and behavior matrix |
1166
1179
  | `phase-3-7a-recommendations.md` | Phase 3.7a full procedural body — computeV0Recommendation call, STATE.md field write, data source guarantee, error mode |
1167
1180
  | `phase-3-7a-recommendations.md` § 3.7b | Phase 3.7b full procedural body — `withDurableCommit` invocation for `sessions.jsonl` + `STATE.md` (#490 AC2), `enabled:false` local no-op, autopilot.jsonl exclusion note |
@@ -36,7 +36,8 @@ Finalize session metrics by reading the wave data accumulated during execution:
36
36
  From the `stagnation` array, aggregate into `stagnation_events`:
37
37
  - `total`: count of entries in the array
38
38
  - `by_pattern`: count by `pattern` value (omit zero-valued keys)
39
- - `by_error_class`: count by `error_class` value (omit zero-valued keys; omit entire sub-object if all entries lack `error_class`)
39
+ - `by_error_class`: count by `error_class` value (omit zero-valued keys; omit entire sub-object if all entries lack `error_class` — only `error-echo` records carry one)
40
+ - `by_source`: count by `source` value — `coordinator` (post-wave review) vs `tail` (the `wave-transcript-tail` monitor, #1114). Same rule: omit zero-valued keys; omit the entire sub-object when no entry carries `source` (pre-#1114 records do not).
40
41
  - `files`: unique list of non-null `file` values (deduplicated)
41
42
  - **Omit the entire `stagnation_events` field if `total == 0`** (keeps historical entries clean).
42
43
 
@@ -67,7 +68,7 @@ Finalize session metrics by reading the wave data accumulated during execution:
67
68
  total_agents: <N>,
68
69
  total_files_changed: <N>,
69
70
  agent_summary: {complete: <N>, partial: <N>, failed: <N>, spiral: <N>},
70
- waves: [/* {wave, role, agent_count, files_changed, quality, planned_files_count?, over_delivery_ratio?} */],
71
+ waves: [/* {wave, role, agent_count, files_changed, quality, agent_count_planned?, agent_count_started?, agent_count_completed?, planned_files_count?, over_delivery_ratio?} */],
71
72
  // effectiveness is CONSTRUCTED EXPLICITLY (#773) — NOT left as an optional
72
73
  // field for the coordinator to remember. Leaving it optional is exactly how
73
74
  // the carryover=0 blind spot recurred (41/41 records read carryover:0).
@@ -110,7 +111,7 @@ Finalize session metrics by reading the wave data accumulated during execution:
110
111
  "total_files_changed": N,
111
112
  "agent_summary": {"complete": N, "partial": N, "failed": N, "spiral": N},
112
113
  "waves": [
113
- {"wave": 1, "role": "Discovery", "agent_count": N, "files_changed": N, "quality": "pass|fail|skip", "planned_files_count": N, "over_delivery_ratio": 0.0},
114
+ {"wave": 1, "role": "Discovery", "agent_count": N, "files_changed": N, "quality": "pass|fail|skip", "agent_count_planned": N, "agent_count_started": N, "agent_count_completed": N, "planned_files_count": N, "over_delivery_ratio": 0.0},
114
115
  ...
115
116
  ],
116
117
  "discovery_stats": {
@@ -152,7 +153,8 @@ Finalize session metrics by reading the wave data accumulated during execution:
152
153
  },
153
154
  "stagnation_events": {
154
155
  "total": N,
155
- "by_pattern": {"error-echo": N, "turn-key-repetition": N, "pagination-spiral": N},
156
+ "by_pattern": {"error-echo": N, "turn-key-repetition": N, "pagination-spiral": N, "psa007-git-write": N, "status-partial": N},
157
+ "by_source": {"coordinator": N, "tail": N},
156
158
  "by_error_class": {"edit-format-friction": N, "scope-denied": N, "command-blocked": N, "other": N},
157
159
  "files": ["<relative path>", "..."]
158
160
  }
@@ -168,6 +170,7 @@ Finalize session metrics by reading the wave data accumulated during execution:
168
170
  > - `review_stats`: populated ONLY when Phase 1.8 dispatched the session-reviewer agent AND it returned findings. Source: the session-reviewer's output summary.
169
171
  > - `effectiveness`: ALWAYS populated from Phase 1 plan verification results, and CONSTRUCTED EXPLICITLY in the METRICS_ENTRY snippet (#773) — never deferred to a "remember to add" optional step (that omission is how `carryover: 0` slipped past 41 records). `completion_rate` = `completed / planned_issues` (0.0-1.0, where 0.0 means nothing was completed). **`carryover` counting rule (#773):** `carryover` is the **length of the Phase 1.65 gate carry-list** — `autoCarry` ∪ the middle-band `ask` items the operator LEFT SELECTED ∪ the answered-question `impliesWork: true` candidates — NOT the raw Phase 1.2+1.3 candidate count. On the fail-open skip (gate disabled / headless / AUQ unavailable), EVERY candidate carries, so `carryover` = the full candidate-list length. Count the gate's OUTPUT (what reaches Phase 5 Step 3 filing), not its INPUT.
170
172
  > - `effectiveness.override_ratio` (#730/H5): OPTIONAL nested field = `overridden_findings / max(total_findings_surfaced, 1)` (float 0.0-1.0). Populate ONLY when Phase 2.6 (Broken-Window Budget) ran this session (`broken-window-budget.enabled: true`). OMIT (do NOT write null/0) otherwise — **absent = "not measured"**, `0.0` = "measured, nothing overridden". `overridden_findings` = the summed `count` of the `orchestrator.finding.overridden` events emitted this session; `total_findings_surfaced` = every MED/LOW+ finding surfaced across Phase 1.8 + wave reviewers.
173
+ > - `waves[].agent_count_planned` / `waves[].agent_count_started` / `waves[].agent_count_completed` (#724/#1115): OPTIONAL per-wave fields, sourced from `wave-loop.md` § Capture wave metrics step 7 — mirror its definitions exactly, do not re-derive them here. `agent_count_planned` = agents named in the session plan for this wave. `agent_count_started` = distinct agents whose `agent-<id>.meta.json` sidecar is present, after any silent-drop re-dispatch — NOT "produced a tool-result" (under background dispatch the launch ack is a result and would count an agent that never ran). `agent_count_completed` = distinct agents whose task-notification (`<status>completed</status>`) arrived. Omit each field when the wave did not measure it — **absent = "not measured"**, never zero-fill; `0` would read as "measured, no agent started", which is the opposite of an unmeasured wave. The two gaps carry the diagnosis: `agent_count_planned > agent_count_started` after re-dispatch is a persistent silent drop, `agent_count_started > agent_count_completed` at wave end is an agent that started and never returned. Both are also logged to STATE.md `## Deviations` by wave-loop.md, so a record and a deviation entry should agree.
171
174
  > - `waves[].planned_files_count` / `waves[].over_delivery_ratio` (#730/H4): OPTIONAL per-wave fields, populated from STATE.md Wave History headers of the form `(planned <P> files → actual <A>, over-delivery <R>)` (written by wave-executor §3a since #730/H4); omit when absent (pre-#730 sessions / grounding-check: false).
172
175
  > - `waves[].suite_passed` / `waves[].suite_failed` / `waves[].suite_platform` (#944): OPTIONAL per-wave fields. Omit all three when absent — absent = "not measured", `suite_failed: 0` = "measured, zero failures".
173
176
  > **`suite_passed` / `suite_failed`: read the event FIRST, the STATE.md header only as fallback (#966 step 3).** Since #954/#967 the between-waves gate wrapper `scripts/run-quality-gate.mjs` emits `orchestrator.quality_gate.{passed,failed}` with a machine-measured `counts: {passed, failed, total}` AND the `wave_number` it resolved from the `wave-scope.json` sidecar, so per-wave attribution needs no wall-clock window join. Payload fields are flat at the record's top level; for each wave `N` of this session: