@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,240 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# audit-repo.sh — full repo audit; runs project + agent audits, plus repo-level checks
|
|
3
|
+
# Usage: bash scripts/audit-repo.sh
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
8
|
+
|
|
9
|
+
source "$ROOT/scripts/lib/functions.sh"
|
|
10
|
+
|
|
11
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
12
|
+
RUN_TIME=$(date +%Y-%m-%d-%H%M)
|
|
13
|
+
LOG_DIR="$ROOT/chief-of-staff/logs/$(date +%Y-%m)"
|
|
14
|
+
mkdir -p "$LOG_DIR"
|
|
15
|
+
REPORT="$LOG_DIR/audit-repo-$RUN_TIME.md"
|
|
16
|
+
|
|
17
|
+
REPO_FAILURES=()
|
|
18
|
+
REPO_WARNINGS=()
|
|
19
|
+
REPO_PASSED=()
|
|
20
|
+
|
|
21
|
+
# === Repo-level checks ===
|
|
22
|
+
|
|
23
|
+
# Universal .mcp.json
|
|
24
|
+
if [ ! -f "$ROOT/.mcp.json" ]; then
|
|
25
|
+
REPO_WARNINGS+=("[.mcp.json] universal MCP config missing")
|
|
26
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
27
|
+
if ! python3 -c "import json; json.load(open('$ROOT/.mcp.json'))" 2>/dev/null; then
|
|
28
|
+
REPO_FAILURES+=("[.mcp.json] invalid JSON")
|
|
29
|
+
else
|
|
30
|
+
REPO_PASSED+=("[.mcp.json] valid")
|
|
31
|
+
fi
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Universal .claude/settings.json
|
|
35
|
+
if [ ! -f "$ROOT/.claude/settings.json" ]; then
|
|
36
|
+
REPO_WARNINGS+=("[.claude/settings.json] universal settings missing")
|
|
37
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
38
|
+
if ! python3 -c "import json; json.load(open('$ROOT/.claude/settings.json'))" 2>/dev/null; then
|
|
39
|
+
REPO_FAILURES+=("[.claude/settings.json] invalid JSON")
|
|
40
|
+
else
|
|
41
|
+
REPO_PASSED+=("[.claude/settings.json] valid")
|
|
42
|
+
fi
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Required root files
|
|
46
|
+
for f in CLAUDE.md conventions.md README.md; do
|
|
47
|
+
if [ ! -f "$ROOT/$f" ]; then
|
|
48
|
+
REPO_FAILURES+=("[$f] missing at repo root")
|
|
49
|
+
else
|
|
50
|
+
REPO_PASSED+=("[$f] present")
|
|
51
|
+
fi
|
|
52
|
+
done
|
|
53
|
+
|
|
54
|
+
# Build a regex of registered functions for matching grandparent dirs.
|
|
55
|
+
REGISTERED_FNS_PIPE=$(read_functions 2>/dev/null | tr '\n' '|' | sed 's/|$//')
|
|
56
|
+
|
|
57
|
+
# Find orphaned instances (instance folder exists but agent doesn't)
|
|
58
|
+
ORPHANS=()
|
|
59
|
+
while IFS= read -r path; do
|
|
60
|
+
ORPHANS+=("$path")
|
|
61
|
+
done < <(find "$ROOT" -type d -path "*/projects/*" -not -path "*/projects/_template*" -not -path "*/_archive/*" 2>/dev/null | while read p; do
|
|
62
|
+
PARENT=$(dirname $(dirname "$p"))
|
|
63
|
+
if [ -d "$PARENT" ] && [ ! -f "$PARENT/agent.md" ]; then
|
|
64
|
+
# This is a project folder under a non-agent — that's actually projects/ root
|
|
65
|
+
# We only care about instances: <fn>/<agent>/projects/<proj>/ where agent.md is missing
|
|
66
|
+
GRANDPARENT=$(dirname "$PARENT")
|
|
67
|
+
GP_NAME=$(basename "$GRANDPARENT")
|
|
68
|
+
if [ -n "$REGISTERED_FNS_PIPE" ] && echo "$GP_NAME" | grep -qE "^($REGISTERED_FNS_PIPE)\$"; then
|
|
69
|
+
echo "$p"
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
done)
|
|
73
|
+
|
|
74
|
+
for orphan in "${ORPHANS[@]:-}"; do
|
|
75
|
+
REL="${orphan#$ROOT/}"
|
|
76
|
+
REPO_WARNINGS+=("[$REL] orphaned instance — parent agent has no agent.md")
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
# Registered function should have a folder
|
|
80
|
+
while IFS= read -r fn; do
|
|
81
|
+
[ -z "$fn" ] && continue
|
|
82
|
+
if [ ! -d "$ROOT/$fn" ]; then
|
|
83
|
+
REPO_FAILURES+=("[$fn] registered in .config/functions.yaml but folder does not exist")
|
|
84
|
+
REPO_FAILURES+=(" → Suggested fix: mkdir $fn && cp <stub README>, OR remove from .config/functions.yaml")
|
|
85
|
+
fi
|
|
86
|
+
done < <(read_functions 2>/dev/null || true)
|
|
87
|
+
|
|
88
|
+
# has_expert: true → EXPERT.md must exist
|
|
89
|
+
while IFS=$'\t' read -r slug has_expert; do
|
|
90
|
+
[ -z "$slug" ] && continue
|
|
91
|
+
if [ "$has_expert" = "true" ] && [ ! -f "$ROOT/$slug/EXPERT.md" ]; then
|
|
92
|
+
REPO_WARNINGS+=("[$slug/EXPERT.md] registry says has_expert=true but file missing")
|
|
93
|
+
fi
|
|
94
|
+
done < <(read_functions_with_metadata 2>/dev/null || true)
|
|
95
|
+
|
|
96
|
+
# HITL channel env vars: every function should have SLACK_HITL_CHANNEL_<FN> in .env.example
|
|
97
|
+
ENV_EXAMPLE="$ROOT/.env.example"
|
|
98
|
+
if [ -f "$ENV_EXAMPLE" ]; then
|
|
99
|
+
while IFS= read -r fn; do
|
|
100
|
+
[ -z "$fn" ] && continue
|
|
101
|
+
var="SLACK_HITL_CHANNEL_$(echo "$fn" | tr '[:lower:]-' '[:upper:]_')"
|
|
102
|
+
if ! grep -q "^${var}=" "$ENV_EXAMPLE"; then
|
|
103
|
+
REPO_WARNINGS+=("[.env.example] missing $var (function '$fn' has no HITL channel env var)")
|
|
104
|
+
fi
|
|
105
|
+
done < <(read_functions 2>/dev/null || true)
|
|
106
|
+
if ! grep -q "^SLACK_HITL_CHANNEL_ADMIN=" "$ENV_EXAMPLE"; then
|
|
107
|
+
REPO_WARNINGS+=("[.env.example] missing SLACK_HITL_CHANNEL_ADMIN (used by dreamer + chief-of-staff)")
|
|
108
|
+
fi
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# Function-shaped top-level dirs not in the registry
|
|
112
|
+
KNOWN_NON_FUNCTIONS="dreamer chief-of-staff projects scripts logs _archive"
|
|
113
|
+
for dir in "$ROOT"/*/; do
|
|
114
|
+
basename=$(basename "$dir")
|
|
115
|
+
[[ "$basename" == .* ]] && continue
|
|
116
|
+
echo "$KNOWN_NON_FUNCTIONS" | grep -qw "$basename" && continue
|
|
117
|
+
if [ -n "$REGISTERED_FNS_PIPE" ] && echo "$basename" | grep -qE "^($REGISTERED_FNS_PIPE)\$"; then
|
|
118
|
+
continue
|
|
119
|
+
fi
|
|
120
|
+
if find "$dir" -maxdepth 2 -name 'agent.md' -type f 2>/dev/null | head -1 | grep -q .; then
|
|
121
|
+
REPO_WARNINGS+=("[$basename/] looks function-shaped but not registered in .config/functions.yaml")
|
|
122
|
+
REPO_WARNINGS+=(" → Suggested fix: add to registry via 'bash scripts/create-function.sh $basename' (or remove if intended)")
|
|
123
|
+
fi
|
|
124
|
+
done
|
|
125
|
+
|
|
126
|
+
# === Run audit-project for each project ===
|
|
127
|
+
PROJECTS=()
|
|
128
|
+
while IFS= read -r path; do
|
|
129
|
+
PROJECTS+=("$path")
|
|
130
|
+
done < <(find "$ROOT/projects" -maxdepth 1 -mindepth 1 -type d -not -name '_template' 2>/dev/null || true)
|
|
131
|
+
|
|
132
|
+
PROJECT_RESULTS=()
|
|
133
|
+
for proj_path in "${PROJECTS[@]:-}"; do
|
|
134
|
+
PROJ=$(basename "$proj_path")
|
|
135
|
+
RESULT=$(bash "$ROOT/scripts/audit-project.sh" "$PROJ" 2>&1 | head -5 || echo " (audit failed)")
|
|
136
|
+
PROJECT_RESULTS+=("### $PROJ")
|
|
137
|
+
PROJECT_RESULTS+=("\`\`\`")
|
|
138
|
+
while IFS= read -r line; do
|
|
139
|
+
PROJECT_RESULTS+=("$line")
|
|
140
|
+
done <<< "$RESULT"
|
|
141
|
+
PROJECT_RESULTS+=("\`\`\`")
|
|
142
|
+
PROJECT_RESULTS+=("")
|
|
143
|
+
done
|
|
144
|
+
|
|
145
|
+
# === Run audit-agent for each agent ===
|
|
146
|
+
AGENTS=()
|
|
147
|
+
while IFS= read -r fn; do
|
|
148
|
+
[ -z "$fn" ] && continue
|
|
149
|
+
if [ -d "$ROOT/$fn" ]; then
|
|
150
|
+
while IFS= read -r path; do
|
|
151
|
+
AGENTS+=("$fn:$(basename "$path")")
|
|
152
|
+
done < <(find "$ROOT/$fn" -maxdepth 1 -mindepth 1 -type d 2>/dev/null || true)
|
|
153
|
+
fi
|
|
154
|
+
done < <(read_functions 2>/dev/null || true)
|
|
155
|
+
|
|
156
|
+
AGENT_RESULTS=()
|
|
157
|
+
for entry in "${AGENTS[@]:-}"; do
|
|
158
|
+
FN="${entry%%:*}"
|
|
159
|
+
AGENT="${entry##*:}"
|
|
160
|
+
RESULT=$(bash "$ROOT/scripts/audit-agent.sh" "$FN" "$AGENT" 2>&1 | head -5 || echo " (audit failed)")
|
|
161
|
+
AGENT_RESULTS+=("### $FN/$AGENT")
|
|
162
|
+
AGENT_RESULTS+=("\`\`\`")
|
|
163
|
+
while IFS= read -r line; do
|
|
164
|
+
AGENT_RESULTS+=("$line")
|
|
165
|
+
done <<< "$RESULT"
|
|
166
|
+
AGENT_RESULTS+=("\`\`\`")
|
|
167
|
+
AGENT_RESULTS+=("")
|
|
168
|
+
done
|
|
169
|
+
|
|
170
|
+
# === Status ===
|
|
171
|
+
if [ ${#REPO_FAILURES[@]} -gt 0 ]; then
|
|
172
|
+
STATUS="fail"
|
|
173
|
+
elif [ ${#REPO_WARNINGS[@]} -gt 0 ]; then
|
|
174
|
+
STATUS="warn"
|
|
175
|
+
else
|
|
176
|
+
STATUS="pass"
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# Write report
|
|
180
|
+
{
|
|
181
|
+
echo "---"
|
|
182
|
+
echo "operation: audit-repo"
|
|
183
|
+
echo "ran: $TIMESTAMP"
|
|
184
|
+
echo "status: $STATUS"
|
|
185
|
+
echo "---"
|
|
186
|
+
echo ""
|
|
187
|
+
echo "# Repo Audit"
|
|
188
|
+
echo ""
|
|
189
|
+
echo "## Summary"
|
|
190
|
+
echo "- Projects audited: ${#PROJECTS[@]}"
|
|
191
|
+
echo "- Agents audited: ${#AGENTS[@]}"
|
|
192
|
+
echo "- Repo-level passed: ${#REPO_PASSED[@]}"
|
|
193
|
+
echo "- Repo-level warnings: ${#REPO_WARNINGS[@]}"
|
|
194
|
+
echo "- Repo-level failures: ${#REPO_FAILURES[@]}"
|
|
195
|
+
echo ""
|
|
196
|
+
if [ ${#REPO_FAILURES[@]} -gt 0 ]; then
|
|
197
|
+
echo "## Repo-level Failures"
|
|
198
|
+
for line in "${REPO_FAILURES[@]}"; do
|
|
199
|
+
echo "- $line"
|
|
200
|
+
done
|
|
201
|
+
echo ""
|
|
202
|
+
fi
|
|
203
|
+
if [ ${#REPO_WARNINGS[@]} -gt 0 ]; then
|
|
204
|
+
echo "## Repo-level Warnings"
|
|
205
|
+
for line in "${REPO_WARNINGS[@]}"; do
|
|
206
|
+
echo "- $line"
|
|
207
|
+
done
|
|
208
|
+
echo ""
|
|
209
|
+
fi
|
|
210
|
+
if [ ${#REPO_PASSED[@]} -gt 0 ]; then
|
|
211
|
+
echo "## Repo-level Passed"
|
|
212
|
+
for line in "${REPO_PASSED[@]}"; do
|
|
213
|
+
echo "- $line"
|
|
214
|
+
done
|
|
215
|
+
echo ""
|
|
216
|
+
fi
|
|
217
|
+
echo "## Project audits (summaries)"
|
|
218
|
+
echo ""
|
|
219
|
+
for line in "${PROJECT_RESULTS[@]:-}"; do
|
|
220
|
+
echo "$line"
|
|
221
|
+
done
|
|
222
|
+
echo ""
|
|
223
|
+
echo "## Agent audits (summaries)"
|
|
224
|
+
echo ""
|
|
225
|
+
for line in "${AGENT_RESULTS[@]:-}"; do
|
|
226
|
+
echo "$line"
|
|
227
|
+
done
|
|
228
|
+
echo ""
|
|
229
|
+
echo "Individual audit reports are in $LOG_DIR/"
|
|
230
|
+
} > "$REPORT"
|
|
231
|
+
|
|
232
|
+
# Print summary
|
|
233
|
+
echo "Repo audit: $STATUS"
|
|
234
|
+
echo " Projects: ${#PROJECTS[@]} | Agents: ${#AGENTS[@]}"
|
|
235
|
+
echo " Repo-level: ${#REPO_PASSED[@]} passed, ${#REPO_WARNINGS[@]} warnings, ${#REPO_FAILURES[@]} failures"
|
|
236
|
+
[ ${#REPO_FAILURES[@]} -gt 0 ] && {
|
|
237
|
+
echo "Repo failures:"
|
|
238
|
+
for line in "${REPO_FAILURES[@]}"; do echo " $line"; done
|
|
239
|
+
}
|
|
240
|
+
echo "Full report: $REPORT"
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# create-function.sh — register a new function category and scaffold its folder
|
|
3
|
+
# Usage: bash scripts/create-function.sh <slug> [--description "..."] [--with-expert] [--no-confirm]
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
8
|
+
CONFIG="$ROOT/.config/functions.yaml"
|
|
9
|
+
|
|
10
|
+
source "$ROOT/scripts/lib/functions.sh"
|
|
11
|
+
|
|
12
|
+
usage() {
|
|
13
|
+
cat <<EOF
|
|
14
|
+
Usage: $0 <slug> [--description "..."] [--with-expert] [--no-confirm]
|
|
15
|
+
|
|
16
|
+
<slug> required, lowercase kebab-case, ^[a-z][a-z0-9-]*\$
|
|
17
|
+
--description one-line description (prompted if omitted in TTY)
|
|
18
|
+
--with-expert scaffold EXPERT.md stub and set has_expert: true
|
|
19
|
+
--no-confirm skip the interactive proliferation prompt
|
|
20
|
+
|
|
21
|
+
Environment:
|
|
22
|
+
AGENT_TEAM_NO_CONFIRM=1 equivalent to --no-confirm
|
|
23
|
+
EOF
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if [ $# -lt 1 ]; then
|
|
27
|
+
usage >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
SLUG=""
|
|
32
|
+
DESCRIPTION=""
|
|
33
|
+
WITH_EXPERT="" # "" | "true" | "false"; "" means unresolved (will prompt)
|
|
34
|
+
DESC_PROVIDED=0
|
|
35
|
+
NO_CONFIRM=0
|
|
36
|
+
|
|
37
|
+
while [ $# -gt 0 ]; do
|
|
38
|
+
case "$1" in
|
|
39
|
+
--description)
|
|
40
|
+
[ $# -lt 2 ] && { echo "ERROR: --description requires a value" >&2; exit 1; }
|
|
41
|
+
DESCRIPTION="$2"
|
|
42
|
+
DESC_PROVIDED=1
|
|
43
|
+
shift 2
|
|
44
|
+
;;
|
|
45
|
+
--description=*)
|
|
46
|
+
DESCRIPTION="${1#--description=}"
|
|
47
|
+
DESC_PROVIDED=1
|
|
48
|
+
shift
|
|
49
|
+
;;
|
|
50
|
+
--with-expert)
|
|
51
|
+
WITH_EXPERT="true"
|
|
52
|
+
shift
|
|
53
|
+
;;
|
|
54
|
+
--no-confirm)
|
|
55
|
+
NO_CONFIRM=1
|
|
56
|
+
shift
|
|
57
|
+
;;
|
|
58
|
+
-h|--help)
|
|
59
|
+
usage
|
|
60
|
+
exit 0
|
|
61
|
+
;;
|
|
62
|
+
--*)
|
|
63
|
+
echo "ERROR: unknown flag '$1'" >&2
|
|
64
|
+
usage >&2
|
|
65
|
+
exit 1
|
|
66
|
+
;;
|
|
67
|
+
*)
|
|
68
|
+
if [ -z "$SLUG" ]; then
|
|
69
|
+
SLUG="$1"
|
|
70
|
+
shift
|
|
71
|
+
else
|
|
72
|
+
echo "ERROR: unexpected positional arg '$1'" >&2
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
if [ "${AGENT_TEAM_NO_CONFIRM:-0}" = "1" ]; then
|
|
80
|
+
NO_CONFIRM=1
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
if [ -z "$SLUG" ]; then
|
|
84
|
+
echo "ERROR: <slug> is required" >&2
|
|
85
|
+
usage >&2
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# 1. Validate slug format
|
|
90
|
+
if ! [[ "$SLUG" =~ ^[a-z][a-z0-9-]*$ ]]; then
|
|
91
|
+
echo "ERROR: slug '$SLUG' is invalid. Must be lowercase, start with a letter, and only contain a-z, 0-9, and hyphens." >&2
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# 2. Check config exists and is parseable
|
|
96
|
+
if [ ! -f "$CONFIG" ]; then
|
|
97
|
+
echo "ERROR: $CONFIG not found or unreadable" >&2
|
|
98
|
+
exit 1
|
|
99
|
+
fi
|
|
100
|
+
if ! read_functions >/dev/null 2>&1; then
|
|
101
|
+
echo "ERROR: failed to read $CONFIG (malformed YAML or unreadable)" >&2
|
|
102
|
+
read_functions >/dev/null # re-run to surface stderr
|
|
103
|
+
exit 1
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# 3. Check slug not already in registry
|
|
107
|
+
if is_valid_function "$SLUG"; then
|
|
108
|
+
echo "ERROR: function '$SLUG' is already registered in $CONFIG" >&2
|
|
109
|
+
exit 1
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# 4. Check folder doesn't exist
|
|
113
|
+
TARGET="$ROOT/$SLUG"
|
|
114
|
+
if [ -e "$TARGET" ]; then
|
|
115
|
+
echo "ERROR: '$TARGET' already exists on disk" >&2
|
|
116
|
+
exit 1
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# 5. Resolve description
|
|
120
|
+
is_tty() { [ -t 0 ] && [ -t 1 ]; }
|
|
121
|
+
|
|
122
|
+
if [ $DESC_PROVIDED -eq 0 ]; then
|
|
123
|
+
if is_tty; then
|
|
124
|
+
printf "Description (one line, e.g. 'Research — discovery, market analysis'): " >&2
|
|
125
|
+
IFS= read -r DESCRIPTION
|
|
126
|
+
fi
|
|
127
|
+
if [ -z "$DESCRIPTION" ]; then
|
|
128
|
+
echo "ERROR: --description not provided and not running interactively" >&2
|
|
129
|
+
exit 1
|
|
130
|
+
fi
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# Strip trailing whitespace
|
|
134
|
+
DESCRIPTION="${DESCRIPTION%"${DESCRIPTION##*[![:space:]]}"}"
|
|
135
|
+
if [ -z "$DESCRIPTION" ]; then
|
|
136
|
+
echo "ERROR: description is empty" >&2
|
|
137
|
+
exit 1
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# 6. Resolve has_expert
|
|
141
|
+
if [ -z "$WITH_EXPERT" ]; then
|
|
142
|
+
if is_tty; then
|
|
143
|
+
printf "Does this function need an expert? (y/N): " >&2
|
|
144
|
+
IFS= read -r ANS
|
|
145
|
+
case "$ANS" in
|
|
146
|
+
y|Y|yes|YES) WITH_EXPERT="true" ;;
|
|
147
|
+
*) WITH_EXPERT="false" ;;
|
|
148
|
+
esac
|
|
149
|
+
else
|
|
150
|
+
WITH_EXPERT="false"
|
|
151
|
+
fi
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# 7. Soft proliferation reminder
|
|
155
|
+
if [ "$NO_CONFIRM" -eq 0 ] && is_tty; then
|
|
156
|
+
cat >&2 <<EOF
|
|
157
|
+
About to create function: $SLUG
|
|
158
|
+
Reminder: function categories should map to how you mentally divide work.
|
|
159
|
+
Will you have at least 2-3 agents in this function within ~90 days?
|
|
160
|
+
If not, the agent likely fits an existing function. Press Ctrl-C to abort,
|
|
161
|
+
or any key to proceed.
|
|
162
|
+
EOF
|
|
163
|
+
read -r -n 1 _ || true
|
|
164
|
+
echo "" >&2
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# === Mutations begin here ===
|
|
168
|
+
# Track what we created so we can roll back on failure.
|
|
169
|
+
CREATED_PATHS=()
|
|
170
|
+
CONFIG_BACKUP="$(mktemp -t functions-yaml-backup.XXXXXX)" || {
|
|
171
|
+
echo "ERROR: could not create backup tempfile" >&2
|
|
172
|
+
exit 1
|
|
173
|
+
}
|
|
174
|
+
cp "$CONFIG" "$CONFIG_BACKUP"
|
|
175
|
+
|
|
176
|
+
rollback() {
|
|
177
|
+
echo "Rolling back partial changes..." >&2
|
|
178
|
+
for p in "${CREATED_PATHS[@]:-}"; do
|
|
179
|
+
[ -e "$p" ] && rm -rf "$p"
|
|
180
|
+
done
|
|
181
|
+
cp "$CONFIG_BACKUP" "$CONFIG"
|
|
182
|
+
}
|
|
183
|
+
trap 'rollback; rm -f "$CONFIG_BACKUP"' ERR
|
|
184
|
+
|
|
185
|
+
# 8. Append entry to YAML
|
|
186
|
+
{
|
|
187
|
+
printf ' - slug: %s\n' "$SLUG"
|
|
188
|
+
printf ' description: %s\n' "$DESCRIPTION"
|
|
189
|
+
printf ' has_expert: %s\n' "$WITH_EXPERT"
|
|
190
|
+
} >> "$CONFIG"
|
|
191
|
+
|
|
192
|
+
# 9. Create folder
|
|
193
|
+
mkdir -p "$TARGET"
|
|
194
|
+
CREATED_PATHS+=("$TARGET")
|
|
195
|
+
|
|
196
|
+
# 10. Stub README.md
|
|
197
|
+
README_PATH="$TARGET/README.md"
|
|
198
|
+
{
|
|
199
|
+
printf '# Global %s agents\n\n' "$SLUG"
|
|
200
|
+
printf '%s\n\n' "$DESCRIPTION"
|
|
201
|
+
printf 'No agents yet. Add via:\n\n'
|
|
202
|
+
printf '```bash\n'
|
|
203
|
+
printf 'bash scripts/new-agent.sh %s <agent-name>\n' "$SLUG"
|
|
204
|
+
printf '```\n\n'
|
|
205
|
+
printf "When you add the first agent here, see \`gtm/sdr/\` as the canonical example of an agent's directory layout.\n"
|
|
206
|
+
} > "$README_PATH"
|
|
207
|
+
|
|
208
|
+
# 11. Optional EXPERT.md stub
|
|
209
|
+
if [ "$WITH_EXPERT" = "true" ]; then
|
|
210
|
+
EXPERT_PATH="$TARGET/EXPERT.md"
|
|
211
|
+
cat > "$EXPERT_PATH" <<EOF
|
|
212
|
+
# $SLUG Expert
|
|
213
|
+
|
|
214
|
+
<!--
|
|
215
|
+
This is a stub. Fill in the expert system prompt for this function.
|
|
216
|
+
|
|
217
|
+
Experts shape SUBSTRATE (project guidelines), not artifacts. They critique
|
|
218
|
+
and generate guideline files in \`projects/<project>/guidelines/\`.
|
|
219
|
+
|
|
220
|
+
Required sections (see other EXPERT.md files for examples once they exist):
|
|
221
|
+
- Identity (1 paragraph)
|
|
222
|
+
- Scope (guide / critique / generate guidelines)
|
|
223
|
+
- Skill routing (table)
|
|
224
|
+
- Practitioner panel (optional, only if it adds value)
|
|
225
|
+
- Read-first protocol
|
|
226
|
+
- Output rules (what writes where, what doesn't)
|
|
227
|
+
- Stage filter (early-stage constraints)
|
|
228
|
+
|
|
229
|
+
Keep it concise. Lean on skills rather than restating their content here.
|
|
230
|
+
-->
|
|
231
|
+
|
|
232
|
+
# Stub: replace with the function's expert system prompt.
|
|
233
|
+
EOF
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
# 12. Append to operation log
|
|
237
|
+
LOG_DIR="$ROOT/chief-of-staff/logs/$(date +%Y-%m)"
|
|
238
|
+
mkdir -p "$LOG_DIR"
|
|
239
|
+
LOG_FILE="$LOG_DIR/operations-$(date +%Y-%m-%d).md"
|
|
240
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
241
|
+
{
|
|
242
|
+
echo ""
|
|
243
|
+
echo "## $TIMESTAMP — create-function: $SLUG"
|
|
244
|
+
echo "Description: $DESCRIPTION"
|
|
245
|
+
echo "has_expert: $WITH_EXPERT"
|
|
246
|
+
} >> "$LOG_FILE"
|
|
247
|
+
|
|
248
|
+
# Success — disarm rollback trap
|
|
249
|
+
trap - ERR
|
|
250
|
+
rm -f "$CONFIG_BACKUP"
|
|
251
|
+
|
|
252
|
+
echo ""
|
|
253
|
+
echo "✓ Function '$SLUG' created."
|
|
254
|
+
echo " Folder: $TARGET/"
|
|
255
|
+
echo " README: $README_PATH"
|
|
256
|
+
[ "$WITH_EXPERT" = "true" ] && echo " EXPERT.md: $TARGET/EXPERT.md (stub)"
|
|
257
|
+
echo " Registry: $CONFIG (entry appended)"
|
|
258
|
+
echo " Log: $LOG_FILE"
|
|
259
|
+
echo ""
|
|
260
|
+
SLUG_ENV=$(echo "$SLUG" | tr '[:lower:]-' '[:upper:]_')
|
|
261
|
+
|
|
262
|
+
echo "Reminders for the new function:"
|
|
263
|
+
echo " 1. HITL routing — agents in this function will route to #${SLUG}"
|
|
264
|
+
echo " - Create the Slack channel #${SLUG}"
|
|
265
|
+
echo " - Add to .env: SLACK_HITL_CHANNEL_${SLUG_ENV}=#${SLUG}"
|
|
266
|
+
[ "$WITH_EXPERT" = "true" ] && echo " 2. Fill in $TARGET/EXPERT.md with the expert system prompt"
|
|
267
|
+
echo " $([ "$WITH_EXPERT" = "true" ] && echo 3 || echo 2). Add the first agent: bash scripts/new-agent.sh $SLUG <agent-name>"
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
# Shared script libraries
|
|
2
2
|
|
|
3
|
-
Helper functions for use across scripts.
|
|
3
|
+
Helper functions for use across scripts.
|
|
4
4
|
|
|
5
5
|
Conventions:
|
|
6
6
|
- Bash: `<n>.sh`, sourced via `source "$(dirname $0)/lib/<n>.sh"`
|
|
7
7
|
- Python: `<n>.py` if needed (use `pip install --break-system-packages`)
|
|
8
8
|
- Keep functions narrow
|
|
9
9
|
|
|
10
|
+
## Current inhabitants
|
|
11
|
+
|
|
12
|
+
- `functions.sh` — read/validate the function registry at `.config/functions.yaml`. Pure bash + falls back to `python3` + `pyyaml` when available for safer YAML parsing.
|
|
13
|
+
- `bindings-prompt.sh` — interactive prompts for per-tool bindings during `new-agent-instance.sh`. Requires `python3` + `pyyaml` for the YAML output rendering (falls back gracefully when unavailable).
|
|
14
|
+
|
|
10
15
|
Example future additions:
|
|
11
16
|
- `lib/lesson.sh` — read/write lesson files, validate schema
|
|
12
17
|
- `lib/run.sh` — append to run files, format frontmatter
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# bindings-prompt.sh — read ## Tools and bindings from agent.md, prompt user, append YAML
|
|
3
|
+
#
|
|
4
|
+
# Args:
|
|
5
|
+
# $1 = path to agent.md (the global agent contract)
|
|
6
|
+
# $2 = path to instance config/default.yaml (will append tools: block)
|
|
7
|
+
#
|
|
8
|
+
# Behavior:
|
|
9
|
+
# 1. Extract YAML block from `## Tools and bindings` in agent.md
|
|
10
|
+
# 2. For each tool/key, prompt user via /dev/tty for value (showing required/optional + description)
|
|
11
|
+
# 3. Empty input or "skip" → write `# TODO: <description>` placeholder
|
|
12
|
+
# 4. Append a `tools:` block to the instance config
|
|
13
|
+
#
|
|
14
|
+
# Non-TTY environments fall back to TODO placeholders for every binding.
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
AGENT_MD="${1:-}"
|
|
19
|
+
INSTANCE_CONFIG="${2:-}"
|
|
20
|
+
|
|
21
|
+
if [ -z "$AGENT_MD" ] || [ -z "$INSTANCE_CONFIG" ]; then
|
|
22
|
+
echo "Usage: $0 <agent.md> <instance-config/default.yaml>" >&2
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
if [ ! -f "$AGENT_MD" ]; then
|
|
27
|
+
echo "ERROR: agent.md not found at $AGENT_MD" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
if [ ! -f "$INSTANCE_CONFIG" ]; then
|
|
31
|
+
echo "ERROR: instance config not found at $INSTANCE_CONFIG" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
36
|
+
echo "ERROR: python3 is required for bindings-prompt.sh" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# Drive prompts + YAML emission via python; pass file paths via env to avoid quoting hell.
|
|
41
|
+
export AGENT_MD INSTANCE_CONFIG
|
|
42
|
+
|
|
43
|
+
FINAL_YAML=$(python3 << 'PYEOF'
|
|
44
|
+
import os, re, sys
|
|
45
|
+
|
|
46
|
+
agent_md = os.environ["AGENT_MD"]
|
|
47
|
+
with open(agent_md) as f:
|
|
48
|
+
content = f.read()
|
|
49
|
+
|
|
50
|
+
m = re.search(r'## Tools and bindings.*?\n```yaml\n(.*?)\n```', content, re.DOTALL)
|
|
51
|
+
if not m:
|
|
52
|
+
sys.stderr.write(f"ERROR: no '## Tools and bindings' YAML block in {agent_md}\n")
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
schema_text = m.group(1)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
import yaml
|
|
59
|
+
except ImportError:
|
|
60
|
+
sys.stderr.write("ERROR: pyyaml required (pip3 install --user pyyaml)\n")
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
schema = yaml.safe_load(schema_text)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
sys.stderr.write(f"ERROR parsing bindings schema: {e}\n")
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
if not isinstance(schema, dict):
|
|
70
|
+
sys.stderr.write("Bindings schema is not a YAML mapping\n")
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
# Try to open /dev/tty for interactive input. Fall back to all-TODO if unavailable.
|
|
74
|
+
try:
|
|
75
|
+
tty = open("/dev/tty", "r")
|
|
76
|
+
interactive = True
|
|
77
|
+
except OSError:
|
|
78
|
+
tty = None
|
|
79
|
+
interactive = False
|
|
80
|
+
sys.stderr.write("(non-interactive environment — all bindings will be TODO placeholders)\n")
|
|
81
|
+
|
|
82
|
+
def yaml_quote(value: str) -> str:
|
|
83
|
+
if value == "":
|
|
84
|
+
return '""'
|
|
85
|
+
if any(c in value for c in [":", "#", "@", "{", "}", "[", "]", ",", "&", "*", "!", "|", ">", "'", '"', "%", "`"]):
|
|
86
|
+
escaped = value.replace('\\', '\\\\').replace('"', '\\"')
|
|
87
|
+
return f'"{escaped}"'
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
out_lines = []
|
|
91
|
+
out_lines.append("")
|
|
92
|
+
out_lines.append("# Tool bindings (filled via chief-of-staff scaffolding prompt)")
|
|
93
|
+
out_lines.append("tools:")
|
|
94
|
+
|
|
95
|
+
for tool, bindings in schema.items():
|
|
96
|
+
if not isinstance(bindings, dict):
|
|
97
|
+
continue
|
|
98
|
+
out_lines.append(f" {tool}:")
|
|
99
|
+
for key, meta in bindings.items():
|
|
100
|
+
if not isinstance(meta, dict):
|
|
101
|
+
continue
|
|
102
|
+
required = bool(meta.get("required", False))
|
|
103
|
+
description = str(meta.get("description", "") or "")
|
|
104
|
+
marker = "(required)" if required else "(optional)"
|
|
105
|
+
|
|
106
|
+
value = ""
|
|
107
|
+
if interactive:
|
|
108
|
+
sys.stderr.write(f"\n {tool}.{key} {marker}\n")
|
|
109
|
+
sys.stderr.write(f" {description}\n")
|
|
110
|
+
sys.stderr.write(f" > ")
|
|
111
|
+
sys.stderr.flush()
|
|
112
|
+
try:
|
|
113
|
+
value = tty.readline().strip()
|
|
114
|
+
except (OSError, KeyboardInterrupt):
|
|
115
|
+
value = ""
|
|
116
|
+
|
|
117
|
+
if value == "" or value.lower() == "skip":
|
|
118
|
+
out_lines.append(f" {key}: # TODO: {description}")
|
|
119
|
+
else:
|
|
120
|
+
out_lines.append(f" {key}: {yaml_quote(value)}")
|
|
121
|
+
|
|
122
|
+
print("\n".join(out_lines))
|
|
123
|
+
|
|
124
|
+
if tty is not None:
|
|
125
|
+
tty.close()
|
|
126
|
+
PYEOF
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Append to instance config
|
|
130
|
+
{
|
|
131
|
+
echo ""
|
|
132
|
+
echo "$FINAL_YAML"
|
|
133
|
+
} >> "$INSTANCE_CONFIG"
|
|
134
|
+
|
|
135
|
+
echo "" >&2
|
|
136
|
+
echo "✓ Tool bindings appended to $INSTANCE_CONFIG" >&2
|
|
@@ -20,7 +20,8 @@ read_functions() {
|
|
|
20
20
|
fi
|
|
21
21
|
if _have_pyyaml; then
|
|
22
22
|
python3 -c "
|
|
23
|
-
import yaml, sys
|
|
23
|
+
import yaml, re, sys
|
|
24
|
+
SLUG_RE = re.compile(r'^[a-z][a-z0-9-]*\$')
|
|
24
25
|
try:
|
|
25
26
|
with open('$config') as f:
|
|
26
27
|
data = yaml.safe_load(f) or {}
|
|
@@ -29,12 +30,23 @@ except yaml.YAMLError as e:
|
|
|
29
30
|
sys.exit(1)
|
|
30
31
|
for fn in data.get('functions', []):
|
|
31
32
|
slug = fn.get('slug', '')
|
|
32
|
-
if slug:
|
|
33
|
-
|
|
33
|
+
if not slug:
|
|
34
|
+
continue
|
|
35
|
+
if not SLUG_RE.match(slug):
|
|
36
|
+
sys.stderr.write(\"ERROR: malformed slug '\" + slug + \"' in $config — must match ^[a-z][a-z0-9-]*\$\n\")
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
print(slug)
|
|
34
39
|
" || return 1
|
|
35
40
|
else
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
local slug
|
|
42
|
+
while IFS= read -r slug; do
|
|
43
|
+
if ! [[ "$slug" =~ ^[a-z][a-z0-9-]*$ ]]; then
|
|
44
|
+
echo "ERROR: malformed slug '$slug' in $config — must match ^[a-z][a-z0-9-]*\$" >&2
|
|
45
|
+
return 1
|
|
46
|
+
fi
|
|
47
|
+
printf '%s\n' "$slug"
|
|
48
|
+
done < <(grep -E '^[[:space:]]*-[[:space:]]*slug:[[:space:]]*' "$config" \
|
|
49
|
+
| sed -E 's/^[[:space:]]*-[[:space:]]*slug:[[:space:]]*//; s/[[:space:]]*$//')
|
|
38
50
|
fi
|
|
39
51
|
}
|
|
40
52
|
|