@blxzer/pactile 0.5.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 (903) hide show
  1. package/CHANGELOG.md +602 -0
  2. package/LICENSE +235 -0
  3. package/README.md +115 -0
  4. package/README.zh-CN.md +115 -0
  5. package/bin/compat-warning.js +11 -0
  6. package/bin/cstl.js +6 -0
  7. package/bin/pactile.js +3 -0
  8. package/bin/smart-search.js +38 -0
  9. package/dist/cli/index.d.ts +3 -0
  10. package/dist/cli/index.d.ts.map +1 -0
  11. package/dist/cli/index.js +362 -0
  12. package/dist/cli/index.js.map +1 -0
  13. package/dist/commands/capability-smoke.d.ts +24 -0
  14. package/dist/commands/capability-smoke.d.ts.map +1 -0
  15. package/dist/commands/capability-smoke.js +83 -0
  16. package/dist/commands/capability-smoke.js.map +1 -0
  17. package/dist/commands/channel/adapters/claude.d.ts +29 -0
  18. package/dist/commands/channel/adapters/claude.d.ts.map +1 -0
  19. package/dist/commands/channel/adapters/claude.js +203 -0
  20. package/dist/commands/channel/adapters/claude.js.map +1 -0
  21. package/dist/commands/channel/adapters/codex.d.ts +85 -0
  22. package/dist/commands/channel/adapters/codex.d.ts.map +1 -0
  23. package/dist/commands/channel/adapters/codex.js +505 -0
  24. package/dist/commands/channel/adapters/codex.js.map +1 -0
  25. package/dist/commands/channel/adapters/index.d.ts +84 -0
  26. package/dist/commands/channel/adapters/index.d.ts.map +1 -0
  27. package/dist/commands/channel/adapters/index.js +115 -0
  28. package/dist/commands/channel/adapters/index.js.map +1 -0
  29. package/dist/commands/channel/adapters/types.d.ts +33 -0
  30. package/dist/commands/channel/adapters/types.d.ts.map +1 -0
  31. package/dist/commands/channel/adapters/types.js +2 -0
  32. package/dist/commands/channel/adapters/types.js.map +1 -0
  33. package/dist/commands/channel/agent-loader.d.ts +32 -0
  34. package/dist/commands/channel/agent-loader.d.ts.map +1 -0
  35. package/dist/commands/channel/agent-loader.js +154 -0
  36. package/dist/commands/channel/agent-loader.js.map +1 -0
  37. package/dist/commands/channel/context-loader.d.ts +26 -0
  38. package/dist/commands/channel/context-loader.d.ts.map +1 -0
  39. package/dist/commands/channel/context-loader.js +290 -0
  40. package/dist/commands/channel/context-loader.js.map +1 -0
  41. package/dist/commands/channel/context.d.ts +16 -0
  42. package/dist/commands/channel/context.d.ts.map +1 -0
  43. package/dist/commands/channel/context.js +83 -0
  44. package/dist/commands/channel/context.js.map +1 -0
  45. package/dist/commands/channel/create.d.ts +27 -0
  46. package/dist/commands/channel/create.d.ts.map +1 -0
  47. package/dist/commands/channel/create.js +39 -0
  48. package/dist/commands/channel/create.js.map +1 -0
  49. package/dist/commands/channel/dev-parse-trace.d.ts +14 -0
  50. package/dist/commands/channel/dev-parse-trace.d.ts.map +1 -0
  51. package/dist/commands/channel/dev-parse-trace.js +70 -0
  52. package/dist/commands/channel/dev-parse-trace.js.map +1 -0
  53. package/dist/commands/channel/guard.d.ts +150 -0
  54. package/dist/commands/channel/guard.d.ts.map +1 -0
  55. package/dist/commands/channel/guard.js +475 -0
  56. package/dist/commands/channel/guard.js.map +1 -0
  57. package/dist/commands/channel/index.d.ts +3 -0
  58. package/dist/commands/channel/index.d.ts.map +1 -0
  59. package/dist/commands/channel/index.js +531 -0
  60. package/dist/commands/channel/index.js.map +1 -0
  61. package/dist/commands/channel/interrupt.d.ts +10 -0
  62. package/dist/commands/channel/interrupt.d.ts.map +1 -0
  63. package/dist/commands/channel/interrupt.js +22 -0
  64. package/dist/commands/channel/interrupt.js.map +1 -0
  65. package/dist/commands/channel/kill.d.ts +7 -0
  66. package/dist/commands/channel/kill.d.ts.map +1 -0
  67. package/dist/commands/channel/kill.js +121 -0
  68. package/dist/commands/channel/kill.js.map +1 -0
  69. package/dist/commands/channel/list.d.ts +17 -0
  70. package/dist/commands/channel/list.d.ts.map +1 -0
  71. package/dist/commands/channel/list.js +233 -0
  72. package/dist/commands/channel/list.js.map +1 -0
  73. package/dist/commands/channel/messages.d.ts +15 -0
  74. package/dist/commands/channel/messages.d.ts.map +1 -0
  75. package/dist/commands/channel/messages.js +245 -0
  76. package/dist/commands/channel/messages.js.map +1 -0
  77. package/dist/commands/channel/rm.d.ts +27 -0
  78. package/dist/commands/channel/rm.d.ts.map +1 -0
  79. package/dist/commands/channel/rm.js +216 -0
  80. package/dist/commands/channel/rm.js.map +1 -0
  81. package/dist/commands/channel/run.d.ts +30 -0
  82. package/dist/commands/channel/run.d.ts.map +1 -0
  83. package/dist/commands/channel/run.js +130 -0
  84. package/dist/commands/channel/run.js.map +1 -0
  85. package/dist/commands/channel/send.d.ts +11 -0
  86. package/dist/commands/channel/send.d.ts.map +1 -0
  87. package/dist/commands/channel/send.js +24 -0
  88. package/dist/commands/channel/send.js.map +1 -0
  89. package/dist/commands/channel/spawn.d.ts +40 -0
  90. package/dist/commands/channel/spawn.d.ts.map +1 -0
  91. package/dist/commands/channel/spawn.js +242 -0
  92. package/dist/commands/channel/spawn.js.map +1 -0
  93. package/dist/commands/channel/store/events.d.ts +39 -0
  94. package/dist/commands/channel/store/events.d.ts.map +1 -0
  95. package/dist/commands/channel/store/events.js +87 -0
  96. package/dist/commands/channel/store/events.js.map +1 -0
  97. package/dist/commands/channel/store/filter.d.ts +3 -0
  98. package/dist/commands/channel/store/filter.d.ts.map +1 -0
  99. package/dist/commands/channel/store/filter.js +2 -0
  100. package/dist/commands/channel/store/filter.js.map +1 -0
  101. package/dist/commands/channel/store/lock.d.ts +23 -0
  102. package/dist/commands/channel/store/lock.d.ts.map +1 -0
  103. package/dist/commands/channel/store/lock.js +99 -0
  104. package/dist/commands/channel/store/lock.js.map +1 -0
  105. package/dist/commands/channel/store/paths.d.ts +63 -0
  106. package/dist/commands/channel/store/paths.d.ts.map +1 -0
  107. package/dist/commands/channel/store/paths.js +247 -0
  108. package/dist/commands/channel/store/paths.js.map +1 -0
  109. package/dist/commands/channel/store/schema.d.ts +27 -0
  110. package/dist/commands/channel/store/schema.d.ts.map +1 -0
  111. package/dist/commands/channel/store/schema.js +34 -0
  112. package/dist/commands/channel/store/schema.js.map +1 -0
  113. package/dist/commands/channel/store/thread-state.d.ts +5 -0
  114. package/dist/commands/channel/store/thread-state.d.ts.map +1 -0
  115. package/dist/commands/channel/store/thread-state.js +16 -0
  116. package/dist/commands/channel/store/thread-state.js.map +1 -0
  117. package/dist/commands/channel/store/watch.d.ts +19 -0
  118. package/dist/commands/channel/store/watch.d.ts.map +1 -0
  119. package/dist/commands/channel/store/watch.js +146 -0
  120. package/dist/commands/channel/store/watch.js.map +1 -0
  121. package/dist/commands/channel/supervisor/idle.d.ts +46 -0
  122. package/dist/commands/channel/supervisor/idle.d.ts.map +1 -0
  123. package/dist/commands/channel/supervisor/idle.js +72 -0
  124. package/dist/commands/channel/supervisor/idle.js.map +1 -0
  125. package/dist/commands/channel/supervisor/inbox.d.ts +30 -0
  126. package/dist/commands/channel/supervisor/inbox.d.ts.map +1 -0
  127. package/dist/commands/channel/supervisor/inbox.js +160 -0
  128. package/dist/commands/channel/supervisor/inbox.js.map +1 -0
  129. package/dist/commands/channel/supervisor/shutdown.d.ts +68 -0
  130. package/dist/commands/channel/supervisor/shutdown.d.ts.map +1 -0
  131. package/dist/commands/channel/supervisor/shutdown.js +146 -0
  132. package/dist/commands/channel/supervisor/shutdown.js.map +1 -0
  133. package/dist/commands/channel/supervisor/stdout.d.ts +51 -0
  134. package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -0
  135. package/dist/commands/channel/supervisor/stdout.js +121 -0
  136. package/dist/commands/channel/supervisor/stdout.js.map +1 -0
  137. package/dist/commands/channel/supervisor/turns.d.ts +31 -0
  138. package/dist/commands/channel/supervisor/turns.d.ts.map +1 -0
  139. package/dist/commands/channel/supervisor/turns.js +45 -0
  140. package/dist/commands/channel/supervisor/turns.js.map +1 -0
  141. package/dist/commands/channel/supervisor/warning.d.ts +48 -0
  142. package/dist/commands/channel/supervisor/warning.d.ts.map +1 -0
  143. package/dist/commands/channel/supervisor/warning.js +77 -0
  144. package/dist/commands/channel/supervisor/warning.js.map +1 -0
  145. package/dist/commands/channel/supervisor.d.ts +59 -0
  146. package/dist/commands/channel/supervisor.d.ts.map +1 -0
  147. package/dist/commands/channel/supervisor.js +345 -0
  148. package/dist/commands/channel/supervisor.js.map +1 -0
  149. package/dist/commands/channel/text-body.d.ts +13 -0
  150. package/dist/commands/channel/text-body.d.ts.map +1 -0
  151. package/dist/commands/channel/text-body.js +47 -0
  152. package/dist/commands/channel/text-body.js.map +1 -0
  153. package/dist/commands/channel/threads.d.ts +39 -0
  154. package/dist/commands/channel/threads.d.ts.map +1 -0
  155. package/dist/commands/channel/threads.js +106 -0
  156. package/dist/commands/channel/threads.js.map +1 -0
  157. package/dist/commands/channel/title.d.ts +12 -0
  158. package/dist/commands/channel/title.d.ts.map +1 -0
  159. package/dist/commands/channel/title.js +24 -0
  160. package/dist/commands/channel/title.js.map +1 -0
  161. package/dist/commands/channel/wait.d.ts +17 -0
  162. package/dist/commands/channel/wait.d.ts.map +1 -0
  163. package/dist/commands/channel/wait.js +75 -0
  164. package/dist/commands/channel/wait.js.map +1 -0
  165. package/dist/commands/detach.d.ts +7 -0
  166. package/dist/commands/detach.d.ts.map +1 -0
  167. package/dist/commands/detach.js +31 -0
  168. package/dist/commands/detach.js.map +1 -0
  169. package/dist/commands/init.d.ts +51 -0
  170. package/dist/commands/init.d.ts.map +1 -0
  171. package/dist/commands/init.js +1660 -0
  172. package/dist/commands/init.js.map +1 -0
  173. package/dist/commands/migrate.d.ts +10 -0
  174. package/dist/commands/migrate.d.ts.map +1 -0
  175. package/dist/commands/migrate.js +38 -0
  176. package/dist/commands/migrate.js.map +1 -0
  177. package/dist/commands/purge.d.ts +9 -0
  178. package/dist/commands/purge.d.ts.map +1 -0
  179. package/dist/commands/purge.js +29 -0
  180. package/dist/commands/purge.js.map +1 -0
  181. package/dist/commands/rollback.d.ts +7 -0
  182. package/dist/commands/rollback.d.ts.map +1 -0
  183. package/dist/commands/rollback.js +27 -0
  184. package/dist/commands/rollback.js.map +1 -0
  185. package/dist/commands/rollout.d.ts +34 -0
  186. package/dist/commands/rollout.d.ts.map +1 -0
  187. package/dist/commands/rollout.js +102 -0
  188. package/dist/commands/rollout.js.map +1 -0
  189. package/dist/commands/uninstall.d.ts +23 -0
  190. package/dist/commands/uninstall.d.ts.map +1 -0
  191. package/dist/commands/uninstall.js +68 -0
  192. package/dist/commands/uninstall.js.map +1 -0
  193. package/dist/commands/update.d.ts +139 -0
  194. package/dist/commands/update.d.ts.map +1 -0
  195. package/dist/commands/update.js +2459 -0
  196. package/dist/commands/update.js.map +1 -0
  197. package/dist/commands/upgrade.d.ts +28 -0
  198. package/dist/commands/upgrade.d.ts.map +1 -0
  199. package/dist/commands/upgrade.js +84 -0
  200. package/dist/commands/upgrade.js.map +1 -0
  201. package/dist/commands/validate-rules.d.ts +6 -0
  202. package/dist/commands/validate-rules.d.ts.map +1 -0
  203. package/dist/commands/validate-rules.js +33 -0
  204. package/dist/commands/validate-rules.js.map +1 -0
  205. package/dist/commands/workflow.d.ts +35 -0
  206. package/dist/commands/workflow.d.ts.map +1 -0
  207. package/dist/commands/workflow.js +220 -0
  208. package/dist/commands/workflow.js.map +1 -0
  209. package/dist/configurators/cursor.d.ts +21 -0
  210. package/dist/configurators/cursor.d.ts.map +1 -0
  211. package/dist/configurators/cursor.js +51 -0
  212. package/dist/configurators/cursor.js.map +1 -0
  213. package/dist/configurators/index.d.ts +68 -0
  214. package/dist/configurators/index.d.ts.map +1 -0
  215. package/dist/configurators/index.js +170 -0
  216. package/dist/configurators/index.js.map +1 -0
  217. package/dist/configurators/shared.d.ts +200 -0
  218. package/dist/configurators/shared.d.ts.map +1 -0
  219. package/dist/configurators/shared.js +602 -0
  220. package/dist/configurators/shared.js.map +1 -0
  221. package/dist/configurators/workflow.d.ts +45 -0
  222. package/dist/configurators/workflow.d.ts.map +1 -0
  223. package/dist/configurators/workflow.js +256 -0
  224. package/dist/configurators/workflow.js.map +1 -0
  225. package/dist/constants/paths.d.ts +93 -0
  226. package/dist/constants/paths.d.ts.map +1 -0
  227. package/dist/constants/paths.js +106 -0
  228. package/dist/constants/paths.js.map +1 -0
  229. package/dist/constants/version.d.ts +9 -0
  230. package/dist/constants/version.d.ts.map +1 -0
  231. package/dist/constants/version.js +15 -0
  232. package/dist/constants/version.js.map +1 -0
  233. package/dist/index.d.ts +10 -0
  234. package/dist/index.d.ts.map +1 -0
  235. package/dist/index.js +10 -0
  236. package/dist/index.js.map +1 -0
  237. package/dist/migrations/index.d.ts +62 -0
  238. package/dist/migrations/index.d.ts.map +1 -0
  239. package/dist/migrations/index.js +187 -0
  240. package/dist/migrations/index.js.map +1 -0
  241. package/dist/migrations/manifests/0.1.0.json +9 -0
  242. package/dist/migrations/manifests/0.1.1.json +9 -0
  243. package/dist/migrations/manifests/0.1.2.json +9 -0
  244. package/dist/migrations/manifests/0.1.3.json +9 -0
  245. package/dist/migrations/manifests/0.1.4.json +9 -0
  246. package/dist/migrations/manifests/0.2.1.json +9 -0
  247. package/dist/migrations/manifests/0.2.10.json +374 -0
  248. package/dist/migrations/manifests/0.2.2.json +9 -0
  249. package/dist/migrations/manifests/0.2.3.json +9 -0
  250. package/dist/migrations/manifests/0.2.4.json +9 -0
  251. package/dist/migrations/manifests/0.2.5.json +9 -0
  252. package/dist/migrations/manifests/0.2.6.json +9 -0
  253. package/dist/migrations/manifests/0.2.7.json +9 -0
  254. package/dist/migrations/manifests/0.2.8.json +9 -0
  255. package/dist/migrations/manifests/0.2.9.json +9 -0
  256. package/dist/migrations/manifests/0.3.0.json +89 -0
  257. package/dist/migrations/manifests/0.3.1.json +32 -0
  258. package/dist/migrations/manifests/0.3.2.json +16 -0
  259. package/dist/migrations/manifests/0.3.3.json +9 -0
  260. package/dist/migrations/manifests/0.3.4.json +9 -0
  261. package/dist/migrations/manifests/0.3.5.json +9 -0
  262. package/dist/migrations/manifests/0.3.6.json +9 -0
  263. package/dist/migrations/manifests/0.4.0.json +9 -0
  264. package/dist/migrations/manifests/0.4.1.json +9 -0
  265. package/dist/migrations/manifests/0.4.2.json +9 -0
  266. package/dist/migrations/manifests/0.4.3.json +9 -0
  267. package/dist/migrations/manifests/0.5.0-beta.0.json +9 -0
  268. package/dist/migrations/manifests/0.5.0-beta.1.json +9 -0
  269. package/dist/migrations/manifests/0.5.0-beta.2.json +9 -0
  270. package/dist/migrations/manifests/0.5.0-beta.3.json +9 -0
  271. package/dist/migrations/manifests/0.5.0-beta.4.json +9 -0
  272. package/dist/migrations/manifests/0.5.0-beta.5.json +9 -0
  273. package/dist/pactile/adapters/codex/index.d.ts +75 -0
  274. package/dist/pactile/adapters/codex/index.d.ts.map +1 -0
  275. package/dist/pactile/adapters/codex/index.js +269 -0
  276. package/dist/pactile/adapters/codex/index.js.map +1 -0
  277. package/dist/pactile/adapters/cursor/index.d.ts +60 -0
  278. package/dist/pactile/adapters/cursor/index.d.ts.map +1 -0
  279. package/dist/pactile/adapters/cursor/index.js +186 -0
  280. package/dist/pactile/adapters/cursor/index.js.map +1 -0
  281. package/dist/pactile/adapters/index.d.ts +4 -0
  282. package/dist/pactile/adapters/index.d.ts.map +1 -0
  283. package/dist/pactile/adapters/index.js +4 -0
  284. package/dist/pactile/adapters/index.js.map +1 -0
  285. package/dist/pactile/adoption/bindings.d.ts +20 -0
  286. package/dist/pactile/adoption/bindings.d.ts.map +1 -0
  287. package/dist/pactile/adoption/bindings.js +99 -0
  288. package/dist/pactile/adoption/bindings.js.map +1 -0
  289. package/dist/pactile/adoption/discovery.d.ts +8 -0
  290. package/dist/pactile/adoption/discovery.d.ts.map +1 -0
  291. package/dist/pactile/adoption/discovery.js +159 -0
  292. package/dist/pactile/adoption/discovery.js.map +1 -0
  293. package/dist/pactile/adoption/inventory.d.ts +28 -0
  294. package/dist/pactile/adoption/inventory.d.ts.map +1 -0
  295. package/dist/pactile/adoption/inventory.js +261 -0
  296. package/dist/pactile/adoption/inventory.js.map +1 -0
  297. package/dist/pactile/adoption/safety.d.ts +19 -0
  298. package/dist/pactile/adoption/safety.d.ts.map +1 -0
  299. package/dist/pactile/adoption/safety.js +88 -0
  300. package/dist/pactile/adoption/safety.js.map +1 -0
  301. package/dist/pactile/adoption/workflow.d.ts +31 -0
  302. package/dist/pactile/adoption/workflow.d.ts.map +1 -0
  303. package/dist/pactile/adoption/workflow.js +259 -0
  304. package/dist/pactile/adoption/workflow.js.map +1 -0
  305. package/dist/pactile/compat/cli-options.d.ts +9 -0
  306. package/dist/pactile/compat/cli-options.d.ts.map +1 -0
  307. package/dist/pactile/compat/cli-options.js +9 -0
  308. package/dist/pactile/compat/cli-options.js.map +1 -0
  309. package/dist/pactile/compat/init-context.d.ts +16 -0
  310. package/dist/pactile/compat/init-context.d.ts.map +1 -0
  311. package/dist/pactile/compat/init-context.js +87 -0
  312. package/dist/pactile/compat/init-context.js.map +1 -0
  313. package/dist/pactile/compat/legacy-migrations.d.ts +8 -0
  314. package/dist/pactile/compat/legacy-migrations.d.ts.map +1 -0
  315. package/dist/pactile/compat/legacy-migrations.js +17 -0
  316. package/dist/pactile/compat/legacy-migrations.js.map +1 -0
  317. package/dist/pactile/compat/retired-alternate-client.d.ts +42 -0
  318. package/dist/pactile/compat/retired-alternate-client.d.ts.map +1 -0
  319. package/dist/pactile/compat/retired-alternate-client.js +153 -0
  320. package/dist/pactile/compat/retired-alternate-client.js.map +1 -0
  321. package/dist/pactile/compat/update-residue.d.ts +4 -0
  322. package/dist/pactile/compat/update-residue.d.ts.map +1 -0
  323. package/dist/pactile/compat/update-residue.js +55 -0
  324. package/dist/pactile/compat/update-residue.js.map +1 -0
  325. package/dist/pactile/exit/index.d.ts +3 -0
  326. package/dist/pactile/exit/index.d.ts.map +1 -0
  327. package/dist/pactile/exit/index.js +3 -0
  328. package/dist/pactile/exit/index.js.map +1 -0
  329. package/dist/pactile/exit/receipt.d.ts +23 -0
  330. package/dist/pactile/exit/receipt.d.ts.map +1 -0
  331. package/dist/pactile/exit/receipt.js +64 -0
  332. package/dist/pactile/exit/receipt.js.map +1 -0
  333. package/dist/pactile/exit/service.d.ts +107 -0
  334. package/dist/pactile/exit/service.d.ts.map +1 -0
  335. package/dist/pactile/exit/service.js +774 -0
  336. package/dist/pactile/exit/service.js.map +1 -0
  337. package/dist/pactile/index.d.ts +26 -0
  338. package/dist/pactile/index.d.ts.map +1 -0
  339. package/dist/pactile/index.js +22 -0
  340. package/dist/pactile/index.js.map +1 -0
  341. package/dist/pactile/lifecycle/command-runner.d.ts +26 -0
  342. package/dist/pactile/lifecycle/command-runner.d.ts.map +1 -0
  343. package/dist/pactile/lifecycle/command-runner.js +129 -0
  344. package/dist/pactile/lifecycle/command-runner.js.map +1 -0
  345. package/dist/pactile/lifecycle/default-adapters.d.ts +5 -0
  346. package/dist/pactile/lifecycle/default-adapters.d.ts.map +1 -0
  347. package/dist/pactile/lifecycle/default-adapters.js +136 -0
  348. package/dist/pactile/lifecycle/default-adapters.js.map +1 -0
  349. package/dist/pactile/lifecycle/index.d.ts +6 -0
  350. package/dist/pactile/lifecycle/index.d.ts.map +1 -0
  351. package/dist/pactile/lifecycle/index.js +6 -0
  352. package/dist/pactile/lifecycle/index.js.map +1 -0
  353. package/dist/pactile/lifecycle/legacy-import.d.ts +16 -0
  354. package/dist/pactile/lifecycle/legacy-import.d.ts.map +1 -0
  355. package/dist/pactile/lifecycle/legacy-import.js +228 -0
  356. package/dist/pactile/lifecycle/legacy-import.js.map +1 -0
  357. package/dist/pactile/lifecycle/orchestrator.d.ts +109 -0
  358. package/dist/pactile/lifecycle/orchestrator.d.ts.map +1 -0
  359. package/dist/pactile/lifecycle/orchestrator.js +1030 -0
  360. package/dist/pactile/lifecycle/orchestrator.js.map +1 -0
  361. package/dist/pactile/lifecycle/project-files.d.ts +28 -0
  362. package/dist/pactile/lifecycle/project-files.d.ts.map +1 -0
  363. package/dist/pactile/lifecycle/project-files.js +297 -0
  364. package/dist/pactile/lifecycle/project-files.js.map +1 -0
  365. package/dist/pactile/middleware/index.d.ts +3 -0
  366. package/dist/pactile/middleware/index.d.ts.map +1 -0
  367. package/dist/pactile/middleware/index.js +2 -0
  368. package/dist/pactile/middleware/index.js.map +1 -0
  369. package/dist/pactile/middleware/manifest-loader.d.ts +22 -0
  370. package/dist/pactile/middleware/manifest-loader.d.ts.map +1 -0
  371. package/dist/pactile/middleware/manifest-loader.js +228 -0
  372. package/dist/pactile/middleware/manifest-loader.js.map +1 -0
  373. package/dist/pactile/migration/index.d.ts +2 -0
  374. package/dist/pactile/migration/index.d.ts.map +1 -0
  375. package/dist/pactile/migration/index.js +2 -0
  376. package/dist/pactile/migration/index.js.map +1 -0
  377. package/dist/pactile/migration/transaction.d.ts +54 -0
  378. package/dist/pactile/migration/transaction.d.ts.map +1 -0
  379. package/dist/pactile/migration/transaction.js +445 -0
  380. package/dist/pactile/migration/transaction.js.map +1 -0
  381. package/dist/pactile/projection/managed-block.d.ts +32 -0
  382. package/dist/pactile/projection/managed-block.d.ts.map +1 -0
  383. package/dist/pactile/projection/managed-block.js +78 -0
  384. package/dist/pactile/projection/managed-block.js.map +1 -0
  385. package/dist/pactile/projection/planner.d.ts +65 -0
  386. package/dist/pactile/projection/planner.d.ts.map +1 -0
  387. package/dist/pactile/projection/planner.js +349 -0
  388. package/dist/pactile/projection/planner.js.map +1 -0
  389. package/dist/pactile/projection/shared/index.d.ts +38 -0
  390. package/dist/pactile/projection/shared/index.d.ts.map +1 -0
  391. package/dist/pactile/projection/shared/index.js +186 -0
  392. package/dist/pactile/projection/shared/index.js.map +1 -0
  393. package/dist/pactile/projection/store.d.ts +72 -0
  394. package/dist/pactile/projection/store.d.ts.map +1 -0
  395. package/dist/pactile/projection/store.js +669 -0
  396. package/dist/pactile/projection/store.js.map +1 -0
  397. package/dist/pactile/projection/structured-merge.d.ts +11 -0
  398. package/dist/pactile/projection/structured-merge.d.ts.map +1 -0
  399. package/dist/pactile/projection/structured-merge.js +347 -0
  400. package/dist/pactile/projection/structured-merge.js.map +1 -0
  401. package/dist/pactile/providers/index.d.ts +3 -0
  402. package/dist/pactile/providers/index.d.ts.map +1 -0
  403. package/dist/pactile/providers/index.js +3 -0
  404. package/dist/pactile/providers/index.js.map +1 -0
  405. package/dist/pactile/providers/probes.d.ts +55 -0
  406. package/dist/pactile/providers/probes.d.ts.map +1 -0
  407. package/dist/pactile/providers/probes.js +164 -0
  408. package/dist/pactile/providers/probes.js.map +1 -0
  409. package/dist/pactile/registry.d.ts +64 -0
  410. package/dist/pactile/registry.d.ts.map +1 -0
  411. package/dist/pactile/registry.js +196 -0
  412. package/dist/pactile/registry.js.map +1 -0
  413. package/dist/pactile/retrieval/boundary.d.ts +18 -0
  414. package/dist/pactile/retrieval/boundary.d.ts.map +1 -0
  415. package/dist/pactile/retrieval/boundary.js +113 -0
  416. package/dist/pactile/retrieval/boundary.js.map +1 -0
  417. package/dist/pactile/retrieval/evidence.d.ts +9 -0
  418. package/dist/pactile/retrieval/evidence.d.ts.map +1 -0
  419. package/dist/pactile/retrieval/evidence.js +327 -0
  420. package/dist/pactile/retrieval/evidence.js.map +1 -0
  421. package/dist/pactile/retrieval/index.d.ts +4 -0
  422. package/dist/pactile/retrieval/index.d.ts.map +1 -0
  423. package/dist/pactile/retrieval/index.js +4 -0
  424. package/dist/pactile/retrieval/index.js.map +1 -0
  425. package/dist/pactile/retrieval/planner.d.ts +23 -0
  426. package/dist/pactile/retrieval/planner.d.ts.map +1 -0
  427. package/dist/pactile/retrieval/planner.js +702 -0
  428. package/dist/pactile/retrieval/planner.js.map +1 -0
  429. package/dist/pactile/retrieval/types.d.ts +108 -0
  430. package/dist/pactile/retrieval/types.d.ts.map +1 -0
  431. package/dist/pactile/retrieval/types.js +36 -0
  432. package/dist/pactile/retrieval/types.js.map +1 -0
  433. package/dist/pactile/runtime/case-fold.d.ts +2 -0
  434. package/dist/pactile/runtime/case-fold.d.ts.map +1 -0
  435. package/dist/pactile/runtime/case-fold.js +31 -0
  436. package/dist/pactile/runtime/case-fold.js.map +1 -0
  437. package/dist/pactile/runtime/index.d.ts +8 -0
  438. package/dist/pactile/runtime/index.d.ts.map +1 -0
  439. package/dist/pactile/runtime/index.js +5 -0
  440. package/dist/pactile/runtime/index.js.map +1 -0
  441. package/dist/pactile/runtime/json-api.d.ts +15 -0
  442. package/dist/pactile/runtime/json-api.d.ts.map +1 -0
  443. package/dist/pactile/runtime/json-api.js +68 -0
  444. package/dist/pactile/runtime/json-api.js.map +1 -0
  445. package/dist/pactile/runtime/paths.d.ts +59 -0
  446. package/dist/pactile/runtime/paths.d.ts.map +1 -0
  447. package/dist/pactile/runtime/paths.js +201 -0
  448. package/dist/pactile/runtime/paths.js.map +1 -0
  449. package/dist/pactile/runtime/stores.d.ts +54 -0
  450. package/dist/pactile/runtime/stores.d.ts.map +1 -0
  451. package/dist/pactile/runtime/stores.js +375 -0
  452. package/dist/pactile/runtime/stores.js.map +1 -0
  453. package/dist/pactile/runtime/unicode-nfc-data.d.ts +12 -0
  454. package/dist/pactile/runtime/unicode-nfc-data.d.ts.map +1 -0
  455. package/dist/pactile/runtime/unicode-nfc-data.js +8 -0
  456. package/dist/pactile/runtime/unicode-nfc-data.js.map +1 -0
  457. package/dist/pactile/runtime/unicode-nfc.d.ts +3 -0
  458. package/dist/pactile/runtime/unicode-nfc.d.ts.map +1 -0
  459. package/dist/pactile/runtime/unicode-nfc.js +77 -0
  460. package/dist/pactile/runtime/unicode-nfc.js.map +1 -0
  461. package/dist/pactile/tiles/catalog.d.ts +35 -0
  462. package/dist/pactile/tiles/catalog.d.ts.map +1 -0
  463. package/dist/pactile/tiles/catalog.js +235 -0
  464. package/dist/pactile/tiles/catalog.js.map +1 -0
  465. package/dist/pactile/tiles/compiler.d.ts +54 -0
  466. package/dist/pactile/tiles/compiler.d.ts.map +1 -0
  467. package/dist/pactile/tiles/compiler.js +270 -0
  468. package/dist/pactile/tiles/compiler.js.map +1 -0
  469. package/dist/pactile/tiles/content/baseline/index.d.ts +5 -0
  470. package/dist/pactile/tiles/content/baseline/index.d.ts.map +1 -0
  471. package/dist/pactile/tiles/content/baseline/index.js +115 -0
  472. package/dist/pactile/tiles/content/baseline/index.js.map +1 -0
  473. package/dist/pactile/tiles/content/index.d.ts +4 -0
  474. package/dist/pactile/tiles/content/index.d.ts.map +1 -0
  475. package/dist/pactile/tiles/content/index.js +4 -0
  476. package/dist/pactile/tiles/content/index.js.map +1 -0
  477. package/dist/pactile/tiles/content/ondemand/index.d.ts +5 -0
  478. package/dist/pactile/tiles/content/ondemand/index.d.ts.map +1 -0
  479. package/dist/pactile/tiles/content/ondemand/index.js +184 -0
  480. package/dist/pactile/tiles/content/ondemand/index.js.map +1 -0
  481. package/dist/pactile/tiles/content/source.d.ts +29 -0
  482. package/dist/pactile/tiles/content/source.d.ts.map +1 -0
  483. package/dist/pactile/tiles/content/source.js +86 -0
  484. package/dist/pactile/tiles/content/source.js.map +1 -0
  485. package/dist/pactile/tiles/loader.d.ts +31 -0
  486. package/dist/pactile/tiles/loader.d.ts.map +1 -0
  487. package/dist/pactile/tiles/loader.js +139 -0
  488. package/dist/pactile/tiles/loader.js.map +1 -0
  489. package/dist/pactile/tiles/safe-yaml.d.ts +11 -0
  490. package/dist/pactile/tiles/safe-yaml.d.ts.map +1 -0
  491. package/dist/pactile/tiles/safe-yaml.js +212 -0
  492. package/dist/pactile/tiles/safe-yaml.js.map +1 -0
  493. package/dist/templates/common/bundled-skills/pactile-check/SKILL.md +161 -0
  494. package/dist/templates/common/bundled-skills/pactile-check/references/fowler-12-smells.md +28 -0
  495. package/dist/templates/common/bundled-skills/pactile-handoff/SKILL.md +117 -0
  496. package/dist/templates/common/bundled-skills/pactile-meta/SKILL.md +75 -0
  497. package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/add-project-local-conventions.md +83 -0
  498. package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/change-agents.md +47 -0
  499. package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/change-context-loading.md +84 -0
  500. package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/change-hooks.md +57 -0
  501. package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/change-skills-or-commands.md +77 -0
  502. package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/change-spec-structure.md +83 -0
  503. package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/change-task-lifecycle.md +88 -0
  504. package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/change-workflow.md +65 -0
  505. package/dist/templates/common/bundled-skills/pactile-meta/references/customize-local/overview.md +55 -0
  506. package/dist/templates/common/bundled-skills/pactile-meta/references/local-architecture/context-injection.md +68 -0
  507. package/dist/templates/common/bundled-skills/pactile-meta/references/local-architecture/generated-files.md +83 -0
  508. package/dist/templates/common/bundled-skills/pactile-meta/references/local-architecture/overview.md +51 -0
  509. package/dist/templates/common/bundled-skills/pactile-meta/references/local-architecture/spec-system.md +102 -0
  510. package/dist/templates/common/bundled-skills/pactile-meta/references/local-architecture/task-system.md +133 -0
  511. package/dist/templates/common/bundled-skills/pactile-meta/references/local-architecture/workflow.md +75 -0
  512. package/dist/templates/common/bundled-skills/pactile-meta/references/local-architecture/workspace-memory.md +71 -0
  513. package/dist/templates/common/bundled-skills/pactile-meta/references/platform-files/agents.md +69 -0
  514. package/dist/templates/common/bundled-skills/pactile-meta/references/platform-files/hooks-and-settings.md +59 -0
  515. package/dist/templates/common/bundled-skills/pactile-meta/references/platform-files/overview.md +45 -0
  516. package/dist/templates/common/bundled-skills/pactile-meta/references/platform-files/platform-map.md +45 -0
  517. package/dist/templates/common/bundled-skills/pactile-meta/references/platform-files/skills-and-commands.md +108 -0
  518. package/dist/templates/common/bundled-skills/pactile-micro-grill/SKILL.md +68 -0
  519. package/dist/templates/common/bundled-skills/pactile-skill-creator/SKILL.md +47 -0
  520. package/dist/templates/common/bundled-skills/pactile-skill-creator/references/authoring-rules.md +171 -0
  521. package/dist/templates/common/bundled-skills/pactile-skill-creator/references/general-authoring.md +189 -0
  522. package/dist/templates/common/bundled-skills/pactile-skill-creator/references/pactile-skill-locations.md +52 -0
  523. package/dist/templates/common/bundled-skills/pactile-skill-creator/references/review-checklist.md +52 -0
  524. package/dist/templates/common/bundled-skills/pactile-spec-bootstrap/SKILL.md +41 -0
  525. package/dist/templates/common/bundled-skills/pactile-spec-bootstrap/references/mcp-setup.md +90 -0
  526. package/dist/templates/common/bundled-skills/pactile-spec-bootstrap/references/repository-analysis.md +59 -0
  527. package/dist/templates/common/bundled-skills/pactile-spec-bootstrap/references/spec-task-planning.md +61 -0
  528. package/dist/templates/common/bundled-skills/pactile-spec-bootstrap/references/spec-writing.md +70 -0
  529. package/dist/templates/common/bundled-skills/smart-search-cli/SKILL.md +393 -0
  530. package/dist/templates/common/bundled-skills/smart-search-cli/agents/openai.yaml +3 -0
  531. package/dist/templates/common/bundled-skills/smart-search-cli/examples/batch-search.md +98 -0
  532. package/dist/templates/common/bundled-skills/smart-search-cli/examples/evidence-gathering.md +89 -0
  533. package/dist/templates/common/bundled-skills/smart-search-cli/references/cli-contract.md +323 -0
  534. package/dist/templates/common/commands/continue.md +62 -0
  535. package/dist/templates/common/commands/finish-work.md +82 -0
  536. package/dist/templates/common/commands/start.md +66 -0
  537. package/dist/templates/common/index.d.ts +68 -0
  538. package/dist/templates/common/index.d.ts.map +1 -0
  539. package/dist/templates/common/index.js +123 -0
  540. package/dist/templates/common/index.js.map +1 -0
  541. package/dist/templates/common/optional-skills/chrome-cdp/SKILL.md +179 -0
  542. package/dist/templates/common/optional-skills/chrome-cdp/examples/fetch-hook-api-capture.md +149 -0
  543. package/dist/templates/common/optional-skills/chrome-cdp/scripts/cdp.mjs +904 -0
  544. package/dist/templates/common/skills/before-dev.md +35 -0
  545. package/dist/templates/common/skills/brainstorm.md +204 -0
  546. package/dist/templates/common/skills/break-loop.md +125 -0
  547. package/dist/templates/common/skills/update-spec.md +364 -0
  548. package/dist/templates/cursor/agents/pactile-check.md +58 -0
  549. package/dist/templates/cursor/agents/pactile-implement.md +56 -0
  550. package/dist/templates/cursor/agents/pactile-research.md +51 -0
  551. package/dist/templates/cursor/commands/handoff.md +101 -0
  552. package/dist/templates/cursor/fixtures/expected-rules.d.ts +14 -0
  553. package/dist/templates/cursor/fixtures/expected-rules.d.ts.map +1 -0
  554. package/dist/templates/cursor/fixtures/expected-rules.js +20 -0
  555. package/dist/templates/cursor/fixtures/expected-rules.js.map +1 -0
  556. package/dist/templates/cursor/hooks.json +52 -0
  557. package/dist/templates/cursor/index.d.ts +19 -0
  558. package/dist/templates/cursor/index.d.ts.map +1 -0
  559. package/dist/templates/cursor/index.js +19 -0
  560. package/dist/templates/cursor/index.js.map +1 -0
  561. package/dist/templates/cursor/rules/pactile-bootstrap.mdc +18 -0
  562. package/dist/templates/cursor/worktrees.json +11 -0
  563. package/dist/templates/extract.d.ts +38 -0
  564. package/dist/templates/extract.d.ts.map +1 -0
  565. package/dist/templates/extract.js +120 -0
  566. package/dist/templates/extract.js.map +1 -0
  567. package/dist/templates/markdown/agents.md +14 -0
  568. package/dist/templates/markdown/framework/artifact-locale-guide.md.txt +93 -0
  569. package/dist/templates/markdown/framework/cursor-context-injection-guide.md.txt +85 -0
  570. package/dist/templates/markdown/framework/cursor-native-modes-guide.md.txt +239 -0
  571. package/dist/templates/markdown/framework/cursor-semantic-compliance.md.txt +41 -0
  572. package/dist/templates/markdown/framework/cursor-subagent-policy.md.txt +90 -0
  573. package/dist/templates/markdown/framework/dogfood-only-surfaces.md.txt +28 -0
  574. package/dist/templates/markdown/framework/execution-strategy.md.txt +45 -0
  575. package/dist/templates/markdown/framework/index.md.txt +31 -0
  576. package/dist/templates/markdown/framework/injection-budget-guide.md.txt +111 -0
  577. package/dist/templates/markdown/framework/internal-skills-cursor-reachability.md.txt +45 -0
  578. package/dist/templates/markdown/framework/middleware-protocol.md.txt +238 -0
  579. package/dist/templates/markdown/framework/parallel-first-execution.md.txt +116 -0
  580. package/dist/templates/markdown/framework/prd-grill-frontier.md.txt +105 -0
  581. package/dist/templates/markdown/framework/release-boundary.md.txt +30 -0
  582. package/dist/templates/markdown/framework/retrieval-daily-guide.md.txt +199 -0
  583. package/dist/templates/markdown/framework/upgrade.md.txt +15 -0
  584. package/dist/templates/markdown/framework/verification-strength-guide.md.txt +185 -0
  585. package/dist/templates/markdown/gitignore.txt +15 -0
  586. package/dist/templates/markdown/index.d.ts +59 -0
  587. package/dist/templates/markdown/index.d.ts.map +1 -0
  588. package/dist/templates/markdown/index.js +144 -0
  589. package/dist/templates/markdown/index.js.map +1 -0
  590. package/dist/templates/markdown/prompts/run-semantic-slice-12.md.txt +25 -0
  591. package/dist/templates/markdown/spec/backend/database-guidelines.md.txt +51 -0
  592. package/dist/templates/markdown/spec/backend/directory-structure.md.txt +54 -0
  593. package/dist/templates/markdown/spec/backend/error-handling.md.txt +51 -0
  594. package/dist/templates/markdown/spec/backend/index.md.txt +38 -0
  595. package/dist/templates/markdown/spec/backend/logging-guidelines.md.txt +51 -0
  596. package/dist/templates/markdown/spec/backend/quality-guidelines.md.txt +51 -0
  597. package/dist/templates/markdown/spec/frontend/component-guidelines.md.txt +59 -0
  598. package/dist/templates/markdown/spec/frontend/directory-structure.md.txt +54 -0
  599. package/dist/templates/markdown/spec/frontend/hook-guidelines.md.txt +51 -0
  600. package/dist/templates/markdown/spec/frontend/index.md.txt +39 -0
  601. package/dist/templates/markdown/spec/frontend/quality-guidelines.md.txt +51 -0
  602. package/dist/templates/markdown/spec/frontend/state-management.md.txt +51 -0
  603. package/dist/templates/markdown/spec/frontend/type-safety.md.txt +51 -0
  604. package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +174 -0
  605. package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +162 -0
  606. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +634 -0
  607. package/dist/templates/markdown/spec/guides/debug-loop-guide.md.txt +227 -0
  608. package/dist/templates/markdown/spec/guides/durable-learning-decision-guide.md.txt +76 -0
  609. package/dist/templates/markdown/spec/guides/e2e-walkthrough-guide.md.txt +93 -0
  610. package/dist/templates/markdown/spec/guides/index.md.txt +131 -0
  611. package/dist/templates/markdown/spec/guides/prototype-guide.md.txt +139 -0
  612. package/dist/templates/markdown/spec/guides/test-discipline-guide.md.txt +179 -0
  613. package/dist/templates/markdown/workspace-index.md +125 -0
  614. package/dist/templates/markdown/worktree.yaml.txt +58 -0
  615. package/dist/templates/pactile/CONTEXT.md +113 -0
  616. package/dist/templates/pactile/config/execution-strategy-rules.json +31 -0
  617. package/dist/templates/pactile/config.yaml +130 -0
  618. package/dist/templates/pactile/docs/adr/README.md +21 -0
  619. package/dist/templates/pactile/gitignore.txt +35 -0
  620. package/dist/templates/pactile/index.d.ts +134 -0
  621. package/dist/templates/pactile/index.d.ts.map +1 -0
  622. package/dist/templates/pactile/index.js +316 -0
  623. package/dist/templates/pactile/index.js.map +1 -0
  624. package/dist/templates/pactile/modules/approval-personal/contract.md +43 -0
  625. package/dist/templates/pactile/modules/candidate-pool/contract.md +37 -0
  626. package/dist/templates/pactile/modules/catalog.d.ts +32 -0
  627. package/dist/templates/pactile/modules/catalog.d.ts.map +1 -0
  628. package/dist/templates/pactile/modules/catalog.js +46 -0
  629. package/dist/templates/pactile/modules/catalog.js.map +1 -0
  630. package/dist/templates/pactile/modules/close-basic/contract.md +38 -0
  631. package/dist/templates/pactile/modules/context-progressive/contract.md +44 -0
  632. package/dist/templates/pactile/modules/debug-recovery/contract.md +44 -0
  633. package/dist/templates/pactile/modules/define-basic/contract.md +39 -0
  634. package/dist/templates/pactile/modules/define-extended/contract.md +45 -0
  635. package/dist/templates/pactile/modules/execute-agent/contract.md +38 -0
  636. package/dist/templates/pactile/modules/independent-check/contract.md +35 -0
  637. package/dist/templates/pactile/modules/index.json +133 -0
  638. package/dist/templates/pactile/modules/intake-basic/contract.md +34 -0
  639. package/dist/templates/pactile/modules/observability-local/contract.md +31 -0
  640. package/dist/templates/pactile/modules/parent-child/contract.md +45 -0
  641. package/dist/templates/pactile/modules/personal-memory/contract.md +56 -0
  642. package/dist/templates/pactile/modules/retention-storage/contract.md +43 -0
  643. package/dist/templates/pactile/modules/retrieval-extended/contract.md +77 -0
  644. package/dist/templates/pactile/modules/session-transfer/contract.md +41 -0
  645. package/dist/templates/pactile/modules/spec-learning/contract.md +50 -0
  646. package/dist/templates/pactile/modules/vcs-integration/contract.md +46 -0
  647. package/dist/templates/pactile/modules/verify-basic/contract.md +38 -0
  648. package/dist/templates/pactile/modules/worker-orchestration/contract.md +47 -0
  649. package/dist/templates/pactile/pool/README.md +149 -0
  650. package/dist/templates/pactile/pool/items/.gitkeep +0 -0
  651. package/dist/templates/pactile/pool/plan.md +50 -0
  652. package/dist/templates/pactile/scripts/__init__.py +5 -0
  653. package/dist/templates/pactile/scripts/add_session.py +547 -0
  654. package/dist/templates/pactile/scripts/build_context_pack.py +17 -0
  655. package/dist/templates/pactile/scripts/build_retrieval_pack.py +376 -0
  656. package/dist/templates/pactile/scripts/codegraph_session_smoke.py +76 -0
  657. package/dist/templates/pactile/scripts/common/__init__.py +103 -0
  658. package/dist/templates/pactile/scripts/common/active_task.py +576 -0
  659. package/dist/templates/pactile/scripts/common/adapter_middleware.py +1373 -0
  660. package/dist/templates/pactile/scripts/common/artifact_locale.py +303 -0
  661. package/dist/templates/pactile/scripts/common/artifact_search.py +587 -0
  662. package/dist/templates/pactile/scripts/common/cli_adapter.py +135 -0
  663. package/dist/templates/pactile/scripts/common/cli_environment.py +155 -0
  664. package/dist/templates/pactile/scripts/common/codebase_retrieval_router.py +1339 -0
  665. package/dist/templates/pactile/scripts/common/config.py +497 -0
  666. package/dist/templates/pactile/scripts/common/context_pack.py +371 -0
  667. package/dist/templates/pactile/scripts/common/cursor_retrieval_env.py +31 -0
  668. package/dist/templates/pactile/scripts/common/developer.py +190 -0
  669. package/dist/templates/pactile/scripts/common/execution_strategy.py +268 -0
  670. package/dist/templates/pactile/scripts/common/full_quality.py +237 -0
  671. package/dist/templates/pactile/scripts/common/git.py +37 -0
  672. package/dist/templates/pactile/scripts/common/git_context.py +198 -0
  673. package/dist/templates/pactile/scripts/common/injection_budget.py +317 -0
  674. package/dist/templates/pactile/scripts/common/io.py +37 -0
  675. package/dist/templates/pactile/scripts/common/kernel_command.py +369 -0
  676. package/dist/templates/pactile/scripts/common/lite_context.py +280 -0
  677. package/dist/templates/pactile/scripts/common/log.py +45 -0
  678. package/dist/templates/pactile/scripts/common/ondemand_topology.py +419 -0
  679. package/dist/templates/pactile/scripts/common/packages_context.py +239 -0
  680. package/dist/templates/pactile/scripts/common/pactile_config.py +131 -0
  681. package/dist/templates/pactile/scripts/common/parallel_declaration.py +369 -0
  682. package/dist/templates/pactile/scripts/common/parent_orchestration.py +1198 -0
  683. package/dist/templates/pactile/scripts/common/paths.py +756 -0
  684. package/dist/templates/pactile/scripts/common/pool_refs.py +119 -0
  685. package/dist/templates/pactile/scripts/common/pool_slice.py +269 -0
  686. package/dist/templates/pactile/scripts/common/pool_store.py +766 -0
  687. package/dist/templates/pactile/scripts/common/project_file_stats.py +91 -0
  688. package/dist/templates/pactile/scripts/common/retrieval_adapter_metadata.py +943 -0
  689. package/dist/templates/pactile/scripts/common/retrieval_agent_instructions.py +123 -0
  690. package/dist/templates/pactile/scripts/common/retrieval_evidence.py +915 -0
  691. package/dist/templates/pactile/scripts/common/retrieval_pack.py +422 -0
  692. package/dist/templates/pactile/scripts/common/retrieval_pack_context.py +122 -0
  693. package/dist/templates/pactile/scripts/common/retrieval_plan_gate.py +61 -0
  694. package/dist/templates/pactile/scripts/common/retrieval_result_ranking.py +104 -0
  695. package/dist/templates/pactile/scripts/common/retrieval_tool_classification.py +166 -0
  696. package/dist/templates/pactile/scripts/common/safe_commit.py +285 -0
  697. package/dist/templates/pactile/scripts/common/semantic_plan_gate.py +53 -0
  698. package/dist/templates/pactile/scripts/common/session_context.py +1213 -0
  699. package/dist/templates/pactile/scripts/common/session_memory.py +386 -0
  700. package/dist/templates/pactile/scripts/common/session_pack.py +886 -0
  701. package/dist/templates/pactile/scripts/common/smart_search_evidence.py +514 -0
  702. package/dist/templates/pactile/scripts/common/smart_search_resolve.py +150 -0
  703. package/dist/templates/pactile/scripts/common/subagent_dispatch.py +592 -0
  704. package/dist/templates/pactile/scripts/common/task_context.py +241 -0
  705. package/dist/templates/pactile/scripts/common/task_dashboard.py +286 -0
  706. package/dist/templates/pactile/scripts/common/task_dependencies.py +648 -0
  707. package/dist/templates/pactile/scripts/common/task_gates.py +2478 -0
  708. package/dist/templates/pactile/scripts/common/task_map.py +703 -0
  709. package/dist/templates/pactile/scripts/common/task_queue.py +188 -0
  710. package/dist/templates/pactile/scripts/common/task_store.py +2252 -0
  711. package/dist/templates/pactile/scripts/common/task_utils.py +274 -0
  712. package/dist/templates/pactile/scripts/common/tasks.py +231 -0
  713. package/dist/templates/pactile/scripts/common/test_adapter_middleware.py +199 -0
  714. package/dist/templates/pactile/scripts/common/test_depends_mode_block.py +509 -0
  715. package/dist/templates/pactile/scripts/common/test_full_quality.py +197 -0
  716. package/dist/templates/pactile/scripts/common/test_kernel_command.py +336 -0
  717. package/dist/templates/pactile/scripts/common/test_lite_path.py +190 -0
  718. package/dist/templates/pactile/scripts/common/test_observable_defaults.py +141 -0
  719. package/dist/templates/pactile/scripts/common/test_ondemand_topology.py +237 -0
  720. package/dist/templates/pactile/scripts/common/test_parallel_declaration.py +264 -0
  721. package/dist/templates/pactile/scripts/common/test_pool_slice.py +198 -0
  722. package/dist/templates/pactile/scripts/common/test_pool_store.py +452 -0
  723. package/dist/templates/pactile/scripts/common/test_retrieval_arbitration.py +331 -0
  724. package/dist/templates/pactile/scripts/common/test_task_dashboard.py +155 -0
  725. package/dist/templates/pactile/scripts/common/test_task_dependencies.py +329 -0
  726. package/dist/templates/pactile/scripts/common/test_task_store_kernel_patch.py +163 -0
  727. package/dist/templates/pactile/scripts/common/types.py +110 -0
  728. package/dist/templates/pactile/scripts/common/workflow_phase.py +194 -0
  729. package/dist/templates/pactile/scripts/compile_session_pack.py +15 -0
  730. package/dist/templates/pactile/scripts/cursor_retrieval_probe.py +64 -0
  731. package/dist/templates/pactile/scripts/cursor_retrieval_probe_prompt.md +33 -0
  732. package/dist/templates/pactile/scripts/generate_dispatch_prompt.py +182 -0
  733. package/dist/templates/pactile/scripts/get_context.py +19 -0
  734. package/dist/templates/pactile/scripts/get_developer.py +26 -0
  735. package/dist/templates/pactile/scripts/hooks/linear_sync.py +243 -0
  736. package/dist/templates/pactile/scripts/init_developer.py +51 -0
  737. package/dist/templates/pactile/scripts/injection_budget_probe.py +69 -0
  738. package/dist/templates/pactile/scripts/pool.py +237 -0
  739. package/dist/templates/pactile/scripts/rank_retrieval_candidates.py +98 -0
  740. package/dist/templates/pactile/scripts/retrieval_probe_matrix_template.json +43 -0
  741. package/dist/templates/pactile/scripts/route_codebase_retrieval.py +72 -0
  742. package/dist/templates/pactile/scripts/run_smart_search.py +12 -0
  743. package/dist/templates/pactile/scripts/score_evidence.py +131 -0
  744. package/dist/templates/pactile/scripts/search_artifacts.py +13 -0
  745. package/dist/templates/pactile/scripts/search_memory.py +12 -0
  746. package/dist/templates/pactile/scripts/spec_health_outcomes.py +160 -0
  747. package/dist/templates/pactile/scripts/task.py +1118 -0
  748. package/dist/templates/pactile/scripts/verify_evidence_probe.py +138 -0
  749. package/dist/templates/pactile/tasks/.gitkeep +0 -0
  750. package/dist/templates/pactile/tasks/locale/en/default-prd.md +27 -0
  751. package/dist/templates/pactile/tasks/locale/zh/default-prd.md +27 -0
  752. package/dist/templates/pactile/tasks/templates/release-execution/design.md +61 -0
  753. package/dist/templates/pactile/tasks/templates/release-execution/handoff-template.md +29 -0
  754. package/dist/templates/pactile/tasks/templates/release-execution/implement.md +29 -0
  755. package/dist/templates/pactile/tasks/templates/release-execution/prd.md +31 -0
  756. package/dist/templates/pactile/tasks/templates/release-readiness/design.md +50 -0
  757. package/dist/templates/pactile/tasks/templates/release-readiness/handoff-template.md +39 -0
  758. package/dist/templates/pactile/tasks/templates/release-readiness/implement.md +29 -0
  759. package/dist/templates/pactile/tasks/templates/release-readiness/prd.md +31 -0
  760. package/dist/templates/pactile/workflow.md +64 -0
  761. package/dist/templates/shared-hooks/event-bridge.py +121 -0
  762. package/dist/templates/shared-hooks/index.d.ts +50 -0
  763. package/dist/templates/shared-hooks/index.d.ts.map +1 -0
  764. package/dist/templates/shared-hooks/index.js +96 -0
  765. package/dist/templates/shared-hooks/index.js.map +1 -0
  766. package/dist/templates/shared-hooks/inject-retrieval-plan.py +217 -0
  767. package/dist/templates/shared-hooks/inject-shell-session-context.py +183 -0
  768. package/dist/templates/shared-hooks/inject-subagent-context.py +267 -0
  769. package/dist/templates/shared-hooks/inject-workflow-state.py +106 -0
  770. package/dist/templates/shared-hooks/rename-session-for-task.py +231 -0
  771. package/dist/templates/shared-hooks/research-end-retrieval-pack.py +345 -0
  772. package/dist/templates/shared-hooks/session-start.py +355 -0
  773. package/dist/templates/shared-hooks/spec-write-audit.py +173 -0
  774. package/dist/templates/template-utils.d.ts +28 -0
  775. package/dist/templates/template-utils.d.ts.map +1 -0
  776. package/dist/templates/template-utils.js +83 -0
  777. package/dist/templates/template-utils.js.map +1 -0
  778. package/dist/types/ai-tools.d.ts +102 -0
  779. package/dist/types/ai-tools.d.ts.map +1 -0
  780. package/dist/types/ai-tools.js +60 -0
  781. package/dist/types/ai-tools.js.map +1 -0
  782. package/dist/types/migration.d.ts +125 -0
  783. package/dist/types/migration.d.ts.map +1 -0
  784. package/dist/types/migration.js +8 -0
  785. package/dist/types/migration.js.map +1 -0
  786. package/dist/utils/agents-md.d.ts +27 -0
  787. package/dist/utils/agents-md.d.ts.map +1 -0
  788. package/dist/utils/agents-md.js +70 -0
  789. package/dist/utils/agents-md.js.map +1 -0
  790. package/dist/utils/codebase-retrieval-router.d.ts +31 -0
  791. package/dist/utils/codebase-retrieval-router.d.ts.map +1 -0
  792. package/dist/utils/codebase-retrieval-router.js +88 -0
  793. package/dist/utils/codebase-retrieval-router.js.map +1 -0
  794. package/dist/utils/compare-versions.d.ts +12 -0
  795. package/dist/utils/compare-versions.d.ts.map +1 -0
  796. package/dist/utils/compare-versions.js +86 -0
  797. package/dist/utils/compare-versions.js.map +1 -0
  798. package/dist/utils/cursor-retrieval-env.d.ts +17 -0
  799. package/dist/utils/cursor-retrieval-env.d.ts.map +1 -0
  800. package/dist/utils/cursor-retrieval-env.js +19 -0
  801. package/dist/utils/cursor-retrieval-env.js.map +1 -0
  802. package/dist/utils/cwd-guard.d.ts +38 -0
  803. package/dist/utils/cwd-guard.d.ts.map +1 -0
  804. package/dist/utils/cwd-guard.js +63 -0
  805. package/dist/utils/cwd-guard.js.map +1 -0
  806. package/dist/utils/file-writer.d.ts +36 -0
  807. package/dist/utils/file-writer.d.ts.map +1 -0
  808. package/dist/utils/file-writer.js +203 -0
  809. package/dist/utils/file-writer.js.map +1 -0
  810. package/dist/utils/manifest-prune.d.ts +59 -0
  811. package/dist/utils/manifest-prune.d.ts.map +1 -0
  812. package/dist/utils/manifest-prune.js +176 -0
  813. package/dist/utils/manifest-prune.js.map +1 -0
  814. package/dist/utils/mirror-check.d.ts +22 -0
  815. package/dist/utils/mirror-check.d.ts.map +1 -0
  816. package/dist/utils/mirror-check.js +90 -0
  817. package/dist/utils/mirror-check.js.map +1 -0
  818. package/dist/utils/normalize-text.d.ts +7 -0
  819. package/dist/utils/normalize-text.d.ts.map +1 -0
  820. package/dist/utils/normalize-text.js +16 -0
  821. package/dist/utils/normalize-text.js.map +1 -0
  822. package/dist/utils/p36-upgrade.d.ts +62 -0
  823. package/dist/utils/p36-upgrade.d.ts.map +1 -0
  824. package/dist/utils/p36-upgrade.js +165 -0
  825. package/dist/utils/p36-upgrade.js.map +1 -0
  826. package/dist/utils/posix.d.ts +13 -0
  827. package/dist/utils/posix.d.ts.map +1 -0
  828. package/dist/utils/posix.js +15 -0
  829. package/dist/utils/posix.js.map +1 -0
  830. package/dist/utils/post-update-smoke.d.ts +7 -0
  831. package/dist/utils/post-update-smoke.d.ts.map +1 -0
  832. package/dist/utils/post-update-smoke.js +40 -0
  833. package/dist/utils/post-update-smoke.js.map +1 -0
  834. package/dist/utils/project-capabilities.d.ts +82 -0
  835. package/dist/utils/project-capabilities.d.ts.map +1 -0
  836. package/dist/utils/project-capabilities.js +691 -0
  837. package/dist/utils/project-capabilities.js.map +1 -0
  838. package/dist/utils/project-detector.d.ts +46 -0
  839. package/dist/utils/project-detector.d.ts.map +1 -0
  840. package/dist/utils/project-detector.js +666 -0
  841. package/dist/utils/project-detector.js.map +1 -0
  842. package/dist/utils/proxy.d.ts +25 -0
  843. package/dist/utils/proxy.d.ts.map +1 -0
  844. package/dist/utils/proxy.js +60 -0
  845. package/dist/utils/proxy.js.map +1 -0
  846. package/dist/utils/readiness.d.ts +40 -0
  847. package/dist/utils/readiness.d.ts.map +1 -0
  848. package/dist/utils/readiness.js +491 -0
  849. package/dist/utils/readiness.js.map +1 -0
  850. package/dist/utils/retrieval-agent-instructions.d.ts +10 -0
  851. package/dist/utils/retrieval-agent-instructions.d.ts.map +1 -0
  852. package/dist/utils/retrieval-agent-instructions.js +97 -0
  853. package/dist/utils/retrieval-agent-instructions.js.map +1 -0
  854. package/dist/utils/retrieval-execution-telemetry.d.ts +53 -0
  855. package/dist/utils/retrieval-execution-telemetry.d.ts.map +1 -0
  856. package/dist/utils/retrieval-execution-telemetry.js +116 -0
  857. package/dist/utils/retrieval-execution-telemetry.js.map +1 -0
  858. package/dist/utils/retrieval-result-ranking.d.ts +34 -0
  859. package/dist/utils/retrieval-result-ranking.d.ts.map +1 -0
  860. package/dist/utils/retrieval-result-ranking.js +71 -0
  861. package/dist/utils/retrieval-result-ranking.js.map +1 -0
  862. package/dist/utils/retrieval-tool-classification.d.ts +18 -0
  863. package/dist/utils/retrieval-tool-classification.d.ts.map +1 -0
  864. package/dist/utils/retrieval-tool-classification.js +118 -0
  865. package/dist/utils/retrieval-tool-classification.js.map +1 -0
  866. package/dist/utils/semantic-plan-gate.d.ts +8 -0
  867. package/dist/utils/semantic-plan-gate.d.ts.map +1 -0
  868. package/dist/utils/semantic-plan-gate.js +24 -0
  869. package/dist/utils/semantic-plan-gate.js.map +1 -0
  870. package/dist/utils/task-json.d.ts +13 -0
  871. package/dist/utils/task-json.d.ts.map +1 -0
  872. package/dist/utils/task-json.js +12 -0
  873. package/dist/utils/task-json.js.map +1 -0
  874. package/dist/utils/template-fetcher.d.ts +150 -0
  875. package/dist/utils/template-fetcher.d.ts.map +1 -0
  876. package/dist/utils/template-fetcher.js +907 -0
  877. package/dist/utils/template-fetcher.js.map +1 -0
  878. package/dist/utils/template-hash.d.ts +123 -0
  879. package/dist/utils/template-hash.d.ts.map +1 -0
  880. package/dist/utils/template-hash.js +375 -0
  881. package/dist/utils/template-hash.js.map +1 -0
  882. package/dist/utils/uninstall-scrubbers.d.ts +30 -0
  883. package/dist/utils/uninstall-scrubbers.d.ts.map +1 -0
  884. package/dist/utils/uninstall-scrubbers.js +196 -0
  885. package/dist/utils/uninstall-scrubbers.js.map +1 -0
  886. package/dist/utils/update-rollout-report.d.ts +171 -0
  887. package/dist/utils/update-rollout-report.d.ts.map +1 -0
  888. package/dist/utils/update-rollout-report.js +126 -0
  889. package/dist/utils/update-rollout-report.js.map +1 -0
  890. package/dist/utils/validate-rules.d.ts +21 -0
  891. package/dist/utils/validate-rules.d.ts.map +1 -0
  892. package/dist/utils/validate-rules.js +88 -0
  893. package/dist/utils/validate-rules.js.map +1 -0
  894. package/dist/utils/workflow-dir.d.ts +15 -0
  895. package/dist/utils/workflow-dir.d.ts.map +1 -0
  896. package/dist/utils/workflow-dir.js +33 -0
  897. package/dist/utils/workflow-dir.js.map +1 -0
  898. package/dist/utils/workflow-resolver.d.ts +86 -0
  899. package/dist/utils/workflow-resolver.d.ts.map +1 -0
  900. package/dist/utils/workflow-resolver.js +265 -0
  901. package/dist/utils/workflow-resolver.js.map +1 -0
  902. package/package.json +117 -0
  903. package/scripts/postinstall.js +6 -0
