@aperant/framework 0.6.4 → 0.6.6

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 (241) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +54 -234
  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/ci-watch/lock.d.mts +53 -0
  10. package/dist/cli/ci-watch/lock.d.mts.map +1 -0
  11. package/dist/cli/ci-watch/lock.mjs +172 -0
  12. package/dist/cli/ci-watch/lock.mjs.map +1 -0
  13. package/dist/cli/ci-watch/state.d.mts +36 -0
  14. package/dist/cli/ci-watch/state.d.mts.map +1 -0
  15. package/dist/cli/ci-watch/state.mjs +103 -0
  16. package/dist/cli/ci-watch/state.mjs.map +1 -0
  17. package/dist/cli/ci-watch/stop-matrix.d.mts +58 -0
  18. package/dist/cli/ci-watch/stop-matrix.d.mts.map +1 -0
  19. package/dist/cli/ci-watch/stop-matrix.mjs +164 -0
  20. package/dist/cli/ci-watch/stop-matrix.mjs.map +1 -0
  21. package/dist/cli/cli-wrappers/ci-watch.d.mts +2 -0
  22. package/dist/cli/cli-wrappers/ci-watch.d.mts.map +1 -0
  23. package/dist/cli/cli-wrappers/ci-watch.mjs +9 -0
  24. package/dist/cli/cli-wrappers/ci-watch.mjs.map +1 -0
  25. package/dist/cli/commands/adr.d.mts +5 -0
  26. package/dist/cli/commands/adr.d.mts.map +1 -0
  27. package/dist/cli/commands/adr.mjs +228 -0
  28. package/dist/cli/commands/adr.mjs.map +1 -0
  29. package/dist/cli/commands/ci-watch.d.mts +7 -0
  30. package/dist/cli/commands/ci-watch.d.mts.map +1 -0
  31. package/dist/cli/commands/ci-watch.mjs +465 -0
  32. package/dist/cli/commands/ci-watch.mjs.map +1 -0
  33. package/dist/cli/commands/commit.d.mts.map +1 -1
  34. package/dist/cli/commands/commit.mjs +25 -8
  35. package/dist/cli/commands/commit.mjs.map +1 -1
  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 +125 -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/modes.d.mts +13 -0
  48. package/dist/cli/commands/modes.d.mts.map +1 -0
  49. package/dist/cli/commands/modes.mjs +221 -0
  50. package/dist/cli/commands/modes.mjs.map +1 -0
  51. package/dist/cli/commands/route.mjs +1 -1
  52. package/dist/cli/commands/route.mjs.map +1 -1
  53. package/dist/cli/commands/task.d.mts.map +1 -1
  54. package/dist/cli/commands/task.mjs +393 -145
  55. package/dist/cli/commands/task.mjs.map +1 -1
  56. package/dist/cli/commands/tokens.d.mts.map +1 -1
  57. package/dist/cli/commands/tokens.mjs +150 -6
  58. package/dist/cli/commands/tokens.mjs.map +1 -1
  59. package/dist/cli/commands/triage.d.mts +8 -0
  60. package/dist/cli/commands/triage.d.mts.map +1 -0
  61. package/dist/cli/commands/triage.mjs +263 -0
  62. package/dist/cli/commands/triage.mjs.map +1 -0
  63. package/dist/cli/config/post-merge-sweep.d.mts +18 -0
  64. package/dist/cli/config/post-merge-sweep.d.mts.map +1 -1
  65. package/dist/cli/config/post-merge-sweep.mjs +48 -3
  66. package/dist/cli/config/post-merge-sweep.mjs.map +1 -1
  67. package/dist/cli/consistency/registry.d.mts +11 -0
  68. package/dist/cli/consistency/registry.d.mts.map +1 -1
  69. package/dist/cli/consistency/registry.mjs +13 -0
  70. package/dist/cli/consistency/registry.mjs.map +1 -1
  71. package/dist/cli/coordination/agent-identity.d.mts +9 -0
  72. package/dist/cli/coordination/agent-identity.d.mts.map +1 -1
  73. package/dist/cli/coordination/agent-identity.mjs +11 -0
  74. package/dist/cli/coordination/agent-identity.mjs.map +1 -1
  75. package/dist/cli/coordination/auto-emit-artifact-ready.d.mts +16 -0
  76. package/dist/cli/coordination/auto-emit-artifact-ready.d.mts.map +1 -0
  77. package/dist/cli/coordination/auto-emit-artifact-ready.mjs +88 -0
  78. package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -0
  79. package/dist/cli/coordination/event-log.d.mts +27 -0
  80. package/dist/cli/coordination/event-log.d.mts.map +1 -1
  81. package/dist/cli/coordination/event-log.mjs +72 -2
  82. package/dist/cli/coordination/event-log.mjs.map +1 -1
  83. package/dist/cli/coordination/event-schema.d.mts +16 -0
  84. package/dist/cli/coordination/event-schema.d.mts.map +1 -0
  85. package/dist/cli/coordination/event-schema.mjs +161 -0
  86. package/dist/cli/coordination/event-schema.mjs.map +1 -0
  87. package/dist/cli/coordination/store.d.mts +6 -0
  88. package/dist/cli/coordination/store.d.mts.map +1 -1
  89. package/dist/cli/coordination/store.mjs +14 -0
  90. package/dist/cli/coordination/store.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 +3 -2
  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/roadmap/conductor-view.d.mts +13 -0
  106. package/dist/cli/roadmap/conductor-view.d.mts.map +1 -0
  107. package/dist/cli/roadmap/conductor-view.mjs +31 -0
  108. package/dist/cli/roadmap/conductor-view.mjs.map +1 -0
  109. package/dist/cli/route/envelope.d.mts +68 -4
  110. package/dist/cli/route/envelope.d.mts.map +1 -1
  111. package/dist/cli/route/envelope.mjs +140 -103
  112. package/dist/cli/route/envelope.mjs.map +1 -1
  113. package/dist/cli/route/skill-discover.d.mts +9 -0
  114. package/dist/cli/route/skill-discover.d.mts.map +1 -1
  115. package/dist/cli/route/skill-discover.mjs +13 -1
  116. package/dist/cli/route/skill-discover.mjs.map +1 -1
  117. package/dist/cli/skill-author/contract.d.mts +1 -0
  118. package/dist/cli/skill-author/contract.d.mts.map +1 -1
  119. package/dist/cli/skill-author/contract.mjs +7 -0
  120. package/dist/cli/skill-author/contract.mjs.map +1 -1
  121. package/dist/cli/task/ids.d.mts +7 -4
  122. package/dist/cli/task/ids.d.mts.map +1 -1
  123. package/dist/cli/task/ids.mjs +11 -10
  124. package/dist/cli/task/ids.mjs.map +1 -1
  125. package/dist/cli/task/worktree-cleanup.d.mts +9 -1
  126. package/dist/cli/task/worktree-cleanup.d.mts.map +1 -1
  127. package/dist/cli/task/worktree-cleanup.mjs +190 -9
  128. package/dist/cli/task/worktree-cleanup.mjs.map +1 -1
  129. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  130. package/dist/plugin/agents/apt-planner.md +1 -1
  131. package/dist/plugin/skills/apt/SKILL.md +111 -5
  132. package/dist/plugin/skills/apt-author-skill/SKILL.md +11 -0
  133. package/dist/plugin/skills/apt-bootstrap/SKILL.md +1 -0
  134. package/dist/plugin/skills/apt-classify/SKILL.md +1 -0
  135. package/dist/plugin/skills/apt-close-task/SKILL.md +1 -0
  136. package/dist/plugin/skills/apt-create-docs/SKILL.md +1 -0
  137. package/dist/plugin/skills/apt-debug/SKILL.md +2 -0
  138. package/dist/plugin/skills/apt-design/SKILL.md +2 -0
  139. package/dist/plugin/skills/apt-discuss/SKILL.md +2 -0
  140. package/dist/plugin/skills/apt-docs/SKILL.md +2 -0
  141. package/dist/plugin/skills/apt-execute/SKILL.md +1 -0
  142. package/dist/plugin/skills/apt-mockup/SKILL.md +2 -0
  143. package/dist/plugin/skills/apt-pause/SKILL.md +1 -0
  144. package/dist/plugin/skills/apt-personas/SKILL.md +1 -0
  145. package/dist/plugin/skills/apt-plan/SKILL.md +2 -0
  146. package/dist/plugin/skills/apt-pr-review/SKILL.md +1 -0
  147. package/dist/plugin/skills/apt-quick/SKILL.md +2 -0
  148. package/dist/plugin/skills/apt-resume/SKILL.md +1 -0
  149. package/dist/plugin/skills/apt-review/SKILL.md +1 -0
  150. package/dist/plugin/skills/apt-roadmap/SKILL.md +1 -0
  151. package/dist/plugin/skills/apt-roundtable/SKILL.md +2 -0
  152. package/dist/plugin/skills/apt-run/SKILL.md +1 -0
  153. package/dist/plugin/skills/apt-scan/SKILL.md +1 -0
  154. package/dist/plugin/skills/apt-setup/SKILL.md +1 -0
  155. package/dist/plugin/skills/apt-ship/SKILL.md +6 -5
  156. package/dist/plugin/skills/apt-stress-test/SKILL.md +1 -0
  157. package/dist/plugin/skills/apt-terminal/SKILL.md +1 -0
  158. package/dist/plugin/skills/apt-update/SKILL.md +5 -0
  159. package/dist/plugin/skills/apt-verify/SKILL.md +1 -0
  160. package/dist/plugin/skills/apt-verify-proof/SKILL.md +1 -0
  161. package/dist/types/config.d.ts +85 -0
  162. package/dist/types/config.d.ts.map +1 -1
  163. package/package.json +9 -1
  164. package/prompts/coder.md +2 -0
  165. package/prompts/conductor-status-check.md +23 -0
  166. package/prompts/conductor-sub-agent.md +57 -0
  167. package/prompts/conductor-system.md +172 -0
  168. package/prompts/planner.md +12 -0
  169. package/prompts/spec_writer.md +9 -0
  170. package/skills/apt/SKILL.md +1 -0
  171. package/skills/apt-close-task/SKILL.md +32 -1
  172. package/skills/apt-debug/SKILL.md +39 -6
  173. package/skills/apt-debug/appendices/diagnose-discipline.md +119 -0
  174. package/skills/apt-diagram/SKILL.md +342 -0
  175. package/skills/apt-diagram/appendices/design-discipline.md +97 -0
  176. package/skills/apt-discuss/SKILL.md +25 -0
  177. package/skills/apt-discuss/appendices/grill-discipline.md +104 -0
  178. package/skills/apt-discuss/appendices/zoom-out-helper.md +79 -0
  179. package/skills/apt-execute/SKILL.md +48 -5
  180. package/skills/apt-execute/appendices/tdd-mode.md +107 -0
  181. package/skills/apt-improve/DEEPENING.md +84 -0
  182. package/skills/apt-improve/INTERFACE-DESIGN.md +97 -0
  183. package/skills/apt-improve/LANGUAGE.md +104 -0
  184. package/skills/apt-improve/SKILL.md +141 -0
  185. package/skills/apt-plan/SKILL.md +159 -4
  186. package/skills/apt-plan/adapters/conductor.md +98 -0
  187. package/skills/apt-planner.md +42 -1
  188. package/skills/apt-pr-review/SKILL.md +46 -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 +30 -0
  193. package/skills/apt-review/SKILL.md +2 -0
  194. package/skills/apt-run/SKILL.md +32 -4
  195. package/skills/apt-setup/SKILL.md +130 -2
  196. package/skills/apt-ship/SKILL.md +63 -1
  197. package/skills/apt-spar/SKILL.md +290 -0
  198. package/skills/apt-triage/AGENT-BRIEF.md +84 -0
  199. package/skills/apt-triage/OUT-OF-SCOPE.md +75 -0
  200. package/skills/apt-triage/SKILL.md +169 -0
  201. package/skills/apt-verify/SKILL.md +3 -0
  202. package/skills/apt-verify-proof/SKILL.md +3 -0
  203. package/skills/apt-watch-ci/SKILL.md +163 -0
  204. package/skills/apt-zoom-out/SKILL.md +130 -0
  205. package/src/cli/ci-watch/lock.mjs +178 -0
  206. package/src/cli/ci-watch/state.mjs +103 -0
  207. package/src/cli/ci-watch/stop-matrix.mjs +181 -0
  208. package/src/cli/cli-wrappers/ci-watch.mjs +9 -0
  209. package/src/cli/commands/adr.mjs +243 -0
  210. package/src/cli/commands/ci-watch.mjs +503 -0
  211. package/src/cli/commands/commit.mjs +27 -8
  212. package/src/cli/commands/context.mjs +244 -0
  213. package/src/cli/commands/event.mjs +131 -24
  214. package/src/cli/commands/host-detect.mjs +33 -7
  215. package/src/cli/commands/modes.mjs +216 -0
  216. package/src/cli/commands/route.mjs +1 -1
  217. package/src/cli/commands/task.mjs +430 -170
  218. package/src/cli/commands/tokens.mjs +157 -6
  219. package/src/cli/commands/triage.mjs +286 -0
  220. package/src/cli/config/post-merge-sweep.mjs +49 -3
  221. package/src/cli/consistency/registry.mjs +14 -0
  222. package/src/cli/coordination/agent-identity.mjs +12 -0
  223. package/src/cli/coordination/auto-emit-artifact-ready.mjs +96 -0
  224. package/src/cli/coordination/event-log.mjs +73 -2
  225. package/src/cli/coordination/event-schema.d.ts +13 -0
  226. package/src/cli/coordination/event-schema.mjs +174 -0
  227. package/src/cli/coordination/store.mjs +14 -0
  228. package/src/cli/dispatch.mjs +15 -1
  229. package/src/cli/gate/registry.mjs +14 -0
  230. package/src/cli/help.mjs +3 -2
  231. package/src/cli/host/detect.mjs +5 -0
  232. package/src/cli/roadmap/conductor-view.d.ts +10 -0
  233. package/src/cli/roadmap/conductor-view.mjs +31 -0
  234. package/src/cli/route/envelope.mjs +140 -106
  235. package/src/cli/route/skill-discover.mjs +14 -1
  236. package/src/cli/skill-author/contract.mjs +7 -0
  237. package/src/cli/task/ids.mjs +15 -13
  238. package/src/cli/task/worktree-cleanup.mjs +191 -9
  239. package/templates/adr-format.md +56 -0
  240. package/templates/config.json +4 -0
  241. package/templates/context-format.md +34 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @internal
