picopilot 0.0.0 → 0.1.0

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 (334) hide show
  1. package/LICENSE +661 -0
  2. package/dist/bin.d.ts +3 -0
  3. package/dist/bin.d.ts.map +1 -0
  4. package/dist/bin.js +4 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/cli.d.ts +15 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +59 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/audio.d.ts +17 -0
  11. package/dist/commands/audio.d.ts.map +1 -0
  12. package/dist/commands/audio.js +315 -0
  13. package/dist/commands/audio.js.map +1 -0
  14. package/dist/commands/export.d.ts +36 -0
  15. package/dist/commands/export.d.ts.map +1 -0
  16. package/dist/commands/export.js +257 -0
  17. package/dist/commands/export.js.map +1 -0
  18. package/dist/commands/gfx.d.ts +28 -0
  19. package/dist/commands/gfx.d.ts.map +1 -0
  20. package/dist/commands/gfx.js +521 -0
  21. package/dist/commands/gfx.js.map +1 -0
  22. package/dist/commands/init.d.ts +34 -0
  23. package/dist/commands/init.d.ts.map +1 -0
  24. package/dist/commands/init.js +151 -0
  25. package/dist/commands/init.js.map +1 -0
  26. package/dist/commands/lint.d.ts +20 -0
  27. package/dist/commands/lint.d.ts.map +1 -0
  28. package/dist/commands/lint.js +90 -0
  29. package/dist/commands/lint.js.map +1 -0
  30. package/dist/commands/minify.d.ts +20 -0
  31. package/dist/commands/minify.d.ts.map +1 -0
  32. package/dist/commands/minify.js +129 -0
  33. package/dist/commands/minify.js.map +1 -0
  34. package/dist/commands/music.d.ts +13 -0
  35. package/dist/commands/music.d.ts.map +1 -0
  36. package/dist/commands/music.js +241 -0
  37. package/dist/commands/music.js.map +1 -0
  38. package/dist/commands/playtest.d.ts +29 -0
  39. package/dist/commands/playtest.d.ts.map +1 -0
  40. package/dist/commands/playtest.js +631 -0
  41. package/dist/commands/playtest.js.map +1 -0
  42. package/dist/commands/run.d.ts +30 -0
  43. package/dist/commands/run.d.ts.map +1 -0
  44. package/dist/commands/run.js +202 -0
  45. package/dist/commands/run.js.map +1 -0
  46. package/dist/commands/serve.d.ts +50 -0
  47. package/dist/commands/serve.d.ts.map +1 -0
  48. package/dist/commands/serve.js +213 -0
  49. package/dist/commands/serve.js.map +1 -0
  50. package/dist/commands/sfx.d.ts +13 -0
  51. package/dist/commands/sfx.d.ts.map +1 -0
  52. package/dist/commands/sfx.js +254 -0
  53. package/dist/commands/sfx.js.map +1 -0
  54. package/dist/commands/tokens.d.ts +36 -0
  55. package/dist/commands/tokens.d.ts.map +1 -0
  56. package/dist/commands/tokens.js +129 -0
  57. package/dist/commands/tokens.js.map +1 -0
  58. package/dist/commands/verify.d.ts +57 -0
  59. package/dist/commands/verify.d.ts.map +1 -0
  60. package/dist/commands/verify.js +213 -0
  61. package/dist/commands/verify.js.map +1 -0
  62. package/dist/commands/version.d.ts +12 -0
  63. package/dist/commands/version.d.ts.map +1 -0
  64. package/dist/commands/version.js +24 -0
  65. package/dist/commands/version.js.map +1 -0
  66. package/dist/engine/audio/index.d.ts +14 -0
  67. package/dist/engine/audio/index.d.ts.map +1 -0
  68. package/dist/engine/audio/index.js +14 -0
  69. package/dist/engine/audio/index.js.map +1 -0
  70. package/dist/engine/audio/music.d.ts +116 -0
  71. package/dist/engine/audio/music.d.ts.map +1 -0
  72. package/dist/engine/audio/music.js +138 -0
  73. package/dist/engine/audio/music.js.map +1 -0
  74. package/dist/engine/audio/section.d.ts +34 -0
  75. package/dist/engine/audio/section.d.ts.map +1 -0
  76. package/dist/engine/audio/section.js +67 -0
  77. package/dist/engine/audio/section.js.map +1 -0
  78. package/dist/engine/audio/sfx.d.ts +141 -0
  79. package/dist/engine/audio/sfx.d.ts.map +1 -0
  80. package/dist/engine/audio/sfx.js +506 -0
  81. package/dist/engine/audio/sfx.js.map +1 -0
  82. package/dist/engine/cart/cart.d.ts +75 -0
  83. package/dist/engine/cart/cart.d.ts.map +1 -0
  84. package/dist/engine/cart/cart.js +153 -0
  85. package/dist/engine/cart/cart.js.map +1 -0
  86. package/dist/engine/cart/errors.d.ts +29 -0
  87. package/dist/engine/cart/errors.d.ts.map +1 -0
  88. package/dist/engine/cart/errors.js +26 -0
  89. package/dist/engine/cart/errors.js.map +1 -0
  90. package/dist/engine/cart/gfx.d.ts +79 -0
  91. package/dist/engine/cart/gfx.d.ts.map +1 -0
  92. package/dist/engine/cart/gfx.js +171 -0
  93. package/dist/engine/cart/gfx.js.map +1 -0
  94. package/dist/engine/cart/index.d.ts +15 -0
  95. package/dist/engine/cart/index.d.ts.map +1 -0
  96. package/dist/engine/cart/index.js +15 -0
  97. package/dist/engine/cart/index.js.map +1 -0
  98. package/dist/engine/cart/map.d.ts +46 -0
  99. package/dist/engine/cart/map.d.ts.map +1 -0
  100. package/dist/engine/cart/map.js +118 -0
  101. package/dist/engine/cart/map.js.map +1 -0
  102. package/dist/engine/config/config.d.ts +74 -0
  103. package/dist/engine/config/config.d.ts.map +1 -0
  104. package/dist/engine/config/config.js +103 -0
  105. package/dist/engine/config/config.js.map +1 -0
  106. package/dist/engine/config/index.d.ts +7 -0
  107. package/dist/engine/config/index.d.ts.map +1 -0
  108. package/dist/engine/config/index.js +7 -0
  109. package/dist/engine/config/index.js.map +1 -0
  110. package/dist/engine/gfx/grid.d.ts +51 -0
  111. package/dist/engine/gfx/grid.d.ts.map +1 -0
  112. package/dist/engine/gfx/grid.js +116 -0
  113. package/dist/engine/gfx/grid.js.map +1 -0
  114. package/dist/engine/gfx/index.d.ts +17 -0
  115. package/dist/engine/gfx/index.d.ts.map +1 -0
  116. package/dist/engine/gfx/index.js +17 -0
  117. package/dist/engine/gfx/index.js.map +1 -0
  118. package/dist/engine/gfx/label.d.ts +34 -0
  119. package/dist/engine/gfx/label.d.ts.map +1 -0
  120. package/dist/engine/gfx/label.js +190 -0
  121. package/dist/engine/gfx/label.js.map +1 -0
  122. package/dist/engine/gfx/overlap.d.ts +42 -0
  123. package/dist/engine/gfx/overlap.d.ts.map +1 -0
  124. package/dist/engine/gfx/overlap.js +57 -0
  125. package/dist/engine/gfx/overlap.js.map +1 -0
  126. package/dist/engine/gfx/palette.d.ts +29 -0
  127. package/dist/engine/gfx/palette.d.ts.map +1 -0
  128. package/dist/engine/gfx/palette.js +46 -0
  129. package/dist/engine/gfx/palette.js.map +1 -0
  130. package/dist/engine/gfx/png.d.ts +31 -0
  131. package/dist/engine/gfx/png.d.ts.map +1 -0
  132. package/dist/engine/gfx/png.js +165 -0
  133. package/dist/engine/gfx/png.js.map +1 -0
  134. package/dist/engine/gfx/render.d.ts +42 -0
  135. package/dist/engine/gfx/render.d.ts.map +1 -0
  136. package/dist/engine/gfx/render.js +72 -0
  137. package/dist/engine/gfx/render.js.map +1 -0
  138. package/dist/engine/index.d.ts +22 -0
  139. package/dist/engine/index.d.ts.map +1 -0
  140. package/dist/engine/index.js +22 -0
  141. package/dist/engine/index.js.map +1 -0
  142. package/dist/engine/pico8/adapter.d.ts +267 -0
  143. package/dist/engine/pico8/adapter.d.ts.map +1 -0
  144. package/dist/engine/pico8/adapter.js +37 -0
  145. package/dist/engine/pico8/adapter.js.map +1 -0
  146. package/dist/engine/pico8/drive.d.ts +308 -0
  147. package/dist/engine/pico8/drive.d.ts.map +1 -0
  148. package/dist/engine/pico8/drive.js +581 -0
  149. package/dist/engine/pico8/drive.js.map +1 -0
  150. package/dist/engine/pico8/harness.d.ts +146 -0
  151. package/dist/engine/pico8/harness.d.ts.map +1 -0
  152. package/dist/engine/pico8/harness.js +251 -0
  153. package/dist/engine/pico8/harness.js.map +1 -0
  154. package/dist/engine/pico8/index.d.ts +21 -0
  155. package/dist/engine/pico8/index.d.ts.map +1 -0
  156. package/dist/engine/pico8/index.js +21 -0
  157. package/dist/engine/pico8/index.js.map +1 -0
  158. package/dist/engine/pico8/sentinel.d.ts +63 -0
  159. package/dist/engine/pico8/sentinel.d.ts.map +1 -0
  160. package/dist/engine/pico8/sentinel.js +86 -0
  161. package/dist/engine/pico8/sentinel.js.map +1 -0
  162. package/dist/engine/pico8/session-daemon-entry.d.ts +10 -0
  163. package/dist/engine/pico8/session-daemon-entry.d.ts.map +1 -0
  164. package/dist/engine/pico8/session-daemon-entry.js +11 -0
  165. package/dist/engine/pico8/session-daemon-entry.js.map +1 -0
  166. package/dist/engine/pico8/session-daemon-main.d.ts +44 -0
  167. package/dist/engine/pico8/session-daemon-main.d.ts.map +1 -0
  168. package/dist/engine/pico8/session-daemon-main.js +71 -0
  169. package/dist/engine/pico8/session-daemon-main.js.map +1 -0
  170. package/dist/engine/pico8/session-daemon.d.ts +68 -0
  171. package/dist/engine/pico8/session-daemon.d.ts.map +1 -0
  172. package/dist/engine/pico8/session-daemon.js +264 -0
  173. package/dist/engine/pico8/session-daemon.js.map +1 -0
  174. package/dist/engine/pico8/session.d.ts +140 -0
  175. package/dist/engine/pico8/session.d.ts.map +1 -0
  176. package/dist/engine/pico8/session.js +250 -0
  177. package/dist/engine/pico8/session.js.map +1 -0
  178. package/dist/engine/pico8/shell.d.ts +103 -0
  179. package/dist/engine/pico8/shell.d.ts.map +1 -0
  180. package/dist/engine/pico8/shell.js +324 -0
  181. package/dist/engine/pico8/shell.js.map +1 -0
  182. package/dist/engine/pico8/supervisor.d.ts +137 -0
  183. package/dist/engine/pico8/supervisor.d.ts.map +1 -0
  184. package/dist/engine/pico8/supervisor.js +140 -0
  185. package/dist/engine/pico8/supervisor.js.map +1 -0
  186. package/dist/engine/scaffold/index.d.ts +8 -0
  187. package/dist/engine/scaffold/index.d.ts.map +1 -0
  188. package/dist/engine/scaffold/index.js +8 -0
  189. package/dist/engine/scaffold/index.js.map +1 -0
  190. package/dist/engine/scaffold/scaffold.d.ts +49 -0
  191. package/dist/engine/scaffold/scaffold.d.ts.map +1 -0
  192. package/dist/engine/scaffold/scaffold.js +217 -0
  193. package/dist/engine/scaffold/scaffold.js.map +1 -0
  194. package/dist/engine/shrinko/adapter.d.ts +167 -0
  195. package/dist/engine/shrinko/adapter.d.ts.map +1 -0
  196. package/dist/engine/shrinko/adapter.js +107 -0
  197. package/dist/engine/shrinko/adapter.js.map +1 -0
  198. package/dist/engine/shrinko/index.d.ts +13 -0
  199. package/dist/engine/shrinko/index.d.ts.map +1 -0
  200. package/dist/engine/shrinko/index.js +13 -0
  201. package/dist/engine/shrinko/index.js.map +1 -0
  202. package/dist/engine/shrinko/shell.d.ts +69 -0
  203. package/dist/engine/shrinko/shell.d.ts.map +1 -0
  204. package/dist/engine/shrinko/shell.js +156 -0
  205. package/dist/engine/shrinko/shell.js.map +1 -0
  206. package/dist/engine/skills/index.d.ts +8 -0
  207. package/dist/engine/skills/index.d.ts.map +1 -0
  208. package/dist/engine/skills/index.js +8 -0
  209. package/dist/engine/skills/index.js.map +1 -0
  210. package/dist/engine/skills/skills.d.ts +104 -0
  211. package/dist/engine/skills/skills.d.ts.map +1 -0
  212. package/dist/engine/skills/skills.js +149 -0
  213. package/dist/engine/skills/skills.js.map +1 -0
  214. package/dist/index.d.ts +10 -0
  215. package/dist/index.d.ts.map +1 -0
  216. package/dist/index.js +10 -0
  217. package/dist/index.js.map +1 -0
  218. package/dist/version.d.ts +2 -0
  219. package/dist/version.d.ts.map +1 -0
  220. package/dist/version.js +19 -0
  221. package/dist/version.js.map +1 -0
  222. package/package.json +48 -2
  223. package/src/bin.ts +4 -0
  224. package/src/cli.ts +61 -0
  225. package/src/commands/audio.test.ts +280 -0
  226. package/src/commands/audio.ts +410 -0
  227. package/src/commands/export.test.ts +310 -0
  228. package/src/commands/export.ts +316 -0
  229. package/src/commands/gfx-io.test.ts +178 -0
  230. package/src/commands/gfx.test.ts +470 -0
  231. package/src/commands/gfx.ts +645 -0
  232. package/src/commands/init.test.ts +460 -0
  233. package/src/commands/init.ts +193 -0
  234. package/src/commands/lint.test.ts +137 -0
  235. package/src/commands/lint.ts +117 -0
  236. package/src/commands/minify.test.ts +166 -0
  237. package/src/commands/minify.ts +156 -0
  238. package/src/commands/music.test.ts +272 -0
  239. package/src/commands/music.ts +288 -0
  240. package/src/commands/playtest.test.ts +317 -0
  241. package/src/commands/playtest.ts +773 -0
  242. package/src/commands/run.test.ts +238 -0
  243. package/src/commands/run.ts +259 -0
  244. package/src/commands/serve.test.ts +218 -0
  245. package/src/commands/serve.ts +272 -0
  246. package/src/commands/sfx.test.ts +349 -0
  247. package/src/commands/sfx.ts +300 -0
  248. package/src/commands/tokens.test.ts +212 -0
  249. package/src/commands/tokens.ts +168 -0
  250. package/src/commands/verify.test.ts +238 -0
  251. package/src/commands/verify.ts +256 -0
  252. package/src/commands/version.test.ts +70 -0
  253. package/src/commands/version.ts +24 -0
  254. package/src/engine/audio/index.ts +47 -0
  255. package/src/engine/audio/music.test.ts +188 -0
  256. package/src/engine/audio/music.ts +218 -0
  257. package/src/engine/audio/section.test.ts +124 -0
  258. package/src/engine/audio/section.ts +79 -0
  259. package/src/engine/audio/sfx.test.ts +410 -0
  260. package/src/engine/audio/sfx.ts +694 -0
  261. package/src/engine/cart/cart.test.ts +304 -0
  262. package/src/engine/cart/cart.ts +211 -0
  263. package/src/engine/cart/errors.ts +39 -0
  264. package/src/engine/cart/fixtures/crlf.p8 +6 -0
  265. package/src/engine/cart/fixtures/full.p8 +174 -0
  266. package/src/engine/cart/fixtures/header-only.p8 +2 -0
  267. package/src/engine/cart/fixtures/malformed-no-header.p8 +2 -0
  268. package/src/engine/cart/fixtures/minimal.p8 +10 -0
  269. package/src/engine/cart/fixtures/no-trailing-newline.p8 +6 -0
  270. package/src/engine/cart/gfx.ts +195 -0
  271. package/src/engine/cart/index.ts +27 -0
  272. package/src/engine/cart/map.ts +134 -0
  273. package/src/engine/config/config.ts +115 -0
  274. package/src/engine/config/index.ts +12 -0
  275. package/src/engine/gfx/grid.test.ts +128 -0
  276. package/src/engine/gfx/grid.ts +136 -0
  277. package/src/engine/gfx/index.ts +34 -0
  278. package/src/engine/gfx/label.test.ts +105 -0
  279. package/src/engine/gfx/label.ts +229 -0
  280. package/src/engine/gfx/overlap.test.ts +71 -0
  281. package/src/engine/gfx/overlap.ts +66 -0
  282. package/src/engine/gfx/palette.test.ts +35 -0
  283. package/src/engine/gfx/palette.ts +54 -0
  284. package/src/engine/gfx/png.test.ts +111 -0
  285. package/src/engine/gfx/png.ts +196 -0
  286. package/src/engine/gfx/render.test.ts +180 -0
  287. package/src/engine/gfx/render.ts +106 -0
  288. package/src/engine/index.ts +21 -0
  289. package/src/engine/pico8/adapter.ts +303 -0
  290. package/src/engine/pico8/drive.test.ts +415 -0
  291. package/src/engine/pico8/drive.ts +729 -0
  292. package/src/engine/pico8/harness.test.ts +181 -0
  293. package/src/engine/pico8/harness.ts +354 -0
  294. package/src/engine/pico8/index.ts +20 -0
  295. package/src/engine/pico8/sentinel.test.ts +80 -0
  296. package/src/engine/pico8/sentinel.ts +93 -0
  297. package/src/engine/pico8/session-daemon-entry.ts +12 -0
  298. package/src/engine/pico8/session-daemon-main.ts +98 -0
  299. package/src/engine/pico8/session-daemon.test.ts +212 -0
  300. package/src/engine/pico8/session-daemon.ts +306 -0
  301. package/src/engine/pico8/session.test.ts +239 -0
  302. package/src/engine/pico8/session.ts +321 -0
  303. package/src/engine/pico8/shell.test.ts +583 -0
  304. package/src/engine/pico8/shell.ts +412 -0
  305. package/src/engine/pico8/supervisor.test.ts +105 -0
  306. package/src/engine/pico8/supervisor.ts +197 -0
  307. package/src/engine/scaffold/index.ts +15 -0
  308. package/src/engine/scaffold/scaffold.ts +236 -0
  309. package/src/engine/shrinko/adapter.ts +241 -0
  310. package/src/engine/shrinko/fixtures/count-no-compressed.txt +2 -0
  311. package/src/engine/shrinko/fixtures/count-over-budget.txt +3 -0
  312. package/src/engine/shrinko/fixtures/count-under-budget.txt +3 -0
  313. package/src/engine/shrinko/index.ts +33 -0
  314. package/src/engine/shrinko/shell.ts +224 -0
  315. package/src/engine/shrinko/shrinko.test.ts +182 -0
  316. package/src/engine/skills/index.ts +14 -0
  317. package/src/engine/skills/skills.test.ts +376 -0
  318. package/src/engine/skills/skills.ts +205 -0
  319. package/src/index.ts +13 -0
  320. package/src/skills/game-design-reference/SKILL.md +70 -0
  321. package/src/skills/game-jam/SKILL.md +45 -0
  322. package/src/skills/picopilot-art/SKILL.md +62 -0
  323. package/src/skills/picopilot-audio/SKILL.md +194 -0
  324. package/src/skills/picopilot-code/SKILL.md +97 -0
  325. package/src/skills/picopilot-code/reference/README.md +42 -0
  326. package/src/skills/picopilot-code/reference/mode7-racing.md +95 -0
  327. package/src/skills/picopilot-code/reference/platformer.md +119 -0
  328. package/src/skills/picopilot-code/reference/puzzle-grid.md +106 -0
  329. package/src/skills/picopilot-code/reference/rpg-menus-dialog.md +122 -0
  330. package/src/skills/picopilot-code/reference/top-down-adventure.md +117 -0
  331. package/src/skills/picopilot-code/reference/twin-stick-arcade.md +128 -0
  332. package/src/skills/picopilot-debug/SKILL.md +195 -0
  333. package/src/skills/picopilot-overview/SKILL.md +52 -0
  334. package/src/version.ts +22 -0
