@aperant/framework 0.6.3 → 0.6.5

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 (244) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +64 -10
  3. package/agents/apt-improver.md +99 -0
  4. package/agents/apt-planner.md +115 -10
  5. package/dist/__test-helpers/run-cmd.d.mts +4 -2
  6. package/dist/__test-helpers/run-cmd.d.mts.map +1 -1
  7. package/dist/__test-helpers/run-cmd.mjs +56 -15
  8. package/dist/__test-helpers/run-cmd.mjs.map +1 -1
  9. package/dist/cli/artifacts/classification.d.mts.map +1 -1
  10. package/dist/cli/artifacts/classification.mjs +10 -0
  11. package/dist/cli/artifacts/classification.mjs.map +1 -1
  12. package/dist/cli/ci-watch/lock.d.mts +53 -0
  13. package/dist/cli/ci-watch/lock.d.mts.map +1 -0
  14. package/dist/cli/ci-watch/lock.mjs +172 -0
  15. package/dist/cli/ci-watch/lock.mjs.map +1 -0
  16. package/dist/cli/ci-watch/state.d.mts +36 -0
  17. package/dist/cli/ci-watch/state.d.mts.map +1 -0
  18. package/dist/cli/ci-watch/state.mjs +103 -0
  19. package/dist/cli/ci-watch/state.mjs.map +1 -0
  20. package/dist/cli/ci-watch/stop-matrix.d.mts +58 -0
  21. package/dist/cli/ci-watch/stop-matrix.d.mts.map +1 -0
  22. package/dist/cli/ci-watch/stop-matrix.mjs +164 -0
  23. package/dist/cli/ci-watch/stop-matrix.mjs.map +1 -0
  24. package/dist/cli/cli-wrappers/ci-watch.d.mts +2 -0
  25. package/dist/cli/cli-wrappers/ci-watch.d.mts.map +1 -0
  26. package/dist/cli/cli-wrappers/ci-watch.mjs +9 -0
  27. package/dist/cli/cli-wrappers/ci-watch.mjs.map +1 -0
  28. package/dist/cli/commands/adr.d.mts +5 -0
  29. package/dist/cli/commands/adr.d.mts.map +1 -0
  30. package/dist/cli/commands/adr.mjs +228 -0
  31. package/dist/cli/commands/adr.mjs.map +1 -0
  32. package/dist/cli/commands/ci-watch.d.mts +7 -0
  33. package/dist/cli/commands/ci-watch.d.mts.map +1 -0
  34. package/dist/cli/commands/ci-watch.mjs +465 -0
  35. package/dist/cli/commands/ci-watch.mjs.map +1 -0
  36. package/dist/cli/commands/context.d.mts +7 -0
  37. package/dist/cli/commands/context.d.mts.map +1 -0
  38. package/dist/cli/commands/context.mjs +224 -0
  39. package/dist/cli/commands/context.mjs.map +1 -0
  40. package/dist/cli/commands/event.d.mts.map +1 -1
  41. package/dist/cli/commands/event.mjs +59 -24
  42. package/dist/cli/commands/event.mjs.map +1 -1
  43. package/dist/cli/commands/host-detect.d.mts +1 -1
  44. package/dist/cli/commands/host-detect.d.mts.map +1 -1
  45. package/dist/cli/commands/host-detect.mjs +30 -3
  46. package/dist/cli/commands/host-detect.mjs.map +1 -1
  47. package/dist/cli/commands/init.d.mts.map +1 -1
  48. package/dist/cli/commands/init.mjs +73 -5
  49. package/dist/cli/commands/init.mjs.map +1 -1
  50. package/dist/cli/commands/modes.d.mts +13 -0
  51. package/dist/cli/commands/modes.d.mts.map +1 -0
  52. package/dist/cli/commands/modes.mjs +220 -0
  53. package/dist/cli/commands/modes.mjs.map +1 -0
  54. package/dist/cli/commands/pr-review-audit-fixer.d.mts +41 -2
  55. package/dist/cli/commands/pr-review-audit-fixer.d.mts.map +1 -1
  56. package/dist/cli/commands/pr-review-audit-fixer.mjs +91 -14
  57. package/dist/cli/commands/pr-review-audit-fixer.mjs.map +1 -1
  58. package/dist/cli/commands/route.d.mts.map +1 -1
  59. package/dist/cli/commands/route.mjs +11 -2
  60. package/dist/cli/commands/route.mjs.map +1 -1
  61. package/dist/cli/commands/task.d.mts.map +1 -1
  62. package/dist/cli/commands/task.mjs +136 -6
  63. package/dist/cli/commands/task.mjs.map +1 -1
  64. package/dist/cli/commands/tokens.d.mts.map +1 -1
  65. package/dist/cli/commands/tokens.mjs +150 -6
  66. package/dist/cli/commands/tokens.mjs.map +1 -1
  67. package/dist/cli/commands/triage.d.mts +8 -0
  68. package/dist/cli/commands/triage.d.mts.map +1 -0
  69. package/dist/cli/commands/triage.mjs +259 -0
  70. package/dist/cli/commands/triage.mjs.map +1 -0
  71. package/dist/cli/config/post-merge-sweep.d.mts +18 -0
  72. package/dist/cli/config/post-merge-sweep.d.mts.map +1 -1
  73. package/dist/cli/config/post-merge-sweep.mjs +48 -3
  74. package/dist/cli/config/post-merge-sweep.mjs.map +1 -1
  75. package/dist/cli/consistency/registry.d.mts +11 -0
  76. package/dist/cli/consistency/registry.d.mts.map +1 -1
  77. package/dist/cli/consistency/registry.mjs +13 -0
  78. package/dist/cli/consistency/registry.mjs.map +1 -1
  79. package/dist/cli/coordination/agent-identity.d.mts +9 -0
  80. package/dist/cli/coordination/agent-identity.d.mts.map +1 -1
  81. package/dist/cli/coordination/agent-identity.mjs +11 -0
  82. package/dist/cli/coordination/agent-identity.mjs.map +1 -1
  83. package/dist/cli/coordination/event-log.d.mts +27 -0
  84. package/dist/cli/coordination/event-log.d.mts.map +1 -1
  85. package/dist/cli/coordination/event-log.mjs +72 -2
  86. package/dist/cli/coordination/event-log.mjs.map +1 -1
  87. package/dist/cli/design/frontmatter-schema.d.mts +3 -3
  88. package/dist/cli/design/frontmatter-schema.d.mts.map +1 -1
  89. package/dist/cli/design/frontmatter-schema.mjs +3 -1
  90. package/dist/cli/design/frontmatter-schema.mjs.map +1 -1
  91. package/dist/cli/dispatch.d.mts.map +1 -1
  92. package/dist/cli/dispatch.mjs +14 -1
  93. package/dist/cli/dispatch.mjs.map +1 -1
  94. package/dist/cli/gate/registry.d.mts +11 -0
  95. package/dist/cli/gate/registry.d.mts.map +1 -1
  96. package/dist/cli/gate/registry.mjs +13 -0
  97. package/dist/cli/gate/registry.mjs.map +1 -1
  98. package/dist/cli/help.d.mts.map +1 -1
  99. package/dist/cli/help.mjs +1 -0
  100. package/dist/cli/help.mjs.map +1 -1
  101. package/dist/cli/host/detect.d.mts +1 -0
  102. package/dist/cli/host/detect.d.mts.map +1 -1
  103. package/dist/cli/host/detect.mjs +5 -0
  104. package/dist/cli/host/detect.mjs.map +1 -1
  105. package/dist/cli/route/envelope.d.mts +68 -4
  106. package/dist/cli/route/envelope.d.mts.map +1 -1
  107. package/dist/cli/route/envelope.mjs +140 -103
  108. package/dist/cli/route/envelope.mjs.map +1 -1
  109. package/dist/cli/route/skill-discover.d.mts +11 -0
  110. package/dist/cli/route/skill-discover.d.mts.map +1 -1
  111. package/dist/cli/route/skill-discover.mjs +46 -1
  112. package/dist/cli/route/skill-discover.mjs.map +1 -1
  113. package/dist/cli/skill-author/contract.d.mts +20 -0
  114. package/dist/cli/skill-author/contract.d.mts.map +1 -1
  115. package/dist/cli/skill-author/contract.mjs +27 -0
  116. package/dist/cli/skill-author/contract.mjs.map +1 -1
  117. package/dist/cli/skill-author/skill-template.d.mts.map +1 -1
  118. package/dist/cli/skill-author/skill-template.mjs +4 -3
  119. package/dist/cli/skill-author/skill-template.mjs.map +1 -1
  120. package/dist/cli/task/worktree-cleanup.d.mts +9 -1
  121. package/dist/cli/task/worktree-cleanup.d.mts.map +1 -1
  122. package/dist/cli/task/worktree-cleanup.mjs +190 -9
  123. package/dist/cli/task/worktree-cleanup.mjs.map +1 -1
  124. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  125. package/dist/plugin/agents/apt-planner.md +1 -1
  126. package/dist/plugin/skills/apt/SKILL.md +111 -5
  127. package/dist/plugin/skills/apt-author-skill/SKILL.md +11 -0
  128. package/dist/plugin/skills/apt-bootstrap/SKILL.md +1 -0
  129. package/dist/plugin/skills/apt-classify/SKILL.md +1 -0
  130. package/dist/plugin/skills/apt-close-task/SKILL.md +1 -0
  131. package/dist/plugin/skills/apt-create-docs/SKILL.md +1 -0
  132. package/dist/plugin/skills/apt-debug/SKILL.md +2 -0
  133. package/dist/plugin/skills/apt-design/SKILL.md +2 -0
  134. package/dist/plugin/skills/apt-discuss/SKILL.md +2 -0
  135. package/dist/plugin/skills/apt-docs/SKILL.md +2 -0
  136. package/dist/plugin/skills/apt-execute/SKILL.md +1 -0
  137. package/dist/plugin/skills/apt-mockup/SKILL.md +2 -0
  138. package/dist/plugin/skills/apt-pause/SKILL.md +1 -0
  139. package/dist/plugin/skills/apt-personas/SKILL.md +1 -0
  140. package/dist/plugin/skills/apt-plan/SKILL.md +2 -0
  141. package/dist/plugin/skills/apt-pr-review/SKILL.md +1 -0
  142. package/dist/plugin/skills/apt-quick/SKILL.md +2 -0
  143. package/dist/plugin/skills/apt-resume/SKILL.md +1 -0
  144. package/dist/plugin/skills/apt-review/SKILL.md +1 -0
  145. package/dist/plugin/skills/apt-roadmap/SKILL.md +1 -0
  146. package/dist/plugin/skills/apt-roundtable/SKILL.md +2 -0
  147. package/dist/plugin/skills/apt-run/SKILL.md +1 -0
  148. package/dist/plugin/skills/apt-scan/SKILL.md +1 -0
  149. package/dist/plugin/skills/apt-setup/SKILL.md +1 -0
  150. package/dist/plugin/skills/apt-ship/SKILL.md +6 -5
  151. package/dist/plugin/skills/apt-stress-test/SKILL.md +1 -0
  152. package/dist/plugin/skills/apt-terminal/SKILL.md +1 -0
  153. package/dist/plugin/skills/apt-update/SKILL.md +5 -0
  154. package/dist/plugin/skills/apt-verify/SKILL.md +1 -0
  155. package/dist/plugin/skills/apt-verify-proof/SKILL.md +1 -0
  156. package/dist/types/config.d.ts +85 -0
  157. package/dist/types/config.d.ts.map +1 -1
  158. package/package.json +125 -122
  159. package/prompts/coder.md +2 -0
  160. package/prompts/planner.md +12 -0
  161. package/prompts/spec_writer.md +9 -0
  162. package/skills/apt/SKILL.md +112 -5
  163. package/skills/apt-author-skill/SKILL.md +11 -0
  164. package/skills/apt-bootstrap/SKILL.md +1 -0
  165. package/skills/apt-classify/SKILL.md +1 -0
  166. package/skills/apt-close-task/SKILL.md +33 -1
  167. package/skills/apt-create-docs/SKILL.md +1 -0
  168. package/skills/apt-debug/SKILL.md +41 -6
  169. package/skills/apt-debug/appendices/diagnose-discipline.md +119 -0
  170. package/skills/apt-design/SKILL.md +2 -0
  171. package/skills/apt-diagram/SKILL.md +342 -0
  172. package/skills/apt-diagram/appendices/design-discipline.md +97 -0
  173. package/skills/apt-discuss/SKILL.md +27 -0
  174. package/skills/apt-discuss/appendices/grill-discipline.md +104 -0
  175. package/skills/apt-discuss/appendices/zoom-out-helper.md +79 -0
  176. package/skills/apt-docs/SKILL.md +2 -0
  177. package/skills/apt-execute/SKILL.md +49 -5
  178. package/skills/apt-execute/appendices/tdd-mode.md +107 -0
  179. package/skills/apt-improve/DEEPENING.md +84 -0
  180. package/skills/apt-improve/INTERFACE-DESIGN.md +97 -0
  181. package/skills/apt-improve/LANGUAGE.md +104 -0
  182. package/skills/apt-improve/SKILL.md +141 -0
  183. package/skills/apt-mockup/SKILL.md +2 -0
  184. package/skills/apt-pause/SKILL.md +1 -0
  185. package/skills/apt-personas/SKILL.md +1 -0
  186. package/skills/apt-plan/SKILL.md +149 -4
  187. package/skills/apt-planner.md +42 -1
  188. package/skills/apt-pr-review/SKILL.md +47 -16
  189. package/skills/apt-prototype/LOGIC.md +109 -0
  190. package/skills/apt-prototype/SKILL.md +143 -0
  191. package/skills/apt-prototype/UI.md +90 -0
  192. package/skills/apt-quick/SKILL.md +32 -0
  193. package/skills/apt-resume/SKILL.md +1 -0
  194. package/skills/apt-review/SKILL.md +3 -0
  195. package/skills/apt-roadmap/SKILL.md +1 -0
  196. package/skills/apt-roundtable/SKILL.md +2 -0
  197. package/skills/apt-run/SKILL.md +33 -4
  198. package/skills/apt-scan/SKILL.md +1 -0
  199. package/skills/apt-setup/SKILL.md +129 -2
  200. package/skills/apt-ship/SKILL.md +52 -4
  201. package/skills/apt-stress-test/SKILL.md +1 -0
  202. package/skills/apt-terminal/SKILL.md +1 -0
  203. package/skills/apt-triage/AGENT-BRIEF.md +84 -0
  204. package/skills/apt-triage/OUT-OF-SCOPE.md +75 -0
  205. package/skills/apt-triage/SKILL.md +169 -0
  206. package/skills/apt-update/SKILL.md +3 -0
  207. package/skills/apt-verify/SKILL.md +4 -0
  208. package/skills/apt-verify-proof/SKILL.md +4 -0
  209. package/skills/apt-watch-ci/SKILL.md +163 -0
  210. package/skills/apt-zoom-out/SKILL.md +130 -0
  211. package/src/cli/artifacts/classification.mjs +10 -0
  212. package/src/cli/ci-watch/lock.mjs +178 -0
  213. package/src/cli/ci-watch/state.mjs +103 -0
  214. package/src/cli/ci-watch/stop-matrix.mjs +181 -0
  215. package/src/cli/cli-wrappers/ci-watch.mjs +9 -0
  216. package/src/cli/commands/adr.mjs +243 -0
  217. package/src/cli/commands/ci-watch.mjs +503 -0
  218. package/src/cli/commands/context.mjs +244 -0
  219. package/src/cli/commands/event.mjs +63 -24
  220. package/src/cli/commands/host-detect.mjs +33 -7
  221. package/src/cli/commands/init.mjs +83 -5
  222. package/src/cli/commands/modes.mjs +215 -0
  223. package/src/cli/commands/pr-review-audit-fixer.mjs +95 -16
  224. package/src/cli/commands/route.mjs +11 -2
  225. package/src/cli/commands/task.mjs +152 -18
  226. package/src/cli/commands/tokens.mjs +157 -6
  227. package/src/cli/commands/triage.mjs +277 -0
  228. package/src/cli/config/post-merge-sweep.mjs +49 -3
  229. package/src/cli/consistency/registry.mjs +14 -0
  230. package/src/cli/coordination/agent-identity.mjs +12 -0
  231. package/src/cli/coordination/event-log.mjs +73 -2
  232. package/src/cli/design/frontmatter-schema.mjs +3 -1
  233. package/src/cli/dispatch.mjs +15 -1
  234. package/src/cli/gate/registry.mjs +14 -0
  235. package/src/cli/help.mjs +1 -0
  236. package/src/cli/host/detect.mjs +5 -0
  237. package/src/cli/route/envelope.mjs +140 -106
  238. package/src/cli/route/skill-discover.mjs +46 -1
  239. package/src/cli/skill-author/contract.mjs +29 -0
  240. package/src/cli/skill-author/skill-template.mjs +4 -3
  241. package/src/cli/task/worktree-cleanup.mjs +191 -9
  242. package/templates/adr-format.md +56 -0
  243. package/templates/config.json +4 -0
  244. package/templates/context-format.md +34 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * commands/modes.mjs — `apt-tools modes list` (Pocock adoption ST-13, AC12).
