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,1710 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Queen utility functions -- extracted from aether-utils.sh
|
|
3
|
+
# Provides: _queen_init, _queen_read, _queen_thresholds, _queen_promote
|
|
4
|
+
# Also includes: _extract_wisdom_sections (helper used only by _queen_read)
|
|
5
|
+
# Note: Uses get_wisdom_threshold() and get_wisdom_thresholds_json() which remain in the main file
|
|
6
|
+
|
|
7
|
+
# ============================================================================
|
|
8
|
+
# _extract_wisdom_sections
|
|
9
|
+
# Helper function to extract wisdom sections from a QUEEN.md file
|
|
10
|
+
# Uses line number approach to avoid macOS awk range issues
|
|
11
|
+
# Usage: _extract_wisdom_sections <file_path>
|
|
12
|
+
# Returns: JSON object with wisdom sections
|
|
13
|
+
# ============================================================================
|
|
14
|
+
_extract_wisdom_sections() {
|
|
15
|
+
local file="$1"
|
|
16
|
+
|
|
17
|
+
# Format detection: check for v2 header "## Build Learnings"
|
|
18
|
+
# If present -> v2 format (4 sections). Otherwise -> v1 format (6 emoji sections, mapped).
|
|
19
|
+
if grep -q '^## Build Learnings$' "$file" 2>/dev/null; then
|
|
20
|
+
# === V2 FORMAT (4 clean sections) ===
|
|
21
|
+
local uprefs_line=$(awk '/^## User Preferences$/ {print NR; exit}' "$file")
|
|
22
|
+
local cpat_line=$(awk '/^## Codebase Patterns$/ {print NR; exit}' "$file")
|
|
23
|
+
local blearn_line=$(awk '/^## Build Learnings$/ {print NR; exit}' "$file")
|
|
24
|
+
local inst_line=$(awk '/^## Instincts$/ {print NR; exit}' "$file")
|
|
25
|
+
local evo_line=$(awk '/^## Evolution Log$/ {print NR; exit}' "$file")
|
|
26
|
+
|
|
27
|
+
local user_prefs codebase_patterns build_learnings instincts
|
|
28
|
+
|
|
29
|
+
# User Preferences: between uprefs_line and next section
|
|
30
|
+
local uprefs_end="${cpat_line:-${blearn_line:-${inst_line:-${evo_line:-999999}}}}"
|
|
31
|
+
if [[ -n "$uprefs_line" ]]; then
|
|
32
|
+
# SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
|
|
33
|
+
user_prefs=$(awk -v s="$uprefs_line" -v e="$uprefs_end" 'NR > s && NR < e {print}' "$file" | sed '/^$/d' | sed '/^---$/d' | jq -Rs '.' 2>/dev/null || echo '""')
|
|
34
|
+
else user_prefs='""'; fi
|
|
35
|
+
|
|
36
|
+
# Codebase Patterns: between cpat_line and next section
|
|
37
|
+
local cpat_end="${blearn_line:-${inst_line:-${evo_line:-999999}}}"
|
|
38
|
+
if [[ -n "$cpat_line" ]]; then
|
|
39
|
+
# SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
|
|
40
|
+
codebase_patterns=$(awk -v s="$cpat_line" -v e="$cpat_end" 'NR > s && NR < e {print}' "$file" | sed '/^$/d' | sed '/^---$/d' | jq -Rs '.' 2>/dev/null || echo '""')
|
|
41
|
+
else codebase_patterns='""'; fi
|
|
42
|
+
|
|
43
|
+
# Build Learnings: between blearn_line and next section
|
|
44
|
+
local blearn_end="${inst_line:-${evo_line:-999999}}"
|
|
45
|
+
if [[ -n "$blearn_line" ]]; then
|
|
46
|
+
# SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
|
|
47
|
+
build_learnings=$(awk -v s="$blearn_line" -v e="$blearn_end" 'NR > s && NR < e {print}' "$file" | sed '/^$/d' | sed '/^---$/d' | jq -Rs '.' 2>/dev/null || echo '""')
|
|
48
|
+
else build_learnings='""'; fi
|
|
49
|
+
|
|
50
|
+
# Instincts: between inst_line and evo_line (or end)
|
|
51
|
+
if [[ -n "$inst_line" ]]; then
|
|
52
|
+
# SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
|
|
53
|
+
instincts=$(awk -v s="$inst_line" -v e="${evo_line:-999999}" 'NR > s && NR < e {print}' "$file" | sed '/^$/d' | sed '/^---$/d' | jq -Rs '.' 2>/dev/null || echo '""')
|
|
54
|
+
else instincts='""'; fi
|
|
55
|
+
|
|
56
|
+
# Output v2 JSON
|
|
57
|
+
echo "{\"user_prefs\":$user_prefs,\"codebase_patterns\":$codebase_patterns,\"build_learnings\":$build_learnings,\"instincts\":$instincts}"
|
|
58
|
+
|
|
59
|
+
else
|
|
60
|
+
# === V1 FORMAT (6 emoji sections, mapped to v2 keys) ===
|
|
61
|
+
local p_line=$(awk '/^## ..? ?Philosophies$/ {print NR; exit}' "$file")
|
|
62
|
+
local pat_line=$(awk '/^## ..? ?Patterns$/ {print NR; exit}' "$file")
|
|
63
|
+
local red_line=$(awk '/^## ..? ?Redirects$/ {print NR; exit}' "$file")
|
|
64
|
+
local stack_line=$(awk '/^## ..? ?Stack Wisdom$/ {print NR; exit}' "$file")
|
|
65
|
+
local dec_line=$(awk '/^## ..? ?Decrees$/ {print NR; exit}' "$file")
|
|
66
|
+
local prefs_line=$(awk '/^## ..? ?User Preferences$/ {print NR; exit}' "$file")
|
|
67
|
+
local evo_line=$(awk '/^## ..? ?Evolution Log$/ {print NR; exit}' "$file")
|
|
68
|
+
|
|
69
|
+
local philosophies patterns redirects stack_wisdom decrees user_prefs
|
|
70
|
+
|
|
71
|
+
if [[ -n "$p_line" && -n "$pat_line" ]]; then
|
|
72
|
+
# SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
|
|
73
|
+
philosophies=$(awk -v s="$p_line" -v e="$pat_line" 'NR > s && NR < e {print}' "$file" | sed '/^$/d' | jq -Rs '.' 2>/dev/null || echo '""')
|
|
74
|
+
else philosophies='""'; fi
|
|
75
|
+
|
|
76
|
+
if [[ -n "$pat_line" && -n "$red_line" ]]; then
|
|
77
|
+
# SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
|
|
78
|
+
patterns=$(awk -v s="$pat_line" -v e="$red_line" 'NR > s && NR < e {print}' "$file" | sed '/^$/d' | jq -Rs '.' 2>/dev/null || echo '""')
|
|
79
|
+
else patterns='""'; fi
|
|
80
|
+
|
|
81
|
+
if [[ -n "$red_line" && -n "$stack_line" ]]; then
|
|
82
|
+
# SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
|
|
83
|
+
redirects=$(awk -v s="$red_line" -v e="$stack_line" 'NR > s && NR < e {print}' "$file" | sed '/^$/d' | jq -Rs '.' 2>/dev/null || echo '""')
|
|
84
|
+
else redirects='""'; fi
|
|
85
|
+
|
|
86
|
+
if [[ -n "$stack_line" && -n "$dec_line" ]]; then
|
|
87
|
+
# SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
|
|
88
|
+
stack_wisdom=$(awk -v s="$stack_line" -v e="$dec_line" 'NR > s && NR < e {print}' "$file" | sed '/^$/d' | jq -Rs '.' 2>/dev/null || echo '""')
|
|
89
|
+
else stack_wisdom='""'; fi
|
|
90
|
+
|
|
91
|
+
local dec_end="${prefs_line:-${evo_line:-999999}}"
|
|
92
|
+
if [[ -n "$dec_line" ]]; then
|
|
93
|
+
# SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
|
|
94
|
+
decrees=$(awk -v s="$dec_line" -v e="$dec_end" 'NR > s && NR < e {print}' "$file" | sed '/^$/d' | jq -Rs '.' 2>/dev/null || echo '""')
|
|
95
|
+
else decrees='""'; fi
|
|
96
|
+
|
|
97
|
+
if [[ -n "$prefs_line" ]]; then
|
|
98
|
+
# SUPPRESS:OK -- read-default: text escaping returns fallback on empty input
|
|
99
|
+
user_prefs=$(awk -v s="$prefs_line" -v e="${evo_line:-999999}" 'NR > s && NR < e {print}' "$file" | sed '/^$/d' | jq -Rs '.' 2>/dev/null || echo '""')
|
|
100
|
+
else user_prefs='""'; fi
|
|
101
|
+
|
|
102
|
+
# Map v1 sections to v2 keys:
|
|
103
|
+
# Philosophies + Patterns + Redirects + Stack Wisdom -> codebase_patterns
|
|
104
|
+
# Decrees + old User Preferences -> user_prefs
|
|
105
|
+
# build_learnings and instincts -> empty for v1 files
|
|
106
|
+
local combined_codebase
|
|
107
|
+
combined_codebase=$(jq -n \
|
|
108
|
+
--arg phil "$philosophies" \
|
|
109
|
+
--arg pat "$patterns" \
|
|
110
|
+
--arg red "$redirects" \
|
|
111
|
+
--arg stack "$stack_wisdom" \
|
|
112
|
+
'[$phil, $pat, $red, $stack] | map(select(. != "" and . != null)) | join("\n")' 2>/dev/null || echo '""')
|
|
113
|
+
|
|
114
|
+
local combined_uprefs
|
|
115
|
+
combined_uprefs=$(jq -n \
|
|
116
|
+
--arg dec "$decrees" \
|
|
117
|
+
--arg up "$user_prefs" \
|
|
118
|
+
'[$dec, $up] | map(select(. != "" and . != null)) | join("\n")' 2>/dev/null || echo '""')
|
|
119
|
+
|
|
120
|
+
echo "{\"user_prefs\":$combined_uprefs,\"codebase_patterns\":$combined_codebase,\"build_learnings\":\"\",\"instincts\":\"\"}"
|
|
121
|
+
fi
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# ============================================================================
|
|
125
|
+
# _queen_init
|
|
126
|
+
# Initialize QUEEN.md from template
|
|
127
|
+
# Creates .aether/QUEEN.md from template if missing
|
|
128
|
+
# Usage: Called via dispatcher as "queen-init"
|
|
129
|
+
# ============================================================================
|
|
130
|
+
_queen_init() {
|
|
131
|
+
local queen_file template_file timestamp path
|
|
132
|
+
queen_file="$AETHER_ROOT/.aether/QUEEN.md"
|
|
133
|
+
|
|
134
|
+
# Check multiple locations for template
|
|
135
|
+
# Order: hub (system/) -> dev (.aether/) -> repo local -> legacy
|
|
136
|
+
template_file=""
|
|
137
|
+
for path in \
|
|
138
|
+
"$HOME/.aether/system/templates/QUEEN.md.template" \
|
|
139
|
+
"$AETHER_ROOT/.aether/templates/QUEEN.md.template" \
|
|
140
|
+
"$HOME/.aether/templates/QUEEN.md.template"; do
|
|
141
|
+
if [[ -f "$path" ]]; then
|
|
142
|
+
template_file="$path"
|
|
143
|
+
break
|
|
144
|
+
fi
|
|
145
|
+
done
|
|
146
|
+
|
|
147
|
+
# Ensure .aether directory exists
|
|
148
|
+
mkdir -p "$AETHER_ROOT/.aether"
|
|
149
|
+
|
|
150
|
+
# Check if QUEEN.md already exists and has content
|
|
151
|
+
if [[ -f "$queen_file" ]] && [[ -s "$queen_file" ]]; then
|
|
152
|
+
json_ok '{"created":false,"path":".aether/QUEEN.md","reason":"already_exists"}'
|
|
153
|
+
exit 0
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# Check if template was found
|
|
157
|
+
if [[ -z "$template_file" ]]; then
|
|
158
|
+
json_err "$E_FILE_NOT_FOUND" \
|
|
159
|
+
"Template not found. Run: npm install -g aether && aether install to restore it." \
|
|
160
|
+
'{"templates_checked":["~/.aether/system/templates/QUEEN.md.template",".aether/templates/QUEEN.md.template","~/.aether/templates/QUEEN.md.template"]}'
|
|
161
|
+
exit 1
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
# Create QUEEN.md from template with timestamp substitution
|
|
165
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
166
|
+
sed -e "s/{TIMESTAMP}/$timestamp/g" "$template_file" > "$queen_file"
|
|
167
|
+
|
|
168
|
+
if [[ -f "$queen_file" ]]; then
|
|
169
|
+
json_ok "$(jq -n --arg source "$template_file" '{created: true, path: ".aether/QUEEN.md", source: $source}')"
|
|
170
|
+
else
|
|
171
|
+
json_err "$E_FILE_NOT_FOUND" "Failed to create QUEEN.md" '{"path":".aether/QUEEN.md"}'
|
|
172
|
+
exit 1
|
|
173
|
+
fi
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# ============================================================================
|
|
177
|
+
# _queen_read
|
|
178
|
+
# Read QUEEN.md and return wisdom as JSON for worker priming
|
|
179
|
+
# Supports two-level loading: global (~/.aether/QUEEN.md) first, then local (.aether/QUEEN.md)
|
|
180
|
+
# Local wisdom extends global - entries are combined per category
|
|
181
|
+
# Usage: Called via dispatcher as "queen-read"
|
|
182
|
+
# ============================================================================
|
|
183
|
+
_queen_read() {
|
|
184
|
+
local queen_global queen_local has_global has_local global_wisdom local_wisdom combined metadata
|
|
185
|
+
local user_prefs codebase_patterns build_learnings instincts result
|
|
186
|
+
queen_global="$HOME/.aether/QUEEN.md"
|
|
187
|
+
queen_local="$AETHER_ROOT/.aether/QUEEN.md"
|
|
188
|
+
|
|
189
|
+
# Track which files exist
|
|
190
|
+
has_global=false
|
|
191
|
+
has_local=false
|
|
192
|
+
|
|
193
|
+
# Check for global QUEEN.md
|
|
194
|
+
if [[ -f "$queen_global" ]]; then
|
|
195
|
+
has_global=true
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# Check for local QUEEN.md
|
|
199
|
+
if [[ -f "$queen_local" ]]; then
|
|
200
|
+
has_local=true
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# FAIL HARD if no QUEEN.md found at all
|
|
204
|
+
if [[ "$has_global" == "false" && "$has_local" == "false" ]]; then
|
|
205
|
+
json_err "$E_FILE_NOT_FOUND" "QUEEN.md not found" '{"global_path":"~/.aether/QUEEN.md","local_path":".aether/QUEEN.md"}'
|
|
206
|
+
exit 1
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
# Extract wisdom from global (if exists) -- _extract_wisdom_sections returns v2 keys
|
|
210
|
+
global_wisdom='{"user_prefs":"","codebase_patterns":"","build_learnings":"","instincts":""}'
|
|
211
|
+
if [[ "$has_global" == "true" ]]; then
|
|
212
|
+
global_wisdom=$(_extract_wisdom_sections "$queen_global")
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# Extract wisdom from local (if exists)
|
|
216
|
+
local_wisdom='{"user_prefs":"","codebase_patterns":"","build_learnings":"","instincts":""}'
|
|
217
|
+
if [[ "$has_local" == "true" ]]; then
|
|
218
|
+
local_wisdom=$(_extract_wisdom_sections "$queen_local")
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
# Combine wisdom: local extends global - content appended (v2 keys)
|
|
222
|
+
combined=$(jq -n \
|
|
223
|
+
--argjson global "$global_wisdom" \
|
|
224
|
+
--argjson local "$local_wisdom" \
|
|
225
|
+
'
|
|
226
|
+
def combine(a; b):
|
|
227
|
+
if a == "" or a == null then b
|
|
228
|
+
elif b == "" or b == null then a
|
|
229
|
+
else a + "\n" + b
|
|
230
|
+
end;
|
|
231
|
+
|
|
232
|
+
{
|
|
233
|
+
user_prefs: combine($global.user_prefs; $local.user_prefs),
|
|
234
|
+
codebase_patterns: combine($global.codebase_patterns; $local.codebase_patterns),
|
|
235
|
+
build_learnings: combine($global.build_learnings; $local.build_learnings),
|
|
236
|
+
instincts: combine($global.instincts; $local.instincts)
|
|
237
|
+
}
|
|
238
|
+
')
|
|
239
|
+
|
|
240
|
+
# Get metadata from local (preferred) or global
|
|
241
|
+
metadata='{"version":"unknown","last_evolved":null,"source":"none"}'
|
|
242
|
+
if [[ "$has_local" == "true" ]]; then
|
|
243
|
+
metadata=$(sed -n '/<!-- METADATA/,/-->/p' "$queen_local" | sed '1d;$d' | tr -d '\n' | sed 's/^[[:space:]]*//')
|
|
244
|
+
elif [[ "$has_global" == "true" ]]; then
|
|
245
|
+
metadata=$(sed -n '/<!-- METADATA/,/-->/p' "$queen_global" | sed '1d;$d' | tr -d '\n' | sed 's/^[[:space:]]*//')
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
# If no metadata found, return empty structure
|
|
249
|
+
if [[ -z "$metadata" ]]; then
|
|
250
|
+
metadata='{"version":"unknown","last_evolved":null,"source":"none","stats":{}}'
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
# Gate 1: Validate metadata is parseable JSON BEFORE using as --argjson
|
|
254
|
+
if ! echo "$metadata" | jq -e . >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON validity
|
|
255
|
+
json_err "$E_JSON_INVALID" \
|
|
256
|
+
"QUEEN.md has a malformed METADATA block — the JSON between <!-- METADATA and --> is invalid. Try: fix the JSON in .aether/QUEEN.md or run queen-init to reset."
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
# Extract individual combined wisdom values (v2 keys)
|
|
260
|
+
user_prefs=$(echo "$combined" | jq -r '.user_prefs')
|
|
261
|
+
codebase_patterns=$(echo "$combined" | jq -r '.codebase_patterns')
|
|
262
|
+
build_learnings=$(echo "$combined" | jq -r '.build_learnings')
|
|
263
|
+
instincts=$(echo "$combined" | jq -r '.instincts')
|
|
264
|
+
|
|
265
|
+
# Build JSON output (v2 keys)
|
|
266
|
+
# Pass shell-level file existence flags to jq (these are authoritative, not $meta.source)
|
|
267
|
+
local hg_json="false"
|
|
268
|
+
local hl_json="false"
|
|
269
|
+
[[ "$has_global" == "true" ]] && hg_json="true"
|
|
270
|
+
[[ "$has_local" == "true" ]] && hl_json="true"
|
|
271
|
+
|
|
272
|
+
result=$(jq -n \
|
|
273
|
+
--argjson meta "$metadata" \
|
|
274
|
+
--arg user_prefs "$user_prefs" \
|
|
275
|
+
--arg codebase_patterns "$codebase_patterns" \
|
|
276
|
+
--arg build_learnings "$build_learnings" \
|
|
277
|
+
--arg instincts "$instincts" \
|
|
278
|
+
--argjson has_g "$hg_json" \
|
|
279
|
+
--argjson has_l "$hl_json" \
|
|
280
|
+
'{
|
|
281
|
+
metadata: $meta,
|
|
282
|
+
wisdom: {
|
|
283
|
+
user_prefs: $user_prefs,
|
|
284
|
+
codebase_patterns: $codebase_patterns,
|
|
285
|
+
build_learnings: $build_learnings,
|
|
286
|
+
instincts: $instincts
|
|
287
|
+
},
|
|
288
|
+
priming: {
|
|
289
|
+
has_user_prefs: ([$user_prefs | split("\n")[] | select(startswith("- "))] | length) > 0,
|
|
290
|
+
has_codebase_patterns: ([$codebase_patterns | split("\n")[] | select(startswith("- "))] | length) > 0,
|
|
291
|
+
has_build_learnings: ([$build_learnings | split("\n")[] | select(startswith("- ") or startswith("#"))] | length) > 0,
|
|
292
|
+
has_instincts: ([$instincts | split("\n")[] | select(startswith("- "))] | length) > 0
|
|
293
|
+
},
|
|
294
|
+
sources: {
|
|
295
|
+
has_global: $has_g,
|
|
296
|
+
has_local: $has_l
|
|
297
|
+
}
|
|
298
|
+
}')
|
|
299
|
+
|
|
300
|
+
# Gate 2: Validate assembled result before returning
|
|
301
|
+
if [[ -z "$result" ]] || ! echo "$result" | jq -e . >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON validity
|
|
302
|
+
json_err "$E_JSON_INVALID" \
|
|
303
|
+
"Couldn't assemble queen-read output. QUEEN.md may have formatting issues. Try: run queen-init to reset."
|
|
304
|
+
fi
|
|
305
|
+
json_ok "$result"
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# ============================================================================
|
|
309
|
+
# _queen_thresholds
|
|
310
|
+
# Return proposal and auto-promotion thresholds for each wisdom type
|
|
311
|
+
# Usage: Called via dispatcher as "queen-thresholds"
|
|
312
|
+
# Note: Uses get_wisdom_thresholds_json() which remains in the main file
|
|
313
|
+
# ============================================================================
|
|
314
|
+
_queen_thresholds() {
|
|
315
|
+
json_ok "$(get_wisdom_thresholds_json)"
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
# ============================================================================
|
|
319
|
+
# _queen_promote
|
|
320
|
+
# Promote a learning to QUEEN.md wisdom
|
|
321
|
+
# Usage: Called via dispatcher as "queen-promote <type> <content> <colony_name>"
|
|
322
|
+
# Types: philosophy, pattern, redirect, stack, decree, failure
|
|
323
|
+
# Note: Uses get_wisdom_threshold() which remains in the main file
|
|
324
|
+
# ============================================================================
|
|
325
|
+
_queen_promote() {
|
|
326
|
+
local wisdom_type content colony_name valid_types type_valid vt queen_file threshold
|
|
327
|
+
local observations_file content_hash observation_data obs_count obs_colonies
|
|
328
|
+
local ts entry tmp_file section_header section_line next_section_line section_end
|
|
329
|
+
local has_placeholder entry_prefix ev_entry ev_separator current_count new_count stat_key
|
|
330
|
+
# Usage: queen-promote <type> <content> <colony_name>
|
|
331
|
+
# Types: philosophy, pattern, redirect, stack, decree
|
|
332
|
+
wisdom_type="${1:-}"
|
|
333
|
+
content="${2:-}"
|
|
334
|
+
colony_name="${3:-}"
|
|
335
|
+
|
|
336
|
+
# Validate required arguments
|
|
337
|
+
[[ -z "$wisdom_type" ]] && json_err "$E_VALIDATION_FAILED" "Usage: queen-promote <type> <content> <colony_name>" '{"missing":"type"}'
|
|
338
|
+
[[ -z "$content" ]] && json_err "$E_VALIDATION_FAILED" "Usage: queen-promote <type> <content> <colony_name>" '{"missing":"content"}'
|
|
339
|
+
[[ -z "$colony_name" ]] && json_err "$E_VALIDATION_FAILED" "Usage: queen-promote <type> <content> <colony_name>" '{"missing":"colony_name"}'
|
|
340
|
+
|
|
341
|
+
# Validate type (failure observations map to pattern when promoted)
|
|
342
|
+
valid_types=("philosophy" "pattern" "redirect" "stack" "decree" "failure")
|
|
343
|
+
type_valid=false
|
|
344
|
+
for vt in "${valid_types[@]}"; do
|
|
345
|
+
[[ "$wisdom_type" == "$vt" ]] && type_valid=true && break
|
|
346
|
+
done
|
|
347
|
+
[[ "$type_valid" == "false" ]] && json_err "$E_VALIDATION_FAILED" "Invalid type: $wisdom_type" '{"valid_types":["philosophy","pattern","redirect","stack","decree","failure"]}'
|
|
348
|
+
|
|
349
|
+
queen_file="$AETHER_ROOT/.aether/QUEEN.md"
|
|
350
|
+
|
|
351
|
+
# Check if QUEEN.md exists
|
|
352
|
+
if [[ ! -f "$queen_file" ]]; then
|
|
353
|
+
json_err "$E_FILE_NOT_FOUND" "QUEEN.md not found" '{"path":".aether/QUEEN.md"}'
|
|
354
|
+
exit 1
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
# Thresholds come from the shared command policy to keep promotion behavior consistent.
|
|
358
|
+
threshold=$(get_wisdom_threshold "$wisdom_type" "propose")
|
|
359
|
+
|
|
360
|
+
# QUEEN-04: Check threshold against learning-observations.json
|
|
361
|
+
# For decrees, always promote immediately (threshold 0)
|
|
362
|
+
# For other types, verify observation count meets threshold
|
|
363
|
+
observations_file="$COLONY_DATA_DIR/learning-observations.json"
|
|
364
|
+
content_hash="sha256:$(echo -n "$content" | sha256sum | cut -d' ' -f1)"
|
|
365
|
+
|
|
366
|
+
if [[ "$wisdom_type" != "decree" ]] && [[ -f "$observations_file" ]]; then
|
|
367
|
+
# Check if this content has been observed enough times
|
|
368
|
+
# SUPPRESS:OK -- read-default: query may return empty
|
|
369
|
+
observation_data=$(jq -r --arg hash "$content_hash" '.observations[] | select(.content_hash == $hash) | {count: .observation_count, colonies: .colonies}' "$observations_file" 2>/dev/null || echo '{}')
|
|
370
|
+
|
|
371
|
+
if [[ -n "$observation_data" ]] && [[ "$observation_data" != '{}' ]]; then
|
|
372
|
+
obs_count=$(echo "$observation_data" | jq -r '.count // 0')
|
|
373
|
+
obs_colonies=$(echo "$observation_data" | jq -r '.colonies // []')
|
|
374
|
+
|
|
375
|
+
if [[ "$obs_count" -lt "$threshold" ]]; then
|
|
376
|
+
json_err "$E_VALIDATION_FAILED" "Threshold not met: $obs_count/$threshold observations" "{\"observation_count\":$obs_count,\"threshold\":$threshold,\"content_hash\":\"$content_hash\"}"
|
|
377
|
+
fi
|
|
378
|
+
else
|
|
379
|
+
# No observations found for this content
|
|
380
|
+
json_err "$E_VALIDATION_FAILED" "No observations found for this content" "{\"threshold\":$threshold,\"content_hash\":\"$content_hash\"}"
|
|
381
|
+
fi
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
385
|
+
|
|
386
|
+
# Map type to section header (v2 format)
|
|
387
|
+
# Old types map to new sections; detect QUEEN.md format first
|
|
388
|
+
local is_v2=false
|
|
389
|
+
if grep -q '^## Build Learnings$' "$queen_file" 2>/dev/null; then
|
|
390
|
+
is_v2=true
|
|
391
|
+
fi
|
|
392
|
+
|
|
393
|
+
if [[ "$is_v2" == "true" ]]; then
|
|
394
|
+
# V2 format: map old types to new clean section headers
|
|
395
|
+
case "$wisdom_type" in
|
|
396
|
+
philosophy) section_header="## Codebase Patterns" ; entry_prefix="[general] " ;;
|
|
397
|
+
pattern|failure) section_header="## Codebase Patterns" ; entry_prefix="" ;;
|
|
398
|
+
redirect) section_header="## Codebase Patterns" ; entry_prefix="AVOID: " ;;
|
|
399
|
+
stack) section_header="## Codebase Patterns" ; entry_prefix="[repo] " ;;
|
|
400
|
+
decree) section_header="## User Preferences" ; entry_prefix="" ;;
|
|
401
|
+
esac
|
|
402
|
+
else
|
|
403
|
+
# V1 format: use original emoji headers
|
|
404
|
+
case "$wisdom_type" in
|
|
405
|
+
philosophy) section_header="## 📜 Philosophies" ; entry_prefix="" ;;
|
|
406
|
+
pattern|failure) section_header="## 🧭 Patterns" ; entry_prefix="" ;;
|
|
407
|
+
redirect) section_header="## ⚠️ Redirects" ; entry_prefix="" ;;
|
|
408
|
+
stack) section_header="## 🔧 Stack Wisdom" ; entry_prefix="" ;;
|
|
409
|
+
decree) section_header="## 🏛️ Decrees" ; entry_prefix="" ;;
|
|
410
|
+
esac
|
|
411
|
+
fi
|
|
412
|
+
|
|
413
|
+
# Build the new entry
|
|
414
|
+
entry="- ${entry_prefix}**${colony_name}** (${ts}): ${content}"
|
|
415
|
+
|
|
416
|
+
# Create temp file for atomic write
|
|
417
|
+
tmp_file="${queen_file}.tmp.$$"
|
|
418
|
+
|
|
419
|
+
# Trap-based cleanup for intermediate temp files on exit/interrupt
|
|
420
|
+
# Compose with _aether_exit_cleanup to preserve lock/temp cleanup
|
|
421
|
+
# SUPPRESS:OK -- cleanup: files may not exist yet
|
|
422
|
+
trap 'rm -f "${tmp_file}" "${tmp_file}".*; _aether_exit_cleanup 2>/dev/null || true' EXIT TERM INT HUP
|
|
423
|
+
|
|
424
|
+
# Find line numbers for section boundaries
|
|
425
|
+
section_line=$(grep -n "^${section_header}$" "$queen_file" | head -1 | cut -d: -f1)
|
|
426
|
+
next_section_line=$(tail -n +$((section_line + 1)) "$queen_file" | grep -n "^## " | head -1 | cut -d: -f1)
|
|
427
|
+
if [[ -n "$next_section_line" ]]; then
|
|
428
|
+
section_end=$((section_line + next_section_line - 1))
|
|
429
|
+
else
|
|
430
|
+
section_end=$(wc -l < "$queen_file")
|
|
431
|
+
fi
|
|
432
|
+
|
|
433
|
+
# SUPPRESS:OK -- read-default: operation returns fallback on failure
|
|
434
|
+
# Check if section has placeholder (grep returns 1 when no matches, handle with || true)
|
|
435
|
+
# SUPPRESS:OK -- existence-test: grep returns 1 when no matches
|
|
436
|
+
has_placeholder=$(sed -n "${section_line},${section_end}p" "$queen_file" | grep -c "No.*recorded yet" || true)
|
|
437
|
+
has_placeholder=${has_placeholder:-0}
|
|
438
|
+
|
|
439
|
+
if [[ "$has_placeholder" -gt 0 ]]; then
|
|
440
|
+
# Replace placeholder with entry - only within the target section
|
|
441
|
+
# Find the specific line number of the placeholder within the section
|
|
442
|
+
placeholder_line=$(sed -n "${section_line},${section_end}p" "$queen_file" | grep -n "^\\*No .* recorded yet" | head -1 | cut -d: -f1)
|
|
443
|
+
if [[ -n "$placeholder_line" ]]; then
|
|
444
|
+
actual_line=$((section_line + placeholder_line - 1))
|
|
445
|
+
# Use head/tail instead of sed c-command for newline safety (sed c\ breaks on multi-line content on macOS)
|
|
446
|
+
{
|
|
447
|
+
head -n $((actual_line - 1)) "$queen_file"
|
|
448
|
+
echo "$entry"
|
|
449
|
+
tail -n +$((actual_line + 1)) "$queen_file"
|
|
450
|
+
} > "$tmp_file"
|
|
451
|
+
else
|
|
452
|
+
# Fallback: insert after section header using head/tail
|
|
453
|
+
{
|
|
454
|
+
head -n "$section_line" "$queen_file"
|
|
455
|
+
echo "$entry"
|
|
456
|
+
tail -n +$((section_line + 1)) "$queen_file"
|
|
457
|
+
} > "$tmp_file"
|
|
458
|
+
fi
|
|
459
|
+
else
|
|
460
|
+
# Insert entry after the description paragraph (after the second empty line in section)
|
|
461
|
+
# The structure is: header, blank, description, blank, [entries...]
|
|
462
|
+
# We want to insert after the blank line following the description
|
|
463
|
+
empty_lines=$(sed -n "$((section_line + 1)),${section_end}p" "$queen_file" | grep -n "^$" | cut -d: -f1)
|
|
464
|
+
# Get the second empty line (after description)
|
|
465
|
+
insert_line=$(echo "$empty_lines" | sed -n '2p')
|
|
466
|
+
if [[ -n "$insert_line" ]]; then
|
|
467
|
+
insert_line=$((section_line + insert_line))
|
|
468
|
+
else
|
|
469
|
+
# Fallback: use first empty line
|
|
470
|
+
insert_line=$(echo "$empty_lines" | head -1)
|
|
471
|
+
if [[ -n "$insert_line" ]]; then
|
|
472
|
+
insert_line=$((section_line + insert_line))
|
|
473
|
+
else
|
|
474
|
+
insert_line=$((section_line + 1))
|
|
475
|
+
fi
|
|
476
|
+
fi
|
|
477
|
+
# Insert the entry after the found line using head/tail for newline safety
|
|
478
|
+
{
|
|
479
|
+
head -n "$insert_line" "$queen_file"
|
|
480
|
+
echo "$entry"
|
|
481
|
+
tail -n +$((insert_line + 1)) "$queen_file"
|
|
482
|
+
} > "$tmp_file"
|
|
483
|
+
fi
|
|
484
|
+
|
|
485
|
+
# Update Evolution Log in temp file
|
|
486
|
+
ev_entry="| ${ts} | ${colony_name} | promoted_${wisdom_type} | Added: ${content:0:50}... |"
|
|
487
|
+
# Find the line after the separator in Evolution Log table
|
|
488
|
+
ev_separator=$(grep -n "^|------|" "$tmp_file" | tail -1 | cut -d: -f1 || true)
|
|
489
|
+
|
|
490
|
+
# Use awk for cross-platform insertion (only if separator found)
|
|
491
|
+
# Use ENVIRON instead of -v for user content to avoid C-escape interpretation (\n, \t, \\)
|
|
492
|
+
if [[ -n "$ev_separator" ]]; then
|
|
493
|
+
AETHER_EV_ENTRY="$ev_entry" awk -v line="$ev_separator" 'NR==line{print; print ENVIRON["AETHER_EV_ENTRY"]; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
|
|
494
|
+
fi
|
|
495
|
+
|
|
496
|
+
# Update METADATA stats in temp file
|
|
497
|
+
# Map wisdom_type to stat key -- detect v2 vs v1 METADATA format
|
|
498
|
+
if [[ "$is_v2" == "true" ]]; then
|
|
499
|
+
# V2 stats keys
|
|
500
|
+
case "$wisdom_type" in
|
|
501
|
+
philosophy|pattern|failure|redirect|stack) stat_key="total_codebase_patterns" ;;
|
|
502
|
+
decree) stat_key="total_user_prefs" ;;
|
|
503
|
+
*) stat_key="total_codebase_patterns" ;;
|
|
504
|
+
esac
|
|
505
|
+
else
|
|
506
|
+
# V1 stats keys (irregular plurals handled)
|
|
507
|
+
case "$wisdom_type" in
|
|
508
|
+
stack) stat_key="total_stack_entries" ;;
|
|
509
|
+
philosophy) stat_key="total_philosophies" ;;
|
|
510
|
+
*) stat_key="total_${wisdom_type}s" ;;
|
|
511
|
+
esac
|
|
512
|
+
fi
|
|
513
|
+
# Read current count from temp file (which has the latest state)
|
|
514
|
+
current_count=$(grep "\"${stat_key}\":" "$tmp_file" 2>/dev/null | grep -o '[0-9]*' | head -1 || true) # SUPPRESS:OK -- read-default: file may not exist
|
|
515
|
+
current_count=${current_count:-0}
|
|
516
|
+
new_count=$((current_count + 1))
|
|
517
|
+
|
|
518
|
+
# Update last_evolved using awk
|
|
519
|
+
awk -v ts="$ts" '/"last_evolved":/ { gsub(/"last_evolved": "[^"]*"/, "\"last_evolved\": \"" ts "\""); } {print}' "$tmp_file" > "${tmp_file}.meta" && mv "${tmp_file}.meta" "$tmp_file"
|
|
520
|
+
|
|
521
|
+
# Update stats count using awk
|
|
522
|
+
awk -v type="$stat_key" -v count="$new_count" '{
|
|
523
|
+
gsub("\"" type "\": [0-9]*", "\"" type "\": " count)
|
|
524
|
+
print
|
|
525
|
+
}' "$tmp_file" > "${tmp_file}.stats" && mv "${tmp_file}.stats" "$tmp_file"
|
|
526
|
+
|
|
527
|
+
# META-02: Update evolution_log in METADATA JSON
|
|
528
|
+
# Add entry with timestamp, action, wisdom_type, content_hash
|
|
529
|
+
ev_log_entry="{\"timestamp\": \"$ts\", \"action\": \"promote\", \"wisdom_type\": \"$wisdom_type\", \"content_hash\": \"$content_hash\", \"colony\": \"$colony_name\"}"
|
|
530
|
+
|
|
531
|
+
# Check if evolution_log exists in metadata, add if not
|
|
532
|
+
# Use ENVIRON instead of -v for user content to avoid C-escape interpretation (\n, \t, \\)
|
|
533
|
+
if ! grep -q '"evolution_log"' "$tmp_file"; then
|
|
534
|
+
# Add evolution_log array after stats
|
|
535
|
+
AETHER_EV_LOG_ENTRY="$ev_log_entry" awk '
|
|
536
|
+
/"stats": \{/ {
|
|
537
|
+
print
|
|
538
|
+
# Read until closing brace of stats
|
|
539
|
+
while (getline > 0) {
|
|
540
|
+
print
|
|
541
|
+
if (/\}/) break
|
|
542
|
+
}
|
|
543
|
+
# Add comma and evolution_log
|
|
544
|
+
print ","
|
|
545
|
+
print " \"evolution_log\": [" ENVIRON["AETHER_EV_LOG_ENTRY"] "]"
|
|
546
|
+
next
|
|
547
|
+
}
|
|
548
|
+
{ print }
|
|
549
|
+
' "$tmp_file" > "${tmp_file}.evlog" && mv "${tmp_file}.evlog" "$tmp_file"
|
|
550
|
+
else
|
|
551
|
+
# Append to existing evolution_log array
|
|
552
|
+
AETHER_EV_LOG_ENTRY="$ev_log_entry" awk '
|
|
553
|
+
/"evolution_log": \[/ {
|
|
554
|
+
# Check if array is empty or has items
|
|
555
|
+
if (/\]/) {
|
|
556
|
+
# Empty array - replace with entry
|
|
557
|
+
gsub(/"evolution_log": \[\]/, "\"evolution_log\": [" ENVIRON["AETHER_EV_LOG_ENTRY"] "]")
|
|
558
|
+
} else {
|
|
559
|
+
# Has items - need to add before closing bracket
|
|
560
|
+
# For now, just print and handle in next iteration
|
|
561
|
+
}
|
|
562
|
+
print
|
|
563
|
+
next
|
|
564
|
+
}
|
|
565
|
+
# Handle multi-line evolution_log arrays
|
|
566
|
+
/"evolution_log": \[/ && !/\]/ {
|
|
567
|
+
print
|
|
568
|
+
getline
|
|
569
|
+
if (/\]/) {
|
|
570
|
+
# Was empty, now add entry
|
|
571
|
+
print ENVIRON["AETHER_EV_LOG_ENTRY"]
|
|
572
|
+
print "]"
|
|
573
|
+
} else {
|
|
574
|
+
# Has items, add comma and entry before closing
|
|
575
|
+
print
|
|
576
|
+
while (getline > 0) {
|
|
577
|
+
if (/^\s*\]/) {
|
|
578
|
+
print ","
|
|
579
|
+
print ENVIRON["AETHER_EV_LOG_ENTRY"]
|
|
580
|
+
print "]"
|
|
581
|
+
break
|
|
582
|
+
}
|
|
583
|
+
print
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
next
|
|
587
|
+
}
|
|
588
|
+
{ print }
|
|
589
|
+
' "$tmp_file" > "${tmp_file}.evlog" && mv "${tmp_file}.evlog" "$tmp_file"
|
|
590
|
+
fi
|
|
591
|
+
|
|
592
|
+
# META-04: Update colonies_contributed mapping in METADATA JSON
|
|
593
|
+
# This maps content_hash to array of colonies that contributed
|
|
594
|
+
# Get colonies from observations file if available
|
|
595
|
+
colonies_json="[]"
|
|
596
|
+
if [[ -f "$observations_file" ]]; then
|
|
597
|
+
# SUPPRESS:OK -- read-default: query may return empty
|
|
598
|
+
colonies_from_obs=$(jq -r --arg hash "$content_hash" '.observations[] | select(.content_hash == $hash) | .colonies // [] | @json' "$observations_file" 2>/dev/null || echo '[]')
|
|
599
|
+
if [[ -n "$colonies_from_obs" ]] && [[ "$colonies_from_obs" != "null" ]]; then
|
|
600
|
+
colonies_json="$colonies_from_obs"
|
|
601
|
+
fi
|
|
602
|
+
fi
|
|
603
|
+
|
|
604
|
+
# Add colonies_contributed object if not present
|
|
605
|
+
if ! grep -q '"colonies_contributed"' "$tmp_file"; then
|
|
606
|
+
# Add after evolution_log or stats
|
|
607
|
+
awk -v hash="$content_hash" -v colonies="$colonies_json" '
|
|
608
|
+
/"evolution_log": / {
|
|
609
|
+
print
|
|
610
|
+
# Skip to end of evolution_log array
|
|
611
|
+
brace_count = 1
|
|
612
|
+
while (getline > 0) {
|
|
613
|
+
print
|
|
614
|
+
if (/\[/) brace_count++
|
|
615
|
+
if (/\]/) brace_count--
|
|
616
|
+
if (brace_count == 0) break
|
|
617
|
+
}
|
|
618
|
+
print ","
|
|
619
|
+
print " \"colonies_contributed\": {"
|
|
620
|
+
print " \"" hash "\": " colonies
|
|
621
|
+
print " }"
|
|
622
|
+
next
|
|
623
|
+
}
|
|
624
|
+
{ print }
|
|
625
|
+
' "$tmp_file" > "${tmp_file}.colmap" && mv "${tmp_file}.colmap" "$tmp_file"
|
|
626
|
+
else
|
|
627
|
+
# Update existing colonies_contributed - add/update entry for this hash
|
|
628
|
+
# Use jq for reliable JSON manipulation
|
|
629
|
+
meta_section=$(sed -n '/<!-- METADATA/,/-->/p' "$tmp_file" | sed '1d;$d' | tr -d '\n' | sed 's/^[[:space:]]*//')
|
|
630
|
+
if [[ -n "$meta_section" ]]; then
|
|
631
|
+
# SUPPRESS:OK -- read-default: returns fallback on failure
|
|
632
|
+
updated_meta=$(echo "$meta_section" | jq --arg hash "$content_hash" --argjson cols "$colonies_json" '.colonies_contributed[$hash] = $cols' 2>/dev/null || echo "$meta_section")
|
|
633
|
+
# Replace metadata section using head/tail to handle multi-line content safely
|
|
634
|
+
# awk -v cannot handle embedded newlines in variable values (C-escape interpretation)
|
|
635
|
+
local meta_start_line meta_end_line
|
|
636
|
+
meta_start_line=$(grep -n "^<!-- METADATA$" "$tmp_file" | head -1 | cut -d: -f1)
|
|
637
|
+
meta_end_line=$(grep -n "^-->$" "$tmp_file" | head -1 | cut -d: -f1)
|
|
638
|
+
if [[ -n "$meta_start_line" && -n "$meta_end_line" ]]; then
|
|
639
|
+
{
|
|
640
|
+
head -n $((meta_start_line - 1)) "$tmp_file"
|
|
641
|
+
printf '<!-- METADATA\n%s\n-->\n' "$updated_meta"
|
|
642
|
+
tail -n +$((meta_end_line + 1)) "$tmp_file"
|
|
643
|
+
} > "${tmp_file}.metaupd" && mv "${tmp_file}.metaupd" "$tmp_file"
|
|
644
|
+
fi
|
|
645
|
+
fi
|
|
646
|
+
fi
|
|
647
|
+
|
|
648
|
+
# Add colony to colonies_contributed array (legacy) if not present
|
|
649
|
+
if ! grep -q "\"${colony_name}\"" "$tmp_file"; then
|
|
650
|
+
# Add to colonies_contributed array using awk - handle empty and non-empty arrays
|
|
651
|
+
awk -v colony="$colony_name" '
|
|
652
|
+
/"colonies_contributed": \[\]/ {
|
|
653
|
+
gsub(/"colonies_contributed": \[\]/, "\"colonies_contributed\": [\"" colony "\"]")
|
|
654
|
+
print
|
|
655
|
+
next
|
|
656
|
+
}
|
|
657
|
+
/"colonies_contributed": \[/ && !/\]/ {
|
|
658
|
+
# Multi-line array, add at next closing bracket
|
|
659
|
+
print
|
|
660
|
+
next
|
|
661
|
+
}
|
|
662
|
+
/"colonies_contributed": \[/ {
|
|
663
|
+
# Single-line array with elements
|
|
664
|
+
gsub(/\]$/, "\"" colony "\", ]")
|
|
665
|
+
print
|
|
666
|
+
next
|
|
667
|
+
}
|
|
668
|
+
{ print }
|
|
669
|
+
' "$tmp_file" > "${tmp_file}.col" && mv "${tmp_file}.col" "$tmp_file"
|
|
670
|
+
fi
|
|
671
|
+
|
|
672
|
+
# Restore default cleanup trap before final move (file becomes permanent)
|
|
673
|
+
trap '_aether_exit_cleanup 2>/dev/null || true' EXIT TERM INT HUP
|
|
674
|
+
|
|
675
|
+
# Safety guard: never overwrite QUEEN.md with empty content
|
|
676
|
+
if [[ ! -s "$tmp_file" ]]; then
|
|
677
|
+
rm -f "$tmp_file"
|
|
678
|
+
json_err "$E_INTERNAL" "queen-promote produced empty output — aborting to protect QUEEN.md"
|
|
679
|
+
return 1
|
|
680
|
+
fi
|
|
681
|
+
|
|
682
|
+
# Atomic move
|
|
683
|
+
mv "$tmp_file" "$queen_file"
|
|
684
|
+
|
|
685
|
+
json_ok "$(jq -n --arg type "$wisdom_type" --arg colony "$colony_name" --arg ts "$ts" --argjson threshold "$threshold" --argjson new_count "$new_count" --arg hash "$content_hash" '{promoted: true, type: $type, colony: $colony, timestamp: $ts, threshold: $threshold, new_count: $new_count, content_hash: $hash}')"
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
# ============================================================================
|
|
689
|
+
# _queen_write_learnings
|
|
690
|
+
# Write build learnings directly to QUEEN.md Build Learnings section
|
|
691
|
+
# Bypasses observation thresholds -- every build writes learnings
|
|
692
|
+
# Usage: queen-write-learnings <phase_id> <phase_name> <learnings_json>
|
|
693
|
+
# learnings_json: [{"claim":"what happened","tag":"repo|general","evidence":"..."}]
|
|
694
|
+
# ============================================================================
|
|
695
|
+
_queen_write_learnings() {
|
|
696
|
+
local phase_id="${1:-}"
|
|
697
|
+
local phase_name="${2:-}"
|
|
698
|
+
local learnings_json="${3:-}"
|
|
699
|
+
|
|
700
|
+
# Validate required arguments
|
|
701
|
+
[[ -z "$phase_id" ]] && { json_err "$E_VALIDATION_FAILED" "Usage: queen-write-learnings <phase_id> <phase_name> <learnings_json>" '{"missing":"phase_id"}'; return 1; }
|
|
702
|
+
[[ -z "$phase_name" ]] && { json_err "$E_VALIDATION_FAILED" "Usage: queen-write-learnings <phase_id> <phase_name> <learnings_json>" '{"missing":"phase_name"}'; return 1; }
|
|
703
|
+
[[ -z "$learnings_json" ]] && { json_err "$E_VALIDATION_FAILED" "Usage: queen-write-learnings <phase_id> <phase_name> <learnings_json>" '{"missing":"learnings_json"}'; return 1; }
|
|
704
|
+
|
|
705
|
+
# Validate learnings_json is valid JSON array
|
|
706
|
+
if ! echo "$learnings_json" | jq -e 'type == "array"' >/dev/null 2>&1; then
|
|
707
|
+
json_err "$E_JSON_INVALID" "learnings_json must be a JSON array" '{"received":"not_array"}'
|
|
708
|
+
return 1
|
|
709
|
+
fi
|
|
710
|
+
|
|
711
|
+
local queen_file="$AETHER_ROOT/.aether/QUEEN.md"
|
|
712
|
+
|
|
713
|
+
if [[ ! -f "$queen_file" ]]; then
|
|
714
|
+
json_err "$E_FILE_NOT_FOUND" "QUEEN.md not found. Run queen-init first." '{"path":".aether/QUEEN.md"}'
|
|
715
|
+
return 1
|
|
716
|
+
fi
|
|
717
|
+
|
|
718
|
+
local ts
|
|
719
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
720
|
+
local date_short
|
|
721
|
+
date_short=$(date -u +"%Y-%m-%d")
|
|
722
|
+
|
|
723
|
+
# Build entries from learnings_json
|
|
724
|
+
local written=0
|
|
725
|
+
local entries=""
|
|
726
|
+
local subsection_header="### Phase ${phase_id}: ${phase_name}"
|
|
727
|
+
|
|
728
|
+
while IFS= read -r encoded_entry; do
|
|
729
|
+
[[ -z "$encoded_entry" ]] && continue
|
|
730
|
+
local claim tag evidence
|
|
731
|
+
claim=$(echo "$encoded_entry" | base64 -d 2>/dev/null | jq -r '.claim // ""' 2>/dev/null) || continue
|
|
732
|
+
tag=$(echo "$encoded_entry" | base64 -d 2>/dev/null | jq -r '.tag // "repo"' 2>/dev/null) || tag="repo"
|
|
733
|
+
evidence=$(echo "$encoded_entry" | base64 -d 2>/dev/null | jq -r '.evidence // ""' 2>/dev/null) || evidence=""
|
|
734
|
+
|
|
735
|
+
[[ -z "$claim" ]] && continue
|
|
736
|
+
|
|
737
|
+
# Dedup check: skip if claim already in QUEEN.md
|
|
738
|
+
if grep -Fq -- "$claim" "$queen_file" 2>/dev/null; then
|
|
739
|
+
continue
|
|
740
|
+
fi
|
|
741
|
+
|
|
742
|
+
local entry_line="- [${tag}] ${claim} -- *Phase ${phase_id} (${phase_name})* (${date_short})"
|
|
743
|
+
entries="${entries}${entry_line}"$'\n'
|
|
744
|
+
written=$((written + 1))
|
|
745
|
+
done < <(echo "$learnings_json" | jq -r '.[] | @base64')
|
|
746
|
+
|
|
747
|
+
# If nothing to write, return early
|
|
748
|
+
if [[ "$written" -eq 0 ]]; then
|
|
749
|
+
json_ok "$(jq -n --arg phase "$phase_id" --arg ts "$ts" \
|
|
750
|
+
'{written: 0, phase: $phase, timestamp: $ts, reason: "all_duplicates_or_empty"}')"
|
|
751
|
+
return 0
|
|
752
|
+
fi
|
|
753
|
+
|
|
754
|
+
# Create temp file for atomic write
|
|
755
|
+
local tmp_file="${queen_file}.tmp.$$"
|
|
756
|
+
cp "$queen_file" "$tmp_file"
|
|
757
|
+
|
|
758
|
+
# Find Build Learnings section
|
|
759
|
+
local section_line
|
|
760
|
+
section_line=$(grep -n '^## Build Learnings$' "$tmp_file" | head -1 | cut -d: -f1)
|
|
761
|
+
|
|
762
|
+
if [[ -z "$section_line" ]]; then
|
|
763
|
+
rm -f "$tmp_file"
|
|
764
|
+
json_err "$E_VALIDATION_FAILED" "Build Learnings section not found in QUEEN.md. Is this a v2 format file?" '{"section":"Build Learnings"}'
|
|
765
|
+
return 1
|
|
766
|
+
fi
|
|
767
|
+
|
|
768
|
+
# Find end of section (next ## header or end of file)
|
|
769
|
+
local next_section_line
|
|
770
|
+
next_section_line=$(tail -n +$((section_line + 1)) "$tmp_file" | grep -n "^## " | head -1 | cut -d: -f1)
|
|
771
|
+
local section_end
|
|
772
|
+
if [[ -n "$next_section_line" ]]; then
|
|
773
|
+
section_end=$((section_line + next_section_line - 1))
|
|
774
|
+
else
|
|
775
|
+
section_end=$(wc -l < "$tmp_file")
|
|
776
|
+
fi
|
|
777
|
+
|
|
778
|
+
# Remove placeholder if present
|
|
779
|
+
# SUPPRESS:OK -- existence-test: grep returns 1 when no matches
|
|
780
|
+
local has_placeholder
|
|
781
|
+
has_placeholder=$(sed -n "${section_line},${section_end}p" "$tmp_file" | grep -c "No build learnings recorded yet" || true)
|
|
782
|
+
has_placeholder=${has_placeholder:-0}
|
|
783
|
+
|
|
784
|
+
if [[ "$has_placeholder" -gt 0 ]]; then
|
|
785
|
+
local placeholder_line
|
|
786
|
+
placeholder_line=$(sed -n "${section_line},${section_end}p" "$tmp_file" | grep -n "^\\*No build learnings recorded yet" | head -1 | cut -d: -f1)
|
|
787
|
+
if [[ -n "$placeholder_line" ]]; then
|
|
788
|
+
local actual_line=$((section_line + placeholder_line - 1))
|
|
789
|
+
sed -i.bak "${actual_line}d" "$tmp_file" && rm -f "${tmp_file}.bak"
|
|
790
|
+
section_end=$((section_end - 1))
|
|
791
|
+
fi
|
|
792
|
+
fi
|
|
793
|
+
|
|
794
|
+
# Check if subsection header already exists
|
|
795
|
+
local has_subsection
|
|
796
|
+
has_subsection=$(sed -n "${section_line},${section_end}p" "$tmp_file" | grep -c "^### Phase ${phase_id}:" || true)
|
|
797
|
+
has_subsection=${has_subsection:-0}
|
|
798
|
+
|
|
799
|
+
if [[ "$has_subsection" -gt 0 ]]; then
|
|
800
|
+
# Find the subsection header line and append entries after it
|
|
801
|
+
local sub_line
|
|
802
|
+
sub_line=$(sed -n "${section_line},${section_end}p" "$tmp_file" | grep -n "^### Phase ${phase_id}:" | head -1 | cut -d: -f1)
|
|
803
|
+
local actual_sub_line=$((section_line + sub_line - 1))
|
|
804
|
+
# Find end of subsection (next ### or next ## or --- or end of section)
|
|
805
|
+
local sub_end_rel
|
|
806
|
+
sub_end_rel=$(tail -n +$((actual_sub_line + 1)) "$tmp_file" | grep -n "^###\|^## \|^---$" | head -1 | cut -d: -f1)
|
|
807
|
+
local insert_at
|
|
808
|
+
if [[ -n "$sub_end_rel" ]]; then
|
|
809
|
+
insert_at=$((actual_sub_line + sub_end_rel - 1))
|
|
810
|
+
else
|
|
811
|
+
insert_at=$section_end
|
|
812
|
+
fi
|
|
813
|
+
# Insert entries before the boundary
|
|
814
|
+
local temp_entries="${tmp_file}.entries"
|
|
815
|
+
{
|
|
816
|
+
head -n "$insert_at" "$tmp_file"
|
|
817
|
+
printf '%s' "$entries"
|
|
818
|
+
tail -n +$((insert_at + 1)) "$tmp_file"
|
|
819
|
+
} > "$temp_entries" && mv "$temp_entries" "$tmp_file"
|
|
820
|
+
else
|
|
821
|
+
# Create new subsection at end of Build Learnings section (before --- or next ##)
|
|
822
|
+
# Find last content line in section (skip trailing --- and blanks)
|
|
823
|
+
local insert_at
|
|
824
|
+
# Insert before the separator (---) that ends the section, or before next ## header
|
|
825
|
+
local sep_line
|
|
826
|
+
sep_line=$(sed -n "${section_line},${section_end}p" "$tmp_file" | grep -n "^---$" | tail -1 | cut -d: -f1)
|
|
827
|
+
if [[ -n "$sep_line" ]]; then
|
|
828
|
+
insert_at=$((section_line + sep_line - 2))
|
|
829
|
+
else
|
|
830
|
+
insert_at=$section_end
|
|
831
|
+
fi
|
|
832
|
+
|
|
833
|
+
local temp_entries="${tmp_file}.entries"
|
|
834
|
+
{
|
|
835
|
+
head -n "$insert_at" "$tmp_file"
|
|
836
|
+
echo ""
|
|
837
|
+
echo "$subsection_header"
|
|
838
|
+
printf '%s' "$entries"
|
|
839
|
+
tail -n +$((insert_at + 1)) "$tmp_file"
|
|
840
|
+
} > "$temp_entries" && mv "$temp_entries" "$tmp_file"
|
|
841
|
+
fi
|
|
842
|
+
|
|
843
|
+
# Update Evolution Log
|
|
844
|
+
local ev_entry="| ${ts} | phase-${phase_id} | build_learnings | Added ${written} learnings from Phase ${phase_id}: ${phase_name} |"
|
|
845
|
+
local ev_separator
|
|
846
|
+
ev_separator=$(grep -n "^|------|" "$tmp_file" | tail -1 | cut -d: -f1 || true)
|
|
847
|
+
if [[ -n "$ev_separator" ]]; then
|
|
848
|
+
# Use ENVIRON instead of -v for user content to avoid C-escape interpretation (\n, \t, \\)
|
|
849
|
+
AETHER_EV_ENTRY="$ev_entry" awk -v line="$ev_separator" 'NR==line{print; print ENVIRON["AETHER_EV_ENTRY"]; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
|
|
850
|
+
fi
|
|
851
|
+
|
|
852
|
+
# Update METADATA stats: increment total_build_learnings
|
|
853
|
+
local current_count
|
|
854
|
+
current_count=$(grep '"total_build_learnings":' "$tmp_file" 2>/dev/null | grep -o '[0-9]*' | head -1 || true)
|
|
855
|
+
current_count=${current_count:-0}
|
|
856
|
+
local new_count=$((current_count + written))
|
|
857
|
+
awk -v count="$new_count" '{
|
|
858
|
+
gsub(/"total_build_learnings": [0-9]*/, "\"total_build_learnings\": " count)
|
|
859
|
+
print
|
|
860
|
+
}' "$tmp_file" > "${tmp_file}.stats" && mv "${tmp_file}.stats" "$tmp_file"
|
|
861
|
+
|
|
862
|
+
# Update last_evolved
|
|
863
|
+
awk -v ts="$ts" '/"last_evolved":/ { gsub(/"last_evolved": "[^"]*"/, "\"last_evolved\": \"" ts "\""); } {print}' "$tmp_file" > "${tmp_file}.meta" && mv "${tmp_file}.meta" "$tmp_file"
|
|
864
|
+
|
|
865
|
+
# Safety guard: never overwrite QUEEN.md with empty content
|
|
866
|
+
if [[ ! -s "$tmp_file" ]]; then
|
|
867
|
+
rm -f "$tmp_file"
|
|
868
|
+
json_err "$E_INTERNAL" "queen-write-learnings produced empty output — aborting to protect QUEEN.md"
|
|
869
|
+
return 1
|
|
870
|
+
fi
|
|
871
|
+
|
|
872
|
+
# Atomic move
|
|
873
|
+
mv "$tmp_file" "$queen_file"
|
|
874
|
+
|
|
875
|
+
json_ok "$(jq -n --argjson written "$written" --arg phase "$phase_id" --arg ts "$ts" \
|
|
876
|
+
'{written: $written, phase: $phase, timestamp: $ts}')"
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
# ============================================================================
|
|
880
|
+
# _queen_promote_instinct
|
|
881
|
+
# Promote a high-confidence instinct to QUEEN.md Instincts section
|
|
882
|
+
# Usage: queen-promote-instinct <trigger> <action> [confidence] [domain]
|
|
883
|
+
# ============================================================================
|
|
884
|
+
_queen_promote_instinct() {
|
|
885
|
+
local trigger="${1:-}"
|
|
886
|
+
local action="${2:-}"
|
|
887
|
+
local confidence="${3:-0.8}"
|
|
888
|
+
local domain="${4:-workflow}"
|
|
889
|
+
|
|
890
|
+
# Validate required arguments
|
|
891
|
+
[[ -z "$trigger" ]] && { json_err "$E_VALIDATION_FAILED" "Usage: queen-promote-instinct <trigger> <action> [confidence] [domain]" '{"missing":"trigger"}'; return 1; }
|
|
892
|
+
[[ -z "$action" ]] && { json_err "$E_VALIDATION_FAILED" "Usage: queen-promote-instinct <trigger> <action> [confidence] [domain]" '{"missing":"action"}'; return 1; }
|
|
893
|
+
|
|
894
|
+
local queen_file="$AETHER_ROOT/.aether/QUEEN.md"
|
|
895
|
+
|
|
896
|
+
if [[ ! -f "$queen_file" ]]; then
|
|
897
|
+
json_err "$E_FILE_NOT_FOUND" "QUEEN.md not found. Run queen-init first." '{"path":".aether/QUEEN.md"}'
|
|
898
|
+
return 1
|
|
899
|
+
fi
|
|
900
|
+
|
|
901
|
+
# Dedup check: skip if action already in QUEEN.md
|
|
902
|
+
if grep -Fq -- "$action" "$queen_file" 2>/dev/null; then
|
|
903
|
+
json_ok '{"promoted":false,"written":0,"reason":"duplicate"}'
|
|
904
|
+
return 0
|
|
905
|
+
fi
|
|
906
|
+
|
|
907
|
+
local ts
|
|
908
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
909
|
+
|
|
910
|
+
# Build entry
|
|
911
|
+
local entry="- [instinct] **${domain}** (${confidence}): When ${trigger}, then ${action}"
|
|
912
|
+
|
|
913
|
+
# Create temp file for atomic write
|
|
914
|
+
local tmp_file="${queen_file}.tmp.$$"
|
|
915
|
+
cp "$queen_file" "$tmp_file"
|
|
916
|
+
|
|
917
|
+
# Find Instincts section
|
|
918
|
+
local section_line
|
|
919
|
+
section_line=$(grep -n '^## Instincts$' "$tmp_file" | head -1 | cut -d: -f1)
|
|
920
|
+
|
|
921
|
+
if [[ -z "$section_line" ]]; then
|
|
922
|
+
rm -f "$tmp_file"
|
|
923
|
+
json_err "$E_VALIDATION_FAILED" "Instincts section not found in QUEEN.md. Is this a v2 format file?" '{"section":"Instincts"}'
|
|
924
|
+
return 1
|
|
925
|
+
fi
|
|
926
|
+
|
|
927
|
+
# Find end of section
|
|
928
|
+
local next_section_line
|
|
929
|
+
next_section_line=$(tail -n +$((section_line + 1)) "$tmp_file" | grep -n "^## " | head -1 | cut -d: -f1)
|
|
930
|
+
local section_end
|
|
931
|
+
if [[ -n "$next_section_line" ]]; then
|
|
932
|
+
section_end=$((section_line + next_section_line - 1))
|
|
933
|
+
else
|
|
934
|
+
section_end=$(wc -l < "$tmp_file")
|
|
935
|
+
fi
|
|
936
|
+
|
|
937
|
+
# Check for placeholder and replace or append
|
|
938
|
+
# SUPPRESS:OK -- existence-test: grep returns 1 when no matches
|
|
939
|
+
local has_placeholder
|
|
940
|
+
has_placeholder=$(sed -n "${section_line},${section_end}p" "$tmp_file" | grep -c "No instincts recorded yet" || true)
|
|
941
|
+
has_placeholder=${has_placeholder:-0}
|
|
942
|
+
|
|
943
|
+
if [[ "$has_placeholder" -gt 0 ]]; then
|
|
944
|
+
local placeholder_line
|
|
945
|
+
placeholder_line=$(sed -n "${section_line},${section_end}p" "$tmp_file" | grep -n "^\\*No instincts recorded yet" | head -1 | cut -d: -f1)
|
|
946
|
+
if [[ -n "$placeholder_line" ]]; then
|
|
947
|
+
local actual_line=$((section_line + placeholder_line - 1))
|
|
948
|
+
# Use head/tail instead of sed c-command for newline safety
|
|
949
|
+
{
|
|
950
|
+
head -n $((actual_line - 1)) "$tmp_file"
|
|
951
|
+
echo "$entry"
|
|
952
|
+
tail -n +$((actual_line + 1)) "$tmp_file"
|
|
953
|
+
} > "${tmp_file}.rep" && mv "${tmp_file}.rep" "$tmp_file"
|
|
954
|
+
fi
|
|
955
|
+
else
|
|
956
|
+
# Append entry before section separator (---) or at end
|
|
957
|
+
local sep_line
|
|
958
|
+
sep_line=$(sed -n "${section_line},${section_end}p" "$tmp_file" | grep -n "^---$" | tail -1 | cut -d: -f1)
|
|
959
|
+
local insert_at
|
|
960
|
+
if [[ -n "$sep_line" ]]; then
|
|
961
|
+
insert_at=$((section_line + sep_line - 2))
|
|
962
|
+
else
|
|
963
|
+
insert_at=$section_end
|
|
964
|
+
fi
|
|
965
|
+
|
|
966
|
+
local temp_entries="${tmp_file}.entries"
|
|
967
|
+
{
|
|
968
|
+
head -n "$insert_at" "$tmp_file"
|
|
969
|
+
echo "$entry"
|
|
970
|
+
tail -n +$((insert_at + 1)) "$tmp_file"
|
|
971
|
+
} > "$temp_entries" && mv "$temp_entries" "$tmp_file"
|
|
972
|
+
fi
|
|
973
|
+
|
|
974
|
+
# Update Evolution Log
|
|
975
|
+
local ev_entry="| ${ts} | instinct | promoted_instinct | ${domain}: ${action:0:50}... |"
|
|
976
|
+
local ev_separator
|
|
977
|
+
ev_separator=$(grep -n "^|------|" "$tmp_file" | tail -1 | cut -d: -f1 || true)
|
|
978
|
+
if [[ -n "$ev_separator" ]]; then
|
|
979
|
+
# Use ENVIRON instead of -v for user content to avoid C-escape interpretation (\n, \t, \\)
|
|
980
|
+
AETHER_EV_ENTRY="$ev_entry" awk -v line="$ev_separator" 'NR==line{print; print ENVIRON["AETHER_EV_ENTRY"]; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
|
|
981
|
+
fi
|
|
982
|
+
|
|
983
|
+
# Update METADATA stats: increment total_instincts
|
|
984
|
+
local current_count
|
|
985
|
+
current_count=$(grep '"total_instincts":' "$tmp_file" 2>/dev/null | grep -o '[0-9]*' | head -1 || true)
|
|
986
|
+
current_count=${current_count:-0}
|
|
987
|
+
local new_count=$((current_count + 1))
|
|
988
|
+
awk -v count="$new_count" '{
|
|
989
|
+
gsub(/"total_instincts": [0-9]*/, "\"total_instincts\": " count)
|
|
990
|
+
print
|
|
991
|
+
}' "$tmp_file" > "${tmp_file}.stats" && mv "${tmp_file}.stats" "$tmp_file"
|
|
992
|
+
|
|
993
|
+
# Update last_evolved
|
|
994
|
+
awk -v ts="$ts" '/"last_evolved":/ { gsub(/"last_evolved": "[^"]*"/, "\"last_evolved\": \"" ts "\""); } {print}' "$tmp_file" > "${tmp_file}.meta" && mv "${tmp_file}.meta" "$tmp_file"
|
|
995
|
+
|
|
996
|
+
# Safety guard: never overwrite QUEEN.md with empty content
|
|
997
|
+
if [[ ! -s "$tmp_file" ]]; then
|
|
998
|
+
rm -f "$tmp_file"
|
|
999
|
+
json_err "$E_INTERNAL" "queen-promote-instinct produced empty output — aborting to protect QUEEN.md"
|
|
1000
|
+
return 1
|
|
1001
|
+
fi
|
|
1002
|
+
|
|
1003
|
+
# Atomic move
|
|
1004
|
+
mv "$tmp_file" "$queen_file"
|
|
1005
|
+
|
|
1006
|
+
json_ok "$(jq -n --arg domain "$domain" --argjson confidence "$confidence" --arg ts "$ts" \
|
|
1007
|
+
'{promoted: true, written: 1, domain: $domain, confidence: $confidence, timestamp: $ts}')"
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
# ============================================================================
|
|
1011
|
+
# _queen_seed_from_hive
|
|
1012
|
+
# Seed QUEEN.md Codebase Patterns section from cross-colony hive wisdom
|
|
1013
|
+
# Usage: queen-seed-from-hive [--domain <csv>] [--limit <N>]
|
|
1014
|
+
# Writes [hive]-tagged entries to Codebase Patterns. NON-BLOCKING.
|
|
1015
|
+
# ============================================================================
|
|
1016
|
+
_queen_seed_from_hive() {
|
|
1017
|
+
local qs_domain=""
|
|
1018
|
+
local qs_limit="5"
|
|
1019
|
+
|
|
1020
|
+
while [[ $# -gt 0 ]]; do
|
|
1021
|
+
case "$1" in
|
|
1022
|
+
--domain) qs_domain="${2:-}"; shift 2 ;;
|
|
1023
|
+
--limit) qs_limit="${2:-5}"; shift 2 ;;
|
|
1024
|
+
*) shift ;;
|
|
1025
|
+
esac
|
|
1026
|
+
done
|
|
1027
|
+
|
|
1028
|
+
local queen_file="$AETHER_ROOT/.aether/QUEEN.md"
|
|
1029
|
+
[[ -f "$queen_file" ]] || { json_ok '{"seeded":0,"reason":"no_queen_md"}'; return 0; }
|
|
1030
|
+
|
|
1031
|
+
# Read hive wisdom with domain filter
|
|
1032
|
+
local hive_args=(hive-read --limit "$qs_limit" --min-confidence 0.5 --format json)
|
|
1033
|
+
[[ -n "$qs_domain" ]] && hive_args+=(--domain "$qs_domain")
|
|
1034
|
+
local hive_result
|
|
1035
|
+
hive_result=$(bash "$0" "${hive_args[@]}" 2>/dev/null) || { json_ok '{"seeded":0,"reason":"hive_read_failed"}'; return 0; }
|
|
1036
|
+
|
|
1037
|
+
local entry_count
|
|
1038
|
+
entry_count=$(echo "$hive_result" | jq -r '.result.total_matched // 0' 2>/dev/null)
|
|
1039
|
+
[[ "$entry_count" -eq 0 ]] && { json_ok '{"seeded":0,"reason":"no_matching_wisdom"}'; return 0; }
|
|
1040
|
+
|
|
1041
|
+
# Build entries for QUEEN.md Codebase Patterns section
|
|
1042
|
+
local entries=""
|
|
1043
|
+
local seeded=0
|
|
1044
|
+
while IFS= read -r encoded; do
|
|
1045
|
+
[[ -z "$encoded" ]] && continue
|
|
1046
|
+
local text confidence
|
|
1047
|
+
text=$(echo "$encoded" | base64 -d | jq -r '.text // empty')
|
|
1048
|
+
confidence=$(echo "$encoded" | base64 -d | jq -r '.confidence // 0')
|
|
1049
|
+
[[ -z "$text" ]] && continue
|
|
1050
|
+
|
|
1051
|
+
# Dedup: skip if already in QUEEN.md
|
|
1052
|
+
if grep -Fq -- "$text" "$queen_file" 2>/dev/null; then continue; fi
|
|
1053
|
+
|
|
1054
|
+
entries="${entries}- [hive] ${text} (cross-colony, confidence: ${confidence})"$'\n'
|
|
1055
|
+
seeded=$((seeded + 1))
|
|
1056
|
+
done < <(echo "$hive_result" | jq -r '.result.entries[] | @base64')
|
|
1057
|
+
|
|
1058
|
+
[[ "$seeded" -eq 0 ]] && { json_ok '{"seeded":0,"reason":"all_duplicates"}'; return 0; }
|
|
1059
|
+
|
|
1060
|
+
# Write to Codebase Patterns section (reuse placeholder removal pattern from _queen_write_learnings)
|
|
1061
|
+
local tmp_file="${queen_file}.tmp.$$"
|
|
1062
|
+
cp "$queen_file" "$tmp_file"
|
|
1063
|
+
|
|
1064
|
+
local section_line
|
|
1065
|
+
section_line=$(grep -n '^## Codebase Patterns$' "$tmp_file" | head -1 | cut -d: -f1)
|
|
1066
|
+
|
|
1067
|
+
if [[ -z "$section_line" ]]; then
|
|
1068
|
+
rm -f "$tmp_file"
|
|
1069
|
+
json_ok '{"seeded":0,"reason":"no_codebase_patterns_section"}'
|
|
1070
|
+
return 0
|
|
1071
|
+
fi
|
|
1072
|
+
|
|
1073
|
+
# Find end of section (next ## header or end of file)
|
|
1074
|
+
local next_section_line
|
|
1075
|
+
next_section_line=$(tail -n +$((section_line + 1)) "$tmp_file" | grep -n "^## " | head -1 | cut -d: -f1)
|
|
1076
|
+
local section_end
|
|
1077
|
+
if [[ -n "$next_section_line" ]]; then
|
|
1078
|
+
section_end=$((section_line + next_section_line - 1))
|
|
1079
|
+
else
|
|
1080
|
+
section_end=$(wc -l < "$tmp_file")
|
|
1081
|
+
fi
|
|
1082
|
+
|
|
1083
|
+
# Remove placeholder if present
|
|
1084
|
+
# SUPPRESS:OK -- existence-test: grep returns 1 when no matches
|
|
1085
|
+
local has_placeholder
|
|
1086
|
+
has_placeholder=$(sed -n "${section_line},${section_end}p" "$tmp_file" | grep -c "No codebase patterns recorded yet" || true)
|
|
1087
|
+
has_placeholder=${has_placeholder:-0}
|
|
1088
|
+
|
|
1089
|
+
if [[ "$has_placeholder" -gt 0 ]]; then
|
|
1090
|
+
local placeholder_line
|
|
1091
|
+
placeholder_line=$(sed -n "${section_line},${section_end}p" "$tmp_file" | grep -n "^\\*No codebase patterns recorded yet" | head -1 | cut -d: -f1)
|
|
1092
|
+
if [[ -n "$placeholder_line" ]]; then
|
|
1093
|
+
local actual_line=$((section_line + placeholder_line - 1))
|
|
1094
|
+
sed -i.bak "${actual_line}d" "$tmp_file" && rm -f "${tmp_file}.bak"
|
|
1095
|
+
section_end=$((section_end - 1))
|
|
1096
|
+
fi
|
|
1097
|
+
fi
|
|
1098
|
+
|
|
1099
|
+
# Insert entries before the separator (---) that ends the section, or at end
|
|
1100
|
+
local sep_line
|
|
1101
|
+
sep_line=$(sed -n "${section_line},${section_end}p" "$tmp_file" | grep -n "^---$" | tail -1 | cut -d: -f1)
|
|
1102
|
+
local insert_at
|
|
1103
|
+
if [[ -n "$sep_line" ]]; then
|
|
1104
|
+
insert_at=$((section_line + sep_line - 2))
|
|
1105
|
+
else
|
|
1106
|
+
insert_at=$section_end
|
|
1107
|
+
fi
|
|
1108
|
+
|
|
1109
|
+
local temp_entries="${tmp_file}.entries"
|
|
1110
|
+
{
|
|
1111
|
+
head -n "$insert_at" "$tmp_file"
|
|
1112
|
+
printf '%s' "$entries"
|
|
1113
|
+
tail -n +$((insert_at + 1)) "$tmp_file"
|
|
1114
|
+
} > "$temp_entries" && mv "$temp_entries" "$tmp_file"
|
|
1115
|
+
|
|
1116
|
+
# Update Evolution Log with seed event
|
|
1117
|
+
local ts
|
|
1118
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1119
|
+
local ev_entry="| ${ts} | hive | seed | Seeded ${seeded} cross-colony patterns from hive |"
|
|
1120
|
+
local ev_separator
|
|
1121
|
+
ev_separator=$(grep -n "^|------|" "$tmp_file" | tail -1 | cut -d: -f1 || true)
|
|
1122
|
+
if [[ -n "$ev_separator" ]]; then
|
|
1123
|
+
# Use ENVIRON instead of -v for user content to avoid C-escape interpretation (\n, \t, \\)
|
|
1124
|
+
AETHER_EV_ENTRY="$ev_entry" awk -v line="$ev_separator" 'NR==line{print; print ENVIRON["AETHER_EV_ENTRY"]; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
|
|
1125
|
+
fi
|
|
1126
|
+
|
|
1127
|
+
# Update METADATA stats: increment total_codebase_patterns
|
|
1128
|
+
local current_count
|
|
1129
|
+
current_count=$(grep '"total_codebase_patterns":' "$tmp_file" 2>/dev/null | grep -o '[0-9]*' | head -1 || true)
|
|
1130
|
+
current_count=${current_count:-0}
|
|
1131
|
+
local new_count=$((current_count + seeded))
|
|
1132
|
+
awk -v count="$new_count" '{
|
|
1133
|
+
gsub(/"total_codebase_patterns": [0-9]*/, "\"total_codebase_patterns\": " count)
|
|
1134
|
+
print
|
|
1135
|
+
}' "$tmp_file" > "${tmp_file}.stats" && mv "${tmp_file}.stats" "$tmp_file"
|
|
1136
|
+
|
|
1137
|
+
# Update last_evolved
|
|
1138
|
+
awk -v ts="$ts" '/"last_evolved":/ { gsub(/"last_evolved": "[^"]*"/, "\"last_evolved\": \"" ts "\""); } {print}' "$tmp_file" > "${tmp_file}.meta" && mv "${tmp_file}.meta" "$tmp_file"
|
|
1139
|
+
|
|
1140
|
+
# Safety guard: never overwrite QUEEN.md with empty content
|
|
1141
|
+
if [[ ! -s "$tmp_file" ]]; then
|
|
1142
|
+
rm -f "$tmp_file"
|
|
1143
|
+
json_err "$E_INTERNAL" "queen-seed-from-hive produced empty output — aborting to protect QUEEN.md"
|
|
1144
|
+
return 1
|
|
1145
|
+
fi
|
|
1146
|
+
|
|
1147
|
+
# Atomic move
|
|
1148
|
+
mv "$tmp_file" "$queen_file"
|
|
1149
|
+
|
|
1150
|
+
json_ok "$(jq -n --argjson seeded "$seeded" '{seeded: $seeded}')"
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
# ============================================================================
|
|
1154
|
+
# _domain_detect
|
|
1155
|
+
# Auto-detect repo domain tags from file/directory presence
|
|
1156
|
+
# Usage: domain-detect
|
|
1157
|
+
# Returns: {"tags":"node,typescript,..."}
|
|
1158
|
+
# ============================================================================
|
|
1159
|
+
_domain_detect() {
|
|
1160
|
+
local tags=""
|
|
1161
|
+
local root="${AETHER_ROOT:-.}"
|
|
1162
|
+
|
|
1163
|
+
[[ -f "$root/package.json" ]] && tags="${tags:+$tags,}node"
|
|
1164
|
+
[[ -f "$root/tsconfig.json" ]] && tags="${tags:+$tags,}typescript"
|
|
1165
|
+
[[ -f "$root/Cargo.toml" ]] && tags="${tags:+$tags,}rust"
|
|
1166
|
+
[[ -f "$root/go.mod" ]] && tags="${tags:+$tags,}go"
|
|
1167
|
+
[[ -f "$root/requirements.txt" || -f "$root/pyproject.toml" ]] && tags="${tags:+$tags,}python"
|
|
1168
|
+
[[ -d "$root/wp-content" || -f "$root/wp-config.php" ]] && tags="${tags:+$tags,}wordpress"
|
|
1169
|
+
[[ -f "$root/Gemfile" ]] && tags="${tags:+$tags,}ruby"
|
|
1170
|
+
[[ -f "$root/.aether/aether-utils.sh" ]] && tags="${tags:+$tags,}aether"
|
|
1171
|
+
[[ -d "$root/.next" || -f "$root/next.config.js" || -f "$root/next.config.ts" ]] && tags="${tags:+$tags,}nextjs"
|
|
1172
|
+
[[ -f "$root/docker-compose.yml" || -f "$root/Dockerfile" ]] && tags="${tags:+$tags,}docker"
|
|
1173
|
+
|
|
1174
|
+
json_ok "$(jq -n --arg tags "$tags" '{tags: $tags}')"
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
# ============================================================================
|
|
1178
|
+
# _queen_migrate
|
|
1179
|
+
# Convert a v1 QUEEN.md (emoji headers) to v2 (clean headers) format
|
|
1180
|
+
# Usage: queen-migrate [--target hub|local]
|
|
1181
|
+
# --target hub -> migrates $HOME/.aether/QUEEN.md
|
|
1182
|
+
# --target local -> migrates $AETHER_ROOT/.aether/QUEEN.md (default)
|
|
1183
|
+
# If already v2 format, prints message and exits 0.
|
|
1184
|
+
# ============================================================================
|
|
1185
|
+
_queen_migrate() {
|
|
1186
|
+
local qm_target="local"
|
|
1187
|
+
|
|
1188
|
+
while [[ $# -gt 0 ]]; do
|
|
1189
|
+
case "$1" in
|
|
1190
|
+
--target) qm_target="${2:-local}"; shift 2 ;;
|
|
1191
|
+
*) shift ;;
|
|
1192
|
+
esac
|
|
1193
|
+
done
|
|
1194
|
+
|
|
1195
|
+
local qm_file
|
|
1196
|
+
if [[ "$qm_target" == "hub" ]]; then
|
|
1197
|
+
qm_file="$HOME/.aether/QUEEN.md"
|
|
1198
|
+
else
|
|
1199
|
+
qm_file="$AETHER_ROOT/.aether/QUEEN.md"
|
|
1200
|
+
fi
|
|
1201
|
+
|
|
1202
|
+
if [[ ! -f "$qm_file" ]]; then
|
|
1203
|
+
json_err "$E_FILE_NOT_FOUND" "QUEEN.md not found at $qm_file" '{"target":"'"$qm_target"'"}'
|
|
1204
|
+
exit 1
|
|
1205
|
+
fi
|
|
1206
|
+
|
|
1207
|
+
# Check if already v2 format
|
|
1208
|
+
if grep -q '^## Build Learnings$' "$qm_file" 2>/dev/null; then # SUPPRESS:OK -- existence-test: format detection
|
|
1209
|
+
json_ok '{"migrated":false,"reason":"Already v2 format"}'
|
|
1210
|
+
return 0
|
|
1211
|
+
fi
|
|
1212
|
+
|
|
1213
|
+
# Extract content from v1 format using _extract_wisdom_sections (maps v1 -> v2 keys)
|
|
1214
|
+
local qm_wisdom
|
|
1215
|
+
qm_wisdom=$(_extract_wisdom_sections "$qm_file")
|
|
1216
|
+
|
|
1217
|
+
# Extract real entries from each section
|
|
1218
|
+
# v1 _extract_wisdom_sections produces double-encoded JSON strings, so we need
|
|
1219
|
+
# to unwrap them: jq -r removes outer quotes, then sed strips remaining inner quotes
|
|
1220
|
+
local qm_uprefs qm_codebase qm_learnings qm_instincts
|
|
1221
|
+
qm_uprefs=$(echo "$qm_wisdom" | jq -r '.user_prefs // ""' 2>/dev/null | sed 's/^"//;s/"$//' | sed 's/\\n/\n/g') # SUPPRESS:OK -- read-default: may be empty
|
|
1222
|
+
qm_uprefs=$(echo "$qm_uprefs" | grep -E '^- ' || true) # SUPPRESS:OK -- grep returns 1 on no matches
|
|
1223
|
+
qm_codebase=$(echo "$qm_wisdom" | jq -r '.codebase_patterns // ""' 2>/dev/null | sed 's/^"//;s/"$//' | sed 's/\\n/\n/g') # SUPPRESS:OK -- read-default: may be empty
|
|
1224
|
+
qm_codebase=$(echo "$qm_codebase" | grep -E '^- ' || true) # SUPPRESS:OK -- grep returns 1 on no matches
|
|
1225
|
+
qm_learnings=$(echo "$qm_wisdom" | jq -r '.build_learnings // ""' 2>/dev/null | sed 's/^"//;s/"$//' | sed 's/\\n/\n/g') # SUPPRESS:OK -- read-default: may be empty
|
|
1226
|
+
qm_learnings=$(echo "$qm_learnings" | grep -E '^- ' || true) # SUPPRESS:OK -- grep returns 1 on no matches
|
|
1227
|
+
qm_instincts=$(echo "$qm_wisdom" | jq -r '.instincts // ""' 2>/dev/null | sed 's/^"//;s/"$//' | sed 's/\\n/\n/g') # SUPPRESS:OK -- read-default: may be empty
|
|
1228
|
+
qm_instincts=$(echo "$qm_instincts" | grep -E '^- ' || true) # SUPPRESS:OK -- grep returns 1 on no matches
|
|
1229
|
+
|
|
1230
|
+
local qm_ts
|
|
1231
|
+
qm_ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1232
|
+
|
|
1233
|
+
# Build fresh v2 QUEEN.md
|
|
1234
|
+
local qm_tmp="${qm_file}.migrate.$$"
|
|
1235
|
+
cat > "$qm_tmp" << MIGRATEEOF
|
|
1236
|
+
# QUEEN.md -- Colony Wisdom
|
|
1237
|
+
|
|
1238
|
+
> Last evolved: $qm_ts
|
|
1239
|
+
> Wisdom version: 2.0.0
|
|
1240
|
+
|
|
1241
|
+
---
|
|
1242
|
+
|
|
1243
|
+
## User Preferences
|
|
1244
|
+
|
|
1245
|
+
Communication style, expertise level, and decision-making patterns observed from the user (the Queen). These shape how the colony communicates and what it prioritizes.
|
|
1246
|
+
|
|
1247
|
+
MIGRATEEOF
|
|
1248
|
+
|
|
1249
|
+
if [[ -n "$qm_uprefs" ]]; then
|
|
1250
|
+
echo "$qm_uprefs" >> "$qm_tmp"
|
|
1251
|
+
else
|
|
1252
|
+
echo "*No user preferences recorded yet.*" >> "$qm_tmp"
|
|
1253
|
+
fi
|
|
1254
|
+
|
|
1255
|
+
cat >> "$qm_tmp" << 'MIGRATEEOF'
|
|
1256
|
+
|
|
1257
|
+
---
|
|
1258
|
+
|
|
1259
|
+
## Codebase Patterns
|
|
1260
|
+
|
|
1261
|
+
Validated approaches that work in this codebase, and anti-patterns to avoid. Includes architecture conventions, naming patterns, error handling style, and technology-specific insights.
|
|
1262
|
+
|
|
1263
|
+
MIGRATEEOF
|
|
1264
|
+
|
|
1265
|
+
if [[ -n "$qm_codebase" ]]; then
|
|
1266
|
+
echo "$qm_codebase" >> "$qm_tmp"
|
|
1267
|
+
else
|
|
1268
|
+
echo "*No codebase patterns recorded yet.*" >> "$qm_tmp"
|
|
1269
|
+
fi
|
|
1270
|
+
|
|
1271
|
+
cat >> "$qm_tmp" << 'MIGRATEEOF'
|
|
1272
|
+
|
|
1273
|
+
---
|
|
1274
|
+
|
|
1275
|
+
## Build Learnings
|
|
1276
|
+
|
|
1277
|
+
What worked and what failed during builds. Captures the full picture of colony experience -- successes, failures, and adjustments.
|
|
1278
|
+
|
|
1279
|
+
MIGRATEEOF
|
|
1280
|
+
|
|
1281
|
+
if [[ -n "$qm_learnings" ]]; then
|
|
1282
|
+
echo "$qm_learnings" >> "$qm_tmp"
|
|
1283
|
+
else
|
|
1284
|
+
echo "*No build learnings recorded yet.*" >> "$qm_tmp"
|
|
1285
|
+
fi
|
|
1286
|
+
|
|
1287
|
+
cat >> "$qm_tmp" << 'MIGRATEEOF'
|
|
1288
|
+
|
|
1289
|
+
---
|
|
1290
|
+
|
|
1291
|
+
## Instincts
|
|
1292
|
+
|
|
1293
|
+
High-confidence behavioral patterns that have been validated through repeated colony work. Auto-promoted when confidence reaches 0.8 or higher.
|
|
1294
|
+
|
|
1295
|
+
MIGRATEEOF
|
|
1296
|
+
|
|
1297
|
+
if [[ -n "$qm_instincts" ]]; then
|
|
1298
|
+
echo "$qm_instincts" >> "$qm_tmp"
|
|
1299
|
+
else
|
|
1300
|
+
echo "*No instincts recorded yet.*" >> "$qm_tmp"
|
|
1301
|
+
fi
|
|
1302
|
+
|
|
1303
|
+
cat >> "$qm_tmp" << MIGRATEEOF
|
|
1304
|
+
|
|
1305
|
+
---
|
|
1306
|
+
|
|
1307
|
+
## Evolution Log
|
|
1308
|
+
|
|
1309
|
+
| Date | Source | Type | Details |
|
|
1310
|
+
|------|--------|------|---------|
|
|
1311
|
+
| $qm_ts | system | migration | Migrated from v1 to v2 format |
|
|
1312
|
+
|
|
1313
|
+
---
|
|
1314
|
+
|
|
1315
|
+
<!-- METADATA {"version":"2.0.0","wisdom_version":"2.0","last_evolved":"$qm_ts","colonies_contributed":[],"stats":{"total_user_prefs":0,"total_codebase_patterns":0,"total_build_learnings":0,"total_instincts":0}} -->
|
|
1316
|
+
MIGRATEEOF
|
|
1317
|
+
|
|
1318
|
+
# Atomic move
|
|
1319
|
+
mv "$qm_tmp" "$qm_file"
|
|
1320
|
+
|
|
1321
|
+
json_ok "$(jq -n --arg target "$qm_target" '{migrated: true, target: $target, format: "v2"}')"
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
# ============================================================================
|
|
1325
|
+
# _colony_name()
|
|
1326
|
+
# Derive human-readable colony name from repo context
|
|
1327
|
+
# Fallback chain: COLONY_STATE.json -> package.json -> directory basename
|
|
1328
|
+
# Usage: colony-name
|
|
1329
|
+
# Returns: {"ok":true,"result":{"name":"Aether Colony","source":"colony_state"}}
|
|
1330
|
+
# ============================================================================
|
|
1331
|
+
_colony_name() {
|
|
1332
|
+
local name=""
|
|
1333
|
+
local source="directory"
|
|
1334
|
+
|
|
1335
|
+
# 1. Check COLONY_STATE.json for pre-set colony_name
|
|
1336
|
+
if [[ -f "$DATA_DIR/COLONY_STATE.json" ]]; then
|
|
1337
|
+
local preset
|
|
1338
|
+
preset=$(jq -r '.colony_name // empty' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || echo "")
|
|
1339
|
+
if [[ -n "$preset" ]]; then
|
|
1340
|
+
name="$preset"
|
|
1341
|
+
source="colony_state"
|
|
1342
|
+
fi
|
|
1343
|
+
fi
|
|
1344
|
+
|
|
1345
|
+
# 2. Fall back to package.json name
|
|
1346
|
+
if [[ -z "$name" ]] && [[ -f "$AETHER_ROOT/package.json" ]]; then
|
|
1347
|
+
local pkg_name
|
|
1348
|
+
pkg_name=$(jq -r '.name // empty' "$AETHER_ROOT/package.json" 2>/dev/null || echo "")
|
|
1349
|
+
if [[ -n "$pkg_name" ]]; then
|
|
1350
|
+
# Strip @scope/ prefix
|
|
1351
|
+
name="${pkg_name#@*/}"
|
|
1352
|
+
source="package_json"
|
|
1353
|
+
fi
|
|
1354
|
+
fi
|
|
1355
|
+
|
|
1356
|
+
# 3. Fall back to directory basename
|
|
1357
|
+
if [[ -z "$name" ]]; then
|
|
1358
|
+
name="$(basename "$AETHER_ROOT")"
|
|
1359
|
+
source="directory"
|
|
1360
|
+
fi
|
|
1361
|
+
|
|
1362
|
+
# Convert kebab-case to title case
|
|
1363
|
+
name=$(echo "$name" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)};1')
|
|
1364
|
+
|
|
1365
|
+
json_ok "$(jq -n --arg name "$name" --arg source "$source" '{name: $name, source: $source}')"
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
# ============================================================================
|
|
1369
|
+
# _colony_depth()
|
|
1370
|
+
# Read or write colony depth setting (controls agent spawn thoroughness)
|
|
1371
|
+
# Values: light, standard (default), deep, full
|
|
1372
|
+
# NOTE: This is "colony depth" (build thoroughness), NOT "spawn depth"
|
|
1373
|
+
# (_spawn_get_depth which controls sub-worker nesting levels 1/2/3).
|
|
1374
|
+
# Usage: colony-depth [get|set <value>]
|
|
1375
|
+
# Returns: {"ok":true,"result":{"depth":"standard","source":"..."}}
|
|
1376
|
+
# ============================================================================
|
|
1377
|
+
_colony_depth() {
|
|
1378
|
+
local action="${1:-get}"
|
|
1379
|
+
|
|
1380
|
+
case "$action" in
|
|
1381
|
+
get)
|
|
1382
|
+
local depth
|
|
1383
|
+
depth=$(jq -r '.colony_depth // "standard"' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null || echo "standard")
|
|
1384
|
+
local source="default"
|
|
1385
|
+
if [[ "$(jq -r '.colony_depth // empty' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null)" != "" ]]; then
|
|
1386
|
+
source="colony_state"
|
|
1387
|
+
fi
|
|
1388
|
+
json_ok "$(jq -n --arg depth "$depth" --arg source "$source" '{depth: $depth, source: $source}')"
|
|
1389
|
+
;;
|
|
1390
|
+
set)
|
|
1391
|
+
local new_depth="${2:-}"
|
|
1392
|
+
case "$new_depth" in
|
|
1393
|
+
light|standard|deep|full)
|
|
1394
|
+
local tmp="${DATA_DIR}/COLONY_STATE.json.tmp.$$"
|
|
1395
|
+
jq --arg d "$new_depth" '.colony_depth = $d' "$DATA_DIR/COLONY_STATE.json" > "$tmp" && mv "$tmp" "$DATA_DIR/COLONY_STATE.json"
|
|
1396
|
+
json_ok "$(jq -n --arg depth "$new_depth" '{depth: $depth, updated: true}')"
|
|
1397
|
+
;;
|
|
1398
|
+
*)
|
|
1399
|
+
json_err "$E_VALIDATION_FAILED" "Invalid depth. Use: light, standard, deep, full" "$(jq -n --arg got "$new_depth" '{got: $got}')"
|
|
1400
|
+
return 1
|
|
1401
|
+
;;
|
|
1402
|
+
esac
|
|
1403
|
+
;;
|
|
1404
|
+
*)
|
|
1405
|
+
json_err "$E_VALIDATION_FAILED" "Usage: colony-depth [get|set <value>]" "{}"
|
|
1406
|
+
return 1
|
|
1407
|
+
;;
|
|
1408
|
+
esac
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
# ============================================================================
|
|
1412
|
+
# _queen_write_charter()
|
|
1413
|
+
# Write or update colony charter content in QUEEN.md using [charter] tags
|
|
1414
|
+
# Writes intent+vision to User Preferences, governance+goals to Codebase Patterns
|
|
1415
|
+
# Handles re-init: removes existing [charter] entries before writing new ones
|
|
1416
|
+
# Usage: charter-write --intent "text" --vision "text" --governance "text" --goals "text" [--colony-name "name"]
|
|
1417
|
+
# Returns: {"ok":true,"result":{"written":N,"updated":bool,"colony_name":"...","timestamp":"..."}}
|
|
1418
|
+
# ============================================================================
|
|
1419
|
+
_queen_write_charter() {
|
|
1420
|
+
local cw_intent=""
|
|
1421
|
+
local cw_vision=""
|
|
1422
|
+
local cw_governance=""
|
|
1423
|
+
local cw_goals=""
|
|
1424
|
+
local cw_colony_name=""
|
|
1425
|
+
|
|
1426
|
+
# Parse arguments
|
|
1427
|
+
while [[ $# -gt 0 ]]; do
|
|
1428
|
+
case "$1" in
|
|
1429
|
+
--intent) cw_intent="${2:-}"; shift 2 ;;
|
|
1430
|
+
--vision) cw_vision="${2:-}"; shift 2 ;;
|
|
1431
|
+
--governance) cw_governance="${2:-}"; shift 2 ;;
|
|
1432
|
+
--goals) cw_goals="${2:-}"; shift 2 ;;
|
|
1433
|
+
--colony-name) cw_colony_name="${2:-}"; shift 2 ;;
|
|
1434
|
+
*) shift ;;
|
|
1435
|
+
esac
|
|
1436
|
+
done
|
|
1437
|
+
|
|
1438
|
+
# At least one content field must be provided
|
|
1439
|
+
if [[ -z "$cw_intent" && -z "$cw_vision" && -z "$cw_governance" && -z "$cw_goals" ]]; then
|
|
1440
|
+
json_err "$E_VALIDATION_FAILED" "Usage: charter-write --intent <text> --vision <text> --governance <text> --goals <text> [--colony-name <name>]" '{"missing":"all_fields"}'
|
|
1441
|
+
return 1
|
|
1442
|
+
fi
|
|
1443
|
+
|
|
1444
|
+
local queen_file="$AETHER_ROOT/.aether/QUEEN.md"
|
|
1445
|
+
if [[ ! -f "$queen_file" ]]; then
|
|
1446
|
+
json_err "$E_FILE_NOT_FOUND" "QUEEN.md not found. Run queen-init first." '{"path":".aether/QUEEN.md"}'
|
|
1447
|
+
return 1
|
|
1448
|
+
fi
|
|
1449
|
+
|
|
1450
|
+
local ts
|
|
1451
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1452
|
+
|
|
1453
|
+
# Derive colony name if not provided
|
|
1454
|
+
if [[ -z "$cw_colony_name" ]]; then
|
|
1455
|
+
local cn_result
|
|
1456
|
+
cn_result=$(bash "$0" colony-name 2>/dev/null) || true
|
|
1457
|
+
cw_colony_name=$(echo "$cn_result" | jq -r '.result.name // ""' 2>/dev/null) || true
|
|
1458
|
+
fi
|
|
1459
|
+
|
|
1460
|
+
# Cap each content field at 200 characters
|
|
1461
|
+
_cap_200() {
|
|
1462
|
+
local val="$1"
|
|
1463
|
+
if [[ ${#val} -gt 200 ]]; then
|
|
1464
|
+
echo "${val:0:197}..."
|
|
1465
|
+
else
|
|
1466
|
+
echo "$val"
|
|
1467
|
+
fi
|
|
1468
|
+
}
|
|
1469
|
+
cw_intent=$(_cap_200 "$cw_intent")
|
|
1470
|
+
cw_vision=$(_cap_200 "$cw_vision")
|
|
1471
|
+
cw_governance=$(_cap_200 "$cw_governance")
|
|
1472
|
+
cw_goals=$(_cap_200 "$cw_goals")
|
|
1473
|
+
|
|
1474
|
+
# Auto-set colony_name in COLONY_STATE.json if not already set
|
|
1475
|
+
if [[ -f "$DATA_DIR/COLONY_STATE.json" ]]; then
|
|
1476
|
+
local current_name
|
|
1477
|
+
current_name=$(jq -r '.colony_name // empty' "$DATA_DIR/COLONY_STATE.json" 2>/dev/null) || true
|
|
1478
|
+
if [[ -z "$current_name" && -n "$cw_colony_name" ]]; then
|
|
1479
|
+
local tmp_state="${DATA_DIR}/COLONY_STATE.json.tmp.$$"
|
|
1480
|
+
jq --arg cn "$cw_colony_name" '.colony_name = $cn' "$DATA_DIR/COLONY_STATE.json" > "$tmp_state" && mv "$tmp_state" "$DATA_DIR/COLONY_STATE.json"
|
|
1481
|
+
fi
|
|
1482
|
+
fi
|
|
1483
|
+
|
|
1484
|
+
# Create temp file for atomic write
|
|
1485
|
+
local tmp_file="${queen_file}.tmp.$$"
|
|
1486
|
+
cp "$queen_file" "$tmp_file"
|
|
1487
|
+
|
|
1488
|
+
# Count existing charter entries before removal (to detect re-init)
|
|
1489
|
+
local existing_charter_count
|
|
1490
|
+
existing_charter_count=$(grep -c '^- \[charter\] ' "$tmp_file" 2>/dev/null || true)
|
|
1491
|
+
existing_charter_count=${existing_charter_count:-0}
|
|
1492
|
+
local is_update="false"
|
|
1493
|
+
if [[ "$existing_charter_count" -gt 0 ]]; then
|
|
1494
|
+
is_update="true"
|
|
1495
|
+
fi
|
|
1496
|
+
|
|
1497
|
+
# Remove ALL existing charter entries (re-init safety -- CHARTER-03)
|
|
1498
|
+
sed -i.bak '/^- \[charter\] /d' "$tmp_file" && rm -f "${tmp_file}.bak"
|
|
1499
|
+
|
|
1500
|
+
# Helper: insert entries into a QUEEN.md section
|
|
1501
|
+
# Usage: _insert_section_entries <tmp_file> <section_name> <placeholder_text> <entries>
|
|
1502
|
+
_insert_section_entries() {
|
|
1503
|
+
local is_file="$1"
|
|
1504
|
+
local is_section="$2"
|
|
1505
|
+
local is_placeholder="$3"
|
|
1506
|
+
local is_entries="$4"
|
|
1507
|
+
local is_tmp="${is_file}.insert"
|
|
1508
|
+
|
|
1509
|
+
local is_section_line
|
|
1510
|
+
is_section_line=$(grep -n "^## ${is_section}\$" "$is_file" | head -1 | cut -d: -f1)
|
|
1511
|
+
|
|
1512
|
+
if [[ -z "$is_section_line" ]]; then
|
|
1513
|
+
return 1
|
|
1514
|
+
fi
|
|
1515
|
+
|
|
1516
|
+
# Find end of section (next ## header or end of file)
|
|
1517
|
+
local is_next_section
|
|
1518
|
+
is_next_section=$(tail -n +$((is_section_line + 1)) "$is_file" | grep -n "^## " | head -1 | cut -d: -f1)
|
|
1519
|
+
local is_section_end
|
|
1520
|
+
if [[ -n "$is_next_section" ]]; then
|
|
1521
|
+
is_section_end=$((is_section_line + is_next_section - 1))
|
|
1522
|
+
else
|
|
1523
|
+
is_section_end=$(wc -l < "$is_file")
|
|
1524
|
+
fi
|
|
1525
|
+
|
|
1526
|
+
# Remove placeholder if present
|
|
1527
|
+
# SUPPRESS:OK -- existence-test: grep returns 1 when no matches
|
|
1528
|
+
local is_has_placeholder
|
|
1529
|
+
is_has_placeholder=$(sed -n "${is_section_line},${is_section_end}p" "$is_file" | grep -c "No .* recorded yet" || true)
|
|
1530
|
+
is_has_placeholder=${is_has_placeholder:-0}
|
|
1531
|
+
|
|
1532
|
+
if [[ "$is_has_placeholder" -gt 0 ]]; then
|
|
1533
|
+
local is_pl_line
|
|
1534
|
+
is_pl_line=$(sed -n "${is_section_line},${is_section_end}p" "$is_file" | grep -n "^\\*No .* recorded yet" | head -1 | cut -d: -f1)
|
|
1535
|
+
if [[ -n "$is_pl_line" ]]; then
|
|
1536
|
+
local is_actual_line=$((is_section_line + is_pl_line - 1))
|
|
1537
|
+
# Replace placeholder with entries
|
|
1538
|
+
{
|
|
1539
|
+
head -n $((is_actual_line - 1)) "$is_file"
|
|
1540
|
+
printf '%s\n' "$is_entries"
|
|
1541
|
+
tail -n +$((is_actual_line + 1)) "$is_file"
|
|
1542
|
+
} > "$is_tmp" && mv "$is_tmp" "$is_file"
|
|
1543
|
+
return 0
|
|
1544
|
+
fi
|
|
1545
|
+
fi
|
|
1546
|
+
|
|
1547
|
+
# No placeholder -- insert before section separator (---) or at section end
|
|
1548
|
+
local is_sep_line
|
|
1549
|
+
is_sep_line=$(sed -n "${is_section_line},${is_section_end}p" "$is_file" | grep -n "^---$" | tail -1 | cut -d: -f1)
|
|
1550
|
+
local is_insert_at
|
|
1551
|
+
if [[ -n "$is_sep_line" ]]; then
|
|
1552
|
+
is_insert_at=$((is_section_line + is_sep_line - 2))
|
|
1553
|
+
else
|
|
1554
|
+
is_insert_at=$is_section_end
|
|
1555
|
+
fi
|
|
1556
|
+
|
|
1557
|
+
{
|
|
1558
|
+
head -n "$is_insert_at" "$is_file"
|
|
1559
|
+
printf '%s\n' "$is_entries"
|
|
1560
|
+
tail -n +$((is_insert_at + 1)) "$is_file"
|
|
1561
|
+
} > "$is_tmp" && mv "$is_tmp" "$is_file"
|
|
1562
|
+
return 0
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
# Build User Preferences entries (intent + vision)
|
|
1566
|
+
local up_entries=""
|
|
1567
|
+
local written=0
|
|
1568
|
+
local up_failed=0
|
|
1569
|
+
local cp_failed=0
|
|
1570
|
+
if [[ -n "$cw_intent" ]]; then
|
|
1571
|
+
up_entries="${up_entries}- [charter] **Intent**: ${cw_intent} (Colony: ${cw_colony_name})"
|
|
1572
|
+
written=$((written + 1))
|
|
1573
|
+
fi
|
|
1574
|
+
if [[ -n "$cw_vision" ]]; then
|
|
1575
|
+
up_entries="${up_entries}"$'\n'"- [charter] **Vision**: ${cw_vision} (Colony: ${cw_colony_name})"
|
|
1576
|
+
written=$((written + 1))
|
|
1577
|
+
fi
|
|
1578
|
+
|
|
1579
|
+
if [[ -n "$up_entries" ]]; then
|
|
1580
|
+
if ! _insert_section_entries "$tmp_file" "User Preferences" "user preferences" "$up_entries"; then
|
|
1581
|
+
up_failed=1
|
|
1582
|
+
echo "[charter-write] WARNING: '## User Preferences' section not found in QUEEN.md; entries dropped" >&2
|
|
1583
|
+
fi
|
|
1584
|
+
fi
|
|
1585
|
+
|
|
1586
|
+
# Build Codebase Patterns entries (governance + goals)
|
|
1587
|
+
local cp_entries=""
|
|
1588
|
+
if [[ -n "$cw_governance" ]]; then
|
|
1589
|
+
cp_entries="${cp_entries}- [charter] **Governance**: ${cw_governance} (Colony: ${cw_colony_name})"
|
|
1590
|
+
written=$((written + 1))
|
|
1591
|
+
fi
|
|
1592
|
+
if [[ -n "$cw_goals" ]]; then
|
|
1593
|
+
cp_entries="${cp_entries}"$'\n'"- [charter] **Goal**: ${cw_goals} (Colony: ${cw_colony_name})"
|
|
1594
|
+
written=$((written + 1))
|
|
1595
|
+
fi
|
|
1596
|
+
|
|
1597
|
+
if [[ -n "$cp_entries" ]]; then
|
|
1598
|
+
if ! _insert_section_entries "$tmp_file" "Codebase Patterns" "codebase patterns" "$cp_entries"; then
|
|
1599
|
+
cp_failed=1
|
|
1600
|
+
echo "[charter-write] WARNING: '## Codebase Patterns' section not found in QUEEN.md; entries dropped" >&2
|
|
1601
|
+
fi
|
|
1602
|
+
fi
|
|
1603
|
+
|
|
1604
|
+
# Update Evolution Log
|
|
1605
|
+
local ev_type="charter_initialized"
|
|
1606
|
+
local ev_details="Colony charter created for ${cw_colony_name}"
|
|
1607
|
+
if [[ "$is_update" == "true" ]]; then
|
|
1608
|
+
ev_type="charter_updated"
|
|
1609
|
+
ev_details="Colony charter updated for ${cw_colony_name}"
|
|
1610
|
+
fi
|
|
1611
|
+
local ev_entry="| ${ts} | system | ${ev_type} | ${ev_details} |"
|
|
1612
|
+
local ev_separator
|
|
1613
|
+
ev_separator=$(grep -n "^|------|" "$tmp_file" | tail -1 | cut -d: -f1 || true)
|
|
1614
|
+
if [[ -n "$ev_separator" ]]; then
|
|
1615
|
+
# Use ENVIRON instead of -v for user content to avoid C-escape interpretation (\n, \t, \\)
|
|
1616
|
+
AETHER_EV_ENTRY="$ev_entry" awk -v line="$ev_separator" 'NR==line{print; print ENVIRON["AETHER_EV_ENTRY"]; next}1' "$tmp_file" > "${tmp_file}.ev" && mv "${tmp_file}.ev" "$tmp_file"
|
|
1617
|
+
fi
|
|
1618
|
+
|
|
1619
|
+
# Update METADATA stats -- count non-charter list items in each section, add charter entries
|
|
1620
|
+
local up_section_line
|
|
1621
|
+
up_section_line=$(grep -n '^## User Preferences$' "$tmp_file" | head -1 | cut -d: -f1 || true)
|
|
1622
|
+
|
|
1623
|
+
# Count charter entries written to User Preferences
|
|
1624
|
+
local up_charter_written=0
|
|
1625
|
+
[[ -n "$cw_intent" ]] && up_charter_written=$((up_charter_written + 1))
|
|
1626
|
+
[[ -n "$cw_vision" ]] && up_charter_written=$((up_charter_written + 1))
|
|
1627
|
+
|
|
1628
|
+
local up_total=0
|
|
1629
|
+
if [[ -n "$up_section_line" ]]; then
|
|
1630
|
+
local up_next_section
|
|
1631
|
+
up_next_section=$(tail -n +$((up_section_line + 1)) "$tmp_file" | grep -n "^## " | head -1 | cut -d: -f1)
|
|
1632
|
+
local up_section_end
|
|
1633
|
+
if [[ -n "$up_next_section" ]]; then
|
|
1634
|
+
up_section_end=$((up_section_line + up_next_section - 1))
|
|
1635
|
+
else
|
|
1636
|
+
up_section_end=$(wc -l < "$tmp_file")
|
|
1637
|
+
fi
|
|
1638
|
+
|
|
1639
|
+
# Count non-charter list items in User Preferences
|
|
1640
|
+
local up_non_charter
|
|
1641
|
+
# Count all list items, then subtract charter ones
|
|
1642
|
+
local up_all_items
|
|
1643
|
+
up_all_items=$(sed -n "${up_section_line},${up_section_end}p" "$tmp_file" | grep -c '^- ' || true)
|
|
1644
|
+
up_all_items=${up_all_items:-0}
|
|
1645
|
+
local up_charter_items
|
|
1646
|
+
up_charter_items=$(sed -n "${up_section_line},${up_section_end}p" "$tmp_file" | grep -c '^- \[charter\] ' || true)
|
|
1647
|
+
up_charter_items=${up_charter_items:-0}
|
|
1648
|
+
up_non_charter=$((up_all_items - up_charter_items))
|
|
1649
|
+
up_total=$((up_non_charter + up_charter_written))
|
|
1650
|
+
fi
|
|
1651
|
+
|
|
1652
|
+
# Count for Codebase Patterns
|
|
1653
|
+
local cp_section_line
|
|
1654
|
+
cp_section_line=$(grep -n '^## Codebase Patterns$' "$tmp_file" | head -1 | cut -d: -f1 || true)
|
|
1655
|
+
|
|
1656
|
+
# Count charter entries written to Codebase Patterns
|
|
1657
|
+
local cp_charter_written=0
|
|
1658
|
+
[[ -n "$cw_governance" ]] && cp_charter_written=$((cp_charter_written + 1))
|
|
1659
|
+
[[ -n "$cw_goals" ]] && cp_charter_written=$((cp_charter_written + 1))
|
|
1660
|
+
|
|
1661
|
+
local cp_total=0
|
|
1662
|
+
if [[ -n "$cp_section_line" ]]; then
|
|
1663
|
+
local cp_next_section
|
|
1664
|
+
cp_next_section=$(tail -n +$((cp_section_line + 1)) "$tmp_file" | grep -n "^## " | head -1 | cut -d: -f1)
|
|
1665
|
+
local cp_section_end
|
|
1666
|
+
if [[ -n "$cp_next_section" ]]; then
|
|
1667
|
+
cp_section_end=$((cp_section_line + cp_next_section - 1))
|
|
1668
|
+
else
|
|
1669
|
+
cp_section_end=$(wc -l < "$tmp_file")
|
|
1670
|
+
fi
|
|
1671
|
+
local cp_all_items
|
|
1672
|
+
cp_all_items=$(sed -n "${cp_section_line},${cp_section_end}p" "$tmp_file" | grep -c '^- ' || true)
|
|
1673
|
+
cp_all_items=${cp_all_items:-0}
|
|
1674
|
+
local cp_charter_items
|
|
1675
|
+
cp_charter_items=$(sed -n "${cp_section_line},${cp_section_end}p" "$tmp_file" | grep -c '^- \[charter\] ' || true)
|
|
1676
|
+
cp_charter_items=${cp_charter_items:-0}
|
|
1677
|
+
local cp_non_charter=$((cp_all_items - cp_charter_items))
|
|
1678
|
+
cp_total=$((cp_non_charter + cp_charter_written))
|
|
1679
|
+
fi
|
|
1680
|
+
|
|
1681
|
+
# Update METADATA stats
|
|
1682
|
+
awk -v up="$up_total" -v cp="$cp_total" '{
|
|
1683
|
+
gsub(/"total_user_prefs": [0-9]*/, "\"total_user_prefs\": " up)
|
|
1684
|
+
gsub(/"total_codebase_patterns": [0-9]*/, "\"total_codebase_patterns\": " cp)
|
|
1685
|
+
print
|
|
1686
|
+
}' "$tmp_file" > "${tmp_file}.stats" && mv "${tmp_file}.stats" "$tmp_file"
|
|
1687
|
+
|
|
1688
|
+
# Update last_evolved
|
|
1689
|
+
awk -v ts="$ts" '/"last_evolved":/ { gsub(/"last_evolved": "[^"]*"/, "\"last_evolved\": \"" ts "\""); } {print}' "$tmp_file" > "${tmp_file}.meta" && mv "${tmp_file}.meta" "$tmp_file"
|
|
1690
|
+
|
|
1691
|
+
# Safety guard: never overwrite QUEEN.md with empty content
|
|
1692
|
+
if [[ ! -s "$tmp_file" ]]; then
|
|
1693
|
+
rm -f "$tmp_file"
|
|
1694
|
+
json_err "$E_INTERNAL" "queen-write-charter produced empty output — aborting to protect QUEEN.md"
|
|
1695
|
+
return 1
|
|
1696
|
+
fi
|
|
1697
|
+
|
|
1698
|
+
# Atomic move
|
|
1699
|
+
mv "$tmp_file" "$queen_file"
|
|
1700
|
+
|
|
1701
|
+
local sections_failed=$((up_failed + cp_failed))
|
|
1702
|
+
if [[ "$written" -eq 0 && "$sections_failed" -gt 0 ]]; then
|
|
1703
|
+
json_err "$E_VALIDATION_FAILED" "charter-write: no entries written; all target sections missing from QUEEN.md" \
|
|
1704
|
+
"{\"written\":${written},\"sections_failed\":${sections_failed},\"colony_name\":\"${cw_colony_name}\",\"timestamp\":\"${ts}\"}"
|
|
1705
|
+
return 1
|
|
1706
|
+
fi
|
|
1707
|
+
json_ok "$(jq -n --argjson written "$written" --argjson updated "$is_update" \
|
|
1708
|
+
--arg colony_name "$cw_colony_name" --arg ts "$ts" --argjson sections_failed "$sections_failed" \
|
|
1709
|
+
'{written: $written, updated: $updated, colony_name: $colony_name, timestamp: $ts, sections_failed: $sections_failed}')"
|
|
1710
|
+
}
|