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.
Files changed (317) hide show
  1. package/.aether/aether-utils.sh +3226 -3345
  2. package/.aether/agents-claude/aether-ambassador.md +265 -0
  3. package/.aether/agents-claude/aether-archaeologist.md +327 -0
  4. package/.aether/agents-claude/aether-architect.md +236 -0
  5. package/.aether/agents-claude/aether-auditor.md +271 -0
  6. package/.aether/agents-claude/aether-builder.md +224 -0
  7. package/.aether/agents-claude/aether-chaos.md +269 -0
  8. package/.aether/agents-claude/aether-chronicler.md +305 -0
  9. package/.aether/agents-claude/aether-gatekeeper.md +330 -0
  10. package/.aether/agents-claude/aether-includer.md +374 -0
  11. package/.aether/agents-claude/aether-keeper.md +272 -0
  12. package/.aether/agents-claude/aether-measurer.md +322 -0
  13. package/.aether/agents-claude/aether-oracle.md +237 -0
  14. package/.aether/agents-claude/aether-probe.md +211 -0
  15. package/.aether/agents-claude/aether-queen.md +330 -0
  16. package/.aether/agents-claude/aether-route-setter.md +178 -0
  17. package/.aether/agents-claude/aether-sage.md +418 -0
  18. package/.aether/agents-claude/aether-scout.md +179 -0
  19. package/.aether/agents-claude/aether-surveyor-disciplines.md +417 -0
  20. package/.aether/agents-claude/aether-surveyor-nest.md +355 -0
  21. package/.aether/agents-claude/aether-surveyor-pathogens.md +289 -0
  22. package/.aether/agents-claude/aether-surveyor-provisions.md +360 -0
  23. package/.aether/agents-claude/aether-tracker.md +270 -0
  24. package/.aether/agents-claude/aether-watcher.md +280 -0
  25. package/.aether/agents-claude/aether-weaver.md +248 -0
  26. package/.aether/commands/archaeology.yaml +653 -0
  27. package/.aether/commands/build.yaml +1221 -0
  28. package/.aether/commands/chaos.yaml +653 -0
  29. package/.aether/commands/colonize.yaml +442 -0
  30. package/.aether/commands/continue.yaml +1484 -0
  31. package/.aether/commands/council.yaml +509 -0
  32. package/.aether/commands/data-clean.yaml +80 -0
  33. package/.aether/commands/dream.yaml +275 -0
  34. package/.aether/commands/entomb.yaml +863 -0
  35. package/.aether/commands/export-signals.yaml +64 -0
  36. package/.aether/commands/feedback.yaml +158 -0
  37. package/.aether/commands/flag.yaml +160 -0
  38. package/.aether/commands/flags.yaml +177 -0
  39. package/.aether/commands/focus.yaml +112 -0
  40. package/.aether/commands/help.yaml +167 -0
  41. package/.aether/commands/history.yaml +137 -0
  42. package/.aether/commands/import-signals.yaml +79 -0
  43. package/.aether/commands/init.yaml +502 -0
  44. package/.aether/commands/insert-phase.yaml +102 -0
  45. package/.aether/commands/interpret.yaml +285 -0
  46. package/.aether/commands/lay-eggs.yaml +224 -0
  47. package/.aether/commands/maturity.yaml +122 -0
  48. package/.aether/commands/memory-details.yaml +74 -0
  49. package/.aether/commands/migrate-state.yaml +174 -0
  50. package/.aether/commands/oracle.yaml +1224 -0
  51. package/.aether/commands/organize.yaml +446 -0
  52. package/.aether/commands/patrol.yaml +621 -0
  53. package/.aether/commands/pause-colony.yaml +424 -0
  54. package/.aether/commands/phase.yaml +124 -0
  55. package/.aether/commands/pheromones.yaml +153 -0
  56. package/.aether/commands/plan.yaml +1364 -0
  57. package/.aether/commands/preferences.yaml +63 -0
  58. package/.aether/commands/quick.yaml +104 -0
  59. package/.aether/commands/redirect.yaml +123 -0
  60. package/.aether/commands/resume-colony.yaml +375 -0
  61. package/.aether/commands/resume.yaml +407 -0
  62. package/.aether/commands/run.yaml +229 -0
  63. package/.aether/commands/seal.yaml +1214 -0
  64. package/.aether/commands/skill-create.yaml +337 -0
  65. package/.aether/commands/status.yaml +408 -0
  66. package/.aether/commands/swarm.yaml +352 -0
  67. package/.aether/commands/tunnels.yaml +814 -0
  68. package/.aether/commands/update.yaml +131 -0
  69. package/.aether/commands/verify-castes.yaml +159 -0
  70. package/.aether/commands/watch.yaml +454 -0
  71. package/.aether/docs/INCIDENT_TEMPLATE.md +32 -0
  72. package/.aether/docs/QUEEN-SYSTEM.md +11 -11
  73. package/.aether/docs/README.md +32 -2
  74. package/.aether/docs/command-playbooks/README.md +23 -0
  75. package/.aether/docs/command-playbooks/build-complete.md +349 -0
  76. package/.aether/docs/command-playbooks/build-context.md +282 -0
  77. package/.aether/docs/command-playbooks/build-full.md +1683 -0
  78. package/.aether/docs/command-playbooks/build-prep.md +284 -0
  79. package/.aether/docs/command-playbooks/build-verify.md +405 -0
  80. package/.aether/docs/command-playbooks/build-wave.md +749 -0
  81. package/.aether/docs/command-playbooks/continue-advance.md +524 -0
  82. package/.aether/docs/command-playbooks/continue-finalize.md +447 -0
  83. package/.aether/docs/command-playbooks/continue-full.md +1725 -0
  84. package/.aether/docs/command-playbooks/continue-gates.md +686 -0
  85. package/.aether/docs/command-playbooks/continue-verify.md +407 -0
  86. package/.aether/docs/context-continuity.md +84 -0
  87. package/.aether/docs/disciplines/DISCIPLINES.md +9 -7
  88. package/.aether/docs/error-codes.md +1 -1
  89. package/.aether/docs/known-issues.md +34 -173
  90. package/.aether/docs/pheromones.md +86 -6
  91. package/.aether/docs/plans/pheromone-display-plan.md +257 -0
  92. package/.aether/docs/queen-commands.md +10 -9
  93. package/.aether/docs/source-of-truth-map.md +132 -0
  94. package/.aether/docs/xml-utilities.md +47 -0
  95. package/.aether/rules/aether-colony.md +23 -13
  96. package/.aether/scripts/incident-test-add.sh +47 -0
  97. package/.aether/scripts/weekly-audit.sh +79 -0
  98. package/.aether/skills/.index.json +649 -0
  99. package/.aether/skills/colony/.manifest.json +16 -0
  100. package/.aether/skills/colony/build-discipline/SKILL.md +78 -0
  101. package/.aether/skills/colony/colony-interaction/SKILL.md +56 -0
  102. package/.aether/skills/colony/colony-lifecycle/SKILL.md +77 -0
  103. package/.aether/skills/colony/colony-visuals/SKILL.md +112 -0
  104. package/.aether/skills/colony/context-management/SKILL.md +80 -0
  105. package/.aether/skills/colony/error-presentation/SKILL.md +99 -0
  106. package/.aether/skills/colony/pheromone-protocol/SKILL.md +79 -0
  107. package/.aether/skills/colony/pheromone-visibility/SKILL.md +81 -0
  108. package/.aether/skills/colony/state-safety/SKILL.md +84 -0
  109. package/.aether/skills/colony/worker-priming/SKILL.md +82 -0
  110. package/.aether/skills/domain/.manifest.json +24 -0
  111. package/.aether/skills/domain/README.md +33 -0
  112. package/.aether/skills/domain/django/SKILL.md +49 -0
  113. package/.aether/skills/domain/docker/SKILL.md +52 -0
  114. package/.aether/skills/domain/golang/SKILL.md +52 -0
  115. package/.aether/skills/domain/graphql/SKILL.md +51 -0
  116. package/.aether/skills/domain/html-css/SKILL.md +48 -0
  117. package/.aether/skills/domain/nextjs/SKILL.md +45 -0
  118. package/.aether/skills/domain/nodejs/SKILL.md +53 -0
  119. package/.aether/skills/domain/postgresql/SKILL.md +53 -0
  120. package/.aether/skills/domain/prisma/SKILL.md +59 -0
  121. package/.aether/skills/domain/python/SKILL.md +50 -0
  122. package/.aether/skills/domain/rails/SKILL.md +52 -0
  123. package/.aether/skills/domain/react/SKILL.md +45 -0
  124. package/.aether/skills/domain/rest-api/SKILL.md +58 -0
  125. package/.aether/skills/domain/svelte/SKILL.md +47 -0
  126. package/.aether/skills/domain/tailwind/SKILL.md +45 -0
  127. package/.aether/skills/domain/testing/SKILL.md +53 -0
  128. package/.aether/skills/domain/typescript/SKILL.md +58 -0
  129. package/.aether/skills/domain/vue/SKILL.md +49 -0
  130. package/.aether/templates/QUEEN.md.template +23 -41
  131. package/.aether/templates/colony-state-reset.jq.template +1 -0
  132. package/.aether/templates/colony-state.template.json +4 -0
  133. package/.aether/templates/learning-observations.template.json +6 -0
  134. package/.aether/templates/midden.template.json +13 -0
  135. package/.aether/templates/pheromones.template.json +6 -0
  136. package/.aether/templates/session.template.json +9 -0
  137. package/.aether/utils/atomic-write.sh +63 -17
  138. package/.aether/utils/chamber-utils.sh +145 -2
  139. package/.aether/utils/council.sh +425 -0
  140. package/.aether/utils/emoji-audit.sh +166 -0
  141. package/.aether/utils/error-handler.sh +21 -7
  142. package/.aether/utils/file-lock.sh +182 -27
  143. package/.aether/utils/flag.sh +278 -0
  144. package/.aether/utils/hive.sh +572 -0
  145. package/.aether/utils/immune.sh +508 -0
  146. package/.aether/utils/learning.sh +1928 -0
  147. package/.aether/utils/midden.sh +520 -0
  148. package/.aether/utils/oracle/oracle.md +168 -0
  149. package/.aether/utils/oracle/oracle.sh +1023 -0
  150. package/.aether/utils/pheromone.sh +2029 -0
  151. package/.aether/utils/queen.sh +1710 -0
  152. package/.aether/utils/scan.sh +860 -0
  153. package/.aether/utils/semantic-cli.sh +10 -8
  154. package/.aether/utils/session.sh +816 -0
  155. package/.aether/utils/skills.sh +509 -0
  156. package/.aether/utils/spawn-tree.sh +103 -271
  157. package/.aether/utils/spawn.sh +260 -0
  158. package/.aether/utils/state-api.sh +389 -0
  159. package/.aether/utils/state-loader.sh +8 -6
  160. package/.aether/utils/suggest.sh +611 -0
  161. package/.aether/utils/swarm-display.sh +10 -1
  162. package/.aether/utils/swarm.sh +1004 -0
  163. package/.aether/utils/watch-spawn-tree.sh +11 -2
  164. package/.aether/utils/xml-compose.sh +2 -2
  165. package/.aether/utils/xml-convert.sh +9 -5
  166. package/.aether/utils/xml-core.sh +5 -9
  167. package/.aether/utils/xml-query.sh +4 -4
  168. package/.aether/workers.md +86 -67
  169. package/.claude/agents/ant/aether-ambassador.md +2 -1
  170. package/.claude/agents/ant/aether-archaeologist.md +6 -1
  171. package/.claude/agents/ant/aether-architect.md +236 -0
  172. package/.claude/agents/ant/aether-auditor.md +6 -1
  173. package/.claude/agents/ant/aether-builder.md +38 -1
  174. package/.claude/agents/ant/aether-chaos.md +2 -1
  175. package/.claude/agents/ant/aether-chronicler.md +1 -0
  176. package/.claude/agents/ant/aether-gatekeeper.md +6 -1
  177. package/.claude/agents/ant/aether-includer.md +1 -0
  178. package/.claude/agents/ant/aether-keeper.md +1 -0
  179. package/.claude/agents/ant/aether-measurer.md +6 -1
  180. package/.claude/agents/ant/aether-oracle.md +237 -0
  181. package/.claude/agents/ant/aether-probe.md +2 -1
  182. package/.claude/agents/ant/aether-queen.md +6 -1
  183. package/.claude/agents/ant/aether-route-setter.md +6 -1
  184. package/.claude/agents/ant/aether-sage.md +68 -3
  185. package/.claude/agents/ant/aether-scout.md +38 -1
  186. package/.claude/agents/ant/aether-surveyor-disciplines.md +2 -1
  187. package/.claude/agents/ant/aether-surveyor-nest.md +2 -1
  188. package/.claude/agents/ant/aether-surveyor-pathogens.md +2 -1
  189. package/.claude/agents/ant/aether-surveyor-provisions.md +2 -1
  190. package/.claude/agents/ant/aether-tracker.md +6 -1
  191. package/.claude/agents/ant/aether-watcher.md +37 -1
  192. package/.claude/agents/ant/aether-weaver.md +2 -1
  193. package/.claude/commands/ant/archaeology.md +1 -8
  194. package/.claude/commands/ant/build.md +43 -1159
  195. package/.claude/commands/ant/chaos.md +1 -14
  196. package/.claude/commands/ant/colonize.md +3 -14
  197. package/.claude/commands/ant/continue.md +40 -1026
  198. package/.claude/commands/ant/council.md +213 -15
  199. package/.claude/commands/ant/data-clean.md +81 -0
  200. package/.claude/commands/ant/dream.md +12 -9
  201. package/.claude/commands/ant/entomb.md +62 -87
  202. package/.claude/commands/ant/export-signals.md +57 -0
  203. package/.claude/commands/ant/feedback.md +18 -0
  204. package/.claude/commands/ant/flag.md +12 -0
  205. package/.claude/commands/ant/flags.md +22 -8
  206. package/.claude/commands/ant/focus.md +18 -0
  207. package/.claude/commands/ant/help.md +40 -8
  208. package/.claude/commands/ant/history.md +3 -0
  209. package/.claude/commands/ant/import-signals.md +71 -0
  210. package/.claude/commands/ant/init.md +349 -191
  211. package/.claude/commands/ant/insert-phase.md +105 -0
  212. package/.claude/commands/ant/interpret.md +11 -0
  213. package/.claude/commands/ant/lay-eggs.md +167 -158
  214. package/.claude/commands/ant/maturity.md +22 -11
  215. package/.claude/commands/ant/memory-details.md +77 -0
  216. package/.claude/commands/ant/migrate-state.md +6 -0
  217. package/.claude/commands/ant/oracle.md +317 -62
  218. package/.claude/commands/ant/organize.md +10 -5
  219. package/.claude/commands/ant/patrol.md +620 -0
  220. package/.claude/commands/ant/pause-colony.md +8 -22
  221. package/.claude/commands/ant/phase.md +26 -37
  222. package/.claude/commands/ant/pheromones.md +156 -0
  223. package/.claude/commands/ant/plan.md +199 -50
  224. package/.claude/commands/ant/preferences.md +65 -0
  225. package/.claude/commands/ant/quick.md +100 -0
  226. package/.claude/commands/ant/redirect.md +18 -0
  227. package/.claude/commands/ant/resume-colony.md +37 -22
  228. package/.claude/commands/ant/resume.md +60 -7
  229. package/.claude/commands/ant/run.md +231 -0
  230. package/.claude/commands/ant/seal.md +506 -78
  231. package/.claude/commands/ant/skill-create.md +286 -0
  232. package/.claude/commands/ant/status.md +171 -1
  233. package/.claude/commands/ant/swarm.md +11 -23
  234. package/.claude/commands/ant/tunnels.md +1 -0
  235. package/.claude/commands/ant/update.md +58 -135
  236. package/.claude/commands/ant/verify-castes.md +90 -42
  237. package/.claude/commands/ant/watch.md +1 -0
  238. package/.opencode/agents/aether-ambassador.md +1 -1
  239. package/.opencode/agents/aether-architect.md +133 -0
  240. package/.opencode/agents/aether-builder.md +3 -3
  241. package/.opencode/agents/aether-oracle.md +137 -0
  242. package/.opencode/agents/aether-queen.md +1 -1
  243. package/.opencode/agents/aether-route-setter.md +1 -1
  244. package/.opencode/agents/aether-scout.md +1 -1
  245. package/.opencode/agents/aether-surveyor-disciplines.md +6 -1
  246. package/.opencode/agents/aether-surveyor-nest.md +6 -1
  247. package/.opencode/agents/aether-surveyor-pathogens.md +6 -1
  248. package/.opencode/agents/aether-surveyor-provisions.md +6 -1
  249. package/.opencode/agents/aether-tracker.md +1 -1
  250. package/.opencode/agents/aether-watcher.md +1 -1
  251. package/.opencode/agents/aether-weaver.md +1 -1
  252. package/.opencode/commands/ant/archaeology.md +7 -14
  253. package/.opencode/commands/ant/build.md +54 -88
  254. package/.opencode/commands/ant/chaos.md +7 -24
  255. package/.opencode/commands/ant/colonize.md +10 -17
  256. package/.opencode/commands/ant/continue.md +595 -66
  257. package/.opencode/commands/ant/council.md +150 -18
  258. package/.opencode/commands/ant/data-clean.md +77 -0
  259. package/.opencode/commands/ant/dream.md +15 -17
  260. package/.opencode/commands/ant/entomb.md +28 -18
  261. package/.opencode/commands/ant/export-signals.md +54 -0
  262. package/.opencode/commands/ant/feedback.md +24 -5
  263. package/.opencode/commands/ant/flag.md +16 -4
  264. package/.opencode/commands/ant/flags.md +24 -10
  265. package/.opencode/commands/ant/focus.md +22 -5
  266. package/.opencode/commands/ant/help.md +41 -8
  267. package/.opencode/commands/ant/history.md +9 -0
  268. package/.opencode/commands/ant/import-signals.md +68 -0
  269. package/.opencode/commands/ant/init.md +396 -154
  270. package/.opencode/commands/ant/insert-phase.md +111 -0
  271. package/.opencode/commands/ant/interpret.md +16 -0
  272. package/.opencode/commands/ant/lay-eggs.md +184 -112
  273. package/.opencode/commands/ant/maturity.md +18 -2
  274. package/.opencode/commands/ant/memory-details.md +83 -0
  275. package/.opencode/commands/ant/migrate-state.md +12 -0
  276. package/.opencode/commands/ant/oracle.md +322 -67
  277. package/.opencode/commands/ant/organize.md +14 -12
  278. package/.opencode/commands/ant/patrol.md +626 -0
  279. package/.opencode/commands/ant/pause-colony.md +12 -29
  280. package/.opencode/commands/ant/phase.md +30 -40
  281. package/.opencode/commands/ant/pheromones.md +162 -0
  282. package/.opencode/commands/ant/plan.md +210 -57
  283. package/.opencode/commands/ant/preferences.md +71 -0
  284. package/.opencode/commands/ant/quick.md +91 -0
  285. package/.opencode/commands/ant/redirect.md +22 -5
  286. package/.opencode/commands/ant/resume-colony.md +41 -29
  287. package/.opencode/commands/ant/resume.md +80 -20
  288. package/.opencode/commands/ant/run.md +237 -0
  289. package/.opencode/commands/ant/seal.md +230 -25
  290. package/.opencode/commands/ant/skill-create.md +63 -0
  291. package/.opencode/commands/ant/status.md +125 -30
  292. package/.opencode/commands/ant/swarm.md +3 -345
  293. package/.opencode/commands/ant/tunnels.md +3 -9
  294. package/.opencode/commands/ant/update.md +63 -127
  295. package/.opencode/commands/ant/verify-castes.md +96 -42
  296. package/.opencode/commands/ant/watch.md +7 -0
  297. package/CHANGELOG.md +368 -1
  298. package/README.md +195 -324
  299. package/bin/cli.js +236 -429
  300. package/bin/generate-commands.js +186 -0
  301. package/bin/generate-commands.sh +128 -89
  302. package/bin/lib/spawn-logger.js +0 -15
  303. package/bin/lib/update-transaction.js +285 -35
  304. package/bin/npx-install.js +178 -0
  305. package/bin/validate-package.sh +85 -3
  306. package/package.json +16 -4
  307. package/.aether/CONTEXT.md +0 -160
  308. package/.aether/docs/QUEEN.md +0 -84
  309. package/.aether/exchange/colony-registry.xml +0 -11
  310. package/.aether/exchange/pheromones.xml +0 -87
  311. package/.aether/exchange/queen-wisdom.xml +0 -14
  312. package/.aether/model-profiles.yaml +0 -100
  313. package/.aether/utils/spawn-with-model.sh +0 -56
  314. package/bin/lib/model-profiles.js +0 -445
  315. package/bin/lib/model-verify.js +0 -288
  316. package/bin/lib/proxy-health.js +0 -253
  317. package/bin/lib/telemetry.js +0 -441
