agent-ops 공통 규칙/스킬/스크립트, 진입 파일(CLAUDE.md, GEMINI.md 등), AI 도구별 ignore 정책을 추가해 워크스페이스 공통 운영 체계를 적용한다.
370 lines
7.8 KiB
Bash
Executable file
370 lines
7.8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
AGENT_OPS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
PROJECT_ROOT="$(cd "$AGENT_OPS_DIR/.." && pwd)"
|
|
WORKSPACE_ROOT="$(cd "$PROJECT_ROOT/.." && pwd)"
|
|
LOCKS_FILE="${AGENT_ROADMAP_LOCKS_FILE:-$WORKSPACE_ROOT/.agent-roadmap-sync/locks.yaml}"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: roadmap-dependency-checker.sh <lock-id> [locks.yaml]
|
|
roadmap-dependency-checker.sh --find-milestone <identity> [locked|rely-on|both] [locks.yaml]
|
|
|
|
Validates locked, rely-on.target, and rely-on.status for <lock-id>.
|
|
Prints true when every rely-on.status is enable.
|
|
Prints false when at least one rely-on.status is disable.
|
|
With --find-milestone, prints matching lock ids as locked:<id> or rely-on:<id>,
|
|
or none when the milestone identity is not referenced.
|
|
|
|
Exit codes:
|
|
0 all dependencies are enabled
|
|
1 at least one dependency is still disabled
|
|
2 configuration, input, or parse error
|
|
For --find-milestone, exit code 0 means the lookup completed; stdout is either
|
|
matching entries or none.
|
|
EOF
|
|
}
|
|
|
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
usage
|
|
exit 0
|
|
fi
|
|
|
|
MODE="check"
|
|
LOCK_ID=""
|
|
FIND_IDENTITY=""
|
|
FIND_DIRECTION="both"
|
|
|
|
if [[ "${1:-}" == "--find-milestone" ]]; then
|
|
MODE="find"
|
|
if [[ $# -lt 2 || $# -gt 4 ]]; then
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
FIND_IDENTITY="$2"
|
|
if [[ -z "$FIND_IDENTITY" ]]; then
|
|
echo "error: milestone identity is required" >&2
|
|
exit 2
|
|
fi
|
|
if [[ $# -ge 3 ]]; then
|
|
case "$3" in
|
|
locked|rely-on|both)
|
|
FIND_DIRECTION="$3"
|
|
if [[ $# -eq 4 ]]; then
|
|
LOCKS_FILE="$4"
|
|
fi
|
|
;;
|
|
*)
|
|
if [[ $# -eq 3 ]]; then
|
|
LOCKS_FILE="$3"
|
|
else
|
|
usage >&2
|
|
exit 2
|
|
fi
|
|
;;
|
|
esac
|
|
fi
|
|
elif [[ $# -lt 1 || $# -gt 2 ]]; then
|
|
usage >&2
|
|
exit 2
|
|
else
|
|
LOCK_ID="$1"
|
|
if [[ -n "${2:-}" ]]; then
|
|
LOCKS_FILE="$2"
|
|
fi
|
|
fi
|
|
|
|
if [[ "$MODE" == "check" && -z "$LOCK_ID" ]]; then
|
|
echo "error: lock id is required" >&2
|
|
exit 2
|
|
fi
|
|
|
|
if [[ ! -r "$LOCKS_FILE" ]]; then
|
|
echo "error: locks file not readable: $LOCKS_FILE" >&2
|
|
exit 2
|
|
fi
|
|
|
|
if [[ "$MODE" == "find" ]]; then
|
|
awk -v identity="$FIND_IDENTITY" -v direction="$FIND_DIRECTION" '
|
|
function trim(value) {
|
|
gsub(/\r/, "", value)
|
|
sub(/^[[:space:]]+/, "", value)
|
|
sub(/[[:space:]]+$/, "", value)
|
|
return value
|
|
}
|
|
|
|
function unquote(value) {
|
|
value = trim(value)
|
|
sub(/[[:space:]]+#.*$/, "", value)
|
|
value = trim(value)
|
|
if (value ~ /^".*"$/ || value ~ /^'\''.*'\''$/) {
|
|
return substr(value, 2, length(value) - 2)
|
|
}
|
|
return trim(value)
|
|
}
|
|
|
|
function reset_entry() {
|
|
entry_id = ""
|
|
locked = ""
|
|
in_rely_on = 0
|
|
rely_count = 0
|
|
delete rely_targets
|
|
}
|
|
|
|
function finish_entry( i) {
|
|
if (entry_id == "") {
|
|
return
|
|
}
|
|
if ((direction == "both" || direction == "locked") && locked == identity) {
|
|
print "locked:" entry_id
|
|
found = 1
|
|
}
|
|
if (direction == "both" || direction == "rely-on") {
|
|
for (i = 1; i <= rely_count; i++) {
|
|
if (rely_targets[i] == identity) {
|
|
print "rely-on:" entry_id
|
|
found = 1
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
BEGIN {
|
|
found = 0
|
|
reset_entry()
|
|
}
|
|
|
|
/^[[:space:]]*($|#)/ {
|
|
next
|
|
}
|
|
|
|
/^[[:space:]]*-[[:space:]]*id:[[:space:]]*/ {
|
|
finish_entry()
|
|
reset_entry()
|
|
value = $0
|
|
sub(/^[[:space:]]*-[[:space:]]*id:[[:space:]]*/, "", value)
|
|
entry_id = unquote(value)
|
|
next
|
|
}
|
|
|
|
entry_id != "" && /^[[:space:]]*locked:[[:space:]]*/ {
|
|
value = $0
|
|
sub(/^[[:space:]]*locked:[[:space:]]*/, "", value)
|
|
locked = unquote(value)
|
|
next
|
|
}
|
|
|
|
entry_id != "" && /^[[:space:]]*rely-on:[[:space:]]*/ {
|
|
in_rely_on = 1
|
|
next
|
|
}
|
|
|
|
entry_id != "" && in_rely_on && /^[[:space:]]*-[[:space:]]*target:[[:space:]]*/ {
|
|
value = $0
|
|
sub(/^[[:space:]]*-[[:space:]]*target:[[:space:]]*/, "", value)
|
|
rely_targets[++rely_count] = unquote(value)
|
|
next
|
|
}
|
|
|
|
entry_id != "" && in_rely_on && /^[[:space:]]*target:[[:space:]]*/ {
|
|
value = $0
|
|
sub(/^[[:space:]]*target:[[:space:]]*/, "", value)
|
|
rely_targets[++rely_count] = unquote(value)
|
|
next
|
|
}
|
|
|
|
END {
|
|
finish_entry()
|
|
if (!found) {
|
|
print "none"
|
|
}
|
|
}
|
|
' "$LOCKS_FILE"
|
|
exit 0
|
|
fi
|
|
|
|
awk -v wanted="$LOCK_ID" '
|
|
function trim(value) {
|
|
gsub(/\r/, "", value)
|
|
sub(/^[[:space:]]+/, "", value)
|
|
sub(/[[:space:]]+$/, "", value)
|
|
return value
|
|
}
|
|
|
|
function unquote(value) {
|
|
value = trim(value)
|
|
sub(/[[:space:]]+#.*$/, "", value)
|
|
value = trim(value)
|
|
if (value ~ /^".*"$/ || value ~ /^'\''.*'\''$/) {
|
|
return substr(value, 2, length(value) - 2)
|
|
}
|
|
return trim(value)
|
|
}
|
|
|
|
function fail(message) {
|
|
error = message
|
|
printf "error: %s\n", error > "/dev/stderr"
|
|
exit 2
|
|
}
|
|
|
|
function finish_dependency() {
|
|
if (!selected || !in_dependency) {
|
|
return
|
|
}
|
|
if (!dependency_has_target) {
|
|
fail("missing rely-on.target for lock id: " wanted)
|
|
}
|
|
if (!dependency_has_status) {
|
|
fail("missing rely-on.status for lock id: " wanted)
|
|
}
|
|
in_dependency = 0
|
|
}
|
|
|
|
function finish_entry() {
|
|
if (!selected) {
|
|
return
|
|
}
|
|
finish_dependency()
|
|
if (!entry_has_locked) {
|
|
fail("missing locked for lock id: " wanted)
|
|
}
|
|
if (total == 0) {
|
|
fail("no rely-on.status entries for lock id: " wanted)
|
|
}
|
|
selected = 0
|
|
}
|
|
|
|
BEGIN {
|
|
selected = 0
|
|
matches = 0
|
|
total = 0
|
|
enabled = 0
|
|
error = ""
|
|
entry_has_locked = 0
|
|
in_rely_on = 0
|
|
in_dependency = 0
|
|
dependency_has_target = 0
|
|
dependency_has_status = 0
|
|
}
|
|
|
|
/^[[:space:]]*($|#)/ {
|
|
next
|
|
}
|
|
|
|
/^[[:space:]]*-[[:space:]]*id:[[:space:]]*/ {
|
|
finish_entry()
|
|
value = $0
|
|
sub(/^[[:space:]]*-[[:space:]]*id:[[:space:]]*/, "", value)
|
|
id = unquote(value)
|
|
if (id == wanted) {
|
|
if (matches > 0) {
|
|
fail("duplicate lock id: " wanted)
|
|
}
|
|
selected = 1
|
|
matches++
|
|
total = 0
|
|
enabled = 0
|
|
entry_has_locked = 0
|
|
in_rely_on = 0
|
|
in_dependency = 0
|
|
dependency_has_target = 0
|
|
dependency_has_status = 0
|
|
}
|
|
next
|
|
}
|
|
|
|
selected && /^[[:space:]]*locked:[[:space:]]*/ {
|
|
value = $0
|
|
sub(/^[[:space:]]*locked:[[:space:]]*/, "", value)
|
|
locked = unquote(value)
|
|
if (locked == "") {
|
|
fail("empty locked for lock id: " wanted)
|
|
}
|
|
entry_has_locked = 1
|
|
next
|
|
}
|
|
|
|
selected && /^[[:space:]]*rely-on:[[:space:]]*/ {
|
|
in_rely_on = 1
|
|
next
|
|
}
|
|
|
|
selected && in_rely_on && /^[[:space:]]*-[[:space:]]*target:[[:space:]]*/ {
|
|
finish_dependency()
|
|
in_dependency = 1
|
|
dependency_has_target = 0
|
|
dependency_has_status = 0
|
|
value = $0
|
|
sub(/^[[:space:]]*-[[:space:]]*target:[[:space:]]*/, "", value)
|
|
target = unquote(value)
|
|
if (target == "") {
|
|
fail("empty rely-on.target for lock id: " wanted)
|
|
}
|
|
dependency_has_target = 1
|
|
next
|
|
}
|
|
|
|
selected && in_rely_on && /^[[:space:]]*target:[[:space:]]*/ {
|
|
if (!in_dependency) {
|
|
fail("rely-on.target outside dependency item for lock id: " wanted)
|
|
}
|
|
value = $0
|
|
sub(/^[[:space:]]*target:[[:space:]]*/, "", value)
|
|
target = unquote(value)
|
|
if (target == "") {
|
|
fail("empty rely-on.target for lock id: " wanted)
|
|
}
|
|
dependency_has_target = 1
|
|
next
|
|
}
|
|
|
|
selected && in_rely_on && /^[[:space:]]*status:[[:space:]]*/ {
|
|
if (!in_dependency) {
|
|
fail("rely-on.status outside dependency item for lock id: " wanted)
|
|
}
|
|
value = $0
|
|
sub(/^[[:space:]]*status:[[:space:]]*/, "", value)
|
|
status = unquote(value)
|
|
if (status != "enable" && status != "disable") {
|
|
fail("invalid rely-on.status for " wanted ": " status)
|
|
}
|
|
if (dependency_has_status) {
|
|
fail("duplicate rely-on.status for lock id: " wanted)
|
|
}
|
|
dependency_has_status = 1
|
|
total++
|
|
if (status == "enable") {
|
|
enabled++
|
|
}
|
|
next
|
|
}
|
|
|
|
selected && in_rely_on && /^[[:space:]]*-[[:space:]]*/ {
|
|
finish_dependency()
|
|
fail("missing rely-on.target for lock id: " wanted)
|
|
}
|
|
|
|
selected && /^[[:space:]]*status:[[:space:]]*/ {
|
|
fail("status outside rely-on for lock id: " wanted)
|
|
}
|
|
|
|
END {
|
|
if (error != "") {
|
|
exit 2
|
|
}
|
|
if (matches == 0) {
|
|
printf "error: lock id not found: %s\n", wanted > "/dev/stderr"
|
|
exit 2
|
|
}
|
|
finish_entry()
|
|
if (total == enabled) {
|
|
print "true"
|
|
exit 0
|
|
}
|
|
print "false"
|
|
exit 1
|
|
}
|
|
' "$LOCKS_FILE"
|