mandrel 1.59.0 → 1.61.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 (267) hide show
  1. package/.agents/README.md +86 -44
  2. package/.agents/docs/SDLC.md +135 -141
  3. package/.agents/docs/configuration.md +77 -20
  4. package/.agents/docs/quality-gates.md +796 -0
  5. package/.agents/docs/workflows.md +6 -9
  6. package/.agents/instructions.md +12 -11
  7. package/.agents/personas/architect.md +1 -1
  8. package/.agents/personas/product.md +1 -1
  9. package/.agents/personas/project-manager.md +14 -14
  10. package/.agents/personas/technical-writer.md +1 -1
  11. package/.agents/rules/changelog-style.md +5 -5
  12. package/.agents/rules/git-conventions.md +3 -3
  13. package/.agents/runtime-deps.json +2 -2
  14. package/.agents/schemas/agentrc.schema.json +3 -3
  15. package/.agents/schemas/dispatch-manifest.json +4 -4
  16. package/.agents/schemas/epic-spec.schema.json +15 -45
  17. package/.agents/schemas/lifecycle/README.md +1 -1
  18. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +1 -1
  19. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +1 -1
  20. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +1 -1
  21. package/.agents/schemas/validation-evidence.schema.json +1 -1
  22. package/.agents/scripts/README.md +2 -2
  23. package/.agents/scripts/acceptance-eval.js +1 -1
  24. package/.agents/scripts/acceptance-spec-reconciler.js +2 -2
  25. package/.agents/scripts/agents-bootstrap-github.js +23 -119
  26. package/.agents/scripts/analyze-execution.js +2 -2
  27. package/.agents/scripts/audit-to-stories.js +1 -1
  28. package/.agents/scripts/check-doc-links.js +2 -3
  29. package/.agents/scripts/diagnose-friction.js +1 -1
  30. package/.agents/scripts/dispatcher.js +2 -2
  31. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  32. package/.agents/scripts/epic-audit-prepare.js +3 -3
  33. package/.agents/scripts/epic-deliver-note-intervention.js +2 -2
  34. package/.agents/scripts/epic-deliver-preflight.js +6 -6
  35. package/.agents/scripts/epic-deliver-prepare.js +1 -1
  36. package/.agents/scripts/epic-execute-record-wave.js +4 -4
  37. package/.agents/scripts/epic-plan-healthcheck.js +6 -10
  38. package/.agents/scripts/epic-plan-spec-validate.js +1 -1
  39. package/.agents/scripts/epic-reconcile.js +11 -29
  40. package/.agents/scripts/evidence-gate.js +1 -1
  41. package/.agents/scripts/generate-workflows-doc.js +1 -1
  42. package/.agents/scripts/hierarchy-gate.js +7 -11
  43. package/.agents/scripts/lib/ITicketingProvider.js +1 -1
  44. package/.agents/scripts/lib/audit-suite/selector.js +1 -1
  45. package/.agents/scripts/lib/audit-to-stories/seed-epic-from-findings.js +2 -2
  46. package/.agents/scripts/lib/baseline-snapshot.js +7 -7
  47. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  48. package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
  49. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +1 -1
  50. package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
  51. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +47 -1
  52. package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
  53. package/.agents/scripts/lib/bootstrap/gh-preflight.js +7 -9
  54. package/.agents/scripts/lib/bootstrap/manifest.js +21 -1
  55. package/.agents/scripts/lib/bootstrap/merge-methods.js +31 -16
  56. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +32 -11
  57. package/.agents/scripts/lib/codebase-snapshot.js +1 -1
  58. package/.agents/scripts/lib/config/explain.js +1 -1
  59. package/.agents/scripts/lib/config/runners.js +2 -2
  60. package/.agents/scripts/lib/config/runtime.js +1 -1
  61. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  62. package/.agents/scripts/lib/config/temp-paths.js +2 -2
  63. package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
  64. package/.agents/scripts/lib/config-settings-schema-quality.js +1 -1
  65. package/.agents/scripts/lib/config-settings-schema.js +3 -3
  66. package/.agents/scripts/lib/detect-package-manager.js +72 -0
  67. package/.agents/scripts/lib/duplicate-search.js +1 -1
  68. package/.agents/scripts/lib/dynamic-workflow/capability.js +1 -1
  69. package/.agents/scripts/lib/epic-plan-clarity.js +1 -1
  70. package/.agents/scripts/lib/epic-plan-ideation.js +1 -1
  71. package/.agents/scripts/lib/errors/index.js +4 -4
  72. package/.agents/scripts/lib/feedback-loop/memory-freshness.js +1 -1
  73. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
  74. package/.agents/scripts/lib/findings/classify-finding.js +1 -1
  75. package/.agents/scripts/lib/findings/promote-finding.js +10 -10
  76. package/.agents/scripts/lib/label-constants.js +3 -4
  77. package/.agents/scripts/lib/label-taxonomy.js +5 -10
  78. package/.agents/scripts/lib/onboard/detect-stack.js +10 -10
  79. package/.agents/scripts/lib/onboard/init-tail.js +218 -0
  80. package/.agents/scripts/lib/onboard/scaffold-docs.js +18 -3
  81. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
  82. package/.agents/scripts/lib/orchestration/code-review.js +5 -5
  83. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +8 -9
  84. package/.agents/scripts/lib/orchestration/dependency-analyzer.js +3 -3
  85. package/.agents/scripts/lib/orchestration/detectors-phase.js +2 -2
  86. package/.agents/scripts/lib/orchestration/dispatch-engine.js +30 -38
  87. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +9 -25
  88. package/.agents/scripts/lib/orchestration/epic-cleanup.js +1 -1
  89. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +8 -8
  90. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +1 -1
  91. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +7 -21
  92. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +3 -3
  93. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +26 -13
  94. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +1 -1
  95. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +1 -1
  96. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +2 -2
  97. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +1 -1
  98. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +3 -3
  99. package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +4 -4
  100. package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +3 -3
  101. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +6 -21
  102. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +7 -7
  103. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -1
  104. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +2 -2
  105. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +4 -4
  106. package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +4 -4
  107. package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +8 -8
  108. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -4
  109. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +7 -15
  110. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +72 -41
  111. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +2 -4
  112. package/.agents/scripts/lib/orchestration/file-assumptions.js +2 -2
  113. package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +1 -1
  114. package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +2 -2
  115. package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +1 -1
  116. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -3
  117. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +1 -1
  118. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +1 -1
  119. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +1 -1
  120. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +1 -1
  121. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +1 -1
  122. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +1 -1
  123. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +1 -1
  124. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
  125. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +1 -1
  126. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +1 -1
  127. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +1 -1
  128. package/.agents/scripts/lib/orchestration/manifest-builder.js +5 -5
  129. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +2 -2
  130. package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +5 -5
  131. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +3 -3
  132. package/.agents/scripts/lib/orchestration/preflight-cache.js +1 -1
  133. package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +1 -1
  134. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +1 -1
  135. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +2 -2
  136. package/.agents/scripts/lib/orchestration/retro-runner.js +3 -3
  137. package/.agents/scripts/lib/orchestration/review-depth.js +1 -1
  138. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +1 -1
  139. package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
  140. package/.agents/scripts/lib/orchestration/spec-renderer.js +36 -73
  141. package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
  142. package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +1 -1
  143. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +2 -2
  144. package/.agents/scripts/lib/orchestration/task-body-validator.js +6 -6
  145. package/.agents/scripts/lib/orchestration/ticket-lease.js +1 -1
  146. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -2
  147. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -10
  148. package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -70
  149. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +5 -12
  150. package/.agents/scripts/lib/orchestration/ticketing/reads.js +8 -8
  151. package/.agents/scripts/lib/orchestration/ticketing/state.js +3 -3
  152. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +2 -2
  153. package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -1
  154. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
  155. package/.agents/scripts/lib/preflight-runner.js +1 -1
  156. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +4 -5
  157. package/.agents/scripts/lib/presentation/manifest-builder.js +28 -34
  158. package/.agents/scripts/lib/presentation/manifest-formatter.js +3 -4
  159. package/.agents/scripts/lib/presentation/manifest-helpers.js +1 -1
  160. package/.agents/scripts/lib/presentation/manifest-procedures.js +4 -4
  161. package/.agents/scripts/lib/presentation/manifest-render-waves.js +4 -23
  162. package/.agents/scripts/lib/presentation/manifest-renderer.js +1 -1
  163. package/.agents/scripts/lib/presentation/manifest-story-views.js +2 -11
  164. package/.agents/scripts/lib/runtime-deps/preflight.js +6 -6
  165. package/.agents/scripts/lib/signals/schema.js +1 -1
  166. package/.agents/scripts/lib/spec/index.js +1 -1
  167. package/.agents/scripts/lib/spec/loader.js +2 -2
  168. package/.agents/scripts/lib/spec/state.js +7 -16
  169. package/.agents/scripts/lib/story-init/context-resolver.js +3 -3
  170. package/.agents/scripts/lib/story-init/state-transitioner.js +2 -2
  171. package/.agents/scripts/lib/story-init/task-graph-builder.js +7 -7
  172. package/.agents/scripts/lib/story-lifecycle.js +8 -8
  173. package/.agents/scripts/lib/story-plan.js +1 -1
  174. package/.agents/scripts/lib/templates/decomposer-prompts.js +59 -52
  175. package/.agents/scripts/lib/wave-runner/tick.js +1 -1
  176. package/.agents/scripts/lib/worktree/node-modules-strategy.js +5 -2
  177. package/.agents/scripts/lifecycle-emit-story-dispatch.js +1 -1
  178. package/.agents/scripts/lifecycle-emit.js +1 -1
  179. package/.agents/scripts/providers/github/board-add.js +1 -1
  180. package/.agents/scripts/providers/github/errors.js +1 -1
  181. package/.agents/scripts/providers/github/mappers.js +2 -2
  182. package/.agents/scripts/providers/github/tickets.js +4 -4
  183. package/.agents/scripts/resync-status-column.js +1 -1
  184. package/.agents/scripts/retro-run.js +2 -2
  185. package/.agents/scripts/run-lint.js +1 -1
  186. package/.agents/scripts/single-story-init.js +1 -1
  187. package/.agents/scripts/stories-wave-tick.js +5 -5
  188. package/.agents/scripts/story-close.js +1 -1
  189. package/.agents/scripts/story-init.js +13 -16
  190. package/.agents/scripts/story-phase.js +5 -5
  191. package/.agents/scripts/story-plan.js +3 -3
  192. package/.agents/scripts/sync-branch-from-base.js +1 -1
  193. package/.agents/scripts/validate-docs-freshness.js +1 -1
  194. package/.agents/scripts/wave-tick.js +1 -1
  195. package/.agents/skills/core/analyze-execution/SKILL.md +2 -2
  196. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +21 -26
  197. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -56
  198. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +4 -4
  199. package/.agents/skills/core/hydrate-context/SKILL.md +2 -2
  200. package/.agents/skills/core/idea-refinement/SKILL.md +4 -4
  201. package/.agents/skills/core/knowledge-transfer/SKILL.md +2 -2
  202. package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +1 -1
  203. package/.agents/skills/core/scope-triage/SKILL.md +9 -10
  204. package/.agents/skills/core/using-agent-skills/SKILL.md +1 -1
  205. package/.agents/skills/skills.index.json +7 -7
  206. package/.agents/templates/agent-protocol.md +2 -2
  207. package/.agents/workflows/agents-update.md +16 -31
  208. package/.agents/workflows/audit-architecture.md +2 -2
  209. package/.agents/workflows/audit-clean-code.md +2 -2
  210. package/.agents/workflows/audit-dependencies.md +1 -1
  211. package/.agents/workflows/audit-devops.md +1 -1
  212. package/.agents/workflows/audit-documentation.md +2 -2
  213. package/.agents/workflows/audit-lighthouse.md +1 -1
  214. package/.agents/workflows/audit-performance.md +2 -2
  215. package/.agents/workflows/audit-privacy.md +1 -1
  216. package/.agents/workflows/audit-quality.md +2 -2
  217. package/.agents/workflows/audit-security.md +2 -2
  218. package/.agents/workflows/audit-seo.md +1 -1
  219. package/.agents/workflows/audit-sre.md +1 -1
  220. package/.agents/workflows/audit-to-stories.md +10 -10
  221. package/.agents/workflows/audit-ux-ui.md +1 -1
  222. package/.agents/workflows/deliver.md +85 -0
  223. package/.agents/workflows/explain.md +3 -3
  224. package/.agents/workflows/git-merge-pr.md +1 -1
  225. package/.agents/workflows/git-pr-all.md +13 -10
  226. package/.agents/workflows/git-push.md +6 -3
  227. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  228. package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
  229. package/.agents/workflows/helpers/agents-sync-config.md +3 -2
  230. package/.agents/workflows/helpers/code-review.md +5 -5
  231. package/.agents/workflows/{epic-deliver.md → helpers/deliver-epic.md} +43 -43
  232. package/.agents/workflows/{story-deliver.md → helpers/deliver-stories.md} +25 -25
  233. package/.agents/workflows/helpers/diagnose.md +1 -1
  234. package/.agents/workflows/helpers/epic-audit.md +6 -6
  235. package/.agents/workflows/helpers/epic-deliver-story.md +13 -13
  236. package/.agents/workflows/helpers/epic-plan-decompose.md +23 -23
  237. package/.agents/workflows/helpers/epic-plan-spec.md +6 -6
  238. package/.agents/workflows/helpers/epic-testing.md +3 -3
  239. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  240. package/.agents/workflows/{epic-plan.md → helpers/plan-epic.md} +84 -84
  241. package/.agents/workflows/{story-plan.md → helpers/plan-story.md} +43 -43
  242. package/.agents/workflows/helpers/signals.md +1 -1
  243. package/.agents/workflows/helpers/single-story-deliver.md +11 -11
  244. package/.agents/workflows/helpers/worktree-lifecycle.md +18 -18
  245. package/.agents/workflows/plan.md +131 -0
  246. package/.agents/workflows/qa-explore.md +1 -1
  247. package/.agents/workflows/qa-run-harness.md +1 -1
  248. package/README.md +19 -39
  249. package/bin/mandrel.js +235 -16
  250. package/docs/CHANGELOG.md +1173 -0
  251. package/lib/cli/doctor.js +45 -3
  252. package/lib/cli/init.js +97 -36
  253. package/lib/cli/registry.js +41 -145
  254. package/lib/cli/sync.js +122 -23
  255. package/lib/cli/uninstall.js +42 -7
  256. package/lib/cli/update.js +524 -210
  257. package/lib/cli/version-helpers.js +59 -0
  258. package/package.json +7 -6
  259. package/.agents/scripts/lib/orchestration/reconciler.js +0 -137
  260. package/.agents/workflows/onboard.md +0 -208
  261. package/lib/cli/__tests__/migrate.test.js +0 -268
  262. package/lib/cli/__tests__/sync-local-zone.test.js +0 -247
  263. package/lib/cli/__tests__/sync.test.js +0 -372
  264. package/lib/cli/__tests__/update-major.test.js +0 -217
  265. package/lib/cli/__tests__/update.test.js +0 -696
  266. package/lib/cli/__tests__/version-check.test.js +0 -398
  267. package/lib/migrations/__tests__/index.test.js +0 -216
