aether-colony 3.1.17 → 5.0.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.
Files changed (183) hide show
  1. package/{runtime → .aether}/CONTEXT.md +1 -1
  2. package/{runtime → .aether}/aether-utils.sh +1772 -98
  3. package/.aether/docs/QUEEN-SYSTEM.md +211 -0
  4. package/.aether/docs/QUEEN.md +84 -0
  5. package/.aether/docs/README.md +68 -0
  6. package/.aether/docs/caste-system.md +48 -0
  7. package/{runtime → .aether/docs/disciplines}/DISCIPLINES.md +8 -8
  8. package/.aether/docs/error-codes.md +268 -0
  9. package/{runtime → .aether}/docs/known-issues.md +42 -26
  10. package/.aether/docs/queen-commands.md +97 -0
  11. package/.aether/exchange/colony-registry.xml +11 -0
  12. package/{runtime → .aether}/exchange/pheromone-xml.sh +2 -1
  13. package/.aether/exchange/pheromones.xml +87 -0
  14. package/.aether/exchange/queen-wisdom.xml +14 -0
  15. package/{runtime → .aether}/exchange/registry-xml.sh +7 -3
  16. package/{runtime → .aether}/exchange/wisdom-xml.sh +11 -4
  17. package/.aether/rules/aether-colony.md +134 -0
  18. package/.aether/schemas/example-prompt-builder.xml +234 -0
  19. package/.aether/templates/colony-state-reset.jq.template +22 -0
  20. package/.aether/templates/colony-state.template.json +35 -0
  21. package/.aether/templates/constraints.template.json +9 -0
  22. package/.aether/templates/crowned-anthill.template.md +36 -0
  23. package/.aether/templates/handoff-build-error.template.md +30 -0
  24. package/.aether/templates/handoff-build-success.template.md +39 -0
  25. package/.aether/templates/handoff.template.md +40 -0
  26. package/{runtime → .aether}/utils/atomic-write.sh +5 -5
  27. package/{runtime → .aether}/utils/chamber-compare.sh +23 -10
  28. package/{runtime → .aether}/utils/chamber-utils.sh +32 -20
  29. package/{runtime → .aether}/utils/error-handler.sh +13 -1
  30. package/{runtime → .aether}/utils/file-lock.sh +49 -13
  31. package/.aether/utils/semantic-cli.sh +413 -0
  32. package/{runtime → .aether}/utils/xml-compose.sh +7 -1
  33. package/.aether/utils/xml-convert.sh +273 -0
  34. package/.aether/utils/xml-query.sh +201 -0
  35. package/.aether/utils/xml-utils.sh +110 -0
  36. package/{runtime → .aether}/workers.md +14 -17
  37. package/.claude/agents/ant/aether-ambassador.md +264 -0
  38. package/.claude/agents/ant/aether-archaeologist.md +322 -0
  39. package/.claude/agents/ant/aether-auditor.md +266 -0
  40. package/.claude/agents/ant/aether-builder.md +187 -0
  41. package/.claude/agents/ant/aether-chaos.md +268 -0
  42. package/.claude/agents/ant/aether-chronicler.md +304 -0
  43. package/.claude/agents/ant/aether-gatekeeper.md +325 -0
  44. package/.claude/agents/ant/aether-includer.md +373 -0
  45. package/.claude/agents/ant/aether-keeper.md +271 -0
  46. package/.claude/agents/ant/aether-measurer.md +317 -0
  47. package/.claude/agents/ant/aether-probe.md +210 -0
  48. package/.claude/agents/ant/aether-queen.md +325 -0
  49. package/.claude/agents/ant/aether-route-setter.md +173 -0
  50. package/.claude/agents/ant/aether-sage.md +353 -0
  51. package/.claude/agents/ant/aether-scout.md +142 -0
  52. package/.claude/agents/ant/aether-surveyor-disciplines.md +416 -0
  53. package/.claude/agents/ant/aether-surveyor-nest.md +354 -0
  54. package/.claude/agents/ant/aether-surveyor-pathogens.md +288 -0
  55. package/.claude/agents/ant/aether-surveyor-provisions.md +359 -0
  56. package/.claude/agents/ant/aether-tracker.md +265 -0
  57. package/.claude/agents/ant/aether-watcher.md +244 -0
  58. package/.claude/agents/ant/aether-weaver.md +247 -0
  59. package/.claude/commands/ant/archaeology.md +16 -7
  60. package/.claude/commands/ant/build.md +415 -284
  61. package/.claude/commands/ant/chaos.md +19 -10
  62. package/.claude/commands/ant/colonize.md +58 -24
  63. package/.claude/commands/ant/continue.md +155 -145
  64. package/.claude/commands/ant/council.md +15 -5
  65. package/.claude/commands/ant/dream.md +16 -7
  66. package/.claude/commands/ant/entomb.md +274 -157
  67. package/.claude/commands/ant/feedback.md +33 -29
  68. package/.claude/commands/ant/flag.md +18 -10
  69. package/.claude/commands/ant/flags.md +14 -6
  70. package/.claude/commands/ant/focus.md +29 -21
  71. package/.claude/commands/ant/help.md +11 -1
  72. package/.claude/commands/ant/history.md +10 -0
  73. package/.claude/commands/ant/init.md +91 -65
  74. package/.claude/commands/ant/interpret.md +15 -4
  75. package/.claude/commands/ant/lay-eggs.md +55 -7
  76. package/.claude/commands/ant/maturity.md +11 -1
  77. package/.claude/commands/ant/migrate-state.md +14 -2
  78. package/.claude/commands/ant/oracle.md +23 -15
  79. package/.claude/commands/ant/organize.md +29 -20
  80. package/.claude/commands/ant/pause-colony.md +17 -7
  81. package/.claude/commands/ant/phase.md +17 -8
  82. package/.claude/commands/ant/plan.md +20 -9
  83. package/.claude/commands/ant/redirect.md +29 -32
  84. package/.claude/commands/ant/resume-colony.md +19 -9
  85. package/.claude/commands/ant/resume.md +272 -96
  86. package/.claude/commands/ant/seal.md +201 -191
  87. package/.claude/commands/ant/status.md +71 -32
  88. package/.claude/commands/ant/swarm.md +26 -44
  89. package/.claude/commands/ant/tunnels.md +279 -105
  90. package/.claude/commands/ant/update.md +81 -20
  91. package/.claude/commands/ant/verify-castes.md +14 -4
  92. package/.claude/commands/ant/watch.md +13 -12
  93. package/.opencode/agents/aether-ambassador.md +63 -20
  94. package/.opencode/agents/aether-archaeologist.md +29 -12
  95. package/.opencode/agents/aether-auditor.md +51 -18
  96. package/.opencode/agents/aether-builder.md +69 -19
  97. package/.opencode/agents/aether-chaos.md +29 -12
  98. package/.opencode/agents/aether-chronicler.md +60 -18
  99. package/.opencode/agents/aether-gatekeeper.md +27 -18
  100. package/.opencode/agents/aether-includer.md +27 -18
  101. package/.opencode/agents/aether-keeper.md +89 -18
  102. package/.opencode/agents/aether-measurer.md +27 -18
  103. package/.opencode/agents/aether-probe.md +60 -18
  104. package/.opencode/agents/aether-queen.md +172 -24
  105. package/.opencode/agents/aether-route-setter.md +57 -12
  106. package/.opencode/agents/aether-sage.md +26 -18
  107. package/.opencode/agents/aether-scout.md +27 -19
  108. package/.opencode/agents/aether-surveyor-disciplines.md +53 -1
  109. package/.opencode/agents/aether-surveyor-nest.md +53 -1
  110. package/.opencode/agents/aether-surveyor-pathogens.md +51 -1
  111. package/.opencode/agents/aether-surveyor-provisions.md +53 -1
  112. package/.opencode/agents/aether-tracker.md +64 -18
  113. package/.opencode/agents/aether-watcher.md +66 -19
  114. package/.opencode/agents/aether-weaver.md +61 -18
  115. package/.opencode/commands/ant/build.md +406 -192
  116. package/.opencode/commands/ant/continue.md +66 -76
  117. package/.opencode/commands/ant/entomb.md +106 -45
  118. package/.opencode/commands/ant/init.md +46 -48
  119. package/.opencode/commands/ant/organize.md +5 -5
  120. package/.opencode/commands/ant/resume.md +334 -0
  121. package/.opencode/commands/ant/seal.md +33 -24
  122. package/.opencode/commands/ant/status.md +11 -0
  123. package/.opencode/commands/ant/tunnels.md +149 -0
  124. package/.opencode/commands/ant/update.md +59 -16
  125. package/CHANGELOG.md +79 -0
  126. package/README.md +135 -353
  127. package/bin/cli.js +243 -122
  128. package/bin/generate-commands.sh +2 -2
  129. package/bin/lib/init.js +13 -3
  130. package/bin/lib/update-transaction.js +119 -117
  131. package/bin/sync-to-runtime.sh +5 -137
  132. package/bin/validate-package.sh +84 -0
  133. package/package.json +9 -6
  134. package/.opencode/agents/aether-architect.md +0 -66
  135. package/.opencode/agents/aether-guardian.md +0 -107
  136. package/.opencode/agents/workers.md +0 -1034
  137. package/runtime/QUEEN_ANT_ARCHITECTURE.md +0 -402
  138. package/runtime/data/signatures.json +0 -41
  139. package/runtime/docs/AETHER-2.0-IMPLEMENTATION-PLAN.md +0 -1343
  140. package/runtime/docs/AETHER-PHEROMONE-SYSTEM-MASTER-SPEC.md +0 -2642
  141. package/runtime/docs/PHEROMONE-INJECTION.md +0 -240
  142. package/runtime/docs/PHEROMONE-INTEGRATION.md +0 -192
  143. package/runtime/docs/PHEROMONE-SYSTEM-DESIGN.md +0 -426
  144. package/runtime/docs/README.md +0 -94
  145. package/runtime/docs/VISUAL-OUTPUT-SPEC.md +0 -219
  146. package/runtime/docs/biological-reference.md +0 -272
  147. package/runtime/docs/codebase-review.md +0 -399
  148. package/runtime/docs/command-sync.md +0 -164
  149. package/runtime/docs/constraints.md +0 -116
  150. package/runtime/docs/implementation-learnings.md +0 -89
  151. package/runtime/docs/namespace.md +0 -148
  152. package/runtime/docs/pathogen-schema-example.json +0 -36
  153. package/runtime/docs/pathogen-schema.md +0 -111
  154. package/runtime/docs/planning-discipline.md +0 -159
  155. package/runtime/docs/progressive-disclosure.md +0 -184
  156. package/runtime/lib/queen-utils.sh +0 -729
  157. package/runtime/planning.md +0 -159
  158. package/runtime/recover.sh +0 -136
  159. package/runtime/utils/xml-utils.sh +0 -2196
  160. package/runtime/workers-new-castes.md +0 -516
  161. /package/{runtime → .aether/docs/disciplines}/coding-standards.md +0 -0
  162. /package/{runtime → .aether/docs/disciplines}/debugging.md +0 -0
  163. /package/{runtime → .aether/docs/disciplines}/learning.md +0 -0
  164. /package/{runtime → .aether/docs/disciplines}/tdd.md +0 -0
  165. /package/{runtime → .aether/docs/disciplines}/verification-loop.md +0 -0
  166. /package/{runtime → .aether/docs/disciplines}/verification.md +0 -0
  167. /package/{runtime → .aether}/docs/pheromones.md +0 -0
  168. /package/{runtime → .aether}/model-profiles.yaml +0 -0
  169. /package/{runtime → .aether}/schemas/aether-types.xsd +0 -0
  170. /package/{runtime → .aether}/schemas/colony-registry.xsd +0 -0
  171. /package/{runtime → .aether}/schemas/pheromone.xsd +0 -0
  172. /package/{runtime → .aether}/schemas/prompt.xsd +0 -0
  173. /package/{runtime → .aether}/schemas/queen-wisdom.xsd +0 -0
  174. /package/{runtime → .aether}/schemas/worker-priming.xsd +0 -0
  175. /package/{runtime → .aether}/templates/QUEEN.md.template +0 -0
  176. /package/{runtime → .aether}/utils/colorize-log.sh +0 -0
  177. /package/{runtime → .aether}/utils/queen-to-md.xsl +0 -0
  178. /package/{runtime → .aether}/utils/spawn-tree.sh +0 -0
  179. /package/{runtime → .aether}/utils/spawn-with-model.sh +0 -0
  180. /package/{runtime → .aether}/utils/state-loader.sh +0 -0
  181. /package/{runtime → .aether}/utils/swarm-display.sh +0 -0
  182. /package/{runtime → .aether}/utils/watch-spawn-tree.sh +0 -0
  183. /package/{runtime → .aether}/utils/xml-core.sh +0 -0