@@ -0,0 +1,256 @@
1
+ import {existsSync, readFileSync} from 'node:fs';
2
+ import {isAbsolute, resolve} from 'node:path';
3
+ import {type Cli, z} from 'incur';
4
+
5
+ import {Cart, CartParseError} from '../engine/cart/index.js';
6
+ import {
7
+ type ShrinkoAdapter,
8
+ ShrinkoParseError,
9
+ ShellShrinkoAdapter,
10
+ } from '../engine/shrinko/index.js';
11
+ import {TOKEN_BUDGET} from './tokens.js';
12
+
13
+ /**
14
+ * The one-line self-scoping the `picopilot verify` envelope always carries, so
15
+ * the connotation of "verify" never misleads an agent into treating a green
16
+ * static gate as terminal (ADR-0003). It states BOTH facts: verify is static,
17
+ * and passing does NOT mean the cart runs.
18
+ */
19
+ export const VERIFY_SCOPE =
20
+ 'static gate: tokens + integrity; passing does NOT mean the cart runs' as const;
21
+
22
+ /**
23
+ * The exit code for the DISTINCT `gate-incapable` outcome (shrinko absent).
24
+ * Chosen as 2 to be categorically separate from `fail` (exit 1, a cart that
25
+ * flunked a check) and from `pass` (exit 0): an absent capability is neither a
26
+ * green gate nor a failed check but a gate that could not run its most
27
+ * important check, so it gets its own nonzero code an agent can branch on.
28
+ */
29
+ export const GATE_INCAPABLE_EXIT = 2;
30
+
31
+ /** See {@link ShrinkoAdapterFactory} in `tokens.ts` — verify reuses the same seam. */
32
+ export type ShrinkoAdapterFactory = (env: NodeJS.ProcessEnv) => ShrinkoAdapter;
33
+
34
+ const defaultAdapterFactory: ShrinkoAdapterFactory = (env) =>
35
+ new ShellShrinkoAdapter({env});
36
+
37
+ /**
38
+ * The result of the integrity check: the cart parses AND round-trips
39
+ * byte-identically through the cart model (well-formed sections). A cart that
40
+ * fails to parse, or one whose serialize does not reproduce the source, is not
41
+ * well-formed and fails integrity.
42
+ */
43
+ type IntegrityResult = {ok: true} | {ok: false; detail: string};
44
+
45
+ /**
46
+ * Runs the STATIC cart-integrity check against `text`: the cart must parse into
47
+ * the cart model and round-trip cleanly (`Cart.parse(text).serialize() ===
48
+ * text`). This is a purely static, well-formedness check — it never runs the
49
+ * cart. A {@link CartParseError} (malformed header/version/section) or a
50
+ * round-trip mismatch is a fail with a human-readable detail.
51
+ */
52
+ function checkIntegrity(text: string): IntegrityResult {
53
+ let cart: Cart;
54
+ try {
55
+ cart = Cart.parse(text);
56
+ } catch (e) {
57
+ if (e instanceof CartParseError) {
58
+ return {
59
+ ok: false,
60
+ detail: `cart does not parse (${e.code}): ${e.message}`,
61
+ };
62
+ }
63
+ throw e;
64
+ }
65
+ if (cart.serialize() !== text) {
66
+ return {
67
+ ok: false,
68
+ detail:
69
+ 'cart does not round-trip cleanly through the cart model (malformed sections)',
70
+ };
71
+ }
72
+ return {ok: true};
73
+ }
74
+
75
+ /**
76
+ * Registers `picopilot verify` (US #15, #18), the STATIC cart-acceptance gate an
77
+ * agent drives each iteration toward.
78
+ *
79
+ * It runs two static checks and returns ONE structured envelope:
80
+ *
81
+ * - **integrity** — the cart parses and round-trips cleanly through the cart
82
+ * model (well-formed sections). No run.
83
+ * - **tokens** — the cart is within the 8,192-token budget (via the shrinko
84
+ * adapter `--count`).
85
+ *
86
+ * (Lint folds in when the lint task lands; in v1-core verify = tokens +
87
+ * integrity.)
88
+ *
89
+ * Three categorical outcomes (ADR-0003):
90
+ *
91
+ * - **pass** (exit 0) — both checks green. The envelope SELF-SCOPES (states it
92
+ * is static and that passing does not mean the cart runs) and CTAs to
93
+ * `picopilot run` ("static checks pass, now confirm it boots"), so the agent
94
+ * is led from well-formed to actually-runs.
95
+ * - **fail** (exit 1) — a check flunked (over budget, or a malformed/non-parsing
96
+ * cart). A structured `verify-failed` error listing which checks failed.
97
+ * - **gate-incapable** (exit {@link GATE_INCAPABLE_EXIT}) — shrinko is ABSENT,
98
+ * so the token check (the #1 failure mode, token bloat) cannot run. verify
99
+ * returns a DISTINCT `gate-incapable` result, NEVER green, categorically
100
+ * separate from pass and fail. This is the gate-as-theatre regression guard:
101
+ * verify must never report green by skipping its most important check.
102
+ *
103
+ * verify is STATIC and never runs the cart, so it has NO PICO-8 dependency (US
104
+ * #19). It reaches shrinko entirely through the {@link ShrinkoAdapter} seam, so
105
+ * a native-TS shrinko would satisfy it unchanged.
106
+ *
107
+ * @param adapterFactory injects the adapter (defaults to the shell adapter);
108
+ * tests pass a stub to drive present / absent / over-budget without the real
109
+ * binary.
110
+ */
111
+ export function registerVerify(
112
+ cli: Cli.Cli,
113
+ adapterFactory: ShrinkoAdapterFactory = defaultAdapterFactory,
114
+ ): void {
115
+ cli.command('verify', {
116
+ description:
117
+ 'Static acceptance gate: run tokens + cart-integrity and return one pass/fail envelope. Does NOT run the cart. Requires shrinko.',
118
+ args: z.object({
119
+ cart: z
120
+ .string()
121
+ .default('main.p8')
122
+ .describe(
123
+ 'The .p8 cart to verify. Defaults to main.p8 in the current folder.',
124
+ ),
125
+ }),
126
+ // Declared so incur resolves it from the (test-overridable) env source and
127
+ // hands it to the shrinko child; verify itself never runs the cart, so it
128
+ // needs no PICO-8 env.
129
+ env: z.object({
130
+ PATH: z
131
+ .string()
132
+ .optional()
133
+ .describe('Used to locate shrinko8 in the child process.'),
134
+ }),
135
+ output: z.object({
136
+ status: z
137
+ .literal('pass')
138
+ .describe('The overall gate result. Only a green gate reaches ok.'),
139
+ scope: z
140
+ .string()
141
+ .describe(
142
+ 'The self-scoping note: verify is static and passing does NOT mean the cart runs.',
143
+ ),
144
+ checks: z
145
+ .object({
146
+ integrity: z
147
+ .boolean()
148
+ .describe('True when the cart parses and round-trips cleanly.'),
149
+ tokens: z
150
+ .boolean()
151
+ .describe('True when the cart is within the 8,192-token budget.'),
152
+ })
153
+ .describe('Per-check pass/fail, so the agent sees WHAT was checked.'),
154
+ tokens: z.number().describe('The token count shrinko reported.'),
155
+ budget: z.number().describe('The token budget checked against (8192).'),
156
+ }),
157
+ examples: [
158
+ {description: 'Verify main.p8'},
159
+ {description: 'Verify a specific cart', args: {cart: 'game.p8'}},
160
+ ],
161
+ async run({args, env, error, ok}) {
162
+ const cartPath = isAbsolute(args.cart)
163
+ ? args.cart
164
+ : resolve(process.cwd(), args.cart);
165
+
166
+ // A missing cart is a picopilot-side error, distinct from a fail: there is
167
+ // nothing to gate. Surface it clearly rather than reporting a hollow fail.
168
+ if (!existsSync(cartPath)) {
169
+ return error({
170
+ code: 'cart-not-found',
171
+ message: `no cart at ${cartPath}`,
172
+ exitCode: 1,
173
+ });
174
+ }
175
+
176
+ const text = readFileSync(cartPath, 'utf8');
177
+
178
+ // Integrity is a static, shrinko-free check: the cart parses + round-trips.
179
+ const integrity = checkIntegrity(text);
180
+
181
+ // Tokens goes through the shrinko seam. Absent shrinko is the gate-incapable
182
+ // trigger (below); a parse failure of shrinko's own output is shrinko-failed.
183
+ const adapter = adapterFactory(env as NodeJS.ProcessEnv);
184
+ let count: Awaited<ReturnType<ShrinkoAdapter['count']>>;
185
+ try {
186
+ count = await adapter.count(cartPath);
187
+ } catch (e) {
188
+ if (e instanceof ShrinkoParseError) {
189
+ return error({
190
+ code: 'shrinko-failed',
191
+ message: `shrinko ran but its --count output could not be parsed: ${e.message}`,
192
+ exitCode: 1,
193
+ });
194
+ }
195
+ throw e;
196
+ }
197
+
198
+ // GATE-INCAPABLE (ADR-0003): shrinko absent → verify CANNOT check the #1
199
+ // failure mode (token bloat). It returns a DISTINCT outcome, NEVER green,
200
+ // never a silent skip. This is the load-bearing regression guard: verify
201
+ // must not report pass by omitting its most important check.
202
+ if (!count.ok) {
203
+ return error({
204
+ code: 'gate-incapable',
205
+ message: `${VERIFY_SCOPE}. gate-incapable: cannot check tokens without shrinko, so verify will not report green. ${count.remedy} (needs: ${count.needs.join(', ')})`,
206
+ exitCode: GATE_INCAPABLE_EXIT,
207
+ });
208
+ }
209
+
210
+ const tokens = count.value.tokens;
211
+ const tokensOk = tokens <= TOKEN_BUDGET;
212
+
213
+ // FAIL (exit 1): a static check flunked. List every failing check so the
214
+ // agent knows what to fix, not just that something did.
215
+ if (!integrity.ok || !tokensOk) {
216
+ const failed: string[] = [];
217
+ if (!integrity.ok) failed.push(`integrity: ${integrity.detail}`);
218
+ if (!tokensOk) {
219
+ failed.push(
220
+ `tokens: ${tokens} exceeds the ${TOKEN_BUDGET}-token budget`,
221
+ );
222
+ }
223
+ return error({
224
+ code: 'verify-failed',
225
+ message: `${VERIFY_SCOPE}. verify FAILED: ${failed.join('; ')}`,
226
+ exitCode: 1,
227
+ });
228
+ }
229
+
230
+ // PASS (exit 0): both static checks green. Self-scope, then CTA to `run` so
231
+ // the agent is led from well-formed to actually-boots (ADR-0003 CTA).
232
+ return ok(
233
+ {
234
+ status: 'pass' as const,
235
+ scope: VERIFY_SCOPE,
236
+ checks: {integrity: true, tokens: true},
237
+ tokens,
238
+ budget: TOKEN_BUDGET,
239
+ },
240
+ {
241
+ cta: {
242
+ description:
243
+ 'Static checks pass. Now confirm the cart actually boots:',
244
+ commands: [
245
+ {
246
+ command: 'run',
247
+ description:
248
+ 'Launch the cart headless to confirm it boots (verify does not run it).',
249
+ },
250
+ ],
251
+ },
252
+ },
253
+ );
254
+ },
255
+ });
256
+ }
@@ -0,0 +1,70 @@
1
+ import {describe, expect, it} from 'vitest';
2
+ import {createCli} from '../cli.js';
3
+ import {VERSION} from '../version.js';
4
+
5
+ /**
6
+ * Drives the CLI end-to-end through incur's `serve(argv, { stdout, exit })` DI,
7
+ * capturing output and the exit code without touching the real process.
8
+ */
9
+ async function run(argv: string[]) {
10
+ let stdout = '';
11
+ let exitCode = 0;
12
+ await createCli().serve(argv, {
13
+ stdout(s) {
14
+ stdout += s;
15
+ },
16
+ exit(code) {
17
+ exitCode = code;
18
+ },
19
+ });
20
+ return {stdout, exitCode};
21
+ }
22
+
23
+ describe('picopilot version', () => {
24
+ it('runs end-to-end and reports the package version, exit 0', async () => {
25
+ const {stdout, exitCode} = await run(['version']);
26
+ expect(exitCode).toBe(0);
27
+ expect(stdout).toContain(VERSION);
28
+ expect(stdout).toContain('picopilot');
29
+ });
30
+
31
+ it('emits TOON by default (unquoted key: value, not JSON braces)', async () => {
32
+ const {stdout} = await run(['version']);
33
+ // TOON renders `name: picopilot`, never the JSON `"name": "picopilot"`.
34
+ expect(stdout).toContain(`version: ${VERSION}`);
35
+ expect(stdout).not.toContain('"version"');
36
+ expect(stdout.trimStart().startsWith('{')).toBe(false);
37
+ });
38
+
39
+ it('emits parseable JSON under --json', async () => {
40
+ const {stdout, exitCode} = await run(['version', '--json']);
41
+ expect(exitCode).toBe(0);
42
+ const parsed = JSON.parse(stdout);
43
+ expect(parsed).toEqual({name: 'picopilot', version: VERSION});
44
+ });
45
+ });
46
+
47
+ describe('picopilot CLI surface', () => {
48
+ it('--help lists the incur agent-discovery built-ins', async () => {
49
+ const {stdout} = await run(['--help']);
50
+ // incur renders `skills add` / `mcp add` as grouped integrations.
51
+ expect(stdout).toMatch(/skills\s+Sync skill files to agents/);
52
+ expect(stdout).toMatch(/mcp\s+Register as MCP server/);
53
+ expect(stdout).toContain('--format');
54
+ expect(stdout).toContain('--llms');
55
+ expect(stdout).toContain('--mcp');
56
+ });
57
+
58
+ it('exposes `skills add` and `mcp add` as real subcommands', async () => {
59
+ const skills = await run(['skills', '--help']);
60
+ expect(skills.stdout).toMatch(/\badd\b/);
61
+ const mcp = await run(['mcp', '--help']);
62
+ expect(mcp.stdout).toMatch(/\badd\b/);
63
+ });
64
+
65
+ it('--llms emits a machine-readable manifest that includes version', async () => {
66
+ const {stdout, exitCode} = await run(['--llms']);
67
+ expect(exitCode).toBe(0);
68
+ expect(stdout).toContain('version');
69
+ });
70
+ });
@@ -0,0 +1,24 @@
1
+ import {type Cli, z} from 'incur';
2
+ import {VERSION} from '../version.js';
3
+
4
+ /**
5
+ * Registers the trivial `version` command onto the given CLI (or command group).
6
+ *
7
+ * This is the skeleton's proof-of-life command: it exercises the full incur
8
+ * pipeline (output schema, the structured envelope, TOON-by-default and `--json`
9
+ * formatting) with no engine dependencies, so the `verify` gate has something
10
+ * real to build and test. Later command groups mount alongside it via their own
11
+ * `register*` helpers, called from `createCli`.
12
+ */
13
+ export function registerVersion(cli: Cli.Cli): void {
14
+ cli.command('version', {
15
+ description: 'Print the picopilot version.',
16
+ output: z.object({
17
+ name: z.string().describe('The tool name.'),
18
+ version: z.string().describe('The installed picopilot version.'),
19
+ }),
20
+ run() {
21
+ return {name: 'picopilot', version: VERSION};
22
+ },
23
+ });
24
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * `engine/audio` — the shrinko-free, PICO-8-free audio codec. Two halves:
3
+ * the picopilot-MML to `__sfx__` transpiler (finding Part A/B, ADR-0005 +
4
+ * ADR-0008) and the structural pattern-list to `__music__` codec (finding A.5 /
5
+ * B.6, ADR-0005).
6
+ *
7
+ * Mirrors the `engine/gfx` shape: a pure module + this index re-export, no
8
+ * adapter. The `sfx from-mml` command is thin wiring over {@link mmlToSfxRow};
9
+ * the `music from-patterns` command over {@link patternsToMusic}.
10
+ */
11
+ export {mergeSfxRow, setMusicSection, SFX_SLOT_COUNT} from './section.js';
12
+ export {
13
+ AudioMmlError,
14
+ type AudioMmlErrorCode,
15
+ DEFAULT_LENGTH,
16
+ DEFAULT_OCTAVE,
17
+ DEFAULT_SPEED,
18
+ FILTER_BUZZ,
19
+ FILTER_DAMPEN_LEVEL,
20
+ FILTER_DETUNE_1,
21
+ FILTER_DETUNE_2,
22
+ FILTER_MAX_LEVEL,
23
+ FILTER_NOIZ,
24
+ FILTER_REVERB,
25
+ mmlToSfxRow,
26
+ PITCH_MAX,
27
+ PITCH_MIN,
28
+ type RowRoundingWarning,
29
+ SFX_MAX_ROWS,
30
+ SFX_ROW_LENGTH,
31
+ type SfxTranspileResult,
32
+ } from './sfx.js';
33
+ export {
34
+ CHANNEL_OFF_BIT,
35
+ type ChannelRef,
36
+ FLAG_LOOP_BACK,
37
+ FLAG_LOOP_START,
38
+ FLAG_STOP,
39
+ MUSIC_PATTERN_MAX,
40
+ MusicError,
41
+ type MusicErrorCode,
42
+ type MusicTranspileResult,
43
+ type MusicWarning,
44
+ type Pattern,
45
+ patternsToMusic,
46
+ SFX_INDEX_MAX,
47
+ } from './music.js';
@@ -0,0 +1,188 @@
1
+ import {describe, expect, it} from 'vitest';
2
+
3
+ import {
4
+ MUSIC_PATTERN_MAX,
5
+ MusicError,
6
+ type Pattern,
7
+ patternsToMusic,
8
+ } from './music.js';
9
+
10
+ /**
11
+ * Table-driven codec tests: a known pattern list -> a known `__music__` row,
12
+ * asserted BYTE-FOR-BYTE against the finding's Part A.5 (`FF CCCCCCCC` = a 2-hex
13
+ * flag byte + space + 4 channel bytes; flag bits loop-start=0x01, loop-back=0x02,
14
+ * stop=0x04, combinable; channel byte = sfx 0..63, bit6=0x40 = off).
15
+ */
16
+ describe('patternsToMusic: one pattern -> one FF CCCCCCCC row (finding A.5)', () => {
17
+ const cases: Array<{name: string; pattern: Pattern; row: string}> = [
18
+ {
19
+ name: 'a plain 4-channel pattern (no flags, no off) -> flag 00',
20
+ pattern: {channels: [0, 1, 2, 3]},
21
+ row: '00 00010203',
22
+ },
23
+ {
24
+ name: 'loop-start alone -> flag 01',
25
+ pattern: {channels: [0, 0, 0, 0], loopStart: true},
26
+ row: '01 00000000',
27
+ },
28
+ {
29
+ name: 'loop-back alone -> flag 02 (with a loop-start elsewhere, no warn)',
30
+ // A lone loopBack would warn; here we test the FLAG BYTE in isolation.
31
+ pattern: {channels: [1, 1, 1, 1], loopBack: true},
32
+ row: '02 01010101',
33
+ },
34
+ {
35
+ name: 'stop alone -> flag 04',
36
+ pattern: {channels: [5, 6, 7, 8], stop: true},
37
+ row: '04 05060708',
38
+ },
39
+ {
40
+ name: 'combined loop-start + stop -> flag 05 (0x01 | 0x04)',
41
+ pattern: {channels: [0, 0, 0, 0], loopStart: true, stop: true},
42
+ row: '05 00000000',
43
+ },
44
+ {
45
+ name: 'all three flags -> flag 07 (0x01 | 0x02 | 0x04)',
46
+ pattern: {
47
+ channels: [0, 0, 0, 0],
48
+ loopStart: true,
49
+ loopBack: true,
50
+ stop: true,
51
+ },
52
+ row: '07 00000000',
53
+ },
54
+ {
55
+ name: 'an off channel sets bit6 -> byte 40 (finding A.5: null = bare 0x40)',
56
+ pattern: {channels: [null, 1, 2, 3]},
57
+ row: '00 40010203',
58
+ },
59
+ {
60
+ name: 'the finding\u2019s verified 0x40|17 = 51 example (off + sfx 17)... but null carries no index, so a null ch0 is 40',
61
+ pattern: {channels: [null, null, null, null]},
62
+ row: '00 40404040',
63
+ },
64
+ {
65
+ name: 'max SFX index 63 -> byte 3f',
66
+ pattern: {channels: [63, 63, 63, 63]},
67
+ row: '00 3f3f3f3f',
68
+ },
69
+ ];
70
+
71
+ for (const {name, pattern, row} of cases) {
72
+ it(name, () => {
73
+ const result = patternsToMusic([pattern]);
74
+ expect(result.section).toBe(`${row}\n`);
75
+ expect(result.patterns).toBe(1);
76
+ });
77
+ }
78
+ });
79
+
80
+ describe('patternsToMusic: "off" is NOT "sfx 0" (finding B.6 / B.7 #9)', () => {
81
+ it('sfx 0 -> byte 00 (a real, playing SFX)', () => {
82
+ expect(patternsToMusic([{channels: [0, 1, 2, 3]}]).section).toBe(
83
+ '00 00010203\n',
84
+ );
85
+ });
86
+
87
+ it('off -> byte 40 (bit6 set, silent), DISTINCT from sfx 0', () => {
88
+ expect(patternsToMusic([{channels: [null, 1, 2, 3]}]).section).toBe(
89
+ '00 40010203\n',
90
+ );
91
+ });
92
+
93
+ it('the two are different bytes for the same channel position', () => {
94
+ const sfx0 = patternsToMusic([{channels: [0, 0, 0, 0]}]).section;
95
+ const off = patternsToMusic([{channels: [null, null, null, null]}]).section;
96
+ expect(sfx0).toBe('00 00000000\n');
97
+ expect(off).toBe('00 40404040\n');
98
+ expect(sfx0).not.toBe(off);
99
+ });
100
+ });
101
+
102
+ describe('patternsToMusic: multiple patterns are song order (finding A.5/B.6)', () => {
103
+ it('emits one row per pattern, top to bottom = song order', () => {
104
+ const result = patternsToMusic([
105
+ {channels: [0, 1, 2, 3], loopStart: true},
106
+ {channels: [4, 5, null, 6]},
107
+ {channels: [0, 0, 0, 0], loopBack: true, stop: true},
108
+ ]);
109
+ expect(result.section).toBe(
110
+ ['01 00010203', '00 04054006', '06 00000000'].join('\n') + '\n',
111
+ );
112
+ expect(result.patterns).toBe(3);
113
+ });
114
+ });
115
+
116
+ describe('patternsToMusic: loop-back without loop-start WARNS, not errors (finding B.6)', () => {
117
+ it('a lone loopBack (no loopStart anywhere) warns about the pattern-0 fall-back', () => {
118
+ const result = patternsToMusic([
119
+ {channels: [0, 0, 0, 0]},
120
+ {channels: [1, 1, 1, 1], loopBack: true},
121
+ ]);
122
+ expect(result.warnings).toHaveLength(1);
123
+ expect(result.warnings[0]!.code).toBe('loop-back-without-loop-start');
124
+ expect(result.warnings[0]!.message).toContain('pattern 0');
125
+ // It still emits (does NOT hard-fail).
126
+ expect(result.patterns).toBe(2);
127
+ });
128
+
129
+ it('a loopBack WITH a loopStart somewhere does NOT warn', () => {
130
+ const result = patternsToMusic([
131
+ {channels: [0, 0, 0, 0], loopStart: true},
132
+ {channels: [1, 1, 1, 1], loopBack: true},
133
+ ]);
134
+ expect(result.warnings).toHaveLength(0);
135
+ });
136
+ });
137
+
138
+ describe('patternsToMusic: out-of-range + malformed refusals (NO silent clamp)', () => {
139
+ it('an SFX index > 63 -> audio-music-sfx-out-of-range', () => {
140
+ try {
141
+ patternsToMusic([{channels: [64, 0, 0, 0]}]);
142
+ expect.fail('expected a MusicError');
143
+ } catch (e) {
144
+ expect(e).toBeInstanceOf(MusicError);
145
+ expect((e as MusicError).code).toBe('audio-music-sfx-out-of-range');
146
+ }
147
+ });
148
+
149
+ it('a negative SFX index -> audio-music-sfx-out-of-range', () => {
150
+ expect(() => patternsToMusic([{channels: [-1, 0, 0, 0]}])).toThrow(
151
+ MusicError,
152
+ );
153
+ });
154
+
155
+ it('the wrong channel count -> audio-music-bad-channel-count', () => {
156
+ try {
157
+ // Deliberately malformed (3 channels) via a cast, mirroring how a
158
+ // hand-authored JSON payload could arrive.
159
+ patternsToMusic([
160
+ {channels: [0, 1, 2] as unknown as Pattern['channels']},
161
+ ]);
162
+ expect.fail('expected a MusicError');
163
+ } catch (e) {
164
+ expect((e as MusicError).code).toBe('audio-music-bad-channel-count');
165
+ }
166
+ });
167
+
168
+ it('an empty list -> audio-music-empty', () => {
169
+ try {
170
+ patternsToMusic([]);
171
+ expect.fail('expected a MusicError');
172
+ } catch (e) {
173
+ expect((e as MusicError).code).toBe('audio-music-empty');
174
+ }
175
+ });
176
+
177
+ it('more than 64 patterns -> audio-music-too-many-patterns', () => {
178
+ const many: Pattern[] = Array.from({length: MUSIC_PATTERN_MAX + 1}, () => ({
179
+ channels: [0, 0, 0, 0] as Pattern['channels'],
180
+ }));
181
+ try {
182
+ patternsToMusic(many);
183
+ expect.fail('expected a MusicError');
184
+ } catch (e) {
185
+ expect((e as MusicError).code).toBe('audio-music-too-many-patterns');
186
+ }
187
+ });
188
+ });