@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,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. Empty for now — add as scripts grow.
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
- print(slug)
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
- grep -E '^[[:space:]]*-[[:space:]]*slug:[[:space:]]*' "$config" \
37
- | sed -E 's/^[[:space:]]*-[[:space:]]*slug:[[:space:]]*//; s/[[:space:]]*$//'
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