@@ -7,13 +7,26 @@ set -euo pipefail
7
7
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
8
  CHAMBERS_DIR="${CHAMBERS_DIR:-.aether/chambers}"
9
9
 
10
+ # Source error-handler.sh for E_* constants and enhanced json_err
11
+ [[ -f "$SCRIPT_DIR/error-handler.sh" ]] && source "$SCRIPT_DIR/error-handler.sh"
12
+
13
+ # Fallback E_* constants (no-ops when error-handler.sh is already loaded)
14
+ : "${E_UNKNOWN:=E_UNKNOWN}"
15
+ : "${E_FILE_NOT_FOUND:=E_FILE_NOT_FOUND}"
16
+ : "${E_VALIDATION_FAILED:=E_VALIDATION_FAILED}"
17
+
10
18
  # JSON output helpers
11
19
  json_ok() { printf '{"ok":true,"result":%s}\n' "$1"; }
12
- json_err() {
13
- local message="${2:-$1}"
14
- printf '{"ok":false,"error":"%s"}\n' "$message" >&2
15
- exit 1
16
- }
20
+
21
+ # Guard: yield to error-handler.sh's enhanced json_err when already loaded
22
+ if ! type json_err &>/dev/null; then
23
+ json_err() {
24
+ local code="${1:-E_UNKNOWN}"
25
+ local message="${2:-An unknown error occurred}"
26
+ printf '{"ok":false,"error":{"code":"%s","message":"%s"}}\n' "$code" "$message" >&2
27
+ exit 1
28
+ }
29
+ fi
17
30
 