3
+ * auto-emit-artifact-ready.mjs — code-enforced plan-ready signal.
4
+ *
5
+ * Promotes the `artifact.ready{kind:'plan'}` emit from a markdown contract
6
+ * in `skills/apt-plan/adapters/conductor.md` to a framework-level
7
+ * postcondition of `apt-tools commit`. This closes the Conductor v2 gap
8
+ * exposed during live verification 2026-05-15: the planner's QUICK /
9
+ * SIMPLE track is hard-exempt from "new interaction discipline"
10
+ * (Pocock Fast Path Guarantee ID-05), so the markdown emit step was
11
+ * never executed for tasks router-classified as simple. The fleet
12
+ * never saw artifact.ready, the PlanReadyCard never auto-promoted.
13
+ *
14
+ * The fix lives in `framework` rather than the skill so:
15
+ * - it works for QUICK + STANDARD + DEEP + COMPLEX uniformly
16
+ * - it doesn't violate the Fast Path Guarantee (a passive log emit
17
+ * is not interaction discipline)
18
+ * - it's the same code path every CLI consumer (Claude Code, Gemini,
19
+ * OpenCode, Codex) traverses via `apt-tools commit`
20
+ *
21
+ * Conditions for emit (ALL must hold — otherwise no-op):
22
+ * 1. `APERANT_TERMINAL_ID` is set in process.env (Conductor PTY context).
23
+ * 2. The committed files include a path matching
24
+ * `.aperant/tasks/{task-id}/implementation_plan.json`.
25
+ *
26
+ * The emit re-uses the same `event-schema` validator and JSONL writer
27
+ * `commands/event.mjs` already uses, so the envelope structure stays
28
+ * byte-identical with manually-emitted ones.
29
+ */
30
+
31
+ import { createHash } from 'node:crypto'
32
+ import { appendFileSync, mkdirSync, readFileSync } from 'node:fs'
33
+ import { hostname } from 'node:os'
34
+ import { join } from 'node:path'
35
+ import { validateConductorEvent } from './event-schema.mjs'
36
+
37
+ const PLAN_PATH_RE = /^(?:\.\/)?\.aperant\/tasks\/([^/]+)\/implementation_plan\.json$/
38
+
39
+ let _incarnationCounter = 0
40
+
41
+ /**
42
+ * @param {{ files: string[], projectRoot: string }} input
43
+ * @returns {{ emitted: false, reason: string } | { emitted: true, envelope: object, file: string }}
44
+ */
45
+ export function maybeAutoEmitPlanReady({ files, projectRoot }) {
46
+ const terminalId = process.env.APERANT_TERMINAL_ID
47
+ if (!terminalId) return { emitted: false, reason: 'APERANT_TERMINAL_ID unset' }
48
+
49
+ const match = files.map((f) => f.replace(/\\/g, '/').match(PLAN_PATH_RE)).find((m) => m !== null)
50
+ if (!match) return { emitted: false, reason: 'no implementation_plan.json in committed files' }
51
+ const taskId = match[1]
52
+
53
+ const planRelPath = `.aperant/tasks/${taskId}/implementation_plan.json`
54
+ const absolutePath = join(projectRoot, planRelPath)
55
+
56
+ let hash
57
+ try {
58
+ const body = readFileSync(absolutePath)
59
+ hash = createHash('sha256').update(body).digest('hex')
60
+ } catch (err) {
61
+ return { emitted: false, reason: `plan file unreadable: ${(err && err.message) || err}` }
62
+ }
63
+
64
+ const data = {
65
+ kind: 'plan',
66
+ path: planRelPath,
67
+ hash,
68
+ request_id: `plan-${taskId}-${Date.now()}`,
69
+ task_id: taskId,
70
+ terminal_id: terminalId,
71
+ }
72
+
73
+ const validation = validateConductorEvent('artifact.ready', data)
74
+ if (!validation.ok) {
75
+ return { emitted: false, reason: `schema violation: ${validation.error}` }
76
+ }
77
+
78
+ const envelope = {
79
+ t: new Date().toISOString(),
80
+ agent: `${hostname()}-${process.pid}`,
81
+ pid: process.pid,
82
+ hostname: hostname(),
83
+ incarnation: ++_incarnationCounter,
84
+ op: 'artifact.ready',
85
+ task: taskId,
86
+ data,
87
+ }
88
+
89
+ const eventsDir = join(projectRoot, '.aperant', 'events')
90
+ mkdirSync(eventsDir, { recursive: true })
91
+ const dayFile = `${new Date().toISOString().slice(0, 10)}.jsonl`
92
+ const fullPath = join(eventsDir, dayFile)
93
+ appendFileSync(fullPath, `${JSON.stringify(envelope)}\n`, 'utf-8')
94
+
95
+ return { emitted: true, envelope, file: dayFile }
96
+ }
@@ -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
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Type declarations for the .mjs validator. Hand-authored because the
3
+ * import path points at src/.mjs directly (no dist roundtrip).
4
+ */
5
+
6
+ export type ValidateResult = { ok: true } | { ok: false; error: string }
7
+
8
+ export function validateConductorEvent(
9
+ op: string,
10
+ data: Record<string, unknown> | undefined | null,
11
+ ): ValidateResult
12
+
13
+ export const CONDUCTOR_VALIDATED_OPS: ReadonlySet<string>
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @internal
3
+ * event-schema.mjs — single source of truth for conductor event payload shapes.
4
+ *
5
+ * Why this lives in `framework`, not `core`:
6
+ * `packages/core` depends on `@aperant/framework`. The schema is shared by
7
+ * the CLI write path (`commands/event.mjs`), the framework lifecycle write
8
+ * seam (`coordination/store.mjs:appendEvent`), and the core IPC write seam
9
+ * (`packages/core/src/runtime/orchestration/conductor/event-log.ts`).
10
+ * Putting it in the lower layer keeps a single rule table that every
11
+ * writer can import.
12
+ *
13
+ * Shape: zero-dep plain JS validator returning a `{ ok, error? }` result.
14
+ * We deliberately avoid zod here so `.mjs` callers (event.mjs, store.mjs)
15
+ * don't need a build step, and so the core TS writer can use the same
16
+ * identity-shaped result without translating between zod and a custom
17
+ * error type.
18
+ *
19
+ * Per Codex roundtable lock 2026-05-15:
20
+ * - `phase.changed` is reserved for PTY busy/idle transitions ONLY.
21
+ * Plan completion uses `artifact.ready{kind:'plan'}`, not phase.changed.
22
+ * - `awaiting-input` / `input-rescinded` MUST carry both terminal_id and
23
+ * request_id so the drawer can pair cards strictly.
24
+ * - `artifact.ready{kind:'plan'}` is the canonical plan-completion signal
25
+ * consumed by the conductor's reviewer queue (Step 5's reviewPlan tool).
26
+ */
27
+
28
+ /** Conductor ops the schema enforces. Other ops pass through unchanged. */
29
+ const CONDUCTOR_OPS = new Set([
30
+ 'phase.changed',
31
+ 'artifact.ready',
32
+ 'awaiting-input',
33
+ 'input-rescinded',
34
+ 'heartbeat',
35
+ 'budget.exceeded',
36
+ 'terminal.enrolled',
37
+ 'terminal.unenrolled',
38
+ 'conductor.intent',
39
+ 'conductor.cross_check',
40
+ 'conductor.realignment',
41
+ // c-4 (action-capable) — gate verdict + boot-replay audit trail
42
+ 'conductor.action.executed',
43
+ 'conductor.action.gate_blocked',
44
+ 'conductor.boot_reconciled',
45
+ ])
46
+
47
+ /** Allowed values for `phase.changed.to` — PTY busy/idle only. */
48
+ const ALLOWED_PHASE_VALUES = new Set(['busy', 'idle'])
49
+
50
+ /** Allowed values for `artifact.ready.kind`. */
51
+ const ALLOWED_ARTIFACT_KINDS = new Set(['plan', 'spec', 'build', 'review'])
52
+
53
+ /**
54
+ * Validate the data payload of a conductor event against the op's contract.
55
+ *
56
+ * @param {string} op
57
+ * @param {Record<string, unknown> | undefined | null} data
58
+ * @returns {{ ok: true } | { ok: false, error: string }}
59
+ */
60
+ export function validateConductorEvent(op, data) {
61
+ if (!CONDUCTOR_OPS.has(op)) return { ok: true }
62
+ const d = data && typeof data === 'object' ? data : {}
63
+
64
+ switch (op) {
65
+ case 'awaiting-input':
66
+ case 'input-rescinded':
67
+ return requireFields(op, d, ['terminal_id', 'request_id'])
68
+
69
+ case 'phase.changed': {
70
+ const r = requireFields(op, d, ['from', 'to', 'terminal_id', 'request_id'])
71
+ if (!r.ok) return r
72
+ if (!ALLOWED_PHASE_VALUES.has(String(d.to))) {
73
+ return {
74
+ ok: false,
75
+ error: `phase.changed.to must be one of [${[...ALLOWED_PHASE_VALUES].join(', ')}], got: ${String(
76
+ d.to,
77
+ )}. Plan completion must use artifact.ready{kind:'plan'}.`,
78
+ }
79
+ }
80
+ return { ok: true }
81
+ }
82
+
83
+ case 'artifact.ready': {
84
+ const r = requireFields(op, d, [
85
+ 'kind',
86
+ 'path',
87
+ 'hash',
88
+ 'request_id',
89
+ 'terminal_id',
90
+ 'task_id',
91
+ ])
92
+ if (!r.ok) return r
93
+ if (!ALLOWED_ARTIFACT_KINDS.has(String(d.kind))) {
94
+ return {
95
+ ok: false,
96
+ error: `artifact.ready.kind must be one of [${[...ALLOWED_ARTIFACT_KINDS].join(
97
+ ', ',
98
+ )}], got: ${String(d.kind)}`,
99
+ }
100
+ }
101
+ return { ok: true }
102
+ }
103
+
104
+ case 'conductor.intent':
105
+ return requireFields(op, d, ['terminal_id', 'request_id', 'action', 'reasoning'])
106
+
107
+ case 'conductor.cross_check':
108
+ return requireFields(op, d, ['terminal_id', 'request_id'])
109
+
110
+ case 'conductor.realignment': {
111
+ const r = requireFields(op, d, ['terminal_id', 'request_id_orig'])
112
+ if (!r.ok) return r
113
+ if (d.attempt_n === undefined || d.attempt_n === null)
114
+ return { ok: false, error: `${op} missing required field(s): attempt_n` }
115
+ return { ok: true }
116
+ }
117
+
118
+ case 'terminal.enrolled':
119
+ case 'terminal.unenrolled':
120
+ case 'heartbeat':
121
+ case 'budget.exceeded':
122
+ return requireFields(op, d, ['terminal_id'])
123
+
124
+ case 'conductor.action.executed':
125
+ return requireFields(op, d, ['terminal_id', 'request_id', 'action_kind', 'idempotency_key'])
126
+
127
+ case 'conductor.action.gate_blocked':
128
+ return requireFields(op, d, [
129
+ 'terminal_id',
130
+ 'request_id',
131
+ 'action_kind',
132
+ 'idempotency_key',
133
+ 'blocked_reason',
134
+ ])
135
+
136
+ case 'conductor.boot_reconciled': {
137
+ // Fleet-wide event — numeric fields only. All optional but if
138
+ // present must be finite numbers (zero allowed).
139
+ const numFields = ['cleaned_leases', 'replayed', 'start_offset']
140
+ for (const f of numFields) {
141
+ if (d[f] === undefined) continue
142
+ if (typeof d[f] !== 'number' || !Number.isFinite(d[f])) {
143
+ return {
144
+ ok: false,
145
+ error: `${op} field ${f} must be a finite number, got: ${String(d[f])}`,
146
+ }
147
+ }
148
+ }
149
+ return { ok: true }
150
+ }
151
+
152
+ default:
153
+ return { ok: true }
154
+ }
155
+ }
156
+
157
+ /** Set of ops the schema validates — exported for test enumeration. */
158
+ export const CONDUCTOR_VALIDATED_OPS = CONDUCTOR_OPS
159
+
160
+ /**
161
+ * @param {string} op
162
+ * @param {Record<string, unknown>} d
163
+ * @param {string[]} fields
164
+ * @returns {{ ok: true } | { ok: false, error: string }}
165
+ */
166
+ function requireFields(op, d, fields) {
167
+ const missing = fields.filter((f) => {
168
+ const v = d[f]
169
+ if (typeof v !== 'string' || v.length === 0) return true
170
+ return false
171
+ })
172
+ if (missing.length === 0) return { ok: true }
173
+ return { ok: false, error: `${op} missing required field(s): ${missing.join(', ')}` }
174
+ }
@@ -32,6 +32,7 @@ import { withFileLock } from '../util/fs-lock.mjs'
32
32
  import { atomicWriteJson } from '../util/io.mjs'
