aether-colony 5.3.2 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/.aether/aether-utils.sh +181 -5
  2. package/.aether/commands/archaeology.yaml +3 -3
  3. package/.aether/commands/build.yaml +80 -45
  4. package/.aether/commands/chaos.yaml +7 -7
  5. package/.aether/commands/colonize.yaml +17 -17
  6. package/.aether/commands/continue.yaml +40 -40
  7. package/.aether/commands/council.yaml +6 -6
  8. package/.aether/commands/data-clean.yaml +3 -3
  9. package/.aether/commands/dream.yaml +2 -2
  10. package/.aether/commands/entomb.yaml +12 -12
  11. package/.aether/commands/export-signals.yaml +2 -2
  12. package/.aether/commands/feedback.yaml +6 -6
  13. package/.aether/commands/flag.yaml +2 -2
  14. package/.aether/commands/flags.yaml +4 -4
  15. package/.aether/commands/focus.yaml +6 -6
  16. package/.aether/commands/help.yaml +1 -1
  17. package/.aether/commands/history.yaml +1 -1
  18. package/.aether/commands/import-signals.yaml +2 -2
  19. package/.aether/commands/init.yaml +44 -27
  20. package/.aether/commands/insert-phase.yaml +1 -1
  21. package/.aether/commands/interpret.yaml +2 -2
  22. package/.aether/commands/lay-eggs.yaml +3 -3
  23. package/.aether/commands/maturity.yaml +2 -2
  24. package/.aether/commands/memory-details.yaml +1 -1
  25. package/.aether/commands/migrate-state.yaml +1 -1
  26. package/.aether/commands/oracle.yaml +147 -82
  27. package/.aether/commands/organize.yaml +5 -5
  28. package/.aether/commands/patrol.yaml +8 -8
  29. package/.aether/commands/pause-colony.yaml +7 -7
  30. package/.aether/commands/phase.yaml +1 -1
  31. package/.aether/commands/pheromones.yaml +1 -1
  32. package/.aether/commands/plan.yaml +14 -14
  33. package/.aether/commands/quick.yaml +4 -4
  34. package/.aether/commands/redirect.yaml +6 -6
  35. package/.aether/commands/resume-colony.yaml +9 -9
  36. package/.aether/commands/resume.yaml +5 -38
  37. package/.aether/commands/run.yaml +10 -10
  38. package/.aether/commands/seal.yaml +33 -33
  39. package/.aether/commands/skill-create.yaml +4 -4
  40. package/.aether/commands/status.yaml +14 -14
  41. package/.aether/commands/swarm.yaml +14 -14
  42. package/.aether/commands/tunnels.yaml +7 -7
  43. package/.aether/commands/update.yaml +1 -1
  44. package/.aether/commands/verify-castes.yaml +3 -3
  45. package/.aether/commands/watch.yaml +15 -15
  46. package/.aether/docs/command-playbooks/build-complete.md +48 -15
  47. package/.aether/docs/command-playbooks/build-context.md +11 -11
  48. package/.aether/docs/command-playbooks/build-full.md +76 -76
  49. package/.aether/docs/command-playbooks/build-prep.md +10 -10
  50. package/.aether/docs/command-playbooks/build-verify.md +27 -27
  51. package/.aether/docs/command-playbooks/build-wave.md +38 -38
  52. package/.aether/docs/command-playbooks/continue-advance.md +60 -27
  53. package/.aether/docs/command-playbooks/continue-finalize.md +25 -11
  54. package/.aether/docs/command-playbooks/continue-full.md +60 -46
  55. package/.aether/docs/command-playbooks/continue-gates.md +18 -18
  56. package/.aether/docs/command-playbooks/continue-verify.md +10 -10
  57. package/.aether/docs/source-of-truth-map.md +10 -10
  58. package/.aether/docs/structural-learning-stack.md +283 -0
  59. package/.aether/templates/colony-state-template.json +1 -0
  60. package/.aether/utils/consolidation-seal.sh +196 -0
  61. package/.aether/utils/consolidation.sh +127 -0
  62. package/.aether/utils/curation-ants/archivist.sh +97 -0
  63. package/.aether/utils/curation-ants/critic.sh +214 -0
  64. package/.aether/utils/curation-ants/herald.sh +102 -0
  65. package/.aether/utils/curation-ants/janitor.sh +121 -0
  66. package/.aether/utils/curation-ants/librarian.sh +99 -0
  67. package/.aether/utils/curation-ants/nurse.sh +153 -0
  68. package/.aether/utils/curation-ants/orchestrator.sh +181 -0
  69. package/.aether/utils/curation-ants/scribe.sh +164 -0
  70. package/.aether/utils/curation-ants/sentinel.sh +119 -0
  71. package/.aether/utils/event-bus.sh +301 -0
  72. package/.aether/utils/graph.sh +559 -0
  73. package/.aether/utils/instinct-store.sh +401 -0
  74. package/.aether/utils/learning.sh +79 -7
  75. package/.aether/utils/oracle/oracle-stop-hook.sh +896 -0
  76. package/.aether/utils/session.sh +13 -0
  77. package/.aether/utils/state-api.sh +1 -1
  78. package/.aether/utils/trust-scoring.sh +347 -0
  79. package/.aether/utils/worktree.sh +97 -0
  80. package/.claude/commands/ant/archaeology.md +2 -2
  81. package/.claude/commands/ant/chaos.md +4 -4
  82. package/.claude/commands/ant/colonize.md +9 -9
  83. package/.claude/commands/ant/council.md +6 -6
  84. package/.claude/commands/ant/data-clean.md +3 -3
  85. package/.claude/commands/ant/dream.md +2 -2
  86. package/.claude/commands/ant/entomb.md +9 -9
  87. package/.claude/commands/ant/export-signals.md +2 -2
  88. package/.claude/commands/ant/feedback.md +4 -4
  89. package/.claude/commands/ant/flag.md +2 -2
  90. package/.claude/commands/ant/flags.md +4 -4
  91. package/.claude/commands/ant/focus.md +4 -4
  92. package/.claude/commands/ant/help.md +1 -1
  93. package/.claude/commands/ant/history.md +1 -1
  94. package/.claude/commands/ant/import-signals.md +2 -2
  95. package/.claude/commands/ant/init.md +44 -27
  96. package/.claude/commands/ant/insert-phase.md +1 -1
  97. package/.claude/commands/ant/interpret.md +2 -2
  98. package/.claude/commands/ant/lay-eggs.md +2 -2
  99. package/.claude/commands/ant/maturity.md +2 -2
  100. package/.claude/commands/ant/memory-details.md +1 -1
  101. package/.claude/commands/ant/migrate-state.md +1 -1
  102. package/.claude/commands/ant/oracle.md +78 -42
  103. package/.claude/commands/ant/organize.md +3 -3
  104. package/.claude/commands/ant/patrol.md +8 -8
  105. package/.claude/commands/ant/pause-colony.md +5 -5
  106. package/.claude/commands/ant/phase.md +1 -1
  107. package/.claude/commands/ant/pheromones.md +1 -1
  108. package/.claude/commands/ant/plan.md +8 -8
  109. package/.claude/commands/ant/quick.md +4 -4
  110. package/.claude/commands/ant/redirect.md +4 -4
  111. package/.claude/commands/ant/resume-colony.md +5 -5
  112. package/.claude/commands/ant/resume.md +17 -29
  113. package/.claude/commands/ant/run.md +10 -10
  114. package/.claude/commands/ant/seal.md +25 -25
  115. package/.claude/commands/ant/skill-create.md +2 -2
  116. package/.claude/commands/ant/status.md +14 -14
  117. package/.claude/commands/ant/swarm.md +14 -14
  118. package/.claude/commands/ant/tunnels.md +4 -4
  119. package/.claude/commands/ant/update.md +1 -1
  120. package/.claude/commands/ant/verify-castes.md +2 -2
  121. package/.claude/commands/ant/watch.md +8 -8
  122. package/.opencode/commands/ant/archaeology.md +1 -1
  123. package/.opencode/commands/ant/build.md +80 -45
  124. package/.opencode/commands/ant/chaos.md +3 -3
  125. package/.opencode/commands/ant/colonize.md +8 -8
  126. package/.opencode/commands/ant/continue.md +40 -40
  127. package/.opencode/commands/ant/council.md +5 -5
  128. package/.opencode/commands/ant/data-clean.md +2 -2
  129. package/.opencode/commands/ant/dream.md +1 -1
  130. package/.opencode/commands/ant/entomb.md +3 -3
  131. package/.opencode/commands/ant/export-signals.md +1 -1
  132. package/.opencode/commands/ant/feedback.md +2 -2
  133. package/.opencode/commands/ant/flag.md +1 -1
  134. package/.opencode/commands/ant/flags.md +3 -3
  135. package/.opencode/commands/ant/focus.md +2 -2
  136. package/.opencode/commands/ant/import-signals.md +1 -1
  137. package/.opencode/commands/ant/init.md +44 -27
  138. package/.opencode/commands/ant/insert-phase.md +1 -1
  139. package/.opencode/commands/ant/interpret.md +1 -1
  140. package/.opencode/commands/ant/lay-eggs.md +2 -2
  141. package/.opencode/commands/ant/maturity.md +1 -1
  142. package/.opencode/commands/ant/memory-details.md +1 -1
  143. package/.opencode/commands/ant/oracle.md +69 -40
  144. package/.opencode/commands/ant/organize.md +2 -2
  145. package/.opencode/commands/ant/patrol.md +8 -8
  146. package/.opencode/commands/ant/pause-colony.md +2 -2
  147. package/.opencode/commands/ant/pheromones.md +1 -1
  148. package/.opencode/commands/ant/plan.md +6 -6
  149. package/.opencode/commands/ant/quick.md +4 -4
  150. package/.opencode/commands/ant/redirect.md +2 -2
  151. package/.opencode/commands/ant/resume-colony.md +4 -4
  152. package/.opencode/commands/ant/resume.md +5 -17
  153. package/.opencode/commands/ant/run.md +10 -10
  154. package/.opencode/commands/ant/seal.md +8 -8
  155. package/.opencode/commands/ant/skill-create.md +2 -2
  156. package/.opencode/commands/ant/status.md +10 -10
  157. package/.opencode/commands/ant/tunnels.md +3 -3
  158. package/.opencode/commands/ant/verify-castes.md +1 -1
  159. package/.opencode/commands/ant/watch.md +7 -7
  160. package/CHANGELOG.md +83 -0
  161. package/README.md +22 -9
  162. package/bin/cli.js +118 -3
  163. package/bin/lib/binary-downloader.js +267 -0
  164. package/bin/lib/update-transaction.js +27 -3
  165. package/bin/lib/version-gate.js +179 -0
  166. package/bin/npx-entry.js +0 -0
  167. package/package.json +1 -1
  168. package/.aether/agents/aether-ambassador.md +0 -140
  169. package/.aether/agents/aether-archaeologist.md +0 -108
  170. package/.aether/agents/aether-architect.md +0 -133
  171. package/.aether/agents/aether-auditor.md +0 -144
  172. package/.aether/agents/aether-builder.md +0 -184
  173. package/.aether/agents/aether-chaos.md +0 -115
  174. package/.aether/agents/aether-chronicler.md +0 -122
  175. package/.aether/agents/aether-gatekeeper.md +0 -116
  176. package/.aether/agents/aether-includer.md +0 -117
  177. package/.aether/agents/aether-keeper.md +0 -177
  178. package/.aether/agents/aether-measurer.md +0 -128
  179. package/.aether/agents/aether-oracle.md +0 -137
  180. package/.aether/agents/aether-probe.md +0 -133
  181. package/.aether/agents/aether-queen.md +0 -286
  182. package/.aether/agents/aether-route-setter.md +0 -130
  183. package/.aether/agents/aether-sage.md +0 -106
  184. package/.aether/agents/aether-scout.md +0 -101
  185. package/.aether/agents/aether-surveyor-disciplines.md +0 -391
  186. package/.aether/agents/aether-surveyor-nest.md +0 -329
  187. package/.aether/agents/aether-surveyor-pathogens.md +0 -264
  188. package/.aether/agents/aether-surveyor-provisions.md +0 -334
  189. package/.aether/agents/aether-tracker.md +0 -137
  190. package/.aether/agents/aether-watcher.md +0 -174
  191. package/.aether/agents/aether-weaver.md +0 -130
  192. package/.aether/commands/claude/archaeology.md +0 -334
  193. package/.aether/commands/claude/build.md +0 -65
  194. package/.aether/commands/claude/chaos.md +0 -336
  195. package/.aether/commands/claude/colonize.md +0 -259
  196. package/.aether/commands/claude/continue.md +0 -60
  197. package/.aether/commands/claude/council.md +0 -507
  198. package/.aether/commands/claude/data-clean.md +0 -81
  199. package/.aether/commands/claude/dream.md +0 -268
  200. package/.aether/commands/claude/entomb.md +0 -498
  201. package/.aether/commands/claude/export-signals.md +0 -57
  202. package/.aether/commands/claude/feedback.md +0 -96
  203. package/.aether/commands/claude/flag.md +0 -151
  204. package/.aether/commands/claude/flags.md +0 -169
  205. package/.aether/commands/claude/focus.md +0 -76
  206. package/.aether/commands/claude/help.md +0 -154
  207. package/.aether/commands/claude/history.md +0 -140
  208. package/.aether/commands/claude/import-signals.md +0 -71
  209. package/.aether/commands/claude/init.md +0 -505
  210. package/.aether/commands/claude/insert-phase.md +0 -105
  211. package/.aether/commands/claude/interpret.md +0 -278
  212. package/.aether/commands/claude/lay-eggs.md +0 -210
  213. package/.aether/commands/claude/maturity.md +0 -113
  214. package/.aether/commands/claude/memory-details.md +0 -77
  215. package/.aether/commands/claude/migrate-state.md +0 -171
  216. package/.aether/commands/claude/oracle.md +0 -642
  217. package/.aether/commands/claude/organize.md +0 -232
  218. package/.aether/commands/claude/patrol.md +0 -620
  219. package/.aether/commands/claude/pause-colony.md +0 -233
  220. package/.aether/commands/claude/phase.md +0 -115
  221. package/.aether/commands/claude/pheromones.md +0 -156
  222. package/.aether/commands/claude/plan.md +0 -693
  223. package/.aether/commands/claude/preferences.md +0 -65
  224. package/.aether/commands/claude/quick.md +0 -100
  225. package/.aether/commands/claude/redirect.md +0 -76
  226. package/.aether/commands/claude/resume-colony.md +0 -197
  227. package/.aether/commands/claude/resume.md +0 -388
  228. package/.aether/commands/claude/run.md +0 -231
  229. package/.aether/commands/claude/seal.md +0 -774
  230. package/.aether/commands/claude/skill-create.md +0 -286
  231. package/.aether/commands/claude/status.md +0 -410
  232. package/.aether/commands/claude/swarm.md +0 -349
  233. package/.aether/commands/claude/tunnels.md +0 -426
  234. package/.aether/commands/claude/update.md +0 -132
  235. package/.aether/commands/claude/verify-castes.md +0 -143
  236. package/.aether/commands/claude/watch.md +0 -239
  237. package/.aether/commands/opencode/archaeology.md +0 -331
  238. package/.aether/commands/opencode/build.md +0 -1168
  239. package/.aether/commands/opencode/chaos.md +0 -329
  240. package/.aether/commands/opencode/colonize.md +0 -195
  241. package/.aether/commands/opencode/continue.md +0 -1436
  242. package/.aether/commands/opencode/council.md +0 -437
  243. package/.aether/commands/opencode/data-clean.md +0 -77
  244. package/.aether/commands/opencode/dream.md +0 -260
  245. package/.aether/commands/opencode/entomb.md +0 -377
  246. package/.aether/commands/opencode/export-signals.md +0 -54
  247. package/.aether/commands/opencode/feedback.md +0 -99
  248. package/.aether/commands/opencode/flag.md +0 -149
  249. package/.aether/commands/opencode/flags.md +0 -167
  250. package/.aether/commands/opencode/focus.md +0 -73
  251. package/.aether/commands/opencode/help.md +0 -157
  252. package/.aether/commands/opencode/history.md +0 -136
  253. package/.aether/commands/opencode/import-signals.md +0 -68
  254. package/.aether/commands/opencode/init.md +0 -518
  255. package/.aether/commands/opencode/insert-phase.md +0 -111
  256. package/.aether/commands/opencode/interpret.md +0 -272
  257. package/.aether/commands/opencode/lay-eggs.md +0 -213
  258. package/.aether/commands/opencode/maturity.md +0 -108
  259. package/.aether/commands/opencode/memory-details.md +0 -83
  260. package/.aether/commands/opencode/migrate-state.md +0 -165
  261. package/.aether/commands/opencode/oracle.md +0 -593
  262. package/.aether/commands/opencode/organize.md +0 -226
  263. package/.aether/commands/opencode/patrol.md +0 -626
  264. package/.aether/commands/opencode/pause-colony.md +0 -203
  265. package/.aether/commands/opencode/phase.md +0 -113
  266. package/.aether/commands/opencode/pheromones.md +0 -162
  267. package/.aether/commands/opencode/plan.md +0 -684
  268. package/.aether/commands/opencode/preferences.md +0 -71
  269. package/.aether/commands/opencode/quick.md +0 -91
  270. package/.aether/commands/opencode/redirect.md +0 -84
  271. package/.aether/commands/opencode/resume-colony.md +0 -190
  272. package/.aether/commands/opencode/resume.md +0 -394
  273. package/.aether/commands/opencode/run.md +0 -237
  274. package/.aether/commands/opencode/seal.md +0 -452
  275. package/.aether/commands/opencode/skill-create.md +0 -63
  276. package/.aether/commands/opencode/status.md +0 -307
  277. package/.aether/commands/opencode/swarm.md +0 -15
  278. package/.aether/commands/opencode/tunnels.md +0 -400
  279. package/.aether/commands/opencode/update.md +0 -127
  280. package/.aether/commands/opencode/verify-castes.md +0 -139
  281. package/.aether/commands/opencode/watch.md +0 -227
