This repository was archived by the owner on Oct 8, 2026. It is now read-only.
Repository navigation
Expand file tree
/
Copy pathcodegen-build
More file actions
executable file
·373 lines (349 loc) · 15.7 KB
/
Copy pathcodegen-build
File metadata and controls
executable file
·373 lines (349 loc) · 15.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
#!/usr/bin/env bash
# codegen-build — canonical entrypoint for all harness-based build invocations.
#
# Usage:
# codegen-build --harness=claude [--stack=<phoenix|static>] [options] PROMPT
#
# Options:
# --harness=claude required: which harness to use
# --stack=<phoenix|static> optional: project stack. When omitted, detected
# from cwd markers (mix.exs -> phoenix,
# vite.config.js -> static; codegen self-build
# markers -> phoenix). Fails loud (non-zero exit)
# when undetectable or ambiguous (both markers
# present) -- never silent-defaults.
# --cwd=<dir> working directory (default: $PWD)
# --model=<m> advisory only -- NOT consumed by the loop.
# Per-role models resolve from
# templates/generator/config.yaml via
# RoleResolver; this flag is accepted (for
# caller compatibility) but has no effect.
# Use --fallback-model for a live override.
# --effort=<e> live one-build override: replaces every
# non-fixed-campaign role's configured effort
# for THIS build only (off|low|medium|high|
# xhigh|max). Threaded via
# CODEGEN_BUILD_EFFORT, not EXTRA_FLAGS --
# never blind-passed to dispatch. Omitted: each
# role's templates/generator/config.yaml
# effort wins. Invalid value: exit 2 before
# any dispatch.
# --max-budget-usd=<n> abort the cycle between role invocations once
# accumulated spend reaches <n> USD (in-flight
# role always completes); threaded via
# CODEGEN_BUILD_MAX_BUDGET_USD, not EXTRA_FLAGS --
# never blind-passed to dispatch. Omitted: no cap.
# --fallback-model=<m> prepend <m> as rung 0 of every role's
# `fallback:` chain for this build only (one-build
# override of templates/generator/config.yaml);
# threaded via CODEGEN_BUILD_FALLBACK_MODEL, not
# EXTRA_FLAGS -- never blind-passed to dispatch
# --commit-subject=<text> explicit substitute for a pitch's own
# commit_subject: frontmatter field; threaded via
# CODEGEN_BUILD_COMMIT_SUBJECT, not EXTRA_FLAGS --
# never blind-passed to dispatch. REQUIRED for a
# literal-prompt build (no frontmatter to read);
# optional for a pitch build that already carries
# commit_subject: -- and the escape hatch for a
# pitch in a repo with no shape workflow.
# --print-dir print this script's directory and exit 0
# --print-argv dry-run: parse + validate every flag, print the
# would-be `mix codegen.loop` argv one arg per
# line on stdout, exit 0. Skips the integrate
# pre-step and the gate-result post-step -- never
# mutates the target cwd, never execs the loop.
# -h, --help print usage and exit 0
# PROMPT the prompt string (required, positional, last arg)
#
# Exit codes from dispatch propagate (0 = success, 1 = error, 2 = config/usage error, 130 = SIGINT).
# STDERR: diagnostics only. STDOUT: byte-transparent (stream-json from harness).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
# Resolve the effort-canonical helper the same way codegen-call resolves its
# own harnesses/ dir: direct/test invocations have harnesses/ alongside this
# script; installed copies (~/.local/bin/) fall back to OCG_CODEGEN_DIR.
_effort_canonical_dir="$SCRIPT_DIR"
if [[ ! -f "$_effort_canonical_dir/harnesses/shared/effort-canonical.sh" ]] && [[ -n "${OCG_CODEGEN_DIR:-}" ]]; then
_effort_canonical_dir="$OCG_CODEGEN_DIR"
fi
# shellcheck source=harnesses/shared/effort-canonical.sh
source "$_effort_canonical_dir/harnesses/shared/effort-canonical.sh"
HARNESS=""
STACK=""
CWD="$PWD"
MODEL=""
EFFORT=""
FALLBACK_MODEL=""
MAX_BUDGET_USD=""
COMMIT_SUBJECT=""
EXTRA_FLAGS=()
PRINT_ARGV=""
PROMPT=""
usage() {
printf 'Usage: codegen-build --harness=claude [--stack=<phoenix|static>] [--cwd=<dir>] [--model=<m>] [--effort=<e>] [--max-budget-usd=<n>] [--fallback-model=<m>] [--commit-subject=<text>] [--print-dir] [--print-argv] [-h|--help] PROMPT\n' >&2
exit 2
}
while [[ $# -gt 0 ]]; do
case "$1" in
--harness=*)
HARNESS="${1#--harness=}"
shift
;;
--harness)
HARNESS="$2"
shift 2
;;
--stack=*)
STACK="${1#--stack=}"
shift
;;
--stack)
STACK="$2"
shift 2
;;
--cwd=*)
CWD="${1#--cwd=}"
shift
;;
--cwd)
CWD="$2"
shift 2
;;
--model=*)
MODEL="${1#--model=}"
shift
;;
--model)
MODEL="$2"
shift 2
;;
--effort=*)
EFFORT="${1#--effort=}"
shift
;;
--effort)
EFFORT="$2"
shift 2
;;
--max-budget-usd=*)
MAX_BUDGET_USD="${1#--max-budget-usd=}"
shift
;;
--max-budget-usd)
MAX_BUDGET_USD="$2"
shift 2
;;
--fallback-model=*)
FALLBACK_MODEL="${1#--fallback-model=}"
shift
;;
--fallback-model)
FALLBACK_MODEL="$2"
shift 2
;;
--commit-subject=*)
COMMIT_SUBJECT="${1#--commit-subject=}"
shift
;;
--commit-subject)
COMMIT_SUBJECT="$2"
shift 2
;;
--print-dir)
printf '%s\n' "$SCRIPT_DIR"
exit 0
;;
--print-argv)
PRINT_ARGV="1"
shift
;;
-h | --help)
printf 'Usage: codegen-build --harness=claude [--stack=<phoenix|static>] [--cwd=<dir>] [--model=<m>] [--effort=<e>] [--max-budget-usd=<n>] [--fallback-model=<m>] [--commit-subject=<text>] [--print-dir] [--print-argv] [-h|--help] PROMPT\n'
exit 0
;;
--)
shift
PROMPT="$*"
break
;;
--*)
printf 'codegen-build: unknown flag %q\n' "$1" >&2
usage
;;
*)
if [[ -z "$PROMPT" ]]; then
PROMPT="$1"
else
PROMPT="$PROMPT $1"
fi
shift
;;
esac
done
# Validate required args
if [[ -z "$HARNESS" ]]; then
printf 'codegen-build: --harness is required\n' >&2
usage
fi
if [[ "$HARNESS" != "claude" ]]; then
printf 'codegen-build: --harness must be "claude", got %q\n' "$HARNESS" >&2
usage
fi
# --stack is optional: use it when passed; otherwise detect from cwd markers.
# Fail loud when undetectable or ambiguous -- never silent-default (a silent
# phoenix default was removed in 66ec8d5; do not re-introduce it).
if [[ -z "$STACK" ]]; then
if [[ -f "$CWD/harnesses/claude/manifest.yaml" && -f "$CWD/templates/generator/generate.sh" ]]; then
# Self-build markers -> phoenix. Fires before the file sniffs so the
# codegen repo's root index.html fixture never mis-detects.
STACK="phoenix"
printf 'codegen-build: stack not passed; detected stack=phoenix from cwd markers\n' >&2
elif [[ -f "$CWD/mix.exs" && -f "$CWD/vite.config.js" ]]; then
printf 'codegen-build: ambiguous stack markers in cwd (mix.exs and vite.config.js both present); pass --stack explicitly\n' >&2
usage
elif [[ -f "$CWD/mix.exs" ]]; then
STACK="phoenix"
printf 'codegen-build: stack not passed; detected stack=phoenix from cwd markers\n' >&2
elif [[ -f "$CWD/vite.config.js" ]]; then
STACK="static"
printf 'codegen-build: stack not passed; detected stack=static from cwd markers\n' >&2
else
printf 'codegen-build: cannot detect stack from cwd; pass --stack=<phoenix|static>\n' >&2
usage
fi
fi
if [[ "$STACK" != "phoenix" && "$STACK" != "static" ]]; then
printf 'codegen-build: --stack must be "phoenix" or "static", got %q\n' "$STACK" >&2
usage
fi
if [[ -n "$EFFORT" ]] && ! codegen_validate_effort "$EFFORT"; then
usage
fi
# ── Pre-step: staleness preflight (refuse builds wired at a behind schema) ──
# Fail-closed: an app whose stamped scaffold schema version is BEHIND the
# codegen repo's current version must be consciously re-integrated first.
# Skipped on self-build (codegen repo has no per-app manifest) and when the
# manifest is absent/unparseable (fresh clone / pre-manifest app → the
# integrate pre-step below stamps a baseline).
if [[ ! (-f "$CWD/harnesses/claude/manifest.yaml" && -f "$CWD/templates/generator/generate.sh") ]]; then
_schema_file="$SCRIPT_DIR/shared/scaffold/SCHEMA_VERSION"
if [[ -f "$_schema_file" && -f "$CWD/codegen/manifest.yaml" ]]; then
_current_ver="$(cat "$_schema_file" 2>/dev/null || echo "")"
_stamped_ver="$(yq '.scaffold_schema_version // ""' "$CWD/codegen/manifest.yaml" 2>/dev/null || echo "")"
# Only refuse on a positively-parsed integer that is strictly behind.
if [[ "$_current_ver" =~ ^[0-9]+$ && "$_stamped_ver" =~ ^[0-9]+$ && "$_stamped_ver" -lt "$_current_ver" ]]; then
printf 'codegen-build: app wired at scaffold schema v%s; codegen now v%s — run '\''codegen-scaffold integrate --stack=%s --cwd=%s'\'' to migrate, then rebuild\n' \
"$_stamped_ver" "$_current_ver" "$STACK" "$CWD" >&2
exit 2
fi
fi
fi
# ── Pre-step: run integrate stage to wire agent files + PROJECT_CONTEXT.md ──────
# Idempotent: all writes are guarded by existence checks in run_integrate_stage.
# Creates PROJECT_CONTEXT.md, restart_server.sh, codegen/ symlinks, Makefile targets.
# The LLM then commits these along with the app code.
# Skipped entirely under --print-argv: a dry-run must never mutate the target cwd.
SCAFFOLD_CMD="$SCRIPT_DIR/codegen-scaffold"
if [[ -z "$PRINT_ARGV" ]] && [[ -x "$SCAFFOLD_CMD" ]]; then
# Self-build guard (content-marker, location-independent): when codegen
# builds itself, $CWD is the codegen repo root. Running integrate would
# re-symlink AGENTS.md/CLAUDE.md over the committed symlinks and re-append
# .gitignore entries. $SCRIPT_DIR is unreliable here (the installed launcher
# lives in ~/.local/bin), so detect the codegen root by the presence of
# BOTH generator artifacts — never present in a downstream scaffolded app.
if [[ -f "$CWD/harnesses/claude/manifest.yaml" && -f "$CWD/templates/generator/generate.sh" ]]; then
printf 'codegen-build: self-build detected (codegen repo markers present in cwd); skipping integrate pre-step\n' >&2
else
# Fatal, not "non-fatal; continuing": this pre-step is what rebuilds
# AGENTS.md, codegen/rules, PROJECT_CONTEXT.md, and .claude/gate-config.sh —
# a failure here means the build would otherwise proceed with no rules
# and no gate config, silently.
_integrate_out=$("$SCAFFOLD_CMD" integrate --stack="$STACK" --cwd="$CWD" 2>&1) || {
printf 'codegen-build: FATAL: codegen-scaffold integrate failed:\n%s\n' "$_integrate_out" >&2
exit 1
}
fi
fi
# Export parsed values for dispatch.sh
INVOCATION_FILE=""
if [[ -z "$PRINT_ARGV" ]]; then
_pending_dir="$CWD/codegen/gate-pending"
mkdir -p "$_pending_dir"
INVOCATION_FILE="$(mktemp "$_pending_dir/codegen-invocation.$(date -u +%s).$$.XXXXXX")"
CODEGEN_BUILD_INVOCATION_ID="$(basename "$INVOCATION_FILE")"
rm -f "$_pending_dir/build-result.json"
trap 'rm -f "$INVOCATION_FILE"' EXIT
fi
export CODEGEN_BUILD_HARNESS="$HARNESS"
export CODEGEN_BUILD_STACK="$STACK"
export CODEGEN_BUILD_CWD="$CWD"
export CODEGEN_BUILD_FALLBACK_MODEL="${FALLBACK_MODEL:-}"
export CODEGEN_BUILD_EFFORT="${EFFORT:-}"
export CODEGEN_BUILD_MAX_BUDGET_USD="${MAX_BUDGET_USD:-}"
export CODEGEN_BUILD_COMMIT_SUBJECT="${COMMIT_SUBJECT:-}"
export CODEGEN_BUILD_PRINT_ARGV="${PRINT_ARGV:-}"
export CODEGEN_BUILD_INVOCATION_ID="${CODEGEN_BUILD_INVOCATION_ID:-}"
export CLAUDE_ASYNC_AGENT_STALL_TIMEOUT_MS=0
export CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0
export CLAUDE_STREAM_IDLE_TIMEOUT_MS=0
DISPATCH="$SCRIPT_DIR/harnesses/$HARNESS/dispatch.sh"
if [[ ! -x "$DISPATCH" ]]; then
printf 'codegen-build: dispatch script not found or not executable: %s\n' "$DISPATCH" >&2
exit 2
fi
_expected_slug="adhoc"
_expected_pitch_path="${PROMPT#@}"
if [[ -n "$PROMPT" ]]; then
if [[ -f "$CWD/$_expected_pitch_path" ]]; then
_expected_slug="$(basename "$_expected_pitch_path" .md)"
elif [[ -f "$_expected_pitch_path" ]]; then
_expected_slug="$(basename "$_expected_pitch_path" .md)"
fi
fi
_dispatch_rc=0
if [[ -n "$PROMPT" ]]; then
"$DISPATCH" "${EXTRA_FLAGS[@]+"${EXTRA_FLAGS[@]}"}" "$PROMPT" || _dispatch_rc=$?
else
"$DISPATCH" "${EXTRA_FLAGS[@]+"${EXTRA_FLAGS[@]}"}" || _dispatch_rc=$?
fi
# ── Post-step: a successful wrapper run needs BOTH fresh loop-owned result
# evidence and a clear authoritative gate verdict. ─────────────────────────────
_gate_result="$CWD/codegen/gate-pending/gate-result.json"
_build_result="$CWD/codegen/gate-pending/build-result.json"
if [[ -z "$PRINT_ARGV" ]] && [[ "$_dispatch_rc" -eq 0 ]]; then
if [[ ! -f "$_build_result" ]]; then
printf 'codegen-build: %s build missing build-result.json after successful dispatch\n' "$HARNESS" >&2
_dispatch_rc=1
else
# HEAD binding is the loop's own contract, and the loop fails OPEN on an
# unresolvable HEAD: `Mix.Tasks.Codegen.Loop.git_head/1` maps a nonzero
# `git rev-parse HEAD` to `:unborn`, and `verify_commit_landed(:unborn, _cwd)`
# returns `{:ok, nil}` — "nothing to compare", not "verification failed".
# The wrapper must mirror that carve-out or the two halves of one contract
# disagree and the wrapper's half wins: an unborn/non-git cwd used to fail
# the whole build with `cannot resolve current HEAD` while the loop had
# already, deliberately, waived the same check. Every OTHER field
# (invocation_id, slug, status, updated_at) stays bound unconditionally —
# only the head equality is waived, and only when there is no HEAD to
# compare against.
_current_head="$(git -C "$CWD" rev-parse HEAD 2>/dev/null || printf '')"
if [[ -z "$_current_head" ]]; then
_head_filter='true'
else
_head_filter='(.head == $expected_head)'
fi
if ! jq -e \
--arg invocation_id "$CODEGEN_BUILD_INVOCATION_ID" \
--arg expected_slug "$_expected_slug" \
--arg expected_head "$_current_head" \
"(.invocation_id == \$invocation_id) and (.slug == \$expected_slug) and (.status == \"success\") and $_head_filter and (.updated_at | type == \"string\" and length > 0)" \
"$_build_result" >/dev/null 2>&1; then
printf 'codegen-build: %s build result missing, malformed, stale, or mismatched\n' "$HARNESS" >&2
_dispatch_rc=1
elif [[ ! -f "$_gate_result" ]] || ! jq -e '.verdict == "clear"' "$_gate_result" >/dev/null 2>&1; then
printf 'codegen-build: %s build gate verdict missing, unreadable, or not clear\n' "$HARNESS" >&2
_dispatch_rc=1
fi
fi
fi
exit "$_dispatch_rc"