@@ -8,17 +8,20 @@
8
8
  # # ... critical section ...
9
9
  # release_lock
10
10
 
11
- # Aether root detection - use git root if available, otherwise use current directory
12
- if git rev-parse --show-toplevel >/dev/null 2>&1; then
13
- AETHER_ROOT="$(git rev-parse --show-toplevel)"
14
- else
15
- AETHER_ROOT="$(pwd)"
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
16
18
  fi
17
19
 
18
20
  LOCK_DIR="$AETHER_ROOT/.aether/locks"
19
21
  LOCK_TIMEOUT=300 # 5 minutes max lock time
20
22
  LOCK_RETRY_INTERVAL=0.5 # Wait 500ms between retries
21
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
22
25
 
23
26
  # Fallback constant — ensures E_LOCK_STALE is defined whether or not error-handler.sh was loaded
24
27
  : "${E_LOCK_STALE:=E_LOCK_STALE}"
@@ -30,18 +33,37 @@ mkdir -p "$LOCK_DIR"
30
33
  # Arguments: file_path (the resource to lock)
31
34
  # Returns: 0 on success, 1 on failure
32
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.
33
39
  acquire_lock() {
34
40
  local file_path="$1"
35
41
  local lock_file="${LOCK_DIR}/$(basename "$file_path").lock"
36
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
37
52
 
38
53
  # Check if lock file exists and is stale
39
54
  if [ -f "$lock_file" ]; then
40
55
  local lock_pid
41
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
+
42
64
  local is_stale=false
43
65
 
44
- # Check age FIRST (before PID check) to handle PID reuse race
66
+ # Compute lock age for timeout-based stale checks when PID is unavailable.
45
67
  local lock_mtime=0
46
68
  # Platform-portable mtime: macOS uses stat -f %m, Linux uses stat -c %Y
47
69
  if stat -f %m "$lock_file" >/dev/null 2>&1; then
@@ -51,32 +73,47 @@ acquire_lock() {
51
73
  fi
52
74
  local lock_age=$(( $(date +%s) - lock_mtime ))
53
75
 
54
- if [[ $lock_age -gt $LOCK_TIMEOUT ]]; then
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
55
80
  is_stale=true
56
- elif [[ -n "$lock_pid" ]] && ! kill -0 "$lock_pid" 2>/dev/null; then
81
+ elif [[ -z "$lock_pid" ]] && [[ $lock_age -gt $LOCK_TIMEOUT ]]; then
57
82
  is_stale=true
58
83
  fi
59
84
 
60
85
  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
86
+ case "$stale_mode" in
87
+ auto)
70
88
  rm -f "$lock_file" "$lock_pid_file"
71
- else
72
- echo "Lock removal declined. Remove manually: rm $lock_file" >&2
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
73
114
  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
79
- fi
115
+ ;;
116
+ esac
80
117
  fi
81
118
  fi
82
119
 
@@ -85,7 +122,7 @@ acquire_lock() {
85
122
  while [ $retry_count -lt $LOCK_MAX_RETRIES ]; do
86
123
  # Try to create lock file atomically
87
124
  if (set -o noclobber; echo $$ > "$lock_file") 2>/dev/null; then
88
- echo $$ > "$lock_pid_file"
125
+ echo $$ > "$lock_pid_file" 2>/dev/null || true
89
126
  export LOCK_ACQUIRED=true
90
127
  export CURRENT_LOCK="$lock_file"
91
128
  return 0
@@ -113,11 +150,129 @@ release_lock() {
113
150
  return 1
114
151
  }
115
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
+
116
268
  # Cleanup function for script exit
117
269
  cleanup_locks() {
118
270
  if [ "$LOCK_ACQUIRED" = "true" ]; then
119
271
  release_lock
120
272
  fi
273
+ if [[ -n "${LOCK_AT_FILE:-}" ]]; then
274
+ release_lock_at "$LOCK_AT_FILE"
275
+ fi
121
276
  }
122
277
 
123
278
  # Register cleanup on exit — includes HUP for SSH disconnect safety
@@ -155,4 +310,4 @@ wait_for_lock() {
155
310
  }
156
311
 
157
312
  # Export functions for use in other scripts
158
- export -f acquire_lock release_lock is_locked get_lock_holder wait_for_lock cleanup_locks
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,278 @@
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
+ # Validate filter_phase as numeric (safe for --argjson)
208
+ if [[ -n "$filter_phase" && "$filter_phase" =~ ^[0-9]+$ ]]; then
209
+ phase_num="$filter_phase"
210
+ else
211
+ phase_num=""
212
+ fi
213
+
214
+ # Build jq command with --arg to prevent filter injection
215
+ # filter_type is passed via --arg (safe string interpolation in jq)
216
+ # phase_num is passed via --argjson (numeric-only, pre-validated)
217
+ if [[ -n "$phase_num" ]]; then
218
+ result=$(jq --arg ft "$filter_type" --argjson ph "$phase_num" --argjson all "$show_all" '
219
+ .flags
220
+ | (if $all | not then [.[] | select(.resolved_at == null)] else . end)
221
+ | (if $ft != "" then [.[] | select(.type == $ft)] else . end)
222
+ | (if $ph != null then [.[] | select(.phase == $ph or .phase == null)] else . end)
223
+ | {flags: ., count: length}
224
+ ' "$flags_file")
225
+ else
226
+ result=$(jq --arg ft "$filter_type" --argjson all "$show_all" '
227
+ .flags
228
+ | (if $all | not then [.[] | select(.resolved_at == null)] else . end)
229
+ | (if $ft != "" then [.[] | select(.type == $ft)] else . end)
230
+ | {flags: ., count: length}
231
+ ' "$flags_file")
232
+ fi
233
+
234
+ json_ok "$result"
235
+ }
236
+
237
+ _flag_auto_resolve() {
238
+ # Auto-resolve flags based on trigger (e.g., build_pass)
239
+ # Usage: flag-auto-resolve <trigger>
240
+ trigger="${1:-build_pass}"
241
+ flags_file="$COLONY_DATA_DIR/flags.json"
242
+
243
+ if [[ ! -f "$flags_file" ]]; then json_ok '{"resolved":0}'; exit 0; fi
244
+
245
+ ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
246
+
247
+ # Acquire lock for atomic flag update (degrade gracefully if locking unavailable)
248
+ if type feature_enabled &>/dev/null && ! feature_enabled "file_locking"; then
249
+ json_warn "W_DEGRADED" "File locking disabled - proceeding without lock"
250
+ else
251
+ acquire_lock "$flags_file" || json_err "$E_LOCK_FAILED" "Failed to acquire lock on flags.json"
252
+ # Ensure lock is always released on exit (BUG-005/BUG-011 fix)
253
+ trap 'release_lock 2>/dev/null || true' EXIT # SUPPRESS:OK -- cleanup: lock may not be held
254
+ fi
255
+
256
+ # Count how many will be resolved
257
+ count=$(jq --arg trigger "$trigger" '
258
+ [.flags[] | select(.auto_resolve_on == $trigger and .resolved_at == null)] | length
259
+ ' "$flags_file") || {
260
+ json_err "$E_JSON_INVALID" "Failed to count flags for auto-resolve"
261
+ }
262
+
263
+ # Resolve them
264
+ updated=$(jq --arg trigger "$trigger" --arg ts "$ts" '
265
+ .flags = [.flags[] | if .auto_resolve_on == $trigger and .resolved_at == null then
266
+ .resolved_at = $ts |
267
+ .resolution = "Auto-resolved on " + $trigger
268
+ else . end]
269
+ ' "$flags_file") || {
270
+ json_err "$E_JSON_INVALID" "Failed to auto-resolve flags"
271
+ }
272
+
273
+ atomic_write "$flags_file" "$updated"
274
+ trap - EXIT
275
+ release_lock 2>/dev/null || true # SUPPRESS:OK -- cleanup: lock may not be held
276
+ json_ok "$(jq -n --argjson count "$count" --arg trigger "$trigger" \
277
+ '{resolved: $count, trigger: $trigger}')"
278
+ }