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
@@ -32,11 +32,11 @@ get_caste_color() {
32
32
  # Caste emojis (must match aether-utils.sh)
33
33
  get_caste_emoji() {
34
34
  case "$1" in
35
- builder) echo "🔨" ;;
36
- watcher) echo "👁️" ;;
37
- scout) echo "🔍" ;;
38
- chaos) echo "🎲" ;;
39
- prime) echo "👑" ;;
35
+ builder) echo "🔨🐜" ;;
36
+ watcher) echo "👁️🐜" ;;
37
+ scout) echo "🔍🐜" ;;
38
+ chaos) echo "🎲🐜" ;;
39
+ prime) echo "👑🐜" ;;
40
40
  *) echo "🐜" ;;
41
41
  esac
42
42
  }
@@ -4,6 +4,7 @@
4
4
 
5
5
  DATA_DIR="${1:-.aether/data}"
6
6
  SPAWN_FILE="$DATA_DIR/spawn-tree.txt"
7
+ VIEW_STATE_FILE="$DATA_DIR/view-state.json"
7
8
 
8
9
  # ANSI colors
9
10
  YELLOW='\033[33m'
@@ -28,6 +29,43 @@ get_emoji() {
28
29
  esac
29
30
  }
30
31
 
32
+ # Load view state
33
+ load_view_state() {
34
+ if [[ -f "$VIEW_STATE_FILE" ]]; then
35
+ cat "$VIEW_STATE_FILE" 2>/dev/null || echo '{}'
36
+ else
37
+ echo '{"tunnel_view":{"expanded":[],"collapsed":["__depth_3_plus__"],"default_expand_depth":2,"show_completed":true}}'
38
+ fi
39
+ }
40
+
41
+ # Check if item is expanded
42
+ is_expanded() {
43
+ local item="$1"
44
+ local depth="${2:-1}"
45
+ local view_state=$(load_view_state)
46
+
47
+ # Check if explicitly expanded
48
+ if echo "$view_state" | jq -e ".tunnel_view.expanded | contains([\"$item\"])" >/dev/null 2>&1; then
49
+ return 0
50
+ fi
51
+
52
+ # Check if depth-based auto-collapse applies
53
+ local default_depth=$(echo "$view_state" | jq -r '.tunnel_view.default_expand_depth // 2')
54
+ if [[ "$depth" -gt "$default_depth" ]]; then
55
+ # Check if __depth_3_plus__ is in collapsed (indicating auto-collapse enabled)
56
+ if echo "$view_state" | jq -e '.tunnel_view.collapsed | contains(["__depth_3_plus__"])' >/dev/null 2>&1; then
57
+ return 1 # Collapsed by depth
58
+ fi
59
+ fi
60
+
61
+ # Check if explicitly collapsed
62
+ if echo "$view_state" | jq -e ".tunnel_view.collapsed | contains([\"$item\"])" >/dev/null 2>&1; then
63
+ return 1
64
+ fi
65
+
66
+ return 0 # Default to expanded
67
+ }
68
+
31
69
  # Status colors