package/lib/cli/sync.js CHANGED
@@ -7,7 +7,8 @@
7
7
  * `./.agents/` directory by **plain file copy** — never a symlink — so
8
8
  * Windows and POSIX behave identically (Tech Spec #3459, Feature #3461).
9
9
  *
10
- * Design contract (per Story #3467 AC and Tech Spec #3459 "API Changes"):
10
+ * Design contract (per Story #3467 AC, Tech Spec #3459 "API Changes", and
11
+ * Story #4046 sync-prune):
11
12
  * - Copy, not symlink. The materialized tree is plain regular files.
12
13
  * - Idempotent. A second run overwrites in place and leaves ./.agents/
13
14
  * byte-identical to the package payload.
@@ -17,12 +18,14 @@
17
18
  * - Exits non-zero with an actionable message when `mandrel` is
18
19
  * not resolvable in node_modules.
19
20
  * - `--dry-run` reports the planned copies and writes nothing.
20
- * - `--force` is accepted; the copy is overwrite-in-place either way, so
21
- * it exists for explicitness/forward-compat and changes no behaviour.
22
21
  * - Local-additions zone. The `.agents/local/` subtree is never copied
23
22
  * into nor pruned from the destination (Story #3498). It is the
24
23
  * consumer-owned space for hand-authored additions that must survive
25
24
  * every re-materialization.
25
+ * - Sync prune (Story #4046). After the copy pass, any file inside the
26
+ * managed `.agents/` zone (everything except `.agents/local/`) that has
27
+ * no counterpart in the package payload is deleted. Consumer additions
28
+ * under `.agents/local/` are never touched.
26
29
  *
27
30
  * Security (Tech Spec #3459 "Postinstall safety"):
28
31
  * - Does nothing beyond a local file copy: no network, no shell, no writes
@@ -42,7 +45,7 @@ import nodeFs from 'node:fs';
42
45
  import { createRequire } from 'node:module';
43
46
  import path from 'node:path';
44
47
 
45
- const PACKAGE_NAME = 'mandrel';
48
+ export const PACKAGE_NAME = 'mandrel';
46
49
 
47
50
  /**
48
51
  * Top-level directory name (relative to `.agents/`) reserved as the
@@ -56,7 +59,16 @@ const PACKAGE_NAME = 'mandrel';
56
59
  * which keeps both the dry-run plan and the real copy free of any
57
60
  * `.agents/local/**` path even if a future payload were to ship one.
58
61
  */
59
- const LOCAL_ZONE_DIR = 'local';
62
+ export const LOCAL_ZONE_DIR = 'local';
63
+
64
+ /**
65
+ * Basename pattern for consumer local-override files (`*.local.<ext>`,
66
+ * e.g. `instructions.local.md`, `foo.local.json`). Matches the gitignore
67
+ * convention (`.agents/*.local.md`, `.agentrc.local.json`) and the override
68
+ * mechanism documented in `.agents/instructions.md` § 1.E. These files never
69
+ * ship in the payload and are exempt from the sync prune pass.
70
+ */
71
+ export const LOCAL_OVERRIDE_RE = /\.local\.[^.]+$/;
60
72
 
61
73
  /**
62
74
  * Default resolver: locate the installed `mandrel` package root by
@@ -68,7 +80,7 @@ const LOCAL_ZONE_DIR = 'local';
68
80
  * @param {string} fromDir - Directory to resolve from (the consumer project).
69
81
  * @returns {string} Absolute path to the package root.
70
82
  */
71
- function defaultResolvePackageRoot(fromDir) {
83
+ export function defaultResolvePackageRoot(fromDir) {
72
84
  // Resolve relative to the consumer project so we find *their* install,
73
85
  // not a copy hoisted next to this CLI module.
74
86
  const requireFrom = createRequire(path.join(fromDir, 'noop.js'));
@@ -88,13 +100,13 @@ function defaultResolvePackageRoot(fromDir) {
88
100
  * from the package payload (Story #3498). See {@link LOCAL_ZONE_DIR}.
89
101
  *
90
102
  * @param {string} dir - Absolute directory to walk.
91
- * @param {typeof nodeFs} fs
103
+ * @param {typeof nodeFs} fsImpl
92
104
  * @param {string} [prefix] - Accumulated relative prefix (internal).
93
105
  * @returns {string[]} Relative file paths.
94
106
  */
95
- function listFiles(dir, fs, prefix = '') {
107
+ export function listFiles(dir, fsImpl, prefix = '') {
96
108
  const out = [];
97
- for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
109
+ for (const ent of fsImpl.readdirSync(dir, { withFileTypes: true })) {
98
110
  // Never enumerate the sync-exempt local-additions zone. Matching on the
99
111
  // empty prefix scopes the skip to the top-level `.agents/local/` only,
100
112
  // leaving any deeper directory that happens to be named `local` intact.
@@ -104,7 +116,55 @@ function listFiles(dir, fs, prefix = '') {
104
116
  const rel = prefix ? path.join(prefix, ent.name) : ent.name;
105
117
  const abs = path.join(dir, ent.name);
106
118
  if (ent.isDirectory()) {
107
- out.push(...listFiles(abs, fs, rel));
119
+ out.push(...listFiles(abs, fsImpl, rel));
120
+ } else {
121
+ out.push(rel);
122
+ }
123
+ }
124
+ return out;
125
+ }
126
+
127
+ /**
128
+ * Recursively enumerate every regular file under `dir`, returning paths
129
+ * relative to `dir` using OS separators. The top-level `local/` subtree is
130
+ * skipped so consumer additions inside `.agents/local/` are never pruned
131
+ * (Story #3498). Files following the `*.local.*` naming convention (e.g.
132
+ * `.agents/instructions.local.md` — the documented consumer override
133
+ * mechanism, instructions.md § 1.E) are also skipped: they never ship in
134
+ * the payload and must survive every sync.
135
+ *
136
+ * Mirrors `listFiles` but operates on the destination tree so we can
137
+ * identify stale files that have no payload counterpart (Story #4046 A3).
138
+ *
139
+ * @param {string} dir - Absolute directory to walk.
140
+ * @param {typeof nodeFs} fsImpl
141
+ * @param {string} [prefix] - Accumulated relative prefix (internal).
142
+ * @returns {string[]} Relative file paths.
143
+ */
144
+ function listDestFiles(dir, fsImpl, prefix = '') {
145
+ const out = [];
146
+ let entries;
147
+ try {
148
+ entries = fsImpl.readdirSync(dir, { withFileTypes: true });
149
+ } catch {
150
+ // Directory absent — nothing to prune.
151
+ return out;
152
+ }
153
+ for (const ent of entries) {
154
+ // The local-additions zone is never pruned; skip it at the top level.
155
+ if (prefix === '' && ent.name === LOCAL_ZONE_DIR && ent.isDirectory()) {
156
+ continue;
157
+ }
158
+ // Consumer local-override files (`*.local.*`, e.g. instructions.local.md,
159
+ // foo.local.json) are gitignored, never shipped in the payload, and a
160
+ // documented override mechanism (instructions.md § 1.E) — never prune.
161
+ if (!ent.isDirectory() && LOCAL_OVERRIDE_RE.test(ent.name)) {
162
+ continue;
163
+ }
164
+ const rel = prefix ? path.join(prefix, ent.name) : ent.name;
165
+ const abs = path.join(dir, ent.name);
166
+ if (ent.isDirectory()) {
167
+ out.push(...listDestFiles(abs, fsImpl, rel));
108
168
  } else {
109
169
  out.push(rel);
110
170
  }
@@ -115,6 +175,11 @@ function listFiles(dir, fs, prefix = '') {
115
175
  /**
116
176
  * Materialize the package's `.agents/` tree into `./.agents/`.
117
177
  *
178
+ * After the copy pass, any file inside the managed zone of the destination
179
+ * `.agents/` tree (everything outside `.agents/local/`) that has no
180
+ * counterpart in the package payload is deleted (sync-prune, Story #4046 A3).
181
+ * Consumer additions placed under `.agents/local/` are never touched.
182
+ *
118
183
  * @param {{
119
184
  * argv?: string[],
120
185
  * resolvePackageRoot?: (fromDir: string) => string,
@@ -124,8 +189,8 @@ function listFiles(dir, fs, prefix = '') {
124
189
  * writeErr?: (s: string) => void,
125
190
  * exit?: (code: number) => void,
126
191
  * }} [opts]
127
- * @returns {{ copied: number, planned: number, dryRun: boolean }} Summary
128
- * (also returned in dry-run / error paths for testability).
192
+ * @returns {{ copied: number, planned: number, pruned: number, dryRun: boolean }}
193
+ * Summary (also returned in dry-run / error paths for testability).
129
194
  */
130
195
  export function runSync({
131
196
  argv = [],
@@ -137,8 +202,6 @@ export function runSync({
137
202
  exit = (code) => process.exit(code),
138
203
  } = {}) {
139
204
  const dryRun = argv.includes('--dry-run');
140
- // `--force` overwrites local edits. The copy is overwrite-in-place
141
- // regardless, so the flag is accepted but does not branch behaviour.
142
205
  const projectRoot = cwd();
143
206
 
144
207
  let packageRoot;
@@ -150,7 +213,7 @@ export function runSync({
150
213
  ` → Install it first: npm install ${PACKAGE_NAME}\n`,
151
214
  );
152
215
  exit(1);
153
- return { copied: 0, planned: 0, dryRun };
216
+ return { copied: 0, planned: 0, pruned: 0, dryRun };
154
217
  }
155
218
 
156
219
  const sourceRoot = path.join(packageRoot, '.agents');
@@ -160,23 +223,36 @@ export function runSync({
160
223
  ` → Reinstall the package: npm install ${PACKAGE_NAME}\n`,
161
224
  );
162
225
  exit(1);
163
- return { copied: 0, planned: 0, dryRun };
226
+ return { copied: 0, planned: 0, pruned: 0, dryRun };
164
227
  }
165
228
 
166
229
  const destRoot = path.join(projectRoot, '.agents');
167
- const files = listFiles(sourceRoot, fs);
230
+ const payloadFiles = listFiles(sourceRoot, fs);
168
231
 
169
232
  if (dryRun) {
170
- for (const rel of files) {
233
+ for (const rel of payloadFiles) {
171
234
  write(`would copy ${path.join('.agents', rel)}\n`);
172
235
  }
236
+ // Compute stale files (managed-zone destination files with no payload
237
+ // counterpart) for the dry-run plan so operators can preview pruning.
238
+ const payloadSet = new Set(payloadFiles);
239
+ const destFiles = listDestFiles(destRoot, fs);
240
+ const stale = destFiles.filter((f) => !payloadSet.has(f));
241
+ for (const rel of stale) {
242
+ write(`would prune ${path.join('.agents', rel)}\n`);
243
+ }
173
244
  write(
174
- `✅ Dry run: ${files.length} file(s) would be materialized into ./.agents/\n`,
245
+ `✅ Dry run: ${payloadFiles.length} file(s) would be materialized, ${stale.length} stale file(s) would be pruned from ./.agents/\n`,
175
246
  );
176
- return { copied: 0, planned: files.length, dryRun: true };
247
+ return {
248
+ copied: 0,
249
+ planned: payloadFiles.length,
250
+ pruned: 0,
251
+ dryRun: true,
252
+ };
177
253
  }
178
254
 
179
- for (const rel of files) {
255
+ for (const rel of payloadFiles) {
180
256
  const src = path.join(sourceRoot, rel);
181
257
  const dest = path.join(destRoot, rel);
182
258
  fs.mkdirSync(path.dirname(dest), { recursive: true });
@@ -185,8 +261,31 @@ export function runSync({
185
261
  fs.copyFileSync(src, dest);
186
262
  }
187
263
 
188
- write(`✅ Materialized ${files.length} file(s) into ./.agents/\n`);
189
- return { copied: files.length, planned: files.length, dryRun: false };
264
+ // Prune pass (Story #4046 A3): remove managed-zone destination files that
265
+ // have no counterpart in the payload. The local-additions zone
266
+ // (.agents/local/) is never enumerated by listDestFiles and is therefore
267
+ // never pruned — consumer additions there are sanctioned, not stale.
268
+ const payloadSet = new Set(payloadFiles);
269
+ const destFiles = listDestFiles(destRoot, fs);
270
+ const staleFiles = destFiles.filter((f) => !payloadSet.has(f));
271
+ for (const rel of staleFiles) {
272
+ const dest = path.join(destRoot, rel);
273
+ fs.unlinkSync(dest);
274
+ }
275
+
276
+ if (staleFiles.length > 0) {
277
+ write(
278
+ `✅ Materialized ${payloadFiles.length} file(s) into ./.agents/ (pruned ${staleFiles.length} stale file(s))\n`,
279
+ );
280
+ } else {
281
+ write(`✅ Materialized ${payloadFiles.length} file(s) into ./.agents/\n`);
282
+ }
283
+ return {
284
+ copied: payloadFiles.length,
285
+ planned: payloadFiles.length,
286
+ pruned: staleFiles.length,
287
+ dryRun: false,
288
+ };
190
289
  }
191
290
 
192
291
  /**
@@ -54,7 +54,6 @@
54
54
 
55
55
  import fs from 'node:fs';
56
56
  import path from 'node:path';
57
- import { fileURLToPath } from 'node:url';
58
57
 
59
58
  import {
60
59
  LEDGER_SCHEMA_VERSION,
@@ -640,6 +639,7 @@ function formatOutcome(outcome) {
640
639
  * write?: (s: string) => void,
641
640
  * exit?: (code: number) => void,
642
641
  * includeGithub?: boolean,
642
+ * dryRun?: boolean,
643
643
  * }} [opts]
644
644
  * @returns {{ revertedCount: number, manualCount: number, ledgerFound: boolean, parseErrorCount: number }}
645
645
  */
@@ -650,6 +650,7 @@ export function runUninstall({
650
650
  write = (s) => process.stdout.write(s),
651
651
  exit = (code) => process.exit(code),
652
652
  includeGithub = false,
653
+ dryRun = false,
653
654
  } = {}) {
654
655
  const root = projectRoot ?? resolveProjectRoot(cwd);
655
656
 
@@ -695,6 +696,39 @@ export function runUninstall({
695
696
 
696
697
  const { fileTargets, manual, executedActionByTarget } = planUninstall(ledger);
697
698
 
699
+ // --- Dry run — show what would be reversed without touching anything. ---
700
+ if (dryRun) {
701
+ write('mandrel uninstall — planned reversal (dry run)\n');
702
+ for (const target of fileTargets) {
703
+ write(
704
+ formatOutcome({
705
+ kind: 'reverted',
706
+ target,
707
+ detail: '(would be reverted)',
708
+ }),
709
+ );
710
+ }
711
+ for (const entry of manual) {
712
+ const note = includeGithub
713
+ ? `${entry.detail} (acknowledged — reverse manually via the GitHub UI/API)`
714
+ : `${entry.detail} (left untouched — pass --include-github to acknowledge)`;
715
+ write(
716
+ formatOutcome({ kind: 'manual', target: entry.target, detail: note }),
717
+ );
718
+ }
719
+ write(
720
+ `Dry run: ${fileTargets.length} file target(s) would be reverted, ` +
721
+ `${manual.length} manual follow-up(s).\n`,
722
+ );
723
+ exit(0);
724
+ return {
725
+ revertedCount: 0,
726
+ manualCount: manual.length,
727
+ ledgerFound: true,
728
+ parseErrorCount: 0,
729
+ };
730
+ }
731
+
698
732
  let revertedCount = 0;
699
733
  let parseErrorCount = 0;
700
734
  for (const target of fileTargets) {
@@ -782,14 +816,15 @@ export function runUninstall({
782
816
  /**
783
817
  * Default export consumed by `bin/mandrel.js`.
784
818
  *
785
- * @param {string[]} argv — supports the single `--include-github` flag.
819
+ * Supported flags:
820
+ * --include-github Acknowledge GitHub-side manual follow-ups.
821
+ * --dry-run Show what would be reversed without touching anything.
822
+ *
823
+ * @param {string[]} argv
786
824
  * @returns {Promise<void>}
787
825
  */
788
826
  export default async function run(argv = []) {
789
827
  const includeGithub = argv.includes('--include-github');
790
- runUninstall({ includeGithub });
828
+ const dryRun = argv.includes('--dry-run');
829
+ runUninstall({ includeGithub, dryRun });
791
830
  }
792
-
793
- // Re-export so tests and callers can reference the resolved module path
794
- // without re-deriving it.
795
- export const __filenameForTests = fileURLToPath(import.meta.url);