@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,277 @@
1
+ /**
2
+ * commands/triage.mjs — `apt-tools triage` (Pocock adoption ST-10, AC9).
3
+ *
4
+ * 5-state triage state machine with extensible metadata for v2 OSS
5
+ * contributor primitives. Backend-configured: writes to
6
+ * `.aperant/tasks/{task-id}/triage.json` by default (local-only); other
7
+ * backends (github-issues, app-inbox) ship as not-implemented stubs.
8
+ *
9
+ * Subcommands:
10
+ * apt-tools triage init <project-dir> --id <task-id> [--category bug|enhancement] [--extra '{...}']
11
+ * apt-tools triage transition <project-dir> --id <task-id> --to <state> [--reason <text>] [--by user|agent]
12
+ * apt-tools triage get <project-dir> --id <task-id>
13
+ *
14
+ * States: needs-triage | needs-info | ready-for-agent | ready-for-human | wontfix
15
+ * Categories: bug | enhancement
16
+ *
17
+ * Fast Path Guarantee (ID-05): QUICK-routed tasks NEVER enter triage.
18
+ * The CLI does NOT enforce this directly — the apt:triage SKILL.md
19
+ * routes around QUICK tasks before invoking the CLI. The CLI is the
20
+ * mechanical state-machine surface; the routing is the skill body's job.
21
+ */
22
+
23
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
24
+ import { dirname, join, resolve } from 'node:path'
25
+ import { parseFlags } from '../util/args.mjs'
26
+ import { err, exitWith, ok } from '../util/result.mjs'
27
+
28
+ const STATES = ['needs-triage', 'needs-info', 'ready-for-agent', 'ready-for-human', 'wontfix']
29
+
30
+ const CATEGORIES = ['bug', 'enhancement']
31
+
32
+ // The 8 valid transitions per spec.md AC9. Anything else is rejected.
33
+ const VALID_TRANSITIONS = new Set([
34
+ 'needs-triage->needs-info',
35
+ 'needs-triage->ready-for-agent',
36
+ 'needs-triage->ready-for-human',
37
+ 'needs-triage->wontfix',
38
+ 'needs-info->ready-for-agent',
39
+ 'needs-info->ready-for-human',
40
+ 'needs-info->wontfix',
41
+ 'needs-info->needs-triage',
42
+ ])
43
+
44
+ const BACKENDS = new Set(['local-only', 'github-issues', 'app-inbox'])
45
+
46
+ /**
47
+ * Read .aperant/config.json and extract task_tracking.backend.
48
+ * Defaults to 'local-only' if the file or the key is missing.
49
+ *
50
+ * @param {string} absProjectDir
51
+ * @returns {string}
52
+ */
53
+ function readBackend(absProjectDir) {
54
+ const configPath = join(absProjectDir, '.aperant', 'config.json')
55
+ if (!existsSync(configPath)) return 'local-only'
56
+ try {
57
+ const parsed = JSON.parse(readFileSync(configPath, 'utf-8'))
58
+ const backend = parsed?.task_tracking?.backend
59
+ if (typeof backend === 'string' && BACKENDS.has(backend)) return backend
60
+ return 'local-only'
61
+ } catch {
62
+ return 'local-only'
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Triage record file path.
68
+ *
69
+ * @param {string} absProjectDir
70
+ * @param {string} taskId
71
+ */
72
+ function triagePath(absProjectDir, taskId) {
73
+ return join(absProjectDir, '.aperant', 'tasks', taskId, 'triage.json')
74
+ }
75
+
76
+ /**
77
+ * Read the current triage record, or null if it doesn't exist.
78
+ *
79
+ * @param {string} path
80
+ */
81
+ function readTriage(path) {
82
+ if (!existsSync(path)) return null
83
+ try {
84
+ return JSON.parse(readFileSync(path, 'utf-8'))
85
+ } catch {
86
+ return null
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Write a triage record.
92
+ *
93
+ * @param {string} path
94
+ * @param {object} record
95
+ */
96
+ function writeTriage(path, record) {
97
+ const dir = dirname(path)
98
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
99
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`)
100
+ }
101
+
102
+ /**
103
+ * Apply the backend mirror step. local-only writes to disk; the other
104
+ * backends return a not-implemented envelope so callers can handle the
105
+ * stub gracefully.
106
+ *
107
+ * @param {string} backend
108
+ * @param {string} absProjectDir
109
+ * @param {string} taskId
110
+ * @param {object} record
111
+ * @returns {null | { status: 'not-implemented', backend: string, v2_issue: string }}
112
+ */
113
+ function mirrorToBackend(backend, absProjectDir, taskId, record) {
114
+ if (backend === 'local-only') {
115
+ writeTriage(triagePath(absProjectDir, taskId), record)
116
+ return null
117
+ }
118
+ // v1 stubs: github-issues and app-inbox return not-implemented.
119
+ return { status: 'not-implemented', backend, v2_issue: '#TBD' }
120
+ }
121
+
122
+ /**
123
+ * Entry: `apt-tools triage <subcommand> <project-dir> [flags]`.
124
+ */
125
+ export function cmdTriage(subcommand, projectDir, extraArgs) {
126
+ if (!subcommand || !['init', 'transition', 'get'].includes(subcommand)) {
127
+ return err(
128
+ 'Usage: apt-tools triage <init|transition|get> <project-dir> --id <task-id> [...]',
129
+ 1,
130
+ { command: 'triage' },
131
+ )
132
+ }
133
+ if (!projectDir) {
134
+ return err('project-dir is required', 1, { command: `triage-${subcommand}` })
135
+ }
136
+ const absProjectDir = resolve(projectDir)
137
+ if (!existsSync(absProjectDir)) {
138
+ return err(`project directory not found: ${absProjectDir}`, 1, {
139
+ command: `triage-${subcommand}`,
140
+ })
141
+ }
142
+
143
+ const flags = parseFlags(extraArgs ?? [])
144
+ const taskId = flags.get('id')
145
+ if (typeof taskId !== 'string' || taskId.trim() === '') {
146
+ return err('--id <task-id> is required', 1, { command: `triage-${subcommand}` })
147
+ }
148
+ const TASK_ID_RE = /^[0-9]{2}-[0-9]{2}-[0-9]{2}_[a-z0-9-]+-[a-z0-9]{4}$/
149
+ if (!TASK_ID_RE.test(taskId)) {
150
+ return err(`--id must match pattern <YY-MM-DD_slug-XXXX> (e.g. 24-01-15_fix-login-a3b2)`, 1, {
151
+ command: `triage-${subcommand}`,
152
+ })
153
+ }
154
+ const backend = readBackend(absProjectDir)
155
+
156
+ if (subcommand === 'init') {
157
+ // Optional --category, optional --extra (JSON string).
158
+ const category = flags.get('category')
159
+ if (category !== undefined && !CATEGORIES.includes(String(category))) {
160
+ return err(`--category must be one of: ${CATEGORIES.join(' | ')}`, 1, {
161
+ command: 'triage-init',
162
+ })
163
+ }
164
+ let extra = {}
165
+ const extraRaw = flags.get('extra')
166
+ if (typeof extraRaw === 'string' && extraRaw.length > 0) {
167
+ try {
168
+ const parsed = JSON.parse(extraRaw)
169
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
170
+ extra = parsed
171
+ } else {
172
+ return err('--extra must be a JSON object string', 1, { command: 'triage-init' })
173
+ }
174
+ } catch (e) {
175
+ return err(`--extra is not valid JSON: ${e.message}`, 1, { command: 'triage-init' })
176
+ }
177
+ }
178
+
179
+ const existing = readTriage(triagePath(absProjectDir, taskId))
180
+ if (existing) {
181
+ return err(`triage record already exists for ${taskId}`, 1, {
182
+ command: 'triage-init',
183
+ current_state: existing.state,
184
+ })
185
+ }
186
+
187
+ const record = {
188
+ task_id: taskId,
189
+ state: 'needs-triage',
190
+ category: typeof category === 'string' ? category : null,
191
+ transitions: [],
192
+ extra,
193
+ }
194
+ const stub = mirrorToBackend(backend, absProjectDir, taskId, record)
195
+ if (stub) return exitWith({ command: 'triage-init', ...stub }, 0)
196
+ return ok({ status: 'ok', command: 'triage-init', backend, record })
197
+ }
198
+
199
+ if (subcommand === 'transition') {
200
+ const toState = flags.get('to')
201
+ if (typeof toState !== 'string' || !STATES.includes(toState)) {
202
+ return err(`--to <state> is required (one of: ${STATES.join(' | ')})`, 1, {
203
+ command: 'triage-transition',
204
+ })
205
+ }
206
+ // SEC-003: cap --reason to 1000 chars to prevent unbounded disk writes.
207
+ const rawReason = flags.get('reason')
208
+ const reason = typeof rawReason === 'string' ? rawReason.slice(0, 1000) : null
209
+ const by =
210
+ typeof flags.get('by') === 'string' && ['user', 'agent'].includes(String(flags.get('by')))
211
+ ? String(flags.get('by'))
212
+ : 'agent'
213
+
214
+ const current = readTriage(triagePath(absProjectDir, taskId))
215
+ if (!current) {
216
+ return err(`no triage record for ${taskId} — run \`triage init\` first`, 1, {
217
+ command: 'triage-transition',
218
+ })
219
+ }
220
+
221
+ const from = current.state
222
+ const transitionKey = `${from}->${toState}`
223
+ if (!VALID_TRANSITIONS.has(transitionKey)) {
224
+ return exitWith(
225
+ {
226
+ status: 'rejected',
227
+ command: 'triage-transition',
228
+ reason: 'invalid-transition',
229
+ from,
230
+ to: toState,
231
+ valid_from_current: STATES.filter((s) => VALID_TRANSITIONS.has(`${from}->${s}`)),
232
+ },
233
+ 1,
234
+ )
235
+ }
236
+
237
+ const transition = {
238
+ from,
239
+ to: toState,
240
+ at: new Date().toISOString(),
241
+ by,
242
+ reason,
243
+ }
244
+ // SEC-002: Only carry known fields forward — do NOT spread the full
245
+ // disk-backed `current` object, which could contain arbitrary unknown
246
+ // keys from a hand-edited triage.json that would silently propagate.
247
+ const next = {
248
+ task_id: current.task_id,
249
+ state: toState,
250
+ category: current.category,
251
+ extra: current.extra ?? {},
252
+ transitions: [...(current.transitions || []), transition],
253
+ }
254
+ const stub = mirrorToBackend(backend, absProjectDir, taskId, next)
255
+ if (stub) return exitWith({ command: 'triage-transition', ...stub }, 0)
256
+ return ok({
257
+ status: 'ok',
258
+ command: 'triage-transition',
259
+ backend,
260
+ transition,
261
+ record: next,
262
+ })
263
+ }
264
+
265
+ // subcommand === 'get'
266
+ const path = triagePath(absProjectDir, taskId)
267
+ const record = readTriage(path)
268
+ if (!record) {
269
+ return exitWith({ status: 'not-found', command: 'triage-get', task_id: taskId, file: path }, 0)
270
+ }
271
+ return ok({ status: 'ok', command: 'triage-get', record, backend })
272
+ }
273
+
274
+ // Exported for testing.
275
+ export const _STATES = STATES
276
+ export const _CATEGORIES = CATEGORIES
277
+ export const _VALID_TRANSITIONS = VALID_TRANSITIONS
@@ -2,6 +2,16 @@
2
2
  * post-merge-sweep.mjs — dispatch-time passive sweeper for stuck
3
3
  * `shipped-pending-merge` tasks (C57 Piece 2).
4
4
  *
5
+ * **Bookkeeping-only contract.** This hook runs the deterministic
6
+ * close half of the post-merge handoff: poll `gh pr view`, flip
7
+ * lifecycle_phase from `shipped-pending-merge` to `completed` via
8
+ * the same `task close` path, strip per-PR authorship overrides,
9
+ * and append a `pending_narration[]` ledger row. The LLM-side
10
+ * narration work (registry sync + ROADMAP prose) is explicitly
11
+ * out of scope here — the user drains the ledger via
12
+ * `/apt:close-task --narrate-only` (see packages/framework/skills/
13
+ * apt-close-task/SKILL.md §4) which is where those side effects belong.
14
+ *
5
15
  * AI agents ship a PR and walk away. Tasks then rot in
6
16
  * `shipped-pending-merge` because nothing automatic invokes
7
17
  * `apt-tools task close-merged`. This hook runs on every `apt-tools`
@@ -11,8 +21,10 @@
11
21
  * whose PRs have terminally landed.
12
22
  *
13
23
  * Non-blocking: never fails the command, never mutates exit code,
14
- * never throws. Single stderr line on actual sweep kick (not on
15
- * every dispatch).
24
+ * never throws. Two stderr lines max per dispatch: the
25
+ * `Auto-sweeping N pending PR(s)` line on actual sweep kick, plus
26
+ * the `Post-merge narration pending` banner when the ledger has
27
+ * un-drained rows. Both lines are gated by `APT_SUPPRESS_MERGE_SWEEP=1`.
16
28
  *
17
29
  * Silenced by `APT_SUPPRESS_MERGE_SWEEP=1`. Skipped when
18
30
  * `APT_INTERNAL_MERGE_SWEEP=1` is set in env (recursion guard for
@@ -90,6 +102,25 @@ export function readShippedCandidates(projectRoot) {
90
102
  return candidates.length === 0 ? null : candidates
91
103
  }
92
104
 
105
+ /**
106
+ * Pure preflight — returns the integer length of state.pending_narration[].
107
+ * Returns 0 on read/parse failure or when the ledger is absent (the banner
108
+ * is suppressed in that case).
109
+ *
110
+ * @internal
111
+ */
112
+ export function readPendingNarrationCount(projectRoot) {
113
+ const statePath = join(projectRoot, '.aperant', 'state.json')
114
+ if (!existsSync(statePath)) return 0
115
+ try {
116
+ const state = JSON.parse(readFileSync(statePath, 'utf-8'))
117
+ const ledger = state?.pending_narration
118
+ return Array.isArray(ledger) ? ledger.length : 0
119
+ } catch {
120
+ return 0
121
+ }
122
+ }
123
+
93
124
  /**
94
125
  * Returns true if the cooldown lock is "fresh" (last sweep too recent).
95
126
  * Lock content is `{pid}:{ms-since-epoch}` but we only consult mtime.
@@ -221,9 +252,24 @@ export function maybeSweepShippedMergedTasks(projectRoot, command, options = {})
221
252
  return { kicked: false, reason: 'no-aperant-dir' }
222
253
  }
223
254
 
255
+ // Narration-pending banner — fires on EVERY dispatch cycle when
256
+ // the ledger has un-drained rows, regardless of whether a sweep is
257
+ // kicked below. Cheap read, no spawn, no cooldown. Same suppression
258
+ // env as the sweep banner (APT_SUPPRESS_MERGE_SWEEP=1 hides both).
259
+ const pendingNarration = readPendingNarrationCount(root)
260
+ if (pendingNarration > 0) {
261
+ try {
262
+ process.stderr.write(
263
+ `[apt-tools] Post-merge narration pending (${pendingNarration}) — run /apt:close-task --narrate-only to refresh features registry + ROADMAP.\n`,
264
+ )
265
+ } catch {
266
+ /* stderr write failure is non-fatal */
267
+ }
268
+ }
269
+
224
270
  const candidates = readShippedCandidates(root)
