aether-colony 5.0.0 → 5.2.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 (317) hide show
  1. package/.aether/aether-utils.sh +3226 -3345
  2. package/.aether/agents-claude/aether-ambassador.md +265 -0
  3. package/.aether/agents-claude/aether-archaeologist.md +327 -0
  4. package/.aether/agents-claude/aether-architect.md +236 -0
  5. package/.aether/agents-claude/aether-auditor.md +271 -0
  6. package/.aether/agents-claude/aether-builder.md +224 -0
  7. package/.aether/agents-claude/aether-chaos.md +269 -0
  8. package/.aether/agents-claude/aether-chronicler.md +305 -0
  9. package/.aether/agents-claude/aether-gatekeeper.md +330 -0
  10. package/.aether/agents-claude/aether-includer.md +374 -0
  11. package/.aether/agents-claude/aether-keeper.md +272 -0
  12. package/.aether/agents-claude/aether-measurer.md +322 -0
  13. package/.aether/agents-claude/aether-oracle.md +237 -0
  14. package/.aether/agents-claude/aether-probe.md +211 -0
  15. package/.aether/agents-claude/aether-queen.md +330 -0
  16. package/.aether/agents-claude/aether-route-setter.md +178 -0
  17. package/.aether/agents-claude/aether-sage.md +418 -0
  18. package/.aether/agents-claude/aether-scout.md +179 -0
  19. package/.aether/agents-claude/aether-surveyor-disciplines.md +417 -0
  20. package/.aether/agents-claude/aether-surveyor-nest.md +355 -0
  21. package/.aether/agents-claude/aether-surveyor-pathogens.md +289 -0
  22. package/.aether/agents-claude/aether-surveyor-provisions.md +360 -0
  23. package/.aether/agents-claude/aether-tracker.md +270 -0
  24. package/.aether/agents-claude/aether-watcher.md +280 -0
  25. package/.aether/agents-claude/aether-weaver.md +248 -0
  26. package/.aether/commands/archaeology.yaml +653 -0
  27. package/.aether/commands/build.yaml +1221 -0
  28. package/.aether/commands/chaos.yaml +653 -0
  29. package/.aether/commands/colonize.yaml +442 -0
  30. package/.aether/commands/continue.yaml +1484 -0
  31. package/.aether/commands/council.yaml +509 -0
  32. package/.aether/commands/data-clean.yaml +80 -0
  33. package/.aether/commands/dream.yaml +275 -0
  34. package/.aether/commands/entomb.yaml +863 -0
  35. package/.aether/commands/export-signals.yaml +64 -0
  36. package/.aether/commands/feedback.yaml +158 -0
  37. package/.aether/commands/flag.yaml +160 -0
  38. package/.aether/commands/flags.yaml +177 -0
  39. package/.aether/commands/focus.yaml +112 -0
  40. package/.aether/commands/help.yaml +167 -0
  41. package/.aether/commands/history.yaml +137 -0
  42. package/.aether/commands/import-signals.yaml +79 -0
  43. package/.aether/commands/init.yaml +502 -0
  44. package/.aether/commands/insert-phase.yaml +102 -0
  45. package/.aether/commands/interpret.yaml +285 -0
  46. package/.aether/commands/lay-eggs.yaml +224 -0
  47. package/.aether/commands/maturity.yaml +122 -0
  48. package/.aether/commands/memory-details.yaml +74 -0
  49. package/.aether/commands/migrate-state.yaml +174 -0
  50. package/.aether/commands/oracle.yaml +1224 -0
  51. package/.aether/commands/organize.yaml +446 -0
  52. package/.aether/commands/patrol.yaml +621 -0
  53. package/.aether/commands/pause-colony.yaml +424 -0
  54. package/.aether/commands/phase.yaml +124 -0
  55. package/.aether/commands/pheromones.yaml +153 -0
  56. package/.aether/commands/plan.yaml +1364 -0
  57. package/.aether/commands/preferences.yaml +63 -0
  58. package/.aether/commands/quick.yaml +104 -0
  59. package/.aether/commands/redirect.yaml +123 -0
  60. package/.aether/commands/resume-colony.yaml +375 -0
  61. package/.aether/commands/resume.yaml +407 -0
  62. package/.aether/commands/run.yaml +229 -0
  63. package/.aether/commands/seal.yaml +1214 -0
  64. package/.aether/commands/skill-create.yaml +337 -0
  65. package/.aether/commands/status.yaml +408 -0
  66. package/.aether/commands/swarm.yaml +352 -0
  67. package/.aether/commands/tunnels.yaml +814 -0
  68. package/.aether/commands/update.yaml +131 -0
  69. package/.aether/commands/verify-castes.yaml +159 -0
  70. package/.aether/commands/watch.yaml +454 -0
  71. package/.aether/docs/INCIDENT_TEMPLATE.md +32 -0
  72. package/.aether/docs/QUEEN-SYSTEM.md +11 -11
  73. package/.aether/docs/README.md +32 -2
  74. package/.aether/docs/command-playbooks/README.md +23 -0
  75. package/.aether/docs/command-playbooks/build-complete.md +349 -0
  76. package/.aether/docs/command-playbooks/build-context.md +282 -0
  77. package/.aether/docs/command-playbooks/build-full.md +1683 -0
  78. package/.aether/docs/command-playbooks/build-prep.md +284 -0
  79. package/.aether/docs/command-playbooks/build-verify.md +405 -0
  80. package/.aether/docs/command-playbooks/build-wave.md +749 -0
  81. package/.aether/docs/command-playbooks/continue-advance.md +524 -0
  82. package/.aether/docs/command-playbooks/continue-finalize.md +447 -0
  83. package/.aether/docs/command-playbooks/continue-full.md +1725 -0
  84. package/.aether/docs/command-playbooks/continue-gates.md +686 -0
  85. package/.aether/docs/command-playbooks/continue-verify.md +407 -0
  86. package/.aether/docs/context-continuity.md +84 -0
  87. package/.aether/docs/disciplines/DISCIPLINES.md +9 -7
  88. package/.aether/docs/error-codes.md +1 -1
  89. package/.aether/docs/known-issues.md +34 -173
  90. package/.aether/docs/pheromones.md +86 -6
  91. package/.aether/docs/plans/pheromone-display-plan.md +257 -0
  92. package/.aether/docs/queen-commands.md +10 -9
  93. package/.aether/docs/source-of-truth-map.md +132 -0
  94. package/.aether/docs/xml-utilities.md +47 -0
  95. package/.aether/rules/aether-colony.md +23 -13
  96. package/.aether/scripts/incident-test-add.sh +47 -0
  97. package/.aether/scripts/weekly-audit.sh +79 -0
  98. package/.aether/skills/.index.json +649 -0
  99. package/.aether/skills/colony/.manifest.json +16 -0
  100. package/.aether/skills/colony/build-discipline/SKILL.md +78 -0
  101. package/.aether/skills/colony/colony-interaction/SKILL.md +56 -0
  102. package/.aether/skills/colony/colony-lifecycle/SKILL.md +77 -0
  103. package/.aether/skills/colony/colony-visuals/SKILL.md +112 -0
  104. package/.aether/skills/colony/context-management/SKILL.md +80 -0
  105. package/.aether/skills/colony/error-presentation/SKILL.md +99 -0
  106. package/.aether/skills/colony/pheromone-protocol/SKILL.md +79 -0
  107. package/.aether/skills/colony/pheromone-visibility/SKILL.md +81 -0
  108. package/.aether/skills/colony/state-safety/SKILL.md +84 -0
  109. package/.aether/skills/colony/worker-priming/SKILL.md +82 -0
  110. package/.aether/skills/domain/.manifest.json +24 -0
  111. package/.aether/skills/domain/README.md +33 -0
  112. package/.aether/skills/domain/django/SKILL.md +49 -0
  113. package/.aether/skills/domain/docker/SKILL.md +52 -0
  114. package/.aether/skills/domain/golang/SKILL.md +52 -0
  115. package/.aether/skills/domain/graphql/SKILL.md +51 -0
  116. package/.aether/skills/domain/html-css/SKILL.md +48 -0
  117. package/.aether/skills/domain/nextjs/SKILL.md +45 -0
  118. package/.aether/skills/domain/nodejs/SKILL.md +53 -0
  119. package/.aether/skills/domain/postgresql/SKILL.md +53 -0
  120. package/.aether/skills/domain/prisma/SKILL.md +59 -0
  121. package/.aether/skills/domain/python/SKILL.md +50 -0
  122. package/.aether/skills/domain/rails/SKILL.md +52 -0
  123. package/.aether/skills/domain/react/SKILL.md +45 -0
  124. package/.aether/skills/domain/rest-api/SKILL.md +58 -0
  125. package/.aether/skills/domain/svelte/SKILL.md +47 -0
  126. package/.aether/skills/domain/tailwind/SKILL.md +45 -0
  127. package/.aether/skills/domain/testing/SKILL.md +53 -0
  128. package/.aether/skills/domain/typescript/SKILL.md +58 -0
  129. package/.aether/skills/domain/vue/SKILL.md +49 -0
  130. package/.aether/templates/QUEEN.md.template +23 -41
  131. package/.aether/templates/colony-state-reset.jq.template +1 -0
  132. package/.aether/templates/colony-state.template.json +4 -0
  133. package/.aether/templates/learning-observations.template.json +6 -0
  134. package/.aether/templates/midden.template.json +13 -0
  135. package/.aether/templates/pheromones.template.json +6 -0
  136. package/.aether/templates/session.template.json +9 -0
  137. package/.aether/utils/atomic-write.sh +63 -17
  138. package/.aether/utils/chamber-utils.sh +145 -2
  139. package/.aether/utils/council.sh +425 -0
  140. package/.aether/utils/emoji-audit.sh +166 -0
  141. package/.aether/utils/error-handler.sh +21 -7
  142. package/.aether/utils/file-lock.sh +182 -27
  143. package/.aether/utils/flag.sh +278 -0
  144. package/.aether/utils/hive.sh +572 -0
  145. package/.aether/utils/immune.sh +508 -0
  146. package/.aether/utils/learning.sh +1928 -0
  147. package/.aether/utils/midden.sh +520 -0
  148. package/.aether/utils/oracle/oracle.md +168 -0
  149. package/.aether/utils/oracle/oracle.sh +1023 -0
  150. package/.aether/utils/pheromone.sh +2029 -0
  151. package/.aether/utils/queen.sh +1710 -0
  152. package/.aether/utils/scan.sh +860 -0
  153. package/.aether/utils/semantic-cli.sh +10 -8
  154. package/.aether/utils/session.sh +816 -0
  155. package/.aether/utils/skills.sh +509 -0
  156. package/.aether/utils/spawn-tree.sh +103 -271
  157. package/.aether/utils/spawn.sh +260 -0
  158. package/.aether/utils/state-api.sh +389 -0
  159. package/.aether/utils/state-loader.sh +8 -6
  160. package/.aether/utils/suggest.sh +611 -0
  161. package/.aether/utils/swarm-display.sh +10 -1
  162. package/.aether/utils/swarm.sh +1004 -0
  163. package/.aether/utils/watch-spawn-tree.sh +11 -2
  164. package/.aether/utils/xml-compose.sh +2 -2
  165. package/.aether/utils/xml-convert.sh +9 -5
  166. package/.aether/utils/xml-core.sh +5 -9
  167. package/.aether/utils/xml-query.sh +4 -4
  168. package/.aether/workers.md +86 -67
  169. package/.claude/agents/ant/aether-ambassador.md +2 -1
  170. package/.claude/agents/ant/aether-archaeologist.md +6 -1
  171. package/.claude/agents/ant/aether-architect.md +236 -0
  172. package/.claude/agents/ant/aether-auditor.md +6 -1
  173. package/.claude/agents/ant/aether-builder.md +38 -1
  174. package/.claude/agents/ant/aether-chaos.md +2 -1
  175. package/.claude/agents/ant/aether-chronicler.md +1 -0
  176. package/.claude/agents/ant/aether-gatekeeper.md +6 -1
  177. package/.claude/agents/ant/aether-includer.md +1 -0
  178. package/.claude/agents/ant/aether-keeper.md +1 -0
  179. package/.claude/agents/ant/aether-measurer.md +6 -1
  180. package/.claude/agents/ant/aether-oracle.md +237 -0
  181. package/.claude/agents/ant/aether-probe.md +2 -1
  182. package/.claude/agents/ant/aether-queen.md +6 -1
  183. package/.claude/agents/ant/aether-route-setter.md +6 -1
  184. package/.claude/agents/ant/aether-sage.md +68 -3
  185. package/.claude/agents/ant/aether-scout.md +38 -1
  186. package/.claude/agents/ant/aether-surveyor-disciplines.md +2 -1
  187. package/.claude/agents/ant/aether-surveyor-nest.md +2 -1
  188. package/.claude/agents/ant/aether-surveyor-pathogens.md +2 -1
  189. package/.claude/agents/ant/aether-surveyor-provisions.md +2 -1
  190. package/.claude/agents/ant/aether-tracker.md +6 -1
  191. package/.claude/agents/ant/aether-watcher.md +37 -1
  192. package/.claude/agents/ant/aether-weaver.md +2 -1
  193. package/.claude/commands/ant/archaeology.md +1 -8
  194. package/.claude/commands/ant/build.md +43 -1159
  195. package/.claude/commands/ant/chaos.md +1 -14
  196. package/.claude/commands/ant/colonize.md +3 -14
  197. package/.claude/commands/ant/continue.md +40 -1026
  198. package/.claude/commands/ant/council.md +213 -15
  199. package/.claude/commands/ant/data-clean.md +81 -0
  200. package/.claude/commands/ant/dream.md +12 -9
  201. package/.claude/commands/ant/entomb.md +62 -87
  202. package/.claude/commands/ant/export-signals.md +57 -0
  203. package/.claude/commands/ant/feedback.md +18 -0
  204. package/.claude/commands/ant/flag.md +12 -0
  205. package/.claude/commands/ant/flags.md +22 -8
  206. package/.claude/commands/ant/focus.md +18 -0
  207. package/.claude/commands/ant/help.md +40 -8
  208. package/.claude/commands/ant/history.md +3 -0
  209. package/.claude/commands/ant/import-signals.md +71 -0
  210. package/.claude/commands/ant/init.md +349 -191
  211. package/.claude/commands/ant/insert-phase.md +105 -0
  212. package/.claude/commands/ant/interpret.md +11 -0
  213. package/.claude/commands/ant/lay-eggs.md +167 -158
  214. package/.claude/commands/ant/maturity.md +22 -11
  215. package/.claude/commands/ant/memory-details.md +77 -0
  216. package/.claude/commands/ant/migrate-state.md +6 -0
  217. package/.claude/commands/ant/oracle.md +317 -62
  218. package/.claude/commands/ant/organize.md +10 -5
  219. package/.claude/commands/ant/patrol.md +620 -0
  220. package/.claude/commands/ant/pause-colony.md +8 -22
  221. package/.claude/commands/ant/phase.md +26 -37
  222. package/.claude/commands/ant/pheromones.md +156 -0
  223. package/.claude/commands/ant/plan.md +199 -50
  224. package/.claude/commands/ant/preferences.md +65 -0
  225. package/.claude/commands/ant/quick.md +100 -0
  226. package/.claude/commands/ant/redirect.md +18 -0
  227. package/.claude/commands/ant/resume-colony.md +37 -22
  228. package/.claude/commands/ant/resume.md +60 -7
  229. package/.claude/commands/ant/run.md +231 -0
  230. package/.claude/commands/ant/seal.md +506 -78
  231. package/.claude/commands/ant/skill-create.md +286 -0
  232. package/.claude/commands/ant/status.md +171 -1
  233. package/.claude/commands/ant/swarm.md +11 -23
  234. package/.claude/commands/ant/tunnels.md +1 -0
  235. package/.claude/commands/ant/update.md +58 -135
  236. package/.claude/commands/ant/verify-castes.md +90 -42
  237. package/.claude/commands/ant/watch.md +1 -0
  238. package/.opencode/agents/aether-ambassador.md +1 -1
  239. package/.opencode/agents/aether-architect.md +133 -0
  240. package/.opencode/agents/aether-builder.md +3 -3
  241. package/.opencode/agents/aether-oracle.md +137 -0
  242. package/.opencode/agents/aether-queen.md +1 -1
  243. package/.opencode/agents/aether-route-setter.md +1 -1
  244. package/.opencode/agents/aether-scout.md +1 -1
  245. package/.opencode/agents/aether-surveyor-disciplines.md +6 -1
  246. package/.opencode/agents/aether-surveyor-nest.md +6 -1
  247. package/.opencode/agents/aether-surveyor-pathogens.md +6 -1
  248. package/.opencode/agents/aether-surveyor-provisions.md +6 -1
  249. package/.opencode/agents/aether-tracker.md +1 -1
  250. package/.opencode/agents/aether-watcher.md +1 -1
  251. package/.opencode/agents/aether-weaver.md +1 -1
  252. package/.opencode/commands/ant/archaeology.md +7 -14
  253. package/.opencode/commands/ant/build.md +54 -88
  254. package/.opencode/commands/ant/chaos.md +7 -24
  255. package/.opencode/commands/ant/colonize.md +10 -17
  256. package/.opencode/commands/ant/continue.md +595 -66
  257. package/.opencode/commands/ant/council.md +150 -18
  258. package/.opencode/commands/ant/data-clean.md +77 -0
  259. package/.opencode/commands/ant/dream.md +15 -17
  260. package/.opencode/commands/ant/entomb.md +28 -18
  261. package/.opencode/commands/ant/export-signals.md +54 -0
  262. package/.opencode/commands/ant/feedback.md +24 -5
  263. package/.opencode/commands/ant/flag.md +16 -4
  264. package/.opencode/commands/ant/flags.md +24 -10
  265. package/.opencode/commands/ant/focus.md +22 -5
  266. package/.opencode/commands/ant/help.md +41 -8
  267. package/.opencode/commands/ant/history.md +9 -0
  268. package/.opencode/commands/ant/import-signals.md +68 -0
  269. package/.opencode/commands/ant/init.md +396 -154
  270. package/.opencode/commands/ant/insert-phase.md +111 -0
  271. package/.opencode/commands/ant/interpret.md +16 -0
  272. package/.opencode/commands/ant/lay-eggs.md +184 -112
  273. package/.opencode/commands/ant/maturity.md +18 -2
  274. package/.opencode/commands/ant/memory-details.md +83 -0
  275. package/.opencode/commands/ant/migrate-state.md +12 -0
  276. package/.opencode/commands/ant/oracle.md +322 -67
  277. package/.opencode/commands/ant/organize.md +14 -12
  278. package/.opencode/commands/ant/patrol.md +626 -0
  279. package/.opencode/commands/ant/pause-colony.md +12 -29
  280. package/.opencode/commands/ant/phase.md +30 -40
  281. package/.opencode/commands/ant/pheromones.md +162 -0
  282. package/.opencode/commands/ant/plan.md +210 -57
  283. package/.opencode/commands/ant/preferences.md +71 -0
  284. package/.opencode/commands/ant/quick.md +91 -0
  285. package/.opencode/commands/ant/redirect.md +22 -5
  286. package/.opencode/commands/ant/resume-colony.md +41 -29
  287. package/.opencode/commands/ant/resume.md +80 -20
  288. package/.opencode/commands/ant/run.md +237 -0
  289. package/.opencode/commands/ant/seal.md +230 -25
  290. package/.opencode/commands/ant/skill-create.md +63 -0
  291. package/.opencode/commands/ant/status.md +125 -30
  292. package/.opencode/commands/ant/swarm.md +3 -345
  293. package/.opencode/commands/ant/tunnels.md +3 -9
  294. package/.opencode/commands/ant/update.md +63 -127
  295. package/.opencode/commands/ant/verify-castes.md +96 -42
  296. package/.opencode/commands/ant/watch.md +7 -0
  297. package/CHANGELOG.md +368 -1
  298. package/README.md +195 -324
  299. package/bin/cli.js +236 -429
  300. package/bin/generate-commands.js +186 -0
  301. package/bin/generate-commands.sh +128 -89
  302. package/bin/lib/spawn-logger.js +0 -15
  303. package/bin/lib/update-transaction.js +285 -35
  304. package/bin/npx-install.js +178 -0
  305. package/bin/validate-package.sh +85 -3
  306. package/package.json +16 -4
  307. package/.aether/CONTEXT.md +0 -160
  308. package/.aether/docs/QUEEN.md +0 -84
  309. package/.aether/exchange/colony-registry.xml +0 -11
  310. package/.aether/exchange/pheromones.xml +0 -87
  311. package/.aether/exchange/queen-wisdom.xml +0 -14
  312. package/.aether/model-profiles.yaml +0 -100
  313. package/.aether/utils/spawn-with-model.sh +0 -56
  314. package/bin/lib/model-profiles.js +0 -445
  315. package/bin/lib/model-verify.js +0 -288
  316. package/bin/lib/proxy-health.js +0 -253
  317. package/bin/lib/telemetry.js +0 -441
