@aperant/framework 0.6.6 → 0.7.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 (150) hide show
  1. package/CHANGELOG.md +215 -0
  2. package/dist/cli/commands/check-version.d.mts.map +1 -1
  3. package/dist/cli/commands/check-version.mjs +76 -1
  4. package/dist/cli/commands/check-version.mjs.map +1 -1
  5. package/dist/cli/commands/ci-watch.d.mts.map +1 -1
  6. package/dist/cli/commands/ci-watch.mjs +73 -5
  7. package/dist/cli/commands/ci-watch.mjs.map +1 -1
  8. package/dist/cli/commands/health-check.mjs +1 -1
  9. package/dist/cli/commands/health-check.mjs.map +1 -1
  10. package/dist/cli/commands/init.d.mts +17 -1
  11. package/dist/cli/commands/init.d.mts.map +1 -1
  12. package/dist/cli/commands/init.mjs +183 -11
  13. package/dist/cli/commands/init.mjs.map +1 -1
  14. package/dist/cli/commands/release-notes.d.mts +11 -0
  15. package/dist/cli/commands/release-notes.d.mts.map +1 -0
  16. package/dist/cli/commands/release-notes.mjs +173 -0
  17. package/dist/cli/commands/release-notes.mjs.map +1 -0
  18. package/dist/cli/commands/route.d.mts.map +1 -1
  19. package/dist/cli/commands/route.mjs +37 -2
  20. package/dist/cli/commands/route.mjs.map +1 -1
  21. package/dist/cli/commands/task.d.mts.map +1 -1
  22. package/dist/cli/commands/task.mjs +48 -0
  23. package/dist/cli/commands/task.mjs.map +1 -1
  24. package/dist/cli/config/load.d.mts +14 -0
  25. package/dist/cli/config/load.d.mts.map +1 -1
  26. package/dist/cli/config/load.mjs +40 -0
  27. package/dist/cli/config/load.mjs.map +1 -1
  28. package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
  29. package/dist/cli/config/upgrade-gitignore.mjs +6 -0
  30. package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
  31. package/dist/cli/consistency/parse-review.d.mts.map +1 -1
  32. package/dist/cli/consistency/parse-review.mjs +4 -1
  33. package/dist/cli/consistency/parse-review.mjs.map +1 -1
  34. package/dist/cli/dispatch.d.mts.map +1 -1
  35. package/dist/cli/dispatch.mjs +2 -0
  36. package/dist/cli/dispatch.mjs.map +1 -1
  37. package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
  38. package/dist/cli/gate/gates/review-clean.mjs +4 -2
  39. package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
  40. package/dist/cli/help.d.mts.map +1 -1
  41. package/dist/cli/help.mjs +1 -0
  42. package/dist/cli/help.mjs.map +1 -1
  43. package/dist/cli/host/detect.d.mts +122 -9
  44. package/dist/cli/host/detect.d.mts.map +1 -1
  45. package/dist/cli/host/detect.mjs +132 -0
  46. package/dist/cli/host/detect.mjs.map +1 -1
  47. package/dist/cli/install/legacy-paths.d.mts +38 -0
  48. package/dist/cli/install/legacy-paths.d.mts.map +1 -0
  49. package/dist/cli/install/legacy-paths.mjs +69 -0
  50. package/dist/cli/install/legacy-paths.mjs.map +1 -0
  51. package/dist/cli/install/runtime-migrate.d.mts +84 -0
  52. package/dist/cli/install/runtime-migrate.d.mts.map +1 -0
  53. package/dist/cli/install/runtime-migrate.mjs +244 -0
  54. package/dist/cli/install/runtime-migrate.mjs.map +1 -0
  55. package/dist/cli/install/update-chips.d.mts +23 -0
  56. package/dist/cli/install/update-chips.d.mts.map +1 -1
  57. package/dist/cli/install/update-chips.mjs +60 -0
  58. package/dist/cli/install/update-chips.mjs.map +1 -1
  59. package/dist/cli/release-notes/compile.d.mts +38 -0
  60. package/dist/cli/release-notes/compile.d.mts.map +1 -0
  61. package/dist/cli/release-notes/compile.mjs +244 -0
  62. package/dist/cli/release-notes/compile.mjs.map +1 -0
  63. package/dist/cli/release-notes/draft.d.mts +73 -0
  64. package/dist/cli/release-notes/draft.d.mts.map +1 -0
  65. package/dist/cli/release-notes/draft.mjs +120 -0
  66. package/dist/cli/release-notes/draft.mjs.map +1 -0
  67. package/dist/cli/release-notes/output-dir.d.mts +20 -0
  68. package/dist/cli/release-notes/output-dir.d.mts.map +1 -0
  69. package/dist/cli/release-notes/output-dir.mjs +42 -0
  70. package/dist/cli/release-notes/output-dir.mjs.map +1 -0
  71. package/dist/cli/release-notes/persona-filter.d.mts +51 -0
  72. package/dist/cli/release-notes/persona-filter.d.mts.map +1 -0
  73. package/dist/cli/release-notes/persona-filter.mjs +127 -0
  74. package/dist/cli/release-notes/persona-filter.mjs.map +1 -0
  75. package/dist/cli/release-notes/publish.d.mts +23 -0
  76. package/dist/cli/release-notes/publish.d.mts.map +1 -0
  77. package/dist/cli/release-notes/publish.mjs +125 -0
  78. package/dist/cli/release-notes/publish.mjs.map +1 -0
  79. package/dist/cli/release-notes/ship-autodraft.d.mts +37 -0
  80. package/dist/cli/release-notes/ship-autodraft.d.mts.map +1 -0
  81. package/dist/cli/release-notes/ship-autodraft.mjs +97 -0
  82. package/dist/cli/release-notes/ship-autodraft.mjs.map +1 -0
  83. package/dist/cli/route/drift-detect.d.mts +20 -0
  84. package/dist/cli/route/drift-detect.d.mts.map +1 -0
  85. package/dist/cli/route/drift-detect.mjs +107 -0
  86. package/dist/cli/route/drift-detect.mjs.map +1 -0
  87. package/dist/cli/util/aperant-section.d.mts +34 -0
  88. package/dist/cli/util/aperant-section.d.mts.map +1 -0
  89. package/dist/cli/util/aperant-section.mjs +127 -0
  90. package/dist/cli/util/aperant-section.mjs.map +1 -0
  91. package/dist/cli/util/copy.d.mts +28 -1
  92. package/dist/cli/util/copy.d.mts.map +1 -1
  93. package/dist/cli/util/copy.mjs +43 -55
  94. package/dist/cli/util/copy.mjs.map +1 -1
  95. package/dist/cli/util/semver.d.mts +17 -0
  96. package/dist/cli/util/semver.d.mts.map +1 -0
  97. package/dist/cli/util/semver.mjs +29 -0
  98. package/dist/cli/util/semver.mjs.map +1 -0
  99. package/dist/cli/util/skill-installs.d.mts +65 -9
  100. package/dist/cli/util/skill-installs.d.mts.map +1 -1
  101. package/dist/cli/util/skill-installs.mjs +130 -21
  102. package/dist/cli/util/skill-installs.mjs.map +1 -1
  103. package/dist/cli/util/version-preflight.d.mts +44 -0
  104. package/dist/cli/util/version-preflight.d.mts.map +1 -0
  105. package/dist/cli/util/version-preflight.mjs +66 -0
  106. package/dist/cli/util/version-preflight.mjs.map +1 -0
  107. package/dist/types/config.d.ts +11 -7
  108. package/dist/types/config.d.ts.map +1 -1
  109. package/package.json +133 -133
  110. package/skills/apt-close-task/SKILL.md +30 -0
  111. package/skills/apt-diagram/SKILL.md +45 -9
  112. package/skills/apt-release-notes/SKILL.md +193 -0
  113. package/skills/apt-release-notes/appendices/persona-voice.md +59 -0
  114. package/skills/apt-setup/SKILL.md +146 -3
  115. package/skills/apt-ship/SKILL.md +63 -4
  116. package/skills/apt-spar/SKILL.md +36 -11
  117. package/skills/apt-update/SKILL.md +77 -10
  118. package/skills/apt-watch-ci/SKILL.md +6 -3
  119. package/src/cli/commands/check-version.mjs +73 -1
  120. package/src/cli/commands/ci-watch.mjs +74 -5
  121. package/src/cli/commands/health-check.mjs +1 -1
  122. package/src/cli/commands/init.mjs +202 -10
  123. package/src/cli/commands/release-notes.mjs +187 -0
  124. package/src/cli/commands/route.mjs +38 -2
  125. package/src/cli/commands/task.mjs +49 -0
  126. package/src/cli/config/load.mjs +37 -0
  127. package/src/cli/config/upgrade-gitignore.mjs +6 -0
  128. package/src/cli/consistency/parse-review.mjs +3 -1
  129. package/src/cli/dispatch.mjs +2 -0
  130. package/src/cli/gate/gates/review-clean.mjs +4 -2
  131. package/src/cli/help.mjs +1 -0
  132. package/src/cli/host/detect.mjs +135 -0
  133. package/src/cli/install/legacy-paths.mjs +69 -0
  134. package/src/cli/install/runtime-migrate.mjs +252 -0
  135. package/src/cli/install/update-chips.mjs +57 -0
  136. package/src/cli/release-notes/compile.mjs +261 -0
  137. package/src/cli/release-notes/draft.mjs +133 -0
  138. package/src/cli/release-notes/output-dir.mjs +52 -0
  139. package/src/cli/release-notes/persona-filter.mjs +126 -0
  140. package/src/cli/release-notes/publish.mjs +128 -0
  141. package/src/cli/release-notes/ship-autodraft.mjs +106 -0
  142. package/src/cli/route/drift-detect.mjs +107 -0
  143. package/src/cli/util/aperant-section.mjs +136 -0
  144. package/src/cli/util/copy.mjs +43 -56
  145. package/src/cli/util/semver.mjs +28 -0
  146. package/src/cli/util/skill-installs.mjs +134 -21
  147. package/src/cli/util/version-preflight.mjs +65 -0
  148. package/templates/aperant-claude-md-appendix.md +37 -0
  149. package/templates/config.json +28 -3
  150. package/workflows/docs.md +12 -0
