discoclaw 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 (393) hide show
  1. package/.context/README.md +42 -0
  2. package/.context/architecture.md +58 -0
  3. package/.context/bot-setup.md +24 -0
  4. package/.context/dev.md +230 -0
  5. package/.context/discord.md +144 -0
  6. package/.context/memory.md +257 -0
  7. package/.context/ops.md +59 -0
  8. package/.context/pa-safety.md +47 -0
  9. package/.context/pa.md +118 -0
  10. package/.context/project.md +43 -0
  11. package/.context/runtime.md +253 -0
  12. package/.context/tasks.md +71 -0
  13. package/.context/tools.md +75 -0
  14. package/.env.example +88 -0
  15. package/.env.example.full +378 -0
  16. package/LICENSE +21 -0
  17. package/README.md +220 -0
  18. package/dist/beads/auto-tag.js +2 -0
  19. package/dist/beads/auto-tag.test.js +62 -0
  20. package/dist/beads/bd-cli.js +9 -0
  21. package/dist/beads/bd-cli.test.js +495 -0
  22. package/dist/beads/bead-hooks-cli.js +149 -0
  23. package/dist/beads/bead-sync-cli.js +5 -0
  24. package/dist/beads/bead-sync-cli.test.js +72 -0
  25. package/dist/beads/bead-sync-coordinator.js +4 -0
  26. package/dist/beads/bead-sync-coordinator.test.js +239 -0
  27. package/dist/beads/bead-sync-watcher.js +2 -0
  28. package/dist/beads/bead-sync-watcher.test.js +96 -0
  29. package/dist/beads/bead-sync.js +7 -0
  30. package/dist/beads/bead-sync.test.js +876 -0
  31. package/dist/beads/bead-thread-cache.js +8 -0
  32. package/dist/beads/bead-thread-cache.test.js +91 -0
  33. package/dist/beads/discord-sync.js +18 -0
  34. package/dist/beads/discord-sync.test.js +782 -0
  35. package/dist/beads/find-bead-by-thread.test.js +36 -0
  36. package/dist/beads/forum-guard.js +2 -0
  37. package/dist/beads/forum-guard.test.js +204 -0
  38. package/dist/beads/initialize.js +3 -0
  39. package/dist/beads/initialize.test.js +304 -0
  40. package/dist/beads/types.js +10 -0
  41. package/dist/cli/daemon-installer.js +225 -0
  42. package/dist/cli/daemon-installer.test.js +289 -0
  43. package/dist/cli/index.js +42 -0
  44. package/dist/cli/init-wizard.js +374 -0
  45. package/dist/cli/init-wizard.test.js +191 -0
  46. package/dist/config.js +385 -0
  47. package/dist/config.test.js +589 -0
  48. package/dist/cron/auto-tag.js +100 -0
  49. package/dist/cron/auto-tag.test.js +91 -0
  50. package/dist/cron/cadence.js +74 -0
  51. package/dist/cron/cadence.test.js +53 -0
  52. package/dist/cron/cron-sync-coordinator.js +66 -0
  53. package/dist/cron/cron-sync-coordinator.test.js +118 -0
  54. package/dist/cron/cron-sync.js +165 -0
  55. package/dist/cron/cron-sync.test.js +228 -0
  56. package/dist/cron/cron-tag-map-watcher.js +128 -0
  57. package/dist/cron/cron-tag-map-watcher.test.js +155 -0
  58. package/dist/cron/default-timezone.js +23 -0
  59. package/dist/cron/default-timezone.test.js +30 -0
  60. package/dist/cron/discord-sync.js +205 -0
  61. package/dist/cron/discord-sync.test.js +353 -0
  62. package/dist/cron/executor.js +303 -0
  63. package/dist/cron/executor.test.js +614 -0
  64. package/dist/cron/forum-sync.js +347 -0
  65. package/dist/cron/forum-sync.test.js +539 -0
  66. package/dist/cron/job-lock.js +164 -0
  67. package/dist/cron/job-lock.test.js +178 -0
  68. package/dist/cron/parser.js +68 -0
  69. package/dist/cron/parser.test.js +115 -0
  70. package/dist/cron/run-control.js +24 -0
  71. package/dist/cron/run-control.test.js +27 -0
  72. package/dist/cron/run-stats.js +265 -0
  73. package/dist/cron/run-stats.test.js +160 -0
  74. package/dist/cron/scheduler.js +97 -0
  75. package/dist/cron/scheduler.test.js +112 -0
  76. package/dist/cron/tag-map.js +47 -0
  77. package/dist/cron/tag-map.test.js +64 -0
  78. package/dist/cron/types.js +1 -0
  79. package/dist/discoclaw-plan-format.test.js +137 -0
  80. package/dist/discoclaw-recipe-format.test.js +137 -0
  81. package/dist/discord/abort-registry.js +70 -0
  82. package/dist/discord/action-categories.js +36 -0
  83. package/dist/discord/action-types.js +1 -0
  84. package/dist/discord/action-utils.js +58 -0
  85. package/dist/discord/action-utils.test.js +58 -0
  86. package/dist/discord/actions-beads.js +1 -0
  87. package/dist/discord/actions-beads.test.js +372 -0
  88. package/dist/discord/actions-bot-profile.js +107 -0
  89. package/dist/discord/actions-bot-profile.test.js +138 -0
  90. package/dist/discord/actions-channels.js +427 -0
  91. package/dist/discord/actions-channels.test.js +697 -0
  92. package/dist/discord/actions-config.js +173 -0
  93. package/dist/discord/actions-config.test.js +322 -0
  94. package/dist/discord/actions-crons.js +586 -0
  95. package/dist/discord/actions-crons.test.js +499 -0
  96. package/dist/discord/actions-defer.js +60 -0
  97. package/dist/discord/actions-defer.test.js +134 -0
  98. package/dist/discord/actions-forge.js +134 -0
  99. package/dist/discord/actions-forge.test.js +206 -0
  100. package/dist/discord/actions-guild.js +301 -0
  101. package/dist/discord/actions-guild.test.js +386 -0
  102. package/dist/discord/actions-memory.js +106 -0
  103. package/dist/discord/actions-memory.test.js +248 -0
  104. package/dist/discord/actions-messaging.js +401 -0
  105. package/dist/discord/actions-messaging.test.js +738 -0
  106. package/dist/discord/actions-moderation.js +65 -0
  107. package/dist/discord/actions-moderation.test.js +88 -0
  108. package/dist/discord/actions-plan.js +445 -0
  109. package/dist/discord/actions-plan.test.js +610 -0
  110. package/dist/discord/actions-poll.js +38 -0
  111. package/dist/discord/actions-poll.test.js +93 -0
  112. package/dist/discord/actions-tasks.js +3 -0
  113. package/dist/discord/actions-tasks.test.js +418 -0
  114. package/dist/discord/actions.js +600 -0
  115. package/dist/discord/actions.test.js +522 -0
  116. package/dist/discord/allowed-mentions.js +3 -0
  117. package/dist/discord/allowed-mentions.test.js +17 -0
  118. package/dist/discord/allowlist.js +29 -0
  119. package/dist/discord/allowlist.test.js +24 -0
  120. package/dist/discord/audit-handler.js +191 -0
  121. package/dist/discord/audit-handler.test.js +361 -0
  122. package/dist/discord/bot.js +141 -0
  123. package/dist/discord/channel-context.js +181 -0
  124. package/dist/discord/defer-scheduler.js +45 -0
  125. package/dist/discord/destructive-confirmation.js +128 -0
  126. package/dist/discord/destructive-confirmation.test.js +49 -0
  127. package/dist/discord/discord-plan-auto-implement.test.js +18 -0
  128. package/dist/discord/durable-memory.js +145 -0
  129. package/dist/discord/durable-memory.test.js +281 -0
  130. package/dist/discord/durable-write-queue.js +4 -0
  131. package/dist/discord/file-download.js +308 -0
  132. package/dist/discord/file-download.test.js +303 -0
  133. package/dist/discord/forge-audit-verdict.js +140 -0
  134. package/dist/discord/forge-auto-implement.js +80 -0
  135. package/dist/discord/forge-auto-implement.test.js +110 -0
  136. package/dist/discord/forge-commands.js +698 -0
  137. package/dist/discord/forge-commands.test.js +1606 -0
  138. package/dist/discord/forge-plan-registry.js +68 -0
  139. package/dist/discord/forge-plan-registry.test.js +127 -0
  140. package/dist/discord/forum-count-sync.js +130 -0
  141. package/dist/discord/forum-count-sync.test.js +200 -0
  142. package/dist/discord/health-command.js +98 -0
  143. package/dist/discord/health-command.test.js +195 -0
  144. package/dist/discord/help-command.js +22 -0
  145. package/dist/discord/help-command.test.js +49 -0
  146. package/dist/discord/image-download.js +201 -0
  147. package/dist/discord/image-download.test.js +499 -0
  148. package/dist/discord/inflight-replies.js +228 -0
  149. package/dist/discord/inflight-replies.test.js +295 -0
  150. package/dist/discord/json-extract.js +110 -0
  151. package/dist/discord/keyed-queue.js +22 -0
  152. package/dist/discord/memory-commands.js +85 -0
  153. package/dist/discord/memory-commands.test.js +159 -0
  154. package/dist/discord/memory-timing.integration.test.js +159 -0
  155. package/dist/discord/message-coordinator.js +2347 -0
  156. package/dist/discord/message-coordinator.onboarding.test.js +183 -0
  157. package/dist/discord/message-coordinator.plan-run.test.js +264 -0
  158. package/dist/discord/message-history.js +53 -0
  159. package/dist/discord/message-history.test.js +95 -0
  160. package/dist/discord/models-command.js +59 -0
  161. package/dist/discord/models-command.test.js +150 -0
  162. package/dist/discord/nickname.test.js +76 -0
  163. package/dist/discord/onboarding-completion.js +55 -0
  164. package/dist/discord/onboarding-completion.test.js +176 -0
  165. package/dist/discord/output-common.js +178 -0
  166. package/dist/discord/output-common.test.js +198 -0
  167. package/dist/discord/output-utils.js +156 -0
  168. package/dist/discord/parse-identity-name.test.js +129 -0
  169. package/dist/discord/plan-commands.js +612 -0
  170. package/dist/discord/plan-commands.test.js +1622 -0
  171. package/dist/discord/plan-manager.js +1491 -0
  172. package/dist/discord/plan-manager.test.js +2380 -0
  173. package/dist/discord/plan-parser.js +110 -0
  174. package/dist/discord/plan-parser.test.js +63 -0
  175. package/dist/discord/plan-run-phase-start.js +20 -0
  176. package/dist/discord/plan-run-phase-start.test.js +29 -0
  177. package/dist/discord/platform-message.js +45 -0
  178. package/dist/discord/platform-message.test.js +110 -0
  179. package/dist/discord/prompt-common.js +240 -0
  180. package/dist/discord/prompt-common.test.js +423 -0
  181. package/dist/discord/reaction-handler.js +691 -0
  182. package/dist/discord/reaction-handler.test.js +1574 -0
  183. package/dist/discord/reaction-prompts.js +118 -0
  184. package/dist/discord/reaction-prompts.test.js +253 -0
  185. package/dist/discord/reply-reference.js +66 -0
  186. package/dist/discord/reply-reference.test.js +125 -0
  187. package/dist/discord/restart-command.js +143 -0
  188. package/dist/discord/restart-command.test.js +196 -0
  189. package/dist/discord/runtime-utils.js +43 -0
  190. package/dist/discord/runtime-utils.test.js +112 -0
  191. package/dist/discord/session-key.js +7 -0
  192. package/dist/discord/session-key.test.js +13 -0
  193. package/dist/discord/shortterm-memory.js +166 -0
  194. package/dist/discord/shortterm-memory.test.js +345 -0
  195. package/dist/discord/shutdown-context.js +122 -0
  196. package/dist/discord/shutdown-context.test.js +279 -0
  197. package/dist/discord/startup-profile.test.js +214 -0
  198. package/dist/discord/status-channel.js +190 -0
  199. package/dist/discord/status-channel.test.js +282 -0
  200. package/dist/discord/status-command.js +206 -0
  201. package/dist/discord/status-command.test.js +341 -0
  202. package/dist/discord/streaming-progress.js +107 -0
  203. package/dist/discord/streaming-progress.test.js +93 -0
  204. package/dist/discord/summarizer.js +89 -0
  205. package/dist/discord/summarizer.test.js +245 -0
  206. package/dist/discord/system-bootstrap.js +396 -0
  207. package/dist/discord/system-bootstrap.test.js +724 -0
  208. package/dist/discord/thread-context.js +169 -0
  209. package/dist/discord/thread-context.test.js +386 -0
  210. package/dist/discord/tool-aware-queue.js +116 -0
  211. package/dist/discord/tool-aware-queue.test.js +180 -0
  212. package/dist/discord/update-command.js +127 -0
  213. package/dist/discord/update-command.test.js +275 -0
  214. package/dist/discord/user-errors.js +40 -0
  215. package/dist/discord/user-errors.test.js +31 -0
  216. package/dist/discord/user-turn-to-durable.js +111 -0
  217. package/dist/discord/user-turn-to-durable.test.js +273 -0
  218. package/dist/discord-followup.test.js +677 -0
  219. package/dist/discord.channel-context.test.js +95 -0
  220. package/dist/discord.fail-closed.test.js +199 -0
  221. package/dist/discord.health-command.integration.test.js +140 -0
  222. package/dist/discord.js +190 -0
  223. package/dist/discord.prompt-context.test.js +1431 -0
  224. package/dist/discord.render.test.js +621 -0
  225. package/dist/discord.status-wiring.test.js +187 -0
  226. package/dist/engine/claudeCli.js +137 -0
  227. package/dist/engine/types.js +1 -0
  228. package/dist/group-queue.js +25 -0
  229. package/dist/health/credential-check.js +175 -0
  230. package/dist/health/credential-check.test.js +401 -0
  231. package/dist/health/startup-healing.js +139 -0
  232. package/dist/health/startup-healing.test.js +298 -0
  233. package/dist/identity.js +36 -0
  234. package/dist/index.js +1378 -0
  235. package/dist/logging/logger-like.js +1 -0
  236. package/dist/observability/memory-sampler.js +51 -0
  237. package/dist/observability/memory-sampler.test.js +93 -0
  238. package/dist/observability/metrics.js +88 -0
  239. package/dist/observability/metrics.test.js +42 -0
  240. package/dist/onboarding/onboarding-flow.js +246 -0
  241. package/dist/onboarding/onboarding-flow.test.js +238 -0
  242. package/dist/onboarding/onboarding-writer.js +102 -0
  243. package/dist/onboarding/onboarding-writer.test.js +143 -0
  244. package/dist/pidlock.js +187 -0
  245. package/dist/pidlock.test.js +128 -0
  246. package/dist/pipeline/engine.js +206 -0
  247. package/dist/pipeline/engine.test.js +771 -0
  248. package/dist/root-policy.js +21 -0
  249. package/dist/root-policy.test.js +55 -0
  250. package/dist/runtime/claude-code-cli.js +35 -0
  251. package/dist/runtime/claude-code-cli.test.js +1199 -0
  252. package/dist/runtime/cli-adapter.js +584 -0
  253. package/dist/runtime/cli-output-parsers.js +108 -0
  254. package/dist/runtime/cli-shared.js +96 -0
  255. package/dist/runtime/cli-shared.test.js +104 -0
  256. package/dist/runtime/cli-strategy.js +6 -0
  257. package/dist/runtime/codex-cli.js +16 -0
  258. package/dist/runtime/codex-cli.test.js +862 -0
  259. package/dist/runtime/concurrency-limit.js +80 -0
  260. package/dist/runtime/concurrency-limit.test.js +137 -0
  261. package/dist/runtime/gemini-cli.js +16 -0
  262. package/dist/runtime/gemini-cli.test.js +413 -0
  263. package/dist/runtime/long-running-process.js +415 -0
  264. package/dist/runtime/long-running-process.test.js +318 -0
  265. package/dist/runtime/model-smoke-helpers.js +160 -0
  266. package/dist/runtime/model-smoke.test.js +194 -0
  267. package/dist/runtime/model-tiers.js +33 -0
  268. package/dist/runtime/model-tiers.test.js +65 -0
  269. package/dist/runtime/openai-auth.js +151 -0
  270. package/dist/runtime/openai-auth.test.js +361 -0
  271. package/dist/runtime/openai-compat.js +178 -0
  272. package/dist/runtime/openai-compat.test.js +449 -0
  273. package/dist/runtime/process-pool.js +93 -0
  274. package/dist/runtime/process-pool.test.js +148 -0
  275. package/dist/runtime/registry.js +15 -0
  276. package/dist/runtime/registry.test.js +47 -0
  277. package/dist/runtime/session-scanner.js +186 -0
  278. package/dist/runtime/session-scanner.test.js +257 -0
  279. package/dist/runtime/strategies/claude-strategy.js +193 -0
  280. package/dist/runtime/strategies/codex-strategy.js +161 -0
  281. package/dist/runtime/strategies/gemini-strategy.js +64 -0
  282. package/dist/runtime/strategies/template-strategy.js +85 -0
  283. package/dist/runtime/tool-capabilities.js +27 -0
  284. package/dist/runtime/tool-capabilities.test.js +24 -0
  285. package/dist/runtime/tool-labels.js +48 -0
  286. package/dist/runtime/types.js +2 -0
  287. package/dist/sessionManager.js +47 -0
  288. package/dist/sessions.js +18 -0
  289. package/dist/tasks/architecture-contract.js +33 -0
  290. package/dist/tasks/architecture-contract.test.js +90 -0
  291. package/dist/tasks/auto-tag.js +50 -0
  292. package/dist/tasks/auto-tag.test.js +64 -0
  293. package/dist/tasks/bd-cli.js +164 -0
  294. package/dist/tasks/bd-cli.test.js +359 -0
  295. package/dist/tasks/bead-sync.js +1 -0
  296. package/dist/tasks/context-summary.js +27 -0
  297. package/dist/tasks/discord-sync.js +3 -0
  298. package/dist/tasks/discord-sync.test.js +685 -0
  299. package/dist/tasks/discord-types.js +4 -0
  300. package/dist/tasks/find-task-by-thread.test.js +36 -0
  301. package/dist/tasks/forum-guard.js +81 -0
  302. package/dist/tasks/forum-guard.test.js +192 -0
  303. package/dist/tasks/initialize.js +77 -0
  304. package/dist/tasks/initialize.test.js +263 -0
  305. package/dist/tasks/logger-types.js +1 -0
  306. package/dist/tasks/metrics-types.js +3 -0
  307. package/dist/tasks/migrate.js +33 -0
  308. package/dist/tasks/migrate.test.js +156 -0
  309. package/dist/tasks/path-defaults.js +67 -0
  310. package/dist/tasks/path-defaults.test.js +73 -0
  311. package/dist/tasks/runtime-types.js +1 -0
  312. package/dist/tasks/service.js +33 -0
  313. package/dist/tasks/service.test.js +51 -0
  314. package/dist/tasks/store.js +238 -0
  315. package/dist/tasks/store.test.js +417 -0
  316. package/dist/tasks/sync-context.js +1 -0
  317. package/dist/tasks/sync-contract.js +24 -0
  318. package/dist/tasks/sync-contract.test.js +25 -0
  319. package/dist/tasks/sync-coordinator-metrics.js +41 -0
  320. package/dist/tasks/sync-coordinator-retries.js +71 -0
  321. package/dist/tasks/sync-coordinator.js +96 -0
  322. package/dist/tasks/sync-coordinator.test.js +501 -0
  323. package/dist/tasks/sync-types.js +1 -0
  324. package/dist/tasks/sync-watcher.js +27 -0
  325. package/dist/tasks/sync-watcher.test.js +92 -0
  326. package/dist/tasks/tag-map.js +36 -0
  327. package/dist/tasks/tag-map.test.js +54 -0
  328. package/dist/tasks/task-action-contract.js +16 -0
  329. package/dist/tasks/task-action-contract.test.js +16 -0
  330. package/dist/tasks/task-action-executor.js +18 -0
  331. package/dist/tasks/task-action-executor.test.js +420 -0
  332. package/dist/tasks/task-action-mutation-helpers.js +17 -0
  333. package/dist/tasks/task-action-mutations.js +151 -0
  334. package/dist/tasks/task-action-prompt.js +62 -0
  335. package/dist/tasks/task-action-read-ops.js +73 -0
  336. package/dist/tasks/task-action-runner-types.js +1 -0
  337. package/dist/tasks/task-action-thread-sync.js +82 -0
  338. package/dist/tasks/task-actions.js +3 -0
  339. package/dist/tasks/task-cli.js +227 -0
  340. package/dist/tasks/task-context.js +1 -0
  341. package/dist/tasks/task-lifecycle.js +46 -0
  342. package/dist/tasks/task-lifecycle.test.js +35 -0
  343. package/dist/tasks/task-sync-apply-plan.js +95 -0
  344. package/dist/tasks/task-sync-apply-types.js +12 -0
  345. package/dist/tasks/task-sync-apply.js +319 -0
  346. package/dist/tasks/task-sync-cli.js +89 -0
  347. package/dist/tasks/task-sync-cli.test.js +70 -0
  348. package/dist/tasks/task-sync-engine.js +88 -0
  349. package/dist/tasks/task-sync-engine.test.js +934 -0
  350. package/dist/tasks/task-sync-phase-apply.js +171 -0
  351. package/dist/tasks/task-sync-pipeline.js +2 -0
  352. package/dist/tasks/task-sync-pipeline.test.js +265 -0
  353. package/dist/tasks/task-sync-reconcile-plan.js +182 -0
  354. package/dist/tasks/task-sync-reconcile.js +144 -0
  355. package/dist/tasks/task-sync.js +56 -0
  356. package/dist/tasks/task-sync.test.js +86 -0
  357. package/dist/tasks/thread-cache.js +42 -0
  358. package/dist/tasks/thread-cache.test.js +89 -0
  359. package/dist/tasks/thread-contracts.test.js +711 -0
  360. package/dist/tasks/thread-forum-ops.js +68 -0
  361. package/dist/tasks/thread-helpers.js +86 -0
  362. package/dist/tasks/thread-helpers.test.js +33 -0
  363. package/dist/tasks/thread-lifecycle-ops.js +144 -0
  364. package/dist/tasks/thread-ops-shared.js +21 -0
  365. package/dist/tasks/thread-ops.js +2 -0
  366. package/dist/tasks/types.js +20 -0
  367. package/dist/tasks/types.test.js +60 -0
  368. package/dist/test-setup.js +11 -0
  369. package/dist/test-setup.test.js +42 -0
  370. package/dist/transport/types.js +1 -0
  371. package/dist/validate.js +41 -0
  372. package/dist/validate.test.js +94 -0
  373. package/dist/version.js +15 -0
  374. package/dist/version.test.js +31 -0
  375. package/dist/webhook/server.js +199 -0
  376. package/dist/webhook/server.test.js +460 -0
  377. package/dist/workspace-bootstrap.js +135 -0
  378. package/dist/workspace-bootstrap.test.js +514 -0
  379. package/dist/workspace-permissions.js +134 -0
  380. package/dist/workspace-permissions.test.js +181 -0
  381. package/package.json +74 -0
  382. package/scripts/cron/cron-tag-map.json +9 -0
  383. package/scripts/tasks/tag-map.json +10 -0
  384. package/systemd/discoclaw.service +19 -0
  385. package/templates/recipes/integration.discoclaw-recipe.md +171 -0
  386. package/templates/workspace/AGENTS.md +217 -0
  387. package/templates/workspace/BOOTSTRAP.md +1 -0
  388. package/templates/workspace/HEARTBEAT.md +10 -0
  389. package/templates/workspace/IDENTITY.md +16 -0
  390. package/templates/workspace/MEMORY.md +24 -0
  391. package/templates/workspace/SOUL.md +52 -0
  392. package/templates/workspace/TOOLS.md +304 -0
  393. package/templates/workspace/USER.md +37 -0
