session-orchestrator 5.11.0 → 5.13.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 (394) hide show
  1. package/.claude-plugin/marketplace.json +12 -1
  2. package/.claude-plugin/plugin.json +4 -1
  3. package/.codex-plugin/agents/explorer.toml +1 -1
  4. package/.codex-plugin/agents/session-reviewer.toml +2 -2
  5. package/.codex-plugin/agents/wave-worker.toml +1 -1
  6. package/.codex-plugin/plugin.json +1 -1
  7. package/.cursor-plugin/plugin.json +1 -1
  8. package/.orchestrator/policy/blocked-commands.json +8 -0
  9. package/.orchestrator/policy/public-export.json +30 -0
  10. package/CHANGELOG.md +180 -3
  11. package/README.md +126 -117
  12. package/agents/analyst.md +7 -2
  13. package/agents/architect-reviewer.md +7 -2
  14. package/agents/code-implementer.md +4 -2
  15. package/agents/db-specialist.md +9 -2
  16. package/agents/dialectic-deriver.md +9 -3
  17. package/agents/docs-writer.md +7 -2
  18. package/agents/eval-judge.md +7 -1
  19. package/agents/qa-strategist.md +7 -2
  20. package/agents/security-reviewer.md +9 -2
  21. package/agents/session-reviewer.md +18 -2
  22. package/agents/skill-applied-judge.md +7 -1
  23. package/agents/test-writer.md +9 -2
  24. package/agents/ui-developer.md +9 -2
  25. package/agents/ux-evaluator.md +6 -1
  26. package/commands/session.md +4 -2
  27. package/commands/templates-ack.md +89 -18
  28. package/docs/agent-authoring.md +30 -11
  29. package/docs/ci-setup.md +38 -2
  30. package/docs/codex-setup.md +33 -9
  31. package/docs/components.md +13 -2
  32. package/docs/cursor-setup.md +3 -3
  33. package/docs/events-schema.md +5 -4
  34. package/docs/evidence-candidates.md +63 -0
  35. package/docs/fleet-checkin-audit.md +7 -4
  36. package/docs/github-mirror-protection.md +150 -3
  37. package/docs/install.md +102 -25
  38. package/docs/instruction-delivery.md +1 -1
  39. package/docs/migration-v4.md +2 -2
  40. package/docs/mods.md +118 -0
  41. package/docs/owner-config-schema.md +1 -0
  42. package/docs/plugin-architecture-v3.md +1 -1
  43. package/docs/pm-skills-marketplace.md +4 -3
  44. package/docs/rule-authoring.md +24 -13
  45. package/docs/scope-collision-guard.md +2 -1
  46. package/docs/session-config-reference.md +55 -21
  47. package/docs/session-config-template.md +32 -24
  48. package/docs/sessions-ledger-repair.md +137 -0
  49. package/docs/vault-docs-architecture.md +1 -1
  50. package/hooks/_lib/hook-import-set.json +406 -73
  51. package/hooks/_lib/hook-root.mjs +301 -0
  52. package/hooks/_lib/lock-bootstrap.mjs +434 -4
  53. package/hooks/_lib/orchestrator-ignore.mjs +3 -0
  54. package/hooks/_lib/subagent-transcript.mjs +2 -2
  55. package/hooks/_lib/vcs-create-matcher.mjs +13 -17
  56. package/hooks/cwd-change-restore.mjs +5 -1
  57. package/hooks/enforce-commands.mjs +4 -3
  58. package/hooks/enforce-scope.mjs +11 -1
  59. package/hooks/hooks-codex.json +48 -2
  60. package/hooks/hooks.json +31 -31
  61. package/hooks/on-session-end.mjs +38 -15
  62. package/hooks/on-session-start.mjs +158 -59
  63. package/hooks/on-stop.mjs +11 -6
  64. package/hooks/post-bash-issue-budget-refund.mjs +6 -1
  65. package/hooks/post-bash-write-verify.mjs +8 -4
  66. package/hooks/post-edit-validate.mjs +45 -52
  67. package/hooks/post-subagent-discovery-validator.mjs +2 -2
  68. package/hooks/post-tool-batch-wave-signal.mjs +8 -4
  69. package/hooks/post-tool-failure-corrective-context.mjs +5 -1
  70. package/hooks/pre-bash-destructive-guard.mjs +220 -8
  71. package/hooks/pre-bash-issue-budget.mjs +6 -1
  72. package/hooks/pre-bash-sessions-ledger-guard.mjs +691 -123
  73. package/hooks/pre-bash-staging-fence.mjs +258 -14
  74. package/hooks/pre-bash-templates-first.mjs +28 -27
  75. package/hooks/run-node.sh +1 -1
  76. package/hooks/skill-invocation-telemetry.mjs +15 -8
  77. package/hooks/subagent-telemetry.mjs +77 -14
  78. package/hooks/wave-scope-commit-guard.mjs +143 -14
  79. package/mods/so-hud/.claude-plugin/plugin.json +11 -0
  80. package/mods/so-hud/README.md +57 -0
  81. package/mods/so-hud/hooks/beacon-io.mjs +101 -0
  82. package/mods/so-hud/hooks/heredoc-scan.ts +394 -0
  83. package/mods/so-hud/hooks/hooks.json +1 -0
  84. package/mods/so-hud/hooks/logic.ts +182 -0
  85. package/mods/so-hud/hooks/register.tsx +670 -0
  86. package/mods/so-hud/hooks/rules.ts +2998 -0
  87. package/mods/so-hud/tsconfig.json +8 -0
  88. package/mods/so-hud/types/index.d.ts +36 -0
  89. package/package.json +4 -3
  90. package/rules/_index.md +1 -1
  91. package/rules/always-on/ask-via-tool.md +1 -1
  92. package/rules/always-on/loop-and-monitor.md +17 -194
  93. package/rules/always-on/parallel-sessions.md +3 -1
  94. package/rules/opt-in-stack/backend-data.md +1 -1
  95. package/rules/opt-in-stack/backend.md +1 -1
  96. package/rules/opt-in-stack/security-web.md +5 -1
  97. package/scripts/agent-usage-census.mjs +79 -0
  98. package/scripts/apply-session-learnings.mjs +34 -6
  99. package/scripts/archive-closed-prds.mjs +21 -5
  100. package/scripts/autopilot.mjs +11 -5
  101. package/scripts/backfill-abandoned-sessions.mjs +13 -3
  102. package/scripts/backfill-evidence-digest.mjs +2 -1
  103. package/scripts/backfill-learnings-from-vault.mjs +61 -6
  104. package/scripts/backfill-sessions.mjs +16 -2
  105. package/scripts/check-package-manager.mjs +26 -8
  106. package/scripts/check-sessions-integrity.mjs +4 -1
  107. package/scripts/ci/assert-vitest-green.mjs +3 -0
  108. package/scripts/ci/windows-smoke.mjs +201 -0
  109. package/scripts/close-precheck.mjs +55 -0
  110. package/scripts/dialectic-deriver.mjs +20 -12
  111. package/scripts/emit-event.mjs +2 -2
  112. package/scripts/emit-session.mjs +239 -20
  113. package/scripts/eval-session.mjs +17 -3
  114. package/scripts/export-hw-learnings.mjs +29 -2
  115. package/scripts/fleet-checkin-audit.mjs +8 -4
  116. package/scripts/fleet-checkin.mjs +73 -3
  117. package/scripts/gc-stale-worktrees.mjs +6 -1
  118. package/scripts/github-export.mjs +128 -0
  119. package/scripts/github-protection-audit.mjs +3 -1
  120. package/scripts/handover-queue-check.mjs +209 -0
  121. package/scripts/harness-audit.mjs +6 -4
  122. package/scripts/lib/agent-frontmatter.mjs +57 -10
  123. package/scripts/lib/agent-status.mjs +125 -7
  124. package/scripts/lib/agent-usage-census.mjs +394 -0
  125. package/scripts/lib/auto-dialectic.mjs +17 -10
  126. package/scripts/lib/auto-dream.mjs +22 -14
  127. package/scripts/lib/autopilot/loop.mjs +3 -1
  128. package/scripts/lib/autopilot/recent-runs.mjs +4 -2
  129. package/scripts/lib/autopilot/stall-sampler.mjs +2 -1
  130. package/scripts/lib/autopilot/telemetry.mjs +5 -1
  131. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  132. package/scripts/lib/baseline-archetypes.mjs +10 -2
  133. package/scripts/lib/blocked-commands-policy.mjs +4 -3
  134. package/scripts/lib/build-live-signals.mjs +13 -9
  135. package/scripts/lib/ci-status-banner.mjs +329 -24
  136. package/scripts/lib/codex/plugin-contract.mjs +8 -0
  137. package/scripts/lib/cold-start-detector.mjs +5 -2
  138. package/scripts/lib/command-blocker.mjs +603 -43
  139. package/scripts/lib/common.mjs +3 -2
  140. package/scripts/lib/config/cross-repo.mjs +74 -7
  141. package/scripts/lib/config/host-paths.mjs +6 -2
  142. package/scripts/lib/config/remote-hosts.mjs +2 -2
  143. package/scripts/lib/config/section-extractor.mjs +65 -11
  144. package/scripts/lib/config/template-keys.mjs +153 -0
  145. package/scripts/lib/config/vault-integration.mjs +1 -0
  146. package/scripts/lib/config.mjs +3 -3
  147. package/scripts/lib/coordinator-snapshot.mjs +1 -1
  148. package/scripts/lib/discovery/triage-state.mjs +2 -1
  149. package/scripts/lib/dispatcher/rank.mjs +5 -7
  150. package/scripts/lib/ecosystem-health.mjs +2 -2
  151. package/scripts/lib/eval/engine.mjs +3 -2
  152. package/scripts/lib/eval/session-resolve.mjs +6 -2
  153. package/scripts/lib/eval/sink.mjs +4 -3
  154. package/scripts/lib/events-retention-banner.mjs +3 -2
  155. package/scripts/lib/events-schema.mjs +10 -0
  156. package/scripts/lib/events.mjs +23 -12
  157. package/scripts/lib/evolve/autonomy-verdict.mjs +4 -9
  158. package/scripts/lib/evolve/autopilot-effectiveness.mjs +8 -18
  159. package/scripts/lib/fleet-protocol.mjs +21 -0
  160. package/scripts/lib/gates/gate-full.mjs +172 -11
  161. package/scripts/lib/gates/gate-helpers.mjs +236 -23
  162. package/scripts/lib/git-config-drift.mjs +141 -7
  163. package/scripts/lib/github-export.mjs +447 -0
  164. package/scripts/lib/gitlab-portfolio/cli.mjs +16 -5
  165. package/scripts/lib/handover-queue-marks.mjs +159 -0
  166. package/scripts/lib/hardware-pattern-detector.mjs +103 -13
  167. package/scripts/lib/harness-audit/categories/category1.mjs +16 -4
  168. package/scripts/lib/harness-audit/categories/category4.mjs +11 -8
  169. package/scripts/lib/harness-audit/categories/category6.mjs +25 -5
  170. package/scripts/lib/harness-audit/categories/category9.mjs +10 -4
  171. package/scripts/lib/host-identity.mjs +9 -5
  172. package/scripts/lib/http-request.mjs +64 -0
  173. package/scripts/lib/instruction-budget-guard.mjs +30 -22
  174. package/scripts/lib/io.mjs +6 -3
  175. package/scripts/lib/issue-budget-reconcile.mjs +3 -2
  176. package/scripts/lib/issue-budget.mjs +36 -7
  177. package/scripts/lib/learnings/evidence-candidates.mjs +154 -0
  178. package/scripts/lib/learnings/expiry-sweep.mjs +99 -7
  179. package/scripts/lib/learnings/io.mjs +289 -15
  180. package/scripts/lib/learnings/select.mjs +53 -4
  181. package/scripts/lib/learnings/store-role.mjs +209 -0
  182. package/scripts/lib/learnings/surface.mjs +46 -7
  183. package/scripts/lib/ledger-path.mjs +113 -0
  184. package/scripts/lib/maintenance-due-banner.mjs +63 -12
  185. package/scripts/lib/memory-banner.mjs +5 -6
  186. package/scripts/lib/memory-proposals/collector.mjs +5 -2
  187. package/scripts/lib/memory-proposals/sink.mjs +7 -4
  188. package/scripts/lib/memory-proposals/store.mjs +22 -5
  189. package/scripts/lib/mode-selector-accuracy.mjs +2 -1
  190. package/scripts/lib/no-follow-open.mjs +57 -0
  191. package/scripts/lib/orphan-reaper/reaper-audit.mjs +4 -2
  192. package/scripts/lib/own-lock-root.mjs +95 -0
  193. package/scripts/lib/owner-interview.mjs +2 -1
  194. package/scripts/lib/owner-yaml.mjs +70 -7
  195. package/scripts/lib/platform.mjs +161 -9
  196. package/scripts/lib/plugin-update-banner.mjs +15 -11
  197. package/scripts/lib/pre-push-local-partial.mjs +117 -0
  198. package/scripts/lib/pre-push-remote-gate.mjs +151 -25
  199. package/scripts/lib/process-group.mjs +270 -34
  200. package/scripts/lib/project-hygiene.mjs +28 -0
  201. package/scripts/lib/quality-gate.mjs +33 -9
  202. package/scripts/lib/reconcile/backlog.mjs +29 -15
  203. package/scripts/lib/reconcile/engine.mjs +26 -2
  204. package/scripts/lib/reconcile/idempotency.mjs +65 -3
  205. package/scripts/lib/reconcile/rule-expiry-sweep.mjs +53 -12
  206. package/scripts/lib/reconcile/verdict-ledger.mjs +224 -0
  207. package/scripts/lib/reconcile-nudge-banner.mjs +40 -6
  208. package/scripts/lib/resource-probe/evaluate.mjs +32 -9
  209. package/scripts/lib/resource-probe/probe-platform.mjs +3 -1
  210. package/scripts/lib/rule-loader.mjs +20 -10
  211. package/scripts/lib/scope-baseline.mjs +123 -8
  212. package/scripts/lib/scope-echo.mjs +5 -2
  213. package/scripts/lib/scope-gate.mjs +55 -20
  214. package/scripts/lib/session-close-backfill.mjs +75 -7
  215. package/scripts/lib/session-end/close-precheck.mjs +151 -0
  216. package/scripts/lib/session-end/phase-skip.mjs +8 -7
  217. package/scripts/lib/session-end/tail-runner.mjs +7 -2
  218. package/scripts/lib/session-end/vault-orphans.mjs +205 -0
  219. package/scripts/lib/session-id.mjs +9 -1
  220. package/scripts/lib/session-identity/own-session.mjs +63 -0
  221. package/scripts/lib/session-invocation.mjs +14 -3
  222. package/scripts/lib/session-lock.mjs +114 -0
  223. package/scripts/lib/session-record-repair.mjs +245 -35
  224. package/scripts/lib/session-registry.mjs +85 -12
  225. package/scripts/lib/session-schema/constants.mjs +9 -0
  226. package/scripts/lib/session-schema/filters.mjs +56 -1
  227. package/scripts/lib/session-schema/validator.mjs +15 -5
  228. package/scripts/lib/session-start-probes.mjs +149 -22
  229. package/scripts/lib/session-token-rollup.mjs +2 -1
  230. package/scripts/lib/session-work-trees.mjs +166 -0
  231. package/scripts/lib/sessions-canonical.mjs +61 -3
  232. package/scripts/lib/sessions-integrity-banner.mjs +17 -39
  233. package/scripts/lib/sessions-staleness-banner.mjs +8 -5
  234. package/scripts/lib/sh-quote.mjs +1 -0
  235. package/scripts/lib/skill-evidence-window.mjs +5 -1
  236. package/scripts/lib/skill-health/join.mjs +10 -9
  237. package/scripts/lib/skill-invocations-schema.mjs +13 -0
  238. package/scripts/lib/skill-judgments-schema.mjs +6 -2
  239. package/scripts/lib/snapshot-probe.mjs +414 -0
  240. package/scripts/lib/so-hud/collect.mjs +174 -0
  241. package/scripts/lib/so-hud/runtime.mjs +87 -0
  242. package/scripts/lib/spiral-carryover.mjs +8 -1
  243. package/scripts/lib/state-md/frontmatter-mutators.mjs +266 -8
  244. package/scripts/lib/state-md/idle-reset.mjs +248 -0
  245. package/scripts/lib/state-md/mission-status.mjs +5 -1
  246. package/scripts/lib/state-md/recommendations.mjs +26 -4
  247. package/scripts/lib/state-md-peer-guard.mjs +2 -0
  248. package/scripts/lib/state-md.mjs +31 -8
  249. package/scripts/lib/sunset/walker.mjs +4 -3
  250. package/scripts/lib/telemetry/consent.mjs +4 -2
  251. package/scripts/lib/telemetry/queue.mjs +3 -3
  252. package/scripts/lib/telemetry/sync.mjs +45 -9
  253. package/scripts/lib/test-runner/artifact-paths.mjs +10 -2
  254. package/scripts/lib/tmux-layout/layouts.mjs +20 -3
  255. package/scripts/lib/ux-grill/paths.mjs +5 -1
  256. package/scripts/lib/validate/check-agents.mjs +37 -6
  257. package/scripts/lib/validate/check-component-paths.mjs +25 -1
  258. package/scripts/lib/validate/check-guard-requires-parity.mjs +3 -3
  259. package/scripts/lib/validate/check-hooks-symmetry.mjs +53 -32
  260. package/scripts/lib/validate/check-json-files.mjs +3 -1
  261. package/scripts/lib/validate/check-learning-provenance.mjs +3 -2
  262. package/scripts/lib/validate/check-learnings-shell-writes.mjs +10 -4
  263. package/scripts/lib/validate/check-metrics-ledger-root.mjs +234 -0
  264. package/scripts/lib/validate/check-owner-leakage.mjs +159 -11
  265. package/scripts/lib/validate/check-test-posix-shell.mjs +246 -0
  266. package/scripts/lib/validate/check-unwired-features.mjs +4 -2
  267. package/scripts/lib/validate/check-vcs-repo-flag-positional.mjs +58 -0
  268. package/scripts/lib/validate/check-vcs-repo-flag.mjs +259 -73
  269. package/scripts/lib/validate/check-workflow-continue-on-error.mjs +63 -0
  270. package/scripts/lib/validate/confidential-names.mjs +3 -1
  271. package/scripts/lib/vault-mirror/migrate-ids.mjs +321 -0
  272. package/scripts/lib/vault-mirror/namespace.mjs +22 -9
  273. package/scripts/lib/vault-mirror/process.mjs +197 -65
  274. package/scripts/lib/vault-mirror/prune.mjs +5 -2
  275. package/scripts/lib/vault-mirror/render-learnings.mjs +8 -7
  276. package/scripts/lib/vault-mirror/render-sessions.mjs +36 -53
  277. package/scripts/lib/vault-mirror/retention.mjs +2 -2
  278. package/scripts/lib/vault-mirror/utils.mjs +106 -13
  279. package/scripts/lib/vault-staleness-banner.mjs +4 -7
  280. package/scripts/lib/vault-status/board-writer.mjs +34 -5
  281. package/scripts/lib/vault-status/narrative-mirror.mjs +66 -7
  282. package/scripts/lib/wave-executor/foreign-dispatch.mjs +20 -0
  283. package/scripts/lib/win-process-tree.mjs +137 -0
  284. package/scripts/lib/workspace.mjs +1 -1
  285. package/scripts/lib/worktree/constants.mjs +1 -1
  286. package/scripts/lib/worktree/lifecycle.mjs +1 -1
  287. package/scripts/lib/worktree/listing.mjs +1 -1
  288. package/scripts/lib/zx.mjs +50 -0
  289. package/scripts/mcp-server.mjs +16 -7
  290. package/scripts/mcp-server.sh +13 -83
  291. package/scripts/measure-policy-cache-effectiveness.mjs +7 -5
  292. package/scripts/memory-propose.mjs +10 -3
  293. package/scripts/migrate-cold-start-seed.mjs +7 -0
  294. package/scripts/migrate-legacy-learnings.sh +22 -0
  295. package/scripts/migrate-sessions-jsonl.mjs +16 -2
  296. package/scripts/migrate-vault-paths.mjs +29 -13
  297. package/scripts/parse-config.mjs +71 -14
  298. package/scripts/print-learnings-index.mjs +25 -2
  299. package/scripts/release-session-lock.mjs +94 -7
  300. package/scripts/release.mjs +451 -24
  301. package/scripts/relocate-vault-corpus.mjs +11 -7
  302. package/scripts/repair-invalid-sessions.mjs +68 -3
  303. package/scripts/resolve-session-invocation.mjs +57 -8
  304. package/scripts/run-migrate-v2-cross-repo.mjs +56 -1
  305. package/scripts/run-quality-gate.mjs +33 -7
  306. package/scripts/self-update.mjs +56 -7
  307. package/scripts/session-commit-files.mjs +133 -0
  308. package/scripts/site-numbers.mjs +52 -9
  309. package/scripts/snapshot-probe.mjs +156 -0
  310. package/scripts/so-hud.mjs +68 -0
  311. package/scripts/state-md-idle-reset.mjs +82 -0
  312. package/scripts/sweep-expired-learnings.mjs +54 -6
  313. package/scripts/sweep-expired-rules.mjs +4 -1
  314. package/scripts/validate-plugin.mjs +22 -0
  315. package/scripts/vault-integration-watcher.mjs +54 -6
  316. package/scripts/vault-mirror.mjs +9 -0
  317. package/scripts/vault-orphans.mjs +171 -0
  318. package/skills/_shared/fleet-protocol.md +166 -3
  319. package/skills/_shared/instruction-file-resolution.md +1 -1
  320. package/skills/_shared/monitor-patterns.md +44 -25
  321. package/skills/_shared/platform-tools.md +7 -0
  322. package/skills/_shared/state-ownership.md +5 -4
  323. package/skills/architecture/references/domain-model.md +1 -1
  324. package/skills/autopilot/SKILL.md +3 -2
  325. package/skills/bootstrap/SKILL.md +3 -1
  326. package/skills/bootstrap/_shared-template.md +4 -4
  327. package/skills/bootstrap/fast-template.md +1 -1
  328. package/skills/bootstrap/private-contract.md +1 -1
  329. package/skills/bootstrap/references/bootstrap-retroactive-flow.md +17 -9
  330. package/skills/bootstrap/templates/agents/project-code-review.md +5 -1
  331. package/skills/bootstrap/templates/agents/project-discovery.md +5 -1
  332. package/skills/bootstrap/templates/agents/project-quality-gate.md +5 -1
  333. package/skills/claude-md-drift-check/SKILL.md +3 -3
  334. package/skills/claude-md-drift-check/checker.mjs +61 -101
  335. package/skills/close/SKILL.md +20 -7
  336. package/skills/discovery/probes/docs-staleness.mjs +7 -5
  337. package/skills/discovery/probes/frontend-slop.mjs +7 -3
  338. package/skills/discovery/probes/ssot-code-diff.mjs +5 -3
  339. package/skills/discovery/probes/supply-chain-slopcheck.mjs +10 -8
  340. package/skills/discovery/probes/vault-narrative-staleness.mjs +30 -10
  341. package/skills/discovery/probes/vault-staleness.mjs +5 -3
  342. package/skills/discovery/probes-session.md +3 -0
  343. package/skills/discovery/probes-vault.md +2 -0
  344. package/skills/dispatcher/SKILL.md +1 -1
  345. package/skills/eval/SKILL.md +3 -1
  346. package/skills/evolve/SKILL.md +2 -0
  347. package/skills/evolve/references/evolve-analyze-mode.md +3 -3
  348. package/skills/evolve/references/evolve-dialectic-mode.md +1 -1
  349. package/skills/gitlab-ops/SKILL.md +7 -6
  350. package/skills/gitlab-portfolio/SKILL.md +2 -2
  351. package/skills/go/SKILL.md +13 -1
  352. package/skills/npm-publish/SKILL.md +5 -2
  353. package/skills/plan/SKILL.md +23 -10
  354. package/skills/plan/mode-feature.md +13 -8
  355. package/skills/plan/mode-new.md +6 -14
  356. package/skills/plan/prd-feature-template.md +19 -0
  357. package/skills/plan/prd-full-template.md +12 -5
  358. package/skills/plan/prd-premortem-panel.md +48 -0
  359. package/skills/plan/prd-reviewer-prompt.md +11 -3
  360. package/skills/plan/soul.md +1 -1
  361. package/skills/portfolio/SKILL.md +5 -6
  362. package/skills/quality-gates/SKILL.md +2 -4
  363. package/skills/reconcile/SKILL.md +27 -4
  364. package/skills/release/SKILL.md +16 -8
  365. package/skills/session-end/SKILL.md +97 -13
  366. package/skills/session-end/phase-3-2-docs-verification.md +1 -0
  367. package/skills/session-end/phase-3-6-tail.md +11 -7
  368. package/skills/session-end/phase-3-7a-recommendations.md +9 -2
  369. package/skills/session-end/plan-verification.md +16 -1
  370. package/skills/session-end/references/phase-3-7e-vault-orphans.md +63 -0
  371. package/skills/session-end/references/phase-3-documentation-updates.md +25 -13
  372. package/skills/session-end/references/phase-5-issue-cleanup.md +1 -1
  373. package/skills/session-end/references/session-summary-template.md +8 -0
  374. package/skills/session-end/session-metrics-write.md +7 -4
  375. package/skills/session-plan/SKILL.md +5 -3
  376. package/skills/session-start/SKILL.md +10 -4
  377. package/skills/session-start/phase-4-5-resource-health.md +11 -5
  378. package/skills/session-start/phase-7-5-mode-selector.md +3 -1
  379. package/skills/session-start/phase-8-5-express-path.md +4 -3
  380. package/skills/session-start/references/phase-1-2-session-lock.md +7 -5
  381. package/skills/session-start/references/phase-1-5-session-continuity.md +13 -5
  382. package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
  383. package/skills/session-start/references/phase-4-ssot-environment-check.md +3 -2
  384. package/skills/tmux-layout/SKILL.md +2 -2
  385. package/skills/vault-sync/validator.mjs +3 -2
  386. package/skills/wave-executor/SKILL.md +5 -4
  387. package/skills/wave-executor/references/wave-executor-state-init.md +3 -3
  388. package/skills/wave-executor/references/wave-loop-dispatch.md +11 -5
  389. package/skills/wave-executor/references/wave-loop-review.md +13 -5
  390. package/skills/wave-executor/references/wave-loop-scope-manifest.md +1 -1
  391. package/AGENTS.md +0 -207
  392. package/assets/og-card.png +0 -0
  393. package/assets/og-card.svg +0 -47
  394. package/assets/wave-lifecycle.svg +0 -98