@@ -0,0 +1,1928 @@
1
+ #!/usr/bin/env bash
2
+ # Learning and instinct utility functions -- extracted from aether-utils.sh
3
+ # Provides: _learning_promote, _learning_inject, _learning_observe, _learning_check_promotion,
4
+ # _learning_promote_auto, _learning_display_proposals, _learning_select_proposals,
5
+ # _learning_defer_proposals, _learning_approve_proposals, _learning_undo_promotions,
6
+ # _instinct_read, _instinct_create, _instinct_apply, _learning_extract_fallback
7
+ # Note: Uses get_wisdom_threshold() and get_wisdom_thresholds_json() from main file.
8
+ # Cross-domain calls (queen-promote, pheromone-write, activity-log, rolling-summary,
9
+ # generate-threshold-bar, parse-selection) are all via subprocess dispatch (bash "$0").
10
+
11
+ # ============================================================================
12
+ # _learning_promote
13
+ # Promote a learning to the global learnings file
14
+ # Usage: learning-promote <content> <source_project> <source_phase> [tags]
15
+ # ============================================================================
16
+ _learning_promote() {
17
+ [[ $# -ge 3 ]] || json_err "$E_VALIDATION_FAILED" "Usage: learning-promote <content> <source_project> <source_phase> [tags]"
18
+ content="$1"
19
+ source_project="$2"
20
+ source_phase="$3"
21
+ tags="${4:-}"
22
+
23
+ mkdir -p "$DATA_DIR"
24
+ global_file="$COLONY_DATA_DIR/learnings.json"
25
+
26
+ if [[ ! -f "$global_file" ]]; then
27
+ atomic_write "$global_file" '{"learnings":[],"version":1}' || json_err "$E_UNKNOWN" "Failed to initialize learnings file"
28
+ fi
29
+
30
+ id="global_$(date -u +%s)_$(head -c 2 /dev/urandom | od -An -tx1 | tr -d ' ')"
31
+ ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
32
+
33
+ if [[ -n "$tags" ]]; then
34
+ tags_json=$(echo "$tags" | jq -R 'split(",")')
35
+ else
36
+ tags_json="[]"
37
+ fi
38
+
39
+ current_count=$(jq '.learnings | length' "$global_file")
40
+ if [[ $current_count -ge 50 ]]; then
41
+ json_ok "{\"promoted\":false,\"reason\":\"cap_reached\",\"current_count\":$current_count,\"cap\":50}"
42
+ exit 0
43
+ fi
44
+
45
+ updated=$(jq --arg id "$id" --arg content "$content" --arg sp "$source_project" \
46
+ --arg phase "$source_phase" --argjson tags "$tags_json" --arg ts "$ts" '
47
+ .learnings += [{
48
+ id: $id,
49
+ content: $content,
50
+ source_project: $sp,
51
+ source_phase: $phase,
52
+ tags: $tags,
53
+ promoted_at: $ts
54
+ }]
55
+ ' "$global_file") || json_err "$E_JSON_INVALID" "Failed to update learnings.json"
56
+
57
+ atomic_write "$global_file" "$updated" || {
58
+ _aether_log_error "Could not save updated learnings"
59
+ json_err "$E_UNKNOWN" "Failed to write learnings file"
60
+ }
61
+ json_ok "$(jq -n --arg id "$id" --argjson count "$((current_count + 1))" '{promoted: true, id: $id, count: $count, cap: 50}')"
62
+ }
63
+
64
+ # ============================================================================
65
+ # _learning_inject
66
+ # Filter learnings by tech keywords for worker context injection
67
+ # Usage: learning-inject <tech_keywords_csv>
68
+ # ============================================================================
69
+ _learning_inject() {
70
+ [[ $# -ge 1 ]] || json_err "$E_VALIDATION_FAILED" "Usage: learning-inject <tech_keywords_csv>"
71
+ keywords="$1"
72
+
73
+ global_file="$COLONY_DATA_DIR/learnings.json"
74
+
75
+ if [[ ! -f "$global_file" ]]; then
76
+ json_ok '{"learnings":[],"count":0}'
77
+ exit 0
78
+ fi
79
+
80
+ json_ok "$(jq --arg kw "$keywords" '
81
+ ($kw | split(",") | map(ascii_downcase | ltrimstr(" ") | rtrimstr(" "))) as $keywords |
82
+ .learnings | map(
83
+ select(
84
+ .tags as $tags |
85
+ ($keywords | any(. as $k | $tags | any(ascii_downcase | contains($k))))
86
+ )
87
+ ) | {learnings: ., count: length}
88
+ ' "$global_file")"
89
+ }
90
+
91
+ # ============================================================================
92
+ # _learning_observe
93
+ # Record observation of a learning across colonies
94
+ # Usage: learning-observe <content> <wisdom_type> [colony_name]
95
+ # Returns: JSON with observation_count, threshold status, and colonies list
96
+ # ============================================================================
97
+ _learning_observe() {
98
+ # Record observation of a learning across colonies
99
+ # Usage: learning-observe <content> <wisdom_type> [colony_name]
100
+ # Returns: JSON with observation_count, threshold status, and colonies list
101
+ content="${1:-}"
102
+ wisdom_type="${2:-}"
103
+ colony_name="${3:-unknown}"
104
+
105
+ # Validate required arguments
106
+ [[ -z "$content" ]] && json_err "$E_VALIDATION_FAILED" "Usage: learning-observe <content> <wisdom_type> [colony_name]" '{"missing":"content"}'
107
+ [[ -z "$wisdom_type" ]] && json_err "$E_VALIDATION_FAILED" "Usage: learning-observe <content> <wisdom_type> [colony_name]" '{"missing":"wisdom_type"}'
108
+
109
+ # Validate wisdom_type
110
+ valid_types=("philosophy" "pattern" "redirect" "stack" "decree" "failure")
111
+ type_valid=false
112
+ for vt in "${valid_types[@]}"; do
113
+ [[ "$wisdom_type" == "$vt" ]] && type_valid=true && break
114
+ done
115
+ [[ "$type_valid" == "false" ]] && json_err "$E_VALIDATION_FAILED" "Invalid wisdom_type: $wisdom_type" '{"valid_types":["philosophy","pattern","redirect","stack","decree","failure"]}'
116
+
117
+ # Generate SHA256 hash of content for deduplication
118
+ content_hash="sha256:$(echo -n "$content" | sha256sum | cut -d' ' -f1)"
119
+
120
+ # Observations file path
121
+ observations_file="$COLONY_DATA_DIR/learning-observations.json"
122
+
123
+ # Ensure data directory exists
124
+ [[ ! -d "$DATA_DIR" ]] && mkdir -p "$DATA_DIR"
125
+
126
+ # Initialize file if it doesn't exist
127
+ if [[ ! -f "$observations_file" ]]; then
128
+ atomic_write "$observations_file" '{"observations":[]}' || {
129
+ _aether_log_error "Could not initialize learning observations file"
130
+ json_err "$E_UNKNOWN" "Failed to create learning observations file"
131
+ }
132
+ fi
133
+
134
+ # Validate JSON structure — circuit breaker with backup recovery
135
+ if ! jq -e . "$observations_file" >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON validity
136
+ # Try to recover from backup (with retry-once per user decision)
137
+ lo_recovered=false
138
+ for lo_attempt in 1 2; do
139
+ for lo_bak in "${observations_file}.bak.1" "${observations_file}.bak.2" "${observations_file}.bak.3"; do
140
+ if [[ -f "$lo_bak" ]] && jq -e . "$lo_bak" >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON validity
141
+ if cp "$lo_bak" "$observations_file" 2>/dev/null; then # SUPPRESS:OK -- cleanup: backup copy is best-effort
142
+ lo_recovered=true
143
+ echo "Warning: Learning observations file was corrupted -- restored from backup. Some recent entries may be missing." >&2
144
+ break 2
145
+ fi
146
+ # cp failed -- will retry on next attempt (silent first retry)
147
+ fi
148
+ done
149
+ # If first attempt found a valid backup but cp failed, the second attempt retries
150
+ # If no valid backup exists, the second attempt won't help -- break early
151
+ [[ "$lo_attempt" -eq 1 ]] && [[ "$lo_recovered" != "true" ]] && break
152
+ done
153
+
154
+ if [[ "$lo_recovered" != "true" ]]; then
155
+ # Check if any backups exist at all
156
+ lo_has_any_backup=false
157
+ for lo_bak in "${observations_file}.bak.1" "${observations_file}.bak.2" "${observations_file}.bak.3"; do
158
+ [[ -f "$lo_bak" ]] && lo_has_any_backup=true && break
159
+ done
160
+
161
+ if [[ "$lo_has_any_backup" == "true" ]]; then
162
+ # Backups exist but ALL are corrupted -- stop and tell user (per locked decision)
163
+ json_err "$E_JSON_INVALID" "Learning observations and all 3 backups are corrupted. Manual recovery needed."
164
+ else
165
+ # No backups ever existed -- safe to reset from template (first-time corruption)
166
+ echo "Warning: Learning observations file was corrupted. Starting fresh -- this is a first-time recovery." >&2
167
+ atomic_write "$observations_file" '{"observations":[]}'
168
+ fi
169
+ fi
170
+ fi
171
+
172
+ # Acquire lock for concurrent access
173
+ if type acquire_lock &>/dev/null; then
174
+ acquire_lock "$observations_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on learning-observations.json"
175
+ trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
176
+ fi
177
+
178
+ # Get current timestamp
179
+ ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
180
+
181
+ # Check if observation with same hash already exists
182
+ existing_index=$(jq -r --arg hash "$content_hash" '.observations | to_entries[] | select(.value.content_hash == $hash) | .key' "$observations_file" | head -1)
183
+
184
+ if [[ -n "$existing_index" ]]; then
185
+ # Existing observation: increment count, update last_seen, add colony if new
186
+ # Rotate backups before write (uses .bak.N naming)
187
+ if [[ -f "$observations_file" ]]; then
188
+ cp -f "${observations_file}.bak.2" "${observations_file}.bak.3" 2>/dev/null || _aether_log_error "Could not rotate observations backup .bak.2 to .bak.3"
189
+ cp -f "${observations_file}.bak.1" "${observations_file}.bak.2" 2>/dev/null || _aether_log_error "Could not rotate observations backup .bak.1 to .bak.2"
190
+ cp -f "$observations_file" "${observations_file}.bak.1" 2>/dev/null || _aether_log_error "Could not create observations backup .bak.1"
191
+ fi
192
+ tmp_file="${observations_file}.tmp.$$"
193
+
194
+ jq --arg hash "$content_hash" \
195
+ --arg colony "$colony_name" \
196
+ --arg ts "$ts" \
197
+ '
198
+ .observations |= map(
199
+ if .content_hash == $hash then
200
+ .observation_count += 1 |
201
+ .last_seen = $ts |
202
+ .colonies = ((.colonies + [$colony]) | unique)
203
+ else
204
+ .
205
+ end
206
+ )' "$observations_file" > "$tmp_file" || {
207
+ _aether_log_error "Could not process observation update"
208
+ rm -f "$tmp_file"
209
+ json_err "$E_JSON_INVALID" "Failed to update observation data"
210
+ }
211
+
212
+ [[ -s "$tmp_file" ]] || {
213
+ _aether_log_error "Observation update produced empty result -- not overwriting"
214
+ rm -f "$tmp_file"
215
+ json_err "$E_JSON_INVALID" "Observation update produced empty result"
216
+ }
217
+
218
+ mv "$tmp_file" "$observations_file" || {
219
+ _aether_log_error "Could not finalize observation file update"
220
+ rm -f "$tmp_file"
221
+ json_err "$E_UNKNOWN" "Failed to rename temporary observations file"
222
+ }
223
+
224
+ # Get updated observation data
225
+ observation_count=$(jq -r --arg hash "$content_hash" '.observations[] | select(.content_hash == $hash) | .observation_count' "$observations_file")
226
+ colonies=$(jq -r --arg hash "$content_hash" '.observations[] | select(.content_hash == $hash) | .colonies' "$observations_file")
227
+ is_new=false
228
+ else
229
+ # New observation: create entry
230
+ # Rotate backups before write (uses .bak.N naming)
231
+ if [[ -f "$observations_file" ]]; then
232
+ cp -f "${observations_file}.bak.2" "${observations_file}.bak.3" 2>/dev/null || _aether_log_error "Could not rotate observations backup .bak.2 to .bak.3"
233
+ cp -f "${observations_file}.bak.1" "${observations_file}.bak.2" 2>/dev/null || _aether_log_error "Could not rotate observations backup .bak.1 to .bak.2"
234
+ cp -f "$observations_file" "${observations_file}.bak.1" 2>/dev/null || _aether_log_error "Could not create observations backup .bak.1"
235
+ fi
236
+ tmp_file="${observations_file}.tmp.$$"
237
+
238
+ jq --arg hash "$content_hash" \
239
+ --arg content "$content" \
240
+ --arg type "$wisdom_type" \
241
+ --arg colony "$colony_name" \
242
+ --arg ts "$ts" \
243
+ '.observations += [{
244
+ "content_hash": $hash,
245
+ "content": $content,
246
+ "wisdom_type": $type,
247
+ "observation_count": 1,
248
+ "first_seen": $ts,
249
+ "last_seen": $ts,
250
+ "colonies": [$colony]
251
+ }]' "$observations_file" > "$tmp_file" || {
252
+ _aether_log_error "Could not create new observation entry"
253
+ rm -f "$tmp_file"
254
+ json_err "$E_JSON_INVALID" "Failed to create observation data"
255
+ }
256
+
257
+ [[ -s "$tmp_file" ]] || {
258
+ _aether_log_error "New observation entry produced empty result -- not overwriting"
259
+ rm -f "$tmp_file"
260
+ json_err "$E_JSON_INVALID" "New observation produced empty result"
261
+ }
262
+
263
+ mv "$tmp_file" "$observations_file" || {
264
+ _aether_log_error "Could not finalize new observation file update"
265
+ rm -f "$tmp_file"
266
+ json_err "$E_UNKNOWN" "Failed to rename temporary observations file"
267
+ }
268
+
269
+ observation_count=1
270
+ colonies="[\"$colony_name\"]"
271
+ is_new=true
272
+ fi
273
+
274
+ # Release lock
275
+ if type release_lock &>/dev/null; then
276
+ release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
277
+ fi
278
+ trap - EXIT
279
+
280
+ # Propose-threshold determines when a learning is queueable/reviewable.
281
+ threshold=$(get_wisdom_threshold "$wisdom_type" "propose")
282
+
283
+ # Determine if threshold is met
284
+ threshold_met=false
285
+ [[ "$observation_count" -ge "$threshold" ]] && threshold_met=true
286
+
287
+ # Return result
288
+ result=$(jq -n \
289
+ --arg hash "$content_hash" \
290
+ --arg content "$content" \
291
+ --arg type "$wisdom_type" \
292
+ --argjson count "$observation_count" \
293
+ --argjson threshold "$threshold" \
294
+ --argjson threshold_met "$threshold_met" \
295
+ --argjson colonies "$colonies" \
296
+ --argjson is_new "$is_new" \
297
+ '{
298
+ content_hash: $hash,
299
+ content: $content,
300
+ wisdom_type: $type,
301
+ observation_count: $count,
302
+ threshold: $threshold,
303
+ threshold_met: $threshold_met,
304
+ colonies: $colonies,
305
+ is_new: $is_new
306
+ }')
307
+
308
+ json_ok "$result"
309
+ }
310
+
311
+ # ============================================================================
312
+ # _learning_check_promotion
313
+ # Check which learnings meet promotion thresholds
314
+ # Usage: learning-check-promotion [path_to_observations_file]
315
+ # Returns: JSON array of proposals meeting thresholds
316
+ # ============================================================================
317
+ _learning_check_promotion() {
318
+ observations_file="${1:-$COLONY_DATA_DIR/learning-observations.json}"
319
+
320
+ # Default to empty file path if not provided and data dir doesn't exist
321
+ if [[ -z "${1:-}" ]] && [[ ! -d "$DATA_DIR" ]]; then
322
+ observations_file=""
323
+ fi
324
+
325
+ # If file doesn't exist or is empty, return empty proposals
326
+ if [[ -z "$observations_file" ]] || [[ ! -f "$observations_file" ]]; then
327
+ json_ok '{"proposals":[]}'
328
+ exit 0
329
+ fi
330
+
331
+ # Validate JSON structure
332
+ if ! jq -e . "$observations_file" >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON validity
333
+ json_err "$E_JSON_INVALID" "learning-observations.json has invalid JSON"
334
+ fi
335
+
336
+ # Build proposals array using the shared threshold table.
337
+ thresholds_json=$(get_wisdom_thresholds_json)
338
+ result=$(jq --argjson thresholds "$thresholds_json" '
339
+ def get_threshold(type):
340
+ ($thresholds[type].propose // 1);
341
+
342
+ {
343
+ proposals: [
344
+ .observations[] |
345
+ select((.observation_count // 0) >= get_threshold(.wisdom_type)) |
346
+ {
347
+ content: .content,
348
+ wisdom_type: .wisdom_type,
349
+ observation_count: .observation_count,
350
+ threshold: get_threshold(.wisdom_type),
351
+ colonies: (.colonies // []),
352
+ ready: true
353
+ }
354
+ ]
355
+ }
356
+ ' "$observations_file" 2>/dev/null || echo '{"proposals":[]}') # SUPPRESS:OK -- read-default: file may not exist yet
357
+
358
+ json_ok "$result"
359
+ }
360
+
361
+ # ============================================================================
362
+ # _learning_promote_auto
363
+ # Auto-promote high-confidence learnings using recurrence policy
364
+ # Usage: learning-promote-auto <wisdom_type> <content> [colony_name] [event_type]
365
+ # ============================================================================
366
+ _learning_promote_auto() {
367
+ wisdom_type="${1:-}"
368
+ content="${2:-}"
369
+ colony_name="${3:-}"
370
+ event_type="${4:-learning}"
371
+
372
+ [[ -z "$wisdom_type" ]] && json_err "$E_VALIDATION_FAILED" "Usage: learning-promote-auto <wisdom_type> <content> [colony_name] [event_type]" '{"missing":"wisdom_type"}'
373
+ [[ -z "$content" ]] && json_err "$E_VALIDATION_FAILED" "Usage: learning-promote-auto <wisdom_type> <content> [colony_name] [event_type]" '{"missing":"content"}'
374
+
375
+ if [[ -z "$colony_name" ]]; then
376
+ colony_name=$(bash "$0" colony-name 2>/dev/null | jq -r '.result.name // ""') || colony_name="unknown"
377
+ [[ -z "$colony_name" ]] && colony_name="unknown"
378
+ fi
379
+
380
+ policy_threshold=$(get_wisdom_threshold "$wisdom_type" "auto")
381
+
382
+ observations_file="$COLONY_DATA_DIR/learning-observations.json"
383
+ content_hash="sha256:$(echo -n "$content" | sha256sum | cut -d' ' -f1)"
384
+ observation_count=0
385
+ colony_count=0
386
+
387
+ if [[ -f "$observations_file" ]]; then
388
+ # SUPPRESS:OK -- read-default: query may return empty
389
+ observation_count=$(jq -r --arg hash "$content_hash" '.observations[]? | select(.content_hash == $hash) | .observation_count // 0' "$observations_file" 2>/dev/null | head -1)
390
+ # SUPPRESS:OK -- read-default: query may return empty
391
+ colony_count=$(jq -r --arg hash "$content_hash" '.observations[]? | select(.content_hash == $hash) | (.colonies // [] | length)' "$observations_file" 2>/dev/null | head -1)
392
+ [[ -z "$observation_count" ]] && observation_count=0
393
+ [[ -z "$colony_count" ]] && colony_count=0
394
+ fi
395
+
396
+ # LRN-01: Recurrence-calibrated confidence
397
+ # Formula: min(0.7 + (observation_count - 1) * 0.05, 0.9)
398
+ lp_confidence=$(awk -v c="${observation_count:-1}" 'BEGIN {
399
+ v = 0.7 + (c - 1) * 0.05
400
+ if (v > 0.9) v = 0.9
401
+ if (v < 0.7) v = 0.7
402
+ printf "%.2f", v
403
+ }')
404
+
405
+ if [[ "$policy_threshold" -gt 0 && "$observation_count" -lt "$policy_threshold" ]]; then
406
+ json_ok "$(jq -n --argjson pt "$policy_threshold" --argjson oc "$observation_count" --argjson cc "$colony_count" --arg et "$event_type" '{promoted: false, reason: "threshold_not_met", policy_threshold: $pt, observation_count: $oc, colony_count: $cc, event_type: $et}')"
407
+ exit 0
408
+ fi
409
+
410
+ queen_file="$AETHER_ROOT/.aether/QUEEN.md"
411
+ if [[ ! -f "$queen_file" ]]; then
412
+ json_ok "{\"promoted\":false,\"reason\":\"queen_missing\",\"policy_threshold\":$policy_threshold,\"observation_count\":$observation_count,\"colony_count\":$colony_count}"
413
+ exit 0
414
+ fi
415
+
416
+ if grep -Fq -- "$content" "$queen_file" 2>/dev/null; then # SUPPRESS:OK -- existence-test: file may not exist
417
+ json_ok "{\"promoted\":false,\"reason\":\"already_promoted\",\"policy_threshold\":$policy_threshold,\"observation_count\":$observation_count,\"colony_count\":$colony_count}"
418
+ exit 0
419
+ fi
420
+
421
+ # SUPPRESS:OK -- read-default: returns fallback on failure
422
+ promote_result=$(bash "$0" queen-promote "$wisdom_type" "$content" "$colony_name" 2>/dev/null || echo '{}')
423
+ if echo "$promote_result" | jq -e '.ok == true' >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON field
424
+ # Also create an instinct from the promoted learning
425
+ bash "$0" instinct-create \
426
+ --trigger "working on $wisdom_type patterns" \
427
+ --action "$content" \
428
+ --confidence "$lp_confidence" \
429
+ --domain "$wisdom_type" \
430
+ --source "promoted_from_learning" \
431
+ # SUPPRESS:OK -- read-default: returns fallback on failure
432
+ --evidence "Auto-promoted after $observation_count observations (confidence: $lp_confidence)" 2>/dev/null \
433
+ || _aether_log_error "Could not create instinct from promoted learning"
434
+ json_ok "$(jq -nc --argjson pt "$policy_threshold" --argjson oc "$observation_count" --argjson cc "$colony_count" --arg et "$event_type" '{promoted: true, mode: "auto", policy_threshold: $pt, observation_count: $oc, colony_count: $cc, event_type: $et}')"
435
+ else
436
+ # SUPPRESS:OK -- read-default: query may return empty
437
+ promote_msg=$(echo "$promote_result" | jq -r '.error.message // "promotion_failed"' 2>/dev/null || echo "promotion_failed")
438
+ result=$(jq -nc \
439
+ --arg reason "promotion_failed" \
440
+ --arg message "$promote_msg" \
441
+ --argjson policy_threshold "$policy_threshold" \
442
+ --argjson observation_count "$observation_count" \
443
+ --argjson colony_count "$colony_count" \
444
+ '{promoted:false, reason:$reason, message:$message, policy_threshold:$policy_threshold, observation_count:$observation_count, colony_count:$colony_count}')
445
+ json_ok "$result"
446
+ fi
447
+ }
448
+
449
+ # ============================================================================
450
+ # _learning_display_proposals
451
+ # Display promotion proposals with checkbox-style UI
452
+ # Usage: learning-display-proposals [observations_file] [--verbose] [--no-color]
453
+ # Returns: Formatted display output (not JSON - for human consumption)
454
+ # ============================================================================
455
+ _learning_display_proposals() {
456
+ verbose=false
457
+ no_color=false
458
+ observations_file=""
459
+
460
+ # Parse arguments
461
+ for arg in "$@"; do
462
+ case "$arg" in
463
+ --verbose) verbose=true ;;
464
+ --no-color) no_color=true ;;
465
+ *)
466
+ # If argument doesn't start with --, treat as file path
467
+ if [[ "$arg" != --* ]] && [[ -z "$observations_file" ]]; then
468
+ observations_file="$arg"
469
+ fi
470
+ ;;
471
+ esac
472
+ done
473
+
474
+ # Detect color support
475
+ use_color=false
476
+ if [[ "$no_color" == "false" ]] && [[ -t 1 ]]; then
477
+ use_color=true
478
+ fi
479
+
480
+ # Color codes
481
+ reset=""
482
+ yellow=""
483
+ red=""
484
+ cyan=""
485
+ if [[ "$use_color" == "true" ]]; then
486
+ reset="\033[0m"
487
+ yellow="\033[33m"
488
+ red="\033[31m"
489
+ cyan="\033[36m"
490
+ fi
491
+
492
+ # Determine observations file path
493
+ if [[ -z "$observations_file" ]]; then
494
+ observations_file="$COLONY_DATA_DIR/learning-observations.json"
495
+ fi
496
+
497
+ # Check if file exists and has content
498
+ if [[ ! -f "$observations_file" ]] || [[ ! -s "$observations_file" ]]; then
499
+ echo "No observations found."
500
+ echo ""
501
+ echo "Observations accumulate as colonies report learnings."
502
+ echo "Run this command again after more activity."
503
+ exit 0
504
+ fi
505
+
506
+ # Get all observations with their thresholds
507
+ thresholds_json=$(get_wisdom_thresholds_json)
508
+ proposals_json=$(jq --argjson thresholds "$thresholds_json" '
509
+ def get_threshold(type):
510
+ ($thresholds[type].propose // 1);
511
+
512
+ {
513
+ proposals: [
514
+ .observations[] |
515
+ {
516
+ content: .content,
517
+ wisdom_type: .wisdom_type,
518
+ observation_count: .observation_count,
519
+ threshold: get_threshold(.wisdom_type),
520
+ colonies: .colonies
521
+ }
522
+ ]
523
+ }
524
+ ' "$observations_file" 2>/dev/null || echo '{"proposals":[]}') # SUPPRESS:OK -- read-default: file may not exist yet
525
+
526
+ # Check if there are any proposals
527
+ proposal_count=$(echo "$proposals_json" | jq '.proposals | length')
528
+ if [[ "$proposal_count" -eq 0 ]]; then
529
+ echo "No proposals ready for promotion."
530
+ echo ""
531
+ echo "Observations accumulate as colonies report learnings."
532
+ echo "Run this command again after more activity."
533
+ exit 0
534
+ fi
535
+
536
+ # Define wisdom types and their display properties
537
+ types=("philosophy" "pattern" "redirect" "stack" "decree" "failure")
538
+ type_emojis=("📜" "🧭" "⚠️" "🔧" "🏛️" "❌")
539
+ type_names=("Philosophies" "Patterns" "Redirects" "Stack Wisdom" "Decrees" "Failures")
540
+ type_thresholds=(
541
+ "$(get_wisdom_threshold philosophy propose)"
542
+ "$(get_wisdom_threshold pattern propose)"
543
+ "$(get_wisdom_threshold redirect propose)"
544
+ "$(get_wisdom_threshold stack propose)"
545
+ "$(get_wisdom_threshold decree propose)"
546
+ "$(get_wisdom_threshold failure propose)"
547
+ )
548
+
549
+ echo ""
550
+ echo "🧠 Promotion Proposals"
551
+ echo "====================="
552
+ echo ""
553
+ echo "Select proposals to promote to QUEEN.md wisdom:"
554
+ echo "(Enter numbers like '1 3 5', or press Enter to defer all)"
555
+ echo ""
556
+
557
+ # Build flat list of all proposals with global numbering
558
+ global_idx=1
559
+ declare -a all_proposals
560
+
561
+ for i in "${!types[@]}"; do
562
+ type="${types[$i]}"
563
+ threshold="${type_thresholds[$i]}"
564
+
565
+ # Get proposals of this type
566
+ type_proposals=$(echo "$proposals_json" | jq --arg t "$type" '.proposals | map(select(.wisdom_type == $t))')
567
+ type_count=$(echo "$type_proposals" | jq 'length')
568
+
569
+ [[ "$type_count" -eq 0 ]] && continue
570
+
571
+ # Print group header
572
+ echo "${type_emojis[$i]} ${type_names[$i]} (threshold: $threshold)"
573
+
574
+ # Process each proposal using index to avoid subshell issues
575
+ for ((j=0; j<type_count; j++)); do
576
+ proposal=$(echo "$type_proposals" | jq -c ".[$j]")
577
+ content=$(sanitize_read_value "$(echo "$proposal" | jq -r '.content')")
578
+ count=$(echo "$proposal" | jq -r '.observation_count')
579
+ prop_threshold=$(echo "$proposal" | jq -r '.threshold')
580
+
581
+ # Truncate content if not verbose
582
+ display_content="$content"
583
+ if [[ "$verbose" != "true" && ${#content} -gt 40 ]]; then
584
+ display_content="${content:0:37}..."
585
+ fi
586
+
587
+ # Get threshold bar
588
+ # SUPPRESS:OK -- read-default: returns fallback on failure
589
+ bar_result=$(bash "$0" generate-threshold-bar "$count" "$prop_threshold" 2>/dev/null | jq -r '.result.bar')
590
+
591
+ # Build warning for below-threshold
592
+ warning=""
593
+ if [[ "$count" -lt "$prop_threshold" ]]; then
594
+ if [[ "$use_color" == "true" ]]; then
595
+ warning=" ${yellow}⚠️ below threshold${reset}"
596
+ else
597
+ warning=" ⚠️ below threshold"
598
+ fi
599
+ fi
600
+
601
+ # Print formatted line
602
+ printf " [ ] %d. \"%s\" %s (%d/%d)%s\n" "$global_idx" "$display_content" "$bar_result" "$count" "$prop_threshold" "$warning"
603
+
604
+ # Store for later reference
605
+ all_proposals+=("$proposal")
606
+
607
+ global_idx=$((global_idx + 1))
608
+ done
609
+
610
+ echo ""
611
+ done
612
+
613
+ echo "───────────────────────────────────────────────────"
614
+ echo ""
615
+ }
616
+
617
+ # ============================================================================
618
+ # _learning_select_proposals
619
+ # Interactive selection of proposals for promotion [DEPRECATED]
620
+ # Usage: learning-select-proposals [--verbose] [--dry-run] [--yes]
621
+ # Returns: JSON with selected/deferred arrays and action taken
622
+ # ============================================================================
623
+ _learning_select_proposals() {
624
+ _deprecation_warning "learning-select-proposals"
625
+ # Interactive selection of proposals for promotion
626
+ # Usage: learning-select-proposals [--verbose] [--dry-run] [--yes]
627
+ # Returns: JSON with selected/deferred arrays and action taken
628
+ #
629
+ # Flow: display proposals -> capture input -> parse selection -> output JSON
630
+
631
+ verbose=false
632
+ dry_run=false
633
+ skip_confirm=false
634
+
635
+ # Parse arguments
636
+ for arg in "$@"; do
637
+ case "$arg" in
638
+ --verbose) verbose=true ;;
639
+ --dry-run) dry_run=true ;;
640
+ --yes) skip_confirm=true ;;
641
+ esac
642
+ done
643
+
644
+ # Get all observations (not just threshold-meeting ones) for display consistency
645
+ # This matches learning-display-proposals behavior
646
+ observations_file="$COLONY_DATA_DIR/learning-observations.json"
647
+ if [[ ! -f "$observations_file" ]]; then
648
+ json_ok '{"selected":[],"deferred":[],"count":0,"action":"none","reason":"no_observations_file"}'
649
+ exit 0
650
+ fi
651
+
652
+ # Build proposals array using same logic as learning-display-proposals
653
+ thresholds_json=$(get_wisdom_thresholds_json)
654
+ proposals_json=$(jq --argjson thresholds "$thresholds_json" '
655
+ def get_threshold(type):
656
+ ($thresholds[type].propose // 1);
657
+
658
+ {
659
+ proposals: [
660
+ .observations[] |
661
+ {
662
+ content: .content,
663
+ wisdom_type: .wisdom_type,
664
+ observation_count: .observation_count,
665
+ threshold: get_threshold(.wisdom_type),
666
+ colonies: .colonies
667
+ }
668
+ ]
669
+ }
670
+ ' "$observations_file" 2>/dev/null || echo '{"proposals":[]}') # SUPPRESS:OK -- read-default: file may not exist yet
671
+
672
+ # Check if we have any proposals
673
+ proposal_count=$(echo "$proposals_json" | jq '.proposals | length')
674
+ if [[ "$proposal_count" -eq 0 ]]; then
675
+ json_ok '{"selected":[],"deferred":[],"count":0,"action":"none","reason":"no_proposals"}'
676
+ exit 0
677
+ fi
678
+
679
+ # Display proposals
680
+ if [[ "$dry_run" == "false" ]]; then
681
+ if [[ "$verbose" == "true" ]]; then
682
+ bash "$0" learning-display-proposals --verbose
683
+ else
684
+ bash "$0" learning-display-proposals
685
+ fi
686
+ fi
687
+
688
+ # Capture user input (unless dry-run)
689
+ selection=""
690
+ if [[ "$dry_run" == "true" ]]; then
691
+ # In dry-run mode, select all proposals
692
+ selection=$(seq 1 $proposal_count | tr '\n' ' ')
693
+ echo "Dry run: would select all $proposal_count proposals"
694
+ else
695
+ echo -n "Enter numbers to select (e.g., '1 3 5'), or press Enter to defer all: "
696
+ read -r selection
697
+ fi
698
+
699
+ # Parse the selection
700
+ parse_result=$(bash "$0" parse-selection "$selection" "$proposal_count")
701
+
702
+ # Check for parse errors
703
+ if ! echo "$parse_result" | jq -e '.ok' >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON field
704
+ # Return the error
705
+ echo "$parse_result"
706
+ exit 1
707
+ fi
708
+
709
+ # Extract selected and deferred arrays
710
+ selected_indices=$(echo "$parse_result" | jq -r '.result.selected // []')
711
+ deferred_indices=$(echo "$parse_result" | jq -r '.result.deferred // []')
712
+ action=$(echo "$parse_result" | jq -r '.result.action // "select"')
713
+ selected_count=$(echo "$selected_indices" | jq 'length')
714
+ deferred_count=$(echo "$deferred_indices" | jq 'length')
715
+
716
+ # Show summary
717
+ if [[ "$dry_run" == "false" ]]; then
718
+ echo ""
719
+ echo "$selected_count proposal(s) selected, $deferred_count deferred"
720
+ fi
721
+
722
+ # Preview and confirmation (if selections made and not skipping)
723
+ confirmed=true
724
+ if [[ "$selected_count" -gt 0 && "$dry_run" == "false" && "$skip_confirm" == "false" ]]; then
725
+ echo ""
726
+ echo "───────────────────────────────────────────────────"
727
+ echo "📋 Selected for Promotion:"
728
+ echo ""
729
+
730
+ # Track below-threshold count for warning
731
+ below_threshold_count=0
732
+
733
+ # Display each selected proposal with full details
734
+ echo "$selected_indices" | jq -r '.[]' | while read -r idx; do
735
+ proposal=$(echo "$proposals_json" | jq -r ".proposals[$idx]")
736
+ content=$(sanitize_read_value "$(echo "$proposal" | jq -r '.content')")
737
+ ptype=$(echo "$proposal" | jq -r '.wisdom_type')
738
+ count=$(echo "$proposal" | jq -r '.observation_count')
739
+ threshold=$(echo "$proposal" | jq -r '.threshold')
740
+
741
+ # Capitalize type for display
742
+ type_display=$(echo "$ptype" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
743
+
744
+ # Check if below threshold
745
+ status=""
746
+ if [[ "$count" -lt "$threshold" ]]; then
747
+ status=" [⚠️ Early promotion - below threshold]"
748
+ below_threshold_count=$((below_threshold_count + 1))
749
+ fi
750
+
751
+ echo " • $type_display: \"$content\"$status"
752
+ done
753
+
754
+ # Show warning if any below threshold
755
+ if [[ "$below_threshold_count" -gt 0 ]]; then
756
+ echo ""
757
+ echo "⚠️ $below_threshold_count item(s) below threshold will be early promoted"
758
+ fi
759
+
760
+ # Confirmation prompt
761
+ echo ""
762
+ echo -n "Proceed with promotion? (y/n): "
763
+ read -r confirm_response
764
+
765
+ if [[ ! "$confirm_response" =~ ^[Yy]$ ]]; then
766
+ confirmed=false
767
+ echo "Selection cancelled. Treating as defer-all."
768
+ # Move all to deferred
769
+ action="defer_all"
770
+ deferred_indices=$(jq -n --argjson s "$selected_indices" --argjson d "$deferred_indices" '($s + $d)')
771
+ selected_indices="[]"
772
+ selected_count=0
773
+ deferred_count=$(echo "$deferred_indices" | jq 'length')
774
+ fi
775
+ fi
776
+
777
+ # Build result JSON
778
+ result=$(jq -n \
779
+ --argjson selected "$selected_indices" \
780
+ --argjson deferred "$deferred_indices" \
781
+ --argjson proposals "$proposals_json" \
782
+ --arg action "$action" \
783
+ --argjson count "$proposal_count" \
784
+ --arg confirmed "$confirmed" \
785
+ '{
786
+ selected: $selected,
787
+ deferred: $deferred,
788
+ count: $count,
789
+ action: $action,
790
+ confirmed: ($confirmed == "true"),
791
+ proposals: $proposals.proposals
792
+ }')
793
+
794
+ json_ok "$result"
795
+ }
796
+
797
+ # ============================================================================
798
+ # _learning_defer_proposals
799
+ # Store unselected proposals in learning-deferred.json for later review
800
+ # Usage: echo '[{proposal1}, {proposal2}]' | bash aether-utils.sh learning-defer-proposals
801
+ # Returns: JSON with count of newly deferred items
802
+ # ============================================================================
803
+ _learning_defer_proposals() {
804
+ # Read proposals from stdin
805
+ proposals_json=$(cat)
806
+
807
+ # Validate input
808
+ if [[ -z "$proposals_json" ]] || [[ "$proposals_json" == "[]" ]]; then
809
+ json_ok '{"deferred":0,"new":0,"expired":0}'
810
+ exit 0
811
+ fi
812
+
813
+ deferred_file="$COLONY_DATA_DIR/learning-deferred.json"
814
+
815
+ # Ensure data directory exists
816
+ [[ ! -d "$DATA_DIR" ]] && mkdir -p "$DATA_DIR"
817
+
818
+ # Acquire lock
819
+ acquire_lock "$deferred_file" 5 || {
820
+ json_err "$E_LOCK_TIMEOUT" "Could not acquire lock on deferred file"
821
+ exit 1
822
+ }
823
+
824
+ # Read existing deferred file or create empty structure
825
+ if [[ -f "$deferred_file" ]]; then
826
+ existing_deferred=$(jq '.deferred // []' "$deferred_file" 2>/dev/null || echo '[]') # SUPPRESS:OK -- read-default: returns fallback if missing
827
+ else
828
+ existing_deferred='[]'
829
+ fi
830
+
831
+ # Current timestamp for new entries
832
+ ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
833
+ current_epoch=$(date +%s)
834
+
835
+ # Add deferred_at timestamp to each new proposal
836
+ new_proposals=$(echo "$proposals_json" | jq --arg ts "$ts" '
837
+ map(. + {deferred_at: $ts})
838
+ ')
839
+
840
+ # Calculate TTL cutoff (30 days ago)
841
+ ttl_cutoff=$((current_epoch - 30 * 24 * 60 * 60))
842
+
843
+ # Filter existing deferred: remove expired entries
844
+ filtered_existing=$(echo "$existing_deferred" | jq --argjson cutoff "$ttl_cutoff" '
845
+ map(select(
846
+ (.deferred_at | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) > $cutoff
847
+ ))
848
+ ' 2>/dev/null || echo '[]') # SUPPRESS:OK -- read-default: returns fallback on failure
849
+
850
+ # Count expired items
851
+ expired_count=$(echo "$existing_deferred" | jq --argjson cutoff "$ttl_cutoff" '
852
+ map(select(
853
+ (.deferred_at | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) <= $cutoff
854
+ )) | length
855
+ ' 2>/dev/null || echo '0') # SUPPRESS:OK -- read-default: returns fallback on failure
856
+
857
+ # Merge new proposals with existing, avoiding duplicates by content_hash
858
+ merged=$(jq -s --argjson new "$new_proposals" '
859
+ def unique_by_hash:
860
+ group_by(.content_hash) | map(first);
861
+
862
+ (.[0] // []) + $new | unique_by_hash
863
+ ' <<< "$filtered_existing")
864
+
865
+ # Count new items (those that weren't in existing)
866
+ existing_hashes=$(echo "$filtered_existing" | jq -r 'map(.content_hash) | join(" ")')
867
+ new_count=0
868
+ if [[ -n "$existing_hashes" ]]; then
869
+ new_count=$(echo "$new_proposals" | jq --arg existing "$existing_hashes" '
870
+ [$existing | split(" ")[]] as $hashes |
871
+ map(select(.content_hash as $h | $hashes | index($h) | not)) |
872
+ length
873
+ ')
874
+ else
875
+ new_count=$(echo "$new_proposals" | jq 'length')
876
+ fi
877
+
878
+ # Write atomically
879
+ tmp_file="${deferred_file}.tmp.$$"
880
+ jq -n --argjson deferred "$merged" '{deferred: $deferred}' > "$tmp_file"
881
+ mv "$tmp_file" "$deferred_file"
882
+
883
+ # Release lock
884
+ release_lock
885
+
886
+ # Log activity
887
+ total_count=$(echo "$merged" | jq 'length')
888
+ bash "$0" activity-log "DEFERRED" "Queen" "Stored $new_count new deferred proposal(s), $expired_count expired removed, $total_count total"
889
+
890
+ json_ok "{\"deferred\":$total_count,\"new\":$new_count,\"expired\":$expired_count}"
891
+ }
892
+
893
+ # ============================================================================
894
+ # _learning_approve_proposals
895
+ # Orchestrate full approval workflow: one-at-a-time display with Approve/Reject/Skip
896
+ # Usage: learning-approve-proposals [--verbose] [--dry-run] [--yes] [--deferred]
897
+ # Returns: JSON summary {promoted, deferred, failed, undo_offered}
898
+ # ============================================================================
899
+ _learning_approve_proposals() {
900
+ verbose=false
901
+ dry_run=false
902
+ skip_confirm=false
903
+ deferred_mode=false
904
+ undo_mode=false
905
+
906
+ # Parse arguments
907
+ for arg in "$@"; do
908
+ case "$arg" in
909
+ --verbose) verbose=true ;;
910
+ --dry-run) dry_run=true ;;
911
+ --yes) skip_confirm=true ;;
912
+ --deferred) deferred_mode=true ;;
913
+ --undo) undo_mode=true ;;
914
+ esac
915
+ done
916
+
917
+ # Handle --undo mode
918
+ if [[ "$undo_mode" == "true" ]]; then
919
+ undo_result=$(bash "$0" learning-undo-promotions 2>&1)
920
+ echo "$undo_result"
921
+ exit 0
922
+ fi
923
+
924
+ # Get colony name via proper subcommand
925
+ colony_name=$(bash "$0" colony-name 2>/dev/null | jq -r '.result.name // ""') || colony_name="unknown"
926
+ [[ -z "$colony_name" ]] && colony_name="unknown"
927
+
928
+ # Load proposals based on mode
929
+ if [[ "$deferred_mode" == "true" ]]; then
930
+ # Load from deferred file
931
+ if [[ ! -f "$COLONY_DATA_DIR/learning-deferred.json" ]]; then
932
+ echo "No deferred proposals to review."
933
+ json_ok '{"promoted":0,"deferred":0,"failed":null,"undo_offered":false}'
934
+ exit 0
935
+ fi
936
+ # SUPPRESS:OK -- read-default: returns fallback on failure
937
+ proposals_json=$(jq '{proposals: .deferred}' "$COLONY_DATA_DIR/learning-deferred.json" 2>/dev/null || echo '{"proposals":[]}')
938
+ echo "📦 Reviewing deferred proposals..."
939
+ echo ""
940
+ else
941
+ # Get proposals directly from learning-check-promotion
942
+ proposals_result=$(bash "$0" learning-check-promotion 2>/dev/null || echo '{"proposals":[]}') # SUPPRESS:OK -- read-default: subcommand may fail
943
+ proposals_json=$(echo "$proposals_result" | jq '{proposals: .result.proposals // []}')
944
+
945
+ # Check if there were any proposals
946
+ proposal_count=$(echo "$proposals_json" | jq '.proposals | length')
947
+ if [[ "$proposal_count" -eq 0 ]]; then
948
+ json_ok '{"promoted":0,"deferred":0,"failed":null,"undo_offered":false}'
949
+ exit 0
950
+ fi
951
+ fi
952
+
953
+ # Get proposal count
954
+ proposal_count=$(echo "$proposals_json" | jq '.proposals | length')
955
+ if [[ "$proposal_count" -eq 0 ]]; then
956
+ echo "No proposals available."
957
+ json_ok '{"promoted":0,"deferred":0,"failed":null,"undo_offered":false}'
958
+ exit 0
959
+ fi
960
+
961
+ # Define wisdom type emojis and names for display
962
+ declare -A type_emojis
963
+ declare -A type_names
964
+ type_emojis=(
965
+ ["philosophy"]="📜"
966
+ ["pattern"]="🧭"
967
+ ["redirect"]="⚠️"
968
+ ["stack"]="🔧"
969
+ ["decree"]="🏛️"
970
+ ["failure"]="❌"
971
+ )
972
+ type_names=(
973
+ ["philosophy"]="Philosophy"
974
+ ["pattern"]="Pattern"
975
+ ["redirect"]="Redirect"
976
+ ["stack"]="Stack Wisdom"
977
+ ["decree"]="Decree"
978
+ ["failure"]="Failure"
979
+ )
980
+
981
+ # Arrays to track results
982
+ approved_proposals=()
983
+ rejected_proposals=()
984
+ skipped_proposals=()
985
+
986
+ # Process proposals one at a time
987
+ echo ""
988
+ echo "🧠 Wisdom Promotion Review"
989
+ echo "══════════════════════════"
990
+ echo ""
991
+ echo "$proposal_count proposal(s) ready for review"
992
+ echo ""
993
+
994
+ for ((i=0; i<proposal_count; i++)); do
995
+ proposal=$(echo "$proposals_json" | jq ".proposals[$i]")
996
+ ptype=$(echo "$proposal" | jq -r '.wisdom_type')
997
+ content=$(sanitize_read_value "$(echo "$proposal" | jq -r '.content')")
998
+ count=$(echo "$proposal" | jq -r '.observation_count // 1')
999
+ threshold=$(echo "$proposal" | jq -r '.threshold // 1')
1000
+
1001
+ emoji="${type_emojis[$ptype]:-📝}"
1002
+ name="${type_names[$ptype]:-$ptype}"
1003
+
1004
+ # Display proposal
1005
+ echo "───────────────────────────────────────────────────"
1006
+ echo "Proposal $((i+1)) of $proposal_count"
1007
+ echo "───────────────────────────────────────────────────"
1008
+ echo ""
1009
+ echo "$emoji $name (observed $count time(s), threshold: $threshold)"
1010
+ echo ""
1011
+ echo "$content"
1012
+ echo ""
1013
+ echo "───────────────────────────────────────────────────"
1014
+
1015
+ # Handle dry-run mode
1016
+ if [[ "$dry_run" == "true" ]]; then
1017
+ echo "Dry run: would approve"
1018
+ approved_proposals+=("$proposal")
1019
+ echo ""
1020
+ continue
1021
+ fi
1022
+
1023
+ # Handle --yes mode (auto-approve all)
1024
+ if [[ "$skip_confirm" == "true" ]]; then
1025
+ approved_proposals+=("$proposal")
1026
+ echo "✓ Auto-approved (--yes mode)"
1027
+ echo ""
1028
+ continue
1029
+ fi
1030
+
1031
+ # Prompt for action
1032
+ echo -n "[A]pprove [R]eject [S]kip Your choice: "
1033
+ read -r choice
1034
+
1035
+ case "$choice" in
1036
+ [Aa]|"approve"|"Approve")
1037
+ approved_proposals+=("$proposal")
1038
+ echo "✓ Approved"
1039
+ ;;
1040
+ [Rr]|"reject"|"Reject")
1041
+ rejected_proposals+=("$proposal")
1042
+ echo "✗ Rejected"
1043
+ ;;
1044
+ [Ss]|""|"skip"|"Skip")
1045
+ skipped_proposals+=("$proposal")
1046
+ echo "→ Skipped"
1047
+ ;;
1048
+ *)
1049
+ # Invalid input - default to skip
1050
+ skipped_proposals+=("$proposal")
1051
+ echo "→ Skipped (invalid input)"
1052
+ ;;
1053
+ esac
1054
+ echo ""
1055
+ done
1056
+
1057
+ # Execute promotions for approved proposals
1058
+ promoted_count=0
1059
+ failed_item=""
1060
+ promoted_items=()
1061
+
1062
+ if [[ ${#approved_proposals[@]} -gt 0 ]]; then
1063
+ echo ""
1064
+ echo "Promoting ${#approved_proposals[@]} observation(s)..."
1065
+ echo ""
1066
+
1067
+ for proposal in "${approved_proposals[@]}"; do
1068
+ ptype=$(echo "$proposal" | jq -r '.wisdom_type')
1069
+ content=$(sanitize_read_value "$(echo "$proposal" | jq -r '.content')")
1070
+
1071
+ if [[ "$dry_run" == "true" ]]; then
1072
+ echo "Dry run: would promote $ptype: \"$content\""
1073
+ ((promoted_count++))
1074
+ promoted_items+=("$proposal")
1075
+ continue
1076
+ fi
1077
+
1078
+ # Call queen-promote
1079
+ promote_result=$(bash "$0" queen-promote "$ptype" "$content" "$colony_name" 2>&1) || {
1080
+ echo "✗ Failed to promote: $content"
1081
+ echo " Error: $promote_result"
1082
+ failed_item="$content"
1083
+ # Prompt for retry on failure
1084
+ echo ""
1085
+ echo -n "Write to QUEEN.md failed. Retry? (y/n): "
1086
+ read -r retry_response
1087
+ if [[ "$retry_response" =~ ^[Yy]$ ]]; then
1088
+ # Retry once
1089
+ promote_result=$(bash "$0" queen-promote "$ptype" "$content" "$colony_name" 2>&1) || {
1090
+ echo "✗ Retry failed. Keeping proposal pending."
1091
+ skipped_proposals+=("$proposal")
1092
+ continue
1093
+ }
1094
+ else
1095
+ echo "Skipping this proposal. It will remain pending."
1096
+ skipped_proposals+=("$proposal")
1097
+ continue
1098
+ fi
1099
+ }
1100
+
1101
+ echo "✓ Promoted ${ptype^}: \"$content\""
1102
+ ((promoted_count++))
1103
+ promoted_items+=("$proposal")
1104
+ done
1105
+ fi
1106
+
1107
+ # Handle deferred proposals (skipped ones go to deferred)
1108
+ deferred_count=${#skipped_proposals[@]}
1109
+ if [[ "$dry_run" == "false" ]] && [[ $deferred_count -gt 0 ]]; then
1110
+ # Convert skipped proposals to JSON array and defer
1111
+ skipped_json=$(printf '%s\n' "${skipped_proposals[@]}" | jq -s '.')
1112
+ echo "$skipped_json" | bash "$0" learning-defer-proposals >/dev/null 2>&1 || _aether_log_error "Could not defer learning proposals"
1113
+ fi
1114
+
1115
+ # Log activity
1116
+ if [[ "$dry_run" == "false" ]]; then
1117
+ bash "$0" activity-log "PROMOTED" "Queen" "Promoted $promoted_count observation(s), deferred $deferred_count, rejected ${#rejected_proposals[@]}"
1118
+ fi
1119
+
1120
+ # Display summary
1121
+ echo ""
1122
+ echo "═══════════════════════════════════════════════════"
1123
+ echo "Summary: $promoted_count approved, ${#rejected_proposals[@]} rejected, $deferred_count skipped"
1124
+ echo "═══════════════════════════════════════════════════"
1125
+ echo ""
1126
+
1127
+ # Offer undo if promotions succeeded
1128
+ undo_offered=false
1129
+ if [[ "$promoted_count" -gt 0 ]] && [[ "$dry_run" == "false" ]] && [[ -z "$failed_item" ]]; then
1130
+ undo_offered=true
1131
+
1132
+ # Store undo info
1133
+ undo_file="$COLONY_DATA_DIR/.promotion-undo.json"
1134
+ promoted_json=$(printf '%s\n' "${promoted_items[@]}" | jq -s '.')
1135
+ jq -n --argjson items "$promoted_json" --arg ts "$(date +%s)" '{promoted: $items, timestamp: ($ts | tonumber)}' > "$undo_file"
1136
+
1137
+ echo -n "Undo these promotions? (y/n): "
1138
+ read -r undo_response
1139
+
1140
+ if [[ "$undo_response" =~ ^[Yy]$ ]]; then
1141
+ echo "Reverting promotions..."
1142
+ undo_result=$(bash "$0" learning-undo-promotions 2>&1)
1143
+ if echo "$undo_result" | jq -e '.ok' >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON field
1144
+ undone_count=$(echo "$undo_result" | jq -r '.result.undone // 0')
1145
+ echo "$undone_count promotion(s) reverted."
1146
+ promoted_count=0
1147
+ else
1148
+ echo "Undo failed: $(echo "$undo_result" | jq -r '.error.message // "Unknown error"')"
1149
+ fi
1150
+ else
1151
+ echo "Promotions kept."
1152
+ fi
1153
+ fi
1154
+
1155
+ # Build result
1156
+ result=$(jq -n \
1157
+ --argjson promoted "$promoted_count" \
1158
+ --argjson deferred "$deferred_count" \
1159
+ --argjson rejected "${#rejected_proposals[@]}" \
1160
+ --arg failed "${failed_item:-null}" \
1161
+ --argjson undo "$undo_offered" \
1162
+ '{promoted: $promoted, deferred: $deferred, rejected: $rejected, failed: $failed, undo_offered: $undo}')
1163
+
1164
+ json_ok "$result"
1165
+ }
1166
+
1167
+ # ============================================================================
1168
+ # _learning_undo_promotions
1169
+ # Revert promotions from QUEEN.md using undo file
1170
+ # Usage: learning-undo-promotions
1171
+ # Returns: JSON with count of undone items
1172
+ # ============================================================================
1173
+ _learning_undo_promotions() {
1174
+ undo_file="$COLONY_DATA_DIR/.promotion-undo.json"
1175
+
1176
+ # Check if undo file exists
1177
+ if [[ ! -f "$undo_file" ]]; then
1178
+ json_err "$E_FILE_NOT_FOUND" "No undo file found. Cannot undo promotions."
1179
+ exit 1
1180
+ fi
1181
+
1182
+ # Read undo data
1183
+ undo_data=$(cat "$undo_file")
1184
+ undo_timestamp=$(echo "$undo_data" | jq -r '.timestamp // 0')
1185
+ current_time=$(date +%s)
1186
+
1187
+ # Check 24h TTL
1188
+ ttl_seconds=$((24 * 60 * 60))
1189
+ time_diff=$((current_time - undo_timestamp))
1190
+
1191
+ if [[ $time_diff -gt $ttl_seconds ]]; then
1192
+ # Remove expired undo file
1193
+ rm -f "$undo_file"
1194
+ json_err "$E_VALIDATION_FAILED" "Undo window expired (24h limit)"
1195
+ exit 1
1196
+ fi
1197
+
1198
+ queen_file="$AETHER_ROOT/.aether/QUEEN.md"
1199
+
1200
+ # Check if QUEEN.md exists
1201
+ if [[ ! -f "$queen_file" ]]; then
1202
+ json_err "$E_FILE_NOT_FOUND" "QUEEN.md not found"
1203
+ exit 1
1204
+ fi
1205
+
1206
+ # Process each promoted item
1207
+ undone_count=0
1208
+ failed_items=()
1209
+
1210
+ # Read promoted items from undo file
1211
+ promoted_items=$(echo "$undo_data" | jq -c '.promoted[]?')
1212
+
1213
+ if [[ -z "$promoted_items" ]]; then
1214
+ rm -f "$undo_file"
1215
+ json_err "$E_VALIDATION_FAILED" "No promoted items in undo file"
1216
+ exit 1
1217
+ fi
1218
+
1219
+ # Create temp file for atomic write
1220
+ tmp_file="${queen_file}.tmp.$$"
1221
+
1222
+ # Copy current QUEEN.md to temp
1223
+ cp "$queen_file" "$tmp_file"
1224
+
1225
+ # Process each item
1226
+ while IFS= read -r item; do
1227
+ [[ -z "$item" ]] && continue
1228
+
1229
+ ptype=$(echo "$item" | jq -r '.wisdom_type')
1230
+ content=$(sanitize_read_value "$(echo "$item" | jq -r '.content')")
1231
+
1232
+ # Map type to section header
1233
+ case "$ptype" in
1234
+ philosophy) section_header="## 📜 Philosophies" ;;
1235
+ pattern) section_header="## 🧭 Patterns" ;;
1236
+ redirect) section_header="## ⚠️ Redirects" ;;
1237
+ stack) section_header="## 🔧 Stack Wisdom" ;;
1238
+ decree) section_header="## 🏛️ Decrees" ;;
1239
+ *) continue ;;
1240
+ esac
1241
+
1242
+ # Escape content for sed (basic escaping)
1243
+ escaped_content=$(echo "$content" | sed 's/[\\/&]/\\&/g')
1244
+
1245
+ # Find and remove the entry from the section
1246
+ # Pattern: - **colony_name** (timestamp): content
1247
+ # We match based on content since that's the unique part
1248
+ if grep -q "${escaped_content}" "$tmp_file" 2>/dev/null; then # SUPPRESS:OK -- existence-test: file may not exist
1249
+ # Remove line containing this content within the section
1250
+ # Use awk to handle section-aware removal
1251
+ awk -v section="$section_header" -v content="$content" '
1252
+ BEGIN { in_section = 0 }
1253
+ $0 == section { in_section = 1 }
1254
+ in_section && $0 ~ /^## / && $0 != section { in_section = 0 }
1255
+ in_section && $0 ~ content { skip = 1; next }
1256
+ { if (!skip) print; skip = 0 }
1257
+ ' "$tmp_file" > "${tmp_file}.new" && mv "${tmp_file}.new" "$tmp_file"
1258
+
1259
+ ((undone_count++))
1260
+ else
1261
+ # Entry already removed or not found
1262
+ failed_items+=("$content")
1263
+ fi
1264
+ done <<< "$promoted_items"
1265
+
1266
+ # Update METADATA stats in temp file - decrement counts
1267
+ case "$ptype" in
1268
+ stack) stat_key="total_stack_entries" ;;
1269
+ philosophy) stat_key="total_philosophies" ;;
1270
+ *) stat_key="total_${ptype}s" ;;
1271
+ esac
1272
+
1273
+ # Decrement stats (but not below 0)
1274
+ current_count=$(grep "\"${stat_key}\":" "$tmp_file" 2>/dev/null | grep -o '[0-9]*' | head -1 || echo "0") # SUPPRESS:OK -- read-default: file may not exist
1275
+ current_count=${current_count:-0}
1276
+ if [[ $current_count -gt 0 ]]; then
1277
+ new_count=$((current_count - 1))
1278
+ awk -v type="$stat_key" -v count="$new_count" '{
1279
+ gsub("\"" type "\": [0-9]*", "\"" type "\": " count)
1280
+ print
1281
+ }' "$tmp_file" > "${tmp_file}.stats" && mv "${tmp_file}.stats" "$tmp_file"
1282
+ fi
1283
+
1284
+ # Atomic move
1285
+ mv "$tmp_file" "$queen_file"
1286
+
1287
+ # Remove undo file after successful revert
1288
+ rm -f "$undo_file"
1289
+
1290
+ # Log activity
1291
+ bash "$0" activity-log "UNDONE" "Queen" "Reverted $undone_count promotion(s) from QUEEN.md"
1292
+
1293
+ # Build result
1294
+ if [[ ${#failed_items[@]} -gt 0 ]]; then
1295
+ failed_json=$(printf '%s\n' "${failed_items[@]}" | jq -R . | jq -s '.')
1296
+ result=$(jq -n --argjson undone "$undone_count" --argjson failed "$failed_json" '{undone: $undone, not_found: $failed}')
1297
+ else
1298
+ result=$(jq -n --argjson undone "$undone_count" '{undone: $undone, not_found: []}')
1299
+ fi
1300
+
1301
+ json_ok "$result"
1302
+ }
1303
+
1304
+ # ============================================================================
1305
+ # _instinct_read
1306
+ # Read learned instincts from COLONY_STATE.json memory
1307
+ # Migrated to state-api facade: uses _state_read_field for read-only access
1308
+ # Usage: instinct-read [--min-confidence N] [--max N] [--domain DOMAIN]
1309
+ # Returns: JSON with filtered, confidence-sorted instincts
1310
+ # ============================================================================
1311
+ _instinct_read() {
1312
+ ir_min_confidence="0.5"
1313
+ ir_max="5"
1314
+ ir_domain=""
1315
+
1316
+ # Parse flags from positional args
1317
+ ir_shift=1
1318
+ while [[ $ir_shift -le $# ]]; do
1319
+ eval "ir_arg=\${$ir_shift}"
1320
+ ir_shift=$((ir_shift + 1))
1321
+ case "$ir_arg" in
1322
+ --min-confidence)
1323
+ eval "ir_min_confidence=\${$ir_shift}"
1324
+ ir_shift=$((ir_shift + 1))
1325
+ ;;
1326
+ --max)
1327
+ eval "ir_max=\${$ir_shift}"
1328
+ ir_shift=$((ir_shift + 1))
1329
+ ;;
1330
+ --domain)
1331
+ eval "ir_domain=\${$ir_shift}"
1332
+ ir_shift=$((ir_shift + 1))
1333
+ ;;
1334
+ esac
1335
+ done
1336
+
1337
+ # Read full state via facade
1338
+ ir_state=$(_state_read_field '.')
1339
+ if [[ -z "$ir_state" ]]; then
1340
+ json_err "$E_FILE_NOT_FOUND" "COLONY_STATE.json not found. Run /ant:init first."
1341
+ fi
1342
+
1343
+ # Check if memory.instincts exists
1344
+ ir_has_instincts=$(echo "$ir_state" | jq 'if .memory.instincts then "yes" else "no" end' 2>/dev/null || echo "no")
1345
+ if [[ "$ir_has_instincts" != '"yes"' ]]; then
1346
+ json_ok '{"instincts":[],"total":0,"filtered":0}'
1347
+ exit 0
1348
+ fi
1349
+
1350
+ ir_result=$(echo "$ir_state" | jq -c \
1351
+ --argjson min_conf "$ir_min_confidence" \
1352
+ --argjson max_count "$ir_max" \
1353
+ --arg domain_filter "$ir_domain" \
1354
+ '
1355
+ (.memory.instincts // []) as $all |
1356
+ ($all | length) as $total |
1357
+ $all
1358
+ | map(select(
1359
+ (.confidence // 0) >= $min_conf
1360
+ and (.status // "hypothesis") != "disproven"
1361
+ and (if $domain_filter != "" then (.domain // "") == $domain_filter else true end)
1362
+ ))
1363
+ | sort_by(-.confidence)
1364
+ | .[:$max_count]
1365
+ | {
1366
+ instincts: .,
1367
+ total: $total,
1368
+ filtered: (. | length)
1369
+ }
1370
+ ' 2>/dev/null)
1371
+
1372
+ if [[ -z "$ir_result" || "$ir_result" == "null" ]]; then
1373
+ json_ok '{"instincts":[],"total":0,"filtered":0}'
1374
+ else
1375
+ json_ok "$ir_result"
1376
+ fi
1377
+ }
1378
+
1379
+ # ============================================================================
1380
+ # _normalize_text
1381
+ # Canonical text form for fuzzy comparison: lowercase, strip punctuation,
1382
+ # collapse whitespace, synonym substitution, stop word removal
1383
+ # Usage: _normalize_text "When Implementing Tests"
1384
+ # Output: stdout (e.g., "writing testing")
1385
+ # ============================================================================
1386
+ _normalize_text() {
1387
+ local text="$1"
1388
+
1389
+ # Guard: empty input
1390
+ [[ -z "$text" ]] && echo "" && return 0
1391
+
1392
+ # Lowercase
1393
+ text=$(echo "$text" | tr '[:upper:]' '[:lower:]')
1394
+
1395
+ # Strip punctuation (keep alphanumeric, spaces, hyphens)
1396
+ text=$(echo "$text" | tr -cd '[:alnum:][:space:]-')
1397
+
1398
+ # Collapse whitespace
1399
+ text=$(echo "$text" | awk '{$1=$1};1')
1400
+
1401
+ # Synonym substitution + stop word removal via awk
1402
+ text=$(echo "$text" | awk 'BEGIN {
1403
+ syn["implementing"] = "writing"; syn["creating"] = "writing"; syn["building"] = "writing";
1404
+ syn["implement"] = "writing"; syn["create"] = "writing"; syn["build"] = "writing";
1405
+ syn["write"] = "writing";
1406
+ syn["tests"] = "testing"; syn["checking"] = "testing"; syn["verifying"] = "testing";
1407
+ syn["fixing"] = "resolving"; syn["repairing"] = "resolving"; syn["patching"] = "resolving";
1408
+ syn["fix"] = "resolving"; syn["repair"] = "resolving"; syn["patch"] = "resolving";
1409
+ syn["resolve"] = "resolving"
1410
+ }
1411
+ {
1412
+ n = split($0, words, " ")
1413
+ out = 0
1414
+ for (i = 1; i <= n; i++) {
1415
+ w = words[i]
1416
+ if (w == "") continue
1417
+ if (w in syn) w = syn[w]
1418
+ # Stop words: when, while, during, before, after
1419
+ if (w == "when" || w == "while" || w == "during" || w == "before" || w == "after") continue
1420
+ printf "%s%s", (out > 0 ? " " : ""), w
1421
+ out++
1422
+ }
1423
+ printf "\n"
1424
+ }')
1425
+
1426
+ echo "$text"
1427
+ }
1428
+
1429
+ # ============================================================================
1430
+ # _jaccard_similarity
1431
+ # Word-level Jaccard similarity between two strings
1432
+ # Usage: _jaccard_similarity "when writing tests" "when implementing tests"
1433
+ # Output: stdout (e.g., "0.80")
1434
+ # ============================================================================
1435
+ _jaccard_similarity() {
1436
+ local text_a="$1"
1437
+ local text_b="$2"
1438
+
1439
+ # Normalize both texts
1440
+ local norm_a norm_b
1441
+ norm_a=$(_normalize_text "$text_a")
1442
+ norm_b=$(_normalize_text "$text_b")
1443
+
1444
+ # Guard: empty after normalization
1445
+ [[ -z "$norm_a" || -z "$norm_b" ]] && echo "0.00" && return 0
1446
+
1447
+ # Compute Jaccard via awk using NUL delimiter between the two texts
1448
+ # Both texts are already normalized (no newlines, no special chars)
1449
+ printf '%s\037%s\n' "$norm_a" "$norm_b" | awk -F'\037' '
1450
+ {
1451
+ split($1, a_words, " ")
1452
+ split($2, b_words, " ")
1453
+
1454
+ # Build set A
1455
+ for (i in a_words) if (a_words[i] != "") set_a[a_words[i]] = 1
1456
+
1457
+ # Build set B
1458
+ for (i in b_words) if (b_words[i] != "") set_b[b_words[i]] = 1
1459
+
1460
+ # Compute intersection and union
1461
+ intersection = 0
1462
+ union = 0
1463
+ for (key in set_a) {
1464
+ union++
1465
+ if (key in set_b) intersection++
1466
+ }
1467
+ for (key in set_b) {
1468
+ if (!(key in set_a)) union++
1469
+ }
1470
+
1471
+ # Guard: avoid division by zero
1472
+ if (union == 0) { printf "0.00\n"; exit }
1473
+
1474
+ printf "%.2f\n", intersection / union
1475
+ }'
1476
+ }
1477
+
1478
+ # ============================================================================
1479
+ # _instinct_create
1480
+ # Create or update an instinct in COLONY_STATE.json
1481
+ # Migrated to state-api facade: uses _state_read_field for reads, _state_mutate for atomic writes
1482
+ # Usage: instinct-create --trigger "when X" --action "do Y" --confidence 0.5 --domain "architecture" --source "phase-3" --evidence "observation"
1483
+ # Deduplicates: if trigger+action matches existing instinct, boosts confidence instead
1484
+ # Fuzzy dedup: if trigger AND action both have >= 0.80 Jaccard similarity, merges into best match
1485
+ # Cap: max 30 instincts, evicts lowest confidence when exceeded
1486
+ # ============================================================================
1487
+ _instinct_create() {
1488
+ ic_trigger=""
1489
+ ic_action=""
1490
+ ic_confidence="0.5"
1491
+ ic_domain="workflow"
1492
+ ic_source=""
1493
+ ic_evidence=""
1494
+
1495
+ while [[ $# -gt 0 ]]; do
1496
+ case "$1" in
1497
+ --trigger) ic_trigger="$2"; shift 2 ;;
1498
+ --action) ic_action="$2"; shift 2 ;;
1499
+ --confidence) ic_confidence="$2"; shift 2 ;;
1500
+ --domain) ic_domain="$2"; shift 2 ;;
1501
+ --source) ic_source="$2"; shift 2 ;;
1502
+ --evidence) ic_evidence="$2"; shift 2 ;;
1503
+ *) shift ;;
1504
+ esac
1505
+ done
1506
+
1507
+ [[ -z "$ic_trigger" ]] && json_err "$E_VALIDATION_FAILED" "instinct-create requires --trigger"
1508
+ [[ -z "$ic_action" ]] && json_err "$E_VALIDATION_FAILED" "instinct-create requires --action"
1509
+
1510
+ # Validate confidence range
1511
+ if ! [[ "$ic_confidence" =~ ^(0(\.[0-9]+)?|1(\.0+)?)$ ]]; then
1512
+ ic_confidence="0.5"
1513
+ fi
1514
+
1515
+ ic_now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1516
+ ic_epoch=$(date +%s)
1517
+ ic_id="instinct_${ic_epoch}"
1518
+
1519
+ # Check for existing instinct with matching trigger+action via facade
1520
+ ic_existing=$(_state_read_field "$(printf '[(.memory.instincts // [])[] | select(.trigger == "%s" and .action == "%s")] | first // null' "$ic_trigger" "$ic_action")")
1521
+
1522
+ if [[ -n "$ic_existing" && "$ic_existing" != "null" ]]; then
1523
+ # Update existing: boost confidence by +0.1, increment applications
1524
+ IC_TRIGGER="$ic_trigger" IC_ACTION="$ic_action" IC_NOW="$ic_now" \
1525
+ _state_mutate '
1526
+ .memory.instincts = [
1527
+ (.memory.instincts // [])[] |
1528
+ if .trigger == env.IC_TRIGGER and .action == env.IC_ACTION then
1529
+ .confidence = ([(.confidence + 0.1), 1.0] | min) |
1530
+ .applications = ((.applications // 0) + 1) |
1531
+ .last_applied = env.IC_NOW
1532
+ else
1533
+ .
1534
+ end
1535
+ ]
1536
+ ' >/dev/null
1537
+
1538
+ # Read updated confidence
1539
+ ic_new_conf=$(_state_read_field "$(printf '[(.memory.instincts // [])[] | select(.trigger == "%s" and .action == "%s")] | first | .confidence // 0' "$ic_trigger" "$ic_action")")
1540
+ json_ok "{\"instinct_id\":\"existing\",\"action\":\"updated\",\"confidence\":$ic_new_conf}"
1541
+ else
1542
+ # --- Fuzzy dedup: check for semantically similar instinct ---
1543
+ ic_all_instincts=$(_state_read_field '.memory.instincts // []')
1544
+ ic_fuzzy_match=""
1545
+
1546
+ if [[ -n "$ic_all_instincts" && "$ic_all_instincts" != "null" && "$ic_all_instincts" != "[]" ]]; then
1547
+ ic_best_sim="0.00"
1548
+ ic_best_id=""
1549
+ ic_best_conf=""
1550
+
1551
+ # Iterate over existing instincts to find best fuzzy match
1552
+ while IFS= read -r ic_line; do
1553
+ [[ -z "$ic_line" ]] && continue
1554
+ ic_ex_trigger=$(echo "$ic_line" | jq -r '.trigger // empty')
1555
+ ic_ex_action=$(echo "$ic_line" | jq -r '.action // empty')
1556
+ ic_ex_id=$(echo "$ic_line" | jq -r '.id // empty')
1557
+ ic_ex_conf=$(echo "$ic_line" | jq -r '.confidence // 0')
1558
+
1559
+ [[ -z "$ic_ex_trigger" || -z "$ic_ex_action" || "$ic_ex_trigger" == "null" || "$ic_ex_action" == "null" ]] && continue
1560
+
1561
+ # Compute Jaccard similarity for trigger and action independently
1562
+ ic_trig_sim=$(_jaccard_similarity "$ic_trigger" "$ic_ex_trigger")
1563
+ ic_act_sim=$(_jaccard_similarity "$ic_action" "$ic_ex_action")
1564
+
1565
+ # Both must exceed 0.80 threshold
1566
+ if (( $(echo "$ic_trig_sim >= 0.80" | bc -l) )) && (( $(echo "$ic_act_sim >= 0.80" | bc -l) )); then
1567
+ # Pick highest similarity; tie-break by higher confidence
1568
+ ic_combined=$(echo "$ic_trig_sim + $ic_act_sim" | bc -l)
1569
+ ic_best_combined=$(echo "${ic_best_sim:-0} + 0" | bc -l)
1570
+ ic_best_conf_num="${ic_best_conf:-0}"
1571
+ if (( $(echo "$ic_combined > $ic_best_combined" | bc -l) )) || \
1572
+ (( $(echo "$ic_combined == $ic_best_combined && $ic_ex_conf >= $ic_best_conf_num" | bc -l) )); then
1573
+ ic_best_sim="$ic_combined"
1574
+ ic_best_id="$ic_ex_id"
1575
+ ic_best_conf="$ic_ex_conf"
1576
+ ic_fuzzy_match="$ic_line"
1577
+ fi
1578
+ fi
1579
+ done < <(echo "$ic_all_instincts" | jq -c '.[]')
1580
+ fi
1581
+
1582
+ if [[ -n "$ic_fuzzy_match" ]]; then
1583
+ # Merge into best matching instinct
1584
+ ic_ex_conf_num=$(echo "$ic_fuzzy_match" | jq -r '.confidence // 0')
1585
+ ic_ex_evidence=$(echo "$ic_fuzzy_match" | jq -c '.evidence // []')
1586
+ ic_ex_trigger=$(echo "$ic_fuzzy_match" | jq -r '.trigger')
1587
+ ic_ex_action=$(echo "$ic_fuzzy_match" | jq -r '.action')
1588
+
1589
+ # Average confidences (use printf to ensure leading zero for valid JSON)
1590
+ ic_new_conf=$(printf "%.2f" "$(echo "scale=4; ($ic_ex_conf_num + $ic_confidence) / 2" | bc -l)")
1591
+ # Keep longer trigger
1592
+ ic_merged_trigger="$ic_ex_trigger"
1593
+ [[ ${#ic_trigger} -gt ${#ic_merged_trigger} ]] && ic_merged_trigger="$ic_trigger"
1594
+ # Keep longer action
1595
+ ic_merged_action="$ic_ex_action"
1596
+ [[ ${#ic_action} -gt ${#ic_merged_action} ]] && ic_merged_action="$ic_action"
1597
+
1598
+ # Build evidence array: existing + new
1599
+ if [[ "$ic_evidence" != "" && "$ic_evidence" != "null" ]]; then
1600
+ ic_merged_evidence=$(echo "$ic_ex_evidence" | jq --arg ev "$ic_evidence" '. + [$ev]')
1601
+ else
1602
+ ic_merged_evidence="$ic_ex_evidence"
1603
+ fi
1604
+
1605
+ IC_FUZZY_ID="$ic_best_id" IC_MERGED_TRIGGER="$ic_merged_trigger" IC_MERGED_ACTION="$ic_merged_action" \
1606
+ IC_NEW_CONF="$ic_new_conf" IC_MERGED_EVIDENCE="$ic_merged_evidence" IC_NOW="$ic_now" \
1607
+ _state_mutate '
1608
+ .memory.instincts = [
1609
+ (.memory.instincts // [])[] |
1610
+ if .id == env.IC_FUZZY_ID then
1611
+ .trigger = env.IC_MERGED_TRIGGER |
1612
+ .action = env.IC_MERGED_ACTION |
1613
+ .confidence = (env.IC_NEW_CONF | tonumber) |
1614
+ .evidence = (env.IC_MERGED_EVIDENCE | fromjson) |
1615
+ .applications = ((.applications // 0) + 1) |
1616
+ .last_applied = env.IC_NOW
1617
+ else
1618
+ .
1619
+ end
1620
+ ]
1621
+ ' >/dev/null
1622
+
1623
+ json_ok "$(jq -n --arg iid "$ic_best_id" --argjson conf "$ic_new_conf" '{instinct_id: $iid, action: "merged", confidence: $conf}')"
1624
+ exit 0
1625
+ fi
1626
+
1627
+ # Create new instinct via _state_mutate (handles locking and backup)
1628
+ IC_ID="$ic_id" IC_TRIGGER="$ic_trigger" IC_ACTION="$ic_action" IC_CONFIDENCE="$ic_confidence" \
1629
+ IC_DOMAIN="$ic_domain" IC_SOURCE="$ic_source" IC_EVIDENCE="$ic_evidence" IC_NOW="$ic_now" \
1630
+ _state_mutate '
1631
+ .memory.instincts = (
1632
+ ((.memory.instincts // []) + [{
1633
+ id: env.IC_ID,
1634
+ trigger: env.IC_TRIGGER,
1635
+ action: env.IC_ACTION,
1636
+ confidence: (env.IC_CONFIDENCE | tonumber),
1637
+ status: "hypothesis",
1638
+ domain: env.IC_DOMAIN,
1639
+ source: env.IC_SOURCE,
1640
+ evidence: [env.IC_EVIDENCE],
1641
+ tested: false,
1642
+ created_at: env.IC_NOW,
1643
+ last_applied: null,
1644
+ applications: 0,
1645
+ successes: 0,
1646
+ failures: 0
1647
+ }])
1648
+ | sort_by(-.confidence)
1649
+ | .[:30]
1650
+ )
1651
+ ' >/dev/null
1652
+
1653
+ json_ok "$(jq -n --arg iid "$ic_id" --argjson conf "$ic_confidence" '{instinct_id: $iid, action: "created", confidence: $conf}')"
1654
+ fi
1655
+ exit 0
1656
+ }
1657
+
1658
+ # ============================================================================
1659
+ # _instinct_apply
1660
+ # Record when an instinct was actually used in practice
1661
+ # Migrated to state-api facade: uses _state_read_field for reads, _state_mutate for atomic writes
1662
+ # Usage: instinct-apply --id <instinct_id> [--outcome success|failure]
1663
+ # Success: boosts confidence by 0.05 (cap 1.0), increments successes
1664
+ # Failure: reduces confidence by 0.1 (floor 0.1), increments failures
1665
+ # ============================================================================
1666
+ _instinct_apply() {
1667
+ ia_id=""
1668
+ ia_outcome="success"
1669
+
1670
+ while [[ $# -gt 0 ]]; do
1671
+ case "$1" in
1672
+ --id) ia_id="$2"; shift 2 ;;
1673
+ --outcome) ia_outcome="$2"; shift 2 ;;
1674
+ *) shift ;;
1675
+ esac
1676
+ done
1677
+
1678
+ [[ -z "$ia_id" ]] && json_err "$E_VALIDATION_FAILED" "instinct-apply requires --id"
1679
+
1680
+ # Validate outcome
1681
+ if [[ "$ia_outcome" != "success" && "$ia_outcome" != "failure" ]]; then
1682
+ json_err "$E_VALIDATION_FAILED" "instinct-apply --outcome must be 'success' or 'failure'"
1683
+ fi
1684
+
1685
+ # Check instinct exists via facade
1686
+ ia_exists=$(_state_read_field "$(printf '[(.memory.instincts // [])[] | select(.id == "%s")] | length > 0' "$ia_id")")
1687
+ if [[ "$ia_exists" != "true" ]]; then
1688
+ json_err "$E_RESOURCE_NOT_FOUND" "Instinct '$ia_id' not found"
1689
+ fi
1690
+
1691
+ ia_now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1692
+
1693
+ # Update the instinct based on outcome via _state_mutate (handles locking and backup)
1694
+ if [[ "$ia_outcome" == "success" ]]; then
1695
+ IA_ID="$ia_id" IA_NOW="$ia_now" \
1696
+ _state_mutate '
1697
+ .memory.instincts = [
1698
+ (.memory.instincts // [])[] |
1699
+ if .id == env.IA_ID then
1700
+ .applications = ((.applications // 0) + 1) |
1701
+ .successes = ((.successes // 0) + 1) |
1702
+ .confidence = ([(.confidence + 0.05), 1.0] | min) |
1703
+ .last_applied = env.IA_NOW
1704
+ else
1705
+ .
1706
+ end
1707
+ ]
1708
+ ' >/dev/null
1709
+ else
1710
+ IA_ID="$ia_id" IA_NOW="$ia_now" \
1711
+ _state_mutate '
1712
+ .memory.instincts = [
1713
+ (.memory.instincts // [])[] |
1714
+ if .id == env.IA_ID then
1715
+ .applications = ((.applications // 0) + 1) |
1716
+ .failures = ((.failures // 0) + 1) |
1717
+ .confidence = ([(.confidence - 0.1), 0.1] | max) |
1718
+ .last_applied = env.IA_NOW
1719
+ else
1720
+ .
1721
+ end
1722
+ ]
1723
+ ' >/dev/null
1724
+ fi
1725
+
1726
+ # Extract updated values for response via facade
1727
+ ia_new_apps=$(_state_read_field "$(printf '[(.memory.instincts // [])[] | select(.id == "%s")] | first | .applications' "$ia_id")")
1728
+ ia_new_conf=$(_state_read_field "$(printf '[(.memory.instincts // [])[] | select(.id == "%s")] | first | .confidence' "$ia_id")")
1729
+
1730
+ json_ok "$(jq -n --arg iid "$ia_id" --argjson apps "$ia_new_apps" --argjson conf "$ia_new_conf" '{applied: true, instinct_id: $iid, applications: $apps, new_confidence: $conf}')"
1731
+ exit 0
1732
+ }
1733
+
1734
+ # ============================================================================
1735
+ # _learning_extract_fallback
1736
+ # Deterministic fallback for learning extraction when builders skip learning output.
1737
+ # Produces structured learning objects from git diff and feeds them through instinct-create.
1738
+ # Usage: learning-extract-fallback (no args -- reads git state and colony state)
1739
+ # Returns: JSON {"learnings": [...], "count": N}
1740
+ # Note: Uses jq for grouping/sorting (bash 3.2 compatible -- no associative arrays)
1741
+ # ============================================================================
1742
+ _learning_extract_fallback() {
1743
+ # Pre-flight: verify git history exists
1744
+ if ! git rev-parse HEAD~1 >/dev/null 2>&1; then
1745
+ json_ok '{"learnings":[],"count":0}'
1746
+ exit 0
1747
+ fi
1748
+
1749
+ # Pre-flight: verify colony state exists
1750
+ if [[ ! -f "$DATA_DIR/COLONY_STATE.json" ]]; then
1751
+ json_ok '{"learnings":[],"count":0}'
1752
+ exit 0
1753
+ fi
1754
+
1755
+ # Read git diff data
1756
+ lef_stat=$(git diff --stat HEAD~1 2>/dev/null || echo "")
1757
+ lef_files=$(git diff --name-only HEAD~1 2>/dev/null || echo "")
1758
+
1759
+ # Guard: no changes
1760
+ if [[ -z "$lef_files" ]]; then
1761
+ json_ok '{"learnings":[],"count":0}'
1762
+ exit 0
1763
+ fi
1764
+
1765
+ # Read last-build-claims.json if it exists for task context
1766
+ lef_claims=""
1767
+ if [[ -f "$COLONY_DATA_DIR/last-build-claims.json" ]]; then
1768
+ lef_claims=$(cat "$COLONY_DATA_DIR/last-build-claims.json" 2>/dev/null || echo "")
1769
+ fi
1770
+
1771
+ # Parse stat data into JSON: extract file path, insertions, deletions per line
1772
+ # git diff --stat format: " path/to/file | 42 +++++---" or " path/to/file | 10"
1773
+ # Also include the full file list for files with 0 insertions (binary/new files)
1774
+ lef_files_json=$(printf '%s\n' "$lef_stat" | awk '
1775
+ {
1776
+ # Skip empty lines
1777
+ if ($0 == "") next
1778
+ # Skip the summary line (e.g., "13 files changed, 693 insertions(+), 128 deletions(-)")
1779
+ if ($0 ~ /files? changed/) next
1780
+ # Split on " | " to get file path and stat
1781
+ idx = index($0, " | ")
1782
+ if (idx > 0) {
1783
+ fpath = substr($0, 1, idx - 1)
1784
+ rest = substr($0, idx + 3)
1785
+ # Extract first two numbers from rest (insertions, deletions)
1786
+ gsub(/[^0-9 ]/, "", rest)
1787
+ n = split(rest, nums, " ")
1788
+ ins = (nums[1] != "" ? nums[1] : 0)
1789
+ del = (nums[2] != "" ? nums[2] : 0)
1790
+ } else {
1791
+ fpath = $0
1792
+ ins = 0
1793
+ del = 0
1794
+ }
1795
+ # Trim whitespace from path (git stat pads with trailing spaces)
1796
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", fpath)
1797
+ if (fpath != "") printf "{\"path\":\"%s\",\"ins\":%d,\"del\":%d}\n", fpath, ins, del
1798
+ }' | jq -sc '.')
1799
+
1800
+ # Filter noise files and categorize using jq
1801
+ lef_filtered=$(echo "$lef_files_json" | jq '[.[] |
1802
+ select((.path | startswith(".aether/data/")) | not) |
1803
+ select((.path | startswith(".aether/dreams/")) | not) |
1804
+ select(.path != "package-lock.json") |
1805
+ select((.path | startswith("node_modules/")) | not) |
1806
+ # Categorize (use extra parens around `or` expressions for jq parser)
1807
+ .category = (
1808
+ if ((.path | test("^(tests|test)/")) or (.path | test("_test\\.")) or (.path | test("\\.test\\.")) or (.path | test("\\.spec\\.")))
1809
+ then "testing"
1810
+ elif ((.path | startswith("src/")) or (.path | startswith("lib/")))
1811
+ then "source"
1812
+ elif ((.path | startswith("docs/")) or (.path | endswith(".md")))
1813
+ then "documentation"
1814
+ elif (.path | test("\\.(json|yaml|yml|toml|env|conf|config)$"))
1815
+ then "configuration"
1816
+ else "source"
1817
+ end
1818
+ ) |
1819
+ # Calculate absolute net change
1820
+ .abs_net = ((.ins - .del) | if . < 0 then -. else . end) |
1821
+ # Determine if test file
1822
+ .is_test = (.category == "testing") |
1823
+ # Skip trivial non-test changes (abs_net < 3)
1824
+ select((.is_test == true) or (.abs_net >= 3)) |
1825
+ # Extract directory
1826
+ .dir = (.path | split("/") | .[0:-1] | join("/"))
1827
+ ]')
1828
+
1829
+ # Guard: no significant changes after filtering
1830
+ lef_sig_count=$(echo "$lef_filtered" | jq 'length')
1831
+ if [[ "$lef_sig_count" -eq 0 ]]; then
1832
+ json_ok '{"learnings":[],"count":0}'
1833
+ exit 0
1834
+ fi
1835
+
1836
+ # Group by category, sort by total change magnitude, cap at 5
1837
+ # Using jq for grouping (bash 3.2 compatible)
1838
+ lef_categories=$(echo "$lef_filtered" | jq -r '
1839
+ group_by(.category) |
1840
+ map({
1841
+ category: .[0].category,
1842
+ file_count: length,
1843
+ total_ins: (map(.ins) | add),
1844
+ total_del: (map(.del) | add),
1845
+ total_change: ((map(.ins) | add) + (map(.del) | add)),
1846
+ dir: (map(.dir) | group_by(.) | map({d: .[0], c: length}) | sort_by(-.c) | .[0].d)
1847
+ }) |
1848
+ sort_by(-.total_change) |
1849
+ .[:5]
1850
+ ')
1851
+
1852
+ # Get current phase for source tag
1853
+ lef_phase=$(_state_read_field '.current_phase // 0')
1854
+ lef_phase=${lef_phase:-0}
1855
+
1856
+ # Generate learning objects
1857
+ lef_learnings="[]"
1858
+ lef_count=0
1859
+
1860
+ lef_cat_count=$(echo "$lef_categories" | jq 'length')
1861
+ for ((i=0; i<lef_cat_count && i<5; i++)); do
1862
+ lef_cat_data=$(echo "$lef_categories" | jq ".[$i]")
1863
+ lef_cat=$(echo "$lef_cat_data" | jq -r '.category')
1864
+ lef_fcount=$(echo "$lef_cat_data" | jq -r '.file_count')
1865
+ lef_fins=$(echo "$lef_cat_data" | jq -r '.total_ins')
1866
+ lef_fdel=$(echo "$lef_cat_data" | jq -r '.total_del')
1867
+ lef_fdir=$(echo "$lef_cat_data" | jq -r '.dir')
1868
+
1869
+ # Build fact string
1870
+ lef_fact="Modified $lef_fcount files in $lef_fdir ($lef_fins+/$lef_fdel- lines)"
1871
+
1872
+ # Build interpretation based on category
1873
+ case "$lef_cat" in
1874
+ testing)
1875
+ if [[ "$lef_fdel" == "0" ]]; then
1876
+ lef_interp="Added tests, improving test coverage"
1877
+ else
1878
+ lef_interp="Modified tests, likely improving test coverage"
1879
+ fi
1880
+ ;;
1881
+ source)
1882
+ if [[ "$lef_fdel" -gt "$lef_fins" ]]; then
1883
+ lef_interp="Refactored or simplified $lef_fdir module code"
1884
+ else
1885
+ lef_interp="Extended or modified $lef_fdir module code"
1886
+ fi
1887
+ ;;
1888
+ configuration)
1889
+ lef_interp="Updated project configuration"
1890
+ ;;
1891
+ documentation)
1892
+ lef_interp="Updated project documentation"
1893
+ ;;
1894
+ *)
1895
+ lef_interp="Modified $lef_fdir files"
1896
+ ;;
1897
+ esac
1898
+
1899
+ # Build trigger and action for instinct-create
1900
+ lef_trigger="when working on $lef_cat"
1901
+ lef_action="$lef_interp"
1902
+
1903
+ # Escape strings for JSON
1904
+ lef_fact_json=$(echo "$lef_fact" | jq -Rs '.')
1905
+ lef_interp_json=$(echo "$lef_interp" | jq -Rs '.')
1906
+
1907
+ # Append to learnings array
1908
+ lef_learnings=$(echo "$lef_learnings" | jq --arg trigger "$lef_trigger" \
1909
+ --arg action "$lef_action" \
1910
+ --argjson fact "$lef_fact_json" \
1911
+ --argjson interp "$lef_interp_json" \
1912
+ '. += [{trigger: $trigger, action: $action, fact: $fact, interpretation: $interp}]')
1913
+
1914
+ # Feed through instinct-create (non-blocking -- failures are logged but don't stop extraction)
1915
+ bash "$0" instinct-create \
1916
+ --trigger "$lef_trigger" \
1917
+ --action "$lef_action" \
1918
+ --confidence 0.5 \
1919
+ --domain "$lef_cat" \
1920
+ --source "fallback-phase-$lef_phase" \
1921
+ --evidence "$lef_fact" >/dev/null 2>&1 || _aether_log_error "Fallback instinct-create failed for $lef_cat"
1922
+
1923
+ lef_count=$((lef_count + 1))
1924
+ done
1925
+
1926
+ json_ok "{\"learnings\":$lef_learnings,\"count\":$lef_count}"
1927
+ exit 0
1928
+ }