@rosetears/aili-pi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (531) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/THIRD_PARTY_NOTICES.md +64 -0
  4. package/extensions/index.ts +6 -0
  5. package/install.sh +5 -0
  6. package/manifests/adapter-evidence.json +45 -0
  7. package/manifests/capabilities.json +124 -0
  8. package/manifests/live-verification.json +26 -0
  9. package/manifests/provenance.json +71 -0
  10. package/manifests/roles.json +474 -0
  11. package/manifests/sbom.json +7797 -0
  12. package/manifests/skill-compatibility.json +8100 -0
  13. package/manifests/subagent-provenance.json +37 -0
  14. package/package.json +91 -0
  15. package/prompts/build.md +8 -0
  16. package/prompts/define.md +8 -0
  17. package/prompts/ideate.md +8 -0
  18. package/prompts/local-review.md +8 -0
  19. package/prompts/ship.md +8 -0
  20. package/roles/agent-evaluator.md +47 -0
  21. package/roles/ai-regression-scout.md +47 -0
  22. package/roles/browser-qa-runner.md +47 -0
  23. package/roles/code-reviewer.md +47 -0
  24. package/roles/code-scout.md +47 -0
  25. package/roles/convergence-reviewer.md +55 -0
  26. package/roles/doc-researcher.md +47 -0
  27. package/roles/e2e-artifact-runner.md +47 -0
  28. package/roles/implementer.md +47 -0
  29. package/roles/opensource-sanitizer.md +47 -0
  30. package/roles/plan-auditor.md +47 -0
  31. package/roles/pr-test-analyzer.md +47 -0
  32. package/roles/security-auditor.md +47 -0
  33. package/roles/silent-failure-reviewer.md +47 -0
  34. package/roles/spec-miner.md +47 -0
  35. package/roles/test-coverage-reviewer.md +47 -0
  36. package/roles/test-engineer.md +48 -0
  37. package/roles/web-performance-auditor.md +47 -0
  38. package/roles/web-researcher.md +47 -0
  39. package/scripts/apply-adapter-evidence.ts +73 -0
  40. package/scripts/bootstrap.sh +180 -0
  41. package/scripts/generate-provenance.ts +143 -0
  42. package/scripts/local-package-e2e.ts +51 -0
  43. package/scripts/sync-roles.ts +213 -0
  44. package/scripts/sync-skills.ts +356 -0
  45. package/scripts/validate-runtime.ts +11 -0
  46. package/skills/academic-paper-review/SKILL.md +81 -0
  47. package/skills/agents-md-initialization/SKILL.md +121 -0
  48. package/skills/agents-md-initialization/references/agents-template.md +83 -0
  49. package/skills/agents-md-initialization/references/agents_md.py +215 -0
  50. package/skills/ai-regression-scout/SKILL.md +24 -0
  51. package/skills/aili-delivery-flow/SKILL.md +125 -0
  52. package/skills/aili-delivery-flow/references/artifact-contracts.md +138 -0
  53. package/skills/aili-delivery-flow/references/backend-routing.md +40 -0
  54. package/skills/aili-delivery-flow/references/build-execution-loop.md +113 -0
  55. package/skills/aili-delivery-flow/references/build-goal-mode.md +3 -0
  56. package/skills/aili-delivery-flow/references/direct-vs-delegated-work.md +38 -0
  57. package/skills/aili-delivery-flow/references/implementation-packages.md +54 -0
  58. package/skills/aili-delivery-flow/references/lifecycle.md +127 -0
  59. package/skills/aili-delivery-flow/references/protocols/acceptance-test-plan.md +18 -0
  60. package/skills/aili-delivery-flow/references/protocols/alignment-questionnaire.md +10 -0
  61. package/skills/aili-delivery-flow/references/protocols/closeout-report.md +101 -0
  62. package/skills/aili-delivery-flow/references/protocols/compact-evidence-pack.md +46 -0
  63. package/skills/aili-delivery-flow/references/protocols/idea-brief.md +10 -0
  64. package/skills/aili-delivery-flow/references/protocols/implementation-package.md +53 -0
  65. package/skills/aili-delivery-flow/references/protocols/research-evidence-pack.md +19 -0
  66. package/skills/aili-delivery-flow/references/protocols/review-report.md +7 -0
  67. package/skills/aili-delivery-flow/references/protocols/spec-draft.md +12 -0
  68. package/skills/aili-delivery-flow/references/protocols/subagent-result.md +60 -0
  69. package/skills/aili-delivery-flow/references/protocols/subagent-task-packet.md +29 -0
  70. package/skills/aili-delivery-flow/references/protocols/worktree-context.md +109 -0
  71. package/skills/aili-delivery-flow/references/questionnaire-policy.md +62 -0
  72. package/skills/aili-delivery-flow/references/review-repair-loop.md +16 -0
  73. package/skills/aili-delivery-flow/references/test-document-policy.md +35 -0
  74. package/skills/android-native-dev/SKILL.md +722 -0
  75. package/skills/android-native-dev/references/accessibility.md +209 -0
  76. package/skills/android-native-dev/references/adaptive-screens.md +231 -0
  77. package/skills/android-native-dev/references/design-style-guide.md +365 -0
  78. package/skills/android-native-dev/references/functional-requirements.md +229 -0
  79. package/skills/android-native-dev/references/motion-system.md +203 -0
  80. package/skills/android-native-dev/references/performance-stability.md +223 -0
  81. package/skills/android-native-dev/references/privacy-security.md +244 -0
  82. package/skills/android-native-dev/references/testing.md +554 -0
  83. package/skills/android-native-dev/references/visual-design.md +246 -0
  84. package/skills/api-and-interface-design/SKILL.md +318 -0
  85. package/skills/browser-qa/SKILL.md +32 -0
  86. package/skills/browser-testing-with-devtools/SKILL.md +323 -0
  87. package/skills/build-failure-repair/SKILL.md +49 -0
  88. package/skills/chart-visualization/SKILL.md +73 -0
  89. package/skills/ci-cd-and-automation/SKILL.md +353 -0
  90. package/skills/code-review-and-quality/SKILL.md +375 -0
  91. package/skills/code-review-quality-gates/SKILL.md +132 -0
  92. package/skills/code-simplification/SKILL.md +371 -0
  93. package/skills/comment-accuracy-review/SKILL.md +37 -0
  94. package/skills/consulting-analysis/SKILL.md +69 -0
  95. package/skills/context-engineering/SKILL.md +375 -0
  96. package/skills/coverage-review/SKILL.md +24 -0
  97. package/skills/data-analysis/SKILL.md +74 -0
  98. package/skills/deprecation-and-migration/SKILL.md +222 -0
  99. package/skills/documentation-and-adrs/SKILL.md +330 -0
  100. package/skills/e2e-artifact-handling/SKILL.md +30 -0
  101. package/skills/evidence-scoped-retrospective/SKILL.md +156 -0
  102. package/skills/explain-by-allegory/SKILL.md +99 -0
  103. package/skills/flutter-dev/SKILL.md +162 -0
  104. package/skills/flutter-dev/references/animations.md +497 -0
  105. package/skills/flutter-dev/references/bloc-state.md +281 -0
  106. package/skills/flutter-dev/references/forms.md +656 -0
  107. package/skills/flutter-dev/references/gorouter-navigation.md +257 -0
  108. package/skills/flutter-dev/references/localization.md +510 -0
  109. package/skills/flutter-dev/references/networking.md +566 -0
  110. package/skills/flutter-dev/references/performance.md +305 -0
  111. package/skills/flutter-dev/references/platform-specific.md +417 -0
  112. package/skills/flutter-dev/references/project-structure.md +273 -0
  113. package/skills/flutter-dev/references/riverpod-state.md +232 -0
  114. package/skills/flutter-dev/references/testing.md +364 -0
  115. package/skills/flutter-dev/references/widget-patterns.md +233 -0
  116. package/skills/frontend-dev/SKILL.md +468 -0
  117. package/skills/frontend-dev/canvas-fonts/ArsenalSC-OFL.txt +93 -0
  118. package/skills/frontend-dev/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  119. package/skills/frontend-dev/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  120. package/skills/frontend-dev/canvas-fonts/BigShoulders-OFL.txt +93 -0
  121. package/skills/frontend-dev/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  122. package/skills/frontend-dev/canvas-fonts/Boldonse-OFL.txt +93 -0
  123. package/skills/frontend-dev/canvas-fonts/Boldonse-Regular.ttf +0 -0
  124. package/skills/frontend-dev/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  125. package/skills/frontend-dev/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
  126. package/skills/frontend-dev/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  127. package/skills/frontend-dev/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  128. package/skills/frontend-dev/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  129. package/skills/frontend-dev/canvas-fonts/CrimsonPro-OFL.txt +93 -0
  130. package/skills/frontend-dev/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  131. package/skills/frontend-dev/canvas-fonts/DMMono-OFL.txt +93 -0
  132. package/skills/frontend-dev/canvas-fonts/DMMono-Regular.ttf +0 -0
  133. package/skills/frontend-dev/canvas-fonts/EricaOne-OFL.txt +94 -0
  134. package/skills/frontend-dev/canvas-fonts/EricaOne-Regular.ttf +0 -0
  135. package/skills/frontend-dev/canvas-fonts/GeistMono-Bold.ttf +0 -0
  136. package/skills/frontend-dev/canvas-fonts/GeistMono-OFL.txt +93 -0
  137. package/skills/frontend-dev/canvas-fonts/GeistMono-Regular.ttf +0 -0
  138. package/skills/frontend-dev/canvas-fonts/Gloock-OFL.txt +93 -0
  139. package/skills/frontend-dev/canvas-fonts/Gloock-Regular.ttf +0 -0
  140. package/skills/frontend-dev/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  141. package/skills/frontend-dev/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
  142. package/skills/frontend-dev/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  143. package/skills/frontend-dev/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  144. package/skills/frontend-dev/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  145. package/skills/frontend-dev/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  146. package/skills/frontend-dev/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  147. package/skills/frontend-dev/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  148. package/skills/frontend-dev/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  149. package/skills/frontend-dev/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  150. package/skills/frontend-dev/canvas-fonts/InstrumentSans-OFL.txt +93 -0
  151. package/skills/frontend-dev/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  152. package/skills/frontend-dev/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  153. package/skills/frontend-dev/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  154. package/skills/frontend-dev/canvas-fonts/Italiana-OFL.txt +93 -0
  155. package/skills/frontend-dev/canvas-fonts/Italiana-Regular.ttf +0 -0
  156. package/skills/frontend-dev/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  157. package/skills/frontend-dev/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
  158. package/skills/frontend-dev/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  159. package/skills/frontend-dev/canvas-fonts/Jura-Light.ttf +0 -0
  160. package/skills/frontend-dev/canvas-fonts/Jura-Medium.ttf +0 -0
  161. package/skills/frontend-dev/canvas-fonts/Jura-OFL.txt +93 -0
  162. package/skills/frontend-dev/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
  163. package/skills/frontend-dev/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  164. package/skills/frontend-dev/canvas-fonts/Lora-Bold.ttf +0 -0
  165. package/skills/frontend-dev/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  166. package/skills/frontend-dev/canvas-fonts/Lora-Italic.ttf +0 -0
  167. package/skills/frontend-dev/canvas-fonts/Lora-OFL.txt +93 -0
  168. package/skills/frontend-dev/canvas-fonts/Lora-Regular.ttf +0 -0
  169. package/skills/frontend-dev/canvas-fonts/NationalPark-Bold.ttf +0 -0
  170. package/skills/frontend-dev/canvas-fonts/NationalPark-OFL.txt +93 -0
  171. package/skills/frontend-dev/canvas-fonts/NationalPark-Regular.ttf +0 -0
  172. package/skills/frontend-dev/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
  173. package/skills/frontend-dev/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  174. package/skills/frontend-dev/canvas-fonts/Outfit-Bold.ttf +0 -0
  175. package/skills/frontend-dev/canvas-fonts/Outfit-OFL.txt +93 -0
  176. package/skills/frontend-dev/canvas-fonts/Outfit-Regular.ttf +0 -0
  177. package/skills/frontend-dev/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  178. package/skills/frontend-dev/canvas-fonts/PixelifySans-OFL.txt +93 -0
  179. package/skills/frontend-dev/canvas-fonts/PoiretOne-OFL.txt +93 -0
  180. package/skills/frontend-dev/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  181. package/skills/frontend-dev/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  182. package/skills/frontend-dev/canvas-fonts/RedHatMono-OFL.txt +93 -0
  183. package/skills/frontend-dev/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  184. package/skills/frontend-dev/canvas-fonts/Silkscreen-OFL.txt +93 -0
  185. package/skills/frontend-dev/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  186. package/skills/frontend-dev/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  187. package/skills/frontend-dev/canvas-fonts/SmoochSans-OFL.txt +93 -0
  188. package/skills/frontend-dev/canvas-fonts/Tektur-Medium.ttf +0 -0
  189. package/skills/frontend-dev/canvas-fonts/Tektur-OFL.txt +93 -0
  190. package/skills/frontend-dev/canvas-fonts/Tektur-Regular.ttf +0 -0
  191. package/skills/frontend-dev/canvas-fonts/WorkSans-Bold.ttf +0 -0
  192. package/skills/frontend-dev/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  193. package/skills/frontend-dev/canvas-fonts/WorkSans-Italic.ttf +0 -0
  194. package/skills/frontend-dev/canvas-fonts/WorkSans-OFL.txt +93 -0
  195. package/skills/frontend-dev/canvas-fonts/WorkSans-Regular.ttf +0 -0
  196. package/skills/frontend-dev/canvas-fonts/YoungSerif-OFL.txt +93 -0
  197. package/skills/frontend-dev/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  198. package/skills/frontend-dev/references/asset-prompt-guide.md +43 -0
  199. package/skills/frontend-dev/references/env-setup.md +33 -0
  200. package/skills/frontend-dev/references/minimax-cli-reference.md +133 -0
  201. package/skills/frontend-dev/references/minimax-image-guide.md +65 -0
  202. package/skills/frontend-dev/references/minimax-music-guide.md +216 -0
  203. package/skills/frontend-dev/references/minimax-tts-guide.md +78 -0
  204. package/skills/frontend-dev/references/minimax-video-guide.md +82 -0
  205. package/skills/frontend-dev/references/minimax-voice-catalog.md +685 -0
  206. package/skills/frontend-dev/references/motion-recipes.md +407 -0
  207. package/skills/frontend-dev/references/troubleshooting.md +85 -0
  208. package/skills/frontend-dev/scripts/minimax_image.py +137 -0
  209. package/skills/frontend-dev/scripts/minimax_music.py +157 -0
  210. package/skills/frontend-dev/scripts/minimax_tts.py +127 -0
  211. package/skills/frontend-dev/scripts/minimax_video.py +187 -0
  212. package/skills/frontend-dev/templates/generator_template.js +223 -0
  213. package/skills/frontend-dev/templates/viewer.html +599 -0
  214. package/skills/frontend-ui-engineering/SKILL.md +367 -0
  215. package/skills/fullstack-dev/SKILL.md +819 -0
  216. package/skills/fullstack-dev/references/api-design.md +444 -0
  217. package/skills/fullstack-dev/references/auth-flow.md +165 -0
  218. package/skills/fullstack-dev/references/db-schema.md +706 -0
  219. package/skills/fullstack-dev/references/django-best-practices.md +466 -0
  220. package/skills/fullstack-dev/references/environment-management.md +78 -0
  221. package/skills/fullstack-dev/references/release-checklist.md +278 -0
  222. package/skills/fullstack-dev/references/technology-selection.md +254 -0
  223. package/skills/fullstack-dev/references/testing-strategy.md +404 -0
  224. package/skills/git-workflow-and-versioning/SKILL.md +446 -0
  225. package/skills/github-evidence-triage/SKILL.md +115 -0
  226. package/skills/harness-evolution/SKILL.md +59 -0
  227. package/skills/harness-evolution/references/activation-matrix.md +13 -0
  228. package/skills/harness-evolution/references/approval-policy.md +20 -0
  229. package/skills/harness-evolution/references/change-report-template.md +33 -0
  230. package/skills/harness-evolution/references/component-taxonomy.md +19 -0
  231. package/skills/harness-evolution/references/verdict-policy.md +13 -0
  232. package/skills/harness-issue-triage/SKILL.md +72 -0
  233. package/skills/harness-issue-triage/references/component-diagnosis.md +25 -0
  234. package/skills/harness-issue-triage/references/triage-report-template.md +34 -0
  235. package/skills/harness-optimization-audit/SKILL.md +125 -0
  236. package/skills/idea-refine/SKILL.md +191 -0
  237. package/skills/idea-refine/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/LICENSE +21 -0
  238. package/skills/idea-refine/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/NOTICE.md +9 -0
  239. package/skills/idea-refine/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/idea-refine/SKILL.upstream.md +178 -0
  240. package/skills/idea-refine/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/idea-refine/examples.md +238 -0
  241. package/skills/idea-refine/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/idea-refine/frameworks.md +99 -0
  242. package/skills/idea-refine/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/idea-refine/refinement-criteria.md +113 -0
  243. package/skills/idea-refine/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/idea-refine/scripts/idea-refine.upstream.sh +15 -0
  244. package/skills/incremental-implementation/SKILL.md +252 -0
  245. package/skills/ios-application-dev/SKILL.md +212 -0
  246. package/skills/ios-application-dev/references/accessibility.md +259 -0
  247. package/skills/ios-application-dev/references/graphics-animation.md +350 -0
  248. package/skills/ios-application-dev/references/layout-system.md +199 -0
  249. package/skills/ios-application-dev/references/metal-shader.md +178 -0
  250. package/skills/ios-application-dev/references/navigation-patterns.md +175 -0
  251. package/skills/ios-application-dev/references/swift-coding-standards.md +757 -0
  252. package/skills/ios-application-dev/references/swiftui-design-guidelines.md +1167 -0
  253. package/skills/ios-application-dev/references/system-integration.md +401 -0
  254. package/skills/ios-application-dev/references/uikit-components.md +297 -0
  255. package/skills/local-review-gate/SKILL.md +207 -0
  256. package/skills/local-review-gate/references/addyosmani-code-review-rubric.md +33 -0
  257. package/skills/local-review-gate/references/codex-github-compatibility.md +26 -0
  258. package/skills/local-review-gate/references/ecc-code-review-adaptation.md +40 -0
  259. package/skills/local-review-gate/references/graphify-local-review.md +33 -0
  260. package/skills/local-review-gate/references/orchestration-adaptation.md +51 -0
  261. package/skills/local-review-gate/references/review-repair-lane-adaptation.md +46 -0
  262. package/skills/local-review-gate/references/upstream-provenance.md +61 -0
  263. package/skills/mature-project-pattern-research/SKILL.md +155 -0
  264. package/skills/mature-project-pattern-research/references/research-rubric.md +45 -0
  265. package/skills/minimax-docx/LICENSE +21 -0
  266. package/skills/minimax-docx/SKILL.md +273 -0
  267. package/skills/minimax-docx/assets/styles/academic_styles.xml +250 -0
  268. package/skills/minimax-docx/assets/styles/corporate_styles.xml +284 -0
  269. package/skills/minimax-docx/assets/styles/default_styles.xml +449 -0
  270. package/skills/minimax-docx/assets/xsd/aesthetic-rules.xsd +470 -0
  271. package/skills/minimax-docx/assets/xsd/business-rules.xsd +130 -0
  272. package/skills/minimax-docx/assets/xsd/common-types.xsd +159 -0
  273. package/skills/minimax-docx/assets/xsd/wml-subset.xsd +589 -0
  274. package/skills/minimax-docx/references/cjk_typography.md +357 -0
  275. package/skills/minimax-docx/references/cjk_university_template_guide.md +184 -0
  276. package/skills/minimax-docx/references/comments_guide.md +191 -0
  277. package/skills/minimax-docx/references/design_good_bad_examples.md +829 -0
  278. package/skills/minimax-docx/references/design_principles.md +819 -0
  279. package/skills/minimax-docx/references/openxml_element_order.md +308 -0
  280. package/skills/minimax-docx/references/openxml_encyclopedia_part1.md +4061 -0
  281. package/skills/minimax-docx/references/openxml_encyclopedia_part2.md +2820 -0
  282. package/skills/minimax-docx/references/openxml_encyclopedia_part3.md +3381 -0
  283. package/skills/minimax-docx/references/openxml_namespaces.md +82 -0
  284. package/skills/minimax-docx/references/openxml_units.md +72 -0
  285. package/skills/minimax-docx/references/scenario_a_create.md +284 -0
  286. package/skills/minimax-docx/references/scenario_b_edit_content.md +295 -0
  287. package/skills/minimax-docx/references/scenario_c_apply_template.md +456 -0
  288. package/skills/minimax-docx/references/track_changes_guide.md +200 -0
  289. package/skills/minimax-docx/references/troubleshooting.md +506 -0
  290. package/skills/minimax-docx/references/typography_guide.md +294 -0
  291. package/skills/minimax-docx/references/xsd_validation_guide.md +158 -0
  292. package/skills/minimax-docx/scripts/doc_to_docx.sh +40 -0
  293. package/skills/minimax-docx/scripts/docx_preview.sh +37 -0
  294. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/MiniMaxAIDocx.Cli.csproj +19 -0
  295. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Cli/Program.cs +18 -0
  296. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/AnalyzeCommand.cs +147 -0
  297. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ApplyTemplateCommand.cs +322 -0
  298. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/CreateCommand.cs +324 -0
  299. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/DiffCommand.cs +155 -0
  300. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/EditContentCommand.cs +487 -0
  301. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/FixOrderCommand.cs +108 -0
  302. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/MergeRunsCommand.cs +122 -0
  303. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Commands/ValidateCommand.cs +107 -0
  304. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/MiniMaxAIDocx.Core.csproj +15 -0
  305. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/CommentSynchronizer.cs +169 -0
  306. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/ElementOrder.cs +80 -0
  307. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/NamespaceConstants.cs +42 -0
  308. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/RunMerger.cs +81 -0
  309. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/StyleAnalyzer.cs +81 -0
  310. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/TrackChangesHelper.cs +99 -0
  311. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/OpenXml/UnitConverter.cs +23 -0
  312. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples.cs +1832 -0
  313. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch1.cs +910 -0
  314. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch2.cs +999 -0
  315. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch3.cs +1048 -0
  316. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/AestheticRecipeSamples_Batch4.cs +1038 -0
  317. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/CharacterFormattingSamples.cs +1020 -0
  318. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/DocumentCreationSamples.cs +1121 -0
  319. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FieldAndTocSamples.cs +624 -0
  320. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/FootnoteAndCommentSamples.cs +675 -0
  321. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/HeaderFooterSamples.cs +838 -0
  322. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ImageSamples.cs +917 -0
  323. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ListAndNumberingSamples.cs +826 -0
  324. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/ParagraphFormattingSamples.cs +1199 -0
  325. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/StyleSystemSamples.cs +1487 -0
  326. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TableSamples.cs +1163 -0
  327. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Samples/TrackChangesSamples.cs +595 -0
  328. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/CjkHelper.cs +39 -0
  329. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/FontDefaults.cs +24 -0
  330. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Typography/PageSizes.cs +20 -0
  331. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/BusinessRuleValidator.cs +224 -0
  332. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/GateCheckValidator.cs +148 -0
  333. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/ValidationResult.cs +23 -0
  334. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.Core/Validation/XsdValidator.cs +69 -0
  335. package/skills/minimax-docx/scripts/dotnet/MiniMaxAIDocx.slnx +4 -0
  336. package/skills/minimax-docx/scripts/env_check.sh +196 -0
  337. package/skills/minimax-docx/scripts/setup.ps1 +274 -0
  338. package/skills/minimax-docx/scripts/setup.sh +504 -0
  339. package/skills/minimax-pdf/README.md +222 -0
  340. package/skills/minimax-pdf/SKILL.md +218 -0
  341. package/skills/minimax-pdf/design/design.md +381 -0
  342. package/skills/minimax-pdf/scripts/cover.py +1579 -0
  343. package/skills/minimax-pdf/scripts/fill_inspect.py +200 -0
  344. package/skills/minimax-pdf/scripts/fill_write.py +242 -0
  345. package/skills/minimax-pdf/scripts/make.sh +491 -0
  346. package/skills/minimax-pdf/scripts/merge.py +112 -0
  347. package/skills/minimax-pdf/scripts/palette.py +521 -0
  348. package/skills/minimax-pdf/scripts/reformat_parse.py +374 -0
  349. package/skills/minimax-pdf/scripts/render_body.py +1052 -0
  350. package/skills/minimax-pdf/scripts/render_cover.js +111 -0
  351. package/skills/minimax-xlsx/SKILL.md +156 -0
  352. package/skills/minimax-xlsx/references/create.md +691 -0
  353. package/skills/minimax-xlsx/references/edit.md +684 -0
  354. package/skills/minimax-xlsx/references/fix.md +37 -0
  355. package/skills/minimax-xlsx/references/format.md +768 -0
  356. package/skills/minimax-xlsx/references/ooxml-cheatsheet.md +231 -0
  357. package/skills/minimax-xlsx/references/read-analyze.md +97 -0
  358. package/skills/minimax-xlsx/references/validate.md +772 -0
  359. package/skills/minimax-xlsx/scripts/formula_check.py +422 -0
  360. package/skills/minimax-xlsx/scripts/libreoffice_recalc.py +248 -0
  361. package/skills/minimax-xlsx/scripts/shared_strings_builder.py +163 -0
  362. package/skills/minimax-xlsx/scripts/style_audit.py +575 -0
  363. package/skills/minimax-xlsx/scripts/xlsx_add_column.py +395 -0
  364. package/skills/minimax-xlsx/scripts/xlsx_insert_row.py +274 -0
  365. package/skills/minimax-xlsx/scripts/xlsx_pack.py +87 -0
  366. package/skills/minimax-xlsx/scripts/xlsx_reader.py +362 -0
  367. package/skills/minimax-xlsx/scripts/xlsx_shift_rows.py +396 -0
  368. package/skills/minimax-xlsx/scripts/xlsx_unpack.py +130 -0
  369. package/skills/minimax-xlsx/templates/minimal_xlsx/[Content_Types].xml +9 -0
  370. package/skills/minimax-xlsx/templates/minimal_xlsx/_rels/.rels +6 -0
  371. package/skills/minimax-xlsx/templates/minimal_xlsx/xl/_rels/workbook.xml.rels +19 -0
  372. package/skills/minimax-xlsx/templates/minimal_xlsx/xl/sharedStrings.xml +33 -0
  373. package/skills/minimax-xlsx/templates/minimal_xlsx/xl/styles.xml +160 -0
  374. package/skills/minimax-xlsx/templates/minimal_xlsx/xl/workbook.xml +30 -0
  375. package/skills/minimax-xlsx/templates/minimal_xlsx/xl/worksheets/sheet1.xml +70 -0
  376. package/skills/newsletter-generation/SKILL.md +71 -0
  377. package/skills/oss-release-readiness/SKILL.md +47 -0
  378. package/skills/parallel-subagent-dispatch/SKILL.md +58 -0
  379. package/skills/performance-optimization/SKILL.md +364 -0
  380. package/skills/planning-and-task-breakdown/SKILL.md +290 -0
  381. package/skills/pptx-generator/SKILL.md +275 -0
  382. package/skills/pptx-generator/references/design-system.md +392 -0
  383. package/skills/pptx-generator/references/editing.md +162 -0
  384. package/skills/pptx-generator/references/pitfalls.md +112 -0
  385. package/skills/pptx-generator/references/pptxgenjs.md +420 -0
  386. package/skills/pptx-generator/references/slide-types.md +413 -0
  387. package/skills/pr-test-analysis/SKILL.md +24 -0
  388. package/skills/react-native-dev/SKILL.md +180 -0
  389. package/skills/react-native-dev/references/animations.md +254 -0
  390. package/skills/react-native-dev/references/components.md +124 -0
  391. package/skills/react-native-dev/references/engineering.md +527 -0
  392. package/skills/react-native-dev/references/forms.md +300 -0
  393. package/skills/react-native-dev/references/native-capabilities.md +163 -0
  394. package/skills/react-native-dev/references/navigation.md +271 -0
  395. package/skills/react-native-dev/references/networking.md +346 -0
  396. package/skills/react-native-dev/references/performance.md +215 -0
  397. package/skills/react-native-dev/references/state-management.md +230 -0
  398. package/skills/react-native-dev/references/styling.md +117 -0
  399. package/skills/react-native-dev/references/testing.md +341 -0
  400. package/skills/requirements-grilling/SKILL.md +447 -0
  401. package/skills/requirements-grilling/references/ADR-FORMAT.md +46 -0
  402. package/skills/requirements-grilling/references/CONTEXT-FORMAT.md +41 -0
  403. package/skills/requirements-grilling/references/INTERVIEW-PACKET-FORMAT.md +62 -0
  404. package/skills/requirements-grilling/references/MIT-LICENSE-MATT-POCOCK.md +25 -0
  405. package/skills/requirements-grilling/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/LICENSE +21 -0
  406. package/skills/requirements-grilling/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/NOTICE.md +9 -0
  407. package/skills/requirements-grilling/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/interview-me/SKILL.upstream.md +225 -0
  408. package/skills/requirements-grilling/references/upstream/mattpocock-skills/9603c1cc8118d08bc1b3bf34cf714f62178dea3b/LICENSE +21 -0
  409. package/skills/requirements-grilling/references/upstream/mattpocock-skills/9603c1cc8118d08bc1b3bf34cf714f62178dea3b/NOTICE.md +12 -0
  410. package/skills/requirements-grilling/references/upstream/mattpocock-skills/9603c1cc8118d08bc1b3bf34cf714f62178dea3b/batch-grill-me/SKILL.upstream.md +15 -0
  411. package/skills/requirements-grilling/references/upstream/mattpocock-skills/9603c1cc8118d08bc1b3bf34cf714f62178dea3b/grill-me/SKILL.upstream.md +7 -0
  412. package/skills/requirements-grilling/references/upstream/mattpocock-skills/9603c1cc8118d08bc1b3bf34cf714f62178dea3b/grilling/SKILL.upstream.md +12 -0
  413. package/skills/review-pipeline/SKILL.md +38 -0
  414. package/skills/rose-memory/SKILL.md +210 -0
  415. package/skills/rose-memory/references/README.md +48 -0
  416. package/skills/rose-memory/references/memory_cli.py +2384 -0
  417. package/skills/rose-memory/references/schema.sql +315 -0
  418. package/skills/security-and-hardening/SKILL.md +369 -0
  419. package/skills/session-handoff/SKILL.md +88 -0
  420. package/skills/session-handoff/references/upstream/mattpocock-skills/391a2701dd948f94f56a39f7533f8eea9a859c87/LICENSE +21 -0
  421. package/skills/session-handoff/references/upstream/mattpocock-skills/391a2701dd948f94f56a39f7533f8eea9a859c87/NOTICE.md +9 -0
  422. package/skills/session-handoff/references/upstream/mattpocock-skills/391a2701dd948f94f56a39f7533f8eea9a859c87/productivity/handoff/SKILL.upstream.md +16 -0
  423. package/skills/session-handoff/scripts/session_handoff.py +764 -0
  424. package/skills/shader-dev/SKILL.md +316 -0
  425. package/skills/shader-dev/reference/ambient-occlusion.md +382 -0
  426. package/skills/shader-dev/reference/analytic-ray-tracing.md +651 -0
  427. package/skills/shader-dev/reference/anti-aliasing.md +71 -0
  428. package/skills/shader-dev/reference/atmospheric-scattering.md +571 -0
  429. package/skills/shader-dev/reference/camera-effects.md +80 -0
  430. package/skills/shader-dev/reference/cellular-automata.md +635 -0
  431. package/skills/shader-dev/reference/color-palette.md +481 -0
  432. package/skills/shader-dev/reference/csg-boolean-operations.md +466 -0
  433. package/skills/shader-dev/reference/domain-repetition.md +436 -0
  434. package/skills/shader-dev/reference/domain-warping.md +419 -0
  435. package/skills/shader-dev/reference/fluid-simulation.md +425 -0
  436. package/skills/shader-dev/reference/fractal-rendering.md +525 -0
  437. package/skills/shader-dev/reference/lighting-model.md +639 -0
  438. package/skills/shader-dev/reference/matrix-transform.md +535 -0
  439. package/skills/shader-dev/reference/multipass-buffer.md +571 -0
  440. package/skills/shader-dev/reference/normal-estimation.md +418 -0
  441. package/skills/shader-dev/reference/particle-system.md +589 -0
  442. package/skills/shader-dev/reference/path-tracing-gi.md +602 -0
  443. package/skills/shader-dev/reference/polar-uv-manipulation.md +521 -0
  444. package/skills/shader-dev/reference/post-processing.md +375 -0
  445. package/skills/shader-dev/reference/procedural-2d-pattern.md +439 -0
  446. package/skills/shader-dev/reference/procedural-noise.md +551 -0
  447. package/skills/shader-dev/reference/ray-marching.md +396 -0
  448. package/skills/shader-dev/reference/sdf-2d.md +724 -0
  449. package/skills/shader-dev/reference/sdf-3d.md +805 -0
  450. package/skills/shader-dev/reference/sdf-tricks.md +63 -0
  451. package/skills/shader-dev/reference/shadow-techniques.md +476 -0
  452. package/skills/shader-dev/reference/simulation-physics.md +644 -0
  453. package/skills/shader-dev/reference/sound-synthesis.md +578 -0
  454. package/skills/shader-dev/reference/terrain-rendering.md +839 -0
  455. package/skills/shader-dev/reference/texture-mapping-advanced.md +87 -0
  456. package/skills/shader-dev/reference/texture-sampling.md +553 -0
  457. package/skills/shader-dev/reference/volumetric-rendering.md +608 -0
  458. package/skills/shader-dev/reference/voronoi-cellular-noise.md +486 -0
  459. package/skills/shader-dev/reference/voxel-rendering.md +701 -0
  460. package/skills/shader-dev/reference/water-ocean.md +445 -0
  461. package/skills/shader-dev/reference/webgl-pitfalls.md +41 -0
  462. package/skills/shader-dev/techniques/ambient-occlusion.md +364 -0
  463. package/skills/shader-dev/techniques/analytic-ray-tracing.md +542 -0
  464. package/skills/shader-dev/techniques/anti-aliasing.md +124 -0
  465. package/skills/shader-dev/techniques/atmospheric-scattering.md +522 -0
  466. package/skills/shader-dev/techniques/camera-effects.md +115 -0
  467. package/skills/shader-dev/techniques/cellular-automata.md +531 -0
  468. package/skills/shader-dev/techniques/color-palette.md +380 -0
  469. package/skills/shader-dev/techniques/csg-boolean-operations.md +491 -0
  470. package/skills/shader-dev/techniques/domain-repetition.md +333 -0
  471. package/skills/shader-dev/techniques/domain-warping.md +414 -0
  472. package/skills/shader-dev/techniques/fluid-simulation.md +1175 -0
  473. package/skills/shader-dev/techniques/fractal-rendering.md +436 -0
  474. package/skills/shader-dev/techniques/lighting-model.md +527 -0
  475. package/skills/shader-dev/techniques/matrix-transform.md +455 -0
  476. package/skills/shader-dev/techniques/multipass-buffer.md +922 -0
  477. package/skills/shader-dev/techniques/normal-estimation.md +318 -0
  478. package/skills/shader-dev/techniques/particle-system.md +1203 -0
  479. package/skills/shader-dev/techniques/path-tracing-gi.md +623 -0
  480. package/skills/shader-dev/techniques/polar-uv-manipulation.md +373 -0
  481. package/skills/shader-dev/techniques/post-processing.md +788 -0
  482. package/skills/shader-dev/techniques/procedural-2d-pattern.md +346 -0
  483. package/skills/shader-dev/techniques/procedural-noise.md +554 -0
  484. package/skills/shader-dev/techniques/ray-marching.md +467 -0
  485. package/skills/shader-dev/techniques/sdf-2d.md +631 -0
  486. package/skills/shader-dev/techniques/sdf-3d.md +589 -0
  487. package/skills/shader-dev/techniques/sdf-tricks.md +100 -0
  488. package/skills/shader-dev/techniques/shadow-techniques.md +776 -0
  489. package/skills/shader-dev/techniques/simulation-physics.md +1542 -0
  490. package/skills/shader-dev/techniques/sound-synthesis.md +490 -0
  491. package/skills/shader-dev/techniques/terrain-rendering.md +408 -0
  492. package/skills/shader-dev/techniques/texture-mapping-advanced.md +121 -0
  493. package/skills/shader-dev/techniques/texture-sampling.md +382 -0
  494. package/skills/shader-dev/techniques/volumetric-rendering.md +375 -0
  495. package/skills/shader-dev/techniques/voronoi-cellular-noise.md +458 -0
  496. package/skills/shader-dev/techniques/voxel-rendering.md +985 -0
  497. package/skills/shader-dev/techniques/water-ocean.md +490 -0
  498. package/skills/shader-dev/techniques/webgl-pitfalls.md +170 -0
  499. package/skills/shipping-and-launch/SKILL.md +334 -0
  500. package/skills/silent-failure-hunting/SKILL.md +24 -0
  501. package/skills/source-driven-development/SKILL.md +223 -0
  502. package/skills/spec-driven-development/SKILL.md +235 -0
  503. package/skills/spec-driven-development/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/LICENSE +21 -0
  504. package/skills/spec-driven-development/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/NOTICE.md +9 -0
  505. package/skills/spec-driven-development/references/upstream/addyosmani-agent-skills/6bcfeb9dae52b11eaad23511acc165109746dbc3/spec-driven-development/SKILL.upstream.md +206 -0
  506. package/skills/strategy-stress-test/SKILL.md +185 -0
  507. package/skills/systematic-literature-review/SKILL.md +77 -0
  508. package/skills/test-document-generator/SKILL.md +152 -0
  509. package/skills/test-document-generator/references/test-document-template.md +45 -0
  510. package/skills/test-driven-development/SKILL.md +399 -0
  511. package/skills/write-skills/GLOSSARY.md +113 -0
  512. package/skills/write-skills/SKILL.md +133 -0
  513. package/skills/write-skills/references/upstream/mattpocock-skills/391a2701dd948f94f56a39f7533f8eea9a859c87/LICENSE +21 -0
  514. package/skills/write-skills/references/upstream/mattpocock-skills/391a2701dd948f94f56a39f7533f8eea9a859c87/NOTICE.md +9 -0
  515. package/skills/write-skills/references/upstream/mattpocock-skills/391a2701dd948f94f56a39f7533f8eea9a859c87/productivity/writing-great-skills/GLOSSARY.md +201 -0
  516. package/skills/write-skills/references/upstream/mattpocock-skills/391a2701dd948f94f56a39f7533f8eea9a859c87/productivity/writing-great-skills/SKILL.upstream.md +83 -0
  517. package/src/runtime/child-guard.ts +70 -0
  518. package/src/runtime/conflicts.ts +25 -0
  519. package/src/runtime/contracts.ts +9 -0
  520. package/src/runtime/doctor.ts +134 -0
  521. package/src/runtime/global-resources.ts +152 -0
  522. package/src/runtime/index.ts +23 -0
  523. package/src/runtime/lifecycle.ts +19 -0
  524. package/src/runtime/native-integrations.ts +44 -0
  525. package/src/runtime/path-boundaries.ts +69 -0
  526. package/src/runtime/registry.ts +213 -0
  527. package/src/runtime/roles.ts +64 -0
  528. package/src/runtime/rose-context.ts +24 -0
  529. package/src/runtime/subagents.ts +426 -0
  530. package/templates/APPEND_SYSTEM.md +12 -0
  531. package/upstream/aili-workflows.lock.json +3226 -0