18
31
  # Load chamber manifest
19
32
  load_chamber() {
@@ -21,7 +34,7 @@ load_chamber() {
21
34
  local manifest_file="$CHAMBERS_DIR/$chamber_name/manifest.json"
22
35
 
23
36
  if [[ ! -f "$manifest_file" ]]; then
24
- json_err "Chamber not found: $chamber_name"
37
+ json_err "$E_FILE_NOT_FOUND" "Chamber not found: $chamber_name. Try: check the chamber name with /ant:tunnels."
25
38
  fi
26
39
 
27
40
  cat "$manifest_file"
@@ -41,7 +54,7 @@ EOF
41
54
  compare)
42
55
  chamber_a="${1:-}"
43
56
  chamber_b="${2:-}"
44
- [[ -z "$chamber_a" || -z "$chamber_b" ]] && json_err "Usage: compare <chamber_a> <chamber_b>"
57
+ [[ -z "$chamber_a" || -z "$chamber_b" ]] && json_err "$E_VALIDATION_FAILED" "Missing arguments. Try: compare <chamber_a> <chamber_b>."
45
58
 
46
59
  # Load both manifests
47
60
  manifest_a=$(load_chamber "$chamber_a")
@@ -93,7 +106,7 @@ EOF
93
106
  diff)
