session-orchestrator 3.22.0 → 3.23.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 (268) 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/repo-audit/SKILL.md +13 -0
  74. package/.cursor/skills/session-end/SKILL.md +13 -0
  75. package/.cursor/skills/session-plan/SKILL.md +13 -0
  76. package/.cursor/skills/session-start/SKILL.md +13 -0
  77. package/.cursor/skills/skill-creator/SKILL.md +13 -0
  78. package/.cursor/skills/spinout/SKILL.md +12 -0
  79. package/.cursor/skills/sunset-review/SKILL.md +13 -0
  80. package/.cursor/skills/test-runner/SKILL.md +13 -0
  81. package/.cursor/skills/tmux-layout/SKILL.md +13 -0
  82. package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
  83. package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
  84. package/.cursor/skills/vault-mirror/SKILL.md +13 -0
  85. package/.cursor/skills/vault-sync/SKILL.md +13 -0
  86. package/.cursor/skills/wave-executor/SKILL.md +13 -0
  87. package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
  88. package/.mcp.json +4 -1
  89. package/CHANGELOG.md +168 -0
  90. package/README.md +18 -15
  91. package/agents/AGENTS.md +23 -4
  92. package/agents/code-implementer.md +2 -1
  93. package/agents/db-specialist.md +2 -1
  94. package/agents/docs-writer.md +3 -1
  95. package/agents/eval-judge.md +1 -1
  96. package/agents/session-reviewer.md +7 -1
  97. package/agents/test-writer.md +2 -1
  98. package/agents/ui-developer.md +2 -1
  99. package/commands/bootstrap.md +2 -2
  100. package/commands/close.md +3 -1
  101. package/commands/go.md +1 -1
  102. package/commands/journey-audit.md +43 -0
  103. package/docs/USER-GUIDE.md +2 -2
  104. package/docs/ci-setup.md +14 -0
  105. package/docs/codex-setup.md +64 -0
  106. package/docs/components.md +6 -6
  107. package/docs/cursor-setup.md +26 -47
  108. package/docs/events-schema.md +76 -4
  109. package/docs/github-mirror-protection.md +197 -0
  110. package/docs/pi-setup.md +2 -0
  111. package/docs/rule-authoring.md +3 -1
  112. package/docs/scope-collision-guard.md +49 -2
  113. package/docs/session-config-reference.md +26 -4
  114. package/docs/session-config-template.md +4 -3
  115. package/docs/telemetry.md +22 -0
  116. package/hooks/_lib/lock-bootstrap.mjs +8 -4
  117. package/hooks/_lib/vcs-create-matcher.mjs +397 -38
  118. package/hooks/enforce-scope.mjs +64 -0
  119. package/hooks/hooks-codex.json +1 -1
  120. package/hooks/hooks-cursor.json +201 -20
  121. package/hooks/hooks-pi.json +1 -1
  122. package/hooks/hooks.json +2 -2
  123. package/hooks/on-session-end.mjs +211 -10
  124. package/hooks/on-session-start.mjs +214 -11
  125. package/hooks/on-stop.mjs +48 -9
  126. package/hooks/post-subagent-discovery-validator.mjs +34 -3
  127. package/hooks/post-tool-batch-wave-signal.mjs +11 -2
  128. package/hooks/pre-bash-issue-budget.mjs +117 -4
  129. package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
  130. package/hooks/pre-bash-staging-fence.mjs +4 -0
  131. package/hooks/pre-task-scope-disjoint.mjs +368 -35
  132. package/hooks/skill-invocation-telemetry.mjs +21 -10
  133. package/monitors/monitors.json +6 -0
  134. package/package.json +1 -1
  135. package/pi/prompts/journey-audit.md +12 -0
  136. package/rules/_index.md +9 -1
  137. package/rules/always-on/ask-via-tool.md +62 -0
  138. package/rules/always-on/bash-harness-pitfalls.md +168 -0
  139. package/rules/always-on/build-value.md +47 -0
  140. package/rules/always-on/cross-session-messaging.md +59 -0
  141. package/rules/always-on/loop-and-monitor.md +221 -0
  142. package/rules/always-on/parallel-sessions.md +142 -12
  143. package/rules/always-on/receiving-review.md +108 -0
  144. package/rules/always-on/test-value.md +40 -0
  145. package/rules/always-on/verification-before-completion.md +77 -0
  146. package/scripts/archive-closed-prds.mjs +258 -18
  147. package/scripts/autopilot.mjs +5 -0
  148. package/scripts/backfill-evidence-digest.mjs +376 -0
  149. package/scripts/cursor-install.mjs +89 -48
  150. package/scripts/export-hw-learnings.mjs +143 -2
  151. package/scripts/express-path.mjs +299 -0
  152. package/scripts/generate-cursor-adapter.mjs +253 -0
  153. package/scripts/github-protection-audit.mjs +358 -0
  154. package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
  155. package/scripts/lib/build-live-signals.mjs +24 -5
  156. package/scripts/lib/ci-status-banner.mjs +158 -11
  157. package/scripts/lib/command-blocker.mjs +70 -0
  158. package/scripts/lib/config/reconcile.mjs +79 -4
  159. package/scripts/lib/config/section-extractor.mjs +235 -36
  160. package/scripts/lib/config-schema.mjs +9 -1
  161. package/scripts/lib/config.mjs +57 -6
  162. package/scripts/lib/convergence-monitor.mjs +13 -2
  163. package/scripts/lib/cursor-hook-bridge.mjs +443 -0
  164. package/scripts/lib/dispatcher/cli.mjs +2 -2
  165. package/scripts/lib/express-path.mjs +327 -0
  166. package/scripts/lib/file-lock.mjs +22 -4
  167. package/scripts/lib/gates/gate-full.mjs +81 -8
  168. package/scripts/lib/gates/gate-helpers.mjs +76 -15
  169. package/scripts/lib/git-config-drift.mjs +134 -5
  170. package/scripts/lib/host-identity.mjs +247 -2
  171. package/scripts/lib/instruction-budget-guard.mjs +31 -1
  172. package/scripts/lib/issue-budget.mjs +229 -30
  173. package/scripts/lib/learnings/io.mjs +55 -10
  174. package/scripts/lib/learnings/schema.mjs +95 -28
  175. package/scripts/lib/lock-reaper.mjs +7 -1
  176. package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
  177. package/scripts/lib/locks/state-md-lock.mjs +8 -1
  178. package/scripts/lib/memory-banner.mjs +5 -2
  179. package/scripts/lib/memory-paths.mjs +15 -6
  180. package/scripts/lib/mode-selector/scoring.mjs +53 -6
  181. package/scripts/lib/platform.mjs +72 -9
  182. package/scripts/lib/plugin-root.mjs +143 -19
  183. package/scripts/lib/project-hygiene.mjs +43 -3
  184. package/scripts/lib/quality-gate.mjs +271 -13
  185. package/scripts/lib/reconcile/emitter.mjs +87 -19
  186. package/scripts/lib/reconcile/engine.mjs +281 -13
  187. package/scripts/lib/reconcile/idempotency.mjs +102 -1
  188. package/scripts/lib/reconcile/renderer.mjs +148 -3
  189. package/scripts/lib/reconcile/sanitize.mjs +40 -17
  190. package/scripts/lib/reconcile/writer.mjs +415 -84
  191. package/scripts/lib/rule-loader.mjs +37 -2
  192. package/scripts/lib/rules-sync.mjs +51 -8
  193. package/scripts/lib/scope-gate.mjs +90 -0
  194. package/scripts/lib/session-close-backfill.mjs +369 -28
  195. package/scripts/lib/session-discovery.mjs +13 -3
  196. package/scripts/lib/session-end/phase-skip.mjs +37 -4
  197. package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
  198. package/scripts/lib/session-id.mjs +30 -14
  199. package/scripts/lib/session-identity/own-session.mjs +159 -0
  200. package/scripts/lib/session-lock.mjs +85 -30
  201. package/scripts/lib/session-schema/normalizer.mjs +70 -3
  202. package/scripts/lib/session-schema/validator.mjs +40 -0
  203. package/scripts/lib/session-start-probes.mjs +608 -0
  204. package/scripts/lib/session-transition.mjs +277 -0
  205. package/scripts/lib/sessions-staleness-banner.mjs +124 -57
  206. package/scripts/lib/spiral-carryover.mjs +90 -9
  207. package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
  208. package/scripts/lib/state-md/mission-status.mjs +350 -52
  209. package/scripts/lib/state-md/yaml-parser.mjs +145 -16
  210. package/scripts/lib/state-md.mjs +12 -2
  211. package/scripts/lib/telemetry/sync.mjs +46 -8
  212. package/scripts/lib/validate/check-agents.mjs +66 -0
  213. package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
  214. package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
  215. package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
  216. package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
  217. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
  218. package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
  219. package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
  220. package/scripts/lib/validate/check-unwired-features.mjs +340 -32
  221. package/scripts/lib/validate/repo-files.mjs +275 -0
  222. package/scripts/lib/validate-vendored-rules.mjs +229 -7
  223. package/scripts/lib/vault-mirror/process.mjs +99 -43
  224. package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
  225. package/scripts/lib/vault-staleness-banner.mjs +76 -6
  226. package/scripts/lib/vault-status/board-writer.mjs +211 -10
  227. package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
  228. package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
  229. package/scripts/lib/wave-transcript-tail.mjs +869 -0
  230. package/scripts/materialize-wave-scope.mjs +209 -12
  231. package/scripts/mcp-server.sh +11 -2
  232. package/scripts/parse-config.mjs +65 -0
  233. package/scripts/token-audit.sh +9 -2
  234. package/scripts/validate-plugin.mjs +3 -0
  235. package/scripts/validate-wave-scope.mjs +67 -0
  236. package/scripts/vault-mirror.mjs +203 -34
  237. package/skills/_shared/monitor-patterns.md +31 -5
  238. package/skills/_shared/parallel-aware-auq.md +1 -1
  239. package/skills/_shared/parallel-aware-preamble.md +4 -2
  240. package/skills/_shared/platform-tools.md +11 -5
  241. package/skills/_shared/state-ownership.md +29 -2
  242. package/skills/autopilot/SKILL.md +5 -1
  243. package/skills/bootstrap/SKILL.md +3 -3
  244. package/skills/bootstrap/_shared-template.md +18 -10
  245. package/skills/bootstrap/deep-template.md +10 -6
  246. package/skills/bootstrap/fast-template.md +15 -8
  247. package/skills/bootstrap/standard-template.md +10 -6
  248. package/skills/claude-md-drift-check/checker.mjs +39 -11
  249. package/skills/dispatcher/SKILL.md +1 -1
  250. package/skills/journey-audit/SKILL.md +269 -0
  251. package/skills/peekaboo-driver/SKILL.md +15 -3
  252. package/skills/persona-panel/SKILL.md +1 -1
  253. package/skills/reconcile/SKILL.md +41 -1
  254. package/skills/session-end/SKILL.md +17 -4
  255. package/skills/session-end/metrics-collection.md +7 -4
  256. package/skills/session-end/phase-3-6-tail.md +11 -3
  257. package/skills/session-end/phase-3-7a-recommendations.md +16 -2
  258. package/skills/session-plan/SKILL.md +6 -1
  259. package/skills/session-plan/wave-template.md +1 -0
  260. package/skills/session-start/SKILL.md +30 -16
  261. package/skills/session-start/phase-7-5-mode-selector.md +15 -3
  262. package/skills/session-start/phase-8-5-express-path.md +77 -12
  263. package/skills/vault-sync/validator.mjs +31 -0
  264. package/skills/wave-executor/SKILL.md +4 -2
  265. package/skills/wave-executor/circuit-breaker.md +34 -9
  266. package/skills/wave-executor/wave-loop.md +102 -19
  267. package/templates/_shared/journey-manifest.md +110 -0
  268. package/templates/_shared/rules/parallel-sessions.md +0 -77
