aether-colony 5.0.0 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.aether/aether-utils.sh +3226 -3345
- package/.aether/agents-claude/aether-ambassador.md +265 -0
- package/.aether/agents-claude/aether-archaeologist.md +327 -0
- package/.aether/agents-claude/aether-architect.md +236 -0
- package/.aether/agents-claude/aether-auditor.md +271 -0
- package/.aether/agents-claude/aether-builder.md +224 -0
- package/.aether/agents-claude/aether-chaos.md +269 -0
- package/.aether/agents-claude/aether-chronicler.md +305 -0
- package/.aether/agents-claude/aether-gatekeeper.md +330 -0
- package/.aether/agents-claude/aether-includer.md +374 -0
- package/.aether/agents-claude/aether-keeper.md +272 -0
- package/.aether/agents-claude/aether-measurer.md +322 -0
- package/.aether/agents-claude/aether-oracle.md +237 -0
- package/.aether/agents-claude/aether-probe.md +211 -0
- package/.aether/agents-claude/aether-queen.md +330 -0
- package/.aether/agents-claude/aether-route-setter.md +178 -0
- package/.aether/agents-claude/aether-sage.md +418 -0
- package/.aether/agents-claude/aether-scout.md +179 -0
- package/.aether/agents-claude/aether-surveyor-disciplines.md +417 -0
- package/.aether/agents-claude/aether-surveyor-nest.md +355 -0
- package/.aether/agents-claude/aether-surveyor-pathogens.md +289 -0
- package/.aether/agents-claude/aether-surveyor-provisions.md +360 -0
- package/.aether/agents-claude/aether-tracker.md +270 -0
- package/.aether/agents-claude/aether-watcher.md +280 -0
- package/.aether/agents-claude/aether-weaver.md +248 -0
- package/.aether/commands/archaeology.yaml +653 -0
- package/.aether/commands/build.yaml +1221 -0
- package/.aether/commands/chaos.yaml +653 -0
- package/.aether/commands/colonize.yaml +442 -0
- package/.aether/commands/continue.yaml +1484 -0
- package/.aether/commands/council.yaml +509 -0
- package/.aether/commands/data-clean.yaml +80 -0
- package/.aether/commands/dream.yaml +275 -0
- package/.aether/commands/entomb.yaml +863 -0
- package/.aether/commands/export-signals.yaml +64 -0
- package/.aether/commands/feedback.yaml +158 -0
- package/.aether/commands/flag.yaml +160 -0
- package/.aether/commands/flags.yaml +177 -0
- package/.aether/commands/focus.yaml +112 -0
- package/.aether/commands/help.yaml +167 -0
- package/.aether/commands/history.yaml +137 -0
- package/.aether/commands/import-signals.yaml +79 -0
- package/.aether/commands/init.yaml +502 -0
- package/.aether/commands/insert-phase.yaml +102 -0
- package/.aether/commands/interpret.yaml +285 -0
- package/.aether/commands/lay-eggs.yaml +224 -0
- package/.aether/commands/maturity.yaml +122 -0
- package/.aether/commands/memory-details.yaml +74 -0
- package/.aether/commands/migrate-state.yaml +174 -0
- package/.aether/commands/oracle.yaml +1224 -0
- package/.aether/commands/organize.yaml +446 -0
- package/.aether/commands/patrol.yaml +621 -0
- package/.aether/commands/pause-colony.yaml +424 -0
- package/.aether/commands/phase.yaml +124 -0
- package/.aether/commands/pheromones.yaml +153 -0
- package/.aether/commands/plan.yaml +1364 -0
- package/.aether/commands/preferences.yaml +63 -0
- package/.aether/commands/quick.yaml +104 -0
- package/.aether/commands/redirect.yaml +123 -0
- package/.aether/commands/resume-colony.yaml +375 -0
- package/.aether/commands/resume.yaml +407 -0
- package/.aether/commands/run.yaml +229 -0
- package/.aether/commands/seal.yaml +1214 -0
- package/.aether/commands/skill-create.yaml +337 -0
- package/.aether/commands/status.yaml +408 -0
- package/.aether/commands/swarm.yaml +352 -0
- package/.aether/commands/tunnels.yaml +814 -0
- package/.aether/commands/update.yaml +131 -0
- package/.aether/commands/verify-castes.yaml +159 -0
- package/.aether/commands/watch.yaml +454 -0
- package/.aether/docs/INCIDENT_TEMPLATE.md +32 -0
- package/.aether/docs/QUEEN-SYSTEM.md +11 -11
- package/.aether/docs/README.md +32 -2
- package/.aether/docs/command-playbooks/README.md +23 -0
- package/.aether/docs/command-playbooks/build-complete.md +349 -0
- package/.aether/docs/command-playbooks/build-context.md +282 -0
- package/.aether/docs/command-playbooks/build-full.md +1683 -0
- package/.aether/docs/command-playbooks/build-prep.md +284 -0
- package/.aether/docs/command-playbooks/build-verify.md +405 -0
- package/.aether/docs/command-playbooks/build-wave.md +749 -0
- package/.aether/docs/command-playbooks/continue-advance.md +524 -0
- package/.aether/docs/command-playbooks/continue-finalize.md +447 -0
- package/.aether/docs/command-playbooks/continue-full.md +1725 -0
- package/.aether/docs/command-playbooks/continue-gates.md +686 -0
- package/.aether/docs/command-playbooks/continue-verify.md +407 -0
- package/.aether/docs/context-continuity.md +84 -0
- package/.aether/docs/disciplines/DISCIPLINES.md +9 -7
- package/.aether/docs/error-codes.md +1 -1
- package/.aether/docs/known-issues.md +34 -173
- package/.aether/docs/pheromones.md +86 -6
- package/.aether/docs/plans/pheromone-display-plan.md +257 -0
- package/.aether/docs/queen-commands.md +10 -9
- package/.aether/docs/source-of-truth-map.md +132 -0
- package/.aether/docs/xml-utilities.md +47 -0
- package/.aether/rules/aether-colony.md +23 -13
- package/.aether/scripts/incident-test-add.sh +47 -0
- package/.aether/scripts/weekly-audit.sh +79 -0
- package/.aether/skills/.index.json +649 -0
- package/.aether/skills/colony/.manifest.json +16 -0
- package/.aether/skills/colony/build-discipline/SKILL.md +78 -0
- package/.aether/skills/colony/colony-interaction/SKILL.md +56 -0
- package/.aether/skills/colony/colony-lifecycle/SKILL.md +77 -0
- package/.aether/skills/colony/colony-visuals/SKILL.md +112 -0
- package/.aether/skills/colony/context-management/SKILL.md +80 -0
- package/.aether/skills/colony/error-presentation/SKILL.md +99 -0
- package/.aether/skills/colony/pheromone-protocol/SKILL.md +79 -0
- package/.aether/skills/colony/pheromone-visibility/SKILL.md +81 -0
- package/.aether/skills/colony/state-safety/SKILL.md +84 -0
- package/.aether/skills/colony/worker-priming/SKILL.md +82 -0
- package/.aether/skills/domain/.manifest.json +24 -0
- package/.aether/skills/domain/README.md +33 -0
- package/.aether/skills/domain/django/SKILL.md +49 -0
- package/.aether/skills/domain/docker/SKILL.md +52 -0
- package/.aether/skills/domain/golang/SKILL.md +52 -0
- package/.aether/skills/domain/graphql/SKILL.md +51 -0
- package/.aether/skills/domain/html-css/SKILL.md +48 -0
- package/.aether/skills/domain/nextjs/SKILL.md +45 -0
- package/.aether/skills/domain/nodejs/SKILL.md +53 -0
- package/.aether/skills/domain/postgresql/SKILL.md +53 -0
- package/.aether/skills/domain/prisma/SKILL.md +59 -0
- package/.aether/skills/domain/python/SKILL.md +50 -0
- package/.aether/skills/domain/rails/SKILL.md +52 -0
- package/.aether/skills/domain/react/SKILL.md +45 -0
- package/.aether/skills/domain/rest-api/SKILL.md +58 -0
- package/.aether/skills/domain/svelte/SKILL.md +47 -0
- package/.aether/skills/domain/tailwind/SKILL.md +45 -0
- package/.aether/skills/domain/testing/SKILL.md +53 -0
- package/.aether/skills/domain/typescript/SKILL.md +58 -0
- package/.aether/skills/domain/vue/SKILL.md +49 -0
- package/.aether/templates/QUEEN.md.template +23 -41
- package/.aether/templates/colony-state-reset.jq.template +1 -0
- package/.aether/templates/colony-state.template.json +4 -0
- package/.aether/templates/learning-observations.template.json +6 -0
- package/.aether/templates/midden.template.json +13 -0
- package/.aether/templates/pheromones.template.json +6 -0
- package/.aether/templates/session.template.json +9 -0
- package/.aether/utils/atomic-write.sh +63 -17
- package/.aether/utils/chamber-utils.sh +145 -2
- package/.aether/utils/council.sh +425 -0
- package/.aether/utils/emoji-audit.sh +166 -0
- package/.aether/utils/error-handler.sh +21 -7
- package/.aether/utils/file-lock.sh +182 -27
- package/.aether/utils/flag.sh +278 -0
- package/.aether/utils/hive.sh +572 -0
- package/.aether/utils/immune.sh +508 -0
- package/.aether/utils/learning.sh +1928 -0
- package/.aether/utils/midden.sh +520 -0
- package/.aether/utils/oracle/oracle.md +168 -0
- package/.aether/utils/oracle/oracle.sh +1023 -0
- package/.aether/utils/pheromone.sh +2029 -0
- package/.aether/utils/queen.sh +1710 -0
- package/.aether/utils/scan.sh +860 -0
- package/.aether/utils/semantic-cli.sh +10 -8
- package/.aether/utils/session.sh +816 -0
- package/.aether/utils/skills.sh +509 -0
- package/.aether/utils/spawn-tree.sh +103 -271
- package/.aether/utils/spawn.sh +260 -0
- package/.aether/utils/state-api.sh +389 -0
- package/.aether/utils/state-loader.sh +8 -6
- package/.aether/utils/suggest.sh +611 -0
- package/.aether/utils/swarm-display.sh +10 -1
- package/.aether/utils/swarm.sh +1004 -0
- package/.aether/utils/watch-spawn-tree.sh +11 -2
- package/.aether/utils/xml-compose.sh +2 -2
- package/.aether/utils/xml-convert.sh +9 -5
- package/.aether/utils/xml-core.sh +5 -9
- package/.aether/utils/xml-query.sh +4 -4
- package/.aether/workers.md +86 -67
- package/.claude/agents/ant/aether-ambassador.md +2 -1
- package/.claude/agents/ant/aether-archaeologist.md +6 -1
- package/.claude/agents/ant/aether-architect.md +236 -0
- package/.claude/agents/ant/aether-auditor.md +6 -1
- package/.claude/agents/ant/aether-builder.md +38 -1
- package/.claude/agents/ant/aether-chaos.md +2 -1
- package/.claude/agents/ant/aether-chronicler.md +1 -0
- package/.claude/agents/ant/aether-gatekeeper.md +6 -1
- package/.claude/agents/ant/aether-includer.md +1 -0
- package/.claude/agents/ant/aether-keeper.md +1 -0
- package/.claude/agents/ant/aether-measurer.md +6 -1
- package/.claude/agents/ant/aether-oracle.md +237 -0
- package/.claude/agents/ant/aether-probe.md +2 -1
- package/.claude/agents/ant/aether-queen.md +6 -1
- package/.claude/agents/ant/aether-route-setter.md +6 -1
- package/.claude/agents/ant/aether-sage.md +68 -3
- package/.claude/agents/ant/aether-scout.md +38 -1
- package/.claude/agents/ant/aether-surveyor-disciplines.md +2 -1
- package/.claude/agents/ant/aether-surveyor-nest.md +2 -1
- package/.claude/agents/ant/aether-surveyor-pathogens.md +2 -1
- package/.claude/agents/ant/aether-surveyor-provisions.md +2 -1
- package/.claude/agents/ant/aether-tracker.md +6 -1
- package/.claude/agents/ant/aether-watcher.md +37 -1
- package/.claude/agents/ant/aether-weaver.md +2 -1
- package/.claude/commands/ant/archaeology.md +1 -8
- package/.claude/commands/ant/build.md +43 -1159
- package/.claude/commands/ant/chaos.md +1 -14
- package/.claude/commands/ant/colonize.md +3 -14
- package/.claude/commands/ant/continue.md +40 -1026
- package/.claude/commands/ant/council.md +213 -15
- package/.claude/commands/ant/data-clean.md +81 -0
- package/.claude/commands/ant/dream.md +12 -9
- package/.claude/commands/ant/entomb.md +62 -87
- package/.claude/commands/ant/export-signals.md +57 -0
- package/.claude/commands/ant/feedback.md +18 -0
- package/.claude/commands/ant/flag.md +12 -0
- package/.claude/commands/ant/flags.md +22 -8
- package/.claude/commands/ant/focus.md +18 -0
- package/.claude/commands/ant/help.md +40 -8
- package/.claude/commands/ant/history.md +3 -0
- package/.claude/commands/ant/import-signals.md +71 -0
- package/.claude/commands/ant/init.md +349 -191
- package/.claude/commands/ant/insert-phase.md +105 -0
- package/.claude/commands/ant/interpret.md +11 -0
- package/.claude/commands/ant/lay-eggs.md +167 -158
- package/.claude/commands/ant/maturity.md +22 -11
- package/.claude/commands/ant/memory-details.md +77 -0
- package/.claude/commands/ant/migrate-state.md +6 -0
- package/.claude/commands/ant/oracle.md +317 -62
- package/.claude/commands/ant/organize.md +10 -5
- package/.claude/commands/ant/patrol.md +620 -0
- package/.claude/commands/ant/pause-colony.md +8 -22
- package/.claude/commands/ant/phase.md +26 -37
- package/.claude/commands/ant/pheromones.md +156 -0
- package/.claude/commands/ant/plan.md +199 -50
- package/.claude/commands/ant/preferences.md +65 -0
- package/.claude/commands/ant/quick.md +100 -0
- package/.claude/commands/ant/redirect.md +18 -0
- package/.claude/commands/ant/resume-colony.md +37 -22
- package/.claude/commands/ant/resume.md +60 -7
- package/.claude/commands/ant/run.md +231 -0
- package/.claude/commands/ant/seal.md +506 -78
- package/.claude/commands/ant/skill-create.md +286 -0
- package/.claude/commands/ant/status.md +171 -1
- package/.claude/commands/ant/swarm.md +11 -23
- package/.claude/commands/ant/tunnels.md +1 -0
- package/.claude/commands/ant/update.md +58 -135
- package/.claude/commands/ant/verify-castes.md +90 -42
- package/.claude/commands/ant/watch.md +1 -0
- package/.opencode/agents/aether-ambassador.md +1 -1
- package/.opencode/agents/aether-architect.md +133 -0
- package/.opencode/agents/aether-builder.md +3 -3
- package/.opencode/agents/aether-oracle.md +137 -0
- package/.opencode/agents/aether-queen.md +1 -1
- package/.opencode/agents/aether-route-setter.md +1 -1
- package/.opencode/agents/aether-scout.md +1 -1
- package/.opencode/agents/aether-surveyor-disciplines.md +6 -1
- package/.opencode/agents/aether-surveyor-nest.md +6 -1
- package/.opencode/agents/aether-surveyor-pathogens.md +6 -1
- package/.opencode/agents/aether-surveyor-provisions.md +6 -1
- package/.opencode/agents/aether-tracker.md +1 -1
- package/.opencode/agents/aether-watcher.md +1 -1
- package/.opencode/agents/aether-weaver.md +1 -1
- package/.opencode/commands/ant/archaeology.md +7 -14
- package/.opencode/commands/ant/build.md +54 -88
- package/.opencode/commands/ant/chaos.md +7 -24
- package/.opencode/commands/ant/colonize.md +10 -17
- package/.opencode/commands/ant/continue.md +595 -66
- package/.opencode/commands/ant/council.md +150 -18
- package/.opencode/commands/ant/data-clean.md +77 -0
- package/.opencode/commands/ant/dream.md +15 -17
- package/.opencode/commands/ant/entomb.md +28 -18
- package/.opencode/commands/ant/export-signals.md +54 -0
- package/.opencode/commands/ant/feedback.md +24 -5
- package/.opencode/commands/ant/flag.md +16 -4
- package/.opencode/commands/ant/flags.md +24 -10
- package/.opencode/commands/ant/focus.md +22 -5
- package/.opencode/commands/ant/help.md +41 -8
- package/.opencode/commands/ant/history.md +9 -0
- package/.opencode/commands/ant/import-signals.md +68 -0
- package/.opencode/commands/ant/init.md +396 -154
- package/.opencode/commands/ant/insert-phase.md +111 -0
- package/.opencode/commands/ant/interpret.md +16 -0
- package/.opencode/commands/ant/lay-eggs.md +184 -112
- package/.opencode/commands/ant/maturity.md +18 -2
- package/.opencode/commands/ant/memory-details.md +83 -0
- package/.opencode/commands/ant/migrate-state.md +12 -0
- package/.opencode/commands/ant/oracle.md +322 -67
- package/.opencode/commands/ant/organize.md +14 -12
- package/.opencode/commands/ant/patrol.md +626 -0
- package/.opencode/commands/ant/pause-colony.md +12 -29
- package/.opencode/commands/ant/phase.md +30 -40
- package/.opencode/commands/ant/pheromones.md +162 -0
- package/.opencode/commands/ant/plan.md +210 -57
- package/.opencode/commands/ant/preferences.md +71 -0
- package/.opencode/commands/ant/quick.md +91 -0
- package/.opencode/commands/ant/redirect.md +22 -5
- package/.opencode/commands/ant/resume-colony.md +41 -29
- package/.opencode/commands/ant/resume.md +80 -20
- package/.opencode/commands/ant/run.md +237 -0
- package/.opencode/commands/ant/seal.md +230 -25
- package/.opencode/commands/ant/skill-create.md +63 -0
- package/.opencode/commands/ant/status.md +125 -30
- package/.opencode/commands/ant/swarm.md +3 -345
- package/.opencode/commands/ant/tunnels.md +3 -9
- package/.opencode/commands/ant/update.md +63 -127
- package/.opencode/commands/ant/verify-castes.md +96 -42
- package/.opencode/commands/ant/watch.md +7 -0
- package/CHANGELOG.md +368 -1
- package/README.md +195 -324
- package/bin/cli.js +236 -429
- package/bin/generate-commands.js +186 -0
- package/bin/generate-commands.sh +128 -89
- package/bin/lib/spawn-logger.js +0 -15
- package/bin/lib/update-transaction.js +285 -35
- package/bin/npx-install.js +178 -0
- package/bin/validate-package.sh +85 -3
- package/package.json +16 -4
- package/.aether/CONTEXT.md +0 -160
- package/.aether/docs/QUEEN.md +0 -84
- package/.aether/exchange/colony-registry.xml +0 -11
- package/.aether/exchange/pheromones.xml +0 -87
- package/.aether/exchange/queen-wisdom.xml +0 -14
- package/.aether/model-profiles.yaml +0 -100
- package/.aether/utils/spawn-with-model.sh +0 -56
- package/bin/lib/model-profiles.js +0 -445
- package/bin/lib/model-verify.js +0 -288
- package/bin/lib/proxy-health.js +0 -253
- package/bin/lib/telemetry.js +0 -441
|
@@ -8,17 +8,20 @@
|
|
|
8
8
|
# # ... critical section ...
|
|
9
9
|
# release_lock
|
|
10
10
|
|
|
11
|
-
# Aether root detection - use git root
|
|
12
|
-
if
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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 [[ -
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
+
}
|