94
107
  chamber_a="${1:-}"
95
108
  chamber_b="${2:-}"
96
- [[ -z "$chamber_a" || -z "$chamber_b" ]] && json_err "Usage: diff <chamber_a> <chamber_b>"
109
+ [[ -z "$chamber_a" || -z "$chamber_b" ]] && json_err "$E_VALIDATION_FAILED" "Missing arguments. Try: diff <chamber_a> <chamber_b>."
97
110
 
98
111
  manifest_a=$(load_chamber "$chamber_a")
99
112
  manifest_b=$(load_chamber "$chamber_b")
@@ -138,7 +151,7 @@ EOF
138
151
  stats)
139
152
  chamber_a="${1:-}"
140
153
  chamber_b="${2:-}"
141
- [[ -z "$chamber_a" || -z "$chamber_b" ]] && json_err "Usage: stats <chamber_a> <chamber_b>"
154
+ [[ -z "$chamber_a" || -z "$chamber_b" ]] && json_err "$E_VALIDATION_FAILED" "Missing arguments. Try: stats <chamber_a> <chamber_b>."
142
155
 
143
156
  manifest_a=$(load_chamber "$chamber_a")
144
157
  manifest_b=$(load_chamber "$chamber_b")
@@ -175,6 +188,6 @@ EOF
175
188
  ;;