package/package.json CHANGED
@@ -1,134 +1,134 @@
1
1
  {
2
- "name": "@aperant/framework",
3
- "version": "0.6.6",
4
- "description": "AI coding framework — composable skills for planning, executing, verifying, and reviewing code with any LLM provider. Works as Claude Code commands, Codex tasks, or programmatically.",
5
- "author": "Mikalsen AI <hello@mikalsen.ai>",
6
- "type": "module",
7
- "main": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "bin": {
10
- "framework": "./bin/apt-tools.mjs",
11
- "apt-tools": "./bin/apt-tools.mjs",
12
- "apt-proof-video": "./bin/apt-proof-video.mjs"
13
- },
14
- "exports": {
15
- ".": {
16
- "types": "./dist/index.d.ts",
17
- "import": "./dist/index.js"
18
- },
19
- "./types": {
20
- "types": "./dist/types/index.d.ts",
21
- "import": "./dist/types/index.js"
22
- },
23
- "./cost": {
24
- "types": "./dist/cost/index.d.ts",
25
- "import": "./dist/cost/index.js"
26
- },
27
- "./mappers": {
28
- "types": "./dist/mappers/index.d.ts",
29
- "import": "./dist/mappers/index.js"
30
- },
31
- "./schemas": {
32
- "types": "./dist/schemas/index.d.ts",
33
- "import": "./dist/schemas/index.js"
34
- },
35
- "./standalone": {
36
- "types": "./dist/standalone/index.d.ts",
37
- "import": "./dist/standalone/index.js"
38
- },
39
- "./coordination/event-schema": {
40
- "types": "./src/cli/coordination/event-schema.d.ts",
41
- "import": "./src/cli/coordination/event-schema.mjs"
42
- },
43
- "./roadmap/conductor-view": {
44
- "types": "./src/cli/roadmap/conductor-view.d.ts",
45
- "import": "./src/cli/roadmap/conductor-view.mjs"
46
- },
47
- "./design/scan": {
48
- "import": "./src/cli/design/scan.mjs"
49
- },
50
- "./design/extract-repo": {
51
- "import": "./src/cli/design/extract-repo.mjs"
52
- },
53
- "./design/synthesize": {
54
- "import": "./src/cli/design/synthesize.mjs"
55
- }
56
- },
57
- "scripts": {
58
- "build": "tsc && node scripts/extract-personas-schema.mjs",
59
- "dev": "tsc --watch --preserveWatchOutput",
60
- "typecheck": "tsc --noEmit",
61
- "lint": "biome check . --diagnostic-level=error",
62
- "lint:fix": "biome check --write . --diagnostic-level=error",
63
- "clean": "rm -rf dist",
64
- "test": "vitest run",
65
- "c28:cutover": "node scripts/c28-cutover.mjs",
66
- "build-plugin": "node scripts/build-plugin.mjs",
67
- "publish-plugin": "node scripts/publish-plugin.mjs",
68
- "publish:verdaccio": "node scripts/publish-verdaccio.mjs",
69
- "release:verdaccio": "pnpm build && pnpm publish:verdaccio",
70
- "ship:verdaccio": "npm version patch --no-git-tag-version && pnpm build && pnpm publish:verdaccio"
71
- },
72
- "files": [
73
- "dist/",
74
- "bin/",
75
- "src/cli/",
76
- "src/cost/",
77
- "commands/",
78
- "skills/",
79
- "agents/",
80
- "prompts/",
81
- "templates/",
82
- "context/",
83
- "workflows/",
84
- "examples/",
85
- "LICENSE",
86
- "README.md",
87
- "CHANGELOG.md",
88
- "!**/__tests__/**",
89
- "!**/*.test.*",
90
- "!**/*.spec.*",
91
- "!**/*.test.d.ts.map",
92
- "!**/*.test.js.map"
93
- ],
94
- "keywords": [
95
- "ai",
96
- "framework",
97
- "coding-assistant",
98
- "claude-code",
99
- "codex",
100
- "aperant"
101
- ],
102
- "license": "AGPL-3.0-only",
103
- "homepage": "https://github.com/Mikalsen-AI/aperant-cloud-desktop/tree/main/packages/framework#readme",
104
- "bugs": {
105
- "url": "https://github.com/Mikalsen-AI/aperant-cloud-desktop/issues"
106
- },
107
- "repository": {
108
- "type": "git",
109
- "url": "https://github.com/Mikalsen-AI/aperant-cloud-desktop.git",
110
- "directory": "packages/framework"
111
- },
112
- "engines": {
113
- "node": ">=18.0.0"
114
- },
115
- "publishConfig": {
116
- "access": "public"
117
- },
118
- "dependencies": {
119
- "@clack/prompts": "^1.2.0",
120
- "proper-lockfile": "^4.1.2",
121
- "yaml": "^2.8.3",
122
- "zod": "^4.3.6"
123
- },
124
- "optionalDependencies": {
125
- "@babel/parser": "^7.29.2",
126
- "@babel/traverse": "^7.29.0",
127
- "@google/genai": "^1.50.1",
128
- "css-tree": "^3.2.1",
129
- "playwright": "^1.59.1"
130
- },
131
- "devDependencies": {
132
- "@vitest/coverage-v8": "^4.1.5"
133
- }
134
- }
2
+ "name": "@aperant/framework",
3
+ "version": "0.7.0",
4
+ "description": "AI coding framework — composable skills for planning, executing, verifying, and reviewing code with any LLM provider. Works as Claude Code commands, Codex tasks, or programmatically.",
5
+ "author": "Mikalsen AI <hello@mikalsen.ai>",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "framework": "./bin/apt-tools.mjs",
11
+ "apt-tools": "./bin/apt-tools.mjs",
12
+ "apt-proof-video": "./bin/apt-proof-video.mjs"
13
+ },
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ },
19
+ "./types": {
20
+ "types": "./dist/types/index.d.ts",
21
+ "import": "./dist/types/index.js"
22
+ },
23
+ "./cost": {
24
+ "types": "./dist/cost/index.d.ts",
25
+ "import": "./dist/cost/index.js"
26
+ },
27
+ "./mappers": {
28
+ "types": "./dist/mappers/index.d.ts",
29
+ "import": "./dist/mappers/index.js"
30
+ },
31
+ "./schemas": {
32
+ "types": "./dist/schemas/index.d.ts",
33
+ "import": "./dist/schemas/index.js"
34
+ },
35
+ "./standalone": {
36
+ "types": "./dist/standalone/index.d.ts",
37
+ "import": "./dist/standalone/index.js"
38
+ },
39
+ "./coordination/event-schema": {
40
+ "types": "./src/cli/coordination/event-schema.d.ts",
41
+ "import": "./src/cli/coordination/event-schema.mjs"
42
+ },
43
+ "./roadmap/conductor-view": {
44
+ "types": "./src/cli/roadmap/conductor-view.d.ts",
45
+ "import": "./src/cli/roadmap/conductor-view.mjs"
46
+ },
47
+ "./design/scan": {
48
+ "import": "./src/cli/design/scan.mjs"
49
+ },
50
+ "./design/extract-repo": {
51
+ "import": "./src/cli/design/extract-repo.mjs"
52
+ },
53
+ "./design/synthesize": {
54
+ "import": "./src/cli/design/synthesize.mjs"
55
+ }
56
+ },
57
+ "files": [
58
+ "dist/",
59
+ "bin/",
60
+ "src/cli/",
61
+ "src/cost/",
62
+ "commands/",
63
+ "skills/",
64
+ "agents/",
65
+ "prompts/",
66
+ "templates/",
67
+ "context/",
68
+ "workflows/",
69
+ "examples/",
70
+ "LICENSE",
71
+ "README.md",
72
+ "CHANGELOG.md",
73
+ "!**/__tests__/**",
74
+ "!**/*.test.*",
75
+ "!**/*.spec.*",
76
+ "!**/*.test.d.ts.map",
77
+ "!**/*.test.js.map"
78
+ ],
79
+ "keywords": [
80
+ "ai",
81
+ "framework",
82
+ "coding-assistant",
83
+ "claude-code",
84
+ "codex",
85
+ "aperant"
86
+ ],
87
+ "license": "AGPL-3.0-only",
88
+ "homepage": "https://github.com/Mikalsen-AI/aperant-cloud-desktop/tree/main/packages/framework#readme",
89
+ "bugs": {
90
+ "url": "https://github.com/Mikalsen-AI/aperant-cloud-desktop/issues"
91
+ },
92
+ "repository": {
93
+ "type": "git",
94
+ "url": "https://github.com/Mikalsen-AI/aperant-cloud-desktop.git",
95
+ "directory": "packages/framework"
96
+ },
97
+ "engines": {
98
+ "node": ">=18.0.0"
99
+ },
100
+ "publishConfig": {
101
+ "access": "public"
102
+ },
103
+ "dependencies": {
104
+ "@clack/prompts": "^1.2.0",
105
+ "proper-lockfile": "^4.1.2",
106
+ "yaml": "^2.8.3",
107
+ "zod": "^4.3.6"
108
+ },
109
+ "optionalDependencies": {
110
+ "@babel/parser": "^7.29.2",
111
+ "@babel/traverse": "^7.29.0",
112
+ "@google/genai": "^1.50.1",
113
+ "css-tree": "^3.2.1",
114
+ "playwright": "^1.59.1"
115
+ },
116
+ "devDependencies": {
117
+ "@vitest/coverage-v8": "^4.1.5"
118
+ },
119
+ "scripts": {
120
+ "build": "tsc && node scripts/extract-personas-schema.mjs",
121
+ "dev": "tsc --watch --preserveWatchOutput",
122
+ "typecheck": "tsc --noEmit",
123
+ "lint": "biome check . --diagnostic-level=error",
124
+ "lint:fix": "biome check --write . --diagnostic-level=error",
125
+ "clean": "rm -rf dist",
126
+ "test": "vitest run",
127
+ "c28:cutover": "node scripts/c28-cutover.mjs",
128
+ "build-plugin": "node scripts/build-plugin.mjs",
129
+ "publish-plugin": "node scripts/publish-plugin.mjs",
130
+ "publish:verdaccio": "node scripts/publish-verdaccio.mjs",
131
+ "release:verdaccio": "pnpm build && pnpm publish:verdaccio",
132
+ "ship:verdaccio": "npm version patch --no-git-tag-version && pnpm build && pnpm publish:verdaccio"
133
+ }
134
+ }
@@ -88,6 +88,31 @@ Do NOT wait on the narrator. Its status lands in `.aperant/digests/.last-run.jso
88
88
 
89
89
  After step 2.5 spawns narrators for the just-closed candidates, the skill ALSO drains `state.pending_narration[]` (entries the passive sweep parked there) — see §4.
90
90
 
91
+ ## 2.6 Drop user-facing release-note fragment (persona-aware release-notes opt-in)
92
+
93
+ This step is additive bookkeeping — it must NEVER block the close path or surface a destructive failure. Runs ONLY when `.aperant/config.json` has `changelog.release_notes.enabled === true`. When the toggle is `false` (the default for projects that don't publish user-facing notes), skip the section entirely.
94
+
95
+ For each entry in the close-merged envelope's `closed[]`:
96
+
97
+ 1. Read the merged PR body via `gh pr view <pr_number> --json body`. Look for a single `Release note:` line (case-insensitive). The line is what `/apt:ship` Section 2.6 auto-drafted (or a manual edit by the author).
98
+ 2. Look for a persona tag in the PR body or in commits. Reasonable sources: the `Release note:` line itself if the author included one (e.g. `Release note: [Maya] …`), a `release-note-persona:` trailer in the most recent commit, or the persona name embedded in the auto-drafted line by §2.6.2 of /apt:ship.
99
+ 3. Invoke the deterministic drafter:
100
+
101
+ ```bash
102
+ node packages/framework/bin/apt-tools.mjs release-notes draft . \
103
+ --pr <pr_number> \
104
+ --note "<release-note-line-text>" \
105
+ [--persona <persona-name>]
106
+ ```
107
+
108
+ This call is synchronous, sub-100ms, and idempotent per subtask 2 — re-running close-task does not duplicate the fragment.
109
+
110
+ 4. On non-zero exit from `release-notes draft`, append a `pending_release_note[]` row to `.aperant/state.json` so a manual re-run can recover. Do NOT block close-merged on this — it is additive bookkeeping, not core lifecycle. Use `apt-tools state-update . --append-pending-release-note '{"task_id": "<id>", "pr_number": <n>, "reason": "<error>"}'` if such a helper exists, otherwise call `apt-tools state-artifacts . --note '<msg>'` or write a one-line warning to stderr.
111
+
112
+ Skip the section quietly when:
113
+ - The PR body has no `Release note:` line AND no auto-draft signal (intentional infra-only PR, or the `no-notes` label was applied at ship time).
114
+ - The CLI returns `{status: 'ok', written: false, reason: 'no-change'}` — already idempotent-handled by subtask 2's contract; treat as success.
115
+
91
116
  ## 3. Summary
92
117
 
93
118
  close-merged emits an envelope with:
@@ -98,6 +123,8 @@ close-merged emits an envelope with:
98
123
 
99
124
  Report the summary verbatim. If anything landed in `skipped[]` with a recoverable reason (`offline`, `rate_limited`), suggest the user re-run once the network clears.
100
125
 
126
+ **Local cleanup recap (best-effort).** On confirmed merge, `task close-merged` runs `computeWorktreeCleanup` against the primary repo (`packages/framework/src/cli/task/worktree-cleanup.mjs:140-230`): `git fetch origin <base>` (failure → recorded as `fetch_failed` warning, cleanup proceeds), `git checkout <base>` (failure → returns `action: 'checkout_failed'` and preserves BOTH the worktree and the task branch — nothing is removed), `git merge --ff-only origin/<base>` (failure → `ff_only_failed` warning, GC still runs), `git worktree remove <wtPath>`, and `git branch -d <taskBranch>` (failure on squash/rebase merges → `branch_delete_failed` warning, `branch_deleted: false` in the envelope; user can `git branch -D` manually). This is why `/apt:watch-ci` auto-merge MUST NOT pass `gh pr merge --delete-branch` — that flag would try to do the same work from inside the worktree where `git switch <base>` cannot succeed (see FRAMEWORK-BUG-019).
127
+
101
128
  ## 4. Narrate-only mode
102
129
 
103
130
  When invoked with `--narrate-only`, skip §1 (no `gh pr view` polling) and §2 (no destructive close path). The deterministic close already ran via the passive post-merge sweep — your job is to drain the `state.pending_narration[]` ledger the sweep parked.
@@ -108,10 +135,13 @@ Flags:
108
135
 
109
136
  Per ledger entry `{ task_id, scope, phase_id, closed_at, pr_number }`:
110
137
 
138
+ 0. **Enumerate the ledger first.** Run `apt-tools task close-merged . --narrate-only [--task <id>]`. The envelope's `closed[]` is the read-only list of pending narration rows (each carries `narration_pending: true`); state.json is not mutated. When `closed[]` is empty, exit early with `{status: 'ok', narrated: [], skipped: [], remaining: 0}` — there is nothing to drain.
111
139
  1. Run `apt-tools features-audit . --apply-stubs --task <task_id>` so the feature registry catches whatever the closed task introduced. If the subprocess exits non-zero, skip the entry with `reason: 'features-audit-failed'` — never block narration on registry update.
112
140
  2. When `phase_id` is non-null, spawn `apt-team-docs-narrator` (background, fire-and-forget) using the same shape as §2.5: `subagent_type: apt-team-docs-narrator`, `run_in_background: true`, `description: Narrate shipped phase {phase_id}`, `prompt: Run in phase-ship mode with --phase {phase_id} --scope {scope}.`. When `phase_id` is null, skip the spawn with `reason: 'no-phase-id'` (quick tasks have no phase to narrate).
113
141
  3. On successful spawn (or successful no-phase skip), drain the row via `apt-tools task narration-drain . --task <task_id>` which acquires `withFileLock(state.json)` and removes the matching `pending_narration[]` entry. On spawn failure, leave the row in place with `reason: 'narrator-spawn-failed'` so the user can re-invoke.
114
142
 
143
+ **At-least-once retry semantics (FRAMEWORK-BUG-020).** Step 0 is read-only — it does NOT remove rows. Steps 1–3 are the existing per-row drain contract: a row stays in `pending_narration[]` until step 3 calls `narration-drain` successfully. If the orchestrator crashes between step 2 (narrator spawn) and step 3 (drain), the row remains parked and a retry will spawn narration again. Re-spawning is safe because the narrator is **anchor-replace idempotent** by design (see `agents/apt-team-docs-narrator.md` §6 / §2 and the "Idempotent operations only" rule in §notes) — it uses HTML-comment anchors to bound the replaced region in ROADMAP.md, so a second run replaces the same span rather than appending.
144
+
115
145
  Emit a summary envelope:
116
146
 
117
147
  ```json