225
271
  if (!candidates) {
226
- return { kicked: false, reason: 'no-candidates' }
272
+ return { kicked: false, reason: 'no-candidates', pending_narration: pendingNarration }
227
273
  }
228
274
 
229
275
  const lockPath = join(root, '.aperant', '.merge-sweep.lock')
@@ -82,3 +82,17 @@ export function getRule(id) {
82
82
  export function __resetRegistryForTests() {
83
83
  registry.clear()
84
84
  }
85
+
86
+ /**
87
+ * Test-only helper — uniformly-named alias for the in-process cmd harness
88
+ * (run-cmd.mjs). Built-in rules self-register via the `rules/index.mjs`
89
+ * barrel at module-import time, which ESM evaluates exactly once per
90
+ * process — so the harness deliberately does NOT call this by default
91
+ * (clearing here would leave subsequent cmd invocations looking at an empty
92
+ * registry). Tests that need an empty registry import this name explicitly.
93
+ *
94
+ * @internal
95
+ */
96
+ export function __resetCaches() {
97
+ registry.clear()
98
+ }
@@ -100,3 +100,15 @@ export function getAgentIdentity(projectDir) {
100
100
  export function __resetAgentIdentityCacheForTests() {
101
101
  cache.clear()
102
102
  }
103
+
104
+ /**
105
+ * Test-only helper — uniformly-named alias for the in-process cmd harness
106
+ * (run-cmd.mjs), which imports `__resetCaches` from each cache host with a
107
+ * single consistent name. Behaviour is identical to
108
+ * `__resetAgentIdentityCacheForTests`.
109
+ *
110
+ * @internal
111
+ */
112
+ export function __resetCaches() {
113
+ cache.clear()
114
+ }
@@ -8,8 +8,19 @@
8
8
  * multiple processes running `task update` / `task close` cannot tear each
9
9
  * other's lines.
10
10
  *
11
- * This module is `@internal` — consumed only through the CoordinationStore
12
- * seam (store.appendEvent). Do NOT import from bin/apt-tools.mjs.
11
+ * Consumption policy:
12
+ * - WRITES go through the CoordinationStore seam (`store.appendEvent`).
13
+ * This is the policy boundary — concurrent writers must coordinate.
14
+ * - READ-ONLY helpers (`readLifecycleEventsForDay`,
15
+ * `findMostRecentEventForTask`, `readTaskEventsOverDays`) MAY be
16
+ * imported directly. They do not mutate state and the event log is
17
+ * append-only, so a stale read is the worst case and is bounded by
18
+ * "the next read picks up new appends." Direct read imports keep the
19
+ * R6 telemetry surface (PR #115) out of the store seam, which is
20
+ * scoped to coordination primitives.
21
+ *
22
+ * Do NOT import this module from bin/apt-tools.mjs directly. CLI commands
23
+ * import the helpers above via their command modules.
13
24
  *
14
25
  * The existing numbered-JSON event surface in commands/event.mjs is NOT
15
26
  * touched — its `rebuild-state` consumer keeps working. The JSONL log is
@@ -86,3 +97,63 @@ export function readLifecycleEventsForDay(projectRoot, day = todayBucket()) {
86
97
  }
87
98
  return out
88
99
  }
100
+
101
+ /**
102
+ * Read events for a task across the today + yesterday UTC day buckets,
103
+ * sorted ascending by `t`. Optionally filtered to events whose `op` starts
104
+ * with `opPrefix`. Covers tasks that span a UTC day boundary without
105
+ * globbing the whole `.aperant/events/` directory.
106
+ *
107
+ * Read-only — does NOT mutate state. Per the original `@internal` policy
108
+ * this module is reached via `store.appendEvent` for WRITES, but read-only
109
+ * helpers like this one (and `readLifecycleEventsForDay`) are direct
110
+ * imports because there is no state to coordinate (PR #115 review finding
111
+ * FIT-001 — comment updated accordingly).
112
+ *
113
+ * @internal
114
+ */
115
+ export function readTaskEventsOverDays(projectRoot, taskId, opPrefix = null) {
116
+ function filterAndPush(events, into) {
117
+ for (const e of events) {
118
+ if (e.task !== taskId) continue
119
+ if (opPrefix && e.op !== opPrefix && !e.op.startsWith(opPrefix)) continue
120
+ into.push(e)
121
+ }
122
+ }
123
+ const today = todayBucket()
124
+ const yesterday = new Date()
125
+ yesterday.setUTCDate(yesterday.getUTCDate() - 1)
126
+ const yest = yesterday.toISOString().slice(0, 10)
127
+ const out = []
128
+ // Yesterday first so the combined array stays chronological.
129
+ filterAndPush(readLifecycleEventsForDay(projectRoot, yest), out)
130
+ filterAndPush(readLifecycleEventsForDay(projectRoot, today), out)
131
+ return out
132
+ }
133
+
134
+ /**
135
+ * Find the most recent event matching `taskId`, optionally filtered to events
136
+ * whose `op` starts with `opPrefix`. Looks at today's bucket first; only
137
+ * reads yesterday's bucket on a today-miss — keeps the common case to a
138
+ * single file read.
139
+ *
140
+ * Used by R6 telemetry to compute `elapsed_ms` between lifecycle transitions
141
+ * (roundtable 2026-05-14). Returns null when no matching event is found.
142
+ *
143
+ * @internal
144
+ */
145
+ export function findMostRecentEventForTask(projectRoot, taskId, opPrefix = null) {
146
+ function search(events) {
147
+ const filtered = events.filter(
148
+ (e) => e.task === taskId && (!opPrefix || e.op === opPrefix || e.op.startsWith(opPrefix)),
149
+ )
150
+ return filtered.length > 0 ? filtered[filtered.length - 1] : null
151
+ }
152
+ const today = todayBucket()
153
+ const todayMatch = search(readLifecycleEventsForDay(projectRoot, today))
154
+ if (todayMatch) return todayMatch
155
+ const yesterday = new Date()
156
+ yesterday.setUTCDate(yesterday.getUTCDate() - 1)
157
+ const yest = yesterday.toISOString().slice(0, 10)
158
+ return search(readLifecycleEventsForDay(projectRoot, yest))
159
+ }
@@ -83,7 +83,9 @@ export const TypographySchema = z
83
83
  .object({
84
84
  families: TypographyFamiliesSchema,
85
85
  scale: z.array(PixelOrRem).min(3), // at least {body, heading, display}
86
- weights: z.array(z.number().int().positive()).optional(),
86
+ weights: z
87
+ .union([z.array(z.number().int().positive()), z.record(z.string(), z.number().positive())])
88
+ .optional(),
87
89
  lineHeight: z.record(z.string(), NonEmptyString).optional(),
88
90
  })
89
91
  .passthrough()
@@ -35,14 +35,17 @@
35
35
  */
36
36
 
37
37
  import { cmdActive } from './commands/active.mjs'
38
+ import { cmdAdr } from './commands/adr.mjs'
38
39
  import { cmdAudit } from './commands/audit.mjs'
39
40
  import { cmdAuthorSkill } from './commands/author-skill.mjs'
40
41
  import { cmdBootstrap } from './commands/bootstrap.mjs'
41
42
  import { cmdCheck } from './commands/check.mjs'
42
43
  import { cmdCheckVersion } from './commands/check-version.mjs'
44
+ import { cmdCiWatch } from './commands/ci-watch.mjs'
43
45
  import { cmdCommit } from './commands/commit.mjs'
44
46
  import { cmdConsistency } from './commands/consistency.mjs'
45
47
  import { cmdConstitution } from './commands/constitution.mjs'
48
+ import { cmdContext } from './commands/context.mjs'
46
49
  import { cmdCoverageCheck } from './commands/coverage-check.mjs'
47
50
  import { cmdCreateDocs } from './commands/create-docs.mjs'
48
51
  import { cmdDesignLint } from './commands/design-lint.mjs'
@@ -83,6 +86,7 @@ import { cmdTerminal } from './commands/terminal.mjs'
83
86
  import { cmdTokenBudget } from './commands/token-budget.mjs'
84
87
  import { cmdTokens } from './commands/tokens.mjs'
85
88
  import { cmdToolchainDetect } from './commands/toolchain-detect.mjs'
89
+ import { cmdTriage } from './commands/triage.mjs'
86
90
  import { cmdUninstall } from './commands/uninstall.mjs'
87
91
  import { cmdValidateEvidence } from './commands/validate-evidence.mjs'
88
92
  import { cmdVariantGallery } from './commands/variant-gallery.mjs'
@@ -231,7 +235,7 @@ export const DISPATCH = {
231
235
  'author-skill': (args) => wrap(cmdAuthorSkill)(args[1], args.slice(2)),
232
236
  'toolchain-detect': (args) => wrap(cmdToolchainDetect)(args[1], args.slice(2)),
233
237
  'host-detect': () => wrap(cmdHostDetect)(),
234
- reconcile: (args) => wrap(cmdReconcile)(args[1]),
238
+ reconcile: (args) => wrap(cmdReconcile)(args[1], args.slice(2)),
235
239
  standards: (args) => wrap(cmdStandards)(args[1], args.slice(2)),
236
240
  'precompact-hook': (args) => wrap(cmdPrecompactHook)(args[1]),
237
241
  'migrate-pr-reviews': (args) => wrap(cmdMigratePrReviews)(args[1]),
@@ -255,8 +259,18 @@ export const DISPATCH = {
255
259
  seed: (args) => wrap(cmdSeed)(args[1], args[2], args.slice(3)),
256
260
  workflow: (args) => wrap(cmdWorkflow)(args[1], args[2], args.slice(3)),
257
261
  'pr-review': (args) => wrap(cmdPrReview)(args[1], args[2], args.slice(3)),
262
+ 'ci-watch': (args) => wrap(cmdCiWatch)(args[1], args[2], args.slice(3)),
258
263
  'plan-approval': (args) => wrap(cmdPlanApproval)(args[1], args[2], args.slice(3)),
259
264
 
265
+ // ---- Pocock adoption (ST-5, ST-6, ST-10, ST-13) artifact writers + discoverability ----
266
+ context: (args) => wrap(cmdContext)(args[1], args[2], args.slice(3)),
267
+ adr: (args) => wrap(cmdAdr)(args[1], args[2], args.slice(3)),
268
+ triage: (args) => wrap(cmdTriage)(args[1], args[2], args.slice(3)),
269
+ modes: async (args) => {
270
+ const { cmdModes } = await import('./commands/modes.mjs')
271
+ return wrap(cmdModes)(args[1], args[2], args.slice(3))
272
+ },
273
+
260
274
  // ---- async cmds — wrap() awaits internally; thrown errors → fail() ----
261
275
  'docs-audit': (args) => wrap(cmdDocsAudit)(args[1], args.slice(2)),
262
276
  gate: (args) => wrap(cmdGate)(args[1], args[2], args.slice(3)),
@@ -101,3 +101,17 @@ export function getGate(id) {
101
101
  export function __resetRegistryForTests() {
102
102
  registry.clear()
103
103
  }
104
+
105
+ /**
106
+ * Test-only helper — uniformly-named alias for the in-process cmd harness
107
+ * (run-cmd.mjs). Built-in gates self-register via the `gates/index.mjs`
108
+ * barrel at module-import time, which ESM evaluates exactly once per
109
+ * process — so the harness deliberately does NOT call this by default
110
+ * (clearing here would leave subsequent cmd invocations looking at an empty
111
+ * registry). Tests that need an empty registry import this name explicitly.
112
+ *
113
+ * @internal
114
+ */
115
+ export function __resetCaches() {
116
+ registry.clear()
117
+ }
package/src/cli/help.mjs CHANGED
@@ -31,6 +31,7 @@ Commands:
31
31
  task update <dir> --id <id> [--lifecycle-phase <p>] [--subtasks-total <N>] [--depends-on <refs>] Update non-linkage task state
32
32
  task move <dir> --id <id> [--to-scope <s>] [--to-milestone <id>] [--to-phase <id>] [--to-roadmap-ref-url <url>] [--reason <text>] [--force] Reassign scope/milestone/phase with history
33
33
  task close <dir> --id <task-id> [--verdict <v>] Close task and move to completed
34
+ 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)
34
35
  task complete-subtask <dir> --task-id <id> --subtask-id <sid|a+b+c> [--commit <sha>] [--notes <text>] Atomic plan ↔ build-progress sync
35
36
  features-audit <dir> [--diff-files <csv>|--since <ref>] [--apply-stubs] [--format json|md] Audit features registry for unregistered feature-qualifying files
36
37
  docs-audit <dir> [--diff-files <csv>|--since <ref>|--dry-run] [--policy <path>] [--format json|md] Audit documented surfaces for docs-drift
@@ -193,6 +193,11 @@ export function detectHost(opts = {}) {
193
193
  worktrees: isClaudeCode,
194
194
  background_agents: isClaudeCode || isCodex,
195
195
  structured_prompts: structuredPrompts,
196
+ // D-03 (post-ship CI watcher) — true for hosts that ship a native
197
+ // recurring-task primitive the watcher can delegate to (CC's CronCreate,
198
+ // Pi's pi-schedule-prompt). False elsewhere; on those hosts the user
199
+ // re-invokes `/apt:watch-ci --tick --pr N` manually.
200
+ cron: isClaudeCode || cli === 'pi',
196
201
  // True when the framework can ask Electron to spawn a sibling Aperant
197
202
  // terminal (with its own worktree + CLI) over the Unix-domain IPC socket.
198
203
  // Requires TERM_PROGRAM=Aperant + APERANT_IPC_SOCKET to be set by the