176
189
 
177
190
  *)
178
- json_err "Unknown command: $cmd. Use: compare, diff, stats"
191
+ json_err "$E_VALIDATION_FAILED" "Unknown command: $cmd. Try: compare, diff, or stats."
179
192
  ;;
180
193
  esac
@@ -27,11 +27,23 @@ SCRIPT_DIR="${SCRIPT_DIR:-$__chamber_utils_dir}"
27
27
 
28
28
  # --- JSON output helpers ---
29
29
  json_ok() { printf '{"ok":true,"result":%s}\n' "$1"; }
30
- json_err() {
31
- local message="${2:-$1}"
32
- printf '{"ok":false,"error":"%s"}\n' "$message" >&2
33
- exit 1
34
- }
30
+
31
+ # Guard: yield to error-handler.sh's enhanced json_err when already loaded
32
+ if ! type json_err &>/dev/null; then
33
+ json_err() {
34
+ local code="${1:-E_UNKNOWN}"
35
+ local message="${2:-An unknown error occurred}"
36
+ printf '{"ok":false,"error":{"code":"%s","message":"%s"}}\n' "$code" "$message" >&2
37
+ exit 1
38
+ }
39
+ fi
40
+
41
+ # Fallback E_* constants (no-ops when error-handler.sh is already loaded)
42
+ : "${E_UNKNOWN:=E_UNKNOWN}"
43
+ : "${E_VALIDATION_FAILED:=E_VALIDATION_FAILED}"
44
+ : "${E_FILE_NOT_FOUND:=E_FILE_NOT_FOUND}"
45
+ : "${E_BASH_ERROR:=E_BASH_ERROR}"
46
+ : "${E_JSON_INVALID:=E_JSON_INVALID}"
35
47
 
36
48
  # --- Chamber Functions ---
37
49
 