@@ -174,8 +174,38 @@ order: (a) consent gate, (b) renderer dispatch, (c) failure handling.
174
174
 
175
175
  ### 5a. Consent gate
176
176
 
177
- Read `.aperant/config.json` and look at `diagram.mcpConsent`. The
178
- shape is:
177
+ Per FRAMEWORK-AUDIT-001, `mcpConsent` is per-user state and lives in
178
+ `.aperant/config.local.json` (gitignored), NOT the shared
179
+ `.aperant/config.json`. The `diagram.mode` field stays in `config.json` as
180
+ team policy. Read the MERGED config (config.json deep-merged with
181
+ config.local.json — local wins on collision) and look at
182
+ `diagram.mcpConsent` via the same in-skill `node -e` pattern Step 2 uses:
183
+
184
+ ```bash
185
+ node -e '
186
+ const fs = require("fs");
187
+ const path = require("path");
188
+ const aperantDir = path.join(process.cwd(), ".aperant");
189
+ function readJson(p) {
190
+ try { return JSON.parse(fs.readFileSync(p, "utf-8")); } catch { return null; }
191
+ }
192
+ const shared = readJson(path.join(aperantDir, "config.json")) || {};
193
+ const local = readJson(path.join(aperantDir, "config.local.json")) || {};
194
+ // Deep-merge — local wins on collision; matches loadMergedProjectConfig semantics.
195
+ function merge(a, b) {
196
+ if (a === null || typeof a !== "object" || Array.isArray(a)) return b;
197
+ if (b === null || typeof b !== "object" || Array.isArray(b)) return b;
198
+ const out = { ...a };
199
+ for (const k of Object.keys(b)) out[k] = (k in a) ? merge(a[k], b[k]) : b[k];
200
+ return out;
201
+ }
202
+ const cfg = merge(shared, local);
203
+ const consent = cfg && cfg.diagram ? cfg.diagram.mcpConsent : null;
204
+ process.stdout.write(JSON.stringify(consent || null));
205
+ '
206
+ ```
207
+
208
+ The shape is:
179
209
 
