@aperant/framework 0.17.0 → 0.20.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 (242) hide show
  1. package/CHANGELOG.md +261 -0
  2. package/agents/apt-executor.md +6 -0
  3. package/agents/apt-planner.md +3 -2
  4. package/bin/features-reconcile-check.mjs +77 -0
  5. package/dist/cli/artifacts/self-stage.d.mts.map +1 -1
  6. package/dist/cli/artifacts/self-stage.mjs +2 -1
  7. package/dist/cli/artifacts/self-stage.mjs.map +1 -1
  8. package/dist/cli/cli-wrappers/features-reconcile.d.mts +2 -0
  9. package/dist/cli/cli-wrappers/features-reconcile.d.mts.map +1 -0
  10. package/dist/cli/cli-wrappers/features-reconcile.mjs +9 -0
  11. package/dist/cli/cli-wrappers/features-reconcile.mjs.map +1 -0
  12. package/dist/cli/commands/ci-watch.mjs +49 -2
  13. package/dist/cli/commands/ci-watch.mjs.map +1 -1
  14. package/dist/cli/commands/commit.mjs +3 -3
  15. package/dist/cli/commands/commit.mjs.map +1 -1
  16. package/dist/cli/commands/event.mjs +16 -16
  17. package/dist/cli/commands/event.mjs.map +1 -1
  18. package/dist/cli/commands/features-reconcile.d.mts +2 -0
  19. package/dist/cli/commands/features-reconcile.d.mts.map +1 -0
  20. package/dist/cli/commands/features-reconcile.mjs +143 -0
  21. package/dist/cli/commands/features-reconcile.mjs.map +1 -0
  22. package/dist/cli/commands/health-check.d.mts +2 -2
  23. package/dist/cli/commands/health-check.mjs +2 -2
  24. package/dist/cli/commands/merge-integrate.mjs +3 -3
  25. package/dist/cli/commands/merge-integrate.mjs.map +1 -1
  26. package/dist/cli/commands/produce.d.mts +9 -0
  27. package/dist/cli/commands/produce.d.mts.map +1 -0
  28. package/dist/cli/commands/produce.mjs +1345 -0
  29. package/dist/cli/commands/produce.mjs.map +1 -0
  30. package/dist/cli/commands/task.d.mts +16 -1
  31. package/dist/cli/commands/task.d.mts.map +1 -1
  32. package/dist/cli/commands/task.mjs +434 -266
  33. package/dist/cli/commands/task.mjs.map +1 -1
  34. package/dist/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
  35. package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -1
  36. package/dist/cli/coordination/event-schema.d.mts +5 -3
  37. package/dist/cli/coordination/event-schema.d.mts.map +1 -1
  38. package/dist/cli/coordination/event-schema.mjs +245 -21
  39. package/dist/cli/coordination/event-schema.mjs.map +1 -1
  40. package/dist/cli/coordination/store.d.mts +2 -2
  41. package/dist/cli/coordination/store.mjs +4 -4
  42. package/dist/cli/coordination/store.mjs.map +1 -1
  43. package/dist/cli/dispatch.d.mts.map +1 -1
  44. package/dist/cli/dispatch.mjs +4 -2
  45. package/dist/cli/dispatch.mjs.map +1 -1
  46. package/dist/cli/features/area-shape.d.mts +11 -0
  47. package/dist/cli/features/area-shape.d.mts.map +1 -0
  48. package/dist/cli/features/area-shape.mjs +141 -0
  49. package/dist/cli/features/area-shape.mjs.map +1 -0
  50. package/dist/cli/features/delta.d.mts +78 -0
  51. package/dist/cli/features/delta.d.mts.map +1 -0
  52. package/dist/cli/features/delta.mjs +591 -0
  53. package/dist/cli/features/delta.mjs.map +1 -0
  54. package/dist/cli/features/reconcile.d.mts +236 -0
  55. package/dist/cli/features/reconcile.d.mts.map +1 -0
  56. package/dist/cli/features/reconcile.mjs +1126 -0
  57. package/dist/cli/features/reconcile.mjs.map +1 -0
  58. package/dist/cli/features/surface-map.d.mts +63 -0
  59. package/dist/cli/features/surface-map.d.mts.map +1 -0
  60. package/dist/cli/features/surface-map.mjs +191 -0
  61. package/dist/cli/features/surface-map.mjs.map +1 -0
  62. package/dist/cli/{commands/features-audit.d.mts → features/write-root.d.mts} +4 -3
  63. package/dist/cli/features/write-root.d.mts.map +1 -0
  64. package/dist/cli/features/write-root.mjs +137 -0
  65. package/dist/cli/features/write-root.mjs.map +1 -0
  66. package/dist/cli/gate/gates/features-reconciled.d.mts +36 -0
  67. package/dist/cli/gate/gates/features-reconciled.d.mts.map +1 -0
  68. package/dist/cli/gate/gates/features-reconciled.mjs +67 -0
  69. package/dist/cli/gate/gates/features-reconciled.mjs.map +1 -0
  70. package/dist/cli/gate/gates/index.mjs +1 -0
  71. package/dist/cli/gate/gates/index.mjs.map +1 -1
  72. package/dist/cli/help.d.mts.map +1 -1
  73. package/dist/cli/help.mjs +31 -1
  74. package/dist/cli/help.mjs.map +1 -1
  75. package/dist/cli/install/legacy-paths.d.mts.map +1 -1
  76. package/dist/cli/install/legacy-paths.mjs +2 -0
  77. package/dist/cli/install/legacy-paths.mjs.map +1 -1
  78. package/dist/cli/produce/blind-probe.d.mts +85 -0
  79. package/dist/cli/produce/blind-probe.d.mts.map +1 -0
  80. package/dist/cli/produce/blind-probe.mjs +217 -0
  81. package/dist/cli/produce/blind-probe.mjs.map +1 -0
  82. package/dist/cli/produce/claim.d.mts +188 -0
  83. package/dist/cli/produce/claim.d.mts.map +1 -0
  84. package/dist/cli/produce/claim.mjs +518 -0
  85. package/dist/cli/produce/claim.mjs.map +1 -0
  86. package/dist/cli/produce/done-gate.d.mts +87 -0
  87. package/dist/cli/produce/done-gate.d.mts.map +1 -0
  88. package/dist/cli/produce/done-gate.mjs +200 -0
  89. package/dist/cli/produce/done-gate.mjs.map +1 -0
  90. package/dist/cli/produce/events.d.mts +77 -0
  91. package/dist/cli/produce/events.d.mts.map +1 -0
  92. package/dist/cli/produce/events.mjs +126 -0
  93. package/dist/cli/produce/events.mjs.map +1 -0
  94. package/dist/cli/produce/evidence-oracle.d.mts +63 -0
  95. package/dist/cli/produce/evidence-oracle.d.mts.map +1 -0
  96. package/dist/cli/produce/evidence-oracle.mjs +122 -0
  97. package/dist/cli/produce/evidence-oracle.mjs.map +1 -0
  98. package/dist/cli/produce/ledger.d.mts +140 -0
  99. package/dist/cli/produce/ledger.d.mts.map +1 -0
  100. package/dist/cli/produce/ledger.mjs +272 -0
  101. package/dist/cli/produce/ledger.mjs.map +1 -0
  102. package/dist/cli/produce/probe-family.d.mts +53 -0
  103. package/dist/cli/produce/probe-family.d.mts.map +1 -0
  104. package/dist/cli/produce/probe-family.mjs +160 -0
  105. package/dist/cli/produce/probe-family.mjs.map +1 -0
  106. package/dist/cli/produce/projection.d.mts +55 -0
  107. package/dist/cli/produce/projection.d.mts.map +1 -0
  108. package/dist/cli/produce/projection.mjs +97 -0
  109. package/dist/cli/produce/projection.mjs.map +1 -0
  110. package/dist/cli/produce/run-id.d.mts +42 -0
  111. package/dist/cli/produce/run-id.d.mts.map +1 -0
  112. package/dist/cli/produce/run-id.mjs +79 -0
  113. package/dist/cli/produce/run-id.mjs.map +1 -0
  114. package/dist/cli/produce/saga.d.mts +180 -0
  115. package/dist/cli/produce/saga.d.mts.map +1 -0
  116. package/dist/cli/produce/saga.mjs +290 -0
  117. package/dist/cli/produce/saga.mjs.map +1 -0
  118. package/dist/cli/produce/scheduler.d.mts +165 -0
  119. package/dist/cli/produce/scheduler.d.mts.map +1 -0
  120. package/dist/cli/produce/scheduler.mjs +399 -0
  121. package/dist/cli/produce/scheduler.mjs.map +1 -0
  122. package/dist/cli/produce/setpoint.d.mts +52 -0
  123. package/dist/cli/produce/setpoint.d.mts.map +1 -0
  124. package/dist/cli/produce/setpoint.mjs +113 -0
  125. package/dist/cli/produce/setpoint.mjs.map +1 -0
  126. package/dist/cli/produce/verification-ttl.d.mts +75 -0
  127. package/dist/cli/produce/verification-ttl.d.mts.map +1 -0
  128. package/dist/cli/produce/verification-ttl.mjs +169 -0
  129. package/dist/cli/produce/verification-ttl.mjs.map +1 -0
  130. package/dist/cli/release-notes/ship-autodraft.mjs +2 -2
  131. package/dist/cli/roadmap/{conductor-view.d.mts → showrunner-view.d.mts} +3 -3
  132. package/dist/cli/roadmap/showrunner-view.d.mts.map +1 -0
  133. package/dist/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +7 -7
  134. package/dist/cli/roadmap/showrunner-view.mjs.map +1 -0
  135. package/dist/cli/variant/gallery.d.mts.map +1 -1
  136. package/dist/cli/variant/gallery.mjs +7 -3
  137. package/dist/cli/variant/gallery.mjs.map +1 -1
  138. package/dist/plugin/.claude-plugin/plugin.json +2 -1
  139. package/dist/plugin/agents/apt-executor.md +6 -0
  140. package/dist/plugin/agents/apt-planner.md +3 -2
  141. package/dist/plugin/skills/apt/SKILL.md +112 -38
  142. package/dist/plugin/skills/apt-close-task/SKILL.md +2 -2
  143. package/dist/plugin/skills/apt-debug/SKILL.md +22 -24
  144. package/dist/plugin/skills/apt-execute/SKILL.md +35 -26
  145. package/dist/plugin/skills/apt-fan-out/SKILL.md +4 -4
  146. package/dist/plugin/skills/apt-handoff/SKILL.md +1 -1
  147. package/dist/plugin/skills/apt-mockup/SKILL.md +1 -1
  148. package/dist/plugin/skills/apt-plan/SKILL.md +5 -5
  149. package/dist/plugin/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
  150. package/dist/plugin/skills/apt-produce/SKILL.md +606 -0
  151. package/dist/plugin/skills/apt-quick/SKILL.md +27 -33
  152. package/dist/plugin/skills/apt-review/SKILL.md +14 -11
  153. package/dist/plugin/skills/apt-run/SKILL.md +132 -3
  154. package/dist/plugin/skills/apt-ship/SKILL.md +29 -5
  155. package/dist/plugin/skills/apt-spar/SKILL.md +5 -3
  156. package/dist/plugin/skills/apt-watch-ci/SKILL.md +6 -4
  157. package/dist/schemas/feature-registry.d.ts.map +1 -1
  158. package/dist/schemas/feature-registry.js +34 -3
  159. package/dist/schemas/feature-registry.js.map +1 -1
  160. package/dist/types/config.d.ts +8 -5
  161. package/dist/types/config.d.ts.map +1 -1
  162. package/package.json +4 -4
  163. package/prompts/{conductor-framework-context.md → showrunner-framework-context.md} +1 -1
  164. package/prompts/{conductor-status-check.md → showrunner-status-check.md} +1 -1
  165. package/prompts/{conductor-sub-agent.md → showrunner-sub-agent.md} +6 -6
  166. package/prompts/{conductor-system.md → showrunner-system.md} +8 -8
  167. package/skills/apt/SKILL.md +112 -38
  168. package/skills/apt-close-task/SKILL.md +2 -2
  169. package/skills/apt-debug/SKILL.md +22 -24
  170. package/skills/apt-execute/SKILL.md +35 -26
  171. package/skills/apt-fan-out/SKILL.md +4 -4
  172. package/skills/apt-handoff/SKILL.md +1 -1
  173. package/skills/apt-mockup/SKILL.md +1 -1
  174. package/skills/apt-plan/SKILL.md +5 -5
  175. package/skills/apt-plan/adapters/{conductor.md → showrunner.md} +16 -16
  176. package/skills/apt-produce/SKILL.md +606 -0
  177. package/skills/apt-quick/SKILL.md +27 -33
  178. package/skills/apt-review/SKILL.md +14 -11
  179. package/skills/apt-run/SKILL.md +132 -3
  180. package/skills/apt-ship/SKILL.md +29 -5
  181. package/skills/apt-spar/SKILL.md +5 -3
  182. package/skills/apt-watch-ci/SKILL.md +6 -4
  183. package/src/cli/artifacts/self-stage.mjs +2 -1
  184. package/src/cli/cli-wrappers/features-reconcile.mjs +9 -0
  185. package/src/cli/commands/ci-watch.mjs +51 -2
  186. package/src/cli/commands/commit.mjs +3 -3
  187. package/src/cli/commands/event.mjs +16 -16
  188. package/src/cli/commands/features-reconcile.mjs +157 -0
  189. package/src/cli/commands/health-check.mjs +2 -2
  190. package/src/cli/commands/merge-integrate.mjs +3 -3
  191. package/src/cli/commands/produce.mjs +1466 -0
  192. package/src/cli/commands/task.mjs +482 -285
  193. package/src/cli/coordination/auto-emit-artifact-ready.mjs +5 -5
  194. package/src/cli/coordination/event-schema.d.ts +4 -2
  195. package/src/cli/coordination/event-schema.mjs +276 -21
  196. package/src/cli/coordination/store.mjs +4 -4
  197. package/src/cli/dispatch.mjs +4 -2
  198. package/src/cli/features/area-shape.mjs +140 -0
  199. package/src/cli/features/delta.mjs +625 -0
  200. package/src/cli/features/reconcile.mjs +1169 -0
  201. package/src/cli/features/surface-map.mjs +192 -0
  202. package/src/cli/features/write-root.mjs +140 -0
  203. package/src/cli/gate/gates/features-reconciled.mjs +70 -0
  204. package/src/cli/gate/gates/index.mjs +1 -0
  205. package/src/cli/help.mjs +31 -1
  206. package/src/cli/install/legacy-paths.mjs +2 -0
  207. package/src/cli/produce/blind-probe.mjs +245 -0
  208. package/src/cli/produce/claim.mjs +543 -0
  209. package/src/cli/produce/done-gate.mjs +238 -0
  210. package/src/cli/produce/events.mjs +131 -0
  211. package/src/cli/produce/evidence-oracle.mjs +133 -0
  212. package/src/cli/produce/ledger.mjs +284 -0
  213. package/src/cli/produce/probe-family.mjs +168 -0
  214. package/src/cli/produce/projection.mjs +105 -0
  215. package/src/cli/produce/run-id.mjs +84 -0
  216. package/src/cli/produce/saga.mjs +303 -0
  217. package/src/cli/produce/scheduler.mjs +423 -0
  218. package/src/cli/produce/setpoint.mjs +122 -0
  219. package/src/cli/produce/verification-ttl.mjs +191 -0
  220. package/src/cli/release-notes/ship-autodraft.mjs +2 -2
  221. package/src/cli/roadmap/showrunner-view.d.ts +10 -0
  222. package/src/cli/roadmap/{conductor-view.mjs → showrunner-view.mjs} +6 -6
  223. package/src/cli/variant/gallery.mjs +7 -3
  224. package/templates/aperant-claude-md-appendix.md +2 -2
  225. package/workflows/scan-features.md +17 -11
  226. package/dist/cli/cli-wrappers/features-audit.d.mts +0 -2
  227. package/dist/cli/cli-wrappers/features-audit.d.mts.map +0 -1
  228. package/dist/cli/cli-wrappers/features-audit.mjs +0 -8
  229. package/dist/cli/cli-wrappers/features-audit.mjs.map +0 -1
  230. package/dist/cli/commands/features-audit.d.mts.map +0 -1
  231. package/dist/cli/commands/features-audit.mjs +0 -293
  232. package/dist/cli/commands/features-audit.mjs.map +0 -1
  233. package/dist/cli/features/registry-audit.d.mts +0 -56
  234. package/dist/cli/features/registry-audit.d.mts.map +0 -1
  235. package/dist/cli/features/registry-audit.mjs +0 -264
  236. package/dist/cli/features/registry-audit.mjs.map +0 -1
  237. package/dist/cli/roadmap/conductor-view.d.mts.map +0 -1
  238. package/dist/cli/roadmap/conductor-view.mjs.map +0 -1
  239. package/src/cli/cli-wrappers/features-audit.mjs +0 -8
  240. package/src/cli/commands/features-audit.mjs +0 -302
  241. package/src/cli/features/registry-audit.mjs +0 -254
  242. package/src/cli/roadmap/conductor-view.d.ts +0 -10
