session-orchestrator 3.20.0 → 3.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/000-session-orchestrator.mdc +3 -2
  5. package/.cursor/rules/030-wave-execution.mdc +10 -8
  6. package/.cursor/rules/040-discovery.mdc +6 -6
  7. package/.cursor/rules/050-plan.mdc +8 -8
  8. package/CHANGELOG.md +515 -0
  9. package/README.md +16 -11
  10. package/agents/analyst.md +1 -1
  11. package/agents/architect-reviewer.md +1 -1
  12. package/agents/code-implementer.md +4 -2
  13. package/agents/db-specialist.md +1 -1
  14. package/agents/dialectic-deriver.md +1 -1
  15. package/agents/docs-writer.md +1 -1
  16. package/agents/memory-proposal-collector.md +7 -5
  17. package/agents/qa-strategist.md +1 -1
  18. package/agents/security-reviewer.md +1 -1
  19. package/agents/session-reviewer.md +42 -1
  20. package/agents/skill-applied-judge.md +1 -1
  21. package/agents/test-writer.md +1 -1
  22. package/agents/ui-developer.md +1 -1
  23. package/agents/ux-evaluator.md +1 -1
  24. package/commands/eli5.md +33 -0
  25. package/commands/release.md +62 -0
  26. package/commands/test.md +2 -2
  27. package/docs/components.md +6 -5
  28. package/docs/migration-v3.md +9 -6
  29. package/docs/persona-panel.md +3 -1
  30. package/docs/scope-collision-guard.md +167 -0
  31. package/docs/session-config-reference.md +31 -8
  32. package/hooks/_lib/lock-bootstrap.mjs +19 -13
  33. package/hooks/enforce-scope.mjs +103 -3
  34. package/hooks/hooks-codex.json +1 -1
  35. package/hooks/hooks.json +21 -1
  36. package/hooks/on-session-end.mjs +76 -97
  37. package/hooks/on-session-start.mjs +195 -104
  38. package/hooks/on-stop.mjs +127 -12
  39. package/hooks/post-bash-write-verify.mjs +8 -32
  40. package/hooks/pre-auq-clarity.mjs +787 -0
  41. package/hooks/pre-bash-issue-budget.mjs +17 -18
  42. package/hooks/pre-task-scope-disjoint.mjs +1042 -0
  43. package/package.json +3 -1
  44. package/pi/prompts/eli5.md +12 -0
  45. package/pi/prompts/release.md +12 -0
  46. package/scripts/auq-audit.mjs +825 -0
  47. package/scripts/autopilot.mjs +10 -9
  48. package/scripts/emit-session.mjs +42 -0
  49. package/scripts/export-hw-learnings.mjs +61 -2
  50. package/scripts/lib/auq/clarity.mjs +1314 -0
  51. package/scripts/lib/auq/parse.mjs +1006 -0
  52. package/scripts/lib/auq/schema.mjs +1457 -0
  53. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  54. package/scripts/lib/backlog-scan.mjs +106 -15
  55. package/scripts/lib/build-live-signals.mjs +7 -3
  56. package/scripts/lib/ci-status-banner.mjs +267 -77
  57. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
  58. package/scripts/lib/config/vault-integration.mjs +12 -1
  59. package/scripts/lib/dispatcher/rank.mjs +4 -7
  60. package/scripts/lib/gates/gate-full.mjs +3 -3
  61. package/scripts/lib/gates/gate-helpers.mjs +17 -6
  62. package/scripts/lib/git-config-drift.mjs +471 -0
  63. package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
  64. package/scripts/lib/io.mjs +432 -7
  65. package/scripts/lib/issue-budget.mjs +63 -9
  66. package/scripts/lib/learnings/select.mjs +157 -3
  67. package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
  68. package/scripts/lib/mirror-issues-banner.mjs +266 -0
  69. package/scripts/lib/named-vault-resolver.mjs +105 -16
  70. package/scripts/lib/owner-interview.mjs +78 -32
  71. package/scripts/lib/peer-cards/schema.mjs +6 -2
  72. package/scripts/lib/peer-discovery.mjs +73 -22
  73. package/scripts/lib/project-hygiene.mjs +64 -4
  74. package/scripts/lib/reconcile/renderer.mjs +17 -4
  75. package/scripts/lib/reconcile/writer.mjs +69 -30
  76. package/scripts/lib/redact-spans.mjs +89 -0
  77. package/scripts/lib/resource-probe/evaluate.mjs +330 -149
  78. package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
  79. package/scripts/lib/resource-probe.mjs +18 -2
  80. package/scripts/lib/scope-baseline.mjs +77 -17
  81. package/scripts/lib/scope-gate.mjs +658 -0
  82. package/scripts/lib/secret-masker.mjs +262 -0
  83. package/scripts/lib/session-lock.mjs +34 -10
  84. package/scripts/lib/session-registry.mjs +9 -1
  85. package/scripts/lib/spiral-carryover.mjs +23 -2
  86. package/scripts/lib/state-md/mission-status.mjs +164 -58
  87. package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
  88. package/scripts/lib/validate/check-agents.mjs +77 -5
  89. package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
  90. package/scripts/lib/validate/check-commands.mjs +2 -20
  91. package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
  92. package/scripts/lib/validate/check-hooks-symmetry.mjs +48 -0
  93. package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
  94. package/scripts/lib/validate/check-rules.mjs +153 -9
  95. package/scripts/lib/validate/check-skills.mjs +191 -0
  96. package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
  97. package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
  98. package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
  99. package/scripts/lib/validate/check-unwired-features.mjs +219 -11
  100. package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
  101. package/scripts/lib/validate/frontmatter-block.mjs +61 -0
  102. package/scripts/lib/validate/tier-inference.mjs +46 -8
  103. package/scripts/lib/vault-backfill/glab.mjs +91 -58
  104. package/scripts/lib/vault-backfill/manifest.mjs +28 -8
  105. package/scripts/lib/vault-mirror/namespace.mjs +146 -1
  106. package/scripts/lib/vault-mirror/process.mjs +264 -31
  107. package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
  108. package/scripts/lib/vault-status/board-writer.mjs +300 -56
  109. package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
  110. package/scripts/lib/vcs-repo-spec.mjs +680 -30
  111. package/scripts/lib/wave-resource-gate.mjs +67 -73
  112. package/scripts/materialize-wave-scope.mjs +281 -0
  113. package/scripts/print-learnings-index.mjs +30 -3
  114. package/scripts/release.mjs +983 -107
  115. package/scripts/run-quality-gate.mjs +14 -0
  116. package/scripts/site-numbers.mjs +1049 -0
  117. package/scripts/validate-plugin.mjs +64 -0
  118. package/scripts/validate-wave-scope.mjs +286 -12
  119. package/scripts/vault-backfill.mjs +32 -5
  120. package/scripts/vault-mirror.mjs +26 -1
  121. package/skills/_shared/monitor-patterns.md +24 -4
  122. package/skills/_shared/parallel-aware-auq.md +30 -24
  123. package/skills/_shared/parallel-aware-preamble.md +31 -2
  124. package/skills/_shared/state-ownership.md +49 -6
  125. package/skills/bootstrap/SKILL.md +2 -1
  126. package/skills/brainstorm/SKILL.md +18 -18
  127. package/skills/brainstorm/soul.md +12 -0
  128. package/skills/claude-md-drift-check/SKILL.md +9 -1
  129. package/skills/debug/SKILL.md +4 -1
  130. package/skills/discovery/SKILL.md +28 -24
  131. package/skills/discovery/issue-templates.md +4 -4
  132. package/skills/discovery/probes-code.md +2 -2
  133. package/skills/discovery/probes-feature.md +6 -6
  134. package/skills/discovery/probes-infra.md +2 -2
  135. package/skills/discovery/probes-session.md +5 -5
  136. package/skills/dispatcher/SKILL.md +10 -1
  137. package/skills/eli5/SKILL.md +43 -0
  138. package/skills/evolve/SKILL.md +8 -9
  139. package/skills/frontmatter-guard/SKILL.md +9 -1
  140. package/skills/gitlab-ops/SKILL.md +73 -59
  141. package/skills/gitlab-portfolio/SKILL.md +10 -1
  142. package/skills/grill/SKILL.md +6 -6
  143. package/skills/grill/soul.md +16 -0
  144. package/skills/memory-cleanup/SKILL.md +20 -7
  145. package/skills/npm-publish/SKILL.md +23 -51
  146. package/skills/peekaboo-driver/SKILL.md +3 -3
  147. package/skills/persona-panel/SKILL.md +3 -1
  148. package/skills/plan/SKILL.md +18 -16
  149. package/skills/plan/mode-feature.md +1 -1
  150. package/skills/plan/mode-new.md +42 -12
  151. package/skills/plan/soul.md +12 -0
  152. package/skills/reconcile/SKILL.md +3 -3
  153. package/skills/repo-audit/SKILL.md +10 -1
  154. package/skills/session-end/SKILL.md +97 -22
  155. package/skills/session-end/metrics-collection.md +1 -1
  156. package/skills/session-end/phase-3-6-tail.md +37 -2
  157. package/skills/session-end/session-metrics-write.md +4 -10
  158. package/skills/session-plan/SKILL.md +2 -2
  159. package/skills/session-plan/wave-template.md +1 -1
  160. package/skills/session-start/SKILL.md +82 -36
  161. package/skills/session-start/phase-2-5-docs-planning.md +8 -8
  162. package/skills/session-start/phase-4-5-resource-health.md +82 -19
  163. package/skills/session-start/soul.md +110 -0
  164. package/skills/spinout/SKILL.md +5 -1
  165. package/skills/sunset-review/SKILL.md +11 -1
  166. package/skills/test-runner/SKILL.md +2 -2
  167. package/skills/tmux-layout/SKILL.md +7 -2
  168. package/skills/using-orchestrator/SKILL.md +1 -1
  169. package/skills/vault-mirror/SKILL.md +10 -1
  170. package/skills/vault-sync/SKILL.md +10 -1
  171. package/skills/vault-sync/validator.mjs +55 -6
  172. package/skills/wave-executor/wave-loop.md +64 -12
  173. package/skills/write-executable-plan/SKILL.md +6 -6
  174. package/scripts/lib/mission-status-schema.mjs +0 -114
  175. package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
  176. package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
  177. package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
  178. package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
  179. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
  180. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
  181. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
  182. package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
  183. package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
  184. package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
  185. package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
  186. package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
  187. package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
  188. package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
  189. package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
  190. package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
  191. package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
  192. package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
  193. package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
  194. package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
  195. package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
  196. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
  197. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
  198. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
  199. package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
  200. package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
  201. package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
  202. package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