@@ -0,0 +1,559 @@
1
+ #!/bin/bash
2
+ # Graph traversal layer for instinct relationships — Aether Structural Learning Stack
3
+ # Provides: _graph_link, _graph_neighbors, _graph_reach, _graph_cluster
4
+ #
5
+ # These functions are sourced by aether-utils.sh at startup.
6
+ # All shared infrastructure (json_ok, json_err, atomic_write,
7
+ # COLONY_DATA_DIR, SCRIPT_DIR, error constants) is available.
8
+ #
9
+ # Graph is stored as JSON at $COLONY_DATA_DIR/instinct-graph.json:
10
+ # {
11
+ # "version": "1.0",
12
+ # "edges": [
13
+ # { "source": "id", "target": "id", "relationship": "type",
14
+ # "weight": 0.5, "created_at": "ISO8601" }
15
+ # ]
16
+ # }
17
+ #
18
+ # Relationship types: reinforces, contradicts, extends, supersedes, related
19
+
20
+ # ============================================================================
21
+ # _graph_init_file
22
+ # Ensure the graph file exists with an empty structure.
23
+ # Internal helper.
24
+ # ============================================================================
25
+ _graph_init_file() {
26
+ local graph_file="$1"
27
+ if [[ ! -f "$graph_file" ]]; then
28
+ local dir
29
+ dir="$(dirname "$graph_file")"
30
+ mkdir -p "$dir"
31
+ atomic_write "$graph_file" '{"version":"1.0","edges":[]}'
32
+ fi
33
+ }
34
+
35
+ # ============================================================================
36
+ # _graph_link
37
+ # Create a directed edge between two instinct IDs. If the same
38
+ # source+target+relationship already exists, update the weight instead.
39
+ #
40
+ # Usage: graph-link --source <id> --target <id> --relationship <type>
41
+ # [--weight <float>]
42
+ #
43
+ # Relationship types: reinforces, contradicts, extends, supersedes, related
44
+ # Default weight: 0.5
45
+ #
46
+ # Output: {edge_id, source, target, relationship, weight, action}
47
+ # ============================================================================
48
+ _graph_link() {
49
+ local source_id=""
50
+ local target_id=""
51
+ local relationship=""
52
+ local weight="0.5"
53
+
54
+ while [[ $# -gt 0 ]]; do
55
+ case "$1" in
56
+ --source)
57
+ source_id="${2:-}"
58
+ shift 2
59
+ ;;
60
+ --target)
61
+ target_id="${2:-}"
62
+ shift 2
63
+ ;;
64
+ --relationship)
65
+ relationship="${2:-}"
66
+ shift 2
67
+ ;;
68
+ --weight)
69
+ weight="${2:-0.5}"
70
+ shift 2
71
+ ;;
72
+ *)
73
+ json_err "$E_VALIDATION_FAILED" "Usage: graph-link --source <id> --target <id> --relationship <type> [--weight <float>]"
74
+ return
75
+ ;;
76
+ esac
77
+ done
78
+
79
+ [[ -z "$source_id" || -z "$target_id" || -z "$relationship" ]] && \
80
+ json_err "$E_VALIDATION_FAILED" "Usage: graph-link --source <id> --target <id> --relationship <type> [--weight <float>]"
81
+
82
+ # Validate relationship type
83
+ case "$relationship" in
84
+ reinforces|contradicts|extends|supersedes|related) ;;
85
+ *)
86
+ json_err "$E_VALIDATION_FAILED" "Unknown relationship: $relationship. Valid: reinforces, contradicts, extends, supersedes, related"
87
+ return
88
+ ;;
89
+ esac
90
+
91
+ # Validate weight is a non-negative number
92
+ if ! [[ "$weight" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
93
+ json_err "$E_VALIDATION_FAILED" "--weight must be a non-negative number, got: $weight"
94
+ return
95
+ fi
96
+
97
+ local graph_file="$COLONY_DATA_DIR/instinct-graph.json"
98
+ _graph_init_file "$graph_file"
99
+
100
+ local ts
101
+ ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
102
+
103
+ # Check if a matching edge (same source+target+relationship) exists
104
+ local existing_edge_id
105
+ existing_edge_id=$(jq -r \
106
+ --arg src "$source_id" \
107
+ --arg tgt "$target_id" \
108
+ --arg rel "$relationship" \
109
+ '.edges[] | select(.source == $src and .target == $tgt and .relationship == $rel) | .edge_id' \
110
+ "$graph_file" | head -1)
111
+
112
+ local action
113
+ local updated
114
+ if [[ -n "$existing_edge_id" ]]; then
115
+ # Update existing edge weight
116
+ action="updated"
117
+ updated=$(jq \
118
+ --arg src "$source_id" \
119
+ --arg tgt "$target_id" \
120
+ --arg rel "$relationship" \
121
+ --argjson w "$weight" \
122
+ '.edges = [.edges[] | if (.source == $src and .target == $tgt and .relationship == $rel) then .weight = $w else . end]' \
123
+ "$graph_file")
124
+ local edge_id="$existing_edge_id"
125
+ else
126
+ # Create new edge
127
+ action="created"
128
+ local edge_id
129
+ edge_id="edge_$(date -u +%s)_$(head -c 2 /dev/urandom | od -An -tx1 | tr -d ' \n')"
130
+ updated=$(jq \
131
+ --arg eid "$edge_id" \
132
+ --arg src "$source_id" \
133
+ --arg tgt "$target_id" \
134
+ --arg rel "$relationship" \
135
+ --argjson w "$weight" \
136
+ --arg ts "$ts" \
137
+ '.edges += [{edge_id: $eid, source: $src, target: $tgt, relationship: $rel, weight: $w, created_at: $ts}]' \
138
+ "$graph_file")
139
+ fi
140
+
141
+ atomic_write "$graph_file" "$updated"
142
+
143
+ # Re-read the final edge_id for the output (handles both create and update paths)
144
+ local final_edge_id
145
+ final_edge_id=$(jq -r \
146
+ --arg src "$source_id" \
147
+ --arg tgt "$target_id" \
148
+ --arg rel "$relationship" \
149
+ '.edges[] | select(.source == $src and .target == $tgt and .relationship == $rel) | .edge_id' \
150
+ "$graph_file" | head -1)
151
+
152
+ json_ok "$(jq -n \
153
+ --arg edge_id "$final_edge_id" \
154
+ --arg source "$source_id" \
155
+ --arg target "$target_id" \
156
+ --arg relationship "$relationship" \
157
+ --argjson weight "$weight" \
158
+ --arg action "$action" \
159
+ '{
160
+ edge_id: $edge_id,
161
+ source: $source,
162
+ target: $target,
163
+ relationship: $relationship,
164
+ weight: $weight,
165
+ action: $action
166
+ }')"
167
+ }
168
+
169
+ # ============================================================================
170
+ # _graph_neighbors
171
+ # Find all nodes connected to a given instinct (1-hop).
172
+ #
173
+ # Usage: graph-neighbors --id <instinct_id> [--direction out|in|both]
174
+ # [--relationship <type>]
175
+ #
176
+ # Default direction: both
177
+ #
178
+ # Output: {neighbors: [{id, relationship, weight, direction}], count}
179
+ # ============================================================================
180
+ _graph_neighbors() {
181
+ local instinct_id=""
182
+ local direction="both"
183
+ local filter_rel=""
184
+
185
+ while [[ $# -gt 0 ]]; do
186
+ case "$1" in
187
+ --id)
188
+ instinct_id="${2:-}"
189
+ shift 2
190
+ ;;
191
+ --direction)
192
+ direction="${2:-both}"
193
+ shift 2
194
+ ;;
195
+ --relationship)
196
+ filter_rel="${2:-}"
197
+ shift 2
198
+ ;;
199
+ *)
200
+ json_err "$E_VALIDATION_FAILED" "Usage: graph-neighbors --id <instinct_id> [--direction out|in|both] [--relationship <type>]"
201
+ return
202
+ ;;
203
+ esac
204
+ done
205
+
206
+ [[ -z "$instinct_id" ]] && \
207
+ json_err "$E_VALIDATION_FAILED" "Usage: graph-neighbors --id <instinct_id> [--direction out|in|both] [--relationship <type>]"
208
+
209
+ case "$direction" in
210
+ out|in|both) ;;
211
+ *)
212
+ json_err "$E_VALIDATION_FAILED" "Invalid direction: $direction. Valid: out, in, both"
213
+ return
214
+ ;;
215
+ esac
216
+
217
+ local graph_file="$COLONY_DATA_DIR/instinct-graph.json"
218
+
219
+ # Return empty if graph file doesn't exist yet
220
+ if [[ ! -f "$graph_file" ]]; then
221
+ json_ok '{"neighbors":[],"count":0}'
222
+ return
223
+ fi
224
+
225
+ local neighbors
226
+ neighbors=$(jq -c \
227
+ --arg id "$instinct_id" \
228
+ --arg dir "$direction" \
229
+ --arg rel "$filter_rel" \
230
+ '
231
+ [
232
+ # Outbound edges: id is the source
233
+ if ($dir == "out" or $dir == "both") then
234
+ .edges[]
235
+ | select(.source == $id)
236
+ | select($rel == "" or .relationship == $rel)
237
+ | {id: .target, relationship: .relationship, weight: .weight, direction: "out"}
238
+ else empty end
239
+ ,
240
+ # Inbound edges: id is the target
241
+ if ($dir == "in" or $dir == "both") then
242
+ .edges[]
243
+ | select(.target == $id)
244
+ | select($rel == "" or .relationship == $rel)
245
+ | {id: .source, relationship: .relationship, weight: .weight, direction: "in"}
246
+ else empty end
247
+ ]
248
+ ' \
249
+ "$graph_file")
250
+
251
+ local count
252
+ count=$(echo "$neighbors" | jq 'length')
253
+
254
+ json_ok "$(jq -n \
255
+ --argjson neighbors "$neighbors" \
256
+ --argjson count "$count" \
257
+ '{neighbors: $neighbors, count: $count}')"
258
+ }
259
+
260
+ # ============================================================================
261
+ # _graph_reach
262
+ # Find all nodes reachable within N hops using iterative BFS.
263
+ # One jq call per hop level to avoid complex recursive expressions.
264
+ #
265
+ # Usage: graph-reach --id <instinct_id> --hops <N> [--min-weight <float>]
266
+ #
267
+ # Max hops enforced at 3 to prevent expensive traversals.
268
+ # Default min-weight: 0.0
269
+ #
270
+ # Output: {reachable: [{id, hop, path}], count, hops_searched}
271
+ # ============================================================================
272
+ _graph_reach() {
273
+ local instinct_id=""
274
+ local hops=""
275
+ local min_weight="0.0"
276
+
277
+ while [[ $# -gt 0 ]]; do
278
+ case "$1" in
279
+ --id)
280
+ instinct_id="${2:-}"
281
+ shift 2
282
+ ;;
283
+ --hops)
284
+ hops="${2:-}"
285
+ shift 2
286
+ ;;
287
+ --min-weight)
288
+ min_weight="${2:-0.0}"
289
+ shift 2
290
+ ;;
291
+ *)
292
+ json_err "$E_VALIDATION_FAILED" "Usage: graph-reach --id <instinct_id> --hops <N> [--min-weight <float>]"
293
+ return
294
+ ;;
295
+ esac
296
+ done
297
+
298
+ [[ -z "$instinct_id" || -z "$hops" ]] && \
299
+ json_err "$E_VALIDATION_FAILED" "Usage: graph-reach --id <instinct_id> --hops <N> [--min-weight <float>]"
300
+
301
+ # Validate hops is a positive integer
302
+ if ! [[ "$hops" =~ ^[0-9]+$ ]] || [[ "$hops" -lt 1 ]]; then
303
+ json_err "$E_VALIDATION_FAILED" "--hops must be a positive integer, got: $hops"
304
+ return
305
+ fi
306
+
307
+ # Clamp hops to max 3
308
+ local MAX_HOPS=3
309
+ local hops_searched="$hops"
310
+ if [[ "$hops" -gt "$MAX_HOPS" ]]; then
311
+ hops_searched="$MAX_HOPS"
312
+ fi
313
+
314
+ # Validate min_weight
315
+ if ! [[ "$min_weight" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
316
+ json_err "$E_VALIDATION_FAILED" "--min-weight must be a non-negative number, got: $min_weight"
317
+ return
318
+ fi
319
+
320
+ local graph_file="$COLONY_DATA_DIR/instinct-graph.json"
321
+
322
+ if [[ ! -f "$graph_file" ]]; then
323
+ json_ok "{\"reachable\":[],\"count\":0,\"hops_searched\":$hops_searched}"
324
+ return
325
+ fi
326
+
327
+ # Read all edges once
328
+ local all_edges
329
+ all_edges=$(jq -c '.edges' "$graph_file")
330
+
331
+ # BFS: frontier is the set of IDs at the current hop level
332
+ # reachable accumulates {id, hop, path} for all visited nodes
333
+ # visited tracks IDs seen to avoid cycles
334
+
335
+ # frontier: JSON array of {id, path} objects
336
+ local frontier
337
+ frontier="[{\"id\":\"$instinct_id\",\"path\":[\"$instinct_id\"]}]"
338
+ local visited
339
+ visited="[\"$instinct_id\"]"
340
+ local reachable="[]"
341
+
342
+ local current_hop=0
343
+ while [[ "$current_hop" -lt "$hops_searched" ]]; do
344
+ current_hop=$((current_hop + 1))
345
+
346
+ # Expand frontier: find all outbound neighbors not yet visited
347
+ local new_nodes
348
+ new_nodes=$(jq -c \
349
+ --argjson edges "$all_edges" \
350
+ --argjson frontier "$frontier" \
351
+ --argjson visited "$visited" \
352
+ --argjson hop "$current_hop" \
353
+ --argjson mw "$min_weight" \
354
+ '
355
+ [
356
+ $frontier[] as $f |
357
+ $edges[] |
358
+ select(.source == $f.id) |
359
+ select(.weight >= $mw) |
360
+ select(.target as $t | ($visited | index($t)) == null) |
361
+ {id: .target, hop: $hop, path: ($f.path + [.target])}
362
+ ] | unique_by(.id)
363
+ ' \
364
+ <<< "null")
365
+
366
+ # If no new nodes found, stop early
367
+ local new_count
368
+ new_count=$(echo "$new_nodes" | jq 'length')
369
+ if [[ "$new_count" -eq 0 ]]; then
370
+ break
371
+ fi
372
+
373
+ # Append new nodes to reachable
374
+ reachable=$(jq -c \
375
+ --argjson existing "$reachable" \
376
+ --argjson new "$new_nodes" \
377
+ '$existing + $new' \
378
+ <<< "null")
379
+
380
+ # Update visited set
381
+ visited=$(jq -c \
382
+ --argjson visited "$visited" \
383
+ --argjson new "$new_nodes" \
384
+ '$visited + [$new[].id]' \
385
+ <<< "null")
386
+
387
+ # Update frontier for next hop
388
+ frontier=$(jq -c \
389
+ --argjson new "$new_nodes" \
390
+ '[.[] | {id: .id, path: .path}]' \
391
+ <<< "$new_nodes")
392
+ done
393
+
394
+ local count
395
+ count=$(echo "$reachable" | jq 'length')
396
+
397
+ json_ok "$(jq -n \
398
+ --argjson reachable "$reachable" \
399
+ --argjson count "$count" \
400
+ --argjson hops_searched "$hops_searched" \
401
+ '{reachable: $reachable, count: $count, hops_searched: $hops_searched}')"
402
+ }
403
+
404
+ # ============================================================================
405
+ # _graph_cluster
406
+ # Find clusters of strongly connected instincts.
407
+ # A cluster is a group of nodes that share >= min-edges connections
408
+ # all with weight >= min-weight.
409
+ #
410
+ # Usage: graph-cluster [--min-edges <N>] [--min-weight <float>]
411
+ #
412
+ # Default min-edges: 2
413
+ # Default min-weight: 0.3
414
+ #
415
+ # Output: {clusters: [{nodes, edge_count, avg_weight}], count}
416
+ # ============================================================================
417
+ _graph_cluster() {
418
+ local min_edges=2
419
+ local min_weight="0.3"
420
+
421
+ while [[ $# -gt 0 ]]; do
422
+ case "$1" in
423
+ --min-edges)
424
+ min_edges="${2:-2}"
425
+ shift 2
426
+ ;;
427
+ --min-weight)
428
+ min_weight="${2:-0.3}"
429
+ shift 2
430
+ ;;
431
+ *)
432
+ json_err "$E_VALIDATION_FAILED" "Usage: graph-cluster [--min-edges <N>] [--min-weight <float>]"
433
+ return
434
+ ;;
435
+ esac
436
+ done
437
+
438
+ # Validate min_edges
439
+ if ! [[ "$min_edges" =~ ^[0-9]+$ ]]; then
440
+ json_err "$E_VALIDATION_FAILED" "--min-edges must be a non-negative integer, got: $min_edges"
441
+ return
442
+ fi
443
+
444
+ # Validate min_weight
445
+ if ! [[ "$min_weight" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
446
+ json_err "$E_VALIDATION_FAILED" "--min-weight must be a non-negative number, got: $min_weight"
447
+ return
448
+ fi
449
+
450
+ local graph_file="$COLONY_DATA_DIR/instinct-graph.json"
451
+
452
+ if [[ ! -f "$graph_file" ]]; then
453
+ json_ok '{"clusters":[],"count":0}'
454
+ return
455
+ fi
456
+
457
+ # Strategy:
458
+ # 1. Filter edges by min-weight
459
+ # 2. For each node, count qualifying edges (in + out)
460
+ # 3. Nodes with >= min-edges form candidates
461
+ # 4. Group connected candidate nodes into clusters via union-find in jq
462
+
463
+ local clusters
464
+ clusters=$(jq -c \
465
+ --argjson min_edges "$min_edges" \
466
+ --argjson min_weight "$min_weight" \
467
+ '
468
+ # Step 1: filter qualifying edges
469
+ (.edges | map(select(.weight >= $min_weight))) as $qual_edges |
470
+
471
+ # Step 2: count edges per node (source + target)
472
+ (
473
+ $qual_edges |
474
+ group_by(.source) |
475
+ map({key: .[0].source, value: length}) |
476
+ from_entries
477
+ ) as $out_counts |
478
+ (
479
+ $qual_edges |
480
+ group_by(.target) |
481
+ map({key: .[0].target, value: length}) |
482
+ from_entries
483
+ ) as $in_counts |
484
+
485
+ # Step 3: build total edge counts per node
486
+ (
487
+ [$qual_edges[] | .source, .target] | unique |
488
+ map({
489
+ id: .,
490
+ edge_count: ((($out_counts[.] // 0) + ($in_counts[.] // 0)))
491
+ }) |
492
+ map(select(.edge_count >= $min_edges))
493
+ ) as $candidates |
494
+
495
+ # Step 4: group candidates into clusters via connected components
496
+ # Build adjacency: pairs of candidate nodes that share a qualifying edge
497
+ (
498
+ $candidates | map(.id)
499
+ ) as $candidate_ids |
500
+
501
+ (
502
+ $qual_edges |
503
+ map(select(
504
+ (.source as $s | ($candidate_ids | index($s)) != null) and
505
+ (.target as $t | ($candidate_ids | index($t)) != null)
506
+ )) |
507
+ map({a: .source, b: .target, w: .weight})
508
+ ) as $adj |
509
+
510
+ # Build clusters: use greedy union approach
511
+ # Start each candidate in its own group, merge groups with shared edges
512
+ reduce $adj[] as $edge (
513
+ ($candidate_ids | map({id: ., group: .}));
514
+ . as $groups |
515
+ ($groups | map(select(.id == $edge.a)) | first.group) as $ga |
516
+ ($groups | map(select(.id == $edge.b)) | first.group) as $gb |
517
+ if $ga == $gb then $groups
518
+ else
519
+ # Merge all nodes with group $gb into group $ga
520
+ [ .[] | if .group == $gb then .group = $ga else . end ]
521
+ end
522
+ ) |
523
+ # Group by cluster label
524
+ group_by(.group) |
525
+ map({
526
+ nodes: map(.id),
527
+ edge_count: (
528
+ . as $cluster_nodes |
529
+ ($cluster_nodes | map(.id)) as $node_ids |
530
+ $adj | map(select(
531
+ (.a as $a | ($node_ids | index($a)) != null) and
532
+ (.b as $b | ($node_ids | index($b)) != null)
533
+ )) | length
534
+ ),
535
+ avg_weight: (
536
+ . as $cluster_nodes |
537
+ ($cluster_nodes | map(.id)) as $node_ids |
538
+ ($adj | map(select(
539
+ (.a as $a | ($node_ids | index($a)) != null) and
540
+ (.b as $b | ($node_ids | index($b)) != null)
541
+ )) | map(.w)) as $weights |
542
+ if ($weights | length) > 0 then
543
+ ($weights | add) / ($weights | length)
544
+ else 0 end
545
+ )
546
+ }) |
547
+ # Only keep clusters with more than 1 node
548
+ map(select(.nodes | length > 1))
549
+ ' \
550
+ "$graph_file")
551
+
552
+ local count
553
+ count=$(echo "$clusters" | jq 'length')
554
+
555
+ json_ok "$(jq -n \
556
+ --argjson clusters "$clusters" \
557
+ --argjson count "$count" \
558
+ '{clusters: $clusters, count: $count}')"
559
+ }