@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.
- package/README.md +7 -2
- package/bin/roster.js +7092 -1167
- package/data/plan-ceilings.yaml +57 -0
- package/package.json +8 -2
- package/skills/chief-of-staff/SKILL.md +158 -2
- package/templates/hooks/banner.sh +47 -0
- package/templates/scaffold/conventions.md +35 -8
- package/templates/scaffold/dreamer/README.md +1 -1
- package/templates/scaffold/gtm/sdr/README.md +1 -6
- package/templates/scaffold/logs/cron/.gitkeep +1 -0
- package/templates/scaffold/ops/EXPERT.md +5 -5
- package/templates/scaffold/scripts/archive-project.sh +98 -0
- package/templates/scaffold/scripts/audit-agent.sh +299 -0
- package/templates/scaffold/scripts/audit-project.sh +361 -0
- package/templates/scaffold/scripts/audit-repo.sh +240 -0
- package/templates/scaffold/scripts/create-function.sh +267 -0
- package/templates/scaffold/scripts/lib/README.md +6 -1
- package/templates/scaffold/scripts/lib/bindings-prompt.sh +136 -0
- package/templates/scaffold/scripts/lib/functions.sh +17 -5
- package/templates/scaffold/scripts/new-agent-instance.sh +114 -0
- package/templates/scaffold/scripts/new-agent.sh +410 -0
- package/templates/scaffold/scripts/remove-agent-from-project.sh +67 -0
- package/templates/scaffold/scripts/rename-project.sh +118 -0
- package/templates/scaffold/scripts/unarchive-project.sh +115 -0
|
@@ -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"
|