@@ -37,8 +37,10 @@ import {
37
37
  yamlQuoteIfNeeded,
38
38
  subjectToSlug,
39
39
  } from '../vault-mirror/utils.mjs';
40
+ import { matchesModuloRedaction } from '../vault-mirror/process.mjs';
40
41
  import { readConfigFile, parseSessionConfig } from '../config.mjs';
41
42
  import { validatePathInsideProject } from '../path-utils.mjs';
43
+ import { createSecretValueMasker } from '../secret-masker.mjs';
42
44
 
43
45
  /** Frontmatter sentinel that identifies generator-owned narrative files. */
44
46
  export const GENERATOR_MARKER = 'session-orchestrator-vault-status-narrative@1';
@@ -178,6 +180,80 @@ export function extractNarrative(stateMdContents) {
178
180
  };
179
181
  }
180
182
 
183
+ // ── Secret masking (#974 / #1025) ───────────────────────────────────────────────
184
+
185
+ /**
186
+ * Mask every env-derived secret VALUE inside an extracted narrative, BEFORE any
187
+ * of it is rendered into the vault file.
188
+ *
189
+ * WHY THIS FILE NEEDS IT AT ALL. `mirrorNarrative` writes into
190
+ * `<vault>/01-projects/<slug>/_session-narrative.md`, and those files are TRACKED
191
+ * AND PUSHED in the operator's vault repo. Its content is STATE.md free text —
192
+ * Deviations, Wave History, mission-status task descriptions — i.e. agent-authored
193
+ * prose that routinely quotes command lines and error output. That is exactly the
194
+ * class a shape-regex cannot catch: the VALUE sits in the prose with no
195
+ * `FOO_TOKEN=` key beside it. A leak here is not fixable by deleting a line — it
196
+ * needs a history rewrite in a foreign repo, which neither this repo's
197
+ * `.gitleaks.toml` nor `check-owner-leakage.mjs` can reach (the vault has no
198
+ * `.husky/` and no active git hooks).
199
+ *
200
+ * WHY THE CALL SITE IS AFTER `extractNarrative`, NOT BEFORE IT. Masking the raw
201
+ * STATE.md string first would let `[REDACTED]` land inside the source
202
+ * FRONTMATTER, where `[…]` reads as a YAML flow sequence. `extractNarrative`
203
+ * parses that frontmatter to obtain `missionStatus`; on a parse error it falls
204
+ * back to `frontmatter = null`, so `missionStatus` becomes null and the Mission
205
+ * Status table SILENTLY disappears from the mirrored file. Masking the parsed
206
+ * narrative keeps the parse on clean input.
207
+ *
208
+ * WHICH OF THE FOUR `vault-mirror/process.mjs` REASONS CARRY HERE — measured, not
209
+ * assumed. Not (1) filename: the basename is the constant `_session-narrative.md`.
210
+ * Not (2) stdout: `mirrorNarrative` prints nothing. Not (3) YAML validity: every
211
+ * output frontmatter field derives from the slug, the date and the repo name —
212
+ * none reads from `narrative`. Only (4) IDEMPOTENCY carries, and it is decisive:
213
+ * `writeNarrative` compares byte-for-byte modulo `updated:`, so masking after
214
+ * that comparison would make every affected file re-render (and re-commit)
215
+ * forever.
216
+ *
217
+ * WHY A GENERIC WALK RATHER THAN FIELD-BY-FIELD. Three of the four narrative keys
218
+ * are strings, but `missionStatus` is an array of objects whose `id`/`task`/
219
+ * `wave`/`status` fields are rendered into the body by `renderMissionTable`. A
220
+ * field-by-field masker would publish a secret sitting in a `task:` description.
221
+ * The walk also stays correct when a future key is added to the narrative shape.
222
+ *
223
+ * NAMED CEILING (build-value BV-004): the masker is built once PER CALL rather
224
+ * than cached in a module-level singleton the way `vault-mirror/process.mjs` does.
225
+ * `mirrorNarrative` runs once per repo per session-end, not in a loop, so the
226
+ * O(env) construction is irrelevant — and a cached masker would silently go stale
227
+ * against a mutated `process.env`, making the result depend on which consumer
228
+ * happened to construct it first in the process. REVISIT TRIGGER: if this module
229
+ * ever gains a per-record or per-wave loop, hoist the masker to the loop head
230
+ * (still not to module scope).
231
+ *
232
+ * Fail-soft by construction: with zero needles the narrative is returned BY
233
+ * REFERENCE (byte-identical downstream), and `mask` passes non-strings through —
234
+ * masking must never be the reason a mirror run dies.
235
+ *
236
+ * @param {{ waveHistory: string, deviations: string, whatNotToRetry: string, missionStatus: object[]|null }} narrative
237
+ * @returns {{ waveHistory: string, deviations: string, whatNotToRetry: string, missionStatus: object[]|null }}
238
+ */
239
+ function maskNarrative(narrative) {
240
+ const { mask, needleCount } = createSecretValueMasker(process.env);
241
+ if (needleCount === 0) return narrative;
242
+
243
+ const walk = (value) => {
244
+ if (typeof value === 'string') return mask(value);
245
+ if (Array.isArray(value)) return value.map(walk);
246
+ if (value && typeof value === 'object') {
247
+ const out = {};
248
+ for (const [k, v] of Object.entries(value)) out[k] = walk(v);
249
+ return out;
250
+ }
251
+ return value;
252
+ };
253
+
254
+ return walk(narrative);
255
+ }
256
+
181
257
  // ── Pure render ────────────────────────────────────────────────────────────────