@@ -27,7 +27,9 @@
27
27
  * - Strip all absolute paths (macOS, Linux system paths, Windows)
28
28
  * - Redact IPv4 addresses
29
29
  * - Redact GitHub/GitLab URLs containing org/repo paths
30
- * - Replace hostname references with `host_class` label
30
+ * - Replace hostname references with `host_class` label — form-based for
31
+ * suffixed names, PLUS value-based for this host's own suffix-stripped
32
+ * `host_id` spellings (#1072 follow-up; see `redactLocalHostNames`)
31
33
  * - Redact email, git author, token patterns
32
34
  * - No free-form text from user — only structured fields
33
35
  * - Round ram/cpu to 1 GB / 10% buckets
@@ -45,6 +47,7 @@
45
47
 
46
48
  import { writeFile, mkdir } from 'node:fs/promises';
47
49
  import { readFileSync } from 'node:fs';
50
+ import os from 'node:os';
48
51
  import path from 'node:path';
49
52
  import {
50
53
  readLearnings,
@@ -58,6 +61,7 @@ import {
58
61
  import { findProjectRoot, resolveInstructionFile, expandTilde } from './lib/common.mjs';
59
62
  import { parseSessionConfig } from './lib/config.mjs';
60
63
  import { createSecretValueMasker } from './lib/secret-masker.mjs';
64
+ import { stableHostname, readHostAliases } from './lib/host-identity.mjs';
61
65
 
62
66
  // Vault-relative default write target (Epic #774 — docs Public-Split removed
63
67
  // the prior in-repo generated telemetry doc in favor of the private Meta-Vault).
@@ -138,6 +142,137 @@ const SIGNED_OFF_RE = /Signed-off-by:[^\n]+/g;
138
142
  // those never appear with a .local/.lan/.home suffix.
139
143
  const HOSTNAME_RE = /\b[A-Za-z0-9]+(?:[-.][A-Za-z0-9]+)*\.(?:local|lan|home|internal|corp)\b/g;
140
144
 
145
+ /** Marker for every host-identifying redaction — form-based AND value-based. */
146
+ const HOSTNAME_MARKER = '<redacted-hostname>';
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Value-based host-identity redaction (#1072 follow-up)
150
+ // ---------------------------------------------------------------------------
151
+ //
152
+ // HOSTNAME_RE above is FORM-based and fires only on a name that still carries a
153
+ // `.local` / `.lan` / `.home` / `.internal` / `.corp` suffix. The session-lock
154
+ // body's `host_id` field is exactly the suffix-STRIPPED lowercase form that
155
+ // `stableHostname()` produces, so a `host_id` value reaching a learning passes
156
+ // through un-redacted. Measured on the reference host 2026-08-24, before this
157
+ // block existed:
158
+ //
159
+ // anonymizeString('ferdinands-macbook-pro') → 'ferdinands-macbook-pro' (no match)
160
+ // anonymizeString('mac') → 'mac' (no match)
161
+ //
162
+ // Widening HOSTNAME_RE to bare labels is NOT the fix: once the suffix is gone a
163
+ // machine name is shape-identical to any other word, so a form rule broad
164
+ // enough to catch it would redact ordinary prose. The discriminating
165
+ // information is not the shape but the VALUE — and the values are known at
166
+ // runtime: `stableHostname(os.hostname())` plus every spelling this machine has
167
+ // recorded about ITSELF in the self-alias ledger (`readHostAliases()`, the
168
+ // #1072 lock-identity ledger). Exact strings, case-insensitive, word-boundary
169
+ // anchored: this host's identifiers precisely, which is the privacy contract.
170
+ //
171
+ // WHERE IT RUNS, AND WHY NOT BESIDE `maskSecretValues` — measured, because the
172
+ // obvious "all value-based rules go first" placement LEAKS. A secret is an
173
+ // opaque span nothing else owns; a hostname is routinely a SUBSTRING of a
174
+ // larger PII span that another rule owns. Two reachable cases, measured
175
+ // 2026-08-24 on the two orderings:
176
+ //
177
+ // 'ferdinand@Ferdinands-MacBook-Pro.local'
178
+ // value-first → 'ferdinand@<redacted-hostname>' ← email local-part leaks
179
+ // host-slot → '<redacted-email>'
180
+ // '/Users/ferdinands-macbook-pro/Projects/secret-client/app.js'
181
+ // value-first → '<redacted-path><redacted-hostname>/Projects/secret-client/app.js'
182
+ // host-slot → '<redacted-path>'
183
+ //
184
+ // The second is the sharper one: UNIX_PATH_RE's tail class is `[^\s"'<>]*`, so
185
+ // the ANGLE BRACKETS of an early marker truncate the path match and strand the
186
+ // rest of the path in the output. (`[REDACTED]`, the #1025 marker, is
187
+ // bracket-free — which is why that one is safe to run first.) Running LAST
188
+ // among the span rules can never lose a hostname in exchange: every rule here
189
+ // only ever replaces a span WITH the hostname inside it, so the name is gone
190
+ // either way.
191
+ //
192
+ // CEILING (BV-004): on a host whose stable name is an ordinary word — a Mac
193
+ // literally named `mac`, a Raspberry Pi named `pi` — every standalone
194
+ // occurrence of that word in an insight is redacted too. Accepted deliberately,
195
+ // and without a minimum-length floor: over-redaction in a PUBLIC export is the
196
+ // cheap direction, and a length floor would be a silent hole for exactly the
197
+ // short names that are most common. Revisit trigger: an exported document whose
198
+ // readability is measurably degraded by this collateral.
199
+
200
+ /** Optional trailing local-network suffix, so `mac.local` is consumed whole. */
201
+ const LOCAL_SUFFIX_GROUP = '(?:\\.(?:local|lan|home|localdomain|internal|corp))?';
202
+
203
+ /**
204
+ * Escape regex metacharacters so a hostname is matched as a literal.
205
+ * @param {string} s
206
+ * @returns {string}
207
+ */
208
+ function escapeRegExp(s) {
209
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
210
+ }
211
+
212
+ /**
213
+ * Compile one alternation over the given names, or `null` when nothing is left
214
+ * to match. Longest name first, because regex alternation is leftmost-first:
215
+ * with `mac` ahead of `mac-pro`, the shorter name would win and strand `-pro`.
216
+ *
217
+ * @param {string[]} names — raw or normalised hostnames; normalised here.
218
+ * @returns {RegExp|null}
219
+ */
220
+ function compileHostNameRe(names) {
221
+ const uniq = [
222
+ ...new Set(names.map((n) => stableHostname(typeof n === 'string' ? n : '')).filter(Boolean)),
223
+ ].sort((a, b) => b.length - a.length || a.localeCompare(b));
224
+ if (uniq.length === 0) return null;
225
+ return new RegExp(`\\b(?:${uniq.map(escapeRegExp).join('|')})${LOCAL_SUFFIX_GROUP}\\b`, 'gi');
226
+ }
227
+
228
+ /**
229
+ * Lazily-built, process-wide alternation over THIS host's own names — the same
230
+ * once-per-process shape as `_secretMasker` below, for the same reason:
231
+ * `readHostAliases()` is a synchronous file read, and rebuilding per record
232
+ * would put one read on EVERY insight and EVERY evidence string.
233
+ *
234
+ * Tests either pass `names` to {@link redactLocalHostNames} directly, or
235
+ * re-import the module after `vi.resetModules()` to rebuild it against a
236
+ * stubbed `SO_HOST_ALIASES_FILE` (the shape the #1025 block in
237
+ * `tests/scripts/export-hw-learnings.test.mjs` already uses).
238
+ *
239
+ * @type {RegExp|null|undefined} `undefined` = not built yet; `null` = nothing to redact.
240
+ */
241
+ let _localHostRe;
242
+
243
+ /**
244
+ * Redact THIS machine's own hostnames — any recorded spelling, with or without
245
+ * a local-network suffix — from a free-form string.
246
+ *
247
+ * Fail-soft: a non-string passes through by reference, and an unresolvable host
248
+ * identity makes this the identity function. Redaction must never be the reason
249
+ * an export dies.
250
+ *
251
+ * @param {string} s
252
+ * @param {string[]} [names] — override the name set (tests). Omitted → this
253
+ * host's `os.hostname()` plus the self-alias ledger, resolved once.
254
+ * @returns {string}
255
+ */
256
+ export function redactLocalHostNames(s, names) {
257
+ if (typeof s !== 'string' || s === '') return s;
258
+
259
+ let re;
260
+ if (Array.isArray(names)) {
261
+ re = compileHostNameRe(names);
262
+ } else {
263
+ if (_localHostRe === undefined) {
264
+ try {
265
+ _localHostRe = compileHostNameRe([stableHostname(os.hostname()), ...readHostAliases()]);
266
+ } catch {
267
+ _localHostRe = null;
268
+ }
269
+ }
270
+ re = _localHostRe;
271
+ }
272
+
273
+ return re ? s.replace(re, HOSTNAME_MARKER) : s;
274
+ }
275
+
141
276
  // ---------------------------------------------------------------------------
142
277
  // Value-based secret masking (#1025) — the SECOND half of the pipeline
143
278
  // ---------------------------------------------------------------------------
@@ -207,7 +342,13 @@ export function anonymizeString(s) {
207
342
  out = out.replace(VCS_URL_RE, '<VCS-URL>');
208
343
  for (const re of ABS_PATH_RES) out = out.replace(re, '<redacted-path>');
209
344
  out = out.replace(IPV4_RE, '<IP>');
210
- out = out.replace(HOSTNAME_RE, '<redacted-hostname>');
345
+ // Both host rules occupy the SAME slot — after every rule that owns a span a
346
+ // hostname can sit inside (email domain, home directory, VCS URL), before
347
+ // TOKEN_RE. The value rule runs first of the two so it consumes name+suffix
348
+ // whole; see the "WHERE IT RUNS" measurement above for why neither may move
349
+ // to the front of the chain.
350
+ out = redactLocalHostNames(out);
351
+ out = out.replace(HOSTNAME_RE, HOSTNAME_MARKER);
211
352
  out = out.replace(TOKEN_RE, '<redacted-token>');
212
353
  return out;
213
354
  }
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * express-path.mjs — the mechanical caller for the Express Path decision (#214, #1119, #1146).
4
+ *
5
+ * ## Why a CLI, and why this is the only shape that works
6
+ *
7
+ * `scripts/lib/express-path.mjs` has held the decision AND its telemetry since
8
+ * #1119. Measured at HEAD 01eb35d, it had ZERO production callers:
9
+ *
10
+ * rg -n "evaluateExpressPath" scripts hooks → the definition, and nothing else
11
+ *
12
+ * Its only "caller" was a fenced ```js block in
13
+ * `skills/session-start/phase-8-5-express-path.md` — prose that no process runs.
14
+ * That is the repo's standing disease (built, not wired): a decision module whose
15
+ * whole point is to record whether the fast path fired, reachable only if a
16
+ * coordinator reads the right paragraph and hand-writes an import.
17
+ *
18
+ * A hook cannot close the gap. Both `sessionType` and `taskCount` exist only
19
+ * AFTER the Phase 8 Q&A resolves, and no hook event fires at that moment. The
20
+ * caller therefore has to be something the coordinator RUNS, in Phase 8.5, with
21
+ * the answers it just received — i.e. this file.
22
+ *
23
+ * ## Usage
24
+ *
25
+ * node scripts/express-path.mjs --repo-root <path> --session-type <type> \
26
+ * --task-count <n> [--parallel-agents true|false] [--config-file <path>]
27
+ *
28
+ * Options:
29
+ * --repo-root <path> REQUIRED. Repo whose `.orchestrator/metrics/events.jsonl`
30
+ * receives the record. Never defaulted: see below.
31
+ * --session-type <type> REQUIRED. The type confirmed in Phase 8 (housekeeping|feature|deep).
32
+ * --task-count <n> REQUIRED. Agreed issue/task scope, a non-negative integer.
33
+ * --parallel-agents <bool> Optional `true`/`false`. Omitting it ASSERTS NOTHING —
34
+ * the field is left out of the record entirely.
35
+ * --config-file <path> Optional. Defaults to CLAUDE.md / AGENTS.md under
36
+ * --repo-root (SO_CONFIG_FILE honoured first, as in
37
+ * parse-config.mjs).
38
+ * --help, -h Print this usage block.
39
+ *
40
+ * Output: ONE JSON line on stdout — the `evaluateExpressPath` verdict verbatim,
41
+ * `{"activated":<bool>,"reasons":[…]}`. Always JSON, so there is no `--json`
42
+ * flag, matching its two siblings `parse-config.mjs` and `emit-session.mjs`.
43
+ * Diagnostics — including the activation banner — go to stderr.
44
+ *
45
+ * Exit codes:
46
+ * 0 — the evaluation COMPLETED. `activated:false` is a completed evaluation and
47
+ * exits 0: a refusal is an answer, not a failure. Callers branch on the
48
+ * stdout `activated` field, never on the exit code.
49
+ * 1 — user/input error: missing or invalid --repo-root / --session-type /
50
+ * --task-count / --parallel-agents, or an unknown flag.
51
+ * 2 — config I/O error: an unreadable or unparseable config file.
52
+ *
53
+ * ## Two deliberate asymmetries
54
+ *
55
+ * `--repo-root` is REQUIRED and is never filled from `SO_PROJECT_DIR`. The
56
+ * library refuses that fallback for a measured reason (#941: an ambient
57
+ * destination put a synthetic record into the operator's real fleet ledger), and
58
+ * a CLI that quietly supplied one would reinstate exactly what the library
59
+ * refuses. A missing repo-root is an input error here, not a default.
60
+ *
61
+ * A config file that EXISTS but cannot be read or parsed exits 2 — it may carry
62
+ * `express-path.enabled: false`, and guessing past an unreadable opt-out would
63
+ * activate a path that skips every inter-wave quality gate. A config file that
64
+ * is simply ABSENT is not an error: there is no opt-out to lose, the documented
65
+ * default (`enabled: true`) applies to the decision, and `enabled` is omitted
66
+ * from the record so "nobody looked" stays distinguishable from "operator said
67
+ * true".
68
+ */
69
+
70
+ import { existsSync, readFileSync, statSync } from 'node:fs';
71
+ import { join, resolve } from 'node:path';
72
+ import { parseArgs } from 'node:util';
73
+ import { resolveInstructionFile } from './lib/common.mjs';
74
+ import { parseSessionConfig } from './lib/config.mjs';
75
+ import { evaluateExpressPath } from './lib/express-path.mjs';
76
+
77
+ const USAGE = [
78
+ 'Usage: node scripts/express-path.mjs --repo-root <path> --session-type <type> --task-count <n>',
79
+ ' [--parallel-agents true|false] [--config-file <path>]',
80
+ '',
81
+ ' --repo-root REQUIRED repo receiving the .orchestrator/metrics/events.jsonl record',
82
+ ' --session-type REQUIRED session type confirmed in session-start Phase 8',
83
+ ' --task-count REQUIRED agreed issue/task scope (non-negative integer)',
84
+ ' --parallel-agents optional true|false; omitted means "not asserted", never false',
85
+ ' --config-file optional defaults to CLAUDE.md / AGENTS.md under --repo-root',
86
+ '',
87
+ 'stdout: one JSON line — {"activated":<bool>,"reasons":[…]}',
88
+ 'Exit codes: 0 evaluation completed (activated true OR false), 1 input error, 2 config I/O error',
89
+ ].join('\n');
90
+
91
+ /** User/input error — usage class. */
92
+ const EXIT_INPUT = 1;
93
+ /** System error — config could not be read or parsed. */
94
+ const EXIT_CONFIG_IO = 2;
95
+
96
+ /**
97
+ * Write a diagnostic line to stderr. stdout carries the verdict and nothing
98
+ * else, so every human-facing line goes here (`cli-design.md` § JSON-First).
99
+ *
100
+ * @param {string} message
101
+ * @returns {void}
102
+ */
103
+ function warn(message) {
104
+ process.stderr.write(`express-path: ${message}\n`);
105
+ }
106
+
107
+ /**
108
+ * Fail with a usage-class diagnostic and the given exit code.
109
+ *
110
+ * @param {string} message
111
+ * @param {number} code
112
+ * @returns {never}
113
+ */
114
+ function fail(message, code) {
115
+ warn(message);
116
+ process.exit(code);
117
+ }
118
+
119
+ /**
120
+ * Resolve the config file to read, anchored at `--repo-root`.
121
+ *
122
+ * The CLAUDE.md → AGENTS.md half is NOT re-implemented here: that walk is
123
+ * `resolveInstructionFile()` in `scripts/lib/common.mjs`, the SSOT for the
124
+ * instruction-file alias rule (`skills/_shared/instruction-file-resolution.md`)
125
+ * that 8 other modules already share. Reusing it also inherits its size > 0
126
+ * guard, which this function lacked: a zero-byte CLAUDE.md used to be accepted
127
+ * and parsed, publishing `enabled` from an empty document as if it had been
128
+ * measured. Only the candidate the shared walk does not know about is layered
129
+ * on top — the `SO_CONFIG_FILE` override `scripts/parse-config.mjs` honours,
130
+ * plus an explicit `--config-file`, which wins outright.
131
+ *
132
+ * Anchoring at `repoRoot` instead of walking up from cwd is deliberate: the
133
+ * repo is already known here, and a walk-up could reach a DIFFERENT repo's
134
+ * config than the one being recorded.
135
+ *
136
+ * Named `resolveRepoConfigPath`, NOT `resolveConfigFile`: that name is already
137
+ * taken twice with two other meanings — `scripts/lib/platform.mjs` maps a
138
+ * PLATFORM to a bare filename, `scripts/lib/ecosystem-wizard/config-writer.mjs`
139
+ * resolves the wizard's own target. A third meaning under one name is a trap
140
+ * for the next person who greps it.
141
+ *
142
+ * @param {string} repoRoot — absolute repo root
143
+ * @param {string|undefined} explicit — value of `--config-file`, if given
144
+ * @returns {string|null} absolute path, or null when no config file exists
145
+ */
146
+ function resolveRepoConfigPath(repoRoot, explicit) {
147
+ if (explicit) {
148
+ const abs = resolve(explicit);
149
+ if (!existsSync(abs)) fail(`config file not found: ${explicit}`, EXIT_CONFIG_IO);
150
+ return abs;
151
+ }
152
+
153
+ if (process.env.SO_CONFIG_FILE) {
154
+ const override = join(repoRoot, process.env.SO_CONFIG_FILE);
155
+ if (existsSync(override)) return override;
156
+ }
157
+
158
+ return resolveInstructionFile(repoRoot)?.path ?? null;
159
+ }
160
+
161
+ /**
162
+ * Read and parse the Session Config.
163
+ *
164
+ * Returns `undefined` — not `{}` — when no config file exists, so
165
+ * `evaluateExpressPath` can tell "not measured" from "measured as absent" and
166
+ * keep `enabled` out of the record. See the module docstring's second asymmetry.
167
+ *
168
+ * @param {string|null} configFile
169
+ * @returns {object|undefined}
170
+ */
171
+ function loadConfig(configFile) {
172
+ if (configFile === null) {
173
+ warn('no CLAUDE.md / AGENTS.md under --repo-root; applying the documented default enabled=true');
174
+ return undefined;
175
+ }
176
+
177
+ let content;
178
+ try {
179
+ content = readFileSync(configFile, 'utf8');
180
+ } catch (err) {
181
+ fail(`failed to read ${configFile}: ${err.message}`, EXIT_CONFIG_IO);
182
+ }
183
+
184
+ try {
185
+ return parseSessionConfig(content);
186
+ } catch (err) {
187
+ fail(`failed to parse ${configFile}: ${err.message}`, EXIT_CONFIG_IO);
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Parse `--parallel-agents`. Tri-state on purpose: `undefined` means the
193
+ * coordinator asserted nothing, which the library records differently from an
194
+ * explicit `false`.
195
+ *
196
+ * @param {string|undefined} raw
197
+ * @returns {boolean|undefined}
198
+ */
199
+ function parseParallelAgents(raw) {
200
+ if (raw === undefined) return undefined;
201
+ const value = String(raw).trim().toLowerCase();
202
+ if (value === 'true') return true;
203
+ if (value === 'false') return false;
204
+ return fail(`--parallel-agents must be "true" or "false" (got: ${raw})`, EXIT_INPUT);
205
+ }
206
+
207
+ async function main() {
208
+ /** @type {{values: Record<string, string|boolean>}} */
209
+ let parsed;
210
+ try {
211
+ parsed = parseArgs({
212
+ args: process.argv.slice(2),
213
+ options: {
214
+ 'repo-root': { type: 'string' },
215
+ 'session-type': { type: 'string' },
216
+ 'task-count': { type: 'string' },
217
+ 'parallel-agents': { type: 'string' },
218
+ 'config-file': { type: 'string' },
219
+ help: { type: 'boolean', short: 'h' },
220
+ },
221
+ allowPositionals: false,
222
+ });
223
+ } catch (err) {
224
+ fail(`${err.message}\n\n${USAGE}`, EXIT_INPUT);
225
+ }
226
+
227
+ const { values } = parsed;
228
+
229
+ if (values.help) {
230
+ process.stdout.write(`${USAGE}\n`);
231
+ process.exit(0);
232
+ }
233
+
234
+ // --repo-root — required, never defaulted from the ambient env (#941).
235
+ const repoRootArg = typeof values['repo-root'] === 'string' ? values['repo-root'].trim() : '';
236
+ if (repoRootArg === '') {
237
+ fail(`--repo-root is required (never defaulted from SO_PROJECT_DIR)\n\n${USAGE}`, EXIT_INPUT);
238
+ }
239
+ const repoRoot = resolve(repoRootArg);
240
+ if (!existsSync(repoRoot) || !statSync(repoRoot).isDirectory()) {
241
+ // A typo'd root would otherwise be CREATED by the emitter's mkdir, leaving
242
+ // an orphan `.orchestrator/metrics/` tree that answers no question.
243
+ fail(`--repo-root is not an existing directory: ${repoRootArg}`, EXIT_INPUT);
244
+ }
245
+
246
+ // --session-type — required. The library fails CLOSED on an unknown type; the
247
+ // CLI refuses it outright so the miss is reported rather than recorded.
248
+ const sessionType =
249
+ typeof values['session-type'] === 'string' ? values['session-type'].trim() : '';
250
+ if (sessionType === '') {
251
+ fail(`--session-type is required\n\n${USAGE}`, EXIT_INPUT);
252
+ }
253
+
254
+ // --task-count — required, non-negative integer.
255
+ const taskCountRaw = typeof values['task-count'] === 'string' ? values['task-count'].trim() : '';
256
+ if (taskCountRaw === '') {
257
+ fail(`--task-count is required\n\n${USAGE}`, EXIT_INPUT);
258
+ }
259
+ const taskCount = Number(taskCountRaw);
260
+ if (!Number.isInteger(taskCount) || taskCount < 0) {
261
+ fail(`--task-count must be a non-negative integer (got: ${taskCountRaw})`, EXIT_INPUT);
262
+ }
263
+
264
+ const parallelAgentsRequired = parseParallelAgents(
265
+ typeof values['parallel-agents'] === 'string' ? values['parallel-agents'] : undefined,
266
+ );
267
+
268
+ const configFile = resolveRepoConfigPath(
269
+ repoRoot,
270
+ typeof values['config-file'] === 'string' ? values['config-file'] : undefined,
271
+ );
272
+ const config = loadConfig(configFile);
273
+
274
+ const verdict = await evaluateExpressPath({
275
+ repoRoot,
276
+ config,
277
+ sessionType,
278
+ taskCount,
279
+ parallelAgentsRequired,
280
+ });
281
+
282
+ // The banner the coordinator echoes into the transcript — `/go` and
283
+ // session-plan's Express Path Short-Circuit both key off this EXACT line
284
+ // (`commands/go.md` § Express Path Detection), so it is written verbatim,
285
+ // without the `express-path:` diagnostic prefix. Still stderr: stdout belongs
286
+ // to the verdict.
287
+ if (verdict.activated) {
288
+ process.stderr.write(
289
+ `Express path activated — ${taskCount} tasks, coordinator-direct, no inter-wave checks.\n`,
290
+ );
291
+ }
292
+
293
+ process.stdout.write(`${JSON.stringify(verdict)}\n`);
294
+ }
295
+
296
+ main().catch((err) => {
297
+ process.stderr.write(`express-path: unexpected error: ${err?.stack ?? err}\n`);
298
+ process.exit(EXIT_CONFIG_IO);
299
+ });