@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,244 @@
1
+ /**
2
+ * commands/context.mjs — `apt-tools context write` (Pocock adoption ST-5, AC4).
3
+ *
4
+ * Upserts a term-block into the project's CONTEXT.md (the persistent
5
+ * project glossary). Format ported from Pocock's CONTEXT-FORMAT.md
6
+ * (see packages/framework/templates/context-format.md for the MIT
7
+ * attribution + field semantics).
8
+ *
9
+ * Idempotent: re-running with the same `--term` updates the existing
10
+ * block in place. Fields not explicitly overridden via flags are
11
+ * preserved from the prior block.
12
+ *
13
+ * CLI:
14
+ * apt-tools context write <project-dir>
15
+ * --term <bold-term>
16
+ * --definition <one-line>
17
+ * [--aliases-to-avoid alias1,alias2]
18
+ * [--cardinality 'one-to-many' | 'one-to-one' | 'many-to-many']
19
+ * [--flagged-ambiguity 'note']
20
+ * [--example-dialogue 'example']
21
+ *
22
+ * Output: JSON envelope { status: 'ok' | 'error',
23
+ * command: 'context-write',
24
+ * file: <absolute path>,
25
+ * term: <term>,
26
+ * operation: 'created' | 'updated' }.
27
+ */
28
+
29
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
30
+ import { resolve } from 'node:path'
31
+ import { parseFlags } from '../util/args.mjs'
32
+ import { err, ok } from '../util/result.mjs'
33
+
34
+ const HEADER = '# Project glossary (CONTEXT.md)'
35
+ const HEADER_NOTE =
36
+ '<!-- Managed by `apt-tools context write`. Hand-edits are preserved across upserts. -->'
37
+
38
+ /**
39
+ * Parse a single term-block out of CONTEXT.md.
40
+ *
41
+ * A block looks like:
42
+ * ## {Term}
43
+ *
44
+ * {Definition line}
45
+ *
46
+ * - **Aliases to avoid:** ...
47
+ * - **Cardinality:** ...
48
+ * - **Flagged ambiguities:** ...
49
+ * - **Example dialogue:** ...
50
+ *
51
+ * Returns `{ definition, aliasesToAvoid, cardinality, flaggedAmbiguity, exampleDialogue }`
52
+ * with each field as a string or null.
53
+ *
54
+ * @param {string} blockText
55
+ */
56
+ function parseBlock(blockText) {
57
+ const lines = blockText.split('\n')
58
+ // First non-empty line after the ## header is the definition.
59
+ let definition = ''
60
+ let i = 0
61
+ while (i < lines.length && lines[i].trim() === '') i++
62
+ if (i < lines.length && !lines[i].startsWith('- ')) {
63
+ definition = lines[i].trim()
64
+ i++
65
+ }
66
+ const fields = {
67
+ aliasesToAvoid: null,
68
+ cardinality: null,
69
+ flaggedAmbiguity: null,
70
+ exampleDialogue: null,
71
+ }
72
+ for (; i < lines.length; i++) {
73
+ const line = lines[i]
74
+ const m = line.match(/^- \*\*([^*]+):\*\*\s*(.*)$/)
75
+ if (!m) continue
76
+ const key = m[1].trim()
77
+ const value = m[2].trim()
78
+ switch (key) {
79
+ case 'Aliases to avoid':
80
+ fields.aliasesToAvoid = value
81
+ break
82
+ case 'Cardinality':
83
+ fields.cardinality = value
84
+ break
85
+ case 'Flagged ambiguities':
86
+ fields.flaggedAmbiguity = value
87
+ break
88
+ case 'Example dialogue':
89
+ fields.exampleDialogue = value
90
+ break
91
+ }
92
+ }
93
+ return { definition, ...fields }
94
+ }
95
+
96
+ /**
97
+ * Render a term-block to markdown.
98
+ *
99
+ * @param {string} term
100
+ * @param {{
101
+ * definition: string,
102
+ * aliasesToAvoid: string|null,
103
+ * cardinality: string|null,
104
+ * flaggedAmbiguity: string|null,
105
+ * exampleDialogue: string|null,
106
+ * }} block
107
+ */
108
+ function renderBlock(term, block) {
109
+ const lines = [`## ${term}`, '', block.definition, '']
110
+ const fields = [
111
+ ['Aliases to avoid', block.aliasesToAvoid],
112
+ ['Cardinality', block.cardinality],
113
+ ['Flagged ambiguities', block.flaggedAmbiguity],
114
+ ['Example dialogue', block.exampleDialogue],
115
+ ]
116
+ for (const [key, value] of fields) {
117
+ if (value !== null && value !== undefined && value !== '') {
118
+ lines.push(`- **${key}:** ${value}`)
119
+ }
120
+ }
121
+ return lines.join('\n')
122
+ }
123
+
124
+ /**
125
+ * Find an existing block by term in the source markdown.
126
+ * Returns `{ start, end, prior }` or `null` if no such block exists.
127
+ * `start` and `end` are inclusive/exclusive offsets into `source`.
128
+ *
129
+ * @param {string} source
130
+ * @param {string} term
131
+ */
132
+ function findBlock(source, term) {
133
+ const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
134
+ const headerRe = new RegExp(`^##\\s+${escaped}\\s*$`, 'm')
135
+ const m = headerRe.exec(source)
136
+ if (!m) return null
137
+ const start = m.index
138
+ // Find the next ## or # header or EOF. LOG-004: including H1 prevents a
139
+ // manually-inserted `# Heading` from being absorbed into the block's content.
140
+ const tail = source.slice(m.index + m[0].length)
141
+ const nextRe = /^#{1,2}\s+/m
142
+ const nextMatch = nextRe.exec(tail)
143
+ const end = nextMatch ? m.index + m[0].length + nextMatch.index : source.length
144
+ const blockText = source.slice(m.index + m[0].length, end)
145
+ return { start, end, prior: parseBlock(blockText) }
146
+ }
147
+
148
+ /**
149
+ * CLI entry: `apt-tools context <subcommand> <project-dir> [flags]`.
150
+ *
151
+ * Only `write` is supported in v1; other subcommands return a usage error.
152
+ */
153
+ export function cmdContext(subcommand, projectDir, extraArgs) {
154
+ if (subcommand !== 'write') {
155
+ return err(
156
+ 'Usage: apt-tools context write <project-dir> --term <term> --definition <line> [--aliases-to-avoid ...] [--cardinality ...] [--flagged-ambiguity ...] [--example-dialogue ...]',
157
+ 1,
158
+ { command: 'context' },
159
+ )
160
+ }
161
+ if (!projectDir) {
162
+ return err(
163
+ 'Usage: apt-tools context write <project-dir> --term <term> --definition <line>',
164
+ 1,
165
+ {
166
+ command: 'context-write',
167
+ },
168
+ )
169
+ }
170
+
171
+ const flags = parseFlags(extraArgs ?? [])
172
+ const term = flags.get('term')
173
+ const definition = flags.get('definition')
174
+ if (typeof term !== 'string' || term.trim() === '') {
175
+ return err('--term <bold-term> is required', 1, { command: 'context-write' })
176
+ }
177
+ if (typeof definition !== 'string' || definition.trim() === '') {
178
+ return err('--definition <one-line> is required', 1, { command: 'context-write' })
179
+ }
180
+
181
+ const absProjectDir = resolve(projectDir)
182
+ if (!existsSync(absProjectDir)) {
183
+ return err(`project directory not found: ${absProjectDir}`, 1, {
184
+ command: 'context-write',
185
+ })
186
+ }
187
+
188
+ const contextPath = resolve(absProjectDir, 'CONTEXT.md')
189
+ let source = ''
190
+ if (existsSync(contextPath)) {
191
+ source = readFileSync(contextPath, 'utf-8')
192
+ } else {
193
+ source = `${HEADER}\n\n${HEADER_NOTE}\n\n`
194
+ }
195
+ // Ensure the file starts with the header even if it existed pre-Pocock.
196
+ if (!source.startsWith(HEADER)) {
197
+ source = `${HEADER}\n\n${HEADER_NOTE}\n\n${source}`
198
+ }
199
+
200
+ const existing = findBlock(source, term)
201
+ const incoming = {
202
+ definition,
203
+ aliasesToAvoid:
204
+ typeof flags.get('aliases-to-avoid') === 'string' ? flags.get('aliases-to-avoid') : null,
205
+ cardinality: typeof flags.get('cardinality') === 'string' ? flags.get('cardinality') : null,
206
+ flaggedAmbiguity:
207
+ typeof flags.get('flagged-ambiguity') === 'string' ? flags.get('flagged-ambiguity') : null,
208
+ exampleDialogue:
209
+ typeof flags.get('example-dialogue') === 'string' ? flags.get('example-dialogue') : null,
210
+ }
211
+
212
+ // Merge: prior block fields are preserved unless explicitly overridden.
213
+ const merged = existing
214
+ ? {
215
+ definition: incoming.definition, // definition always overrides
216
+ aliasesToAvoid: incoming.aliasesToAvoid ?? existing.prior.aliasesToAvoid,
217
+ cardinality: incoming.cardinality ?? existing.prior.cardinality,
218
+ flaggedAmbiguity: incoming.flaggedAmbiguity ?? existing.prior.flaggedAmbiguity,
219
+ exampleDialogue: incoming.exampleDialogue ?? existing.prior.exampleDialogue,
220
+ }
221
+ : incoming
222
+
223
+ const rendered = renderBlock(term, merged)
224
+ let updated
225
+ if (existing) {
226
+ // Replace the existing block (header + body) with the rendered one.
227
+ const before = source.slice(0, existing.start).replace(/\n+$/, '')
228
+ const after = source.slice(existing.end).replace(/^\n+/, '')
229
+ updated = `${before}\n\n${rendered}\n${after.length > 0 ? `\n${after}` : ''}`
230
+ } else {
231
+ // Append a new block at the end.
232
+ updated = `${source.replace(/\n+$/, '')}\n\n${rendered}\n`
233
+ }
234
+
235
+ writeFileSync(contextPath, updated)
236
+
237
+ return ok({
238
+ status: 'ok',
239
+ command: 'context-write',
240
+ file: contextPath,
241
+ term,
242
+ operation: existing ? 'updated' : 'created',
243
+ })
244
+ }
@@ -2,7 +2,7 @@
2
2
  * commands/event.mjs — append-only event log for coordination history (C29).
