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,1928 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Learning and instinct utility functions -- extracted from aether-utils.sh
|
|
3
|
+
# Provides: _learning_promote, _learning_inject, _learning_observe, _learning_check_promotion,
|
|
4
|
+
# _learning_promote_auto, _learning_display_proposals, _learning_select_proposals,
|
|
5
|
+
# _learning_defer_proposals, _learning_approve_proposals, _learning_undo_promotions,
|
|
6
|
+
# _instinct_read, _instinct_create, _instinct_apply, _learning_extract_fallback
|
|
7
|
+
# Note: Uses get_wisdom_threshold() and get_wisdom_thresholds_json() from main file.
|
|
8
|
+
# Cross-domain calls (queen-promote, pheromone-write, activity-log, rolling-summary,
|
|
9
|
+
# generate-threshold-bar, parse-selection) are all via subprocess dispatch (bash "$0").
|
|
10
|
+
|
|
11
|
+
# ============================================================================
|
|
12
|
+
# _learning_promote
|
|
13
|
+
# Promote a learning to the global learnings file
|
|
14
|
+
# Usage: learning-promote <content> <source_project> <source_phase> [tags]
|
|
15
|
+
# ============================================================================
|
|
16
|
+
_learning_promote() {
|
|
17
|
+
[[ $# -ge 3 ]] || json_err "$E_VALIDATION_FAILED" "Usage: learning-promote <content> <source_project> <source_phase> [tags]"
|
|
18
|
+
content="$1"
|
|
19
|
+
source_project="$2"
|
|
20
|
+
source_phase="$3"
|
|
21
|
+
tags="${4:-}"
|
|
22
|
+
|
|
23
|
+
mkdir -p "$DATA_DIR"
|
|
24
|
+
global_file="$COLONY_DATA_DIR/learnings.json"
|
|
25
|
+
|
|
26
|
+
if [[ ! -f "$global_file" ]]; then
|
|
27
|
+
atomic_write "$global_file" '{"learnings":[],"version":1}' || json_err "$E_UNKNOWN" "Failed to initialize learnings file"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
id="global_$(date -u +%s)_$(head -c 2 /dev/urandom | od -An -tx1 | tr -d ' ')"
|
|
31
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
32
|
+
|
|
33
|
+
if [[ -n "$tags" ]]; then
|
|
34
|
+
tags_json=$(echo "$tags" | jq -R 'split(",")')
|
|
35
|
+
else
|
|
36
|
+
tags_json="[]"
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
current_count=$(jq '.learnings | length' "$global_file")
|
|
40
|
+
if [[ $current_count -ge 50 ]]; then
|
|
41
|
+
json_ok "{\"promoted\":false,\"reason\":\"cap_reached\",\"current_count\":$current_count,\"cap\":50}"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
updated=$(jq --arg id "$id" --arg content "$content" --arg sp "$source_project" \
|
|
46
|
+
--arg phase "$source_phase" --argjson tags "$tags_json" --arg ts "$ts" '
|
|
47
|
+
.learnings += [{
|
|
48
|
+
id: $id,
|
|
49
|
+
content: $content,
|
|
50
|
+
source_project: $sp,
|
|
51
|
+
source_phase: $phase,
|
|
52
|
+
tags: $tags,
|
|
53
|
+
promoted_at: $ts
|
|
54
|
+
}]
|
|
55
|
+
' "$global_file") || json_err "$E_JSON_INVALID" "Failed to update learnings.json"
|
|
56
|
+
|
|
57
|
+
atomic_write "$global_file" "$updated" || {
|
|
58
|
+
_aether_log_error "Could not save updated learnings"
|
|
59
|
+
json_err "$E_UNKNOWN" "Failed to write learnings file"
|
|
60
|
+
}
|
|
61
|
+
json_ok "$(jq -n --arg id "$id" --argjson count "$((current_count + 1))" '{promoted: true, id: $id, count: $count, cap: 50}')"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# ============================================================================
|
|
65
|
+
# _learning_inject
|
|
66
|
+
# Filter learnings by tech keywords for worker context injection
|
|
67
|
+
# Usage: learning-inject <tech_keywords_csv>
|
|
68
|
+
# ============================================================================
|
|
69
|
+
_learning_inject() {
|
|
70
|
+
[[ $# -ge 1 ]] || json_err "$E_VALIDATION_FAILED" "Usage: learning-inject <tech_keywords_csv>"
|
|
71
|
+
keywords="$1"
|
|
72
|
+
|
|
73
|
+
global_file="$COLONY_DATA_DIR/learnings.json"
|
|
74
|
+
|
|
75
|
+
if [[ ! -f "$global_file" ]]; then
|
|
76
|
+
json_ok '{"learnings":[],"count":0}'
|
|
77
|
+
exit 0
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
json_ok "$(jq --arg kw "$keywords" '
|
|
81
|
+
($kw | split(",") | map(ascii_downcase | ltrimstr(" ") | rtrimstr(" "))) as $keywords |
|
|
82
|
+
.learnings | map(
|
|
83
|
+
select(
|
|
84
|
+
.tags as $tags |
|
|
85
|
+
($keywords | any(. as $k | $tags | any(ascii_downcase | contains($k))))
|
|
86
|
+
)
|
|
87
|
+
) | {learnings: ., count: length}
|
|
88
|
+
' "$global_file")"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# ============================================================================
|
|
92
|
+
# _learning_observe
|
|
93
|
+
# Record observation of a learning across colonies
|
|
94
|
+
# Usage: learning-observe <content> <wisdom_type> [colony_name]
|
|
95
|
+
# Returns: JSON with observation_count, threshold status, and colonies list
|
|
96
|
+
# ============================================================================
|
|
97
|
+
_learning_observe() {
|
|
98
|
+
# Record observation of a learning across colonies
|
|
99
|
+
# Usage: learning-observe <content> <wisdom_type> [colony_name]
|
|
100
|
+
# Returns: JSON with observation_count, threshold status, and colonies list
|
|
101
|
+
content="${1:-}"
|
|
102
|
+
wisdom_type="${2:-}"
|
|
103
|
+
colony_name="${3:-unknown}"
|
|
104
|
+
|
|
105
|
+
# Validate required arguments
|
|
106
|
+
[[ -z "$content" ]] && json_err "$E_VALIDATION_FAILED" "Usage: learning-observe <content> <wisdom_type> [colony_name]" '{"missing":"content"}'
|
|
107
|
+
[[ -z "$wisdom_type" ]] && json_err "$E_VALIDATION_FAILED" "Usage: learning-observe <content> <wisdom_type> [colony_name]" '{"missing":"wisdom_type"}'
|
|
108
|
+
|
|
109
|
+
# Validate wisdom_type
|
|
110
|
+
valid_types=("philosophy" "pattern" "redirect" "stack" "decree" "failure")
|
|
111
|
+
type_valid=false
|
|
112
|
+
for vt in "${valid_types[@]}"; do
|
|
113
|
+
[[ "$wisdom_type" == "$vt" ]] && type_valid=true && break
|
|
114
|
+
done
|
|
115
|
+
[[ "$type_valid" == "false" ]] && json_err "$E_VALIDATION_FAILED" "Invalid wisdom_type: $wisdom_type" '{"valid_types":["philosophy","pattern","redirect","stack","decree","failure"]}'
|
|
116
|
+
|
|
117
|
+
# Generate SHA256 hash of content for deduplication
|
|
118
|
+
content_hash="sha256:$(echo -n "$content" | sha256sum | cut -d' ' -f1)"
|
|
119
|
+
|
|
120
|
+
# Observations file path
|
|
121
|
+
observations_file="$COLONY_DATA_DIR/learning-observations.json"
|
|
122
|
+
|
|
123
|
+
# Ensure data directory exists
|
|
124
|
+
[[ ! -d "$DATA_DIR" ]] && mkdir -p "$DATA_DIR"
|
|
125
|
+
|
|
126
|
+
# Initialize file if it doesn't exist
|
|
127
|
+
if [[ ! -f "$observations_file" ]]; then
|
|
128
|
+
atomic_write "$observations_file" '{"observations":[]}' || {
|
|
129
|
+
_aether_log_error "Could not initialize learning observations file"
|
|
130
|
+
json_err "$E_UNKNOWN" "Failed to create learning observations file"
|
|
131
|
+
}
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# Validate JSON structure — circuit breaker with backup recovery
|
|
135
|
+
if ! jq -e . "$observations_file" >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON validity
|
|
136
|
+
# Try to recover from backup (with retry-once per user decision)
|
|
137
|
+
lo_recovered=false
|
|
138
|
+
for lo_attempt in 1 2; do
|
|
139
|
+
for lo_bak in "${observations_file}.bak.1" "${observations_file}.bak.2" "${observations_file}.bak.3"; do
|
|
140
|
+
if [[ -f "$lo_bak" ]] && jq -e . "$lo_bak" >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON validity
|
|
141
|
+
if cp "$lo_bak" "$observations_file" 2>/dev/null; then # SUPPRESS:OK -- cleanup: backup copy is best-effort
|
|
142
|
+
lo_recovered=true
|
|
143
|
+
echo "Warning: Learning observations file was corrupted -- restored from backup. Some recent entries may be missing." >&2
|
|
144
|
+
break 2
|
|
145
|
+
fi
|
|
146
|
+
# cp failed -- will retry on next attempt (silent first retry)
|
|
147
|
+
fi
|
|
148
|
+
done
|
|
149
|
+
# If first attempt found a valid backup but cp failed, the second attempt retries
|
|
150
|
+
# If no valid backup exists, the second attempt won't help -- break early
|
|
151
|
+
[[ "$lo_attempt" -eq 1 ]] && [[ "$lo_recovered" != "true" ]] && break
|
|
152
|
+
done
|
|
153
|
+
|
|
154
|
+
if [[ "$lo_recovered" != "true" ]]; then
|
|
155
|
+
# Check if any backups exist at all
|
|
156
|
+
lo_has_any_backup=false
|
|
157
|
+
for lo_bak in "${observations_file}.bak.1" "${observations_file}.bak.2" "${observations_file}.bak.3"; do
|
|
158
|
+
[[ -f "$lo_bak" ]] && lo_has_any_backup=true && break
|
|
159
|
+
done
|
|
160
|
+
|
|
161
|
+
if [[ "$lo_has_any_backup" == "true" ]]; then
|
|
162
|
+
# Backups exist but ALL are corrupted -- stop and tell user (per locked decision)
|
|
163
|
+
json_err "$E_JSON_INVALID" "Learning observations and all 3 backups are corrupted. Manual recovery needed."
|
|
164
|
+
else
|
|
165
|
+
# No backups ever existed -- safe to reset from template (first-time corruption)
|
|
166
|
+
echo "Warning: Learning observations file was corrupted. Starting fresh -- this is a first-time recovery." >&2
|
|
167
|
+
atomic_write "$observations_file" '{"observations":[]}'
|
|
168
|
+
fi
|
|
169
|
+
fi
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# Acquire lock for concurrent access
|
|
173
|
+
if type acquire_lock &>/dev/null; then
|
|
174
|
+
acquire_lock "$observations_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on learning-observations.json"
|
|
175
|
+
trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
# Get current timestamp
|
|
179
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
180
|
+
|
|
181
|
+
# Check if observation with same hash already exists
|
|
182
|
+
existing_index=$(jq -r --arg hash "$content_hash" '.observations | to_entries[] | select(.value.content_hash == $hash) | .key' "$observations_file" | head -1)
|
|
183
|
+
|
|
184
|
+
if [[ -n "$existing_index" ]]; then
|
|
185
|
+
# Existing observation: increment count, update last_seen, add colony if new
|
|
186
|
+
# Rotate backups before write (uses .bak.N naming)
|
|
187
|
+
if [[ -f "$observations_file" ]]; then
|
|
188
|
+
cp -f "${observations_file}.bak.2" "${observations_file}.bak.3" 2>/dev/null || _aether_log_error "Could not rotate observations backup .bak.2 to .bak.3"
|
|
189
|
+
cp -f "${observations_file}.bak.1" "${observations_file}.bak.2" 2>/dev/null || _aether_log_error "Could not rotate observations backup .bak.1 to .bak.2"
|
|
190
|
+
cp -f "$observations_file" "${observations_file}.bak.1" 2>/dev/null || _aether_log_error "Could not create observations backup .bak.1"
|
|
191
|
+
fi
|
|
192
|
+
tmp_file="${observations_file}.tmp.$$"
|
|
193
|
+
|
|
194
|
+
jq --arg hash "$content_hash" \
|
|
195
|
+
--arg colony "$colony_name" \
|
|
196
|
+
--arg ts "$ts" \
|
|
197
|
+
'
|
|
198
|
+
.observations |= map(
|
|
199
|
+
if .content_hash == $hash then
|
|
200
|
+
.observation_count += 1 |
|
|
201
|
+
.last_seen = $ts |
|
|
202
|
+
.colonies = ((.colonies + [$colony]) | unique)
|
|
203
|
+
else
|
|
204
|
+
.
|
|
205
|
+
end
|
|
206
|
+
)' "$observations_file" > "$tmp_file" || {
|
|
207
|
+
_aether_log_error "Could not process observation update"
|
|
208
|
+
rm -f "$tmp_file"
|
|
209
|
+
json_err "$E_JSON_INVALID" "Failed to update observation data"
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
[[ -s "$tmp_file" ]] || {
|
|
213
|
+
_aether_log_error "Observation update produced empty result -- not overwriting"
|
|
214
|
+
rm -f "$tmp_file"
|
|
215
|
+
json_err "$E_JSON_INVALID" "Observation update produced empty result"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
mv "$tmp_file" "$observations_file" || {
|
|
219
|
+
_aether_log_error "Could not finalize observation file update"
|
|
220
|
+
rm -f "$tmp_file"
|
|
221
|
+
json_err "$E_UNKNOWN" "Failed to rename temporary observations file"
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# Get updated observation data
|
|
225
|
+
observation_count=$(jq -r --arg hash "$content_hash" '.observations[] | select(.content_hash == $hash) | .observation_count' "$observations_file")
|
|
226
|
+
colonies=$(jq -r --arg hash "$content_hash" '.observations[] | select(.content_hash == $hash) | .colonies' "$observations_file")
|
|
227
|
+
is_new=false
|
|
228
|
+
else
|
|
229
|
+
# New observation: create entry
|
|
230
|
+
# Rotate backups before write (uses .bak.N naming)
|
|
231
|
+
if [[ -f "$observations_file" ]]; then
|
|
232
|
+
cp -f "${observations_file}.bak.2" "${observations_file}.bak.3" 2>/dev/null || _aether_log_error "Could not rotate observations backup .bak.2 to .bak.3"
|
|
233
|
+
cp -f "${observations_file}.bak.1" "${observations_file}.bak.2" 2>/dev/null || _aether_log_error "Could not rotate observations backup .bak.1 to .bak.2"
|
|
234
|
+
cp -f "$observations_file" "${observations_file}.bak.1" 2>/dev/null || _aether_log_error "Could not create observations backup .bak.1"
|
|
235
|
+
fi
|
|
236
|
+
tmp_file="${observations_file}.tmp.$$"
|
|
237
|
+
|
|
238
|
+
jq --arg hash "$content_hash" \
|
|
239
|
+
--arg content "$content" \
|
|
240
|
+
--arg type "$wisdom_type" \
|
|
241
|
+
--arg colony "$colony_name" \
|
|
242
|
+
--arg ts "$ts" \
|
|
243
|
+
'.observations += [{
|
|
244
|
+
"content_hash": $hash,
|
|
245
|
+
"content": $content,
|
|
246
|
+
"wisdom_type": $type,
|
|
247
|
+
"observation_count": 1,
|
|
248
|
+
"first_seen": $ts,
|
|
249
|
+
"last_seen": $ts,
|
|
250
|
+
"colonies": [$colony]
|
|
251
|
+
}]' "$observations_file" > "$tmp_file" || {
|
|
252
|
+
_aether_log_error "Could not create new observation entry"
|
|
253
|
+
rm -f "$tmp_file"
|
|
254
|
+
json_err "$E_JSON_INVALID" "Failed to create observation data"
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
[[ -s "$tmp_file" ]] || {
|
|
258
|
+
_aether_log_error "New observation entry produced empty result -- not overwriting"
|
|
259
|
+
rm -f "$tmp_file"
|
|
260
|
+
json_err "$E_JSON_INVALID" "New observation produced empty result"
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
mv "$tmp_file" "$observations_file" || {
|
|
264
|
+
_aether_log_error "Could not finalize new observation file update"
|
|
265
|
+
rm -f "$tmp_file"
|
|
266
|
+
json_err "$E_UNKNOWN" "Failed to rename temporary observations file"
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
observation_count=1
|
|
270
|
+
colonies="[\"$colony_name\"]"
|
|
271
|
+
is_new=true
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# Release lock
|
|
275
|
+
if type release_lock &>/dev/null; then
|
|
276
|
+
release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
|
|
277
|
+
fi
|
|
278
|
+
trap - EXIT
|
|
279
|
+
|
|
280
|
+
# Propose-threshold determines when a learning is queueable/reviewable.
|
|
281
|
+
threshold=$(get_wisdom_threshold "$wisdom_type" "propose")
|
|
282
|
+
|
|
283
|
+
# Determine if threshold is met
|
|
284
|
+
threshold_met=false
|
|
285
|
+
[[ "$observation_count" -ge "$threshold" ]] && threshold_met=true
|
|
286
|
+
|
|
287
|
+
# Return result
|
|
288
|
+
result=$(jq -n \
|
|
289
|
+
--arg hash "$content_hash" \
|
|
290
|
+
--arg content "$content" \
|
|
291
|
+
--arg type "$wisdom_type" \
|
|
292
|
+
--argjson count "$observation_count" \
|
|
293
|
+
--argjson threshold "$threshold" \
|
|
294
|
+
--argjson threshold_met "$threshold_met" \
|
|
295
|
+
--argjson colonies "$colonies" \
|
|
296
|
+
--argjson is_new "$is_new" \
|
|
297
|
+
'{
|
|
298
|
+
content_hash: $hash,
|
|
299
|
+
content: $content,
|
|
300
|
+
wisdom_type: $type,
|
|
301
|
+
observation_count: $count,
|
|
302
|
+
threshold: $threshold,
|
|
303
|
+
threshold_met: $threshold_met,
|
|
304
|
+
colonies: $colonies,
|
|
305
|
+
is_new: $is_new
|
|
306
|
+
}')
|
|
307
|
+
|
|
308
|
+
json_ok "$result"
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# ============================================================================
|
|
312
|
+
# _learning_check_promotion
|
|
313
|
+
# Check which learnings meet promotion thresholds
|
|
314
|
+
# Usage: learning-check-promotion [path_to_observations_file]
|
|
315
|
+
# Returns: JSON array of proposals meeting thresholds
|
|
316
|
+
# ============================================================================
|
|
317
|
+
_learning_check_promotion() {
|
|
318
|
+
observations_file="${1:-$COLONY_DATA_DIR/learning-observations.json}"
|
|
319
|
+
|
|
320
|
+
# Default to empty file path if not provided and data dir doesn't exist
|
|
321
|
+
if [[ -z "${1:-}" ]] && [[ ! -d "$DATA_DIR" ]]; then
|
|
322
|
+
observations_file=""
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
# If file doesn't exist or is empty, return empty proposals
|
|
326
|
+
if [[ -z "$observations_file" ]] || [[ ! -f "$observations_file" ]]; then
|
|
327
|
+
json_ok '{"proposals":[]}'
|
|
328
|
+
exit 0
|
|
329
|
+
fi
|
|
330
|
+
|
|
331
|
+
# Validate JSON structure
|
|
332
|
+
if ! jq -e . "$observations_file" >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON validity
|
|
333
|
+
json_err "$E_JSON_INVALID" "learning-observations.json has invalid JSON"
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
# Build proposals array using the shared threshold table.
|
|
337
|
+
thresholds_json=$(get_wisdom_thresholds_json)
|
|
338
|
+
result=$(jq --argjson thresholds "$thresholds_json" '
|
|
339
|
+
def get_threshold(type):
|
|
340
|
+
($thresholds[type].propose // 1);
|
|
341
|
+
|
|
342
|
+
{
|
|
343
|
+
proposals: [
|
|
344
|
+
.observations[] |
|
|
345
|
+
select((.observation_count // 0) >= get_threshold(.wisdom_type)) |
|
|
346
|
+
{
|
|
347
|
+
content: .content,
|
|
348
|
+
wisdom_type: .wisdom_type,
|
|
349
|
+
observation_count: .observation_count,
|
|
350
|
+
threshold: get_threshold(.wisdom_type),
|
|
351
|
+
colonies: (.colonies // []),
|
|
352
|
+
ready: true
|
|
353
|
+
}
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
' "$observations_file" 2>/dev/null || echo '{"proposals":[]}') # SUPPRESS:OK -- read-default: file may not exist yet
|
|
357
|
+
|
|
358
|
+
json_ok "$result"
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
# ============================================================================
|
|
362
|
+
# _learning_promote_auto
|
|
363
|
+
# Auto-promote high-confidence learnings using recurrence policy
|
|
364
|
+
# Usage: learning-promote-auto <wisdom_type> <content> [colony_name] [event_type]
|
|
365
|
+
# ============================================================================
|
|
366
|
+
_learning_promote_auto() {
|
|
367
|
+
wisdom_type="${1:-}"
|
|
368
|
+
content="${2:-}"
|
|
369
|
+
colony_name="${3:-}"
|
|
370
|
+
event_type="${4:-learning}"
|
|
371
|
+
|
|
372
|
+
[[ -z "$wisdom_type" ]] && json_err "$E_VALIDATION_FAILED" "Usage: learning-promote-auto <wisdom_type> <content> [colony_name] [event_type]" '{"missing":"wisdom_type"}'
|
|
373
|
+
[[ -z "$content" ]] && json_err "$E_VALIDATION_FAILED" "Usage: learning-promote-auto <wisdom_type> <content> [colony_name] [event_type]" '{"missing":"content"}'
|
|
374
|
+
|
|
375
|
+
if [[ -z "$colony_name" ]]; then
|
|
376
|
+
colony_name=$(bash "$0" colony-name 2>/dev/null | jq -r '.result.name // ""') || colony_name="unknown"
|
|
377
|
+
[[ -z "$colony_name" ]] && colony_name="unknown"
|
|
378
|
+
fi
|
|
379
|
+
|
|
380
|
+
policy_threshold=$(get_wisdom_threshold "$wisdom_type" "auto")
|
|
381
|
+
|
|
382
|
+
observations_file="$COLONY_DATA_DIR/learning-observations.json"
|
|
383
|
+
content_hash="sha256:$(echo -n "$content" | sha256sum | cut -d' ' -f1)"
|
|
384
|
+
observation_count=0
|
|
385
|
+
colony_count=0
|
|
386
|
+
|
|
387
|
+
if [[ -f "$observations_file" ]]; then
|
|
388
|
+
# SUPPRESS:OK -- read-default: query may return empty
|
|
389
|
+
observation_count=$(jq -r --arg hash "$content_hash" '.observations[]? | select(.content_hash == $hash) | .observation_count // 0' "$observations_file" 2>/dev/null | head -1)
|
|
390
|
+
# SUPPRESS:OK -- read-default: query may return empty
|
|
391
|
+
colony_count=$(jq -r --arg hash "$content_hash" '.observations[]? | select(.content_hash == $hash) | (.colonies // [] | length)' "$observations_file" 2>/dev/null | head -1)
|
|
392
|
+
[[ -z "$observation_count" ]] && observation_count=0
|
|
393
|
+
[[ -z "$colony_count" ]] && colony_count=0
|
|
394
|
+
fi
|
|
395
|
+
|
|
396
|
+
# LRN-01: Recurrence-calibrated confidence
|
|
397
|
+
# Formula: min(0.7 + (observation_count - 1) * 0.05, 0.9)
|
|
398
|
+
lp_confidence=$(awk -v c="${observation_count:-1}" 'BEGIN {
|
|
399
|
+
v = 0.7 + (c - 1) * 0.05
|
|
400
|
+
if (v > 0.9) v = 0.9
|
|
401
|
+
if (v < 0.7) v = 0.7
|
|
402
|
+
printf "%.2f", v
|
|
403
|
+
}')
|
|
404
|
+
|
|
405
|
+
if [[ "$policy_threshold" -gt 0 && "$observation_count" -lt "$policy_threshold" ]]; then
|
|
406
|
+
json_ok "$(jq -n --argjson pt "$policy_threshold" --argjson oc "$observation_count" --argjson cc "$colony_count" --arg et "$event_type" '{promoted: false, reason: "threshold_not_met", policy_threshold: $pt, observation_count: $oc, colony_count: $cc, event_type: $et}')"
|
|
407
|
+
exit 0
|
|
408
|
+
fi
|
|
409
|
+
|
|
410
|
+
queen_file="$AETHER_ROOT/.aether/QUEEN.md"
|
|
411
|
+
if [[ ! -f "$queen_file" ]]; then
|
|
412
|
+
json_ok "{\"promoted\":false,\"reason\":\"queen_missing\",\"policy_threshold\":$policy_threshold,\"observation_count\":$observation_count,\"colony_count\":$colony_count}"
|
|
413
|
+
exit 0
|
|
414
|
+
fi
|
|
415
|
+
|
|
416
|
+
if grep -Fq -- "$content" "$queen_file" 2>/dev/null; then # SUPPRESS:OK -- existence-test: file may not exist
|
|
417
|
+
json_ok "{\"promoted\":false,\"reason\":\"already_promoted\",\"policy_threshold\":$policy_threshold,\"observation_count\":$observation_count,\"colony_count\":$colony_count}"
|
|
418
|
+
exit 0
|
|
419
|
+
fi
|
|
420
|
+
|
|
421
|
+
# SUPPRESS:OK -- read-default: returns fallback on failure
|
|
422
|
+
promote_result=$(bash "$0" queen-promote "$wisdom_type" "$content" "$colony_name" 2>/dev/null || echo '{}')
|
|
423
|
+
if echo "$promote_result" | jq -e '.ok == true' >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON field
|
|
424
|
+
# Also create an instinct from the promoted learning
|
|
425
|
+
bash "$0" instinct-create \
|
|
426
|
+
--trigger "working on $wisdom_type patterns" \
|
|
427
|
+
--action "$content" \
|
|
428
|
+
--confidence "$lp_confidence" \
|
|
429
|
+
--domain "$wisdom_type" \
|
|
430
|
+
--source "promoted_from_learning" \
|
|
431
|
+
# SUPPRESS:OK -- read-default: returns fallback on failure
|
|
432
|
+
--evidence "Auto-promoted after $observation_count observations (confidence: $lp_confidence)" 2>/dev/null \
|
|
433
|
+
|| _aether_log_error "Could not create instinct from promoted learning"
|
|
434
|
+
json_ok "$(jq -nc --argjson pt "$policy_threshold" --argjson oc "$observation_count" --argjson cc "$colony_count" --arg et "$event_type" '{promoted: true, mode: "auto", policy_threshold: $pt, observation_count: $oc, colony_count: $cc, event_type: $et}')"
|
|
435
|
+
else
|
|
436
|
+
# SUPPRESS:OK -- read-default: query may return empty
|
|
437
|
+
promote_msg=$(echo "$promote_result" | jq -r '.error.message // "promotion_failed"' 2>/dev/null || echo "promotion_failed")
|
|
438
|
+
result=$(jq -nc \
|
|
439
|
+
--arg reason "promotion_failed" \
|
|
440
|
+
--arg message "$promote_msg" \
|
|
441
|
+
--argjson policy_threshold "$policy_threshold" \
|
|
442
|
+
--argjson observation_count "$observation_count" \
|
|
443
|
+
--argjson colony_count "$colony_count" \
|
|
444
|
+
'{promoted:false, reason:$reason, message:$message, policy_threshold:$policy_threshold, observation_count:$observation_count, colony_count:$colony_count}')
|
|
445
|
+
json_ok "$result"
|
|
446
|
+
fi
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
# ============================================================================
|
|
450
|
+
# _learning_display_proposals
|
|
451
|
+
# Display promotion proposals with checkbox-style UI
|
|
452
|
+
# Usage: learning-display-proposals [observations_file] [--verbose] [--no-color]
|
|
453
|
+
# Returns: Formatted display output (not JSON - for human consumption)
|
|
454
|
+
# ============================================================================
|
|
455
|
+
_learning_display_proposals() {
|
|
456
|
+
verbose=false
|
|
457
|
+
no_color=false
|
|
458
|
+
observations_file=""
|
|
459
|
+
|
|
460
|
+
# Parse arguments
|
|
461
|
+
for arg in "$@"; do
|
|
462
|
+
case "$arg" in
|
|
463
|
+
--verbose) verbose=true ;;
|
|
464
|
+
--no-color) no_color=true ;;
|
|
465
|
+
*)
|
|
466
|
+
# If argument doesn't start with --, treat as file path
|
|
467
|
+
if [[ "$arg" != --* ]] && [[ -z "$observations_file" ]]; then
|
|
468
|
+
observations_file="$arg"
|
|
469
|
+
fi
|
|
470
|
+
;;
|
|
471
|
+
esac
|
|
472
|
+
done
|
|
473
|
+
|
|
474
|
+
# Detect color support
|
|
475
|
+
use_color=false
|
|
476
|
+
if [[ "$no_color" == "false" ]] && [[ -t 1 ]]; then
|
|
477
|
+
use_color=true
|
|
478
|
+
fi
|
|
479
|
+
|
|
480
|
+
# Color codes
|
|
481
|
+
reset=""
|
|
482
|
+
yellow=""
|
|
483
|
+
red=""
|
|
484
|
+
cyan=""
|
|
485
|
+
if [[ "$use_color" == "true" ]]; then
|
|
486
|
+
reset="\033[0m"
|
|
487
|
+
yellow="\033[33m"
|
|
488
|
+
red="\033[31m"
|
|
489
|
+
cyan="\033[36m"
|
|
490
|
+
fi
|
|
491
|
+
|
|
492
|
+
# Determine observations file path
|
|
493
|
+
if [[ -z "$observations_file" ]]; then
|
|
494
|
+
observations_file="$COLONY_DATA_DIR/learning-observations.json"
|
|
495
|
+
fi
|
|
496
|
+
|
|
497
|
+
# Check if file exists and has content
|
|
498
|
+
if [[ ! -f "$observations_file" ]] || [[ ! -s "$observations_file" ]]; then
|
|
499
|
+
echo "No observations found."
|
|
500
|
+
echo ""
|
|
501
|
+
echo "Observations accumulate as colonies report learnings."
|
|
502
|
+
echo "Run this command again after more activity."
|
|
503
|
+
exit 0
|
|
504
|
+
fi
|
|
505
|
+
|
|
506
|
+
# Get all observations with their thresholds
|
|
507
|
+
thresholds_json=$(get_wisdom_thresholds_json)
|
|
508
|
+
proposals_json=$(jq --argjson thresholds "$thresholds_json" '
|
|
509
|
+
def get_threshold(type):
|
|
510
|
+
($thresholds[type].propose // 1);
|
|
511
|
+
|
|
512
|
+
{
|
|
513
|
+
proposals: [
|
|
514
|
+
.observations[] |
|
|
515
|
+
{
|
|
516
|
+
content: .content,
|
|
517
|
+
wisdom_type: .wisdom_type,
|
|
518
|
+
observation_count: .observation_count,
|
|
519
|
+
threshold: get_threshold(.wisdom_type),
|
|
520
|
+
colonies: .colonies
|
|
521
|
+
}
|
|
522
|
+
]
|
|
523
|
+
}
|
|
524
|
+
' "$observations_file" 2>/dev/null || echo '{"proposals":[]}') # SUPPRESS:OK -- read-default: file may not exist yet
|
|
525
|
+
|
|
526
|
+
# Check if there are any proposals
|
|
527
|
+
proposal_count=$(echo "$proposals_json" | jq '.proposals | length')
|
|
528
|
+
if [[ "$proposal_count" -eq 0 ]]; then
|
|
529
|
+
echo "No proposals ready for promotion."
|
|
530
|
+
echo ""
|
|
531
|
+
echo "Observations accumulate as colonies report learnings."
|
|
532
|
+
echo "Run this command again after more activity."
|
|
533
|
+
exit 0
|
|
534
|
+
fi
|
|
535
|
+
|
|
536
|
+
# Define wisdom types and their display properties
|
|
537
|
+
types=("philosophy" "pattern" "redirect" "stack" "decree" "failure")
|
|
538
|
+
type_emojis=("📜" "🧭" "⚠️" "🔧" "🏛️" "❌")
|
|
539
|
+
type_names=("Philosophies" "Patterns" "Redirects" "Stack Wisdom" "Decrees" "Failures")
|
|
540
|
+
type_thresholds=(
|
|
541
|
+
"$(get_wisdom_threshold philosophy propose)"
|
|
542
|
+
"$(get_wisdom_threshold pattern propose)"
|
|
543
|
+
"$(get_wisdom_threshold redirect propose)"
|
|
544
|
+
"$(get_wisdom_threshold stack propose)"
|
|
545
|
+
"$(get_wisdom_threshold decree propose)"
|
|
546
|
+
"$(get_wisdom_threshold failure propose)"
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
echo ""
|
|
550
|
+
echo "🧠 Promotion Proposals"
|
|
551
|
+
echo "====================="
|
|
552
|
+
echo ""
|
|
553
|
+
echo "Select proposals to promote to QUEEN.md wisdom:"
|
|
554
|
+
echo "(Enter numbers like '1 3 5', or press Enter to defer all)"
|
|
555
|
+
echo ""
|
|
556
|
+
|
|
557
|
+
# Build flat list of all proposals with global numbering
|
|
558
|
+
global_idx=1
|
|
559
|
+
declare -a all_proposals
|
|
560
|
+
|
|
561
|
+
for i in "${!types[@]}"; do
|
|
562
|
+
type="${types[$i]}"
|
|
563
|
+
threshold="${type_thresholds[$i]}"
|
|
564
|
+
|
|
565
|
+
# Get proposals of this type
|
|
566
|
+
type_proposals=$(echo "$proposals_json" | jq --arg t "$type" '.proposals | map(select(.wisdom_type == $t))')
|
|
567
|
+
type_count=$(echo "$type_proposals" | jq 'length')
|
|
568
|
+
|
|
569
|
+
[[ "$type_count" -eq 0 ]] && continue
|
|
570
|
+
|
|
571
|
+
# Print group header
|
|
572
|
+
echo "${type_emojis[$i]} ${type_names[$i]} (threshold: $threshold)"
|
|
573
|
+
|
|
574
|
+
# Process each proposal using index to avoid subshell issues
|
|
575
|
+
for ((j=0; j<type_count; j++)); do
|
|
576
|
+
proposal=$(echo "$type_proposals" | jq -c ".[$j]")
|
|
577
|
+
content=$(sanitize_read_value "$(echo "$proposal" | jq -r '.content')")
|
|
578
|
+
count=$(echo "$proposal" | jq -r '.observation_count')
|
|
579
|
+
prop_threshold=$(echo "$proposal" | jq -r '.threshold')
|
|
580
|
+
|
|
581
|
+
# Truncate content if not verbose
|
|
582
|
+
display_content="$content"
|
|
583
|
+
if [[ "$verbose" != "true" && ${#content} -gt 40 ]]; then
|
|
584
|
+
display_content="${content:0:37}..."
|
|
585
|
+
fi
|
|
586
|
+
|
|
587
|
+
# Get threshold bar
|
|
588
|
+
# SUPPRESS:OK -- read-default: returns fallback on failure
|
|
589
|
+
bar_result=$(bash "$0" generate-threshold-bar "$count" "$prop_threshold" 2>/dev/null | jq -r '.result.bar')
|
|
590
|
+
|
|
591
|
+
# Build warning for below-threshold
|
|
592
|
+
warning=""
|
|
593
|
+
if [[ "$count" -lt "$prop_threshold" ]]; then
|
|
594
|
+
if [[ "$use_color" == "true" ]]; then
|
|
595
|
+
warning=" ${yellow}⚠️ below threshold${reset}"
|
|
596
|
+
else
|
|
597
|
+
warning=" ⚠️ below threshold"
|
|
598
|
+
fi
|
|
599
|
+
fi
|
|
600
|
+
|
|
601
|
+
# Print formatted line
|
|
602
|
+
printf " [ ] %d. \"%s\" %s (%d/%d)%s\n" "$global_idx" "$display_content" "$bar_result" "$count" "$prop_threshold" "$warning"
|
|
603
|
+
|
|
604
|
+
# Store for later reference
|
|
605
|
+
all_proposals+=("$proposal")
|
|
606
|
+
|
|
607
|
+
global_idx=$((global_idx + 1))
|
|
608
|
+
done
|
|
609
|
+
|
|
610
|
+
echo ""
|
|
611
|
+
done
|
|
612
|
+
|
|
613
|
+
echo "───────────────────────────────────────────────────"
|
|
614
|
+
echo ""
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
# ============================================================================
|
|
618
|
+
# _learning_select_proposals
|
|
619
|
+
# Interactive selection of proposals for promotion [DEPRECATED]
|
|
620
|
+
# Usage: learning-select-proposals [--verbose] [--dry-run] [--yes]
|
|
621
|
+
# Returns: JSON with selected/deferred arrays and action taken
|
|
622
|
+
# ============================================================================
|
|
623
|
+
_learning_select_proposals() {
|
|
624
|
+
_deprecation_warning "learning-select-proposals"
|
|
625
|
+
# Interactive selection of proposals for promotion
|
|
626
|
+
# Usage: learning-select-proposals [--verbose] [--dry-run] [--yes]
|
|
627
|
+
# Returns: JSON with selected/deferred arrays and action taken
|
|
628
|
+
#
|
|
629
|
+
# Flow: display proposals -> capture input -> parse selection -> output JSON
|
|
630
|
+
|
|
631
|
+
verbose=false
|
|
632
|
+
dry_run=false
|
|
633
|
+
skip_confirm=false
|
|
634
|
+
|
|
635
|
+
# Parse arguments
|
|
636
|
+
for arg in "$@"; do
|
|
637
|
+
case "$arg" in
|
|
638
|
+
--verbose) verbose=true ;;
|
|
639
|
+
--dry-run) dry_run=true ;;
|
|
640
|
+
--yes) skip_confirm=true ;;
|
|
641
|
+
esac
|
|
642
|
+
done
|
|
643
|
+
|
|
644
|
+
# Get all observations (not just threshold-meeting ones) for display consistency
|
|
645
|
+
# This matches learning-display-proposals behavior
|
|
646
|
+
observations_file="$COLONY_DATA_DIR/learning-observations.json"
|
|
647
|
+
if [[ ! -f "$observations_file" ]]; then
|
|
648
|
+
json_ok '{"selected":[],"deferred":[],"count":0,"action":"none","reason":"no_observations_file"}'
|
|
649
|
+
exit 0
|
|
650
|
+
fi
|
|
651
|
+
|
|
652
|
+
# Build proposals array using same logic as learning-display-proposals
|
|
653
|
+
thresholds_json=$(get_wisdom_thresholds_json)
|
|
654
|
+
proposals_json=$(jq --argjson thresholds "$thresholds_json" '
|
|
655
|
+
def get_threshold(type):
|
|
656
|
+
($thresholds[type].propose // 1);
|
|
657
|
+
|
|
658
|
+
{
|
|
659
|
+
proposals: [
|
|
660
|
+
.observations[] |
|
|
661
|
+
{
|
|
662
|
+
content: .content,
|
|
663
|
+
wisdom_type: .wisdom_type,
|
|
664
|
+
observation_count: .observation_count,
|
|
665
|
+
threshold: get_threshold(.wisdom_type),
|
|
666
|
+
colonies: .colonies
|
|
667
|
+
}
|
|
668
|
+
]
|
|
669
|
+
}
|
|
670
|
+
' "$observations_file" 2>/dev/null || echo '{"proposals":[]}') # SUPPRESS:OK -- read-default: file may not exist yet
|
|
671
|
+
|
|
672
|
+
# Check if we have any proposals
|
|
673
|
+
proposal_count=$(echo "$proposals_json" | jq '.proposals | length')
|
|
674
|
+
if [[ "$proposal_count" -eq 0 ]]; then
|
|
675
|
+
json_ok '{"selected":[],"deferred":[],"count":0,"action":"none","reason":"no_proposals"}'
|
|
676
|
+
exit 0
|
|
677
|
+
fi
|
|
678
|
+
|
|
679
|
+
# Display proposals
|
|
680
|
+
if [[ "$dry_run" == "false" ]]; then
|
|
681
|
+
if [[ "$verbose" == "true" ]]; then
|
|
682
|
+
bash "$0" learning-display-proposals --verbose
|
|
683
|
+
else
|
|
684
|
+
bash "$0" learning-display-proposals
|
|
685
|
+
fi
|
|
686
|
+
fi
|
|
687
|
+
|
|
688
|
+
# Capture user input (unless dry-run)
|
|
689
|
+
selection=""
|
|
690
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
691
|
+
# In dry-run mode, select all proposals
|
|
692
|
+
selection=$(seq 1 $proposal_count | tr '\n' ' ')
|
|
693
|
+
echo "Dry run: would select all $proposal_count proposals"
|
|
694
|
+
else
|
|
695
|
+
echo -n "Enter numbers to select (e.g., '1 3 5'), or press Enter to defer all: "
|
|
696
|
+
read -r selection
|
|
697
|
+
fi
|
|
698
|
+
|
|
699
|
+
# Parse the selection
|
|
700
|
+
parse_result=$(bash "$0" parse-selection "$selection" "$proposal_count")
|
|
701
|
+
|
|
702
|
+
# Check for parse errors
|
|
703
|
+
if ! echo "$parse_result" | jq -e '.ok' >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON field
|
|
704
|
+
# Return the error
|
|
705
|
+
echo "$parse_result"
|
|
706
|
+
exit 1
|
|
707
|
+
fi
|
|
708
|
+
|
|
709
|
+
# Extract selected and deferred arrays
|
|
710
|
+
selected_indices=$(echo "$parse_result" | jq -r '.result.selected // []')
|
|
711
|
+
deferred_indices=$(echo "$parse_result" | jq -r '.result.deferred // []')
|
|
712
|
+
action=$(echo "$parse_result" | jq -r '.result.action // "select"')
|
|
713
|
+
selected_count=$(echo "$selected_indices" | jq 'length')
|
|
714
|
+
deferred_count=$(echo "$deferred_indices" | jq 'length')
|
|
715
|
+
|
|
716
|
+
# Show summary
|
|
717
|
+
if [[ "$dry_run" == "false" ]]; then
|
|
718
|
+
echo ""
|
|
719
|
+
echo "$selected_count proposal(s) selected, $deferred_count deferred"
|
|
720
|
+
fi
|
|
721
|
+
|
|
722
|
+
# Preview and confirmation (if selections made and not skipping)
|
|
723
|
+
confirmed=true
|
|
724
|
+
if [[ "$selected_count" -gt 0 && "$dry_run" == "false" && "$skip_confirm" == "false" ]]; then
|
|
725
|
+
echo ""
|
|
726
|
+
echo "───────────────────────────────────────────────────"
|
|
727
|
+
echo "📋 Selected for Promotion:"
|
|
728
|
+
echo ""
|
|
729
|
+
|
|
730
|
+
# Track below-threshold count for warning
|
|
731
|
+
below_threshold_count=0
|
|
732
|
+
|
|
733
|
+
# Display each selected proposal with full details
|
|
734
|
+
echo "$selected_indices" | jq -r '.[]' | while read -r idx; do
|
|
735
|
+
proposal=$(echo "$proposals_json" | jq -r ".proposals[$idx]")
|
|
736
|
+
content=$(sanitize_read_value "$(echo "$proposal" | jq -r '.content')")
|
|
737
|
+
ptype=$(echo "$proposal" | jq -r '.wisdom_type')
|
|
738
|
+
count=$(echo "$proposal" | jq -r '.observation_count')
|
|
739
|
+
threshold=$(echo "$proposal" | jq -r '.threshold')
|
|
740
|
+
|
|
741
|
+
# Capitalize type for display
|
|
742
|
+
type_display=$(echo "$ptype" | awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}')
|
|
743
|
+
|
|
744
|
+
# Check if below threshold
|
|
745
|
+
status=""
|
|
746
|
+
if [[ "$count" -lt "$threshold" ]]; then
|
|
747
|
+
status=" [⚠️ Early promotion - below threshold]"
|
|
748
|
+
below_threshold_count=$((below_threshold_count + 1))
|
|
749
|
+
fi
|
|
750
|
+
|
|
751
|
+
echo " • $type_display: \"$content\"$status"
|
|
752
|
+
done
|
|
753
|
+
|
|
754
|
+
# Show warning if any below threshold
|
|
755
|
+
if [[ "$below_threshold_count" -gt 0 ]]; then
|
|
756
|
+
echo ""
|
|
757
|
+
echo "⚠️ $below_threshold_count item(s) below threshold will be early promoted"
|
|
758
|
+
fi
|
|
759
|
+
|
|
760
|
+
# Confirmation prompt
|
|
761
|
+
echo ""
|
|
762
|
+
echo -n "Proceed with promotion? (y/n): "
|
|
763
|
+
read -r confirm_response
|
|
764
|
+
|
|
765
|
+
if [[ ! "$confirm_response" =~ ^[Yy]$ ]]; then
|
|
766
|
+
confirmed=false
|
|
767
|
+
echo "Selection cancelled. Treating as defer-all."
|
|
768
|
+
# Move all to deferred
|
|
769
|
+
action="defer_all"
|
|
770
|
+
deferred_indices=$(jq -n --argjson s "$selected_indices" --argjson d "$deferred_indices" '($s + $d)')
|
|
771
|
+
selected_indices="[]"
|
|
772
|
+
selected_count=0
|
|
773
|
+
deferred_count=$(echo "$deferred_indices" | jq 'length')
|
|
774
|
+
fi
|
|
775
|
+
fi
|
|
776
|
+
|
|
777
|
+
# Build result JSON
|
|
778
|
+
result=$(jq -n \
|
|
779
|
+
--argjson selected "$selected_indices" \
|
|
780
|
+
--argjson deferred "$deferred_indices" \
|
|
781
|
+
--argjson proposals "$proposals_json" \
|
|
782
|
+
--arg action "$action" \
|
|
783
|
+
--argjson count "$proposal_count" \
|
|
784
|
+
--arg confirmed "$confirmed" \
|
|
785
|
+
'{
|
|
786
|
+
selected: $selected,
|
|
787
|
+
deferred: $deferred,
|
|
788
|
+
count: $count,
|
|
789
|
+
action: $action,
|
|
790
|
+
confirmed: ($confirmed == "true"),
|
|
791
|
+
proposals: $proposals.proposals
|
|
792
|
+
}')
|
|
793
|
+
|
|
794
|
+
json_ok "$result"
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
# ============================================================================
|
|
798
|
+
# _learning_defer_proposals
|
|
799
|
+
# Store unselected proposals in learning-deferred.json for later review
|
|
800
|
+
# Usage: echo '[{proposal1}, {proposal2}]' | bash aether-utils.sh learning-defer-proposals
|
|
801
|
+
# Returns: JSON with count of newly deferred items
|
|
802
|
+
# ============================================================================
|
|
803
|
+
_learning_defer_proposals() {
|
|
804
|
+
# Read proposals from stdin
|
|
805
|
+
proposals_json=$(cat)
|
|
806
|
+
|
|
807
|
+
# Validate input
|
|
808
|
+
if [[ -z "$proposals_json" ]] || [[ "$proposals_json" == "[]" ]]; then
|
|
809
|
+
json_ok '{"deferred":0,"new":0,"expired":0}'
|
|
810
|
+
exit 0
|
|
811
|
+
fi
|
|
812
|
+
|
|
813
|
+
deferred_file="$COLONY_DATA_DIR/learning-deferred.json"
|
|
814
|
+
|
|
815
|
+
# Ensure data directory exists
|
|
816
|
+
[[ ! -d "$DATA_DIR" ]] && mkdir -p "$DATA_DIR"
|
|
817
|
+
|
|
818
|
+
# Acquire lock
|
|
819
|
+
acquire_lock "$deferred_file" 5 || {
|
|
820
|
+
json_err "$E_LOCK_TIMEOUT" "Could not acquire lock on deferred file"
|
|
821
|
+
exit 1
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
# Read existing deferred file or create empty structure
|
|
825
|
+
if [[ -f "$deferred_file" ]]; then
|
|
826
|
+
existing_deferred=$(jq '.deferred // []' "$deferred_file" 2>/dev/null || echo '[]') # SUPPRESS:OK -- read-default: returns fallback if missing
|
|
827
|
+
else
|
|
828
|
+
existing_deferred='[]'
|
|
829
|
+
fi
|
|
830
|
+
|
|
831
|
+
# Current timestamp for new entries
|
|
832
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
833
|
+
current_epoch=$(date +%s)
|
|
834
|
+
|
|
835
|
+
# Add deferred_at timestamp to each new proposal
|
|
836
|
+
new_proposals=$(echo "$proposals_json" | jq --arg ts "$ts" '
|
|
837
|
+
map(. + {deferred_at: $ts})
|
|
838
|
+
')
|
|
839
|
+
|
|
840
|
+
# Calculate TTL cutoff (30 days ago)
|
|
841
|
+
ttl_cutoff=$((current_epoch - 30 * 24 * 60 * 60))
|
|
842
|
+
|
|
843
|
+
# Filter existing deferred: remove expired entries
|
|
844
|
+
filtered_existing=$(echo "$existing_deferred" | jq --argjson cutoff "$ttl_cutoff" '
|
|
845
|
+
map(select(
|
|
846
|
+
(.deferred_at | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) > $cutoff
|
|
847
|
+
))
|
|
848
|
+
' 2>/dev/null || echo '[]') # SUPPRESS:OK -- read-default: returns fallback on failure
|
|
849
|
+
|
|
850
|
+
# Count expired items
|
|
851
|
+
expired_count=$(echo "$existing_deferred" | jq --argjson cutoff "$ttl_cutoff" '
|
|
852
|
+
map(select(
|
|
853
|
+
(.deferred_at | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) <= $cutoff
|
|
854
|
+
)) | length
|
|
855
|
+
' 2>/dev/null || echo '0') # SUPPRESS:OK -- read-default: returns fallback on failure
|
|
856
|
+
|
|
857
|
+
# Merge new proposals with existing, avoiding duplicates by content_hash
|
|
858
|
+
merged=$(jq -s --argjson new "$new_proposals" '
|
|
859
|
+
def unique_by_hash:
|
|
860
|
+
group_by(.content_hash) | map(first);
|
|
861
|
+
|
|
862
|
+
(.[0] // []) + $new | unique_by_hash
|
|
863
|
+
' <<< "$filtered_existing")
|
|
864
|
+
|
|
865
|
+
# Count new items (those that weren't in existing)
|
|
866
|
+
existing_hashes=$(echo "$filtered_existing" | jq -r 'map(.content_hash) | join(" ")')
|
|
867
|
+
new_count=0
|
|
868
|
+
if [[ -n "$existing_hashes" ]]; then
|
|
869
|
+
new_count=$(echo "$new_proposals" | jq --arg existing "$existing_hashes" '
|
|
870
|
+
[$existing | split(" ")[]] as $hashes |
|
|
871
|
+
map(select(.content_hash as $h | $hashes | index($h) | not)) |
|
|
872
|
+
length
|
|
873
|
+
')
|
|
874
|
+
else
|
|
875
|
+
new_count=$(echo "$new_proposals" | jq 'length')
|
|
876
|
+
fi
|
|
877
|
+
|
|
878
|
+
# Write atomically
|
|
879
|
+
tmp_file="${deferred_file}.tmp.$$"
|
|
880
|
+
jq -n --argjson deferred "$merged" '{deferred: $deferred}' > "$tmp_file"
|
|
881
|
+
mv "$tmp_file" "$deferred_file"
|
|
882
|
+
|
|
883
|
+
# Release lock
|
|
884
|
+
release_lock
|
|
885
|
+
|
|
886
|
+
# Log activity
|
|
887
|
+
total_count=$(echo "$merged" | jq 'length')
|
|
888
|
+
bash "$0" activity-log "DEFERRED" "Queen" "Stored $new_count new deferred proposal(s), $expired_count expired removed, $total_count total"
|
|
889
|
+
|
|
890
|
+
json_ok "{\"deferred\":$total_count,\"new\":$new_count,\"expired\":$expired_count}"
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
# ============================================================================
|
|
894
|
+
# _learning_approve_proposals
|
|
895
|
+
# Orchestrate full approval workflow: one-at-a-time display with Approve/Reject/Skip
|
|
896
|
+
# Usage: learning-approve-proposals [--verbose] [--dry-run] [--yes] [--deferred]
|
|
897
|
+
# Returns: JSON summary {promoted, deferred, failed, undo_offered}
|
|
898
|
+
# ============================================================================
|
|
899
|
+
_learning_approve_proposals() {
|
|
900
|
+
verbose=false
|
|
901
|
+
dry_run=false
|
|
902
|
+
skip_confirm=false
|
|
903
|
+
deferred_mode=false
|
|
904
|
+
undo_mode=false
|
|
905
|
+
|
|
906
|
+
# Parse arguments
|
|
907
|
+
for arg in "$@"; do
|
|
908
|
+
case "$arg" in
|
|
909
|
+
--verbose) verbose=true ;;
|
|
910
|
+
--dry-run) dry_run=true ;;
|
|
911
|
+
--yes) skip_confirm=true ;;
|
|
912
|
+
--deferred) deferred_mode=true ;;
|
|
913
|
+
--undo) undo_mode=true ;;
|
|
914
|
+
esac
|
|
915
|
+
done
|
|
916
|
+
|
|
917
|
+
# Handle --undo mode
|
|
918
|
+
if [[ "$undo_mode" == "true" ]]; then
|
|
919
|
+
undo_result=$(bash "$0" learning-undo-promotions 2>&1)
|
|
920
|
+
echo "$undo_result"
|
|
921
|
+
exit 0
|
|
922
|
+
fi
|
|
923
|
+
|
|
924
|
+
# Get colony name via proper subcommand
|
|
925
|
+
colony_name=$(bash "$0" colony-name 2>/dev/null | jq -r '.result.name // ""') || colony_name="unknown"
|
|
926
|
+
[[ -z "$colony_name" ]] && colony_name="unknown"
|
|
927
|
+
|
|
928
|
+
# Load proposals based on mode
|
|
929
|
+
if [[ "$deferred_mode" == "true" ]]; then
|
|
930
|
+
# Load from deferred file
|
|
931
|
+
if [[ ! -f "$COLONY_DATA_DIR/learning-deferred.json" ]]; then
|
|
932
|
+
echo "No deferred proposals to review."
|
|
933
|
+
json_ok '{"promoted":0,"deferred":0,"failed":null,"undo_offered":false}'
|
|
934
|
+
exit 0
|
|
935
|
+
fi
|
|
936
|
+
# SUPPRESS:OK -- read-default: returns fallback on failure
|
|
937
|
+
proposals_json=$(jq '{proposals: .deferred}' "$COLONY_DATA_DIR/learning-deferred.json" 2>/dev/null || echo '{"proposals":[]}')
|
|
938
|
+
echo "📦 Reviewing deferred proposals..."
|
|
939
|
+
echo ""
|
|
940
|
+
else
|
|
941
|
+
# Get proposals directly from learning-check-promotion
|
|
942
|
+
proposals_result=$(bash "$0" learning-check-promotion 2>/dev/null || echo '{"proposals":[]}') # SUPPRESS:OK -- read-default: subcommand may fail
|
|
943
|
+
proposals_json=$(echo "$proposals_result" | jq '{proposals: .result.proposals // []}')
|
|
944
|
+
|
|
945
|
+
# Check if there were any proposals
|
|
946
|
+
proposal_count=$(echo "$proposals_json" | jq '.proposals | length')
|
|
947
|
+
if [[ "$proposal_count" -eq 0 ]]; then
|
|
948
|
+
json_ok '{"promoted":0,"deferred":0,"failed":null,"undo_offered":false}'
|
|
949
|
+
exit 0
|
|
950
|
+
fi
|
|
951
|
+
fi
|
|
952
|
+
|
|
953
|
+
# Get proposal count
|
|
954
|
+
proposal_count=$(echo "$proposals_json" | jq '.proposals | length')
|
|
955
|
+
if [[ "$proposal_count" -eq 0 ]]; then
|
|
956
|
+
echo "No proposals available."
|
|
957
|
+
json_ok '{"promoted":0,"deferred":0,"failed":null,"undo_offered":false}'
|
|
958
|
+
exit 0
|
|
959
|
+
fi
|
|
960
|
+
|
|
961
|
+
# Define wisdom type emojis and names for display
|
|
962
|
+
declare -A type_emojis
|
|
963
|
+
declare -A type_names
|
|
964
|
+
type_emojis=(
|
|
965
|
+
["philosophy"]="📜"
|
|
966
|
+
["pattern"]="🧭"
|
|
967
|
+
["redirect"]="⚠️"
|
|
968
|
+
["stack"]="🔧"
|
|
969
|
+
["decree"]="🏛️"
|
|
970
|
+
["failure"]="❌"
|
|
971
|
+
)
|
|
972
|
+
type_names=(
|
|
973
|
+
["philosophy"]="Philosophy"
|
|
974
|
+
["pattern"]="Pattern"
|
|
975
|
+
["redirect"]="Redirect"
|
|
976
|
+
["stack"]="Stack Wisdom"
|
|
977
|
+
["decree"]="Decree"
|
|
978
|
+
["failure"]="Failure"
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
# Arrays to track results
|
|
982
|
+
approved_proposals=()
|
|
983
|
+
rejected_proposals=()
|
|
984
|
+
skipped_proposals=()
|
|
985
|
+
|
|
986
|
+
# Process proposals one at a time
|
|
987
|
+
echo ""
|
|
988
|
+
echo "🧠 Wisdom Promotion Review"
|
|
989
|
+
echo "══════════════════════════"
|
|
990
|
+
echo ""
|
|
991
|
+
echo "$proposal_count proposal(s) ready for review"
|
|
992
|
+
echo ""
|
|
993
|
+
|
|
994
|
+
for ((i=0; i<proposal_count; i++)); do
|
|
995
|
+
proposal=$(echo "$proposals_json" | jq ".proposals[$i]")
|
|
996
|
+
ptype=$(echo "$proposal" | jq -r '.wisdom_type')
|
|
997
|
+
content=$(sanitize_read_value "$(echo "$proposal" | jq -r '.content')")
|
|
998
|
+
count=$(echo "$proposal" | jq -r '.observation_count // 1')
|
|
999
|
+
threshold=$(echo "$proposal" | jq -r '.threshold // 1')
|
|
1000
|
+
|
|
1001
|
+
emoji="${type_emojis[$ptype]:-📝}"
|
|
1002
|
+
name="${type_names[$ptype]:-$ptype}"
|
|
1003
|
+
|
|
1004
|
+
# Display proposal
|
|
1005
|
+
echo "───────────────────────────────────────────────────"
|
|
1006
|
+
echo "Proposal $((i+1)) of $proposal_count"
|
|
1007
|
+
echo "───────────────────────────────────────────────────"
|
|
1008
|
+
echo ""
|
|
1009
|
+
echo "$emoji $name (observed $count time(s), threshold: $threshold)"
|
|
1010
|
+
echo ""
|
|
1011
|
+
echo "$content"
|
|
1012
|
+
echo ""
|
|
1013
|
+
echo "───────────────────────────────────────────────────"
|
|
1014
|
+
|
|
1015
|
+
# Handle dry-run mode
|
|
1016
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
1017
|
+
echo "Dry run: would approve"
|
|
1018
|
+
approved_proposals+=("$proposal")
|
|
1019
|
+
echo ""
|
|
1020
|
+
continue
|
|
1021
|
+
fi
|
|
1022
|
+
|
|
1023
|
+
# Handle --yes mode (auto-approve all)
|
|
1024
|
+
if [[ "$skip_confirm" == "true" ]]; then
|
|
1025
|
+
approved_proposals+=("$proposal")
|
|
1026
|
+
echo "✓ Auto-approved (--yes mode)"
|
|
1027
|
+
echo ""
|
|
1028
|
+
continue
|
|
1029
|
+
fi
|
|
1030
|
+
|
|
1031
|
+
# Prompt for action
|
|
1032
|
+
echo -n "[A]pprove [R]eject [S]kip Your choice: "
|
|
1033
|
+
read -r choice
|
|
1034
|
+
|
|
1035
|
+
case "$choice" in
|
|
1036
|
+
[Aa]|"approve"|"Approve")
|
|
1037
|
+
approved_proposals+=("$proposal")
|
|
1038
|
+
echo "✓ Approved"
|
|
1039
|
+
;;
|
|
1040
|
+
[Rr]|"reject"|"Reject")
|
|
1041
|
+
rejected_proposals+=("$proposal")
|
|
1042
|
+
echo "✗ Rejected"
|
|
1043
|
+
;;
|
|
1044
|
+
[Ss]|""|"skip"|"Skip")
|
|
1045
|
+
skipped_proposals+=("$proposal")
|
|
1046
|
+
echo "→ Skipped"
|
|
1047
|
+
;;
|
|
1048
|
+
*)
|
|
1049
|
+
# Invalid input - default to skip
|
|
1050
|
+
skipped_proposals+=("$proposal")
|
|
1051
|
+
echo "→ Skipped (invalid input)"
|
|
1052
|
+
;;
|
|
1053
|
+
esac
|
|
1054
|
+
echo ""
|
|
1055
|
+
done
|
|
1056
|
+
|
|
1057
|
+
# Execute promotions for approved proposals
|
|
1058
|
+
promoted_count=0
|
|
1059
|
+
failed_item=""
|
|
1060
|
+
promoted_items=()
|
|
1061
|
+
|
|
1062
|
+
if [[ ${#approved_proposals[@]} -gt 0 ]]; then
|
|
1063
|
+
echo ""
|
|
1064
|
+
echo "Promoting ${#approved_proposals[@]} observation(s)..."
|
|
1065
|
+
echo ""
|
|
1066
|
+
|
|
1067
|
+
for proposal in "${approved_proposals[@]}"; do
|
|
1068
|
+
ptype=$(echo "$proposal" | jq -r '.wisdom_type')
|
|
1069
|
+
content=$(sanitize_read_value "$(echo "$proposal" | jq -r '.content')")
|
|
1070
|
+
|
|
1071
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
1072
|
+
echo "Dry run: would promote $ptype: \"$content\""
|
|
1073
|
+
((promoted_count++))
|
|
1074
|
+
promoted_items+=("$proposal")
|
|
1075
|
+
continue
|
|
1076
|
+
fi
|
|
1077
|
+
|
|
1078
|
+
# Call queen-promote
|
|
1079
|
+
promote_result=$(bash "$0" queen-promote "$ptype" "$content" "$colony_name" 2>&1) || {
|
|
1080
|
+
echo "✗ Failed to promote: $content"
|
|
1081
|
+
echo " Error: $promote_result"
|
|
1082
|
+
failed_item="$content"
|
|
1083
|
+
# Prompt for retry on failure
|
|
1084
|
+
echo ""
|
|
1085
|
+
echo -n "Write to QUEEN.md failed. Retry? (y/n): "
|
|
1086
|
+
read -r retry_response
|
|
1087
|
+
if [[ "$retry_response" =~ ^[Yy]$ ]]; then
|
|
1088
|
+
# Retry once
|
|
1089
|
+
promote_result=$(bash "$0" queen-promote "$ptype" "$content" "$colony_name" 2>&1) || {
|
|
1090
|
+
echo "✗ Retry failed. Keeping proposal pending."
|
|
1091
|
+
skipped_proposals+=("$proposal")
|
|
1092
|
+
continue
|
|
1093
|
+
}
|
|
1094
|
+
else
|
|
1095
|
+
echo "Skipping this proposal. It will remain pending."
|
|
1096
|
+
skipped_proposals+=("$proposal")
|
|
1097
|
+
continue
|
|
1098
|
+
fi
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
echo "✓ Promoted ${ptype^}: \"$content\""
|
|
1102
|
+
((promoted_count++))
|
|
1103
|
+
promoted_items+=("$proposal")
|
|
1104
|
+
done
|
|
1105
|
+
fi
|
|
1106
|
+
|
|
1107
|
+
# Handle deferred proposals (skipped ones go to deferred)
|
|
1108
|
+
deferred_count=${#skipped_proposals[@]}
|
|
1109
|
+
if [[ "$dry_run" == "false" ]] && [[ $deferred_count -gt 0 ]]; then
|
|
1110
|
+
# Convert skipped proposals to JSON array and defer
|
|
1111
|
+
skipped_json=$(printf '%s\n' "${skipped_proposals[@]}" | jq -s '.')
|
|
1112
|
+
echo "$skipped_json" | bash "$0" learning-defer-proposals >/dev/null 2>&1 || _aether_log_error "Could not defer learning proposals"
|
|
1113
|
+
fi
|
|
1114
|
+
|
|
1115
|
+
# Log activity
|
|
1116
|
+
if [[ "$dry_run" == "false" ]]; then
|
|
1117
|
+
bash "$0" activity-log "PROMOTED" "Queen" "Promoted $promoted_count observation(s), deferred $deferred_count, rejected ${#rejected_proposals[@]}"
|
|
1118
|
+
fi
|
|
1119
|
+
|
|
1120
|
+
# Display summary
|
|
1121
|
+
echo ""
|
|
1122
|
+
echo "═══════════════════════════════════════════════════"
|
|
1123
|
+
echo "Summary: $promoted_count approved, ${#rejected_proposals[@]} rejected, $deferred_count skipped"
|
|
1124
|
+
echo "═══════════════════════════════════════════════════"
|
|
1125
|
+
echo ""
|
|
1126
|
+
|
|
1127
|
+
# Offer undo if promotions succeeded
|
|
1128
|
+
undo_offered=false
|
|
1129
|
+
if [[ "$promoted_count" -gt 0 ]] && [[ "$dry_run" == "false" ]] && [[ -z "$failed_item" ]]; then
|
|
1130
|
+
undo_offered=true
|
|
1131
|
+
|
|
1132
|
+
# Store undo info
|
|
1133
|
+
undo_file="$COLONY_DATA_DIR/.promotion-undo.json"
|
|
1134
|
+
promoted_json=$(printf '%s\n' "${promoted_items[@]}" | jq -s '.')
|
|
1135
|
+
jq -n --argjson items "$promoted_json" --arg ts "$(date +%s)" '{promoted: $items, timestamp: ($ts | tonumber)}' > "$undo_file"
|
|
1136
|
+
|
|
1137
|
+
echo -n "Undo these promotions? (y/n): "
|
|
1138
|
+
read -r undo_response
|
|
1139
|
+
|
|
1140
|
+
if [[ "$undo_response" =~ ^[Yy]$ ]]; then
|
|
1141
|
+
echo "Reverting promotions..."
|
|
1142
|
+
undo_result=$(bash "$0" learning-undo-promotions 2>&1)
|
|
1143
|
+
if echo "$undo_result" | jq -e '.ok' >/dev/null 2>&1; then # SUPPRESS:OK -- validation: testing JSON field
|
|
1144
|
+
undone_count=$(echo "$undo_result" | jq -r '.result.undone // 0')
|
|
1145
|
+
echo "$undone_count promotion(s) reverted."
|
|
1146
|
+
promoted_count=0
|
|
1147
|
+
else
|
|
1148
|
+
echo "Undo failed: $(echo "$undo_result" | jq -r '.error.message // "Unknown error"')"
|
|
1149
|
+
fi
|
|
1150
|
+
else
|
|
1151
|
+
echo "Promotions kept."
|
|
1152
|
+
fi
|
|
1153
|
+
fi
|
|
1154
|
+
|
|
1155
|
+
# Build result
|
|
1156
|
+
result=$(jq -n \
|
|
1157
|
+
--argjson promoted "$promoted_count" \
|
|
1158
|
+
--argjson deferred "$deferred_count" \
|
|
1159
|
+
--argjson rejected "${#rejected_proposals[@]}" \
|
|
1160
|
+
--arg failed "${failed_item:-null}" \
|
|
1161
|
+
--argjson undo "$undo_offered" \
|
|
1162
|
+
'{promoted: $promoted, deferred: $deferred, rejected: $rejected, failed: $failed, undo_offered: $undo}')
|
|
1163
|
+
|
|
1164
|
+
json_ok "$result"
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
# ============================================================================
|
|
1168
|
+
# _learning_undo_promotions
|
|
1169
|
+
# Revert promotions from QUEEN.md using undo file
|
|
1170
|
+
# Usage: learning-undo-promotions
|
|
1171
|
+
# Returns: JSON with count of undone items
|
|
1172
|
+
# ============================================================================
|
|
1173
|
+
_learning_undo_promotions() {
|
|
1174
|
+
undo_file="$COLONY_DATA_DIR/.promotion-undo.json"
|
|
1175
|
+
|
|
1176
|
+
# Check if undo file exists
|
|
1177
|
+
if [[ ! -f "$undo_file" ]]; then
|
|
1178
|
+
json_err "$E_FILE_NOT_FOUND" "No undo file found. Cannot undo promotions."
|
|
1179
|
+
exit 1
|
|
1180
|
+
fi
|
|
1181
|
+
|
|
1182
|
+
# Read undo data
|
|
1183
|
+
undo_data=$(cat "$undo_file")
|
|
1184
|
+
undo_timestamp=$(echo "$undo_data" | jq -r '.timestamp // 0')
|
|
1185
|
+
current_time=$(date +%s)
|
|
1186
|
+
|
|
1187
|
+
# Check 24h TTL
|
|
1188
|
+
ttl_seconds=$((24 * 60 * 60))
|
|
1189
|
+
time_diff=$((current_time - undo_timestamp))
|
|
1190
|
+
|
|
1191
|
+
if [[ $time_diff -gt $ttl_seconds ]]; then
|
|
1192
|
+
# Remove expired undo file
|
|
1193
|
+
rm -f "$undo_file"
|
|
1194
|
+
json_err "$E_VALIDATION_FAILED" "Undo window expired (24h limit)"
|
|
1195
|
+
exit 1
|
|
1196
|
+
fi
|
|
1197
|
+
|
|
1198
|
+
queen_file="$AETHER_ROOT/.aether/QUEEN.md"
|
|
1199
|
+
|
|
1200
|
+
# Check if QUEEN.md exists
|
|
1201
|
+
if [[ ! -f "$queen_file" ]]; then
|
|
1202
|
+
json_err "$E_FILE_NOT_FOUND" "QUEEN.md not found"
|
|
1203
|
+
exit 1
|
|
1204
|
+
fi
|
|
1205
|
+
|
|
1206
|
+
# Process each promoted item
|
|
1207
|
+
undone_count=0
|
|
1208
|
+
failed_items=()
|
|
1209
|
+
|
|
1210
|
+
# Read promoted items from undo file
|
|
1211
|
+
promoted_items=$(echo "$undo_data" | jq -c '.promoted[]?')
|
|
1212
|
+
|
|
1213
|
+
if [[ -z "$promoted_items" ]]; then
|
|
1214
|
+
rm -f "$undo_file"
|
|
1215
|
+
json_err "$E_VALIDATION_FAILED" "No promoted items in undo file"
|
|
1216
|
+
exit 1
|
|
1217
|
+
fi
|
|
1218
|
+
|
|
1219
|
+
# Create temp file for atomic write
|
|
1220
|
+
tmp_file="${queen_file}.tmp.$$"
|
|
1221
|
+
|
|
1222
|
+
# Copy current QUEEN.md to temp
|
|
1223
|
+
cp "$queen_file" "$tmp_file"
|
|
1224
|
+
|
|
1225
|
+
# Process each item
|
|
1226
|
+
while IFS= read -r item; do
|
|
1227
|
+
[[ -z "$item" ]] && continue
|
|
1228
|
+
|
|
1229
|
+
ptype=$(echo "$item" | jq -r '.wisdom_type')
|
|
1230
|
+
content=$(sanitize_read_value "$(echo "$item" | jq -r '.content')")
|
|
1231
|
+
|
|
1232
|
+
# Map type to section header
|
|
1233
|
+
case "$ptype" in
|
|
1234
|
+
philosophy) section_header="## 📜 Philosophies" ;;
|
|
1235
|
+
pattern) section_header="## 🧭 Patterns" ;;
|
|
1236
|
+
redirect) section_header="## ⚠️ Redirects" ;;
|
|
1237
|
+
stack) section_header="## 🔧 Stack Wisdom" ;;
|
|
1238
|
+
decree) section_header="## 🏛️ Decrees" ;;
|
|
1239
|
+
*) continue ;;
|
|
1240
|
+
esac
|
|
1241
|
+
|
|
1242
|
+
# Escape content for sed (basic escaping)
|
|
1243
|
+
escaped_content=$(echo "$content" | sed 's/[\\/&]/\\&/g')
|
|
1244
|
+
|
|
1245
|
+
# Find and remove the entry from the section
|
|
1246
|
+
# Pattern: - **colony_name** (timestamp): content
|
|
1247
|
+
# We match based on content since that's the unique part
|
|
1248
|
+
if grep -q "${escaped_content}" "$tmp_file" 2>/dev/null; then # SUPPRESS:OK -- existence-test: file may not exist
|
|
1249
|
+
# Remove line containing this content within the section
|
|
1250
|
+
# Use awk to handle section-aware removal
|
|
1251
|
+
awk -v section="$section_header" -v content="$content" '
|
|
1252
|
+
BEGIN { in_section = 0 }
|
|
1253
|
+
$0 == section { in_section = 1 }
|
|
1254
|
+
in_section && $0 ~ /^## / && $0 != section { in_section = 0 }
|
|
1255
|
+
in_section && $0 ~ content { skip = 1; next }
|
|
1256
|
+
{ if (!skip) print; skip = 0 }
|
|
1257
|
+
' "$tmp_file" > "${tmp_file}.new" && mv "${tmp_file}.new" "$tmp_file"
|
|
1258
|
+
|
|
1259
|
+
((undone_count++))
|
|
1260
|
+
else
|
|
1261
|
+
# Entry already removed or not found
|
|
1262
|
+
failed_items+=("$content")
|
|
1263
|
+
fi
|
|
1264
|
+
done <<< "$promoted_items"
|
|
1265
|
+
|
|
1266
|
+
# Update METADATA stats in temp file - decrement counts
|
|
1267
|
+
case "$ptype" in
|
|
1268
|
+
stack) stat_key="total_stack_entries" ;;
|
|
1269
|
+
philosophy) stat_key="total_philosophies" ;;
|
|
1270
|
+
*) stat_key="total_${ptype}s" ;;
|
|
1271
|
+
esac
|
|
1272
|
+
|
|
1273
|
+
# Decrement stats (but not below 0)
|
|
1274
|
+
current_count=$(grep "\"${stat_key}\":" "$tmp_file" 2>/dev/null | grep -o '[0-9]*' | head -1 || echo "0") # SUPPRESS:OK -- read-default: file may not exist
|
|
1275
|
+
current_count=${current_count:-0}
|
|
1276
|
+
if [[ $current_count -gt 0 ]]; then
|
|
1277
|
+
new_count=$((current_count - 1))
|
|
1278
|
+
awk -v type="$stat_key" -v count="$new_count" '{
|
|
1279
|
+
gsub("\"" type "\": [0-9]*", "\"" type "\": " count)
|
|
1280
|
+
print
|
|
1281
|
+
}' "$tmp_file" > "${tmp_file}.stats" && mv "${tmp_file}.stats" "$tmp_file"
|
|
1282
|
+
fi
|
|
1283
|
+
|
|
1284
|
+
# Atomic move
|
|
1285
|
+
mv "$tmp_file" "$queen_file"
|
|
1286
|
+
|
|
1287
|
+
# Remove undo file after successful revert
|
|
1288
|
+
rm -f "$undo_file"
|
|
1289
|
+
|
|
1290
|
+
# Log activity
|
|
1291
|
+
bash "$0" activity-log "UNDONE" "Queen" "Reverted $undone_count promotion(s) from QUEEN.md"
|
|
1292
|
+
|
|
1293
|
+
# Build result
|
|
1294
|
+
if [[ ${#failed_items[@]} -gt 0 ]]; then
|
|
1295
|
+
failed_json=$(printf '%s\n' "${failed_items[@]}" | jq -R . | jq -s '.')
|
|
1296
|
+
result=$(jq -n --argjson undone "$undone_count" --argjson failed "$failed_json" '{undone: $undone, not_found: $failed}')
|
|
1297
|
+
else
|
|
1298
|
+
result=$(jq -n --argjson undone "$undone_count" '{undone: $undone, not_found: []}')
|
|
1299
|
+
fi
|
|
1300
|
+
|
|
1301
|
+
json_ok "$result"
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
# ============================================================================
|
|
1305
|
+
# _instinct_read
|
|
1306
|
+
# Read learned instincts from COLONY_STATE.json memory
|
|
1307
|
+
# Migrated to state-api facade: uses _state_read_field for read-only access
|
|
1308
|
+
# Usage: instinct-read [--min-confidence N] [--max N] [--domain DOMAIN]
|
|
1309
|
+
# Returns: JSON with filtered, confidence-sorted instincts
|
|
1310
|
+
# ============================================================================
|
|
1311
|
+
_instinct_read() {
|
|
1312
|
+
ir_min_confidence="0.5"
|
|
1313
|
+
ir_max="5"
|
|
1314
|
+
ir_domain=""
|
|
1315
|
+
|
|
1316
|
+
# Parse flags from positional args
|
|
1317
|
+
ir_shift=1
|
|
1318
|
+
while [[ $ir_shift -le $# ]]; do
|
|
1319
|
+
eval "ir_arg=\${$ir_shift}"
|
|
1320
|
+
ir_shift=$((ir_shift + 1))
|
|
1321
|
+
case "$ir_arg" in
|
|
1322
|
+
--min-confidence)
|
|
1323
|
+
eval "ir_min_confidence=\${$ir_shift}"
|
|
1324
|
+
ir_shift=$((ir_shift + 1))
|
|
1325
|
+
;;
|
|
1326
|
+
--max)
|
|
1327
|
+
eval "ir_max=\${$ir_shift}"
|
|
1328
|
+
ir_shift=$((ir_shift + 1))
|
|
1329
|
+
;;
|
|
1330
|
+
--domain)
|
|
1331
|
+
eval "ir_domain=\${$ir_shift}"
|
|
1332
|
+
ir_shift=$((ir_shift + 1))
|
|
1333
|
+
;;
|
|
1334
|
+
esac
|
|
1335
|
+
done
|
|
1336
|
+
|
|
1337
|
+
# Read full state via facade
|
|
1338
|
+
ir_state=$(_state_read_field '.')
|
|
1339
|
+
if [[ -z "$ir_state" ]]; then
|
|
1340
|
+
json_err "$E_FILE_NOT_FOUND" "COLONY_STATE.json not found. Run /ant:init first."
|
|
1341
|
+
fi
|
|
1342
|
+
|
|
1343
|
+
# Check if memory.instincts exists
|
|
1344
|
+
ir_has_instincts=$(echo "$ir_state" | jq 'if .memory.instincts then "yes" else "no" end' 2>/dev/null || echo "no")
|
|
1345
|
+
if [[ "$ir_has_instincts" != '"yes"' ]]; then
|
|
1346
|
+
json_ok '{"instincts":[],"total":0,"filtered":0}'
|
|
1347
|
+
exit 0
|
|
1348
|
+
fi
|
|
1349
|
+
|
|
1350
|
+
ir_result=$(echo "$ir_state" | jq -c \
|
|
1351
|
+
--argjson min_conf "$ir_min_confidence" \
|
|
1352
|
+
--argjson max_count "$ir_max" \
|
|
1353
|
+
--arg domain_filter "$ir_domain" \
|
|
1354
|
+
'
|
|
1355
|
+
(.memory.instincts // []) as $all |
|
|
1356
|
+
($all | length) as $total |
|
|
1357
|
+
$all
|
|
1358
|
+
| map(select(
|
|
1359
|
+
(.confidence // 0) >= $min_conf
|
|
1360
|
+
and (.status // "hypothesis") != "disproven"
|
|
1361
|
+
and (if $domain_filter != "" then (.domain // "") == $domain_filter else true end)
|
|
1362
|
+
))
|
|
1363
|
+
| sort_by(-.confidence)
|
|
1364
|
+
| .[:$max_count]
|
|
1365
|
+
| {
|
|
1366
|
+
instincts: .,
|
|
1367
|
+
total: $total,
|
|
1368
|
+
filtered: (. | length)
|
|
1369
|
+
}
|
|
1370
|
+
' 2>/dev/null)
|
|
1371
|
+
|
|
1372
|
+
if [[ -z "$ir_result" || "$ir_result" == "null" ]]; then
|
|
1373
|
+
json_ok '{"instincts":[],"total":0,"filtered":0}'
|
|
1374
|
+
else
|
|
1375
|
+
json_ok "$ir_result"
|
|
1376
|
+
fi
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
# ============================================================================
|
|
1380
|
+
# _normalize_text
|
|
1381
|
+
# Canonical text form for fuzzy comparison: lowercase, strip punctuation,
|
|
1382
|
+
# collapse whitespace, synonym substitution, stop word removal
|
|
1383
|
+
# Usage: _normalize_text "When Implementing Tests"
|
|
1384
|
+
# Output: stdout (e.g., "writing testing")
|
|
1385
|
+
# ============================================================================
|
|
1386
|
+
_normalize_text() {
|
|
1387
|
+
local text="$1"
|
|
1388
|
+
|
|
1389
|
+
# Guard: empty input
|
|
1390
|
+
[[ -z "$text" ]] && echo "" && return 0
|
|
1391
|
+
|
|
1392
|
+
# Lowercase
|
|
1393
|
+
text=$(echo "$text" | tr '[:upper:]' '[:lower:]')
|
|
1394
|
+
|
|
1395
|
+
# Strip punctuation (keep alphanumeric, spaces, hyphens)
|
|
1396
|
+
text=$(echo "$text" | tr -cd '[:alnum:][:space:]-')
|
|
1397
|
+
|
|
1398
|
+
# Collapse whitespace
|
|
1399
|
+
text=$(echo "$text" | awk '{$1=$1};1')
|
|
1400
|
+
|
|
1401
|
+
# Synonym substitution + stop word removal via awk
|
|
1402
|
+
text=$(echo "$text" | awk 'BEGIN {
|
|
1403
|
+
syn["implementing"] = "writing"; syn["creating"] = "writing"; syn["building"] = "writing";
|
|
1404
|
+
syn["implement"] = "writing"; syn["create"] = "writing"; syn["build"] = "writing";
|
|
1405
|
+
syn["write"] = "writing";
|
|
1406
|
+
syn["tests"] = "testing"; syn["checking"] = "testing"; syn["verifying"] = "testing";
|
|
1407
|
+
syn["fixing"] = "resolving"; syn["repairing"] = "resolving"; syn["patching"] = "resolving";
|
|
1408
|
+
syn["fix"] = "resolving"; syn["repair"] = "resolving"; syn["patch"] = "resolving";
|
|
1409
|
+
syn["resolve"] = "resolving"
|
|
1410
|
+
}
|
|
1411
|
+
{
|
|
1412
|
+
n = split($0, words, " ")
|
|
1413
|
+
out = 0
|
|
1414
|
+
for (i = 1; i <= n; i++) {
|
|
1415
|
+
w = words[i]
|
|
1416
|
+
if (w == "") continue
|
|
1417
|
+
if (w in syn) w = syn[w]
|
|
1418
|
+
# Stop words: when, while, during, before, after
|
|
1419
|
+
if (w == "when" || w == "while" || w == "during" || w == "before" || w == "after") continue
|
|
1420
|
+
printf "%s%s", (out > 0 ? " " : ""), w
|
|
1421
|
+
out++
|
|
1422
|
+
}
|
|
1423
|
+
printf "\n"
|
|
1424
|
+
}')
|
|
1425
|
+
|
|
1426
|
+
echo "$text"
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
# ============================================================================
|
|
1430
|
+
# _jaccard_similarity
|
|
1431
|
+
# Word-level Jaccard similarity between two strings
|
|
1432
|
+
# Usage: _jaccard_similarity "when writing tests" "when implementing tests"
|
|
1433
|
+
# Output: stdout (e.g., "0.80")
|
|
1434
|
+
# ============================================================================
|
|
1435
|
+
_jaccard_similarity() {
|
|
1436
|
+
local text_a="$1"
|
|
1437
|
+
local text_b="$2"
|
|
1438
|
+
|
|
1439
|
+
# Normalize both texts
|
|
1440
|
+
local norm_a norm_b
|
|
1441
|
+
norm_a=$(_normalize_text "$text_a")
|
|
1442
|
+
norm_b=$(_normalize_text "$text_b")
|
|
1443
|
+
|
|
1444
|
+
# Guard: empty after normalization
|
|
1445
|
+
[[ -z "$norm_a" || -z "$norm_b" ]] && echo "0.00" && return 0
|
|
1446
|
+
|
|
1447
|
+
# Compute Jaccard via awk using NUL delimiter between the two texts
|
|
1448
|
+
# Both texts are already normalized (no newlines, no special chars)
|
|
1449
|
+
printf '%s\037%s\n' "$norm_a" "$norm_b" | awk -F'\037' '
|
|
1450
|
+
{
|
|
1451
|
+
split($1, a_words, " ")
|
|
1452
|
+
split($2, b_words, " ")
|
|
1453
|
+
|
|
1454
|
+
# Build set A
|
|
1455
|
+
for (i in a_words) if (a_words[i] != "") set_a[a_words[i]] = 1
|
|
1456
|
+
|
|
1457
|
+
# Build set B
|
|
1458
|
+
for (i in b_words) if (b_words[i] != "") set_b[b_words[i]] = 1
|
|
1459
|
+
|
|
1460
|
+
# Compute intersection and union
|
|
1461
|
+
intersection = 0
|
|
1462
|
+
union = 0
|
|
1463
|
+
for (key in set_a) {
|
|
1464
|
+
union++
|
|
1465
|
+
if (key in set_b) intersection++
|
|
1466
|
+
}
|
|
1467
|
+
for (key in set_b) {
|
|
1468
|
+
if (!(key in set_a)) union++
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
# Guard: avoid division by zero
|
|
1472
|
+
if (union == 0) { printf "0.00\n"; exit }
|
|
1473
|
+
|
|
1474
|
+
printf "%.2f\n", intersection / union
|
|
1475
|
+
}'
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
# ============================================================================
|
|
1479
|
+
# _instinct_create
|
|
1480
|
+
# Create or update an instinct in COLONY_STATE.json
|
|
1481
|
+
# Migrated to state-api facade: uses _state_read_field for reads, _state_mutate for atomic writes
|
|
1482
|
+
# Usage: instinct-create --trigger "when X" --action "do Y" --confidence 0.5 --domain "architecture" --source "phase-3" --evidence "observation"
|
|
1483
|
+
# Deduplicates: if trigger+action matches existing instinct, boosts confidence instead
|
|
1484
|
+
# Fuzzy dedup: if trigger AND action both have >= 0.80 Jaccard similarity, merges into best match
|
|
1485
|
+
# Cap: max 30 instincts, evicts lowest confidence when exceeded
|
|
1486
|
+
# ============================================================================
|
|
1487
|
+
_instinct_create() {
|
|
1488
|
+
ic_trigger=""
|
|
1489
|
+
ic_action=""
|
|
1490
|
+
ic_confidence="0.5"
|
|
1491
|
+
ic_domain="workflow"
|
|
1492
|
+
ic_source=""
|
|
1493
|
+
ic_evidence=""
|
|
1494
|
+
|
|
1495
|
+
while [[ $# -gt 0 ]]; do
|
|
1496
|
+
case "$1" in
|
|
1497
|
+
--trigger) ic_trigger="$2"; shift 2 ;;
|
|
1498
|
+
--action) ic_action="$2"; shift 2 ;;
|
|
1499
|
+
--confidence) ic_confidence="$2"; shift 2 ;;
|
|
1500
|
+
--domain) ic_domain="$2"; shift 2 ;;
|
|
1501
|
+
--source) ic_source="$2"; shift 2 ;;
|
|
1502
|
+
--evidence) ic_evidence="$2"; shift 2 ;;
|
|
1503
|
+
*) shift ;;
|
|
1504
|
+
esac
|
|
1505
|
+
done
|
|
1506
|
+
|
|
1507
|
+
[[ -z "$ic_trigger" ]] && json_err "$E_VALIDATION_FAILED" "instinct-create requires --trigger"
|
|
1508
|
+
[[ -z "$ic_action" ]] && json_err "$E_VALIDATION_FAILED" "instinct-create requires --action"
|
|
1509
|
+
|
|
1510
|
+
# Validate confidence range
|
|
1511
|
+
if ! [[ "$ic_confidence" =~ ^(0(\.[0-9]+)?|1(\.0+)?)$ ]]; then
|
|
1512
|
+
ic_confidence="0.5"
|
|
1513
|
+
fi
|
|
1514
|
+
|
|
1515
|
+
ic_now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1516
|
+
ic_epoch=$(date +%s)
|
|
1517
|
+
ic_id="instinct_${ic_epoch}"
|
|
1518
|
+
|
|
1519
|
+
# Check for existing instinct with matching trigger+action via facade
|
|
1520
|
+
ic_existing=$(_state_read_field "$(printf '[(.memory.instincts // [])[] | select(.trigger == "%s" and .action == "%s")] | first // null' "$ic_trigger" "$ic_action")")
|
|
1521
|
+
|
|
1522
|
+
if [[ -n "$ic_existing" && "$ic_existing" != "null" ]]; then
|
|
1523
|
+
# Update existing: boost confidence by +0.1, increment applications
|
|
1524
|
+
IC_TRIGGER="$ic_trigger" IC_ACTION="$ic_action" IC_NOW="$ic_now" \
|
|
1525
|
+
_state_mutate '
|
|
1526
|
+
.memory.instincts = [
|
|
1527
|
+
(.memory.instincts // [])[] |
|
|
1528
|
+
if .trigger == env.IC_TRIGGER and .action == env.IC_ACTION then
|
|
1529
|
+
.confidence = ([(.confidence + 0.1), 1.0] | min) |
|
|
1530
|
+
.applications = ((.applications // 0) + 1) |
|
|
1531
|
+
.last_applied = env.IC_NOW
|
|
1532
|
+
else
|
|
1533
|
+
.
|
|
1534
|
+
end
|
|
1535
|
+
]
|
|
1536
|
+
' >/dev/null
|
|
1537
|
+
|
|
1538
|
+
# Read updated confidence
|
|
1539
|
+
ic_new_conf=$(_state_read_field "$(printf '[(.memory.instincts // [])[] | select(.trigger == "%s" and .action == "%s")] | first | .confidence // 0' "$ic_trigger" "$ic_action")")
|
|
1540
|
+
json_ok "{\"instinct_id\":\"existing\",\"action\":\"updated\",\"confidence\":$ic_new_conf}"
|
|
1541
|
+
else
|
|
1542
|
+
# --- Fuzzy dedup: check for semantically similar instinct ---
|
|
1543
|
+
ic_all_instincts=$(_state_read_field '.memory.instincts // []')
|
|
1544
|
+
ic_fuzzy_match=""
|
|
1545
|
+
|
|
1546
|
+
if [[ -n "$ic_all_instincts" && "$ic_all_instincts" != "null" && "$ic_all_instincts" != "[]" ]]; then
|
|
1547
|
+
ic_best_sim="0.00"
|
|
1548
|
+
ic_best_id=""
|
|
1549
|
+
ic_best_conf=""
|
|
1550
|
+
|
|
1551
|
+
# Iterate over existing instincts to find best fuzzy match
|
|
1552
|
+
while IFS= read -r ic_line; do
|
|
1553
|
+
[[ -z "$ic_line" ]] && continue
|
|
1554
|
+
ic_ex_trigger=$(echo "$ic_line" | jq -r '.trigger // empty')
|
|
1555
|
+
ic_ex_action=$(echo "$ic_line" | jq -r '.action // empty')
|
|
1556
|
+
ic_ex_id=$(echo "$ic_line" | jq -r '.id // empty')
|
|
1557
|
+
ic_ex_conf=$(echo "$ic_line" | jq -r '.confidence // 0')
|
|
1558
|
+
|
|
1559
|
+
[[ -z "$ic_ex_trigger" || -z "$ic_ex_action" || "$ic_ex_trigger" == "null" || "$ic_ex_action" == "null" ]] && continue
|
|
1560
|
+
|
|
1561
|
+
# Compute Jaccard similarity for trigger and action independently
|
|
1562
|
+
ic_trig_sim=$(_jaccard_similarity "$ic_trigger" "$ic_ex_trigger")
|
|
1563
|
+
ic_act_sim=$(_jaccard_similarity "$ic_action" "$ic_ex_action")
|
|
1564
|
+
|
|
1565
|
+
# Both must exceed 0.80 threshold
|
|
1566
|
+
if (( $(echo "$ic_trig_sim >= 0.80" | bc -l) )) && (( $(echo "$ic_act_sim >= 0.80" | bc -l) )); then
|
|
1567
|
+
# Pick highest similarity; tie-break by higher confidence
|
|
1568
|
+
ic_combined=$(echo "$ic_trig_sim + $ic_act_sim" | bc -l)
|
|
1569
|
+
ic_best_combined=$(echo "${ic_best_sim:-0} + 0" | bc -l)
|
|
1570
|
+
ic_best_conf_num="${ic_best_conf:-0}"
|
|
1571
|
+
if (( $(echo "$ic_combined > $ic_best_combined" | bc -l) )) || \
|
|
1572
|
+
(( $(echo "$ic_combined == $ic_best_combined && $ic_ex_conf >= $ic_best_conf_num" | bc -l) )); then
|
|
1573
|
+
ic_best_sim="$ic_combined"
|
|
1574
|
+
ic_best_id="$ic_ex_id"
|
|
1575
|
+
ic_best_conf="$ic_ex_conf"
|
|
1576
|
+
ic_fuzzy_match="$ic_line"
|
|
1577
|
+
fi
|
|
1578
|
+
fi
|
|
1579
|
+
done < <(echo "$ic_all_instincts" | jq -c '.[]')
|
|
1580
|
+
fi
|
|
1581
|
+
|
|
1582
|
+
if [[ -n "$ic_fuzzy_match" ]]; then
|
|
1583
|
+
# Merge into best matching instinct
|
|
1584
|
+
ic_ex_conf_num=$(echo "$ic_fuzzy_match" | jq -r '.confidence // 0')
|
|
1585
|
+
ic_ex_evidence=$(echo "$ic_fuzzy_match" | jq -c '.evidence // []')
|
|
1586
|
+
ic_ex_trigger=$(echo "$ic_fuzzy_match" | jq -r '.trigger')
|
|
1587
|
+
ic_ex_action=$(echo "$ic_fuzzy_match" | jq -r '.action')
|
|
1588
|
+
|
|
1589
|
+
# Average confidences (use printf to ensure leading zero for valid JSON)
|
|
1590
|
+
ic_new_conf=$(printf "%.2f" "$(echo "scale=4; ($ic_ex_conf_num + $ic_confidence) / 2" | bc -l)")
|
|
1591
|
+
# Keep longer trigger
|
|
1592
|
+
ic_merged_trigger="$ic_ex_trigger"
|
|
1593
|
+
[[ ${#ic_trigger} -gt ${#ic_merged_trigger} ]] && ic_merged_trigger="$ic_trigger"
|
|
1594
|
+
# Keep longer action
|
|
1595
|
+
ic_merged_action="$ic_ex_action"
|
|
1596
|
+
[[ ${#ic_action} -gt ${#ic_merged_action} ]] && ic_merged_action="$ic_action"
|
|
1597
|
+
|
|
1598
|
+
# Build evidence array: existing + new
|
|
1599
|
+
if [[ "$ic_evidence" != "" && "$ic_evidence" != "null" ]]; then
|
|
1600
|
+
ic_merged_evidence=$(echo "$ic_ex_evidence" | jq --arg ev "$ic_evidence" '. + [$ev]')
|
|
1601
|
+
else
|
|
1602
|
+
ic_merged_evidence="$ic_ex_evidence"
|
|
1603
|
+
fi
|
|
1604
|
+
|
|
1605
|
+
IC_FUZZY_ID="$ic_best_id" IC_MERGED_TRIGGER="$ic_merged_trigger" IC_MERGED_ACTION="$ic_merged_action" \
|
|
1606
|
+
IC_NEW_CONF="$ic_new_conf" IC_MERGED_EVIDENCE="$ic_merged_evidence" IC_NOW="$ic_now" \
|
|
1607
|
+
_state_mutate '
|
|
1608
|
+
.memory.instincts = [
|
|
1609
|
+
(.memory.instincts // [])[] |
|
|
1610
|
+
if .id == env.IC_FUZZY_ID then
|
|
1611
|
+
.trigger = env.IC_MERGED_TRIGGER |
|
|
1612
|
+
.action = env.IC_MERGED_ACTION |
|
|
1613
|
+
.confidence = (env.IC_NEW_CONF | tonumber) |
|
|
1614
|
+
.evidence = (env.IC_MERGED_EVIDENCE | fromjson) |
|
|
1615
|
+
.applications = ((.applications // 0) + 1) |
|
|
1616
|
+
.last_applied = env.IC_NOW
|
|
1617
|
+
else
|
|
1618
|
+
.
|
|
1619
|
+
end
|
|
1620
|
+
]
|
|
1621
|
+
' >/dev/null
|
|
1622
|
+
|
|
1623
|
+
json_ok "$(jq -n --arg iid "$ic_best_id" --argjson conf "$ic_new_conf" '{instinct_id: $iid, action: "merged", confidence: $conf}')"
|
|
1624
|
+
exit 0
|
|
1625
|
+
fi
|
|
1626
|
+
|
|
1627
|
+
# Create new instinct via _state_mutate (handles locking and backup)
|
|
1628
|
+
IC_ID="$ic_id" IC_TRIGGER="$ic_trigger" IC_ACTION="$ic_action" IC_CONFIDENCE="$ic_confidence" \
|
|
1629
|
+
IC_DOMAIN="$ic_domain" IC_SOURCE="$ic_source" IC_EVIDENCE="$ic_evidence" IC_NOW="$ic_now" \
|
|
1630
|
+
_state_mutate '
|
|
1631
|
+
.memory.instincts = (
|
|
1632
|
+
((.memory.instincts // []) + [{
|
|
1633
|
+
id: env.IC_ID,
|
|
1634
|
+
trigger: env.IC_TRIGGER,
|
|
1635
|
+
action: env.IC_ACTION,
|
|
1636
|
+
confidence: (env.IC_CONFIDENCE | tonumber),
|
|
1637
|
+
status: "hypothesis",
|
|
1638
|
+
domain: env.IC_DOMAIN,
|
|
1639
|
+
source: env.IC_SOURCE,
|
|
1640
|
+
evidence: [env.IC_EVIDENCE],
|
|
1641
|
+
tested: false,
|
|
1642
|
+
created_at: env.IC_NOW,
|
|
1643
|
+
last_applied: null,
|
|
1644
|
+
applications: 0,
|
|
1645
|
+
successes: 0,
|
|
1646
|
+
failures: 0
|
|
1647
|
+
}])
|
|
1648
|
+
| sort_by(-.confidence)
|
|
1649
|
+
| .[:30]
|
|
1650
|
+
)
|
|
1651
|
+
' >/dev/null
|
|
1652
|
+
|
|
1653
|
+
json_ok "$(jq -n --arg iid "$ic_id" --argjson conf "$ic_confidence" '{instinct_id: $iid, action: "created", confidence: $conf}')"
|
|
1654
|
+
fi
|
|
1655
|
+
exit 0
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
# ============================================================================
|
|
1659
|
+
# _instinct_apply
|
|
1660
|
+
# Record when an instinct was actually used in practice
|
|
1661
|
+
# Migrated to state-api facade: uses _state_read_field for reads, _state_mutate for atomic writes
|
|
1662
|
+
# Usage: instinct-apply --id <instinct_id> [--outcome success|failure]
|
|
1663
|
+
# Success: boosts confidence by 0.05 (cap 1.0), increments successes
|
|
1664
|
+
# Failure: reduces confidence by 0.1 (floor 0.1), increments failures
|
|
1665
|
+
# ============================================================================
|
|
1666
|
+
_instinct_apply() {
|
|
1667
|
+
ia_id=""
|
|
1668
|
+
ia_outcome="success"
|
|
1669
|
+
|
|
1670
|
+
while [[ $# -gt 0 ]]; do
|
|
1671
|
+
case "$1" in
|
|
1672
|
+
--id) ia_id="$2"; shift 2 ;;
|
|
1673
|
+
--outcome) ia_outcome="$2"; shift 2 ;;
|
|
1674
|
+
*) shift ;;
|
|
1675
|
+
esac
|
|
1676
|
+
done
|
|
1677
|
+
|
|
1678
|
+
[[ -z "$ia_id" ]] && json_err "$E_VALIDATION_FAILED" "instinct-apply requires --id"
|
|
1679
|
+
|
|
1680
|
+
# Validate outcome
|
|
1681
|
+
if [[ "$ia_outcome" != "success" && "$ia_outcome" != "failure" ]]; then
|
|
1682
|
+
json_err "$E_VALIDATION_FAILED" "instinct-apply --outcome must be 'success' or 'failure'"
|
|
1683
|
+
fi
|
|
1684
|
+
|
|
1685
|
+
# Check instinct exists via facade
|
|
1686
|
+
ia_exists=$(_state_read_field "$(printf '[(.memory.instincts // [])[] | select(.id == "%s")] | length > 0' "$ia_id")")
|
|
1687
|
+
if [[ "$ia_exists" != "true" ]]; then
|
|
1688
|
+
json_err "$E_RESOURCE_NOT_FOUND" "Instinct '$ia_id' not found"
|
|
1689
|
+
fi
|
|
1690
|
+
|
|
1691
|
+
ia_now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1692
|
+
|
|
1693
|
+
# Update the instinct based on outcome via _state_mutate (handles locking and backup)
|
|
1694
|
+
if [[ "$ia_outcome" == "success" ]]; then
|
|
1695
|
+
IA_ID="$ia_id" IA_NOW="$ia_now" \
|
|
1696
|
+
_state_mutate '
|
|
1697
|
+
.memory.instincts = [
|
|
1698
|
+
(.memory.instincts // [])[] |
|
|
1699
|
+
if .id == env.IA_ID then
|
|
1700
|
+
.applications = ((.applications // 0) + 1) |
|
|
1701
|
+
.successes = ((.successes // 0) + 1) |
|
|
1702
|
+
.confidence = ([(.confidence + 0.05), 1.0] | min) |
|
|
1703
|
+
.last_applied = env.IA_NOW
|
|
1704
|
+
else
|
|
1705
|
+
.
|
|
1706
|
+
end
|
|
1707
|
+
]
|
|
1708
|
+
' >/dev/null
|
|
1709
|
+
else
|
|
1710
|
+
IA_ID="$ia_id" IA_NOW="$ia_now" \
|
|
1711
|
+
_state_mutate '
|
|
1712
|
+
.memory.instincts = [
|
|
1713
|
+
(.memory.instincts // [])[] |
|
|
1714
|
+
if .id == env.IA_ID then
|
|
1715
|
+
.applications = ((.applications // 0) + 1) |
|
|
1716
|
+
.failures = ((.failures // 0) + 1) |
|
|
1717
|
+
.confidence = ([(.confidence - 0.1), 0.1] | max) |
|
|
1718
|
+
.last_applied = env.IA_NOW
|
|
1719
|
+
else
|
|
1720
|
+
.
|
|
1721
|
+
end
|
|
1722
|
+
]
|
|
1723
|
+
' >/dev/null
|
|
1724
|
+
fi
|
|
1725
|
+
|
|
1726
|
+
# Extract updated values for response via facade
|
|
1727
|
+
ia_new_apps=$(_state_read_field "$(printf '[(.memory.instincts // [])[] | select(.id == "%s")] | first | .applications' "$ia_id")")
|
|
1728
|
+
ia_new_conf=$(_state_read_field "$(printf '[(.memory.instincts // [])[] | select(.id == "%s")] | first | .confidence' "$ia_id")")
|
|
1729
|
+
|
|
1730
|
+
json_ok "$(jq -n --arg iid "$ia_id" --argjson apps "$ia_new_apps" --argjson conf "$ia_new_conf" '{applied: true, instinct_id: $iid, applications: $apps, new_confidence: $conf}')"
|
|
1731
|
+
exit 0
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
# ============================================================================
|
|
1735
|
+
# _learning_extract_fallback
|
|
1736
|
+
# Deterministic fallback for learning extraction when builders skip learning output.
|
|
1737
|
+
# Produces structured learning objects from git diff and feeds them through instinct-create.
|
|
1738
|
+
# Usage: learning-extract-fallback (no args -- reads git state and colony state)
|
|
1739
|
+
# Returns: JSON {"learnings": [...], "count": N}
|
|
1740
|
+
# Note: Uses jq for grouping/sorting (bash 3.2 compatible -- no associative arrays)
|
|
1741
|
+
# ============================================================================
|
|
1742
|
+
_learning_extract_fallback() {
|
|
1743
|
+
# Pre-flight: verify git history exists
|
|
1744
|
+
if ! git rev-parse HEAD~1 >/dev/null 2>&1; then
|
|
1745
|
+
json_ok '{"learnings":[],"count":0}'
|
|
1746
|
+
exit 0
|
|
1747
|
+
fi
|
|
1748
|
+
|
|
1749
|
+
# Pre-flight: verify colony state exists
|
|
1750
|
+
if [[ ! -f "$DATA_DIR/COLONY_STATE.json" ]]; then
|
|
1751
|
+
json_ok '{"learnings":[],"count":0}'
|
|
1752
|
+
exit 0
|
|
1753
|
+
fi
|
|
1754
|
+
|
|
1755
|
+
# Read git diff data
|
|
1756
|
+
lef_stat=$(git diff --stat HEAD~1 2>/dev/null || echo "")
|
|
1757
|
+
lef_files=$(git diff --name-only HEAD~1 2>/dev/null || echo "")
|
|
1758
|
+
|
|
1759
|
+
# Guard: no changes
|
|
1760
|
+
if [[ -z "$lef_files" ]]; then
|
|
1761
|
+
json_ok '{"learnings":[],"count":0}'
|
|
1762
|
+
exit 0
|
|
1763
|
+
fi
|
|
1764
|
+
|
|
1765
|
+
# Read last-build-claims.json if it exists for task context
|
|
1766
|
+
lef_claims=""
|
|
1767
|
+
if [[ -f "$COLONY_DATA_DIR/last-build-claims.json" ]]; then
|
|
1768
|
+
lef_claims=$(cat "$COLONY_DATA_DIR/last-build-claims.json" 2>/dev/null || echo "")
|
|
1769
|
+
fi
|
|
1770
|
+
|
|
1771
|
+
# Parse stat data into JSON: extract file path, insertions, deletions per line
|
|
1772
|
+
# git diff --stat format: " path/to/file | 42 +++++---" or " path/to/file | 10"
|
|
1773
|
+
# Also include the full file list for files with 0 insertions (binary/new files)
|
|
1774
|
+
lef_files_json=$(printf '%s\n' "$lef_stat" | awk '
|
|
1775
|
+
{
|
|
1776
|
+
# Skip empty lines
|
|
1777
|
+
if ($0 == "") next
|
|
1778
|
+
# Skip the summary line (e.g., "13 files changed, 693 insertions(+), 128 deletions(-)")
|
|
1779
|
+
if ($0 ~ /files? changed/) next
|
|
1780
|
+
# Split on " | " to get file path and stat
|
|
1781
|
+
idx = index($0, " | ")
|
|
1782
|
+
if (idx > 0) {
|
|
1783
|
+
fpath = substr($0, 1, idx - 1)
|
|
1784
|
+
rest = substr($0, idx + 3)
|
|
1785
|
+
# Extract first two numbers from rest (insertions, deletions)
|
|
1786
|
+
gsub(/[^0-9 ]/, "", rest)
|
|
1787
|
+
n = split(rest, nums, " ")
|
|
1788
|
+
ins = (nums[1] != "" ? nums[1] : 0)
|
|
1789
|
+
del = (nums[2] != "" ? nums[2] : 0)
|
|
1790
|
+
} else {
|
|
1791
|
+
fpath = $0
|
|
1792
|
+
ins = 0
|
|
1793
|
+
del = 0
|
|
1794
|
+
}
|
|
1795
|
+
# Trim whitespace from path (git stat pads with trailing spaces)
|
|
1796
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", fpath)
|
|
1797
|
+
if (fpath != "") printf "{\"path\":\"%s\",\"ins\":%d,\"del\":%d}\n", fpath, ins, del
|
|
1798
|
+
}' | jq -sc '.')
|
|
1799
|
+
|
|
1800
|
+
# Filter noise files and categorize using jq
|
|
1801
|
+
lef_filtered=$(echo "$lef_files_json" | jq '[.[] |
|
|
1802
|
+
select((.path | startswith(".aether/data/")) | not) |
|
|
1803
|
+
select((.path | startswith(".aether/dreams/")) | not) |
|
|
1804
|
+
select(.path != "package-lock.json") |
|
|
1805
|
+
select((.path | startswith("node_modules/")) | not) |
|
|
1806
|
+
# Categorize (use extra parens around `or` expressions for jq parser)
|
|
1807
|
+
.category = (
|
|
1808
|
+
if ((.path | test("^(tests|test)/")) or (.path | test("_test\\.")) or (.path | test("\\.test\\.")) or (.path | test("\\.spec\\.")))
|
|
1809
|
+
then "testing"
|
|
1810
|
+
elif ((.path | startswith("src/")) or (.path | startswith("lib/")))
|
|
1811
|
+
then "source"
|
|
1812
|
+
elif ((.path | startswith("docs/")) or (.path | endswith(".md")))
|
|
1813
|
+
then "documentation"
|
|
1814
|
+
elif (.path | test("\\.(json|yaml|yml|toml|env|conf|config)$"))
|
|
1815
|
+
then "configuration"
|
|
1816
|
+
else "source"
|
|
1817
|
+
end
|
|
1818
|
+
) |
|
|
1819
|
+
# Calculate absolute net change
|
|
1820
|
+
.abs_net = ((.ins - .del) | if . < 0 then -. else . end) |
|
|
1821
|
+
# Determine if test file
|
|
1822
|
+
.is_test = (.category == "testing") |
|
|
1823
|
+
# Skip trivial non-test changes (abs_net < 3)
|
|
1824
|
+
select((.is_test == true) or (.abs_net >= 3)) |
|
|
1825
|
+
# Extract directory
|
|
1826
|
+
.dir = (.path | split("/") | .[0:-1] | join("/"))
|
|
1827
|
+
]')
|
|
1828
|
+
|
|
1829
|
+
# Guard: no significant changes after filtering
|
|
1830
|
+
lef_sig_count=$(echo "$lef_filtered" | jq 'length')
|
|
1831
|
+
if [[ "$lef_sig_count" -eq 0 ]]; then
|
|
1832
|
+
json_ok '{"learnings":[],"count":0}'
|
|
1833
|
+
exit 0
|
|
1834
|
+
fi
|
|
1835
|
+
|
|
1836
|
+
# Group by category, sort by total change magnitude, cap at 5
|
|
1837
|
+
# Using jq for grouping (bash 3.2 compatible)
|
|
1838
|
+
lef_categories=$(echo "$lef_filtered" | jq -r '
|
|
1839
|
+
group_by(.category) |
|
|
1840
|
+
map({
|
|
1841
|
+
category: .[0].category,
|
|
1842
|
+
file_count: length,
|
|
1843
|
+
total_ins: (map(.ins) | add),
|
|
1844
|
+
total_del: (map(.del) | add),
|
|
1845
|
+
total_change: ((map(.ins) | add) + (map(.del) | add)),
|
|
1846
|
+
dir: (map(.dir) | group_by(.) | map({d: .[0], c: length}) | sort_by(-.c) | .[0].d)
|
|
1847
|
+
}) |
|
|
1848
|
+
sort_by(-.total_change) |
|
|
1849
|
+
.[:5]
|
|
1850
|
+
')
|
|
1851
|
+
|
|
1852
|
+
# Get current phase for source tag
|
|
1853
|
+
lef_phase=$(_state_read_field '.current_phase // 0')
|
|
1854
|
+
lef_phase=${lef_phase:-0}
|
|
1855
|
+
|
|
1856
|
+
# Generate learning objects
|
|
1857
|
+
lef_learnings="[]"
|
|
1858
|
+
lef_count=0
|
|
1859
|
+
|
|
1860
|
+
lef_cat_count=$(echo "$lef_categories" | jq 'length')
|
|
1861
|
+
for ((i=0; i<lef_cat_count && i<5; i++)); do
|
|
1862
|
+
lef_cat_data=$(echo "$lef_categories" | jq ".[$i]")
|
|
1863
|
+
lef_cat=$(echo "$lef_cat_data" | jq -r '.category')
|
|
1864
|
+
lef_fcount=$(echo "$lef_cat_data" | jq -r '.file_count')
|
|
1865
|
+
lef_fins=$(echo "$lef_cat_data" | jq -r '.total_ins')
|
|
1866
|
+
lef_fdel=$(echo "$lef_cat_data" | jq -r '.total_del')
|
|
1867
|
+
lef_fdir=$(echo "$lef_cat_data" | jq -r '.dir')
|
|
1868
|
+
|
|
1869
|
+
# Build fact string
|
|
1870
|
+
lef_fact="Modified $lef_fcount files in $lef_fdir ($lef_fins+/$lef_fdel- lines)"
|
|
1871
|
+
|
|
1872
|
+
# Build interpretation based on category
|
|
1873
|
+
case "$lef_cat" in
|
|
1874
|
+
testing)
|
|
1875
|
+
if [[ "$lef_fdel" == "0" ]]; then
|
|
1876
|
+
lef_interp="Added tests, improving test coverage"
|
|
1877
|
+
else
|
|
1878
|
+
lef_interp="Modified tests, likely improving test coverage"
|
|
1879
|
+
fi
|
|
1880
|
+
;;
|
|
1881
|
+
source)
|
|
1882
|
+
if [[ "$lef_fdel" -gt "$lef_fins" ]]; then
|
|
1883
|
+
lef_interp="Refactored or simplified $lef_fdir module code"
|
|
1884
|
+
else
|
|
1885
|
+
lef_interp="Extended or modified $lef_fdir module code"
|
|
1886
|
+
fi
|
|
1887
|
+
;;
|
|
1888
|
+
configuration)
|
|
1889
|
+
lef_interp="Updated project configuration"
|
|
1890
|
+
;;
|
|
1891
|
+
documentation)
|
|
1892
|
+
lef_interp="Updated project documentation"
|
|
1893
|
+
;;
|
|
1894
|
+
*)
|
|
1895
|
+
lef_interp="Modified $lef_fdir files"
|
|
1896
|
+
;;
|
|
1897
|
+
esac
|
|
1898
|
+
|
|
1899
|
+
# Build trigger and action for instinct-create
|
|
1900
|
+
lef_trigger="when working on $lef_cat"
|
|
1901
|
+
lef_action="$lef_interp"
|
|
1902
|
+
|
|
1903
|
+
# Escape strings for JSON
|
|
1904
|
+
lef_fact_json=$(echo "$lef_fact" | jq -Rs '.')
|
|
1905
|
+
lef_interp_json=$(echo "$lef_interp" | jq -Rs '.')
|
|
1906
|
+
|
|
1907
|
+
# Append to learnings array
|
|
1908
|
+
lef_learnings=$(echo "$lef_learnings" | jq --arg trigger "$lef_trigger" \
|
|
1909
|
+
--arg action "$lef_action" \
|
|
1910
|
+
--argjson fact "$lef_fact_json" \
|
|
1911
|
+
--argjson interp "$lef_interp_json" \
|
|
1912
|
+
'. += [{trigger: $trigger, action: $action, fact: $fact, interpretation: $interp}]')
|
|
1913
|
+
|
|
1914
|
+
# Feed through instinct-create (non-blocking -- failures are logged but don't stop extraction)
|
|
1915
|
+
bash "$0" instinct-create \
|
|
1916
|
+
--trigger "$lef_trigger" \
|
|
1917
|
+
--action "$lef_action" \
|
|
1918
|
+
--confidence 0.5 \
|
|
1919
|
+
--domain "$lef_cat" \
|
|
1920
|
+
--source "fallback-phase-$lef_phase" \
|
|
1921
|
+
--evidence "$lef_fact" >/dev/null 2>&1 || _aether_log_error "Fallback instinct-create failed for $lef_cat"
|
|
1922
|
+
|
|
1923
|
+
lef_count=$((lef_count + 1))
|
|
1924
|
+
done
|
|
1925
|
+
|
|
1926
|
+
json_ok "{\"learnings\":$lef_learnings,\"count\":$lef_count}"
|
|
1927
|
+
exit 0
|
|
1928
|
+
}
|