180
210
  ```json
181
211
  { "host": "<host string>", "decision": "granted" | "denied", "grantedAt": "<ISO 8601 timestamp>" }
@@ -196,7 +226,7 @@ Decide whether to prompt:
196
226
  - If `diagram.mcpConsent.host !== <configured MCP host>` → prompt.
197
227
  - If `diagram.mcpConsent.host === <configured MCP host>` AND `diagram.mcpConsent.decision === "denied"` → hard-error immediately with the exact denied text (no re-prompt):
198
228
  ```
199
- diagram MCP consent denied; re-run with `--mode=file` or change diagram.mcpConsent in .aperant/config.json
229
+ diagram MCP consent denied; re-run with `--mode=file` or change diagram.mcpConsent in .aperant/config.local.json
200
230
  ```
201
231
  - Otherwise (host matches AND decision is `"granted"`) → the existing consent applies; proceed to 5b.
202
232
 
@@ -206,23 +236,25 @@ When prompting, print a single line naming the host and ask `granted / denied`:
206
236
  apt:diagram is about to send diagram data to the Excalidraw MCP server at <host>. Type `granted` to allow, `denied` to refuse.
207
237
  ```
208
238
 
209
- Read one line of input. Persist the decision back to
210
- `.aperant/config.json` with a read-mutate-write JSON pattern:
239
+ Read one line of input. Persist the decision to
240
+ `.aperant/config.local.json` (NOT `config.json` `mcpConsent` is per-user
241
+ state per FRAMEWORK-AUDIT-001; the local file is gitignored). The
242
+ read-mutate-write JSON pattern targets the local file only:
211
243
 
