@firatcand/roster 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env bash
2
+ # audit-agent.sh — checks agent structure completeness, reports issues with suggested fixes
3
+ # Usage: bash scripts/audit-agent.sh <function> <agent>
4
+
5
+ set -euo pipefail
6
+
7
+ if [ $# -ne 2 ]; then
8
+ echo "Usage: $0 <function> <agent>"
9
+ exit 1
10
+ fi
11
+
12
+ FN="$1"
13
+ AGENT="$2"
14
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
15
+ AGENT_DIR="$ROOT/$FN/$AGENT"
16
+
17
+ source "$ROOT/scripts/lib/functions.sh"
18
+
19
+ if ! is_valid_function "$FN"; then
20
+ echo "ERROR: '$FN' is not a registered function." >&2
21
+ echo "Registered functions:" >&2
22
+ read_functions | sed 's/^/ - /' >&2
23
+ exit 1
24
+ fi
25
+
26
+ if [ ! -d "$AGENT_DIR" ]; then
27
+ echo "ERROR: Agent '$FN/$AGENT' not found at $AGENT_DIR"
28
+ exit 1
29
+ fi
30
+
31
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
32
+ RUN_TIME=$(date +%Y-%m-%d-%H%M)
33
+ LOG_DIR="$ROOT/chief-of-staff/logs/$(date +%Y-%m)"
34
+ mkdir -p "$LOG_DIR"
35
+ REPORT="$LOG_DIR/audit-$FN-$AGENT-$RUN_TIME.md"
36
+
37
+ FAILURES=()
38
+ WARNINGS=()
39
+ PASSED=()
40
+
41
+ # === agent.md required sections ===
42
+ if [ ! -f "$AGENT_DIR/agent.md" ]; then
43
+ FAILURES+=("[$FN/$AGENT/agent.md] missing")
44
+ else
45
+ REQUIRED_SECTIONS=("## Purpose" "## Inputs" "## Plans" "## Subagents" "## Tools and bindings" "## Outputs" "## Approval" "## Lessons protocol")
46
+ MISSING=()
47
+ for section in "${REQUIRED_SECTIONS[@]}"; do
48
+ if ! grep -qF "$section" "$AGENT_DIR/agent.md"; then
49
+ MISSING+=("$section")
50
+ fi
51
+ done
52
+ if [ ${#MISSING[@]} -gt 0 ]; then
53
+ FAILURES+=("[$FN/$AGENT/agent.md] missing required sections: ${MISSING[*]}")
54
+ FAILURES+=(" → Suggested fix: add the missing sections per conventions.md § 'Agent contract'")
55
+ else
56
+ PASSED+=("[$FN/$AGENT/agent.md] all required sections present")
57
+ fi
58
+
59
+ # Steps section should NOT be present anymore — workflows live in plans/
60
+ if grep -qE '^## Steps' "$AGENT_DIR/agent.md"; then
61
+ WARNINGS+=("[$FN/$AGENT/agent.md] still has a '## Steps' section — workflow logic should live in plans/<plan>.yaml, not agent.md")
62
+ WARNINGS+=(" → Suggested fix: extract workflow steps into a plan file under $FN/$AGENT/plans/ and remove the section from agent.md")
63
+ fi
64
+ fi
65
+
66
+ # === plans/ directory ===
67
+ PLANS_DIR="$AGENT_DIR/plans"
68
+ if [ ! -d "$PLANS_DIR" ]; then
69
+ WARNINGS+=("[$FN/$AGENT/plans/] missing — agent has no plans declared")
70
+ WARNINGS+=(" → Suggested fix: mkdir $PLANS_DIR && add at least one .yaml plan file")
71
+ else
72
+ PLAN_COUNT=$(find "$PLANS_DIR" -maxdepth 1 -name '*.yaml' -type f 2>/dev/null | wc -l | tr -d ' ')
73
+ if [ "$PLAN_COUNT" -eq 0 ]; then
74
+ WARNINGS+=("[$FN/$AGENT/plans/] empty — agent has no plans declared")
75
+ WARNINGS+=(" → Suggested fix: add at least one .yaml plan to $PLANS_DIR")
76
+ else
77
+ PASSED+=("[$FN/$AGENT/plans/] $PLAN_COUNT plan(s)")
78
+ for plan in "$PLANS_DIR"/*.yaml; do
79
+ [ -f "$plan" ] || continue
80
+ REL="${plan#$ROOT/}"
81
+ if command -v python3 >/dev/null 2>&1; then
82
+ if ! python3 -c "import yaml; yaml.safe_load(open('$plan'))" 2>/dev/null; then
83
+ FAILURES+=("[$REL] YAML parse error")
84
+ else
85
+ PASSED+=("[$REL] valid YAML")
86
+ fi
87
+ fi
88
+ done
89
+ fi
90
+ fi
91
+
92
+ # === Slash command file ===
93
+ SLASH_CMD="$ROOT/.claude/commands/$AGENT.md"
94
+ if [ ! -f "$SLASH_CMD" ]; then
95
+ WARNINGS+=("[.claude/commands/$AGENT.md] missing — slash command not registered")
96
+ WARNINGS+=(" → Suggested fix: scaffold via 'bash scripts/new-agent.sh' template, or copy from another agent's slash command file")
97
+ else
98
+ PASSED+=("[.claude/commands/$AGENT.md] present")
99
+ fi
100
+
101
+ # README
102
+ if [ ! -f "$AGENT_DIR/README.md" ]; then
103
+ FAILURES+=("[$FN/$AGENT/README.md] missing")
104
+ else
105
+ PASSED+=("[$FN/$AGENT/README.md] present")
106
+ fi
107
+
108
+ # .mcp.json valid JSON
109
+ if [ ! -f "$AGENT_DIR/.mcp.json" ]; then
110
+ WARNINGS+=("[$FN/$AGENT/.mcp.json] missing (no agent-scoped MCPs configured)")
111
+ else
112
+ if command -v python3 >/dev/null 2>&1; then
113
+ if ! python3 -c "import json; json.load(open('$AGENT_DIR/.mcp.json'))" 2>/dev/null; then
114
+ FAILURES+=("[$FN/$AGENT/.mcp.json] invalid JSON")
115
+ FAILURES+=(" → Suggested fix: validate with: python3 -c 'import json; json.load(open(\"$AGENT_DIR/.mcp.json\"))'")
116
+ else
117
+ PASSED+=("[$FN/$AGENT/.mcp.json] valid JSON")
118
+ fi
119
+ else
120
+ PASSED+=("[$FN/$AGENT/.mcp.json] present (JSON not validated, python3 missing)")
121
+ fi
122
+ fi
123
+
124
+ # .claude/settings.json
125
+ if [ ! -f "$AGENT_DIR/.claude/settings.json" ]; then
126
+ WARNINGS+=("[$FN/$AGENT/.claude/settings.json] missing")
127
+ else
128
+ if command -v python3 >/dev/null 2>&1; then
129
+ if ! python3 -c "import json; json.load(open('$AGENT_DIR/.claude/settings.json'))" 2>/dev/null; then
130
+ FAILURES+=("[$FN/$AGENT/.claude/settings.json] invalid JSON")
131
+ else
132
+ PASSED+=("[$FN/$AGENT/.claude/settings.json] valid JSON")
133
+ fi
134
+ else
135
+ PASSED+=("[$FN/$AGENT/.claude/settings.json] present")
136
+ fi
137
+ fi
138
+
139
+ # subagents/ exists
140
+ if [ ! -d "$AGENT_DIR/subagents" ]; then
141
+ WARNINGS+=("[$FN/$AGENT/subagents/] missing (may be intentional for very simple agents)")
142
+ else
143
+ # Check each subagent has required sections
144
+ for sub in "$AGENT_DIR/subagents"/*.md; do
145
+ [ -f "$sub" ] || continue
146
+ BASENAME=$(basename "$sub")
147
+ [ "$BASENAME" = "_template.md" ] && continue
148
+ REL="${sub#$ROOT/}"
149
+ SUB_REQUIRED=("## Role" "## Inputs" "## Output" "## Tools" "## Boundaries" "## Quality bar")
150
+ SUB_MISSING=()
151
+ for section in "${SUB_REQUIRED[@]}"; do
152
+ if ! grep -qF "$section" "$sub"; then
153
+ SUB_MISSING+=("$section")
154
+ fi
155
+ done
156
+ if [ ${#SUB_MISSING[@]} -gt 0 ]; then
157
+ WARNINGS+=("[$REL] missing sections: ${SUB_MISSING[*]}")
158
+ else
159
+ PASSED+=("[$REL] all subagent sections present")
160
+ fi
161
+ done
162
+ fi
163
+
164
+ # playbook/ exists
165
+ if [ ! -d "$AGENT_DIR/playbook" ]; then
166
+ WARNINGS+=("[$FN/$AGENT/playbook/] missing")
167
+ else
168
+ PASSED+=("[$FN/$AGENT/playbook/] present")
169
+ fi
170
+
171
+ # projects/_template/ exists
172
+ if [ ! -d "$AGENT_DIR/projects/_template" ]; then
173
+ FAILURES+=("[$FN/$AGENT/projects/_template/] missing — new instances cannot be created without it")
174
+ FAILURES+=(" → Suggested fix: rebuild via 'bash scripts/new-agent.sh $FN $AGENT' (will fail if exists; manually copy from gtm/sdr/projects/_template/ as reference)")
175
+ else
176
+ PASSED+=("[$FN/$AGENT/projects/_template/] present")
177
+ fi
178
+
179
+ # === Each instance ===
180
+ INSTANCES=()
181
+ while IFS= read -r path; do
182
+ INSTANCES+=("$path")
183
+ done < <(find "$AGENT_DIR/projects" -maxdepth 1 -mindepth 1 -type d -not -name '_template' 2>/dev/null || true)
184
+
185
+ for inst in "${INSTANCES[@]}"; do
186
+ REL="${inst#$ROOT/}"
187
+ PROJECT_NAME=$(basename "$inst")
188
+
189
+ CONFIG="$inst/config/default.yaml"
190
+ if [ ! -f "$CONFIG" ]; then
191
+ FAILURES+=("[$REL/config/default.yaml] missing")
192
+ else
193
+ if command -v python3 >/dev/null 2>&1; then
194
+ if ! python3 -c "import yaml; list(yaml.safe_load_all(open('$CONFIG')))" 2>/dev/null; then
195
+ FAILURES+=("[$REL/config/default.yaml] YAML parse error")
196
+ else
197
+ DECLARED=$(python3 -c "import yaml; docs = list(yaml.safe_load_all(open('$CONFIG'))); print((docs[0] or {}).get('project', ''))" 2>/dev/null || echo "")
198
+ if [ "$DECLARED" != "$PROJECT_NAME" ]; then
199
+ FAILURES+=("[$REL/config/default.yaml] project field '$DECLARED' doesn't match folder '$PROJECT_NAME'")
200
+ else
201
+ PASSED+=("[$REL/config/default.yaml] valid")
202
+ fi
203
+ fi
204
+ fi
205
+ fi
206
+
207
+ for d in log/runs log/feedback playbook; do
208
+ if [ ! -d "$inst/$d" ]; then
209
+ WARNINGS+=("[$REL/$d/] missing")
210
+ fi
211
+ done
212
+
213
+ if [ ! -f "$inst/asset-references.md" ]; then
214
+ WARNINGS+=("[$REL/asset-references.md] missing")
215
+ fi
216
+ done
217
+
218
+ # === Status ===
219
+ if [ ${#FAILURES[@]} -gt 0 ]; then
220
+ STATUS="fail"
221
+ elif [ ${#WARNINGS[@]} -gt 0 ]; then
222
+ STATUS="warn"
223
+ else
224
+ STATUS="pass"
225
+ fi
226
+
227
+ count_items() {
228
+ local arr=("$@")
229
+ local n=0
230
+ for item in "${arr[@]}"; do
231
+ if ! [[ "$item" =~ ^[[:space:]]*→ ]]; then
232
+ n=$((n+1))
233
+ fi
234
+ done
235
+ echo $n
236
+ }
237
+
238
+ N_FAIL=0
239
+ for item in "${FAILURES[@]:-}"; do
240
+ [ -z "$item" ] && continue
241
+ [[ "$item" =~ ^[[:space:]]+→ ]] && continue
242
+ N_FAIL=$((N_FAIL + 1))
243
+ done
244
+ N_WARN=0
245
+ for item in "${WARNINGS[@]:-}"; do
246
+ [ -z "$item" ] && continue
247
+ [[ "$item" =~ ^[[:space:]]+→ ]] && continue
248
+ N_WARN=$((N_WARN + 1))
249
+ done
250
+ N_PASS=${#PASSED[@]}
251
+
252
+ # Write report
253
+ {
254
+ echo "---"
255
+ echo "operation: audit-agent"
256
+ echo "function: $FN"
257
+ echo "agent: $AGENT"
258
+ echo "ran: $TIMESTAMP"
259
+ echo "status: $STATUS"
260
+ echo "---"
261
+ echo ""
262
+ echo "# Audit: $FN/$AGENT"
263
+ echo ""
264
+ echo "## Summary"
265
+ echo "- $N_PASS passed"
266
+ echo "- $N_WARN warnings"
267
+ echo "- $N_FAIL failures"
268
+ echo "- ${#INSTANCES[@]} instance(s) audited"
269
+ echo ""
270
+ if [ $N_FAIL -gt 0 ]; then
271
+ echo "## Failures"
272
+ for line in "${FAILURES[@]}"; do
273
+ echo "- $line"
274
+ done
275
+ echo ""
276
+ fi
277
+ if [ $N_WARN -gt 0 ]; then
278
+ echo "## Warnings"
279
+ for line in "${WARNINGS[@]}"; do
280
+ echo "- $line"
281
+ done
282
+ echo ""
283
+ fi
284
+ if [ $N_PASS -gt 0 ]; then
285
+ echo "## Passed"
286
+ for line in "${PASSED[@]}"; do
287
+ echo "- $line"
288
+ done
289
+ fi
290
+ } > "$REPORT"
291
+
292
+ echo "Audit: $FN/$AGENT — $STATUS"
293
+ echo " Passed: $N_PASS, Warnings: $N_WARN, Failures: $N_FAIL"
294
+ echo " Instances audited: ${#INSTANCES[@]}"
295
+ [ $N_FAIL -gt 0 ] && {
296
+ echo "Failures:"
297
+ for line in "${FAILURES[@]}"; do echo " $line"; done
298
+ }
299
+ echo "Full report: $REPORT"
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env bash
2
+ # audit-project.sh — checks project completeness, reports issues with suggested fixes
3
+ # Usage: bash scripts/audit-project.sh <project>
4
+
5
+ set -euo pipefail
6
+
7
+ if [ $# -ne 1 ]; then
8
+ echo "Usage: $0 <project>"
9
+ exit 1
10
+ fi
11
+
12
+ PROJECT="$1"
13
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
14
+ PROJECT_DIR="$ROOT/projects/$PROJECT"
15
+
16
+ if [ ! -d "$PROJECT_DIR" ]; then
17
+ echo "ERROR: Project '$PROJECT' not found at $PROJECT_DIR"
18
+ exit 1
19
+ fi
20
+
21
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
22
+ RUN_TIME=$(date +%Y-%m-%d-%H%M)
23
+ LOG_DIR="$ROOT/chief-of-staff/logs/$(date +%Y-%m)"
24
+ mkdir -p "$LOG_DIR"
25
+ REPORT="$LOG_DIR/audit-$PROJECT-$RUN_TIME.md"
26
+
27
+ # Buffers
28
+ FAILURES=()
29
+ WARNINGS=()
30
+ PASSED=()
31
+
32
+ # Check helper: file exists and isn't template content
33
+ is_template() {
34
+ local file="$1"
35
+ # Heuristic: contains a placeholder pattern like <something descriptive>
36
+ # We use the simple heuristic of looking for "<3 adjectives" or "<list>" — common template strings
37
+ if grep -qE '<[a-z0-9 ,/.:-]+>' "$file" 2>/dev/null; then
38
+ return 0
39
+ fi
40
+ return 1
41
+ }
42
+
43
+ check_required_guideline() {
44
+ local file="$1"
45
+ local rel="${file#$ROOT/}"
46
+ if [ ! -f "$file" ]; then
47
+ FAILURES+=("[$rel] required file missing")
48
+ FAILURES+=(" → Suggested fix: copy from projects/_template/${rel#projects/$PROJECT/}")
49
+ return
50
+ fi
51
+ if is_template "$file"; then
52
+ FAILURES+=("[$rel] still contains template placeholders (e.g., <list>, <3 adjectives>)")
53
+ FAILURES+=(" → Suggested fix: edit $rel to replace all <placeholder> markers with real content")
54
+ return
55
+ fi
56
+ PASSED+=("[$rel] OK (filled in)")
57
+ }
58
+
59
+ check_optional_guideline() {
60
+ local file="$1"
61
+ local rel="${file#$ROOT/}"
62
+ if [ ! -f "$file" ]; then
63
+ WARNINGS+=("[$rel] optional file missing")
64
+ WARNINGS+=(" → Suggested fix: copy from projects/_template/${rel#projects/$PROJECT/} if needed")
65
+ return
66
+ fi
67
+ PASSED+=("[$rel] present")
68
+ }
69
+
70
+ # === Required guidelines ===
71
+ check_required_guideline "$PROJECT_DIR/guidelines/voice.md"
72
+ check_required_guideline "$PROJECT_DIR/guidelines/design.md"
73
+ check_required_guideline "$PROJECT_DIR/guidelines/design-tokens.md"
74
+ check_required_guideline "$PROJECT_DIR/guidelines/brand-book.md"
75
+ check_required_guideline "$PROJECT_DIR/guidelines/messaging.md"
76
+ check_required_guideline "$PROJECT_DIR/guidelines/asset-links.md"
77
+
78
+ # ICPs: at least one non-template file
79
+ if [ ! -d "$PROJECT_DIR/guidelines/icps" ]; then
80
+ FAILURES+=("[projects/$PROJECT/guidelines/icps/] directory missing")
81
+ FAILURES+=(" → Suggested fix: mkdir -p $PROJECT_DIR/guidelines/icps && cp projects/_template/guidelines/icps/_persona-template.md $PROJECT_DIR/guidelines/icps/")
82
+ else
83
+ ICP_COUNT=$(find "$PROJECT_DIR/guidelines/icps" -type f -name '*.md' -not -name '_persona-template.md' | wc -l)
84
+ if [ "$ICP_COUNT" -eq 0 ]; then
85
+ FAILURES+=("[projects/$PROJECT/guidelines/icps/] no persona file (only _persona-template.md or empty)")
86
+ FAILURES+=(" → Suggested fix: cp projects/_template/guidelines/icps/_persona-template.md $PROJECT_DIR/guidelines/icps/<persona-slug>.md and fill in")
87
+ else
88
+ PASSED+=("[projects/$PROJECT/guidelines/icps/] $ICP_COUNT persona file(s)")
89
+ fi
90
+ fi
91
+
92
+ # === Optional guidelines ===
93
+ check_optional_guideline "$PROJECT_DIR/guidelines/do-and-dont.md"
94
+ check_optional_guideline "$PROJECT_DIR/guidelines/compliance.md"
95
+ check_optional_guideline "$PROJECT_DIR/guidelines/competitors.md"
96
+
97
+ # === Project root files ===
98
+ if [ -f "$PROJECT_DIR/CLAUDE.md" ]; then
99
+ if grep -q '<Project Name>' "$PROJECT_DIR/CLAUDE.md" 2>/dev/null; then
100
+ FAILURES+=("[projects/$PROJECT/CLAUDE.md] still contains <Project Name> placeholder")
101
+ FAILURES+=(" → Suggested fix: edit projects/$PROJECT/CLAUDE.md and fill in identity, audience, agents")
102
+ else
103
+ PASSED+=("[projects/$PROJECT/CLAUDE.md] filled in")
104
+ fi
105
+ else
106
+ FAILURES+=("[projects/$PROJECT/CLAUDE.md] missing")
107
+ fi
108
+
109
+ if [ -f "$PROJECT_DIR/state.md" ]; then
110
+ PASSED+=("[projects/$PROJECT/state.md] present")
111
+ else
112
+ WARNINGS+=("[projects/$PROJECT/state.md] missing")
113
+ WARNINGS+=(" → Suggested fix: cp projects/_template/state.md $PROJECT_DIR/state.md")
114
+ fi
115
+
116
+ # === Agent instances ===
117
+ INSTANCES=()
118
+ while IFS= read -r path; do
119
+ INSTANCES+=("$path")
120
+ done < <(find "$ROOT" -type d -path "*/projects/$PROJECT" -not -path "*/_template/*" -not -path "*/_archive/*" 2>/dev/null | grep -v "^$PROJECT_DIR$" || true)
121
+
122
+ LISTED_INSTANCES=()
123
+ if [ -f "$PROJECT_DIR/CLAUDE.md" ]; then
124
+ while IFS= read -r line; do
125
+ LISTED_INSTANCES+=("$line")
126
+ done < <(grep -oE '`[a-z0-9-]+/[a-z0-9-]+/projects/[a-z0-9-]+/`' "$PROJECT_DIR/CLAUDE.md" 2>/dev/null | tr -d '`' || true)
127
+ fi
128
+
129
+ for inst in "${INSTANCES[@]}"; do
130
+ REL="${inst#$ROOT/}"
131
+
132
+ # config/default.yaml
133
+ CONFIG="$inst/config/default.yaml"
134
+ if [ ! -f "$CONFIG" ]; then
135
+ FAILURES+=("[$REL/config/default.yaml] missing")
136
+ else
137
+ # Try YAML parse (use python3 if available, else just check file is non-empty)
138
+ if command -v python3 >/dev/null 2>&1; then
139
+ if ! python3 -c "import yaml; list(yaml.safe_load_all(open('$CONFIG')))" 2>/dev/null; then
140
+ FAILURES+=("[$REL/config/default.yaml] YAML parse error")
141
+ FAILURES+=(" → Suggested fix: check YAML syntax with: python3 -c 'import yaml; list(yaml.safe_load_all(open(\"$CONFIG\"))'")
142
+ else
143
+ # Check project field matches folder
144
+ DECLARED_PROJECT=$(python3 -c "import yaml; docs = list(yaml.safe_load_all(open('$CONFIG'))); print((docs[0] or {}).get('project', ''))" 2>/dev/null || echo "")
145
+ if [ "$DECLARED_PROJECT" != "$PROJECT" ]; then
146
+ FAILURES+=("[$REL/config/default.yaml] project field is '$DECLARED_PROJECT', expected '$PROJECT'")
147
+ FAILURES+=(" → Suggested fix: edit $REL/config/default.yaml and set 'project: $PROJECT'")
148
+ else
149
+ PASSED+=("[$REL/config/default.yaml] valid YAML, project matches")
150
+ fi
151
+ fi
152
+ else
153
+ PASSED+=("[$REL/config/default.yaml] present (YAML not validated, python3 missing)")
154
+ fi
155
+ fi
156
+
157
+ # asset-references.md
158
+ if [ -f "$inst/asset-references.md" ]; then
159
+ PASSED+=("[$REL/asset-references.md] present")
160
+ else
161
+ WARNINGS+=("[$REL/asset-references.md] missing")
162
+ fi
163
+
164
+ # === Tool bindings: TODO required → fail; TODO optional → warn ===
165
+ # Resolve agent.md from the instance path: $inst is .../<fn>/<agent>/projects/<project>
166
+ INST_PARENT="$(cd "$inst/../.." && pwd)"
167
+ AGENT_MD_FOR_INST="$INST_PARENT/agent.md"
168
+ if [ -f "$CONFIG" ] && [ -f "$AGENT_MD_FOR_INST" ] && command -v python3 >/dev/null 2>&1; then
169
+ BINDING_REPORT=$(CONFIG_PATH="$CONFIG" AGENT_MD_PATH="$AGENT_MD_FOR_INST" REL_PATH="$REL" python3 << 'PYEOF' 2>/dev/null || true
170
+ import os, re, sys
171
+ config_path = os.environ["CONFIG_PATH"]
172
+ agent_md = os.environ["AGENT_MD_PATH"]
173
+ rel = os.environ["REL_PATH"]
174
+
175
+ try:
176
+ import yaml
177
+ except ImportError:
178
+ sys.exit(0)
179
+
180
+ with open(agent_md) as f:
181
+ am = f.read()
182
+ sm = re.search(r'## Tools and bindings.*?\n```yaml\n(.*?)\n```', am, re.DOTALL)
183
+ if not sm:
184
+ sys.exit(0)
185
+ try:
186
+ schema = yaml.safe_load(sm.group(1)) or {}
187
+ except Exception:
188
+ sys.exit(0)
189
+
190
+ with open(config_path) as f:
191
+ raw = f.read()
192
+
193
+ # Walk lines inside the tools: block; capture (tool, key) pairs whose value is TODO.
194
+ in_tools = False
195
+ current_tool = None
196
+ todos = []
197
+ for line in raw.split("\n"):
198
+ if re.match(r'^tools:\s*$', line):
199
+ in_tools = True
200
+ continue
201
+ if not in_tools:
202
+ continue
203
+ if re.match(r'^[A-Za-z]', line):
204
+ # left the tools block (back to top-level key)
205
+ in_tools = False
206
+ current_tool = None
207
+ continue
208
+ tm = re.match(r'^ ([a-z_][a-z0-9_]*):\s*$', line)
209
+ if tm:
210
+ current_tool = tm.group(1)
211
+ continue
212
+ bm = re.match(r'^ ([a-z_][a-z0-9_]*):\s*#\s*TODO\b', line)
213
+ if bm and current_tool:
214
+ todos.append((current_tool, bm.group(1)))
215
+
216
+ for tool, key in todos:
217
+ tool_schema = schema.get(tool, {}) if isinstance(schema, dict) else {}
218
+ key_schema = tool_schema.get(key, {}) if isinstance(tool_schema, dict) else {}
219
+ required = bool(key_schema.get("required", False)) if isinstance(key_schema, dict) else False
220
+ severity = "FAIL" if required else "WARN"
221
+ print(f"{severity}\t{tool}.{key}")
222
+ PYEOF
223
+ )
224
+ while IFS=$'\t' read -r severity binding; do
225
+ [ -z "$severity" ] && continue
226
+ if [ "$severity" = "FAIL" ]; then
227
+ FAILURES+=("[$REL/config/default.yaml] required tool binding '$binding' is TODO")
228
+ FAILURES+=(" → Suggested fix: edit $REL/config/default.yaml and set tools.$binding to a real value")
229
+ else
230
+ WARNINGS+=("[$REL/config/default.yaml] optional tool binding '$binding' is TODO (will be skipped at runtime)")
231
+ fi
232
+ done <<< "$BINDING_REPORT"
233
+ fi
234
+
235
+ # Required directories
236
+ for d in log/runs log/feedback playbook; do
237
+ if [ -d "$inst/$d" ]; then
238
+ PASSED+=("[$REL/$d/] present")
239
+ else
240
+ WARNINGS+=("[$REL/$d/] missing (will be auto-created on first run)")
241
+ fi
242
+ done
243
+
244
+ # Is this instance listed in CLAUDE.md?
245
+ EXPECTED_PATH=$(echo "$REL/" | sed 's|/projects/.*|/projects/'"$PROJECT"'/|')
246
+ FOUND=0
247
+ for listed in "${LISTED_INSTANCES[@]}"; do
248
+ if [ "$listed" = "$EXPECTED_PATH" ]; then
249
+ FOUND=1
250
+ break
251
+ fi
252
+ done
253
+ if [ $FOUND -eq 0 ]; then
254
+ WARNINGS+=("[$REL] instance not listed in projects/$PROJECT/CLAUDE.md ## Active agent instances")
255
+ WARNINGS+=(" → Suggested fix: add a line under '## Active agent instances' in CLAUDE.md")
256
+ fi
257
+ done
258
+
259
+ # Listed instances that don't exist
260
+ for listed in "${LISTED_INSTANCES[@]}"; do
261
+ if [ ! -d "$ROOT/${listed%/}" ]; then
262
+ WARNINGS+=("[$listed] listed in CLAUDE.md but folder does not exist")
263
+ WARNINGS+=(" → Suggested fix: either create the instance with new-agent-instance.sh or remove the line from CLAUDE.md")
264
+ fi
265
+ done
266
+
267
+ # === Determine status ===
268
+ if [ ${#FAILURES[@]} -gt 0 ]; then
269
+ STATUS="fail"
270
+ elif [ ${#WARNINGS[@]} -gt 0 ]; then
271
+ STATUS="warn"
272
+ else
273
+ STATUS="pass"
274
+ fi
275
+
276
+ # Count items (each FAILURE/WARNING entry is sometimes 2 lines: msg + suggested fix)
277
+ # Real count is half the FAILURES/WARNINGS for entries with → Suggested fix
278
+ count_items() {
279
+ local arr=("$@")
280
+ local n=0
281
+ for item in "${arr[@]}"; do
282
+ if ! [[ "$item" =~ ^[[:space:]]*→ ]]; then
283
+ n=$((n+1))
284
+ fi
285
+ done
286
+ echo $n
287
+ }
288
+
289
+ N_FAIL=0
290
+ for item in "${FAILURES[@]:-}"; do
291
+ [ -z "$item" ] && continue
292
+ [[ "$item" =~ ^[[:space:]]+→ ]] && continue
293
+ N_FAIL=$((N_FAIL + 1))
294
+ done
295
+ N_WARN=0
296
+ for item in "${WARNINGS[@]:-}"; do
297
+ [ -z "$item" ] && continue
298
+ [[ "$item" =~ ^[[:space:]]+→ ]] && continue
299
+ N_WARN=$((N_WARN + 1))
300
+ done
301
+ N_PASS=${#PASSED[@]}
302
+
303
+ # === Write report ===
304
+ {
305
+ echo "---"
306
+ echo "operation: audit-project"
307
+ echo "project: $PROJECT"
308
+ echo "ran: $TIMESTAMP"
309
+ echo "status: $STATUS"
310
+ echo "---"
311
+ echo ""
312
+ echo "# Audit: $PROJECT"
313
+ echo ""
314
+ echo "## Summary"
315
+ echo "- $N_PASS passed"
316
+ echo "- $N_WARN warnings"
317
+ echo "- $N_FAIL failures"
318
+ echo ""
319
+ if [ $N_FAIL -gt 0 ]; then
320
+ echo "## Failures"
321
+ for line in "${FAILURES[@]}"; do
322
+ echo "- $line"
323
+ done
324
+ echo ""
325
+ fi
326
+ if [ $N_WARN -gt 0 ]; then
327
+ echo "## Warnings"
328
+ for line in "${WARNINGS[@]}"; do
329
+ echo "- $line"
330
+ done
331
+ echo ""
332
+ fi
333
+ if [ $N_PASS -gt 0 ]; then
334
+ echo "## Passed"
335
+ for line in "${PASSED[@]}"; do
336
+ echo "- $line"
337
+ done
338
+ fi
339
+ } > "$REPORT"
340
+
341
+ # === Print summary ===
342
+ echo "Audit: $PROJECT — $STATUS"
343
+ echo " Passed: $N_PASS"
344
+ echo " Warnings: $N_WARN"
345
+ echo " Failures: $N_FAIL"
346
+ echo ""
347
+ if [ $N_FAIL -gt 0 ]; then
348
+ echo "Failures:"
349
+ for line in "${FAILURES[@]}"; do
350
+ echo " $line"
351
+ done
352
+ echo ""
353
+ fi
354
+ if [ $N_WARN -gt 0 ] && [ $N_WARN -le 5 ]; then
355
+ echo "Warnings:"
356
+ for line in "${WARNINGS[@]}"; do
357
+ echo " $line"
358
+ done
359
+ echo ""
360
+ fi
361
+ echo "Full report: $REPORT"