@@ -0,0 +1,192 @@
1
+ /**
2
+ * surface-map.mjs — feature-surface qualification + area suggestion for the
3
+ * features-reconcile delta ledger (replaces registry-audit.mjs's
4
+ * `detectFeatureKind`/`inferArea`).
5
+ *
6
+ * Dependency-free: node:-builtins only (in fact, zero imports) — this module
7
+ * sits inside the bare-node CI entry's import closure (ID-08).
8
+ *
9
+ * THE MAPPING TABLE (ID-06) — this header IS the contract:
10
+ *
11
+ * | Path family | kind | area resolution |
12
+ * |--------------------------------------------------------------------|------------------|----------------------------------------------------|
13
+ * | packages/ui/src/sections/<a>/** /*.tsx | ui-component | alias(<a>) — deterministic |
14
+ * | packages/ui/src/{components,design-system}/** /*.tsx | ui-component | 'chrome' (existing precedent in chrome.json) |
15
+ * | packages/ui/src/hooks/use-*.ts{,x} | hook | token-match against known areas, else null |
16
+ * | apps/desktop/src/renderer/src/** /*.tsx | ui-component | token-match (features/settings/** → settings), else null |
17
+ * | apps/web/src/app/<seg>/** | ui-component | alias(<seg>) when it names a known area, else null |
18
+ * | apps/desktop/src/main/ipc-handlers/*.ts | ipc-channel | token-match (inbox-*-handlers → inbox), else null |
19
+ * | packages/backend/convex/** /*.ts | backend-function | token-match, else null |
20
+ * | packages/core/src/{services,pipeline,workflows,providers,inbox,terminal,credentials}/** /*.ts | core-service | first path segment / token-match, else null |
21
+ *
22
+ * Exclusions (always, checked BEFORE any family match): test files
23
+ * (`\.(test|spec)\.`), `__tests__/` dirs, `.stories.` files, `_generated/`
24
+ * dirs.
25
+ *
26
+ * Area rules:
27
+ * - AREA_ALIASES = { terminal: 'terminals', conductor: 'showrunner' } is
28
+ * applied at EVERY resolution site.
29
+ * - `hooks` / `ipc` / `services` are NEVER emitted as areas (the junk-drawer
30
+ * failure mode is structurally impossible) — they are filtered even if a
31
+ * caller supplies them in `knownAreas`.
32
+ * - `area: null` means "the writer agent must assign it" — `--check` and
33
+ * `--write` fail closed on a null area.
34
+ * - Token-match compares path tokens (split on `/ - _ .`) against the
35
+ * caller-supplied `knownAreas` list, alias-normalized, with a simple
36
+ * plural tolerance (`competitor` matches `competitors`).
37
+ */
38
+
39
+ export const AREA_ALIASES = Object.freeze({
40
+ terminal: 'terminals',
41
+ conductor: 'showrunner',
42
+ })
43
+
44
+ /** Pseudo-areas that must never be suggested (retired junk drawers). */
45
+ const JUNK_AREAS = new Set(['hooks', 'ipc', 'services'])
46
+
47
+ /** Apply the alias map to a raw area token. */
48
+ export function applyAreaAlias(area) {
49
+ if (typeof area !== 'string') return area
50
+ return AREA_ALIASES[area] ?? area
51
+ }
52
+
53
+ /**
54
+ * Always-on exclusions: test files, __tests__ dirs, storybook stories,
55
+ * generated code.
56
+ */
57
+ export function isExcludedPath(norm) {
58
+ if (/\.(test|spec)\./.test(norm)) return true
59
+ if (/(^|\/)__tests__\//.test(norm)) return true
60
+ if (/\.stories\./.test(norm)) return true
61
+ if (/(^|\/)_generated\//.test(norm)) return true
62
+ return false
63
+ }
64
+
65
+ /**
66
+ * Normalize the caller-supplied known-area list: strings only, junk areas
67
+ * dropped (they are never legal suggestions even if a stale file exists).
68
+ */
69
+ function normalizeKnownAreas(knownAreas) {
70
+ const out = new Set()
71
+ for (const a of knownAreas || []) {
72
+ if (typeof a !== 'string' || !a) continue
73
+ if (JUNK_AREAS.has(a)) continue
74
+ out.add(a)
75
+ }
76
+ return out
77
+ }
78
+
79
+ /**
80
+ * Token-match a set of path tokens against the known-area set.
81
+ * A token `t` matches area `a` when alias(t) === a or alias(t) + 's' === a.
82
+ * Returns the first matching area (token order), or null.
83
+ */
84
+ function tokenMatchArea(tokens, knownSet) {
85
+ for (const raw of tokens) {
86
+ if (!raw) continue
87
+ const t = applyAreaAlias(raw.toLowerCase())
88
+ if (JUNK_AREAS.has(t)) continue
89
+ if (knownSet.has(t)) return t
90
+ if (knownSet.has(`${t}s`)) return `${t}s`
91
+ }
92
+ return null
93
+ }
94
+
95
+ /**
96
+ * Split a path fragment into candidate tokens: segments, kebab/underscore/dot
97
+ * parts, and camelCase words (convex files are camelCase — roadmapItems.ts).
98
+ */
99
+ function tokenize(fragment) {
100
+ const tokens = []
101
+ for (const seg of fragment.split('/')) {
102
+ if (!seg) continue
103
+ tokens.push(seg)
104
+ for (const part of seg.split(/[-_.]/)) {
105
+ if (!part) continue
106
+ if (part !== seg) tokens.push(part)
107
+ for (const camel of part.split(/(?=[A-Z])/)) {
108
+ if (camel && camel !== part) tokens.push(camel)
109
+ }
110
+ }
111
+ }
112
+ return tokens
113
+ }
114
+
115
+ /**
116
+ * Qualify a repo-relative file path as a feature surface.
117
+ *
118
+ * @param {string} filepath — repo-relative path (posix or win separators)
119
+ * @param {string[]} [knownAreas] — existing registry area names (callers pass
120
+ * the `.aperant/features/*.json` basenames); used only for token-match
121
+ * families. Deterministic families (sections, design-system) need none.
122
+ * @returns {{ kind: string, suggestedArea: string | null } | null}
123
+ * null when the path does not qualify at all.
124
+ */
125
+ export function qualifiesAsFeatureSurface(filepath, knownAreas = []) {
126
+ if (!filepath || typeof filepath !== 'string') return null
127
+ const norm = filepath.replace(/\\/g, '/').replace(/^\.\//, '')
128
+ if (isExcludedPath(norm)) return null
129
+ const knownSet = normalizeKnownAreas(knownAreas)
130
+
131
+ // packages/ui/src/sections/<a>/**/*.tsx → ui-component, alias(<a>) deterministic
132
+ let m = /^packages\/ui\/src\/sections\/([^/]+)\/.+\.tsx$/.exec(norm)
133
+ if (m) {
134
+ const area = applyAreaAlias(m[1])
135
+ return { kind: 'ui-component', suggestedArea: JUNK_AREAS.has(area) ? null : area }
136
+ }
137
+
138
+ // packages/ui/src/{components,design-system}/**/*.tsx → ui-component, chrome
139
+ if (/^packages\/ui\/src\/(components|design-system)\/.+\.tsx$/.test(norm)) {
140
+ return { kind: 'ui-component', suggestedArea: 'chrome' }
141
+ }
142
+
143
+ // packages/ui/src/hooks/use-*.ts{,x} → hook, token-match
144
+ m = /^packages\/ui\/src\/hooks\/(use-[A-Za-z0-9-]+)\.tsx?$/.exec(norm)
145
+ if (m) {
146
+ const tokens = tokenize(m[1]).filter((t) => t !== 'use')
147
+ return { kind: 'hook', suggestedArea: tokenMatchArea(tokens, knownSet) }
148
+ }
149
+
150
+ // apps/desktop/src/renderer/src/**/*.tsx → ui-component, token-match
151
+ m = /^apps\/desktop\/src\/renderer\/src\/(.+)\.tsx$/.exec(norm)
152
+ if (m) {
153
+ return { kind: 'ui-component', suggestedArea: tokenMatchArea(tokenize(m[1]), knownSet) }
154
+ }
155
+
156
+ // apps/web/src/app/<seg>/** → ui-component, alias(<seg>) when known
157
+ m = /^apps\/web\/src\/app\/([^/]+)\/.+\.tsx?$/.exec(norm)
158
+ if (m) {
159
+ const seg = applyAreaAlias(m[1])
160
+ const area = !JUNK_AREAS.has(seg) && knownSet.has(seg) ? seg : null
161
+ return { kind: 'ui-component', suggestedArea: area }
162
+ }
163
+
164
+ // apps/desktop/src/main/ipc-handlers/*.ts → ipc-channel, token-match
165
+ m = /^apps\/desktop\/src\/main\/ipc-handlers\/([^/]+)\.ts$/.exec(norm)
166
+ if (m) {
167
+ return { kind: 'ipc-channel', suggestedArea: tokenMatchArea(tokenize(m[1]), knownSet) }
168
+ }
169
+
170
+ // packages/backend/convex/**/*.ts → backend-function, token-match
171
+ m = /^packages\/backend\/convex\/(.+)\.ts$/.exec(norm)
172
+ if (m) {
173
+ return { kind: 'backend-function', suggestedArea: tokenMatchArea(tokenize(m[1]), knownSet) }
174
+ }
175
+
176
+ // packages/core/src/{services,pipeline,workflows,providers,inbox,terminal,credentials}/**/*.ts
177
+ // → core-service; first path segment / token-match.
178
+ m =
179
+ /^packages\/core\/src\/(services|pipeline|workflows|providers|inbox|terminal|credentials)\/(.+)\.ts$/.exec(
180
+ norm,
181
+ )
182
+ if (m) {
183
+ // The family segment itself is the first candidate token (pipeline →
184
+ // pipeline, terminal → terminals via alias, inbox → inbox); junk
185
+ // segments (services) and non-area segments (workflows, providers,
186
+ // credentials) fall through to token-match on the rest of the path.
187
+ const tokens = [m[1], ...tokenize(m[2])]
188
+ return { kind: 'core-service', suggestedArea: tokenMatchArea(tokens, knownSet) }
189
+ }
190
+
191
+ return null
192
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * write-root.mjs — BUG-025b worktree write-redirect resolution (ID-11).
3
+ *
4
+ * Moved verbatim from the retired legacy audit command so the mutating
5
+ * features-reconcile verbs (--draft / --write) keep the exact 4-layer
6
+ * behaviour: registry writes historically leaked generated `*.json` into main
7
+ * when the executor invoked the CLI from a worktree-isolated task. The fix
8
+ * applies a 4-step resolution order (see resolveWriteRoot below):
9
+ * (1) cwd IS a worktree root → write there (current behaviour)
10
+ * (2) --task-id maps to an active non-terminal worktree task →
11
+ * redirect writes to that worktree's `.aperant/features/`
12
+ * (3) multiple active worktree-isolated tasks + no --task-id → error
13
+ * (4) `--no-worktree-redirect` opt-out preserves layer (1) behaviour
14
+ */
15
+
16
+ import { spawnSync } from 'node:child_process'
17
+ import { existsSync, readFileSync } from 'node:fs'
18
+ import { join, resolve, sep } from 'node:path'
19
+ import { resolveMainRepoRoot } from '../util/state-paths.mjs'
20
+
21
+ const TERMINAL_PHASES = new Set(['completed', 'cancelled', 'closed', 'shipped', 'merged'])
22
+
23
+ /**
24
+ * BUG-025b — resolve where the mutating features-reconcile verbs should write.
25
+ * See module header for the 4-step order. Returns `{ writeRoot, redirect_notice }`
26
+ * on success or a synthetic error envelope (`{ error: { ... } }`) on the
27
+ * ambiguous-worktree case.
28
+ *
29
+ * @internal — exported for unit testing.
30
+ *
31
+ * @param {{projectDir: string, taskId?: string | null, noRedirect?: boolean}} args
32
+ * @returns {{writeRoot?: string, redirect_notice?: string | null, error?: {kind: string, message: string, active_worktree_tasks: string[]}}}
33
+ */
34
+ export function resolveWriteRoot({ projectDir, taskId, noRedirect }) {
35
+ const root = resolve(projectDir)
36
+ if (noRedirect === true) {
37
+ return { writeRoot: root, redirect_notice: null }
38
+ }
39
+ // Layer (1): cwd IS a worktree root. Worktrees have `git rev-parse
40
+ // --git-dir` != `git rev-parse --git-common-dir`. Falling through to
41
+ // the existing behaviour writes to this worktree's .aperant/features.
42
+ // Fast path: git sets GIT_DIR and GIT_COMMON_DIR in the environment
43
+ // when running inside a worktree hook context — if both are set and
44
+ // differ, we are in a worktree. This env-var check is ONLY valid at
45
+ // Layer (1) where we ask "is this process in a worktree?"; it must
46
+ // NOT be used to validate an untrusted external path (Layer 2 below).
47
+ const envDir = process.env.GIT_DIR
48
+ const envCommonDir = process.env.GIT_COMMON_DIR
49
+ if (envDir && envCommonDir) {
50
+ if (resolve(envDir) !== resolve(envCommonDir)) {
51
+ return { writeRoot: root, redirect_notice: null }
52
+ }
53
+ } else if (isWorktreeRoot(root)) {
54
+ return { writeRoot: root, redirect_notice: null }
55
+ }
56
+ // Layer (2): --task-id maps to an active non-terminal worktree-isolated
57
+ // task → redirect.
58
+ const stateActive = readActiveTasks(root)
59
+ if (taskId) {
60
+ const task = stateActive[taskId]
61
+ if (
62
+ task &&
63
+ !TERMINAL_PHASES.has(task.lifecycle_phase) &&
64
+ typeof task.worktree_path === 'string' &&
65
+ existsSync(task.worktree_path)
66
+ ) {
67
+ const resolvedWorktree = resolve(task.worktree_path)
68
+ const worktreesDir = resolve(root, '.aperant', 'worktrees') + sep
69
+ const isUnderProject = resolvedWorktree.startsWith(root + sep) || resolvedWorktree === root
70
+ const isUnderWorktrees = resolvedWorktree.startsWith(worktreesDir)
71
+ // Accept only paths under the project root, under .aperant/worktrees/,
72
+ // or confirmed as a real git worktree by git itself. Rejects arbitrary
73
+ // paths injected via a tampered state.json while preserving test
74
+ // fixtures that use ad-hoc worktree paths outside the project tree.
75
+ if (!isUnderProject && !isUnderWorktrees && !isWorktreeRoot(resolvedWorktree)) {
76
+ return { writeRoot: root, redirect_notice: null }
77
+ }
78
+ return {
79
+ writeRoot: resolvedWorktree,
80
+ redirect_notice: `features-reconcile redirected writes to worktree ${resolvedWorktree} (--task-id ${taskId})`,
81
+ }
82
+ }
83
+ }
84
+ // Layer (3): multiple active worktree-isolated tasks AND no --task-id
85
+ // → actionable error. Single-worktree case is OK to default to root
86
+ // (the executor will set up the worktree's registry on first scan).
87
+ const activeWorktreeTasks = Object.entries(stateActive).filter(([_id, t]) => {
88
+ return (
89
+ t &&
90
+ !TERMINAL_PHASES.has(t.lifecycle_phase) &&
91
+ typeof t.worktree_path === 'string' &&
92
+ existsSync(t.worktree_path)
93
+ )
94
+ })
95
+ if (!taskId && activeWorktreeTasks.length > 1) {
96
+ return {
97
+ error: {
98
+ kind: 'ambiguous-worktree-context',
99
+ message: 'ambiguous worktree context; pass --task-id <id> or --no-worktree-redirect',
100
+ active_worktree_tasks: activeWorktreeTasks.map(([id]) => id),
101
+ },
102
+ }
103
+ }
104
+ // Default: current behaviour (write into cwd).
105
+ return { writeRoot: root, redirect_notice: null }
106
+ }
107
+
108
+ export function isWorktreeRoot(projectDir) {
109
+ try {
110
+ const dirRes = spawnSync('git', ['rev-parse', '--git-dir'], {
111
+ cwd: projectDir,
112
+ encoding: 'utf-8',
113
+ stdio: ['ignore', 'pipe', 'ignore'],
114
+ })
115
+ const commonRes = spawnSync('git', ['rev-parse', '--git-common-dir'], {
116
+ cwd: projectDir,
117
+ encoding: 'utf-8',
118
+ stdio: ['ignore', 'pipe', 'ignore'],
119
+ })
120
+ if (dirRes.status !== 0 || commonRes.status !== 0) return false
121
+ return (dirRes.stdout || '').trim() !== (commonRes.stdout || '').trim()
122
+ } catch {
123
+ return false
124
+ }
125
+ }
126
+
127
+ export function readActiveTasks(projectDir) {
128
+ // state-fork-fix: read the AUTHORITATIVE main board — readActiveTasks finds
129
+ // each task's worktree_path for the BUG-025b feature-write redirect, so it
130
+ // MUST read main or the redirect-target lookup itself forks. (The feature
131
+ // WRITE path / resolveWriteRoot stays untouched.)
132
+ const statePath = join(resolveMainRepoRoot(projectDir), '.aperant', 'state.json')
133
+ if (!existsSync(statePath)) return {}
134
+ try {
135
+ const parsed = JSON.parse(readFileSync(statePath, 'utf-8'))
136
+ return parsed?.active_tasks ?? {}
137
+ } catch {
138
+ return {}
139
+ }
140
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @internal
3
+ * features-reconciled — blocking gate for `apt:ship` (ID-10).
4
+ *
5
+ * Runs the features-reconcile 4-clause checker IN-PROCESS (no shell-out)
6
+ * against the task's worktree (when present), verifying the write-then-verify
7
+ * ordering: apt-ship §0.0 materializes pending deltas BEFORE the §0 gate
8
+ * check reaches this gate.
9
+ *
10
+ * Deliberately has NO applies_to_profile — unlike verify-approved /
11
+ * review-clean, this gate runs under BOTH profiles so QUICK/DEBUG-shipped PRs
12
+ * are covered too (per gate/registry.mjs, omitting applies_to_profile means
13
+ * the gate applies universally).
14
+ *
15
+ * Deliberately returns NO inputs[] — the deltas/receipts/area dir contents
16
+ * are dynamic and the check is cheap, so the verdict is never cached.
17
+ */
18
+
19
+ import { existsSync } from 'node:fs'
20
+ import { checkReconciled, draftRepairCommand } from '../../features/reconcile.mjs'
21
+ import { registerGate } from '../registry.mjs'
22
+
23
+ const gate = {
24
+ id: 'features-reconciled',
25
+ description: 'feature-registry deltas reconciled (coverage + schema + integrity + no orphans)',
26
+ severity: 'blocking',
27
+ skills: ['ship'],
28
+ async check(ctx) {
29
+ const worktree = ctx.taskRecord?.worktree_path
30
+ const checkRoot =
31
+ typeof worktree === 'string' && worktree && existsSync(worktree) ? worktree : ctx.sharedRoot
32
+ const base = ctx.taskRecord?.base_branch ?? 'main'
33
+ let result
34
+ try {
35
+ result = checkReconciled({ root: checkRoot, base, taskId: ctx.taskId })
36
+ } catch (e) {
37
+ return {
38
+ status: 'fail',
39
+ reason: `features-reconcile check threw: ${e.message}`,
40
+ evidence: { checkRoot, base },
41
+ }
42
+ }
43
+ if (result.status === 'skip') {
44
+ return {
45
+ status: 'skip',
46
+ reason: `features-reconcile skipped: ${result.reason} (docs_policy opt-out)`,
47
+ evidence: { checkRoot, policy: result.policy ?? null },
48
+ }
49
+ }
50
+ if (result.status === 'ok') {
51
+ return {
52
+ status: 'pass',
53
+ reason: `feature registry reconciled (${result.qualifying_files?.length ?? 0} qualifying file(s) covered; schema valid)`,
54
+ evidence: { checkRoot, base: result.base },
55
+ }
56
+ }
57
+ const findings = result.findings ?? []
58
+ return {
59
+ status: 'fail',
60
+ reason:
61
+ `feature registry NOT reconciled — ${findings.length} finding(s). ` +
62
+ `Repair: ${draftRepairCommand(ctx.taskId)} (fill dispositions), then ` +
63
+ `node packages/framework/bin/apt-tools.mjs features-reconcile . --write --task-id ${ctx.taskId}`,
64
+ evidence: { checkRoot, base: result.base, findings },
65
+ }
66
+ },
67
+ }
68
+
69
+ registerGate(gate)
70
+ export default gate
@@ -18,6 +18,7 @@ import './verify-approved.mjs'
18
18
  import './review-clean.mjs'
19
19
  import './followups-adjudicated.mjs'
20
20
  import './gitignore-in-sync.mjs'
21
+ import './features-reconciled.mjs'
21
22
  import './consistency-check.mjs'
22
23
  import './tdd-iron-law.mjs'
23
24
  import './r-persona-alignment.mjs'
package/src/cli/help.mjs CHANGED
@@ -29,6 +29,7 @@ Commands:
29
29
  risk-check <dir> --paths <p1> [--operation <type>] Classify file paths by risk level
30
30
 
31
31
  task create <dir> --description <text> [--scope <s>] [--milestone <id>] [--phase <id>] [--roadmap-ref-url <url>] [--depends-on <refs>] [--track <T>] [--autonomy <N>] Create isolated task
32
+ task ensure <dir> --description <text> --intent <create-new|require-existing|self-managed|none> [--track <T>] [--autonomy <N>] [--scope <s>] [--id <id>] Deterministic idempotent allocator: create-new provisions a per-task worktree (idempotent on re-run), require-existing resolves the active task without minting, none/self-managed allocate nothing
32
33
  task list <dir> [--scope <s>] [--blocked-by <id>] List active tasks (optionally filtered)
33
34
  task get <dir> --id <task-id> Get task details and artifacts
34
35
  task update <dir> --id <id> [--lifecycle-phase <p>] [--pr-url <url>] [--subtasks-total <N>] [--depends-on <refs>] Update non-linkage task state; --pr-url may arm ci-watch per ci_watch.after_ship
@@ -37,7 +38,7 @@ Commands:
37
38
  task close-merged <dir> [--task <id>] [--narrate-only] [--dry-run] Sweep shipped-pending-merge tasks for confirmed merges (--narrate-only returns pending_narration[] entries read-only without closing).
38
39
  task narration-drain <dir> --task <task-id> Remove a row from state.pending_narration[] under withFileLock (called by /apt:close-task --narrate-only after a successful narrator spawn)
39
40
  task complete-subtask <dir> --task-id <id> --subtask-id <sid|a+b+c> [--commit <sha>] [--notes <text>] Atomic plan ↔ build-progress sync
40
- features-audit <dir> [--task-id <id>] [--no-worktree-redirect] [--diff-files <csv>|--since <ref>] [--apply-stubs] [--format json|md] Audit features registry for unregistered feature-qualifying files (BUG-025b: --task-id redirects writes to active task's worktree)
41
+ features-reconcile <dir> (--draft|--write|--check|--gc) [--task-id <id>] [--subtask-id <id>] [--base <ref>] [--dry-run] [--warn-only] [--max-age-days <n>] [--no-worktree-redirect] Feature-registry delta ledger: --draft scaffolds a per-writer delta from the base..HEAD diff, --write materializes deltas into area JSONs + a receipt, --check verifies coverage/schema/integrity/orphans, --gc prunes merged receipts (BUG-025b: --task-id redirects writes to active task's worktree)
41
42
  docs-audit <dir> [--diff-files <csv>|--since <ref>|--dry-run] [--policy <path>] [--format json|md] Audit documented surfaces for docs-drift
42
43
 
43
44
  lock claim <dir> --subtask <id> Claim exclusive lock on a subtask
@@ -136,6 +137,35 @@ Commands:
136
137
  ci-watch start|stop|status|tick <dir> [--pr <N>] [...] /apt:watch-ci primitives — see SKILL.md for full flag matrix
137
138
  ci-watch sweep <dir> --orphaned [--dry-run] BUG-027: enumerate ci-watches state files, query gh per PR, remove terminal-state (MERGED|CLOSED) entries (manual GC; rate-limited)
138
139
 
140
+ produce start <dir> --outcome "<o>" [--run-id <id>] /apt:produce M1 — freeze an immutable acceptance setpoint under .aperant/produce/<run-id>/ (idempotent-reuse on --run-id, never overwrites); emits produce.run.started + produce.setpoint.frozen
141
+ produce add-milestone <dir> --run-id <id> --title "<t>" [--id <mid>] Append produce.roadmap.milestone_added + refresh the derived roadmap.json projection cache
142
+ produce set-milestone-status <dir> --run-id <id> --milestone <mid> --status <planned|in_progress|done> [--verdict-path <p>] Append produce.roadmap.milestone_status_changed; --status done is the M2 two-gate adjudication (visible oracle AND blind probe)
143
+ produce status <dir> --run-id <id> Re-project the roadmap from the append-only event log (no in-memory authority)
144
+ produce tick <dir> --run-id <id> Run exactly ONE wake tick — re-project from disk, emit produce.tick, compute next_action + next_tick_hint, then exit (no loop)
145
+ produce record-implementer <dir> --run-id <id> --milestone <mid> --implementer-family <fam> /apt:produce M2 — record the implementer family (the cross-family disjointness anchor)
146
+ produce resolve-probe-family <dir> --run-id <id> [--milestone <mid>] [--implementer-family <fam>] [--host-family <fam>] [--available-families a,b,c] M2 — resolve a probe/oracle family DISJOINT from the implementer + a LIVE model (never hardcoded); available:false when none
147
+ produce oracle prepare <dir> --run-id <id> --milestone <mid> --verdict-path <p> M2 — extract verify-proof's v2 verdict (must be approved) + tests[] evidence into a cross-family judge payload
148
+ produce oracle record <dir> --run-id <id> --milestone <mid> --verdict <pass|fail> --oracle-family <fam> M2 — record produce.oracle.judged (refused when oracle_family === implementer)
149
+ produce probe freeze <dir> --run-id <id> --milestone <mid> --probe-file <p> --author-family <fam> M2 — freeze an immutable held-out probe set (hash + family only; refused when author_family === implementer)
150
+ produce probe record <dir> --run-id <id> --milestone <mid> --result <pass|fail> --probe-family <fam> --head-sha <sha> M2 — record produce.probe.run (result/family/sha only; refused when probe_family === implementer)
151
+ produce probe sweep <dir> --run-id <id> [--sample N] M2 — deterministic rotating sample of already-done milestones to blind-re-probe against HEAD
152
+ produce canary <dir> --run-id <id> M2 — green-on-visible MINUS green-on-blind + per-milestone breakdown (the reward-hacking canary)
153
+ produce declare-criteria <dir> --run-id <id> --milestone <mid> --criterion "<text>" [--criterion-id <cid>] [--scope-change "<reason>"] /apt:produce M3 — open one acceptance criterion (V += 1); rejected after descent unless --scope-change justifies the increase (anti-windup)
154
+ produce progress <dir> --run-id <id> M3 — re-read the Lyapunov V-series from the log: open-criteria count strictly decreasing across shipped milestones + any un-sanctioned-increase violations (flags, never rejects)
155
+ produce saga enter <dir> --run-id <id> --milestone <mid> --phase <plan|execute|verify|review|ship> /apt:produce M4 — append produce.saga.phase_entered (separate fold from the roadmap projection)
156
+ produce saga complete <dir> --run-id <id> --milestone <mid> --phase <plan|execute|verify|review|ship> M4 — append produce.saga.phase_completed
157
+ produce saga compensate <dir> --run-id <id> --milestone <mid> --phase <p> [--reason "<r>"] M4 — fire the FORWARD compensator (verify|review|ship ships a revert + reopens + re-plans; NEVER rolls back a merged PR); records the descriptor, does not execute it
158
+ produce saga resolve-reviewer <dir> --run-id <id> --milestone <mid> [--host-family <fam>] [--available-families a,b,c] M4 — resolve a REVIEW-phase reviewer family DISJOINT from the implementer + a LIVE model (reuses resolveProbeFamily; never hardcoded); available:false when none
159
+ produce saga review <dir> --run-id <id> --milestone <mid> --verdict <pass|fail> --reviewer-family <fam> M4 — record produce.saga.reviewed (refused when reviewer_family === implementer)
160
+ produce saga status <dir> --run-id <id> M4 — re-project per-milestone saga phase state + pending compensation from the log (no roadmap status mutation)
161
+ produce ttl select <dir> --run-id <id> --head-sha <sha> [--sample N] M4 — deterministic rotating sample of OLD done milestones STALE against HEAD (reuses the M2 rotation; a milestone re-verified against current HEAD is excluded as fresh)
162
+ produce ttl reverify <dir> --run-id <id> --milestone <mid> --result <pass|fail> --probe-family <fam> --head-sha <sha> M4 — cross-family re-verify; --result fail appends ttl.reverified + ttl.regression_detected + fires the FORWARD compensator (ships a revert, not a rollback)
163
+ produce schedule <dir> --run-id <id> [--wip N] /apt:produce M5 — WSJF-rank the ready milestones (cost-of-delay / job-size) + a WIP-limited pick; dependency-aware readiness (blocked / cycle), M4-regression re-entry; read-only
164
+ produce claim declare <dir> --run-id <id> --profile <p> --claim-file <path> [--claim-id <id>] /apt:produce M6 — validate + freeze an immutable Claim Envelope to claims/<claim-id>.json (interop, not a 4th store); rejects a global/unbounded scope, empty preconditions, or missing expiry fail-closed; logs produce.claim.declared (content_hash only)
165
+ produce claim list <dir> --run-id <id> M6 — re-project the claim ledger (declared − expired) folded from the append-only log; produce.claim.* never enters the roadmap projection
166
+ produce claim expire <dir> --run-id <id> --claim-id <id> [--reason "<r>"] M6 — append produce.claim.expired; the claim drops out of the ledger's active set (anti-corrosion manual expiry)
167
+ produce claim consume <dir> --run-id <id> [--milestone <mid>] M6 — emit the STRUCTURED delta {claim_id, targets:[{kind, requirement, ref?}]} derived from resolution_verifier (never a lesson-text paste); skips expired / HEAD-invalidated / precondition-unmatched claims
168
+
139
169
  followups check <dir> --task-id <id> G45: derive the expected follow-up universe (Out of Scope ∪ raw review rows ∪ qa findings) + report missing/unadjudicated/unfixed/undispositioned rows; exit 1 when the ledger is dishonest
140
170
  followups disposition <dir> --task-id <id> --row <row-id> --kind <filed_task|executed_commit|parked_note|dropped_by_user> --ref <ref> [--reason <text>] G45: flip a ledger row to dispositioned (atomic; dropped_by_user requires --reason)
141
171
  followups policy <dir> --task-id <id> G45: effective disposition policy (ask|file|chain + max_chain_depth) from config.followups when set, else autonomy-derived
@@ -108,6 +108,7 @@ export const LEGACY_INSTALL_PATHS = Object.freeze({
108
108
  'agents/apt-personas.md',
109
109
  'agents/apt-plan.md',
110
110
  'agents/apt-pr-review.md',
111
+ 'agents/apt-produce.md',
111
112
  'agents/apt-prototype.md',
112
113
  'agents/apt-quick.md',
113
114
  'agents/apt-release-notes.md',
@@ -153,6 +154,7 @@ export const LEGACY_INSTALL_PATHS = Object.freeze({
153
154
  'commands/apt-personas.md',
154
155
  'commands/apt-plan.md',
155
156
  'commands/apt-pr-review.md',
157
+ 'commands/apt-produce.md',
156
158
  'commands/apt-prototype.md',
157
159
  'commands/apt-quick.md',
158
160
  'commands/apt-release-notes.md',