@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,181 @@
1
+ /**
2
+ * ci-watch/stop-matrix.mjs — pure stop-matrix classifier (D-06).
3
+ *
4
+ * `classify({checks, state, prMetadata})` returns
5
+ * { action, counts_toward_budget, reason }
6
+ *
7
+ * No I/O, no fs, no gh CLI shell-outs — caller pre-fetches `checks` (the
8
+ * payload from `gh pr checks --json name,bucket,state,conclusion,detailsUrl`)
9
+ * and `prMetadata` (`gh pr view --json state,reviewDecision`) and passes them
10
+ * in. The orchestrator in commands/ci-watch.mjs handles the side effects
11
+ * implied by the action.
12
+ *
13
+ * Predicate order matters — short-circuits go first (merged/closed/green/cap
14
+ * fire before any per-check inspection). See spec.md §9 table for the full
15
+ * ordering.
16
+ *
17
+ * The 11 actions:
18
+ * merged, closed, green, attempts-cap, wall-cap, reviewer-pause,
19
+ * identical-log, flake-skip, infra-backoff, diverged-rebased,
20
+ * diverged-bailed, fix
21
+ *
22
+ * `counts_toward_budget` is `true` for fix attempts (including rebase
23
+ * attempts and rebase-bail); `false` for flake-skip / infra-backoff; `false`
24
+ * (with a sentinel meaning "n/a") for terminal actions.
25
+ */
26
+
27
+ const ATTEMPTS_CAP = 5
28
+
29
+ const INFRA_FAILURE_CLASSES = new Set(['runner-oom', 'runner-cancelled', 'gh-5xx', 'timeout'])
30
+
31
+ /**
32
+ * @typedef {Object} CheckRow
33
+ * @property {string} [name]
34
+ * @property {string} [conclusion] e.g. 'success' | 'failure' | 'cancelled' | 'timed_out' | 'neutral'
35
+ * @property {string} [state]
36
+ * @property {string} [bucket]
37
+ * @property {string} [failure_class] — optional pre-classified hint (test-only seam)
38
+ * @property {string} [log_fingerprint] — optional digest for identical-log detection
39
+ *
40
+ * @typedef {Object} ClassifyInput
41
+ * @property {CheckRow[]} checks
42
+ * @property {Object} state — per-PR state (see state.mjs §8)
43
+ * @property {Object} prMetadata — { state: 'OPEN'|'CLOSED'|'MERGED', reviewDecision?: string }
44
+ *
45
+ * @typedef {Object} ClassifyOutput
46
+ * @property {string} action
47
+ * @property {boolean} counts_toward_budget
48
+ * @property {string} reason
49
+ *
50
+ * @param {ClassifyInput} input
51
+ * @returns {ClassifyOutput}
52
+ */
53
+ export function classify({ checks, state, prMetadata }) {
54
+ const pr = prMetadata || {}
55
+ const st = state || {}
56
+ const rows = Array.isArray(checks) ? checks : []
57
+
58
+ // 1. MERGED.
59
+ if (pr.state === 'MERGED') {
60
+ return { action: 'merged', counts_toward_budget: false, reason: 'pr is merged' }
61
+ }
62
+ // 2. CLOSED (not merged).
63
+ if (pr.state === 'CLOSED') {
64
+ return { action: 'closed', counts_toward_budget: false, reason: 'pr is closed unmerged' }
65
+ }
66
+
67
+ const failed = rows.filter((c) => isFailing(c))
68
+ const allGreen = rows.length > 0 && rows.every((c) => c.conclusion === 'success')
69
+
70
+ // 3. All checks green.
71
+ if (allGreen) {
72
+ return { action: 'green', counts_toward_budget: false, reason: 'all checks succeeded' }
73
+ }
74
+
75
+ // 3b. No checks registered yet — CI hasn't enqueued jobs. Wait without
76
+ // burning a fix attempt.
77
+ if (rows.length === 0) {
78
+ return { action: 'wait', counts_toward_budget: false, reason: 'no checks registered yet' }
79
+ }
80
+
81
+ // 4. Attempts cap (only when red present — otherwise green/merged/closed would have fired).
82
+ const attempts = st.attempts || 0
83
+ if (attempts >= ATTEMPTS_CAP && failed.length > 0) {
84
+ return {
85
+ action: 'attempts-cap',
86
+ counts_toward_budget: false,
87
+ reason: `${attempts} fix attempts used; cap is ${ATTEMPTS_CAP}`,
88
+ }
89
+ }
90
+
91
+ // 5. Wall-clock cap.
92
+ if (st.deadline && Date.now() > new Date(st.deadline).getTime() && failed.length > 0) {
93
+ return {
94
+ action: 'wall-cap',
95
+ counts_toward_budget: false,
96
+ reason: 'past 4h deadline',
97
+ }
98
+ }
99
+
100
+ // 6. Reviewer asked for changes.
101
+ if (pr.reviewDecision === 'CHANGES_REQUESTED') {
102
+ return {
103
+ action: 'reviewer-pause',
104
+ counts_toward_budget: false,
105
+ reason: 'reviewer requested changes — pausing',
106
+ }
107
+ }
108
+
109
+ // 7. Identical-log → non-converging fixer.
110
+ // Compare aggregate fingerprints (same format as the write side in ci-watch.mjs).
111
+ if (attempts > 0 && st.last_status && failed.length > 0) {
112
+ const currentFingerprint = failed
113
+ .map((f) => `${f.name}:${f.log_fingerprint ?? ''}`)
114
+ .sort()
115
+ .join('|')
116
+ if (currentFingerprint === st.last_status) {
117
+ return {
118
+ action: 'identical-log',
119
+ counts_toward_budget: false,
120
+ reason: 'failed-check fingerprint identical to last attempt',
121
+ }
122
+ }
123
+ }
124
+
125
+ // 8. Flake-skip — same check failed then passed on rerun, no commit between.
126
+ // The classifier sees this as an explicit hint from the caller: failed rows
127
+ // classified as `bucket === 'flake'` or `failure_class === 'flake'` are the
128
+ // flaky-test variety and must not burn a fix attempt. Fires when EVERY
129
+ // remaining failure is a flake — if any non-flake failure is present, we
130
+ // fall through and the real failure governs.
131
+ if (
132
+ failed.length > 0 &&
133
+ failed.every((c) => c.bucket === 'flake' || c.failure_class === 'flake')
134
+ ) {
135
+ return {
136
+ action: 'flake-skip',
137
+ counts_toward_budget: false,
138
+ reason: 'check transient — passed on rerun',
139
+ }
140
+ }
141
+
142
+ // 9. Infra failure class.
143
+ if (failed.some((c) => INFRA_FAILURE_CLASSES.has(c.failure_class))) {
144
+ return {
145
+ action: 'infra-backoff',
146
+ counts_toward_budget: false,
147
+ reason: 'infra-class failure — backoff and retry',
148
+ }
149
+ }
150
+
151
+ // 10. Diverged branch — caller has already done the fetch + dry-rebase and
152
+ // passes the outcome in `prMetadata.diverged`:
153
+ // { rebased: true } → action 'diverged-rebased' (counts)
154
+ // { rebased: false } → action 'diverged-bailed' (counts)
155
+ if (pr.diverged && typeof pr.diverged === 'object') {
156
+ if (pr.diverged.rebased === true) {
157
+ return {
158
+ action: 'diverged-rebased',
159
+ counts_toward_budget: true,
160
+ reason: 'origin ahead; rebased successfully',
161
+ }
162
+ }
163
+ return {
164
+ action: 'diverged-bailed',
165
+ counts_toward_budget: true,
166
+ reason: 'origin ahead; rebase conflict',
167
+ }
168
+ }
169
+
170
+ // 11. Default red — go fix.
171
+ return {
172
+ action: 'fix',
173
+ counts_toward_budget: true,
174
+ reason: 'red checks present — attempting fix',
175
+ }
176
+ }
177
+
178
+ export function isFailing(check) {
179
+ const c = check.conclusion
180
+ return c === 'failure' || c === 'cancelled' || c === 'timed_out' || c === 'action_required'
181
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * cli-wrappers/ci-watch.mjs — Layer 2 wrapper for the ci-watch cmd.
3
+ *
4
+ * Today this is a passthrough — DISPATCH calls wrap(cmdCiWatch) directly.
5
+ * Re-exporting under cliCiWatch keeps the file-per-cmd structural seam
6
+ * (mirrors task.mjs / pr-review.mjs).
7
+ */
8
+
9
+ export { cmdCiWatch as cliCiWatch } from '../commands/ci-watch.mjs'
@@ -0,0 +1,243 @@
1
+ /**
2
+ * commands/adr.mjs — `apt-tools adr write` (Pocock adoption ST-6, AC5).
3
+ *
4
+ * Writes an ADR under `docs/adr/NNNN-slug.md` ONLY when all three Nygard
5
+ * gates pass: hard-to-reverse AND surprising-without-context AND
6
+ * real-trade-off. When any gate fails, the writer returns a `filtered`
7
+ * envelope and writes nothing — the decision stays in
8
+ * `.aperant/context/notes/{task-id}.md` instead.
9
+ *
10
+ * CLI:
11
+ * apt-tools adr write <project-dir>
12
+ * --title <slug>
13
+ * --hard-to-reverse <true|false>
14
+ * --surprising-without-context <true|false>
15
+ * --real-trade-off <true|false>
16
+ * --decision <text>
17
+ * --consequences <text>
18
+ * [--context <text>]
19
+ * [--status Proposed|Accepted|Deprecated|Superseded]
20
+ *
21
+ * Output: JSON envelope.
22
+ * On filter: { status: 'filtered', failed_gates: string[], reason: 'three-gate-filter' }
23
+ * On write: { status: 'ok', command: 'adr-write', file: <abs path>, number: N, slug: '...' }
24
+ */
25
+
26
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
27
+ import { join, resolve } from 'node:path'
28
+ import { parseFlags } from '../util/args.mjs'
29
+ import { err, exitWith, ok } from '../util/result.mjs'
30
+
31
+ /**
32
+ * Parse a flag value as a strict boolean. Returns null on unrecognized form
33
+ * so callers can distinguish "missing" from "false".
34
+ *
35
+ * @param {unknown} v
36
+ * @returns {boolean|null}
37
+ */
38
+ function parseBool(v) {
39
+ if (v === true) return true
40
+ if (typeof v !== 'string') return null
41
+ const low = v.toLowerCase()
42
+ if (low === 'true' || low === 'yes' || low === '1') return true
43
+ if (low === 'false' || low === 'no' || low === '0') return false
44
+ return null
45
+ }
46
+
47
+ /**
48
+ * Slugify a title for the filename (NNNN-slug.md).
49
+ *
50
+ * @param {string} title
51
+ */
52
+ function slugify(title) {
53
+ return title
54
+ .toLowerCase()
55
+ .trim()
56
+ .replace(/[^\w\s-]/g, '')
57
+ .replace(/\s+/g, '-')
58
+ .replace(/-+/g, '-')
59
+ .replace(/^-|-$/g, '')
60
+ }
61
+
62
+ /**
63
+ * Find the next ADR sequence number by scanning `docs/adr/`.
64
+ *
65
+ * @param {string} adrDir
66
+ * @returns {number}
67
+ */
68
+ function nextAdrNumber(adrDir) {
69
+ if (!existsSync(adrDir)) return 1
70
+ const entries = readdirSync(adrDir)
71
+ let max = 0
72
+ for (const name of entries) {
73
+ const m = name.match(/^(\d{4})-/)
74
+ if (!m) continue
75
+ const n = Number.parseInt(m[1], 10)
76
+ if (n > max) max = n
77
+ }
78
+ return max + 1
79
+ }
80
+
81
+ /**
82
+ * Render the ADR markdown body.
83
+ *
84
+ * @param {{
85
+ * number: number,
86
+ * title: string,
87
+ * status: string,
88
+ * date: string,
89
+ * context: string,
90
+ * decision: string,
91
+ * consequences: string,
92
+ * hardToReverse: boolean,
93
+ * surprisingWithoutContext: boolean,
94
+ * realTradeOff: boolean,
95
+ * }} opts
96
+ */
97
+ function renderAdr(opts) {
98
+ const paddedNumber = String(opts.number).padStart(4, '0')
99
+ return [
100
+ `# ADR-${paddedNumber}: ${opts.title}`,
101
+ '',
102
+ `**Status:** ${opts.status}`,
103
+ `**Date:** ${opts.date}`,
104
+ '',
105
+ '## Context',
106
+ '',
107
+ opts.context,
108
+ '',
109
+ '## Decision',
110
+ '',
111
+ opts.decision,
112
+ '',
113
+ '## Consequences',
114
+ '',
115
+ opts.consequences,
116
+ '',
117
+ '---',
118
+ '',
119
+ '## Nygard 3-gate filter (audit trail)',
120
+ '',
121
+ 'This ADR was written via `apt-tools adr write` and passed all three of',
122
+ "Nygard's filter gates. Reversible / obvious / trade-off-free decisions",
123
+ 'stay in `.aperant/context/notes/{task-id}.md` and do NOT become ADRs.',
124
+ '',
125
+ `- **Hard to reverse:** ${opts.hardToReverse}`,
126
+ `- **Surprising without context:** ${opts.surprisingWithoutContext}`,
127
+ `- **Real trade-off:** ${opts.realTradeOff}`,
128
+ '',
129
+ ].join('\n')
130
+ }
131
+
132
+ /**
133
+ * CLI entry: `apt-tools adr <subcommand> <project-dir> [flags]`.
134
+ */
135
+ export function cmdAdr(subcommand, projectDir, extraArgs) {
136
+ if (subcommand !== 'write') {
137
+ return err(
138
+ 'Usage: apt-tools adr write <project-dir> --title <slug> --hard-to-reverse <bool> --surprising-without-context <bool> --real-trade-off <bool> --decision <text> --consequences <text> [--context <text>] [--status <status>]',
139
+ 1,
140
+ { command: 'adr' },
141
+ )
142
+ }
143
+ if (!projectDir) {
144
+ return err('project-dir is required', 1, { command: 'adr-write' })
145
+ }
146
+
147
+ const flags = parseFlags(extraArgs ?? [])
148
+ const title = flags.get('title')
149
+ if (typeof title !== 'string' || title.trim() === '') {
150
+ return err('--title <slug> is required', 1, { command: 'adr-write' })
151
+ }
152
+
153
+ // Three Nygard gates — every one must be parseable; missing means "not
154
+ // asked", which is the same as "no" for the filter.
155
+ const hardToReverse = parseBool(flags.get('hard-to-reverse'))
156
+ const surprisingWithoutContext = parseBool(flags.get('surprising-without-context'))
157
+ const realTradeOff = parseBool(flags.get('real-trade-off'))
158
+
159
+ for (const [name, value] of [
160
+ ['--hard-to-reverse', hardToReverse],
161
+ ['--surprising-without-context', surprisingWithoutContext],
162
+ ['--real-trade-off', realTradeOff],
163
+ ]) {
164
+ if (value === null) {
165
+ return err(`${name} <true|false> is required`, 1, { command: 'adr-write' })
166
+ }
167
+ }
168
+
169
+ const decision = flags.get('decision')
170
+ const consequences = flags.get('consequences')
171
+ if (typeof decision !== 'string' || decision.trim() === '') {
172
+ return err('--decision <text> is required', 1, { command: 'adr-write' })
173
+ }
174
+ if (typeof consequences !== 'string' || consequences.trim() === '') {
175
+ return err('--consequences <text> is required', 1, { command: 'adr-write' })
176
+ }
177
+
178
+ // Three-gate filter — every gate must be true to write.
179
+ const failed = []
180
+ if (!hardToReverse) failed.push('hard-to-reverse')
181
+ if (!surprisingWithoutContext) failed.push('surprising-without-context')
182
+ if (!realTradeOff) failed.push('real-trade-off')
183
+
184
+ if (failed.length > 0) {
185
+ return exitWith(
186
+ {
187
+ status: 'filtered',
188
+ command: 'adr-write',
189
+ reason: 'three-gate-filter',
190
+ failed_gates: failed,
191
+ note: 'Decision stays in .aperant/context/notes/{task-id}.md. ADRs are reserved for hard-to-reverse, surprising, real-trade-off decisions.',
192
+ },
193
+ 0,
194
+ )
195
+ }
196
+
197
+ // All three gates passed — write the ADR.
198
+ const absProjectDir = resolve(projectDir)
199
+ if (!existsSync(absProjectDir)) {
200
+ return err(`project directory not found: ${absProjectDir}`, 1, { command: 'adr-write' })
201
+ }
202
+
203
+ const adrDir = join(absProjectDir, 'docs', 'adr')
204
+ if (!existsSync(adrDir)) {
205
+ mkdirSync(adrDir, { recursive: true })
206
+ }
207
+ const number = nextAdrNumber(adrDir)
208
+ const slug = slugify(title)
209
+ const paddedNumber = String(number).padStart(4, '0')
210
+ const filename = `${paddedNumber}-${slug}.md`
211
+ const filePath = join(adrDir, filename)
212
+
213
+ const status = (typeof flags.get('status') === 'string' && flags.get('status')) || 'Proposed'
214
+ const context =
215
+ (typeof flags.get('context') === 'string' && flags.get('context')) ||
216
+ '{Context not supplied — provide via --context on a re-run.}'
217
+ const date = new Date().toISOString().slice(0, 10)
218
+
219
+ const body = renderAdr({
220
+ number,
221
+ title,
222
+ status: String(status),
223
+ date,
224
+ context: String(context),
225
+ decision,
226
+ consequences,
227
+ hardToReverse: true,
228
+ surprisingWithoutContext: true,
229
+ realTradeOff: true,
230
+ })
231
+
232
+ writeFileSync(filePath, body)
233
+
234
+ return ok({
235
+ status: 'ok',
236
+ command: 'adr-write',
237
+ file: filePath,
238
+ number,
239
+ slug,
240
+ title,
241
+ passed_gates: ['hard-to-reverse', 'surprising-without-context', 'real-trade-off'],
242
+ })
243
+ }