master
sh 291 lines 7.28 KB
Raw
1 #!/usr/bin/env bash
2 # Read-only audit for the project-local SOW system.
3 # Never modifies files.
4
5 set -uo pipefail
6
7 if [ -t 1 ]; then
8 RED=$'\033[0;31m'
9 GREEN=$'\033[0;32m'
10 YELLOW=$'\033[1;33m'
11 BLUE=$'\033[0;34m'
12 GRAY=$'\033[0;90m'
13 NC=$'\033[0m'
14 else
15 RED=""
16 GREEN=""
17 YELLOW=""
18 BLUE=""
19 GRAY=""
20 NC=""
21 fi
22
23 failures=0
24 warnings=0
25
26 ok() {
27 echo " ${GREEN}OK${NC} $*"
28 }
29
30 fail() {
31 echo " ${RED}--${NC} $*"
32 failures=$((failures + 1))
33 }
34
35 warn() {
36 echo " ${YELLOW}--${NC} $*"
37 warnings=$((warnings + 1))
38 }
39
40 section() {
41 echo
42 echo "${BLUE}-- $* --${NC}"
43 }
44
45 read_sow_status() {
46 awk '
47 function clean(s, a) {
48 gsub(/^[[:space:]]+|[[:space:]]+$/, "", s)
49 gsub(/`/, "", s)
50 gsub(/\*\*/, "", s)
51 sub(/^Status:[[:space:]]*/, "", s)
52 sub(/^status:[[:space:]]*/, "", s)
53 gsub(/^[[:space:]]+|[[:space:]]+$/, "", s)
54 split(s, a, /[[:space:]|—]+/)
55 print a[1]
56 exit
57 }
58 /^Status:[[:space:]]*/ { clean($0) }
59 /^status:[[:space:]]*/ { clean($0) }
60 /^\*\*Status:\*\*[[:space:]]*/ { clean($0) }
61 /^## Status[[:space:]]*$/ { in_status = 1; next }
62 in_status && NF { clean($0) }
63 ' "$1" 2>/dev/null
64 }
65
66 echo "${BLUE}=== SOW audit (cwd=$(pwd)) ===${NC}"
67
68 section "initialization marker"
69 if [ -f AGENTS.md ]; then
70 if grep -q "^Project SOW status: initialized$" AGENTS.md; then
71 ok "marker present in AGENTS.md"
72 else
73 fail "AGENTS.md exists but Project SOW status marker is missing"
74 fi
75 else
76 fail "AGENTS.md is missing"
77 fi
78
79 section "canonical AGENTS.md sections"
80 required_sections=(
81 "## Goals"
82 "## SOW System"
83 "### Roles"
84 "### Git Worktrees"
85 "### Sensitive Data In Durable Artifacts"
86 "### Durable AI-Facing Artifact Formatting"
87 "### Open-Source Reference Evidence"
88 "### Pre-Implementation Gate"
89 "### SOW Completion And Merge"
90 "### Enforcement"
91 "### Regressions"
92 "### Project Skills"
93 "### Specs"
94 "### Project-specific overrides"
95 )
96
97 for heading in "${required_sections[@]}"; do
98 if grep -qF "$heading" AGENTS.md 2>/dev/null; then
99 ok "$heading"
100 else
101 fail "$heading is missing"
102 fi
103 done
104
105 if grep -qF "CRITICAL: Never write raw sensitive data to durable artifacts." AGENTS.md 2>/dev/null; then
106 ok "CRITICAL sensitive-data warning"
107 else
108 fail "CRITICAL sensitive-data warning is missing from AGENTS.md"
109 fi
110
111 section "SOW layout"
112 for path in .agents/sow/active .agents/sow/specs; do
113 if [ -d "$path" ]; then
114 ok "$path exists"
115 else
116 fail "$path is missing"
117 fi
118 done
119
120 for path in .agents/sow/SOW.template.md .agents/sow/audit.sh .agents/sow/scan-sensitive.sh .agents/sow/specs/README.md; do
121 if [ -f "$path" ]; then
122 ok "$path exists"
123 else
124 fail "$path is missing"
125 fi
126 done
127
128 for legacy_dir in .agents/sow/pending .agents/sow/current .agents/sow/done; do
129 if [ -e "$legacy_dir" ]; then
130 fail "$legacy_dir must not exist in the active-only SOW model"
131 else
132 ok "$legacy_dir absent"
133 fi
134 done
135
136 section "active SOW files"
137 active_count=0
138 if [ -d .agents/sow/active ]; then
139 while IFS= read -r sow; do
140 [ -n "$sow" ] || continue
141 active_count=$((active_count + 1))
142 status=$(read_sow_status "$sow")
143
144 case "$status" in
145 planning|ready|in-progress|paused|completed)
146 ok "$sow status=$status"
147 ;;
148 "")
149 fail "$sow has no Status"
150 ;;
151 *)
152 fail "$sow has invalid Status: $status"
153 ;;
154 esac
155
156 for needle in \
157 "## Pre-Implementation Gate" \
158 "Sensitive data handling plan:" \
159 "Sensitive data gate:" \
160 "## Validation" \
161 "## Artifact Maintenance Gate"
162 do
163 if grep -qF "$needle" "$sow"; then
164 ok "$sow contains $needle"
165 else
166 fail "$sow is missing $needle"
167 fi
168 done
169 done < <(find .agents/sow/active -maxdepth 1 -type f -name 'SOW-*.md' 2>/dev/null | sort)
170 fi
171
172 if [ "$active_count" -eq 0 ]; then
173 ok "no active branch-local SOW files"
174 else
175 warn "$active_count active branch-local SOW file(s); they must be deleted before merge"
176 fi
177
178 section "spec index"
179 if [ -f .agents/sow/specs/README.md ]; then
180 while IFS= read -r spec; do
181 [ -n "$spec" ] || continue
182 rel=${spec#.agents/sow/specs/}
183 if grep -qF "]($rel)" .agents/sow/specs/README.md; then
184 ok "$rel is listed in specs/README.md"
185 else
186 fail "$rel is missing from specs/README.md"
187 fi
188 done < <(find .agents/sow/specs -maxdepth 1 -type f -name '*.md' ! -name README.md 2>/dev/null | sort)
189
190 while IFS= read -r link; do
191 [ -n "$link" ] || continue
192 if [ -f ".agents/sow/specs/$link" ]; then
193 ok "spec index link resolves: $link"
194 else
195 fail "spec index link is broken: $link"
196 fi
197 done < <(grep -oE '\]\([A-Za-z0-9._-]+\.md\)' .agents/sow/specs/README.md 2>/dev/null | sed 's/^](//; s/)$//' | sort -u)
198 else
199 fail ".agents/sow/specs/README.md is missing"
200 fi
201
202 section "spec references"
203 if command -v rg >/dev/null 2>&1; then
204 while IFS= read -r ref; do
205 [ -n "$ref" ] || continue
206 if [ -f "$ref" ]; then
207 ok "spec reference resolves: $ref"
208 else
209 fail "spec reference is broken: $ref"
210 fi
211 done < <(
212 rg --no-filename -o '\.agents/sow/specs/[A-Za-z0-9._-]+\.md' \
213 AGENTS.md .agents/skills .agents/sow/specs docs src \
214 -g '*.md' -g 'SKILL.md' -g '*.sh' -g '*.yml' \
215 2>/dev/null | sort -u
216 )
217 else
218 warn "ripgrep not available; skipped spec reference audit"
219 fi
220
221 section "legacy SOW references"
222 if command -v rg >/dev/null 2>&1; then
223 legacy_refs=$(rg --line-number 'SOW-[0-9]{4}\b' \
224 AGENTS.md .agents .github docs src \
225 -g '*.md' -g 'SKILL.md' -g '*.sh' -g '*.yml' \
226 -g '!TODO*.md' \
227 -g '!**/TODO*.md' \
228 -g '!**/.agents/sow/active/SOW-*.md' \
229 2>/dev/null || true)
230
231 if [ -n "$legacy_refs" ]; then
232 printf '%s\n' "$legacy_refs"
233 fail "legacy SOW-NNNN references remain in durable files"
234 else
235 ok "no legacy SOW-NNNN references in durable files"
236 fi
237 else
238 warn "ripgrep not available; skipped legacy SOW reference audit"
239 fi
240
241 section "sensitive data"
242 scan_files=()
243
244 for path in AGENTS.md CLAUDE.md GEMINI.md .agents/ENV.md .agents/sow/SOW.template.md .agents/sow/specs/README.md; do
245 [ -f "$path" ] && scan_files+=("$path")
246 done
247
248 if [ -d .agents/sow/specs ]; then
249 while IFS= read -r file; do
250 scan_files+=("$file")
251 done < <(find .agents/sow/specs -type f -name '*.md' 2>/dev/null | sort)
252 fi
253
254 if [ -d .agents/sow/active ]; then
255 while IFS= read -r file; do
256 scan_files+=("$file")
257 done < <(find .agents/sow/active -type f -name '*.md' 2>/dev/null | sort)
258 fi
259
260 if [ -d .agents/skills ]; then
261 while IFS= read -r file; do
262 scan_files+=("$file")
263 done < <(find .agents/skills -type f 2>/dev/null | sort)
264 fi
265
266 if [ -d .agents/skill-verification ]; then
267 while IFS= read -r file; do
268 scan_files+=("$file")
269 done < <(find .agents/skill-verification -type f 2>/dev/null | sort)
270 fi
271
272 if [ "${#scan_files[@]}" -eq 0 ]; then
273 warn "no files selected for sensitive-data scan"
274 elif bash .agents/sow/scan-sensitive.sh "${scan_files[@]}"; then
275 ok "sensitive-data scan passed (${#scan_files[@]} files)"
276 else
277 fail "sensitive-data scan found potential leaks"
278 fi
279
280 section "summary"
281 if [ "$failures" -eq 0 ]; then
282 if [ "$warnings" -eq 0 ]; then
283 echo "${GREEN}PASS${NC} no SOW audit failures or warnings"
284 else
285 echo "${YELLOW}PASS${NC} no SOW audit failures; warnings=$warnings"
286 fi
287 exit 0
288 fi
289
290 echo "${RED}FAIL${NC} failures=$failures warnings=$warnings"
291 exit 1