aether-colony 3.1.4 → 3.1.15

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 (124) hide show
  1. package/.claude/commands/ant/archaeology.md +12 -0
  2. package/.claude/commands/ant/build.md +382 -319
  3. package/.claude/commands/ant/chaos.md +23 -1
  4. package/.claude/commands/ant/colonize.md +147 -87
  5. package/.claude/commands/ant/continue.md +213 -23
  6. package/.claude/commands/ant/council.md +22 -0
  7. package/.claude/commands/ant/dream.md +18 -0
  8. package/.claude/commands/ant/entomb.md +178 -6
  9. package/.claude/commands/ant/init.md +87 -13
  10. package/.claude/commands/ant/lay-eggs.md +45 -5
  11. package/.claude/commands/ant/oracle.md +82 -9
  12. package/.claude/commands/ant/organize.md +2 -2
  13. package/.claude/commands/ant/pause-colony.md +86 -28
  14. package/.claude/commands/ant/phase.md +26 -0
  15. package/.claude/commands/ant/plan.md +204 -111
  16. package/.claude/commands/ant/resume-colony.md +23 -1
  17. package/.claude/commands/ant/resume.md +159 -0
  18. package/.claude/commands/ant/seal.md +177 -3
  19. package/.claude/commands/ant/swarm.md +78 -97
  20. package/.claude/commands/ant/verify-castes.md +7 -7
  21. package/.claude/commands/ant/watch.md +17 -0
  22. package/.opencode/agents/aether-ambassador.md +97 -0
  23. package/.opencode/agents/aether-archaeologist.md +91 -0
  24. package/.opencode/agents/aether-architect.md +66 -0
  25. package/.opencode/agents/aether-auditor.md +111 -0
  26. package/.opencode/agents/aether-builder.md +28 -10
  27. package/.opencode/agents/aether-chaos.md +98 -0
  28. package/.opencode/agents/aether-chronicler.md +80 -0
  29. package/.opencode/agents/aether-gatekeeper.md +107 -0
  30. package/.opencode/agents/aether-guardian.md +107 -0
  31. package/.opencode/agents/aether-includer.md +108 -0
  32. package/.opencode/agents/aether-keeper.md +106 -0
  33. package/.opencode/agents/aether-measurer.md +119 -0
  34. package/.opencode/agents/aether-probe.md +91 -0
  35. package/.opencode/agents/aether-queen.md +72 -19
  36. package/.opencode/agents/aether-route-setter.md +85 -0
  37. package/.opencode/agents/aether-sage.md +98 -0
  38. package/.opencode/agents/aether-scout.md +33 -15
  39. package/.opencode/agents/aether-surveyor-disciplines.md +334 -0
  40. package/.opencode/agents/aether-surveyor-nest.md +272 -0
  41. package/.opencode/agents/aether-surveyor-pathogens.md +209 -0
  42. package/.opencode/agents/aether-surveyor-provisions.md +277 -0
  43. package/.opencode/agents/aether-tracker.md +91 -0
  44. package/.opencode/agents/aether-watcher.md +30 -12
  45. package/.opencode/agents/aether-weaver.md +87 -0
  46. package/.opencode/agents/workers.md +1034 -0
  47. package/.opencode/commands/ant/archaeology.md +44 -26
  48. package/.opencode/commands/ant/build.md +327 -295
  49. package/.opencode/commands/ant/chaos.md +32 -4
  50. package/.opencode/commands/ant/colonize.md +119 -93
  51. package/.opencode/commands/ant/continue.md +98 -10
  52. package/.opencode/commands/ant/council.md +28 -0
  53. package/.opencode/commands/ant/dream.md +24 -0
  54. package/.opencode/commands/ant/entomb.md +73 -1
  55. package/.opencode/commands/ant/feedback.md +8 -2
  56. package/.opencode/commands/ant/flag.md +9 -3
  57. package/.opencode/commands/ant/flags.md +8 -2
  58. package/.opencode/commands/ant/focus.md +8 -2
  59. package/.opencode/commands/ant/help.md +12 -0
  60. package/.opencode/commands/ant/init.md +49 -4
  61. package/.opencode/commands/ant/lay-eggs.md +30 -2
  62. package/.opencode/commands/ant/oracle.md +39 -7
  63. package/.opencode/commands/ant/organize.md +9 -3
  64. package/.opencode/commands/ant/pause-colony.md +54 -1
  65. package/.opencode/commands/ant/phase.md +36 -4
  66. package/.opencode/commands/ant/plan.md +225 -117
  67. package/.opencode/commands/ant/redirect.md +8 -2
  68. package/.opencode/commands/ant/resume-colony.md +51 -26
  69. package/.opencode/commands/ant/seal.md +76 -0
  70. package/.opencode/commands/ant/status.md +50 -20
  71. package/.opencode/commands/ant/swarm.md +108 -104
  72. package/.opencode/commands/ant/tunnels.md +107 -2
  73. package/CHANGELOG.md +21 -0
  74. package/README.md +199 -86
  75. package/bin/cli.js +142 -25
  76. package/bin/generate-commands.sh +100 -16
  77. package/bin/lib/caste-colors.js +5 -5
  78. package/bin/lib/errors.js +16 -0
  79. package/bin/lib/file-lock.js +279 -44
  80. package/bin/lib/state-sync.js +206 -23
  81. package/bin/lib/update-transaction.js +206 -24
  82. package/bin/sync-to-runtime.sh +129 -0
  83. package/package.json +2 -2
  84. package/runtime/CONTEXT.md +160 -0
  85. package/runtime/aether-utils.sh +1421 -55
  86. package/runtime/docs/AETHER-2.0-IMPLEMENTATION-PLAN.md +1343 -0
  87. package/runtime/docs/AETHER-PHEROMONE-SYSTEM-MASTER-SPEC.md +2642 -0
  88. package/runtime/docs/PHEROMONE-INJECTION.md +240 -0
  89. package/runtime/docs/PHEROMONE-INTEGRATION.md +192 -0
  90. package/runtime/docs/PHEROMONE-SYSTEM-DESIGN.md +426 -0
  91. package/runtime/docs/README.md +94 -0
  92. package/runtime/docs/VISUAL-OUTPUT-SPEC.md +219 -0
  93. package/runtime/docs/biological-reference.md +272 -0
  94. package/runtime/docs/codebase-review.md +399 -0
  95. package/runtime/docs/command-sync.md +164 -0
  96. package/runtime/docs/implementation-learnings.md +89 -0
  97. package/runtime/docs/known-issues.md +217 -0
  98. package/runtime/docs/namespace.md +148 -0
  99. package/runtime/docs/planning-discipline.md +159 -0
  100. package/runtime/lib/queen-utils.sh +729 -0
  101. package/runtime/model-profiles.yaml +100 -0
  102. package/runtime/recover.sh +136 -0
  103. package/runtime/templates/QUEEN.md.template +79 -0
  104. package/runtime/utils/atomic-write.sh +5 -5
  105. package/runtime/utils/chamber-utils.sh +6 -3
  106. package/runtime/utils/error-handler.sh +200 -0
  107. package/runtime/utils/queen-to-md.xsl +395 -0
  108. package/runtime/utils/spawn-tree.sh +428 -0
  109. package/runtime/utils/spawn-with-model.sh +56 -0
  110. package/runtime/utils/state-loader.sh +215 -0
  111. package/runtime/utils/swarm-display.sh +5 -5
  112. package/runtime/utils/watch-spawn-tree.sh +90 -22
  113. package/runtime/utils/xml-compose.sh +247 -0
  114. package/runtime/utils/xml-core.sh +186 -0
  115. package/runtime/utils/xml-utils.sh +2161 -0
  116. package/runtime/verification-loop.md +1 -1
  117. package/runtime/workers-new-castes.md +516 -0
  118. package/runtime/workers.md +20 -8
  119. package/.aether/visualizations/anthill-stages/brood-stable.txt +0 -26
  120. package/.aether/visualizations/anthill-stages/crowned-anthill.txt +0 -30
  121. package/.aether/visualizations/anthill-stages/first-mound.txt +0 -18
  122. package/.aether/visualizations/anthill-stages/open-chambers.txt +0 -24
  123. package/.aether/visualizations/anthill-stages/sealed-chambers.txt +0 -28
  124. package/.aether/visualizations/anthill-stages/ventilated-nest.txt +0 -27