32
70
  get_status_color() {
33
71
  case "$1" in
@@ -46,7 +84,7 @@ render_tree() {
46
84
  cat << 'EOF'
47
85
  .-.
48
86
  (o o) AETHER COLONY
49
- | O | Spawn Tree
87
+ | O | Spawn Tree (Collapsible)
50
88
  `-`
51
89
  EOF
52
90
  echo -e "${RESET}"
@@ -110,6 +148,22 @@ EOF
110
148
  color=$(get_status_color "$status")
111
149
  task="${worker_task[$name]}"
112
150
 
151
+ # Check if collapsed
152
+ local collapsed=false
153
+ local child_count=0
154
+
155
+ # Count children
156
+ for child in "${!workers[@]}"; do
157
+ if [[ "${workers[$child]}" == "$name" ]]; then
158
+ child_count=$((child_count + 1))
159
+ fi
160
+ done
161
+
162
+ # Check collapse state (only if has children)
163
+ if [[ $child_count -gt 0 ]] && ! is_expanded "$name" "$depth"; then
164
+ collapsed=true
165
+ fi
166
+
113
167
  # Truncate task for display
114
168
  [[ ${#task} -gt 30 ]] && task="${task:0:27}..."
115
169
 
@@ -120,30 +174,42 @@ EOF
120
174
  connector="├──"
121
175
  fi
122
176
 
123
- echo -e "${indent}${DIM}${connector}${RESET} ${emoji} ${color}${name}${RESET}: ${task} ${DIM}[depth $depth]${RESET}"
124
-
125
- # Find children of this worker
126
- local children=()
127
- for child in "${!workers[@]}"; do
128
- if [[ "${workers[$child]}" == "$name" ]]; then
129
- children+=("$child")
177
+ # Show expand/collapse indicator
178
+ local expand_indicator=""
179
+ if [[ $child_count -gt 0 ]]; then
180
+ if [[ "$collapsed" == "true" ]]; then
181
+ expand_indicator="▶ [$child_count hidden] "
182
+ else
183
+ expand_indicator=""
130
184
  fi
131
- done
132
-
133
- # Render children
134
- local child_indent="${indent} "
135
- if [[ "$is_last" != "true" ]]; then
136
- child_indent="${indent}${DIM}│${RESET} "
137
185
  fi
138
186
 
139
- local child_count=${#children[@]}
140
- local child_idx=0
141
- for child in "${children[@]}"; do
142
- child_idx=$((child_idx + 1))
143
- local child_is_last="false"
144
- [[ $child_idx -eq $child_count ]] && child_is_last="true"
145
- render_worker "$child" "$child_indent" $((depth + 1)) "$child_is_last"
146
- done
187
+ echo -e "${indent}${DIM}${connector}${RESET} ${emoji} ${color}${name}${RESET}: ${expand_indicator}${task} ${DIM}[depth $depth]${RESET}"
188
+
189
+ # Render children if not collapsed
190
+ if [[ "$collapsed" != "true" ]]; then
191
+ local children=()
192
+ for child in "${!workers[@]}"; do
193
+ if [[ "${workers[$child]}" == "$name" ]]; then
194
+ children+=("$child")
195
+ fi
196
+ done
197
+
198
+ local child_count=${#children[@]}
199
+ local child_idx=0
200
+ for child in "${children[@]}"; do
201
+ child_idx=$((child_idx + 1))
202
+ local child_is_last="false"
203
+ [[ $child_idx -eq $child_count ]] && child_is_last="true"
204
+
205
+ local child_indent="${indent} "
206
+ if [[ "$is_last" != "true" ]]; then
207
+ child_indent="${indent}${DIM}│${RESET} "
208
+ fi
209
+
210
+ render_worker "$child" "$child_indent" $((depth + 1)) "$child_is_last"
211
+ done
212
+ fi
147
213
  }
148
214
 
149
215
  # Render root workers (spawned by Queen) at depth 1
@@ -162,6 +228,8 @@ EOF
162
228
  completed=$(grep -c "completed" "$SPAWN_FILE" 2>/dev/null || echo "0")
163
229
  active=$(grep -c "spawned" "$SPAWN_FILE" 2>/dev/null || echo "0")
164
230
  echo -e "Workers: ${GREEN}$completed completed${RESET} | ${YELLOW}$active active${RESET}"
231
+ echo ""
232
+ echo -e "${DIM}Controls: e+<name> to expand, c+<name> to collapse${RESET}"
165
233
  }
166
234
 
167
235
  # Initial render
@@ -0,0 +1,247 @@
1
+ #!/bin/bash
2
+ # XML Composition Functions for Worker Priming
3
+ # Part of xml-utils.sh - XInclude-based modular configuration composition
4
+ #
5
+ # Usage: source .aether/utils/xml-compose.sh
6
+ # xml-compose <input_xml> [output_xml]
7
+ # xml-compose-worker-priming <priming_xml> [output_xml]
8
+ #
9
+ # These functions enable declarative composition of worker configurations
10
+ # using XInclude directives to merge queen-wisdom, active-trails, and stack-profiles.
11
+
12
+ set -euo pipefail
13
+
14
+ # Note: This file should be sourced AFTER xml-utils.sh
15
+ # It relies on xml_json_ok, xml_json_err, and XMLLINT_AVAILABLE variables
16
+
17
+ # ============================================================================
18
+ # Path Validation (Security)
19
+ # ============================================================================
20
+
21
+ # xml-validate-include-path: Validate XInclude path for traversal attacks
22
+ # Usage: xml-validate-include-path <include_path> <base_dir>
23
+ # Returns: absolute path on success, exits with error on failure
24
+ xml-validate-include-path() {
25
+ local include_path="$1"
26
+ local base_dir="$2"
27
+
28
+ # Resolve base directory to absolute path
29
+ local allowed_dir
30
+ allowed_dir=$(cd "$base_dir" 2>/dev/null && pwd) || {
31
+ xml_json_err "INVALID_BASE_DIR" \
32
+ "Base directory does not exist" \
33
+ "dir=$base_dir"
34
+ return 1
35
+ }
36
+
37
+ # First check: reject paths with traversal sequences
38
+ if [[ "$include_path" =~ \.\.[\/] ]] || [[ "$include_path" =~ [\/]\.\. ]]; then
39
+ xml_json_err "PATH_TRAVERSAL_DETECTED" \
40
+ "Path contains traversal sequences" \
41
+ "path=$include_path"
42
+ return 1
43
+ fi
44
+
45
+ # Build resolved path
46
+ local resolved_path
47
+ if [[ "$include_path" == /* ]]; then
48
+ # Absolute path - must start with allowed_dir
49
+ if [[ ! "$include_path" =~ ^"$allowed_dir" ]]; then
50
+ xml_json_err "PATH_TRAVERSAL_BLOCKED" \
51
+ "Absolute path outside allowed directory" \
52
+ "path=$include_path, allowed=$allowed_dir"
53
+ return 1
54
+ fi
55
+ resolved_path="$include_path"
56
+ else
57
+ # Relative path - resolve against base_dir
58
+ resolved_path="$allowed_dir/$include_path"
59
+ fi
60
+
61
+ # Normalize path (remove . and .. components manually for portability)
62
+ local normalized_path="$resolved_path"
63
+
64
+ # Verify final path is within allowed directory
65
+ if [[ ! "$normalized_path" =~ ^"$allowed_dir" ]]; then
66
+ xml_json_err "PATH_TRAVERSAL_BLOCKED" \
67
+ "Resolved path outside allowed directory" \
68
+ "path=$normalized_path, allowed=$allowed_dir"
69
+ return 1
70
+ fi
71
+
72
+ echo "$normalized_path"
73
+ }
74
+
75
+ # ============================================================================
76
+ # XInclude Composition Functions
77
+ # ============================================================================
78
+
79
+ # xml-compose: Resolve XInclude directives in worker priming documents
80
+ # Usage: xml-compose <input_xml> [output_xml]
81
+ # Returns: {"ok":true,"result":{"composed":true,"output":"...","sources_resolved":N}}
82
+ xml-compose() {
83
+ local input_xml="${1:-}"
84
+ local output_xml="${2:-}"
85
+
86
+ [[ -z "$input_xml" ]] && { xml_json_err "Missing input XML file argument"; return 1; }
87
+ [[ -f "$input_xml" ]] || { xml_json_err "Input XML file not found: $input_xml"; return 1; }
88
+
89
+ # Check well-formedness first
90
+ local well_formed_result
91
+ well_formed_result=$(xml-well-formed "$input_xml" 2>/dev/null)
92
+ if ! echo "$well_formed_result" | jq -e '.result.well_formed' >/dev/null 2>&1; then
93
+ xml_json_err "Input XML is not well-formed"
94
+ return 1
95
+ fi
96
+
97
+ # Use xmllint for XInclude processing if available (with XXE protection)
98
+ if [[ "$XMLLINT_AVAILABLE" == "true" ]]; then
99
+ local composed
100
+ composed=$(xmllint --nonet --noent --xinclude --format "$input_xml" 2>/dev/null) || {
101
+ xml_json_err "XInclude composition failed - check that included files exist"
102
+ return 1
103
+ }
104
+
105
+ # Determine output destination
106
+ if [[ -n "$output_xml" ]]; then
107
+ echo "$composed" > "$output_xml"
108
+ local escaped_output
109
+ escaped_output=$(echo "$output_xml" | jq -Rs '.[:-1]')
110
+ xml_json_ok "{\"composed\":true,\"output\":$escaped_output,\"sources_resolved\":\"auto\"}"
111
+ else
112
+ # Output to stdout wrapped in JSON
113
+ local escaped_composed
114
+ escaped_composed=$(echo "$composed" | jq -Rs '.')
115
+ xml_json_ok "{\"composed\":true,\"xml\":$escaped_composed,\"sources_resolved\":\"auto\"}"
116
+ fi
117
+ return 0
118
+ fi
119
+
120
+ # No xmllint available - error explicitly (manual fallback removed for security)
121
+ xml_json_err "XMLLINT_REQUIRED" \
122
+ "xmllint is required for secure XInclude processing" \
123
+ "install_hint='brew install libxml2'"
124
+ return 1
125
+ }
126
+
127
+ # xml-list-includes: List all XInclude references in a document
128
+ # Usage: xml-list-includes <xml_file>
129
+ # Returns: {"ok":true,"result":{"includes":[{"href":"...","parse":"..."},...]}}
130
+ xml-list-includes() {
131
+ local xml_file="${1:-}"
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
+ local includes_json="[]"
137
+
138
+ if [[ "$XMLSTARLET_AVAILABLE" == "true" ]]; then
139
+ # Use xmlstarlet for proper namespace-aware extraction
140
+ includes_json=$(xmlstarlet sel -N xi="http://www.w3.org/2001/XInclude" \
141
+ -t -m "//xi:include" \
142
+ -o '{"href":"' -v "@href" -o '","parse":"' -v "@parse" -o '","xpointer":"' -v "@xpointer" -o '"}' \
143
+ -n "$xml_file" 2>/dev/null | jq -s '.' || echo "[]")
144
+ elif [[ "$XMLLINT_AVAILABLE" == "true" ]]; then
145
+ # Fallback: grep for xi:include (less reliable but portable)
146
+ local base_dir
147
+ base_dir=$(dirname "$xml_file")
148
+
149
+ includes_json=$(grep -oE 'xi:include[^>]*href="[^"]+"' "$xml_file" 2>/dev/null | while read -r match; do
150
+ local href
151
+ href=$(echo "$match" | grep -oE 'href="[^"]+"' | cut -d'"' -f2)
152
+ local parse
153
+ parse=$(echo "$match" | grep -oE 'parse="[^"]+"' | cut -d'"' -f2 || echo "xml")
154
+ echo "{\"href\":\"$href\",\"parse\":\"$parse\",\"resolved\":\"$base_dir/$href\"}"
155
+ done | jq -s '.' || echo "[]")
156
+ else
157
+ xml_json_err "No XML tool available. Install xmlstarlet or libxml2."
158
+ return 1
159
+ fi
160
+
161
+ local count
162
+ count=$(echo "$includes_json" | jq 'length')
163
+ xml_json_ok "{\"includes\":$includes_json,\"count\":$count}"
164
+ }
165
+
166
+ # xml-compose-worker-priming: Specialized composition for worker priming documents
167
+ # Usage: xml-compose-worker-priming <priming_xml> [output_xml]
168
+ # Returns: {"ok":true,"result":{"composed":true,"worker_id":"...","caste":"...","sources":{...}}}
169
+ xml-compose-worker-priming() {
170
+ local priming_xml="${1:-}"
171
+ local output_xml="${2:-}"
172
+
173
+ [[ -z "$priming_xml" ]] && { xml_json_err "Missing priming XML file argument"; return 1; }
174
+ [[ -f "$priming_xml" ]] || { xml_json_err "Priming XML file not found: $priming_xml"; return 1; }
175
+
176
+ # Validate against schema if available
177
+ local schema_file=".aether/schemas/worker-priming.xsd"
178
+ if [[ -f "$schema_file" ]] && [[ "$XMLLINT_AVAILABLE" == "true" ]]; then
179
+ local validation
180
+ validation=$(xml-validate "$priming_xml" "$schema_file" 2>/dev/null)
181
+ if ! echo "$validation" | jq -e '.result.valid' >/dev/null 2>&1; then
182
+ xml_json_err "Worker priming XML failed schema validation"
183
+ return 1
184
+ fi
185
+ fi
186
+
187
+ # Extract worker identity before composition
188
+ local worker_id worker_caste
189
+ if [[ "$XMLSTARLET_AVAILABLE" == "true" ]]; then
190
+ worker_id=$(xmlstarlet sel -t -v "//*[local-name()='worker-identity']/@id" "$priming_xml" 2>/dev/null || echo "unknown")
191
+ worker_caste=$(xmlstarlet sel -t -v "//*[local-name()='worker-identity']/*[local-name()='caste']" "$priming_xml" 2>/dev/null || echo "unknown")
192
+ else
193
+ # Fallback: sed extraction (portable, no grep -P)
194
+ worker_id=$(sed -n 's/.*worker-identity[^>]*id="\([^"]*\)".*/\1/p' "$priming_xml" | head -1 || echo "unknown")
195
+ worker_caste=$(sed -n 's/.*<caste>\([^<]*\)<\/caste>.*/\1/p' "$priming_xml" | head -1 || echo "unknown")
196
+ fi
197
+
198
+ # Compose the document
199
+ local compose_result
200
+ if [[ -n "$output_xml" ]]; then
201
+ compose_result=$(xml-compose "$priming_xml" "$output_xml" 2>/dev/null)
202
+ else
203
+ compose_result=$(xml-compose "$priming_xml" 2>/dev/null)
204
+ fi
205
+
206
+ if ! echo "$compose_result" | jq -e '.ok' >/dev/null 2>&1; then
207
+ xml_json_err "Composition failed: $(echo "$compose_result" | jq -r '.error // "unknown"')"
208
+ return 1
209
+ fi
210
+
211
+ # Count sources from different sections
212
+ local queen_wisdom_count active_trails_count stack_profiles_count
213
+ if [[ "$XMLSTARLET_AVAILABLE" == "true" ]] && [[ -n "$output_xml" ]]; then
214
+ queen_wisdom_count=$(xmlstarlet sel -t -v "count(//*[local-name()='queen-wisdom']/*[local-name()='wisdom-source'])" "$output_xml" 2>/dev/null || echo "0")
215
+ active_trails_count=$(xmlstarlet sel -t -v "count(//*[local-name()='active-trails']/*[local-name()='trail-source'])" "$output_xml" 2>/dev/null || echo "0")
216
+ stack_profiles_count=$(xmlstarlet sel -t -v "count(//*[local-name()='stack-profiles']/*[local-name()='profile-source'])" "$output_xml" 2>/dev/null || echo "0")
217
+ else
218
+ queen_wisdom_count="unknown"
219
+ active_trails_count="unknown"
220
+ stack_profiles_count="unknown"
221
+ fi
222
+
223
+ # Build result
224
+ local result_json
225
+ result_json=$(jq -n \
226
+ --arg worker_id "$worker_id" \
227
+ --arg caste "$worker_caste" \
228
+ --arg queen_wisdom "$queen_wisdom_count" \
229
+ --arg active_trails "$active_trails_count" \
230
+ --arg stack_profiles "$stack_profiles_count" \
231
+ '{
232
+ composed: true,
233
+ worker_id: $worker_id,
234
+ caste: $caste,
235
+ sources: {
236
+ queen_wisdom: $queen_wisdom,
237
+ active_trails: $active_trails,
238
+ stack_profiles: $stack_profiles
239
+ }
240
+ }')
241
+
242
+ xml_json_ok "$result_json"
243
+ }
244
+
245
+ # Export functions
246
+ export -f xml-compose xml-list-includes xml-compose-worker-priming
247
+ export -f xml-validate-include-path
@@ -0,0 +1,186 @@
1
+ #!/bin/bash
2
+ # XML Core Utilities
3
+ # Fundamental XML operations: validation, formatting, escaping
4
+ #
5
+ # Usage: source .aether/utils/xml-core.sh
6
+ # xml-validate <xml_file> <xsd_file>
7
+ # xml-well-formed <xml_file>
8
+ # xml-format <xml_file>
9
+ # xml-escape <text>
10
+ # xml-unescape <text>
11
+
12
+ # ============================================================================
13
+ # Feature Detection
14
+ # ============================================================================
15
+
16
+ # Check for required XML tools (only if not already set)
17
+ if [[ -z "${XMLLINT_AVAILABLE:-}" ]]; then
18
+ XMLLINT_AVAILABLE=false
19
+ if command -v xmllint >/dev/null 2>&1; then
20
+ XMLLINT_AVAILABLE=true
21
+ fi
22
+ fi
23
+
24
+ if [[ -z "${XMLSTARLET_AVAILABLE:-}" ]]; then
25
+ XMLSTARLET_AVAILABLE=false
26
+ if command -v xmlstarlet >/dev/null 2>&1; then
27
+ XMLSTARLET_AVAILABLE=true
28
+ fi
29
+ fi
30
+
31
+ if [[ -z "${XSLTPROC_AVAILABLE:-}" ]]; then
32
+ XSLTPROC_AVAILABLE=false
33
+ if command -v xsltproc >/dev/null 2>&1; then
34
+ XSLTPROC_AVAILABLE=true
35
+ fi
36
+ fi
37
+
38
+ # ============================================================================
39
+ # JSON Output Helpers
40
+ # ============================================================================
41
+
42
+ xml_json_ok() { printf '{"ok":true,"result":%s}\n' "$1"; }
43
+
44
+ xml_json_err() {
45
+ local code="${1:-UNKNOWN_ERROR}"
46
+ local message="${2:-$1}"
47
+ local details="${3:-}"
48
+ if [[ -n "$details" ]]; then
49
+ printf '{"ok":false,"error":"%s","code":"%s","details":"%s"}\n' "$message" "$code" "$details" >&2
50
+ else
51
+ printf '{"ok":false,"error":"%s","code":"%s"}\n' "$message" "$code" >&2
52
+ fi
53
+ return 1
54
+ }
55
+
56
+ # ============================================================================
57
+ # Core XML Functions
58
+ # ============================================================================
59
+
60
+ # xml-detect-tools: Detect available XML tools
61
+ # Usage: xml-detect-tools
62
+ # Returns: {"ok":true,"result":{"xmllint":true,"xmlstarlet":false,...}}
63
+ xml-detect-tools() {
64
+ xml_json_ok "{\"xmllint\":$XMLLINT_AVAILABLE,\"xmlstarlet\":$XMLSTARLET_AVAILABLE,\"xsltproc\":$XSLTPROC_AVAILABLE}"
65
+ }
66
+
67
+ # xml-validate: Validate XML against XSD schema using xmllint
68
+ # Usage: xml-validate <xml_file> <xsd_file>
69
+ # Returns: {"ok":true,"result":{"valid":true,"errors":[]}} or error
70
+ xml-validate() {
71
+ local xml_file="${1:-}"
72
+ local xsd_file="${2:-}"
73
+
74
+ # Validate arguments
75
+ [[ -z "$xml_file" ]] && { xml_json_err "MISSING_ARG" "Missing XML file argument"; return 1; }
76
+ [[ -z "$xsd_file" ]] && { xml_json_err "MISSING_ARG" "Missing XSD schema file argument"; return 1; }
77
+ [[ -f "$xml_file" ]] || { xml_json_err "FILE_NOT_FOUND" "XML file not found: $xml_file"; return 1; }
78
+ [[ -f "$xsd_file" ]] || { xml_json_err "FILE_NOT_FOUND" "XSD schema file not found: $xsd_file"; return 1; }
79
+
80
+ # Check for xmllint
81
+ if [[ "$XMLLINT_AVAILABLE" != "true" ]]; then
82
+ xml_json_err "TOOL_NOT_AVAILABLE" "xmllint not available. Install libxml2 utilities."
83
+ return 1
84
+ fi
85
+
86
+ # Validate XML against XSD (with XXE protection)
87
+ local errors
88
+ errors=$(xmllint --nonet --noent --noout --schema "$xsd_file" "$xml_file" 2>&1) && {
89
+ xml_json_ok '{"valid":true,"errors":[]}'
90
+ return 0
91
+ } || {
92
+ # Escape errors for JSON
93
+ local escaped_errors
94
+ escaped_errors=$(echo "$errors" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' ')
95
+ xml_json_ok "{\"valid\":false,\"errors\":[\"$escaped_errors\"]}"
96
+ return 0
97
+ }
98
+ }
99
+
100
+ # xml-well-formed: Check if XML is well-formed (no schema validation)
101
+ # Usage: xml-well-formed <xml_file>
102
+ # Returns: {"ok":true,"result":{"well_formed":true}} or error
103
+ xml-well-formed() {
104
+ local xml_file="${1:-}"
105
+
106
+ [[ -z "$xml_file" ]] && { xml_json_err "MISSING_ARG" "Missing XML file argument"; return 1; }
107
+ [[ -f "$xml_file" ]] || { xml_json_err "FILE_NOT_FOUND" "XML file not found: $xml_file"; return 1; }
108
+
109
+ if [[ "$XMLLINT_AVAILABLE" != "true" ]]; then
110
+ xml_json_err "TOOL_NOT_AVAILABLE" "xmllint not available. Install libxml2 utilities."
111
+ return 1
112
+ fi
113
+
114
+ # Check well-formedness with XXE protection
115
+ if xmllint --nonet --noent --noout "$xml_file" 2>/dev/null; then
116
+ xml_json_ok '{"well_formed":true}'
117
+ return 0
118
+ else
119
+ xml_json_ok '{"well_formed":false}'
120
+ return 0
121
+ fi
122
+ }
123
+
124
+ # xml-format: Pretty-print XML with proper indentation
125
+ # Usage: xml-format <xml_file> [output_file]
126
+ # Returns: {"ok":true,"result":{"formatted":true,"output":"..."}} or writes to file
127
+ xml-format() {
128
+ local xml_file="${1:-}"
129
+ local output_file="${2:-}"
130
+
131
+ [[ -z "$xml_file" ]] && { xml_json_err "MISSING_ARG" "Missing XML file argument"; return 1; }
132
+ [[ -f "$xml_file" ]] || { xml_json_err "FILE_NOT_FOUND" "XML file not found: $xml_file"; return 1; }
133
+
134
+ if [[ "$XMLLINT_AVAILABLE" != "true" ]]; then
135
+ xml_json_err "TOOL_NOT_AVAILABLE" "xmllint not available. Install libxml2 utilities."
136
+ return 1
137
+ fi
138
+
139
+ local formatted
140
+ formatted=$(xmllint --nonet --noent --format "$xml_file" 2>/dev/null) || {
141
+ xml_json_err "PARSE_ERROR" "Failed to parse XML file"
142
+ return 1
143
+ }
144
+
145
+ if [[ -n "$output_file" ]]; then
146
+ echo "$formatted" > "$output_file"
147
+ xml_json_ok "{\"formatted\":true,\"path\":\"$output_file\"}"
148
+ else
149
+ # Escape for JSON
150
+ local escaped
151
+ escaped=$(echo "$formatted" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ')
152
+ xml_json_ok "{\"formatted\":true,\"output\":\"$escaped\"}"
153
+ fi
154
+ }
155
+
156
+ # xml-escape: Escape special XML characters
157
+ # Usage: xml-escape <text>
158
+ # Returns: Escaped text (not JSON - direct output)
159
+ xml-escape() {
160
+ local text="${1:-}"
161
+ # Escape &, <, >, ", '
162
+ echo "$text" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g; s/'"'"'/\&apos;/g'
163
+ }
164
+
165
+ # xml-unescape: Unescape XML entities
166
+ # Usage: xml-unescape <text>
167
+ # Returns: Unescaped text (not JSON - direct output)
168
+ xml-unescape() {
169
+ local text="${1:-}"
170
+ # Unescape XML entities
171
+ echo "$text" | sed 's/&amp;/\&/g; s/&lt;/</g; s/&gt;/>/g; s/&quot;/"/g; s/&apos;/'"'"'/g'
172
+ }
173
+
174
+ # xml-escape-content: Escape content for XML CDATA or text nodes
175
+ # Internal helper for consistent escaping
176
+ _xml_escape_content() {
177
+ local content="${1:-}"
178
+ # Basic XML escaping for text content
179
+ echo "$content" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g'
180
+ }
181
+
182
+ # Export functions for use by other modules
183
+ export -f xml_json_ok xml_json_err
184
+ export -f xml-detect-tools xml-validate xml-well-formed xml-format
185
+ export -f xml-escape xml-unescape _xml_escape_content
186
+ export XMLLINT_AVAILABLE XMLSTARLET_AVAILABLE XSLTPROC_AVAILABLE