33
33
  import { getAgentIdentity, resolveSharedRoot } from './agent-identity.mjs'
34
34
  import { appendLifecycleEvent } from './event-log.mjs'
35
+ import { validateConductorEvent } from './event-schema.mjs'
35
36
  import {
36
37
  listActive as teamStatusListActive,
37
38
  removeSelf as teamStatusRemoveSelf,
@@ -128,9 +129,22 @@ export function createCoordinationStore(projectRoot) {
128
129
  * @internal — R10 append-only event log. Emits one JSONL line per
129
130
  * call via appendLifecycleEvent. Best-effort: filesystem errors are
130
131
  * swallowed so a missing events/ dir never breaks task lifecycle.
132
+ *
133
+ * Step 1 — schema gate: conductor ops written via the store seam are
134
+ * validated against `event-schema.mjs` before they reach disk.
135
+ * Schema violations throw (caught here and surfaced via console.warn)
136
+ * because a malformed conductor event would silently break the
137
+ * drawer's pairing logic — we want it loud, not best-effort.
131
138
  */
132
139
  appendEvent(evt) {
133
140
  try {
141
+ if (evt && typeof evt.op === 'string') {
142
+ const validation = validateConductorEvent(evt.op, evt.data)
143
+ if (!validation.ok) {
144
+ console.warn(`[store.appendEvent] schema violation: ${validation.error}`)
145
+ return null
146
+ }
147
+ }
134
148
  return appendLifecycleEvent(projectRoot, evt)
135
149
  } catch {
136
150
  /* best-effort — never block a lifecycle transition on the log */
@@ -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
@@ -28,9 +28,10 @@ Commands:
28
28
  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
29
29
  task list <dir> [--scope <s>] [--blocked-by <id>] List active tasks (optionally filtered)
30
30
  task get <dir> --id <task-id> Get task details and artifacts
31
- task update <dir> --id <id> [--lifecycle-phase <p>] [--subtasks-total <N>] [--depends-on <refs>] Update non-linkage task state
31
+ 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
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
@@ -130,5 +131,5 @@ Flags:
130
131
 
131
132
  All output is JSON.`
132
133
 
133
- process.stdout.write(help + '\n')
134
+ process.stdout.write(`${help}\n`)
134
135
  }
@@ -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
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Type declarations for the .mjs conductor-view helper.
3
+ * Hand-authored — the import path points at src/.mjs directly.
4
+ */
5
+
6
+ export interface ConductorRoadmapView {
7
+ scopes: Array<{ scope: string; roadmap: unknown }>
8
+ }
9
+
10
+ export function buildConductorRoadmapView(projectRoot: string): ConductorRoadmapView
@@ -0,0 +1,31 @@
1
+ /**
2
+ * conductor-view.mjs — read-only roadmap surface for the Aperant
3
+ * Conductor's `getRoadmap` tool.
4
+ *
5
+ * Wraps `loadRoadmapForScope` + `listRoadmapScopes` so the Conductor sees
6
+ * the same overlay-aware view every framework command sees. Direct reads
7
+ * of `.aperant/roadmap/*\/phases/*.json` or `milestones.json` would miss
8
+ * the dual-write overlay (seed_id from legacy blob, etc.) — Codex
9
+ * roundtable correction #3.
10
+ *
11
+ * Step 5 of Conductor v2 ship order.
12
+ */
13
+
14
+ import { loadRoadmapForScope } from './io.mjs'
15
+ import { listRoadmapScopes } from './paths.mjs'
16
+
17
+ /**
18
+ * Build a flat per-scope roadmap snapshot for the Conductor.
19
+ *
20
+ * @param {string} projectRoot
21
+ * @returns {{ scopes: Array<{ scope: string, roadmap: unknown }> }}
22
+ */
23
+ export function buildConductorRoadmapView(projectRoot) {
24
+ const scopes = listRoadmapScopes(projectRoot)
25
+ const out = []
26
+ for (const scope of scopes) {
27
+ const roadmap = loadRoadmapForScope(projectRoot, scope)
28
+ if (roadmap !== null) out.push({ scope, roadmap })
29
+ }
30
+ return { scopes: out }
31
+ }