@@ -0,0 +1,134 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { KNOWN_TOOLS } from './config.js';
4
+ const VALID_TIERS = new Set(['readonly', 'standard', 'full', 'custom']);
5
+ export const MAX_NOTE_LENGTH = 500;
6
+ export const TIER_TOOLS = {
7
+ readonly: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch'],
8
+ standard: ['Read', 'Edit', 'Glob', 'Grep', 'WebSearch', 'WebFetch'],
9
+ full: ['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'WebSearch', 'WebFetch'],
10
+ };
11
+ function validatedNote(obj, filePath, log) {
12
+ if (typeof obj.note !== 'string')
13
+ return {};
14
+ if (obj.note.length > MAX_NOTE_LENGTH) {
15
+ log?.warn?.({ filePath, length: obj.note.length, max: MAX_NOTE_LENGTH }, 'workspace-permissions: note exceeds max length, ignoring');
16
+ return {};
17
+ }
18
+ return { note: obj.note };
19
+ }
20
+ /**
21
+ * Load and validate workspace/PERMISSIONS.json. Returns null if the file
22
+ * doesn't exist or is invalid (with a warning logged for invalid files).
23
+ */
24
+ export async function loadWorkspacePermissions(workspaceCwd, log) {
25
+ const filePath = path.join(workspaceCwd, 'PERMISSIONS.json');
26
+ let raw;
27
+ try {
28
+ raw = await fs.readFile(filePath, 'utf-8');
29
+ }
30
+ catch {
31
+ return null; // File doesn't exist — use fallback.
32
+ }
33
+ let parsed;
34
+ try {
35
+ parsed = JSON.parse(raw);
36
+ }
37
+ catch {
38
+ log?.warn?.({ filePath }, 'workspace-permissions: invalid JSON, ignoring');
39
+ return null;
40
+ }
41
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
42
+ log?.warn?.({ filePath }, 'workspace-permissions: expected object, ignoring');
43
+ return null;
44
+ }
45
+ const obj = parsed;
46
+ if (typeof obj.tier !== 'string' || !VALID_TIERS.has(obj.tier)) {
47
+ log?.warn?.({ filePath, tier: obj.tier }, 'workspace-permissions: invalid tier, ignoring');
48
+ return null;
49
+ }
50
+ const tier = obj.tier;
51
+ if (tier === 'custom') {
52
+ if (!Array.isArray(obj.tools) || !obj.tools.every((t) => typeof t === 'string')) {
53
+ log?.warn?.({ filePath }, 'workspace-permissions: custom tier requires tools array, ignoring');
54
+ return null;
55
+ }
56
+ const tools = obj.tools;
57
+ if (tools.length === 0) {
58
+ log?.warn?.({ filePath }, 'workspace-permissions: custom tier has empty tools array');
59
+ }
60
+ const unknown = tools.filter((t) => !KNOWN_TOOLS.has(t));
61
+ if (unknown.length) {
62
+ log?.warn?.({ filePath, unknown }, 'workspace-permissions: unknown tool names');
63
+ }
64
+ return {
65
+ tier,
66
+ tools,
67
+ ...validatedNote(obj, filePath, log),
68
+ };
69
+ }
70
+ return {
71
+ tier,
72
+ ...validatedNote(obj, filePath, log),
73
+ };
74
+ }
75
+ /**
76
+ * Probe workspace/PERMISSIONS.json and return a discriminated result
77
+ * indicating whether the file is valid, missing, or invalid (with reason).
78
+ */
79
+ export async function probeWorkspacePermissions(workspaceCwd) {
80
+ const filePath = path.join(workspaceCwd, 'PERMISSIONS.json');
81
+ let raw;
82
+ try {
83
+ raw = await fs.readFile(filePath, 'utf-8');
84
+ }
85
+ catch {
86
+ return { status: 'missing' };
87
+ }
88
+ let parsed;
89
+ try {
90
+ parsed = JSON.parse(raw);
91
+ }
92
+ catch {
93
+ return { status: 'invalid', reason: 'invalid JSON' };
94
+ }
95
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
96
+ return { status: 'invalid', reason: 'expected object' };
97
+ }
98
+ const obj = parsed;
99
+ if (typeof obj.tier !== 'string' || !VALID_TIERS.has(obj.tier)) {
100
+ return { status: 'invalid', reason: `invalid tier: ${JSON.stringify(obj.tier)}` };
101
+ }
102
+ const tier = obj.tier;
103
+ if (tier === 'custom') {
104
+ if (!Array.isArray(obj.tools) || !obj.tools.every((t) => typeof t === 'string')) {
105
+ return { status: 'invalid', reason: 'custom tier requires tools array' };
106
+ }
107
+ return {
108
+ status: 'valid',
109
+ permissions: {
110
+ tier,
111
+ tools: obj.tools,
112
+ ...validatedNote(obj, filePath),
113
+ },
114
+ };
115
+ }
116
+ return {
117
+ status: 'valid',
118
+ permissions: {
119
+ tier,
120
+ ...validatedNote(obj, filePath),
121
+ },
122
+ };
123
+ }
124
+ /**
125
+ * Resolve the effective tools array. Workspace permissions take precedence
126
+ * over the env-var-based tools list.
127
+ */
128
+ export function resolveTools(permissions, envTools) {
129
+ if (!permissions)
130
+ return envTools;
131
+ if (permissions.tier === 'custom')
132
+ return permissions.tools ?? envTools;
133
+ return TIER_TOOLS[permissions.tier];
134
+ }
@@ -0,0 +1,181 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { loadWorkspacePermissions, probeWorkspacePermissions, resolveTools, MAX_NOTE_LENGTH } from './workspace-permissions.js';
6
+ describe('loadWorkspacePermissions', () => {
7
+ const dirs = [];
8
+ afterEach(async () => {
9
+ for (const d of dirs) {
10
+ await fs.rm(d, { recursive: true, force: true });
11
+ }
12
+ dirs.length = 0;
13
+ });
14
+ function tmpDir() {
15
+ const p = fs.mkdtemp(path.join(os.tmpdir(), 'ws-perm-'));
16
+ p.then((d) => dirs.push(d));
17
+ return p;
18
+ }
19
+ it('returns null when file is missing', async () => {
20
+ const dir = await tmpDir();
21
+ expect(await loadWorkspacePermissions(dir)).toBeNull();
22
+ });
23
+ it('parses valid standard tier', async () => {
24
+ const dir = await tmpDir();
25
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '{"tier":"standard"}');
26
+ const result = await loadWorkspacePermissions(dir);
27
+ expect(result).toEqual({ tier: 'standard' });
28
+ });
29
+ it('parses valid custom tier with tools', async () => {
30
+ const dir = await tmpDir();
31
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '{"tier":"custom","tools":["Read","Edit"]}');
32
+ const result = await loadWorkspacePermissions(dir);
33
+ expect(result).toEqual({ tier: 'custom', tools: ['Read', 'Edit'] });
34
+ });
35
+ it('preserves optional note field', async () => {
36
+ const dir = await tmpDir();
37
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '{"tier":"readonly","note":"No file changes."}');
38
+ const result = await loadWorkspacePermissions(dir);
39
+ expect(result).toEqual({ tier: 'readonly', note: 'No file changes.' });
40
+ });
41
+ it('returns null and warns on invalid JSON', async () => {
42
+ const dir = await tmpDir();
43
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '{{bad');
44
+ const log = { warn: vi.fn() };
45
+ expect(await loadWorkspacePermissions(dir, log)).toBeNull();
46
+ expect(log.warn).toHaveBeenCalledOnce();
47
+ });
48
+ it('returns null and warns on invalid tier', async () => {
49
+ const dir = await tmpDir();
50
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '{"tier":"godmode"}');
51
+ const log = { warn: vi.fn() };
52
+ expect(await loadWorkspacePermissions(dir, log)).toBeNull();
53
+ expect(log.warn).toHaveBeenCalledOnce();
54
+ });
55
+ it('returns null when custom tier lacks tools array', async () => {
56
+ const dir = await tmpDir();
57
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '{"tier":"custom"}');
58
+ const log = { warn: vi.fn() };
59
+ expect(await loadWorkspacePermissions(dir, log)).toBeNull();
60
+ expect(log.warn).toHaveBeenCalledOnce();
61
+ });
62
+ it('custom tier with empty tools array parses and warns', async () => {
63
+ const dir = await tmpDir();
64
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '{"tier":"custom","tools":[]}');
65
+ const log = { warn: vi.fn() };
66
+ const result = await loadWorkspacePermissions(dir, log);
67
+ expect(result).toEqual({ tier: 'custom', tools: [] });
68
+ expect(log.warn).toHaveBeenCalledOnce();
69
+ });
70
+ it('custom tier with unknown tool names parses and warns', async () => {
71
+ const dir = await tmpDir();
72
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '{"tier":"custom","tools":["Read","NotARealTool"]}');
73
+ const log = { warn: vi.fn() };
74
+ const result = await loadWorkspacePermissions(dir, log);
75
+ expect(result).toEqual({ tier: 'custom', tools: ['Read', 'NotARealTool'] });
76
+ expect(log.warn).toHaveBeenCalledOnce();
77
+ });
78
+ it('omits note exceeding MAX_NOTE_LENGTH and warns', async () => {
79
+ const dir = await tmpDir();
80
+ const longNote = 'x'.repeat(MAX_NOTE_LENGTH + 1);
81
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), JSON.stringify({ tier: 'readonly', note: longNote }));
82
+ const log = { warn: vi.fn() };
83
+ const result = await loadWorkspacePermissions(dir, log);
84
+ expect(result).toEqual({ tier: 'readonly' });
85
+ expect(log.warn).toHaveBeenCalledOnce();
86
+ });
87
+ it('preserves note at exactly MAX_NOTE_LENGTH', async () => {
88
+ const dir = await tmpDir();
89
+ const note = 'x'.repeat(MAX_NOTE_LENGTH);
90
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), JSON.stringify({ tier: 'standard', note }));
91
+ const result = await loadWorkspacePermissions(dir);
92
+ expect(result).toEqual({ tier: 'standard', note });
93
+ });
94
+ it('accepts note containing newlines', async () => {
95
+ const dir = await tmpDir();
96
+ const note = 'line1\nline2\nline3';
97
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), JSON.stringify({ tier: 'readonly', note }));
98
+ const result = await loadWorkspacePermissions(dir);
99
+ expect(result).toEqual({ tier: 'readonly', note });
100
+ });
101
+ });
102
+ describe('probeWorkspacePermissions', () => {
103
+ const dirs = [];
104
+ afterEach(async () => {
105
+ for (const d of dirs) {
106
+ await fs.rm(d, { recursive: true, force: true });
107
+ }
108
+ dirs.length = 0;
109
+ });
110
+ function tmpDir() {
111
+ const p = fs.mkdtemp(path.join(os.tmpdir(), 'ws-probe-'));
112
+ p.then((d) => dirs.push(d));
113
+ return p;
114
+ }
115
+ it('returns missing when file does not exist', async () => {
116
+ const dir = await tmpDir();
117
+ const result = await probeWorkspacePermissions(dir);
118
+ expect(result).toEqual({ status: 'missing' });
119
+ });
120
+ it('returns invalid with reason for bad JSON', async () => {
121
+ const dir = await tmpDir();
122
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '{{bad');
123
+ const result = await probeWorkspacePermissions(dir);
124
+ expect(result).toEqual({ status: 'invalid', reason: 'invalid JSON' });
125
+ });
126
+ it('returns invalid with reason for non-object', async () => {
127
+ const dir = await tmpDir();
128
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '"just a string"');
129
+ const result = await probeWorkspacePermissions(dir);
130
+ expect(result).toEqual({ status: 'invalid', reason: 'expected object' });
131
+ });
132
+ it('returns invalid with reason for bad tier', async () => {
133
+ const dir = await tmpDir();
134
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '{"tier":"godmode"}');
135
+ const result = await probeWorkspacePermissions(dir);
136
+ expect(result).toEqual({ status: 'invalid', reason: 'invalid tier: "godmode"' });
137
+ });
138
+ it('returns invalid when custom tier lacks tools array', async () => {
139
+ const dir = await tmpDir();
140
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '{"tier":"custom"}');
141
+ const result = await probeWorkspacePermissions(dir);
142
+ expect(result).toEqual({ status: 'invalid', reason: 'custom tier requires tools array' });
143
+ });
144
+ it('returns valid with permissions for standard tier', async () => {
145
+ const dir = await tmpDir();
146
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '{"tier":"standard"}');
147
+ const result = await probeWorkspacePermissions(dir);
148
+ expect(result).toEqual({ status: 'valid', permissions: { tier: 'standard' } });
149
+ });
150
+ it('returns valid with permissions for custom tier', async () => {
151
+ const dir = await tmpDir();
152
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '{"tier":"custom","tools":["Read","Edit"]}');
153
+ const result = await probeWorkspacePermissions(dir);
154
+ expect(result).toEqual({ status: 'valid', permissions: { tier: 'custom', tools: ['Read', 'Edit'] } });
155
+ });
156
+ it('returns valid with note when present', async () => {
157
+ const dir = await tmpDir();
158
+ await fs.writeFile(path.join(dir, 'PERMISSIONS.json'), '{"tier":"readonly","note":"No writes."}');
159
+ const result = await probeWorkspacePermissions(dir);
160
+ expect(result).toEqual({ status: 'valid', permissions: { tier: 'readonly', note: 'No writes.' } });
161
+ });
162
+ });
163
+ describe('resolveTools', () => {
164
+ const envTools = ['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'WebSearch', 'WebFetch'];
165
+ it('returns env tools when permissions is null', () => {
166
+ expect(resolveTools(null, envTools)).toBe(envTools);
167
+ });
168
+ it('returns correct tools for readonly tier', () => {
169
+ expect(resolveTools({ tier: 'readonly' }, envTools)).toEqual(['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch']);
170
+ });
171
+ it('returns correct tools for standard tier', () => {
172
+ expect(resolveTools({ tier: 'standard' }, envTools)).toEqual(['Read', 'Edit', 'Glob', 'Grep', 'WebSearch', 'WebFetch']);
173
+ });
174
+ it('returns correct tools for full tier', () => {
175
+ expect(resolveTools({ tier: 'full' }, envTools)).toEqual(['Bash', 'Read', 'Write', 'Edit', 'Glob', 'Grep', 'WebSearch', 'WebFetch']);
176
+ });
177
+ it('uses custom tools array for custom tier', () => {
178
+ const custom = ['Read', 'WebSearch'];
179
+ expect(resolveTools({ tier: 'custom', tools: custom }, envTools)).toEqual(custom);
180
+ });
181
+ });
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "discoclaw",
3
+ "version": "0.1.0",
4
+ "description": "Minimal Discord bridge routing messages to AI runtimes",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/DiscoClaw/discoclaw.git"
9
+ },
10
+ "bin": {
11
+ "discoclaw": "./dist/cli/index.js"
12
+ },
13
+ "files": [
14
+ "dist/",
15
+ "templates/",
16
+ "systemd/",
17
+ ".context/",
18
+ ".env.example",
19
+ ".env.example.full",
20
+ "scripts/cron/cron-tag-map.json",
21
+ "scripts/tasks/tag-map.json"
22
+ ],
23
+ "type": "module",
24
+ "packageManager": "pnpm@10.28.2",
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "scripts": {
29
+ "prepare": "simple-git-hooks",
30
+ "dev": "tsx src/index.ts",
31
+ "build": "tsc -p tsconfig.json",
32
+ "start": "node dist/index.js",
33
+ "discord:invite-url": "tsx scripts/discord-invite-url.ts",
34
+ "discord:smoke-test": "node scripts/discord-smoke-test.mjs",
35
+ "discord:join-threads": "node scripts/discord-join-threads.mjs",
36
+ "claude:install-skills": "tsx scripts/install-claude-skills.ts",
37
+ "sync:discord-context": "tsx scripts/sync-discord-context.ts",
38
+ "guard:legacy": "tsx scripts/legacy-token-guard.ts",
39
+ "preflight": "tsx scripts/doctor.ts",
40
+ "preflight:online": "tsx scripts/doctor.ts --check-connection",
41
+ "doctor": "tsx scripts/doctor.ts",
42
+ "doctor:online": "tsx scripts/doctor.ts --check-connection",
43
+ "setup": "tsx scripts/setup.ts",
44
+ "test": "vitest run --passWithNoTests",
45
+ "prepublishOnly": "pnpm build && pnpm test",
46
+ "review:sections": "tsx scripts/review-sections.ts"
47
+ },
48
+ "dependencies": {
49
+ "croner": "^10.0.1",
50
+ "discord.js": "^14.19.3",
51
+ "dotenv": "^16.4.5",
52
+ "execa": "^9.5.2",
53
+ "pino": "^9.7.0"
54
+ },
55
+ "simple-git-hooks": {
56
+ "pre-push": "pnpm build && pnpm test"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public",
60
+ "registry": "https://registry.npmjs.org/"
61
+ },
62
+ "pnpm": {
63
+ "onlyBuiltDependencies": [
64
+ "simple-git-hooks"
65
+ ]
66
+ },
67
+ "devDependencies": {
68
+ "@types/node": "^22.13.1",
69
+ "simple-git-hooks": "^2.13.1",
70
+ "tsx": "^4.19.2",
71
+ "typescript": "^5.7.3",
72
+ "vitest": "^2.1.9"
73
+ }
74
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "report": "",
3
+ "maintenance": "",
4
+ "monitor": "",
5
+ "hourly": "",
6
+ "daily": "",
7
+ "weekly": "",
8
+ "monthly": ""
9
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "feature": "1469352147605786634",
3
+ "bug": "1469352147865829456",
4
+ "chore": "1473884551016349910",
5
+ "docs": "1473884551842496634",
6
+ "blocked": "1469352147865829462",
7
+ "open": "1472818699126706393",
8
+ "in_progress": "1472818699126706394",
9
+ "closed": "1472818699126706395"
10
+ }
@@ -0,0 +1,19 @@
1
+ [Unit]
2
+ Description=Discoclaw
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ WorkingDirectory=%h/code/discoclaw
9
+ # Keep secrets local; this file should exist on the host (not committed).
10
+ EnvironmentFile=%h/code/discoclaw/.env
11
+ # Build first: `pnpm build` (this service runs `dist/`).
12
+ ExecStart=/usr/bin/node %h/code/discoclaw/dist/index.js
13
+ Restart=on-failure
14
+ RestartSec=15
15
+ StartLimitIntervalSec=600
16
+ StartLimitBurst=3
17
+
18
+ [Install]
19
+ WantedBy=default.target
@@ -0,0 +1,171 @@
1
+ ---
2
+ spec_version: "1.0"
3
+ plan_id: "replace-with-kebab-id"
4
+ title: "Replace with clear integration title"
5
+ author: "Your name or handle"
6
+ source: "manual"
7
+ license: "MIT"
8
+ created_at: "2026-02-11T00:00:00Z"
9
+ integration_type: "runtime"
10
+ discoclaw_min_version: "0.1.0"
11
+ risk_level: "low"
12
+ ---
13
+
14
+ <!--
15
+ Author instructions (remove before sharing):
16
+ - Keep required headings exactly as written.
17
+ - Use YAML frontmatter for metadata in all plans.
18
+ - For low-risk plans, JSON contract blocks are recommended but optional.
19
+ - For medium/high-risk plans, JSON contract blocks are required.
20
+ -->
21
+
22
+ # DiscoClaw Recipe
23
+
24
+ ## Metadata
25
+
26
+ Canonical metadata lives in YAML frontmatter.
27
+
28
+ Optional notes:
29
+
30
+ - Distribution notes:
31
+ - Ownership notes:
32
+
33
+ ## Use Case
34
+
35
+ - Problem:
36
+ - Who benefits:
37
+ - What this unlocks for another DiscoClaw user:
38
+
39
+ ## Scope
40
+
41
+ In scope:
42
+
43
+ - Item 1
44
+ - Item 2
45
+
46
+ Out of scope:
47
+
48
+ - Item A
49
+ - Item B
50
+
51
+ ## Integration Contract
52
+
53
+ `implementation_contract` JSON block (required for medium/high risk; recommended for low risk):
54
+
55
+ ```json
56
+ {
57
+ "files_add": [
58
+ "path/to/new-file"
59
+ ],
60
+ "files_modify": [
61
+ "path/to/existing-file"
62
+ ],
63
+ "env_changes": [
64
+ {
65
+ "name": "ENV_VAR_NAME",
66
+ "required": false,
67
+ "default": "",
68
+ "description": "What this controls"
69
+ }
70
+ ],
71
+ "runtime_behavior_changes": [
72
+ "Describe visible behavior changes"
73
+ ],
74
+ "out_of_scope": [
75
+ "Non-goal"
76
+ ]
77
+ }
78
+ ```
79
+
80
+ If omitting JSON for low-risk plans, include equivalent prose for files, env/config, behavior, and out-of-scope.
81
+
82
+ Local repo mapping:
83
+
84
+ - Primary entrypoints:
85
+ - Files that may differ by user setup:
86
+ - Mapping notes for alternate layouts:
87
+
88
+ Compatibility notes:
89
+
90
+ - Minimum DiscoClaw version:
91
+ - Known incompatibilities:
92
+ - Backward-compatible fallback behavior:
93
+
94
+ ## Implementation Steps
95
+
96
+ 1. Step one with concrete file-level action.
97
+ 2. Step two with concrete file-level action.
98
+ 3. Step three with concrete file-level action.
99
+
100
+ ## Acceptance Tests
101
+
102
+ `acceptance_contract` JSON block (required for medium/high risk; recommended for low risk):
103
+
104
+ ```json
105
+ {
106
+ "scenarios": [
107
+ {
108
+ "name": "Happy path",
109
+ "type": "integration",
110
+ "steps": [
111
+ "Run action"
112
+ ],
113
+ "expected": [
114
+ "Expected outcome"
115
+ ]
116
+ }
117
+ ],
118
+ "required_checks": [
119
+ "pnpm build",
120
+ "pnpm test"
121
+ ]
122
+ }
123
+ ```
124
+
125
+ If omitting JSON for low-risk plans, include equivalent prose for scenarios and expected outcomes.
126
+
127
+ Manual test notes:
128
+
129
+ - Test account/channel assumptions:
130
+ - Required fixtures or mock data:
131
+ - Observability/log lines to verify:
132
+
133
+ ## Risk, Permissions, Rollback
134
+
135
+ Risk rationale:
136
+
137
+ - Why this is low/medium/high risk.
138
+
139
+ Required permissions/capabilities:
140
+
141
+ - Discord permissions:
142
+ - Runtime tools/capabilities:
143
+ - Env vars or secrets:
144
+
145
+ Rollback plan:
146
+
147
+ 1. Revert files:
148
+ 2. Revert config/env:
149
+ 3. Restart/redeploy steps:
150
+ 4. Verification steps after rollback:
151
+
152
+ ## Handoff Prompt (Consumer Agent)
153
+
154
+ Use this prompt when another DiscoClaw user asks their agent to implement this plan:
155
+
156
+ ```text
157
+ Read this .discoclaw-recipe.md file and produce a decision-complete implementation checklist mapped to local repo files. Validate required headings, YAML frontmatter metadata, and risk-gated JSON contract requirements first. Do not start coding until explicitly asked.
158
+ ```
159
+
160
+ ## Changelog
161
+
162
+ - 2026-02-11: Initial version.
163
+
164
+ Human approval checklist:
165
+
166
+ - [ ] Required headings are present.
167
+ - [ ] YAML frontmatter metadata is complete.
168
+ - [ ] Metadata includes author/source/license.
169
+ - [ ] Risk, permissions, and rollback are explicit.
170
+ - [ ] JSON contract blocks satisfy risk-level requirements.
171
+ - [ ] Handoff prompt is included and clear.