aether-colony 3.1.17 → 5.1.0
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 +5354 -0
- 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 +438 -0
- package/.aether/commands/continue.yaml +1484 -0
- package/.aether/commands/council.yaml +304 -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 +469 -0
- package/.aether/commands/insert-phase.yaml +98 -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 +1313 -0
- package/.aether/commands/preferences.yaml +63 -0
- package/.aether/commands/redirect.yaml +123 -0
- package/.aether/commands/resume-colony.yaml +373 -0
- package/.aether/commands/resume.yaml +398 -0
- package/.aether/commands/run.yaml +193 -0
- package/.aether/commands/seal.yaml +1205 -0
- package/.aether/commands/skill-create.yaml +337 -0
- package/.aether/commands/status.yaml +364 -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 +211 -0
- package/.aether/docs/README.md +98 -0
- package/.aether/docs/caste-system.md +48 -0
- 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 +1682 -0
- package/.aether/docs/command-playbooks/build-prep.md +283 -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 +1724 -0
- package/.aether/docs/command-playbooks/continue-gates.md +686 -0
- package/.aether/docs/command-playbooks/continue-verify.md +406 -0
- package/.aether/docs/context-continuity.md +84 -0
- package/{runtime → .aether/docs/disciplines}/DISCIPLINES.md +13 -11
- package/.aether/docs/error-codes.md +268 -0
- package/.aether/docs/known-issues.md +94 -0
- package/{runtime → .aether}/docs/pheromones.md +86 -6
- package/.aether/docs/plans/pheromone-display-plan.md +257 -0
- package/.aether/docs/queen-commands.md +98 -0
- package/.aether/docs/source-of-truth-map.md +132 -0
- package/.aether/docs/xml-utilities.md +47 -0
- package/{runtime → .aether}/exchange/pheromone-xml.sh +2 -1
- package/{runtime → .aether}/exchange/registry-xml.sh +7 -3
- package/{runtime → .aether}/exchange/wisdom-xml.sh +11 -4
- package/.aether/rules/aether-colony.md +144 -0
- package/.aether/schemas/example-prompt-builder.xml +234 -0
- 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 +61 -0
- package/.aether/templates/colony-state-reset.jq.template +23 -0
- package/.aether/templates/colony-state.template.json +39 -0
- package/.aether/templates/constraints.template.json +9 -0
- package/.aether/templates/crowned-anthill.template.md +36 -0
- package/.aether/templates/handoff-build-error.template.md +30 -0
- package/.aether/templates/handoff-build-success.template.md +39 -0
- package/.aether/templates/handoff.template.md +40 -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/{runtime → .aether}/utils/atomic-write.sh +68 -22
- package/{runtime → .aether}/utils/chamber-compare.sh +23 -10
- package/.aether/utils/chamber-utils.sh +440 -0
- package/.aether/utils/emoji-audit.sh +166 -0
- package/{runtime → .aether}/utils/error-handler.sh +34 -8
- package/.aether/utils/file-lock.sh +313 -0
- package/.aether/utils/flag.sh +267 -0
- package/.aether/utils/hive.sh +572 -0
- package/.aether/utils/learning.sh +1928 -0
- package/.aether/utils/midden.sh +342 -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 +1698 -0
- package/.aether/utils/scan.sh +860 -0
- package/.aether/utils/semantic-cli.sh +415 -0
- package/.aether/utils/session.sh +552 -0
- package/.aether/utils/skills.sh +509 -0
- package/.aether/utils/spawn-tree.sh +260 -0
- package/.aether/utils/spawn.sh +260 -0
- package/.aether/utils/state-api.sh +199 -0
- package/{runtime → .aether}/utils/state-loader.sh +8 -6
- package/.aether/utils/suggest.sh +611 -0
- package/{runtime → .aether}/utils/swarm-display.sh +10 -1
- package/.aether/utils/swarm.sh +1004 -0
- package/{runtime → .aether}/utils/watch-spawn-tree.sh +11 -2
- package/{runtime → .aether}/utils/xml-compose.sh +9 -3
- package/.aether/utils/xml-convert.sh +277 -0
- package/{runtime → .aether}/utils/xml-core.sh +5 -9
- package/.aether/utils/xml-query.sh +201 -0
- package/.aether/utils/xml-utils.sh +110 -0
- package/{runtime → .aether}/workers.md +97 -81
- package/.claude/agents/ant/aether-ambassador.md +265 -0
- package/.claude/agents/ant/aether-archaeologist.md +327 -0
- package/.claude/agents/ant/aether-architect.md +236 -0
- package/.claude/agents/ant/aether-auditor.md +271 -0
- package/.claude/agents/ant/aether-builder.md +224 -0
- package/.claude/agents/ant/aether-chaos.md +269 -0
- package/.claude/agents/ant/aether-chronicler.md +305 -0
- package/.claude/agents/ant/aether-gatekeeper.md +330 -0
- package/.claude/agents/ant/aether-includer.md +374 -0
- package/.claude/agents/ant/aether-keeper.md +272 -0
- package/.claude/agents/ant/aether-measurer.md +322 -0
- package/.claude/agents/ant/aether-oracle.md +237 -0
- package/.claude/agents/ant/aether-probe.md +211 -0
- package/.claude/agents/ant/aether-queen.md +330 -0
- package/.claude/agents/ant/aether-route-setter.md +178 -0
- package/.claude/agents/ant/aether-sage.md +418 -0
- package/.claude/agents/ant/aether-scout.md +179 -0
- package/.claude/agents/ant/aether-surveyor-disciplines.md +417 -0
- package/.claude/agents/ant/aether-surveyor-nest.md +355 -0
- package/.claude/agents/ant/aether-surveyor-pathogens.md +289 -0
- package/.claude/agents/ant/aether-surveyor-provisions.md +360 -0
- package/.claude/agents/ant/aether-tracker.md +270 -0
- package/.claude/agents/ant/aether-watcher.md +280 -0
- package/.claude/agents/ant/aether-weaver.md +248 -0
- package/.claude/commands/ant/archaeology.md +16 -14
- package/.claude/commands/ant/build.md +43 -1028
- package/.claude/commands/ant/chaos.md +19 -23
- package/.claude/commands/ant/colonize.md +52 -31
- package/.claude/commands/ant/continue.md +40 -1016
- package/.claude/commands/ant/council.md +21 -18
- package/.claude/commands/ant/data-clean.md +81 -0
- package/.claude/commands/ant/dream.md +27 -15
- package/.claude/commands/ant/entomb.md +317 -225
- package/.claude/commands/ant/export-signals.md +57 -0
- package/.claude/commands/ant/feedback.md +48 -26
- package/.claude/commands/ant/flag.md +30 -10
- package/.claude/commands/ant/flags.md +34 -12
- package/.claude/commands/ant/focus.md +45 -19
- package/.claude/commands/ant/help.md +50 -8
- package/.claude/commands/ant/history.md +13 -0
- package/.claude/commands/ant/import-signals.md +71 -0
- package/.claude/commands/ant/init.md +345 -194
- package/.claude/commands/ant/insert-phase.md +101 -0
- package/.claude/commands/ant/interpret.md +26 -4
- package/.claude/commands/ant/lay-eggs.md +184 -127
- package/.claude/commands/ant/maturity.md +32 -11
- package/.claude/commands/ant/memory-details.md +77 -0
- package/.claude/commands/ant/migrate-state.md +20 -2
- package/.claude/commands/ant/oracle.md +337 -74
- package/.claude/commands/ant/organize.md +39 -25
- package/.claude/commands/ant/patrol.md +620 -0
- package/.claude/commands/ant/pause-colony.md +23 -27
- package/.claude/commands/ant/phase.md +40 -42
- package/.claude/commands/ant/pheromones.md +156 -0
- package/.claude/commands/ant/plan.md +185 -51
- package/.claude/commands/ant/preferences.md +65 -0
- package/.claude/commands/ant/redirect.md +45 -30
- package/.claude/commands/ant/resume-colony.md +51 -27
- package/.claude/commands/ant/resume.md +314 -94
- package/.claude/commands/ant/run.md +195 -0
- package/.claude/commands/ant/seal.md +650 -221
- package/.claude/commands/ant/skill-create.md +286 -0
- package/.claude/commands/ant/status.md +196 -31
- package/.claude/commands/ant/swarm.md +16 -46
- package/.claude/commands/ant/tunnels.md +280 -105
- package/.claude/commands/ant/update.md +73 -89
- package/.claude/commands/ant/verify-castes.md +100 -42
- package/.claude/commands/ant/watch.md +14 -12
- package/.opencode/agents/aether-ambassador.md +63 -20
- package/.opencode/agents/aether-archaeologist.md +29 -12
- package/.opencode/agents/aether-architect.md +103 -36
- package/.opencode/agents/aether-auditor.md +51 -18
- package/.opencode/agents/aether-builder.md +70 -20
- package/.opencode/agents/aether-chaos.md +29 -12
- package/.opencode/agents/aether-chronicler.md +60 -18
- package/.opencode/agents/aether-gatekeeper.md +27 -18
- package/.opencode/agents/aether-includer.md +27 -18
- package/.opencode/agents/aether-keeper.md +89 -18
- package/.opencode/agents/aether-measurer.md +27 -18
- package/.opencode/agents/aether-oracle.md +137 -0
- package/.opencode/agents/aether-probe.md +60 -18
- package/.opencode/agents/aether-queen.md +172 -24
- package/.opencode/agents/aether-route-setter.md +57 -12
- package/.opencode/agents/aether-sage.md +26 -18
- package/.opencode/agents/aether-scout.md +28 -20
- package/.opencode/agents/aether-surveyor-disciplines.md +59 -2
- package/.opencode/agents/aether-surveyor-nest.md +59 -2
- package/.opencode/agents/aether-surveyor-pathogens.md +57 -2
- package/.opencode/agents/aether-surveyor-provisions.md +59 -2
- package/.opencode/agents/aether-tracker.md +64 -18
- package/.opencode/agents/aether-watcher.md +66 -19
- package/.opencode/agents/aether-weaver.md +61 -18
- package/.opencode/commands/ant/archaeology.md +7 -14
- package/.opencode/commands/ant/build.md +437 -257
- package/.opencode/commands/ant/chaos.md +7 -24
- package/.opencode/commands/ant/colonize.md +8 -17
- package/.opencode/commands/ant/continue.md +661 -142
- package/.opencode/commands/ant/council.md +11 -22
- package/.opencode/commands/ant/data-clean.md +77 -0
- package/.opencode/commands/ant/dream.md +15 -17
- package/.opencode/commands/ant/entomb.md +133 -62
- 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 +374 -167
- package/.opencode/commands/ant/insert-phase.md +107 -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 +18 -16
- 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 +184 -56
- package/.opencode/commands/ant/preferences.md +71 -0
- package/.opencode/commands/ant/redirect.md +22 -5
- package/.opencode/commands/ant/resume-colony.md +38 -27
- package/.opencode/commands/ant/resume.md +385 -0
- package/.opencode/commands/ant/run.md +201 -0
- package/.opencode/commands/ant/seal.md +259 -45
- package/.opencode/commands/ant/skill-create.md +63 -0
- package/.opencode/commands/ant/status.md +135 -31
- package/.opencode/commands/ant/swarm.md +3 -345
- package/.opencode/commands/ant/tunnels.md +152 -9
- package/.opencode/commands/ant/update.md +70 -91
- package/.opencode/commands/ant/verify-castes.md +96 -42
- package/.opencode/commands/ant/watch.md +7 -0
- package/CHANGELOG.md +356 -0
- package/README.md +203 -573
- package/bin/cli.js +455 -527
- package/bin/generate-commands.js +186 -0
- package/bin/generate-commands.sh +127 -88
- package/bin/lib/init.js +13 -3
- package/bin/lib/spawn-logger.js +0 -15
- package/bin/lib/update-transaction.js +392 -140
- package/bin/npx-install.js +178 -0
- package/bin/sync-to-runtime.sh +5 -137
- package/bin/validate-package.sh +166 -0
- package/package.json +14 -7
- package/.opencode/agents/aether-guardian.md +0 -107
- package/.opencode/agents/workers.md +0 -1034
- 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
- package/runtime/CONTEXT.md +0 -160
- package/runtime/QUEEN_ANT_ARCHITECTURE.md +0 -402
- package/runtime/aether-utils.sh +0 -3879
- package/runtime/data/signatures.json +0 -41
- package/runtime/docs/AETHER-2.0-IMPLEMENTATION-PLAN.md +0 -1343
- package/runtime/docs/AETHER-PHEROMONE-SYSTEM-MASTER-SPEC.md +0 -2642
- package/runtime/docs/PHEROMONE-INJECTION.md +0 -240
- package/runtime/docs/PHEROMONE-INTEGRATION.md +0 -192
- package/runtime/docs/PHEROMONE-SYSTEM-DESIGN.md +0 -426
- package/runtime/docs/README.md +0 -94
- package/runtime/docs/VISUAL-OUTPUT-SPEC.md +0 -219
- package/runtime/docs/biological-reference.md +0 -272
- package/runtime/docs/codebase-review.md +0 -399
- package/runtime/docs/command-sync.md +0 -164
- package/runtime/docs/constraints.md +0 -116
- package/runtime/docs/implementation-learnings.md +0 -89
- package/runtime/docs/known-issues.md +0 -217
- package/runtime/docs/namespace.md +0 -148
- package/runtime/docs/pathogen-schema-example.json +0 -36
- package/runtime/docs/pathogen-schema.md +0 -111
- package/runtime/docs/planning-discipline.md +0 -159
- package/runtime/docs/progressive-disclosure.md +0 -184
- package/runtime/lib/queen-utils.sh +0 -729
- package/runtime/model-profiles.yaml +0 -100
- package/runtime/planning.md +0 -159
- package/runtime/recover.sh +0 -136
- package/runtime/templates/QUEEN.md.template +0 -79
- package/runtime/utils/chamber-utils.sh +0 -285
- package/runtime/utils/file-lock.sh +0 -122
- package/runtime/utils/spawn-tree.sh +0 -428
- package/runtime/utils/spawn-with-model.sh +0 -56
- package/runtime/utils/xml-utils.sh +0 -2196
- package/runtime/workers-new-castes.md +0 -516
- /package/{runtime → .aether/docs/disciplines}/coding-standards.md +0 -0
- /package/{runtime → .aether/docs/disciplines}/debugging.md +0 -0
- /package/{runtime → .aether/docs/disciplines}/learning.md +0 -0
- /package/{runtime → .aether/docs/disciplines}/tdd.md +0 -0
- /package/{runtime → .aether/docs/disciplines}/verification-loop.md +0 -0
- /package/{runtime → .aether/docs/disciplines}/verification.md +0 -0
- /package/{runtime → .aether}/schemas/aether-types.xsd +0 -0
- /package/{runtime → .aether}/schemas/colony-registry.xsd +0 -0
- /package/{runtime → .aether}/schemas/pheromone.xsd +0 -0
- /package/{runtime → .aether}/schemas/prompt.xsd +0 -0
- /package/{runtime → .aether}/schemas/queen-wisdom.xsd +0 -0
- /package/{runtime → .aether}/schemas/worker-priming.xsd +0 -0
- /package/{runtime → .aether}/utils/colorize-log.sh +0 -0
- /package/{runtime → .aether}/utils/queen-to-md.xsl +0 -0
|
@@ -13,10 +13,13 @@ E_REPO_NOT_INITIALIZED="E_REPO_NOT_INITIALIZED"
|
|
|
13
13
|
E_FILE_NOT_FOUND="E_FILE_NOT_FOUND"
|
|
14
14
|
E_JSON_INVALID="E_JSON_INVALID"
|
|
15
15
|
E_LOCK_FAILED="E_LOCK_FAILED"
|
|
16
|
+
E_LOCK_STALE="E_LOCK_STALE"
|
|
16
17
|
E_GIT_ERROR="E_GIT_ERROR"
|
|
17
18
|
E_VALIDATION_FAILED="E_VALIDATION_FAILED"
|
|
18
19
|
E_FEATURE_UNAVAILABLE="E_FEATURE_UNAVAILABLE"
|
|
19
20
|
E_BASH_ERROR="E_BASH_ERROR"
|
|
21
|
+
E_DEPENDENCY_MISSING="E_DEPENDENCY_MISSING"
|
|
22
|
+
E_RESOURCE_NOT_FOUND="E_RESOURCE_NOT_FOUND"
|
|
20
23
|
|
|
21
24
|
# --- Recovery Suggestion Functions (internal, prefixed with _) ---
|
|
22
25
|
_recovery_hub_not_found() { echo '"Run: aether install"'; }
|
|
@@ -24,8 +27,11 @@ _recovery_repo_not_init() { echo '"Run /ant:init in this repo first"'; }
|
|
|
24
27
|
_recovery_file_not_found() { echo '"Check file path and permissions"'; }
|
|
25
28
|
_recovery_json_invalid() { echo '"Validate JSON syntax"'; }
|
|
26
29
|
_recovery_lock_failed() { echo '"Wait for other operations to complete"'; }
|
|
30
|
+
_recovery_lock_stale() { echo '"Remove the stale lock file manually or run: aether force-unlock"'; }
|
|
27
31
|
_recovery_git_error() { echo '"Check git status and resolve conflicts"'; }
|
|
28
32
|
_recovery_default() { echo 'null'; }
|
|
33
|
+
_recovery_dependency_missing() { echo '"Install the required dependency"'; }
|
|
34
|
+
_recovery_resource_not_found() { echo '"Check that the resource exists and try again"'; }
|
|
29
35
|
|
|
30
36
|
# Get recovery suggestion based on error code
|
|
31
37
|
_get_recovery() {
|
|
@@ -36,7 +42,10 @@ _get_recovery() {
|
|
|
36
42
|
"$E_FILE_NOT_FOUND") _recovery_file_not_found ;;
|
|
37
43
|
"$E_JSON_INVALID") _recovery_json_invalid ;;
|
|
38
44
|
"$E_LOCK_FAILED") _recovery_lock_failed ;;
|
|
45
|
+
"$E_LOCK_STALE") _recovery_lock_stale ;;
|
|
39
46
|
"$E_GIT_ERROR") _recovery_git_error ;;
|
|
47
|
+
"$E_DEPENDENCY_MISSING") _recovery_dependency_missing ;;
|
|
48
|
+
"$E_RESOURCE_NOT_FOUND") _recovery_resource_not_found ;;
|
|
40
49
|
*) _recovery_default ;;
|
|
41
50
|
esac
|
|
42
51
|
}
|
|
@@ -78,8 +87,8 @@ json_err() {
|
|
|
78
87
|
"$code" "$escaped_message" "$details_json" "$recovery" "$timestamp" >&2
|
|
79
88
|
|
|
80
89
|
# Log to activity.log (best effort)
|
|
81
|
-
if [[ -n "${
|
|
82
|
-
echo "[$timestamp] ERROR $code: $escaped_message" >> "$
|
|
90
|
+
if [[ -n "${COLONY_DATA_DIR:-}" ]]; then
|
|
91
|
+
echo "[$timestamp] ERROR $code: $escaped_message" >> "$COLONY_DATA_DIR/activity.log" 2>/dev/null || true
|
|
83
92
|
fi
|
|
84
93
|
|
|
85
94
|
exit 1
|
|
@@ -102,8 +111,22 @@ json_warn() {
|
|
|
102
111
|
"$code" "$escaped_message" "$timestamp"
|
|
103
112
|
|
|
104
113
|
# Log to activity.log (best effort)
|
|
105
|
-
if [[ -n "${
|
|
106
|
-
echo "[$timestamp] WARN $code: $escaped_message" >> "$
|
|
114
|
+
if [[ -n "${COLONY_DATA_DIR:-}" ]]; then
|
|
115
|
+
echo "[$timestamp] WARN $code: $escaped_message" >> "$COLONY_DATA_DIR/activity.log" 2>/dev/null || true
|
|
116
|
+
fi
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# --- _aether_log_error function for surfaced errors ---
|
|
120
|
+
# Dual output: [error] prefix to stderr (screen) + timestamped entry to errors.log (file)
|
|
121
|
+
# Distinct from: json_err (structured JSON), json_warn (non-fatal JSON), ⚠ (recovery), [trimmed] (budget)
|
|
122
|
+
_aether_log_error() {
|
|
123
|
+
local message="$1"
|
|
124
|
+
local timestamp
|
|
125
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
126
|
+
echo "[error] $message" >&2
|
|
127
|
+
if [[ -n "${COLONY_DATA_DIR:-}" ]]; then
|
|
128
|
+
mkdir -p "$DATA_DIR" 2>/dev/null # SUPPRESS:OK -- idempotent: ensure dir exists
|
|
129
|
+
echo "[$timestamp] $message" >> "$COLONY_DATA_DIR/errors.log" 2>/dev/null # SUPPRESS:OK -- cleanup: log write is best-effort
|
|
107
130
|
fi
|
|
108
131
|
}
|
|
109
132
|
|
|
@@ -130,8 +153,8 @@ error_handler() {
|
|
|
130
153
|
"$E_BASH_ERROR" "$details" "$(_recovery_default)" "$timestamp" >&2
|
|
131
154
|
|
|
132
155
|
# Log to activity.log (best effort)
|
|
133
|
-
if [[ -n "${
|
|
134
|
-
echo "[$timestamp] ERROR $E_BASH_ERROR: Command failed at line $line_num (exit $exit_code)" >> "$
|
|
156
|
+
if [[ -n "${COLONY_DATA_DIR:-}" ]]; then
|
|
157
|
+
echo "[$timestamp] ERROR $E_BASH_ERROR: Command failed at line $line_num (exit $exit_code)" >> "$COLONY_DATA_DIR/activity.log" 2>/dev/null || true
|
|
135
158
|
fi
|
|
136
159
|
|
|
137
160
|
exit 1
|
|
@@ -189,12 +212,15 @@ feature_log_degradation() {
|
|
|
189
212
|
}
|
|
190
213
|
|
|
191
214
|
# --- Export all functions and variables ---
|
|
192
|
-
export -f json_err json_warn error_handler
|
|
215
|
+
export -f json_err json_warn _aether_log_error error_handler
|
|
193
216
|
export -f feature_enable feature_disable feature_enabled feature_log_degradation
|
|
194
217
|
export -f _get_recovery _recovery_hub_not_found _recovery_repo_not_init
|
|
195
218
|
export -f _recovery_file_not_found _recovery_json_invalid _recovery_lock_failed
|
|
219
|
+
export -f _recovery_lock_stale
|
|
196
220
|
export -f _recovery_git_error _recovery_default _feature_reason
|
|
221
|
+
export -f _recovery_dependency_missing _recovery_resource_not_found
|
|
197
222
|
export E_UNKNOWN E_HUB_NOT_FOUND E_REPO_NOT_INITIALIZED E_FILE_NOT_FOUND
|
|
198
|
-
export E_JSON_INVALID E_LOCK_FAILED E_GIT_ERROR E_VALIDATION_FAILED
|
|
223
|
+
export E_JSON_INVALID E_LOCK_FAILED E_LOCK_STALE E_GIT_ERROR E_VALIDATION_FAILED
|
|
199
224
|
export E_FEATURE_UNAVAILABLE E_BASH_ERROR
|
|
225
|
+
export E_DEPENDENCY_MISSING E_RESOURCE_NOT_FOUND
|
|
200
226
|
export _FEATURES_DISABLED
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Aether File Lock Utility
|
|
3
|
+
# Implements file locking for concurrent colony access prevention
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# source .aether/utils/file-lock.sh
|
|
7
|
+
# acquire_lock /path/to/file.lock
|
|
8
|
+
# # ... critical section ...
|
|
9
|
+
# release_lock
|
|
10
|
+
|
|
11
|
+
# Aether root detection - respect existing AETHER_ROOT, or use git root, or use current directory
|
|
12
|
+
if [[ -z "${AETHER_ROOT:-}" ]]; then
|
|
13
|
+
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
14
|
+
AETHER_ROOT="$(git rev-parse --show-toplevel)"
|
|
15
|
+
else
|
|
16
|
+
AETHER_ROOT="$(pwd)"
|
|
17
|
+
fi
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
LOCK_DIR="$AETHER_ROOT/.aether/locks"
|
|
21
|
+
LOCK_TIMEOUT=300 # 5 minutes max lock time
|
|
22
|
+
LOCK_RETRY_INTERVAL=0.5 # Wait 500ms between retries
|
|
23
|
+
LOCK_MAX_RETRIES=100 # Total 50 seconds max wait
|
|
24
|
+
LOCK_AT_FILE="" # Tracks lock file path for acquire_lock_at/release_lock_at
|
|
25
|
+
|
|
26
|
+
# Fallback constant — ensures E_LOCK_STALE is defined whether or not error-handler.sh was loaded
|
|
27
|
+
: "${E_LOCK_STALE:=E_LOCK_STALE}"
|
|
28
|
+
|
|
29
|
+
# Create lock directory if it doesn't exist
|
|
30
|
+
mkdir -p "$LOCK_DIR"
|
|
31
|
+
|
|
32
|
+
# Acquire a file lock using noclobber
|
|
33
|
+
# Arguments: file_path (the resource to lock)
|
|
34
|
+
# Returns: 0 on success, 1 on failure
|
|
35
|
+
# Globals: LOCK_ACQUIRED (set to true when lock acquired), CURRENT_LOCK (set to lock file path)
|
|
36
|
+
# Behavior:
|
|
37
|
+
# - In non-interactive mode, stale locks are auto-cleaned by default.
|
|
38
|
+
# - Override with AETHER_STALE_LOCK_MODE=error|prompt|auto.
|
|
39
|
+
acquire_lock() {
|
|
40
|
+
local file_path="$1"
|
|
41
|
+
local lock_file="${LOCK_DIR}/$(basename "$file_path").lock"
|
|
42
|
+
local lock_pid_file="${lock_file}.pid"
|
|
43
|
+
local stale_mode="${AETHER_STALE_LOCK_MODE:-}"
|
|
44
|
+
|
|
45
|
+
if [[ -z "$stale_mode" ]]; then
|
|
46
|
+
if [[ -t 2 ]]; then
|
|
47
|
+
stale_mode="prompt"
|
|
48
|
+
else
|
|
49
|
+
stale_mode="auto"
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Check if lock file exists and is stale
|
|
54
|
+
if [ -f "$lock_file" ]; then
|
|
55
|
+
local lock_pid
|
|
56
|
+
lock_pid=$(cat "$lock_pid_file" 2>/dev/null || echo "")
|
|
57
|
+
if [[ -z "$lock_pid" ]]; then
|
|
58
|
+
# Fallback to lock file payload if .pid sidecar is missing/corrupt.
|
|
59
|
+
lock_pid=$(cat "$lock_file" 2>/dev/null || echo "")
|
|
60
|
+
fi
|
|
61
|
+
lock_pid=$(echo "$lock_pid" | tr -d '[:space:]')
|
|
62
|
+
[[ "$lock_pid" =~ ^[0-9]+$ ]] || lock_pid=""
|
|
63
|
+
|
|
64
|
+
local is_stale=false
|
|
65
|
+
|
|
66
|
+
# Compute lock age for timeout-based stale checks when PID is unavailable.
|
|
67
|
+
local lock_mtime=0
|
|
68
|
+
# Platform-portable mtime: macOS uses stat -f %m, Linux uses stat -c %Y
|
|
69
|
+
if stat -f %m "$lock_file" >/dev/null 2>&1; then
|
|
70
|
+
lock_mtime=$(stat -f %m "$lock_file" 2>/dev/null || echo 0)
|
|
71
|
+
else
|
|
72
|
+
lock_mtime=$(stat -c %Y "$lock_file" 2>/dev/null || echo 0)
|
|
73
|
+
fi
|
|
74
|
+
local lock_age=$(( $(date +%s) - lock_mtime ))
|
|
75
|
+
|
|
76
|
+
# Mark stale only when we can do so safely:
|
|
77
|
+
# - PID is known and not running
|
|
78
|
+
# - No PID could be determined and lock exceeded timeout
|
|
79
|
+
if [[ -n "$lock_pid" ]] && ! kill -0 "$lock_pid" 2>/dev/null; then
|
|
80
|
+
is_stale=true
|
|
81
|
+
elif [[ -z "$lock_pid" ]] && [[ $lock_age -gt $LOCK_TIMEOUT ]]; then
|
|
82
|
+
is_stale=true
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
if [[ "$is_stale" == "true" ]]; then
|
|
86
|
+
case "$stale_mode" in
|
|
87
|
+
auto)
|
|
88
|
+
rm -f "$lock_file" "$lock_pid_file"
|
|
89
|
+
# Track stale lock cleanup in safety stats (best-effort)
|
|
90
|
+
type _safety_stats_increment &>/dev/null && _safety_stats_increment "stale_locks_cleaned" 2>/dev/null || true
|
|
91
|
+
;;
|
|
92
|
+
prompt)
|
|
93
|
+
if [[ -t 2 ]]; then
|
|
94
|
+
echo "" >&2
|
|
95
|
+
echo "Warning: stale lock detected (PID ${lock_pid:-unknown} not running, age ${lock_age}s)" >&2
|
|
96
|
+
echo "Lock file: $lock_file" >&2
|
|
97
|
+
printf "Remove stale lock and continue? [y/N] " >&2
|
|
98
|
+
local response
|
|
99
|
+
read -r response < /dev/tty
|
|
100
|
+
if [[ "$response" =~ ^[Yy]$ ]]; then
|
|
101
|
+
rm -f "$lock_file" "$lock_pid_file"
|
|
102
|
+
type _safety_stats_increment &>/dev/null && _safety_stats_increment "stale_locks_cleaned" 2>/dev/null || true
|
|
103
|
+
else
|
|
104
|
+
echo "Lock removal declined. Remove manually: rm $lock_file" >&2
|
|
105
|
+
return 1
|
|
106
|
+
fi
|
|
107
|
+
else
|
|
108
|
+
printf '{"ok":false,"error":{"code":"%s","message":"Stale lock found. Remove manually: %s"}}\n' "$E_LOCK_STALE" "$lock_file" >&2
|
|
109
|
+
return 1
|
|
110
|
+
fi
|
|
111
|
+
;;
|
|
112
|
+
error|*)
|
|
113
|
+
printf '{"ok":false,"error":{"code":"%s","message":"Stale lock found. Remove manually: %s"}}\n' "$E_LOCK_STALE" "$lock_file" >&2
|
|
114
|
+
return 1
|
|
115
|
+
;;
|
|
116
|
+
esac
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Try to acquire lock with timeout
|
|
121
|
+
local retry_count=0
|
|
122
|
+
while [ $retry_count -lt $LOCK_MAX_RETRIES ]; do
|
|
123
|
+
# Try to create lock file atomically
|
|
124
|
+
if (set -o noclobber; echo $$ > "$lock_file") 2>/dev/null; then
|
|
125
|
+
echo $$ > "$lock_pid_file" 2>/dev/null || true
|
|
126
|
+
export LOCK_ACQUIRED=true
|
|
127
|
+
export CURRENT_LOCK="$lock_file"
|
|
128
|
+
return 0
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
retry_count=$((retry_count + 1))
|
|
132
|
+
if [ $retry_count -lt $LOCK_MAX_RETRIES ]; then
|
|
133
|
+
sleep $LOCK_RETRY_INTERVAL
|
|
134
|
+
fi
|
|
135
|
+
done
|
|
136
|
+
|
|
137
|
+
echo "Failed to acquire lock for $file_path after $LOCK_MAX_RETRIES attempts" >&2
|
|
138
|
+
return 1
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Release a file lock
|
|
142
|
+
# Arguments: None (uses CURRENT_LOCK global set by acquire_lock)
|
|
143
|
+
release_lock() {
|
|
144
|
+
if [ "$LOCK_ACQUIRED" = "true" ] && [ -n "$CURRENT_LOCK" ]; then
|
|
145
|
+
rm -f "$CURRENT_LOCK" "${CURRENT_LOCK}.pid"
|
|
146
|
+
export LOCK_ACQUIRED=false
|
|
147
|
+
export CURRENT_LOCK=""
|
|
148
|
+
return 0
|
|
149
|
+
fi
|
|
150
|
+
return 1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# Acquire a lock in a specified directory with optional colony tag
|
|
154
|
+
# Unlike acquire_lock (which uses the global LOCK_DIR), this function takes an
|
|
155
|
+
# explicit lock directory parameter — no global state mutation required.
|
|
156
|
+
# Arguments:
|
|
157
|
+
# $1 = file_path (resource to lock)
|
|
158
|
+
# $2 = lock_dir (directory to place lock file in)
|
|
159
|
+
# $3 = colony_tag (optional, included in lock filename for debuggability)
|
|
160
|
+
# Returns: 0 on success, 1 on failure
|
|
161
|
+
# Sets: LOCK_AT_FILE (path to the acquired lock file for release)
|
|
162
|
+
acquire_lock_at() {
|
|
163
|
+
local file_path="$1"
|
|
164
|
+
local lock_dir="$2"
|
|
165
|
+
local colony_tag="${3:-}"
|
|
166
|
+
|
|
167
|
+
local lock_name
|
|
168
|
+
if [[ -n "$colony_tag" ]]; then
|
|
169
|
+
lock_name="$(basename "$file_path").${colony_tag}.lock"
|
|
170
|
+
else
|
|
171
|
+
lock_name="$(basename "$file_path").lock"
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
local lock_file="${lock_dir}/${lock_name}"
|
|
175
|
+
local lock_pid_file="${lock_file}.pid"
|
|
176
|
+
mkdir -p "$lock_dir"
|
|
177
|
+
|
|
178
|
+
local stale_mode="${AETHER_STALE_LOCK_MODE:-}"
|
|
179
|
+
if [[ -z "$stale_mode" ]]; then
|
|
180
|
+
if [[ -t 2 ]]; then stale_mode="prompt"; else stale_mode="auto"; fi
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# Stale lock detection (reuses same logic as acquire_lock)
|
|
184
|
+
if [[ -f "$lock_file" ]]; then
|
|
185
|
+
local lock_pid
|
|
186
|
+
lock_pid=$(cat "$lock_pid_file" 2>/dev/null || echo "")
|
|
187
|
+
[[ -z "$lock_pid" ]] && lock_pid=$(cat "$lock_file" 2>/dev/null || echo "")
|
|
188
|
+
lock_pid=$(echo "$lock_pid" | tr -d '[:space:]')
|
|
189
|
+
[[ "$lock_pid" =~ ^[0-9]+$ ]] || lock_pid=""
|
|
190
|
+
|
|
191
|
+
local is_stale=false
|
|
192
|
+
local lock_mtime=0
|
|
193
|
+
if stat -f %m "$lock_file" >/dev/null 2>&1; then
|
|
194
|
+
lock_mtime=$(stat -f %m "$lock_file" 2>/dev/null || echo 0)
|
|
195
|
+
else
|
|
196
|
+
lock_mtime=$(stat -c %Y "$lock_file" 2>/dev/null || echo 0)
|
|
197
|
+
fi
|
|
198
|
+
local lock_age=$(( $(date +%s) - lock_mtime ))
|
|
199
|
+
|
|
200
|
+
if [[ -n "$lock_pid" ]] && ! kill -0 "$lock_pid" 2>/dev/null; then
|
|
201
|
+
is_stale=true
|
|
202
|
+
elif [[ -z "$lock_pid" ]] && [[ $lock_age -gt $LOCK_TIMEOUT ]]; then
|
|
203
|
+
is_stale=true
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
if [[ "$is_stale" == "true" ]]; then
|
|
207
|
+
case "$stale_mode" in
|
|
208
|
+
auto)
|
|
209
|
+
rm -f "$lock_file" "$lock_pid_file"
|
|
210
|
+
type _safety_stats_increment &>/dev/null && _safety_stats_increment "stale_locks_cleaned" 2>/dev/null || true
|
|
211
|
+
;;
|
|
212
|
+
prompt)
|
|
213
|
+
if [[ -t 2 ]]; then
|
|
214
|
+
echo "" >&2
|
|
215
|
+
echo "Warning: stale lock detected (PID ${lock_pid:-unknown} not running, age ${lock_age}s)" >&2
|
|
216
|
+
echo "Lock file: $lock_file" >&2
|
|
217
|
+
printf "Remove stale lock and continue? [y/N] " >&2
|
|
218
|
+
local response
|
|
219
|
+
read -r response < /dev/tty
|
|
220
|
+
if [[ "$response" =~ ^[Yy]$ ]]; then
|
|
221
|
+
rm -f "$lock_file" "$lock_pid_file"
|
|
222
|
+
type _safety_stats_increment &>/dev/null && _safety_stats_increment "stale_locks_cleaned" 2>/dev/null || true
|
|
223
|
+
else
|
|
224
|
+
echo "Lock removal declined. Remove manually: rm $lock_file" >&2
|
|
225
|
+
return 1
|
|
226
|
+
fi
|
|
227
|
+
else
|
|
228
|
+
printf '{"ok":false,"error":{"code":"%s","message":"Stale lock found. Remove manually: %s"}}\n' "$E_LOCK_STALE" "$lock_file" >&2
|
|
229
|
+
return 1
|
|
230
|
+
fi
|
|
231
|
+
;;
|
|
232
|
+
error|*)
|
|
233
|
+
printf '{"ok":false,"error":{"code":"%s","message":"Stale lock found. Remove manually: %s"}}\n' "$E_LOCK_STALE" "$lock_file" >&2
|
|
234
|
+
return 1
|
|
235
|
+
;;
|
|
236
|
+
esac
|
|
237
|
+
fi
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
# Retry loop (same as acquire_lock)
|
|
241
|
+
local retry_count=0
|
|
242
|
+
while [[ $retry_count -lt $LOCK_MAX_RETRIES ]]; do
|
|
243
|
+
if (set -o noclobber; echo $$ > "$lock_file") 2>/dev/null; then
|
|
244
|
+
echo $$ > "$lock_pid_file" 2>/dev/null || true
|
|
245
|
+
LOCK_AT_FILE="$lock_file"
|
|
246
|
+
return 0
|
|
247
|
+
fi
|
|
248
|
+
retry_count=$((retry_count + 1))
|
|
249
|
+
[[ $retry_count -lt $LOCK_MAX_RETRIES ]] && sleep $LOCK_RETRY_INTERVAL
|
|
250
|
+
done
|
|
251
|
+
|
|
252
|
+
echo "Failed to acquire lock for $file_path in $lock_dir after $LOCK_MAX_RETRIES attempts" >&2
|
|
253
|
+
return 1
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Release a lock acquired with acquire_lock_at
|
|
257
|
+
# Arguments: $1 = lock_file (the LOCK_AT_FILE value from acquire_lock_at)
|
|
258
|
+
release_lock_at() {
|
|
259
|
+
local lock_file="${1:-$LOCK_AT_FILE}"
|
|
260
|
+
if [[ -n "$lock_file" && -f "$lock_file" ]]; then
|
|
261
|
+
rm -f "$lock_file" "${lock_file}.pid"
|
|
262
|
+
[[ "$lock_file" == "$LOCK_AT_FILE" ]] && LOCK_AT_FILE=""
|
|
263
|
+
return 0
|
|
264
|
+
fi
|
|
265
|
+
return 1
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# Cleanup function for script exit
|
|
269
|
+
cleanup_locks() {
|
|
270
|
+
if [ "$LOCK_ACQUIRED" = "true" ]; then
|
|
271
|
+
release_lock
|
|
272
|
+
fi
|
|
273
|
+
if [[ -n "${LOCK_AT_FILE:-}" ]]; then
|
|
274
|
+
release_lock_at "$LOCK_AT_FILE"
|
|
275
|
+
fi
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# Register cleanup on exit — includes HUP for SSH disconnect safety
|
|
279
|
+
trap cleanup_locks EXIT TERM INT HUP
|
|
280
|
+
|
|
281
|
+
# Check if a file is currently locked
|
|
282
|
+
is_locked() {
|
|
283
|
+
local file_path="$1"
|
|
284
|
+
local lock_file="${LOCK_DIR}/$(basename "$file_path").lock"
|
|
285
|
+
[ -f "$lock_file" ]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
# Get PID of process holding lock
|
|
289
|
+
get_lock_holder() {
|
|
290
|
+
local file_path="$1"
|
|
291
|
+
local lock_file="${LOCK_DIR}/$(basename "$file_path").lock.pid"
|
|
292
|
+
cat "$lock_file" 2>/dev/null || echo ""
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# Wait for lock to be released
|
|
296
|
+
wait_for_lock() {
|
|
297
|
+
local file_path="$1"
|
|
298
|
+
local max_wait=${2:-$LOCK_TIMEOUT}
|
|
299
|
+
local waited=0
|
|
300
|
+
|
|
301
|
+
while is_locked "$file_path" && [ $waited -lt $max_wait ]; do
|
|
302
|
+
sleep 1
|
|
303
|
+
waited=$((waited + 1))
|
|
304
|
+
done
|
|
305
|
+
|
|
306
|
+
if [ $waited -ge $max_wait ]; then
|
|
307
|
+
return 1
|
|
308
|
+
fi
|
|
309
|
+
return 0
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# Export functions for use in other scripts
|
|
313
|
+
export -f acquire_lock release_lock acquire_lock_at release_lock_at is_locked get_lock_holder wait_for_lock cleanup_locks
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Flag utility functions — extracted from aether-utils.sh
|
|
3
|
+
# Provides: _flag_add, _flag_check_blockers, _flag_resolve, _flag_acknowledge, _flag_list, _flag_auto_resolve
|
|
4
|
+
#
|
|
5
|
+
# These functions are sourced by aether-utils.sh at startup.
|
|
6
|
+
# All shared infrastructure (json_ok, json_err, json_warn, atomic_write, acquire_lock,
|
|
7
|
+
# release_lock, feature_enabled, LOCK_DIR, DATA_DIR, SCRIPT_DIR, error constants) is available.
|
|
8
|
+
|
|
9
|
+
_flag_add() {
|
|
10
|
+
# Add a project-specific flag (blocker, issue, or note)
|
|
11
|
+
# Usage: flag-add <type> <title> <description> [source] [phase]
|
|
12
|
+
# Types: blocker (critical, blocks advancement), issue (high, warning), note (low, info)
|
|
13
|
+
type="${1:-issue}"
|
|
14
|
+
title="${2:-}"
|
|
15
|
+
desc="${3:-}"
|
|
16
|
+
source="${4:-manual}"
|
|
17
|
+
phase="${5:-null}"
|
|
18
|
+
[[ -z "$title" ]] && json_err "$E_VALIDATION_FAILED" "Usage: flag-add <type> <title> <description> [source] [phase]"
|
|
19
|
+
|
|
20
|
+
mkdir -p "$COLONY_DATA_DIR"
|
|
21
|
+
flags_file="$COLONY_DATA_DIR/flags.json"
|
|
22
|
+
|
|
23
|
+
if [[ ! -f "$flags_file" ]]; then
|
|
24
|
+
atomic_write "$flags_file" '{"version":1,"flags":[]}' || json_err "$E_UNKNOWN" "Failed to initialize flags file"
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
id="flag_$(date -u +%s)_$(head -c 2 /dev/urandom | od -An -tx1 | tr -d ' ')"
|
|
28
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
29
|
+
|
|
30
|
+
# Acquire lock for atomic flag update (degrade gracefully if locking unavailable)
|
|
31
|
+
if type feature_enabled &>/dev/null && ! feature_enabled "file_locking"; then
|
|
32
|
+
json_warn "W_DEGRADED" "File locking disabled - proceeding without lock: $(type _feature_reason &>/dev/null && _feature_reason file_locking || echo 'unknown')"
|
|
33
|
+
else
|
|
34
|
+
acquire_lock "$flags_file" || {
|
|
35
|
+
if type json_err &>/dev/null; then
|
|
36
|
+
json_err "$E_LOCK_FAILED" "Failed to acquire lock on flags.json"
|
|
37
|
+
else
|
|
38
|
+
echo '{"ok":false,"error":"Failed to acquire lock on flags.json"}' >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
}
|
|
42
|
+
# Ensure lock is always released on exit (BUG-002 fix)
|
|
43
|
+
trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Map type to severity
|
|
47
|
+
case "$type" in
|
|
48
|
+
blocker) severity="critical" ;;
|
|
49
|
+
issue) severity="high" ;;
|
|
50
|
+
note) severity="low" ;;
|
|
51
|
+
*) severity="medium" ;;
|
|
52
|
+
esac
|
|
53
|
+
|
|
54
|
+
# Handle phase as number or null
|
|
55
|
+
if [[ "$phase" =~ ^[0-9]+$ ]]; then
|
|
56
|
+
phase_jq="$phase"
|
|
57
|
+
else
|
|
58
|
+
phase_jq="null"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
updated=$(jq --arg id "$id" --arg type "$type" --arg sev "$severity" \
|
|
62
|
+
--arg title "$title" --arg desc "$desc" --arg source "$source" \
|
|
63
|
+
--argjson phase "$phase_jq" --arg ts "$ts" '
|
|
64
|
+
.flags += [{
|
|
65
|
+
id: $id,
|
|
66
|
+
type: $type,
|
|
67
|
+
severity: $sev,
|
|
68
|
+
title: $title,
|
|
69
|
+
description: $desc,
|
|
70
|
+
source: $source,
|
|
71
|
+
phase: $phase,
|
|
72
|
+
created_at: $ts,
|
|
73
|
+
acknowledged_at: null,
|
|
74
|
+
resolved_at: null,
|
|
75
|
+
resolution: null,
|
|
76
|
+
auto_resolve_on: (if $type == "blocker" and ($source | test("chaos") | not) then "build_pass" else null end)
|
|
77
|
+
}]
|
|
78
|
+
' "$flags_file") || { json_err "$E_JSON_INVALID" "Failed to add flag"; }
|
|
79
|
+
|
|
80
|
+
atomic_write "$flags_file" "$updated"
|
|
81
|
+
trap - EXIT
|
|
82
|
+
release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
|
|
83
|
+
json_ok "$(jq -n --arg id "$id" --arg type "$type" --arg severity "$severity" \
|
|
84
|
+
'{id: $id, type: $type, severity: $severity}')"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_flag_check_blockers() {
|
|
88
|
+
# Count unresolved blockers for the current phase
|
|
89
|
+
# Usage: flag-check-blockers [phase]
|
|
90
|
+
phase="${1:-}"
|
|
91
|
+
flags_file="$COLONY_DATA_DIR/flags.json"
|
|
92
|
+
|
|
93
|
+
if [[ ! -f "$flags_file" ]]; then
|
|
94
|
+
json_ok '{"blockers":0,"issues":0,"notes":0}'
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
if [[ -n "$phase" && "$phase" =~ ^[0-9]+$ ]]; then
|
|
99
|
+
# Filter by phase
|
|
100
|
+
result=$(jq --argjson phase "$phase" '{
|
|
101
|
+
blockers: [.flags[] | select(.type == "blocker" and .resolved_at == null and (.phase == $phase or .phase == null))] | length,
|
|
102
|
+
issues: [.flags[] | select(.type == "issue" and .resolved_at == null and (.phase == $phase or .phase == null))] | length,
|
|
103
|
+
notes: [.flags[] | select(.type == "note" and .resolved_at == null and (.phase == $phase or .phase == null))] | length
|
|
104
|
+
}' "$flags_file")
|
|
105
|
+
else
|
|
106
|
+
# All unresolved
|
|
107
|
+
result=$(jq '{
|
|
108
|
+
blockers: [.flags[] | select(.type == "blocker" and .resolved_at == null)] | length,
|
|
109
|
+
issues: [.flags[] | select(.type == "issue" and .resolved_at == null)] | length,
|
|
110
|
+
notes: [.flags[] | select(.type == "note" and .resolved_at == null)] | length
|
|
111
|
+
}' "$flags_file")
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
json_ok "$result"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_flag_resolve() {
|
|
118
|
+
# Resolve a flag with optional resolution message
|
|
119
|
+
# Usage: flag-resolve <flag_id> [resolution_message]
|
|
120
|
+
flag_id="${1:-}"
|
|
121
|
+
resolution="${2:-Resolved}"
|
|
122
|
+
[[ -z "$flag_id" ]] && json_err "$E_VALIDATION_FAILED" "Usage: flag-resolve <flag_id> [resolution_message]"
|
|
123
|
+
|
|
124
|
+
flags_file="$COLONY_DATA_DIR/flags.json"
|
|
125
|
+
[[ ! -f "$flags_file" ]] && json_err "$E_FILE_NOT_FOUND" "No flags file found"
|
|
126
|
+
|
|
127
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
128
|
+
|
|
129
|
+
# Acquire lock for atomic flag update (degrade gracefully if locking unavailable)
|
|
130
|
+
if type feature_enabled &>/dev/null && ! feature_enabled "file_locking"; then
|
|
131
|
+
json_warn "W_DEGRADED" "File locking disabled - proceeding without lock"
|
|
132
|
+
else
|
|
133
|
+
acquire_lock "$flags_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on flags.json"
|
|
134
|
+
trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
updated=$(jq --arg id "$flag_id" --arg res "$resolution" --arg ts "$ts" '
|
|
138
|
+
.flags = [.flags[] | if .id == $id then
|
|
139
|
+
.resolved_at = $ts |
|
|
140
|
+
.resolution = $res
|
|
141
|
+
else . end]
|
|
142
|
+
' "$flags_file") || {
|
|
143
|
+
json_err "$E_JSON_INVALID" "Failed to resolve flag"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
atomic_write "$flags_file" "$updated"
|
|
147
|
+
trap - EXIT
|
|
148
|
+
release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
|
|
149
|
+
json_ok "$(jq -n --arg id "$flag_id" '{resolved: $id}')"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_flag_acknowledge() {
|
|
153
|
+
# Acknowledge a flag (issue continues but noted)
|
|
154
|
+
# Usage: flag-acknowledge <flag_id>
|
|
155
|
+
flag_id="${1:-}"
|
|
156
|
+
[[ -z "$flag_id" ]] && json_err "$E_VALIDATION_FAILED" "Usage: flag-acknowledge <flag_id>"
|
|
157
|
+
|
|
158
|
+
flags_file="$COLONY_DATA_DIR/flags.json"
|
|
159
|
+
[[ ! -f "$flags_file" ]] && json_err "$E_FILE_NOT_FOUND" "No flags file found"
|
|
160
|
+
|
|
161
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
162
|
+
|
|
163
|
+
# Acquire lock for atomic flag update (degrade gracefully if locking unavailable)
|
|
164
|
+
if type feature_enabled &>/dev/null && ! feature_enabled "file_locking"; then
|
|
165
|
+
json_warn "W_DEGRADED" "File locking disabled - proceeding without lock"
|
|
166
|
+
else
|
|
167
|
+
acquire_lock "$flags_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on flags.json"
|
|
168
|
+
trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
updated=$(jq --arg id "$flag_id" --arg ts "$ts" '
|
|
172
|
+
.flags = [.flags[] | if .id == $id then
|
|
173
|
+
.acknowledged_at = $ts
|
|
174
|
+
else . end]
|
|
175
|
+
' "$flags_file") || {
|
|
176
|
+
json_err "$E_JSON_INVALID" "Failed to acknowledge flag"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
atomic_write "$flags_file" "$updated"
|
|
180
|
+
trap - EXIT
|
|
181
|
+
release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
|
|
182
|
+
json_ok "$(jq -n --arg id "$flag_id" '{acknowledged: $id}')"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_flag_list() {
|
|
186
|
+
# List flags, optionally filtered
|
|
187
|
+
# Usage: flag-list [--all] [--type blocker|issue|note] [--phase N]
|
|
188
|
+
flags_file="$COLONY_DATA_DIR/flags.json"
|
|
189
|
+
show_all="false"
|
|
190
|
+
filter_type=""
|
|
191
|
+
filter_phase=""
|
|
192
|
+
|
|
193
|
+
while [[ $# -gt 0 ]]; do
|
|
194
|
+
case "$1" in
|
|
195
|
+
--all) show_all="true"; shift ;;
|
|
196
|
+
--type) filter_type="$2"; shift 2 ;;
|
|
197
|
+
--phase) filter_phase="$2"; shift 2 ;;
|
|
198
|
+
*) shift ;;
|
|
199
|
+
esac
|
|
200
|
+
done
|
|
201
|
+
|
|
202
|
+
if [[ ! -f "$flags_file" ]]; then
|
|
203
|
+
json_ok '{"flags":[],"count":0}'
|
|
204
|
+
exit 0
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
# Build jq filter
|
|
208
|
+
jq_filter='.flags'
|
|
209
|
+
|
|
210
|
+
if [[ "$show_all" != "true" ]]; then
|
|
211
|
+
jq_filter+=' | [.[] | select(.resolved_at == null)]'
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
if [[ -n "$filter_type" ]]; then
|
|
215
|
+
jq_filter+=" | [.[] | select(.type == \"$filter_type\")]"
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
if [[ -n "$filter_phase" && "$filter_phase" =~ ^[0-9]+$ ]]; then
|
|
219
|
+
jq_filter+=" | [.[] | select(.phase == $filter_phase or .phase == null)]"
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
result=$(jq "{flags: ($jq_filter), count: ($jq_filter | length)}" "$flags_file")
|
|
223
|
+
json_ok "$result"
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
_flag_auto_resolve() {
|
|
227
|
+
# Auto-resolve flags based on trigger (e.g., build_pass)
|
|
228
|
+
# Usage: flag-auto-resolve <trigger>
|
|
229
|
+
trigger="${1:-build_pass}"
|
|
230
|
+
flags_file="$COLONY_DATA_DIR/flags.json"
|
|
231
|
+
|
|
232
|
+
if [[ ! -f "$flags_file" ]]; then json_ok '{"resolved":0}'; exit 0; fi
|
|
233
|
+
|
|
234
|
+
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
235
|
+
|
|
236
|
+
# Acquire lock for atomic flag update (degrade gracefully if locking unavailable)
|
|
237
|
+
if type feature_enabled &>/dev/null && ! feature_enabled "file_locking"; then
|
|
238
|
+
json_warn "W_DEGRADED" "File locking disabled - proceeding without lock"
|
|
239
|
+
else
|
|
240
|
+
acquire_lock "$flags_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on flags.json"
|
|
241
|
+
# Ensure lock is always released on exit (BUG-005/BUG-011 fix)
|
|
242
|
+
trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
# Count how many will be resolved
|
|
246
|
+
count=$(jq --arg trigger "$trigger" '
|
|
247
|
+
[.flags[] | select(.auto_resolve_on == $trigger and .resolved_at == null)] | length
|
|
248
|
+
' "$flags_file") || {
|
|
249
|
+
json_err "$E_JSON_INVALID" "Failed to count flags for auto-resolve"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# Resolve them
|
|
253
|
+
updated=$(jq --arg trigger "$trigger" --arg ts "$ts" '
|
|
254
|
+
.flags = [.flags[] | if .auto_resolve_on == $trigger and .resolved_at == null then
|
|
255
|
+
.resolved_at = $ts |
|
|
256
|
+
.resolution = "Auto-resolved on " + $trigger
|
|
257
|
+
else . end]
|
|
258
|
+
' "$flags_file") || {
|
|
259
|
+
json_err "$E_JSON_INVALID" "Failed to auto-resolve flags"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
atomic_write "$flags_file" "$updated"
|
|
263
|
+
trap - EXIT
|
|
264
|
+
release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
|
|
265
|
+
json_ok "$(jq -n --argjson count "$count" --arg trigger "$trigger" \
|
|
266
|
+
'{resolved: $count, trigger: $trigger}')"
|
|
267
|
+
}
|