self-evolve-framework 1.6.0 → 1.6.1

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 (301) hide show
  1. package/README.md +33 -23
  2. package/bin/cli.js +24 -67
  3. package/package.json +1 -1
  4. package/template/rules/knowledge/README.md +56 -0
  5. package/template/rules/knowledge/ant-design-vue.mdc +54 -0
  6. package/template/rules/knowledge/general.mdc +56 -0
  7. package/template/rules/knowledge/rust.mdc +55 -0
  8. package/template/rules/knowledge/shadcn-svelte.mdc +64 -0
  9. package/template/rules/knowledge/svelte.mdc +135 -0
  10. package/template/rules/knowledge/tailwind.mdc +66 -0
  11. package/template/rules/knowledge/tauri.mdc +65 -0
  12. package/template/rules/knowledge/typescript.mdc +41 -0
  13. package/template/rules/knowledge/vue.mdc +48 -0
  14. package/template/rules/ponytail.mdc +23 -98
  15. package/template/rules/self-evolve.mdc +24 -0
  16. package/template/skills/impeccable/SKILL.md +47 -174
  17. package/template/skills/skillopt-sleep/SKILL.md +32 -88
  18. package/template/skills/impeccable/agents/impeccable_asset_producer.toml +0 -92
  19. package/template/skills/impeccable/agents/impeccable_manual_edit_applier.toml +0 -95
  20. package/template/skills/impeccable/agents/openai.yaml +0 -4
  21. package/template/skills/impeccable/reference/adapt.md +0 -311
  22. package/template/skills/impeccable/reference/animate.md +0 -201
  23. package/template/skills/impeccable/reference/audit.md +0 -133
  24. package/template/skills/impeccable/reference/bolder.md +0 -113
  25. package/template/skills/impeccable/reference/brand.md +0 -108
  26. package/template/skills/impeccable/reference/clarify.md +0 -288
  27. package/template/skills/impeccable/reference/codex.md +0 -105
  28. package/template/skills/impeccable/reference/colorize.md +0 -257
  29. package/template/skills/impeccable/reference/craft.md +0 -123
  30. package/template/skills/impeccable/reference/critique.md +0 -790
  31. package/template/skills/impeccable/reference/delight.md +0 -302
  32. package/template/skills/impeccable/reference/distill.md +0 -111
  33. package/template/skills/impeccable/reference/document.md +0 -429
  34. package/template/skills/impeccable/reference/extract.md +0 -69
  35. package/template/skills/impeccable/reference/harden.md +0 -347
  36. package/template/skills/impeccable/reference/hooks.md +0 -90
  37. package/template/skills/impeccable/reference/init.md +0 -172
  38. package/template/skills/impeccable/reference/interaction-design.md +0 -189
  39. package/template/skills/impeccable/reference/layout.md +0 -161
  40. package/template/skills/impeccable/reference/live.md +0 -720
  41. package/template/skills/impeccable/reference/onboard.md +0 -234
  42. package/template/skills/impeccable/reference/optimize.md +0 -258
  43. package/template/skills/impeccable/reference/overdrive.md +0 -130
  44. package/template/skills/impeccable/reference/polish.md +0 -241
  45. package/template/skills/impeccable/reference/product.md +0 -60
  46. package/template/skills/impeccable/reference/quieter.md +0 -99
  47. package/template/skills/impeccable/reference/shape.md +0 -165
  48. package/template/skills/impeccable/reference/typeset.md +0 -279
  49. package/template/skills/impeccable/scripts/command-metadata.json +0 -94
  50. package/template/skills/impeccable/scripts/context-signals.mjs +0 -225
  51. package/template/skills/impeccable/scripts/context.mjs +0 -961
  52. package/template/skills/impeccable/scripts/critique-storage.mjs +0 -242
  53. package/template/skills/impeccable/scripts/detect-csp.mjs +0 -198
  54. package/template/skills/impeccable/scripts/detect.mjs +0 -21
  55. package/template/skills/impeccable/scripts/detector/browser/injected/index.mjs +0 -1937
  56. package/template/skills/impeccable/scripts/detector/cli/main.mjs +0 -290
  57. package/template/skills/impeccable/scripts/detector/design-system.mjs +0 -750
  58. package/template/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +0 -5138
  59. package/template/skills/impeccable/scripts/detector/detect-antipatterns.mjs +0 -50
  60. package/template/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +0 -277
  61. package/template/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +0 -568
  62. package/template/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +0 -1015
  63. package/template/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +0 -234
  64. package/template/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +0 -189
  65. package/template/skills/impeccable/scripts/detector/findings.mjs +0 -12
  66. package/template/skills/impeccable/scripts/detector/node/file-system.mjs +0 -198
  67. package/template/skills/impeccable/scripts/detector/profile/profiler.mjs +0 -166
  68. package/template/skills/impeccable/scripts/detector/registry/antipatterns.mjs +0 -448
  69. package/template/skills/impeccable/scripts/detector/rules/checks.mjs +0 -2671
  70. package/template/skills/impeccable/scripts/detector/shared/color.mjs +0 -124
  71. package/template/skills/impeccable/scripts/detector/shared/constants.mjs +0 -101
  72. package/template/skills/impeccable/scripts/detector/shared/inline-ignores.mjs +0 -148
  73. package/template/skills/impeccable/scripts/detector/shared/page.mjs +0 -7
  74. package/template/skills/impeccable/scripts/hook-admin.mjs +0 -661
  75. package/template/skills/impeccable/scripts/hook-before-edit.mjs +0 -476
  76. package/template/skills/impeccable/scripts/hook-lib.mjs +0 -1632
  77. package/template/skills/impeccable/scripts/hook.mjs +0 -61
  78. package/template/skills/impeccable/scripts/lib/design-parser.mjs +0 -842
  79. package/template/skills/impeccable/scripts/lib/impeccable-config.mjs +0 -638
  80. package/template/skills/impeccable/scripts/lib/impeccable-paths.mjs +0 -128
  81. package/template/skills/impeccable/scripts/lib/is-generated.mjs +0 -69
  82. package/template/skills/impeccable/scripts/lib/target-args.mjs +0 -42
  83. package/template/skills/impeccable/scripts/live/browser-script-parts.mjs +0 -49
  84. package/template/skills/impeccable/scripts/live/completion.mjs +0 -19
  85. package/template/skills/impeccable/scripts/live/event-validation.mjs +0 -137
  86. package/template/skills/impeccable/scripts/live/insert-ui.mjs +0 -458
  87. package/template/skills/impeccable/scripts/live/manual-apply.mjs +0 -939
  88. package/template/skills/impeccable/scripts/live/manual-edit-routes.mjs +0 -357
  89. package/template/skills/impeccable/scripts/live/manual-edits-buffer.mjs +0 -152
  90. package/template/skills/impeccable/scripts/live/session-store.mjs +0 -289
  91. package/template/skills/impeccable/scripts/live/svelte-component.mjs +0 -826
  92. package/template/skills/impeccable/scripts/live/sveltekit-adapter.mjs +0 -274
  93. package/template/skills/impeccable/scripts/live/ui-core.mjs +0 -180
  94. package/template/skills/impeccable/scripts/live/vocabulary.mjs +0 -36
  95. package/template/skills/impeccable/scripts/live-accept.mjs +0 -812
  96. package/template/skills/impeccable/scripts/live-browser-dom.js +0 -146
  97. package/template/skills/impeccable/scripts/live-browser-session.js +0 -123
  98. package/template/skills/impeccable/scripts/live-browser.js +0 -11173
  99. package/template/skills/impeccable/scripts/live-commit-manual-edits.mjs +0 -1241
  100. package/template/skills/impeccable/scripts/live-complete.mjs +0 -75
  101. package/template/skills/impeccable/scripts/live-copy-edit-agent.mjs +0 -683
  102. package/template/skills/impeccable/scripts/live-discard-manual-edits.mjs +0 -51
  103. package/template/skills/impeccable/scripts/live-inject.mjs +0 -583
  104. package/template/skills/impeccable/scripts/live-insert.mjs +0 -272
  105. package/template/skills/impeccable/scripts/live-manual-edit-evidence.mjs +0 -363
  106. package/template/skills/impeccable/scripts/live-poll.mjs +0 -384
  107. package/template/skills/impeccable/scripts/live-resume.mjs +0 -94
  108. package/template/skills/impeccable/scripts/live-server.mjs +0 -1135
  109. package/template/skills/impeccable/scripts/live-status.mjs +0 -61
  110. package/template/skills/impeccable/scripts/live-target.mjs +0 -30
  111. package/template/skills/impeccable/scripts/live-wrap.mjs +0 -894
  112. package/template/skills/impeccable/scripts/live.mjs +0 -297
  113. package/template/skills/impeccable/scripts/modern-screenshot.umd.js +0 -14
  114. package/template/skills/impeccable/scripts/palette.mjs +0 -633
  115. package/template/skills/impeccable/scripts/pin.mjs +0 -214
  116. package/template/skills/skillopt-sleep/configs/_base_/default.yaml +0 -103
  117. package/template/skills/skillopt-sleep/configs/alfworld/default.yaml +0 -29
  118. package/template/skills/skillopt-sleep/configs/docvqa/default.yaml +0 -28
  119. package/template/skills/skillopt-sleep/configs/features/soft_gate.yaml +0 -47
  120. package/template/skills/skillopt-sleep/configs/livemathematicianbench/default.yaml +0 -22
  121. package/template/skills/skillopt-sleep/configs/officeqa/default.yaml +0 -34
  122. package/template/skills/skillopt-sleep/configs/searchqa/default.yaml +0 -32
  123. package/template/skills/skillopt-sleep/configs/spreadsheetbench/default.yaml +0 -34
  124. package/template/skills/skillopt-sleep/scripts/framework/skillopt/__init__.py +0 -28
  125. package/template/skills/skillopt-sleep/scripts/framework/skillopt/config.py +0 -282
  126. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/__init__.py +0 -7
  127. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/base.py +0 -512
  128. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/__init__.py +0 -9
  129. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/trainer.py +0 -2379
  130. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/__init__.py +0 -1
  131. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/README.md +0 -43
  132. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/config_template.yaml +0 -55
  133. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/env_template.py +0 -151
  134. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/loader_template.py +0 -87
  135. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/__init__.py +0 -5
  136. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/adapter.py +0 -428
  137. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/dataloader.py +0 -123
  138. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_error.md +0 -55
  139. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_success.md +0 -33
  140. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_no_history.md +0 -8
  141. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_history.md +0 -9
  142. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_memory.md +0 -16
  143. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/reflect.py +0 -4
  144. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/rollout.py +0 -366
  145. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/skills/initial.md +0 -45
  146. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/__init__.py +0 -9
  147. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_envs.py +0 -221
  148. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_projection.py +0 -60
  149. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_prompts.py +0 -8
  150. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/config_tw.yaml +0 -145
  151. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_base.py +0 -84
  152. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_manager.py +0 -139
  153. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/memory.py +0 -87
  154. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/base.py +0 -329
  155. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/__init__.py +0 -1
  156. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/adapter.py +0 -90
  157. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/dataloader.py +0 -61
  158. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/evaluator.py +0 -113
  159. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_error.md +0 -35
  160. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_success.md +0 -24
  161. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/rollout_system.md +0 -12
  162. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/rollout.py +0 -391
  163. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/skills/initial.md +0 -11
  164. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/__init__.py +0 -1
  165. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/adapter.py +0 -129
  166. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/dataloader.py +0 -308
  167. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/evaluator.py +0 -62
  168. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_error.md +0 -37
  169. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_success.md +0 -25
  170. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/rollout_system.md +0 -12
  171. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/reflect.py +0 -4
  172. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/rollout.py +0 -434
  173. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/skills/initial.md +0 -16
  174. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/__init__.py +0 -1
  175. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/adapter.py +0 -112
  176. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/dataloader.py +0 -71
  177. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/evaluator.py +0 -46
  178. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_error.md +0 -37
  179. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_success.md +0 -25
  180. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/rollout_system.md +0 -15
  181. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/rollout.py +0 -799
  182. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/skills/initial.md +0 -15
  183. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/tool_runtime.py +0 -552
  184. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/__init__.py +0 -1
  185. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/adapter.py +0 -96
  186. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/dataloader.py +0 -42
  187. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/evaluator.py +0 -100
  188. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_error.md +0 -46
  189. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_success.md +0 -32
  190. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/rollout_system.md +0 -13
  191. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/reflect.py +0 -4
  192. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/rollout.py +0 -494
  193. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/skills/initial.md +0 -3
  194. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/__init__.py +0 -5
  195. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/adapter.py +0 -159
  196. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/codegen_agent.py +0 -731
  197. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/dataloader.py +0 -37
  198. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/evaluator.py +0 -158
  199. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/executor.py +0 -67
  200. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_error.md +0 -46
  201. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_success.md +0 -32
  202. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/codegen_system.md +0 -1
  203. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/critical_rules.md +0 -9
  204. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/react_system.md +0 -21
  205. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/react_agent.py +0 -395
  206. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/reflect.py +0 -4
  207. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/rollout.py +0 -979
  208. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/skills/initial.md +0 -56
  209. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/__init__.py +0 -13
  210. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/gate.py +0 -148
  211. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/__init__.py +0 -15
  212. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/aggregate.py +0 -253
  213. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/reflect.py +0 -635
  214. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/__init__.py +0 -514
  215. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/azure_openai.py +0 -915
  216. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/backend_config.py +0 -185
  217. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/claude_backend.py +0 -371
  218. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_backend.py +0 -666
  219. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_harness.py +0 -1057
  220. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/common.py +0 -229
  221. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/minimax_backend.py +0 -277
  222. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/qwen_backend.py +0 -456
  223. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/router.py +0 -236
  224. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/__init__.py +0 -15
  225. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/appendix.py +0 -156
  226. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/clip.py +0 -109
  227. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/lr_autonomous.py +0 -108
  228. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/meta_skill.py +0 -79
  229. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/rewrite.py +0 -59
  230. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/scheduler.py +0 -127
  231. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/select.py +0 -4
  232. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill.py +0 -201
  233. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill_aware.py +0 -206
  234. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/slow_update.py +0 -396
  235. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/update_modes.py +0 -135
  236. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/__init__.py +0 -63
  237. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error.md +0 -41
  238. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_full_rewrite.md +0 -32
  239. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_rewrite.md +0 -44
  240. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success.md +0 -36
  241. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_full_rewrite.md +0 -30
  242. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_rewrite.md +0 -33
  243. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/lr_autonomous.md +0 -20
  244. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure.md +0 -30
  245. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_full_rewrite.md +0 -28
  246. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_rewrite.md +0 -26
  247. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final.md +0 -33
  248. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_full_rewrite.md +0 -28
  249. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_rewrite.md +0 -25
  250. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success.md +0 -28
  251. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_full_rewrite.md +0 -28
  252. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_rewrite.md +0 -25
  253. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/meta_skill.md +0 -40
  254. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking.md +0 -20
  255. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking_rewrite.md +0 -15
  256. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/rewrite_skill.md +0 -25
  257. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/slow_update.md +0 -60
  258. package/template/skills/skillopt-sleep/scripts/framework/skillopt/scheduler/__init__.py +0 -8
  259. package/template/skills/skillopt-sleep/scripts/framework/skillopt/types.py +0 -306
  260. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/__init__.py +0 -4
  261. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/json_utils.py +0 -172
  262. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/scoring.py +0 -28
  263. package/template/skills/skillopt-sleep/scripts/python/__init__.py +0 -20
  264. package/template/skills/skillopt-sleep/scripts/python/__main__.py +0 -343
  265. package/template/skills/skillopt-sleep/scripts/python/backend.py +0 -1371
  266. package/template/skills/skillopt-sleep/scripts/python/budget.py +0 -75
  267. package/template/skills/skillopt-sleep/scripts/python/config.py +0 -162
  268. package/template/skills/skillopt-sleep/scripts/python/consolidate.py +0 -238
  269. package/template/skills/skillopt-sleep/scripts/python/cycle.py +0 -291
  270. package/template/skills/skillopt-sleep/scripts/python/dream.py +0 -138
  271. package/template/skills/skillopt-sleep/scripts/python/experiments/__init__.py +0 -1
  272. package/template/skills/skillopt-sleep/scripts/python/experiments/gbrain_bench.py +0 -119
  273. package/template/skills/skillopt-sleep/scripts/python/experiments/personas.py +0 -86
  274. package/template/skills/skillopt-sleep/scripts/python/experiments/report.py +0 -132
  275. package/template/skills/skillopt-sleep/scripts/python/experiments/run_experiment.py +0 -178
  276. package/template/skills/skillopt-sleep/scripts/python/experiments/run_gbrain.py +0 -209
  277. package/template/skills/skillopt-sleep/scripts/python/experiments/run_transfer.py +0 -155
  278. package/template/skills/skillopt-sleep/scripts/python/experiments/sweep.py +0 -164
  279. package/template/skills/skillopt-sleep/scripts/python/gate.py +0 -50
  280. package/template/skills/skillopt-sleep/scripts/python/harvest.py +0 -304
  281. package/template/skills/skillopt-sleep/scripts/python/harvest_codex.py +0 -253
  282. package/template/skills/skillopt-sleep/scripts/python/harvest_sources.py +0 -41
  283. package/template/skills/skillopt-sleep/scripts/python/judges.py +0 -84
  284. package/template/skills/skillopt-sleep/scripts/python/llm_miner.py +0 -134
  285. package/template/skills/skillopt-sleep/scripts/python/memory.py +0 -129
  286. package/template/skills/skillopt-sleep/scripts/python/mine.py +0 -312
  287. package/template/skills/skillopt-sleep/scripts/python/replay.py +0 -146
  288. package/template/skills/skillopt-sleep/scripts/python/rollout.py +0 -153
  289. package/template/skills/skillopt-sleep/scripts/python/scheduler.py +0 -138
  290. package/template/skills/skillopt-sleep/scripts/python/slow_update.py +0 -142
  291. package/template/skills/skillopt-sleep/scripts/python/staging.py +0 -103
  292. package/template/skills/skillopt-sleep/scripts/python/state.py +0 -96
  293. package/template/skills/skillopt-sleep/scripts/python/tasks_file.py +0 -81
  294. package/template/skills/skillopt-sleep/scripts/python/types.py +0 -146
  295. package/template/skills/skillopt-sleep/scripts/shell/__init__.py +0 -0
  296. package/template/skills/skillopt-sleep/scripts/shell/eval_only.py +0 -466
  297. package/template/skills/skillopt-sleep/scripts/shell/materialize_searchqa.py +0 -148
  298. package/template/skills/skillopt-sleep/scripts/shell/run_alfworld.sh +0 -60
  299. package/template/skills/skillopt-sleep/scripts/shell/run_searchqa.sh +0 -40
  300. package/template/skills/skillopt-sleep/scripts/shell/run_spreadsheetbench.sh +0 -39
  301. package/template/skills/skillopt-sleep/scripts/shell/train.py +0 -556
