@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
@@ -2,16 +2,18 @@
2
2
  * run-cmd.mjs — D-05 in-process command harness.
3
3
  *
4
4
  * `runCmd(cmdFn, args, opts)` invokes a Layer 1 command function in-process
5
- * (no subprocess), captures stdout, traps any process.exit calls (via a
6
- * try/finally restore), and returns:
5
+ * (no subprocess), captures stdout + stderr, traps any process.exit calls
6
+ * (via a try/finally restore), stubs `process.stdin.isTTY` /
7
+ * `process.stdout.isTTY` to `false` for the trapped invocation duration, and
8
+ * returns:
7
9
  *
8
- * { result, stdout, exitCode }
10
+ * { result, stdout, stderr, exitCode }
9
11
  *
10
12
  * `result` is the cmd function's return value (typically `{envelope, exitCode}`
11
13
  * after the migration; `null` if the cmd called process.exit before returning).
12
- * `stdout` is the captured stdout.write payload (string). `exitCode` is the
13
- * effective exit code: the trapped exit code if process.exit was called, else
14
- * `result.exitCode`, else 0.
14
+ * `stdout` / `stderr` are the captured `*.write` payloads (string). `exitCode`
15
+ * is the effective exit code: the trapped exit code if process.exit was called,
16
+ * else `result.exitCode`, else 0.
15
17
  *
16
18
  * D-05 (d) decision: strictExit DEFAULTS TO TRUE — the harness throws a
17
19
  * strict-mode-violation Error any time a Layer 1 cmd calls process.exit.
@@ -19,10 +21,23 @@
19
21
  * `{ strictExit: false }` until the underlying cmd is rewritten. After Wave 5
20
22
  * (subtask 25) zero `strictExit: false` opt-outs remain in __tests__/.
21
23
  *
22
- * Restoration: process.exit and process.stdout.write are saved at entry and
23
- * restored unconditionally in a try/finally. A test that crashes mid-call
24
- * still leaves the global state byte-identical for the next test in the file.
24
+ * Restoration: process.exit, process.stdout.write, process.stderr.write, and
25
+ * the two `isTTY` bits are saved at entry and restored unconditionally in a
26
+ * try/finally. A test that crashes mid-call still leaves the global state
27
+ * byte-identical for the next test in the file.
28
+ *
29
+ * Cache resets (F3 subtask 1): by default the harness clears the two
30
+ * path-keyed module-graph caches that can leak across cmd invocations within
31
+ * a single vitest worker — `skill-discover._discoverCache` and the
32
+ * `agent-identity` identity cache. The `gate/registry` and
33
+ * `consistency/registry` registries are NOT cleared by default because their
34
+ * built-in entries are populated by barrel imports that ESM evaluates exactly
35
+ * ONCE per process — clearing them would leave subsequent cmd invocations
36
+ * looking at an empty registry. Tests that specifically need an empty
37
+ * registry import `__resetCaches()` from those modules directly.
25
38
  */