@@ -0,0 +1,394 @@
1
+ // ---- Heredoc bodies the shell expands (#1696) ---------------------------------
2
+ //
3
+ // One pass over a command text with an explicit state machine (quotes, `$'…'`, `${…}`, `$(…)`, backticks,
4
+ // arithmetic, comments) and a queue of the heredoc operators read on the current line. At a newline that the
5
+ // shell reads as a command terminator, the queued bodies follow in order; each body line is read once, so the
6
+ // whole scan is linear in the text. The scanner judges nothing itself: `secretIn` names the secret a body line
7
+ // expands, `prints` says whether the stage a heredoc feeds reaches the transcript — both supplied by rules.ts.
8
+
9
+ /** A stage of a pipeline as the scanner cut it: `[start, end)` in the text, -1 while open; `pipe`: the stage after `|`. */
10
+ type Stage = { start: number; end: number; pipe: Stage | null; cuts: Array<[number, number]>; verdict: boolean | undefined }
11
+
12
+ type Pending = { delimiter: string; quoted: boolean; tabs: boolean; stage: Stage; nested: boolean }
13
+
14
+ /** What a stage feeds: its text without the heredoc operators, and the next stage (`null`: unreadable, `undefined`: none). */
15
+ export type HeredocStage = { text: string | null; next: string | null | undefined; nested: boolean }
16
+
17
+ export type HeredocFinding =
18
+ | { kind: 'body'; name: string; delimiter: string }
19
+ | { kind: 'fail-closed'; reason: 'unbalanced' | 'too-many' | 'no-delimiter' | 'unreadable' }
20
+
21
+ // Frames of the state machine. `code` frames read commands and heredoc operators; the others only quoting.
22
+ type Frame = 'root' | 'subst' | 'paren' | 'procsub' | 'tick' | 'dq' | 'brace' | 'dqbrace' | 'arith' | 'bracket'
23
+
24
+ // Heredoc operators queued on one line before the bodies are read. Real commands use one or two; past the cap the
25
+ // line is not read (the operator count was the lever of the reverted attempt's quadratic run) — fail-closed.
26
+ // NAMED CEILING (BV-004): 16; revisit if a legitimate command needs more heredocs on one line.
27
+ const MAX_PENDING = 16
28
+ // The characters after which an unquoted `#` starts a comment (the start of a word).
29
+ const WORD_BREAK = /[\s;&|()<>]/
30
+ // Where an unquoted heredoc delimiter word ends. Not `\r`: the shell keeps it in the word, so `<<EOF` on a CRLF line
31
+ // closes only at `EOF\r` — ending the word there too keeps the body from running past the shell's closing line.
32
+ const WORD_END = /[ \t\n;&|<>()]/
33
+
34
+ // Reserved words after which the next word is a command again (`if x; then case …`).
35
+ const COMMAND_WORDS: ReadonlySet<string> = new Set(['then', 'do', 'else', 'elif', 'if', 'while', 'until', '!', 'time'])
36
+
37
+ const CODE: ReadonlySet<Frame> = new Set(['root', 'subst', 'paren', 'procsub', 'tick'])
38
+ // Frames whose output a command reads instead of the transcript seeing it directly: judged as printing (fail-closed).
39
+ const CAPTURED: ReadonlySet<Frame> = new Set(['subst', 'procsub', 'tick'])
40
+
41
+ /**
42
+ * Reads every heredoc in `text` and returns the first one whose body expands a secret while its stage prints
43
+ * (`prints`), or a fail-closed finding: an unbalanced quote or construct at the end of the text, more than
44
+ * {@link MAX_PENDING} operators on one line, or an operator without a delimiter word. A quoted delimiter
45
+ * (`<<'EOF'`, `<<"EOF"`, `<<\EOF`, any quote in the word) keeps its body literal and is never a finding. Whether a
46
+ * fail-closed finding matters (a secret named anywhere in the text) is the caller's call. Stops at the first finding.
47
+ */
48
+ export function scanHeredocs(
49
+ text: string,
50
+ secretIn: (line: string) => string | undefined,
51
+ prints: (stage: HeredocStage) => boolean,
52
+ ): HeredocFinding | null {
53
+ const frames: Frame[] = ['root']
54
+ // One open stage per code frame, parallel to `frames` (null for quoting frames).
55
+ const stages: Array<Stage | null> = [newStage(0)]
56
+ let pending: Pending[] = []
57
+ // Per frame: `(`/`[` inside arithmetic, `{` inside a brace, open `case` words inside a code frame.
58
+ const depth: number[] = [0]
59
+ const n = text.length
60
+ // The `)` that ended the last case pattern: the word after it is a command (`a) case y in …`), unlike after `$(x)`.
61
+ let patternClose = -1
62
+
63
+ const top = (): Frame => frames[frames.length - 1] ?? 'root'
64
+ const push = (frame: Frame, at: number): void => {
65
+ frames.push(frame)
66
+ stages.push(CODE.has(frame) ? newStage(at) : null)
67
+ depth.push(0)
68
+ }
69
+ const pop = (at: number): void => {
70
+ const stage = stages[stages.length - 1]
71
+ if (stage !== null && stage !== undefined && stage.end === -1) stage.end = at
72
+ frames.pop()
73
+ stages.pop()
74
+ depth.pop()
75
+ }
76
+ // Ends the open stage of the current code frame at `at`; a pipe links it to the stage that starts at `from`.
77
+ const cut = (at: number, from: number, pipe: boolean): void => {
78
+ const stage = stages[stages.length - 1]
79
+ if (stage === null || stage === undefined) return
80
+ stage.end = at
81
+ const next = newStage(from)
82
+ if (pipe) stage.pipe = next
83
+ stages[stages.length - 1] = next
84
+ }
85
+ const nested = (): boolean => frames.some(frame => CAPTURED.has(frame))
86
+
87
+ for (let i = 0; i < n; i += 1) {
88
+ const ch = text[i] ?? ''
89
+ const next = text[i + 1] ?? ''
90
+ const frame = top()
91
+
92
+ if (frame === 'dq' || frame === 'dqbrace' || frame === 'brace') {
93
+ if (ch === '\\') i += 1
94
+ else if (frame === 'dq' && ch === '"') pop(i)
95
+ else if (frame !== 'dq' && ch === '}') pop(i)
96
+ // A raw `{` inside `${…}` (`"${x:-{}"`): the shells disagree on which `}` closes it — fail-closed (#1696 M2).
97
+ // A nested `${` opens its own frame through openDollar.
98
+ else if (frame !== 'dq' && ch === '{') return { kind: 'fail-closed', reason: 'unreadable' }
99
+ else if (frame !== 'dq' && ch === '"') push('dq', i)
100
+ else if (frame === 'brace' && ch === "'") {
101
+ const close = text.indexOf("'", i + 1)
102
+ if (close === -1) return { kind: 'fail-closed', reason: 'unbalanced' }
103
+ i = close
104
+ } else if (ch === '$' || ch === '`') i = openDollar(i, frame === 'brace' ? 'brace' : 'dqbrace')
105
+ continue
106
+ }
107
+ if (frame === 'arith' || frame === 'bracket') {
108
+ const [open, close] = frame === 'arith' ? ['(', ')'] : ['[', ']']
109
+ const d = depth[depth.length - 1] ?? 0
110
+ // A newline, or `$((`/`((` closed by `) )` instead of `))`, is a command in a subshell, not arithmetic: the
111
+ // shells run `$((cat <<EOF … ) )` (#1696 M1) — fail-closed rather than reading its heredoc as arithmetic.
112
+ if (ch === '\n') return { kind: 'fail-closed', reason: 'unreadable' }
113
+ if (ch === open) depth[depth.length - 1] = d + 1
114
+ else if (ch === close && d > 0) depth[depth.length - 1] = d - 1
115
+ else if (ch === close) {
116
+ if (frame === 'arith' && next !== ')') return { kind: 'fail-closed', reason: 'unreadable' }
117
+ if (frame === 'arith') i += 1
118
+ pop(i)
119
+ }
120
+ continue
121
+ }
122
+
123
+ // A code frame.
124
+ if (ch === '\\') {
125
+ i += 1
126
+ continue
127
+ }
128
+ if (ch === "'") {
129
+ const close = text.indexOf("'", i + 1)
130
+ if (close === -1) return { kind: 'fail-closed', reason: 'unbalanced' }
131
+ i = close
132
+ continue
133
+ }
134
+ if (ch === '"') {
135
+ push('dq', i)
136
+ continue
137
+ }
138
+ if (ch === '$' || ch === '`') {
139
+ if (ch === '`' && frame === 'tick') pop(i)
140
+ else i = openDollar(i, 'code')
141
+ continue
142
+ }
143
+ if (ch === '#' && (i === 0 || WORD_BREAK.test(text[i - 1] ?? ''))) {
144
+ const nl = text.indexOf('\n', i)
145
+ i = (nl === -1 ? n : nl) - 1
146
+ continue
147
+ }
148
+ if (ch === '<' && next === '<' && text[i + 2] === '<') {
149
+ i += 2
150
+ continue
151
+ }
152
+ if (ch === '<' && next === '<') {
153
+ const op = readOperator(i + 2)
154
+ if (op === null) return { kind: 'fail-closed', reason: 'no-delimiter' }
155
+ if (op === 'unbalanced') return { kind: 'fail-closed', reason: 'unbalanced' }
156
+ const stage = stages[stages.length - 1] ?? newStage(i)
157
+ stage.cuts.push([i, op.end])
158
+ pending.push({ delimiter: op.delimiter, quoted: op.quoted, tabs: op.tabs, stage, nested: nested() })
159
+ if (pending.length > MAX_PENDING) return { kind: 'fail-closed', reason: 'too-many' }
160
+ i = op.end - 1
161
+ continue
162
+ }
163
+ if ((ch === '<' || ch === '>') && next === '(') {
164
+ push('procsub', i + 2)
165
+ i += 1
166
+ continue
167
+ }
168
+ if (ch === '(' && next === '(') {
169
+ push('arith', i)
170
+ i += 1
171
+ continue
172
+ }
173
+ if (ch === '(') {
174
+ cut(i, i + 1, false)
175
+ push('paren', i + 1)
176
+ continue
177
+ }
178
+ if ((ch === 'c' || ch === 'e') && (i === 0 || WORD_BREAK.test(text[i - 1] ?? '')) && commandPosition(i)) {
179
+ // In a code frame the depth counts open `case` words: the `)` after a pattern closes no `$(` (#1696:
180
+ // `"$(case x in x) cat <<A;; esac …)"` read the `)` as the end and the heredoc as quoted text). Only in
181
+ // command position: `echo esac` is an argument and closes nothing (M3).
182
+ const word = /^(?:case|esac)(?=[ \t\n;&|<>()]|$)/.exec(text.slice(i, i + 5))?.[0]
183
+ const d = depth[depth.length - 1] ?? 0
184
+ if (word === 'case') depth[depth.length - 1] = d + 1
185
+ else if (word === 'esac' && d > 0) depth[depth.length - 1] = d - 1
186
+ if (word !== undefined) i += 3
187
+ continue
188
+ }
189
+ if (ch === ')') {
190
+ if ((depth[depth.length - 1] ?? 0) > 0) patternClose = i
191
+ else if (frame === 'subst' || frame === 'paren' || frame === 'procsub') pop(i)
192
+ continue
193
+ }
194
+ if (ch === '|') {
195
+ if (next === '|') {
196
+ cut(i, i + 2, false)
197
+ i += 1
198
+ } else {
199
+ cut(i, i + (next === '&' ? 2 : 1), true)
200
+ if (next === '&') i += 1
201
+ }
202
+ continue
203
+ }
204
+ if (ch === ';' || ch === '&') {
205
+ // `&&`, `;;`, a lone `&` end the command; `&>` / `>&` / `<&` are redirections, read with the stage.
206
+ if (ch === '&' && (next === '>' || text[i - 1] === '>' || text[i - 1] === '<')) continue
207
+ const width = next === ch ? 2 : 1
208
+ cut(i, i + width, false)
209
+ i += width - 1
210
+ continue
211
+ }
212
+ if (ch === '\n') {
213
+ cut(i, i + 1, false)
214
+ if (pending.length === 0) continue
215
+ const found = readBodies(i + 1)
216
+ if (typeof found !== 'number') return found
217
+ pending = []
218
+ // The bodies end at `found`, the start of the next command line; the code frame is unchanged.
219
+ const stage = stages[stages.length - 1]
220
+ if (stage !== null && stage !== undefined) stage.start = found
221
+ i = found - 1
222
+ }
223
+ }
224
+ // Operators on the last line with no newline after them have no body.
225
+ return frames.length === 1 ? null : { kind: 'fail-closed', reason: 'unbalanced' }
226
+
227
+ /**
228
+ * Whether the word at `at` stands where a command starts: at the text start, after `;`, `&`, `|`, `(`, `{`, a
229
+ * backtick, a newline or the `)` of a case pattern, or after a reserved word that takes a command (`then`, `do`, …). Reads back over the
230
+ * blanks and the one word before `at` only, so every character is read back at most once (linear).
231
+ */
232
+ function commandPosition(at: number): boolean {
233
+ let p = at - 1
234
+ while (p >= 0 && (text[p] === ' ' || text[p] === '\t')) p -= 1
235
+ if (p < 0 || p === patternClose || /[;&|({`\n]/.test(text[p] ?? '')) return true
236
+ let w = p
237
+ while (w >= 0 && /[a-z!]/.test(text[w] ?? '')) w -= 1
238
+ if (w >= 0 && !/[\s;&|({`]/.test(text[w] ?? '')) return false
239
+ return COMMAND_WORDS.has(text.slice(w + 1, p + 1))
240
+ }
241
+
242
+ /** Opens the construct a `$` or backtick at `at` starts; returns the index the loop continues from. */
243
+ function openDollar(at: number, context: 'code' | 'brace' | 'dqbrace'): number {
244
+ const ch = text[at]
245
+ const next = text[at + 1]
246
+ if (ch === '`') {
247
+ push('tick', at + 1)
248
+ return at
249
+ }
250
+ if (next === '(' && text[at + 2] === '(') {
251
+ push('arith', at)
252
+ return at + 2
253
+ }
254
+ if (next === '(') {
255
+ push('subst', at + 2)
256
+ return at + 1
257
+ }
258
+ if (next === '[') {
259
+ push('bracket', at)
260
+ return at + 1
261
+ }
262
+ if (next === '{') {
263
+ push(context === 'code' || context === 'brace' ? 'brace' : 'dqbrace', at)
264
+ return at + 1
265
+ }
266
+ if (next === "'" && context !== 'dqbrace') {
267
+ // `$'…'`: a backslash escapes the next character, `\'` included.
268
+ for (let j = at + 2; j < n; j += 1) {
269
+ if (text[j] === '\\') j += 1
270
+ else if (text[j] === "'") return j
271
+ }
272
+ return n
273
+ }
274
+ return at
275
+ }
276
+
277
+ /** The delimiter word after `<<` at `from`; null when there is none, 'unbalanced' for an open quote in it. */
278
+ function readOperator(from: number): { delimiter: string; quoted: boolean; tabs: boolean; end: number } | null | 'unbalanced' {
279
+ let j = from
280
+ const tabs = text[j] === '-'
281
+ if (tabs) j += 1
282
+ while (text[j] === ' ' || text[j] === '\t') j += 1
283
+ const parts: string[] = []
284
+ let quoted = false
285
+ for (; j < n; j += 1) {
286
+ const c = text[j] ?? ''
287
+ if (WORD_END.test(c)) break
288
+ // A substitution in the delimiter word (`<<$(x)`): the shells take it literally, this reading would end the
289
+ // word at its `(` — fail-closed instead of guessing (#1696 M4).
290
+ if ((c === '$' && text[j + 1] === '(') || c === '`') return null
291
+ if (c === '\\') {
292
+ // `\` + newline continues the line and quotes nothing: `<<EO\<LF>F` is the unquoted delimiter EOF.
293
+ if (text[j + 1] !== '\n') {
294
+ quoted = true
295
+ parts.push(text[j + 1] ?? '')
296
+ }
297
+ j += 1
298
+ } else if (c === "'" || c === '"') {
299
+ quoted = true
300
+ let k = j + 1
301
+ for (; k < n && text[k] !== c; k += 1) {
302
+ if (c === '"' && text[k] === '\\') k += 1
303
+ }
304
+ if (k >= n) return 'unbalanced'
305
+ parts.push(text.slice(j + 1, k))
306
+ j = k
307
+ } else parts.push(c)
308
+ }
309
+ const delimiter = parts.join('')
310
+ return delimiter === '' && !quoted ? null : { delimiter, quoted, tabs, end: j }
311
+ }
312
+
313
+ /**
314
+ * Reads the queued bodies from `from` in order: each runs to its closing line or to the end of the text. Returns
315
+ * the index past the last closing line, or the first finding. Each stage is judged at most once (its verdict is
316
+ * kept), so many heredocs feeding one stage cost one judgement.
317
+ */
318
+ function readBodies(from: number): number | HeredocFinding {
319
+ let at = from
320
+ for (const doc of pending) {
321
+ let hit = false
322
+ let closed = false
323
+ // An unquoted body joins a line that ends in an odd run of `\` with the next one, as the shell does: neither
324
+ // `foo\` + `EOF` closes the body nor does `$T_TO\` + `KEN` hide the name. A quoted body keeps every line.
325
+ let carried = ''
326
+ // Whether the current physical line continues an earlier one: `carried` alone cannot tell, because a continued
327
+ // line that was only `\` leaves it empty (#1696 R2: `<<-EOF`, `\`, `<tab>EOF` does not close in zsh).
328
+ let continued = false
329
+ while (at <= n) {
330
+ const nl = text.indexOf('\n', at)
331
+ const end = nl === -1 ? n : nl
332
+ let physical = text.slice(at, end)
333
+ at = nl === -1 ? n + 1 : nl + 1
334
+ // `<<-` strips the tabs at the start of the logical line only: a continued line keeps its own (#1696 M5).
335
+ if (doc.tabs && !continued) physical = physical.replace(/^\t+/, '')
336
+ if (!doc.quoted && nl !== -1 && oddSlashes(physical)) {
337
+ carried += physical.slice(0, -1)
338
+ continued = true
339
+ continue
340
+ }
341
+ const line = carried + physical
342
+ carried = ''
343
+ continued = false
344
+ if (line === doc.delimiter) {
345
+ closed = true
346
+ break
347
+ }
348
+ if (hit) continue
349
+ const name = secretIn(line)
350
+ if (name === undefined) continue
351
+ hit = true
352
+ if (!doc.quoted && judge(doc)) return { kind: 'body', name, delimiter: doc.delimiter }
353
+ }
354
+ // A literal body that never closes swallowed the rest of the text unread: if the shell closed it where this
355
+ // reading did not (a delimiter told apart differently), a secret behind it went unseen — fail-closed.
356
+ if (doc.quoted && hit && !closed) return { kind: 'fail-closed', reason: 'unbalanced' }
357
+ }
358
+ return Math.min(at, n)
359
+ }
360
+
361
+ function judge(doc: Pending): boolean {
362
+ const { stage } = doc
363
+ if (stage.verdict === undefined) {
364
+ const pipe = stage.pipe
365
+ stage.verdict = prints({
366
+ text: stage.end === -1 ? null : stageText(stage),
367
+ next: pipe === null ? undefined : pipe.end === -1 ? null : stageText(pipe),
368
+ nested: doc.nested,
369
+ })
370
+ }
371
+ return stage.verdict
372
+ }
373
+
374
+ function stageText(stage: Stage): string {
375
+ let out = ''
376
+ let from = stage.start
377
+ for (const [a, b] of stage.cuts) {
378
+ out += `${text.slice(from, a)} `
379
+ from = b
380
+ }
381
+ return out + text.slice(from, stage.end)
382
+ }
383
+ }
384
+
385
+ /** Whether a line ends in an odd run of backslashes (the last one escapes the newline). */
386
+ function oddSlashes(line: string): boolean {
387
+ let count = 0
388
+ for (let j = line.length - 1; j >= 0 && line[j] === '\\'; j -= 1) count += 1
389
+ return count % 2 === 1
390
+ }
391
+
392
+ function newStage(start: number): Stage {
393
+ return { start, end: -1, pipe: null, cuts: [], verdict: undefined }
394
+ }
@@ -0,0 +1 @@
1
+ { "modules": ["./register.tsx"] }
@@ -0,0 +1,182 @@
1
+ import type { SoBeacon, SoProfile, SoSnapshot } from '../types/index'
2
+
3
+ export function ownerQuestion(answer: string): boolean {
4
+ let fence: string | null = null
5
+ const plain = answer.split('\n').filter(line => {
6
+ const mark = /^\s{0,3}(`{3,}|~{3,})/.exec(line)?.[1]
7
+ if (mark) {
8
+ if (!fence) fence = mark
9
+ else if (mark[0] === fence[0] && mark.length >= fence.length) fence = null
10
+ return false
11
+ }
12
+ return fence === null
13
+ }).join('\n').trim()
14
+ return plain.endsWith('?') || /weiter mit|soll ich|welche option|freigabe für/i.test(plain.slice(-400))
15
+ }
16
+ export const tables = {
17
+ de: { red: 'rot', error: 'Fehler', off: 'aus', partial: 'teilweise gemessen', setup: 'nicht eingerichtet', norun: 'noch kein Lauf', measurement: 'Messung fehlgeschlagen', mirror: 'Vault-Spiegel fehlgeschlagen', peer: 'Peer im selben Working Copy', scope: 'Peer ohne bekannten Scope im selben Working Copy', context: 'Kontext', cold: 'Cache kalt' },
18
+ en: { red: 'red', error: 'error', off: 'off', partial: 'partially measured', setup: 'not configured', norun: 'no run yet', measurement: 'Measurement failed', mirror: 'Vault mirror failed', peer: 'Peer in the same working copy', scope: 'Peer with unknown scope in the same working copy', context: 'Context', cold: 'Cache cold' },
19
+ } as const
20
+ /** Bound each displayed field; replace terminal controls and line separators. */
21
+ export function displayText(value: unknown, limit = 120): string {
22
+ const plain = String(value ?? '').replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, ' ')
23
+ return plain.length <= limit ? plain : `${plain.slice(0, limit - 1)}…`
24
+ }
25
+ const measurementProblems = new Set(['refresh-failed', 'collector-failed', 'invalid-snapshot'])
26
+ function measurementFailure(problems: readonly string[]): string[] {
27
+ return problems.filter(p => measurementProblems.has(p))
28
+ }
29
+ export function time(at: string | null): string {
30
+ if (!at || !Number.isFinite(Date.parse(at))) return '?'
31
+ const d = new Date(at)
32
+ return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
33
+ }
34
+ export function statusText(s: SoSnapshot | null, profile: SoProfile, language: keyof typeof tables = 'de', problems: readonly string[] = []): string {
35
+ const t = tables[language]
36
+ if (!s) {
37
+ const failed = measurementFailure(problems)
38
+ return displayText(`SO ${failed.length ? `${t.measurement} (${failed.join(', ')})` : t.setup} · ${profile}`)
39
+ }
40
+ const g = s.gate.last
41
+ const vault = s.vault
42
+ const bad = vault.last_run && (vault.last_run.aborted || (vault.last_run.failed ?? 0) > 0)
43
+ // enabled null has two causes: no vault-integration block (not configured) or an unreadable CLAUDE.md/AGENTS.md (unknown).
44
+ const vaultUnset = s.degraded.includes('config:unreadable') ? '?' : t.setup
45
+ // Enabled but never mirrored is a state, not an unknown; '?' stays for a run whose time does not parse.
46
+ const vaultOn = vault.last_run === null ? t.norun : time(vault.last_run?.at ?? null)
47
+ // A repo where no session ever wrote events is fresh, not partially measured (F13): nothing is missing there.
48
+ const partial = s.degraded.some(d => d !== 'events:absent')
49
+ return displayText([s.state.session_type, s.wave.number !== null ? `W${s.wave.number}/${s.state.total_waves ?? '?'}` : null,
50
+ s.wave.role, agentsText(s.agents),
51
+ g ? `Gate ${g.passed === true ? 'ok' : g.passed === false ? t.red : '?'} ${time(g.at)}` : null,
52
+ `${s.degraded.some(d => /discovery|registry/.test(d)) ? '?' : s.peers.length} Peer`, `Vault ${vault.enabled === false ? t.off : bad ? t.error : vault.enabled === true ? vaultOn : vaultUnset}`, profile,
53
+ partial ? t.partial : null].filter(Boolean).map(value => displayText(value)).join(' · '), 800)
54
+ }
55
+ /**
56
+ * The running agents of this session from the collector's live map (#1580, #1633): a number is measured, `null`
57
+ * is unknown (`? Agents`, a map older than the session says nothing). Shown not at all without a session
58
+ * (`no-session`: nothing to count for) or from a collector before #1580 that sends no `running` — never as 0.
59
+ */
60
+ function agentsText(agents: SoSnapshot['agents'] | undefined): string | null {
61
+ const running = agents?.running
62
+ if (running === undefined || agents?.stale_reason === 'no-session') return null
63
+ const decayed = agents?.decayed
64
+ // Agents without a Stop past 24 h (a parallel collector change): counted apart, shown only when there are some.
65
+ const stale = typeof decayed === 'number' && Number.isSafeInteger(decayed) && decayed > 0 ? ` (+${decayed} ?)` : ''
66
+ return `${typeof running === 'number' && Number.isSafeInteger(running) && running >= 0 ? running : '?'} Agents${stale}`
67
+ }
68
+ export function findings(s: SoSnapshot | null, context: { percent?: number; tokens?: number }, lastRequest: string | null, now: number, problems: readonly string[] = []): string[] {
69
+ const failed = measurementFailure(problems)
70
+ const out: string[] = failed.length ? [`${tables.de.measurement}: ${failed.join(', ')}`] : []
71
+ if (s?.gate.last?.passed === false) out.push(`Gate rot: ${s.gate.last.variant ?? '?'} ${s.gate.last.check_failed === true ? 'check_failed' : s.gate.last.exit_code ?? '?'}`)
72
+ if (s?.peers.some(p => p.gleiches_working_copy)) out.push(s.peers.some(p => p.gleiches_working_copy && p.scope_paths === null) ? tables.de.scope : tables.de.peer)
73
+ if (s?.vault.enabled && s.vault.last_run && (s.vault.last_run.aborted || (s.vault.last_run.failed ?? 0) > 0)) out.push(`${tables.de.mirror}: ${s.vault.last_run.aborted ?? s.vault.last_run.failed}`)
74
+ if ((context.percent ?? -1) >= 80) out.push(`Kontext ${context.percent} %`)
75
+ if (lastRequest && now - Date.parse(lastRequest) >= 3_600_000 && (context.tokens ?? 0) >= 100_000) out.push(tables.de.cold)
76
+ return out.map(value => displayText(value))
77
+ }
78
+ export const validId = (v: unknown): v is string => typeof v === 'string' && /^[a-zA-Z0-9_-]{1,200}$/.test(v)
79
+ export function validateBeacon(v: unknown): v is SoBeacon {
80
+ if (!v || typeof v !== 'object') return false
81
+ const b = v as SoBeacon
82
+ const date = (s: unknown) => typeof s === 'string' && Number.isFinite(Date.parse(s))
83
+ const nullableString = (s: unknown) => s === null || typeof s === 'string'
84
+ return b.schema_version === 1 && validId(b.session_id) && ['arbeitet', 'wartet', 'bereit', 'blockiert', 'unbekannt'].includes(b.zustand)
85
+ && date(b.seit) && date(b.aktualisiert_at) && (b.letzter_request_at === null || date(b.letzter_request_at))
86
+ && [b.name, b.repo_name, b.branch, b.modell].every(nullableString)
87
+ && (b.pid === null || Number.isSafeInteger(b.pid) && b.pid > 0)
88
+ && (b.welle === null || Number.isSafeInteger(b.welle) && b.welle > 0)
89
+ && (b.kontext_prozent === null || Number.isFinite(b.kontext_prozent) && b.kontext_prozent >= 0 && b.kontext_prozent <= 100)
90
+ && typeof b.heuristisch === 'boolean' && [null, 'dialog', 'freigabe', 'frage'].includes(b.warte_grund)
91
+ && (b.zustand === 'wartet' ? b.warte_grund !== null : b.warte_grund === null)
92
+ && b.heuristisch === (b.warte_grund === 'frage')
93
+ }
94
+ export function ageBeacon(b: SoBeacon, now: number, alive: boolean): SoBeacon {
95
+ return now - Date.parse(b.aktualisiert_at) > 120_000 && !alive
96
+ ? { ...b, zustand: 'unbekannt', warte_grund: null, heuristisch: false } : b
97
+ }
98
+ export function completeState(answer: string): Pick<SoBeacon, 'zustand' | 'warte_grund' | 'heuristisch'> {
99
+ return ownerQuestion(answer) ? { zustand: 'wartet', warte_grund: 'frage', heuristisch: true } : { zustand: 'bereit', warte_grund: null, heuristisch: false }
100
+ }
101
+
102
+ // ---- Which hooks the git steps before a push can fire (#1574 proposal 3) ----
103
+ // Per step as rules.ts records it (`gitStep`), the hook names githooks(5) of git 2.56 lists for that command.
104
+ // A ref update fires reference-transaction, an index write post-index-change; commit and fetch end in
105
+ // `gc --auto` (pre-auto-gc); a step that reads the index may query a core.fsmonitor hook. Options a step may carry
106
+ // are not seen here (`commit --amend` → post-rewrite, `--no-verify` skips pre-commit), so each entry is the union.
107
+ const FIREABLE_HOOKS: ReadonlyMap<string, readonly string[]> = new Map<string, readonly string[]>([
108
+ ['add', ['post-index-change', 'fsmonitor-watchman']],
109
+ ['status', ['post-index-change', 'fsmonitor-watchman']],
110
+ ['diff', ['post-index-change', 'fsmonitor-watchman']],
111
+ ['ls-files', ['fsmonitor-watchman']],
112
+ ['commit', ['pre-commit', 'prepare-commit-msg', 'commit-msg', 'post-commit', 'post-rewrite', 'reference-transaction', 'post-index-change', 'pre-auto-gc', 'fsmonitor-watchman']],
113
+ ['fetch', ['reference-transaction', 'pre-auto-gc']],
114
+ ['checkout -- <paths>', ['post-checkout', 'post-index-change', 'fsmonitor-watchman']],
115
+ ...['log', 'show', 'log [signature]', 'show [signature]', 'rev-parse', 'branch --show-current', 'tag --list'].map((step): [string, readonly string[]] => [step, []]),
116
+ ])
117
+
118
+ /**
119
+ * The hook names the git steps earlier on a push's line can fire; null when any step is not in the map, which
120
+ * means any hook may run (fail-closed). Steps outside rules.ts's LOCAL_GIT already ask there, so they need no entry.
121
+ */
122
+ export function fireableHooks(steps: readonly string[] | undefined): ReadonlySet<string> | null {
123
+ const names = new Set<string>()
124
+ for (const step of steps ?? []) {
125
+ const fired = FIREABLE_HOOKS.get(step)
126
+ if (fired === undefined) return null
127
+ for (const name of fired) names.add(name)
128
+ }
129
+ return names
130
+ }
131
+
132
+ /**
133
+ * Whether a hook defined in config (`git config --null --get-regexp ^hook\.` output) runs on one of `names`
134
+ * (null: on any event): a `hook.<friendly-name>.event` naming one of them, or a friendly name that is one.
135
+ * `.enabled` is not honoured (a disabled hook still counts). null when the output does not parse or holds a
136
+ * `hook.*` key git 2.56 does not document (asks).
137
+ */
138
+ export function configHooksFire(output: string, names: ReadonlySet<string> | null): boolean | null {
139
+ if (!output.endsWith('\0')) return null
140
+ let fires = false
141
+ for (const entry of output.slice(0, -1).split('\0')) {
142
+ const cut = entry.indexOf('\n')
143
+ const key = cut === -1 ? entry : entry.slice(0, cut)
144
+ if (key === 'hook.jobs') continue
145
+ const parts = /^hook\.(.+)\.(command|event|enabled|parallel|jobs)$/.exec(key)
146
+ if (!parts) return null
147
+ const [, friendly = '', variable] = parts
148
+ if (cut === -1 && (variable === 'event' || variable === 'command')) return null
149
+ const value = entry.slice(cut + 1).trim().toLowerCase()
150
+ if (variable === 'event' && value !== '' && (names === null || names.has(value))) fires = true
151
+ if (variable === 'command' && (names === null || names.has(friendly.toLowerCase()))) fires = true
152
+ }
153
+ return fires
154
+ }
155
+
156
+ // ---- Profile band and /profile (#1552). SSOT of the effects: docs/mods.md § Profiles. ----
157
+ export const PROFILES: readonly SoProfile[] = ['locker', 'ausgewogen', 'streng']
158
+ const PROFILE_EFFECT: Record<SoProfile, string> = {
159
+ locker: 'Regel-Fallen und Kollisionen nur Warnung.',
160
+ ausgewogen: 'Regel-Fallen abgelehnt, Kollision fragt.',
161
+ streng: 'Regel-Fallen und Kollisionen abgelehnt.',
162
+ }
163
+ const PROFILE_ALWAYS = 'Auf jedem Profil: Secrets ausgeben wird abgelehnt, Außenwirkung wird zweimal bestätigt (interaktiv).'
164
+ /** Any value outside the three profiles falls back to the default `ausgewogen`, as the engine does for a stored value outside `options`. */
165
+ export function profileOf(value: unknown): SoProfile {
166
+ return value === 'locker' || value === 'streng' ? value : 'ausgewogen'
167
+ }
168
+ /** The band's lines: the profile (marked when it is only the default), what it loosens or locks, what holds on every profile. */
169
+ export function profileLines(profile: SoProfile, chosen: boolean): string[] {
170
+ return [`Profil: ${profile}${chosen ? '' : ' (Vorgabe, noch nicht gewählt)'}`, PROFILE_EFFECT[profile], PROFILE_ALWAYS]
171
+ }
172
+ /** /profile's output: readable alone in `claude -p "/profile"`, where no band is drawn. */
173
+ export function profileText(profile: SoProfile, chosen: boolean): string {
174
+ return [...profileLines(profile, chosen), `Wechseln: im Band über dem Prompt (${PROFILES.join(', ')}) oder in /config, Eintrag strictness von so-hud.`].join('\n')
175
+ }
176
+ /** Whether settings hold an explicit strictness for this plugin under `pluginConfigs[<plugin>[@<marketplace>]].options`. */
177
+ export function holdsProfile(settings: unknown, plugin: string): boolean {
178
+ const configs: unknown = (settings as { pluginConfigs?: unknown } | null)?.pluginConfigs
179
+ if (!configs || typeof configs !== 'object') return false
180
+ return Object.entries(configs).some(([key, entry]) => key.split('@')[0] === plugin
181
+ && (PROFILES as readonly unknown[]).includes((entry as { options?: { strictness?: unknown } } | null)?.options?.strictness))
182
+ }