@@ -93,20 +105,20 @@ chamber_create() {
93
105
  local learnings_json="$9"
94
106
 
95
107
  # Validate inputs
96
- [[ -z "$chamber_dir" ]] && json_err "chamber_dir is required"
97
- [[ -z "$state_file" ]] && json_err "state_file is required"
98
- [[ ! -f "$state_file" ]] && json_err "State file not found: $state_file"
108
+ [[ -z "$chamber_dir" ]] && json_err "$E_VALIDATION_FAILED" "chamber_dir argument is required. Try: pass the chamber directory path."
109
+ [[ -z "$state_file" ]] && json_err "$E_VALIDATION_FAILED" "state_file argument is required. Try: pass the state file path."
110
+ [[ ! -f "$state_file" ]] && json_err "$E_FILE_NOT_FOUND" "State file not found: $state_file. Try: check the file path."
99
111
 
100
112
  # Create chamber directory
101
- mkdir -p "$chamber_dir" || json_err "Failed to create chamber directory: $chamber_dir"
113
+ mkdir -p "$chamber_dir" || json_err "$E_BASH_ERROR" "Couldn't create chamber directory: $chamber_dir. Try: check disk space and permissions."
102
114
 
103
115
  # Copy state file to chamber
104
116
  local target_state="$chamber_dir/COLONY_STATE.json"
105
- cp "$state_file" "$target_state" || json_err "Failed to copy state file"
117
+ cp "$state_file" "$target_state" || json_err "$E_BASH_ERROR" "Couldn't copy the state file. Try: check disk space and permissions."
106
118
 
107
119
  # Compute hash of the copied state file
108
120
  local state_hash=$(chamber_compute_hash "$target_state")
109
- [[ -z "$state_hash" ]] && json_err "Failed to compute hash of state file"
121
+ [[ -z "$state_hash" ]] && json_err "$E_BASH_ERROR" "Couldn't compute state file hash. Try: check that shasum is available."
110
122
 
111
123
  # Generate timestamp
112
124
  local entombed_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
@@ -132,14 +144,14 @@ EOF
132
144
 
133
145
  # Write manifest atomically if atomic_write is available, otherwise direct
134
146
  if type atomic_write &>/dev/null; then
135
- atomic_write "$manifest_file" "$manifest_content" || json_err "Failed to write manifest"
147
+ atomic_write "$manifest_file" "$manifest_content" || json_err "$E_BASH_ERROR" "Couldn't write chamber manifest. Try: check disk space."
136
148
  else
137
- echo "$manifest_content" > "$manifest_file" || json_err "Failed to write manifest"
149
+ echo "$manifest_content" > "$manifest_file" || json_err "$E_BASH_ERROR" "Couldn't write chamber manifest. Try: check disk space."
138
150
  fi
139
151
 
140
152
  # Verify the manifest was written correctly
141
153
  if [[ ! -f "$manifest_file" ]]; then
142
- json_err "Manifest file not created"
154
+ json_err "$E_FILE_NOT_FOUND" "Chamber manifest wasn't created. Try: check disk space and permissions."
143
155
  fi
144
156
 
145
157
  # Return success with chamber info
@@ -168,23 +180,23 @@ chamber_verify() {
168
180
  local chamber_dir="$1"
169
181
 
170
182
  # Validate inputs
171
- [[ -z "$chamber_dir" ]] && json_err "chamber_dir is required"
172
- [[ ! -d "$chamber_dir" ]] && json_err "Chamber directory not found: $chamber_dir"
183
+ [[ -z "$chamber_dir" ]] && json_err "$E_VALIDATION_FAILED" "chamber_dir argument is required. Try: pass the chamber directory path."
184
+ [[ ! -d "$chamber_dir" ]] && json_err "$E_FILE_NOT_FOUND" "Chamber directory not found: $chamber_dir. Try: check the path."
173
185
 
174
186
  local manifest_file="$chamber_dir/manifest.json"
175
187
  local state_file="$chamber_dir/COLONY_STATE.json"
176
188
 
177
189
  # Check required files exist
178
- [[ ! -f "$manifest_file" ]] && json_err "Manifest not found in chamber"
179
- [[ ! -f "$state_file" ]] && json_err "COLONY_STATE.json not found in chamber"
190
+ [[ ! -f "$manifest_file" ]] && json_err "$E_FILE_NOT_FOUND" "Manifest not found in chamber. Try: verify the chamber was created correctly."
191
+ [[ ! -f "$state_file" ]] && json_err "$E_FILE_NOT_FOUND" "COLONY_STATE.json not found in chamber. Try: re-entomb the colony."
180
192
 
181
193
  # Read stored hash from manifest
182
194
  local stored_hash=$(jq -r '.files["COLONY_STATE.json"] // empty' "$manifest_file" 2>/dev/null)
183
- [[ -z "$stored_hash" ]] && json_err "No hash found in manifest for COLONY_STATE.json"
195
+ [[ -z "$stored_hash" ]] && json_err "$E_JSON_INVALID" "No hash found in manifest. Try: re-entomb the colony."
184
196
 
185
197
  # Compute current hash
186
198
  local current_hash=$(chamber_compute_hash "$state_file")
187
- [[ -z "$current_hash" ]] && json_err "Failed to compute hash of state file"
199
+ [[ -z "$current_hash" ]] && json_err "$E_BASH_ERROR" "Couldn't compute state file hash. Try: check that shasum is available."
188
200
 
189
201
  # Compare hashes
190
202
  if [[ "$stored_hash" != "$current_hash" ]]; then
@@ -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
  }
@@ -193,8 +202,11 @@ export -f json_err json_warn error_handler
193
202
  export -f feature_enable feature_disable feature_enabled feature_log_degradation
194
203
  export -f _get_recovery _recovery_hub_not_found _recovery_repo_not_init
195
204
  export -f _recovery_file_not_found _recovery_json_invalid _recovery_lock_failed
205
+ export -f _recovery_lock_stale
196
206
  export -f _recovery_git_error _recovery_default _feature_reason
207
+ export -f _recovery_dependency_missing _recovery_resource_not_found
197
208
  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
209
+ export E_JSON_INVALID E_LOCK_FAILED E_LOCK_STALE E_GIT_ERROR E_VALIDATION_FAILED
199
210
  export E_FEATURE_UNAVAILABLE E_BASH_ERROR
211
+ export E_DEPENDENCY_MISSING E_RESOURCE_NOT_FOUND
200
212
  export _FEATURES_DISABLED
@@ -6,7 +6,7 @@
6
6
  # source .aether/utils/file-lock.sh
7
7
  # acquire_lock /path/to/file.lock
8
8
  # # ... critical section ...
9
- # release_lock /path/to/file.lock
9
+ # release_lock
10
10
 
11
11
  # Aether root detection - use git root if available, otherwise use current directory
12
12
  if git rev-parse --show-toplevel >/dev/null 2>&1; then
@@ -20,13 +20,16 @@ LOCK_TIMEOUT=300 # 5 minutes max lock time
20
20
  LOCK_RETRY_INTERVAL=0.5 # Wait 500ms between retries
21
21
  LOCK_MAX_RETRIES=100 # Total 50 seconds max wait
22
22
 
23
+ # Fallback constant — ensures E_LOCK_STALE is defined whether or not error-handler.sh was loaded
24
+ : "${E_LOCK_STALE:=E_LOCK_STALE}"
25
+
23
26
  # Create lock directory if it doesn't exist
24
27
  mkdir -p "$LOCK_DIR"
25
28
 
26
- # Acquire a file lock using flock
29
+ # Acquire a file lock using noclobber
27
30
  # Arguments: file_path (the resource to lock)
28
31
  # Returns: 0 on success, 1 on failure
29
- # Globals: LOCK_ACQUIRED (set to true when lock acquired)
32
+ # Globals: LOCK_ACQUIRED (set to true when lock acquired), CURRENT_LOCK (set to lock file path)
30
33
  acquire_lock() {
31
34
  local file_path="$1"
32
35
  local lock_file="${LOCK_DIR}/$(basename "$file_path").lock"
@@ -34,12 +37,45 @@ acquire_lock() {
34
37
 
35
38
  # Check if lock file exists and is stale
36
39
  if [ -f "$lock_file" ]; then
37
- local lock_pid=$(cat "$lock_pid_file" 2>/dev/null || echo "")
38
- if [ -n "$lock_pid" ]; then
39
- # Check if process is still running
40
- if ! kill -0 "$lock_pid" 2>/dev/null; then
41
- echo "Lock stale (PID $lock_pid not running), cleaning up..."
42
- rm -f "$lock_file" "$lock_pid_file"
40
+ local lock_pid
41
+ lock_pid=$(cat "$lock_pid_file" 2>/dev/null || echo "")
42
+ local is_stale=false
43
+
44
+ # Check age FIRST (before PID check) to handle PID reuse race
45
+ local lock_mtime=0
46
+ # Platform-portable mtime: macOS uses stat -f %m, Linux uses stat -c %Y
47
+ if stat -f %m "$lock_file" >/dev/null 2>&1; then
48
+ lock_mtime=$(stat -f %m "$lock_file" 2>/dev/null || echo 0)
49
+ else
50
+ lock_mtime=$(stat -c %Y "$lock_file" 2>/dev/null || echo 0)
51
+ fi
52
+ local lock_age=$(( $(date +%s) - lock_mtime ))
53
+
54
+ if [[ $lock_age -gt $LOCK_TIMEOUT ]]; then
55
+ is_stale=true
56
+ elif [[ -n "$lock_pid" ]] && ! kill -0 "$lock_pid" 2>/dev/null; then
57
+ is_stale=true
58
+ fi
59
+
60
+ if [[ "$is_stale" == "true" ]]; then
61
+ # TTY-gated user prompt — never auto-remove stale locks (locked user decision)
62
+ if [[ -t 2 ]]; then
63
+ echo "" >&2
64
+ echo "Warning: stale lock detected (PID ${lock_pid:-unknown} not running, age ${lock_age}s)" >&2
65
+ echo "Lock file: $lock_file" >&2
66
+ printf "Remove stale lock and continue? [y/N] " >&2
67
+ local response
68
+ read -r response < /dev/tty
69
+ if [[ "$response" =~ ^[Yy]$ ]]; then
70
+ rm -f "$lock_file" "$lock_pid_file"
71
+ else
72
+ echo "Lock removal declined. Remove manually: rm $lock_file" >&2
73
+ return 1
74
+ fi
75
+ else
76
+ # Non-interactive: fail with structured JSON error — do NOT auto-remove
77
+ printf '{"ok":false,"error":{"code":"%s","message":"Stale lock found. Remove manually: %s"}}\n' "$E_LOCK_STALE" "$lock_file" >&2
78
+ return 1
43
79
  fi
44
80
  fi
45
81
  fi
@@ -61,12 +97,12 @@ acquire_lock() {
61
97
  fi
62
98
  done
63
99
 
64
- echo "Failed to acquire lock for $file_path after $LOCK_MAX_RETRIES attempts"
100
+ echo "Failed to acquire lock for $file_path after $LOCK_MAX_RETRIES attempts" >&2
65
101
  return 1
66
102
  }
67
103
 
68
104
  # Release a file lock
69
- # Arguments: None (uses CURRENT_LOCK from acquire_lock)
105
+ # Arguments: None (uses CURRENT_LOCK global set by acquire_lock)
70
106
  release_lock() {
71
107
  if [ "$LOCK_ACQUIRED" = "true" ] && [ -n "$CURRENT_LOCK" ]; then
72
108
  rm -f "$CURRENT_LOCK" "${CURRENT_LOCK}.pid"
@@ -84,8 +120,8 @@ cleanup_locks() {
84
120
  fi
85
121
  }
86
122
 
87
- # Register cleanup on exit
88
- trap cleanup_locks EXIT TERM INT
123
+ # Register cleanup on exit — includes HUP for SSH disconnect safety
124
+ trap cleanup_locks EXIT TERM INT HUP
89
125
 
90
126
  # Check if a file is currently locked
91
127
  is_locked() {