212
244
  ```bash
213
245
  node -e '
214
246
  const fs = require("fs");
215
247
  const path = require("path");
216
- const cfgPath = path.join(process.cwd(), ".aperant/config.json");
248
+ const cfgPath = path.join(process.cwd(), ".aperant/config.local.json");
217
249
  const decision = process.env.APT_DIAGRAM_DECISION; // "granted" | "denied"
218
250
  if (decision !== "granted" && decision !== "denied") {
219
251
  process.stderr.write("invalid decision\n");
220
252
  process.exit(1);
221
253
  }
222
254
  const host = process.env.APT_DIAGRAM_HOST;
223
- let cfg = { diagram: { mode: "mcp", mcpConsent: null } };
255
+ let cfg = { diagram: { mcpConsent: null } };
224
256
  try { cfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8")); } catch {}
225
- cfg.diagram = cfg.diagram || { mode: "mcp", mcpConsent: null };
257
+ cfg.diagram = cfg.diagram || { mcpConsent: null };
226
258
  cfg.diagram.mcpConsent = {
227
259
  host,
228
260
  decision,
@@ -232,11 +264,15 @@ node -e '
232
264
  '
233
265
  ```
234
266
 
267
+ Note: the write target above is `config.local.json` (gitignored, per-user).
268
+ The shared `config.json` is not touched — `diagram.mode` lives there and
269
+ stays team-shared policy.
270
+
235
271
  If the decision is `"denied"`, exit with EXACTLY this text (verbatim,
236
272
  no rewording, no extra punctuation):
