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