39
+ import { __resetCaches as __resetAgentIdentityCaches } from '../cli/coordination/agent-identity.mjs';
40
+ import { __resetCaches as __resetSkillDiscoverCaches } from '../cli/route/skill-discover.mjs';
26
41
  class ExitTrap extends Error {
27
42
  constructor(code) {
28
43
  super(`apt-tools cmd called process.exit(${code})`);
@@ -32,18 +47,26 @@ class ExitTrap extends Error {
32
47
  /**
33
48
  * @param {Function} cmdFn — the Layer 1 cmd function (sync or async).
34
49
  * @param {unknown[]} [args=[]] — argv-shaped positional args.
35
- * @param {{ strictExit?: boolean, env?: Record<string, string> }} [opts]
36
- * @returns {Promise<{ result: unknown, stdout: string, exitCode: number }>}
50
+ * @param {{ strictExit?: boolean, resetCaches?: boolean, env?: Record<string, string> }} [opts]
51
+ * @returns {Promise<{ result: unknown, stdout: string, stderr: string, exitCode: number }>}
37
52
  */
38
53
  export async function runCmd(cmdFn, args = [], opts = {}) {
39
- const { strictExit = true } = opts;
54
+ const { strictExit = true, resetCaches = true } = opts;
55
+ if (resetCaches) {
56
+ __resetSkillDiscoverCaches();
57
+ __resetAgentIdentityCaches();
58
+ }
40
59
  let trappedExit = null;
41
60
  const origExit = process.exit;
42
- // Save the unbound original — assignment to process.stdout.write replaces
61
+ // Save the unbound originals — assignment to process.stdout.write replaces
43
62
  // the property; restoration of the original reference returns the global
44
63
  // to byte-identical state (Function-identity check in self-tests).
45
64
  const origWrite = process.stdout.write;
65
+ const origErrWrite = process.stderr.write;
66
+ const origStdinIsTTY = process.stdin.isTTY;
67
+ const origStdoutIsTTY = process.stdout.isTTY;
46
68
  const stdoutBuf = [];
69
+ const stderrBuf = [];
47
70
  process.exit = (code) => {
48
71
  trappedExit = typeof code === 'number' ? code : 0;
49
72
  if (strictExit) {
@@ -59,6 +82,16 @@ export async function runCmd(cmdFn, args = [], opts = {}) {
59
82
  stdoutBuf.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8'));
60
83
  return true;
61
84
  };
85
+ process.stderr.write = (chunk, ..._rest) => {
86
+ stderrBuf.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8'));
87
+ return true;
88
+ };
89
+ // F3 subtask 1 (Q3): defensively force the non-interactive branch for any
90
+ // cmd that gates on isTTY (init.mjs:127,167,1013 + cmdState's TTY sidecar).
91
+ // Under vitest workers isTTY is usually `undefined`, but stubbing to `false`
92
+ // removes the Node-version drift risk.
93
+ process.stdin.isTTY = false;
94
+ process.stdout.isTTY = false;
62
95
  try {
63
96
  const result = await cmdFn(...args);
64
97
  const exitCode = trappedExit !== null
@@ -66,11 +99,16 @@ export async function runCmd(cmdFn, args = [], opts = {}) {
66
99
  : typeof result === 'object' && result !== null && typeof result.exitCode === 'number'
67
100
  ? result.exitCode
68
101
  : 0;
69
- return { result, stdout: stdoutBuf.join(''), exitCode };
102
+ return { result, stdout: stdoutBuf.join(''), stderr: stderrBuf.join(''), exitCode };
70
103
  }
71
104
  catch (e) {
72
105
  if (e instanceof ExitTrap) {
73
- return { result: null, stdout: stdoutBuf.join(''), exitCode: e.code };
106
+ return {
107
+ result: null,
108
+ stdout: stdoutBuf.join(''),
109
+ stderr: stderrBuf.join(''),
110
+ exitCode: e.code,
111
+ };
74
112
  }
75
113
  // Re-throw non-trap errors (including the strict-mode-violation Error
76
114
  // thrown above) so tests can assert via expect(...).rejects.
@@ -79,6 +117,9 @@ export async function runCmd(cmdFn, args = [], opts = {}) {
79
117
  finally {
80
118
  process.exit = origExit;
81
119
  process.stdout.write = origWrite;
120
+ process.stderr.write = origErrWrite;
121
+ process.stdin.isTTY = origStdinIsTTY;
122
+ process.stdout.isTTY = origStdoutIsTTY;
82
123
  }
83
124
  }
84
125
  //# sourceMappingURL=run-cmd.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"run-cmd.mjs","sourceRoot":"","sources":["../../src/__test-helpers/run-cmd.mjs"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,QAAS,SAAQ,KAAK;IAC3B,YAAY,IAAI;QACf,KAAK,CAAC,qCAAqC,IAAI,GAAG,CAAC,CAAA;QACnD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IACjB,CAAC;CACD;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,KAAK,EAAE,IAAI,GAAG,EAAE,EAAE,IAAI,GAAG,EAAE;IACvD,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,GAAG,IAAI,CAAA;IAElC,IAAI,WAAW,GAAG,IAAI,CAAA;IACtB,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAA;IAC7B,0EAA0E;IAC1E,yEAAyE;IACzE,mEAAmE;IACnE,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAA;IACtC,MAAM,SAAS,GAAG,EAAE,CAAA;IAEpB,OAAO,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,EAAE;QACvB,WAAW,GAAG,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;QACjD,IAAI,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CACd,gBAAgB,WAAW,iDAAiD;gBAC3E,uFAAuF;gBACvF,yFAAyF,CAC1F,CAAA;QACF,CAAC;QACD,kEAAkE;QAClE,sDAAsD;QACtD,MAAM,IAAI,QAAQ,CAAC,WAAW,CAAC,CAAA;IAChC,CAAC,CAAA;IAED,OAAO,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,EAAE,EAAE;QAC1C,SAAS,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;QACxF,OAAO,IAAI,CAAA;IACZ,CAAC,CAAA;IAED,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,CAAA;QACnC,MAAM,QAAQ,GACb,WAAW,KAAK,IAAI;YACnB,CAAC,CAAC,WAAW;YACb,CAAC,CAAC,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ;gBACrF,CAAC,CAAC,MAAM,CAAC,QAAQ;gBACjB,CAAC,CAAC,CAAC,CAAA;QACN,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAA;IACxD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACZ,IAAI,CAAC,YAAY,QAAQ,EAAE,CAAC;YAC3B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QACtE,CAAC;QACD,sEAAsE;QACtE,6DAA6D;QAC7D,MAAM,CAAC,CAAA;IACR,CAAC;YAAS,CAAC;QACV,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAA;QACvB,OAAO,CAAC,MAAM,CAAC,KAAK,GAAG,SAAS,CAAA;IACjC,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"run-cmd.mjs","sourceRoot":"","sources":["../../src/__test-helpers/run-cmd.mjs"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @param {string} root
3
+ * @param {number|string} pr
4
+ */
5
+ export function readLock(root: string, pr: number | string): any;
6
+ /**
7
+ * Try to acquire the per-PR lock. On EEXIST, parse the existing lock — if the
8
+ * holder PID is dead AND TTL has elapsed, unlink + retry once. Otherwise
9
+ * return `{status:'busy', holder}`.
10
+ *
11
+ * @param {string} root
12
+ * @param {number|string} pr
13
+ * @param {string} sessionId
14
+ * @param {number} [pid=process.pid]
15
+ */
16
+ export function acquireLock(root: string, pr: number | string, sessionId: string, pid?: number): {
17
+ status: string;
18
+ lock: {
19
+ sessionId: string;
20
+ pid: number;
21
+ acquiredAt: string;
22
+ ttl_seconds: number;
23
+ };
24
+ holder?: undefined;
25
+ } | {
26
+ status: string;
27
+ holder: any;
28
+ lock?: undefined;
29
+ };
30
+ /**
31
+ * Release the per-PR lock. Owner must match (`sessionId` equality). Returns
32
+ * `true` if released, `false` if the lock is missing or owned by someone
33
+ * else.
34
+ *
35
+ * @param {string} root
36
+ * @param {number|string} pr
37
+ * @param {string} sessionId
38
+ */
39
+ export function releaseLock(root: string, pr: number | string, sessionId: string): boolean;
40
+ /**
41
+ * Is the given lock stale (i.e., recoverable by a new caller)? Per D-05,
42
+ * BOTH conditions are required: dead PID AND past TTL.
43
+ *
44
+ * Exposed for unit tests; the orchestrator does not call this directly.
45
+ *
46
+ * @param {{ pid: number, acquiredAt: string, ttl_seconds: number }} lockData
47
+ */
48
+ export function isStale(lockData: {
49
+ pid: number;
50
+ acquiredAt: string;
51
+ ttl_seconds: number;
52
+ }): boolean;
53
+ //# sourceMappingURL=lock.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.d.mts","sourceRoot":"","sources":["../../../src/cli/ci-watch/lock.mjs"],"names":[],"mappings":"AA8CA;;;GAGG;AACH,+BAHW,MAAM,MACN,MAAM,GAAC,MAAM,OAWvB;AAYD;;;;;;;;;GASG;AACH,kCALW,MAAM,MACN,MAAM,GAAC,MAAM,aACb,MAAM,QACN,MAAM;;;;;;;;;;;;;EAwDhB;AAED;;;;;;;;GAQG;AACH,kCAJW,MAAM,MACN,MAAM,GAAC,MAAM,aACb,MAAM,WAiBhB;AAED;;;;;;;GAOG;AACH,kCAFW;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,WAQlE"}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * ci-watch/lock.mjs — per-PR O_EXCL atomic lock (D-05).
3
+ *
4
+ * Mirrors Claude Code's `cronTasksLock.ts`: a per-PR `.aperant/ci-watches/{pr}.lock`
5
+ * file created with `openSync(..., 'wx')` (O_EXCL). Two concurrent callers
6
+ * cannot both create the file; the loser reads the existing lock and decides
7
+ * whether to declare BUSY or recover a stale lock.
8
+ *
9
+ * Stale-recovery (spec.md §8): a lock is recoverable iff
10
+ * (a) holder PID is dead (`!isPidAlive`), AND
11
+ * (b) `Date.now() - acquiredAt > ttl_seconds*1000`.
12
+ * Live PID alone is NEVER recoverable, even past TTL — TTL alone is not
13
+ * enough. Per D-05: PID-based stale recovery.
14
+ *
15
+ * Lock file shape (spec.md §8):
16
+ * { sessionId, pid, acquiredAt, ttl_seconds }
17
+ *
18
+ * Return shapes:
19
+ * acquireLock → { status: 'ok', lock: {...} }
20
+ * OR { status: 'busy', holder: {...} }
21
+ * releaseLock → boolean (true on release, false if owner mismatch / missing)
22
+ * readLock → lock object or null
23
+ */
24
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeSync, } from 'node:fs';
25
+ import { join, resolve } from 'node:path';
26
+ import { isPidAlive } from '../lock/pid.mjs';
27
+ const TTL_SECONDS = 1800; // 30 min — D-05
28
+ function watchesDir(root) {
29
+ return join(resolve(root), '.aperant', 'ci-watches');
30
+ }
31
+ function lockPath(root, pr) {
32
+ return join(watchesDir(root), `${pr}.lock`);
33
+ }
34
+ /**
35
+ * @param {string} root
36
+ * @param {number|string} pr
37
+ */
38
+ export function readLock(root, pr) {
39
+ const path = lockPath(root, pr);
40
+ if (!existsSync(path))
41
+ return null;
42
+ try {
43
+ return JSON.parse(readFileSync(path, 'utf-8'));
44
+ }
45
+ catch {
46
+ // A corrupted lock is treated as no lock — re-acquire will overwrite.
47
+ return null;
48
+ }
49
+ }
50
+ function writeLockFile(path, payload) {
51
+ mkdirSync(resolve(path, '..'), { recursive: true });
52
+ const fd = openSync(path, 'wx');
53
+ try {
54
+ writeSync(fd, `${JSON.stringify(payload, null, 2)}\n`, 0, 'utf-8');
55
+ }
56
+ finally {
57
+ closeSync(fd);
58
+ }
59
+ }
60
+ /**
61
+ * Try to acquire the per-PR lock. On EEXIST, parse the existing lock — if the
62
+ * holder PID is dead AND TTL has elapsed, unlink + retry once. Otherwise
63
+ * return `{status:'busy', holder}`.
64
+ *
65
+ * @param {string} root
66
+ * @param {number|string} pr
67
+ * @param {string} sessionId
68
+ * @param {number} [pid=process.pid]
69
+ */
70
+ export function acquireLock(root, pr, sessionId, pid = process.pid) {
71
+ const path = lockPath(root, pr);
72
+ const payload = {
73
+ sessionId,
74
+ pid,
75
+ acquiredAt: new Date().toISOString(),
76
+ ttl_seconds: TTL_SECONDS,
77
+ };
78
+ try {
79
+ writeLockFile(path, payload);
80
+ return { status: 'ok', lock: payload };
81
+ }
82
+ catch (err) {
83
+ if (err.code !== 'EEXIST')
84
+ throw err;
85
+ }
86
+ // Lock file exists — inspect.
87
+ const existing = readLock(root, pr);
88
+ if (!existing) {
89
+ // Corrupted lock — treat as recoverable, unlink + retry once.
90
+ try {
91
+ unlinkSync(path);
92
+ }
93
+ catch {
94
+ /* race — another caller cleaned up */
95
+ }
96
+ try {
97
+ writeLockFile(path, payload);
98
+ return { status: 'ok', lock: payload };
99
+ }
100
+ catch (err) {
101
+ if (err.code === 'EEXIST') {
102
+ return { status: 'busy', holder: readLock(root, pr) };
103
+ }
104
+ throw err;
105
+ }
106
+ }
107
+ if (isStale(existing)) {
108
+ try {
109
+ unlinkSync(path);
110
+ }
111
+ catch {
112
+ /* race — another caller cleaned up */
113
+ }
114
+ try {
115
+ writeLockFile(path, payload);
116
+ return { status: 'ok', lock: payload };
117
+ }
118
+ catch (err) {
119
+ if (err.code === 'EEXIST') {
120
+ return { status: 'busy', holder: readLock(root, pr) };
121
+ }
122
+ throw err;
123
+ }
124
+ }
125
+ return { status: 'busy', holder: existing };
126
+ }
127
+ /**
128
+ * Release the per-PR lock. Owner must match (`sessionId` equality). Returns
129
+ * `true` if released, `false` if the lock is missing or owned by someone
130
+ * else.
131
+ *
132
+ * @param {string} root
133
+ * @param {number|string} pr
134
+ * @param {string} sessionId
135
+ */
136
+ export function releaseLock(root, pr, sessionId) {
137
+ const path = lockPath(root, pr);
138
+ if (!existsSync(path))
139
+ return false;
140
+ const existing = readLock(root, pr);
141
+ if (!existing) {
142
+ // Corrupted — fail open (let next caller clean up).
143
+ return false;
144
+ }
145
+ if (existing.sessionId !== sessionId)
146
+ return false;
147
+ try {
148
+ unlinkSync(path);
149
+ return true;
150
+ }
151
+ catch {
152
+ return false;
153
+ }
154
+ }
155
+ /**
156
+ * Is the given lock stale (i.e., recoverable by a new caller)? Per D-05,
157
+ * BOTH conditions are required: dead PID AND past TTL.
158
+ *
159
+ * Exposed for unit tests; the orchestrator does not call this directly.
160
+ *
161
+ * @param {{ pid: number, acquiredAt: string, ttl_seconds: number }} lockData
162
+ */
163
+ export function isStale(lockData) {
164
+ if (!lockData || typeof lockData !== 'object')
165
+ return false;
166
+ const ttlMs = (lockData.ttl_seconds || TTL_SECONDS) * 1000;
167
+ const age = Date.now() - new Date(lockData.acquiredAt).getTime();
168
+ if (age <= ttlMs)
169
+ return false;
170
+ return !isPidAlive(lockData.pid);
171
+ }
172
+ //# sourceMappingURL=lock.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.mjs","sourceRoot":"","sources":["../../../src/cli/ci-watch/lock.mjs"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Read the per-PR state file. Returns `null` when the file does not exist.
3
+ * Returns `{ error: 'corrupted', message }` when the file exists but is not
4
+ * parseable JSON — callers route that to the infra-backoff branch.
5
+ *
6
+ * @param {string} root
7
+ * @param {number|string} pr
8
+ */
9
+ export function readState(root: string, pr: number | string): any;
10
+ /**
11
+ * Atomically write the per-PR state file. Caller passes the full state object
12
+ * — this function does not merge, it overwrites. Use `readState` first if a
13
+ * merge is needed.
14
+ *
15
+ * @param {string} root
16
+ * @param {number|string} pr
17
+ * @param {object} state
18
+ */
19
+ export function writeState(root: string, pr: number | string, state: object): void;
20
+ /**
21
+ * Delete the per-PR state file. No-op when the file does not exist.
22
+ *
23
+ * @param {string} root
24
+ * @param {number|string} pr
25
+ */
26
+ export function deleteState(root: string, pr: number | string): void;
27
+ /**
28
+ * Enumerate every per-PR state file under `.aperant/ci-watches/`. Returns an
29
+ * array of parsed state objects in lexicographic filename order. Skips files
30
+ * whose name does not match `<pr>.json` and skips corrupted files (their
31
+ * presence does not block the rest of the listing).
32
+ *
33
+ * @param {string} root
34
+ */
35
+ export function listWatches(root: string): any[];
36
+ //# sourceMappingURL=state.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.mts","sourceRoot":"","sources":["../../../src/cli/ci-watch/state.mjs"],"names":[],"mappings":"AAsCA;;;;;;;GAOG;AACH,gCAHW,MAAM,MACN,MAAM,GAAC,MAAM,OAUvB;AAED;;;;;;;;GAQG;AACH,iCAJW,MAAM,MACN,MAAM,GAAC,MAAM,SACb,MAAM,QAIhB;AAED;;;;;GAKG;AACH,kCAHW,MAAM,MACN,MAAM,GAAC,MAAM,QAKvB;AAED;;;;;;;GAOG;AACH,kCAFW,MAAM,SAgBhB"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * ci-watch/state.mjs — per-PR state-file primitives for the /apt:watch-ci
3
+ * post-ship watcher (D-04).
4
+ *
5
+ * Storage shape (`.aperant/ci-watches/{pr}.json`), spec.md §8:
6
+ *
7
+ * {
8
+ * pr, task_id, host, scheduler_handle | null,
9
+ * attempts, started_at, deadline, last_poll,
10
+ * last_status | null, next_tick_interval_ms,
11
+ * paused, auto_merge_on_green
12
+ * }
13
+ *
14
+ * This module is pure read/write — classification logic lives in
15
+ * stop-matrix.mjs, the orchestrator lives in commands/ci-watch.mjs.
16
+ *
17
+ * Crash-safety: `writeState` goes through `atomicWriteJson` (tmp + fsync +
18
+ * rename). `readState` returns `null` for a missing file and never throws on
19
+ * a missing path — callers can distinguish "no watch yet" from "watch is
20
+ * corrupted".
21
+ *
22
+ * The corrupted-JSON branch returns a recoverable error envelope rather than
23
+ * throwing so the orchestrator can surface it without crashing the cron
24
+ * payload mid-tick.
25
+ */
26
+ import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
27
+ import { join, resolve } from 'node:path';
28
+ import { atomicWriteJson } from '../util/io.mjs';
29
+ function watchesDir(root) {
30
+ return join(resolve(root), '.aperant', 'ci-watches');
31
+ }
32
+ function stateFile(root, pr) {
33
+ return join(watchesDir(root), `${pr}.json`);
34
+ }
35
+ /**
36
+ * Read the per-PR state file. Returns `null` when the file does not exist.
37
+ * Returns `{ error: 'corrupted', message }` when the file exists but is not
38
+ * parseable JSON — callers route that to the infra-backoff branch.
39
+ *
40
+ * @param {string} root
41
+ * @param {number|string} pr
42
+ */
43
+ export function readState(root, pr) {
44
+ const path = stateFile(root, pr);
45
+ if (!existsSync(path))
46
+ return null;
47
+ try {
48
+ return JSON.parse(readFileSync(path, 'utf-8'));
49
+ }
50
+ catch (err) {
51
+ return { error: 'corrupted', message: err.message };
52
+ }
53
+ }
54
+ /**
55
+ * Atomically write the per-PR state file. Caller passes the full state object
56
+ * — this function does not merge, it overwrites. Use `readState` first if a
57
+ * merge is needed.
58
+ *
59
+ * @param {string} root
60
+ * @param {number|string} pr
61
+ * @param {object} state
62
+ */
63
+ export function writeState(root, pr, state) {
64
+ atomicWriteJson(stateFile(root, pr), state);
65
+ }
66
+ /**
67
+ * Delete the per-PR state file. No-op when the file does not exist.
68
+ *
69
+ * @param {string} root
70
+ * @param {number|string} pr
71
+ */
72
+ export function deleteState(root, pr) {
73
+ const path = stateFile(root, pr);
74
+ if (existsSync(path))
75
+ unlinkSync(path);
76
+ }
77
+ /**
78
+ * Enumerate every per-PR state file under `.aperant/ci-watches/`. Returns an
79
+ * array of parsed state objects in lexicographic filename order. Skips files
80
+ * whose name does not match `<pr>.json` and skips corrupted files (their
81
+ * presence does not block the rest of the listing).
82
+ *
83
+ * @param {string} root
84
+ */
85
+ export function listWatches(root) {
86
+ const dir = watchesDir(root);
87
+ if (!existsSync(dir))
88
+ return [];
89
+ const watches = [];
90
+ for (const f of readdirSync(dir).sort()) {
91
+ if (!/^\d+\.json$/.test(f))
92
+ continue;
93
+ try {
94
+ watches.push(JSON.parse(readFileSync(join(dir, f), 'utf-8')));
95
+ }
96
+ catch {
97
+ // Skip corrupted state files — listWatches is a read-only enumerator
98
+ // and must not throw on partial corruption.
99
+ }
100
+ }
101
+ return watches;
102
+ }
103
+ //# sourceMappingURL=state.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.mjs","sourceRoot":"","sources":["../../../src/cli/ci-watch/state.mjs"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAC3E,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAEhD,SAAS,UAAU,CAAC,IAAI;IACvB,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,YAAY,CAAC,CAAA;AACrD,CAAC;AAED,SAAS,SAAS,CAAC,IAAI,EAAE,EAAE;IAC1B,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,CAAA;AAC5C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,IAAI,EAAE,EAAE;IACjC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;IAChC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAClC,IAAI,CAAC;QACJ,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAA;IAC/C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAA;IACpD,CAAC;AACF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,IAAI,EAAE,EAAE,EAAE,KAAK;IACzC,eAAe,CAAC,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC,CAAA;AAC5C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,IAAI,EAAE,EAAE;IACnC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;IAChC,IAAI,UAAU,CAAC,IAAI,CAAC;QAAE,UAAU,CAAC,IAAI,CAAC,CAAA;AACvC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,IAAI;IAC/B,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,CAAA;IAC5B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAA;IAC/B,MAAM,OAAO,GAAG,EAAE,CAAA;IAClB,KAAK,MAAM,CAAC,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QACzC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,SAAQ;QACpC,IAAI,CAAC;YACJ,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;QAC9D,CAAC;QAAC,MAAM,CAAC;YACR,qEAAqE;YACrE,4CAA4C;QAC7C,CAAC;IACF,CAAC;IACD,OAAO,OAAO,CAAA;AACf,CAAC"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @typedef {Object} CheckRow
3
+ * @property {string} [name]
4
+ * @property {string} [conclusion] e.g. 'success' | 'failure' | 'cancelled' | 'timed_out' | 'neutral'
5
+ * @property {string} [state]
6
+ * @property {string} [bucket]
7
+ * @property {string} [failure_class] — optional pre-classified hint (test-only seam)
8
+ * @property {string} [log_fingerprint] — optional digest for identical-log detection
9
+ *
10
+ * @typedef {Object} ClassifyInput
11
+ * @property {CheckRow[]} checks
12
+ * @property {Object} state — per-PR state (see state.mjs §8)
13
+ * @property {Object} prMetadata — { state: 'OPEN'|'CLOSED'|'MERGED', reviewDecision?: string }
14
+ *
15
+ * @typedef {Object} ClassifyOutput
16
+ * @property {string} action
17
+ * @property {boolean} counts_toward_budget
18
+ * @property {string} reason
19
+ *
20
+ * @param {ClassifyInput} input
21
+ * @returns {ClassifyOutput}
22
+ */
23
+ export function classify({ checks, state, prMetadata }: ClassifyInput): ClassifyOutput;
24
+ export function isFailing(check: any): boolean;
25
+ export type CheckRow = {
26
+ name?: string | undefined;
27
+ /**
28
+ * e.g. 'success' | 'failure' | 'cancelled' | 'timed_out' | 'neutral'
29
+ */
30
+ conclusion?: string | undefined;
31
+ state?: string | undefined;
32
+ bucket?: string | undefined;
33
+ /**
34
+ * — optional pre-classified hint (test-only seam)
35
+ */
36
+ failure_class?: string | undefined;
37
+ /**
38
+ * — optional digest for identical-log detection
39
+ */
40
+ log_fingerprint?: string | undefined;
41
+ };
42
+ export type ClassifyInput = {
43
+ checks: CheckRow[];
44
+ /**
45
+ * — per-PR state (see state.mjs §8)
46
+ */
47
+ state: Object;
48
+ /**
49
+ * — { state: 'OPEN'|'CLOSED'|'MERGED', reviewDecision?: string }
50
+ */
51
+ prMetadata: Object;
52
+ };
53
+ export type ClassifyOutput = {
54
+ action: string;
55
+ counts_toward_budget: boolean;
56
+ reason: string;
57
+ };
58
+ //# sourceMappingURL=stop-matrix.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stop-matrix.d.mts","sourceRoot":"","sources":["../../../src/cli/ci-watch/stop-matrix.mjs"],"names":[],"mappings":"AA8BA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wDAHW,aAAa,GACX,cAAc,CA6H1B;AAED,+CAGC;;;;;;;;;;;;;;;;;;;YA5Ia,QAAQ,EAAE;;;;WACV,MAAM;;;;gBACN,MAAM;;;YAGN,MAAM;0BACN,OAAO;YACP,MAAM"}