237
273
 
238
274
  ```
239
- diagram MCP consent denied; re-run with `--mode=file` or change diagram.mcpConsent in .aperant/config.json
275
+ diagram MCP consent denied; re-run with `--mode=file` or change diagram.mcpConsent in .aperant/config.local.json
240
276
  ```
241
277
 
242
278
  If the decision is `"granted"`, proceed to 5b.
@@ -0,0 +1,193 @@
1
+ ---
2
+ name: apt:release-notes
3
+ description: "Persona-aware user-facing release notes — draft, compile, publish"
4
+ apt-skill-version: {{APT_VERSION}}
5
+ stage: ops
6
+ intent: capture
7
+ when_to_use: "The user wants to draft, compile, or publish persona-aware user-facing release notes for a project that opted in via /apt:setup."
8
+ user_invocable: true
9
+ internal: false
10
+ spawns_agent: false
11
+ agent_name: null
12
+ task_context: none
13
+ default_track: DEEP
14
+ default_execution_mode: auto
15
+ execution_modes:
16
+ - auto
17
+ - step
18
+ allowed-tools: "Bash, Read, Write, Edit, Grep, Glob"
19
+ argument-hint: "apt:release-notes [--draft --pr <n> | --compile <tag> | --publish <tag>]"
20
+ gates: []
21
+ ---
22
+
23
+ <objective>
24
+ Produce user-facing release notes for a project that opted in via
25
+ `/apt:setup` → Batch 9 → "Publish user-facing release notes? Yes". This
26
+ skill is separate from `/apt:docs changelog` — the dev-flavored
27
+ `CHANGELOG.md` surface stays its responsibility; this skill owns the
28
+ persona-voice-tuned narrative that ships to end users.
29
+
30
+ Three modes:
31
+
32
+ - `--draft --pr <n>` — write a per-PR fragment file (towncrier-style).
33
+ Usually called automatically by `/apt:close-task` on merge; rarely
34
+ invoked by hand.
35
+ - `--compile <tag>` — at release time, read every accumulated fragment,
36
+ filter by the configured persona tier, write a single per-tag
37
+ narrative file, and clear the unreleased dir.
38
+ - `--publish <tag>` — shell `gh release create/edit` to push the
39
+ compiled body to the GitHub Release.
40
+
41
+ Rules of the road (locked):
42
+
43
+ - **Per-project opt-in (spec ID-7).** Presence of `.aperant/personas.json`
44
+ does NOT auto-enable this skill. The toggle is
45
+ `config.changelog.release_notes.enabled` — opt-in is explicit via
46
+ `/apt:setup` Batch 9.
47
+ - **No LLM calls in the CLI (spec ID-3).** The deterministic verbs
48
+ (`apt-tools release-notes draft|compile|publish`) handle file I/O only.
49
+ Persona-voice tuning is authored by you — the host LLM — using the
50
+ `appendices/persona-voice.md` loader.
51
+ - **Triage bucket is non-negotiable (spec ID-6).** Fragments without a
52
+ recognized persona tag MUST surface in a `## Needs Triage` section at
53
+ compile time. Never silent-drop. This is the human curator's safety
54
+ net against trust collapse over time.
55
+ - **Fast Path Guarantee (CLAUDE.md).** This skill is DEEP-track-only
56
+ surface area. `/apt:quick` never enters this skill; the
57
+ `apt-release-notes-quick-exempt.test.ts` regression test pins the
58
+ exemption.
59
+ </objective>
60
+
61
+ <your_environment>
62
+ - **Working directory:** The project root
63
+ - **apt-tools path:** `node packages/framework/bin/apt-tools.mjs` or the locally installed `apt-tools` binary.
64
+ - **gh CLI dependency:** required only for `--publish`. The `publish` verb returns a graceful `{status: 'skipped', reason: 'gh-not-found'}` envelope when `gh` is unavailable — the skill never crashes the host on a missing binary.
65
+ - **Output:** `docs/releases/_unreleased/PR-<n>.md` (fragments) + `docs/releases/<tag>.md` (compiled) + GitHub Release body (when published).
66
+ </your_environment>
67
+
68
+ <state_files>
69
+ ## State Files
70
+
71
+ **Reads:**
72
+ - `.aperant/config.json` — `changelog.release_notes.{enabled, output_dir, persona_filter, require_pr_field}` and the stage blocks.
73
+ - `.aperant/personas.json` — persona roster + tier; used by `compile` to filter fragments.
74
+ - `docs/releases/_unreleased/PR-*.md` — fragments accumulated since the last tag.
75
+ - `PROJECT.md` (when present) — Core Value section feeds the hero narrative the host LLM authors at compile time.
76
+
77
+ **Writes:**
78
+ - `docs/releases/_unreleased/PR-<n>.md` — towncrier-style fragments (draft mode).
79
+ - `docs/releases/<tag>.md` — per-tag compiled narrative (compile mode).
80
+ - GitHub Release body via `gh release create/edit` (publish mode).
81
+ </state_files>
82
+
83
+ <process>
84
+
85
+ ## 0. Hard-Gate + Opt-in Check
86
+
87
+ No hard gates (`gates: []`). Before running ANY mode, confirm the project opted in:
88
+
89
+ ```bash
90
+ rn_enabled=$(jq -r '.changelog.release_notes.enabled // false' .aperant/config.json)
91
+ if [ "$rn_enabled" != "true" ]; then
92
+ echo "[apt:release-notes] This project has not opted into user-facing release notes."
93
+ echo " Run /apt:setup → 'Changelog & Release Notes' to enable."
94
+ exit 0
95
+ fi
96
+ ```
97
+
98
+ When opted out, exit cleanly — never crash, never write any file.
99
+
100
+ ## 1. Parse Flags
101
+
102
+ The skill's three modes are mutually exclusive. Parse `$ARGUMENTS`:
103
+
104
+ - `--draft --pr <n> --note "<text>" [--persona <name>] [--commits <sha,sha,...>] [--date <YYYY-MM-DD>]` → Draft mode (§2).
105
+ - `--compile <tag>` → Compile mode (§3).
106
+ - `--publish <tag> [--dry-run]` → Publish mode (§4).
107
+
108
+ If no mode flag is passed, display a short usage message and exit.
109
+
110
+ ## 2. Draft Mode
111
+
112
+ Used to write a per-PR fragment. Usually this is called automatically by `/apt:close-task` on merge (see `apt-close-task/SKILL.md` §2.6), but it can be invoked by hand for backfills or for manual fragments.
113
+
114
+ Hand the inputs to the deterministic CLI:
115
+
116
+ ```bash
117
+ node packages/framework/bin/apt-tools.mjs release-notes draft . \
118
+ --pr {pr_number} \
119
+ --note "{one-sentence user-impact note}" \
120
+ [--persona {persona-name}] \
121
+ [--date {ISO-date}]
122
+ ```
123
+
124
+ The CLI is idempotent — re-running with identical inputs returns `{status: 'ok', written: false, reason: 'no-change'}`. Trust the envelope; do NOT manually inspect the file before running.
125
+
126
+ When you (the host LLM) compose the note, consult `appendices/persona-voice.md` for the persona-voice posture loader. The fragment goes into the user-facing pipeline; voice-tune accordingly.
127
+
128
+ ## 3. Compile Mode
129
+
130
+ Used at release time to fold every accumulated fragment into a single per-tag narrative. The CLI does the deterministic work — your job is the narrative pass on the compiled body if the user wants polish beyond the structured per-PR list.
131
+
132
+ ```bash
133
+ node packages/framework/bin/apt-tools.mjs release-notes compile . --tag {tag}
134
+ ```
135
+
136
+ The envelope reports:
137
+
138
+ ```json
139
+ {
140
+ "status": "ok",
141
+ "command": "release-notes-compile",
142
+ "tag": "{tag}",
143
+ "path": "docs/releases/{tag}.md",
144
+ "fragments_total": N,
145
+ "fragments_in_body": M,
146
+ "fragments_in_triage": K,
147
+ "fragments_filtered": F,
148
+ "cleared_unreleased": true
149
+ }
150
+ ```
151
+
152
+ If `fragments_in_triage > 0`, ALERT the user before proceeding to publish — those fragments need persona curation. Offer two options:
153
+
154
+ 1. Edit the compiled file directly to move triage entries into the right tier section (and re-tag the persona in the original PR if appropriate).
155
+ 2. Proceed to publish with the triage section intact — sometimes the right call when the unmapped change is genuinely cross-cutting.
156
+
157
+ If `fragments_total === 0`, gently note that there's nothing to compile yet (likely the user hasn't merged any PRs since the last release).
158
+
159
+ ## 4. Publish Mode
160
+
161
+ Push the compiled body to the GitHub Release:
162
+
163
+ ```bash
164
+ node packages/framework/bin/apt-tools.mjs release-notes publish . --tag {tag}
165
+ ```
166
+
167
+ The CLI handles both `gh release create` (new release) and `gh release edit` (existing release). When `gh` is not on PATH, the envelope returns `{status: 'skipped', reason: 'gh-not-found'}` — surface the message to the user and suggest installing the GitHub CLI.
168
+
169
+ For local-only verification, pass `--dry-run` to skip the network call and just confirm the body file exists.
170
+
171
+ ## 5. Re-invocation
172
+
173
+ Each mode is safe to re-run:
174
+
175
+ - `draft` — idempotent per spec AC2.
176
+ - `compile` — overwrites `<tag>.md` and re-clears `_unreleased/`. Running compile twice in a row with no new fragments produces an empty body — careful.
177
+ - `publish` — `gh release edit` is the second call's branch, so the GitHub Release body is replaced cleanly.
178
+
179
+ </process>
180
+
181
+ <appendices>
182
+ - `appendices/persona-voice.md` — Aperant-authored posture loader for voice-tuning the note text per persona archetype.
183
+ </appendices>
184
+
185
+ <integration>
186
+ | Skill | How it interacts |
187
+ |-------|------------------|
188
+ | `/apt:setup` Batch 9 | Writes the opt-in toggle + audience + persona-filter that this skill reads. |
189
+ | `/apt:ship` §2.6 | Auto-drafts the `Release note: …` line in the PR body BEFORE this skill runs; ship blocks if `require_pr_field=true` AND no signal. |
190
+ | `/apt:close-task` §2.6 | Invokes `apt-tools release-notes draft` on merge — drops the per-PR fragment automatically. |
191
+ | `/apt:docs changelog` | The dev-flavored sibling. Stays its own surface; this skill does NOT touch `CHANGELOG.md`. |
192
+ | `/apt:personas` | Produces the `.aperant/personas.json` roster that compile mode filters against. |
193
+ </integration>