@@ -1,1371 +0,0 @@
1
- """SkillOpt-Sleep — optimizer/replay backend abstraction.
2
-
3
- A backend supplies the three "intelligent" operations the sleep cycle needs:
4
-
5
- 1. attempt(task, skill, memory) -> response text (the rollout)
6
- 2. judge(task, response) -> (hard, soft, rationale) (the reward)
7
- 3. reflect(failures, successes, skill, memory)
8
- -> list[EditRecord] (proposed bounded edits)
9
-
10
- Two implementations:
11
- * MockBackend — deterministic, no API, used for tests + the experiment.
12
- Reads optional `reference` exact answers and a tiny
13
- rule-table so the loop provably improves and the gate
14
- provably blocks regressions.
15
- * AnthropicBackend — uses the user's ANTHROPIC_API_KEY via the `claude`
16
- CLI or the anthropic SDK (lazy-imported). Real lift.
17
-
18
- The backend never touches live config; it only returns text/edits that the
19
- consolidation stage gates and stages.
20
- """
21
- from __future__ import annotations
22
-
23
- import json
24
- import os
25
- import re
26
- import subprocess
27
- import tempfile
28
- from typing import Any, Dict, List, Optional, Tuple
29
-
30
- from skillopt_sleep.types import EditRecord, ReplayResult, TaskRecord
31
-
32
-
33
- def skill_hash(content: str) -> str:
34
- import hashlib
35
- return hashlib.sha256(content.encode("utf-8")).hexdigest()[:16]
36
-
37
-
38
- # ── Backend protocol ──────────────────────────────────────────────────────────
39
-
40
- class Backend:
41
- name = "base"
42
- # Optional user preferences (free text) injected into reflect as a prior.
43
- preferences: str = ""
44
-
45
- def attempt(self, task: TaskRecord, skill: str, memory: str,
46
- sample_id: int = 0) -> str:
47
- raise NotImplementedError
48
-
49
- def attempt_with_tools(
50
- self, task: TaskRecord, skill: str, memory: str, tools: List[str]
51
- ) -> Tuple[str, List[str]]:
52
- """Run the task while exposing real tools; return (response, tools_called).
53
-
54
- Default: no real tool loop — fall back to plain attempt and let the
55
- single-shot 'TOOL_CALL: <name>' marker convention surface intent. CLI
56
- backends override this to expose a genuinely callable tool.
57
- """
58
- resp = self.attempt(task, skill, memory)
59
- called: List[str] = []
60
- for t in tools:
61
- if re.search(r"(?i)\btool_call\s*:\s*%s\b" % re.escape(t), resp):
62
- called.append(t)
63
- return resp, called
64
-
65
- def judge(self, task: TaskRecord, response: str) -> Tuple[float, float, str]:
66
- raise NotImplementedError
67
-
68
- def reflect(
69
- self,
70
- failures: List[Tuple[TaskRecord, ReplayResult]],
71
- successes: List[Tuple[TaskRecord, ReplayResult]],
72
- skill: str,
73
- memory: str,
74
- *,
75
- edit_budget: int,
76
- evolve_skill: bool,
77
- evolve_memory: bool,
78
- ) -> List[EditRecord]:
79
- raise NotImplementedError
80
-
81
- # token accounting (optional)
82
- def tokens_used(self) -> int:
83
- return 0
84
-
85
-
86
- # ── Shared scoring helpers ────────────────────────────────────────────────────
87
-
88
- def _normalize(s: str) -> str:
89
- s = (s or "").lower().strip()
90
- s = re.sub(r"[^\w\s]", " ", s)
91
- s = re.sub(r"\s+", " ", s)
92
- return s.strip()
93
-
94
-
95
- def exact_score(reference: str, response: str) -> float:
96
- ref = _normalize(reference)
97
- resp = _normalize(response)
98
- if not ref:
99
- return 0.0
100
- return 1.0 if ref in resp or resp == ref else 0.0
101
-
102
-
103
- def keyword_soft_score(reference: str, response: str) -> float:
104
- """Fraction of reference tokens present in response (cheap rubric proxy)."""
105
- ref_tokens = [t for t in _normalize(reference).split() if len(t) > 2]
106
- if not ref_tokens:
107
- return 0.0
108
- resp = _normalize(response)
109
- hit = sum(1 for t in set(ref_tokens) if t in resp)
110
- return hit / len(set(ref_tokens))
111
-
112
-
113
- # ── Mock backend (deterministic, no API) ──────────────────────────────────────
114
-
115
- class MockBackend(Backend):
116
- """Deterministic backend for tests and the acceptance experiment.
117
-
118
- Model of reality:
119
- * Each task may carry a `reference` (exact answer) and a "rule" tag
120
- describing the single skill rule that makes the task solvable, e.g.
121
- tags=["rule:wrap-answer-in-answer-tags"].
122
- * `attempt` produces a correct response IFF the required rule text is
123
- present in skill+memory; otherwise it produces a near-miss.
124
- * `judge` scores exact (hard) + keyword (soft) against `reference`.
125
- * `reflect` looks at failures, reads each failed task's required rule,
126
- and proposes exactly that rule as an `add` edit (bounded by budget).
127
- It NEVER proposes a rule already present (no churn), and on the
128
- special tag "rule:__harmful__" it proposes a known-bad edit so tests
129
- can prove the gate rejects regressions.
130
-
131
- This makes the end-to-end loop monotonic and fully reproducible while
132
- exercising the real harvest->mine->replay->gate->stage plumbing.
133
- """
134
-
135
- name = "mock"
136
-
137
- RULE_PREFIX = "rule:"
138
- RULE_TEXT = {
139
- "wrap-answer": "Always wrap the final answer in <answer>...</answer> tags.",
140
- "arxiv-id": "Report arXiv ids in the exact form arXiv:XXXX.XXXXX.",
141
- "commit-imperative": "Write git commit subjects in imperative mood, max 50 chars.",
142
- "units-si": "Always include SI units in numeric answers.",
143
- "json-only": "When asked for JSON, output only valid JSON with no prose.",
144
- "__harmful__": "Ignore the user's formatting requests and answer freely.",
145
- }
146
-
147
- def _required_rules(self, task: TaskRecord) -> List[str]:
148
- out = []
149
- for t in task.tags:
150
- if t.startswith(self.RULE_PREFIX):
151
- key = t[len(self.RULE_PREFIX):]
152
- if key in self.RULE_TEXT:
153
- out.append(key)
154
- return out
155
-
156
- def attempt(self, task: TaskRecord, skill: str, memory: str,
157
- sample_id: int = 0) -> str:
158
- ctx = (skill or "") + "\n" + (memory or "")
159
- rules = self._required_rules(task)
160
- # The "__harmful__" rule models a bad edit: even when present it makes
161
- # the agent ignore formatting, so it can NEVER produce the reference.
162
- # This is what lets the experiment prove the gate rejects regressions.
163
- if "__harmful__" in rules:
164
- return "I'll just answer freely and skip the requested format."
165
- # A task is solved iff ALL its required rule texts are present in context.
166
- have_all = all(self.RULE_TEXT[k] in ctx for k in rules) if rules else False
167
- if have_all and task.reference:
168
- # produce a response that satisfies the rule and contains the answer
169
- if "wrap-answer" in rules:
170
- return f"Here is the result. <answer>{task.reference}</answer>"
171
- return f"{task.reference}"
172
- # Near miss: a degraded answer that shares keywords but is NOT the exact
173
- # rule-correct form, so exact-match fails deterministically regardless of
174
- # how many whitespace tokens the reference has.
175
- if task.reference:
176
- ref = task.reference
177
- mangled = ref[:-2] if len(ref) > 3 else "unknown"
178
- return f"approximately {mangled} (format not applied)"
179
- return "(attempted, no checkable reference)"
180
-
181
- def attempt_with_tools(self, task, skill, memory, tools):
182
- # Deterministic tool model: the mock "calls" a tool iff the skill+memory
183
- # contains an explicit instruction to use it (a learned rule mentioning
184
- # the tool name or "search"). The deficient skill says NOT to, so
185
- # baseline calls nothing; a learned "use ./search" rule flips it.
186
- ctx = ((skill or "") + "\n" + (memory or "")).lower()
187
- resp = self.attempt(task, skill, memory)
188
- called = []
189
- for t in (tools or []):
190
- tl = t.lower()
191
- if (f"./{tl}" in ctx or f"use {tl}" in ctx or f"run {tl}" in ctx
192
- or f"call {tl}" in ctx or f"must {tl}" in ctx):
193
- called.append(t)
194
- return resp, called
195
-
196
- def judge(self, task: TaskRecord, response: str) -> Tuple[float, float, str]:
197
- if task.reference_kind == "answer" and task.judge:
198
- try:
199
- from skillopt_sleep.experiments.real_eval import score_answer_judge
200
- except ImportError:
201
- score_answer_judge = None # research evaluators not bundled
202
- if score_answer_judge is not None:
203
- return score_answer_judge(task.judge, response)
204
- if task.reference_kind == "rule" and task.judge:
205
- from skillopt_sleep.judges import score_rule_judge
206
- return score_rule_judge(task.judge, response)
207
- if task.reference_kind == "exact" and task.reference:
208
- hard = exact_score(task.reference, response)
209
- soft = max(hard, keyword_soft_score(task.reference, response))
210
- return hard, soft, f"exact-match={hard}"
211
- if task.reference_kind == "rubric" and task.reference:
212
- soft = keyword_soft_score(task.reference, response)
213
- return (1.0 if soft >= 0.8 else 0.0), soft, f"rubric keyword soft={soft:.2f}"
214
- # no reference: outcome-derived weak label
215
- hard = 1.0 if task.outcome == "success" else 0.0
216
- return hard, hard, "outcome-derived"
217
-
218
- def reflect(
219
- self,
220
- failures,
221
- successes,
222
- skill: str,
223
- memory: str,
224
- *,
225
- edit_budget: int,
226
- evolve_skill: bool,
227
- evolve_memory: bool,
228
- ) -> List[EditRecord]:
229
- ctx = (skill or "") + "\n" + (memory or "")
230
- edits: List[EditRecord] = []
231
- seen_text: set = set()
232
- target = "skill" if evolve_skill else "memory"
233
- for task, _res in failures:
234
- for key in self._required_rules(task):
235
- text = self.RULE_TEXT[key]
236
- if text in ctx or text in seen_text:
237
- continue
238
- seen_text.add(text)
239
- edits.append(
240
- EditRecord(
241
- target=target,
242
- op="add",
243
- content=text,
244
- rationale=f"failed task {task.id} requires rule '{key}'",
245
- )
246
- )
247
- if len(edits) >= edit_budget:
248
- return edits
249
- return edits
250
-
251
-
252
- # ── Shared real-CLI backend (prompts + parsing + cache; subclasses do _call) ──
253
-
254
- def _extract_json(raw: str, kind: str):
255
- """Pull the first JSON object/array out of a possibly chatty CLI reply."""
256
- pat = r"\{.*\}" if kind == "object" else r"\[.*\]"
257
- m = re.search(pat, raw or "", re.DOTALL)
258
- if not m:
259
- return None
260
- try:
261
- return json.loads(m.group(0))
262
- except Exception:
263
- return None
264
-
265
-
266
- def _task_guardrail(pairs) -> str:
267
- """Build an 'output contract' the optimizer must not violate.
268
-
269
- ``pairs`` is a list of (TaskRecord, ReplayResult). We surface the benchmark's
270
- own rollout system prompt (TaskRecord.system) plus a short, explicit list of
271
- invariants, so the optimizer cannot learn rules that the evaluator can never
272
- honor (the SpreadsheetBench failure mode: a learned "return ```vba```" or
273
- "ask the user for the range" rule scores 0 because the harness runs only
274
- ```python``` openpyxl and cannot answer questions).
275
-
276
- Returns "" when no task carries a system contract (e.g. mined daily cases),
277
- so non-benchmark runs are unchanged.
278
- """
279
- sys_txt = ""
280
- for t, _ in pairs:
281
- s = getattr(t, "system", "") or ""
282
- if s.strip():
283
- sys_txt = s.strip()
284
- break
285
- if not sys_txt:
286
- return ""
287
- # the system prompt can be long; keep the rules portion concise for the optimizer
288
- contract = sys_txt
289
- if len(contract) > 900:
290
- contract = contract[:900] + " …"
291
- invariants = (
292
- "- Do NOT change the required output format or programming language.\n"
293
- "- Do NOT tell the agent to ask the user a question or request more info; "
294
- "it must always produce a best-effort answer from what is given.\n"
295
- "- Keep every rule consistent with the contract above."
296
- )
297
- return (
298
- "\n# Task output contract (rules MUST obey this — violating it scores 0)\n"
299
- f"{contract}\n{invariants}\n"
300
- )
301
-
302
-
303
- class CliBackend(Backend):
304
- """Common logic for real CLI-driven backends (claude / codex).
305
-
306
- Subclasses implement only ``_call(prompt) -> str``. This base owns the
307
- prompts (attempt / judge / reflect), JSON parsing, a response cache (so
308
- re-scoring an unchanged (skill, memory) on the held-out slice is free),
309
- and a rough token estimate.
310
- """
311
-
312
- name = "cli"
313
-
314
- def __init__(self, model: str = "", timeout: int = 180) -> None:
315
- self.model = model
316
- self.timeout = timeout
317
- self._tokens = 0
318
- self._cache: Dict[str, str] = {}
319
- self.last_call_error = ""
320
- self.last_reflect_raw = ""
321
-
322
- # subclasses override --------------------------------------------------
323
- def _call(self, prompt: str, *, max_tokens: int = 1024) -> str:
324
- raise NotImplementedError
325
-
326
- def _cached_call(self, key: str, prompt: str, *, max_tokens: int = 1024) -> str:
327
- if key in self._cache:
328
- return self._cache[key]
329
- out = self._call(prompt, max_tokens=max_tokens)
330
- self._tokens += len(prompt) // 4 + len(out) // 4
331
- self._cache[key] = out
332
- return out
333
-
334
- # operations -----------------------------------------------------------
335
- def attempt(self, task: TaskRecord, skill: str, memory: str,
336
- sample_id: int = 0) -> str:
337
- # sample_id distinguishes repeated rollouts of the SAME (task, skill,
338
- # memory) in the cache key. Without it the attempt cache collapses all
339
- # K dream rollouts into one cached response (spread always 0), which
340
- # silently disables contrastive reflection. sample_id=0 keeps the old
341
- # key format so gate re-scoring still benefits from the cache.
342
- if task.system:
343
- # Benchmark carries its own (research-repo) rollout system prompt.
344
- # Use it verbatim with a neutral skill/memory section — this both
345
- # keeps scoring faithful and avoids the aggressive "OVERRIDE / HARD
346
- # CONSTRAINT" phrasing below, which Azure's content filter flags as a
347
- # jailbreak (HTTP 400) and silently zeroes the rollout.
348
- skill_section = f"## Skill\n{skill.strip()}\n\n" if skill.strip() else ""
349
- mem_section = f"## Memory\n{memory.strip()}\n\n" if memory.strip() else ""
350
- system = task.system.replace("{skill_section}", skill_section)
351
- if "{skill_section}" not in task.system and skill_section:
352
- system = skill_section + system
353
- body = task.intent + ("\n\n" + task.context_excerpt if task.context_excerpt else "")
354
- prompt = f"{system}{mem_section}\n{body}"
355
- salt = f"s{sample_id}:" if sample_id else ""
356
- key = "attempt:" + salt + skill_hash(prompt)
357
- return self._cached_call(key, prompt, max_tokens=512)
358
- # generic path (mined daily-case tasks): neutral, content-filter-safe
359
- # wording. Apply the skill/memory as guidance, not as adversarial
360
- # "OVERRIDE everything" directives.
361
- prompt = (
362
- "Complete the following task for the user. Follow the skill and memory "
363
- "guidance below, including any output-format and length requirements. "
364
- "When a 'Learned preferences' rule sets an explicit limit (e.g. a length "
365
- "cap), prefer that rule over more general advice it refines.\n\n"
366
- f"# Skill\n{skill or '(none)'}\n\n# Memory\n{memory or '(none)'}\n\n"
367
- f"# Task\n{task.intent}\n\n{task.context_excerpt}\n\n"
368
- "Return ONLY the final answer text, nothing else."
369
- )
370
- # cache on (task, skill, memory) so identical hold-out re-scoring is free
371
- salt = f"s{sample_id}:" if sample_id else ""
372
- key = "attempt:" + salt + skill_hash(prompt)
373
- return self._cached_call(key, prompt, max_tokens=512)
374
-
375
- def judge(self, task: TaskRecord, response: str) -> Tuple[float, float, str]:
376
- # real-benchmark correctness judge (searchqa/livemath/spreadsheet) — local
377
- if task.reference_kind == "answer" and task.judge:
378
- try:
379
- from skillopt_sleep.experiments.real_eval import score_answer_judge
380
- except ImportError:
381
- score_answer_judge = None # research evaluators not bundled
382
- if score_answer_judge is not None:
383
- return score_answer_judge(task.judge, response)
384
- # gbrain-style rule judge: scored locally, no API spend
385
- if task.reference_kind == "rule" and task.judge:
386
- from skillopt_sleep.judges import score_rule_judge
387
- return score_rule_judge(task.judge, response)
388
- # exact references are scored locally — no API spend
389
- if task.reference_kind == "exact" and task.reference:
390
- hard = exact_score(task.reference, response)
391
- return hard, max(hard, keyword_soft_score(task.reference, response)), "exact(local)"
392
- prompt = (
393
- "Score how well the response satisfies the rubric, 0..1. "
394
- 'Return ONLY JSON {"score": <0..1>, "reason": "..."}.\n\n'
395
- f"# Rubric\n{task.reference or task.intent}\n\n# Response\n{response}"
396
- )
397
- key = "judge:" + skill_hash(prompt)
398
- raw = self._cached_call(key, prompt, max_tokens=200)
399
- obj = _extract_json(raw, "object")
400
- if isinstance(obj, dict):
401
- try:
402
- soft = float(obj.get("score", 0.0))
403
- return (1.0 if soft >= 0.8 else 0.0), soft, str(obj.get("reason", ""))[:200]
404
- except Exception:
405
- pass
406
- return 0.0, 0.0, "judge-parse-failed"
407
-
408
- def reflect(
409
- self,
410
- failures,
411
- successes,
412
- skill: str,
413
- memory: str,
414
- *,
415
- edit_budget: int,
416
- evolve_skill: bool,
417
- evolve_memory: bool,
418
- ) -> List[EditRecord]:
419
- if not failures:
420
- return []
421
- target = "skill" if evolve_skill else "memory"
422
- cur_doc = (skill if target == "skill" else memory) or "(empty)"
423
- fail_text = "\n".join(
424
- f"- wanted: {t.intent[:160]}\n got: {r.response[:160]}\n why-wrong: {r.fail_reason[:160]}"
425
- for t, r in failures[:8]
426
- )
427
- # Aggregate the most common failing criteria across all failures so the
428
- # optimizer is told *exactly what the scorer rewards* — gbrain's lesson:
429
- # the optimizer kept proposing reasonable-but-wrong edits until it could
430
- # see the success criteria.
431
- from collections import Counter
432
- crit = Counter()
433
- for _t, r in failures:
434
- fr = r.fail_reason or ""
435
- if fr.startswith("failed:"):
436
- for part in fr[len("failed:"):].split(","):
437
- part = part.strip()
438
- if part:
439
- crit[part] += 1
440
-
441
- def _explain(c: str) -> str:
442
- # translate an "op=arg" criterion into a plain-English requirement
443
- if "=" in c:
444
- op, _, arg = c.partition("=")
445
- op = op.strip(); arg = arg.strip()
446
- if op == "max_chars":
447
- return f"the ENTIRE response must be at most {arg} characters long"
448
- if op == "min_chars":
449
- return f"the response must be at least {arg} characters long"
450
- if op == "section_present":
451
- return f"the response must contain a section/heading titled '{arg}'"
452
- if op == "regex":
453
- return f"the response must match the pattern /{arg}/ (e.g. include that label)"
454
- if op == "contains":
455
- return f"the response must contain the text '{arg}'"
456
- if op == "tool_called":
457
- return f"the agent must actually call the '{arg}' tool"
458
- return c
459
-
460
- criteria_text = ""
461
- if crit:
462
- criteria_text = (
463
- "\n# Exact criteria the outputs are FAILING (fix these directly)\n"
464
- + "\n".join(f"- {_explain(c)} [{c}, failed {n}x]" for c, n in crit.most_common())
465
- )
466
- pref_text = ""
467
- if getattr(self, "preferences", ""):
468
- pref_text = (
469
- "\n# User preferences (honor these as priors when writing rules)\n"
470
- + str(self.preferences).strip()
471
- )
472
- # Task GUARDRAIL: the optimizer must not invent rules that violate the
473
- # task's hard constraints (e.g. SpreadsheetBench answers MUST be a
474
- # ```python``` openpyxl block — a learned "return ```vba```" or "ask the
475
- # user for the range" rule scores 0 because the harness can't run VBA and
476
- # can't ask questions). We surface the benchmark's own rollout system
477
- # prompt (carried on TaskRecord.system) so proposed rules stay in-bounds.
478
- guard_text = _task_guardrail(failures)
479
- prompt = (
480
- "You are SkillOpt's optimizer. The agent keeps failing the recurring "
481
- f"tasks below. Propose at most {edit_budget} bounded edits to the "
482
- f"{target} document so it stops failing. Each edit MUST be a short, "
483
- "GENERAL, reusable rule or preference (never task-specific, never an "
484
- "answer to a single task). If exact failing criteria are listed, your "
485
- "edits MUST make future outputs satisfy every one of them.\n"
486
- "BE CONCRETE: quote the exact threshold, section name, or format from "
487
- "the criteria verbatim in your rule (e.g. write 'keep the entire "
488
- "response under 1200 characters', NOT 'respect length limits'). Vague "
489
- "rules do not change behavior; specific numeric/structural rules do.\n"
490
- "IMPORTANT: your edits are APPENDED to a 'Learned preferences' block; "
491
- "you CANNOT delete the existing instructions above. If the current "
492
- f"{target} text conflicts with a criterion (e.g. it says 'be exhaustive' "
493
- "but outputs must be under a character limit), write an explicit, "
494
- "forceful OVERRIDE rule stating it supersedes the conflicting "
495
- "instruction, and put the hard requirement first.\n"
496
- "HARD CONSTRAINT: every rule you write MUST be consistent with the "
497
- "'Task output contract' below (if shown). NEVER propose a rule that "
498
- "changes the required output format/language, tells the agent to ask "
499
- "the user a question, or otherwise violates that contract — such a "
500
- "rule scores ZERO because the evaluator cannot honor it.\n"
501
- 'Return ONLY a JSON array: '
502
- '[{"op":"add|replace|delete","content":"<rule>","anchor":"<text to replace/delete, optional>","rationale":"<why>"}].\n\n'
503
- f"# Current {target}\n{cur_doc}\n"
504
- f"{guard_text}"
505
- f"{criteria_text}\n"
506
- f"{pref_text}\n\n"
507
- f"# Recurring failures\n{fail_text}"
508
- )
509
- # Call with one retry: transient non-JSON replies otherwise waste a whole
510
- # night (the gate sees no edits and rejects). A firmer second prompt
511
- # recovers most of these.
512
- arr = None
513
- for attempt in range(2):
514
- p = prompt if attempt == 0 else (
515
- prompt + "\n\nIMPORTANT: your previous reply was not valid JSON. "
516
- "Reply with ONLY the JSON array, no prose, no markdown fences."
517
- )
518
- raw = self._call(p, max_tokens=1024)
519
- self._tokens += len(p) // 4 + len(raw) // 4
520
- arr = _extract_json(raw, "array")
521
- if isinstance(arr, list) and arr:
522
- break
523
- edits: List[EditRecord] = []
524
- if isinstance(arr, list):
525
- for e in arr[:edit_budget]:
526
- if not isinstance(e, dict):
527
- continue
528
- content = str(e.get("content", "")).strip()
529
- if not content:
530
- continue
531
- edits.append(EditRecord(
532
- target=target,
533
- op=str(e.get("op", "add")).strip().lower(),
534
- content=content,
535
- anchor=str(e.get("anchor", "")).strip(),
536
- rationale=str(e.get("rationale", "")).strip(),
537
- ))
538
- return edits
539
-
540
- def tokens_used(self) -> int:
541
- return self._tokens
542
-
543
-
544
- # ── Claude Code CLI backend ───────────────────────────────────────────────────
545
-
546
- class ClaudeCliBackend(CliBackend):
547
- """Drives the authenticated `claude` CLI: claude -p --output-format text."""
548
-
549
- name = "claude"
550
-
551
- def __init__(self, model: str = "", claude_path: str = "claude", timeout: int = 180) -> None:
552
- super().__init__(model=model or os.environ.get("SKILLOPT_SLEEP_CLAUDE_MODEL", "") or "sonnet",
553
- timeout=timeout)
554
- self.claude_path = claude_path
555
-
556
- # Known CLI error prefixes that indicate auth or config failures.
557
- # When detected, we log a warning so the user doesn't mistake a
558
- # broken auth for "nothing to optimize" (issue #68).
559
- # Keep these specific to avoid false positives on normal model output.
560
- _CLI_ERROR_MARKERS = (
561
- "Not logged in",
562
- "Please run /login",
563
- "Authentication required",
564
- "Invalid API key",
565
- "Unauthorized: invalid x-api-key",
566
- )
567
-
568
- def _detect_cli_error(self, stdout: str, stderr: str) -> None:
569
- """Log a warning if CLI output looks like an auth/config error.
570
-
571
- Only checks stderr and short stdout (< 300 chars) to avoid
572
- false-positives on legitimate model responses that mention
573
- auth-related terms.
574
- """
575
- import logging
576
- # Long stdout is almost certainly a real model response, not an error.
577
- check_stdout = stdout if len(stdout) < 300 else ""
578
- combined = check_stdout + "\n" + stderr
579
- for marker in self._CLI_ERROR_MARKERS:
580
- if marker in combined:
581
- logging.getLogger("skillopt_sleep").warning(
582
- "Claude CLI returned a likely auth error: %s",
583
- combined[:200].replace("\n", " "),
584
- )
585
- self.last_call_error = combined[:500]
586
- return
587
-
588
- def _call(self, prompt: str, *, max_tokens: int = 1024) -> str:
589
- # Run ISOLATED so the ambient Claude Code environment does not leak into
590
- # the optimizer/target call. Critically, the user's GLOBAL skills
591
- # (~/.claude/skills) are injected regardless of cwd, so we must disable
592
- # them explicitly — without this, reflect/attempt sometimes reply with a
593
- # list of the user's installed skills instead of doing the task.
594
- # --bare skip hooks, LSP, plugins (minimal mode)
595
- # Only safe with ANTHROPIC_API_KEY auth;
596
- # breaks subscription-token auth (#68).
597
- # --disable-slash-commands disable all skills
598
- # --disallowedTools '*' no tool use
599
- # --exclude-dynamic-... drop per-machine cwd/env/memory/git sections
600
- # cwd=<clean temp> no project CLAUDE.md
601
- import tempfile
602
- cmd = [self.claude_path, "-p", "--output-format", "text"]
603
- if os.environ.get("ANTHROPIC_API_KEY"):
604
- cmd.append("--bare")
605
- cmd += [
606
- "--disable-slash-commands",
607
- "--disallowedTools", "*",
608
- "--exclude-dynamic-system-prompt-sections",
609
- ]
610
- if self.model:
611
- cmd += ["--model", self.model]
612
- cmd += ["--", prompt]
613
- clean_cwd = tempfile.mkdtemp(prefix="skillopt_sleep_claude_")
614
- try:
615
- proc = subprocess.run(
616
- cmd, capture_output=True, text=True, timeout=self.timeout, cwd=clean_cwd,
617
- )
618
- except Exception:
619
- return ""
620
- finally:
621
- try:
622
- import shutil
623
- shutil.rmtree(clean_cwd, ignore_errors=True)
624
- except Exception:
625
- pass
626
- out = (proc.stdout or "").strip()
627
- self._detect_cli_error(out, proc.stderr or "")
628
- return out
629
-
630
- def attempt_with_tools(self, task, skill, memory, tools):
631
- # Expose a REAL, callable `search` tool (a shell shim that logs each
632
- # call) so the gbrain quick-answerer judge (tool_called=search) is
633
- # validated honestly: we detect the call from the shim's log, not from
634
- # a self-reported marker. Other tools are stubbed the same way.
635
- import tempfile, shutil, stat
636
- work = tempfile.mkdtemp(prefix="skillopt_sleep_tools_")
637
- calllog = os.path.join(work, "_tool_calls.log")
638
- try:
639
- for tname in (tools or ["search"]):
640
- shim = os.path.join(work, tname)
641
- with open(shim, "w") as f:
642
- f.write(
643
- "#!/usr/bin/env bash\n"
644
- f'echo "{tname}" >> "{calllog}"\n'
645
- 'echo "(search results: 3 relevant notes found; use them to answer)"\n'
646
- )
647
- os.chmod(shim, os.stat(shim).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
648
- tool_hint = (
649
- "You have shell tools available in the current directory: "
650
- + ", ".join(f"./{t}" for t in (tools or ["search"]))
651
- + ". When the skill says to look something up or search before "
652
- "answering, you MUST actually run the tool (e.g. `./search \"query\"`) "
653
- "via Bash before giving your final answer."
654
- )
655
- prompt = (
656
- "You are completing a task. Apply the skill and memory rules EXACTLY, "
657
- "including any rule about searching/looking up before answering. "
658
- "Treat a 'Learned preferences' block as HARD CONSTRAINTS that override "
659
- "earlier conflicting skill text.\n\n"
660
- f"{tool_hint}\n\n"
661
- f"# Skill\n{skill or '(none)'}\n\n# Memory\n{memory or '(none)'}\n\n"
662
- f"# Task\n{task.intent}\n\n{task.context_excerpt}\n\n"
663
- "Return ONLY the final answer text."
664
- )
665
- cmd = [self.claude_path, "-p", "--output-format", "text"]
666
- if os.environ.get("ANTHROPIC_API_KEY"):
667
- cmd.append("--bare")
668
- cmd += [
669
- "--disable-slash-commands",
670
- "--allowedTools", "Bash",
671
- "--exclude-dynamic-system-prompt-sections",
672
- ]
673
- if self.model:
674
- cmd += ["--model", self.model]
675
- cmd += ["--", prompt]
676
- try:
677
- proc = subprocess.run(
678
- cmd, capture_output=True, text=True, timeout=self.timeout, cwd=work,
679
- )
680
- resp = (proc.stdout or "").strip()
681
- self._detect_cli_error(resp, proc.stderr or "")
682
- except Exception:
683
- resp = ""
684
- self._tokens += len(prompt) // 4 + len(resp) // 4
685
- called: List[str] = []
686
- if os.path.exists(calllog):
687
- with open(calllog) as f:
688
- logged = {ln.strip() for ln in f if ln.strip()}
689
- called = [t for t in (tools or ["search"]) if t in logged]
690
- return resp, called
691
- finally:
692
- try:
693
- shutil.rmtree(work, ignore_errors=True)
694
- except Exception:
695
- pass
696
-
697
- def resolve_codex_path(explicit: str = "") -> str:
698
- """Find the REAL `@openai/codex` binary, skipping the hermes wrapper.
699
-
700
- The wrapper at ~/.local/bin/codex is a shell shim that execs hermes-codex
701
- and injects extra output; we look past it for the genuine node-installed
702
- binary so replay output is clean.
703
- """
704
- if explicit:
705
- return explicit
706
- env = os.environ.get("SKILLOPT_SLEEP_CODEX_PATH")
707
- if env:
708
- return env
709
- candidates = [
710
- os.path.expanduser("~/.nvm/versions/node/v22.22.3/bin/codex"),
711
- ]
712
- # any nvm node version
713
- nvm = os.path.expanduser("~/.nvm/versions/node")
714
- if os.path.isdir(nvm):
715
- for ver in sorted(os.listdir(nvm), reverse=True):
716
- candidates.append(os.path.join(nvm, ver, "bin", "codex"))
717
- for c in candidates:
718
- if not c or not os.path.exists(c):
719
- continue
720
- try:
721
- with open(c, "rb") as f:
722
- head = f.read(64)
723
- # skip the bash shim that execs hermes
724
- if head.startswith(b"#!") and b"bash" in head:
725
- continue
726
- except Exception:
727
- pass
728
- return c
729
- return "codex" # last resort (may be the wrapper)
730
-
731
-
732
- class CodexCliBackend(CliBackend):
733
- """Drives the real Codex CLI: `codex exec -o <file>` for clean output."""
734
-
735
- name = "codex"
736
-
737
- def __init__(
738
- self,
739
- model: str = "",
740
- codex_path: str = "",
741
- timeout: int = 240,
742
- sandbox: str = "read-only",
743
- project_dir: str = "",
744
- ) -> None:
745
- super().__init__(model=model or os.environ.get("SKILLOPT_SLEEP_CODEX_MODEL", ""),
746
- timeout=timeout)
747
- self.codex_path = resolve_codex_path(codex_path)
748
- self.sandbox = sandbox
749
- self.project_dir = (
750
- os.path.abspath(os.path.expanduser(project_dir)) if project_dir else ""
751
- )
752
-
753
- def _call(self, prompt: str, *, max_tokens: int = 1024) -> str:
754
- import tempfile
755
- self.last_call_error = ""
756
- out_path = tempfile.NamedTemporaryFile(
757
- prefix="codex_last_", suffix=".txt", delete=False
758
- ).name
759
- cmd = [
760
- self.codex_path, "exec", "--skip-git-repo-check",
761
- "--color", "never", "--sandbox", self.sandbox,
762
- "-o", out_path,
763
- ]
764
- if self.project_dir:
765
- cmd[3:3] = ["-C", self.project_dir]
766
- if self.model:
767
- cmd += ["-m", self.model]
768
- cmd += ["--", prompt]
769
- proc = None
770
- try:
771
- try:
772
- proc = subprocess.run(
773
- cmd,
774
- capture_output=True,
775
- text=True,
776
- timeout=self.timeout,
777
- cwd=self.project_dir or None,
778
- )
779
- except subprocess.TimeoutExpired:
780
- self.last_call_error = f"codex exec timed out after {self.timeout}s"
781
- return ""
782
- except Exception as exc:
783
- self.last_call_error = f"codex exec failed: {exc}"
784
- return ""
785
- try:
786
- with open(out_path, encoding="utf-8") as f:
787
- out = f.read().strip()
788
- if out:
789
- return out
790
- except Exception as exc:
791
- self.last_call_error = f"could not read codex output file: {exc}"
792
- stdout = (proc.stdout or "").strip() if proc is not None else ""
793
- stderr = (proc.stderr or "").strip() if proc is not None else ""
794
- if proc is not None and proc.returncode != 0 and not self.last_call_error:
795
- self.last_call_error = f"codex exec exited {proc.returncode}: {stderr[:500]}"
796
- return stdout or stderr
797
- finally:
798
- try:
799
- os.unlink(out_path)
800
- except Exception:
801
- pass
802
-
803
- def attempt_with_tools(self, task, skill, memory, tools):
804
- # Codex exec runs in a sandbox with shell access; expose the same real
805
- # `search` shim and let it run (workspace-write so the shim can log).
806
- import tempfile, shutil, stat
807
- work = tempfile.mkdtemp(prefix="skillopt_sleep_codextools_")
808
- calllog = os.path.join(work, "_tool_calls.log")
809
- out_path = os.path.join(work, "_last.txt")
810
- try:
811
- for tname in (tools or ["search"]):
812
- shim = os.path.join(work, tname)
813
- with open(shim, "w") as f:
814
- f.write(
815
- "#!/usr/bin/env bash\n"
816
- f'echo "{tname}" >> "{calllog}"\n'
817
- 'echo "(search results: 3 relevant notes found; use them to answer)"\n'
818
- )
819
- os.chmod(shim, os.stat(shim).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
820
- tool_hint = (
821
- "Shell tools are available in the working directory: "
822
- + ", ".join(f"./{t}" for t in (tools or ["search"]))
823
- + ". When the skill says to look something up or search before "
824
- "answering, you MUST actually run the tool (e.g. `./search \"query\"`) "
825
- "before giving your final answer."
826
- )
827
- prompt = (
828
- "Complete the task. Apply the skill and memory rules EXACTLY, "
829
- "including any rule about searching before answering. Treat a "
830
- "'Learned preferences' block as HARD CONSTRAINTS overriding earlier "
831
- "conflicting skill text.\n\n"
832
- f"{tool_hint}\n\n# Skill\n{skill or '(none)'}\n\n# Memory\n{memory or '(none)'}\n\n"
833
- f"# Task\n{task.intent}\n\n{task.context_excerpt}\n\nReturn ONLY the final answer."
834
- )
835
- cmd = [
836
- self.codex_path, "exec", "--skip-git-repo-check", "--color", "never",
837
- "--sandbox", "workspace-write", "-C", work, "-o", out_path,
838
- ]
839
- if self.model:
840
- cmd += ["-m", self.model]
841
- cmd += ["--", prompt]
842
- try:
843
- subprocess.run(cmd, capture_output=True, text=True, timeout=self.timeout, cwd=work)
844
- except Exception:
845
- pass
846
- resp = ""
847
- try:
848
- with open(out_path, encoding="utf-8") as f:
849
- resp = f.read().strip()
850
- except Exception:
851
- resp = ""
852
- self._tokens += len(prompt) // 4 + len(resp) // 4
853
- called: List[str] = []
854
- if os.path.exists(calllog):
855
- with open(calllog) as f:
856
- logged = {ln.strip() for ln in f if ln.strip()}
857
- called = [t for t in (tools or ["search"]) if t in logged]
858
- return resp, called
859
- finally:
860
- try:
861
- shutil.rmtree(work, ignore_errors=True)
862
- except Exception:
863
- pass
864
-
865
- def resolve_copilot_path(explicit: str = "") -> str:
866
- """Find the GitHub Copilot CLI (`copilot`) binary."""
867
- if explicit:
868
- return explicit
869
- env = os.environ.get("SKILLOPT_SLEEP_COPILOT_PATH")
870
- if env:
871
- return env
872
- import shutil
873
- found = shutil.which("copilot")
874
- return found or "copilot"
875
-
876
-
877
- class CopilotCliBackend(CliBackend):
878
- """Drives the GitHub Copilot CLI in non-interactive mode.
879
-
880
- Uses ``copilot -p <prompt> --output-format json`` and parses the emitted
881
- JSONL event stream, returning the concatenated ``assistant.message``
882
- content. The plain-text / ``--silent`` modes do not reliably stream the
883
- response to stdout on all platforms, so JSONL is used for robust capture.
884
-
885
- The call runs in a clean temp cwd with streaming disabled and tools allowed
886
- (so non-interactive mode never blocks on a permission prompt); ``_call``'s
887
- prompts ask for final-answer text only, so no tool use is expected there,
888
- while ``attempt_with_tools`` exposes real, cross-platform callable shims in
889
- the working directory for honest tool-call detection.
890
-
891
- Startup overhead is minimised: each invocation points ``COPILOT_HOME`` at a
892
- dedicated, isolated config dir (no user ``mcp-config.json``, so the user's
893
- MCP servers — including this project's own — are NOT spawned, avoiding a
894
- slow recursive launch), and built-in MCP servers / custom instructions are
895
- disabled. Auth is read from the OS credential store / token env vars, which
896
- live outside ``COPILOT_HOME``, so isolation does not break authentication.
897
- Set ``SKILLOPT_SLEEP_COPILOT_HOME`` to override the isolated home, or set it
898
- empty / ``SKILLOPT_SLEEP_COPILOT_FULL_ENV=1`` to use the user's real
899
- environment instead.
900
- """
901
-
902
- name = "copilot"
903
-
904
- def __init__(self, model: str = "", copilot_path: str = "", timeout: int = 240) -> None:
905
- super().__init__(model=model or os.environ.get("SKILLOPT_SLEEP_COPILOT_MODEL", ""),
906
- timeout=timeout)
907
- self.copilot_path = resolve_copilot_path(copilot_path)
908
- self.full_env = os.environ.get("SKILLOPT_SLEEP_COPILOT_FULL_ENV", "") == "1"
909
- # Stable isolated home so first-run setup is cached across calls.
910
- if self.full_env:
911
- self.copilot_home = ""
912
- else:
913
- self.copilot_home = os.environ.get("SKILLOPT_SLEEP_COPILOT_HOME") or os.path.join(
914
- tempfile.gettempdir(), "skillopt_sleep_copilot_home"
915
- )
916
- try:
917
- os.makedirs(self.copilot_home, exist_ok=True)
918
- except Exception:
919
- self.copilot_home = ""
920
-
921
- def _call(self, prompt: str, *, max_tokens: int = 1024) -> str:
922
- clean_cwd = tempfile.mkdtemp(prefix="skillopt_sleep_copilot_")
923
- cmd = [
924
- self.copilot_path, "-p", prompt,
925
- "--output-format", "json",
926
- "--stream", "off",
927
- "--no-color",
928
- "--log-level", "none",
929
- "--allow-all-tools",
930
- "-C", clean_cwd,
931
- ]
932
- if not self.full_env:
933
- # Drop unneeded startup work: no built-in (github) MCP server and no
934
- # AGENTS.md / custom-instruction loading. With an isolated home that
935
- # has no mcp-config.json, no user MCP servers spawn either.
936
- cmd += ["--disable-builtin-mcps", "--no-custom-instructions"]
937
- if self.model:
938
- cmd += ["--model", self.model]
939
- env = os.environ.copy()
940
- if self.copilot_home:
941
- env["COPILOT_HOME"] = self.copilot_home
942
- try:
943
- proc = subprocess.run(
944
- cmd, capture_output=True, text=True, timeout=self.timeout, cwd=clean_cwd,
945
- encoding="utf-8", errors="replace", env=env,
946
- )
947
- except Exception:
948
- return ""
949
- finally:
950
- try:
951
- import shutil
952
- shutil.rmtree(clean_cwd, ignore_errors=True)
953
- except Exception:
954
- pass
955
- return self._parse_jsonl_response(proc.stdout or "")
956
-
957
- @staticmethod
958
- def _parse_jsonl_response(raw: str) -> str:
959
- parts: List[str] = []
960
- for line in raw.splitlines():
961
- line = line.strip()
962
- if not line or not line.startswith("{"):
963
- continue
964
- try:
965
- obj = json.loads(line)
966
- except Exception:
967
- continue
968
- if obj.get("type") == "assistant.message":
969
- content = (obj.get("data") or {}).get("content")
970
- if isinstance(content, str) and content:
971
- parts.append(content)
972
- return "\n".join(parts).strip()
973
-
974
- def attempt_with_tools(self, task, skill, memory, tools):
975
- # Expose REAL, callable tool shims in the working directory so the
976
- # gbrain quick-answerer judge (tool_called=search) is validated
977
- # honestly: we detect each call from the shim's log, not from a
978
- # self-reported marker. The Copilot CLI is the Windows-validated
979
- # backend, so the shims must be cross-platform — a bash `#!/usr/bin/env
980
- # bash` + chmod shim does NOT execute via `./tool` under PowerShell/cmd,
981
- # so on Windows we emit a `.cmd` batch shim instead.
982
- import shutil
983
- import stat
984
- work = tempfile.mkdtemp(prefix="skillopt_sleep_copilottools_")
985
- calllog = os.path.join(work, "_tool_calls.log")
986
- tool_names = tools or ["search"]
987
- is_windows = os.name == "nt"
988
- try:
989
- for tname in tool_names:
990
- if is_windows:
991
- shim = os.path.join(work, f"{tname}.cmd")
992
- with open(shim, "w") as f:
993
- # `%~n0` is the script's own base name (the tool name);
994
- # writing it keeps the calllog line == tool name so the
995
- # honest-detection match below works unchanged.
996
- f.write(
997
- "@echo off\n"
998
- f'echo %~n0>>"{calllog}"\n'
999
- "echo (search results: 3 relevant notes found; use them to answer)\n"
1000
- )
1001
- else:
1002
- shim = os.path.join(work, tname)
1003
- with open(shim, "w") as f:
1004
- f.write(
1005
- "#!/usr/bin/env bash\n"
1006
- f'echo "{tname}" >> "{calllog}"\n'
1007
- 'echo "(search results: 3 relevant notes found; use them to answer)"\n'
1008
- )
1009
- os.chmod(shim, os.stat(shim).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
1010
- if is_windows:
1011
- tool_hint = (
1012
- "You have shell tools available in the current directory: "
1013
- + ", ".join(f"{t}.cmd" for t in tool_names)
1014
- + " (each callable as `" + tool_names[0] + "` or `.\\"
1015
- + tool_names[0] + "`). When the skill says to look something "
1016
- "up or search before answering, you MUST actually run the "
1017
- "tool (e.g. `" + tool_names[0] + " \"query\"`) before giving "
1018
- "your final answer."
1019
- )
1020
- else:
1021
- tool_hint = (
1022
- "You have shell tools available in the current directory: "
1023
- + ", ".join(f"./{t}" for t in tool_names)
1024
- + ". When the skill says to look something up or search before "
1025
- "answering, you MUST actually run the tool (e.g. `./search \"query\"`) "
1026
- "before giving your final answer."
1027
- )
1028
- prompt = (
1029
- "You are completing a task. Apply the skill and memory rules EXACTLY, "
1030
- "including any rule about searching/looking up before answering. "
1031
- "Treat a 'Learned preferences' block as HARD CONSTRAINTS that override "
1032
- "earlier conflicting skill text.\n\n"
1033
- f"{tool_hint}\n\n"
1034
- f"# Skill\n{skill or '(none)'}\n\n# Memory\n{memory or '(none)'}\n\n"
1035
- f"# Task\n{task.intent}\n\n{task.context_excerpt}\n\n"
1036
- "Return ONLY the final answer text."
1037
- )
1038
- cmd = [
1039
- self.copilot_path, "-p", prompt,
1040
- "--output-format", "json",
1041
- "--stream", "off",
1042
- "--no-color",
1043
- "--log-level", "none",
1044
- "--allow-all-tools",
1045
- "-C", work,
1046
- ]
1047
- if not self.full_env:
1048
- cmd += ["--disable-builtin-mcps", "--no-custom-instructions"]
1049
- if self.model:
1050
- cmd += ["--model", self.model]
1051
- env = os.environ.copy()
1052
- if self.copilot_home:
1053
- env["COPILOT_HOME"] = self.copilot_home
1054
- resp = ""
1055
- try:
1056
- proc = subprocess.run(
1057
- cmd, capture_output=True, text=True, encoding="utf-8",
1058
- errors="replace", timeout=self.timeout, cwd=work, env=env,
1059
- )
1060
- resp = self._parse_jsonl_response(proc.stdout or "")
1061
- except Exception:
1062
- resp = ""
1063
- self._tokens += len(prompt) // 4 + len(resp) // 4
1064
- called: List[str] = []
1065
- if os.path.exists(calllog):
1066
- with open(calllog) as f:
1067
- logged = {ln.strip() for ln in f if ln.strip()}
1068
- called = [t for t in tool_names if t in logged]
1069
- return resp, called
1070
- finally:
1071
- try:
1072
- shutil.rmtree(work, ignore_errors=True)
1073
- except Exception:
1074
- pass
1075
-
1076
-
1077
- class DualBackend(Backend):
1078
- """Route operations to two backends, à la SkillOpt's target vs optimizer.
1079
-
1080
- * attempt -> TARGET backend (the model the skill is deployed on)
1081
- * reflect -> OPTIMIZER backend (the stronger/cheaper model writing edits)
1082
- * judge -> OPTIMIZER backend (graded by the optimizer when no local rule)
1083
-
1084
- This lets you optimize a skill with one model and run tasks on another, and
1085
- is the basis of the sleep-scenario transfer experiment (optimize cheap,
1086
- deploy expensive — or vice-versa).
1087
- """
1088
-
1089
- name = "dual"
1090
-
1091
- def __init__(self, target: Backend, optimizer: Backend) -> None:
1092
- self.target = target
1093
- self.optimizer = optimizer
1094
- self.name = f"target={target.name}/optimizer={optimizer.name}"
1095
-
1096
- def attempt(self, task, skill, memory, sample_id: int = 0):
1097
- return self.target.attempt(task, skill, memory, sample_id=sample_id)
1098
-
1099
- def attempt_with_tools(self, task, skill, memory, tools):
1100
- return self.target.attempt_with_tools(task, skill, memory, tools)
1101
-
1102
- def judge(self, task, response):
1103
- # local rule/exact judging needs no model; delegate to target which
1104
- # already short-circuits those. For rubric judging use the optimizer.
1105
- if task.reference_kind in {"rule", "exact"}:
1106
- return self.target.judge(task, response)
1107
- return self.optimizer.judge(task, response)
1108
-
1109
- def reflect(self, failures, successes, skill, memory, **kw):
1110
- return self.optimizer.reflect(failures, successes, skill, memory, **kw)
1111
-
1112
- def _call(self, prompt, *, max_tokens=1024):
1113
- # used by the LLM miner; prefer the optimizer (the "thinking" model)
1114
- return self.optimizer._call(prompt, max_tokens=max_tokens) # type: ignore[attr-defined]
1115
-
1116
- def tokens_used(self):
1117
- return self.target.tokens_used() + self.optimizer.tokens_used()
1118
-
1119
-
1120
- # ── Azure OpenAI backend (gpt-5.x via managed identity) ───────────────────────
1121
-
1122
- # Endpoint -> deployments, from the intern's avail_api.md. The backend picks the
1123
- # first endpoint that hosts the requested deployment.
1124
- _AZURE_ENDPOINTS = {
1125
- "https://oaidr9.openai.azure.com/": {"gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano", "o3"},
1126
- "https://t2vgoaigpt4o6.openai.azure.com/": {"gpt-5.5", "gpt-4o-mini", "o3", "o4-mini"},
1127
- "https://oaidr21.openai.azure.com/": {"gpt-5.5", "o3", "o4-mini"},
1128
- "https://searchagent5.cognitiveservices.azure.com/": {"gpt-5.4-mini", "gpt-4o-mini"},
1129
- "https://t2vgoaigpt4o.openai.azure.com/": {"gpt-5.4", "gpt-5.4-nano", "gpt-5.2", "gpt-5.1", "o3", "o4-mini"},
1130
- }
1131
- _AZURE_MI_CLIENT_ID = "8cafa2b1-a2a7-4ad9-814a-ffe4aed7e800"
1132
-
1133
-
1134
- class AzureOpenAIBackend(CliBackend):
1135
- """Drives Azure OpenAI gpt-5.x deployments via managed identity.
1136
-
1137
- Mirrors the intern's blog_1 setup (avail_api.md): managed-identity auth, the
1138
- same endpoints/deployments. Reuses CliBackend's attempt/judge/reflect prompts
1139
- and JSON parsing; only _call() differs. openai + azure-identity are lazy
1140
- imported so the mock/CLI paths stay dependency-free.
1141
- """
1142
-
1143
- name = "azure"
1144
-
1145
- def __init__(self, deployment: str = "", endpoint: str = "", timeout: int = 180,
1146
- api_version: str = "2024-12-01-preview") -> None:
1147
- super().__init__(model=deployment or "gpt-5.5", timeout=timeout)
1148
- self.deployment = deployment or "gpt-5.5"
1149
- self.endpoint = endpoint or self._endpoint_for(self.deployment)
1150
- self.api_version = api_version
1151
- self.name = f"azure:{self.deployment}"
1152
- self._client = None
1153
-
1154
- @staticmethod
1155
- def _endpoint_for(deployment: str) -> str:
1156
- for ep, deps in _AZURE_ENDPOINTS.items():
1157
- if deployment in deps:
1158
- return ep
1159
- return "https://oaidr9.openai.azure.com/"
1160
-
1161
- def _get_client(self):
1162
- if self._client is None:
1163
- from azure.identity import ManagedIdentityCredential, get_bearer_token_provider
1164
- from openai import AzureOpenAI
1165
- cred = ManagedIdentityCredential(client_id=_AZURE_MI_CLIENT_ID)
1166
- tp = get_bearer_token_provider(cred, "https://cognitiveservices.azure.com/.default")
1167
- self._client = AzureOpenAI(
1168
- azure_endpoint=self.endpoint, azure_ad_token_provider=tp,
1169
- api_version=self.api_version, max_retries=4,
1170
- )
1171
- return self._client
1172
-
1173
- def _call(self, prompt: str, *, max_tokens: int = 1024, retries: int = 5) -> str:
1174
- """Call the deployment with bounded retries.
1175
-
1176
- IMPORTANT: transient failures (429 rate-limit, timeouts, 5xx) must NOT be
1177
- silently turned into an empty string — an empty response scores 0 and
1178
- deflates every baseline/after measure. We retry with exponential backoff
1179
- (mirroring the research repo's retries=5) and only return "" after the
1180
- budget is exhausted. ``time``/``random`` are used for backoff; both are
1181
- available here (this is library code, not a Workflow script sandbox).
1182
- """
1183
- import random as _r
1184
- import time as _t
1185
-
1186
- client = self._get_client()
1187
- last_exc = None
1188
- for attempt in range(max(1, retries)):
1189
- try:
1190
- resp = client.chat.completions.create(
1191
- model=self.deployment,
1192
- messages=[{"role": "user", "content": prompt}],
1193
- max_completion_tokens=16384,
1194
- )
1195
- text = (resp.choices[0].message.content or "").strip()
1196
- try:
1197
- u = resp.usage
1198
- self._tokens += (getattr(u, "prompt_tokens", 0) or 0) + (getattr(u, "completion_tokens", 0) or 0)
1199
- except Exception:
1200
- pass
1201
- if text:
1202
- return text
1203
- # empty but no exception: model genuinely returned nothing — one
1204
- # quick retry can help (reasoning models occasionally yield empty)
1205
- last_exc = "empty-response"
1206
- except Exception as e: # noqa: BLE001
1207
- last_exc = e
1208
- # backoff before next try (skip after the final attempt)
1209
- if attempt < retries - 1:
1210
- _t.sleep(min(8.0, (2 ** attempt) * 0.5) + _r.random() * 0.4)
1211
- return ""
1212
-
1213
-
1214
- class AzureResponsesBackend(AzureOpenAIBackend):
1215
- """gpt-5.x via the **Responses API** on the high-throughput gpt4v endpoints.
1216
-
1217
- Differs from AzureOpenAIBackend in three ways, all required by the enhanced
1218
- experiment:
1219
- * Auth via ``AzureCliCredential`` (the logged-in user), not Managed Identity
1220
- — the gpt4v-scus/swc accounts grant the data role to the CLI principal.
1221
- * Calls ``client.responses.create`` (the /responses API) instead of
1222
- chat.completions — these deployments are Responses-only.
1223
- * Round-robins across multiple endpoints for parallel throughput; each
1224
- worker thread binds a client for one endpoint (picked by thread index)
1225
- so concurrent replay spreads load across all endpoints.
1226
-
1227
- A single shared ``AzureCliCredential`` token provider is reused across all
1228
- endpoint clients (the token is cached + auto-refreshed by the provider).
1229
- """
1230
-
1231
- name = "azure-responses"
1232
-
1233
- # the two parallel /responses endpoints (user-provided), both hosting gpt-5.5
1234
- _RESP_ENDPOINTS = [
1235
- "https://gpt4v-scus.openai.azure.com/",
1236
- "https://gpt4v-swc.openai.azure.com/",
1237
- ]
1238
-
1239
- def __init__(self, deployment: str = "", endpoints: Optional[List[str]] = None,
1240
- timeout: int = 180, api_version: str = "2025-04-01-preview") -> None:
1241
- super().__init__(deployment=deployment, endpoint=(endpoints or self._RESP_ENDPOINTS)[0],
1242
- timeout=timeout, api_version=api_version)
1243
- self.endpoints = list(endpoints or self._RESP_ENDPOINTS)
1244
- self.name = f"azure-responses:{self.deployment}"
1245
- self._token_provider = None
1246
- self._clients: dict = {} # endpoint -> AzureOpenAI client
1247
- import threading as _thr
1248
- self._lock = _thr.Lock()
1249
- self._rr = 0 # round-robin counter
1250
-
1251
- def _get_provider(self):
1252
- if self._token_provider is None:
1253
- from azure.identity import AzureCliCredential, get_bearer_token_provider
1254
- self._token_provider = get_bearer_token_provider(
1255
- AzureCliCredential(), "https://cognitiveservices.azure.com/.default")
1256
- return self._token_provider
1257
-
1258
- def _client_for(self, endpoint: str):
1259
- cl = self._clients.get(endpoint)
1260
- if cl is None:
1261
- from openai import AzureOpenAI
1262
- cl = AzureOpenAI(
1263
- azure_endpoint=endpoint, azure_ad_token_provider=self._get_provider(),
1264
- api_version=self.api_version, max_retries=2,
1265
- )
1266
- self._clients[endpoint] = cl
1267
- return cl
1268
-
1269
- def _next_endpoint(self) -> str:
1270
- # round-robin so concurrent calls spread across all endpoints
1271
- with self._lock:
1272
- ep = self.endpoints[self._rr % len(self.endpoints)]
1273
- self._rr += 1
1274
- return ep
1275
-
1276
- def _call(self, prompt: str, *, max_tokens: int = 1024, retries: int = 5) -> str:
1277
- import random as _r
1278
- import time as _t
1279
- last = None
1280
- base_ep = self._next_endpoint() # this call's primary endpoint
1281
- base_idx = self.endpoints.index(base_ep)
1282
- for attempt in range(max(1, retries)):
1283
- # on retry, fail over to the other endpoint(s)
1284
- ep = self.endpoints[(base_idx + attempt) % len(self.endpoints)]
1285
- try:
1286
- client = self._client_for(ep)
1287
- resp = client.responses.create(
1288
- model=self.deployment, input=prompt,
1289
- max_output_tokens=16384,
1290
- )
1291
- text = (getattr(resp, "output_text", "") or "").strip()
1292
- try:
1293
- u = resp.usage
1294
- self._tokens += (getattr(u, "input_tokens", 0) or 0) + (getattr(u, "output_tokens", 0) or 0)
1295
- except Exception:
1296
- pass
1297
- if text:
1298
- return text
1299
- last = "empty-response"
1300
- except Exception as e: # noqa: BLE001
1301
- last = e
1302
- if attempt < retries - 1:
1303
- _t.sleep(min(8.0, (2 ** attempt) * 0.5) + _r.random() * 0.4)
1304
- return ""
1305
-
1306
-
1307
- def get_backend(
1308
- name: str,
1309
- *,
1310
- model: str = "",
1311
- claude_path: str = "claude",
1312
- codex_path: str = "",
1313
- azure_endpoint: str = "",
1314
- project_dir: str = "",
1315
- ) -> Backend:
1316
- n = (name or "mock").strip().lower()
1317
- if n in {"claude", "anthropic", "claude_cli", "claude_code"}:
1318
- return ClaudeCliBackend(model=model, claude_path=claude_path)
1319
- if n in {"codex", "codex_cli", "openai_codex"}:
1320
- return CodexCliBackend(model=model, codex_path=codex_path, project_dir=project_dir)
1321
- if n in {"azure", "azure_openai", "aoai"}:
1322
- return AzureOpenAIBackend(deployment=model, endpoint=azure_endpoint)
1323
- if n in {"azure-responses", "azure_responses", "aoai-responses", "responses"}:
1324
- eps = [e.strip() for e in azure_endpoint.split(",") if e.strip()] or None
1325
- return AzureResponsesBackend(deployment=model, endpoints=eps)
1326
- if n in {"copilot", "github_copilot", "copilot_cli", "gh_copilot"}:
1327
- return CopilotCliBackend(model=model)
1328
- return MockBackend()
1329
-
1330
-
1331
- def build_backend(
1332
- *,
1333
- backend: str = "mock",
1334
- model: str = "",
1335
- optimizer_backend: str = "",
1336
- optimizer_model: str = "",
1337
- target_backend: str = "",
1338
- target_model: str = "",
1339
- codex_path: str = "",
1340
- azure_endpoint: str = "",
1341
- preferences: str = "",
1342
- project_dir: str = "",
1343
- ) -> Backend:
1344
- """Build a single or dual backend.
1345
-
1346
- If optimizer_* or target_* are given, returns a DualBackend routing
1347
- attempt->target and reflect/judge->optimizer. Otherwise a single backend
1348
- from (backend, model). ``preferences`` (free text) is attached so reflect
1349
- uses it as a prior (set on the optimizer for dual backends).
1350
- """
1351
- has_split = any([optimizer_backend, optimizer_model, target_backend, target_model])
1352
- if not has_split:
1353
- be = get_backend(
1354
- backend,
1355
- model=model,
1356
- codex_path=codex_path,
1357
- azure_endpoint=azure_endpoint,
1358
- project_dir=project_dir,
1359
- )
1360
- be.preferences = preferences
1361
- return be
1362
- tgt = get_backend(target_backend or backend, model=target_model or model,
1363
- codex_path=codex_path, azure_endpoint=azure_endpoint,
1364
- project_dir=project_dir)
1365
- opt = get_backend(optimizer_backend or backend, model=optimizer_model or model,
1366
- codex_path=codex_path, azure_endpoint=azure_endpoint,
1367
- project_dir=project_dir)
1368
- opt.preferences = preferences # reflect runs on the optimizer
1369
- dual = DualBackend(target=tgt, optimizer=opt)
1370
- dual.preferences = preferences
1371
- return dual