@@ -0,0 +1,2478 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Task quality gate helpers.
4
+
5
+ Gate records are stored in task.json under:
6
+ quality_gate_results.transitions[transition][gate]
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import re
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+
18
+ from .task_dependencies import (
19
+ describe_dependencies,
20
+ read_depends_mode,
21
+ scope_children_for_task,
22
+ )
23
+ from .task_map import (
24
+ get_child_state,
25
+ load_task_map,
26
+ validate_child_archive_state,
27
+ validate_parent_children_complete,
28
+ )
29
+ from .full_quality import full_quality_archive_errors, rigor_from_required_controls
30
+
31
+ SCHEMA_VERSION = 1
32
+ BASELINE_GATE = "baseline-check"
33
+
34
+ KNOWN_TRANSITIONS = {
35
+ "start-execution",
36
+ "full-task-complete",
37
+ "child-review",
38
+ "parent-changes",
39
+ "parent-accepted",
40
+ "parent-integrating",
41
+ "parent-integrated",
42
+ "parent-cancelled",
43
+ }
44
+
45
+ REVIEW_GATES = {
46
+ "requirements-review",
47
+ "code-review",
48
+ "architecture-review",
49
+ "architecture-deep-review",
50
+ "integration-review",
51
+ }
52
+
53
+ KNOWN_GATES = {BASELINE_GATE, *REVIEW_GATES}
54
+ RESULTS = {"PASS", "FAIL", "SKIPPED"}
55
+ FAIL_ROOT_CAUSES = {
56
+ "implementation-defect": "Execution",
57
+ "contract-changing-defect": "Planning",
58
+ "validation-environment-blocker": "Verification / Review",
59
+ }
60
+
61
+ PROFILE_DEFAULT_GATES = {
62
+ "standard": ["requirements-review", "code-review"],
63
+ "strict": ["requirements-review", "code-review"],
64
+ "architecture": [
65
+ "requirements-review",
66
+ "architecture-review",
67
+ "code-review",
68
+ ],
69
+ }
70
+
71
+ ALLOWED_EXECUTION_MODES = {"inline", "worker", "child-task"}
72
+ ALLOWED_ISOLATION = {"main-worktree", "git-worktree"}
73
+ ALLOWED_VERIFICATION_PROFILES = {"standard", "strict", "architecture"}
74
+ ALLOWED_RETRIEVAL_PROFILES = {
75
+ "exact-only",
76
+ "semantic",
77
+ "structure",
78
+ "architecture-memory",
79
+ }
80
+ ALLOWED_GATE_MODES = {"profile", "explicit"}
81
+
82
+ MAX_SHORT_FIELD = 240
83
+ MAX_REASON = 500
84
+ REVIEWER_RE = re.compile(r"^[A-Za-z0-9_.:@/-]+$")
85
+ VALIDATION_EVIDENCE_RE = re.compile(
86
+ r"(?im)^\s*(?:[-*]\s*)?validation"
87
+ r"(?:\s+(?:commands?|results?|evidence))?\s*:\s*(\S[^\r\n]*)$"
88
+ )
89
+ ACCEPTANCE_EVIDENCE_RE = re.compile(
90
+ r"(?im)^\s*(?:[-*]\s*)?(?:final\s+|user\s+)?acceptance"
91
+ r"(?:\s+evidence)?\s*:\s*(\S[^\r\n]*)$"
92
+ )
93
+ ACCEPTED_BY_USER_RE = re.compile(
94
+ r"(?im)^\s*(?:[-*]\s*)?accepted\s+by\s+user\s*:\s*(\S[^\r\n]*)$"
95
+ )
96
+ NO_DURABLE_LEARNING_RE = re.compile(r"(?i)\bno\s+durable\s+learning\b")
97
+ DURABLE_LEARNING_EVIDENCE_RE = re.compile(
98
+ r"(?im)^\s*(?:[-*]\s*)?(?:durable\s+learning(?:\s+decision)?|learning\s+decision|"
99
+ r"spec\s+updates?|spec\s+update\s+(?:needed|evidence)|updated\s+spec|"
100
+ r"retrospective(?:\.md)?|learning\s+artifact)\s*:\s*(\S[^\r\n]*)$"
101
+ )
102
+ INTEGRATION_EVIDENCE_RE = re.compile(
103
+ r"(?im)^\s*(?:[-*]\s*)?(?:final\s+)?integration"
104
+ r"(?:\s+evidence)?\s*:\s*(\S[^\r\n]*)$"
105
+ )
106
+ REVIEWED_CHANGE_SET_RE = re.compile(
107
+ r"(?im)^\s*(?:[-*]\s*)?(?:reviewed\s+)?"
108
+ r"(?:change[- ]set|changeset|diff|git\s+diff|ref|git\s+ref)"
109
+ r"(?:\s+(?:identity|evidence|summary|ref))?\s*:\s*(\S[^\r\n]*)$"
110
+ )
111
+ CHECK_EVIDENCE_RE = re.compile(
112
+ r"(?im)^\s*(?:[-*]\s*)?(?:check\s+evidence|pactile-check(?:\s+evidence)?)"
113
+ r"\s*:\s*(\S[^\r\n]*)$"
114
+ )
115
+ PLACEHOLDER_VALUES_RE = re.compile(
116
+ r"(?i)^(TBD|TODO|待定|待补充|N/?A|NA|NONE|-|\.\.\.)$"
117
+ )
118
+ PRD_ACCEPTANCE_CRITERIA_HEADING_RE = re.compile(
119
+ r"(?im)^\s*#{1,6}\s*acceptance\s+criteria\s*$"
120
+ )
121
+ PRD_ACCEPTANCE_ITEM_RE = re.compile(
122
+ r"(?im)^\s*-\s*\[[ xX]\]\s+(.+)$"
123
+ )
124
+ PRD_PLACEHOLDER_RE = re.compile(r"(?i)^\s*(TBD|TODO|待定|待补充)(?:\s*[.::])?\s*$")
125
+ AUTO_PLANNING_REVIEWER = "pactile-cli"
126
+ START_EXECUTION_AUTO_GATES = frozenset({"requirements-review", "architecture-review"})
127
+
128
+ PROFILE_BASELINE_EIGHT = (
129
+ "intake-basic",
130
+ "define-basic",
131
+ "approval-personal",
132
+ "execute-agent",
133
+ "verify-basic",
134
+ "close-basic",
135
+ "context-progressive",
136
+ "observability-local",
137
+ )
138
+ REQUIRED_LIFECYCLE_SLOTS = (
139
+ "define-basic",
140
+ "approval-personal",
141
+ "execute-agent",
142
+ "verify-basic",
143
+ "close-basic",
144
+ )
145
+
146
+ STABLE_TASK_KEYS = (
147
+ "id",
148
+ "name",
149
+ "title",
150
+ "description",
151
+ "status",
152
+ "dev_type",
153
+ "scope",
154
+ "package",
155
+ "priority",
156
+ "creator",
157
+ "assignee",
158
+ "parent",
159
+ "children",
160
+ "subtasks",
161
+ "relatedFiles",
162
+ "notes",
163
+ "meta",
164
+ "branch",
165
+ "base_branch",
166
+ "task_kind",
167
+ "task_type",
168
+ "kind",
169
+ "mode",
170
+ "contract_epoch",
171
+ "depends_on",
172
+ )
173
+
174
+
175
+ @dataclass
176
+ class GateGuardResult:
177
+ """Result from a protected-transition guard."""
178
+
179
+ ok: bool
180
+ errors: list[str] = field(default_factory=list)
181
+ warnings: list[str] = field(default_factory=list)
182
+ contract_fingerprint: str = ""
183
+ artifact_fingerprints: dict[str, str] = field(default_factory=dict)
184
+ required_gates: list[str] = field(default_factory=list)
185
+ baseline_record: dict | None = None
186
+ is_full_task: bool = False
187
+ closeout_profile: str = "lite"
188
+ auto_gate_records: dict[str, dict] = field(default_factory=dict)
189
+ deps_blocking_summary: list[str] = field(default_factory=list)
190
+
191
+
192
+ def utc_now() -> str:
193
+ """Return a compact UTC timestamp for task.json records."""
194
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace(
195
+ "+00:00", "Z"
196
+ )
197
+
198
+
199
+ def normalize_result(result: str) -> str:
200
+ """Normalize a CLI result value."""
201
+ return result.strip().upper()
202
+
203
+
204
+ def wave_c_stop_read_confirmed(start: Path | None = None) -> bool:
205
+ """Return True after ``pactile update`` wrote the Wave C confirm flag.
206
+
207
+ Unconfirmed projects keep leftover dual-read. Missing ``.pactile`` is
208
+ unconfirmed (unit fixtures without a repo root stay dual-read).
209
+ """
210
+ from .paths import DIR_WORKFLOW, get_repo_root
211
+
212
+ root = get_repo_root(start)
213
+ path = root / DIR_WORKFLOW / ".p36-wave-c.json"
214
+ if not path.is_file():
215
+ return False
216
+ try:
217
+ parsed = json.loads(path.read_text(encoding="utf-8"))
218
+ except (OSError, json.JSONDecodeError):
219
+ return False
220
+ return isinstance(parsed, dict) and parsed.get("confirmed") is True
221
+
222
+
223
+ def task_closeout_profile(task_dir: Path, task_data: dict | None = None) -> str:
224
+ """Return lite, full, or parent closeout profile.
225
+
226
+ New Tasks use persisted ``required_controls`` / Topology only. Missing
227
+ contract is explicit Lite. File existence and ``children[]`` are not
228
+ used to infer Rigor or Parent. Leftover kind/mode/classification are
229
+ dual-read only until Wave C is confirmed.
230
+ """
231
+ data = task_data or {}
232
+ topology = data.get("topology") if isinstance(data.get("topology"), dict) else {}
233
+ if topology.get("kind") == "parent-child":
234
+ return "parent"
235
+
236
+ persisted = rigor_from_required_controls(data)
237
+ if persisted in ("lite", "full"):
238
+ return persisted
239
+
240
+ if wave_c_stop_read_confirmed(task_dir):
241
+ return "lite"
242
+
243
+ meta = data.get("meta") if isinstance(data.get("meta"), dict) else {}
244
+ candidates = [
245
+ data.get("task_kind"),
246
+ data.get("task_type"),
247
+ data.get("kind"),
248
+ data.get("mode"),
249
+ meta.get("task_kind"),
250
+ meta.get("task_type"),
251
+ meta.get("classification"),
252
+ meta.get("mode"),
253
+ ]
254
+ for value in candidates:
255
+ if not isinstance(value, str):
256
+ continue
257
+ normalized = value.lower().replace("_", "-")
258
+ if "parent" in normalized:
259
+ return "parent"
260
+ if "full" in normalized:
261
+ return "full"
262
+ if "lite" in normalized:
263
+ return "lite"
264
+
265
+ return "lite"
266
+
267
+
268
+ def is_full_task(task_dir: Path, task_data: dict | None = None) -> bool:
269
+ """Return True when a task should satisfy Full Task gates."""
270
+ return task_closeout_profile(task_dir, task_data) == "full"
271
+
272
+
273
+ def resolve_strategy_contract_for_gates(
274
+ task_dir: Path,
275
+ task_data: dict | None,
276
+ ) -> tuple[dict, list[str]]:
277
+ """Return the Development Strategy Contract used for gate fingerprints."""
278
+ if task_data is None:
279
+ return {}, []
280
+ profile = task_closeout_profile(task_dir, task_data)
281
+ if profile == "full":
282
+ return read_strategy_contract(task_dir)
283
+ if profile == "parent" and (task_dir / "implement.md").is_file():
284
+ return read_strategy_contract(task_dir)
285
+ return {}, []
286
+
287
+
288
+ def read_strategy_contract(task_dir: Path) -> tuple[dict, list[str]]:
289
+ """Parse the lightweight Development Strategy Contract from implement.md."""
290
+ implement_path = task_dir / "implement.md"
291
+ if not implement_path.is_file():
292
+ return {}, ["implement.md"]
293
+
294
+ try:
295
+ lines = implement_path.read_text(encoding="utf-8").splitlines()
296
+ except OSError:
297
+ return {}, ["implement.md could not be read"]
298
+
299
+ contract: dict = {}
300
+ section: str | None = None
301
+ list_key: str | None = None
302
+ started = False
303
+
304
+ top_level_keys = {
305
+ "execution_mode",
306
+ "isolation",
307
+ "verification_profile",
308
+ "retrieval_profile",
309
+ "optional_capabilities",
310
+ "quality_gates",
311
+ }
312
+
313
+ for raw_line in lines:
314
+ line = raw_line.rstrip()
315
+ stripped = line.strip()
316
+ if not stripped:
317
+ continue
318
+ if stripped.startswith("#") or stripped.startswith("```"):
319
+ continue
320
+
321
+ top_match = re.match(r"^([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$", stripped)
322
+ if top_match:
323
+ key = top_match.group(1)
324
+ value = top_match.group(2).strip()
325
+ if key in top_level_keys:
326
+ started = True
327
+ section = key if key == "quality_gates" else None
328
+ list_key = key if key == "optional_capabilities" and not value else None
329
+ if key == "quality_gates":
330
+ contract.setdefault("quality_gates", {})
331
+ elif key == "optional_capabilities":
332
+ if value:
333
+ contract[key] = _parse_inline_list(value)
334
+ else:
335
+ contract[key] = []
336
+ else:
337
+ contract[key] = value
338
+ continue
339
+
340
+ if not started:
341
+ continue
342
+
343
+ nested_match = re.match(r"^\s+([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$", line)
344
+ if nested_match and section == "quality_gates":
345
+ key = nested_match.group(1)
346
+ value = nested_match.group(2).strip()
347
+ gates = contract.setdefault("quality_gates", {})
348
+ if value:
349
+ gates[key] = _parse_inline_list(value)
350
+ list_key = None
351
+ else:
352
+ gates[key] = []
353
+ list_key = f"quality_gates.{key}"
354
+ continue
355
+
356
+ item_match = re.match(r"^\s*-\s+(.+)$", line)
357
+ if item_match:
358
+ item = item_match.group(1).strip()
359
+ if list_key == "optional_capabilities":
360
+ contract.setdefault("optional_capabilities", []).append(item)
361
+ elif list_key and list_key.startswith("quality_gates."):
362
+ gate_key = list_key.split(".", 1)[1]
363
+ gates = contract.setdefault("quality_gates", {})
364
+ gates.setdefault(gate_key, []).append(item)
365
+
366
+ errors = validate_strategy_contract(contract)
367
+ return contract, errors
368
+
369
+
370
+ def validate_strategy_contract(contract: dict) -> list[str]:
371
+ """Validate the parsed Development Strategy Contract."""
372
+ errors: list[str] = []
373
+ required = [
374
+ "execution_mode",
375
+ "isolation",
376
+ "verification_profile",
377
+ "retrieval_profile",
378
+ "optional_capabilities",
379
+ "quality_gates",
380
+ ]
381
+ for key in required:
382
+ if key not in contract:
383
+ errors.append(f"Development Strategy Contract missing {key}")
384
+
385
+ execution_mode = contract.get("execution_mode")
386
+ if execution_mode and execution_mode not in ALLOWED_EXECUTION_MODES:
387
+ errors.append(f"invalid execution_mode: {execution_mode}")
388
+
389
+ isolation = contract.get("isolation")
390
+ if isolation and isolation not in ALLOWED_ISOLATION:
391
+ errors.append(f"invalid isolation: {isolation}")
392
+
393
+ verification_profile = contract.get("verification_profile")
394
+ if verification_profile and verification_profile not in ALLOWED_VERIFICATION_PROFILES:
395
+ errors.append(f"invalid verification_profile: {verification_profile}")
396
+
397
+ retrieval_profile = contract.get("retrieval_profile")
398
+ if retrieval_profile and retrieval_profile not in ALLOWED_RETRIEVAL_PROFILES:
399
+ errors.append(f"invalid retrieval_profile: {retrieval_profile}")
400
+
401
+ optional_capabilities = contract.get("optional_capabilities")
402
+ if "optional_capabilities" in contract and not isinstance(optional_capabilities, list):
403
+ errors.append("optional_capabilities must be a list")
404
+
405
+ gates = contract.get("quality_gates")
406
+ if isinstance(gates, dict):
407
+ errors.extend(_validate_quality_gates(gates, verification_profile))
408
+ elif "quality_gates" in contract:
409
+ errors.append("quality_gates must be a mapping")
410
+
411
+ return errors
412
+
413
+
414
+ def enabled_gates_for_contract(contract: dict) -> list[str]:
415
+ """Return enabled non-baseline gates from a parsed contract."""
416
+ gates = contract.get("quality_gates")
417
+ if not isinstance(gates, dict):
418
+ return []
419
+
420
+ mode = gates.get("mode", "profile")
421
+ if mode == "explicit":
422
+ enabled = gates.get("enabled", [])
423
+ return [g for g in enabled if isinstance(g, str)]
424
+
425
+ profile = gates.get("profile") or contract.get("verification_profile") or "standard"
426
+ return list(PROFILE_DEFAULT_GATES.get(profile, PROFILE_DEFAULT_GATES["standard"]))
427
+
428
+
429
+ def required_gates_for_transition(transition: str, contract: dict) -> list[str]:
430
+ """Return non-baseline gates required before a transition."""
431
+ enabled = set(enabled_gates_for_contract(contract))
432
+
433
+ if transition == "start-execution":
434
+ required = ["requirements-review"]
435
+ if "architecture-review" in enabled:
436
+ required.append("architecture-review")
437
+ return required
438
+
439
+ if transition in ("full-task-complete", "child-review"):
440
+ required = ["code-review"]
441
+ if "architecture-review" in enabled:
442
+ required.append("architecture-review")
443
+ if "architecture-deep-review" in enabled:
444
+ required.append("architecture-deep-review")
445
+ return required
446
+
447
+ if transition == "parent-integrated":
448
+ return ["integration-review"]
449
+
450
+ return []
451
+
452
+
453
+ def compute_contract_fingerprint(
454
+ task_dir: Path,
455
+ task_data: dict,
456
+ contract: dict | None = None,
457
+ ) -> str:
458
+ """Compute a task-level fingerprint excluding generated result fields."""
459
+ payload = {
460
+ "schema_version": SCHEMA_VERSION,
461
+ "task_dir": task_dir.name,
462
+ "stable_task": _stable_task_data(task_data),
463
+ "strategy_contract": contract or {},
464
+ }
465
+ return _hash_payload(payload)
466
+
467
+
468
+ def compute_artifact_fingerprint(
469
+ task_dir: Path,
470
+ task_data: dict,
471
+ transition: str,
472
+ gate: str,
473
+ ) -> str:
474
+ """Compute a transition/gate-scoped artifact fingerprint."""
475
+ files = _artifact_files_for(transition, gate)
476
+ payload = {
477
+ "schema_version": SCHEMA_VERSION,
478
+ "transition": transition,
479
+ "gate": gate,
480
+ "task_dir": task_dir.name,
481
+ "stable_task": _stable_task_data(task_data),
482
+ "parent_contract": _parent_contract_metadata(
483
+ task_dir,
484
+ task_data,
485
+ transition,
486
+ ),
487
+ "reviewed_change_set": _reviewed_change_set_metadata(
488
+ task_dir,
489
+ transition,
490
+ ),
491
+ "files": _read_artifact_files(task_dir, files),
492
+ }
493
+ return _hash_payload(payload)
494
+
495
+
496
+ def make_baseline_record(
497
+ task_dir: Path,
498
+ task_data: dict,
499
+ transition: str,
500
+ contract: dict | None = None,
501
+ evidence: str = "task.json",
502
+ ) -> dict:
503
+ """Build a CLI-owned baseline-check PASS record."""
504
+ contract_fingerprint = compute_contract_fingerprint(task_dir, task_data, contract)
505
+ artifact_fingerprint = compute_artifact_fingerprint(
506
+ task_dir, task_data, transition, BASELINE_GATE
507
+ )
508
+ return {
509
+ "schema_version": SCHEMA_VERSION,
510
+ "transition": transition,
511
+ "gate": BASELINE_GATE,
512
+ "result": "PASS",
513
+ "reviewer": "pactile-cli",
514
+ "evidence": evidence,
515
+ "checked_at": utc_now(),
516
+ "contract_fingerprint": contract_fingerprint,
517
+ "artifact_fingerprint": artifact_fingerprint,
518
+ "issue_fingerprint": None,
519
+ "consecutive_failures": 0,
520
+ "approved_skip": None,
521
+ }
522
+
523
+
524
+ def write_gate_record(task_data: dict, transition: str, gate: str, record: dict) -> None:
525
+ """Write a gate record into task_data in-place.
526
+
527
+ This mutates memory only. Stage 2 persists `quality_gate_results` as a
528
+ Kernel Command projection (same command as `record-gate` / start / archive).
529
+ Do not treat this helper as an independent `task.json` writer.
530
+ """
531
+ qgr = task_data.get("quality_gate_results")
532
+ if not isinstance(qgr, dict):
533
+ qgr = {}
534
+ task_data["quality_gate_results"] = qgr
535
+
536
+ qgr["schema_version"] = SCHEMA_VERSION
537
+ qgr["contract_fingerprint"] = record.get("contract_fingerprint")
538
+ qgr["artifact_fingerprint"] = record.get("artifact_fingerprint")
539
+ transitions = qgr.get("transitions")
540
+ if not isinstance(transitions, dict):
541
+ transitions = {}
542
+ qgr["transitions"] = transitions
543
+
544
+ transition_records = transitions.get(transition)
545
+ if not isinstance(transition_records, dict):
546
+ transition_records = {}
547
+ transitions[transition] = transition_records
548
+ transition_records[gate] = record
549
+
550
+
551
+ KERNEL_PROJECTION_EXTRA_KEYS = (
552
+ "quality_gate_results",
553
+ "execution_approval",
554
+ "required_controls",
555
+ "ac_evidence_ledger",
556
+ "independent_check",
557
+ "notes_projection",
558
+ "topology",
559
+ "dependency_graph",
560
+ "ondemand_modules",
561
+ "baseline_modules",
562
+ "depends_on",
563
+ "dependency_satisfied",
564
+ "decompose_proposal",
565
+ "profile_health",
566
+ "event_bridge",
567
+ "middleware_providers",
568
+ "capability_router",
569
+ "required_capabilities",
570
+ )
571
+
572
+
573
+ def collect_kernel_projection_extras(task_data: dict) -> dict:
574
+ """Non-canonical task.json fields owned by the same Kernel Command."""
575
+ extras: dict = {}
576
+ for key in KERNEL_PROJECTION_EXTRA_KEYS:
577
+ if key in task_data:
578
+ extras[key] = task_data[key]
579
+ return extras
580
+
581
+
582
+ NOTES_PROJECTION_SCHEMA_VERSION = 1
583
+ NOTES_PROJECTION_MAX_TOKEN_BUDGET = 150
584
+ NOTES_PROJECTION_SOURCE_LEDGER = "ac_evidence_ledger"
585
+ NOTES_PROJECTION_SOURCE_INDEPENDENT_CHECK = "independent_check"
586
+ NOTES_PROJECTION_SOURCE_VERIFY = "verify.md"
587
+ # Same high|medium|low labels as retrieval_evidence.TRUST_SCORE. Provenance
588
+ # floor only — never a claim that the point text is factually correct.
589
+ NOTES_PROJECTION_CONFIDENCE_HIGH = "high"
590
+ NOTES_PROJECTION_CONFIDENCE_MEDIUM = "medium"
591
+ NOTES_PROJECTION_CONFIDENCE_LOW = "low"
592
+
593
+
594
+ def _ledger_evidence_points(task_data: dict) -> list[str]:
595
+ """Best-effort key points from ``ac_evidence_ledger.items[].evidence_ref``."""
596
+ ledger = task_data.get("ac_evidence_ledger")
597
+ if not isinstance(ledger, dict):
598
+ return []
599
+ items = ledger.get("items")
600
+ if not isinstance(items, list):
601
+ return []
602
+ points: list[str] = []
603
+ for item in items:
604
+ if not isinstance(item, dict):
605
+ continue
606
+ evidence = item.get("evidence_ref")
607
+ if isinstance(evidence, str) and evidence.strip():
608
+ points.append(evidence.strip())
609
+ return points
610
+
611
+
612
+ def _independent_check_note(task_data: dict) -> str:
613
+ check = task_data.get("independent_check")
614
+ if not isinstance(check, dict):
615
+ return ""
616
+ result = check.get("result")
617
+ mode = check.get("mode")
618
+ evidence = check.get("evidence")
619
+ parts = []
620
+ if isinstance(result, str) and result.strip():
621
+ parts.append(f"independent check {result}")
622
+ if isinstance(mode, str) and mode.strip():
623
+ parts.append(f"mode={mode}")
624
+ if isinstance(evidence, str) and evidence.strip():
625
+ parts.append(evidence.strip())
626
+ return "; ".join(parts)
627
+
628
+
629
+ def _verify_md_note(task_dir: Path) -> str:
630
+ """Extract archive-evidence lines from verify.md for the notes summary."""
631
+ verify_path = task_dir / "verify.md"
632
+ if not verify_path.is_file():
633
+ return ""
634
+ try:
635
+ content = verify_path.read_text(encoding="utf-8")
636
+ except OSError:
637
+ return ""
638
+ sections: list[str] = []
639
+ for pattern in (VALIDATION_EVIDENCE_RE, ACCEPTANCE_EVIDENCE_RE):
640
+ for match in pattern.finditer(content):
641
+ if match.lastindex and match.lastindex >= 1:
642
+ value = match.group(1).strip()
643
+ if _is_substantive_evidence(value):
644
+ sections.append(value)
645
+ break
646
+ for match in DURABLE_LEARNING_EVIDENCE_RE.finditer(content):
647
+ if match.lastindex and match.lastindex >= 1:
648
+ value = match.group(1).strip()
649
+ if _is_substantive_evidence(value) and not NO_DURABLE_LEARNING_RE.search(value):
650
+ sections.append(value)
651
+ break
652
+ return "; ".join(sections)
653
+
654
+
655
+ def _independent_check_passed(task_data: dict) -> bool:
656
+ check = task_data.get("independent_check")
657
+ if not isinstance(check, dict):
658
+ return False
659
+ result = check.get("result")
660
+ return isinstance(result, str) and result.strip().upper() == "PASS"
661
+
662
+
663
+ def _has_user_approval_signal(task_data: dict) -> bool:
664
+ """True when start-execution approval or a user-approved gate skip is present.
665
+
666
+ Presence only; does not judge whether the approval is still fresh.
667
+ """
668
+ approval = task_data.get("execution_approval")
669
+ if isinstance(approval, dict) and approval:
670
+ return True
671
+ qgr = task_data.get("quality_gate_results")
672
+ if not isinstance(qgr, dict):
673
+ return False
674
+ transitions = qgr.get("transitions")
675
+ if not isinstance(transitions, dict):
676
+ return False
677
+ for transition_records in transitions.values():
678
+ if not isinstance(transition_records, dict):
679
+ continue
680
+ for record in transition_records.values():
681
+ if not isinstance(record, dict):
682
+ continue
683
+ if record.get("approved_by") == "user":
684
+ return True
685
+ approved_skip = record.get("approved_skip")
686
+ if isinstance(approved_skip, dict) and approved_skip.get("approved_by") == "user":
687
+ return True
688
+ return False
689
+
690
+
691
+ def _notes_point_confidence(
692
+ source: str,
693
+ *,
694
+ independent_check_pass: bool,
695
+ has_user_approval: bool,
696
+ ) -> str:
697
+ """Provenance floor for one notes point. Missing signals stay ``low``.
698
+
699
+ Independent check PASS raises that note (and, with user approval, other
700
+ notes) to ``high``. A non-empty ledger ``evidence_ref`` floors at
701
+ ``medium`` and is not auto-raised to ``high`` without the PASS+approval
702
+ pair. User approval alone floors at ``medium``. Never fails the caller.
703
+ """
704
+ if source == NOTES_PROJECTION_SOURCE_INDEPENDENT_CHECK and independent_check_pass:
705
+ return NOTES_PROJECTION_CONFIDENCE_HIGH
706
+ if has_user_approval and independent_check_pass:
707
+ return NOTES_PROJECTION_CONFIDENCE_HIGH
708
+ if source == NOTES_PROJECTION_SOURCE_LEDGER:
709
+ return NOTES_PROJECTION_CONFIDENCE_MEDIUM
710
+ if has_user_approval:
711
+ return NOTES_PROJECTION_CONFIDENCE_MEDIUM
712
+ return NOTES_PROJECTION_CONFIDENCE_LOW
713
+
714
+
715
+ def _notes_projection_point(
716
+ text: str,
717
+ source: str,
718
+ *,
719
+ independent_check_pass: bool,
720
+ has_user_approval: bool,
721
+ ) -> dict:
722
+ return {
723
+ "text": text,
724
+ "source": source,
725
+ "confidence": _notes_point_confidence(
726
+ source,
727
+ independent_check_pass=independent_check_pass,
728
+ has_user_approval=has_user_approval,
729
+ ),
730
+ }
731
+
732
+
733
+ def _collect_notes_projection_points(task_dir: Path, task_data: dict) -> list[dict]:
734
+ independent_check_pass = _independent_check_passed(task_data)
735
+ has_user_approval = _has_user_approval_signal(task_data)
736
+ points: list[dict] = []
737
+ for text in _ledger_evidence_points(task_data):
738
+ points.append(
739
+ _notes_projection_point(
740
+ text,
741
+ NOTES_PROJECTION_SOURCE_LEDGER,
742
+ independent_check_pass=independent_check_pass,
743
+ has_user_approval=has_user_approval,
744
+ )
745
+ )
746
+ independent_note = _independent_check_note(task_data)
747
+ if independent_note:
748
+ points.append(
749
+ _notes_projection_point(
750
+ independent_note,
751
+ NOTES_PROJECTION_SOURCE_INDEPENDENT_CHECK,
752
+ independent_check_pass=independent_check_pass,
753
+ has_user_approval=has_user_approval,
754
+ )
755
+ )
756
+ verify_note = _verify_md_note(task_dir)
757
+ if verify_note:
758
+ points.append(
759
+ _notes_projection_point(
760
+ verify_note,
761
+ NOTES_PROJECTION_SOURCE_VERIFY,
762
+ independent_check_pass=independent_check_pass,
763
+ has_user_approval=has_user_approval,
764
+ )
765
+ )
766
+ return points
767
+
768
+
769
+ def build_notes_projection(
770
+ task_dir: Path,
771
+ task_data: dict,
772
+ archived_rel: str = "",
773
+ ) -> dict:
774
+ """Build the ``notes_projection`` slot for kernel.json extras.
775
+
776
+ Summarizes re-usable key points from ``ac_evidence_ledger`` /
777
+ ``independent_check`` / ``verify.md`` (best-effort) and points at the
778
+ archived task artifacts. Each point carries ``source`` and a provenance
779
+ ``confidence`` (high|medium|low). Missing key points never blocks:
780
+ ``points`` is ``[]``, summary is left empty, and pointers still filled.
781
+ Low confidence never fails archive.
782
+ """
783
+ from .session_memory import JOURNAL_SNIPPET_MAX_TOKENS, truncate_to_token_budget
784
+
785
+ points = _collect_notes_projection_points(task_dir, task_data)
786
+
787
+ summary = truncate_to_token_budget(
788
+ "; ".join(point["text"] for point in points),
789
+ max_tokens=JOURNAL_SNIPPET_MAX_TOKENS,
790
+ )
791
+
792
+ pointers: list[str] = []
793
+ if archived_rel:
794
+ pointers.append(archived_rel)
795
+ if (task_dir / "verify.md").is_file():
796
+ pointers.append(f"{archived_rel}/verify.md")
797
+ elif task_dir.is_dir():
798
+ pointers.append(str(task_dir))
799
+
800
+ return {
801
+ "schema_version": NOTES_PROJECTION_SCHEMA_VERSION,
802
+ "summary": summary,
803
+ "pointers": pointers,
804
+ "source": "cmd_archive",
805
+ "points": points,
806
+ }
807
+
808
+
809
+ def build_reviewer_gate_record(
810
+ task_dir: Path,
811
+ task_data: dict,
812
+ transition: str,
813
+ gate: str,
814
+ result: str,
815
+ reviewer: str,
816
+ evidence: str,
817
+ issue_fingerprint: str | None = None,
818
+ issue_summary: str | None = None,
819
+ root_cause: str | None = None,
820
+ skip_approved_by: str | None = None,
821
+ skip_reason: str | None = None,
822
+ contract_fingerprint: str | None = None,
823
+ artifact_fingerprint: str | None = None,
824
+ ) -> tuple[dict | None, list[str], list[str]]:
825
+ """Validate and build a non-baseline reviewer gate record."""
826
+ normalized_result = normalize_result(result)
827
+ errors = validate_reviewer_gate_input(
828
+ transition=transition,
829
+ gate=gate,
830
+ result=normalized_result,
831
+ reviewer=reviewer,
832
+ evidence=evidence,
833
+ issue_fingerprint=issue_fingerprint,
834
+ issue_summary=issue_summary,
835
+ root_cause=root_cause,
836
+ skip_approved_by=skip_approved_by,
837
+ skip_reason=skip_reason,
838
+ )
839
+ warnings: list[str] = []
840
+ if errors:
841
+ return None, errors, warnings
842
+
843
+ if normalized_result in ("PASS", "SKIPPED"):
844
+ errors.extend(
845
+ validate_transition_readiness(
846
+ task_dir,
847
+ task_data,
848
+ transition,
849
+ gate=gate,
850
+ mode="record",
851
+ )
852
+ )
853
+ if errors:
854
+ return None, errors, warnings
855
+
856
+ contract, contract_errors = resolve_strategy_contract_for_gates(task_dir, task_data)
857
+ if contract_errors:
858
+ errors.extend(contract_errors)
859
+ return None, errors, warnings
860
+
861
+ current_contract_fingerprint = compute_contract_fingerprint(
862
+ task_dir, task_data, contract
863
+ )
864
+ current_artifact_fingerprint = compute_artifact_fingerprint(
865
+ task_dir, task_data, transition, gate
866
+ )
867
+ if contract_fingerprint and contract_fingerprint != current_contract_fingerprint:
868
+ errors.append("provided contract fingerprint does not match current task artifacts")
869
+ if artifact_fingerprint and artifact_fingerprint != current_artifact_fingerprint:
870
+ errors.append("provided artifact fingerprint does not match current task artifacts")
871
+ if errors:
872
+ return None, errors, warnings
873
+
874
+ previous = _get_gate_record(task_data, transition, gate)
875
+ consecutive_failures = 0
876
+ required_user_choice = None
877
+ if normalized_result == "FAIL":
878
+ consecutive_failures = _next_consecutive_failures(previous, issue_fingerprint)
879
+ if consecutive_failures > 3:
880
+ required_user_choice = {
881
+ "required": True,
882
+ "reason": "same gate and issue fingerprint failed more than three times",
883
+ "options": [
884
+ "re-plan",
885
+ "continue-fixing",
886
+ "user-approved-skip-if-allowed",
887
+ ],
888
+ }
889
+ warnings.append(
890
+ "same gate and issue fingerprint has failed more than three times; ask the user to choose re-plan, continue fixing, or user-approved skip if allowed"
891
+ )
892
+
893
+ approved_skip = None
894
+ if normalized_result == "SKIPPED":
895
+ approved_skip = {
896
+ "approved_by": skip_approved_by,
897
+ "reason": skip_reason,
898
+ "approved_at": utc_now(),
899
+ }
900
+
901
+ record = {
902
+ "schema_version": SCHEMA_VERSION,
903
+ "transition": transition,
904
+ "gate": gate,
905
+ "result": normalized_result,
906
+ "reviewer": reviewer,
907
+ "evidence": evidence,
908
+ "checked_at": utc_now(),
909
+ "contract_fingerprint": current_contract_fingerprint,
910
+ "artifact_fingerprint": current_artifact_fingerprint,
911
+ "issue_fingerprint": issue_fingerprint if normalized_result == "FAIL" else None,
912
+ "root_cause": root_cause if normalized_result == "FAIL" else None,
913
+ "route": _route_for_fail_root_cause(root_cause) if normalized_result == "FAIL" else None,
914
+ "consecutive_failures": consecutive_failures,
915
+ "required_user_choice": required_user_choice,
916
+ "approved_skip": approved_skip,
917
+ }
918
+ if issue_summary and normalized_result == "FAIL":
919
+ record["issue_summary"] = issue_summary
920
+ return record, [], warnings
921
+
922
+
923
+ def validate_start_execution(
924
+ task_dir: Path,
925
+ task_data: dict | None,
926
+ approved: bool,
927
+ *,
928
+ ignore_deps: bool = False,
929
+ enforce_deps_block: bool = False,
930
+ ) -> GateGuardResult:
931
+ """Validate start-execution readiness.
932
+
933
+ Dependency policy (Plan B, per adjudication 05 §3.1/§3.2):
934
+ - enforce_deps_block=True is passed ONLY by the mutating `--approved` path.
935
+ mode=block + unmet deps then fail unless ignore_deps=True.
936
+ - mode=off silences dependency warnings entirely.
937
+ - every other path (--check, warn mode) keeps Plan A semantics: deps are
938
+ warnings that never fail the guard.
939
+ """
940
+ errors: list[str] = []
941
+ if task_data is None:
942
+ return GateGuardResult(ok=False, errors=["task.json"])
943
+
944
+ errors.extend(_required_lifecycle_slot_errors(task_dir, task_data))
945
+
946
+ status = task_data.get("status")
947
+ if status not in ("planning", "in_progress"):
948
+ errors.append(f"task status must be planning or in_progress, got {status!r}")
949
+
950
+ errors.extend(_required_file_errors(task_dir, ["prd.md"]))
951
+
952
+ has_design = (task_dir / "design.md").is_file()
953
+ has_implement = (task_dir / "implement.md").is_file()
954
+ if has_design != has_implement:
955
+ errors.append("design.md and implement.md must be present together for Full Tasks")
956
+
957
+ full_task = is_full_task(task_dir, task_data)
958
+ contract: dict = {}
959
+ required_gates: list[str] = []
960
+ if full_task:
961
+ contract, contract_errors = read_strategy_contract(task_dir)
962
+ errors.extend(contract_errors)
963
+ if not contract_errors:
964
+ required_gates = required_gates_for_transition("start-execution", contract)
965
+
966
+ contract_fingerprint = compute_contract_fingerprint(task_dir, task_data, contract)
967
+ artifact_fingerprints = {
968
+ BASELINE_GATE: compute_artifact_fingerprint(
969
+ task_dir, task_data, "start-execution", BASELINE_GATE
970
+ )
971
+ }
972
+
973
+ auto_gate_records: dict[str, dict] = {}
974
+ for gate in required_gates:
975
+ artifact_fingerprints[gate] = compute_artifact_fingerprint(
976
+ task_dir, task_data, "start-execution", gate
977
+ )
978
+ gate_errors = _validate_gate_record_for_transition(
979
+ task_data=task_data,
980
+ transition="start-execution",
981
+ gate=gate,
982
+ contract_fingerprint=contract_fingerprint,
983
+ artifact_fingerprint=artifact_fingerprints[gate],
984
+ )
985
+ if gate_errors and gate in START_EXECUTION_AUTO_GATES and _only_missing_gate_record(
986
+ gate_errors
987
+ ):
988
+ readiness_errors = _start_execution_planning_gate_readiness_errors(
989
+ task_dir,
990
+ gate,
991
+ full_task=full_task,
992
+ contract=contract,
993
+ )
994
+ if readiness_errors:
995
+ errors.extend(readiness_errors)
996
+ else:
997
+ auto_gate_records[gate] = make_planning_review_gate_record(
998
+ task_dir=task_dir,
999
+ task_data=task_data,
1000
+ transition="start-execution",
1001
+ gate=gate,
1002
+ contract=contract,
1003
+ contract_fingerprint=contract_fingerprint,
1004
+ artifact_fingerprint=artifact_fingerprints[gate],
1005
+ )
1006
+ else:
1007
+ errors.extend(gate_errors)
1008
+
1009
+ if approved:
1010
+ errors.extend(
1011
+ _validate_existing_execution_approval(
1012
+ task_data=task_data,
1013
+ contract_fingerprint=contract_fingerprint,
1014
+ artifact_fingerprint=artifact_fingerprints[BASELINE_GATE],
1015
+ )
1016
+ )
1017
+
1018
+ if not approved:
1019
+ errors.append("--approved is required for mutation")
1020
+
1021
+ warnings: list[str] = []
1022
+ deps_blocking_summary: list[str] = []
1023
+ if task_data is not None:
1024
+ scope = scope_children_for_task(task_dir, task_data)
1025
+ report = describe_dependencies(
1026
+ task_dir,
1027
+ task_data,
1028
+ scope_children=scope,
1029
+ )
1030
+ depends_mode = read_depends_mode(task_data)
1031
+ if enforce_deps_block and depends_mode == "block":
1032
+ deps_blocking_summary = report.blocking_errors()
1033
+ if ignore_deps:
1034
+ warnings.extend(report.warnings())
1035
+ else:
1036
+ errors.extend(deps_blocking_summary)
1037
+ elif depends_mode != "off":
1038
+ warnings.extend(report.warnings())
1039
+ if depends_mode == "block" and not enforce_deps_block:
1040
+ warnings.append(
1041
+ "dependency policy mode=block: --approved would fail unless --ignore-deps"
1042
+ )
1043
+
1044
+ baseline_record = None
1045
+ if not errors:
1046
+ baseline_record = make_baseline_record(
1047
+ task_dir, task_data, "start-execution", contract
1048
+ )
1049
+
1050
+ return GateGuardResult(
1051
+ ok=not errors,
1052
+ errors=errors,
1053
+ warnings=warnings,
1054
+ contract_fingerprint=contract_fingerprint,
1055
+ artifact_fingerprints=artifact_fingerprints,
1056
+ required_gates=required_gates,
1057
+ baseline_record=baseline_record,
1058
+ is_full_task=full_task,
1059
+ auto_gate_records=auto_gate_records,
1060
+ deps_blocking_summary=deps_blocking_summary,
1061
+ )
1062
+
1063
+
1064
+ def _validate_existing_execution_approval(
1065
+ task_data: dict,
1066
+ contract_fingerprint: str,
1067
+ artifact_fingerprint: str,
1068
+ ) -> list[str]:
1069
+ """Reject stale approval state before re-approving an in-progress task."""
1070
+ approval = task_data.get("execution_approval")
1071
+ if not isinstance(approval, dict):
1072
+ return []
1073
+
1074
+ errors: list[str] = []
1075
+ if approval.get("transition") != "start-execution":
1076
+ errors.append("stale execution approval: transition mismatch")
1077
+ if approval.get("contract_fingerprint") != contract_fingerprint:
1078
+ errors.append("stale execution approval: contract fingerprint changed")
1079
+ if approval.get("artifact_fingerprint") != artifact_fingerprint:
1080
+ errors.append("stale execution approval: artifact fingerprint changed")
1081
+ if errors:
1082
+ errors.append(
1083
+ "return to Planning, refresh gates, run start-execution --check, and ask for explicit approval again"
1084
+ )
1085
+ return errors
1086
+
1087
+
1088
+ def validate_start_execution_check(
1089
+ task_dir: Path,
1090
+ task_data: dict | None,
1091
+ ) -> GateGuardResult:
1092
+ """Validate start-execution preflight without requiring approval.
1093
+
1094
+ NOTE (adjudication 05 §3.1 trap): the check path deliberately passes
1095
+ enforce_deps_block=False — even when the task is in mode=block, --check
1096
+ must never fail on dependencies; it only surfaces warnings. Only the
1097
+ mutating --approved path may set enforce_deps_block=True.
1098
+ """
1099
+ return validate_start_execution(
1100
+ task_dir,
1101
+ task_data,
1102
+ approved=True,
1103
+ enforce_deps_block=False,
1104
+ )
1105
+
1106
+
1107
+ def validate_transition_readiness(
1108
+ task_dir: Path,
1109
+ task_data: dict | None,
1110
+ transition: str,
1111
+ *,
1112
+ gate: str | None = None,
1113
+ mode: str = "record",
1114
+ ) -> list[str]:
1115
+ """Validate transition evidence and, in complete mode, required gate records."""
1116
+ if task_data is None:
1117
+ return ["task.json"]
1118
+
1119
+ errors: list[str] = []
1120
+ if transition not in KNOWN_TRANSITIONS:
1121
+ errors.append(f"unknown transition: {transition}")
1122
+ return errors
1123
+
1124
+ profile = task_closeout_profile(task_dir, task_data)
1125
+
1126
+ if mode == "record":
1127
+ if not gate:
1128
+ errors.append("gate is required for record readiness checks")
1129
+ return errors
1130
+ required_signals = _evidence_requirements_for_gate(transition, gate)
1131
+ errors.extend(_evidence_errors_for_signals(task_dir, task_data, required_signals))
1132
+ return errors
1133
+
1134
+ if transition == "child-review":
1135
+ if profile != "full":
1136
+ return errors
1137
+ contract, contract_errors = read_strategy_contract(task_dir)
1138
+ errors.extend(contract_errors)
1139
+ if contract_errors:
1140
+ return errors
1141
+ return errors + _complete_transition_gate_errors(
1142
+ task_dir,
1143
+ task_data,
1144
+ transition,
1145
+ contract,
1146
+ required_gates_for_transition(transition, contract),
1147
+ )
1148
+
1149
+ if transition == "full-task-complete":
1150
+ if profile != "full":
1151
+ return errors
1152
+ contract, contract_errors = read_strategy_contract(task_dir)
1153
+ errors.extend(contract_errors)
1154
+ if contract_errors:
1155
+ return errors
1156
+ return errors + _complete_transition_gate_errors(
1157
+ task_dir,
1158
+ task_data,
1159
+ transition,
1160
+ contract,
1161
+ required_gates_for_transition(transition, contract),
1162
+ )
1163
+
1164
+ if transition == "parent-integrated":
1165
+ if profile != "parent":
1166
+ return errors
1167
+ child_names = task_data.get("children")
1168
+ if isinstance(child_names, list):
1169
+ errors.extend(
1170
+ validate_parent_children_complete(
1171
+ task_dir,
1172
+ [name for name in child_names if isinstance(name, str)],
1173
+ )
1174
+ )
1175
+ errors.extend(
1176
+ _evidence_errors_for_signals(task_dir, task_data, ["integration"])
1177
+ )
1178
+ contract, contract_errors = resolve_strategy_contract_for_gates(
1179
+ task_dir,
1180
+ task_data,
1181
+ )
1182
+ errors.extend(contract_errors)
1183
+ if contract_errors:
1184
+ return errors
1185
+ return errors + _complete_transition_gate_errors(
1186
+ task_dir,
1187
+ task_data,
1188
+ transition,
1189
+ contract,
1190
+ required_gates_for_transition(transition, contract),
1191
+ )
1192
+
1193
+ return errors
1194
+
1195
+
1196
+ def _complete_transition_gate_errors(
1197
+ task_dir: Path,
1198
+ task_data: dict,
1199
+ transition: str,
1200
+ contract: dict,
1201
+ required_gates: list[str],
1202
+ ) -> list[str]:
1203
+ errors: list[str] = []
1204
+ contract_fingerprint = compute_contract_fingerprint(task_dir, task_data, contract)
1205
+ for gate_name in required_gates:
1206
+ artifact_fingerprint = compute_artifact_fingerprint(
1207
+ task_dir, task_data, transition, gate_name
1208
+ )
1209
+ errors.extend(
1210
+ _validate_gate_record_for_transition(
1211
+ task_data=task_data,
1212
+ transition=transition,
1213
+ gate=gate_name,
1214
+ contract_fingerprint=contract_fingerprint,
1215
+ artifact_fingerprint=artifact_fingerprint,
1216
+ )
1217
+ )
1218
+ errors.extend(
1219
+ _evidence_errors_for_signals(
1220
+ task_dir,
1221
+ task_data,
1222
+ _evidence_requirements_for_gate(transition, gate_name),
1223
+ )
1224
+ )
1225
+ return errors
1226
+
1227
+
1228
+ def _evidence_requirements_for_gate(transition: str, gate: str) -> list[str]:
1229
+ if gate == "code-review" and transition in ("full-task-complete", "child-review"):
1230
+ return ["validation", "check_evidence", "reviewed_change_set"]
1231
+ if gate == "integration-review" and transition == "parent-integrated":
1232
+ return ["integration"]
1233
+ if gate in ("architecture-review", "architecture-deep-review") and transition in (
1234
+ "full-task-complete",
1235
+ "child-review",
1236
+ ):
1237
+ return ["validation", "check_evidence"]
1238
+ return []
1239
+
1240
+
1241
+ def _evidence_errors_for_signals(
1242
+ task_dir: Path,
1243
+ task_data: dict,
1244
+ required_signals: list[str],
1245
+ ) -> list[str]:
1246
+ status = verify_evidence_status(task_dir, task_data)
1247
+ label_map = {
1248
+ "validation": "verify.md missing substantive validation evidence",
1249
+ "check_evidence": "verify.md missing check evidence",
1250
+ "acceptance": "verify.md missing final acceptance evidence",
1251
+ "durable_learning": "verify.md missing durable-learning decision evidence",
1252
+ "integration": "verify.md missing final integration evidence",
1253
+ "reviewed_change_set": (
1254
+ "verify.md or handoff.md missing reviewed change-set evidence"
1255
+ ),
1256
+ }
1257
+ errors: list[str] = []
1258
+ for signal in required_signals:
1259
+ if not status.get(signal):
1260
+ errors.append(label_map.get(signal, f"missing evidence signal: {signal}"))
1261
+ return errors
1262
+
1263
+
1264
+ def verify_evidence_status(task_dir: Path, task_data: dict) -> dict[str, bool]:
1265
+ """Return substantive evidence signals from verify.md and handoff.md."""
1266
+ verify_path = task_dir / "verify.md"
1267
+ content = ""
1268
+ if verify_path.is_file():
1269
+ try:
1270
+ content = verify_path.read_text(encoding="utf-8")
1271
+ except OSError:
1272
+ content = ""
1273
+
1274
+ handoff_path = task_dir / "handoff.md"
1275
+ handoff_content = ""
1276
+ if handoff_path.is_file():
1277
+ try:
1278
+ handoff_content = handoff_path.read_text(encoding="utf-8")
1279
+ except OSError:
1280
+ handoff_content = ""
1281
+
1282
+ child_names = task_data.get("children")
1283
+ has_children = isinstance(child_names, list) and bool(child_names)
1284
+ return {
1285
+ "validation": _has_substantive_validation_evidence(content),
1286
+ "check_evidence": _has_substantive_check_evidence(content),
1287
+ "acceptance": _has_substantive_acceptance_evidence(content),
1288
+ "durable_learning": _has_durable_learning_evidence(content),
1289
+ "integration": (
1290
+ _has_substantive_integration_evidence(content) if has_children else True
1291
+ ),
1292
+ "reviewed_change_set": (
1293
+ _has_substantive_reviewed_change_set(content)
1294
+ or _has_substantive_reviewed_change_set(handoff_content)
1295
+ ),
1296
+ }
1297
+
1298
+
1299
+ def validate_archive(
1300
+ task_dir: Path,
1301
+ task_data: dict | None,
1302
+ ) -> GateGuardResult:
1303
+ """Validate archive readiness without mutating task files."""
1304
+ errors: list[str] = []
1305
+ if task_data is None:
1306
+ return GateGuardResult(ok=False, errors=["task.json"])
1307
+
1308
+ errors.extend(_required_lifecycle_slot_errors(task_dir, task_data))
1309
+ errors.extend(_required_file_errors(task_dir, ["verify.md"]))
1310
+ errors.extend(_verify_evidence_errors(task_dir, task_data))
1311
+ errors.extend(full_quality_archive_errors(task_dir, task_data))
1312
+
1313
+ parent_name = task_data.get("parent")
1314
+ if isinstance(parent_name, str) and parent_name:
1315
+ parent_dir = task_dir.parent / parent_name
1316
+ if not parent_dir.is_dir():
1317
+ errors.append(f"parent task not found: {parent_name}")
1318
+ else:
1319
+ errors.extend(validate_child_archive_state(parent_dir, task_dir.name))
1320
+ if get_child_state(parent_dir, task_dir.name) == "integrated":
1321
+ errors.extend(_required_file_errors(task_dir, ["handoff.md"]))
1322
+
1323
+ child_names = task_data.get("children")
1324
+ if isinstance(child_names, list) and child_names:
1325
+ errors.extend(
1326
+ validate_parent_children_complete(
1327
+ task_dir,
1328
+ [name for name in child_names if isinstance(name, str)],
1329
+ )
1330
+ )
1331
+
1332
+ profile = task_closeout_profile(task_dir, task_data)
1333
+ contract: dict = {}
1334
+ required_gates: list[str] = []
1335
+ artifact_fingerprints: dict[str, str] = {}
1336
+ baseline_record = None
1337
+
1338
+ if profile == "full":
1339
+ contract, contract_errors = read_strategy_contract(task_dir)
1340
+ errors.extend(contract_errors)
1341
+ if not contract_errors:
1342
+ required_gates = required_gates_for_transition(
1343
+ "full-task-complete", contract
1344
+ )
1345
+ errors.extend(
1346
+ validate_transition_readiness(
1347
+ task_dir,
1348
+ task_data,
1349
+ "full-task-complete",
1350
+ mode="complete",
1351
+ )
1352
+ )
1353
+ elif profile == "parent":
1354
+ contract, contract_errors = resolve_strategy_contract_for_gates(
1355
+ task_dir,
1356
+ task_data,
1357
+ )
1358
+ errors.extend(contract_errors)
1359
+ if not contract_errors:
1360
+ errors.extend(
1361
+ validate_transition_readiness(
1362
+ task_dir,
1363
+ task_data,
1364
+ "parent-integrated",
1365
+ mode="complete",
1366
+ )
1367
+ )
1368
+ required_gates = required_gates_for_transition("parent-integrated", contract)
1369
+
1370
+ contract_fingerprint = compute_contract_fingerprint(task_dir, task_data, contract)
1371
+
1372
+ if profile == "full" and not errors:
1373
+ artifact_fingerprints[BASELINE_GATE] = compute_artifact_fingerprint(
1374
+ task_dir, task_data, "full-task-complete", BASELINE_GATE
1375
+ )
1376
+ for gate in required_gates:
1377
+ artifact_fingerprints[gate] = compute_artifact_fingerprint(
1378
+ task_dir, task_data, "full-task-complete", gate
1379
+ )
1380
+ baseline_record = make_baseline_record(
1381
+ task_dir, task_data, "full-task-complete", contract
1382
+ )
1383
+
1384
+ return GateGuardResult(
1385
+ ok=not errors,
1386
+ errors=errors,
1387
+ contract_fingerprint=contract_fingerprint,
1388
+ artifact_fingerprints=artifact_fingerprints,
1389
+ required_gates=required_gates,
1390
+ baseline_record=baseline_record,
1391
+ is_full_task=profile == "full",
1392
+ closeout_profile=profile,
1393
+ )
1394
+
1395
+
1396
+ def _verify_evidence_errors(task_dir: Path, task_data: dict) -> list[str]:
1397
+ """Validate human-readable archive evidence in verify.md."""
1398
+ verify_path = task_dir / "verify.md"
1399
+ if not verify_path.is_file():
1400
+ return []
1401
+
1402
+ try:
1403
+ verify_path.read_text(encoding="utf-8")
1404
+ except OSError:
1405
+ return ["verify.md could not be read for archive evidence"]
1406
+
1407
+ errors: list[str] = []
1408
+ status = verify_evidence_status(task_dir, task_data)
1409
+ if not status["validation"]:
1410
+ errors.append("verify.md missing validation evidence")
1411
+ if not status["acceptance"]:
1412
+ errors.append("verify.md missing final acceptance evidence")
1413
+ # Durable-learning is a learning decision, not task evidence. For Lite
1414
+ # (one-off bugfix etc.) it is not a required slot: skip it so a Lite can
1415
+ # close without forcing an explicit learning disposition (allow N/A).
1416
+ # Full / Parent tasks still require a durable-learning decision.
1417
+ if task_closeout_profile(task_dir, task_data) != "lite" and not status["durable_learning"]:
1418
+ errors.append("verify.md missing durable-learning decision evidence")
1419
+ if not status["integration"]:
1420
+ errors.append("verify.md missing final integration evidence")
1421
+
1422
+ return errors
1423
+
1424
+
1425
+ def _is_substantive_evidence(value: str | None) -> bool:
1426
+ if not value or not isinstance(value, str):
1427
+ return False
1428
+ stripped = value.strip()
1429
+ if len(stripped) < 3:
1430
+ return False
1431
+ return not PLACEHOLDER_VALUES_RE.match(stripped)
1432
+
1433
+
1434
+ def _has_substantive_line_match(content: str, pattern: re.Pattern[str]) -> bool:
1435
+ if not content:
1436
+ return False
1437
+ for match in pattern.finditer(content):
1438
+ if match.lastindex and match.lastindex >= 1:
1439
+ if _is_substantive_evidence(match.group(1)):
1440
+ return True
1441
+ continue
1442
+ line = match.group(0)
1443
+ if ":" in line:
1444
+ value = line.split(":", 1)[1].strip()
1445
+ if _is_substantive_evidence(value):
1446
+ return True
1447
+ return False
1448
+
1449
+
1450
+ def _has_substantive_validation_evidence(content: str) -> bool:
1451
+ return _has_substantive_line_match(content, VALIDATION_EVIDENCE_RE)
1452
+
1453
+
1454
+ def _has_substantive_check_evidence(content: str) -> bool:
1455
+ return _has_substantive_line_match(content, CHECK_EVIDENCE_RE)
1456
+
1457
+
1458
+ def _has_substantive_acceptance_evidence(content: str) -> bool:
1459
+ return (
1460
+ _has_substantive_line_match(content, ACCEPTANCE_EVIDENCE_RE)
1461
+ or _has_substantive_line_match(content, ACCEPTED_BY_USER_RE)
1462
+ )
1463
+
1464
+
1465
+ def _has_substantive_integration_evidence(content: str) -> bool:
1466
+ return _has_substantive_line_match(content, INTEGRATION_EVIDENCE_RE)
1467
+
1468
+
1469
+ def _has_substantive_reviewed_change_set(content: str) -> bool:
1470
+ if not content:
1471
+ return False
1472
+ for match in REVIEWED_CHANGE_SET_RE.finditer(content):
1473
+ if _is_substantive_evidence(match.group(1)):
1474
+ return True
1475
+ return False
1476
+
1477
+
1478
+ def _has_validation_evidence(content: str) -> bool:
1479
+ return _has_substantive_validation_evidence(content)
1480
+
1481
+
1482
+ def _has_final_acceptance_evidence(content: str) -> bool:
1483
+ return _has_substantive_acceptance_evidence(content)
1484
+
1485
+
1486
+ def _has_durable_learning_evidence(content: str) -> bool:
1487
+ return bool(
1488
+ NO_DURABLE_LEARNING_RE.search(content)
1489
+ or _has_substantive_line_match(content, DURABLE_LEARNING_EVIDENCE_RE)
1490
+ )
1491
+
1492
+
1493
+ def durable_learning_decision_status(content: str) -> dict[str, bool]:
1494
+ """Return which durable-learning outcomes are signaled in verify.md text."""
1495
+ return {
1496
+ "no_durable_learning": bool(NO_DURABLE_LEARNING_RE.search(content)),
1497
+ "spec_update": bool(
1498
+ re.search(
1499
+ r"(?im)^\s*(?:[-*]\s*)?(?:spec\s+updates?|spec\s+update\s+"
1500
+ r"(?:needed|evidence)|updated\s+spec)\s*:\s*\S",
1501
+ content,
1502
+ )
1503
+ ),
1504
+ "learning_artifact": bool(
1505
+ re.search(
1506
+ r"(?im)^\s*(?:[-*]\s*)?(?:learning\s+artifact|retrospective(?:\.md)?)\s*:\s*\S",
1507
+ content,
1508
+ )
1509
+ or re.search(
1510
+ r"(?im)^\s*(?:[-*]\s*)?(?:durable\s+learning|learning\s+decision)\s*:\s*\S",
1511
+ content,
1512
+ )
1513
+ ),
1514
+ "any": _has_durable_learning_evidence(content),
1515
+ }
1516
+
1517
+
1518
+ def suggest_spec_targets(repo_root: Path, task_dir: Path, task_data: dict) -> list[str]:
1519
+ """Suggest existing spec paths from task scope; does not invent new rules."""
1520
+ suggestions: list[str] = []
1521
+ spec_root = repo_root / ".pactile" / "spec"
1522
+ if not spec_root.is_dir():
1523
+ return suggestions
1524
+
1525
+ package = task_data.get("package")
1526
+ if isinstance(package, str) and package.strip():
1527
+ pkg_index = spec_root / package.strip() / "index.md"
1528
+ if pkg_index.is_file():
1529
+ suggestions.append(f".pactile/spec/{package.strip()}/index.md")
1530
+
1531
+ scope = task_data.get("scope")
1532
+ if isinstance(scope, str) and scope.strip():
1533
+ scope_parts = [p for p in scope.strip().replace("\\", "/").split("/") if p]
1534
+ scope_path = spec_root.joinpath(*scope_parts) if scope_parts else spec_root
1535
+ if scope_path.is_dir():
1536
+ index = scope_path / "index.md"
1537
+ if index.is_file():
1538
+ suggestions.append(
1539
+ f".pactile/spec/{scope_path.relative_to(spec_root).as_posix()}/index.md"
1540
+ )
1541
+
1542
+ guide_paths = (
1543
+ "guides/durable-learning-decision-guide.md",
1544
+ "guides/index.md",
1545
+ )
1546
+ for rel in guide_paths:
1547
+ path = spec_root / rel
1548
+ if path.is_file():
1549
+ suggestions.append(f".pactile/spec/{rel}")
1550
+
1551
+ return _dedupe_preserve_order(suggestions)[:6]
1552
+
1553
+
1554
+ def build_spec_update_scaffold(
1555
+ repo_root: Path,
1556
+ task_dir: Path,
1557
+ task_data: dict,
1558
+ *,
1559
+ trigger: str | None = None,
1560
+ ) -> str:
1561
+ """Markdown checklist for spec capture; user/reviewer must confirm before editing specs."""
1562
+ targets = suggest_spec_targets(repo_root, task_dir, task_data)
1563
+ rel_task = f".pactile/tasks/{task_dir.name}"
1564
+ lines = [
1565
+ "## Spec update scaffold (reviewer-confirmed)",
1566
+ "",
1567
+ "_Suggestions only — do not treat this block as project policy until a human confirms._",
1568
+ "",
1569
+ ]
1570
+ if trigger:
1571
+ lines.extend([f"Trigger: {trigger.strip()}", ""])
1572
+ lines.extend(
1573
+ [
1574
+ "1. Decide outcome in verify.md:",
1575
+ " - Routine: `Durable learning decision: no durable learning`",
1576
+ " - Reusable insight: `Spec update evidence: .pactile/spec/<path>` after edits",
1577
+ f" - Already documented: `Learning artifact: {rel_task}/handoff.md`",
1578
+ "",
1579
+ "2. Use `/pactile:update-spec` or `/pactile:break-loop` for depth; never auto-write specs.",
1580
+ "",
1581
+ ]
1582
+ )
1583
+ if targets:
1584
+ lines.append("3. Existing spec indexes to consider (from task scope):")
1585
+ for target in targets:
1586
+ lines.append(f" - `{target}`")
1587
+ lines.append("")
1588
+ else:
1589
+ lines.append(
1590
+ "3. Browse `.pactile/spec/<package-or-layer>/index.md` for the right code-spec file."
1591
+ )
1592
+ lines.append("")
1593
+ lines.append(
1594
+ "4. Re-run `python ./.pactile/scripts/task.py archive <task> --check` after verify.md is final."
1595
+ )
1596
+ lines.append("")
1597
+ return "\n".join(lines)
1598
+
1599
+
1600
+ def _learning_decision_draft_lines(task_dir: Path, task_data: dict) -> list[str]:
1601
+ """Default durable-learning block for prepare-archive-evidence."""
1602
+ repo_root = task_dir.parent.parent.parent
1603
+ targets = suggest_spec_targets(repo_root, task_dir, task_data)
1604
+ target_hint = targets[0] if targets else ".pactile/spec/<layer>/index.md"
1605
+ return [
1606
+ "Durable learning decision: no durable learning for this task scope.",
1607
+ "",
1608
+ "# Replace the line above with ONE of these before archive:",
1609
+ f"# Spec update evidence: {target_hint}",
1610
+ f"# Learning artifact: .pactile/tasks/{task_dir.name}/handoff.md",
1611
+ "# Spec update needed: (brief reason) — then run /pactile:update-spec and point Spec update evidence at the edited file",
1612
+ "",
1613
+ ]
1614
+
1615
+
1616
+ ARCHIVE_EVIDENCE_DRAFT_MARKER = "<!-- pactile:archive-evidence-draft -->"
1617
+
1618
+
1619
+ def _verify_evidence_status(task_dir: Path, task_data: dict) -> dict[str, bool]:
1620
+ """Return which archive evidence sections are present in verify.md."""
1621
+ status = verify_evidence_status(task_dir, task_data)
1622
+ return {
1623
+ "validation": status["validation"],
1624
+ "acceptance": status["acceptance"],
1625
+ "durable_learning": status["durable_learning"],
1626
+ "integration": status["integration"],
1627
+ }
1628
+
1629
+
1630
+ def archive_repair_hints(
1631
+ errors: list[str],
1632
+ task_dir: Path,
1633
+ task_data: dict,
1634
+ guard: GateGuardResult,
1635
+ ) -> list[str]:
1636
+ """Map archive validation errors to actionable next-step hints."""
1637
+ hints: list[str] = []
1638
+ task_ref = task_dir.name
1639
+ rel_task = f".pactile/tasks/{task_ref}"
1640
+
1641
+ for error in errors:
1642
+ if error == "verify.md":
1643
+ hints.append(
1644
+ f"Create verify.md, then run: python ./.pactile/scripts/task.py "
1645
+ f"prepare-archive-evidence {rel_task}"
1646
+ )
1647
+ continue
1648
+ if error == "verify.md missing validation evidence":
1649
+ hints.append(
1650
+ "Add a grep-friendly line such as "
1651
+ "'Validation commands: <command> — <outcome>' to verify.md, "
1652
+ f"or run: python ./.pactile/scripts/task.py prepare-archive-evidence {rel_task}"
1653
+ )
1654
+ continue
1655
+ if error == "verify.md missing final acceptance evidence":
1656
+ hints.append(
1657
+ "Add 'Final acceptance evidence: <criteria met>' or "
1658
+ "'Accepted by user: <who/when>' to verify.md, "
1659
+ f"or run prepare-archive-evidence {rel_task}"
1660
+ )
1661
+ continue
1662
+ if error == "verify.md missing durable-learning decision evidence":
1663
+ hints.append(
1664
+ "Durable learning decision (pick one grep-friendly line in verify.md): "
1665
+ "'Durable learning decision: no durable learning' for routine work; "
1666
+ "'Spec update evidence: .pactile/spec/<path>' after /pactile:update-spec; "
1667
+ "'Learning artifact: <path>' when handoff/retrospective already captures the insight. "
1668
+ f"Or run: python ./.pactile/scripts/task.py prepare-archive-evidence {rel_task}"
1669
+ )
1670
+ continue
1671
+ if error == "verify.md missing final integration evidence":
1672
+ hints.append(
1673
+ "Parent tasks need 'Final integration evidence: <child handoffs / task-map>'. "
1674
+ f"Run prepare-archive-evidence {rel_task} to draft a section from task-map.md"
1675
+ )
1676
+ continue
1677
+ if error.startswith("missing gate record:"):
1678
+ _rest = error.split(":", 1)[1].strip()
1679
+ _transition, gate = _rest.split("/", 1)
1680
+ transition = _transition.strip()
1681
+ if transition == "parent-integrated":
1682
+ hints.append(
1683
+ f"Parent archive needs record-gate for parent-integrated/{gate}: "
1684
+ f"python ./.pactile/scripts/task.py record-gate {rel_task} "
1685
+ f"--transition parent-integrated --gate {gate} --result PASS "
1686
+ f"--reviewer parent --evidence task-map.md"
1687
+ )
1688
+ elif transition == "child-review":
1689
+ hints.append(
1690
+ f"Full Child acceptance needs record-gate for child-review/{gate}: "
1691
+ f"python ./.pactile/scripts/task.py record-gate {rel_task} "
1692
+ f"--transition child-review --gate {gate} --result PASS "
1693
+ f"--reviewer parent --evidence verify.md"
1694
+ )
1695
+ else:
1696
+ hints.append(
1697
+ f"Record reviewer gate after explicit review (never auto-PASS): "
1698
+ f"python ./.pactile/scripts/task.py record-gate {rel_task} "
1699
+ f"--transition {transition} --gate {gate} --result PASS "
1700
+ f"--reviewer <reviewer-id> --evidence verify.md"
1701
+ )
1702
+ continue
1703
+ if error == "verify.md missing check evidence":
1704
+ hints.append(
1705
+ "Add a grep-friendly line such as "
1706
+ "'Check evidence: <pactile-check summary or manual review notes>' to verify.md"
1707
+ )
1708
+ continue
1709
+ if error == (
1710
+ "verify.md or handoff.md missing reviewed change-set evidence"
1711
+ ):
1712
+ hints.append(
1713
+ "Add 'Reviewed change-set: <git ref or diff summary>' to verify.md or handoff.md"
1714
+ )
1715
+ continue
1716
+ if error == "verify.md missing substantive validation evidence":
1717
+ hints.append(
1718
+ "Replace placeholder validation lines with substantive command/outcome text in verify.md"
1719
+ )
1720
+ continue
1721
+ if error.startswith("gate failed:"):
1722
+ hints.append(
1723
+ "Resolve the FAIL gate (fix, re-review, or user-approved SKIPPED) "
1724
+ "before archive."
1725
+ )
1726
+ continue
1727
+ if error.startswith("stale "):
1728
+ hints.append(
1729
+ "Artifacts changed since the gate was recorded. Re-run review and "
1730
+ "record-gate for full-task-complete, then archive --check again."
1731
+ )
1732
+ continue
1733
+ if "child" in error and "integrated or cancelled" in error:
1734
+ hints.append(
1735
+ "Advance each child in parent task-map to integrated or cancelled "
1736
+ "via integrate-child before parent archive."
1737
+ )
1738
+ continue
1739
+ if error == "handoff.md" or error.startswith("handoff.md"):
1740
+ hints.append(
1741
+ "Integrated children require handoff.md on the child task before archive."
1742
+ )
1743
+ continue
1744
+
1745
+ if not hints and not guard.ok:
1746
+ hints.append(
1747
+ f"Run: python ./.pactile/scripts/task.py prepare-archive-evidence {rel_task} "
1748
+ "then archive --check again."
1749
+ )
1750
+
1751
+ if guard.is_full_task and guard.required_gates:
1752
+ missing_reviewer = any(
1753
+ e.startswith("missing gate record: full-task-complete/")
1754
+ for e in errors
1755
+ )
1756
+ if missing_reviewer and not any("record-gate" in h for h in hints):
1757
+ for gate in guard.required_gates:
1758
+ hints.append(
1759
+ f"Full Task archive needs record-gate for full-task-complete/{gate} "
1760
+ "(reviewer action required)."
1761
+ )
1762
+
1763
+ return _dedupe_preserve_order(hints)
1764
+
1765
+
1766
+ def _dedupe_preserve_order(items: list[str]) -> list[str]:
1767
+ seen: set[str] = set()
1768
+ out: list[str] = []
1769
+ for item in items:
1770
+ if item in seen:
1771
+ continue
1772
+ seen.add(item)
1773
+ out.append(item)
1774
+ return out
1775
+
1776
+
1777
+ def build_archive_evidence_draft(
1778
+ task_dir: Path,
1779
+ task_data: dict,
1780
+ guard: GateGuardResult | None = None,
1781
+ ) -> str:
1782
+ """Build markdown sections that satisfy archive evidence regex checks."""
1783
+ if guard is None:
1784
+ guard = validate_archive(task_dir, task_data)
1785
+
1786
+ status = _verify_evidence_status(task_dir, task_data)
1787
+ child_names = task_data.get("children")
1788
+ has_children = isinstance(child_names, list) and bool(child_names)
1789
+ lines = [
1790
+ "",
1791
+ "## Archive evidence (draft)",
1792
+ "",
1793
+ ARCHIVE_EVIDENCE_DRAFT_MARKER,
1794
+ "",
1795
+ "_Auto-drafted by prepare-archive-evidence. Edit placeholders before archive._",
1796
+ "",
1797
+ ]
1798
+
1799
+ if not status["validation"]:
1800
+ lines.extend(
1801
+ [
1802
+ "Validation commands: (fill in commands and outcomes, e.g. pnpm test — pass)",
1803
+ "",
1804
+ ]
1805
+ )
1806
+ if not status["acceptance"]:
1807
+ lines.extend(
1808
+ [
1809
+ "Final acceptance evidence: (describe acceptance criteria met for this task)",
1810
+ "",
1811
+ ]
1812
+ )
1813
+ if not status["durable_learning"]:
1814
+ lines.extend(_learning_decision_draft_lines(task_dir, task_data))
1815
+ if has_children and not status["integration"]:
1816
+ summary = _integration_draft_summary(task_dir, child_names)
1817
+ lines.extend(
1818
+ [
1819
+ f"Final integration evidence: {summary}",
1820
+ "",
1821
+ ]
1822
+ )
1823
+
1824
+ if guard.is_full_task and guard.required_gates:
1825
+ rel_task = f".pactile/tasks/{task_dir.name}"
1826
+ lines.extend(
1827
+ [
1828
+ "## Completion gate preparation (not recorded)",
1829
+ "",
1830
+ "Reviewer gates are **not** recorded by this helper. After review, run:",
1831
+ "",
1832
+ ]
1833
+ )
1834
+ for gate in guard.required_gates:
1835
+ lines.append(
1836
+ f"- `python ./.pactile/scripts/task.py record-gate {rel_task} "
1837
+ f"--transition full-task-complete --gate {gate} --result PASS "
1838
+ f"--reviewer <reviewer-id> --evidence verify.md`"
1839
+ )
1840
+ lines.append("")
1841
+
1842
+ return "\n".join(lines).rstrip() + "\n"
1843
+
1844
+
1845
+ def _integration_draft_summary(task_dir: Path, child_names: list) -> str:
1846
+ """Summarize child integration states for a parent integration evidence line."""
1847
+ names = [n for n in child_names if isinstance(n, str)]
1848
+ if not names:
1849
+ return "parent children integrated per task-map.md"
1850
+
1851
+ data, _body = load_task_map(task_dir)
1852
+ states: list[str] = []
1853
+ if isinstance(data, dict):
1854
+ children = data.get("children")
1855
+ if isinstance(children, list):
1856
+ by_id = {
1857
+ entry.get("id"): entry.get("state")
1858
+ for entry in children
1859
+ if isinstance(entry, dict) and entry.get("id")
1860
+ }
1861
+ for name in names:
1862
+ state = by_id.get(name, "unknown")
1863
+ states.append(f"{name}={state}")
1864
+
1865
+ if states:
1866
+ return "children " + ", ".join(states) + " per task-map.md"
1867
+ return "all structural children terminal in task-map.md before parent archive"
1868
+
1869
+
1870
+ def prepare_archive_evidence(
1871
+ task_dir: Path,
1872
+ task_data: dict | None,
1873
+ *,
1874
+ dry_run: bool = False,
1875
+ ) -> tuple[bool, list[str]]:
1876
+ """
1877
+ Append missing archive evidence sections to verify.md without rewriting user text.
1878
+
1879
+ Returns (changed, messages).
1880
+ """
1881
+ messages: list[str] = []
1882
+ if task_data is None:
1883
+ return False, ["task.json missing or invalid"]
1884
+
1885
+ verify_path = task_dir / "verify.md"
1886
+ guard = validate_archive(task_dir, task_data)
1887
+
1888
+ if verify_path.is_file():
1889
+ try:
1890
+ existing = verify_path.read_text(encoding="utf-8")
1891
+ except OSError:
1892
+ return False, ["verify.md could not be read"]
1893
+ if ARCHIVE_EVIDENCE_DRAFT_MARKER in existing:
1894
+ messages.append(
1895
+ "verify.md already contains an archive evidence draft block; "
1896
+ "edit it in place instead of re-running prepare."
1897
+ )
1898
+ still_missing = _verify_evidence_errors(task_dir, task_data)
1899
+ if not still_missing:
1900
+ return False, messages
1901
+ messages.append(
1902
+ "Some evidence is still missing after the draft block; "
1903
+ "fill placeholders or add lines outside the draft section."
1904
+ )
1905
+ return False, messages
1906
+ content = existing
1907
+ else:
1908
+ content = "# Verification Evidence\n"
1909
+
1910
+ draft = build_archive_evidence_draft(task_dir, task_data, guard)
1911
+ status = _verify_evidence_status(task_dir, task_data)
1912
+ child_names = task_data.get("children")
1913
+ has_children = isinstance(child_names, list) and bool(child_names)
1914
+ needs_draft = (
1915
+ not status["validation"]
1916
+ or not status["acceptance"]
1917
+ or not status["durable_learning"]
1918
+ or (has_children and not status["integration"])
1919
+ or (guard.is_full_task and guard.required_gates)
1920
+ )
1921
+ if not needs_draft:
1922
+ messages.append("No missing archive evidence sections to draft.")
1923
+ return False, messages
1924
+
1925
+ new_content = content.rstrip() + "\n" + draft
1926
+ if dry_run:
1927
+ messages.append("Dry run: would append archive evidence draft to verify.md")
1928
+ return True, messages
1929
+
1930
+ try:
1931
+ verify_path.write_text(new_content, encoding="utf-8")
1932
+ except OSError as exc:
1933
+ return False, [f"failed to write verify.md: {exc}"]
1934
+
1935
+ messages.append("Appended archive evidence draft to verify.md")
1936
+ remaining = _verify_evidence_errors(task_dir, task_data)
1937
+ gate_errors = [
1938
+ e
1939
+ for e in validate_archive(task_dir, task_data).errors
1940
+ if e.startswith("missing gate record:")
1941
+ or e.startswith("gate failed:")
1942
+ or e.startswith("stale ")
1943
+ ]
1944
+ if remaining:
1945
+ messages.append(
1946
+ "Still missing after draft (edit placeholders): " + "; ".join(remaining)
1947
+ )
1948
+ if gate_errors:
1949
+ messages.append(
1950
+ "Completion gates still require explicit record-gate: "
1951
+ + "; ".join(gate_errors)
1952
+ )
1953
+ return True, messages
1954
+
1955
+
1956
+ def validate_reviewer_gate_input(
1957
+ transition: str,
1958
+ gate: str,
1959
+ result: str,
1960
+ reviewer: str,
1961
+ evidence: str,
1962
+ issue_fingerprint: str | None = None,
1963
+ issue_summary: str | None = None,
1964
+ root_cause: str | None = None,
1965
+ skip_approved_by: str | None = None,
1966
+ skip_reason: str | None = None,
1967
+ ) -> list[str]:
1968
+ """Validate CLI inputs for record-gate."""
1969
+ errors: list[str] = []
1970
+
1971
+ if transition not in KNOWN_TRANSITIONS:
1972
+ errors.append(f"unknown transition: {transition}")
1973
+ if gate == BASELINE_GATE:
1974
+ errors.append("baseline-check is CLI-owned and cannot be recorded manually")
1975
+ elif gate not in REVIEW_GATES:
1976
+ errors.append(f"unknown gate: {gate}")
1977
+ if result not in RESULTS:
1978
+ errors.append(f"invalid result: {result}")
1979
+ if not _is_short_token(reviewer) or not REVIEWER_RE.match(reviewer):
1980
+ errors.append("reviewer must be a short identifier")
1981
+ if not _is_short_text(evidence):
1982
+ errors.append("evidence must be a short reference, not a review body")
1983
+
1984
+ if result == "PASS":
1985
+ if issue_fingerprint:
1986
+ errors.append("--issue-fingerprint is only valid for FAIL")
1987
+ if root_cause:
1988
+ errors.append("--root-cause is only valid for FAIL")
1989
+ if skip_approved_by or skip_reason:
1990
+ errors.append("skip metadata is only valid for SKIPPED")
1991
+ elif result == "FAIL":
1992
+ if not _is_short_token(issue_fingerprint):
1993
+ errors.append("FAIL requires --issue-fingerprint")
1994
+ if root_cause not in FAIL_ROOT_CAUSES:
1995
+ errors.append(
1996
+ "FAIL requires --root-cause implementation-defect|contract-changing-defect|validation-environment-blocker"
1997
+ )
1998
+ if issue_summary and not _is_short_text(issue_summary, MAX_REASON):
1999
+ errors.append("--issue-summary must be short and single-line")
2000
+ if skip_approved_by or skip_reason:
2001
+ errors.append("skip metadata is only valid for SKIPPED")
2002
+ elif result == "SKIPPED":
2003
+ if issue_fingerprint or issue_summary or root_cause:
2004
+ errors.append("issue metadata is only valid for FAIL")
2005
+ if skip_approved_by != "user":
2006
+ errors.append("SKIPPED requires --skip-approved-by user")
2007
+ if not _is_short_text(skip_reason, MAX_REASON):
2008
+ errors.append("SKIPPED requires --skip-reason")
2009
+
2010
+ return errors
2011
+
2012
+
2013
+ def _route_for_fail_root_cause(root_cause: str | None) -> str | None:
2014
+ if root_cause is None:
2015
+ return None
2016
+ return FAIL_ROOT_CAUSES.get(root_cause)
2017
+
2018
+
2019
+ def _validate_quality_gates(
2020
+ gates: dict,
2021
+ verification_profile: str | None,
2022
+ ) -> list[str]:
2023
+ errors: list[str] = []
2024
+ mode = gates.get("mode")
2025
+ if mode not in ALLOWED_GATE_MODES:
2026
+ errors.append("quality_gates.mode must be profile or explicit")
2027
+
2028
+ profile = gates.get("profile")
2029
+ if profile and profile not in ALLOWED_VERIFICATION_PROFILES:
2030
+ errors.append(f"invalid quality_gates.profile: {profile}")
2031
+
2032
+ enabled = gates.get("enabled", [])
2033
+ disabled = gates.get("disabled", [])
2034
+ if enabled is None:
2035
+ enabled = []
2036
+ if disabled is None:
2037
+ disabled = []
2038
+
2039
+ if mode == "explicit":
2040
+ if not isinstance(enabled, list):
2041
+ errors.append("quality_gates.enabled must be a list in explicit mode")
2042
+ if not isinstance(disabled, list):
2043
+ errors.append("quality_gates.disabled must be a list in explicit mode")
2044
+
2045
+ all_configured = []
2046
+ if isinstance(enabled, list):
2047
+ all_configured.extend(enabled)
2048
+ if isinstance(disabled, list):
2049
+ all_configured.extend(disabled)
2050
+
2051
+ for gate in all_configured:
2052
+ if gate not in KNOWN_GATES:
2053
+ errors.append(f"unknown quality gate: {gate}")
2054
+
2055
+ if BASELINE_GATE in disabled:
2056
+ errors.append("baseline-check cannot be disabled")
2057
+
2058
+ active = set(enabled) if mode == "explicit" else set(
2059
+ PROFILE_DEFAULT_GATES.get(
2060
+ profile or verification_profile or "standard",
2061
+ PROFILE_DEFAULT_GATES["standard"],
2062
+ )
2063
+ )
2064
+ if "architecture-deep-review" in active and "architecture-review" not in active:
2065
+ errors.append("architecture-deep-review requires architecture-review")
2066
+
2067
+ return errors
2068
+
2069
+
2070
+ def _parse_inline_list(value: str) -> list[str] | str:
2071
+ value = value.strip()
2072
+ if value.startswith("[") and value.endswith("]"):
2073
+ inner = value[1:-1].strip()
2074
+ if not inner:
2075
+ return []
2076
+ return [
2077
+ item.strip().strip("'\"")
2078
+ for item in inner.split(",")
2079
+ if item.strip()
2080
+ ]
2081
+ return value
2082
+
2083
+
2084
+ def _hash_payload(payload: dict) -> str:
2085
+ body = json.dumps(payload, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
2086
+ digest = hashlib.sha256(body.encode("utf-8")).hexdigest()
2087
+ return f"sha256:{digest}"
2088
+
2089
+
2090
+ def _stable_task_data(task_data: dict) -> dict:
2091
+ stable: dict = {}
2092
+ for key in STABLE_TASK_KEYS:
2093
+ if key in task_data:
2094
+ stable[key] = task_data.get(key)
2095
+ return stable
2096
+
2097
+
2098
+ def _parent_contract_metadata(
2099
+ task_dir: Path,
2100
+ task_data: dict,
2101
+ transition: str,
2102
+ ) -> dict | None:
2103
+ if transition == "child-review":
2104
+ parent_name = task_data.get("parent")
2105
+ if not isinstance(parent_name, str) or not parent_name:
2106
+ return None
2107
+ parent_dir = task_dir.parent / parent_name
2108
+ data, _ = load_task_map(parent_dir)
2109
+ return _task_map_contract_metadata("child", parent_dir, data)
2110
+
2111
+ if transition.startswith("parent-"):
2112
+ data, _ = load_task_map(task_dir)
2113
+ return _task_map_contract_metadata("parent", task_dir, data)
2114
+
2115
+ return None
2116
+
2117
+
2118
+ def _task_map_contract_metadata(
2119
+ role: str,
2120
+ parent_dir: Path,
2121
+ task_map_data: dict | None,
2122
+ ) -> dict:
2123
+ return {
2124
+ "role": role,
2125
+ "parent": parent_dir.name,
2126
+ "task_map_found": task_map_data is not None,
2127
+ "contract_epoch": (
2128
+ task_map_data.get("contract_epoch")
2129
+ if isinstance(task_map_data, dict)
2130
+ else None
2131
+ ),
2132
+ }
2133
+
2134
+
2135
+ def _reviewed_change_set_metadata(
2136
+ task_dir: Path,
2137
+ transition: str,
2138
+ ) -> dict | None:
2139
+ evidence_files = _change_set_evidence_files_for(transition)
2140
+ if not evidence_files:
2141
+ return None
2142
+
2143
+ files = []
2144
+ for name in evidence_files:
2145
+ path = task_dir / name
2146
+ try:
2147
+ content = path.read_text(encoding="utf-8") if path.is_file() else ""
2148
+ except OSError:
2149
+ content = ""
2150
+ files.append(
2151
+ {
2152
+ "path": name,
2153
+ "present": path.is_file(),
2154
+ "entries": _extract_reviewed_change_set_entries(content),
2155
+ }
2156
+ )
2157
+ return {"files": files}
2158
+
2159
+
2160
+ def _change_set_evidence_files_for(transition: str) -> list[str]:
2161
+ if transition == "full-task-complete":
2162
+ return ["verify.md"]
2163
+ if transition == "child-review":
2164
+ return ["verify.md", "handoff.md"]
2165
+ if transition.startswith("parent-"):
2166
+ return ["task-map.md", "verify.md"]
2167
+ return []
2168
+
2169
+
2170
+ def _extract_reviewed_change_set_entries(content: str) -> list[str]:
2171
+ return [
2172
+ match.group(1).strip()
2173
+ for match in REVIEWED_CHANGE_SET_RE.finditer(content)
2174
+ ]
2175
+
2176
+
2177
+ def _only_missing_gate_record(errors: list[str]) -> bool:
2178
+ return bool(errors) and all(
2179
+ error.startswith("missing gate record:") for error in errors
2180
+ )
2181
+
2182
+
2183
+ def make_planning_review_gate_record(
2184
+ task_dir: Path,
2185
+ task_data: dict,
2186
+ transition: str,
2187
+ gate: str,
2188
+ contract: dict,
2189
+ contract_fingerprint: str,
2190
+ artifact_fingerprint: str,
2191
+ ) -> dict:
2192
+ """Build a CLI-owned PASS record when planning artifacts satisfy the gate."""
2193
+ _ = task_dir, task_data, contract
2194
+ evidence_by_gate = {
2195
+ "requirements-review": "prd.md",
2196
+ "architecture-review": "design.md+implement.md",
2197
+ }
2198
+ return {
2199
+ "schema_version": SCHEMA_VERSION,
2200
+ "transition": transition,
2201
+ "gate": gate,
2202
+ "result": "PASS",
2203
+ "reviewer": AUTO_PLANNING_REVIEWER,
2204
+ "evidence": evidence_by_gate.get(gate, "planning-artifacts"),
2205
+ "checked_at": utc_now(),
2206
+ "contract_fingerprint": contract_fingerprint,
2207
+ "artifact_fingerprint": artifact_fingerprint,
2208
+ "issue_fingerprint": None,
2209
+ "root_cause": None,
2210
+ "route": None,
2211
+ "consecutive_failures": 0,
2212
+ "required_user_choice": None,
2213
+ "approved_skip": None,
2214
+ "auto_recorded": True,
2215
+ "auto_record_reason": "planning-artifacts",
2216
+ }
2217
+
2218
+
2219
+ def _start_execution_planning_gate_readiness_errors(
2220
+ task_dir: Path,
2221
+ gate: str,
2222
+ *,
2223
+ full_task: bool,
2224
+ contract: dict,
2225
+ ) -> list[str]:
2226
+ if gate == "requirements-review":
2227
+ return _prd_requirements_review_errors(task_dir)
2228
+ if gate == "architecture-review":
2229
+ errors = _required_file_errors(task_dir, ["design.md", "implement.md"])
2230
+ if not full_task:
2231
+ errors.append(
2232
+ "architecture-review requires design.md and implement.md (Full Task)"
2233
+ )
2234
+ errors.extend(validate_strategy_contract(contract))
2235
+ return errors
2236
+ return [f"planning gate readiness check unsupported: {gate}"]
2237
+
2238
+
2239
+ def _prd_requirements_review_errors(task_dir: Path) -> list[str]:
2240
+ path = task_dir / "prd.md"
2241
+ if not path.is_file():
2242
+ return ["prd.md missing for requirements-review"]
2243
+ try:
2244
+ content = path.read_text(encoding="utf-8")
2245
+ except OSError:
2246
+ return ["prd.md could not be read for requirements-review"]
2247
+ if not content.strip():
2248
+ return ["prd.md is empty (requirements-review)"]
2249
+ if not PRD_ACCEPTANCE_CRITERIA_HEADING_RE.search(content):
2250
+ return [
2251
+ "prd.md missing Acceptance Criteria heading (complete PRD Grill before start-execution)"
2252
+ ]
2253
+ criteria: list[str] = []
2254
+ for match in PRD_ACCEPTANCE_ITEM_RE.finditer(content):
2255
+ text = match.group(1).strip()
2256
+ if text and not PRD_PLACEHOLDER_RE.match(text):
2257
+ criteria.append(text)
2258
+ if not criteria:
2259
+ return [
2260
+ "prd.md needs at least one non-placeholder Acceptance Criteria item (replace TBD checkboxes)"
2261
+ ]
2262
+ return []
2263
+
2264
+
2265
+ def start_execution_repair_hints(
2266
+ errors: list[str],
2267
+ task_dir: Path,
2268
+ ) -> list[str]:
2269
+ """Map start-execution validation errors to actionable planning hints."""
2270
+ hints: list[str] = []
2271
+ rel_task = f".pactile/tasks/{task_dir.name}"
2272
+ for error in errors:
2273
+ if error.startswith("missing gate record:"):
2274
+ _rest = error.split(":", 1)[1].strip()
2275
+ _transition, gate = _rest.split("/", 1)
2276
+ if gate in START_EXECUTION_AUTO_GATES:
2277
+ hints.append(
2278
+ "Complete planning artifacts so Pactile can auto-record this gate on "
2279
+ f"`start-execution --approved` (requirements: prd.md Acceptance Criteria; "
2280
+ "architecture: design.md + valid implement.md contract)."
2281
+ )
2282
+ else:
2283
+ hints.append(
2284
+ f"python ./.pactile/scripts/task.py record-gate {rel_task} "
2285
+ f"--transition start-execution --gate {gate} --result PASS "
2286
+ f"--reviewer <reviewer-id> --evidence verify.md"
2287
+ )
2288
+ continue
2289
+ if "Acceptance Criteria" in error or "TBD" in error:
2290
+ hints.append(
2291
+ "Finish PRD Grill in pactile-brainstorm: fill Goal, Acceptance Criteria "
2292
+ "(non-TBD checkboxes), then re-run start-execution --check."
2293
+ )
2294
+ continue
2295
+ if error.startswith("missing required lifecycle slot:"):
2296
+ hints.append(
2297
+ "Restore the required Baseline slot in baseline_modules.active "
2298
+ "(define-basic, approval-personal, execute-agent, verify-basic, "
2299
+ "close-basic). Missing required slots cannot fake-green."
2300
+ )
2301
+ continue
2302
+ if "Development Strategy Contract" in error or error.startswith("invalid "):
2303
+ hints.append(
2304
+ "Fix the Development Strategy Contract block in implement.md "
2305
+ "(execution_mode, isolation, verification_profile, quality_gates)."
2306
+ )
2307
+ continue
2308
+ if error == "design.md and implement.md must be present together for Full Tasks":
2309
+ hints.append(
2310
+ "Add both design.md and implement.md for Full Tasks, or stay PRD-only for Lite."
2311
+ )
2312
+ return _dedupe_preserve_order(hints)
2313
+
2314
+
2315
+ def _artifact_files_for(transition: str, gate: str) -> list[str]:
2316
+ if transition == "start-execution":
2317
+ return ["prd.md", "design.md", "implement.md"]
2318
+ if transition == "full-task-complete":
2319
+ return ["prd.md", "design.md", "implement.md", "verify.md"]
2320
+ if transition == "child-review":
2321
+ return ["prd.md", "design.md", "implement.md", "verify.md", "handoff.md"]
2322
+ if transition in ("parent-accepted", "parent-integrated"):
2323
+ return ["task-map.md", "verify.md"]
2324
+ _ = gate
2325
+ return ["prd.md", "verify.md"]
2326
+
2327
+
2328
+ def _read_artifact_files(task_dir: Path, names: list[str]) -> list[dict]:
2329
+ files = []
2330
+ for name in names:
2331
+ path = task_dir / name
2332
+ try:
2333
+ content = path.read_text(encoding="utf-8") if path.is_file() else None
2334
+ except OSError:
2335
+ content = None
2336
+ files.append({"path": name, "content": content})
2337
+ return files
2338
+
2339
+
2340
+ def _required_file_errors(task_dir: Path, names: list[str]) -> list[str]:
2341
+ errors = []
2342
+ for name in names:
2343
+ path = task_dir / name
2344
+ if not path.is_file():
2345
+ errors.append(name)
2346
+ continue
2347
+ try:
2348
+ if not path.read_text(encoding="utf-8").strip():
2349
+ errors.append(f"{name} is empty")
2350
+ except OSError:
2351
+ errors.append(f"{name} could not be read")
2352
+ return errors
2353
+
2354
+
2355
+ def baseline_active_ids(task_dir: Path, task_data: dict | None) -> list[str]:
2356
+ """Installed-and-active Baseline ids. Missing extras default to all eight."""
2357
+ block = None
2358
+ data = task_data if isinstance(task_data, dict) else {}
2359
+ if isinstance(data.get("baseline_modules"), dict):
2360
+ block = data["baseline_modules"]
2361
+ else:
2362
+ kernel_path = task_dir / "kernel.json"
2363
+ kernel: dict = {}
2364
+ if kernel_path.is_file():
2365
+ try:
2366
+ parsed = json.loads(kernel_path.read_text(encoding="utf-8"))
2367
+ except (OSError, json.JSONDecodeError):
2368
+ parsed = {}
2369
+ if isinstance(parsed, dict):
2370
+ kernel = parsed
2371
+ projection = (
2372
+ kernel.get("projection") if isinstance(kernel.get("projection"), dict) else {}
2373
+ )
2374
+ extras = projection.get("extras") if isinstance(projection.get("extras"), dict) else {}
2375
+ if isinstance(extras.get("baseline_modules"), dict):
2376
+ block = extras["baseline_modules"]
2377
+ if not isinstance(block, dict) or "active" not in block:
2378
+ return list(PROFILE_BASELINE_EIGHT)
2379
+ raw = block.get("active")
2380
+ if not isinstance(raw, list):
2381
+ return []
2382
+ allowed = set(PROFILE_BASELINE_EIGHT)
2383
+ out: list[str] = []
2384
+ for item in raw:
2385
+ if not isinstance(item, str):
2386
+ continue
2387
+ name = item.strip()
2388
+ if name in allowed and name not in out:
2389
+ out.append(name)
2390
+ return out
2391
+
2392
+
2393
+ def _required_lifecycle_slot_errors(task_dir: Path, task_data: dict | None) -> list[str]:
2394
+ active = set(baseline_active_ids(task_dir, task_data))
2395
+ return [
2396
+ f"missing required lifecycle slot: {slot}"
2397
+ for slot in REQUIRED_LIFECYCLE_SLOTS
2398
+ if slot not in active
2399
+ ]
2400
+
2401
+
2402
+ def _get_gate_record(task_data: dict, transition: str, gate: str) -> dict | None:
2403
+ qgr = task_data.get("quality_gate_results")
2404
+ if not isinstance(qgr, dict):
2405
+ return None
2406
+ transitions = qgr.get("transitions")
2407
+ if not isinstance(transitions, dict):
2408
+ return None
2409
+ transition_records = transitions.get(transition)
2410
+ if not isinstance(transition_records, dict):
2411
+ return None
2412
+ record = transition_records.get(gate)
2413
+ return record if isinstance(record, dict) else None
2414
+
2415
+
2416
+ def _validate_gate_record_for_transition(
2417
+ task_data: dict,
2418
+ transition: str,
2419
+ gate: str,
2420
+ contract_fingerprint: str,
2421
+ artifact_fingerprint: str,
2422
+ ) -> list[str]:
2423
+ errors: list[str] = []
2424
+ record = _get_gate_record(task_data, transition, gate)
2425
+ if record is None:
2426
+ errors.append(f"missing gate record: {transition}/{gate}")
2427
+ return errors
2428
+
2429
+ result = record.get("result")
2430
+ if result == "FAIL":
2431
+ errors.append(f"gate failed: {transition}/{gate}")
2432
+ return errors
2433
+ if result not in ("PASS", "SKIPPED"):
2434
+ errors.append(f"gate record has invalid result: {transition}/{gate}")
2435
+ return errors
2436
+ if record.get("contract_fingerprint") != contract_fingerprint:
2437
+ errors.append(f"stale contract fingerprint: {transition}/{gate}")
2438
+ if record.get("artifact_fingerprint") != artifact_fingerprint:
2439
+ errors.append(f"stale artifact fingerprint: {transition}/{gate}")
2440
+ if result == "SKIPPED":
2441
+ approved_skip = record.get("approved_skip")
2442
+ if not isinstance(approved_skip, dict):
2443
+ errors.append(f"SKIPPED gate lacks approval metadata: {transition}/{gate}")
2444
+ elif approved_skip.get("approved_by") != "user" or not approved_skip.get("reason"):
2445
+ errors.append(f"invalid SKIPPED approval metadata: {transition}/{gate}")
2446
+ return errors
2447
+
2448
+
2449
+ def _next_consecutive_failures(
2450
+ previous: dict | None,
2451
+ issue_fingerprint: str | None,
2452
+ ) -> int:
2453
+ if not previous or previous.get("result") != "FAIL":
2454
+ return 1
2455
+ if previous.get("issue_fingerprint") != issue_fingerprint:
2456
+ return 1
2457
+ previous_count = previous.get("consecutive_failures")
2458
+ return int(previous_count) + 1 if isinstance(previous_count, int) else 2
2459
+
2460
+
2461
+ def _is_short_token(value: str | None, max_len: int = MAX_SHORT_FIELD) -> bool:
2462
+ if not value or not isinstance(value, str):
2463
+ return False
2464
+ if len(value) > max_len:
2465
+ return False
2466
+ if any(ch.isspace() for ch in value):
2467
+ return False
2468
+ return True
2469
+
2470
+
2471
+ def _is_short_text(value: str | None, max_len: int = MAX_SHORT_FIELD) -> bool:
2472
+ if not value or not isinstance(value, str):
2473
+ return False
2474
+ if len(value) > max_len:
2475
+ return False
2476
+ if "\n" in value or "\r" in value:
2477
+ return False
2478
+ return True