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,2029 @@
1
+ #!/usr/bin/env bash
2
+ # Pheromone utility functions -- extracted from aether-utils.sh
3
+ # Provides: _pheromone_export_eternal, _pheromone_write, _pheromone_count, _pheromone_display,
4
+ # _pheromone_read, _pheromone_prime, _colony_prime, _pheromone_expire,
5
+ # _eternal_init, _eternal_store, _pheromone_export_xml, _pheromone_import_xml,
6
+ # _pheromone_validate_xml
7
+ # Note: colony-prime is the most complex function (~706 lines). Moved verbatim.
8
+ # Calls hive-read via subprocess (safe). eternal-init and eternal-store are
9
+ # tightly coupled to pheromone-expire.
10
+ # Uses SCRIPT_DIR, AETHER_ROOT, DATA_DIR, HOME from main file preamble.
11
+
12
+
13
+ # ============================================================================
14
+ # _pheromone_export_eternal
15
+ # Export pheromones to eternal XML format (distinct from xml-utils.sh pheromone-export function)
16
+ # ============================================================================
17
+ _pheromone_export_eternal() {
18
+ _deprecation_warning "pheromone-export-eternal"
19
+ # Export pheromones to eternal XML format (distinct from xml-utils.sh pheromone-export function)
20
+ # Usage: pheromone-export-eternal [input_json] [output_xml]
21
+ # input_json: Path to pheromones.json (default: .aether/data/pheromones.json)
22
+ # output_xml: Path to output XML (default: ~/.aether/eternal/pheromones.xml)
23
+
24
+ input_json="${1:-.aether/data/pheromones.json}"
25
+ output_xml="${2:-$HOME/.aether/eternal/pheromones.xml}"
26
+ schema_file="${3:-$SCRIPT_DIR/schemas/pheromone.xsd}"
27
+
28
+ # Ensure xml-utils.sh is sourced
29
+ if ! type pheromone-export &>/dev/null; then
30
+ [[ -f "$SCRIPT_DIR/utils/xml-utils.sh" ]] && source "$SCRIPT_DIR/utils/xml-utils.sh"
31
+ fi
32
+
33
+ if type pheromone-export &>/dev/null; then
34
+ pheromone-export "$input_json" "$output_xml" "$schema_file"
35
+ else
36
+ json_err "$E_DEPENDENCY_MISSING" "xml-utils.sh not available. Try: run aether update to restore utility scripts."
37
+ fi
38
+ }
39
+
40
+ # ============================================================================
41
+ # _pheromone_write
42
+ # Write a pheromone signal to pheromones.json
43
+ # ============================================================================
44
+ _pheromone_write() {
45
+ # Write a pheromone signal to pheromones.json
46
+ # Usage: pheromone-write <type> <content> [--strength N] [--ttl TTL] [--source SOURCE] [--reason REASON]
47
+ # type: FOCUS, REDIRECT, or FEEDBACK
48
+ # content: signal text (required, max 500 chars)
49
+ # --strength: 0.0-1.0 (defaults: REDIRECT=0.9, FOCUS=0.8, FEEDBACK=0.7)
50
+ # --ttl: phase_end (default), 2h, 1d, 7d, 30d, etc.
51
+ # --source: user (default), worker:builder, system
52
+ # --reason: human-readable explanation
53
+
54
+ pw_type="${1:-}"
55
+ pw_content="${2:-}"
56
+
57
+ # Validate type
58
+ if [[ -z "$pw_type" ]]; then
59
+ json_err "$E_VALIDATION_FAILED" "pheromone-write requires <type> argument (FOCUS, REDIRECT, or FEEDBACK)"
60
+ fi
61
+
62
+ pw_type=$(echo "$pw_type" | tr '[:lower:]' '[:upper:]')
63
+ case "$pw_type" in
64
+ FOCUS|REDIRECT|FEEDBACK) ;;
65
+ *) json_err "$E_VALIDATION_FAILED" "Invalid pheromone type: $pw_type. Must be FOCUS, REDIRECT, or FEEDBACK" ;;
66
+ esac
67
+
68
+ if [[ -z "$pw_content" ]]; then
69
+ json_err "$E_VALIDATION_FAILED" "pheromone-write requires <content> argument"
70
+ fi
71
+
72
+ # Sanitize and bound input content to reduce injection risk in prompt contexts.
73
+
74
+ # Check for XML tag injection BEFORE escaping angle brackets.
75
+ # Content is injected into worker prompts via colony-prime, so raw XML
76
+ # structural tags could break prompt boundaries.
77
+ if echo "$pw_content" | grep -Eiq '<[[:space:]]*/?(system|prompt|instructions|system-reminder|assistant|user|human)'; then
78
+ json_err "$E_VALIDATION_FAILED" "Pheromone content rejected: XML tag injection pattern detected"
79
+ fi
80
+
81
+ pw_content="${pw_content//</&lt;}"
82
+ pw_content="${pw_content//>/&gt;}"
83
+ pw_content="${pw_content:0:500}"
84
+ if echo "$pw_content" | grep -Eiq '(\$\(|`|(^|[[:space:]])curl([[:space:]]|$)|(^|[[:space:]])wget([[:space:]]|$)|(^|[[:space:]])rm([[:space:]]|$))'; then
85
+ json_err "$E_VALIDATION_FAILED" "Pheromone content rejected: potential injection pattern"
86
+ fi
87
+
88
+ # Check for prompt injection text patterns. These phrases attempt to
89
+ # override LLM instructions when the content is injected into prompts.
90
+ if echo "$pw_content" | grep -Eiq '(ignore\s+(all\s+)?(previous\s+|prior\s+|above\s+)?instructions|disregard\s+(above|previous|all)|you are now |new instructions:|system prompt)'; then
91
+ json_err "$E_VALIDATION_FAILED" "Pheromone content rejected: prompt injection pattern detected"
92
+ fi
93
+
94
+ # Parse optional flags from remaining args (after type and content)
95
+ pw_strength=""
96
+ pw_ttl="phase_end"
97
+ pw_source="user"
98
+ pw_reason=""
99
+
100
+ shift 2 # shift past type and content
101
+ while [[ $# -gt 0 ]]; do
102
+ case "$1" in
103
+ --strength) pw_strength="$2"; shift 2 ;;
104
+ --ttl) pw_ttl="$2"; shift 2 ;;
105
+ --source) pw_source="$2"; shift 2 ;;
106
+ --reason) pw_reason="$2"; shift 2 ;;
107
+ *) shift ;;
108
+ esac
109
+ done
110
+
111
+ # Apply default strength by type
112
+ if [[ -z "$pw_strength" ]]; then
113
+ case "$pw_type" in
114
+ REDIRECT) pw_strength="0.9" ;;
115
+ FOCUS) pw_strength="0.8" ;;
116
+ FEEDBACK) pw_strength="0.7" ;;
117
+ esac
118
+ fi
119
+
120
+ if ! [[ "$pw_strength" =~ ^(0(\.[0-9]+)?|1(\.0+)?)$ ]]; then
121
+ json_err "$E_VALIDATION_FAILED" "Strength must be a number between 0.0 and 1.0" "{\"provided\":\"$pw_strength\"}"
122
+ fi
123
+
124
+ # Apply default reason by type
125
+ if [[ -z "$pw_reason" ]]; then
126
+ pw_type_lower_r=$(echo "$pw_type" | tr '[:upper:]' '[:lower:]')
127
+ pw_reason="User emitted via /ant:${pw_type_lower_r}"
128
+ fi
129
+
130
+ # Set priority by type
131
+ case "$pw_type" in
132
+ REDIRECT) pw_priority="high" ;;
133
+ FOCUS) pw_priority="normal" ;;
134
+ FEEDBACK) pw_priority="low" ;;
135
+ esac
136
+
137
+ # Generate ID and timestamps
138
+ pw_epoch=$(date +%s)
139
+ pw_rand=$(( RANDOM % 10000 ))
140
+ pw_type_lower=$(echo "$pw_type" | tr '[:upper:]' '[:lower:]')
141
+ pw_id="sig_${pw_type_lower}_${pw_epoch}_${pw_rand}"
142
+ pw_created=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
143
+
144
+ # Compute expires_at from TTL
145
+ if [[ "$pw_ttl" == "phase_end" ]]; then
146
+ pw_expires="phase_end"
147
+ else
148
+ pw_ttl_secs=0
149
+ if [[ "$pw_ttl" =~ ^([0-9]+)m$ ]]; then
150
+ pw_ttl_secs=$(( ${BASH_REMATCH[1]} * 60 ))
151
+ elif [[ "$pw_ttl" =~ ^([0-9]+)h$ ]]; then
152
+ pw_ttl_secs=$(( ${BASH_REMATCH[1]} * 3600 ))
153
+ elif [[ "$pw_ttl" =~ ^([0-9]+)d$ ]]; then
154
+ pw_ttl_secs=$(( ${BASH_REMATCH[1]} * 86400 ))
155
+ fi
156
+ if [[ $pw_ttl_secs -gt 0 ]]; then
157
+ pw_expires_epoch=$(( pw_epoch + pw_ttl_secs ))
158
+ # SUPPRESS:OK -- cross-platform: macOS date-from-epoch syntax
159
+ # SUPPRESS:OK -- cross-platform: macOS vs Linux date/stat flags
160
+ pw_expires=$(date -u -r "$pw_expires_epoch" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
161
+ date -u -d "@$pw_expires_epoch" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
162
+ echo "phase_end")
163
+ else
164
+ pw_expires="phase_end"
165
+ fi
166
+ fi
167
+
168
+ pw_file="$COLONY_DATA_DIR/pheromones.json"
169
+
170
+ pw_lock_held=false
171
+ if type acquire_lock &>/dev/null; then
172
+ acquire_lock "$pw_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on pheromones.json"
173
+ pw_lock_held=true
174
+ # Trap ensures lock release on unexpected exit (json_err calls exit 1)
175
+ trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
176
+ fi
177
+
178
+ # Initialize pheromones.json if missing
179
+ if [[ ! -f "$pw_file" ]]; then
180
+ pw_colony_id="aether-dev"
181
+ # MIGRATE: direct COLONY_STATE.json access -- use _state_read_field instead
182
+ if [[ -f "$DATA_DIR/COLONY_STATE.json" ]]; then
183
+ # SUPPRESS:OK -- read-default: query may return empty
184
+ pw_colony_id=$(jq -r '.session_id // "aether-dev"' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || echo "aether-dev")
185
+ fi
186
+ pw_init_content=$(printf '{\n "version": "1.0.0",\n "colony_id": "%s",\n "generated_at": "%s",\n "signals": []\n}\n' \
187
+ "$pw_colony_id" "$pw_created")
188
+ atomic_write "$pw_file" "$pw_init_content" || {
189
+ _aether_log_error "Could not initialize pheromones file"
190
+ json_err "$E_UNKNOWN" "Failed to create pheromones file"
191
+ }
192
+ fi
193
+
194
+ # Compute SHA-256 content hash for deduplication
195
+ pw_hash=$(echo -n "$pw_content" | shasum -a 256 | cut -d' ' -f1)
196
+
197
+ # Check for existing active signal with same type and content_hash
198
+ # SUPPRESS:OK -- read-default: file may not exist yet
199
+ pw_existing_count=$(jq \
200
+ --arg type "$pw_type" \
201
+ --arg hash "$pw_hash" \
202
+ '[.signals[] | select(.active == true and .type == $type and .content_hash == $hash)] | length' \
203
+ "$pw_file" 2>/dev/null || echo "0") # SUPPRESS:OK -- read-default: returns fallback on failure
204
+
205
+ pw_action="created"
206
+
207
+ if [[ "$pw_existing_count" -gt 0 ]]; then
208
+ # Reinforce existing signal: update strength to max, reset created_at, increment reinforcement_count
209
+ pw_action="reinforced"
210
+
211
+ # Get the reinforced signal's ID for output (before modification)
212
+ # SUPPRESS:OK -- read-default: file may not exist yet
213
+ pw_id=$(jq -r \
214
+ --arg type "$pw_type" \
215
+ --arg hash "$pw_hash" \
216
+ '[.signals[] | select(.active == true and .type == $type and .content_hash == $hash)][0].id' \
217
+ "$pw_file" 2>/dev/null || echo "$pw_id") # SUPPRESS:OK -- read-default: returns fallback on failure
218
+
219
+ pw_updated=$(jq \
220
+ --arg type "$pw_type" \
221
+ --arg hash "$pw_hash" \
222
+ --argjson new_strength "$pw_strength" \
223
+ --arg new_created "$pw_created" \
224
+ '
225
+ .signals = [.signals[] |
226
+ if (.active == true and .type == $type and .content_hash == $hash) then
227
+ .strength = ([.strength, $new_strength] | max) |
228
+ .created_at = $new_created |
229
+ .reinforcement_count = ((.reinforcement_count // 0) + 1)
230
+ else
231
+ .
232
+ end
233
+ ]
234
+ ' "$pw_file" 2>/dev/null) # SUPPRESS:OK -- read-default: file may not exist yet
235
+
236
+ if [[ -z "$pw_updated" ]]; then
237
+ [[ "$pw_lock_held" == "true" ]] && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
238
+ json_err "${E_JSON_INVALID:-E_JSON_INVALID}" "Failed to reinforce signal in pheromones.json — jq parse error"
239
+ fi
240
+ else
241
+ # Build new signal object with content_hash and append
242
+ pw_signal=$(jq -n \
243
+ --arg id "$pw_id" \
244
+ --arg type "$pw_type" \
245
+ --arg priority "$pw_priority" \
246
+ --arg source "$pw_source" \
247
+ --arg created_at "$pw_created" \
248
+ --arg expires_at "$pw_expires" \
249
+ --argjson active true \
250
+ --argjson strength "$pw_strength" \
251
+ --arg reason "$pw_reason" \
252
+ --arg content "$pw_content" \
253
+ --arg content_hash "$pw_hash" \
254
+ --argjson reinforcement_count 0 \
255
+ '{id: $id, type: $type, priority: $priority, source: $source, created_at: $created_at, expires_at: $expires_at, active: $active, strength: ($strength | tonumber), reason: $reason, content: {text: $content}, content_hash: $content_hash, reinforcement_count: $reinforcement_count}')
256
+
257
+ pw_updated=$(jq --argjson sig "$pw_signal" '.signals += [$sig]' "$pw_file") || {
258
+ _aether_log_error "Could not append signal to pheromones.json"
259
+ }
260
+ if [[ -z "$pw_updated" || "$pw_updated" == "null" ]]; then
261
+ [[ "$pw_lock_held" == "true" ]] && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
262
+ json_err "${E_JSON_INVALID:-E_JSON_INVALID}" "Failed to update pheromones.json — jq parse error"
263
+ fi
264
+ fi
265
+
266
+ atomic_write "$pw_file" "$pw_updated" || {
267
+ [[ "$pw_lock_held" == "true" ]] && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
268
+ json_err "$E_JSON_INVALID" "Failed to write pheromones.json"
269
+ }
270
+ [[ "$pw_lock_held" == "true" ]] && { release_lock 2>/dev/null || true; trap - EXIT; } # SUPPRESS:OK -- cleanup: lock may not be held
271
+
272
+ # Backward compatibility: also write to constraints.json
273
+ pw_cfile="$COLONY_DATA_DIR/constraints.json"
274
+ if [[ "$pw_type" == "FOCUS" ]]; then
275
+ if [[ ! -f "$pw_cfile" ]]; then
276
+ atomic_write "$pw_cfile" '{"version":"1.0","focus":[],"constraints":[]}' || _aether_log_error "Could not initialize constraints file"
277
+ fi
278
+ pw_cfile_updated=$(jq --arg txt "$pw_content" '
279
+ .focus += [$txt] |
280
+ if (.focus | length) > 5 then .focus = .focus[-5:] else . end
281
+ ' "$pw_cfile" 2>/dev/null) # SUPPRESS:OK -- read-default: file may not exist yet
282
+ if [[ -n "$pw_cfile_updated" ]]; then
283
+ atomic_write "$pw_cfile" "$pw_cfile_updated" || _aether_log_error "Could not save focus constraint"
284
+ fi
285
+ elif [[ "$pw_type" == "REDIRECT" ]]; then
286
+ if [[ ! -f "$pw_cfile" ]]; then
287
+ atomic_write "$pw_cfile" '{"version":"1.0","focus":[],"constraints":[]}' || _aether_log_error "Could not initialize constraints file"
288
+ fi
289
+ pw_constraint=$(jq -n \
290
+ --arg id "c_${pw_epoch}" \
291
+ --arg content "$pw_content" \
292
+ --arg source "user:redirect" \
293
+ --arg created_at "$pw_created" \
294
+ '{id: $id, type: "AVOID", content: $content, source: $source, created_at: $created_at}')
295
+ pw_cfile_updated=$(jq --argjson c "$pw_constraint" '
296
+ .constraints += [$c] |
297
+ if (.constraints | length) > 10 then .constraints = .constraints[-10:] else . end
298
+ ' "$pw_cfile" 2>/dev/null) # SUPPRESS:OK -- read-default: file may not exist yet
299
+ if [[ -n "$pw_cfile_updated" ]]; then
300
+ atomic_write "$pw_cfile" "$pw_cfile_updated" || _aether_log_error "Could not save redirect constraint"
301
+ fi
302
+ fi
303
+
304
+ # Get active signal count
305
+ # SUPPRESS:OK -- read-default: query may return empty
306
+ pw_active_count=$(jq '[.signals[] | select(.active == true)] | length' "$pw_file" 2>/dev/null || echo "0")
307
+
308
+ json_ok "$(jq -n --arg signal_id "$pw_id" --arg type "$pw_type" --arg action "$pw_action" --argjson active_count "$pw_active_count" '{signal_id: $signal_id, type: $type, action: $action, active_count: $active_count}')"
309
+ }
310
+
311
+ # ============================================================================
312
+ # _pheromone_count
313
+ # Count active pheromone signals by type
314
+ # ============================================================================
315
+ _pheromone_count() {
316
+ # Count active pheromone signals by type
317
+ # Usage: pheromone-count
318
+ # Returns: JSON with per-type counts
319
+
320
+ pc_file="$COLONY_DATA_DIR/pheromones.json"
321
+
322
+ if [[ ! -f "$pc_file" ]]; then
323
+ json_ok '{"focus":0,"redirect":0,"feedback":0,"total":0}'
324
+ else
325
+ pc_result=$(jq -c '{
326
+ focus: ([.signals[] | select(.active == true and .type == "FOCUS")] | length),
327
+ redirect: ([.signals[] | select(.active == true and .type == "REDIRECT")] | length),
328
+ feedback: ([.signals[] | select(.active == true and .type == "FEEDBACK")] | length),
329
+ total: ([.signals[] | select(.active == true)] | length)
330
+ }' "$pc_file" 2>/dev/null) # SUPPRESS:OK -- read-default: operation may fail
331
+ if [[ -z "$pc_result" ]]; then
332
+ json_ok '{"focus":0,"redirect":0,"feedback":0,"total":0}'
333
+ else
334
+ json_ok "$pc_result"
335
+ fi
336
+ fi
337
+ }
338
+
339
+ # ============================================================================
340
+ # _pheromone_display
341
+ # Display active pheromones in formatted table
342
+ # ============================================================================
343
+ _pheromone_display() {
344
+ # Display active pheromones in formatted table
345
+ # Usage: pheromone-display [type]
346
+ # type: Optional filter (focus/redirect/feedback) or 'all' (default: all)
347
+ # Returns: Formatted table string (human-readable)
348
+
349
+ pd_file="$COLONY_DATA_DIR/pheromones.json"
350
+ pd_type="${1:-all}"
351
+ pd_now_iso=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
352
+
353
+ if [[ ! -f "$pd_file" ]]; then
354
+ echo "No pheromones active. Colony has no signals."
355
+ echo ""
356
+ echo "Inject signals with:"
357
+ echo " /ant:focus \"area\" - Guide attention"
358
+ echo " /ant:redirect \"avoid\" - Set hard constraint"
359
+ echo " /ant:feedback \"note\" - Provide guidance"
360
+ exit 0
361
+ fi
362
+
363
+ # Get signals with decay calculation (same as pheromone-read)
364
+ pd_signals=$(jq -c \
365
+ --arg now_iso "$pd_now_iso" \
366
+ --arg type_filter "$pd_type" \
367
+ '
368
+ def to_epoch(ts):
369
+ if ts == null or ts == "" or ts == "phase_end" then null
370
+ else
371
+ (ts | split("T")) as $parts |
372
+ ($parts[0] | split("-")) as $d |
373
+ ($parts[1] | rtrimstr("Z") | split(":")) as $t |
374
+ (($d[0] | tonumber) - 1970) * 365 * 86400 +
375
+ (($d[1] | tonumber) - 1) * 30 * 86400 +
376
+ (($d[2] | tonumber) - 1) * 86400 +
377
+ ($t[0] | tonumber) * 3600 +
378
+ ($t[1] | tonumber) * 60 +
379
+ ($t[2] | rtrimstr("Z") | tonumber)
380
+ end;
381
+
382
+ def decay_days(t):
383
+ if t == "FOCUS" then 30
384
+ elif t == "REDIRECT" then 60
385
+ else 90
386
+ end;
387
+
388
+ (to_epoch($now_iso)) as $now |
389
+ .signals | map(
390
+ (to_epoch(.created_at)) as $created_epoch |
391
+ (if $created_epoch != null then ($now - $created_epoch) / 86400 else 0 end) as $elapsed_days |
392
+ (decay_days(.type)) as $dd |
393
+ ((.strength // 0.8) * (1 - ($elapsed_days / $dd))) as $eff_raw |
394
+ (if $eff_raw < 0 then 0 else $eff_raw end) as $eff |
395
+ {
396
+ id: .id,
397
+ type: .type,
398
+ content: .content,
399
+ strength: (.strength // 0.8),
400
+ effective_strength: $eff,
401
+ elapsed_days: $elapsed_days,
402
+ remaining_days: ($dd - $elapsed_days),
403
+ created_at: .created_at,
404
+ active: (.active != false and $eff >= 0.1)
405
+ }
406
+ )
407
+ | map(select(.active == true))
408
+ | map(select(if $type_filter == "all" or $type_filter == "" then true else (.type | ascii_downcase) == ($type_filter | ascii_downcase) end))
409
+ | sort_by(-.effective_strength)
410
+ ' "$pd_file" 2>/dev/null) # SUPPRESS:OK -- read-default: file may not exist yet
411
+
412
+ if [[ -z "$pd_signals" || "$pd_signals" == "[]" ]]; then
413
+ echo "No active pheromones found."
414
+ if [[ "$pd_type" != "all" ]]; then
415
+ echo "Filter: $pd_type"
416
+ fi
417
+ exit 0
418
+ fi
419
+
420
+ # Count by type
421
+ pd_focus=$(echo "$pd_signals" | jq '[.[] | select(.type == "FOCUS")] | length')
422
+ pd_redirect=$(echo "$pd_signals" | jq '[.[] | select(.type == "REDIRECT")] | length')
423
+ pd_feedback=$(echo "$pd_signals" | jq '[.[] | select(.type == "FEEDBACK")] | length')
424
+ pd_total=$(echo "$pd_signals" | jq 'length')
425
+
426
+ # Display header
427
+ echo ""
428
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
429
+ echo " A C T I V E P H E R O M O N E S"
430
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
431
+ echo ""
432
+
433
+ # Display FOCUS signals
434
+ if [[ "$pd_focus" -gt 0 && ("$pd_type" == "all" || "$pd_type" == "focus") ]]; then
435
+ echo "🎯 FOCUS (Pay attention here)"
436
+ echo "$pd_signals" | jq -r '.[] | select(.type == "FOCUS") | " \n [\(.effective_strength * 100 | floor)%] \"\(.content.text // .content // "no content")\"\n └── \(.elapsed_days | floor)d ago, \(.remaining_days | floor)d remaining"' | head -20
437
+ echo ""
438
+ fi
439
+
440
+ # Display REDIRECT signals
441
+ if [[ "$pd_redirect" -gt 0 && ("$pd_type" == "all" || "$pd_type" == "redirect") ]]; then
442
+ echo "🚫 REDIRECT (Hard constraints - DO NOT do this)"
443
+ echo "$pd_signals" | jq -r '.[] | select(.type == "REDIRECT") | " \n [\(.effective_strength * 100 | floor)%] \"\(.content.text // .content // "no content")\"\n └── \(.elapsed_days | floor)d ago, \(.remaining_days | floor)d remaining"' | head -20
444
+ echo ""
445
+ fi
446
+
447
+ # Display FEEDBACK signals
448
+ if [[ "$pd_feedback" -gt 0 && ("$pd_type" == "all" || "$pd_type" == "feedback") ]]; then
449
+ echo "💬 FEEDBACK (Guidance to consider)"
450
+ echo "$pd_signals" | jq -r '.[] | select(.type == "FEEDBACK") | " \n [\(.effective_strength * 100 | floor)%] \"\(.content.text // .content // "no content")\"\n └── \(.elapsed_days | floor)d ago, \(.remaining_days | floor)d remaining"' | head -20
451
+ echo ""
452
+ fi
453
+
454
+ # Display footer
455
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
456
+ echo "$pd_total signal(s) active | Decay: FOCUS 30d, REDIRECT 60d, FEEDBACK 90d"
457
+ }
458
+
459
+ # ============================================================================
460
+ # _pheromone_read
461
+ # Read pheromones from colony data with decay calculation
462
+ # ============================================================================
463
+ _pheromone_read() {
464
+ # Read pheromones from colony data with decay calculation
465
+ # Usage: pheromone-read [type]
466
+ # type: Filter by pheromone type (focus, redirect, feedback) or 'all' (default: all)
467
+ # Returns: JSON object with pheromones array including effective_strength
468
+
469
+ pher_type="${1:-all}"
470
+ pher_file="$COLONY_DATA_DIR/pheromones.json"
471
+
472
+ # Check if file exists
473
+ if [[ ! -f "$pher_file" ]]; then
474
+ json_err "$E_FILE_NOT_FOUND" "Pheromones file not found. Run /ant:colonize first to initialize the colony."
475
+ fi
476
+
477
+ # Get current time as ISO for consistent epoch conversion
478
+ pher_now_iso=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
479
+
480
+ # Apply decay and expiry at read time
481
+ # Decay rates: FOCUS=30d, REDIRECT=60d, FEEDBACK/PATTERN=90d
482
+ # effective_strength = original_strength * (1 - elapsed_days / decay_days)
483
+ # If effective_strength < 0.1, mark inactive
484
+ # Also check expires_at: if not "phase_end" and past expiry, mark inactive
485
+ pher_type_upper=$(echo "$pher_type" | tr '[:lower:]' '[:upper:]')
486
+
487
+ pher_result=$(jq -c \
488
+ --arg now_iso "$pher_now_iso" \
489
+ --arg type_filter "$pher_type_upper" \
490
+ '
491
+ # Rough ISO-8601 to epoch: accumulate years*365d + month*30d + days + time
492
+ def to_epoch(ts):
493
+ if ts == null or ts == "" or ts == "phase_end" then null
494
+ else
495
+ (ts | split("T")) as $parts |
496
+ ($parts[0] | split("-")) as $d |
497
+ ($parts[1] | rtrimstr("Z") | split(":")) as $t |
498
+ (($d[0] | tonumber) - 1970) * 365 * 86400 +
499
+ (($d[1] | tonumber) - 1) * 30 * 86400 +
500
+ (($d[2] | tonumber) - 1) * 86400 +
501
+ ($t[0] | tonumber) * 3600 +
502
+ ($t[1] | tonumber) * 60 +
503
+ ($t[2] | rtrimstr("Z") | tonumber)
504
+ end;
505
+
506
+ def decay_days(t):
507
+ if t == "FOCUS" then 30
508
+ elif t == "REDIRECT" then 60
509
+ else 90
510
+ end;
511
+
512
+ (to_epoch($now_iso)) as $now |
513
+ .signals | map(
514
+ (to_epoch(.created_at)) as $created_epoch |
515
+ (if $created_epoch != null then ($now - $created_epoch) / 86400 else 0 end) as $elapsed_days |
516
+ (decay_days(.type)) as $dd |
517
+ ((.strength // 0.8) * (1 - ($elapsed_days / $dd))) as $eff_raw |
518
+ (if $eff_raw < 0 then 0 else $eff_raw end) as $eff |
519
+ (to_epoch(.expires_at)) as $exp_epoch |
520
+ ($exp_epoch != null and $exp_epoch <= $now) as $expired |
521
+ ($eff < 0.1 or $expired) as $deactivate |
522
+ . + {
523
+ effective_strength: (($eff * 100 | round) / 100),
524
+ active: (if $deactivate then false elif .active == false then false else true end)
525
+ }
526
+ ) |
527
+ map(select(.active == true)) |
528
+ if $type_filter != "ALL" then
529
+ map(select(.type == $type_filter))
530
+ else
531
+ .
532
+ end
533
+ ' "$pher_file" 2>/dev/null) # SUPPRESS:OK -- read-default: file may not exist yet
534
+
535
+ if [[ -z "$pher_result" || "$pher_result" == "null" ]]; then
536
+ json_ok '{"version":"1.0.0","signals":[]}'
537
+ else
538
+ pher_version=$(jq -r '.version // "1.0.0"' "$pher_file" 2>/dev/null || echo "1.0.0") # SUPPRESS:OK -- read-default: file may not exist yet
539
+ pher_colony=$(jq -r '.colony_id // "unknown"' "$pher_file" 2>/dev/null || echo "unknown") # SUPPRESS:OK -- read-default: file may not exist yet
540
+ json_ok "$(jq -n --arg version "$pher_version" --arg colony_id "$pher_colony" --argjson signals "$pher_result" '{version: $version, colony_id: $colony_id, signals: $signals}')"
541
+ fi
542
+ }
543
+
544
+ # ============================================================================
545
+ # _pheromone_prime
546
+ # Combine active pheromone signals and learned instincts into a prompt-ready block
547
+ # ============================================================================
548
+ _pheromone_prime() {
549
+ # Combine active pheromone signals and learned instincts into a prompt-ready block
550
+ # Usage: pheromone-prime [--compact] [--max-signals N] [--max-instincts N]
551
+ # Returns: JSON with signal_count, instinct_count, prompt_section, log_line
552
+
553
+ pp_compact=false
554
+ pp_max_signals=0
555
+ pp_max_instincts=5
556
+ while [[ $# -gt 0 ]]; do
557
+ case "$1" in
558
+ --compact) pp_compact=true ;;
559
+ --max-signals) shift; pp_max_signals="${1:-8}" ;;
560
+ --max-instincts) shift; pp_max_instincts="${1:-3}" ;;
561
+ esac
562
+ shift
563
+ done
564
+ [[ "$pp_max_signals" =~ ^[0-9]+$ ]] || pp_max_signals=8
565
+ [[ "$pp_max_instincts" =~ ^[0-9]+$ ]] || pp_max_instincts=3
566
+ [[ "$pp_max_signals" -lt 1 ]] && pp_max_signals=8
567
+ [[ "$pp_max_instincts" -lt 1 ]] && pp_max_instincts=3
568
+
569
+ pp_pher_file="$COLONY_DATA_DIR/pheromones.json"
570
+ # MIGRATE: direct COLONY_STATE.json access -- use _state_read_field instead
571
+ pp_state_file="$DATA_DIR/COLONY_STATE.json"
572
+ pp_now_iso=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
573
+
574
+ # Read active signals (same decay logic as pheromone-read)
575
+ pp_signals="[]"
576
+ if [[ -f "$pp_pher_file" ]]; then
577
+ pp_signals=$(jq -c \
578
+ --arg now_iso "$pp_now_iso" \
579
+ '
580
+ def to_epoch(ts):
581
+ if ts == null or ts == "" or ts == "phase_end" then null
582
+ else
583
+ (ts | split("T")) as $parts |
584
+ ($parts[0] | split("-")) as $d |
585
+ ($parts[1] | rtrimstr("Z") | split(":")) as $t |
586
+ (($d[0] | tonumber) - 1970) * 365 * 86400 +
587
+ (($d[1] | tonumber) - 1) * 30 * 86400 +
588
+ (($d[2] | tonumber) - 1) * 86400 +
589
+ ($t[0] | tonumber) * 3600 +
590
+ ($t[1] | tonumber) * 60 +
591
+ ($t[2] | rtrimstr("Z") | tonumber)
592
+ end;
593
+
594
+ def decay_days(t):
595
+ if t == "FOCUS" then 30
596
+ elif t == "REDIRECT" then 60
597
+ else 90
598
+ end;
599
+
600
+ (to_epoch($now_iso)) as $now |
601
+ .signals | map(
602
+ (to_epoch(.created_at)) as $created_epoch |
603
+ (if $created_epoch != null then ($now - $created_epoch) / 86400 else 0 end) as $elapsed_days |
604
+ (decay_days(.type)) as $dd |
605
+ ((.strength // 0.8) * (1 - ($elapsed_days / $dd))) as $eff_raw |
606
+ (if $eff_raw < 0 then 0 else $eff_raw end) as $eff |
607
+ (to_epoch(.expires_at)) as $exp_epoch |
608
+ ($exp_epoch != null and $exp_epoch <= $now) as $expired |
609
+ ($eff < 0.1 or $expired) as $deactivate |
610
+ . + {
611
+ effective_strength: (($eff * 100 | round) / 100),
612
+ active: (if $deactivate then false elif .active == false then false else true end)
613
+ }
614
+ ) |
615
+ map(select(.active == true))
616
+ ' "$pp_pher_file" 2>/dev/null || echo "[]") # SUPPRESS:OK -- read-default: file may not exist yet
617
+ fi
618
+
619
+ if [[ -z "$pp_signals" || "$pp_signals" == "null" ]]; then
620
+ pp_signals="[]"
621
+ fi
622
+
623
+ if [[ "$pp_compact" == "true" ]]; then
624
+ pp_signals=$(echo "$pp_signals" | jq -c --argjson max "$pp_max_signals" '
625
+ map(. + {priority: (if .type == "REDIRECT" then 1 elif .type == "FOCUS" then 2 elif .type == "FEEDBACK" then 3 elif .type == "POSITION" then 4 else 5 end)})
626
+ | sort_by(.priority, -(.effective_strength // 0))
627
+ | .[:$max]
628
+ | map(del(.priority))
629
+ ' 2>/dev/null || echo "[]") # SUPPRESS:OK -- read-default: returns fallback on failure
630
+ fi
631
+
632
+ # Read instincts (confidence >= 0.5, not disproven)
633
+ pp_instincts="[]"
634
+ if [[ -f "$pp_state_file" ]]; then
635
+ pp_instincts=$(jq -c \
636
+ --argjson max "$pp_max_instincts" \
637
+ '
638
+ (.memory.instincts // [])
639
+ | map(select(
640
+ (.confidence // 0) >= 0.5
641
+ and (.status // "hypothesis") != "disproven"
642
+ ))
643
+ | sort_by(-.confidence)
644
+ | .[:$max]
645
+ ' "$pp_state_file" 2>/dev/null || echo "[]") # SUPPRESS:OK -- read-default: file may not exist yet
646
+ fi
647
+
648
+ if [[ -z "$pp_instincts" || "$pp_instincts" == "null" ]]; then
649
+ pp_instincts="[]"
650
+ fi
651
+
652
+ pp_signal_count=$(echo "$pp_signals" | jq 'length' 2>/dev/null || echo "0") # SUPPRESS:OK -- read-default: file may not exist yet
653
+ pp_instinct_count=$(echo "$pp_instincts" | jq 'length' 2>/dev/null || echo "0") # SUPPRESS:OK -- read-default: file may not exist yet
654
+
655
+ # Build prompt section
656
+ if [[ "$pp_signal_count" -eq 0 && "$pp_instinct_count" -eq 0 ]]; then
657
+ pp_section=""
658
+ pp_log_line="Primed: 0 signals, 0 instincts"
659
+ else
660
+ if [[ "$pp_compact" == "true" ]]; then
661
+ pp_section="--- COMPACT SIGNALS ---"$'\n'
662
+ else
663
+ pp_section="--- ACTIVE SIGNALS (Colony Guidance) ---"$'\n'
664
+ fi
665
+
666
+ # FOCUS signals
667
+ # SUPPRESS:OK -- read-default: file may not exist yet
668
+ pp_focus=$(echo "$pp_signals" | jq -r 'map(select(.type == "FOCUS")) | .[] | "[" + ((.effective_strength * 10 | round) / 10 | tostring) + "] " + (.content.text // (if (.content | type) == "string" then .content else "" end))' 2>/dev/null || echo "")
669
+ if [[ -n "$pp_focus" ]]; then
670
+ pp_section+=$'\n'"FOCUS (Pay attention to):"$'\n'"$pp_focus"$'\n'
671
+ fi
672
+
673
+ # REDIRECT signals
674
+ # SUPPRESS:OK -- read-default: file may not exist yet
675
+ pp_redirect=$(echo "$pp_signals" | jq -r 'map(select(.type == "REDIRECT")) | .[] | "[" + ((.effective_strength * 10 | round) / 10 | tostring) + "] " + (.content.text // (if (.content | type) == "string" then .content else "" end))' 2>/dev/null || echo "")
676
+ if [[ -n "$pp_redirect" ]]; then
677
+ pp_section+=$'\n'"REDIRECT (HARD CONSTRAINTS - MUST follow):"$'\n'"$pp_redirect"$'\n'
678
+ fi
679
+
680
+ # FEEDBACK signals
681
+ # SUPPRESS:OK -- read-default: file may not exist yet
682
+ pp_feedback=$(echo "$pp_signals" | jq -r 'map(select(.type == "FEEDBACK")) | .[] | "[" + ((.effective_strength * 10 | round) / 10 | tostring) + "] " + (.content.text // (if (.content | type) == "string" then .content else "" end))' 2>/dev/null || echo "")
683
+ if [[ -n "$pp_feedback" ]]; then
684
+ pp_section+=$'\n'"FEEDBACK (Flexible guidance):"$'\n'"$pp_feedback"$'\n'
685
+ fi
686
+
687
+ # POSITION signals
688
+ # SUPPRESS:OK -- read-default: file may not exist yet
689
+ pp_position=$(echo "$pp_signals" | jq -r 'map(select(.type == "POSITION")) | .[] | "[" + ((.effective_strength * 10 | round) / 10 | tostring) + "] " + (.content.text // (if (.content | type) == "string" then .content else "" end))' 2>/dev/null || echo "")
690
+ if [[ -n "$pp_position" ]]; then
691
+ pp_section+=$'\n'"POSITION (Where work last progressed):"$'\n'"$pp_position"$'\n'
692
+ fi
693
+
694
+ # Instincts section (domain-grouped)
695
+ if [[ "$pp_instinct_count" -gt 0 ]]; then
696
+ if [[ "$pp_compact" == "true" ]]; then
697
+ pp_section+=$'\n'"--- INSTINCTS (Learned Behaviors) ---"$'\n'
698
+ else
699
+ pp_section+=$'\n'"--- INSTINCTS (Learned Behaviors) ---"$'\n'
700
+ pp_section+="Weight by confidence - higher = stronger guidance:"$'\n'
701
+ fi
702
+
703
+ # Group instincts by domain per user decision
704
+ pp_instinct_lines=$(echo "$pp_instincts" | jq -r '
705
+ group_by(.domain // "general")
706
+ | map({
707
+ domain: (.[0].domain // "general"),
708
+ items: [.[] | " [" + ((.confidence * 10 | round) / 10 | tostring) + "] When " + (.trigger | if test("^[Ww]hen ") then sub("^[Ww]hen "; "") else . end) + " -> " + .action]
709
+ })
710
+ | sort_by(.domain)
711
+ | .[]
712
+ | "\n" + (.domain | ascii_upcase | .[0:1]) + (.domain | .[1:]) + ":" + "\n" + (.items | join("\n"))
713
+ ' 2>/dev/null || echo "") # SUPPRESS:OK -- read-default: returns fallback on failure
714
+
715
+ if [[ -n "$pp_instinct_lines" ]]; then
716
+ pp_section+="$pp_instinct_lines"$'\n'
717
+ fi
718
+ fi
719
+
720
+ pp_section+=$'\n'"--- END COLONY CONTEXT ---"
721
+
722
+ pp_log_line="Primed: ${pp_signal_count} signals, ${pp_instinct_count} instincts"
723
+ fi
724
+
725
+ # Escape section for JSON embedding (use printf to avoid appending extra newline)
726
+ pp_section_json=$(printf '%s' "$pp_section" | jq -Rs '.' 2>/dev/null || echo '""') # SUPPRESS:OK -- read-default: returns fallback if missing
727
+ # SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
728
+ pp_log_json=$(printf '%s' "$pp_log_line" | jq -Rs '.' 2>/dev/null || echo '"Primed: 0 signals, 0 instincts"')
729
+
730
+ json_ok "$(jq -n --argjson signal_count "$pp_signal_count" --argjson instinct_count "$pp_instinct_count" --argjson prompt_section "$pp_section_json" --argjson log_line "$pp_log_json" '{signal_count: $signal_count, instinct_count: $instinct_count, prompt_section: $prompt_section, log_line: $log_line}')"
731
+ }
732
+
733
+ # ============================================================================
734
+ # _colony_prime
735
+ # Unified colony priming: combines wisdom (QUEEN.md) + signals + instincts into single output
736
+ # ============================================================================
737
+ _colony_prime() {
738
+ # Unified colony priming: combines wisdom (QUEEN.md) + signals + instincts into single output
739
+ # Usage: colony-prime [--compact]
740
+ # Returns: JSON with wisdom, signals, prompt_section
741
+ # Error handling: QUEEN.md missing = FAIL HARD; pheromones.json missing = warn but continue
742
+
743
+ cp_compact=false
744
+ if [[ "${1:-}" == "--compact" ]]; then
745
+ cp_compact=true
746
+ fi
747
+
748
+ # Total character budget for cp_final_prompt
749
+ cp_max_chars=8000
750
+ if [[ "$cp_compact" == "true" ]]; then
751
+ cp_max_chars=4000
752
+ fi
753
+
754
+ cp_global_queen="$HOME/.aether/QUEEN.md"
755
+ cp_local_queen="$AETHER_ROOT/.aether/QUEEN.md"
756
+
757
+ # Track if we have any QUEEN.md
758
+ cp_has_global=false
759
+ cp_has_local=false
760
+ cp_wisdom_json='{}'
761
+
762
+ # Initialize empty wisdom objects (used if file doesn't exist) -- v2 keys
763
+ cp_global_wisdom='{"user_prefs":"","codebase_patterns":"","build_learnings":"","instincts":""}'
764
+ cp_local_wisdom='{"user_prefs":"","codebase_patterns":"","build_learnings":"","instincts":""}'
765
+
766
+ # Helper to filter wisdom entries, keeping only actual entries and phase headers
767
+ # Strips description paragraphs, placeholder text, and boilerplate
768
+ # Returns only lines starting with "- " (entries) or "### " (phase headers)
769
+ _filter_wisdom_entries() {
770
+ local raw="$1"
771
+ if [[ -z "$raw" || "$raw" == "null" ]]; then
772
+ echo ""
773
+ return
774
+ fi
775
+ echo "$raw" | grep -E '^(- |### )' || echo "" # SUPPRESS:OK -- grep returns 1 on no matches
776
+ }
777
+
778
+ # Helper to extract wisdom sections from a QUEEN.md file
779
+ # Uses line number approach to avoid macOS awk range issues
780
+ # Supports both v2 (4-section) and v1 (6-emoji-section) formats
781
+ _extract_wisdom() {
782
+ local queen_file="$1"
783
+
784
+ # Format detection: check for v2 header "## Build Learnings"
785
+ if grep -q '^## Build Learnings$' "$queen_file" 2>/dev/null; then
786
+ # === V2 FORMAT (4 clean sections) ===
787
+ local uprefs_line=$(awk '/^## User Preferences$/ {print NR; exit}' "$queen_file")
788
+ local cpat_line=$(awk '/^## Codebase Patterns$/ {print NR; exit}' "$queen_file")
789
+ local blearn_line=$(awk '/^## Build Learnings$/ {print NR; exit}' "$queen_file")
790
+ local inst_line=$(awk '/^## Instincts$/ {print NR; exit}' "$queen_file")
791
+ local evo_line=$(awk '/^## Evolution Log$/ {print NR; exit}' "$queen_file")
792
+
793
+ local user_prefs codebase_patterns build_learnings instincts
794
+
795
+ local uprefs_end="${cpat_line:-${blearn_line:-${inst_line:-${evo_line:-999999}}}}"
796
+ if [[ -n "$uprefs_line" ]]; then
797
+ # SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
798
+ user_prefs=$(awk -v s="$uprefs_line" -v e="$uprefs_end" 'NR > s && NR < e {print}' "$queen_file" | sed '/^$/d' | sed '/^---$/d' | jq -Rs '.' 2>/dev/null || echo '""')
799
+ else user_prefs='""'; fi
800
+
801
+ local cpat_end="${blearn_line:-${inst_line:-${evo_line:-999999}}}"
802
+ if [[ -n "$cpat_line" ]]; then
803
+ # SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
804
+ codebase_patterns=$(awk -v s="$cpat_line" -v e="$cpat_end" 'NR > s && NR < e {print}' "$queen_file" | sed '/^$/d' | sed '/^---$/d' | jq -Rs '.' 2>/dev/null || echo '""')
805
+ else codebase_patterns='""'; fi
806
+
807
+ local blearn_end="${inst_line:-${evo_line:-999999}}"
808
+ if [[ -n "$blearn_line" ]]; then
809
+ # SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
810
+ build_learnings=$(awk -v s="$blearn_line" -v e="$blearn_end" 'NR > s && NR < e {print}' "$queen_file" | sed '/^$/d' | sed '/^---$/d' | jq -Rs '.' 2>/dev/null || echo '""')
811
+ else build_learnings='""'; fi
812
+
813
+ if [[ -n "$inst_line" ]]; then
814
+ # SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
815
+ instincts=$(awk -v s="$inst_line" -v e="${evo_line:-999999}" 'NR > s && NR < e {print}' "$queen_file" | sed '/^$/d' | sed '/^---$/d' | jq -Rs '.' 2>/dev/null || echo '""')
816
+ else instincts='""'; fi
817
+
818
+ user_prefs=${user_prefs:-'""'}
819
+ codebase_patterns=${codebase_patterns:-'""'}
820
+ build_learnings=${build_learnings:-'""'}
821
+ instincts=${instincts:-'""'}
822
+
823
+ echo "{\"user_prefs\":$user_prefs,\"codebase_patterns\":$codebase_patterns,\"build_learnings\":$build_learnings,\"instincts\":$instincts}"
824
+
825
+ else
826
+ # === V1 FORMAT (6 emoji sections, mapped to v2 keys) ===
827
+ local p_line=$(awk '/^## ..? ?Philosophies$/ {print NR; exit}' "$queen_file")
828
+ local pat_line=$(awk '/^## ..? ?Patterns$/ {print NR; exit}' "$queen_file")
829
+ local red_line=$(awk '/^## ..? ?Redirects$/ {print NR; exit}' "$queen_file")
830
+ local stack_line=$(awk '/^## ..? ?Stack Wisdom$/ {print NR; exit}' "$queen_file")
831
+ local dec_line=$(awk '/^## ..? ?Decrees$/ {print NR; exit}' "$queen_file")
832
+ local prefs_line=$(awk '/^## ..? ?User Preferences$/ {print NR; exit}' "$queen_file")
833
+ local evo_line=$(awk '/^## ..? ?Evolution Log$/ {print NR; exit}' "$queen_file")
834
+
835
+ local philosophies patterns redirects stack_wisdom decrees user_prefs
836
+
837
+ if [[ -n "$p_line" && -n "$pat_line" ]]; then
838
+ # SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
839
+ philosophies=$(awk -v s="$p_line" -v e="$pat_line" 'NR > s && NR < e {print}' "$queen_file" | sed '/^$/d' | jq -Rs '.' 2>/dev/null || echo '""')
840
+ else philosophies='""'; fi
841
+ if [[ -n "$pat_line" && -n "$red_line" ]]; then
842
+ # SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
843
+ patterns=$(awk -v s="$pat_line" -v e="$red_line" 'NR > s && NR < e {print}' "$queen_file" | sed '/^$/d' | jq -Rs '.' 2>/dev/null || echo '""')
844
+ else patterns='""'; fi
845
+ if [[ -n "$red_line" && -n "$stack_line" ]]; then
846
+ # SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
847
+ redirects=$(awk -v s="$red_line" -v e="$stack_line" 'NR > s && NR < e {print}' "$queen_file" | sed '/^$/d' | jq -Rs '.' 2>/dev/null || echo '""')
848
+ else redirects='""'; fi
849
+ if [[ -n "$stack_line" && -n "$dec_line" ]]; then
850
+ # SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
851
+ stack_wisdom=$(awk -v s="$stack_line" -v e="$dec_line" 'NR > s && NR < e {print}' "$queen_file" | sed '/^$/d' | jq -Rs '.' 2>/dev/null || echo '""')
852
+ else stack_wisdom='""'; fi
853
+
854
+ local dec_end="${prefs_line:-${evo_line:-999999}}"
855
+ if [[ -n "$dec_line" ]]; then
856
+ # SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
857
+ decrees=$(awk -v s="$dec_line" -v e="$dec_end" 'NR > s && NR < e {print}' "$queen_file" | sed '/^$/d' | jq -Rs '.' 2>/dev/null || echo '""')
858
+ else decrees='""'; fi
859
+
860
+ if [[ -n "$prefs_line" ]]; then
861
+ # SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
862
+ user_prefs=$(awk -v s="$prefs_line" -v e="${evo_line:-999999}" 'NR > s && NR < e {print}' "$queen_file" | sed '/^$/d' | jq -Rs '.' 2>/dev/null || echo '""')
863
+ else user_prefs='""'; fi
864
+
865
+ philosophies=${philosophies:-'""'}
866
+ patterns=${patterns:-'""'}
867
+ redirects=${redirects:-'""'}
868
+ stack_wisdom=${stack_wisdom:-'""'}
869
+ decrees=${decrees:-'""'}
870
+ user_prefs=${user_prefs:-'""'}
871
+
872
+ # Map v1 -> v2: combine old sections into new keys
873
+ local combined_codebase
874
+ combined_codebase=$(jq -n \
875
+ --arg phil "$philosophies" \
876
+ --arg pat "$patterns" \
877
+ --arg red "$redirects" \
878
+ --arg stack "$stack_wisdom" \
879
+ '[$phil, $pat, $red, $stack] | map(select(. != "" and . != null)) | join("\n")' 2>/dev/null || echo '""')
880
+
881
+ local combined_uprefs
882
+ combined_uprefs=$(jq -n \
883
+ --arg dec "$decrees" \
884
+ --arg up "$user_prefs" \
885
+ '[$dec, $up] | map(select(. != "" and . != null)) | join("\n")' 2>/dev/null || echo '""')
886
+
887
+ echo "{\"user_prefs\":$combined_uprefs,\"codebase_patterns\":$combined_codebase,\"build_learnings\":\"\",\"instincts\":\"\"}"
888
+ fi
889
+ }
890
+
891
+ # Detect if global and local QUEEN.md point to the same file (e.g., HOME == AETHER_ROOT in tests)
892
+ # In that case, treat as local only to avoid double-loading the same content
893
+ cp_same_queen=false
894
+ if [[ -f "$cp_global_queen" && -f "$cp_local_queen" ]]; then
895
+ cp_global_real=$(cd "$(dirname "$cp_global_queen")" && pwd)/$(basename "$cp_global_queen") 2>/dev/null || true # SUPPRESS:OK -- read-default: path resolution
896
+ cp_local_real=$(cd "$(dirname "$cp_local_queen")" && pwd)/$(basename "$cp_local_queen") 2>/dev/null || true # SUPPRESS:OK -- read-default: path resolution
897
+ if [[ "$cp_global_real" == "$cp_local_real" ]]; then
898
+ cp_same_queen=true
899
+ fi
900
+ fi
901
+
902
+ # Load global QUEEN.md first (~/.aether/QUEEN.md)
903
+ # Skip if same file as local (will be loaded as local instead)
904
+ if [[ -f "$cp_global_queen" && "$cp_same_queen" == "false" ]]; then
905
+ cp_has_global=true
906
+ # Auto-migrate global QUEEN.md from v1 to v2 if needed (Phase 20)
907
+ if ! grep -q '^## Build Learnings$' "$cp_global_queen" 2>/dev/null; then # SUPPRESS:OK -- existence-test: format detection
908
+ "$SCRIPT_DIR/aether-utils.sh" queen-migrate --target hub 2>/dev/null || true # SUPPRESS:OK -- cleanup: migration is best-effort
909
+ fi
910
+ cp_global_wisdom=$(_extract_wisdom "$cp_global_queen" "g")
911
+ fi
912
+
913
+ # Load local QUEEN.md second (.aether/QUEEN.md)
914
+ if [[ -f "$cp_local_queen" ]]; then
915
+ cp_has_local=true
916
+ # Auto-migrate local QUEEN.md if same as global and was v1 (edge case: HOME == AETHER_ROOT)
917
+ if [[ "$cp_same_queen" == "true" ]] && ! grep -q '^## Build Learnings$' "$cp_local_queen" 2>/dev/null; then # SUPPRESS:OK -- existence-test: format detection
918
+ "$SCRIPT_DIR/aether-utils.sh" queen-migrate --target local 2>/dev/null || true # SUPPRESS:OK -- cleanup: migration is best-effort
919
+ fi
920
+ cp_local_wisdom=$(_extract_wisdom "$cp_local_queen" "l")
921
+ fi
922
+
923
+ # FAIL HARD if no QUEEN.md found at all
924
+ if [[ "$cp_has_global" == "false" && "$cp_has_local" == "false" ]]; then
925
+ json_err "$E_FILE_NOT_FOUND" \
926
+ "QUEEN.md not found in either ~/.aether/QUEEN.md or .aether/QUEEN.md. Run /ant:init to create a colony." \
927
+ '{"global_path":"~/.aether/QUEEN.md","local_path":".aether/QUEEN.md"}'
928
+ exit 1
929
+ fi
930
+
931
+ # Process global and local wisdom independently (Phase 20: split sections)
932
+ # --- GLOBAL wisdom extraction ---
933
+ cp_global_codebase_raw=$(echo "$cp_global_wisdom" | jq -r '.codebase_patterns // ""' 2>/dev/null) # SUPPRESS:OK -- read-default: may be empty
934
+ cp_global_instincts_raw=$(echo "$cp_global_wisdom" | jq -r '.instincts // ""' 2>/dev/null) # SUPPRESS:OK -- read-default: may be empty
935
+ cp_global_prefs_raw=$(echo "$cp_global_wisdom" | jq -r '.user_prefs // ""' 2>/dev/null) # SUPPRESS:OK -- read-default: may be empty
936
+
937
+ # --- LOCAL wisdom extraction ---
938
+ cp_local_codebase_raw=$(echo "$cp_local_wisdom" | jq -r '.codebase_patterns // ""' 2>/dev/null) # SUPPRESS:OK -- read-default: may be empty
939
+ cp_local_learnings_raw=$(echo "$cp_local_wisdom" | jq -r '.build_learnings // ""' 2>/dev/null) # SUPPRESS:OK -- read-default: may be empty
940
+ cp_local_instincts_raw=$(echo "$cp_local_wisdom" | jq -r '.instincts // ""' 2>/dev/null) # SUPPRESS:OK -- read-default: may be empty
941
+ cp_local_prefs_raw=$(echo "$cp_local_wisdom" | jq -r '.user_prefs // ""' 2>/dev/null) # SUPPRESS:OK -- read-default: may be empty
942
+
943
+ # --- Filter entries independently ---
944
+ cp_global_codebase=$(_filter_wisdom_entries "$cp_global_codebase_raw")
945
+ cp_global_instincts=$(_filter_wisdom_entries "$cp_global_instincts_raw")
946
+ cp_local_codebase=$(_filter_wisdom_entries "$cp_local_codebase_raw")
947
+ cp_local_learnings=$(_filter_wisdom_entries "$cp_local_learnings_raw")
948
+ cp_local_instincts=$(_filter_wisdom_entries "$cp_local_instincts_raw")
949
+
950
+ # Get metadata from local QUEEN.md if exists, otherwise global
951
+ cp_metadata='{"version":"unknown","last_evolved":null,"source":"none"}'
952
+ if [[ "$cp_has_local" == "true" ]]; then
953
+ cp_metadata=$(sed -n '/<!-- METADATA/,/-->/p' "$cp_local_queen" | sed '1d;$d' | tr -d '\n' | sed 's/^[[:space:]]*//')
954
+ if [[ -n "$cp_metadata" ]] && echo "$cp_metadata" | jq -e . >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON validity
955
+ # SUPPRESS:OK -- read-default: returns fallback on failure
956
+ cp_metadata=$(echo "$cp_metadata" | jq '. + {"source":"local"}' 2>/dev/null || echo "$cp_metadata")
957
+ else
958
+ cp_metadata='{"version":"unknown","last_evolved":null,"source":"local","note":"malformed"}'
959
+ fi
960
+ elif [[ "$cp_has_global" == "true" ]]; then
961
+ cp_metadata=$(sed -n '/<!-- METADATA/,/-->/p' "$cp_global_queen" | sed '1d;$d' | tr -d '\n' | sed 's/^[[:space:]]*//')
962
+ if [[ -n "$cp_metadata" ]] && echo "$cp_metadata" | jq -e . >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON validity
963
+ # SUPPRESS:OK -- read-default: returns fallback on failure
964
+ cp_metadata=$(echo "$cp_metadata" | jq '. + {"source":"global"}' 2>/dev/null || echo "$cp_metadata")
965
+ else
966
+ cp_metadata='{"version":"unknown","last_evolved":null,"source":"global","note":"malformed"}'
967
+ fi
968
+ fi
969
+
970
+ # Now get signals + instincts via pheromone-prime
971
+ # Trap error: if pheromones.json missing, warn but continue
972
+ # Call pheromone-prime by re-invoking the script (it's a case branch, not a function)
973
+ cp_signals_json='{"signal_count":0,"instinct_count":0,"prompt_section":"","log_line":"Primed: no pheromones (file missing)"}'
974
+ cp_pher_warn=""
975
+ if [[ -f "$COLONY_DATA_DIR/pheromones.json" ]]; then
976
+ if [[ "$cp_compact" == "true" ]]; then
977
+ # SUPPRESS:OK -- read-default: subcommand call returns fallback on failure
978
+ cp_signals_raw=$("$SCRIPT_DIR/aether-utils.sh" pheromone-prime --compact --max-signals 8 --max-instincts 3 2>/dev/null) || cp_signals_raw=""
979
+ else
980
+ cp_signals_raw=$("$SCRIPT_DIR/aether-utils.sh" pheromone-prime 2>/dev/null) || cp_signals_raw="" # SUPPRESS:OK -- read-default: subcommand may fail
981
+ fi
982
+ # SUPPRESS:OK -- read-default: query may return empty
983
+ cp_signals_json=$(echo "$cp_signals_raw" | jq -c '.result // {"signal_count":0,"instinct_count":0,"prompt_section":"","log_line":"Primed: 0 signals, 0 instincts"}' 2>/dev/null || echo '{"signal_count":0,"instinct_count":0,"prompt_section":"","log_line":"Primed: 0 signals, 0 instincts"}')
984
+ else
985
+ cp_pher_warn="WARNING: pheromones.json not found - continuing without signals"
986
+ fi
987
+
988
+ # Extract components from pheromone-prime output
989
+ cp_signal_count=$(echo "$cp_signals_json" | jq -r '.signal_count // 0' 2>/dev/null || echo "0") # SUPPRESS:OK -- read-default: file may not exist yet
990
+ cp_instinct_count=$(echo "$cp_signals_json" | jq -r '.instinct_count // 0' 2>/dev/null || echo "0") # SUPPRESS:OK -- read-default: file may not exist yet
991
+ cp_prompt_section=$(echo "$cp_signals_json" | jq -r '.prompt_section // ""' 2>/dev/null || echo "") # SUPPRESS:OK -- read-default: file may not exist yet
992
+ # SUPPRESS:OK -- read-default: query may return empty
993
+ cp_log_line=$(echo "$cp_signals_json" | jq -r '.log_line // "Primed: 0 signals, 0 instincts"' 2>/dev/null || echo "Primed: 0 signals, 0 instincts")
994
+
995
+ # Append warning if pheromones missing
996
+ if [[ -n "$cp_pher_warn" ]]; then
997
+ cp_log_line="$cp_log_line; $cp_pher_warn"
998
+ fi
999
+
1000
+ # Build prompt_section that combines wisdom + signals
1001
+ # Each section is stored separately for budget enforcement
1002
+ cp_final_prompt=""
1003
+ cp_sec_queen_global=""
1004
+ cp_sec_queen_local=""
1005
+ cp_sec_user_prefs=""
1006
+ cp_sec_hive=""
1007
+ cp_sec_capsule=""
1008
+ cp_sec_learnings=""
1009
+ cp_sec_decisions=""
1010
+ cp_sec_blockers=""
1011
+ cp_sec_rolling=""
1012
+ cp_sec_signals=""
1013
+
1014
+ # Build GLOBAL QUEEN WISDOM section (only if real filtered content exists)
1015
+ if [[ -n "$cp_global_codebase" || -n "$cp_global_instincts" ]]; then
1016
+ cp_sec_queen_global+="--- QUEEN WISDOM (Global -- All Colonies) ---"$'\n'
1017
+
1018
+ if [[ -n "$cp_global_codebase" ]]; then
1019
+ cp_sec_queen_global+=$'\n'"Codebase Patterns:"$'\n'"$cp_global_codebase"$'\n'
1020
+ fi
1021
+ if [[ -n "$cp_global_instincts" ]]; then
1022
+ cp_sec_queen_global+=$'\n'"Instincts:"$'\n'"$cp_global_instincts"$'\n'
1023
+ fi
1024
+
1025
+ cp_sec_queen_global+=$'\n'"--- END QUEEN WISDOM (Global) ---"$'\n'
1026
+ fi
1027
+
1028
+ # Build LOCAL (Colony-Specific) QUEEN WISDOM section
1029
+ if [[ -n "$cp_local_codebase" || -n "$cp_local_learnings" || -n "$cp_local_instincts" ]]; then
1030
+ cp_sec_queen_local+="--- QUEEN WISDOM (Colony-Specific) ---"$'\n'
1031
+
1032
+ if [[ -n "$cp_local_codebase" ]]; then
1033
+ cp_sec_queen_local+=$'\n'"Codebase Patterns:"$'\n'"$cp_local_codebase"$'\n'
1034
+ fi
1035
+ if [[ -n "$cp_local_learnings" ]]; then
1036
+ cp_sec_queen_local+=$'\n'"Build Learnings:"$'\n'"$cp_local_learnings"$'\n'
1037
+ fi
1038
+ if [[ -n "$cp_local_instincts" ]]; then
1039
+ cp_sec_queen_local+=$'\n'"Instincts:"$'\n'"$cp_local_instincts"$'\n'
1040
+ fi
1041
+
1042
+ cp_sec_queen_local+=$'\n'"--- END QUEEN WISDOM (Colony-Specific) ---"$'\n'
1043
+ fi
1044
+
1045
+ # Build USER PREFERENCES section with source labels (Phase 20)
1046
+ cp_sec_user_prefs=""
1047
+ cp_user_prefs_count=0
1048
+
1049
+ # Label global prefs with [global] prefix
1050
+ cp_global_prefs_labeled=""
1051
+ if [[ -n "$cp_global_prefs_raw" && "$cp_global_prefs_raw" != "null" ]]; then
1052
+ cp_global_prefs_labeled=$(echo "$cp_global_prefs_raw" | grep '^- ' | sed 's/^- /- [global] /' || true) # SUPPRESS:OK -- grep returns 1 on no matches
1053
+ fi
1054
+
1055
+ # Label local prefs with [local] prefix
1056
+ cp_local_prefs_labeled=""
1057
+ if [[ -n "$cp_local_prefs_raw" && "$cp_local_prefs_raw" != "null" ]]; then
1058
+ cp_local_prefs_labeled=$(echo "$cp_local_prefs_raw" | grep '^- ' | sed 's/^- /- [local] /' || true) # SUPPRESS:OK -- grep returns 1 on no matches
1059
+ fi
1060
+
1061
+ # Combine labeled prefs
1062
+ cp_all_prefs=""
1063
+ [[ -n "$cp_global_prefs_labeled" ]] && cp_all_prefs+="$cp_global_prefs_labeled"$'\n'
1064
+ [[ -n "$cp_local_prefs_labeled" ]] && cp_all_prefs+="$cp_local_prefs_labeled"$'\n'
1065
+
1066
+ if [[ -n "$cp_all_prefs" ]]; then
1067
+ cp_user_prefs_count=$(echo "$cp_all_prefs" | grep -c '^- ' || echo "0") # SUPPRESS:OK -- read-default: grep returns 1 when no matches
1068
+ if [[ "$cp_user_prefs_count" -gt 0 ]]; then
1069
+ cp_sec_user_prefs=$'\n'"--- USER PREFERENCES ---"$'\n'
1070
+ cp_sec_user_prefs+="$cp_all_prefs"
1071
+ cp_sec_user_prefs+="--- END USER PREFERENCES ---"$'\n'
1072
+ cp_log_line="$cp_log_line, $cp_user_prefs_count user_prefs"
1073
+ fi
1074
+ fi
1075
+
1076
+ # === Hive-wisdom injection (HIVE-01) ===
1077
+ # Primary: use hive-read with domain tags from registry for scoped wisdom
1078
+ # Fallback: read high_value_signals from ~/.aether/eternal/memory.json
1079
+ cp_hive_count=0
1080
+ cp_sec_hive=""
1081
+ cp_hive_source=""
1082
+
1083
+ cp_max_hive=5
1084
+ if [[ "$cp_compact" == "true" ]]; then
1085
+ cp_max_hive=3
1086
+ fi
1087
+
1088
+ # Get domain tags for current repo from registry
1089
+ cp_repo_path="${AETHER_ROOT:-$(pwd)}"
1090
+ # SUPPRESS:OK -- read-default: file may not exist yet
1091
+ cp_domain_tags=$(jq -r --arg repo "$cp_repo_path" \
1092
+ '[.repos[] | select(.path == $repo) | .domain_tags // []] | .[0] // [] | join(",")' \
1093
+ "$HOME/.aether/registry.json" 2>/dev/null || echo "") # SUPPRESS:OK -- read-default: returns fallback on failure
1094
+
1095
+ # Try hive-read first (domain-scoped retrieval from ~/.aether/hive/wisdom.json)
1096
+ cp_hive_result=""
1097
+ if [[ -n "$cp_domain_tags" ]]; then
1098
+ # SUPPRESS:OK -- read-default: subcommand call returns fallback on failure
1099
+ cp_hive_result=$(bash "$SCRIPT_DIR/aether-utils.sh" hive-read --domain "$cp_domain_tags" --limit "$cp_max_hive" --format text 2>/dev/null) || cp_hive_result=""
1100
+ else
1101
+ # SUPPRESS:OK -- read-default: subcommand call returns fallback on failure
1102
+ cp_hive_result=$(bash "$SCRIPT_DIR/aether-utils.sh" hive-read --limit "$cp_max_hive" --format text 2>/dev/null) || cp_hive_result=""
1103
+ fi
1104
+
1105
+ cp_hive_matched=0
1106
+ if [[ -n "$cp_hive_result" ]]; then
1107
+ # SUPPRESS:OK -- read-default: query may return empty
1108
+ cp_hive_matched=$(echo "$cp_hive_result" | jq -r '.result.total_matched // 0' 2>/dev/null || echo "0")
1109
+ fi
1110
+
1111
+ if [[ "$cp_hive_matched" -gt 0 ]]; then
1112
+ # Use hive-read text output
1113
+ cp_hive_text=$(echo "$cp_hive_result" | jq -r '.result.text // ""' 2>/dev/null || echo "") # SUPPRESS:OK -- read-default: file may not exist yet
1114
+ cp_hive_count="$cp_hive_matched"
1115
+ if [[ "$cp_hive_count" -gt "$cp_max_hive" ]]; then
1116
+ cp_hive_count="$cp_max_hive"
1117
+ fi
1118
+ cp_hive_source="hive"
1119
+
1120
+ # Build header with domain info
1121
+ if [[ -n "$cp_domain_tags" ]]; then
1122
+ cp_domain_display=$(echo "$cp_domain_tags" | tr ',' ', ')
1123
+ cp_hive_section="--- HIVE WISDOM (Domain: $cp_domain_display) ---"$'\n'
1124
+ else
1125
+ cp_hive_section="--- HIVE WISDOM (All Domains) ---"$'\n'
1126
+ fi
1127
+
1128
+ # Add hive-read text lines
1129
+ if [[ -n "$cp_hive_text" && "$cp_hive_text" != "(no wisdom entries)" ]]; then
1130
+ while IFS= read -r cp_hive_line; do
1131
+ [[ -n "$cp_hive_line" ]] && cp_hive_section+="- $cp_hive_line"$'\n'
1132
+ done <<< "$cp_hive_text"
1133
+ fi
1134
+
1135
+ cp_hive_section+="--- END HIVE WISDOM ---"
1136
+ cp_sec_hive=$'\n'"$cp_hive_section"$'\n'
1137
+ cp_log_line="$cp_log_line, $cp_hive_count hive"
1138
+ else
1139
+ # Fallback: read from eternal memory (legacy)
1140
+ cp_hive_file="$HOME/.aether/eternal/memory.json"
1141
+ if [[ -f "$cp_hive_file" ]]; then
1142
+ cp_hive_signals=$(jq -r \
1143
+ --argjson max "$cp_max_hive" \
1144
+ '
1145
+ .high_value_signals // []
1146
+ | .[:$max]
1147
+ ' "$cp_hive_file" 2>/dev/null || echo "[]") # SUPPRESS:OK -- read-default: file may not exist yet
1148
+
1149
+ cp_hive_count=$(echo "$cp_hive_signals" | jq 'length' 2>/dev/null || echo "0") # SUPPRESS:OK -- read-default: file may not exist yet
1150
+
1151
+ if [[ "$cp_hive_count" -gt 0 ]]; then
1152
+ cp_hive_section="--- HIVE WISDOM (Cross-Colony Patterns) ---"$'\n'
1153
+ cp_hive_source="eternal"
1154
+
1155
+ cp_hive_lines=$(echo "$cp_hive_signals" | jq -r '
1156
+ .[] | "[" + (.type // "UNKNOWN") + " | " + ((.strength // 0) | tostring) + "] " + (.content // "")
1157
+ ' 2>/dev/null || echo "") # SUPPRESS:OK -- read-default: returns fallback on failure
1158
+
1159
+ if [[ -n "$cp_hive_lines" ]]; then
1160
+ while IFS= read -r cp_hive_line; do
1161
+ [[ -n "$cp_hive_line" ]] && cp_hive_section+="- $cp_hive_line"$'\n'
1162
+ done <<< "$cp_hive_lines"
1163
+ fi
1164
+
1165
+ cp_hive_section+="--- END HIVE WISDOM ---"
1166
+
1167
+ cp_sec_hive=$'\n'"$cp_hive_section"$'\n'
1168
+ cp_log_line="$cp_log_line, $cp_hive_count hive"
1169
+ fi
1170
+ fi
1171
+ fi
1172
+ # === END hive-wisdom injection ===
1173
+
1174
+ # Add compact context capsule for low-token continuity
1175
+ cp_capsule_prompt=""
1176
+ # SUPPRESS:OK -- read-default: subcommand call returns fallback on failure
1177
+ cp_capsule_raw=$("$SCRIPT_DIR/aether-utils.sh" context-capsule --compact --json 2>/dev/null) || cp_capsule_raw=""
1178
+ # SUPPRESS:OK -- read-default: query may return empty
1179
+ cp_capsule_prompt=$(echo "$cp_capsule_raw" | jq -r '.result.prompt_section // ""' 2>/dev/null || echo "")
1180
+ if [[ -n "$cp_capsule_prompt" ]]; then
1181
+ cp_sec_capsule=$'\n'"$cp_capsule_prompt"$'\n'
1182
+ fi
1183
+
1184
+ # === Phase learnings injection ===
1185
+ # MIGRATE: direct COLONY_STATE.json access -- use _state_read_field instead
1186
+ # Extract validated learnings from previous phases in COLONY_STATE.json
1187
+ # and format as actionable guidance for builders
1188
+ cp_current_phase=$(jq -r '.current_phase // 0' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || echo "0") # SUPPRESS:OK -- read-default: file may not exist yet
1189
+
1190
+ cp_max_learnings=15
1191
+ if [[ "$cp_compact" == "true" ]]; then
1192
+ cp_max_learnings=5
1193
+ fi
1194
+
1195
+ cp_learning_claims=$(jq -r \
1196
+ --argjson current "$cp_current_phase" \
1197
+ --argjson max "$cp_max_learnings" \
1198
+ '
1199
+ [
1200
+ (.memory.phase_learnings // [])[]
1201
+ | select((.phase | type) == "string" or ((.phase | tonumber) < $current))
1202
+ | .phase as $p | .phase_name as $pn
1203
+ | .learnings[]
1204
+ | select(.status == "validated")
1205
+ | {phase: $p, phase_name: $pn, claim: .claim}
1206
+ ]
1207
+ | unique_by(.claim)
1208
+ | .[:$max]
1209
+ ' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || echo "[]") # SUPPRESS:OK -- read-default: file may not exist yet
1210
+
1211
+ cp_learning_count=$(echo "$cp_learning_claims" | jq 'length' 2>/dev/null || echo "0") # SUPPRESS:OK -- read-default: file may not exist yet
1212
+
1213
+ if [[ "$cp_learning_count" -gt 0 ]]; then
1214
+ cp_learning_section="--- PHASE LEARNINGS (Previous Phase Insights) ---"
1215
+
1216
+ cp_learning_lines=$(echo "$cp_learning_claims" | jq -r '
1217
+ group_by(.phase)
1218
+ | map({
1219
+ phase: .[0].phase,
1220
+ phase_name: .[0].phase_name,
1221
+ claims: [.[].claim]
1222
+ })
1223
+ | sort_by(if .phase == "inherited" then -1 else (.phase | tonumber) end)
1224
+ | .[]
1225
+ | "\n"
1226
+ + (if .phase == "inherited" then "Inherited"
1227
+ elif .phase_name != "" then "Phase " + (.phase | tostring) + " (" + .phase_name + ")"
1228
+ else "Phase " + (.phase | tostring)
1229
+ end)
1230
+ + ":"
1231
+ + "\n" + (.claims | map(" - " + .) | join("\n"))
1232
+ ' 2>/dev/null || echo "") # SUPPRESS:OK -- read-default: returns fallback on failure
1233
+
1234
+ if [[ -n "$cp_learning_lines" ]]; then
1235
+ cp_learning_section+="$cp_learning_lines"$'\n'
1236
+ fi
1237
+
1238
+ cp_learning_section+=$'\n'"--- END PHASE LEARNINGS ---"
1239
+
1240
+ cp_sec_learnings=$'\n'"$cp_learning_section"$'\n'
1241
+
1242
+ cp_log_line="$cp_log_line, $cp_learning_count learnings"
1243
+ fi
1244
+ # === End phase learnings injection ===
1245
+
1246
+ # === CONTEXT.md decision injection (CTX-01) ===
1247
+ # Extract key decisions from CONTEXT.md "Recent Decisions" table
1248
+ # and inject as actionable context for builders
1249
+ cp_ctx_file="$AETHER_ROOT/.aether/CONTEXT.md"
1250
+ cp_decision_count=0
1251
+
1252
+ cp_decisions=""
1253
+ if [[ -f "$cp_ctx_file" ]]; then
1254
+ cp_decisions=$(awk '
1255
+ /^## .*Recent Decisions/ { in_section=1; next }
1256
+ in_section && /^\| Date / { next }
1257
+ in_section && /^\|[-]+/ { next }
1258
+ in_section && /^---/ { exit }
1259
+ in_section && /^\| [0-9]{4}-[0-9]{2}/ {
1260
+ split($0, fields, "|")
1261
+ decision = fields[3]
1262
+ rationale = fields[4]
1263
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", decision)
1264
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", rationale)
1265
+ if (decision != "") {
1266
+ if (rationale != "" && rationale != "-") {
1267
+ print decision " (" rationale ")"
1268
+ } else {
1269
+ print decision
1270
+ }
1271
+ }
1272
+ }
1273
+ ' "$cp_ctx_file" 2>/dev/null || echo "") # SUPPRESS:OK -- read-default: file may not exist yet
1274
+ fi
1275
+
1276
+ cp_max_decisions=5
1277
+ if [[ "$cp_compact" == "true" ]]; then
1278
+ cp_max_decisions=3
1279
+ fi
1280
+
1281
+ if [[ -n "$cp_decisions" ]]; then
1282
+ cp_trimmed_decisions=$(echo "$cp_decisions" | tail -n "$cp_max_decisions")
1283
+ cp_decision_count=$(echo "$cp_trimmed_decisions" | grep -c '.' || echo "0") # SUPPRESS:OK -- read-default: grep returns 1 when no matches
1284
+
1285
+ if [[ "$cp_decision_count" -gt 0 ]]; then
1286
+ cp_decision_section="--- KEY DECISIONS (Active Decisions) ---"$'\n'
1287
+ while IFS= read -r cp_dec_line; do
1288
+ [[ -n "$cp_dec_line" ]] && cp_decision_section+="- $cp_dec_line"$'\n'
1289
+ done <<< "$cp_trimmed_decisions"
1290
+ cp_decision_section+="--- END KEY DECISIONS ---"
1291
+
1292
+ cp_sec_decisions=$'\n'"$cp_decision_section"$'\n'
1293
+ cp_log_line="$cp_log_line, $cp_decision_count decisions"
1294
+ fi
1295
+ fi
1296
+ # === END CONTEXT.md decision injection ===
1297
+
1298
+ # === Blocker flag injection (CTX-02) ===
1299
+ # Extract unresolved blocker flags for the current phase from flags.json
1300
+ # and inject as REDIRECT-priority warnings distinct from user pheromones
1301
+ cp_flags_file="$COLONY_DATA_DIR/flags.json"
1302
+ cp_blocker_count=0
1303
+
1304
+ cp_blockers=""
1305
+ if [[ -f "$cp_flags_file" ]]; then
1306
+ cp_blockers=$(jq -r \
1307
+ --argjson phase "$cp_current_phase" \
1308
+ '
1309
+ .flags
1310
+ | map(select(
1311
+ .type == "blocker"
1312
+ and .resolved_at == null
1313
+ and (.phase == $phase or .phase == null)
1314
+ ))
1315
+ | map("[source: " + (.source // "unknown") + "] " + .title + "\n " + (.description // ""))
1316
+ | .[]
1317
+ ' "$cp_flags_file" 2>/dev/null || echo "") # SUPPRESS:OK -- read-default: file may not exist yet
1318
+ fi
1319
+
1320
+ cp_max_blockers=3
1321
+ if [[ "$cp_compact" == "true" ]]; then
1322
+ cp_max_blockers=2
1323
+ fi
1324
+
1325
+ if [[ -n "$cp_blockers" ]]; then
1326
+ cp_blocker_count=$(echo "$cp_blockers" | grep -c '^\[source:' || echo "0") # SUPPRESS:OK -- read-default: grep returns 1 when no matches
1327
+
1328
+ if [[ "$cp_blocker_count" -gt 0 ]]; then
1329
+ cp_blocker_section="--- BLOCKER WARNINGS (Unresolved Build Blockers) ---"$'\n'
1330
+ cp_blocker_section+="These are critical issues that MUST be addressed. Treat as REDIRECT-priority."$'\n'
1331
+
1332
+ cp_blocker_idx=0
1333
+ while IFS= read -r cp_blk_line; do
1334
+ if [[ "$cp_blocker_idx" -ge "$cp_max_blockers" ]]; then break; fi
1335
+ if [[ "$cp_blk_line" == \[source:* ]]; then
1336
+ ((cp_blocker_idx++)) || true # SUPPRESS:OK -- cleanup: arithmetic overflow is safe
1337
+ if [[ "$cp_blocker_idx" -gt "$cp_max_blockers" ]]; then break; fi
1338
+ fi
1339
+ [[ -n "$cp_blk_line" ]] && cp_blocker_section+="$cp_blk_line"$'\n'
1340
+ done <<< "$cp_blockers"
1341
+
1342
+ cp_blocker_section+="--- END BLOCKER WARNINGS ---"
1343
+
1344
+ cp_sec_blockers=$'\n'"$cp_blocker_section"$'\n'
1345
+ cp_log_line="$cp_log_line, $cp_blocker_count blockers"
1346
+ fi
1347
+ fi
1348
+ # === END blocker flag injection ===
1349
+
1350
+ # === Rolling-summary injection (MEM-02) ===
1351
+ # Read last 5 entries directly (not via context-capsule which truncates)
1352
+ cp_roll_count=5
1353
+ cp_roll_entries=""
1354
+ if [[ -f "$COLONY_DATA_DIR/rolling-summary.log" ]]; then
1355
+ # SUPPRESS:OK -- read-default: file may not exist
1356
+ # SUPPRESS:OK -- read-default: file may not exist yet
1357
+ cp_roll_entries=$(tail -n "$cp_roll_count" "$COLONY_DATA_DIR/rolling-summary.log" 2>/dev/null | \
1358
+ awk -F'|' 'NF >= 4 {printf "- [%s] %s: %s\n", $1, $2, $4}')
1359
+ fi
1360
+
1361
+ if [[ -n "$cp_roll_entries" ]]; then
1362
+ cp_sec_rolling=$'\n'"--- RECENT ACTIVITY (Colony Narrative) ---"$'\n'
1363
+ cp_sec_rolling+="$cp_roll_entries"$'\n'
1364
+ cp_sec_rolling+="--- END RECENT ACTIVITY ---"$'\n'
1365
+
1366
+ cp_roll_actual=$(echo "$cp_roll_entries" | grep -c '.' || echo "0") # SUPPRESS:OK -- read-default: grep returns 1 when no matches
1367
+ cp_log_line="$cp_log_line, $cp_roll_actual activity entries"
1368
+ fi
1369
+ # === END rolling-summary injection ===
1370
+
1371
+ # Add pheromone signals section
1372
+ if [[ -n "$cp_prompt_section" && "$cp_prompt_section" != "null" ]]; then
1373
+ cp_sec_signals=$'\n'"$cp_prompt_section"
1374
+ fi
1375
+
1376
+ # === Budget enforcement (BUDGET-01) ===
1377
+ # Assemble cp_final_prompt from sections, respecting cp_max_chars budget.
1378
+ # Truncation priority (trim first to last):
1379
+ # rolling-summary > phase-learnings > key-decisions > hive-wisdom >
1380
+ # context-capsule > user-prefs > queen-wisdom-global > queen-wisdom-local > pheromone-signals (NEVER trim REDIRECTs)
1381
+ # Blockers are always kept (REDIRECT-priority).
1382
+
1383
+ # Assemble all sections in original order (Phase 20: split queen sections)
1384
+ cp_final_prompt="$cp_sec_queen_global$cp_sec_queen_local$cp_sec_user_prefs$cp_sec_hive$cp_sec_capsule$cp_sec_learnings$cp_sec_decisions$cp_sec_blockers$cp_sec_rolling$cp_sec_signals"
1385
+
1386
+ cp_budget_len=${#cp_final_prompt}
1387
+
1388
+ if [[ "$cp_budget_len" -gt "$cp_max_chars" ]]; then
1389
+ # Over budget -- trim sections in priority order (first = trimmed first)
1390
+ cp_budget_trimmed_list=""
1391
+
1392
+ # 1. Trim rolling-summary
1393
+ if [[ "$cp_budget_len" -gt "$cp_max_chars" && -n "$cp_sec_rolling" ]]; then
1394
+ cp_sec_rolling=""
1395
+ cp_budget_trimmed_list="rolling-summary"
1396
+ cp_final_prompt="$cp_sec_queen_global$cp_sec_queen_local$cp_sec_user_prefs$cp_sec_hive$cp_sec_capsule$cp_sec_learnings$cp_sec_decisions$cp_sec_blockers$cp_sec_rolling$cp_sec_signals"
1397
+ cp_budget_len=${#cp_final_prompt}
1398
+ fi
1399
+
1400
+ # 2. Trim phase-learnings
1401
+ if [[ "$cp_budget_len" -gt "$cp_max_chars" && -n "$cp_sec_learnings" ]]; then
1402
+ cp_sec_learnings=""
1403
+ cp_budget_trimmed_list="${cp_budget_trimmed_list:+$cp_budget_trimmed_list,}phase-learnings"
1404
+ cp_final_prompt="$cp_sec_queen_global$cp_sec_queen_local$cp_sec_user_prefs$cp_sec_hive$cp_sec_capsule$cp_sec_learnings$cp_sec_decisions$cp_sec_blockers$cp_sec_rolling$cp_sec_signals"
1405
+ cp_budget_len=${#cp_final_prompt}
1406
+ fi
1407
+
1408
+ # 3. Trim key-decisions
1409
+ if [[ "$cp_budget_len" -gt "$cp_max_chars" && -n "$cp_sec_decisions" ]]; then
1410
+ cp_sec_decisions=""
1411
+ cp_budget_trimmed_list="${cp_budget_trimmed_list:+$cp_budget_trimmed_list,}key-decisions"
1412
+ cp_final_prompt="$cp_sec_queen_global$cp_sec_queen_local$cp_sec_user_prefs$cp_sec_hive$cp_sec_capsule$cp_sec_learnings$cp_sec_decisions$cp_sec_blockers$cp_sec_rolling$cp_sec_signals"
1413
+ cp_budget_len=${#cp_final_prompt}
1414
+ fi
1415
+
1416
+ # 4. Trim hive-wisdom
1417
+ if [[ "$cp_budget_len" -gt "$cp_max_chars" && -n "$cp_sec_hive" ]]; then
1418
+ cp_sec_hive=""
1419
+ cp_budget_trimmed_list="${cp_budget_trimmed_list:+$cp_budget_trimmed_list,}hive-wisdom"
1420
+ cp_final_prompt="$cp_sec_queen_global$cp_sec_queen_local$cp_sec_user_prefs$cp_sec_hive$cp_sec_capsule$cp_sec_learnings$cp_sec_decisions$cp_sec_blockers$cp_sec_rolling$cp_sec_signals"
1421
+ cp_budget_len=${#cp_final_prompt}
1422
+ fi
1423
+
1424
+ # 5. Trim context-capsule
1425
+ if [[ "$cp_budget_len" -gt "$cp_max_chars" && -n "$cp_sec_capsule" ]]; then
1426
+ cp_sec_capsule=""
1427
+ cp_budget_trimmed_list="${cp_budget_trimmed_list:+$cp_budget_trimmed_list,}context-capsule"
1428
+ cp_final_prompt="$cp_sec_queen_global$cp_sec_queen_local$cp_sec_user_prefs$cp_sec_hive$cp_sec_capsule$cp_sec_learnings$cp_sec_decisions$cp_sec_blockers$cp_sec_rolling$cp_sec_signals"
1429
+ cp_budget_len=${#cp_final_prompt}
1430
+ fi
1431
+
1432
+ # 6. Trim user-prefs
1433
+ if [[ "$cp_budget_len" -gt "$cp_max_chars" && -n "$cp_sec_user_prefs" ]]; then
1434
+ cp_sec_user_prefs=""
1435
+ cp_budget_trimmed_list="${cp_budget_trimmed_list:+$cp_budget_trimmed_list,}user-prefs"
1436
+ cp_final_prompt="$cp_sec_queen_global$cp_sec_queen_local$cp_sec_user_prefs$cp_sec_hive$cp_sec_capsule$cp_sec_learnings$cp_sec_decisions$cp_sec_blockers$cp_sec_rolling$cp_sec_signals"
1437
+ cp_budget_len=${#cp_final_prompt}
1438
+ fi
1439
+
1440
+ # 7. Trim queen-wisdom-global (trim global before local -- local is more relevant)
1441
+ if [[ "$cp_budget_len" -gt "$cp_max_chars" && -n "$cp_sec_queen_global" ]]; then
1442
+ cp_sec_queen_global=""
1443
+ cp_budget_trimmed_list="${cp_budget_trimmed_list:+$cp_budget_trimmed_list,}queen-wisdom-global"
1444
+ cp_final_prompt="$cp_sec_queen_global$cp_sec_queen_local$cp_sec_user_prefs$cp_sec_hive$cp_sec_capsule$cp_sec_learnings$cp_sec_decisions$cp_sec_blockers$cp_sec_rolling$cp_sec_signals"
1445
+ cp_budget_len=${#cp_final_prompt}
1446
+ fi
1447
+
1448
+ # 8. Trim queen-wisdom-local
1449
+ if [[ "$cp_budget_len" -gt "$cp_max_chars" && -n "$cp_sec_queen_local" ]]; then
1450
+ cp_sec_queen_local=""
1451
+ cp_budget_trimmed_list="${cp_budget_trimmed_list:+$cp_budget_trimmed_list,}queen-wisdom-local"
1452
+ cp_final_prompt="$cp_sec_queen_global$cp_sec_queen_local$cp_sec_user_prefs$cp_sec_hive$cp_sec_capsule$cp_sec_learnings$cp_sec_decisions$cp_sec_blockers$cp_sec_rolling$cp_sec_signals"
1453
+ cp_budget_len=${#cp_final_prompt}
1454
+ fi
1455
+
1456
+ # 9. Trim pheromone-signals (preserve REDIRECTs)
1457
+ if [[ "$cp_budget_len" -gt "$cp_max_chars" && -n "$cp_sec_signals" ]]; then
1458
+ # Extract REDIRECT lines and preserve them
1459
+ cp_redirect_preserved=""
1460
+ if [[ "$cp_sec_signals" == *"REDIRECT (HARD CONSTRAINTS"* ]]; then
1461
+ cp_redirect_lines=""
1462
+ cp_in_redirect=false
1463
+ while IFS= read -r cp_rl; do
1464
+ if [[ "$cp_rl" == *"REDIRECT (HARD CONSTRAINTS"* ]]; then
1465
+ cp_in_redirect=true
1466
+ cp_redirect_lines+="$cp_rl"$'\n'
1467
+ elif [[ "$cp_in_redirect" == "true" ]]; then
1468
+ if [[ "$cp_rl" == "FOCUS "* ]] || [[ "$cp_rl" == "FEEDBACK "* ]] || \
1469
+ [[ "$cp_rl" == "POSITION "* ]] || [[ "$cp_rl" == "--- "* ]]; then
1470
+ cp_in_redirect=false
1471
+ else
1472
+ cp_redirect_lines+="$cp_rl"$'\n'
1473
+ fi
1474
+ fi
1475
+ done <<< "$cp_sec_signals"
1476
+ if [[ -n "$cp_redirect_lines" ]]; then
1477
+ cp_redirect_preserved=$'\n'"--- ACTIVE SIGNALS (Colony Guidance) ---"$'\n'
1478
+ cp_redirect_preserved+=$'\n'"$cp_redirect_lines"
1479
+ cp_redirect_preserved+=$'\n'"--- END COLONY CONTEXT ---"
1480
+ fi
1481
+ fi
1482
+ cp_sec_signals="$cp_redirect_preserved"
1483
+ cp_budget_trimmed_list="${cp_budget_trimmed_list:+$cp_budget_trimmed_list,}pheromone-signals"
1484
+ cp_final_prompt="$cp_sec_queen_global$cp_sec_queen_local$cp_sec_user_prefs$cp_sec_hive$cp_sec_capsule$cp_sec_learnings$cp_sec_decisions$cp_sec_blockers$cp_sec_rolling$cp_sec_signals"
1485
+ cp_budget_len=${#cp_final_prompt}
1486
+ fi
1487
+
1488
+ # Append truncation note to log line
1489
+ if [[ -n "$cp_budget_trimmed_list" ]]; then
1490
+ cp_log_line="$cp_log_line, truncated: $cp_budget_trimmed_list (budget: ${cp_max_chars})"
1491
+ fi
1492
+ fi
1493
+ # === END Budget enforcement ===
1494
+
1495
+ # === Budget trimming notification (REL-06) ===
1496
+ cp_trimmed_notice=""
1497
+ cp_trimmed_high_priority=false
1498
+
1499
+ if [[ -n "${cp_budget_trimmed_list:-}" ]]; then
1500
+ cp_trimmed_sections=$(echo "$cp_budget_trimmed_list" | tr ',' ', ')
1501
+
1502
+ if [[ "$cp_budget_trimmed_list" == *"key-decisions"* ]] || \
1503
+ [[ "$cp_budget_trimmed_list" == *"pheromone-signals"* ]]; then
1504
+ cp_trimmed_high_priority=true
1505
+ cp_trimmed_notice="[!trimmed] Context exceeded ${cp_max_chars}-char budget. Dropped: ${cp_trimmed_sections}. HIGH-PRIORITY items were trimmed -- key decisions or redirect signals may be missing."
1506
+ echo "[!trimmed] Colony context exceeded budget. High-priority sections dropped: $cp_trimmed_sections" >&2
1507
+ else
1508
+ cp_trimmed_notice="[trimmed] Context exceeded ${cp_max_chars}-char budget. Dropped: ${cp_trimmed_sections}."
1509
+ echo "[trimmed] Colony context exceeded budget. Dropped: $cp_trimmed_sections" >&2
1510
+ fi
1511
+ fi
1512
+ # === END Budget trimming notification ===
1513
+
1514
+ # Escape for JSON
1515
+ cp_prompt_json=$(printf '%s' "$cp_final_prompt" | jq -Rs '.' 2>/dev/null || echo '""') # SUPPRESS:OK -- read-default: returns fallback if missing
1516
+ # SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
1517
+ cp_log_json=$(printf '%s' "$cp_log_line" | jq -Rs '.' 2>/dev/null || echo '"Primed: 0 signals, 0 instincts"')
1518
+
1519
+ # Build final unified output (Phase 20: split global/local wisdom)
1520
+ cp_result=$(jq -n \
1521
+ --argjson meta "$cp_metadata" \
1522
+ --argjson wisdom_global "$cp_global_wisdom" \
1523
+ --argjson wisdom_local "$cp_local_wisdom" \
1524
+ --argjson signals "$cp_signals_json" \
1525
+ --arg prompt "$cp_final_prompt" \
1526
+ --arg prompt_json "$cp_prompt_json" \
1527
+ --arg log "$cp_log_line" \
1528
+ --arg log_json "$cp_log_json" \
1529
+ --arg trimmed_notice "$cp_trimmed_notice" \
1530
+ --argjson trimmed_high_priority "${cp_trimmed_high_priority:-false}" \
1531
+ '{
1532
+ metadata: $meta,
1533
+ wisdom: { global: $wisdom_global, local: $wisdom_local },
1534
+ signals: {
1535
+ signal_count: ($signals.signal_count // 0),
1536
+ instinct_count: ($signals.instinct_count // 0),
1537
+ active_signals: ($signals.prompt_section // "")
1538
+ },
1539
+ prompt_section: $prompt,
1540
+ log_line: $log,
1541
+ trimmed_notice: $trimmed_notice,
1542
+ trimmed_high_priority: $trimmed_high_priority
1543
+ }')
1544
+
1545
+ # Validate result
1546
+ if [[ -z "$cp_result" ]] || ! echo "$cp_result" | jq -e . >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON validity
1547
+ json_err "$E_JSON_INVALID" \
1548
+ "Couldn't assemble colony-prime output" \
1549
+ '{"error":"assembly_failed"}'
1550
+ fi
1551
+
1552
+ json_ok "$cp_result"
1553
+ }
1554
+
1555
+ # ============================================================================
1556
+ # _pheromone_expire
1557
+ # Archive expired pheromone signals to midden
1558
+ # ============================================================================
1559
+ _pheromone_expire() {
1560
+ # Archive expired pheromone signals to midden
1561
+ # Usage: pheromone-expire [--phase-end-only]
1562
+ #
1563
+ # Two modes:
1564
+ # --phase-end-only Only expire signals where expires_at == "phase_end"
1565
+ # (no flag) Expire signals where expires_at is an ISO-8601 timestamp
1566
+ # <= now, AND signals where effective_strength < 0.1
1567
+
1568
+ phe_phase_end_only="false"
1569
+ while [[ $# -gt 0 ]]; do
1570
+ case "$1" in
1571
+ --phase-end-only) phe_phase_end_only="true"; shift ;;
1572
+ *) shift ;;
1573
+ esac
1574
+ done
1575
+
1576
+ phe_pheromones_file="$COLONY_DATA_DIR/pheromones.json"
1577
+ phe_midden_dir="$COLONY_DATA_DIR/midden"
1578
+ phe_midden_file="$phe_midden_dir/midden.json"
1579
+
1580
+ # Handle missing pheromones.json gracefully
1581
+ if [[ ! -f "$phe_pheromones_file" ]]; then
1582
+ json_ok '{"expired_count":0,"remaining_active":0,"midden_total":0}'
1583
+ exit 0
1584
+ fi
1585
+
1586
+ # Ensure midden directory and file exist
1587
+ mkdir -p "$phe_midden_dir"
1588
+ if [[ ! -f "$phe_midden_file" ]]; then
1589
+ atomic_write "$phe_midden_file" '{"version":"1.0.0","archived_at_count":0,"signals":[]}' || {
1590
+ _aether_log_error "Could not initialize midden archive file"
1591
+ json_err "$E_UNKNOWN" "Failed to create midden archive file"
1592
+ }
1593
+ fi
1594
+
1595
+ phe_now_iso=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1596
+ phe_archived_at="$phe_now_iso"
1597
+
1598
+ # MIGRATE: direct COLONY_STATE.json access -- use _state_read_field instead
1599
+ # Compute pause_duration from COLONY_STATE.json (pause-aware TTL)
1600
+ phe_pause_duration=0
1601
+ if [[ -f "$DATA_DIR/COLONY_STATE.json" ]]; then
1602
+ phe_paused_at=$(jq -r '.paused_at // empty' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || true) # SUPPRESS:OK -- read-default: file may not exist yet
1603
+ phe_resumed_at=$(jq -r '.resumed_at // empty' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || true) # SUPPRESS:OK -- read-default: file may not exist yet
1604
+ if [[ -n "$phe_paused_at" && -n "$phe_resumed_at" ]]; then
1605
+ # SUPPRESS:OK -- cross-platform: macOS vs Linux date/stat flags
1606
+ phe_paused_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$phe_paused_at" +%s 2>/dev/null || date -d "$phe_paused_at" +%s 2>/dev/null || echo 0)
1607
+ # SUPPRESS:OK -- cross-platform: macOS vs Linux date/stat flags
1608
+ phe_resumed_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$phe_resumed_at" +%s 2>/dev/null || date -d "$phe_resumed_at" +%s 2>/dev/null || echo 0)
1609
+ if [[ "$phe_resumed_epoch" -gt "$phe_paused_epoch" ]]; then
1610
+ phe_pause_duration=$(( phe_resumed_epoch - phe_paused_epoch ))
1611
+ fi
1612
+ fi
1613
+ fi
1614
+
1615
+ # Identify expired signal IDs
1616
+ # We'll use jq to find signals to expire, then update in bash
1617
+ if [[ "$phe_phase_end_only" == "true" ]]; then
1618
+ # Only expire signals where expires_at == "phase_end"
1619
+ # SUPPRESS:OK -- read-default: query may return empty
1620
+ phe_expired_ids=$(jq -r '.signals[] | select(.active == true and .expires_at == "phase_end") | .id' "$phe_pheromones_file" 2>/dev/null || true)
1621
+ else
1622
+ # Expire time-based expired signals (pause-aware) AND decay-expired signals
1623
+ phe_expired_ids=$(jq -r --arg now_iso "$phe_now_iso" --argjson pause_secs "$phe_pause_duration" '
1624
+ def to_epoch(ts):
1625
+ if ts == null or ts == "" or ts == "phase_end" then null
1626
+ else
1627
+ (ts | split("T")) as $parts |
1628
+ ($parts[0] | split("-")) as $d |
1629
+ ($parts[1] | rtrimstr("Z") | split(":")) as $t |
1630
+ (($d[0] | tonumber) - 1970) * 365 * 86400 +
1631
+ (($d[1] | tonumber) - 1) * 30 * 86400 +
1632
+ (($d[2] | tonumber) - 1) * 86400 +
1633
+ ($t[0] | tonumber) * 3600 +
1634
+ ($t[1] | tonumber) * 60 +
1635
+ ($t[2] | rtrimstr("Z") | tonumber)
1636
+ end;
1637
+ (to_epoch($now_iso)) as $now |
1638
+ .signals[] |
1639
+ select(.active == true) |
1640
+ select(
1641
+ (.expires_at != "phase_end" and .expires_at != null and .expires_at != "") and
1642
+ (
1643
+ (to_epoch(.expires_at)) + $pause_secs <= $now
1644
+ )
1645
+ ) |
1646
+ .id
1647
+ ' "$phe_pheromones_file" 2>/dev/null || true) # SUPPRESS:OK -- read-default: file may not exist yet
1648
+ fi
1649
+
1650
+ # Count expired signals
1651
+ phe_expired_count=0
1652
+ if [[ -n "$phe_expired_ids" ]]; then
1653
+ phe_expired_count=$(echo "$phe_expired_ids" | grep -c . 2>/dev/null || echo 0) # SUPPRESS:OK -- read-default: count defaults to 0 if file missing
1654
+ fi
1655
+
1656
+ # If nothing to expire, return counts
1657
+ if [[ "$phe_expired_count" -eq 0 ]]; then
1658
+ # SUPPRESS:OK -- read-default: query may return empty
1659
+ phe_remaining=$(jq '[.signals[] | select(.active == true)] | length' "$phe_pheromones_file" 2>/dev/null || echo 0)
1660
+ phe_midden_total=$(jq '.signals | length' "$phe_midden_file" 2>/dev/null || echo 0) # SUPPRESS:OK -- read-default: file may not exist yet
1661
+ json_ok "{\"expired_count\":0,\"remaining_active\":$phe_remaining,\"midden_total\":$phe_midden_total}"
1662
+ exit 0
1663
+ fi
1664
+
1665
+ # Build jq args for IDs to expire
1666
+ phe_id_array=$(echo "$phe_expired_ids" | jq -R . | jq -s . 2>/dev/null || echo '[]') # SUPPRESS:OK -- read-default: returns fallback if missing
1667
+
1668
+ # Extract expired signal objects (with archived_at added)
1669
+ phe_expired_objects=$(jq --argjson ids "$phe_id_array" --arg archived_at "$phe_archived_at" '
1670
+ [.signals[] | select(.id as $id | $ids | any(. == $id)) | . + {"archived_at": $archived_at, "active": false}]
1671
+ ' "$phe_pheromones_file" 2>/dev/null || echo '[]') # SUPPRESS:OK -- read-default: file may not exist yet
1672
+
1673
+ # Promote high-value expired signals to eternal memory before archival.
1674
+ # Use decayed effective_strength (not raw .strength) for promotion threshold.
1675
+ phe_eternal_promoted=0
1676
+ while IFS= read -r phe_signal; do
1677
+ [[ -z "$phe_signal" ]] && continue
1678
+ phe_strength_int=$(echo "$phe_signal" | jq -r --arg now_iso "$phe_now_iso" '
1679
+ def to_epoch(ts):
1680
+ if ts == null or ts == "" or ts == "phase_end" then null
1681
+ else
1682
+ (ts | split("T")) as $parts |
1683
+ ($parts[0] | split("-")) as $d |
1684
+ ($parts[1] | rtrimstr("Z") | split(":")) as $t |
1685
+ (($d[0] | tonumber) - 1970) * 365 * 86400 +
1686
+ (($d[1] | tonumber) - 1) * 30 * 86400 +
1687
+ (($d[2] | tonumber) - 1) * 86400 +
1688
+ ($t[0] | tonumber) * 3600 +
1689
+ ($t[1] | tonumber) * 60 +
1690
+ ($t[2] | rtrimstr("Z") | tonumber)
1691
+ end;
1692
+ def decay_days(t):
1693
+ if t == "FOCUS" then 30
1694
+ elif t == "REDIRECT" then 60
1695
+ else 90
1696
+ end;
1697
+ (to_epoch($now_iso)) as $now |
1698
+ (to_epoch(.created_at)) as $created |
1699
+ (if $created != null then ($now - $created) / 86400 else 0 end) as $elapsed |
1700
+ (decay_days(.type // "FEEDBACK")) as $dd |
1701
+ ((.strength // 0) * (1 - ($elapsed / $dd))) as $eff_raw |
1702
+ (if $eff_raw < 0 then 0 else $eff_raw end) as $eff |
1703
+ (($eff * 100) | floor)
1704
+ ' 2>/dev/null || echo "0") # SUPPRESS:OK -- read-default: returns fallback on failure
1705
+ if [[ "$phe_strength_int" -gt 80 ]]; then
1706
+ phe_text=$(sanitize_read_value "$(echo "$phe_signal" | jq -r '.content.text // ""' 2>/dev/null || echo "")") # SUPPRESS:OK -- read-default: file may not exist yet
1707
+ phe_type=$(echo "$phe_signal" | jq -r '.type // "UNKNOWN"' 2>/dev/null || echo "UNKNOWN") # SUPPRESS:OK -- read-default: file may not exist yet
1708
+ phe_source=$(echo "$phe_signal" | jq -r '.source // "unknown"' 2>/dev/null || echo "unknown") # SUPPRESS:OK -- read-default: file may not exist yet
1709
+ phe_id=$(echo "$phe_signal" | jq -r '.id // ""' 2>/dev/null || echo "") # SUPPRESS:OK -- read-default: file may not exist yet
1710
+ if [[ -n "$phe_text" ]]; then
1711
+ # SUPPRESS:OK -- cleanup: side-effect is best-effort
1712
+ if bash "$0" eternal-store "$phe_text" --type "$phe_type" --source "$phe_source" --strength "$(echo "$phe_signal" | jq -r '.strength // 0')" --signal-id "$phe_id" --reason "promoted_on_expire" >/dev/null 2>&1; then
1713
+ phe_eternal_promoted=$((phe_eternal_promoted + 1))
1714
+ fi
1715
+ fi
1716
+ fi
1717
+ done < <(echo "$phe_expired_objects" | jq -c '.[]' 2>/dev/null || true) # SUPPRESS:OK -- read-default: returns fallback if missing
1718
+
1719
+ # Update pheromones.json: set active=false for expired signals (do NOT remove them)
1720
+ local phe_updated_pheromones
1721
+ phe_updated_pheromones=$(jq --argjson ids "$phe_id_array" '
1722
+ .signals = [.signals[] | if (.id as $id | $ids | any(. == $id)) then .active = false else . end]
1723
+ ' "$phe_pheromones_file") || {
1724
+ _aether_log_error "Could not process pheromone expiration update"
1725
+ }
1726
+
1727
+ if [[ -n "$phe_updated_pheromones" && "$phe_updated_pheromones" != "null" ]]; then
1728
+ phe_lock_held=false
1729
+ if type acquire_lock &>/dev/null; then
1730
+ acquire_lock "$phe_pheromones_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on pheromones.json"
1731
+ phe_lock_held=true
1732
+ trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
1733
+ fi
1734
+ atomic_write "$phe_pheromones_file" "$phe_updated_pheromones" || {
1735
+ [[ "$phe_lock_held" == "true" ]] && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
1736
+ json_err "$E_JSON_INVALID" "Failed to write pheromones.json"
1737
+ }
1738
+ [[ "$phe_lock_held" == "true" ]] && { release_lock 2>/dev/null || true; trap - EXIT; } # SUPPRESS:OK -- cleanup: lock may not be held
1739
+ fi
1740
+
1741
+ # Append expired signals to midden.json
1742
+ local phe_midden_updated
1743
+ phe_midden_updated=$(jq --argjson new_signals "$phe_expired_objects" '
1744
+ .signals += $new_signals |
1745
+ .archived_at_count = (.signals | length)
1746
+ ' "$phe_midden_file") || {
1747
+ _aether_log_error "Could not process midden archival update"
1748
+ }
1749
+
1750
+ if [[ -n "$phe_midden_updated" && "$phe_midden_updated" != "null" ]]; then
1751
+ phe_midden_lock_held=false
1752
+ if type acquire_lock &>/dev/null; then
1753
+ acquire_lock "$phe_midden_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on midden.json"
1754
+ phe_midden_lock_held=true
1755
+ trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
1756
+ fi
1757
+ atomic_write "$phe_midden_file" "$phe_midden_updated" || {
1758
+ [[ "$phe_midden_lock_held" == "true" ]] && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
1759
+ json_err "$E_JSON_INVALID" "Failed to write midden.json"
1760
+ }
1761
+ [[ "$phe_midden_lock_held" == "true" ]] && { release_lock 2>/dev/null || true; trap - EXIT; } # SUPPRESS:OK -- cleanup: lock may not be held
1762
+ fi
1763
+
1764
+ # SUPPRESS:OK -- read-default: query may return empty
1765
+ phe_remaining_active=$(jq '[.signals[] | select(.active == true)] | length' "$phe_pheromones_file" 2>/dev/null || echo 0)
1766
+ phe_midden_total=$(jq '.signals | length' "$phe_midden_file" 2>/dev/null || echo 0) # SUPPRESS:OK -- read-default: file may not exist yet
1767
+
1768
+ json_ok "{\"expired_count\":$phe_expired_count,\"remaining_active\":$phe_remaining_active,\"midden_total\":$phe_midden_total,\"eternal_promoted\":$phe_eternal_promoted}"
1769
+ }
1770
+
1771
+ # ============================================================================
1772
+ # _eternal_init
1773
+ # Initialize the ~/.aether/eternal/ directory and memory.json schema
1774
+ # ============================================================================
1775
+ _eternal_init() {
1776
+ # Initialize the ~/.aether/eternal/ directory and memory.json schema
1777
+ # Usage: eternal-init
1778
+ # Idempotent: safe to call multiple times
1779
+
1780
+ ei_eternal_dir="$HOME/.aether/eternal"
1781
+ ei_memory_file="$ei_eternal_dir/memory.json"
1782
+ ei_already_existed="false"
1783
+
1784
+ mkdir -p "$ei_eternal_dir"
1785
+
1786
+ if [[ -f "$ei_memory_file" ]]; then
1787
+ ei_already_existed="true"
1788
+ else
1789
+ ei_created_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1790
+ local ei_init_content
1791
+ ei_init_content=$(printf '%s\n' "{
1792
+ \"version\": \"1.0.0\",
1793
+ \"created_at\": \"$ei_created_at\",
1794
+ \"colonies\": [],
1795
+ \"high_value_signals\": [],
1796
+ \"cross_session_patterns\": []
1797
+ }")
1798
+ atomic_write "$ei_memory_file" "$ei_init_content" || {
1799
+ _aether_log_error "Could not initialize eternal memory file"
1800
+ json_err "$E_UNKNOWN" "Failed to create eternal memory file"
1801
+ }
1802
+ fi
1803
+
1804
+ json_ok "$(jq -n --arg dir "$ei_eternal_dir" --argjson already_existed "$ei_already_existed" '{dir: $dir, initialized: true, already_existed: $already_existed}')"
1805
+ }
1806
+
1807
+ # ============================================================================
1808
+ # _eternal_store
1809
+ # Store a high-value signal in eternal memory.
1810
+ # ============================================================================
1811
+ _eternal_store() {
1812
+ # Store a high-value signal in eternal memory.
1813
+ # Usage: eternal-store <content> [--type TYPE] [--source SOURCE] [--strength N] [--signal-id ID] [--reason TEXT] [--created-at ISO8601] [--archived-at ISO8601]
1814
+ es_content="${1:-}"
1815
+ [[ -z "$es_content" ]] && json_err "$E_VALIDATION_FAILED" "Usage: eternal-store <content> [--type TYPE] [--source SOURCE] [--strength N] [--signal-id ID] [--reason TEXT] [--created-at ISO8601] [--archived-at ISO8601]" '{"missing":"content"}'
1816
+
1817
+ es_type="UNKNOWN"
1818
+ es_source="unknown"
1819
+ es_strength="0.0"
1820
+ es_signal_id=""
1821
+ es_reason="manual_store"
1822
+ es_created_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
1823
+ es_archived_at="$es_created_at"
1824
+
1825
+ shift
1826
+ while [[ $# -gt 0 ]]; do
1827
+ case "$1" in
1828
+ --type) es_type="${2:-UNKNOWN}"; shift 2 ;;
1829
+ --source) es_source="${2:-unknown}"; shift 2 ;;
1830
+ --strength) es_strength="${2:-0.0}"; shift 2 ;;
1831
+ --signal-id) es_signal_id="${2:-}"; shift 2 ;;
1832
+ --reason) es_reason="${2:-manual_store}"; shift 2 ;;
1833
+ --created-at) es_created_at="${2:-$es_created_at}"; shift 2 ;;
1834
+ --archived-at) es_archived_at="${2:-$es_archived_at}"; shift 2 ;;
1835
+ *) shift ;;
1836
+ esac
1837
+ done
1838
+
1839
+ if ! [[ "$es_strength" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
1840
+ json_err "$E_VALIDATION_FAILED" "Strength must be numeric" "{\"provided\":\"$es_strength\"}"
1841
+ fi
1842
+
1843
+ # SUPPRESS:OK -- cleanup: side-effect is best-effort
1844
+ bash "$0" eternal-init >/dev/null 2>&1 || json_err "$E_FILE_NOT_FOUND" "Unable to initialize eternal memory"
1845
+
1846
+ es_memory_file="$HOME/.aether/eternal/memory.json"
1847
+ [[ -f "$es_memory_file" ]] || json_err "$E_FILE_NOT_FOUND" "Eternal memory file not found"
1848
+
1849
+ if ! jq -e . "$es_memory_file" >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON validity
1850
+ json_err "$E_JSON_INVALID" "Eternal memory JSON is invalid"
1851
+ fi
1852
+
1853
+ es_entry=$(jq -n \
1854
+ --arg content "$es_content" \
1855
+ --arg type "$es_type" \
1856
+ --arg source "$es_source" \
1857
+ --arg signal_id "$es_signal_id" \
1858
+ --arg reason "$es_reason" \
1859
+ --arg created_at "$es_created_at" \
1860
+ --arg archived_at "$es_archived_at" \
1861
+ --argjson strength "$es_strength" \
1862
+ '{
1863
+ content: $content,
1864
+ type: $type,
1865
+ source: $source,
1866
+ signal_id: $signal_id,
1867
+ reason: $reason,
1868
+ strength: $strength,
1869
+ created_at: $created_at,
1870
+ archived_at: $archived_at
1871
+ }')
1872
+
1873
+ es_lock_held=false
1874
+ if type acquire_lock &>/dev/null; then
1875
+ acquire_lock "$es_memory_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on eternal memory"
1876
+ es_lock_held=true
1877
+ # Trap ensures lock release on unexpected exit (json_err calls exit 1)
1878
+ trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
1879
+ fi
1880
+
1881
+ es_updated=$(jq --argjson entry "$es_entry" '
1882
+ .high_value_signals = ((.high_value_signals // []) + [$entry]) |
1883
+ if (.high_value_signals | length) > 500 then .high_value_signals = .high_value_signals[-500:] else . end |
1884
+ .last_updated = $entry.archived_at
1885
+ ' "$es_memory_file" 2>/dev/null) || { # SUPPRESS:OK -- read-default: file may not exist yet
1886
+ [[ "$es_lock_held" == "true" ]] && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
1887
+ json_err "$E_JSON_INVALID" "Failed to update eternal memory"
1888
+ }
1889
+
1890
+ atomic_write "$es_memory_file" "$es_updated" || {
1891
+ [[ "$es_lock_held" == "true" ]] && release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
1892
+ json_err "$E_JSON_INVALID" "Failed to write eternal memory"
1893
+ }
1894
+
1895
+ [[ "$es_lock_held" == "true" ]] && { release_lock 2>/dev/null || true; trap - EXIT; } # SUPPRESS:OK -- cleanup: lock may not be held
1896
+ json_ok "$(jq -n --arg signal_id "$es_signal_id" --arg type "$es_type" '{stored: true, signal_id: $signal_id, type: $type}')"
1897
+ }
1898
+
1899
+ # ============================================================================
1900
+ # _pheromone_export_xml
1901
+ # Export pheromones.json to XML format
1902
+ # ============================================================================
1903
+ _pheromone_export_xml() {
1904
+ # Export pheromones.json to XML format
1905
+ # Usage: pheromone-export-xml [output_file]
1906
+ # Default output: .aether/exchange/pheromones.xml
1907
+
1908
+ pex_output="${1:-$SCRIPT_DIR/exchange/pheromones.xml}"
1909
+ pex_pheromones="$COLONY_DATA_DIR/pheromones.json"
1910
+
1911
+ # Graceful degradation: check for xmllint
1912
+ if ! command -v xmllint >/dev/null 2>&1; then
1913
+ json_err "$E_FEATURE_UNAVAILABLE" "xmllint is not installed. Try: xcode-select --install on macOS."
1914
+ fi
1915
+
1916
+ # Check pheromones.json exists
1917
+ if [[ ! -f "$pex_pheromones" ]]; then
1918
+ json_err "$E_FILE_NOT_FOUND" "Couldn't find pheromones.json. Try: run /ant:init first."
1919
+ fi
1920
+
1921
+ # Ensure output directory exists
1922
+ mkdir -p "$(dirname "$pex_output")"
1923
+
1924
+ # Source the exchange script
1925
+ source "$SCRIPT_DIR/exchange/pheromone-xml.sh"
1926
+
1927
+ # Call the export function
1928
+ xml-pheromone-export "$pex_pheromones" "$pex_output"
1929
+ }
1930
+
1931
+ # ============================================================================
1932
+ # _pheromone_import_xml
1933
+ # Import pheromone signals from XML into pheromones.json
1934
+ # ============================================================================
1935
+ _pheromone_import_xml() {
1936
+ # Import pheromone signals from XML into pheromones.json
1937
+ # Usage: pheromone-import-xml <xml_file> [colony_prefix]
1938
+ # When colony_prefix is provided, imported signal IDs are tagged with "${prefix}:" before merge
1939
+
1940
+ pix_xml="${1:-}"
1941
+ pix_colony_prefix="${2:-}"
1942
+ pix_pheromones="$COLONY_DATA_DIR/pheromones.json"
1943
+
1944
+ if [[ -z "$pix_xml" ]]; then
1945
+ json_err "$E_VALIDATION_FAILED" "Missing XML file argument. Try: pheromone-import-xml <xml_file> [colony_prefix]."
1946
+ fi
1947
+
1948
+ if [[ ! -f "$pix_xml" ]]; then
1949
+ json_err "$E_FILE_NOT_FOUND" "XML file not found: $pix_xml. Try: check the file path."
1950
+ fi
1951
+
1952
+ # Graceful degradation: check for xmllint
1953
+ if ! command -v xmllint >/dev/null 2>&1; then
1954
+ json_err "$E_FEATURE_UNAVAILABLE" "xmllint is not installed. Try: xcode-select --install on macOS."
1955
+ fi
1956
+
1957
+ # Source the exchange script
1958
+ source "$SCRIPT_DIR/exchange/pheromone-xml.sh"
1959
+
1960
+ # Import XML to get JSON signals
1961
+ pix_imported=$(xml-pheromone-import "$pix_xml")
1962
+
1963
+ # Extract actual signal array from result.json | fromjson | .signals
1964
+ # (result.signals is an integer count — must unpack result.json to get the array)
1965
+ # SUPPRESS:OK -- read-default: query may return empty
1966
+ pix_raw_signals=$(echo "$pix_imported" | jq -r '.result.json // "{}"' | jq -c '.signals // []' 2>/dev/null || echo '[]')
1967
+
1968
+ # Apply colony prefix to imported signal IDs (when provided)
1969
+ # This prevents ID collisions and tags signals with their source colony
1970
+ if [[ -n "$pix_colony_prefix" ]]; then
1971
+ # SUPPRESS:OK -- read-default: returns fallback on failure
1972
+ pix_prefixed_signals=$(echo "$pix_raw_signals" | jq --arg prefix "$pix_colony_prefix" '[.[] | .id = ($prefix + ":" + .id)]' 2>/dev/null || echo '[]')
1973
+ else
1974
+ pix_prefixed_signals="$pix_raw_signals"
1975
+ fi
1976
+
1977
+ # If pheromones.json exists, merge; otherwise create
1978
+ if [[ -f "$pix_pheromones" ]]; then
1979
+ # Merge: imported signals first, existing signals last
1980
+ # map(last) keeps current colony's version on ID collision — current colony always wins
1981
+ pix_merged=$(jq -s --argjson new_signals "$pix_prefixed_signals" '
1982
+ .[0] as $existing |
1983
+ {
1984
+ signals: ([$new_signals[], $existing.signals[]] | group_by(.id) | map(last)),
1985
+ version: $existing.version,
1986
+ colony_id: $existing.colony_id
1987
+ }
1988
+ ' "$pix_pheromones" 2>/dev/null) # SUPPRESS:OK -- read-default: file may not exist yet
1989
+
1990
+ if [[ -n "$pix_merged" ]]; then
1991
+ printf '%s\n' "$pix_merged" > "$pix_pheromones"
1992
+ fi
1993
+ fi
1994
+
1995
+ pix_count=$(echo "$pix_raw_signals" | jq 'length' 2>/dev/null || echo 0) # SUPPRESS:OK -- read-default: file may not exist yet
1996
+ json_ok "$(jq -n --argjson signal_count "$pix_count" --arg source "$pix_xml" '{imported: true, signal_count: $signal_count, source: $source}')"
1997
+ }
1998
+
1999
+ # ============================================================================
2000
+ # _pheromone_validate_xml
2001
+ # Validate pheromone XML against XSD schema
2002
+ # ============================================================================
2003
+ _pheromone_validate_xml() {
2004
+ # Validate pheromone XML against XSD schema
2005
+ # Usage: pheromone-validate-xml <xml_file>
2006
+
2007
+ pvx_xml="${1:-}"
2008
+ pvx_xsd="$SCRIPT_DIR/schemas/pheromone.xsd"
2009
+
2010
+ if [[ -z "$pvx_xml" ]]; then
2011
+ json_err "$E_VALIDATION_FAILED" "Missing XML file argument. Try: pheromone-validate-xml <xml_file>."
2012
+ fi
2013
+
2014
+ if [[ ! -f "$pvx_xml" ]]; then
2015
+ json_err "$E_FILE_NOT_FOUND" "XML file not found: $pvx_xml. Try: check the file path."
2016
+ fi
2017
+
2018
+ # Graceful degradation: check for xmllint
2019
+ if ! command -v xmllint >/dev/null 2>&1; then
2020
+ json_err "$E_FEATURE_UNAVAILABLE" "xmllint is not installed. Try: xcode-select --install on macOS."
2021
+ fi
2022
+
2023
+ # Source the exchange script
2024
+ source "$SCRIPT_DIR/exchange/pheromone-xml.sh"
2025
+
2026
+ # Call validate function
2027
+ xml-pheromone-validate "$pvx_xml" "$pvx_xsd"
2028
+ }
2029
+