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.
- package/.claude/commands/ant/archaeology.md +12 -0
- package/.claude/commands/ant/build.md +382 -319
- package/.claude/commands/ant/chaos.md +23 -1
- package/.claude/commands/ant/colonize.md +147 -87
- package/.claude/commands/ant/continue.md +213 -23
- package/.claude/commands/ant/council.md +22 -0
- package/.claude/commands/ant/dream.md +18 -0
- package/.claude/commands/ant/entomb.md +178 -6
- package/.claude/commands/ant/init.md +87 -13
- package/.claude/commands/ant/lay-eggs.md +45 -5
- package/.claude/commands/ant/oracle.md +82 -9
- package/.claude/commands/ant/organize.md +2 -2
- package/.claude/commands/ant/pause-colony.md +86 -28
- package/.claude/commands/ant/phase.md +26 -0
- package/.claude/commands/ant/plan.md +204 -111
- package/.claude/commands/ant/resume-colony.md +23 -1
- package/.claude/commands/ant/resume.md +159 -0
- package/.claude/commands/ant/seal.md +177 -3
- package/.claude/commands/ant/swarm.md +78 -97
- package/.claude/commands/ant/verify-castes.md +7 -7
- package/.claude/commands/ant/watch.md +17 -0
- package/.opencode/agents/aether-ambassador.md +97 -0
- package/.opencode/agents/aether-archaeologist.md +91 -0
- package/.opencode/agents/aether-architect.md +66 -0
- package/.opencode/agents/aether-auditor.md +111 -0
- package/.opencode/agents/aether-builder.md +28 -10
- package/.opencode/agents/aether-chaos.md +98 -0
- package/.opencode/agents/aether-chronicler.md +80 -0
- package/.opencode/agents/aether-gatekeeper.md +107 -0
- package/.opencode/agents/aether-guardian.md +107 -0
- package/.opencode/agents/aether-includer.md +108 -0
- package/.opencode/agents/aether-keeper.md +106 -0
- package/.opencode/agents/aether-measurer.md +119 -0
- package/.opencode/agents/aether-probe.md +91 -0
- package/.opencode/agents/aether-queen.md +72 -19
- package/.opencode/agents/aether-route-setter.md +85 -0
- package/.opencode/agents/aether-sage.md +98 -0
- package/.opencode/agents/aether-scout.md +33 -15
- package/.opencode/agents/aether-surveyor-disciplines.md +334 -0
- package/.opencode/agents/aether-surveyor-nest.md +272 -0
- package/.opencode/agents/aether-surveyor-pathogens.md +209 -0
- package/.opencode/agents/aether-surveyor-provisions.md +277 -0
- package/.opencode/agents/aether-tracker.md +91 -0
- package/.opencode/agents/aether-watcher.md +30 -12
- package/.opencode/agents/aether-weaver.md +87 -0
- package/.opencode/agents/workers.md +1034 -0
- package/.opencode/commands/ant/archaeology.md +44 -26
- package/.opencode/commands/ant/build.md +327 -295
- package/.opencode/commands/ant/chaos.md +32 -4
- package/.opencode/commands/ant/colonize.md +119 -93
- package/.opencode/commands/ant/continue.md +98 -10
- package/.opencode/commands/ant/council.md +28 -0
- package/.opencode/commands/ant/dream.md +24 -0
- package/.opencode/commands/ant/entomb.md +73 -1
- package/.opencode/commands/ant/feedback.md +8 -2
- package/.opencode/commands/ant/flag.md +9 -3
- package/.opencode/commands/ant/flags.md +8 -2
- package/.opencode/commands/ant/focus.md +8 -2
- package/.opencode/commands/ant/help.md +12 -0
- package/.opencode/commands/ant/init.md +49 -4
- package/.opencode/commands/ant/lay-eggs.md +30 -2
- package/.opencode/commands/ant/oracle.md +39 -7
- package/.opencode/commands/ant/organize.md +9 -3
- package/.opencode/commands/ant/pause-colony.md +54 -1
- package/.opencode/commands/ant/phase.md +36 -4
- package/.opencode/commands/ant/plan.md +225 -117
- package/.opencode/commands/ant/redirect.md +8 -2
- package/.opencode/commands/ant/resume-colony.md +51 -26
- package/.opencode/commands/ant/seal.md +76 -0
- package/.opencode/commands/ant/status.md +50 -20
- package/.opencode/commands/ant/swarm.md +108 -104
- package/.opencode/commands/ant/tunnels.md +107 -2
- package/CHANGELOG.md +21 -0
- package/README.md +199 -86
- package/bin/cli.js +142 -25
- package/bin/generate-commands.sh +100 -16
- package/bin/lib/caste-colors.js +5 -5
- package/bin/lib/errors.js +16 -0
- package/bin/lib/file-lock.js +279 -44
- package/bin/lib/state-sync.js +206 -23
- package/bin/lib/update-transaction.js +206 -24
- package/bin/sync-to-runtime.sh +129 -0
- package/package.json +2 -2
- package/runtime/CONTEXT.md +160 -0
- package/runtime/aether-utils.sh +1421 -55
- package/runtime/docs/AETHER-2.0-IMPLEMENTATION-PLAN.md +1343 -0
- package/runtime/docs/AETHER-PHEROMONE-SYSTEM-MASTER-SPEC.md +2642 -0
- package/runtime/docs/PHEROMONE-INJECTION.md +240 -0
- package/runtime/docs/PHEROMONE-INTEGRATION.md +192 -0
- package/runtime/docs/PHEROMONE-SYSTEM-DESIGN.md +426 -0
- package/runtime/docs/README.md +94 -0
- package/runtime/docs/VISUAL-OUTPUT-SPEC.md +219 -0
- package/runtime/docs/biological-reference.md +272 -0
- package/runtime/docs/codebase-review.md +399 -0
- package/runtime/docs/command-sync.md +164 -0
- package/runtime/docs/implementation-learnings.md +89 -0
- package/runtime/docs/known-issues.md +217 -0
- package/runtime/docs/namespace.md +148 -0
- package/runtime/docs/planning-discipline.md +159 -0
- package/runtime/lib/queen-utils.sh +729 -0
- package/runtime/model-profiles.yaml +100 -0
- package/runtime/recover.sh +136 -0
- package/runtime/templates/QUEEN.md.template +79 -0
- package/runtime/utils/atomic-write.sh +5 -5
- package/runtime/utils/chamber-utils.sh +6 -3
- package/runtime/utils/error-handler.sh +200 -0
- package/runtime/utils/queen-to-md.xsl +395 -0
- package/runtime/utils/spawn-tree.sh +428 -0
- package/runtime/utils/spawn-with-model.sh +56 -0
- package/runtime/utils/state-loader.sh +215 -0
- package/runtime/utils/swarm-display.sh +5 -5
- package/runtime/utils/watch-spawn-tree.sh +90 -22
- package/runtime/utils/xml-compose.sh +247 -0
- package/runtime/utils/xml-core.sh +186 -0
- package/runtime/utils/xml-utils.sh +2161 -0
- package/runtime/verification-loop.md +1 -1
- package/runtime/workers-new-castes.md +516 -0
- package/runtime/workers.md +20 -8
- package/.aether/visualizations/anthill-stages/brood-stable.txt +0 -26
- package/.aether/visualizations/anthill-stages/crowned-anthill.txt +0 -30
- package/.aether/visualizations/anthill-stages/first-mound.txt +0 -18
- package/.aether/visualizations/anthill-stages/open-chambers.txt +0 -24
- package/.aether/visualizations/anthill-stages/sealed-chambers.txt +0 -28
- 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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
local
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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/&/\&/g; s/</\</g; s/>/\>/g; s/"/\"/g; s/'"'"'/\'/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/&/\&/g; s/</</g; s/>/>/g; s/"/"/g; s/'/'"'"'/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/&/\&/g; s/</\</g; s/>/\>/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
|