3
+ *
4
+ * Power-user discoverability tool. Returns a JSON envelope with one
5
+ * record per discovered skill, exposing modes / flags / content-formats /
6
+ * config-driven behavior so a power user can build a mental model of
7
+ * the framework without reading 30 SKILL.md files.
8
+ *
9
+ * Adding a new skill auto-appears in `modes list` output without any
10
+ * code changes — the discovery is filesystem-scan-based (mirrors the
11
+ * router's eager skill-discover module).
12
+ *
13
+ * CLI:
14
+ * apt-tools modes list [project-dir]
15
+ *
16
+ * Output envelope:
17
+ * { status: 'ok', command: 'modes-list',
18
+ * skills: [{ name, modes, flags, content_formats, config_keys, file_path }] }
19
+ *
20
+ * Field semantics:
21
+ * - name — frontmatter `name` (e.g. "apt:plan")
22
+ * - modes — frontmatter `execution_modes` (array)
23
+ * - flags — parsed from frontmatter `argument-hint`
24
+ * - content_formats — frontmatter `produces` (optional array; [] when absent)
25
+ * - config_keys — hardcoded skill→config-paths map maintained inline
26
+ */
27
+
28
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
29
+ import { join, resolve } from 'node:path'
30
+ import { parse as parseYaml } from 'yaml'
31
+ import { ok } from '../util/result.mjs'
32
+
33
+ const FRONTMATTER_RE = /^---\s*([\s\S]*?)\s*---/
34
+
35
+ /**
36
+ * Hardcoded skill→config-paths map. Maintained inline so adding a new
37
+ * config-dependent skill is a one-line change.
38
+ *
39
+ * Per AC12, this is the mapping `modes list` consumes to enumerate
40
+ * which config keys a skill cares about. Skills that don't touch
41
+ * config are omitted (their config_keys is `[]`).
42
+ *
43
+ * @type {Record<string, string[]>}
44
+ */
45
+ const SKILL_CONFIG_KEYS = {
46
+ 'apt:execute': ['coding.tdd_default', 'tdd.iron_law'],
47
+ 'apt:triage': [
48
+ 'task_tracking.backend',
49
+ 'task_tracking.publish_to_tracker',
50
+ 'task_tracking.tracker_url',
51
+ 'task_tracking.tracker_label_vocabulary',
52
+ ],
53
+ 'apt:prototype': ['coding.prototype_discipline'],
54
+ 'apt:verify': ['verification.evidence_capture', 'verification.max_iterations'],
55
+ 'apt:verify-proof': ['verification.open_report', 'verification.report_formats'],
56
+ 'apt:review': ['pipeline.skip_review'],
57
+ 'apt:ship': ['pipeline.auto_branch', 'pipeline.branch_prefix'],
58
+ 'apt:setup': ['*'], // setup edits config — wildcard surface
59
+ 'apt:run': ['parallelization.enabled', 'parallelization.max_agents'],
60
+ }
61
+
62
+ /**
63
+ * Parse the `argument-hint` frontmatter field for `--flag` tokens.
64
+ * Returns a deduplicated array.
65
+ *
66
+ * @param {string|undefined} hint
67
+ * @returns {string[]}
68
+ */
69
+ function parseFlags(hint) {
70
+ if (typeof hint !== 'string') return []
71
+ const flags = new Set()
72
+ for (const m of hint.matchAll(/--[a-z][a-z0-9-]*/gi)) {
73
+ flags.add(m[0])
74
+ }
75
+ return [...flags]
76
+ }
77
+
78
+ /**
79
+ * Walk a skills root and return every apt-<slug>/SKILL.md it contains.
80
+ *
81
+ * @param {string} root
82
+ */
83
+ function walkSkillRoot(root) {
84
+ if (!existsSync(root)) return []
85
+ let stat
86
+ try {
87
+ stat = statSync(root)
88
+ } catch {
89
+ return []
90
+ }
91
+ if (!stat.isDirectory()) return []
92
+ let entries
93
+ try {
94
+ entries = readdirSync(root, { withFileTypes: true })
95
+ } catch {
96
+ return []
97
+ }
98
+ const out = []
99
+ for (const entry of entries) {
100
+ if (!entry.name.startsWith('apt')) continue
101
+ if (!entry.isDirectory()) continue
102
+ const skillFile = join(root, entry.name, 'SKILL.md')
103
+ if (existsSync(skillFile)) out.push(skillFile)
104
+ }
105
+ return out
106
+ }
107
+
108
+ /**
109
+ * Parse a single SKILL.md into a modes-list record. Returns null if
110
+ * the frontmatter is unparseable.
111
+ *
112
+ * @param {string} file
113
+ */
114
+ function readSkillForModes(file) {
115
+ let text
116
+ try {
117
+ text = readFileSync(file, 'utf-8')
118
+ } catch {
119
+ return null
120
+ }
121
+ const m = text.match(FRONTMATTER_RE)
122
+ if (!m) return null
123
+ let parsed
124
+ try {
125
+ parsed = parseYaml(m[1])
126
+ } catch {
127
+ return null
128
+ }
129
+ if (parsed === null || typeof parsed !== 'object') return null
130
+
131
+ const name = typeof parsed.name === 'string' ? parsed.name : null
132
+ if (name === null) return null
133
+
134
+ const modes = Array.isArray(parsed.execution_modes)
135
+ ? parsed.execution_modes.filter((m) => typeof m === 'string')
136
+ : []
137
+ const flags = parseFlags(parsed['argument-hint'])
138
+ const content_formats = Array.isArray(parsed.produces)
139
+ ? parsed.produces.filter((p) => typeof p === 'string')
140
+ : []
141
+ // config_keys: prefer frontmatter `config_keys:` field when present; fall back
142
+ // to the hardcoded SKILL_CONFIG_KEYS map so skills not yet migrated still work.
143
+ const config_keys = Array.isArray(parsed.config_keys)
144
+ ? parsed.config_keys.filter((k) => typeof k === 'string')
145
+ : SKILL_CONFIG_KEYS[name] || []
146
+
147
+ return {
148
+ name,
149
+ modes,
150
+ flags,
151
+ content_formats,
152
+ config_keys,
153
+ file_path: file,
154
+ }
155
+ }
156
+
157
+ /**
158
+ * CLI entry: `apt-tools modes <subcommand> [project-dir]`.
159
+ *
160
+ * Subcommands: `list` (the only v1 subcommand).
161
+ *
162
+ * The trusted-skill-roots discovery follows the same pattern as the
163
+ * router's skill-discover module, but at minimum scans
164
+ * `packages/framework/skills/` relative to the framework checkout. For
165
+ * project-installed skills, the project's `.aperant/deps/.../skills`
166
+ * tree is also scanned when present.
167
+ */
168
+ export function cmdModes(subcommand, projectDir, _extraArgs) {
169
+ const cmd = subcommand || 'list'
170
+ if (cmd !== 'list') {
171
+ return ok({
172
+ status: 'error',
173
+ command: 'modes',
174
+ error: `Unknown subcommand: ${cmd}. Use 'list'.`,
175
+ })
176
+ }
177
+
178
+ // Project-dir is optional for modes list — defaults to cwd.
179
+ const absProjectDir = projectDir ? resolve(projectDir) : process.cwd()
180
+
181
+ // Compute trusted skill roots. The framework's own skills root
182
+ // (relative to this file) is always included; project-installed
183
+ // roots (.aperant/deps/...) are added when present.
184
+ const roots = new Set()
185
+ // Framework checkout root: this file is at
186
+ // .../packages/framework/src/cli/commands/modes.mjs → 5 levels up
187
+ // to the framework root, then skills/.
188
+ const frameworkRoot = resolve(new URL('../../../skills', import.meta.url).pathname)
189
+ if (existsSync(frameworkRoot)) roots.add(frameworkRoot)
190
+ const projectSkillRoot = join(absProjectDir, 'packages/framework/skills')
191
+ if (existsSync(projectSkillRoot)) roots.add(projectSkillRoot)
192
+ const installedRoot = join(absProjectDir, '.aperant/deps/node_modules/@aperant/framework/skills')
193
+ if (existsSync(installedRoot)) roots.add(installedRoot)
194
+
195
+ const seen = new Set()
196
+ const skills = []
197
+ for (const root of roots) {
198
+ for (const file of walkSkillRoot(root)) {
199
+ const record = readSkillForModes(file)
200
+ if (!record) continue
201
+ if (seen.has(record.name)) continue // first-writer-wins
202
+ seen.add(record.name)
203
+ skills.push(record)
204
+ }
205
+ }
206
+ skills.sort((a, b) => a.name.localeCompare(b.name))
207
+
208
+ return ok({
209
+ status: 'ok',
210
+ command: 'modes-list',
211
+ project_dir: absProjectDir,
212
+ skills,
213
+ scanned_roots: [...roots],
214
+ })
215
+ }
@@ -117,7 +117,8 @@ function extractTopLevelSection(markdown, heading) {
117
117
  }
118
118
 
119
119
  /**
120
- * Parse a fixes-applied.md produced by apt-pr-review-fixer. Returns
120
+ * Parse a fixes-applied.md produced by apt-pr-review-fixer OR a
121
+ * self-review.md produced by apt-pr-review-self-reviewer. Returns
121
122
  * `{ fixes: Array<{id, title, file, verification}>, skipped: Array<{id}>, exists: true }`.
122
123
  * Non-existent or empty report returns `{ exists: false, fixes: [], skipped: [] }`.
123
124
  *
@@ -125,24 +126,57 @@ function extractTopLevelSection(markdown, heading) {
125
126
  * `### <id>` block. We strip any trailing `(created)` / `(modify)` etc
126
127
  * annotation the fixer template shows as free-form text so the intersect
127
128
  * step can match against `git diff --name-only` output cleanly.
129
+ *
130
+ * Dual-schema support (PR #95 Defect 2):
131
+ * - Fixer reports use `## Fixes Applied` with `### <ID> — title` headers
132
+ * (e.g. `### QUA-001 — title`). Real finding ids carry through.
133
+ * - Self-reviewer reports use `## Fixes Applied by Self-Reviewer` with
134
+ * `### Fix N: description` headers. These have no real finding id, so
135
+ * we synthesize `self-reviewer-fix-${N}` to satisfy the
136
+ * `report.fixes.length > 0` guard in decideVerdict rule 3 while
137
+ * letting the diff-intersect step (the real check) decide the verdict.
138
+ *
139
+ * The two heading texts are disjoint — fixers emit one, self-reviewers
140
+ * emit the other. If both somehow appear in the same file (not expected
141
+ * in practice), both are parsed and fixer entries come first.
128
142
  */
129
143
  export function parseFixesAppliedMarkdown(md) {
130
144
  if (!md || typeof md !== 'string' || !md.trim()) {
131
- return { exists: false, fixes: [], skipped: [] }
145
+ return { exists: false, fixes: [], skipped: [], schema: 'fixer' }
132
146
  }
133
147
  const fixesSection = extractTopLevelSection(md, 'Fixes Applied')
148
+ const selfReviewerSection = extractTopLevelSection(md, 'Fixes Applied by Self-Reviewer')
134
149
  const skippedSection = extractTopLevelSection(md, 'Skipped')
135
- const fixes = parseFixEntries(fixesSection)
150
+ const fixes = [
151
+ ...parseFixEntries(fixesSection),
152
+ ...parseSelfReviewerFixEntries(selfReviewerSection),
153
+ ]
136
154
  const skipped = parseSkippedEntries(skippedSection)
137
- return { exists: true, fixes, skipped }
155
+ // Detect which heading was present; fixer takes precedence when both appear.
156
+ const schema = fixesSection ? 'fixer' : 'self-reviewer'
157
+ return { exists: true, fixes, skipped, schema }
138
158
  }
139
159
 
140
- function parseFixEntries(section) {
160
+ /**
161
+ * Parse the body of a `## Fixes Applied by Self-Reviewer` section into
162
+ * entries shaped like fixer entries (id, title, file, files, verification).
163
+ *
164
+ * The self-reviewer agent emits `### Fix N: description` per fix instead
165
+ * of `### ID — title`, because the reviewer doesn't carry the original
166
+ * finding ids forward. We synthesize id `self-reviewer-fix-${N}` so the
167
+ * rest of the audit pipeline (decideVerdict rule 3 guard, audit.json
168
+ * envelope) stays schema-agnostic. The body shape — `- **File**:` /
169
+ * `- **Change**:` — is identical to fixer entries, so we reuse
170
+ * extractFileFields verbatim. Verification is not part of the self-
171
+ * reviewer schema (their PASS/FAIL signal lives in `## Verification
172
+ * Results` and is already covered by parseFixerStatusLine), so we leave
173
+ * `verification: null` and let the gate's diff-intersect step decide.
174
+ */
175
+ function parseSelfReviewerFixEntries(section) {
141
176
  if (!section) return []
142
- // `### <id> <title>` OR `### <id> -- <title>` OR `### <id>` alone.
143
- // The fixer template uses em-dash + space; we tolerate either.
177
+ // `### Fix 1: description` / `### Fix 2: description`.
144
178
  const entries = []
145
- const headerRx = /^###\s+([A-Z]+-\d+)(?:\s*[—-]{1,2}\s*(.+))?$/gm
179
+ const headerRx = /^###\s+Fix\s+(\d+)(?:\s*:\s*(.+))?$/gm
146
180
  const matches = [...section.matchAll(headerRx)]
147
181
  for (let i = 0; i < matches.length; i++) {
148
182
  const m = matches[i]
@@ -151,10 +185,8 @@ function parseFixEntries(section) {
151
185
  const body = section.slice(start, nextStart)
152
186
  const files = extractFileFields(body)
153
187
  entries.push({
154
- id: m[1],
188
+ id: `self-reviewer-fix-${m[1]}`,
155
189
  title: m[2] ? m[2].trim() : null,
156
- // Back-compat: single-path consumers + tests still see `file`
157
- // (the first extracted path). New code should prefer `files`.
158
190
  file: files[0] ?? null,
159
191
  files,
160
192
  verification: extractVerificationField(body),
@@ -163,6 +195,48 @@ function parseFixEntries(section) {
163
195
  return entries
164
196
  }
165
197
 
198
+ function parseFixEntries(section) {
199
+ if (!section) return []
200
+ // `### <id> — <title>` OR `### <id> -- <title>` OR `### <id>` alone.
201
+ // The fixer template uses em-dash + space; we tolerate either.
202
+ //
203
+ // PR #95 (2026-05-12) widened the capture to accept combined-id headers
204
+ // like `### QUA-003 + FIT-001 — title` where one body explains multiple
205
+ // findings. The locked separator alphabet is `+`, `,`, and ` and ` —
206
+ // the exact set observed in live fixer reports. We do NOT widen to a
207
+ // generic `\W+` splitter; the alphabet stays narrow so a typo like
208
+ // `### QUA-003-foo` cannot be mis-parsed as id `QUA-003`. Combined
209
+ // headers produce one entry per id, all sharing the same body fields
210
+ // (files, verification, title).
211
+ const entries = []
212
+ const headerRx =
213
+ /^###\s+([A-Z]+-\d+(?:\s*(?:\+|,|\s+and\s+)\s*[A-Z]+-\d+)*)(?:\s*[—-]{1,2}\s*(.+))?$/gm
214
+ const idSplitRx = /\s*(?:\+|,|\s+and\s+)\s*/
215
+ const matches = [...section.matchAll(headerRx)]
216
+ for (let i = 0; i < matches.length; i++) {
217
+ const m = matches[i]
218
+ const start = m.index
219
+ const nextStart = i + 1 < matches.length ? matches[i + 1].index : section.length
220
+ const body = section.slice(start, nextStart)
221
+ const files = extractFileFields(body)
222
+ const title = m[2] ? m[2].trim() : null
223
+ const verification = extractVerificationField(body)
224
+ const ids = m[1].split(idSplitRx)
225
+ for (const id of ids) {
226
+ entries.push({
227
+ id,
228
+ title,
229
+ // Back-compat: single-path consumers + tests still see `file`
230
+ // (the first extracted path). New code should prefer `files`.
231
+ file: files[0] ?? null,
232
+ files,
233
+ verification,
234
+ })
235
+ }
236
+ }
237
+ return entries
238
+ }
239
+
166
240
  function parseSkippedEntries(section) {
167
241
  if (!section) return []
168
242
  const entries = []
@@ -518,12 +592,14 @@ export function decideVerdict({ statusLine, fixesApplied, diffFiles }) {
518
592
  // fail-closed trap. The report could be scaffolded with headers but
519
593
  // no actual entries.
520
594
  if (claimed.fixed > 0 && report.fixes.length === 0) {
595
+ const heading =
596
+ report.schema === 'self-reviewer' ? '## Fixes Applied by Self-Reviewer' : '## Fixes Applied'
521
597
  return {
522
598
  verdict: 'hallucinated',
523
599
  reason:
524
600
  `fixer claimed FIXED=${claimed.fixed} but fixes-applied.md has zero ` +
525
601
  `### <id>` +
526
- ` entries under ## Fixes Applied`,
602
+ ` entries under ${heading}`,
527
603
  claimed,
528
604
  matched: [],
529
605
  unmatched: [],
@@ -551,10 +627,13 @@ export function decideVerdict({ statusLine, fixesApplied, diffFiles }) {
551
627
  // Main rule: FIXED > 0 AND PASS — intersect claimed files with the
552
628
  // worktree diff. Empty intersection = hallucination.
553
629
  if (claimed.fixed > 0 && claimed.verification === 'PASS') {
554
- const claimedFiles = report.fixes.flatMap((f) =>
555
- Array.isArray(f.files) ? f.files : f.file ? [f.file] : [],
556
- )
557
- const { matched, unmatched } = intersectClaimedAndStaged(claimedFiles, diffFiles)
630
+ const claimedFiles = [
631
+ ...new Set(
632
+ report.fixes.flatMap((f) => (Array.isArray(f.files) ? f.files : f.file ? [f.file] : [])),
633
+ ),
634
+ ]
635
+ const { matched: rawMatched, unmatched } = intersectClaimedAndStaged(claimedFiles, diffFiles)
636
+ const matched = [...new Set(rawMatched)]
558
637
  if (matched.length === 0) {
559
638
  return {
560
639
  verdict: 'hallucinated',
@@ -138,7 +138,7 @@ const PR_REVIEW_SIGNAL = /(?:\bPR\s*#?\s*\d+|#\d+|\bpull\s*request\s*#?\s*\d+)\b
138
138
  * so this keeps the two paths consistent.
139
139
  *
140
140
  * @param {import('../route/skill-discover.mjs').DiscoveredSkill[]} skills
141
- * @returns {{ agentOf: (slug: string) => string|null, spawnsAgent: (slug: string) => boolean, knownSlugs: Set<string>, isUserInvocable: (slug: string) => boolean }}
141
+ * @returns {{ agentOf: (slug: string) => string|null, spawnsAgent: (slug: string) => boolean, taskContextOf: (slug: string) => string|null, defaultTrackOf: (slug: string) => string|null, knownSlugs: Set<string>, isUserInvocable: (slug: string) => boolean }}
142
142
  */
143
143
  function skillIndex(skills) {
144
144
  const byName = new Map()
@@ -160,6 +160,12 @@ function skillIndex(skills) {
160
160
  spawnsAgent(slug) {
161
161
  return Boolean(byName.get(slug)?.spawns_agent)
162
162
  },
163
+ taskContextOf(slug) {
164
+ return byName.get(slug)?.task_context ?? null
165
+ },
166
+ defaultTrackOf(slug) {
167
+ return byName.get(slug)?.default_track ?? null
168
+ },
163
169
  isUserInvocable(slug) {
164
170
  return userInvocableSlugs.has(slug)
165
171
  },
@@ -322,6 +328,7 @@ export function cmdRoute(projectDir, extraArgs) {
322
328
  if (!isPrReviewFreetext && index.knownSlugs.has(firstWord)) {
323
329
  const shouldSpawn = index.spawnsAgent(firstWord)
324
330
  const gate_preview = previewGatesFor(firstWord)
331
+ const defaultTrack = index.defaultTrackOf(firstWord)
325
332
  return ok({
326
333
  status: 'ok',
327
334
  command: 'route',
@@ -330,6 +337,8 @@ export function cmdRoute(projectDir, extraArgs) {
330
337
  skill_args: remainingArgs || null,
331
338
  spawn_agent: shouldSpawn,
332
339
  agent: shouldSpawn ? index.agentOf(firstWord) || `apt-${firstWord}` : null,
340
+ task_context: index.taskContextOf(firstWord),
341
+ ...(defaultTrack ? { default_track: defaultTrack } : {}),
333
342
  host_capabilities,
334
343
  task_isolation,
335
344
  update_check,
@@ -430,7 +439,7 @@ export function cmdRoute(projectDir, extraArgs) {
430
439
  {
431
440
  status: 'error',
432
441
  command: 'route',
433
- error_code: 'envelope_overflow',
442
+ error_code: 'skill_description_too_long',
434
443
  reason: e instanceof Error ? e.message : String(e),
435
444
  },
436
445
  1,