3
3
  */
4
4
 
5
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
5
+ import { appendFileSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
6
6
  import { hostname } from 'node:os'
7
7
  import { join, resolve } from 'node:path'
8
8
  import { readLifecycleEventsForDay } from '../coordination/event-log.mjs'
@@ -11,6 +11,47 @@ import { detectWorktree } from '../git/worktree-detect.mjs'
11
11
  import { parseFlags } from '../util/args.mjs'
12
12
  import { err, ok } from '../util/result.mjs'
13
13
 
14
+ // QUA-003 / PRF-002 — single source of truth for valid event types.
15
+ // VALID_EVENT_TYPES is the authoritative list; CI_WATCH_EVENT_TYPES is the O(1)
16
+ // membership Set used to short-circuit ci-watch.* events to the JSONL path (D-04).
17
+ const VALID_EVENT_TYPES = [
18
+ 'task-claimed',
19
+ 'task-completed',
20
+ 'task-failed',
21
+ 'wave-started',
22
+ 'wave-completed',
23
+ 'install.started',
24
+ 'install.ok',
25
+ 'install.failed',
26
+ 'ci-watch.armed',
27
+ 'ci-watch.tick',
28
+ 'ci-watch.fix-pushed',
29
+ 'ci-watch.stopped',
30
+ ]
31
+
32
+ const CI_WATCH_EVENT_TYPES = new Set([
33
+ 'ci-watch.armed',
34
+ 'ci-watch.tick',
35
+ 'ci-watch.fix-pushed',
36
+ 'ci-watch.stopped',
37
+ ])
38
+
39
+ /**
40
+ * Append a ci-watch event to the per-day JSONL file (D-04).
41
+ * O(1) — no directory scan. Creates the events dir if missing.
42
+ * @param {string} eventsDir
43
+ * @param {object} payload
44
+ * @returns {{ jsonlFile: string, jsonlPath: string }}
45
+ */
46
+ function appendToJsonl(eventsDir, payload) {
47
+ mkdirSync(eventsDir, { recursive: true })
48
+ const today = new Date().toISOString().slice(0, 10)
49
+ const jsonlFile = `${today}.jsonl`
50
+ const jsonlPath = join(eventsDir, jsonlFile)
51
+ appendFileSync(jsonlPath, `${JSON.stringify(payload)}\n`, 'utf-8')
52
+ return { jsonlFile, jsonlPath }
53
+ }
54
+
14
55
  export function cmdEvent(subcommand, projectDir, extraArgs) {
15
56
  if (!subcommand)
16
57
  return err('Usage: apt-tools event <append|list|rebuild-state> <project-dir> [flags]')
@@ -38,18 +79,12 @@ export function cmdEvent(subcommand, projectDir, extraArgs) {
38
79
  // install.started — { task_id, ecosystem, pid, started_at, log_path }
39
80
  // install.ok — { task_id, ecosystem, duration_ms, log_path }
40
81
  // install.failed — { task_id, ecosystem, exit_code, duration_ms, reason, log_path }
41
- const validTypes = [
42
- 'task-claimed',
43
- 'task-completed',
44
- 'task-failed',
45
- 'wave-started',
46
- 'wave-completed',
47
- 'install.started',
48
- 'install.ok',
49
- 'install.failed',
50
- ]
51
- if (!validTypes.includes(type)) {
52
- return err(`Invalid event type: ${type}. Valid types: ${validTypes.join(', ')}`)
82
+ // C28 (post-ship CI watcher D-04) — added ci-watch.{armed,tick,fix-pushed,stopped}
83
+ // so /apt:watch-ci tick / start / stop can stream events into
84
+ // .aperant/events/{today}.jsonl. The rebuild-state switch below
85
+ // silently ignores them (they're informational, not task-lifecycle).
86
+ if (!VALID_EVENT_TYPES.includes(type)) {
87
+ return err(`Invalid event type: ${type}. Valid types: ${VALID_EVENT_TYPES.join(', ')}`)
53
88
  }
54
89
 
55
90
  let data = {}
@@ -61,6 +96,20 @@ export function cmdEvent(subcommand, projectDir, extraArgs) {
61
96
  }
62
97
  }
63
98
 
99
+ // PRF-002 — ci-watch.* events go to the per-day JSONL path (D-04).
100
+ // O(1) append; no directory scan needed.
101
+ if (CI_WATCH_EVENT_TYPES.has(type)) {
102
+ const event = {
103
+ type,
104
+ subtask,
105
+ worker: `${hostname()}-${process.pid}`,
106
+ timestamp: new Date().toISOString(),
107
+ data,
108
+ }
109
+ const { jsonlFile } = appendToJsonl(eventsDir, event)
110
+ return ok({ status: 'ok', command: 'event-append', event_file: jsonlFile, event })
111
+ }
112
+
64
113
  // Find next sequence number — scan existing files for highest number
65
114
  const existingFiles = readdirSync(eventsDir).filter((f) => f.endsWith('.json'))
66
115
  let maxId = 0
@@ -169,17 +218,7 @@ export function cmdEvent(subcommand, projectDir, extraArgs) {
169
218
  const event = JSON.parse(readFileSync(join(eventsDir, f), 'utf-8'))
170
219
 
171
220
  // Validate event type before processing (T-11-04 mitigation)
172
- const validTypes = [
173
- 'task-claimed',
174
- 'task-completed',
175
- 'task-failed',
176
- 'wave-started',
177
- 'wave-completed',
178
- 'install.started',
179
- 'install.ok',
180
- 'install.failed',
181
- ]
182
- if (!validTypes.includes(event.type)) continue
221
+ if (!VALID_EVENT_TYPES.includes(event.type)) continue
183
222
 
184
223
  eventsProcessed++
185
224
 
@@ -22,14 +22,30 @@ export function cmdHostDetect() {
22
22
  })
23
23
  }
24
24
 
25
- export function cmdReconcile(projectDir) {
26
- if (!projectDir) return err('Usage: apt-tools reconcile <project-dir>')
25
+ /**
26
+ * Parse `--track <quick|standard|deep>` from positional args. Returns the
27
+ * lowercased track or null when the flag is absent. Unknown values are
28
+ * silently ignored to preserve backward-compat — the caller falls back to
29
+ * global phase_delegation in that case.
30
+ */
31
+ function parseTrackFlag(args) {
32
+ if (!Array.isArray(args)) return null
33
+ const idx = args.indexOf('--track')
34
+ if (idx === -1 || idx === args.length - 1) return null
35
+ const val = String(args[idx + 1]).toLowerCase()
36
+ return val === 'quick' || val === 'standard' || val === 'deep' ? val : null
37
+ }
38
+
39
+ export function cmdReconcile(projectDir, extraArgs = []) {
40
+ if (!projectDir)
41
+ return err('Usage: apt-tools reconcile <project-dir> [--track quick|standard|deep]')
27
42
  const targetDir = resolve(projectDir)
28
43
 
29
44
  if (!existsSync(targetDir)) return err(`Directory does not exist: ${targetDir}`)
30
45
 
31
46
  const host = detectHost()
32
47
  const { capabilities } = host
48
+ const track = parseTrackFlag(extraArgs)
33
49
 
34
50
  // Read orchestration config — default all fields to 'auto' if missing
35
51
  let orchestrationConfig = {
@@ -37,6 +53,7 @@ export function cmdReconcile(projectDir) {
37
53
  review_mode: 'auto',
38
54
  debate_mode: 'auto',
39
55
  }
56
+ let perTrack = null
40
57
 
41
58
  const configPath = join(targetDir, '.aperant', 'config.json')
42
59
  if (existsSync(configPath)) {
@@ -48,18 +65,22 @@ export function cmdReconcile(projectDir) {
48
65
  review_mode: cfg.orchestration.review_mode ?? 'auto',
49
66
  debate_mode: cfg.orchestration.debate_mode ?? 'auto',
50
67
  }
68
+ perTrack = cfg.orchestration.per_track ?? null
51
69
  }
52
70
  } catch {
53
71
  // Malformed config — use defaults
54
72
  }
55
73
  }
56
74
 
75
+ // Per-track phase_delegation override (roundtable 2026-05-14 R7):
76
+ // when --track is passed AND per_track[track] is set, use that as
77
+ // the requested mode for phase_delegation. Otherwise fall back to
78
+ // the global value. review_mode and debate_mode are not track-scoped.
79
+ const requestedPhaseDelegation =
80
+ track && perTrack?.[track] ? perTrack[track] : orchestrationConfig.phase_delegation
81
+
57
82
  // Resolve each dimension
58
- const pdResult = resolveMode(
59
- orchestrationConfig.phase_delegation,
60
- capabilities,
61
- 'phase_delegation',
62
- )
83
+ const pdResult = resolveMode(requestedPhaseDelegation, capabilities, 'phase_delegation')
63
84
  const rmResult = resolveMode(orchestrationConfig.review_mode, capabilities, 'review_mode')
64
85
  const dmResult = resolveMode(orchestrationConfig.debate_mode, capabilities, 'debate_mode')
65
86
 
@@ -79,6 +100,11 @@ export function cmdReconcile(projectDir) {
79
100
  resolved,
80
101
  }
81
102
 
103
+ if (track) {
104
+ result.track = track
105
+ result.track_source = perTrack && perTrack[track] ? 'per_track' : 'fallback'
106
+ }
107
+
82
108
  if (downgrades.length > 0) {
83
109
  result.downgrades = downgrades
84
110
  }
@@ -15,6 +15,7 @@
15
15
  * half is replaced.
16
16
  */
17
17
 
18
+ import { execSync } from 'node:child_process'
18
19
  import {
19
20
  copyFileSync,
20
21
  existsSync,
@@ -222,6 +223,51 @@ function buildRerunWithHint(projectDir, parsedFlags, requiresExplicit) {
222
223
  return `apt-tools init ${projectDir} ${flags.join(' ')}`.trim()
223
224
  }
224
225
 
226
+ /**
227
+ * Infer `share.visibility` from the target's git tracking state.
228
+ *
229
+ * Repos that already track files inside `.aperant/` or any runtime root
230
+ * (`.claude/`, `.codex/`, …) are *behaviourally* team-mode: contributors
231
+ * pull the framework's installed files via git, not via local
232
+ * `apt-tools init`. Treating them as solo on an upgrade (the old `--yes`
233
+ * default did exactly this — bumped the schema, silently wrote
234
+ * `visibility: 'solo'`, and the project-gitignore allow-list flipped)
235
+ * is a destructive, non-obvious surprise. The 0.6.0/0.6.1 dogfood
236
+ * incidents both hit this rake.
237
+ *
238
+ * Returns:
239
+ * - `'team'` when any tracked file lives under `.aperant/` or a runtime
240
+ * root.
241
+ * - `'solo'` when the dir is a git repo but no such tracked file
242
+ * exists.
243
+ * - `undefined` when the target isn't a git repo (caller falls back to
244
+ * the descriptor default, which preserves greenfield behaviour).
245
+ *
246
+ * @param {string} targetDir
247
+ * @returns {'team'|'solo'|undefined}
248
+ */
249
+ function inferVisibilityFromGit(targetDir) {
250
+ try {
251
+ execSync('git rev-parse --is-inside-work-tree 2>/dev/null', {
252
+ cwd: targetDir,
253
+ stdio: ['ignore', 'ignore', 'ignore'],
254
+ })
255
+ } catch {
256
+ return undefined
257
+ }
258
+ const probePaths = ['.aperant', ...RUNTIMES.map((r) => r.installRoot)]
259
+ try {
260
+ const out = execSync(`git ls-files -- ${probePaths.map((p) => `'${p}'`).join(' ')}`, {
261
+ cwd: targetDir,
262
+ encoding: 'utf8',
263
+ stdio: ['ignore', 'pipe', 'ignore'],
264
+ }).trim()
265
+ return out.length > 0 ? 'team' : 'solo'
266
+ } catch {
267
+ return 'solo'
268
+ }
269
+ }
270
+
225
271
  /**
226
272
  * Apply Class A defaults that were collapsed under --yes /
227
273
  * --accept-new-defaults back into the migrated config so finishInit
@@ -243,21 +289,36 @@ function buildRerunWithHint(projectDir, parsedFlags, requiresExplicit) {
243
289
  * writes — keeps the prototype-pollution guard and dot-path semantics in
244
290
  * one place.
245
291
  *
292
+ * `overrides` lets callers substitute the descriptor default for a
293
+ * specific shortKey — used in the upgrade path to honour
294
+ * `inferVisibilityFromGit`, so a team repo that ran `--yes` doesn't get
295
+ * silently flipped to solo.
296
+ *
246
297
  * @param {{ migrated: Record<string, unknown>, applied_defaults: Array<{ key: string, value: unknown, why: string, source?: string }> }} migration
247
298
  * @param {Array<{ key: string, why: string, default?: unknown }>} collapsedEntries
248
299
  * @param {string} source `--yes` or `--accept-new-defaults`.
249
300
  * @param {unknown} templateVersion Template's version, applied to migrated.
301
+ * @param {Record<string, unknown>} [overrides] shortKey → inferred value.
250
302
  */
251
- function applyCollapsedClassAToMigration(migration, collapsedEntries, source, templateVersion) {
303
+ function applyCollapsedClassAToMigration(
304
+ migration,
305
+ collapsedEntries,
306
+ source,
307
+ templateVersion,
308
+ overrides = {},
309
+ ) {
252
310
  for (const r of collapsedEntries) {
253
311
  const desc = findClassDescriptor(r.key)
254
312
  if (!desc) continue
255
- setAt(migration.migrated, desc.key, desc.default)
313
+ const hasOverride = Object.hasOwn(overrides, desc.shortKey)
314
+ const value = hasOverride ? overrides[desc.shortKey] : desc.default
315
+ const recordedSource = hasOverride ? `${source} (inferred from git tracking)` : source
316
+ setAt(migration.migrated, desc.key, value)
256
317
  migration.applied_defaults.push({
257
318
  key: desc.shortKey,
258
- value: desc.default,
319
+ value,
259
320
  why: r.why,
260
- source,
321
+ source: recordedSource,
261
322
  })
262
323
  }
263
324
  if (templateVersion !== undefined) {
@@ -318,7 +379,24 @@ function nonInteractiveAnswers(targetDir, canonicalRoot, archetype, parsedFlags)
318
379
  // solo-mode — re-introducing the 0.6.0 dogfood bug.
319
380
  let requires = migration.requires_explicit
320
381
  if (parsedFlags.yes && requires.length > 0) {
321
- applyCollapsedClassAToMigration(migration, requires, '--yes', templateConfig.version)
382
+ // Prefer evidence over descriptor defaults: a repo that already
383
+ // tracks `.aperant/` or any runtime root is team-mode by
384
+ // behaviour. Without this, `--yes` flips long-tracked repos to
385
+ // solo on the v3→v4 schema bump (0.6.0/0.6.1 dogfood incidents).
386
+ // Explicit --solo/--team in parsedFlags wins; only collapse-time
387
+ // inference runs here.
388
+ const overrides = {}
389
+ if (!parsedFlags.visibility && requires.some((r) => r.key === 'visibility')) {
390
+ const inferred = inferVisibilityFromGit(targetDir)
391
+ if (inferred !== undefined) overrides.visibility = inferred
392
+ }
393
+ applyCollapsedClassAToMigration(
394
+ migration,
395
+ requires,
396
+ '--yes',
397
+ templateConfig.version,
398
+ overrides,
399
+ )
322
400
  requires = []
323
401
  } else if (parsedFlags.accept_new_defaults && requires.length > 0) {
324
402
  // --accept-new-defaults applies template defaults for new Class A