@@ -0,0 +1,2384 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import difflib
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import re
10
+ import sqlite3
11
+ import sys
12
+ import uuid
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import Any, Callable, Iterable
16
+
17
+
18
+ SCHEMA_VERSION = 1
19
+ MIGRATION_NAME = "v1_initial_memory_schema"
20
+ DEFAULT_DB = "memory/memory.db"
21
+ DEFAULT_AGENT = "rose"
22
+ DEFAULT_TARGET = "AGENTS.md"
23
+
24
+ SIGNAL_WEIGHTS = {
25
+ "long_term_instruction": 3,
26
+ "correction_after_drift": 2,
27
+ "review_rejection": 3,
28
+ "ordinary_preference": 1,
29
+ "evidence_backed_failure": 1,
30
+ "risk_signal": 3,
31
+ "user_confirmation": 5,
32
+ }
33
+
34
+ SEVERITY_RANK = {
35
+ "low": 0,
36
+ "normal": 1,
37
+ "high": 2,
38
+ "critical": 3,
39
+ }
40
+
41
+ REQUIRED_TABLES = {
42
+ "schema_migration",
43
+ "memory_receipt",
44
+ "session",
45
+ "task",
46
+ "memory_event",
47
+ "memory_fact",
48
+ "claim",
49
+ "finding",
50
+ "evidence",
51
+ "rule_candidate",
52
+ "rule_candidate_evidence",
53
+ "instruction_snapshot",
54
+ "rule_patch",
55
+ "rule_decision",
56
+ "rule_promotion",
57
+ "audit_run",
58
+ "search_doc",
59
+ "search_fts",
60
+ }
61
+
62
+
63
+ class CliError(Exception):
64
+ def __init__(self, error_code: str, message: str, suggested_action: str = "") -> None:
65
+ super().__init__(message)
66
+ self.error_code = error_code
67
+ self.message = message
68
+ self.suggested_action = suggested_action
69
+
70
+
71
+ def utc_now() -> str:
72
+ return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
73
+
74
+
75
+ def project_default() -> str:
76
+ return Path.cwd().name
77
+
78
+
79
+ def json_dump(value: Any) -> str:
80
+ return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
81
+
82
+
83
+ def emit(value: dict[str, Any], status: int = 0) -> None:
84
+ print(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True))
85
+ raise SystemExit(status)
86
+
87
+
88
+ def sha256_text(text: str) -> str:
89
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
90
+
91
+
92
+ def file_hash(path: str) -> str:
93
+ target = Path(path)
94
+ if not target.exists():
95
+ return sha256_text("")
96
+ return hashlib.sha256(target.read_bytes()).hexdigest()
97
+
98
+
99
+ def read_text_if_exists(path: str) -> str:
100
+ target = Path(path)
101
+ if not target.exists():
102
+ return ""
103
+ return target.read_text(encoding="utf-8")
104
+
105
+
106
+ def write_text(path: str, content: str) -> None:
107
+ Path(path).write_text(content, encoding="utf-8")
108
+
109
+
110
+ def normalize_rule_text(rule_text: str) -> str:
111
+ normalized = re.sub(r"\s+", " ", rule_text.strip())
112
+ if normalized.endswith("."):
113
+ normalized = normalized[:-1]
114
+ return normalized.lower()
115
+
116
+
117
+ def normalize_rule_key(rule_text: str) -> str:
118
+ normalized = re.sub(r"[^a-z0-9]+", "_", normalize_rule_text(rule_text))
119
+ normalized = normalized.strip("_")
120
+ return normalized[:80] or f"rule_{uuid.uuid4().hex[:12]}"
121
+
122
+
123
+ def query_tokens(raw_query: str) -> list[str]:
124
+ tokens = re.findall(r"[A-Za-z0-9_]+", raw_query)
125
+ if not tokens:
126
+ raise CliError("INVALID_SEARCH_QUERY", "search query must contain at least one word token")
127
+ return tokens
128
+
129
+
130
+ def safe_fts_query(raw_query: str) -> str:
131
+ tokens = query_tokens(raw_query)
132
+ return " OR ".join(f'"{token}"' for token in tokens)
133
+
134
+
135
+ def token_like_clause(columns: list[str], tokens: list[str]) -> tuple[str, list[str]]:
136
+ clauses: list[str] = []
137
+ values: list[str] = []
138
+ for token in tokens:
139
+ like_value = f"%{token}%"
140
+ clauses.append("(" + " OR ".join(f"{column} LIKE ?" for column in columns) + ")")
141
+ values.extend([like_value] * len(columns))
142
+ return "(" + " OR ".join(clauses) + ")", values
143
+
144
+
145
+ def safe_metadata(raw: str | None) -> str:
146
+ if not raw:
147
+ return "{}"
148
+ try:
149
+ parsed = json.loads(raw)
150
+ except json.JSONDecodeError as exc:
151
+ raise CliError("INVALID_JSON", f"metadata_json is invalid JSON: {exc}") from exc
152
+ if not isinstance(parsed, dict):
153
+ raise CliError("INVALID_JSON", "metadata_json must be a JSON object")
154
+ return json_dump(parsed)
155
+
156
+
157
+ def sensitive_indexable(sensitive: bool, indexable: bool) -> tuple[int, int]:
158
+ if sensitive:
159
+ return 1, 0
160
+ return 0, 1 if indexable else 0
161
+
162
+
163
+ def row_to_dict(row: sqlite3.Row | None) -> dict[str, Any] | None:
164
+ if row is None:
165
+ return None
166
+ return {key: row[key] for key in row.keys()}
167
+
168
+
169
+ def schema_sql() -> str:
170
+ return """
171
+ CREATE TABLE IF NOT EXISTS schema_migration (
172
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
173
+ version INTEGER NOT NULL,
174
+ name TEXT NOT NULL,
175
+ checksum TEXT NOT NULL,
176
+ applied_at TEXT NOT NULL,
177
+ UNIQUE(version)
178
+ );
179
+
180
+ CREATE TABLE IF NOT EXISTS memory_receipt (
181
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
182
+ receipt_key TEXT NOT NULL UNIQUE,
183
+ operation TEXT NOT NULL,
184
+ status TEXT NOT NULL CHECK (status IN ('ok','error')),
185
+ payload_json TEXT NOT NULL DEFAULT '{}',
186
+ created_at TEXT NOT NULL
187
+ );
188
+
189
+ CREATE TABLE IF NOT EXISTS session (
190
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
191
+ session_key TEXT NOT NULL UNIQUE,
192
+ project TEXT NOT NULL,
193
+ agent TEXT NOT NULL,
194
+ started_at TEXT NOT NULL,
195
+ ended_at TEXT,
196
+ metadata_json TEXT NOT NULL DEFAULT '{}'
197
+ );
198
+
199
+ CREATE TABLE IF NOT EXISTS task (
200
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
201
+ task_key TEXT NOT NULL UNIQUE,
202
+ session_id INTEGER REFERENCES session(id) ON DELETE SET NULL,
203
+ project TEXT NOT NULL,
204
+ agent TEXT NOT NULL,
205
+ title TEXT NOT NULL,
206
+ scope_text TEXT NOT NULL DEFAULT '',
207
+ status TEXT NOT NULL CHECK (status IN ('active','blocked','completed','cancelled')),
208
+ started_at TEXT NOT NULL,
209
+ completed_at TEXT,
210
+ outcome TEXT,
211
+ metadata_json TEXT NOT NULL DEFAULT '{}'
212
+ );
213
+
214
+ CREATE TABLE IF NOT EXISTS memory_event (
215
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
216
+ session_id INTEGER REFERENCES session(id) ON DELETE SET NULL,
217
+ task_id INTEGER REFERENCES task(id) ON DELETE SET NULL,
218
+ event_type TEXT NOT NULL,
219
+ state TEXT,
220
+ summary TEXT NOT NULL DEFAULT '',
221
+ payload_json TEXT NOT NULL DEFAULT '{}',
222
+ created_at TEXT NOT NULL
223
+ );
224
+
225
+ CREATE TABLE IF NOT EXISTS memory_fact (
226
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
227
+ session_id INTEGER REFERENCES session(id) ON DELETE SET NULL,
228
+ project TEXT NOT NULL,
229
+ agent TEXT NOT NULL,
230
+ fact_key TEXT NOT NULL,
231
+ fact_text TEXT NOT NULL,
232
+ category TEXT NOT NULL,
233
+ confidence TEXT NOT NULL DEFAULT 'medium'
234
+ CHECK (confidence IN ('low','medium','high')),
235
+ status TEXT NOT NULL DEFAULT 'active'
236
+ CHECK (status IN ('active','superseded','rejected','expired')),
237
+ sensitive INTEGER NOT NULL DEFAULT 0 CHECK (sensitive IN (0,1)),
238
+ indexable INTEGER NOT NULL DEFAULT 1 CHECK (indexable IN (0,1)),
239
+ first_seen_at TEXT NOT NULL,
240
+ last_seen_at TEXT NOT NULL,
241
+ superseded_by INTEGER REFERENCES memory_fact(id),
242
+ metadata_json TEXT NOT NULL DEFAULT '{}',
243
+ UNIQUE(project, agent, fact_key)
244
+ );
245
+
246
+ CREATE TABLE IF NOT EXISTS claim (
247
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
248
+ session_id INTEGER REFERENCES session(id) ON DELETE SET NULL,
249
+ task_id INTEGER REFERENCES task(id) ON DELETE SET NULL,
250
+ project TEXT NOT NULL,
251
+ agent TEXT NOT NULL,
252
+ claim_key TEXT NOT NULL,
253
+ claim_text TEXT NOT NULL,
254
+ verification_status TEXT NOT NULL
255
+ CHECK (verification_status IN ('unverified','verified','disputed','superseded')),
256
+ evidence_summary TEXT NOT NULL DEFAULT '',
257
+ sensitive INTEGER NOT NULL DEFAULT 0 CHECK (sensitive IN (0,1)),
258
+ indexable INTEGER NOT NULL DEFAULT 1 CHECK (indexable IN (0,1)),
259
+ created_at TEXT NOT NULL,
260
+ updated_at TEXT NOT NULL,
261
+ metadata_json TEXT NOT NULL DEFAULT '{}',
262
+ UNIQUE(project, agent, claim_key)
263
+ );
264
+
265
+ CREATE TABLE IF NOT EXISTS finding (
266
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
267
+ session_id INTEGER REFERENCES session(id) ON DELETE SET NULL,
268
+ task_id INTEGER REFERENCES task(id) ON DELETE SET NULL,
269
+ project TEXT NOT NULL,
270
+ agent TEXT NOT NULL,
271
+ kind TEXT NOT NULL
272
+ CHECK (kind IN ('correction','failure','review_rejection','preference','risk','observation')),
273
+ category TEXT NOT NULL,
274
+ severity TEXT NOT NULL DEFAULT 'normal'
275
+ CHECK (severity IN ('low','normal','high','critical')),
276
+ wrong_behavior TEXT NOT NULL DEFAULT '',
277
+ correct_behavior TEXT NOT NULL DEFAULT '',
278
+ summary TEXT NOT NULL,
279
+ raw_ref TEXT NOT NULL DEFAULT '',
280
+ sensitive INTEGER NOT NULL DEFAULT 0 CHECK (sensitive IN (0,1)),
281
+ indexable INTEGER NOT NULL DEFAULT 1 CHECK (indexable IN (0,1)),
282
+ created_at TEXT NOT NULL,
283
+ metadata_json TEXT NOT NULL DEFAULT '{}'
284
+ );
285
+
286
+ CREATE TABLE IF NOT EXISTS evidence (
287
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
288
+ session_id INTEGER REFERENCES session(id) ON DELETE SET NULL,
289
+ task_id INTEGER REFERENCES task(id) ON DELETE SET NULL,
290
+ project TEXT NOT NULL,
291
+ agent TEXT NOT NULL,
292
+ source_type TEXT NOT NULL
293
+ CHECK (source_type IN ('finding','claim','memory_event','task','file','diff','test','user_confirmation')),
294
+ source_id INTEGER,
295
+ evidence_key TEXT NOT NULL,
296
+ evidence_text TEXT NOT NULL,
297
+ evidence_hash TEXT NOT NULL,
298
+ weight INTEGER NOT NULL DEFAULT 1 CHECK (weight >= 0),
299
+ sensitive INTEGER NOT NULL DEFAULT 0 CHECK (sensitive IN (0,1)),
300
+ indexable INTEGER NOT NULL DEFAULT 1 CHECK (indexable IN (0,1)),
301
+ created_at TEXT NOT NULL,
302
+ metadata_json TEXT NOT NULL DEFAULT '{}',
303
+ UNIQUE(project, agent, evidence_key)
304
+ );
305
+
306
+ CREATE TABLE IF NOT EXISTS rule_candidate (
307
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
308
+ project TEXT NOT NULL,
309
+ agent TEXT NOT NULL,
310
+ rule_key TEXT NOT NULL,
311
+ normalized_rule_hash TEXT NOT NULL,
312
+ rule_text TEXT NOT NULL,
313
+ category TEXT NOT NULL,
314
+ severity TEXT NOT NULL DEFAULT 'normal'
315
+ CHECK (severity IN ('low','normal','high','critical')),
316
+ status TEXT NOT NULL DEFAULT 'memory'
317
+ CHECK (status IN (
318
+ 'memory',
319
+ 'candidate',
320
+ 'needs_reconciliation',
321
+ 'approved',
322
+ 'promoted',
323
+ 'rejected',
324
+ 'superseded',
325
+ 'expired'
326
+ )),
327
+ score INTEGER NOT NULL DEFAULT 0 CHECK (score >= 0),
328
+ mention_count INTEGER NOT NULL DEFAULT 0 CHECK (mention_count >= 0),
329
+ session_count INTEGER NOT NULL DEFAULT 0 CHECK (session_count >= 0),
330
+ evidence_count INTEGER NOT NULL DEFAULT 0 CHECK (evidence_count >= 0),
331
+ promotion_target TEXT NOT NULL DEFAULT 'AGENTS.md',
332
+ target_scope TEXT NOT NULL DEFAULT 'project_root_agents'
333
+ CHECK (target_scope IN ('project_root_agents')),
334
+ conflicts_json TEXT NOT NULL DEFAULT '[]',
335
+ requires_user_approval INTEGER NOT NULL DEFAULT 1 CHECK (requires_user_approval IN (0,1)),
336
+ first_seen_at TEXT NOT NULL,
337
+ last_seen_at TEXT NOT NULL,
338
+ approved_at TEXT,
339
+ promoted_at TEXT,
340
+ superseded_by INTEGER REFERENCES rule_candidate(id),
341
+ metadata_json TEXT NOT NULL DEFAULT '{}',
342
+ UNIQUE(project, agent, rule_key)
343
+ );
344
+
345
+ CREATE TABLE IF NOT EXISTS rule_candidate_evidence (
346
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
347
+ rule_id INTEGER NOT NULL REFERENCES rule_candidate(id) ON DELETE CASCADE,
348
+ evidence_id INTEGER NOT NULL REFERENCES evidence(id) ON DELETE RESTRICT,
349
+ signal_type TEXT NOT NULL
350
+ CHECK (signal_type IN (
351
+ 'long_term_instruction',
352
+ 'correction_after_drift',
353
+ 'review_rejection',
354
+ 'ordinary_preference',
355
+ 'evidence_backed_failure',
356
+ 'risk_signal',
357
+ 'user_confirmation'
358
+ )),
359
+ weight INTEGER NOT NULL CHECK (weight >= 0),
360
+ created_at TEXT NOT NULL,
361
+ UNIQUE(rule_id, evidence_id)
362
+ );
363
+
364
+ CREATE TABLE IF NOT EXISTS instruction_snapshot (
365
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
366
+ target_path TEXT NOT NULL,
367
+ target_scope TEXT NOT NULL
368
+ CHECK (target_scope IN ('project_root_agents')),
369
+ hash_algorithm TEXT NOT NULL DEFAULT 'sha256',
370
+ content_hash TEXT NOT NULL,
371
+ summary TEXT NOT NULL DEFAULT '',
372
+ captured_at TEXT NOT NULL,
373
+ metadata_json TEXT NOT NULL DEFAULT '{}'
374
+ );
375
+
376
+ CREATE TABLE IF NOT EXISTS rule_patch (
377
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
378
+ rule_id INTEGER NOT NULL REFERENCES rule_candidate(id) ON DELETE CASCADE,
379
+ target_path TEXT NOT NULL DEFAULT 'AGENTS.md',
380
+ target_scope TEXT NOT NULL DEFAULT 'project_root_agents'
381
+ CHECK (target_scope IN ('project_root_agents')),
382
+ base_snapshot_id INTEGER NOT NULL REFERENCES instruction_snapshot(id),
383
+ base_hash TEXT NOT NULL,
384
+ patch_hash TEXT NOT NULL,
385
+ proposed_rule_hash TEXT NOT NULL,
386
+ patch_text TEXT NOT NULL,
387
+ status TEXT NOT NULL DEFAULT 'proposed'
388
+ CHECK (status IN ('proposed','approved','rejected','applied','stale','failed')),
389
+ created_at TEXT NOT NULL,
390
+ updated_at TEXT NOT NULL,
391
+ metadata_json TEXT NOT NULL DEFAULT '{}'
392
+ );
393
+
394
+ CREATE TABLE IF NOT EXISTS rule_decision (
395
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
396
+ rule_id INTEGER NOT NULL REFERENCES rule_candidate(id) ON DELETE CASCADE,
397
+ patch_id INTEGER NOT NULL REFERENCES rule_patch(id) ON DELETE CASCADE,
398
+ decision TEXT NOT NULL
399
+ CHECK (decision IN ('approved','rejected','superseded','needs_reconciliation')),
400
+ approved_rule_hash TEXT,
401
+ approved_patch_hash TEXT,
402
+ actor TEXT NOT NULL,
403
+ reason TEXT NOT NULL DEFAULT '',
404
+ decided_at TEXT NOT NULL,
405
+ metadata_json TEXT NOT NULL DEFAULT '{}'
406
+ );
407
+
408
+ CREATE TABLE IF NOT EXISTS rule_promotion (
409
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
410
+ rule_id INTEGER NOT NULL REFERENCES rule_candidate(id) ON DELETE CASCADE,
411
+ patch_id INTEGER NOT NULL REFERENCES rule_patch(id) ON DELETE CASCADE,
412
+ decision_id INTEGER NOT NULL REFERENCES rule_decision(id) ON DELETE RESTRICT,
413
+ mode TEXT NOT NULL CHECK (mode IN ('apply','record_applied')),
414
+ target_path TEXT NOT NULL,
415
+ old_target_hash TEXT NOT NULL,
416
+ new_target_hash TEXT NOT NULL,
417
+ applied_by TEXT NOT NULL,
418
+ applied_at TEXT NOT NULL,
419
+ metadata_json TEXT NOT NULL DEFAULT '{}',
420
+ UNIQUE(patch_id)
421
+ );
422
+
423
+ CREATE TABLE IF NOT EXISTS audit_run (
424
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
425
+ audit_key TEXT NOT NULL UNIQUE,
426
+ operation TEXT NOT NULL,
427
+ status TEXT NOT NULL CHECK (status IN ('ok','error')),
428
+ checks_json TEXT NOT NULL DEFAULT '{}',
429
+ created_at TEXT NOT NULL
430
+ );
431
+
432
+ CREATE TABLE IF NOT EXISTS search_doc (
433
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
434
+ project TEXT NOT NULL,
435
+ agent TEXT NOT NULL,
436
+ doc_type TEXT NOT NULL,
437
+ source_table TEXT,
438
+ source_id INTEGER,
439
+ title TEXT NOT NULL DEFAULT '',
440
+ body TEXT NOT NULL,
441
+ summary TEXT NOT NULL DEFAULT '',
442
+ sensitive INTEGER NOT NULL DEFAULT 0 CHECK (sensitive IN (0,1)),
443
+ indexable INTEGER NOT NULL DEFAULT 1 CHECK (indexable IN (0,1)),
444
+ created_at TEXT NOT NULL,
445
+ updated_at TEXT NOT NULL,
446
+ metadata_json TEXT NOT NULL DEFAULT '{}'
447
+ );
448
+
449
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_search_doc_source
450
+ ON search_doc(project, agent, source_table, source_id)
451
+ WHERE source_table IS NOT NULL AND source_id IS NOT NULL;
452
+
453
+ CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5(
454
+ title,
455
+ body,
456
+ summary,
457
+ content='search_doc',
458
+ content_rowid='id'
459
+ );
460
+
461
+ CREATE TRIGGER IF NOT EXISTS search_doc_ai AFTER INSERT ON search_doc
462
+ WHEN new.sensitive = 0 AND new.indexable = 1
463
+ BEGIN
464
+ INSERT INTO search_fts(rowid, title, body, summary)
465
+ VALUES (new.id, new.title, new.body, new.summary);
466
+ END;
467
+
468
+ CREATE TRIGGER IF NOT EXISTS search_doc_ad AFTER DELETE ON search_doc
469
+ WHEN old.sensitive = 0 AND old.indexable = 1
470
+ BEGIN
471
+ INSERT INTO search_fts(search_fts, rowid, title, body, summary)
472
+ VALUES ('delete', old.id, old.title, old.body, old.summary);
473
+ END;
474
+
475
+ CREATE TRIGGER IF NOT EXISTS search_doc_au AFTER UPDATE ON search_doc
476
+ BEGIN
477
+ INSERT INTO search_fts(search_fts, rowid, title, body, summary)
478
+ SELECT 'delete', old.id, old.title, old.body, old.summary
479
+ WHERE old.sensitive = 0 AND old.indexable = 1;
480
+
481
+ INSERT INTO search_fts(rowid, title, body, summary)
482
+ SELECT new.id, new.title, new.body, new.summary
483
+ WHERE new.sensitive = 0 AND new.indexable = 1;
484
+ END;
485
+ """
486
+
487
+
488
+ def schema_checksum() -> str:
489
+ return sha256_text(schema_sql())
490
+
491
+
492
+ def connect(db_path: str, create_parent: bool = False) -> sqlite3.Connection:
493
+ if create_parent:
494
+ parent = Path(db_path).parent
495
+ if str(parent) and str(parent) != ".":
496
+ parent.mkdir(parents=True, exist_ok=True)
497
+ conn = sqlite3.connect(db_path)
498
+ conn.row_factory = sqlite3.Row
499
+ conn.execute("PRAGMA foreign_keys = ON")
500
+ conn.execute("PRAGMA busy_timeout = 5000")
501
+ conn.execute("PRAGMA journal_mode = WAL")
502
+ return conn
503
+
504
+
505
+ def ensure_schema(conn: sqlite3.Connection) -> bool:
506
+ conn.executescript(schema_sql())
507
+ existing = conn.execute(
508
+ "SELECT id FROM schema_migration WHERE version = ?",
509
+ (SCHEMA_VERSION,),
510
+ ).fetchone()
511
+ if existing:
512
+ return False
513
+ conn.execute(
514
+ """
515
+ INSERT INTO schema_migration (version, name, checksum, applied_at)
516
+ VALUES (?, ?, ?, ?)
517
+ """,
518
+ (SCHEMA_VERSION, MIGRATION_NAME, schema_checksum(), utc_now()),
519
+ )
520
+ return True
521
+
522
+
523
+ def write_receipt(
524
+ conn: sqlite3.Connection,
525
+ operation: str,
526
+ payload: dict[str, Any],
527
+ status: str = "ok",
528
+ ) -> str:
529
+ receipt_key = f"wr_{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}_{uuid.uuid4().hex[:8]}"
530
+ conn.execute(
531
+ """
532
+ INSERT INTO memory_receipt (receipt_key, operation, status, payload_json, created_at)
533
+ VALUES (?, ?, ?, ?, ?)
534
+ """,
535
+ (receipt_key, operation, status, json_dump(payload), utc_now()),
536
+ )
537
+ return receipt_key
538
+
539
+
540
+ def require_schema(conn: sqlite3.Connection) -> None:
541
+ row = conn.execute(
542
+ "SELECT version, checksum FROM schema_migration WHERE version = ?",
543
+ (SCHEMA_VERSION,),
544
+ ).fetchone()
545
+ if not row:
546
+ raise CliError(
547
+ "SCHEMA_MISSING",
548
+ "memory database is missing the required schema version",
549
+ "run: rose-memory init --db memory/memory.db",
550
+ )
551
+ if row["checksum"] != schema_checksum():
552
+ raise CliError(
553
+ "SCHEMA_CHECKSUM_MISMATCH",
554
+ "schema checksum does not match this rose-memory CLI version",
555
+ "run: rose-memory migrate --db memory/memory.db",
556
+ )
557
+
558
+
559
+ def table_exists(conn: sqlite3.Connection, table_name: str) -> bool:
560
+ row = conn.execute(
561
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?",
562
+ (table_name,),
563
+ ).fetchone()
564
+ return row is not None
565
+
566
+
567
+ def dedupe_search_docs(conn: sqlite3.Connection) -> int:
568
+ if not table_exists(conn, "search_doc"):
569
+ return 0
570
+ duplicate_count = conn.execute(
571
+ """
572
+ SELECT COUNT(*)
573
+ FROM search_doc
574
+ WHERE source_table IS NOT NULL
575
+ AND source_id IS NOT NULL
576
+ AND id NOT IN (
577
+ SELECT MAX(id)
578
+ FROM search_doc
579
+ WHERE source_table IS NOT NULL AND source_id IS NOT NULL
580
+ GROUP BY project, agent, source_table, source_id
581
+ )
582
+ """
583
+ ).fetchone()[0]
584
+ conn.execute(
585
+ """
586
+ DELETE FROM search_doc
587
+ WHERE source_table IS NOT NULL
588
+ AND source_id IS NOT NULL
589
+ AND id NOT IN (
590
+ SELECT MAX(id)
591
+ FROM search_doc
592
+ WHERE source_table IS NOT NULL AND source_id IS NOT NULL
593
+ GROUP BY project, agent, source_table, source_id
594
+ )
595
+ """
596
+ )
597
+ return int(duplicate_count)
598
+
599
+
600
+ def rebuild_fts(conn: sqlite3.Connection) -> bool:
601
+ if not table_exists(conn, "search_fts"):
602
+ return False
603
+ conn.execute("INSERT INTO search_fts(search_fts) VALUES ('rebuild')")
604
+ return True
605
+
606
+
607
+ def get_by_key(
608
+ conn: sqlite3.Connection,
609
+ table: str,
610
+ key_column: str,
611
+ key_value: str | None,
612
+ ) -> sqlite3.Row | None:
613
+ if not key_value:
614
+ return None
615
+ return conn.execute(
616
+ f"SELECT * FROM {table} WHERE {key_column} = ?",
617
+ (key_value,),
618
+ ).fetchone()
619
+
620
+
621
+ def require_by_id(conn: sqlite3.Connection, table: str, row_id: int) -> sqlite3.Row:
622
+ row = conn.execute(f"SELECT * FROM {table} WHERE id = ?", (row_id,)).fetchone()
623
+ if not row:
624
+ raise CliError("NOT_FOUND", f"{table} id {row_id} does not exist")
625
+ return row
626
+
627
+
628
+ def require_session(conn: sqlite3.Connection, session_key: str) -> sqlite3.Row:
629
+ row = get_by_key(conn, "session", "session_key", session_key)
630
+ if not row:
631
+ raise CliError("SESSION_NOT_FOUND", f"session {session_key!r} does not exist")
632
+ return row
633
+
634
+
635
+ def optional_task(conn: sqlite3.Connection, task_key: str | None) -> sqlite3.Row | None:
636
+ return get_by_key(conn, "task", "task_key", task_key)
637
+
638
+
639
+ def index_search_doc(
640
+ conn: sqlite3.Connection,
641
+ project: str,
642
+ agent: str,
643
+ doc_type: str,
644
+ source_table: str,
645
+ source_id: int,
646
+ title: str,
647
+ body: str,
648
+ summary: str,
649
+ sensitive: int,
650
+ indexable: int,
651
+ metadata: dict[str, Any] | None = None,
652
+ ) -> int | None:
653
+ conn.execute(
654
+ """
655
+ DELETE FROM search_doc
656
+ WHERE project = ? AND agent = ? AND source_table = ? AND source_id = ?
657
+ """,
658
+ (project, agent, source_table, source_id),
659
+ )
660
+ if sensitive or not indexable:
661
+ return None
662
+ now = utc_now()
663
+ cursor = conn.execute(
664
+ """
665
+ INSERT INTO search_doc (
666
+ project, agent, doc_type, source_table, source_id, title, body, summary,
667
+ sensitive, indexable, created_at, updated_at, metadata_json
668
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
669
+ """,
670
+ (
671
+ project,
672
+ agent,
673
+ doc_type,
674
+ source_table,
675
+ source_id,
676
+ title,
677
+ body,
678
+ summary,
679
+ sensitive,
680
+ indexable,
681
+ now,
682
+ now,
683
+ json_dump(metadata or {}),
684
+ ),
685
+ )
686
+ return int(cursor.lastrowid)
687
+
688
+
689
+ def add_evidence_row(
690
+ conn: sqlite3.Connection,
691
+ session_id: int | None,
692
+ task_id: int | None,
693
+ project: str,
694
+ agent: str,
695
+ source_type: str,
696
+ source_id: int | None,
697
+ evidence_key: str,
698
+ evidence_text: str,
699
+ weight: int,
700
+ sensitive: int,
701
+ indexable: int,
702
+ metadata_json: str = "{}",
703
+ ) -> int:
704
+ evidence_hash = sha256_text(evidence_text)
705
+ cursor = conn.execute(
706
+ """
707
+ INSERT INTO evidence (
708
+ session_id, task_id, project, agent, source_type, source_id, evidence_key,
709
+ evidence_text, evidence_hash, weight, sensitive, indexable, created_at, metadata_json
710
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
711
+ """,
712
+ (
713
+ session_id,
714
+ task_id,
715
+ project,
716
+ agent,
717
+ source_type,
718
+ source_id,
719
+ evidence_key,
720
+ evidence_text,
721
+ evidence_hash,
722
+ weight,
723
+ sensitive,
724
+ indexable,
725
+ utc_now(),
726
+ metadata_json,
727
+ ),
728
+ )
729
+ evidence_id = int(cursor.lastrowid)
730
+ index_search_doc(
731
+ conn,
732
+ project,
733
+ agent,
734
+ "evidence",
735
+ "evidence",
736
+ evidence_id,
737
+ evidence_key,
738
+ evidence_text,
739
+ evidence_text[:240],
740
+ sensitive,
741
+ indexable,
742
+ )
743
+ return evidence_id
744
+
745
+
746
+ def write_command(
747
+ args: argparse.Namespace,
748
+ operation: str,
749
+ handler: Callable[[sqlite3.Connection], dict[str, Any]],
750
+ create_parent: bool = False,
751
+ ) -> None:
752
+ try:
753
+ conn = connect(args.db, create_parent=create_parent)
754
+ with conn:
755
+ if operation not in {"init", "migrate"}:
756
+ require_schema(conn)
757
+ result = handler(conn)
758
+ receipt_key = write_receipt(conn, operation, result)
759
+ conn.close()
760
+ emit({"ok": True, "receipt_id": receipt_key, "operation": operation, "result": result})
761
+ except CliError as exc:
762
+ emit(
763
+ {
764
+ "ok": False,
765
+ "error_code": exc.error_code,
766
+ "message": exc.message,
767
+ "suggested_action": exc.suggested_action,
768
+ },
769
+ status=1,
770
+ )
771
+ except sqlite3.Error as exc:
772
+ emit(
773
+ {
774
+ "ok": False,
775
+ "error_code": "SQLITE_ERROR",
776
+ "message": str(exc),
777
+ "suggested_action": "run doctor or inspect the database schema",
778
+ },
779
+ status=1,
780
+ )
781
+
782
+
783
+ def read_command(
784
+ args: argparse.Namespace,
785
+ handler: Callable[[sqlite3.Connection], dict[str, Any]],
786
+ ) -> None:
787
+ try:
788
+ if not Path(args.db).exists():
789
+ raise CliError("DB_MISSING", f"database {args.db!r} does not exist")
790
+ conn = connect(args.db)
791
+ require_schema(conn)
792
+ result = handler(conn)
793
+ conn.close()
794
+ emit({"ok": True, "result": result})
795
+ except CliError as exc:
796
+ emit(
797
+ {
798
+ "ok": False,
799
+ "error_code": exc.error_code,
800
+ "message": exc.message,
801
+ "suggested_action": exc.suggested_action,
802
+ },
803
+ status=1,
804
+ )
805
+ except sqlite3.Error as exc:
806
+ emit(
807
+ {
808
+ "ok": False,
809
+ "error_code": "SQLITE_ERROR",
810
+ "message": str(exc),
811
+ "suggested_action": "run doctor or inspect the database schema",
812
+ },
813
+ status=1,
814
+ )
815
+
816
+
817
+ def command_init(args: argparse.Namespace) -> None:
818
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
819
+ created = ensure_schema(conn)
820
+ session_count = conn.execute("SELECT COUNT(*) AS c FROM session").fetchone()["c"]
821
+ if session_count == 0:
822
+ now = utc_now()
823
+ conn.execute(
824
+ """
825
+ INSERT INTO session (session_key, project, agent, started_at, metadata_json)
826
+ VALUES (?, ?, ?, ?, ?)
827
+ """,
828
+ ("bootstrap", args.project, args.agent, now, json_dump({"source": "init"})),
829
+ )
830
+ session_id = conn.execute(
831
+ "SELECT id FROM session WHERE session_key = ?", ("bootstrap",)
832
+ ).fetchone()["id"]
833
+ conn.execute(
834
+ """
835
+ INSERT INTO memory_event (session_id, event_type, state, summary, payload_json, created_at)
836
+ VALUES (?, ?, ?, ?, ?, ?)
837
+ """,
838
+ (
839
+ session_id,
840
+ "CHECKPOINT",
841
+ "ACTIVE",
842
+ "Initial memory checkpoint.",
843
+ json_dump({"source": "init"}),
844
+ now,
845
+ ),
846
+ )
847
+ return {
848
+ "db": args.db,
849
+ "schema_version": SCHEMA_VERSION,
850
+ "schema_created": created,
851
+ "checkpoint_exists": True,
852
+ }
853
+
854
+ write_command(args, "init", handler, create_parent=True)
855
+
856
+
857
+ def command_migrate(args: argparse.Namespace) -> None:
858
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
859
+ deduped_search_docs = dedupe_search_docs(conn)
860
+ conn.executescript(schema_sql())
861
+ fts_rebuilt = rebuild_fts(conn)
862
+ existing = conn.execute(
863
+ "SELECT id, checksum FROM schema_migration WHERE version = ?",
864
+ (SCHEMA_VERSION,),
865
+ ).fetchone()
866
+ now = utc_now()
867
+ if existing:
868
+ updated = existing["checksum"] != schema_checksum()
869
+ conn.execute(
870
+ """
871
+ UPDATE schema_migration
872
+ SET name = ?, checksum = ?, applied_at = ?
873
+ WHERE version = ?
874
+ """,
875
+ (MIGRATION_NAME, schema_checksum(), now, SCHEMA_VERSION),
876
+ )
877
+ else:
878
+ updated = True
879
+ conn.execute(
880
+ """
881
+ INSERT INTO schema_migration (version, name, checksum, applied_at)
882
+ VALUES (?, ?, ?, ?)
883
+ """,
884
+ (SCHEMA_VERSION, MIGRATION_NAME, schema_checksum(), now),
885
+ )
886
+ return {
887
+ "db": args.db,
888
+ "schema_version": SCHEMA_VERSION,
889
+ "migration_updated": updated,
890
+ "deduped_search_docs": deduped_search_docs,
891
+ "fts_rebuilt": fts_rebuilt,
892
+ }
893
+
894
+ write_command(args, "migrate", handler, create_parent=True)
895
+
896
+
897
+ def doctor_checks(conn: sqlite3.Connection) -> dict[str, Any]:
898
+ tables = {
899
+ row["name"]
900
+ for row in conn.execute(
901
+ "SELECT name FROM sqlite_master WHERE type IN ('table','view')"
902
+ ).fetchall()
903
+ }
904
+ missing = sorted(REQUIRED_TABLES - tables)
905
+ migrations = [row_to_dict(row) for row in conn.execute("SELECT * FROM schema_migration").fetchall()]
906
+ fk_errors = [dict(row) for row in conn.execute("PRAGMA foreign_key_check").fetchall()]
907
+ integrity = conn.execute("PRAGMA integrity_check").fetchone()[0]
908
+ foreign_keys = conn.execute("PRAGMA foreign_keys").fetchone()[0]
909
+ journal_mode = conn.execute("PRAGMA journal_mode").fetchone()[0]
910
+ sensitive_indexed = conn.execute(
911
+ "SELECT COUNT(*) FROM search_doc WHERE sensitive = 1 AND indexable = 1"
912
+ ).fetchone()[0]
913
+ orphan_rule_evidence = conn.execute(
914
+ """
915
+ SELECT COUNT(*)
916
+ FROM rule_candidate_evidence rce
917
+ LEFT JOIN evidence e ON e.id = rce.evidence_id
918
+ WHERE e.id IS NULL
919
+ """
920
+ ).fetchone()[0]
921
+ stale_approved = conn.execute(
922
+ """
923
+ SELECT COUNT(*)
924
+ FROM rule_patch
925
+ WHERE status = 'approved'
926
+ AND target_path = ?
927
+ AND base_hash != ?
928
+ """,
929
+ (DEFAULT_TARGET, file_hash(DEFAULT_TARGET)),
930
+ ).fetchone()[0]
931
+ status = (
932
+ "ok"
933
+ if not missing
934
+ and not fk_errors
935
+ and integrity == "ok"
936
+ and foreign_keys == 1
937
+ and sensitive_indexed == 0
938
+ and orphan_rule_evidence == 0
939
+ and stale_approved == 0
940
+ else "error"
941
+ )
942
+ return {
943
+ "status": status,
944
+ "schema_version": SCHEMA_VERSION,
945
+ "missing_tables": missing,
946
+ "migrations": migrations,
947
+ "foreign_keys_enabled": bool(foreign_keys),
948
+ "journal_mode": journal_mode,
949
+ "integrity_check": integrity,
950
+ "foreign_key_errors": fk_errors,
951
+ "sensitive_indexable_violations": sensitive_indexed,
952
+ "orphan_rule_candidate_evidence": orphan_rule_evidence,
953
+ "stale_approved_patches": stale_approved,
954
+ }
955
+
956
+
957
+ def command_doctor(args: argparse.Namespace) -> None:
958
+ try:
959
+ if not Path(args.db).exists():
960
+ raise CliError("DB_MISSING", f"database {args.db!r} does not exist")
961
+ conn = connect(args.db)
962
+ checks = doctor_checks(conn)
963
+ if args.record:
964
+ with conn:
965
+ audit_key = f"audit_{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}_{uuid.uuid4().hex[:8]}"
966
+ conn.execute(
967
+ """
968
+ INSERT INTO audit_run (audit_key, operation, status, checks_json, created_at)
969
+ VALUES (?, ?, ?, ?, ?)
970
+ """,
971
+ (audit_key, "doctor", checks["status"], json_dump(checks), utc_now()),
972
+ )
973
+ receipt_key = write_receipt(conn, "doctor", {"audit_key": audit_key, "checks": checks})
974
+ conn.close()
975
+ emit({"ok": checks["status"] == "ok", "receipt_id": receipt_key, "result": checks})
976
+ conn.close()
977
+ emit({"ok": checks["status"] == "ok", "result": checks}, status=0 if checks["status"] == "ok" else 1)
978
+ except CliError as exc:
979
+ emit(
980
+ {
981
+ "ok": False,
982
+ "error_code": exc.error_code,
983
+ "message": exc.message,
984
+ "suggested_action": exc.suggested_action,
985
+ },
986
+ status=1,
987
+ )
988
+ except sqlite3.Error as exc:
989
+ emit({"ok": False, "error_code": "SQLITE_ERROR", "message": str(exc)}, status=1)
990
+
991
+
992
+ def command_session_start(args: argparse.Namespace) -> None:
993
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
994
+ session_key = args.session_key or f"session_{uuid.uuid4().hex[:12]}"
995
+ now = utc_now()
996
+ conn.execute(
997
+ """
998
+ INSERT INTO session (session_key, project, agent, started_at, metadata_json)
999
+ VALUES (?, ?, ?, ?, ?)
1000
+ """,
1001
+ (session_key, args.project, args.agent, now, safe_metadata(args.metadata_json)),
1002
+ )
1003
+ session_id = conn.execute(
1004
+ "SELECT id FROM session WHERE session_key = ?", (session_key,)
1005
+ ).fetchone()["id"]
1006
+ conn.execute(
1007
+ """
1008
+ INSERT INTO memory_event (session_id, event_type, state, summary, payload_json, created_at)
1009
+ VALUES (?, 'CHECKPOINT', 'ACTIVE', ?, ?, ?)
1010
+ """,
1011
+ (session_id, "Session started.", json_dump({"session_key": session_key}), now),
1012
+ )
1013
+ return {"session_id": session_id, "session_key": session_key}
1014
+
1015
+ write_command(args, "session.start", handler)
1016
+
1017
+
1018
+ def command_session_end(args: argparse.Namespace) -> None:
1019
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1020
+ session = require_session(conn, args.session_key)
1021
+ now = utc_now()
1022
+ conn.execute("UPDATE session SET ended_at = ? WHERE id = ?", (now, session["id"]))
1023
+ conn.execute(
1024
+ """
1025
+ INSERT INTO memory_event (session_id, event_type, state, summary, payload_json, created_at)
1026
+ VALUES (?, 'CHECKPOINT', 'IDLE', ?, ?, ?)
1027
+ """,
1028
+ (session["id"], args.summary or "Session ended.", json_dump({}), now),
1029
+ )
1030
+ return {"session_id": session["id"], "session_key": args.session_key, "ended_at": now}
1031
+
1032
+ write_command(args, "session.end", handler)
1033
+
1034
+
1035
+ def command_task_start(args: argparse.Namespace) -> None:
1036
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1037
+ session = require_session(conn, args.session_key)
1038
+ task_key = args.task_key or f"task_{uuid.uuid4().hex[:12]}"
1039
+ now = utc_now()
1040
+ conn.execute(
1041
+ """
1042
+ INSERT INTO task (
1043
+ task_key, session_id, project, agent, title, scope_text, status, started_at, metadata_json
1044
+ ) VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?)
1045
+ """,
1046
+ (
1047
+ task_key,
1048
+ session["id"],
1049
+ args.project,
1050
+ args.agent,
1051
+ args.title,
1052
+ args.scope_text or "",
1053
+ now,
1054
+ safe_metadata(args.metadata_json),
1055
+ ),
1056
+ )
1057
+ task_id = conn.execute("SELECT id FROM task WHERE task_key = ?", (task_key,)).fetchone()["id"]
1058
+ conn.execute(
1059
+ """
1060
+ INSERT INTO memory_event (session_id, task_id, event_type, state, summary, payload_json, created_at)
1061
+ VALUES (?, ?, 'CHECKPOINT', 'ACTIVE', ?, ?, ?)
1062
+ """,
1063
+ (session["id"], task_id, f"Task started: {args.title}", json_dump({"task_key": task_key}), now),
1064
+ )
1065
+ return {"task_id": task_id, "task_key": task_key, "session_id": session["id"]}
1066
+
1067
+ write_command(args, "task.start", handler)
1068
+
1069
+
1070
+ def command_task_update(args: argparse.Namespace) -> None:
1071
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1072
+ task = get_by_key(conn, "task", "task_key", args.task_key)
1073
+ if not task:
1074
+ raise CliError("TASK_NOT_FOUND", f"task {args.task_key!r} does not exist")
1075
+ status = args.status or task["status"]
1076
+ conn.execute(
1077
+ """
1078
+ UPDATE task
1079
+ SET title = COALESCE(?, title),
1080
+ scope_text = COALESCE(?, scope_text),
1081
+ status = ?,
1082
+ outcome = COALESCE(?, outcome),
1083
+ metadata_json = CASE WHEN ? IS NULL THEN metadata_json ELSE ? END
1084
+ WHERE id = ?
1085
+ """,
1086
+ (
1087
+ args.title,
1088
+ args.scope_text,
1089
+ status,
1090
+ args.outcome,
1091
+ args.metadata_json,
1092
+ safe_metadata(args.metadata_json) if args.metadata_json else None,
1093
+ task["id"],
1094
+ ),
1095
+ )
1096
+ return {"task_id": task["id"], "task_key": args.task_key, "status": status}
1097
+
1098
+ write_command(args, "task.update", handler)
1099
+
1100
+
1101
+ def command_task_complete(args: argparse.Namespace) -> None:
1102
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1103
+ task = get_by_key(conn, "task", "task_key", args.task_key)
1104
+ if not task:
1105
+ raise CliError("TASK_NOT_FOUND", f"task {args.task_key!r} does not exist")
1106
+ now = utc_now()
1107
+ conn.execute(
1108
+ """
1109
+ UPDATE task
1110
+ SET status = 'completed', completed_at = ?, outcome = ?
1111
+ WHERE id = ?
1112
+ """,
1113
+ (now, args.outcome or task["outcome"], task["id"]),
1114
+ )
1115
+ conn.execute(
1116
+ """
1117
+ INSERT INTO memory_event (session_id, task_id, event_type, state, summary, payload_json, created_at)
1118
+ VALUES (?, ?, 'COMPLETE', 'IDLE', ?, ?, ?)
1119
+ """,
1120
+ (task["session_id"], task["id"], args.summary or "Task completed.", json_dump({}), now),
1121
+ )
1122
+ return {"task_id": task["id"], "task_key": args.task_key, "completed_at": now}
1123
+
1124
+ write_command(args, "task.complete", handler)
1125
+
1126
+
1127
+ def command_event_add(args: argparse.Namespace) -> None:
1128
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1129
+ session = require_session(conn, args.session_key) if args.session_key else None
1130
+ task = optional_task(conn, args.task_key)
1131
+ if args.task_key and not task:
1132
+ raise CliError("TASK_NOT_FOUND", f"task {args.task_key!r} does not exist")
1133
+ cursor = conn.execute(
1134
+ """
1135
+ INSERT INTO memory_event (session_id, task_id, event_type, state, summary, payload_json, created_at)
1136
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1137
+ """,
1138
+ (
1139
+ session["id"] if session else None,
1140
+ task["id"] if task else None,
1141
+ args.event_type,
1142
+ args.state,
1143
+ args.summary or "",
1144
+ safe_metadata(args.payload_json),
1145
+ utc_now(),
1146
+ ),
1147
+ )
1148
+ return {"event_id": int(cursor.lastrowid), "event_type": args.event_type, "state": args.state}
1149
+
1150
+ write_command(args, "event.add", handler)
1151
+
1152
+
1153
+ def command_fact_add(args: argparse.Namespace) -> None:
1154
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1155
+ session = require_session(conn, args.session_key) if args.session_key else None
1156
+ sensitive, indexable = sensitive_indexable(args.sensitive, args.indexable)
1157
+ fact_key = args.fact_key or normalize_rule_key(args.fact_text)
1158
+ now = utc_now()
1159
+ conn.execute(
1160
+ """
1161
+ INSERT INTO memory_fact (
1162
+ session_id, project, agent, fact_key, fact_text, category, confidence, status,
1163
+ sensitive, indexable, first_seen_at, last_seen_at, metadata_json
1164
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?)
1165
+ ON CONFLICT(project, agent, fact_key) DO UPDATE SET
1166
+ fact_text = excluded.fact_text,
1167
+ category = excluded.category,
1168
+ confidence = excluded.confidence,
1169
+ sensitive = excluded.sensitive,
1170
+ indexable = excluded.indexable,
1171
+ last_seen_at = excluded.last_seen_at,
1172
+ metadata_json = excluded.metadata_json
1173
+ """,
1174
+ (
1175
+ session["id"] if session else None,
1176
+ args.project,
1177
+ args.agent,
1178
+ fact_key,
1179
+ args.fact_text,
1180
+ args.category,
1181
+ args.confidence,
1182
+ sensitive,
1183
+ indexable,
1184
+ now,
1185
+ now,
1186
+ safe_metadata(args.metadata_json),
1187
+ ),
1188
+ )
1189
+ fact = conn.execute(
1190
+ "SELECT * FROM memory_fact WHERE project = ? AND agent = ? AND fact_key = ?",
1191
+ (args.project, args.agent, fact_key),
1192
+ ).fetchone()
1193
+ index_search_doc(
1194
+ conn,
1195
+ args.project,
1196
+ args.agent,
1197
+ "memory_fact",
1198
+ "memory_fact",
1199
+ fact["id"],
1200
+ fact_key,
1201
+ args.fact_text,
1202
+ args.fact_text[:240],
1203
+ sensitive,
1204
+ indexable,
1205
+ )
1206
+ return {"fact_id": fact["id"], "fact_key": fact_key}
1207
+
1208
+ write_command(args, "fact.add", handler)
1209
+
1210
+
1211
+ def command_claim_add(args: argparse.Namespace) -> None:
1212
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1213
+ session = require_session(conn, args.session_key) if args.session_key else None
1214
+ task = optional_task(conn, args.task_key)
1215
+ if args.task_key and not task:
1216
+ raise CliError("TASK_NOT_FOUND", f"task {args.task_key!r} does not exist")
1217
+ sensitive, indexable = sensitive_indexable(args.sensitive, args.indexable)
1218
+ claim_key = args.claim_key or normalize_rule_key(args.claim_text)
1219
+ now = utc_now()
1220
+ conn.execute(
1221
+ """
1222
+ INSERT INTO claim (
1223
+ session_id, task_id, project, agent, claim_key, claim_text, verification_status,
1224
+ evidence_summary, sensitive, indexable, created_at, updated_at, metadata_json
1225
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1226
+ ON CONFLICT(project, agent, claim_key) DO UPDATE SET
1227
+ claim_text = excluded.claim_text,
1228
+ verification_status = excluded.verification_status,
1229
+ evidence_summary = excluded.evidence_summary,
1230
+ sensitive = excluded.sensitive,
1231
+ indexable = excluded.indexable,
1232
+ updated_at = excluded.updated_at,
1233
+ metadata_json = excluded.metadata_json
1234
+ """,
1235
+ (
1236
+ session["id"] if session else None,
1237
+ task["id"] if task else None,
1238
+ args.project,
1239
+ args.agent,
1240
+ claim_key,
1241
+ args.claim_text,
1242
+ args.verification_status,
1243
+ args.evidence_summary or "",
1244
+ sensitive,
1245
+ indexable,
1246
+ now,
1247
+ now,
1248
+ safe_metadata(args.metadata_json),
1249
+ ),
1250
+ )
1251
+ claim = conn.execute(
1252
+ "SELECT * FROM claim WHERE project = ? AND agent = ? AND claim_key = ?",
1253
+ (args.project, args.agent, claim_key),
1254
+ ).fetchone()
1255
+ index_search_doc(
1256
+ conn,
1257
+ args.project,
1258
+ args.agent,
1259
+ "claim",
1260
+ "claim",
1261
+ claim["id"],
1262
+ claim_key,
1263
+ args.claim_text,
1264
+ args.evidence_summary or args.claim_text[:240],
1265
+ sensitive,
1266
+ indexable,
1267
+ )
1268
+ return {"claim_id": claim["id"], "claim_key": claim_key}
1269
+
1270
+ write_command(args, "claim.add", handler)
1271
+
1272
+
1273
+ def command_finding_add(args: argparse.Namespace) -> None:
1274
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1275
+ session = require_session(conn, args.session_key)
1276
+ task = optional_task(conn, args.task_key)
1277
+ if args.task_key and not task:
1278
+ raise CliError("TASK_NOT_FOUND", f"task {args.task_key!r} does not exist")
1279
+ sensitive, indexable = sensitive_indexable(args.sensitive, args.indexable)
1280
+ now = utc_now()
1281
+ cursor = conn.execute(
1282
+ """
1283
+ INSERT INTO finding (
1284
+ session_id, task_id, project, agent, kind, category, severity, wrong_behavior,
1285
+ correct_behavior, summary, raw_ref, sensitive, indexable, created_at, metadata_json
1286
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1287
+ """,
1288
+ (
1289
+ session["id"],
1290
+ task["id"] if task else None,
1291
+ args.project,
1292
+ args.agent,
1293
+ args.kind,
1294
+ args.category,
1295
+ args.severity,
1296
+ args.wrong_behavior or "",
1297
+ args.correct_behavior or "",
1298
+ args.summary,
1299
+ args.raw_ref or "",
1300
+ sensitive,
1301
+ indexable,
1302
+ now,
1303
+ safe_metadata(args.metadata_json),
1304
+ ),
1305
+ )
1306
+ finding_id = int(cursor.lastrowid)
1307
+ evidence_text = "\n".join(
1308
+ part
1309
+ for part in [
1310
+ args.summary,
1311
+ f"Wrong behavior: {args.wrong_behavior}" if args.wrong_behavior else "",
1312
+ f"Correct behavior: {args.correct_behavior}" if args.correct_behavior else "",
1313
+ f"Raw ref: {args.raw_ref}" if args.raw_ref else "",
1314
+ ]
1315
+ if part
1316
+ )
1317
+ evidence_key = args.evidence_key or f"finding:{finding_id}:{sha256_text(evidence_text)[:12]}"
1318
+ evidence_id = add_evidence_row(
1319
+ conn,
1320
+ session["id"],
1321
+ task["id"] if task else None,
1322
+ args.project,
1323
+ args.agent,
1324
+ "finding",
1325
+ finding_id,
1326
+ evidence_key,
1327
+ evidence_text,
1328
+ 1,
1329
+ sensitive,
1330
+ indexable,
1331
+ json_dump({"finding_id": finding_id}),
1332
+ )
1333
+ index_search_doc(
1334
+ conn,
1335
+ args.project,
1336
+ args.agent,
1337
+ "finding",
1338
+ "finding",
1339
+ finding_id,
1340
+ args.category,
1341
+ evidence_text,
1342
+ args.summary,
1343
+ sensitive,
1344
+ indexable,
1345
+ )
1346
+ return {"finding_id": finding_id, "evidence_id": evidence_id}
1347
+
1348
+ write_command(args, "finding.add", handler)
1349
+
1350
+
1351
+ def command_evidence_add(args: argparse.Namespace) -> None:
1352
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1353
+ session = require_session(conn, args.session_key) if args.session_key else None
1354
+ task = optional_task(conn, args.task_key)
1355
+ if args.task_key and not task:
1356
+ raise CliError("TASK_NOT_FOUND", f"task {args.task_key!r} does not exist")
1357
+ sensitive, indexable = sensitive_indexable(args.sensitive, args.indexable)
1358
+ evidence_key = args.evidence_key or f"{args.source_type}:{sha256_text(args.evidence_text)[:16]}"
1359
+ evidence_id = add_evidence_row(
1360
+ conn,
1361
+ session["id"] if session else None,
1362
+ task["id"] if task else None,
1363
+ args.project,
1364
+ args.agent,
1365
+ args.source_type,
1366
+ args.source_id,
1367
+ evidence_key,
1368
+ args.evidence_text,
1369
+ args.weight,
1370
+ sensitive,
1371
+ indexable,
1372
+ safe_metadata(args.metadata_json),
1373
+ )
1374
+ return {"evidence_id": evidence_id, "evidence_key": evidence_key}
1375
+
1376
+ write_command(args, "evidence.add", handler)
1377
+
1378
+
1379
+ def recompute_rule(conn: sqlite3.Connection, rule_id: int) -> dict[str, Any]:
1380
+ rule = require_by_id(conn, "rule_candidate", rule_id)
1381
+ summary = conn.execute(
1382
+ """
1383
+ SELECT
1384
+ COALESCE(SUM(rce.weight), 0) AS score,
1385
+ COUNT(rce.id) AS evidence_count,
1386
+ COUNT(rce.id) AS mention_count,
1387
+ COUNT(DISTINCT e.session_id) AS session_count
1388
+ FROM rule_candidate_evidence rce
1389
+ JOIN evidence e ON e.id = rce.evidence_id
1390
+ WHERE rce.rule_id = ?
1391
+ """,
1392
+ (rule_id,),
1393
+ ).fetchone()
1394
+ score = int(summary["score"] or 0)
1395
+ evidence_count = int(summary["evidence_count"] or 0)
1396
+ mention_count = int(summary["mention_count"] or 0)
1397
+ session_count = int(summary["session_count"] or 0)
1398
+ current_status = rule["status"]
1399
+ status = current_status
1400
+ if current_status not in {"approved", "promoted", "rejected", "superseded", "expired"}:
1401
+ if rule["conflicts_json"] and rule["conflicts_json"] != "[]":
1402
+ status = "needs_reconciliation"
1403
+ elif score >= 5 and session_count >= 2 and evidence_count >= 1:
1404
+ status = "candidate"
1405
+ elif rule["severity"] in {"high", "critical"}:
1406
+ status = "candidate"
1407
+ else:
1408
+ status = "memory"
1409
+ conn.execute(
1410
+ """
1411
+ UPDATE rule_candidate
1412
+ SET score = ?, mention_count = ?, session_count = ?, evidence_count = ?,
1413
+ status = ?, last_seen_at = ?
1414
+ WHERE id = ?
1415
+ """,
1416
+ (score, mention_count, session_count, evidence_count, status, utc_now(), rule_id),
1417
+ )
1418
+ return {
1419
+ "score": score,
1420
+ "mention_count": mention_count,
1421
+ "session_count": session_count,
1422
+ "evidence_count": evidence_count,
1423
+ "status": status,
1424
+ }
1425
+
1426
+
1427
+ def command_rule_observe(args: argparse.Namespace) -> None:
1428
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1429
+ evidence = require_by_id(conn, "evidence", args.evidence_id)
1430
+ rule_key = args.rule_key or normalize_rule_key(args.rule_text)
1431
+ normalized_hash = sha256_text(normalize_rule_text(args.rule_text))
1432
+ existing = conn.execute(
1433
+ "SELECT * FROM rule_candidate WHERE project = ? AND agent = ? AND rule_key = ?",
1434
+ (args.project, args.agent, rule_key),
1435
+ ).fetchone()
1436
+ status_before = existing["status"] if existing else None
1437
+ score_before = existing["score"] if existing else 0
1438
+ now = utc_now()
1439
+ severity = args.severity
1440
+ if existing and SEVERITY_RANK[existing["severity"]] > SEVERITY_RANK[severity]:
1441
+ severity = existing["severity"]
1442
+ if existing:
1443
+ conn.execute(
1444
+ """
1445
+ UPDATE rule_candidate
1446
+ SET normalized_rule_hash = ?, rule_text = ?, category = ?, severity = ?, last_seen_at = ?
1447
+ WHERE id = ?
1448
+ """,
1449
+ (normalized_hash, args.rule_text, args.category, severity, now, existing["id"]),
1450
+ )
1451
+ rule_id = existing["id"]
1452
+ created = False
1453
+ else:
1454
+ cursor = conn.execute(
1455
+ """
1456
+ INSERT INTO rule_candidate (
1457
+ project, agent, rule_key, normalized_rule_hash, rule_text, category, severity,
1458
+ status, promotion_target, target_scope, first_seen_at, last_seen_at, metadata_json
1459
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'memory', 'AGENTS.md', 'project_root_agents', ?, ?, ?)
1460
+ """,
1461
+ (
1462
+ args.project,
1463
+ args.agent,
1464
+ rule_key,
1465
+ normalized_hash,
1466
+ args.rule_text,
1467
+ args.category,
1468
+ severity,
1469
+ now,
1470
+ now,
1471
+ safe_metadata(args.metadata_json),
1472
+ ),
1473
+ )
1474
+ rule_id = int(cursor.lastrowid)
1475
+ created = True
1476
+ weight = SIGNAL_WEIGHTS[args.signal_type]
1477
+ conn.execute(
1478
+ """
1479
+ INSERT OR IGNORE INTO rule_candidate_evidence (rule_id, evidence_id, signal_type, weight, created_at)
1480
+ VALUES (?, ?, ?, ?, ?)
1481
+ """,
1482
+ (rule_id, evidence["id"], args.signal_type, weight, now),
1483
+ )
1484
+ summary = recompute_rule(conn, rule_id)
1485
+ return {
1486
+ "created": created,
1487
+ "rule_id": rule_id,
1488
+ "rule_key": rule_key,
1489
+ "status_before": status_before,
1490
+ "status_after": summary["status"],
1491
+ "score_before": score_before,
1492
+ "score_after": summary["score"],
1493
+ "session_count": summary["session_count"],
1494
+ "evidence_count": summary["evidence_count"],
1495
+ "requires_user_approval": True,
1496
+ "suggest_project_agents_promotion": summary["score"] >= 7 or severity in {"high", "critical"},
1497
+ }
1498
+
1499
+ write_command(args, "rule.observe", handler)
1500
+
1501
+
1502
+ def command_rule_list(args: argparse.Namespace) -> None:
1503
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1504
+ values: list[Any] = [args.project, args.agent]
1505
+ where = "WHERE project = ? AND agent = ?"
1506
+ if args.status:
1507
+ where += " AND status = ?"
1508
+ values.append(args.status)
1509
+ rows = conn.execute(
1510
+ f"SELECT * FROM rule_candidate {where} ORDER BY last_seen_at DESC LIMIT ?",
1511
+ (*values, args.limit),
1512
+ ).fetchall()
1513
+ return {"rules": [row_to_dict(row) for row in rows]}
1514
+
1515
+ read_command(args, handler)
1516
+
1517
+
1518
+ def build_agents_content(base_content: str, rule_text: str) -> str:
1519
+ rule_line = f"- {rule_text.strip()}"
1520
+ if rule_line in base_content:
1521
+ raise CliError("RULE_ALREADY_PRESENT", "target AGENTS.md already contains this rule")
1522
+ if not base_content.strip():
1523
+ return f"# Agent Instructions\n\n## Rule Promotion\n\n{rule_line}\n"
1524
+ separator = "" if base_content.endswith("\n") else "\n"
1525
+ return f"{base_content}{separator}\n## Rule Promotion\n\n{rule_line}\n"
1526
+
1527
+
1528
+ def unified_patch(old: str, new: str, target: str) -> str:
1529
+ return "".join(
1530
+ difflib.unified_diff(
1531
+ old.splitlines(keepends=True),
1532
+ new.splitlines(keepends=True),
1533
+ fromfile=f"a/{target}",
1534
+ tofile=f"b/{target}",
1535
+ )
1536
+ )
1537
+
1538
+
1539
+ def command_rule_propose(args: argparse.Namespace) -> None:
1540
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1541
+ if args.target != DEFAULT_TARGET:
1542
+ raise CliError("UNSUPPORTED_TARGET", "v1 rule promotion only targets repo root AGENTS.md")
1543
+ rule = require_by_id(conn, "rule_candidate", args.rule_id)
1544
+ if rule["status"] not in {"candidate", "memory", "needs_reconciliation"}:
1545
+ raise CliError("INVALID_RULE_STATUS", f"cannot propose patch from status {rule['status']!r}")
1546
+ if rule["status"] == "needs_reconciliation":
1547
+ raise CliError("NEEDS_RECONCILIATION", "resolve rule conflicts before proposing a patch")
1548
+ base_content = read_text_if_exists(args.target)
1549
+ proposed_content = build_agents_content(base_content, rule["rule_text"])
1550
+ base_hash = sha256_text(base_content)
1551
+ new_hash = sha256_text(proposed_content)
1552
+ patch_text = unified_patch(base_content, proposed_content, args.target)
1553
+ patch_hash = sha256_text(patch_text)
1554
+ now = utc_now()
1555
+ snapshot_cursor = conn.execute(
1556
+ """
1557
+ INSERT INTO instruction_snapshot (
1558
+ target_path, target_scope, content_hash, summary, captured_at, metadata_json
1559
+ ) VALUES (?, 'project_root_agents', ?, ?, ?, ?)
1560
+ """,
1561
+ (
1562
+ args.target,
1563
+ base_hash,
1564
+ f"Snapshot before proposing rule {rule['rule_key']}",
1565
+ now,
1566
+ json_dump({"target_exists": Path(args.target).exists()}),
1567
+ ),
1568
+ )
1569
+ patch_cursor = conn.execute(
1570
+ """
1571
+ INSERT INTO rule_patch (
1572
+ rule_id, target_path, target_scope, base_snapshot_id, base_hash, patch_hash,
1573
+ proposed_rule_hash, patch_text, status, created_at, updated_at, metadata_json
1574
+ ) VALUES (?, ?, 'project_root_agents', ?, ?, ?, ?, ?, 'proposed', ?, ?, ?)
1575
+ """,
1576
+ (
1577
+ rule["id"],
1578
+ args.target,
1579
+ int(snapshot_cursor.lastrowid),
1580
+ base_hash,
1581
+ patch_hash,
1582
+ rule["normalized_rule_hash"],
1583
+ patch_text,
1584
+ now,
1585
+ now,
1586
+ json_dump({"proposed_content_hash": new_hash, "proposed_content": proposed_content}),
1587
+ ),
1588
+ )
1589
+ return {
1590
+ "rule_id": rule["id"],
1591
+ "patch_id": int(patch_cursor.lastrowid),
1592
+ "target_path": args.target,
1593
+ "base_hash": base_hash,
1594
+ "patch_hash": patch_hash,
1595
+ "proposed_content_hash": new_hash,
1596
+ "patch_preview": patch_text,
1597
+ "requires_user_approval": True,
1598
+ }
1599
+
1600
+ write_command(args, "rule.propose", handler)
1601
+
1602
+
1603
+ def command_rule_approve(args: argparse.Namespace) -> None:
1604
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1605
+ patch = require_by_id(conn, "rule_patch", args.patch_id)
1606
+ rule = require_by_id(conn, "rule_candidate", patch["rule_id"])
1607
+ if patch["status"] != "proposed":
1608
+ raise CliError("INVALID_PATCH_STATUS", f"patch status is {patch['status']!r}, expected 'proposed'")
1609
+ if args.patch_hash != patch["patch_hash"]:
1610
+ raise CliError("PATCH_HASH_MISMATCH", "approved patch hash does not match the proposed patch")
1611
+ if args.approval_evidence_id:
1612
+ evidence = require_by_id(conn, "evidence", args.approval_evidence_id)
1613
+ if evidence["source_type"] != "user_confirmation":
1614
+ raise CliError("INVALID_APPROVAL_EVIDENCE", "approval evidence must have source_type='user_confirmation'")
1615
+ now = utc_now()
1616
+ decision_cursor = conn.execute(
1617
+ """
1618
+ INSERT INTO rule_decision (
1619
+ rule_id, patch_id, decision, approved_rule_hash, approved_patch_hash,
1620
+ actor, reason, decided_at, metadata_json
1621
+ ) VALUES (?, ?, 'approved', ?, ?, ?, ?, ?, ?)
1622
+ """,
1623
+ (
1624
+ rule["id"],
1625
+ patch["id"],
1626
+ rule["normalized_rule_hash"],
1627
+ patch["patch_hash"],
1628
+ args.actor,
1629
+ args.reason or "",
1630
+ now,
1631
+ json_dump({"approval_evidence_id": args.approval_evidence_id}),
1632
+ ),
1633
+ )
1634
+ conn.execute("UPDATE rule_patch SET status = 'approved', updated_at = ? WHERE id = ?", (now, patch["id"]))
1635
+ conn.execute("UPDATE rule_candidate SET status = 'approved', approved_at = ? WHERE id = ?", (now, rule["id"]))
1636
+ return {"decision_id": int(decision_cursor.lastrowid), "rule_id": rule["id"], "patch_id": patch["id"]}
1637
+
1638
+ write_command(args, "rule.approve", handler)
1639
+
1640
+
1641
+ def command_rule_reject(args: argparse.Namespace) -> None:
1642
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1643
+ patch = require_by_id(conn, "rule_patch", args.patch_id)
1644
+ rule = require_by_id(conn, "rule_candidate", patch["rule_id"])
1645
+ now = utc_now()
1646
+ decision_cursor = conn.execute(
1647
+ """
1648
+ INSERT INTO rule_decision (rule_id, patch_id, decision, actor, reason, decided_at, metadata_json)
1649
+ VALUES (?, ?, 'rejected', ?, ?, ?, ?)
1650
+ """,
1651
+ (rule["id"], patch["id"], args.actor, args.reason or "", now, "{}"),
1652
+ )
1653
+ conn.execute("UPDATE rule_patch SET status = 'rejected', updated_at = ? WHERE id = ?", (now, patch["id"]))
1654
+ conn.execute("UPDATE rule_candidate SET status = 'rejected' WHERE id = ?", (rule["id"],))
1655
+ return {"decision_id": int(decision_cursor.lastrowid), "rule_id": rule["id"], "patch_id": patch["id"]}
1656
+
1657
+ write_command(args, "rule.reject", handler)
1658
+
1659
+
1660
+ def command_rule_reconcile(args: argparse.Namespace) -> None:
1661
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1662
+ rule = require_by_id(conn, "rule_candidate", args.rule_id)
1663
+ now = utc_now()
1664
+ conn.execute(
1665
+ """
1666
+ UPDATE rule_candidate
1667
+ SET status = ?, conflicts_json = ?, last_seen_at = ?
1668
+ WHERE id = ?
1669
+ """,
1670
+ (args.status, args.conflicts_json or "[]", now, rule["id"]),
1671
+ )
1672
+ return {"rule_id": rule["id"], "status_before": rule["status"], "status_after": args.status}
1673
+
1674
+ write_command(args, "rule.reconcile", handler)
1675
+
1676
+
1677
+ def latest_approved_decision(conn: sqlite3.Connection, patch_id: int) -> sqlite3.Row:
1678
+ decision = conn.execute(
1679
+ """
1680
+ SELECT * FROM rule_decision
1681
+ WHERE patch_id = ? AND decision = 'approved'
1682
+ ORDER BY decided_at DESC, id DESC
1683
+ LIMIT 1
1684
+ """,
1685
+ (patch_id,),
1686
+ ).fetchone()
1687
+ if not decision:
1688
+ raise CliError("APPROVAL_MISSING", "patch has no approved user decision")
1689
+ return decision
1690
+
1691
+
1692
+ def proposed_content_from_patch(patch: sqlite3.Row) -> str:
1693
+ metadata = json.loads(patch["metadata_json"] or "{}")
1694
+ proposed_content = metadata.get("proposed_content")
1695
+ if not isinstance(proposed_content, str):
1696
+ raise CliError("PATCH_CONTENT_MISSING", "rule_patch metadata is missing proposed_content")
1697
+ return proposed_content
1698
+
1699
+
1700
+ def validate_promotion(conn: sqlite3.Connection, patch_id: int) -> tuple[sqlite3.Row, sqlite3.Row, sqlite3.Row, str]:
1701
+ patch = require_by_id(conn, "rule_patch", patch_id)
1702
+ rule = require_by_id(conn, "rule_candidate", patch["rule_id"])
1703
+ decision = latest_approved_decision(conn, patch_id)
1704
+ proposed_content = proposed_content_from_patch(patch)
1705
+ if rule["status"] != "approved":
1706
+ raise CliError("INVALID_RULE_STATUS", f"rule status is {rule['status']!r}, expected 'approved'")
1707
+ if patch["status"] != "approved":
1708
+ raise CliError("INVALID_PATCH_STATUS", f"patch status is {patch['status']!r}, expected 'approved'")
1709
+ if decision["patch_id"] != patch["id"]:
1710
+ raise CliError("APPROVAL_PATCH_MISMATCH", "approval decision does not match patch")
1711
+ if decision["approved_patch_hash"] != patch["patch_hash"]:
1712
+ raise CliError("APPROVAL_PATCH_HASH_MISMATCH", "approved patch hash does not match current patch hash")
1713
+ if decision["approved_rule_hash"] != rule["normalized_rule_hash"]:
1714
+ raise CliError("APPROVAL_RULE_HASH_MISMATCH", "approved rule hash does not match current rule hash")
1715
+ if patch["target_path"] != DEFAULT_TARGET or patch["target_scope"] != "project_root_agents":
1716
+ raise CliError("UNSUPPORTED_TARGET", "v1 promotion only supports repo root AGENTS.md")
1717
+ return patch, rule, decision, proposed_content
1718
+
1719
+
1720
+ def command_rule_promote(args: argparse.Namespace) -> None:
1721
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1722
+ patch, rule, decision, proposed_content = validate_promotion(conn, args.patch_id)
1723
+ current_hash = file_hash(patch["target_path"])
1724
+ new_hash = sha256_text(proposed_content)
1725
+ if args.apply:
1726
+ if current_hash != patch["base_hash"]:
1727
+ conn.execute("UPDATE rule_patch SET status = 'stale', updated_at = ? WHERE id = ?", (utc_now(), patch["id"]))
1728
+ raise CliError("PATCH_STALE", "AGENTS.md changed after the patch was proposed", "run rule propose again")
1729
+ write_text(patch["target_path"], proposed_content)
1730
+ observed_new_hash = file_hash(patch["target_path"])
1731
+ old_target_hash = current_hash
1732
+ else:
1733
+ if current_hash != new_hash:
1734
+ raise CliError("TARGET_HASH_MISMATCH", "target file does not match the approved proposed content")
1735
+ observed_new_hash = current_hash
1736
+ old_target_hash = patch["base_hash"]
1737
+ if observed_new_hash != new_hash:
1738
+ raise CliError("TARGET_HASH_MISMATCH", "applied target hash does not match approved proposed content")
1739
+ now = utc_now()
1740
+ cursor = conn.execute(
1741
+ """
1742
+ INSERT INTO rule_promotion (
1743
+ rule_id, patch_id, decision_id, mode, target_path, old_target_hash, new_target_hash,
1744
+ applied_by, applied_at, metadata_json
1745
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1746
+ """,
1747
+ (
1748
+ rule["id"],
1749
+ patch["id"],
1750
+ decision["id"],
1751
+ "apply" if args.apply else "record_applied",
1752
+ patch["target_path"],
1753
+ old_target_hash,
1754
+ observed_new_hash,
1755
+ args.actor,
1756
+ now,
1757
+ "{}",
1758
+ ),
1759
+ )
1760
+ conn.execute("UPDATE rule_patch SET status = 'applied', updated_at = ? WHERE id = ?", (now, patch["id"]))
1761
+ conn.execute("UPDATE rule_candidate SET status = 'promoted', promoted_at = ? WHERE id = ?", (now, rule["id"]))
1762
+ return {
1763
+ "promotion_id": int(cursor.lastrowid),
1764
+ "rule_id": rule["id"],
1765
+ "patch_id": patch["id"],
1766
+ "mode": "apply" if args.apply else "record_applied",
1767
+ "target_path": patch["target_path"],
1768
+ "new_target_hash": observed_new_hash,
1769
+ }
1770
+
1771
+ write_command(args, "rule.promote", handler)
1772
+
1773
+
1774
+ def command_search(args: argparse.Namespace) -> None:
1775
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1776
+ query = safe_fts_query(args.query)
1777
+ rows = conn.execute(
1778
+ """
1779
+ SELECT d.id, d.doc_type, d.source_table, d.source_id, d.title, d.summary,
1780
+ snippet(search_fts, 1, '[', ']', '...', 12) AS snippet
1781
+ FROM search_fts
1782
+ JOIN search_doc d ON d.id = search_fts.rowid
1783
+ WHERE search_fts MATCH ?
1784
+ AND d.project = ?
1785
+ AND d.agent = ?
1786
+ AND d.sensitive = 0
1787
+ AND d.indexable = 1
1788
+ LIMIT ?
1789
+ """,
1790
+ (query, args.project, args.agent, args.limit),
1791
+ ).fetchall()
1792
+ return {"items": [row_to_dict(row) for row in rows]}
1793
+
1794
+ read_command(args, handler)
1795
+
1796
+
1797
+ def command_pack(args: argparse.Namespace) -> None:
1798
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1799
+ limit = 8 if args.mode == "direct" else 16
1800
+ tokens = query_tokens(args.query)
1801
+ fts_query = safe_fts_query(args.query)
1802
+ rule_clause, rule_values = token_like_clause(["rule_key", "rule_text", "category"], tokens)
1803
+ fact_clause, fact_values = token_like_clause(["fact_key", "fact_text", "category"], tokens)
1804
+ search_rows = conn.execute(
1805
+ """
1806
+ SELECT d.id, d.doc_type, d.source_table, d.source_id, d.title, d.summary,
1807
+ snippet(search_fts, 1, '[', ']', '...', 12) AS snippet
1808
+ FROM search_fts
1809
+ JOIN search_doc d ON d.id = search_fts.rowid
1810
+ WHERE search_fts MATCH ?
1811
+ AND d.project = ?
1812
+ AND d.agent = ?
1813
+ AND d.sensitive = 0
1814
+ AND d.indexable = 1
1815
+ LIMIT ?
1816
+ """,
1817
+ (fts_query, args.project, args.agent, limit),
1818
+ ).fetchall()
1819
+ candidates = conn.execute(
1820
+ f"""
1821
+ SELECT id, rule_key, rule_text, status, score, session_count, evidence_count
1822
+ FROM rule_candidate
1823
+ WHERE project = ? AND agent = ?
1824
+ AND status IN ('candidate','needs_reconciliation','approved','promoted')
1825
+ AND {rule_clause}
1826
+ ORDER BY score DESC, last_seen_at DESC
1827
+ LIMIT ?
1828
+ """,
1829
+ (args.project, args.agent, *rule_values, limit),
1830
+ ).fetchall()
1831
+ facts = conn.execute(
1832
+ f"""
1833
+ SELECT id, fact_key, fact_text, category, confidence
1834
+ FROM memory_fact
1835
+ WHERE project = ? AND agent = ? AND status = 'active' AND sensitive = 0
1836
+ AND {fact_clause}
1837
+ ORDER BY last_seen_at DESC
1838
+ LIMIT ?
1839
+ """,
1840
+ (args.project, args.agent, *fact_values, limit),
1841
+ ).fetchall()
1842
+ return {
1843
+ "mode": args.mode,
1844
+ "budget": args.budget,
1845
+ "items": [{"type": "search_doc", **row_to_dict(row)} for row in search_rows]
1846
+ + [
1847
+ {"type": "rule_candidate", **row_to_dict(row)} for row in candidates
1848
+ ]
1849
+ + [{"type": "memory_fact", **row_to_dict(row)} for row in facts],
1850
+ }
1851
+
1852
+ read_command(args, handler)
1853
+
1854
+
1855
+ def command_receipt_show(args: argparse.Namespace) -> None:
1856
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1857
+ row = conn.execute(
1858
+ "SELECT * FROM memory_receipt WHERE receipt_key = ?",
1859
+ (args.receipt_key,),
1860
+ ).fetchone()
1861
+ if not row:
1862
+ raise CliError("RECEIPT_NOT_FOUND", f"receipt {args.receipt_key!r} does not exist")
1863
+ return {"receipt": row_to_dict(row)}
1864
+
1865
+ read_command(args, handler)
1866
+
1867
+
1868
+ def command_complete(args: argparse.Namespace) -> None:
1869
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1870
+ session = require_session(conn, args.session_key) if args.session_key else None
1871
+ task = optional_task(conn, args.task_key)
1872
+ if args.task_key and not task:
1873
+ raise CliError("TASK_NOT_FOUND", f"task {args.task_key!r} does not exist")
1874
+ now = utc_now()
1875
+ if task:
1876
+ conn.execute(
1877
+ """
1878
+ UPDATE task SET status = 'completed', completed_at = ?, outcome = COALESCE(?, outcome)
1879
+ WHERE id = ?
1880
+ """,
1881
+ (now, args.outcome, task["id"]),
1882
+ )
1883
+ evidence_ids = args.evidence_id or []
1884
+ evidence_refs = args.evidence_ref or []
1885
+ if not evidence_ids and not evidence_refs and not args.no_durable_memory_promoted:
1886
+ raise CliError(
1887
+ "COMPLETION_EVIDENCE_REQUIRED",
1888
+ "complete requires --evidence-id/--evidence-ref or --no-durable-memory-promoted",
1889
+ )
1890
+ for evidence_id in evidence_ids:
1891
+ require_by_id(conn, "evidence", evidence_id)
1892
+ event_cursor = conn.execute(
1893
+ """
1894
+ INSERT INTO memory_event (session_id, task_id, event_type, state, summary, payload_json, created_at)
1895
+ VALUES (?, ?, 'COMPLETE', 'IDLE', ?, ?, ?)
1896
+ """,
1897
+ (
1898
+ session["id"] if session else (task["session_id"] if task else None),
1899
+ task["id"] if task else None,
1900
+ args.summary or "Task complete.",
1901
+ json_dump(
1902
+ {
1903
+ "outcome": args.outcome or "",
1904
+ "evidence_ids": evidence_ids,
1905
+ "evidence_refs": evidence_refs,
1906
+ "no_durable_memory_promoted": args.no_durable_memory_promoted,
1907
+ }
1908
+ ),
1909
+ now,
1910
+ ),
1911
+ )
1912
+ return {"event_id": int(event_cursor.lastrowid), "state": "IDLE"}
1913
+
1914
+ write_command(args, "complete", handler)
1915
+
1916
+
1917
+ def command_remember_requirement(args: argparse.Namespace) -> None:
1918
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1919
+ session = require_session(conn, args.session_key) if args.session_key else None
1920
+ task = optional_task(conn, args.task_key)
1921
+ if args.task_key and not task:
1922
+ raise CliError("TASK_NOT_FOUND", f"task {args.task_key!r} does not exist")
1923
+ fact_key = args.fact_key or normalize_rule_key(args.text)
1924
+ now = utc_now()
1925
+ metadata = {
1926
+ "source": args.source,
1927
+ "task_key": args.task_key or "",
1928
+ "memory_layer": "requirement",
1929
+ }
1930
+ conn.execute(
1931
+ """
1932
+ INSERT INTO memory_fact (
1933
+ session_id, project, agent, fact_key, fact_text, category, confidence, status,
1934
+ sensitive, indexable, first_seen_at, last_seen_at, metadata_json
1935
+ ) VALUES (?, ?, ?, ?, ?, 'requirement', ?, 'active', 0, 1, ?, ?, ?)
1936
+ ON CONFLICT(project, agent, fact_key) DO UPDATE SET
1937
+ fact_text = excluded.fact_text,
1938
+ category = excluded.category,
1939
+ confidence = excluded.confidence,
1940
+ last_seen_at = excluded.last_seen_at,
1941
+ metadata_json = excluded.metadata_json
1942
+ """,
1943
+ (
1944
+ session["id"] if session else None,
1945
+ args.project,
1946
+ args.agent,
1947
+ fact_key,
1948
+ args.text,
1949
+ args.confidence,
1950
+ now,
1951
+ now,
1952
+ json_dump(metadata),
1953
+ ),
1954
+ )
1955
+ fact = conn.execute(
1956
+ "SELECT * FROM memory_fact WHERE project = ? AND agent = ? AND fact_key = ?",
1957
+ (args.project, args.agent, fact_key),
1958
+ ).fetchone()
1959
+ index_search_doc(
1960
+ conn,
1961
+ args.project,
1962
+ args.agent,
1963
+ "memory_fact",
1964
+ "memory_fact",
1965
+ fact["id"],
1966
+ fact_key,
1967
+ args.text,
1968
+ args.text[:240],
1969
+ 0,
1970
+ 1,
1971
+ metadata,
1972
+ )
1973
+ return {"fact_id": fact["id"], "fact_key": fact_key, "task_key": args.task_key or ""}
1974
+
1975
+ write_command(args, "remember-requirement", handler)
1976
+
1977
+
1978
+ def command_checkpoint(args: argparse.Namespace) -> None:
1979
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
1980
+ session = require_session(conn, args.session_key) if args.session_key else None
1981
+ task = optional_task(conn, args.task_key)
1982
+ if args.task_key and not task:
1983
+ raise CliError("TASK_NOT_FOUND", f"task {args.task_key!r} does not exist")
1984
+ summary_parts = [
1985
+ f"Goal: {args.goal}",
1986
+ f"Scope: {args.scope}",
1987
+ f"Progress: {args.progress}",
1988
+ ]
1989
+ if args.evidence_ref:
1990
+ summary_parts.append("Evidence: " + "; ".join(args.evidence_ref))
1991
+ payload = {
1992
+ "goal": args.goal,
1993
+ "scope": args.scope,
1994
+ "progress": args.progress,
1995
+ "files_touched": args.file or [],
1996
+ "evidence_refs": args.evidence_ref or [],
1997
+ "memory_layer": "task_checkpoint",
1998
+ }
1999
+ cursor = conn.execute(
2000
+ """
2001
+ INSERT INTO memory_event (session_id, task_id, event_type, state, summary, payload_json, created_at)
2002
+ VALUES (?, ?, 'CHECKPOINT', 'ACTIVE', ?, ?, ?)
2003
+ """,
2004
+ (
2005
+ session["id"] if session else (task["session_id"] if task else None),
2006
+ task["id"] if task else None,
2007
+ " | ".join(summary_parts),
2008
+ json_dump(payload),
2009
+ utc_now(),
2010
+ ),
2011
+ )
2012
+ return {"event_id": int(cursor.lastrowid), "state": "ACTIVE", "task_key": args.task_key or ""}
2013
+
2014
+ write_command(args, "checkpoint", handler)
2015
+
2016
+
2017
+ def command_pack_current(args: argparse.Namespace) -> None:
2018
+ def handler(conn: sqlite3.Connection) -> dict[str, Any]:
2019
+ task = optional_task(conn, args.task_key)
2020
+ if args.task_key and not task:
2021
+ raise CliError("TASK_NOT_FOUND", f"task {args.task_key!r} does not exist")
2022
+ if not task:
2023
+ task = conn.execute(
2024
+ """
2025
+ SELECT * FROM task
2026
+ WHERE project = ? AND agent = ? AND status = 'active'
2027
+ ORDER BY started_at DESC, id DESC
2028
+ LIMIT 1
2029
+ """,
2030
+ (args.project, args.agent),
2031
+ ).fetchone()
2032
+ checkpoint_sql = """
2033
+ SELECT e.id, e.event_type, e.state, e.summary, e.payload_json, e.created_at,
2034
+ t.task_key, t.title AS task_title
2035
+ FROM memory_event e
2036
+ LEFT JOIN task t ON t.id = e.task_id
2037
+ WHERE e.event_type = 'CHECKPOINT'
2038
+ AND e.state = 'ACTIVE'
2039
+ """
2040
+ checkpoint_values: list[Any] = []
2041
+ if task:
2042
+ checkpoint_sql += " AND e.task_id = ?"
2043
+ checkpoint_values.append(task["id"])
2044
+ checkpoint_sql += " ORDER BY e.created_at DESC, e.id DESC LIMIT 3"
2045
+ checkpoints = conn.execute(checkpoint_sql, checkpoint_values).fetchall()
2046
+ requirements = conn.execute(
2047
+ """
2048
+ SELECT id, fact_key, fact_text, category, confidence, last_seen_at, metadata_json
2049
+ FROM memory_fact
2050
+ WHERE project = ? AND agent = ? AND status = 'active' AND sensitive = 0
2051
+ AND category IN ('requirement', 'preference', 'decision')
2052
+ ORDER BY last_seen_at DESC, id DESC
2053
+ LIMIT 8
2054
+ """,
2055
+ (args.project, args.agent),
2056
+ ).fetchall()
2057
+ findings = conn.execute(
2058
+ """
2059
+ SELECT id, kind, category, severity, summary, correct_behavior, raw_ref, created_at
2060
+ FROM finding
2061
+ WHERE project = ? AND agent = ? AND sensitive = 0
2062
+ AND kind IN ('correction', 'failure', 'preference', 'risk', 'observation')
2063
+ ORDER BY created_at DESC, id DESC
2064
+ LIMIT 6
2065
+ """,
2066
+ (args.project, args.agent),
2067
+ ).fetchall()
2068
+ evidence = conn.execute(
2069
+ """
2070
+ SELECT id, source_type, evidence_key, evidence_text, weight, created_at
2071
+ FROM evidence
2072
+ WHERE project = ? AND agent = ? AND sensitive = 0
2073
+ AND source_type IN ('file', 'diff', 'test', 'user_confirmation')
2074
+ ORDER BY created_at DESC, id DESC
2075
+ LIMIT 6
2076
+ """,
2077
+ (args.project, args.agent),
2078
+ ).fetchall()
2079
+ return {
2080
+ "budget": args.budget,
2081
+ "query": "current active task requirements decisions evidence",
2082
+ "active_task": row_to_dict(task),
2083
+ "active_checkpoints": [row_to_dict(row) for row in checkpoints],
2084
+ "recent_requirement_decisions": [row_to_dict(row) for row in requirements],
2085
+ "relevant_durable_findings": [row_to_dict(row) for row in findings],
2086
+ "recent_evidence": [row_to_dict(row) for row in evidence],
2087
+ }
2088
+
2089
+ read_command(args, handler)
2090
+
2091
+
2092
+ def add_db_arg(parser: argparse.ArgumentParser) -> None:
2093
+ parser.add_argument("--db", default=DEFAULT_DB, help="SQLite memory DB path")
2094
+
2095
+
2096
+ def add_project_agent_args(parser: argparse.ArgumentParser) -> None:
2097
+ parser.add_argument("--project", default=project_default())
2098
+ parser.add_argument("--agent", default=DEFAULT_AGENT)
2099
+
2100
+
2101
+ def add_metadata_arg(parser: argparse.ArgumentParser) -> None:
2102
+ parser.add_argument("--metadata-json", default="{}")
2103
+
2104
+
2105
+ def build_parser() -> argparse.ArgumentParser:
2106
+ parser = argparse.ArgumentParser(description="ROSE project-local SQLite memory CLI")
2107
+ sub = parser.add_subparsers(dest="command", required=True)
2108
+
2109
+ init = sub.add_parser("init")
2110
+ add_db_arg(init)
2111
+ add_project_agent_args(init)
2112
+ init.set_defaults(func=command_init)
2113
+
2114
+ doctor = sub.add_parser("doctor")
2115
+ add_db_arg(doctor)
2116
+ doctor.add_argument("--record", action="store_true")
2117
+ doctor.set_defaults(func=command_doctor)
2118
+
2119
+ migrate = sub.add_parser("migrate")
2120
+ add_db_arg(migrate)
2121
+ migrate.set_defaults(func=command_migrate)
2122
+
2123
+ session = sub.add_parser("session")
2124
+ session_sub = session.add_subparsers(dest="session_command", required=True)
2125
+ session_start = session_sub.add_parser("start")
2126
+ add_db_arg(session_start)
2127
+ add_project_agent_args(session_start)
2128
+ add_metadata_arg(session_start)
2129
+ session_start.add_argument("--session-key")
2130
+ session_start.set_defaults(func=command_session_start)
2131
+ session_end = session_sub.add_parser("end")
2132
+ add_db_arg(session_end)
2133
+ session_end.add_argument("--session-key", required=True)
2134
+ session_end.add_argument("--summary")
2135
+ session_end.set_defaults(func=command_session_end)
2136
+
2137
+ task = sub.add_parser("task")
2138
+ task_sub = task.add_subparsers(dest="task_command", required=True)
2139
+ task_start = task_sub.add_parser("start")
2140
+ add_db_arg(task_start)
2141
+ add_project_agent_args(task_start)
2142
+ add_metadata_arg(task_start)
2143
+ task_start.add_argument("--session-key", required=True)
2144
+ task_start.add_argument("--task-key")
2145
+ task_start.add_argument("--title", required=True)
2146
+ task_start.add_argument("--scope-text")
2147
+ task_start.set_defaults(func=command_task_start)
2148
+ task_update = task_sub.add_parser("update")
2149
+ add_db_arg(task_update)
2150
+ add_metadata_arg(task_update)
2151
+ task_update.add_argument("--task-key", required=True)
2152
+ task_update.add_argument("--title")
2153
+ task_update.add_argument("--scope-text")
2154
+ task_update.add_argument("--status", choices=["active", "blocked", "completed", "cancelled"])
2155
+ task_update.add_argument("--outcome")
2156
+ task_update.set_defaults(func=command_task_update)
2157
+ task_complete = task_sub.add_parser("complete")
2158
+ add_db_arg(task_complete)
2159
+ task_complete.add_argument("--task-key", required=True)
2160
+ task_complete.add_argument("--outcome")
2161
+ task_complete.add_argument("--summary")
2162
+ task_complete.set_defaults(func=command_task_complete)
2163
+
2164
+ event = sub.add_parser("event")
2165
+ event_sub = event.add_subparsers(dest="event_command", required=True)
2166
+ event_add = event_sub.add_parser("add")
2167
+ add_db_arg(event_add)
2168
+ event_add.add_argument("--session-key")
2169
+ event_add.add_argument("--task-key")
2170
+ event_add.add_argument("--event-type", required=True)
2171
+ event_add.add_argument("--state")
2172
+ event_add.add_argument("--summary")
2173
+ event_add.add_argument("--payload-json", default="{}")
2174
+ event_add.set_defaults(func=command_event_add)
2175
+
2176
+ fact = sub.add_parser("fact")
2177
+ fact_sub = fact.add_subparsers(dest="fact_command", required=True)
2178
+ fact_add = fact_sub.add_parser("add")
2179
+ add_db_arg(fact_add)
2180
+ add_project_agent_args(fact_add)
2181
+ add_metadata_arg(fact_add)
2182
+ fact_add.add_argument("--session-key")
2183
+ fact_add.add_argument("--fact-key")
2184
+ fact_add.add_argument("--fact-text", required=True)
2185
+ fact_add.add_argument("--category", required=True)
2186
+ fact_add.add_argument("--confidence", choices=["low", "medium", "high"], default="medium")
2187
+ fact_add.add_argument("--sensitive", action="store_true")
2188
+ fact_add.add_argument("--no-index", dest="indexable", action="store_false", default=True)
2189
+ fact_add.set_defaults(func=command_fact_add)
2190
+
2191
+ claim = sub.add_parser("claim")
2192
+ claim_sub = claim.add_subparsers(dest="claim_command", required=True)
2193
+ claim_add = claim_sub.add_parser("add")
2194
+ add_db_arg(claim_add)
2195
+ add_project_agent_args(claim_add)
2196
+ add_metadata_arg(claim_add)
2197
+ claim_add.add_argument("--session-key")
2198
+ claim_add.add_argument("--task-key")
2199
+ claim_add.add_argument("--claim-key")
2200
+ claim_add.add_argument("--claim-text", required=True)
2201
+ claim_add.add_argument(
2202
+ "--verification-status",
2203
+ choices=["unverified", "verified", "disputed", "superseded"],
2204
+ default="unverified",
2205
+ )
2206
+ claim_add.add_argument("--evidence-summary")
2207
+ claim_add.add_argument("--sensitive", action="store_true")
2208
+ claim_add.add_argument("--no-index", dest="indexable", action="store_false", default=True)
2209
+ claim_add.set_defaults(func=command_claim_add)
2210
+
2211
+ finding = sub.add_parser("finding")
2212
+ finding_sub = finding.add_subparsers(dest="finding_command", required=True)
2213
+ finding_add = finding_sub.add_parser("add")
2214
+ add_db_arg(finding_add)
2215
+ add_project_agent_args(finding_add)
2216
+ add_metadata_arg(finding_add)
2217
+ finding_add.add_argument("--session-key", required=True)
2218
+ finding_add.add_argument("--task-key")
2219
+ finding_add.add_argument(
2220
+ "--kind",
2221
+ required=True,
2222
+ choices=["correction", "failure", "review_rejection", "preference", "risk", "observation"],
2223
+ )
2224
+ finding_add.add_argument("--category", required=True)
2225
+ finding_add.add_argument("--severity", choices=["low", "normal", "high", "critical"], default="normal")
2226
+ finding_add.add_argument("--summary", required=True)
2227
+ finding_add.add_argument("--wrong-behavior")
2228
+ finding_add.add_argument("--correct-behavior")
2229
+ finding_add.add_argument("--raw-ref")
2230
+ finding_add.add_argument("--evidence-key")
2231
+ finding_add.add_argument("--sensitive", action="store_true")
2232
+ finding_add.add_argument("--no-index", dest="indexable", action="store_false", default=True)
2233
+ finding_add.set_defaults(func=command_finding_add)
2234
+
2235
+ evidence = sub.add_parser("evidence")
2236
+ evidence_sub = evidence.add_subparsers(dest="evidence_command", required=True)
2237
+ evidence_add = evidence_sub.add_parser("add")
2238
+ add_db_arg(evidence_add)
2239
+ add_project_agent_args(evidence_add)
2240
+ add_metadata_arg(evidence_add)
2241
+ evidence_add.add_argument("--session-key")
2242
+ evidence_add.add_argument("--task-key")
2243
+ evidence_add.add_argument(
2244
+ "--source-type",
2245
+ required=True,
2246
+ choices=["finding", "claim", "memory_event", "task", "file", "diff", "test", "user_confirmation"],
2247
+ )
2248
+ evidence_add.add_argument("--source-id", type=int)
2249
+ evidence_add.add_argument("--evidence-key")
2250
+ evidence_add.add_argument("--evidence-text", required=True)
2251
+ evidence_add.add_argument("--weight", type=int, default=1)
2252
+ evidence_add.add_argument("--sensitive", action="store_true")
2253
+ evidence_add.add_argument("--no-index", dest="indexable", action="store_false", default=True)
2254
+ evidence_add.set_defaults(func=command_evidence_add)
2255
+
2256
+ rule = sub.add_parser("rule")
2257
+ rule_sub = rule.add_subparsers(dest="rule_command", required=True)
2258
+ rule_observe = rule_sub.add_parser("observe")
2259
+ add_db_arg(rule_observe)
2260
+ add_project_agent_args(rule_observe)
2261
+ add_metadata_arg(rule_observe)
2262
+ rule_observe.add_argument("--evidence-id", type=int, required=True)
2263
+ rule_observe.add_argument("--rule-key")
2264
+ rule_observe.add_argument("--rule-text", required=True)
2265
+ rule_observe.add_argument("--category", required=True)
2266
+ rule_observe.add_argument("--severity", choices=["low", "normal", "high", "critical"], default="normal")
2267
+ rule_observe.add_argument("--signal-type", required=True, choices=sorted(SIGNAL_WEIGHTS))
2268
+ rule_observe.set_defaults(func=command_rule_observe)
2269
+ rule_list = rule_sub.add_parser("list")
2270
+ add_db_arg(rule_list)
2271
+ add_project_agent_args(rule_list)
2272
+ rule_list.add_argument(
2273
+ "--status",
2274
+ choices=["memory", "candidate", "needs_reconciliation", "approved", "promoted", "rejected", "superseded", "expired"],
2275
+ )
2276
+ rule_list.add_argument("--limit", type=int, default=50)
2277
+ rule_list.set_defaults(func=command_rule_list)
2278
+ rule_propose = rule_sub.add_parser("propose")
2279
+ add_db_arg(rule_propose)
2280
+ rule_propose.add_argument("--rule-id", type=int, required=True)
2281
+ rule_propose.add_argument("--target", default=DEFAULT_TARGET)
2282
+ rule_propose.set_defaults(func=command_rule_propose)
2283
+ rule_approve = rule_sub.add_parser("approve")
2284
+ add_db_arg(rule_approve)
2285
+ rule_approve.add_argument("--patch-id", type=int, required=True)
2286
+ rule_approve.add_argument("--patch-hash", required=True)
2287
+ rule_approve.add_argument("--actor", required=True)
2288
+ rule_approve.add_argument("--reason")
2289
+ rule_approve.add_argument("--approval-evidence-id", type=int)
2290
+ rule_approve.set_defaults(func=command_rule_approve)
2291
+ rule_reject = rule_sub.add_parser("reject")
2292
+ add_db_arg(rule_reject)
2293
+ rule_reject.add_argument("--patch-id", type=int, required=True)
2294
+ rule_reject.add_argument("--actor", required=True)
2295
+ rule_reject.add_argument("--reason")
2296
+ rule_reject.set_defaults(func=command_rule_reject)
2297
+ rule_reconcile = rule_sub.add_parser("reconcile")
2298
+ add_db_arg(rule_reconcile)
2299
+ rule_reconcile.add_argument("--rule-id", type=int, required=True)
2300
+ rule_reconcile.add_argument("--status", required=True, choices=["candidate", "rejected", "superseded"])
2301
+ rule_reconcile.add_argument("--conflicts-json", default="[]")
2302
+ rule_reconcile.set_defaults(func=command_rule_reconcile)
2303
+ rule_promote = rule_sub.add_parser("promote")
2304
+ add_db_arg(rule_promote)
2305
+ rule_promote.add_argument("--patch-id", type=int, required=True)
2306
+ mode = rule_promote.add_mutually_exclusive_group(required=True)
2307
+ mode.add_argument("--apply", action="store_true")
2308
+ mode.add_argument("--record-applied", action="store_true")
2309
+ rule_promote.add_argument("--actor", required=True)
2310
+ rule_promote.set_defaults(func=command_rule_promote)
2311
+
2312
+ search = sub.add_parser("search")
2313
+ add_db_arg(search)
2314
+ add_project_agent_args(search)
2315
+ search.add_argument("query")
2316
+ search.add_argument("--limit", type=int, default=10)
2317
+ search.set_defaults(func=command_search)
2318
+
2319
+ pack = sub.add_parser("pack")
2320
+ add_db_arg(pack)
2321
+ add_project_agent_args(pack)
2322
+ pack.add_argument("query")
2323
+ pack.add_argument("--mode", choices=["direct", "spec", "research"], default="direct")
2324
+ pack.add_argument("--budget", type=int, default=800)
2325
+ pack.set_defaults(func=command_pack)
2326
+
2327
+ receipt = sub.add_parser("receipt")
2328
+ receipt_sub = receipt.add_subparsers(dest="receipt_command", required=True)
2329
+ receipt_show = receipt_sub.add_parser("show")
2330
+ add_db_arg(receipt_show)
2331
+ receipt_show.add_argument("receipt_key")
2332
+ receipt_show.set_defaults(func=command_receipt_show)
2333
+
2334
+ complete = sub.add_parser("complete")
2335
+ add_db_arg(complete)
2336
+ complete.add_argument("--session-key")
2337
+ complete.add_argument("--task-key")
2338
+ complete.add_argument("--outcome")
2339
+ complete.add_argument("--summary")
2340
+ complete.add_argument("--evidence-id", type=int, action="append")
2341
+ complete.add_argument("--evidence-ref", action="append")
2342
+ complete.add_argument("--no-durable-memory-promoted", action="store_true")
2343
+ complete.set_defaults(func=command_complete)
2344
+
2345
+ remember_requirement = sub.add_parser("remember-requirement")
2346
+ add_db_arg(remember_requirement)
2347
+ add_project_agent_args(remember_requirement)
2348
+ remember_requirement.add_argument("--session-key")
2349
+ remember_requirement.add_argument("--task-key")
2350
+ remember_requirement.add_argument("--fact-key")
2351
+ remember_requirement.add_argument("--text", required=True)
2352
+ remember_requirement.add_argument("--source", default="conversation")
2353
+ remember_requirement.add_argument("--confidence", choices=["low", "medium", "high"], default="high")
2354
+ remember_requirement.set_defaults(func=command_remember_requirement)
2355
+
2356
+ checkpoint = sub.add_parser("checkpoint")
2357
+ add_db_arg(checkpoint)
2358
+ checkpoint.add_argument("--session-key")
2359
+ checkpoint.add_argument("--task-key")
2360
+ checkpoint.add_argument("--goal", required=True)
2361
+ checkpoint.add_argument("--scope", required=True)
2362
+ checkpoint.add_argument("--progress", required=True)
2363
+ checkpoint.add_argument("--file", action="append")
2364
+ checkpoint.add_argument("--evidence-ref", action="append")
2365
+ checkpoint.set_defaults(func=command_checkpoint)
2366
+
2367
+ pack_current = sub.add_parser("pack-current")
2368
+ add_db_arg(pack_current)
2369
+ add_project_agent_args(pack_current)
2370
+ pack_current.add_argument("--task-key")
2371
+ pack_current.add_argument("--budget", type=int, default=1200)
2372
+ pack_current.set_defaults(func=command_pack_current)
2373
+
2374
+ return parser
2375
+
2376
+
2377
+ def main(argv: Iterable[str] | None = None) -> None:
2378
+ parser = build_parser()
2379
+ args = parser.parse_args(list(argv) if argv is not None else None)
2380
+ args.func(args)
2381
+
2382
+
2383
+ if __name__ == "__main__":
2384
+ main()