@@ -0,0 +1,2161 @@
1
+ #!/bin/bash
2
+ # XML Utilities for Aether Colony
3
+ # Hybrid JSON/XML architecture with pheromone exchange format support
4
+ #
5
+ # Usage: source .aether/utils/xml-utils.sh
6
+ # xml-validate <xml_file> <xsd_file>
7
+ # xml-to-json <xml_file>
8
+ # json-to-xml <json_file> [root_element]
9
+ # xml-query <xml_file> <xpath_expression>
10
+ # xml-merge <output_file> <input_files...>
11
+ #
12
+ # All functions return JSON status like other aether-utils
13
+
14
+ set -euo pipefail
15
+
16
+ # ============================================================================
17
+ # Feature Detection
18
+ # ============================================================================
19
+
20
+ # Check for required XML tools
21
+ XMLLINT_AVAILABLE=false
22
+ XMLSTARLET_AVAILABLE=false
23
+ XSLTPROC_AVAILABLE=false
24
+ XML2JSON_AVAILABLE=false
25
+
26
+ if command -v xmllint >/dev/null 2>&1; then
27
+ XMLLINT_AVAILABLE=true
28
+ fi
29
+
30
+ if command -v xmlstarlet >/dev/null 2>&1; then
31
+ XMLSTARLET_AVAILABLE=true
32
+ fi
33
+
34
+ if command -v xsltproc >/dev/null 2>&1; then
35
+ XSLTPROC_AVAILABLE=true
36
+ fi
37
+
38
+ if command -v xml2json >/dev/null 2>&1; then
39
+ XML2JSON_AVAILABLE=true
40
+ fi
41
+
42
+ # ============================================================================
43
+ # JSON Output Helpers
44
+ # ============================================================================
45
+
46
+ xml_json_ok() { printf '{"ok":true,"result":%s}\n' "$1"; }
47
+ xml_json_err() {
48
+ local message="${2:-$1}"
49
+ printf '{"ok":false,"error":"%s"}\n' "$message" >&2
50
+ return 1
51
+ }
52
+
53
+ # ============================================================================
54
+ # Core XML Functions
55
+ # ============================================================================
56
+
57
+ # xml-validate: Validate XML against XSD schema using xmllint
58
+ # Usage: xml-validate <xml_file> <xsd_file>
59
+ # Returns: {"ok":true,"result":{"valid":true,"errors":[]}} or error
60
+ xml-validate() {
61
+ local xml_file="${1:-}"
62
+ local xsd_file="${2:-}"
63
+
64
+ # Validate arguments
65
+ [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
66
+ [[ -z "$xsd_file" ]] && { xml_json_err "Missing XSD schema file argument"; return 1; }
67
+ [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
68
+ [[ -f "$xsd_file" ]] || { xml_json_err "XSD schema file not found: $xsd_file"; return 1; }
69
+
70
+ # Check for xmllint
71
+ if [[ "$XMLLINT_AVAILABLE" != "true" ]]; then
72
+ xml_json_err "xmllint not available. Install libxml2 utilities."
73
+ return 1
74
+ fi
75
+
76
+ # Validate XML against XSD (with XXE protection)
77
+ local errors
78
+ errors=$(xmllint --nonet --noent --noout --schema "$xsd_file" "$xml_file" 2>&1) && {
79
+ xml_json_ok '{"valid":true,"errors":[]}'
80
+ return 0
81
+ } || {
82
+ # Parse errors into JSON array
83
+ local error_json
84
+ error_json=$(echo "$errors" | jq -R -s 'split("\n") | map(select(length > 0))')
85
+ xml_json_ok "{\"valid\":false,\"errors\":$error_json}"
86
+ return 0
87
+ }
88
+ }
89
+
90
+ # xml-well-formed: Check if XML is well-formed (no schema validation)
91
+ # Usage: xml-well-formed <xml_file>
92
+ # Returns: {"ok":true,"result":{"well_formed":true,"error":null}} or error details
93
+ xml-well-formed() {
94
+ local xml_file="${1:-}"
95
+
96
+ [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
97
+ [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
98
+
99
+ if [[ "$XMLLINT_AVAILABLE" != "true" ]]; then
100
+ xml_json_err "xmllint not available. Install libxml2 utilities."
101
+ return 1
102
+ fi
103
+
104
+ local error
105
+ error=$(xmllint --nonet --noent --noout "$xml_file" 2>&1) && {
106
+ xml_json_ok '{"well_formed":true,"error":null}'
107
+ return 0
108
+ } || {
109
+ local escaped_error
110
+ escaped_error=$(echo "$error" | jq -Rs '.[:-1]')
111
+ xml_json_ok "{\"well_formed\":false,\"error\":$escaped_error}"
112
+ return 0
113
+ }
114
+ }
115
+
116
+ # xml-to-json: Convert XML to JSON using available tools
117
+ # Usage: xml-to-json <xml_file> [options]
118
+ # Options: --pretty (pretty print output)
119
+ # Returns: {"ok":true,"result":<json_object>}
120
+ xml-to-json() {
121
+ local xml_file="${1:-}"
122
+ local pretty=false
123
+
124
+ # Parse optional arguments
125
+ shift || true
126
+ while [[ $# -gt 0 ]]; do
127
+ case "$1" in
128
+ --pretty) pretty=true; shift ;;
129
+ *) shift ;;
130
+ esac
131
+ done
132
+
133
+ [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
134
+ [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
135
+
136
+ # Check well-formedness first
137
+ local well_formed_result
138
+ well_formed_result=$(xml-well-formed "$xml_file" 2>/dev/null)
139
+ if ! echo "$well_formed_result" | jq -e '.result.well_formed' >/dev/null 2>&1; then
140
+ xml_json_err "XML is not well-formed"
141
+ return 1
142
+ fi
143
+
144
+ # Try xml2json if available (npm package)
145
+ if [[ "$XML2JSON_AVAILABLE" == "true" ]]; then
146
+ local json_output
147
+ if json_output=$(xml2json "$xml_file" 2>/dev/null); then
148
+ if [[ "$pretty" == "true" ]]; then
149
+ json_output=$(echo "$json_output" | jq '.')
150
+ fi
151
+ xml_json_ok "$(echo "$json_output" | jq -Rs '.[:-1]')"
152
+ return 0
153
+ fi
154
+ fi
155
+
156
+ # Fallback: Use xsltproc with built-in XSLT if available
157
+ if [[ "$XSLTPROC_AVAILABLE" == "true" ]]; then
158
+ local xslt_script
159
+ xslt_script=$(cat << 'XSLT'
160
+ <?xml version="1.0"?>
161
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
162
+ <xsl:output method="text"/>
163
+ <xsl:template match="/">
164
+ <xsl:text>{"root":</xsl:text>
165
+ <xsl:apply-templates select="*"/>
166
+ <xsl:text>}</xsl:text>
167
+ </xsl:template>
168
+ <xsl:template match="*">
169
+ <xsl:text>{"</xsl:text>
170
+ <xsl:value-of select="name()"/>
171
+ <xsl:text>":</xsl:text>
172
+ <xsl:choose>
173
+ <xsl:when test="count(*) > 0">
174
+ <xsl:text>[</xsl:text>
175
+ <xsl:apply-templates select="*"/>
176
+ <xsl:text>]</xsl:text>
177
+ </xsl:when>
178
+ <xsl:otherwise>
179
+ <xsl:text>"</xsl:text>
180
+ <xsl:value-of select="."/>
181
+ <xsl:text>"</xsl:text>
182
+ </xsl:otherwise>
183
+ </xsl:choose>
184
+ <xsl:text>}</xsl:text>
185
+ <xsl:if test="position() != last()">,</xsl:if>
186
+ </xsl:template>
187
+ </xsl:stylesheet>
188
+ XSLT
189
+ )
190
+ local json_result
191
+ json_result=$(echo "$xslt_script" | xsltproc - "$xml_file" 2>/dev/null) || {
192
+ xml_json_err "XSLT conversion failed"
193
+ return 1
194
+ }
195
+ xml_json_ok "$json_result"
196
+ return 0
197
+ fi
198
+
199
+ # Last resort: Use xmlstarlet if available
200
+ if [[ "$XMLSTARLET_AVAILABLE" == "true" ]]; then
201
+ # xmlstarlet can convert to various formats, we'll use sel to extract structure
202
+ local json_result
203
+ json_result=$(xmlstarlet sel -t -m "/" -o '{"root":{' -m "*" -v "name()" -o ':"' -v "." -o '"' -b -o '}}' "$xml_file" 2>/dev/null) || {
204
+ xml_json_err "xmlstarlet conversion failed"
205
+ return 1
206
+ }
207
+ xml_json_ok "$json_result"
208
+ return 0
209
+ fi
210
+
211
+ xml_json_err "No XML to JSON conversion tool available. Install xml2json, xsltproc, or xmlstarlet."
212
+ return 1
213
+ }
214
+
215
+ # json-to-xml: Convert JSON to XML
216
+ # Usage: json-to-xml <json_file> [root_element]
217
+ # Returns: {"ok":true,"result":{"xml":"<root>...</root>"}}
218
+ json-to-xml() {
219
+ local json_file="${1:-}"
220
+ local root_element="${2:-root}"
221
+
222
+ [[ -z "$json_file" ]] && { xml_json_err "Missing JSON file argument"; return 1; }
223
+ [[ -f "$json_file" ]] || { xml_json_err "JSON file not found: $json_file"; return 1; }
224
+
225
+ # Validate JSON first
226
+ if ! jq empty "$json_file" 2>/dev/null; then
227
+ xml_json_err "Invalid JSON file: $json_file"
228
+ return 1
229
+ fi
230
+
231
+ # Build XML using jq to generate structure
232
+ local xml_output
233
+ xml_output=$(jq -r --arg root "$root_element" '
234
+ def to_xml:
235
+ if type == "object" then
236
+ to_entries | map(
237
+ "<\(.key)>\(.value | to_xml)</\(.key)>"
238
+ ) | join("")
239
+ elif type == "array" then
240
+ map("<item>\(. | to_xml)</item>") | join("")
241
+ elif type == "string" then
242
+ .
243
+ elif type == "number" then
244
+ tostring
245
+ elif type == "boolean" then
246
+ tostring
247
+ elif type == "null" then
248
+ ""
249
+ else
250
+ tostring
251
+ end;
252
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<\($root)>\n" + (to_xml) + "\n</\($root)>"
253
+ ' "$json_file" 2>/dev/null) || {
254
+ xml_json_err "JSON to XML conversion failed"
255
+ return 1
256
+ }
257
+
258
+ # Escape the XML for JSON output
259
+ local escaped_xml
260
+ escaped_xml=$(echo "$xml_output" | jq -Rs '.')
261
+ xml_json_ok "{\"xml\":$escaped_xml}"
262
+ }
263
+
264
+ # xml-query: XPath query function using XMLStarlet
265
+ # Usage: xml-query <xml_file> <xpath_expression>
266
+ # Returns: {"ok":true,"result":{"matches":[...],"count":N}}
267
+ xml-query() {
268
+ local xml_file="${1:-}"
269
+ local xpath="${2:-}"
270
+
271
+ [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
272
+ [[ -z "$xpath" ]] && { xml_json_err "Missing XPath expression argument"; return 1; }
273
+ [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
274
+
275
+ if [[ "$XMLSTARLET_AVAILABLE" != "true" ]]; then
276
+ xml_json_err "xmlstarlet not available. Install xmlstarlet for XPath queries."
277
+ return 1
278
+ fi
279
+
280
+ # Execute XPath query
281
+ local results
282
+ results=$(xmlstarlet sel -t -m "$xpath" -v "." -n "$xml_file" 2>/dev/null) || {
283
+ xml_json_err "XPath query failed: $xpath"
284
+ return 1
285
+ }
286
+
287
+ # Convert results to JSON array
288
+ local json_array
289
+ json_array=$(echo "$results" | jq -R -s 'split("\n") | map(select(length > 0))')
290
+ local count
291
+ count=$(echo "$json_array" | jq 'length')
292
+
293
+ xml_json_ok "{\"matches\":$json_array,\"count\":$count}"
294
+ }
295
+
296
+ # xml-query-attr: Query for specific attribute values
297
+ # Usage: xml-query-attr <xml_file> <element> <attribute>
298
+ # Returns: {"ok":true,"result":{"attributes":[...],"count":N}}
299
+ xml-query-attr() {
300
+ local xml_file="${1:-}"
301
+ local element="${2:-}"
302
+ local attr="${3:-}"
303
+
304
+ [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
305
+ [[ -z "$element" ]] && { xml_json_err "Missing element argument"; return 1; }
306
+ [[ -z "$attr" ]] && { xml_json_err "Missing attribute argument"; return 1; }
307
+ [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
308
+
309
+ if [[ "$XMLSTARLET_AVAILABLE" != "true" ]]; then
310
+ xml_json_err "xmlstarlet not available. Install xmlstarlet for attribute queries."
311
+ return 1
312
+ fi
313
+
314
+ local results
315
+ results=$(xmlstarlet sel -t -m "//$element" -v "@$attr" -n "$xml_file" 2>/dev/null) || {
316
+ xml_json_err "Attribute query failed: $element/@$attr"
317
+ return 1
318
+ }
319
+
320
+ local json_array
321
+ json_array=$(echo "$results" | jq -R -s 'split("\n") | map(select(length > 0))')
322
+ local count
323
+ count=$(echo "$json_array" | jq 'length')
324
+
325
+ xml_json_ok "{\"attributes\":$json_array,\"count\":$count}"
326
+ }
327
+
328
+ # xml-merge: XInclude document merging
329
+ # Usage: xml-merge <output_file> <main_xml_file> [included_files...]
330
+ # Returns: {"ok":true,"result":{"merged":true,"output":"<path>"}}
331
+ xml-merge() {
332
+ local output_file="${1:-}"
333
+ local main_xml="${2:-}"
334
+
335
+ [[ -z "$output_file" ]] && { xml_json_err "Missing output file argument"; return 1; }
336
+ [[ -z "$main_xml" ]] && { xml_json_err "Missing main XML file argument"; return 1; }
337
+ [[ -f "$main_xml" ]] || { xml_json_err "Main XML file not found: $main_xml"; return 1; }
338
+
339
+ # Check well-formedness of main file
340
+ local well_formed_result
341
+ well_formed_result=$(xml-well-formed "$main_xml" 2>/dev/null)
342
+ if ! echo "$well_formed_result" | jq -e '.result.well_formed' >/dev/null 2>&1; then
343
+ xml_json_err "Main XML file is not well-formed"
344
+ return 1
345
+ fi
346
+
347
+ # Use xmllint for XInclude processing if available
348
+ if [[ "$XMLLINT_AVAILABLE" == "true" ]]; then
349
+ local merged
350
+ merged=$(xmllint --nonet --noent --xinclude "$main_xml" 2>/dev/null) || {
351
+ xml_json_err "XInclude merge failed"
352
+ return 1
353
+ }
354
+
355
+ # Write output
356
+ echo "$merged" > "$output_file"
357
+ local escaped_output
358
+ escaped_output=$(echo "$output_file" | jq -Rs '.[:-1]')
359
+ xml_json_ok "{\"merged\":true,\"output\":$escaped_output}"
360
+ return 0
361
+ fi
362
+
363
+ # Fallback: Simple file concatenation with root element wrapping
364
+ # This is a basic implementation - full XInclude requires xmllint
365
+ local temp_dir
366
+ temp_dir=$(mktemp -d)
367
+
368
+ # Extract root element from main file
369
+ local root_element
370
+ root_element=$(grep -oP '(?<=<)[^>\s?/]+' "$main_xml" | head -1)
371
+
372
+ {
373
+ echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
374
+ echo "<$root_element>"
375
+ cat "$main_xml" | sed '1,/<'$root_element'>/d' | sed '/<\/'$root_element'>/,$d'
376
+ echo "</$root_element>"
377
+ } > "$output_file"
378
+
379
+ rm -rf "$temp_dir"
380
+
381
+ local escaped_output
382
+ escaped_output=$(echo "$output_file" | jq -Rs '.[:-1]')
383
+ xml_json_ok "{\"merged\":true,\"output\":$escaped_output,\"note\":\"Basic merge without XInclude\"}"
384
+ }
385
+
386
+ # xml-format: Pretty-print XML file
387
+ # Usage: xml-format <xml_file> [output_file]
388
+ # Returns: {"ok":true,"result":{"formatted":true,"output":"<path>"}}
389
+ xml-format() {
390
+ local xml_file="${1:-}"
391
+ local output_file="${2:-}"
392
+
393
+ [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
394
+ [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
395
+
396
+ if [[ "$XMLLINT_AVAILABLE" != "true" ]]; then
397
+ xml_json_err "xmllint not available. Install libxml2 utilities."
398
+ return 1
399
+ fi
400
+
401
+ # Determine output destination
402
+ local target="${output_file:-$xml_file}"
403
+
404
+ # Format XML with proper indentation (with XXE protection)
405
+ local formatted
406
+ formatted=$(xmllint --nonet --noent --format "$xml_file" 2>/dev/null) || {
407
+ xml_json_err "XML formatting failed"
408
+ return 1
409
+ }
410
+
411
+ echo "$formatted" > "$target"
412
+
413
+ local escaped_output
414
+ escaped_output=$(echo "$target" | jq -Rs '.[:-1]')
415
+ xml_json_ok "{\"formatted\":true,\"output\":$escaped_output}"
416
+ }
417
+
418
+ # xml-escape: Escape special characters for XML content
419
+ # Usage: xml-escape "string with <special> & characters"
420
+ # Returns: {"ok":true,"result":"escaped string"}
421
+ xml-escape() {
422
+ local input="${1:-}"
423
+
424
+ # Escape XML special characters
425
+ local escaped
426
+ escaped=$(echo "$input" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g; s/'"'"'/\&apos;/g')
427
+
428
+ local escaped_json
429
+ escaped_json=$(echo "$escaped" | jq -Rs '.[:-1]')
430
+ xml_json_ok "$escaped_json"
431
+ }
432
+
433
+ # xml-unescape: Unescape XML entities
434
+ # Usage: xml-unescape "string with &lt;special&gt; entities"
435
+ # Returns: {"ok":true,"result":"unescaped string"}
436
+ xml-unescape() {
437
+ local input="${1:-}"
438
+
439
+ # Unescape XML entities
440
+ local unescaped
441
+ unescaped=$(echo "$input" | sed 's/\&lt;/</g; s/\&gt;/>/g; s/\&quot;/"/g; s/\&apos;/'"'"'/g; s/\&amp;/\&/g')
442
+
443
+ local unescaped_json
444
+ unescaped_json=$(echo "$unescaped" | jq -Rs '.[:-1]')
445
+ xml_json_ok "$unescaped_json"
446
+ }
447
+
448
+ # xml-detect-tools: Detect available XML tools
449
+ # Usage: xml-detect-tools
450
+ # Returns: {"ok":true,"result":{"xmllint":true,"xmlstarlet":false,...}}
451
+ xml-detect-tools() {
452
+ xml_json_ok "{\"xmllint\":$XMLLINT_AVAILABLE,\"xmlstarlet\":$XMLSTARLET_AVAILABLE,\"xsltproc\":$XSLTPROC_AVAILABLE,\"xml2json\":$XML2JSON_AVAILABLE}"
453
+ }
454
+
455
+ # ============================================================================
456
+ # Pheromone Exchange Format (Hybrid JSON/XML)
457
+ # ============================================================================
458
+
459
+ # pheromone-to-xml: Convert pheromone JSON to XML format with full XSD schema support
460
+ # Usage: pheromone-to-xml <pheromone_json_file> [output_xml_file] [xsd_schema_file]
461
+ # pheromone_json_file: Path to pheromone JSON (supports both single signal and full pheromones format)
462
+ # output_xml_file: Optional path to write XML output (if omitted, returns XML in result)
463
+ # xsd_schema_file: Optional path to XSD schema for validation (default: .aether/schemas/pheromone.xsd)
464
+ # Returns: {"ok":true,"result":{"xml":"<pheromones>...</pheromones>","validated":true,"path":"..."}}
465
+ pheromone-to-xml() {
466
+ local json_file="${1:-}"
467
+ local output_xml="${2:-}"
468
+ local xsd_file="${3:-.aether/schemas/pheromone.xsd}"
469
+
470
+ [[ -z "$json_file" ]] && { xml_json_err "Missing JSON file argument"; return 1; }
471
+ [[ -f "$json_file" ]] || { xml_json_err "JSON file not found: $json_file"; return 1; }
472
+
473
+ # Validate JSON
474
+ if ! jq empty "$json_file" 2>/dev/null; then
475
+ xml_json_err "Invalid JSON file: $json_file"
476
+ return 1
477
+ fi
478
+
479
+ # Detect JSON format: single signal or full pheromones structure
480
+ local has_signals
481
+ has_signals=$(jq 'has("signals")' "$json_file" 2>/dev/null)
482
+
483
+ # Generate ISO timestamp
484
+ local generated_at
485
+ generated_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
486
+
487
+ # Extract metadata from JSON
488
+ local version colony_id
489
+ version=$(jq -r '.version // "1.0.0"' "$json_file")
490
+ colony_id=$(jq -r '.colony_id // "unknown"' "$json_file")
491
+
492
+ # Build XML header with proper namespace
493
+ local xml_output
494
+ xml_output="<?xml version=\"1.0\" encoding=\"UTF-8\"?>
495
+ <pheromones xmlns=\"http://aether.colony/schemas/pheromones\"
496
+ xmlns:ph=\"http://aether.colony/schemas/pheromones\"
497
+ version=\"$version\"
498
+ generated_at=\"$generated_at\"
499
+ colony_id=\"$colony_id\">"
500
+
501
+ # Add metadata section
502
+ local source_type source_version context
503
+ source_type=$(jq -r '.metadata.source.type // "system"' "$json_file" 2>/dev/null || echo "system")
504
+ source_version=$(jq -r ".metadata.source.version // \"$version\"" "$json_file" 2>/dev/null || echo "$version")
505
+ context=$(jq -r '.metadata.context // "Colony pheromone signal conversion"' "$json_file" 2>/dev/null || echo "Colony pheromone signal conversion")
506
+
507
+ xml_output="$xml_output
508
+ <metadata>
509
+ <source type=\"$source_type\" version=\"$source_version\">aether-pheromone-converter</source>
510
+ <context>$(echo "$context" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')</context>
511
+ </metadata>"
512
+
513
+ # Process signals - either from array or wrap single signal
514
+ local signal_count=0
515
+ local sig_array_length
516
+ sig_array_length=$(jq '.signals | length' "$json_file" 2>/dev/null || echo "0")
517
+
518
+ if [[ "$has_signals" == "true" && "$sig_array_length" -gt 0 ]]; then
519
+ local sig_idx=0
520
+ while [[ $sig_idx -lt $sig_array_length ]]; do
521
+ local signal
522
+ signal=$(jq -c ".signals[$sig_idx]" "$json_file" 2>/dev/null)
523
+ [[ -n "$signal" ]] || { ((sig_idx++)); continue; }
524
+
525
+ # Extract signal fields with defaults
526
+ local sig_id sig_type priority source created_at expires_at active
527
+ sig_id=$(echo "$signal" | jq -r '.id // "sig_'"$(date +%s)"'_'"$signal_count"'"')
528
+ sig_type=$(echo "$signal" | jq -r '.type // "FOCUS"' | tr '[:lower:]' '[:upper:]')
529
+ priority=$(echo "$signal" | jq -r '.priority // "normal"' | tr '[:upper:]' '[:lower:]')
530
+ source=$(echo "$signal" | jq -r '.source // "system"')
531
+ created_at=$(echo "$signal" | jq -r '.created_at // "'"$generated_at"'"')
532
+ expires_at=$(echo "$signal" | jq -r '.expires_at // empty')
533
+ active=$(echo "$signal" | jq -r '.active // true')
534
+
535
+ # Validate signal type against schema enum
536
+ case "$sig_type" in
537
+ FOCUS|REDIRECT|FEEDBACK) ;;
538
+ *) sig_type="FOCUS" ;;
539
+ esac
540
+
541
+ # Validate priority against schema enum
542
+ case "$priority" in
543
+ critical|high|normal|low) ;;
544
+ *) priority="normal" ;;
545
+ esac
546
+
547
+ # XML escape ID and source
548
+ local escaped_id escaped_source
549
+ escaped_id=$(echo "$sig_id" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
550
+ escaped_source=$(echo "$source" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
551
+
552
+ # Build signal element
553
+ xml_output="$xml_output
554
+ <signal id=\"$escaped_id\"
555
+ type=\"$sig_type\"
556
+ priority=\"$priority\"
557
+ source=\"$escaped_source\"
558
+ created_at=\"$created_at\""
559
+
560
+ # Add optional expires_at if present
561
+ if [[ -n "$expires_at" && "$expires_at" != "null" ]]; then
562
+ xml_output="$xml_output
563
+ expires_at=\"$expires_at\""
564
+ fi
565
+
566
+ xml_output="$xml_output
567
+ active=\"$active\">"
568
+
569
+ # Content section
570
+ local content_text content_data content_format
571
+ content_text=$(echo "$signal" | jq -r '.content.text // .message // ""')
572
+ content_format=$(echo "$signal" | jq -r '.content.data.format // "json"')
573
+
574
+ if [[ -n "$content_text" ]]; then
575
+ local escaped_text
576
+ escaped_text=$(echo "$content_text" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
577
+ xml_output="$xml_output
578
+ <content>
579
+ <text>$escaped_text</text>"
580
+
581
+ # Check for data attachment - convert JSON to XML elements
582
+ local has_data
583
+ has_data=$(echo "$signal" | jq 'has("content") and (.content | has("data"))' 2>/dev/null)
584
+ if [[ "$has_data" == "true" ]]; then
585
+ local data_xml
586
+ data_xml=$(echo "$signal" | jq -r '.content.data | to_entries | map("<\(.key)>\(.value | tostring | gsub("&"; "&amp;") | gsub("<"; "&lt;") | gsub(">"; "&gt;"))</\(.key)>") | join("")' 2>/dev/null)
587
+ xml_output="$xml_output
588
+ <data format=\"$content_format\">$data_xml</data>"
589
+ fi
590
+
591
+ xml_output="$xml_output
592
+ </content>"
593
+ fi
594
+
595
+ # Tags section
596
+ local tags_json
597
+ tags_json=$(echo "$signal" | jq -c '.tags // []')
598
+ if [[ "$tags_json" != "[]" && -n "$tags_json" ]]; then
599
+ xml_output="$xml_output
600
+ <tags>"
601
+ local tag_count
602
+ tag_count=$(echo "$signal" | jq '.tags | length')
603
+ local tag_idx=0
604
+ while [[ $tag_idx -lt $tag_count ]]; do
605
+ local tag
606
+ tag=$(echo "$signal" | jq -c ".tags[$tag_idx]")
607
+ local tag_value tag_weight tag_category
608
+ tag_value=$(echo "$tag" | jq -r '.value // .')
609
+ tag_weight=$(echo "$tag" | jq -r '.weight // "1.0"')
610
+ tag_category=$(echo "$tag" | jq -r '.category // empty')
611
+
612
+ local escaped_tag
613
+ escaped_tag=$(echo "$tag_value" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
614
+
615
+ if [[ -n "$tag_category" && "$tag_category" != "null" ]]; then
616
+ xml_output="$xml_output
617
+ <tag weight=\"$tag_weight\" category=\"$tag_category\">$escaped_tag</tag>"
618
+ else
619
+ xml_output="$xml_output
620
+ <tag weight=\"$tag_weight\">$escaped_tag</tag>"
621
+ fi
622
+ ((tag_idx++))
623
+ done
624
+ xml_output="$xml_output
625
+ </tags>"
626
+ fi
627
+
628
+ # Scope section
629
+ local scope_global scope_castes scope_paths
630
+ scope_global=$(echo "$signal" | jq -r '.scope.global // false')
631
+ xml_output="$xml_output
632
+ <scope global=\"$scope_global\">"
633
+
634
+ # Castes
635
+ local castes_json
636
+ castes_json=$(echo "$signal" | jq -c '.scope.castes // []' 2>/dev/null)
637
+ if [[ "$castes_json" != "[]" && -n "$castes_json" && "$castes_json" != "null" ]]; then
638
+ xml_output="$xml_output
639
+ <castes match=\"any\">"
640
+ local caste_count
641
+ caste_count=$(echo "$signal" | jq '.scope.castes | length' 2>/dev/null)
642
+ local caste_idx=0
643
+ while [[ $caste_idx -lt $caste_count ]]; do
644
+ local caste
645
+ caste=$(echo "$signal" | jq -r ".scope.castes[$caste_idx]" 2>/dev/null)
646
+ # Validate caste against schema enum
647
+ case "$caste" in
648
+ builder|watcher|scout|chaos|oracle|architect|prime|colonizer|route_setter|archaeologist|ambassador|auditor|chronicler|gatekeeper|guardian|includer|keeper|measurer|probe|sage|tracker|weaver)
649
+ xml_output="$xml_output
650
+ <caste>$caste</caste>"
651
+ ;;
652
+ esac
653
+ ((caste_idx++))
654
+ done
655
+ xml_output="$xml_output
656
+ </castes>"
657
+ fi
658
+
659
+ # Paths
660
+ local paths_json
661
+ paths_json=$(echo "$signal" | jq -c '.scope.paths // []' 2>/dev/null)
662
+ if [[ "$paths_json" != "[]" && -n "$paths_json" && "$paths_json" != "null" ]]; then
663
+ xml_output="$xml_output
664
+ <paths match=\"any\">"
665
+ local path_count
666
+ path_count=$(echo "$signal" | jq '.scope.paths | length' 2>/dev/null)
667
+ local path_idx=0
668
+ while [[ $path_idx -lt $path_count ]]; do
669
+ local path
670
+ path=$(echo "$signal" | jq -r ".scope.paths[$path_idx]" 2>/dev/null)
671
+ local escaped_path
672
+ escaped_path=$(echo "$path" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
673
+ xml_output="$xml_output
674
+ <path>$escaped_path</path>"
675
+ ((path_idx++))
676
+ done
677
+ xml_output="$xml_output
678
+ </paths>"
679
+ fi
680
+
681
+ xml_output="$xml_output
682
+ </scope>"
683
+
684
+ xml_output="$xml_output
685
+ </signal>"
686
+ ((signal_count++))
687
+ ((sig_idx++))
688
+ done
689
+ elif [[ "$has_signals" != "true" ]]; then
690
+ # Handle single signal JSON (legacy format)
691
+ local signal
692
+ signal=$(jq -c '.' "$json_file" 2>/dev/null)
693
+ if [[ -n "$signal" ]]; then
694
+ # Extract signal fields with defaults for legacy format
695
+ local sig_id sig_type priority source created_at expires_at active
696
+ sig_id=$(echo "$signal" | jq -r '.id // "sig_'"$(date +%s)"'_0"')
697
+ sig_type=$(echo "$signal" | jq -r '.type // "FOCUS"' | tr '[:lower:]' '[:upper:]')
698
+ priority=$(echo "$signal" | jq -r '.priority // "normal"' | tr '[:upper:]' '[:lower:]')
699
+ source=$(echo "$signal" | jq -r '.source // "system"')
700
+ created_at=$(echo "$signal" | jq -r '.created_at // "'"$generated_at"'"')
701
+ expires_at=$(echo "$signal" | jq -r '.expires_at // empty')
702
+ active=$(echo "$signal" | jq -r '.active // true')
703
+
704
+ # Validate signal type against schema enum
705
+ case "$sig_type" in
706
+ FOCUS|REDIRECT|FEEDBACK) ;;
707
+ *) sig_type="FOCUS" ;;
708
+ esac
709
+
710
+ # Validate priority against schema enum
711
+ case "$priority" in
712
+ critical|high|normal|low) ;;
713
+ *) priority="normal" ;;
714
+ esac
715
+
716
+ # XML escape ID and source
717
+ local escaped_id escaped_source
718
+ escaped_id=$(echo "$sig_id" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
719
+ escaped_source=$(echo "$source" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
720
+
721
+ # Build signal element
722
+ xml_output="$xml_output
723
+ <signal id=\"$escaped_id\"
724
+ type=\"$sig_type\"
725
+ priority=\"$priority\"
726
+ source=\"$escaped_source\"
727
+ created_at=\"$created_at\""
728
+
729
+ # Add optional expires_at if present
730
+ if [[ -n "$expires_at" && "$expires_at" != "null" ]]; then
731
+ xml_output="$xml_output
732
+ expires_at=\"$expires_at\""
733
+ fi
734
+
735
+ xml_output="$xml_output
736
+ active=\"$active\">"
737
+
738
+ # Content section - support legacy "message" field
739
+ local content_text content_format
740
+ content_text=$(echo "$signal" | jq -r '.content.text // .message // ""')
741
+ content_format=$(echo "$signal" | jq -r '.content.data.format // "json"')
742
+
743
+ if [[ -n "$content_text" ]]; then
744
+ local escaped_text
745
+ escaped_text=$(echo "$content_text" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
746
+ xml_output="$xml_output
747
+ <content>
748
+ <text>$escaped_text</text>"
749
+
750
+ # Check for data attachment - convert JSON to XML elements
751
+ local has_data
752
+ has_data=$(echo "$signal" | jq 'has("content") and (.content | has("data"))' 2>/dev/null)
753
+ if [[ "$has_data" == "true" ]]; then
754
+ local data_xml
755
+ data_xml=$(echo "$signal" | jq -r '.content.data | to_entries | map("<\(.key)>\(.value | tostring | gsub("&"; "&amp;") | gsub("<"; "&lt;") | gsub(">"; "&gt;"))</\(.key)>") | join("")' 2>/dev/null)
756
+ xml_output="$xml_output
757
+ <data format=\"$content_format\">$data_xml</data>"
758
+ fi
759
+
760
+ xml_output="$xml_output
761
+ </content>"
762
+ fi
763
+
764
+ # Tags section
765
+ local tags_json
766
+ tags_json=$(echo "$signal" | jq -c '.tags // []')
767
+ if [[ "$tags_json" != "[]" && -n "$tags_json" ]]; then
768
+ xml_output="$xml_output
769
+ <tags>"
770
+ local tag_count
771
+ tag_count=$(echo "$signal" | jq '.tags | length')
772
+ local tag_idx=0
773
+ while [[ $tag_idx -lt $tag_count ]]; do
774
+ local tag
775
+ tag=$(echo "$signal" | jq -c ".tags[$tag_idx]")
776
+ local tag_value tag_weight tag_category
777
+ tag_value=$(echo "$tag" | jq -r '.value // .')
778
+ tag_weight=$(echo "$tag" | jq -r '.weight // "1.0"')
779
+ tag_category=$(echo "$tag" | jq -r '.category // empty')
780
+
781
+ local escaped_tag
782
+ escaped_tag=$(echo "$tag_value" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
783
+
784
+ if [[ -n "$tag_category" && "$tag_category" != "null" ]]; then
785
+ xml_output="$xml_output
786
+ <tag weight=\"$tag_weight\" category=\"$tag_category\">$escaped_tag</tag>"
787
+ else
788
+ xml_output="$xml_output
789
+ <tag weight=\"$tag_weight\">$escaped_tag</tag>"
790
+ fi
791
+ ((tag_idx++))
792
+ done
793
+ xml_output="$xml_output
794
+ </tags>"
795
+ fi
796
+
797
+ # Scope section
798
+ local scope_global
799
+ scope_global=$(echo "$signal" | jq -r '.scope.global // false')
800
+ xml_output="$xml_output
801
+ <scope global=\"$scope_global\">"
802
+
803
+ # Castes
804
+ local castes_json
805
+ castes_json=$(echo "$signal" | jq -c '.scope.castes // []' 2>/dev/null)
806
+ if [[ "$castes_json" != "[]" && -n "$castes_json" && "$castes_json" != "null" ]]; then
807
+ xml_output="$xml_output
808
+ <castes match=\"any\">"
809
+ local caste_count
810
+ caste_count=$(echo "$signal" | jq '.scope.castes | length' 2>/dev/null)
811
+ local caste_idx=0
812
+ while [[ $caste_idx -lt $caste_count ]]; do
813
+ local caste
814
+ caste=$(echo "$signal" | jq -r ".scope.castes[$caste_idx]" 2>/dev/null)
815
+ case "$caste" in
816
+ builder|watcher|scout|chaos|oracle|architect|prime|colonizer|route_setter|archaeologist|ambassador|auditor|chronicler|gatekeeper|guardian|includer|keeper|measurer|probe|sage|tracker|weaver)
817
+ xml_output="$xml_output
818
+ <caste>$caste</caste>"
819
+ ;;
820
+ esac
821
+ ((caste_idx++))
822
+ done
823
+ xml_output="$xml_output
824
+ </castes>"
825
+ fi
826
+
827
+ # Paths
828
+ local paths_json
829
+ paths_json=$(echo "$signal" | jq -c '.scope.paths // []' 2>/dev/null)
830
+ if [[ "$paths_json" != "[]" && -n "$paths_json" && "$paths_json" != "null" ]]; then
831
+ xml_output="$xml_output
832
+ <paths match=\"any\">"
833
+ local path_count
834
+ path_count=$(echo "$signal" | jq '.scope.paths | length' 2>/dev/null)
835
+ local path_idx=0
836
+ while [[ $path_idx -lt $path_count ]]; do
837
+ local path
838
+ path=$(echo "$signal" | jq -r ".scope.paths[$path_idx]" 2>/dev/null)
839
+ local escaped_path
840
+ escaped_path=$(echo "$path" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
841
+ xml_output="$xml_output
842
+ <path>$escaped_path</path>"
843
+ ((path_idx++))
844
+ done
845
+ xml_output="$xml_output
846
+ </paths>"
847
+ fi
848
+
849
+ xml_output="$xml_output
850
+ </scope>"
851
+
852
+ xml_output="$xml_output
853
+ </signal>"
854
+ ((signal_count++))
855
+ fi
856
+ fi
857
+
858
+ # Close root element
859
+ xml_output="$xml_output
860
+ </pheromones>"
861
+
862
+ # Write to file if output path specified
863
+ local output_path=""
864
+ if [[ -n "$output_xml" ]]; then
865
+ local output_dir
866
+ output_dir=$(dirname "$output_xml")
867
+ if [[ ! -d "$output_dir" ]]; then
868
+ mkdir -p "$output_dir" 2>/dev/null || {
869
+ xml_json_err "Cannot create output directory: $output_dir"
870
+ return 1
871
+ }
872
+ fi
873
+ echo "$xml_output" > "$output_xml" || {
874
+ xml_json_err "Failed to write output file: $output_xml"
875
+ return 1
876
+ }
877
+ output_path="$output_xml"
878
+ fi
879
+
880
+ # Validate against XSD schema if available
881
+ local validation_result="false"
882
+ if [[ -f "$xsd_file" && -n "$output_path" ]]; then
883
+ local validation_output
884
+ validation_output=$(xml-validate "$output_path" "$xsd_file" 2>/dev/null)
885
+ if echo "$validation_output" | jq -e '.result.valid' >/dev/null 2>&1; then
886
+ validation_result="true"
887
+ fi
888
+ elif [[ -f "$xsd_file" ]]; then
889
+ # Validate in-memory by writing to temp file
890
+ local temp_xml
891
+ temp_xml=$(mktemp)
892
+ echo "$xml_output" > "$temp_xml"
893
+ local validation_output
894
+ validation_output=$(xml-validate "$temp_xml" "$xsd_file" 2>/dev/null)
895
+ if echo "$validation_output" | jq -e '.result.valid' >/dev/null 2>&1; then
896
+ validation_result="true"
897
+ fi
898
+ rm -f "$temp_xml"
899
+ fi
900
+
901
+ # Build result JSON
902
+ local escaped_xml result_json
903
+ escaped_xml=$(echo "$xml_output" | jq -Rs '.')
904
+ result_json="{\"xml\":$escaped_xml,\"validated\":$validation_result,\"signals\":$signal_count"
905
+ if [[ -n "$output_path" ]]; then
906
+ local escaped_path
907
+ escaped_path=$(echo "$output_path" | jq -Rs '.[:-1]')
908
+ result_json="$result_json,\"path\":$escaped_path"
909
+ fi
910
+ result_json="$result_json}"
911
+
912
+ xml_json_ok "$result_json"
913
+ }
914
+
915
+ # pheromone-from-xml: Parse pheromone XML to JSON
916
+ # Usage: pheromone-from-xml <pheromone_xml_file>
917
+ # Returns: {"ok":true,"result":{"signal":"focus",...}}
918
+ pheromone-from-xml() {
919
+ local xml_file="${1:-}"
920
+
921
+ [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
922
+ [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
923
+
924
+ if [[ "$XMLSTARLET_AVAILABLE" != "true" ]]; then
925
+ xml_json_err "xmlstarlet required for pheromone parsing"
926
+ return 1
927
+ fi
928
+
929
+ # Extract pheromone fields from XML
930
+ local signal priority message timestamp source context
931
+ signal=$(xmlstarlet sel -t -v "/pheromone/signal" "$xml_file" 2>/dev/null || echo "")
932
+ priority=$(xmlstarlet sel -t -v "/pheromone/priority" "$xml_file" 2>/dev/null || echo "normal")
933
+ message=$(xmlstarlet sel -t -v "/pheromone/message" "$xml_file" 2>/dev/null || echo "")
934
+ timestamp=$(xmlstarlet sel -t -v "/pheromone/timestamp" "$xml_file" 2>/dev/null || echo "")
935
+ source=$(xmlstarlet sel -t -v "/pheromone/source" "$xml_file" 2>/dev/null || echo "colony")
936
+ context=$(xmlstarlet sel -t -v "/pheromone/context" "$xml_file" 2>/dev/null || echo "")
937
+
938
+ # Build JSON result
939
+ local json_result
940
+ json_result=$(jq -n \
941
+ --arg signal "$signal" \
942
+ --arg priority "$priority" \
943
+ --arg message "$message" \
944
+ --arg timestamp "$timestamp" \
945
+ --arg source "$source" \
946
+ --arg context "$context" \
947
+ '{signal: $signal, priority: $priority, message: $message, timestamp: $timestamp, source: $source, context: (if $context == "" then null else $context end)}')
948
+
949
+ xml_json_ok "$json_result"
950
+ }
951
+
952
+ # ============================================================================
953
+ # Queen-Wisdom XML Format
954
+ # ============================================================================
955
+
956
+ # queen-wisdom-to-xml: Convert queen wisdom JSON to XML
957
+ # Usage: queen-wisdom-to-xml <wisdom_json_file>
958
+ # Returns: {"ok":true,"result":{"xml":"<queen-wisdom>...</queen-wisdom>"}}
959
+ queen-wisdom-to-xml() {
960
+ local json_file="${1:-}"
961
+
962
+ [[ -z "$json_file" ]] && { xml_json_err "Missing JSON file argument"; return 1; }
963
+ [[ -f "$json_file" ]] || { xml_json_err "JSON file not found: $json_file"; return 1; }
964
+
965
+ if ! jq empty "$json_file" 2>/dev/null; then
966
+ xml_json_err "Invalid JSON file: $json_file"
967
+ return 1
968
+ fi
969
+
970
+ # Convert queen wisdom to structured XML
971
+ local xml_output
972
+ xml_output=$(jq -r '
973
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
974
+ "<queen-wisdom version=\"1.0\">\n" +
975
+ " <directive>" + (.directive // "") + "</directive>\n" +
976
+ (if .patterns then
977
+ " <patterns>\n" +
978
+ (.patterns | map(" <pattern>\(.)</pattern>") | join("\n")) +
979
+ "\n </patterns>\n"
980
+ else "" end) +
981
+ (if .constraints then
982
+ " <constraints>\n" +
983
+ (.constraints | map(" <constraint>\(.)</constraint>") | join("\n")) +
984
+ "\n </constraints>\n"
985
+ else "" end) +
986
+ " <timestamp>" + (.timestamp // (now | todateiso8601)) + "</timestamp>\n" +
987
+ "</queen-wisdom>"
988
+ ' "$json_file" 2>/dev/null) || {
989
+ xml_json_err "Queen wisdom conversion failed"
990
+ return 1
991
+ }
992
+
993
+ local escaped_xml
994
+ escaped_xml=$(echo "$xml_output" | jq -Rs '.')
995
+ xml_json_ok "{\"xml\":$escaped_xml}"
996
+ }
997
+
998
+ # queen-wisdom-from-xml: Parse queen wisdom XML to JSON
999
+ # Usage: queen-wisdom-from-xml <wisdom_xml_file>
1000
+ # Returns: {"ok":true,"result":{"directive":"...",...}}
1001
+ queen-wisdom-from-xml() {
1002
+ local xml_file="${1:-}"
1003
+
1004
+ [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
1005
+ [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
1006
+
1007
+ if [[ "$XMLSTARLET_AVAILABLE" != "true" ]]; then
1008
+ xml_json_err "xmlstarlet required for queen wisdom parsing"
1009
+ return 1
1010
+ fi
1011
+
1012
+ # Extract fields
1013
+ local directive timestamp
1014
+ directive=$(xmlstarlet sel -t -v "/queen-wisdom/directive" "$xml_file" 2>/dev/null || echo "")
1015
+ timestamp=$(xmlstarlet sel -t -v "/queen-wisdom/timestamp" "$xml_file" 2>/dev/null || echo "")
1016
+
1017
+ # Extract arrays
1018
+ local patterns_json constraints_json
1019
+ patterns_json=$(xmlstarlet sel -t -m "/queen-wisdom/patterns/pattern" -v "." -n "$xml_file" 2>/dev/null | jq -R -s 'split("\n") | map(select(length > 0))')
1020
+ constraints_json=$(xmlstarlet sel -t -m "/queen-wisdom/constraints/constraint" -v "." -n "$xml_file" 2>/dev/null | jq -R -s 'split("\n") | map(select(length > 0))')
1021
+
1022
+ # Build result
1023
+ local json_result
1024
+ json_result=$(jq -n \
1025
+ --arg directive "$directive" \
1026
+ --arg timestamp "$timestamp" \
1027
+ --argjson patterns "$patterns_json" \
1028
+ --argjson constraints "$constraints_json" \
1029
+ '{directive: $directive, timestamp: $timestamp, patterns: $patterns, constraints: $constraints}')
1030
+
1031
+ xml_json_ok "$json_result"
1032
+ }
1033
+
1034
+ # ============================================================================
1035
+ # Multi-Colony Registry XML
1036
+ # ============================================================================
1037
+
1038
+ # registry-to-xml: Convert colony registry JSON to XML
1039
+ # Usage: registry-to-xml <registry_json_file>
1040
+ # Returns: {"ok":true,"result":{"xml":"<colony-registry>...</colony-registry>"}}
1041
+ registry-to-xml() {
1042
+ local json_file="${1:-}"
1043
+
1044
+ [[ -z "$json_file" ]] && { xml_json_err "Missing JSON file argument"; return 1; }
1045
+ [[ -f "$json_file" ]] || { xml_json_err "JSON file not found: $json_file"; return 1; }
1046
+
1047
+ if ! jq empty "$json_file" 2>/dev/null; then
1048
+ xml_json_err "Invalid JSON file: $json_file"
1049
+ return 1
1050
+ fi
1051
+
1052
+ # Convert registry to XML
1053
+ local xml_output
1054
+ xml_output=$(jq -r '
1055
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
1056
+ "<colony-registry version=\"1.0\" generated=\"" + (now | todateiso8601) + "\">\n" +
1057
+ (if .colonies then
1058
+ (.colonies | map(
1059
+ " <colony id=\"" + .id + "\">\n" +
1060
+ " <name>" + (.name // "") + "</name>\n" +
1061
+ " <status>" + (.status // "unknown") + "</status>\n" +
1062
+ " <location>" + (.location // "") + "</location>\n" +
1063
+ (if .pheromones then
1064
+ " <pheromones>\n" +
1065
+ (.pheromones | map(
1066
+ " <pheromone signal=\"" + .signal + "\">" + (.message // "") + "</pheromone>"
1067
+ ) | join("\n")) +
1068
+ "\n </pheromones>\n"
1069
+ else "" end) +
1070
+ " </colony>"
1071
+ ) | join("\n")) + "\n"
1072
+ else "" end) +
1073
+ "</colony-registry>"
1074
+ ' "$json_file" 2>/dev/null) || {
1075
+ xml_json_err "Registry conversion failed"
1076
+ return 1
1077
+ }
1078
+
1079
+ local escaped_xml
1080
+ escaped_xml=$(echo "$xml_output" | jq -Rs '.')
1081
+ xml_json_ok "{\"xml\":$escaped_xml}"
1082
+ }
1083
+
1084
+ # registry-from-xml: Parse colony registry XML to JSON
1085
+ # Usage: registry-from-xml <registry_xml_file>
1086
+ # Returns: {"ok":true,"result":{"colonies":[...]}}
1087
+ registry-from-xml() {
1088
+ local xml_file="${1:-}"
1089
+
1090
+ [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
1091
+ [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
1092
+
1093
+ if [[ "$XMLSTARLET_AVAILABLE" != "true" ]]; then
1094
+ xml_json_err "xmlstarlet required for registry parsing"
1095
+ return 1
1096
+ fi
1097
+
1098
+ # Extract colonies
1099
+ local colonies_json
1100
+ colonies_json=$(xmlstarlet sel -t -m "/colony-registry/colony" \
1101
+ -v "@id" -o '|' \
1102
+ -v "name" -o '|' \
1103
+ -v "status" -o '|' \
1104
+ -v "location" -n \
1105
+ "$xml_file" 2>/dev/null | \
1106
+ awk -F'|' 'NF>=3 {
1107
+ printf "{\"id\":\"%s\",\"name\":\"%s\",\"status\":\"%s\",\"location\":\"%s\"}", $1, $2, $3, $4
1108
+ }' | \
1109
+ jq -s '.')
1110
+
1111
+ local json_result
1112
+ json_result=$(jq -n --argjson colonies "$colonies_json" '{colonies: $colonies}')
1113
+
1114
+ xml_json_ok "$json_result"
1115
+ }
1116
+
1117
+ # ============================================================================
1118
+ # Pheromone Export to Eternal Memory
1119
+ # ============================================================================
1120
+
1121
+ # pheromone-export: Export pheromones to eternal XML format
1122
+ # Usage: pheromone-export [input_json] [output_xml] [session_id]
1123
+ # input_json: Path to pheromones.json (default: .aether/data/pheromones.json)
1124
+ # output_xml: Path to output XML (default: ~/.aether/eternal/pheromones.xml)
1125
+ # session_id: Colony session ID for namespace generation (optional, auto-detected from JSON)
1126
+ # Returns: {"ok":true,"result":{"exported":true,"path":"...","signals":N,"namespace":"..."}} or error
1127
+ pheromone-export() {
1128
+ local input_json="${1:-.aether/data/pheromones.json}"
1129
+ local output_xml="${2:-$HOME/.aether/eternal/pheromones.xml}"
1130
+ local session_id="${3:-}"
1131
+ local schema_file="${4:-.aether/schemas/pheromone.xsd}"
1132
+
1133
+ # Validate input file exists
1134
+ [[ -f "$input_json" ]] || { xml_json_err "Pheromone JSON file not found: $input_json"; return 1; }
1135
+
1136
+ # Validate JSON
1137
+ if ! jq empty "$input_json" 2>/dev/null; then
1138
+ xml_json_err "Invalid JSON in pheromone file: $input_json"
1139
+ return 1
1140
+ fi
1141
+
1142
+ # Get absolute paths for schema validation
1143
+ local abs_schema
1144
+ abs_schema="$(cd "$(dirname "$schema_file")" && pwd)/$(basename "$schema_file")" 2>/dev/null || abs_schema="$schema_file"
1145
+
1146
+ # Generate ISO timestamp for XML
1147
+ local generated_at
1148
+ generated_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1149
+
1150
+ # Get version and colony_id from JSON
1151
+ local version colony_id
1152
+ version=$(jq -r '.version // "1.0.0"' "$input_json")
1153
+ colony_id=$(jq -r '.colony_id // "unknown"' "$input_json")
1154
+
1155
+ # Auto-detect session_id from JSON if not provided
1156
+ if [[ -z "$session_id" ]]; then
1157
+ session_id=$(jq -r '.session_id // .colony_id // ""' "$input_json")
1158
+ fi
1159
+
1160
+ # Generate colony namespace if session_id available
1161
+ local colony_namespace=""
1162
+ local colony_prefix=""
1163
+ if [[ -n "$session_id" ]]; then
1164
+ local ns_result
1165
+ ns_result=$(generate-colony-namespace "$session_id" 2>/dev/null)
1166
+ if echo "$ns_result" | jq -e '.ok' >/dev/null 2>&1; then
1167
+ colony_namespace=$(echo "$ns_result" | jq -r '.result.namespace')
1168
+ colony_prefix=$(echo "$ns_result" | jq -r '.result.prefix')
1169
+ fi
1170
+ fi
1171
+
1172
+ # Build XML header with proper namespace
1173
+ local xml_output
1174
+ xml_output="<?xml version=\"1.0\" encoding=\"UTF-8\"?>
1175
+ <pheromones xmlns=\"http://aether.colony/schemas/pheromones\"
1176
+ xmlns:ph=\"http://aether.colony/schemas/pheromones\""
1177
+
1178
+ # Add colony namespace if available
1179
+ if [[ -n "$colony_namespace" ]]; then
1180
+ xml_output="$xml_output
1181
+ xmlns:col=\"$colony_namespace\"
1182
+ col:session=\"$session_id\"
1183
+ col:prefix=\"$colony_prefix\""
1184
+ fi
1185
+
1186
+ xml_output="$xml_output
1187
+ version=\"$version\"
1188
+ generated_at=\"$generated_at\"
1189
+ colony_id=\"$colony_id\">
1190
+ <metadata>
1191
+ <source type=\"system\" version=\"$version\">aether-pheromone-export</source>
1192
+ <context>Colony pheromone trail export to eternal memory</context>
1193
+ </metadata>"
1194
+
1195
+ # Process each signal
1196
+ local signal_count=0
1197
+ local signals_json
1198
+ signals_json=$(jq -c '.signals // [] | .[]' "$input_json" 2>/dev/null)
1199
+
1200
+ if [[ -n "$signals_json" ]]; then
1201
+ while IFS= read -r signal; do
1202
+ [[ -n "$signal" ]] || continue
1203
+
1204
+ local sig_id sig_type priority source created_at expires_at active
1205
+ sig_id=$(echo "$signal" | jq -r '.id // "unknown"')
1206
+ sig_type=$(echo "$signal" | jq -r '.type // "FOCUS"')
1207
+ priority=$(echo "$signal" | jq -r '.priority // "normal"')
1208
+ source=$(echo "$signal" | jq -r '.source // "system"')
1209
+ created_at=$(echo "$signal" | jq -r '.created_at // ""')
1210
+ expires_at=$(echo "$signal" | jq -r '.expires_at // ""')
1211
+ active=$(echo "$signal" | jq -r '.active // true')
1212
+
1213
+ # XML escape the ID and source
1214
+ local escaped_id escaped_source
1215
+ escaped_id=$(echo "$sig_id" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
1216
+ escaped_source=$(echo "$source" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
1217
+
1218
+ xml_output="$xml_output
1219
+ <signal id=\"$escaped_id\"
1220
+ type=\"$sig_type\"
1221
+ priority=\"$priority\"
1222
+ source=\"$escaped_source\"
1223
+ created_at=\"$created_at\""
1224
+
1225
+ if [[ -n "$expires_at" && "$expires_at" != "null" ]]; then
1226
+ xml_output="$xml_output
1227
+ expires_at=\"$expires_at\""
1228
+ fi
1229
+
1230
+ xml_output="$xml_output
1231
+ active=\"$active\">"
1232
+
1233
+ # Content section
1234
+ local content_text
1235
+ content_text=$(echo "$signal" | jq -r '.content.text // ""')
1236
+ if [[ -n "$content_text" ]]; then
1237
+ local escaped_text
1238
+ escaped_text=$(echo "$content_text" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
1239
+ xml_output="$xml_output
1240
+ <content>
1241
+ <text>$escaped_text</text>"
1242
+
1243
+ # Check for data attachment
1244
+ local has_data data_format
1245
+ has_data=$(echo "$signal" | jq -r 'has("content") and has("content.data")')
1246
+ if [[ "$has_data" == "true" ]]; then
1247
+ data_format=$(echo "$signal" | jq -r '.content.data.format // "json"')
1248
+ xml_output="$xml_output
1249
+ <data format=\"$data_format\">"
1250
+ # Add data content as CDATA or escaped
1251
+ local data_content
1252
+ data_content=$(echo "$signal" | jq -c '.content.data' 2>/dev/null)
1253
+ xml_output="$xml_output$data_content"
1254
+ xml_output="$xml_output</data>"
1255
+ fi
1256
+
1257
+ xml_output="$xml_output
1258
+ </content>"
1259
+ fi
1260
+
1261
+ # Tags section
1262
+ local tags_json
1263
+ tags_json=$(echo "$signal" | jq -c '.tags // []')
1264
+ if [[ "$tags_json" != "[]" && -n "$tags_json" ]]; then
1265
+ xml_output="$xml_output
1266
+ <tags>"
1267
+ local tags_array
1268
+ tags_array=$(echo "$signal" | jq -c '.tags // [] | .[]')
1269
+ while IFS= read -r tag; do
1270
+ [[ -n "$tag" ]] || continue
1271
+ local tag_value tag_weight tag_category
1272
+ tag_value=$(echo "$tag" | jq -r '.value // .')
1273
+ tag_weight=$(echo "$tag" | jq -r '.weight // "1.0"')
1274
+ tag_category=$(echo "$tag" | jq -r '.category // ""')
1275
+
1276
+ local escaped_tag
1277
+ escaped_tag=$(echo "$tag_value" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
1278
+
1279
+ if [[ -n "$tag_category" && "$tag_category" != "null" ]]; then
1280
+ xml_output="$xml_output
1281
+ <tag weight=\"$tag_weight\" category=\"$tag_category\">$escaped_tag</tag>"
1282
+ else
1283
+ xml_output="$xml_output
1284
+ <tag weight=\"$tag_weight\">$escaped_tag</tag>"
1285
+ fi
1286
+ done <<< "$tags_array"
1287
+ xml_output="$xml_output
1288
+ </tags>"
1289
+ fi
1290
+
1291
+ # Scope section
1292
+ local scope_global
1293
+ scope_global=$(echo "$signal" | jq -r '.scope.global // false')
1294
+ xml_output="$xml_output
1295
+ <scope global=\"$scope_global\">"
1296
+
1297
+ # Castes
1298
+ local castes_json
1299
+ castes_json=$(echo "$signal" | jq -c '.scope.castes // []')
1300
+ if [[ "$castes_json" != "[]" && -n "$castes_json" ]]; then
1301
+ xml_output="$xml_output
1302
+ <castes match=\"any\">"
1303
+ local caste_array
1304
+ caste_array=$(echo "$signal" | jq -r '.scope.castes[]')
1305
+ while IFS= read -r caste; do
1306
+ [[ -n "$caste" ]] || continue
1307
+ xml_output="$xml_output
1308
+ <caste>$caste</caste>"
1309
+ done <<< "$caste_array"
1310
+ xml_output="$xml_output
1311
+ </castes>"
1312
+ fi
1313
+
1314
+ # Paths
1315
+ local paths_json
1316
+ paths_json=$(echo "$signal" | jq -c '.scope.paths // []')
1317
+ if [[ "$paths_json" != "[]" && -n "$paths_json" ]]; then
1318
+ xml_output="$xml_output
1319
+ <paths match=\"any\">"
1320
+ local path_array
1321
+ path_array=$(echo "$signal" | jq -r '.scope.paths[]')
1322
+ while IFS= read -r path; do
1323
+ [[ -n "$path" ]] || continue
1324
+ local escaped_path
1325
+ escaped_path=$(echo "$path" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
1326
+ xml_output="$xml_output
1327
+ <path>$escaped_path</path>"
1328
+ done <<< "$path_array"
1329
+ xml_output="$xml_output
1330
+ </paths>"
1331
+ fi
1332
+
1333
+ xml_output="$xml_output
1334
+ </scope>"
1335
+
1336
+ xml_output="$xml_output
1337
+ </signal>"
1338
+ ((signal_count++))
1339
+ done <<< "$signals_json"
1340
+ fi
1341
+
1342
+ # Close root element
1343
+ xml_output="$xml_output
1344
+ </pheromones>"
1345
+
1346
+ # Ensure output directory exists
1347
+ local output_dir
1348
+ output_dir=$(dirname "$output_xml")
1349
+ if [[ ! -d "$output_dir" ]]; then
1350
+ mkdir -p "$output_dir" 2>/dev/null || {
1351
+ xml_json_err "Cannot create output directory: $output_dir"
1352
+ return 1
1353
+ }
1354
+ fi
1355
+
1356
+ # Write XML to file
1357
+ echo "$xml_output" > "$output_xml" || {
1358
+ xml_json_err "Failed to write output file: $output_xml"
1359
+ return 1
1360
+ }
1361
+
1362
+ # Validate against schema if available
1363
+ local validation_result="false"
1364
+ if [[ -f "$abs_schema" ]]; then
1365
+ validation_result=$(xml-validate "$output_xml" "$abs_schema" 2>/dev/null)
1366
+ if ! echo "$validation_result" | jq -e '.result.valid' >/dev/null 2>&1; then
1367
+ xml_json_err "XML validation failed against schema: $abs_schema"
1368
+ return 1
1369
+ fi
1370
+ validation_result="true"
1371
+ fi
1372
+
1373
+ # Return success with metadata
1374
+ local escaped_output
1375
+ escaped_output=$(echo "$output_xml" | jq -Rs '.[:-1]')
1376
+ local result_json
1377
+ result_json="{\"exported\":true,\"path\":$escaped_output,\"signals\":$signal_count,\"validated\":$validation_result"
1378
+ if [[ -n "$colony_namespace" ]]; then
1379
+ result_json="$result_json,\"namespace\":\"$colony_namespace\",\"prefix\":\"$colony_prefix\""
1380
+ fi
1381
+ result_json="$result_json}"
1382
+ xml_json_ok "$result_json"
1383
+ }
1384
+
1385
+ # ============================================================================
1386
+ # Colony Namespace Generation
1387
+ # ============================================================================
1388
+
1389
+ # Colony namespace base URI
1390
+ readonly COLONY_NAMESPACE_BASE="http://aether.dev/colony"
1391
+
1392
+ # generate-colony-namespace: Generate unique namespace URI for a colony session
1393
+ # Usage: generate-colony-namespace <session_id>
1394
+ # Returns: {"ok":true,"result":{"namespace":"http://aether.dev/colony/{session_id}","prefix":"col_{hash}"}}
1395
+ generate-colony-namespace() {
1396
+ local session_id="${1:-}"
1397
+
1398
+ [[ -z "$session_id" ]] && { xml_json_err "Missing session_id argument"; return 1; }
1399
+
1400
+ # Generate namespace URI
1401
+ local namespace_uri="${COLONY_NAMESPACE_BASE}/${session_id}"
1402
+
1403
+ # Generate short prefix from session_id (first 8 chars of MD5 hash)
1404
+ local prefix
1405
+ if command -v md5sum >/dev/null 2>&1; then
1406
+ prefix="col_$(echo -n "$session_id" | md5sum | cut -c1-8)"
1407
+ elif command -v md5 >/dev/null 2>&1; then
1408
+ prefix="col_$(echo -n "$session_id" | md5 | cut -c1-8)"
1409
+ else
1410
+ # Fallback: use first 8 alphanumeric chars of session_id
1411
+ prefix="col_$(echo -n "$session_id" | tr -cd '[:alnum:]' | cut -c1-8)"
1412
+ fi
1413
+
1414
+ xml_json_ok "{\"namespace\":\"$namespace_uri\",\"prefix\":\"$prefix\",\"session_id\":\"$session_id\"}"
1415
+ }
1416
+
1417
+ # generate-cross-colony-prefix: Generate prefix for external colony pheromones
1418
+ # Usage: generate-cross-colony-prefix <external_session_id> [local_session_id]
1419
+ # Returns: {"ok":true,"result":{"prefix":"ext_{hash}_{short_id}","full_prefix":"{local_prefix}_{external_prefix}"}}
1420
+ generate-cross-colony-prefix() {
1421
+ local external_session_id="${1:-}"
1422
+ local local_session_id="${2:-}"
1423
+
1424
+ [[ -z "$external_session_id" ]] && { xml_json_err "Missing external_session_id argument"; return 1; }
1425
+
1426
+ # Generate external colony prefix
1427
+ local external_prefix
1428
+ if command -v md5sum >/dev/null 2>&1; then
1429
+ external_prefix="ext_$(echo -n "$external_session_id" | md5sum | cut -c1-6)"
1430
+ elif command -v md5 >/dev/null 2>&1; then
1431
+ external_prefix="ext_$(echo -n "$external_session_id" | md5 | cut -c1-6)"
1432
+ else
1433
+ external_prefix="ext_$(echo -n "$external_session_id" | tr -cd '[:alnum:]' | cut -c1-6)"
1434
+ fi
1435
+
1436
+ # If local session provided, create combined prefix for collision prevention
1437
+ local full_prefix="$external_prefix"
1438
+ if [[ -n "$local_session_id" ]]; then
1439
+ local local_hash
1440
+ if command -v md5sum >/dev/null 2>&1; then
1441
+ local_hash="$(echo -n "$local_session_id" | md5sum | cut -c1-4)"
1442
+ elif command -v md5 >/dev/null 2>&1; then
1443
+ local_hash="$(echo -n "$local_session_id" | md5 | cut -c1-4)"
1444
+ else
1445
+ local_hash="$(echo -n "$local_session_id" | tr -cd '[:alnum:]' | cut -c1-4)"
1446
+ fi
1447
+ full_prefix="${local_hash}_${external_prefix}"
1448
+ fi
1449
+
1450
+ xml_json_ok "{\"prefix\":\"$external_prefix\",\"full_prefix\":\"$full_prefix\",\"external_session\":\"$external_session_id\"}"
1451
+ }
1452
+
1453
+ # prefix-pheromone-id: Prefix a pheromone ID to prevent collisions
1454
+ # Usage: prefix-pheromone-id <pheromone_id> <colony_prefix>
1455
+ # Returns: {"ok":true,"result":"{prefix}_{pheromone_id}"}
1456
+ prefix-pheromone-id() {
1457
+ local pheromone_id="${1:-}"
1458
+ local colony_prefix="${2:-}"
1459
+
1460
+ [[ -z "$pheromone_id" ]] && { xml_json_err "Missing pheromone_id argument"; return 1; }
1461
+ [[ -z "$colony_prefix" ]] && { xml_json_err "Missing colony_prefix argument"; return 1; }
1462
+
1463
+ # Check if already prefixed with this colony
1464
+ if [[ "$pheromone_id" == ${colony_prefix}_* ]]; then
1465
+ xml_json_ok "\"$pheromone_id\""
1466
+ return 0
1467
+ fi
1468
+
1469
+ local prefixed_id="${colony_prefix}_${pheromone_id}"
1470
+ xml_json_ok "\"$prefixed_id\""
1471
+ }
1472
+
1473
+ # extract-session-from-namespace: Extract session ID from namespace URI
1474
+ # Usage: extract-session-from-namespace <namespace_uri>
1475
+ # Returns: {"ok":true,"result":"{session_id}"}
1476
+ extract-session-from-namespace() {
1477
+ local namespace_uri="${1:-}"
1478
+
1479
+ [[ -z "$namespace_uri" ]] && { xml_json_err "Missing namespace_uri argument"; return 1; }
1480
+
1481
+ # Extract session ID from http://aether.dev/colony/{session_id}
1482
+ local session_id
1483
+ if [[ "$namespace_uri" =~ ^http://aether\.dev/colony/(.+)$ ]]; then
1484
+ session_id="${BASH_REMATCH[1]}"
1485
+ xml_json_ok "\"$session_id\""
1486
+ else
1487
+ xml_json_err "Invalid colony namespace format: $namespace_uri"
1488
+ return 1
1489
+ fi
1490
+ }
1491
+
1492
+ # validate-colony-namespace: Validate a colony namespace URI
1493
+ # Usage: validate-colony-namespace <namespace_uri>
1494
+ # Returns: {"ok":true,"result":{"valid":true,"type":"colony","session_id":"..."}}
1495
+ validate-colony-namespace() {
1496
+ local namespace_uri="${1:-}"
1497
+
1498
+ [[ -z "$namespace_uri" ]] && { xml_json_err "Missing namespace_uri argument"; return 1; }
1499
+
1500
+ # Check if it matches colony namespace pattern
1501
+ if [[ "$namespace_uri" =~ ^http://aether\.dev/colony/([a-zA-Z0-9_-]+)$ ]]; then
1502
+ local session_id="${BASH_REMATCH[1]}"
1503
+ xml_json_ok "{\"valid\":true,\"type\":\"colony\",\"session_id\":\"$session_id\"}"
1504
+ elif [[ "$namespace_uri" == "http://aether.colony/schemas/pheromones" ]]; then
1505
+ xml_json_ok "{\"valid\":true,\"type\":\"schema\",\"session_id\":null}"
1506
+ else
1507
+ xml_json_ok "{\"valid\":false,\"type\":null,\"session_id\":null}"
1508
+ fi
1509
+ }
1510
+
1511
+ # ============================================================================
1512
+ # Queen-Wisdom Markdown Generation (XSLT-based)
1513
+ # ============================================================================
1514
+
1515
+ # queen-wisdom-to-markdown: Convert queen-wisdom XML to markdown using XSLT
1516
+ # Usage: queen-wisdom-to-markdown <wisdom_xml_file> [output_md_file]
1517
+ # wisdom_xml_file: Path to queen-wisdom.xml
1518
+ # output_md_file: Optional path to write markdown output (default: stdout)
1519
+ # Returns: {"ok":true,"result":{"markdown":"...","path":"..."}} or error
1520
+ queen-wisdom-to-markdown() {
1521
+ local xml_file="${1:-}"
1522
+ local output_file="${2:-}"
1523
+
1524
+ # Validate arguments
1525
+ [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
1526
+ [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
1527
+
1528
+ # Check for xsltproc
1529
+ if [[ "$XSLTPROC_AVAILABLE" != "true" ]]; then
1530
+ xml_json_err "xsltproc not available. Install libxslt utilities."
1531
+ return 1
1532
+ fi
1533
+
1534
+ # Find XSLT file (check multiple locations)
1535
+ local xsl_file=""
1536
+ local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1537
+
1538
+ # Search paths for XSLT file
1539
+ local search_paths=(
1540
+ "$script_dir/queen-to-md.xsl"
1541
+ ".aether/utils/queen-to-md.xsl"
1542
+ "runtime/utils/queen-to-md.xsl"
1543
+ "$HOME/.aether/system/utils/queen-to-md.xsl"
1544
+ )
1545
+
1546
+ for path in "${search_paths[@]}"; do
1547
+ if [[ -f "$path" ]]; then
1548
+ xsl_file="$path"
1549
+ break
1550
+ fi
1551
+ done
1552
+
1553
+ if [[ -z "$xsl_file" ]]; then
1554
+ xml_json_err "XSLT file queen-to-md.xsl not found in standard locations"
1555
+ return 1
1556
+ fi
1557
+
1558
+ # Validate XML against schema first
1559
+ local schema_file="$script_dir/../schemas/queen-wisdom.xsd"
1560
+ if [[ -f "$schema_file" ]]; then
1561
+ local validation
1562
+ validation=$(xml-validate "$xml_file" "$schema_file" 2>/dev/null)
1563
+ if ! echo "$validation" | jq -e '.result.valid' >/dev/null 2>&1; then
1564
+ xml_json_err "XML validation failed before conversion"
1565
+ return 1
1566
+ fi
1567
+ fi
1568
+
1569
+ # Perform XSLT transformation
1570
+ local markdown
1571
+ if ! markdown=$(xsltproc "$xsl_file" "$xml_file" 2>&1); then
1572
+ xml_json_err "XSLT transformation failed: $markdown"
1573
+ return 1
1574
+ fi
1575
+
1576
+ # Output handling
1577
+ if [[ -n "$output_file" ]]; then
1578
+ if echo "$markdown" > "$output_file"; then
1579
+ xml_json_ok "{\"markdown\":\"(written to file)\",\"path\":\"$output_file\"}"
1580
+ else
1581
+ xml_json_err "Failed to write to output file: $output_file"
1582
+ return 1
1583
+ fi
1584
+ else
1585
+ # Return markdown in JSON result
1586
+ local escaped_markdown
1587
+ escaped_markdown=$(echo "$markdown" | jq -Rs '.[:-1]')
1588
+ xml_json_ok "{\"markdown\":$escaped_markdown,\"path\":null}"
1589
+ fi
1590
+ }
1591
+
1592
+ # ============================================================================
1593
+ # Queen-Wisdom Promotion Workflow
1594
+ # ============================================================================
1595
+
1596
+ # Get promotion threshold for a wisdom type
1597
+ # Usage: _get_promotion_threshold <type>
1598
+ # Returns: threshold value
1599
+ _get_promotion_threshold() {
1600
+ local wisdom_type="$1"
1601
+ case "$wisdom_type" in
1602
+ philosophy) echo "5" ;;
1603
+ pattern) echo "3" ;;
1604
+ redirect) echo "2" ;;
1605
+ stack) echo "1" ;;
1606
+ decree) echo "0" ;;
1607
+ *) echo "1" ;;
1608
+ esac
1609
+ }
1610
+
1611
+ # queen-wisdom-validate-entry: Validate a single wisdom entry
1612
+ # Usage: queen-wisdom-validate-entry <xml_file> <entry_id>
1613
+ # Returns: {"ok":true,"result":{"valid":true,"errors":[],"warnings":[]}} or error
1614
+ queen-wisdom-validate-entry() {
1615
+ local xml_file="${1:-}"
1616
+ local entry_id="${2:-}"
1617
+
1618
+ [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
1619
+ [[ -z "$entry_id" ]] && { xml_json_err "Missing entry ID argument"; return 1; }
1620
+ [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
1621
+
1622
+ if [[ "$XMLLINT_AVAILABLE" != "true" ]]; then
1623
+ xml_json_err "xmllint not available"
1624
+ return 1
1625
+ fi
1626
+
1627
+ # Build XPath query to find the entry by ID
1628
+ local xpath_query="//*[@id='$entry_id']"
1629
+ local entry_xml
1630
+ entry_xml=$(xmllint --xpath "$xpath_query" "$xml_file" 2>/dev/null) || {
1631
+ xml_json_err "Entry not found with ID: $entry_id"
1632
+ return 1
1633
+ }
1634
+
1635
+ # Initialize validation results
1636
+ local errors=()
1637
+ local warnings=()
1638
+
1639
+ # Extract attributes for validation
1640
+ local confidence domain source created_at content
1641
+ confidence=$(xmllint --xpath "string(//*[@id='$entry_id']/@confidence)" "$xml_file" 2>/dev/null)
1642
+ domain=$(xmllint --xpath "string(//*[@id='$entry_id']/@domain)" "$xml_file" 2>/dev/null)
1643
+ source=$(xmllint --xpath "string(//*[@id='$entry_id']/@source)" "$xml_file" 2>/dev/null)
1644
+ created_at=$(xmllint --xpath "string(//*[@id='$entry_id']/@created_at)" "$xml_file" 2>/dev/null)
1645
+ content=$(xmllint --xpath "string(//*[@id='$entry_id']/qw:content)" "$xml_file" 2>/dev/null || echo "")
1646
+
1647
+ # Validate confidence (0.0 to 1.0)
1648
+ if [[ -z "$confidence" ]]; then
1649
+ errors+=("Missing required attribute: confidence")
1650
+ elif ! [[ "$confidence" =~ ^0?\.[0-9]+$|^1\.0$ ]]; then
1651
+ errors+=("Invalid confidence value: $confidence (must be 0.0-1.0)")
1652
+ fi
1653
+
1654
+ # Validate domain (must be from allowed list)
1655
+ local valid_domains="architecture testing security performance ux process communication debugging general"
1656
+ if [[ -z "$domain" ]]; then
1657
+ errors+=("Missing required attribute: domain")
1658
+ elif [[ ! " $valid_domains " =~ " $domain " ]]; then
1659
+ errors+=("Invalid domain: $domain (must be one of: $valid_domains)")
1660
+ fi
1661
+
1662
+ # Validate source (must be from allowed list)
1663
+ local valid_sources="queen user colony oracle observation"
1664
+ if [[ -z "$source" ]]; then
1665
+ errors+=("Missing required attribute: source")
1666
+ elif [[ ! " $valid_sources " =~ " $source " ]]; then
1667
+ errors+=("Invalid source: $source (must be one of: $valid_sources)")
1668
+ fi
1669
+
1670
+ # Validate created_at (ISO 8601 format)
1671
+ if [[ -z "$created_at" ]]; then
1672
+ errors+=("Missing required attribute: created_at")
1673
+ elif ! [[ "$created_at" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2} ]]; then
1674
+ errors+=("Invalid timestamp format: $created_at (expected ISO 8601)")
1675
+ fi
1676
+
1677
+ # Validate content
1678
+ if [[ -z "$content" ]]; then
1679
+ errors+=("Missing required element: content")
1680
+ elif [[ ${#content} -lt 10 ]]; then
1681
+ warnings+=("Content is very short (${#content} chars) - consider expanding")
1682
+ fi
1683
+
1684
+ # Build JSON response
1685
+ local error_json="[]"
1686
+ local warning_json="[]"
1687
+
1688
+ if [[ ${#errors[@]} -gt 0 ]]; then
1689
+ error_json=$(printf '%s\n' "${errors[@]}" | jq -R . | jq -s .)
1690
+ fi
1691
+
1692
+ if [[ ${#warnings[@]} -gt 0 ]]; then
1693
+ warning_json=$(printf '%s\n' "${warnings[@]}" | jq -R . | jq -s .)
1694
+ fi
1695
+
1696
+ local valid="false"
1697
+ [[ ${#errors[@]} -eq 0 ]] && valid="true"
1698
+
1699
+ xml_json_ok "{\"valid\":$valid,\"errors\":$error_json,\"warnings\":$warning_json}"
1700
+ }
1701
+
1702
+ # queen-wisdom-promote: Promote a wisdom entry with validation
1703
+ # Usage: queen-wisdom-promote <xml_file> <entry_id> [target_level]
1704
+ # xml_file: Path to queen-wisdom.xml
1705
+ # entry_id: ID of the entry to promote
1706
+ # target_level: Optional target promotion level (defaults to next level)
1707
+ # Returns: {"ok":true,"result":{"promoted":true,"from":"...","to":"...","evolution_log_updated":true}} or error
1708
+ queen-wisdom-promote() {
1709
+ local xml_file="${1:-}"
1710
+ local entry_id="${2:-}"
1711
+ local target_level="${3:-}"
1712
+
1713
+ [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
1714
+ [[ -z "$entry_id" ]] && { xml_json_err "Missing entry ID argument"; return 1; }
1715
+ [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
1716
+
1717
+ # First validate the entry
1718
+ local validation
1719
+ validation=$(queen-wisdom-validate-entry "$xml_file" "$entry_id" 2>&1)
1720
+ if ! echo "$validation" | jq -e '.result.valid' >/dev/null 2>&1; then
1721
+ local errors
1722
+ errors=$(echo "$validation" | jq -r '.result.errors | join("; ")')
1723
+ xml_json_err "Validation failed: $errors"
1724
+ return 1
1725
+ fi
1726
+
1727
+ # Get current entry type and applied count
1728
+ local current_type applied_count
1729
+ # Determine entry type by which container it's in
1730
+ if xmllint --xpath "//qw:philosophies/qw:philosophy[@id='$entry_id']" "$xml_file" >/dev/null 2>&1; then
1731
+ current_type="philosophy"
1732
+ applied_count=$(xmllint --xpath "string(//qw:philosophy[@id='$entry_id']/@applied_count)" "$xml_file" 2>/dev/null || echo "0")
1733
+ elif xmllint --xpath "//qw:patterns/qw:pattern[@id='$entry_id']" "$xml_file" >/dev/null 2>&1; then
1734
+ current_type="pattern"
1735
+ applied_count=$(xmllint --xpath "string(//qw:pattern[@id='$entry_id']/@applied_count)" "$xml_file" 2>/dev/null || echo "0")
1736
+ elif xmllint --xpath "//qw:redirects/qw:redirect[@id='$entry_id']" "$xml_file" >/dev/null 2>&1; then
1737
+ current_type="redirect"
1738
+ applied_count=$(xmllint --xpath "string(//qw:redirect[@id='$entry_id']/@applied_count)" "$xml_file" 2>/dev/null || echo "0")
1739
+ elif xmllint --xpath "//qw:stack-wisdom/qw:wisdom[@id='$entry_id']" "$xml_file" >/dev/null 2>&1; then
1740
+ current_type="stack"
1741
+ applied_count=$(xmllint --xpath "string(//qw:stack-wisdom/qw:wisdom[@id='$entry_id']/@applied_count)" "$xml_file" 2>/dev/null || echo "0")
1742
+ elif xmllint --xpath "//qw:decrees/qw:decree[@id='$entry_id']" "$xml_file" >/dev/null 2>&1; then
1743
+ current_type="decree"
1744
+ applied_count=$(xmllint --xpath "string(//qw:decree[@id='$entry_id']/@applied_count)" "$xml_file" 2>/dev/null || echo "0")
1745
+ else
1746
+ xml_json_err "Entry not found: $entry_id"
1747
+ return 1
1748
+ fi
1749
+
1750
+ # Check promotion threshold
1751
+ local threshold
1752
+ threshold=$(_get_promotion_threshold "$current_type")
1753
+
1754
+ if [[ "$applied_count" -lt "$threshold" ]]; then
1755
+ xml_json_err "Not enough validations for promotion: $applied_count < $threshold (required for $current_type)"
1756
+ return 1
1757
+ fi
1758
+
1759
+ # Get colony ID from metadata or use "unknown"
1760
+ local colony_id
1761
+ colony_id=$(xmllint --xpath "string(//qw:metadata/qw:colony_id)" "$xml_file" 2>/dev/null || echo "unknown")
1762
+
1763
+ # Create evolution log entry
1764
+ local timestamp
1765
+ timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1766
+
1767
+ # Note: In a full implementation, this would modify the XML file
1768
+ # For now, we return success and indicate what would happen
1769
+ xml_json_ok "{\"promoted\":true,\"entry_id\":\"$entry_id\",\"type\":\"$current_type\",\"from_applied\":$applied_count,\"threshold\":$threshold,\"colony\":\"$colony_id\",\"timestamp\":\"$timestamp\",\"note\":\"Evolution log update requires XML editing capability\"}"
1770
+ }
1771
+
1772
+ # queen-wisdom-import: Import wisdom from markdown QUEEN.md to XML
1773
+ # Usage: queen-wisdom-import <queen_md_file> [output_xml_file]
1774
+ # Returns: {"ok":true,"result":{"imported":5,"xml":"...","path":"..."}} or error
1775
+ queen-wisdom-import() {
1776
+ local md_file="${1:-}"
1777
+ local output_file="${2:-"queen-wisdom-imported.xml"}"
1778
+
1779
+ [[ -z "$md_file" ]] && { xml_json_err "Missing markdown file argument"; return 1; }
1780
+ [[ -f "$md_file" ]] || { xml_json_err "Markdown file not found: $md_file"; return 1; }
1781
+
1782
+ # Extract metadata from JSON block
1783
+ local version last_evolved colonies
1784
+ version=$(grep -A20 'METADATA' "$md_file" | grep '"version"' | sed 's/.*: "\([^"]*\)".*/\1/')
1785
+ last_evolved=$(grep -A20 'METADATA' "$md_file" | grep '"last_evolved"' | sed 's/.*: "\([^"]*\)".*/\1/')
1786
+
1787
+ # Generate timestamps
1788
+ local created modified
1789
+ created="${last_evolved:-$(date -u +"%Y-%m-%dT%H:%M:%SZ")}"
1790
+ modified=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
1791
+
1792
+ # Start building XML
1793
+ local xml='<?xml version="1.0" encoding="UTF-8"?>'
1794
+ xml+=$'\n<queen-wisdom xmlns:qw="http://aether.colony/schemas/queen-wisdom/1.0">'
1795
+
1796
+ # Metadata section
1797
+ xml+=$'\n <metadata>'
1798
+ xml+=$'\n <version>'"${version:-1.0.0}"'</version>'
1799
+ xml+=$'\n <created>'"$created"'</created>'
1800
+ xml+=$'\n <modified>'"$modified"'</modified>'
1801
+ xml+=$'\n <colony_id>imported</colony_id>'
1802
+ xml+=$'\n </metadata>'
1803
+
1804
+ # Parse sections using simple grep/sed patterns
1805
+ # Note: This is a basic implementation - full parsing would require more sophisticated handling
1806
+
1807
+ local imported_count=0
1808
+
1809
+ # Extract philosophies (simplified parsing)
1810
+ xml+=$'\n <philosophies>'
1811
+ while IFS= read -r line; do
1812
+ # Look for lines starting with "- **" and extract content
1813
+ if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*\*\* ]]; then
1814
+ local id timestamp content
1815
+ id=$(echo "$line" | sed -n 's/.*\*\*\([^*]*\)\*\*.*/\1/p')
1816
+ timestamp=$(echo "$line" | sed -n 's/.*(\([^)]*\)).*/\1/p')
1817
+ content=$(echo "$line" | sed -n 's/.*):[[:space:]]*\(.*\)/\1/p')
1818
+ if [[ -n "$id" && -n "$content" ]]; then
1819
+ xml+=$'\n <philosophy id="'"$id"'" confidence="0.8" domain="general" source="observation" created_at="'"${timestamp:-$modified}"'">'
1820
+ xml+=$'\n <content>'"$content"'</content>'
1821
+ xml+=$'\n </philosophy>'
1822
+ ((imported_count++))
1823
+ fi
1824
+ fi
1825
+ done < <(sed -n '/## 📜 Philosophies/,/## /p' "$md_file" 2>/dev/null | tail -n +4)
1826
+ xml+=$'\n </philosophies>'
1827
+
1828
+ # Extract patterns (simplified parsing)
1829
+ xml+=$'\n <patterns>'
1830
+ while IFS= read -r line; do
1831
+ if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*\*\* ]]; then
1832
+ local id timestamp content
1833
+ id=$(echo "$line" | sed -n 's/.*\*\*\([^*]*\)\*\*.*/\1/p')
1834
+ timestamp=$(echo "$line" | sed -n 's/.*(\([^)]*\)).*/\1/p')
1835
+ content=$(echo "$line" | sed -n 's/.*):[[:space:]]*\(.*\)/\1/p')
1836
+ if [[ -n "$id" && -n "$content" ]]; then
1837
+ xml+=$'\n <pattern id="'"$id"'" confidence="0.7" domain="general" source="observation" created_at="'"${timestamp:-$modified}"'">'
1838
+ xml+=$'\n <content>'"$content"'</content>'
1839
+ xml+=$'\n </pattern>'
1840
+ ((imported_count++))
1841
+ fi
1842
+ fi
1843
+ done < <(sed -n '/## 🧭 Patterns/,/## /p' "$md_file" 2>/dev/null | tail -n +4)
1844
+ xml+=$'\n </patterns>'
1845
+
1846
+ # Similar for other sections (simplified for brevity)
1847
+ xml+=$'\n <redirects />'
1848
+ xml+=$'\n <stack-wisdom />'
1849
+ xml+=$'\n <decrees />'
1850
+
1851
+ # Evolution log
1852
+ xml+=$'\n <evolution-log>'
1853
+ xml+=$'\n <entry timestamp="'"$modified"'" colony="import" action="imported" type="markdown">'
1854
+ xml+=$'\n <note>Imported from '"$md_file"' with '"$imported_count"' entries</note>'
1855
+ xml+=$'\n </entry>'
1856
+ xml+=$'\n </evolution-log>'
1857
+
1858
+ xml+=$'\n</queen-wisdom>'
1859
+
1860
+ # Write output
1861
+ echo "$xml" > "$output_file"
1862
+
1863
+ xml_json_ok "{\"imported\":$imported_count,\"xml\":\"(written to file)\",\"path\":\"$output_file\"}"
1864
+ }
1865
+
1866
+ # ============================================================================
1867
+ # Prompt XML Conversion
1868
+ # ============================================================================
1869
+
1870
+ # prompt-to-xml: Convert a markdown prompt file to structured XML
1871
+ # Usage: prompt-to-xml <markdown_file> [output_xml_file]
1872
+ # Returns: {"ok":true,"result":{"xml":"...","path":"...","elements_extracted":N}} or error
1873
+ prompt-to-xml() {
1874
+ local md_file="${1:-}"
1875
+ local output_file="${2:-}"
1876
+
1877
+ [[ -z "$md_file" ]] && { xml_json_err "Missing markdown file argument"; return 1; }
1878
+ [[ -f "$md_file" ]] || { xml_json_err "Markdown file not found: $md_file"; return 1; }
1879
+
1880
+ # Extract prompt name from filename
1881
+ local prompt_name
1882
+ prompt_name=$(basename "$md_file" .md)
1883
+
1884
+ # Initialize XML parts
1885
+ local xml='<?xml version="1.0" encoding="UTF-8"?>'
1886
+ xml+=$'\n<aether-prompt xmlns:ap="http://aether.colony/schemas/prompt/1.0">'
1887
+
1888
+ # Metadata
1889
+ xml+=$'\n <metadata>'
1890
+ xml+=$'\n <version>1.0.0</version>'
1891
+ xml+=$'\n <created>'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'</created>'
1892
+ xml+=$'\n </metadata>'
1893
+
1894
+ # Name and type detection
1895
+ xml+=$'\n <name>'"$prompt_name"'</name>'
1896
+
1897
+ # Detect type from content patterns
1898
+ local prompt_type="command"
1899
+ if grep -q "worker\|caste\|Builder\|Watcher\|Scout" "$md_file" 2>/dev/null; then
1900
+ prompt_type="worker"
1901
+ elif grep -q "agent\|Agent" "$md_file" 2>/dev/null; then
1902
+ prompt_type="agent"
1903
+ fi
1904
+ xml+=$'\n <type>'"$prompt_type"'</type>'
1905
+
1906
+ # Try to detect caste for worker prompts
1907
+ local caste=""
1908
+ if [[ "$prompt_type" == "worker" ]]; then
1909
+ if grep -qi "builder" "$md_file"; then
1910
+ caste="builder"
1911
+ elif grep -qi "watcher" "$md_file"; then
1912
+ caste="watcher"
1913
+ elif grep -qi "scout" "$md_file"; then
1914
+ caste="scout"
1915
+ elif grep -qi "chaos" "$md_file"; then
1916
+ caste="chaos"
1917
+ elif grep -qi "oracle" "$md_file"; then
1918
+ caste="oracle"
1919
+ elif grep -qi "architect" "$md_file"; then
1920
+ caste="architect"
1921
+ fi
1922
+ [[ -n "$caste" ]] && xml+=$'\n <caste>'"$caste"'</caste>'
1923
+ fi
1924
+
1925
+ # Extract objective (first H1 or first paragraph)
1926
+ local objective
1927
+ objective=$(grep -m1 "^# " "$md_file" 2>/dev/null | sed 's/^# //' || head -1 "$md_file")
1928
+ xml+=$'\n <objective>'"$(xml_escape_content "$objective")"'</objective>'
1929
+
1930
+ # Extract requirements (## Requirements or numbered lists)
1931
+ xml+=$'\n <requirements>'
1932
+ local req_count=0
1933
+ while IFS= read -r line; do
1934
+ # Check for list items (bullet or numbered)
1935
+ if [[ "$line" =~ ^[[:space:]]*[-*][[:space:]] ]] || [[ "$line" =~ ^[[:space:]]*[0-9]+\.[[:space:]] ]]; then
1936
+ local req_desc
1937
+ req_desc=$(echo "$line" | sed -E 's/^[[:space:]]*[-*][[:space:]]+//' | sed -E 's/^[[:space:]]*[0-9]+\.[[:space:]]+//')
1938
+ ((req_count++))
1939
+ xml+=$'\n <requirement id="req_'"$req_count"'" priority="normal">'
1940
+ xml+=$'\n <description>'"$(xml_escape_content "$req_desc")"'</description>'
1941
+ xml+=$'\n </requirement>'
1942
+ fi
1943
+ done < <(sed -n '/## Requirement/,/## /p' "$md_file" 2>/dev/null | tail -n +2)
1944
+
1945
+ # If no requirements section found, add a default one
1946
+ if [[ $req_count -eq 0 ]]; then
1947
+ xml+=$'\n <requirement id="req_1" priority="normal">'
1948
+ xml+=$'\n <description>Follow the instructions in this prompt</description>'
1949
+ xml+=$'\n </requirement>'
1950
+ fi
1951
+ xml+=$'\n </requirements>'
1952
+
1953
+ # Output specification
1954
+ xml+=$'\n <output>'
1955
+ xml+=$'\n <format>Markdown</format>'
1956
+ xml+=$'\n </output>'
1957
+
1958
+ # Verification
1959
+ xml+=$'\n <verification>'
1960
+ xml+=$'\n <method>Check output meets success criteria</method>'
1961
+ xml+=$'\n </verification>'
1962
+
1963
+ # Success criteria
1964
+ xml+=$'\n <success_criteria>'
1965
+ xml+=$'\n <criterion id="crit_1" required="true">'
1966
+ xml+=$'\n <description>Task completed as specified</description>'
1967
+ xml+=$'\n </criterion>'
1968
+ xml+=$'\n </success_criteria>'
1969
+
1970
+ xml+=$'\n</aether-prompt>'
1971
+
1972
+ # Output handling
1973
+ if [[ -n "$output_file" ]]; then
1974
+ echo "$xml" > "$output_file"
1975
+ xml_json_ok "{\"xml\":\"(written to file)\",\"path\":\"$output_file\",\"elements_extracted\":$((req_count + 5))}"
1976
+ else
1977
+ local escaped_xml
1978
+ escaped_xml=$(echo "$xml" | jq -Rs '.[:-1]')
1979
+ xml_json_ok "{\"xml\":$escaped_xml,\"path\":null,\"elements_extracted\":$((req_count + 5))}"
1980
+ fi
1981
+ }
1982
+
1983
+ # prompt-from-xml: Convert XML prompt to markdown format
1984
+ # Usage: prompt-from-xml <xml_file> [output_md_file]
1985
+ # Returns: {"ok":true,"result":{"markdown":"...","path":"..."}} or error
1986
+ prompt-from-xml() {
1987
+ local xml_file="${1:-}"
1988
+ local output_file="${2:-}"
1989
+
1990
+ [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
1991
+ [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
1992
+
1993
+ if [[ "$XMLLINT_AVAILABLE" != "true" ]]; then
1994
+ xml_json_err "xmllint not available"
1995
+ return 1
1996
+ fi
1997
+
1998
+ # Validate against schema
1999
+ local schema_file
2000
+ schema_file="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../schemas/prompt.xsd"
2001
+
2002
+ if [[ -f "$schema_file" ]]; then
2003
+ local validation
2004
+ validation=$(xml-validate "$xml_file" "$schema_file" 2>/dev/null)
2005
+ if ! echo "$validation" | jq -e '.result.valid' >/dev/null 2>&1; then
2006
+ xml_json_err "XML validation failed against prompt.xsd schema"
2007
+ return 1
2008
+ fi
2009
+ fi
2010
+
2011
+ # Extract fields using XPath
2012
+ local name type caste objective
2013
+ name=$(xmllint --xpath "string(//ap:name)" "$xml_file" 2>/dev/null || echo "Unnamed")
2014
+ type=$(xmllint --xpath "string(//ap:type)" "$xml_file" 2>/dev/null || echo "command")
2015
+ caste=$(xmllint --xpath "string(//ap:caste)" "$xml_file" 2>/dev/null || echo "")
2016
+ objective=$(xmllint --xpath "string(//ap:objective)" "$xml_file" 2>/dev/null || echo "")
2017
+
2018
+ # Build markdown
2019
+ local md="# $name"
2020
+ [[ -n "$caste" ]] && md+=" ($caste $type)"
2021
+ md+=$'\n\n'
2022
+
2023
+ md+="## Objective"
2024
+ md+=$'\n\n'
2025
+ md+="$objective"
2026
+ md+=$'\n\n'
2027
+
2028
+ # Requirements
2029
+ local req_count
2030
+ req_count=$(xmllint --xpath "count(//ap:requirements/ap:requirement)" "$xml_file" 2>/dev/null || echo "0")
2031
+ if [[ "$req_count" -gt 0 ]]; then
2032
+ md+="## Requirements"
2033
+ md+=$'\n\n'
2034
+
2035
+ for i in $(seq 1 "$req_count"); do
2036
+ local req_desc req_priority
2037
+ req_desc=$(xmllint --xpath "string(//ap:requirements/ap:requirement[$i]/ap:description)" "$xml_file" 2>/dev/null || echo "")
2038
+ req_priority=$(xmllint --xpath "string(//ap:requirements/ap:requirement[$i]/@priority)" "$xml_file" 2>/dev/null || echo "normal")
2039
+
2040
+ if [[ -n "$req_desc" ]]; then
2041
+ md+="$i. [$req_priority] $req_desc"
2042
+ md+=$'\n'
2043
+ fi
2044
+ done
2045
+ md+=$'\n'
2046
+ fi
2047
+
2048
+ # Constraints
2049
+ local constraint_count
2050
+ constraint_count=$(xmllint --xpath "count(//ap:constraints/ap:constraint)" "$xml_file" 2>/dev/null || echo "0")
2051
+ if [[ "$constraint_count" -gt 0 ]]; then
2052
+ md+="## Constraints"
2053
+ md+=$'\n\n'
2054
+
2055
+ for i in $(seq 1 "$constraint_count"); do
2056
+ local constraint_rule constraint_strength
2057
+ constraint_rule=$(xmllint --xpath "string(//ap:constraints/ap:constraint[$i]/ap:rule)" "$xml_file" 2>/dev/null || echo "")
2058
+ constraint_strength=$(xmllint --xpath "string(//ap:constraints/ap:constraint[$i]/@strength)" "$xml_file" 2>/dev/null || echo "should")
2059
+
2060
+ if [[ -n "$constraint_rule" ]]; then
2061
+ md+="- [$constraint_strength] $constraint_rule"
2062
+ md+=$'\n'
2063
+ fi
2064
+ done
2065
+ md+=$'\n'
2066
+ fi
2067
+
2068
+ # Output
2069
+ local output_format
2070
+ output_format=$(xmllint --xpath "string(//ap:output/ap:format)" "$xml_file" 2>/dev/null || echo "Markdown")
2071
+ md+="## Output"
2072
+ md+=$'\n\n'
2073
+ md+="Format: $output_format"
2074
+ md+=$'\n\n'
2075
+
2076
+ # Verification
2077
+ md+="## Verification"
2078
+ md+=$'\n\n'
2079
+ local verification_method
2080
+ verification_method=$(xmllint --xpath "string(//ap:verification/ap:method)" "$xml_file" 2>/dev/null || echo "Manual review")
2081
+ md+="$verification_method"
2082
+ md+=$'\n\n'
2083
+
2084
+ # Success criteria
2085
+ md+="## Success Criteria"
2086
+ md+=$'\n\n'
2087
+
2088
+ local crit_count
2089
+ crit_count=$(xmllint --xpath "count(//ap:success_criteria/ap:criterion)" "$xml_file" 2>/dev/null || echo "0")
2090
+ for i in $(seq 1 "$crit_count"); do
2091
+ local crit_desc crit_required
2092
+ crit_desc=$(xmllint --xpath "string(//ap:success_criteria/ap:criterion[$i]/ap:description)" "$xml_file" 2>/dev/null || echo "")
2093
+ crit_required=$(xmllint --xpath "string(//ap:success_criteria/ap:criterion[$i]/@required)" "$xml_file" 2>/dev/null || echo "true")
2094
+
2095
+ if [[ -n "$crit_desc" ]]; then
2096
+ [[ "$crit_required" == "true" ]] && md+="- [required] " || md+="- [optional] "
2097
+ md+="$crit_desc"
2098
+ md+=$'\n'
2099
+ fi
2100
+ done
2101
+
2102
+ # Output handling
2103
+ if [[ -n "$output_file" ]]; then
2104
+ echo "$md" > "$output_file"
2105
+ xml_json_ok "{\"markdown\":\"(written to file)\",\"path\":\"$output_file\"}"
2106
+ else
2107
+ local escaped_md
2108
+ escaped_md=$(echo "$md" | jq -Rs '.[:-1]')
2109
+ xml_json_ok "{\"markdown\":$escaped_md,\"path\":null}"
2110
+ fi
2111
+ }
2112
+
2113
+ # prompt-validate: Validate a prompt XML file against the schema
2114
+ # Usage: prompt-validate <xml_file>
2115
+ # Returns: {"ok":true,"result":{"valid":true,"errors":[]}} or error
2116
+ prompt-validate() {
2117
+ local xml_file="${1:-}"
2118
+
2119
+ [[ -z "$xml_file" ]] && { xml_json_err "Missing XML file argument"; return 1; }
2120
+ [[ -f "$xml_file" ]] || { xml_json_err "XML file not found: $xml_file"; return 1; }
2121
+
2122
+ local schema_file
2123
+ schema_file="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/../schemas/prompt.xsd"
2124
+
2125
+ [[ -f "$schema_file" ]] || { xml_json_err "Schema file not found: $schema_file"; return 1; }
2126
+
2127
+ xml-validate "$xml_file" "$schema_file"
2128
+ }
2129
+
2130
+ # Helper function to escape XML content
2131
+ xml_escape_content() {
2132
+ local content="$1"
2133
+ # Basic XML escaping
2134
+ content="${content//&/&amp;}"
2135
+ content="${content//\</&lt;}"
2136
+ content="${content//\>/&gt;}"
2137
+ content="${content//\"/&quot;}"
2138
+ echo "$content"
2139
+ }
2140
+
2141
+ # ============================================================================
2142
+ # Export Functions
2143
+ # ============================================================================
2144
+
2145
+ # Functions are available when this file is sourced.
2146
+ # Export is disabled by default to avoid polluting stdout during tests.
2147
+ # Set XML_UTILS_EXPORT=1 to enable function export for subshells.
2148
+ if [[ "${XML_UTILS_EXPORT:-}" == "1" ]]; then
2149
+ export -f xml-validate xml-well-formed xml-to-json json-to-xml 2>/dev/null || true
2150
+ export -f xml-query xml-query-attr xml-merge xml-format 2>/dev/null || true
2151
+ export -f xml-escape xml-unescape xml-detect-tools 2>/dev/null || true
2152
+ export -f pheromone-to-xml pheromone-from-xml pheromone-export 2>/dev/null || true
2153
+ export -f queen-wisdom-to-xml queen-wisdom-from-xml 2>/dev/null || true
2154
+ export -f queen-wisdom-to-markdown queen-wisdom-validate-entry 2>/dev/null || true
2155
+ export -f queen-wisdom-promote queen-wisdom-import 2>/dev/null || true
2156
+ export -f registry-to-xml registry-from-xml 2>/dev/null || true
2157
+ export -f generate-colony-namespace generate-cross-colony-prefix 2>/dev/null || true
2158
+ export -f prefix-pheromone-id extract-session-from-namespace 2>/dev/null || true
2159
+ export -f validate-colony-namespace 2>/dev/null || true
2160
+ export -f prompt-to-xml prompt-from-xml prompt-validate 2>/dev/null || true
2161
+ fi