182
258
 
183
259
  /**
@@ -328,10 +404,28 @@ function normalizeUpdated(content) {
328
404
  * a generator. Return { action: 'skipped-handwritten' }.
329
405
  * 2. skip-handwritten: existing file with no `_generator` or a FOREIGN `_generator`
330
406
  * marker is hand-authored / owned by another generator → never overwrite.
331
- * 3. skip-noop: rendered content (modulo the `updated:` timestamp) byte-identical
332
- * to the existing content → no write.
407
+ * 3. skip-noop: rendered content (modulo the `updated:` timestamp AND modulo any
408
+ * `[REDACTED]` span already on disk) matches the existing content → no write.
333
409
  * 4. otherwise write (mkdir -p the parent dir first).
334
410
  *
411
+ * WHY STEP 3 IS NOT A PLAIN BYTE COMPARE (#1025). Masking is env-derived and the
412
+ * env is not part of STATE.md, so the two sides of this comparison can be masked
413
+ * DIFFERENTLY: a file written while `FOO_TOKEN` was set carries `[REDACTED]`, and
414
+ * a later run with that var absent renders the RAW value. A byte compare then
415
+ * reports "changed" and this function PUBLISHES THE RAW SECRET into a file that is
416
+ * tracked and pushed in the operator's vault repo — where the vault has no
417
+ * `.husky/` and no active git hooks, so nothing catches it and only a history
418
+ * rewrite in a foreign repo removes it. Reproduced without touching the source
419
+ * between runs: run 1 (env set) → `skipped-noop`, run 2 (env absent) → `written`
420
+ * with the raw value on disk.
421
+ *
422
+ * `matchesModuloRedaction` is IMPORTED from `../vault-mirror/process.mjs`, not
423
+ * re-derived here: `secret-masker.mjs`'s header states this compensation as a
424
+ * contract binding on every consumer that diffs a written artifact against a fresh
425
+ * render, and a contract held at two addresses is a contract that drifts. This
426
+ * module already depends on `vault-mirror/utils.mjs`, and `vault-mirror` imports
427
+ * nothing from `vault-status`, so the direction adds no cycle.
428
+ *
335
429
  * @param {{
336
430
  * outputPath: string,
337
431
  * content: string,
@@ -379,8 +473,16 @@ export function writeNarrative(opts) {
379
473
  return { action: 'skipped-handwritten', path: outputPath };
380
474
  }
381
475
 
382
- // skip-noop: content identical modulo the `updated:` timestamp.
383
- if (normalizeUpdated(existingContent) === normalizeUpdated(content)) {
476
+ // skip-noop: content identical modulo the `updated:` timestamp, or —
477
+ // #1025 identical once every `[REDACTED]` span already on disk is read
478
+ // as a wildcard. See the guard-order note above for why the second arm is
479
+ // load-bearing rather than a nicety.
480
+ const existingNormalized = normalizeUpdated(existingContent);
481
+ const candidateNormalized = normalizeUpdated(content);
482
+ if (
483
+ existingNormalized === candidateNormalized ||
484
+ matchesModuloRedaction(existingNormalized, candidateNormalized)
485
+ ) {
384
486
  return { action: 'skipped-noop', path: outputPath };
385
487
  }
386
488
  }
@@ -466,6 +568,15 @@ function resolveLooseSlug(vaultDir, candidateSlug, fsSeam = {}) {
466
568
  * resolved vault path is validated to live inside the (home-expanded) vault root
467
569
  * before any write.
468
570
  *
571
+ * THIS FUNCTION WRITES TO THE OPERATOR'S REAL VAULT UNLESS YOU STOP IT. `vault-dir`
572
+ * resolves HOST-LOCALLY — `SO_VAULT_DIR` > `owner.yaml` `paths.vault-dir` >
573
+ * committed Session Config (`scripts/lib/config/host-paths.mjs`) — so the literal
574
+ * value in a CLAUDE.md you just wrote into a tmp dir LOSES to both overrides. A
575
+ * probe that passes a synthetic `repoRoot` and nothing else has already written
576
+ * into the live vault once (#1025 review). Any caller that must not touch the real
577
+ * vault passes `hostPaths: { env: {}, ownerConfig: undefined }` (see below),
578
+ * `dryRun: true`, or a fully injected `fs`; a tmp `repoRoot` alone is NOT enough.
579
+ *
469
580
  * @param {{
470
581
  * repoRoot: string,
471
582
  * repo: string,
@@ -543,7 +654,10 @@ export async function mirrorNarrative(opts) {
543
654
  return { action: 'skipped-no-statemd', path: outputPath };
544
655
  }
545
656
 
546
- const narrative = extractNarrative(stateContents);
657
+ // #1025: the ONE masking site for the narrative mirror — after the frontmatter
658
+ // parse (so `[REDACTED]` can never break it) and before the render, the
659
+ // idempotency comparison and the write. See maskNarrative above.
660
+ const narrative = maskNarrative(extractNarrative(stateContents));
547
661
  const content = renderNarrative({ repo: repoName, narrative, now });
548
662
 
549
663
  return writeNarrative({ outputPath, content, dryRun, fs: injectedFs });