This repository was archived by the owner on Oct 8, 2026. It is now read-only.
Repository navigation
Expand file tree
/
Copy pathcodegen-advise
More file actions
executable file
·479 lines (435 loc) · 17.7 KB
/
Copy pathcodegen-advise
File metadata and controls
executable file
·479 lines (435 loc) · 17.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
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
#!/usr/bin/env bash
# codegen-advise — black-box "problem in, diagnosis out" advisor call: the
# SAME build harness, at a STRONGER model and higher effort than the caller,
# handed a MACHINE-BUILT evidence packet instead of free-text narration.
#
# Usage:
# codegen-advise --harness=claude_code --cwd=<abs-path> --context @<abs-path>
# codegen-advise --harness=claude_code --cwd=<abs-path> <<< "problem text on stdin"
#
# --harness names the CURRENT build harness (the one asking); this script
# maps it to a stronger tier of the SAME harness via a FIXED internal
# mapping — deliberate operator decision (see pitch "a stuck developer asks
# before it guesses" D1): not configurable. There is one installed harness
# today, so a cross-vendor second opinion is unbuildable by definition; when
# a second harness exists, restoring a cross-vendor flip is a separate,
# later bet.
#
# --cwd is REQUIRED: the packet assembler collects git state (HEAD, diff,
# base attribution, gate/process artifacts) from THIS path. A silent
# default to the process cwd would collect the WRONG repo's state when the
# caller shells this from a directory other than the project root (see pitch
# "the advisor is handed a paragraph" D-9) — so omitting it is a loud usage
# error, never a fallback.
#
# The caller's --context/stdin text becomes section 1 of an assembled
# packet (question, failure signature, base attribution, attempt/stage,
# source+test slices, exact failure output, current diff, process/gate
# artifacts) bounded at 65536 bytes total. See § PACKET ASSEMBLY below.
#
# Exit codes:
# 0 — advisor diagnosis printed on stdout (one JSON object)
# 1 — codegen-call returned a non-success envelope / unparseable stdout
# 2 — usage error: missing/unknown --harness, missing --cwd, missing/empty --context,
# or sections 1-4 alone exceed the packet ceiling
# 127 — codegen-call binary not found
set -euo pipefail
# Derive CODEGEN_DIR from this script's own location (works symlinked via PATH).
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
CODEGEN_DIR="$SCRIPT_DIR"
CODEGEN_CALL="$CODEGEN_DIR/codegen-call"
SYSTEM_PROMPT_FILE="$CODEGEN_DIR/harnesses/claude/advise-system-prompt.md"
JSON_SCHEMA_FILE="$CODEGEN_DIR/harnesses/claude/advise.schema.json"
# Packet ceiling: 64 KiB. The packet crosses argv twice (codegen-advise ->
# codegen-call -- "$CONTEXT_TEXT" -> the harness CLI) AND is exported as the
# env var CODEGEN_CALL_PROMPT (codegen-call:346). Linux caps a single argv
# or env string at MAX_ARG_STRLEN = 131072 bytes, hardcoded, not tunable.
# 64 KiB sits at 2x headroom under that limit, so the darwin/linux
# difference stops being load-bearing (darwin measured to tolerate a
# 260000-byte single arg with no trouble).
PACKET_CEILING_BYTES=65536
# Per-file/per-section truncation bound for the diff and source-slice
# sections (lines, not bytes — the byte ceiling is enforced on the WHOLE
# assembled packet after all sections are built).
DIFF_LINES_PER_FILE=200
FAILURE_OUTPUT_LINES=200
SOURCE_SLICE_CONTEXT_LINES=40
HARNESS=""
CWD=""
CONTEXT_PATH=""
CONTEXT_FROM_STDIN=""
usage() {
printf 'Usage: codegen-advise --harness=claude_code --cwd=<abs-path> (--context @<path> | via stdin)\n' >&2
exit 2
}
while [[ $# -gt 0 ]]; do
case "$1" in
--harness=*)
HARNESS="${1#--harness=}"
shift
;;
--harness)
HARNESS="$2"
shift 2
;;
--cwd=*)
CWD="${1#--cwd=}"
shift
;;
--cwd)
CWD="$2"
shift 2
;;
--context=*)
CONTEXT_PATH="${1#--context=}"
shift
;;
--context)
CONTEXT_PATH="$2"
shift 2
;;
--*)
printf 'codegen-advise: unknown flag %q\n' "$1" >&2
usage
;;
*)
printf 'codegen-advise: unexpected positional arg %q\n' "$1" >&2
usage
;;
esac
done
if [[ -z "$HARNESS" ]]; then
printf 'codegen-advise: --harness is required\n' >&2
usage
fi
if [[ "$HARNESS" != "claude_code" ]]; then
printf 'codegen-advise: --harness must be "claude_code", got %q\n' "$HARNESS" >&2
usage
fi
if [[ -z "$CWD" ]]; then
printf 'codegen-advise: --cwd is required (the repo the build is happening in)\n' >&2
usage
fi
# Resolve the problem context: @<path> flag, or stdin when the flag is absent.
CONTEXT_TEXT=""
if [[ -n "$CONTEXT_PATH" ]]; then
if [[ "$CONTEXT_PATH" != @* ]]; then
printf 'codegen-advise: --context value must start with @ (got %q)\n' "$CONTEXT_PATH" >&2
usage
fi
_path="${CONTEXT_PATH#@}"
if [[ ! -f "$_path" ]]; then
printf 'codegen-advise: --context file not found: %s\n' "$_path" >&2
usage
fi
CONTEXT_TEXT="$(cat "$_path")"
else
CONTEXT_TEXT="$(cat)"
CONTEXT_FROM_STDIN="1"
fi
if [[ -z "${CONTEXT_TEXT//[[:space:]]/}" ]]; then
if [[ -n "$CONTEXT_FROM_STDIN" ]]; then
printf 'codegen-advise: stdin context is empty\n' >&2
else
printf 'codegen-advise: --context file is empty\n' >&2
fi
usage
fi
# ---------------------------------------------------------------------------
# § PACKET ASSEMBLY
#
# Builds a bounded, machine-collected evidence packet from git state at
# --cwd, then hands it (not the bare caller question) to codegen-call.
# Section absence is explicit ("[unavailable: <reason>]"), never silent —
# an advisor told nothing about a section must not be able to mistake that
# for "nothing there". Sections 1-4 (question, failure signature,
# attempt/stage, base attribution) are NEVER truncated — they are cheap and
# load-bearing; exceeding the ceiling on them alone is a usage error (exit
# 2), never a silently-shrunk question. Sections 5-8 shrink, in this order,
# when the assembled packet exceeds PACKET_CEILING_BYTES: (7) current diff,
# (6) exact failure output, (5) source+test slices, (8) process/gate
# artifacts.
# ---------------------------------------------------------------------------
_in_git_repo() {
git -C "$CWD" rev-parse --is-inside-work-tree >/dev/null 2>&1
}
# section_1_question — the caller's own text. Whole, never truncated.
section_1_question() {
printf '## 1. The question\n\n%s\n' "$CONTEXT_TEXT"
}
# section_2_failure_signature — best-effort extraction of a gate/test
# failure signature from gate-result.json, when present. Never fails the
# assembly when absent.
section_2_failure_signature() {
local result_file="$CWD/codegen/gate-pending/gate-result.json"
if [[ ! -f "$result_file" ]]; then
printf '## 2. Failure signature\n\n[unavailable: no codegen/gate-pending/gate-result.json at --cwd]\n'
return 0
fi
local verdict witness gate log
verdict="$(jq -r '.verdict // ""' "$result_file" 2>/dev/null || true)"
witness="$(jq -r '.witness // ""' "$result_file" 2>/dev/null || true)"
gate="$(jq -r '.gate // ""' "$result_file" 2>/dev/null || true)"
log="$(jq -r '.log // ""' "$result_file" 2>/dev/null || true)"
printf '## 2. Failure signature\n\nGate: %s\nVerdict: %s\nWitness: %s\nLog: %s\n' \
"$gate" "$verdict" "$witness" "$log"
}
# section_3_base_attribution — HEAD, its subject, and (when the failure
# signature named a file) the last commit that touched it. This is the
# section that would have surfaced the observed incident's real cause: the
# commit that INTRODUCED the failing test, not just the commit HEAD sits on.
section_3_base_attribution() {
if ! _in_git_repo; then
printf '## 3. Base attribution\n\n[unavailable: --cwd is not a git work tree]\n'
return 0
fi
local head_sha head_subject
head_sha="$(git -C "$CWD" rev-parse HEAD 2>/dev/null || echo "")"
head_subject="$(git -C "$CWD" log -1 --format=%s 2>/dev/null || echo "")"
printf '## 3. Base attribution\n\nHEAD: %s\nHEAD subject: %s\n' "$head_sha" "$head_subject"
local witness_file
witness_file="$(jq -r '.witness // ""' "$CWD/codegen/gate-pending/gate-result.json" 2>/dev/null || true)"
witness_file="${witness_file%%:*}"
witness_file="${witness_file%% *}"
if [[ -n "$witness_file" ]] && [[ -f "$CWD/$witness_file" ]]; then
local intro_sha intro_subject
intro_sha="$(git -C "$CWD" log -1 --format=%H -- "$witness_file" 2>/dev/null || echo "")"
intro_subject="$(git -C "$CWD" log -1 --format=%s -- "$witness_file" 2>/dev/null || echo "")"
printf 'Last commit touching %s: %s (%s)\n' "$witness_file" "$intro_sha" "$intro_subject"
fi
}
# section_4_attempt_stage — attempt number, ceiling, stage, final-attempt
# flag, reproduction count. Sourced from env vars the CALLER (codegen-advise's
# own caller — the loop or an operator) may set; absent is explicit, never
# guessed.
section_4_attempt_stage() {
printf '## 4. Attempt / stage\n\n'
printf 'Attempt: %s\n' "${CODEGEN_ADVISE_ATTEMPT:-unavailable}"
printf 'Ceiling: %s\n' "${CODEGEN_ADVISE_CEILING:-unavailable}"
printf 'Stage: %s\n' "${CODEGEN_ADVISE_STAGE:-unavailable}"
printf 'Final attempt: %s\n' "${CODEGEN_ADVISE_FINAL_ATTEMPT:-unavailable}"
printf 'Reproduction count: %s\n' "${CODEGEN_ADVISE_REPRO_COUNT:-unavailable}"
}
# section_5_source_slices — +/-40 lines around the witness location named in
# gate-result.json, when parseable as file:line.
section_5_source_slices() {
local result_file="$CWD/codegen/gate-pending/gate-result.json"
local witness
witness="$(jq -r '.witness // ""' "$result_file" 2>/dev/null || true)"
if [[ -z "$witness" ]]; then
printf '## 5. Source + test slices\n\n[unavailable: no witness in gate-result.json]\n'
return 0
fi
local loc="${witness%% —*}"
local file="${loc%%:*}"
local line="${loc##*:}"
if [[ ! -f "$CWD/$file" ]] || ! [[ "$line" =~ ^[0-9]+$ ]]; then
printf '## 5. Source + test slices\n\n[unavailable: witness location %q not a readable file:line]\n' "$witness"
return 0
fi
local start=$((line - SOURCE_SLICE_CONTEXT_LINES))
[[ $start -lt 1 ]] && start=1
local end=$((line + SOURCE_SLICE_CONTEXT_LINES))
printf '## 5. Source + test slices\n\n%s (lines %d-%d):\n\n' "$file" "$start" "$end"
awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {print}' "$CWD/$file" 2>/dev/null || true
}
# section_6_failure_output — tail of the gate log (last FAILURE_OUTPUT_LINES
# lines), head+tail when truncated.
section_6_failure_output() {
local log_path="$CWD/codegen/gate-pending/gate-run.log"
if [[ ! -f "$log_path" ]]; then
printf '## 6. Exact failure output\n\n[unavailable: no codegen/gate-pending/gate-run.log at --cwd]\n'
return 0
fi
local total_lines
total_lines="$(wc -l <"$log_path" | tr -d '[:space:]')"
printf '## 6. Exact failure output\n\n'
if [[ "$total_lines" -gt "$FAILURE_OUTPUT_LINES" ]]; then
printf '[truncated: last %d of %d lines]\n\n' "$FAILURE_OUTPUT_LINES" "$total_lines"
tail -n "$FAILURE_OUTPUT_LINES" "$log_path"
else
cat "$log_path"
fi
}
# section_7_diff — current diff + untracked status, bounded per-file.
section_7_diff() {
if ! _in_git_repo; then
printf '## 7. Current diff\n\n[unavailable: --cwd is not a git work tree]\n'
return 0
fi
printf '## 7. Current diff\n\n'
local files
files="$(git -C "$CWD" diff --name-only 2>/dev/null || true)"
if [[ -z "$files" ]]; then
printf '[no unstaged diff]\n'
else
local f total kept
while IFS= read -r f; do
[[ -z "$f" ]] && continue
total="$(git -C "$CWD" diff -- "$f" 2>/dev/null | wc -l | tr -d '[:space:]')"
printf -- '--- %s ---\n' "$f"
if [[ "$total" -gt "$DIFF_LINES_PER_FILE" ]]; then
kept="$(git -C "$CWD" diff -- "$f" 2>/dev/null | head -n "$DIFF_LINES_PER_FILE")"
printf '%s\n[truncated: %d of %d lines]\n\n' "$kept" "$DIFF_LINES_PER_FILE" "$total"
else
git -C "$CWD" diff -- "$f" 2>/dev/null
printf '\n'
fi
done <<<"$files"
fi
local status
status="$(git -C "$CWD" status --porcelain --untracked-files=all 2>/dev/null || true)"
printf 'Status (porcelain):\n%s\n' "$status"
}
# section_8_process_artifacts — terminal-state.json, active dossier stage,
# gate-result verdict (redundant with section 2's verdict line, kept small
# and separate since section 2 is failure-signature-scoped while this is
# process-state-scoped).
section_8_process_artifacts() {
printf '## 8. Process / gate artifacts\n\n'
local terminal_file="$CWD/codegen/gate-pending/terminal-state.json"
if [[ -f "$terminal_file" ]]; then
printf 'terminal-state.json: %s\n' "$(cat "$terminal_file")"
else
printf 'terminal-state.json: [absent]\n'
fi
local recoveries_dir="$CWD/codegen/gate-pending/recoveries"
if [[ -d "$recoveries_dir" ]]; then
local active
active="$(find "$recoveries_dir" -maxdepth 2 -name '*.json' 2>/dev/null | head -n 5)"
if [[ -n "$active" ]]; then
printf 'Active recovery dossiers:\n%s\n' "$active"
else
printf 'Active recovery dossiers: [none]\n'
fi
else
printf 'Active recovery dossiers: [none]\n'
fi
}
# Assembles sections 1-4 (never truncated) and checks their combined size
# against the ceiling BEFORE building the optional sections at all — a
# packet whose required core alone exceeds the ceiling fails loud rather
# than silently truncating the question.
_assemble_core() {
section_1_question
printf '\n'
section_2_failure_signature
printf '\n'
section_3_base_attribution
printf '\n'
section_4_attempt_stage
}
CORE_PACKET="$(_assemble_core)"
CORE_BYTES=${#CORE_PACKET}
if [[ "$CORE_BYTES" -gt "$PACKET_CEILING_BYTES" ]]; then
printf 'codegen-advise: sections 1-4 (question/failure-signature/base-attribution/attempt-stage) alone are %d bytes, exceeding the %d-byte packet ceiling — refusing rather than truncating the question\n' \
"$CORE_BYTES" "$PACKET_CEILING_BYTES" >&2
exit 2
fi
# Optional sections, built in truncation priority order (last-to-shrink
# first, so appending in this order lets a simple tail-trim shrink section 7
# before section 6, etc. — see the truncation loop below).
SEC5="$(section_5_source_slices)"
SEC6="$(section_6_failure_output)"
SEC7="$(section_7_diff)"
SEC8="$(section_8_process_artifacts)"
_assemble_full() {
printf '%s\n\n' "$CORE_PACKET"
printf '%s\n\n' "$SEC7"
printf '%s\n\n' "$SEC6"
printf '%s\n\n' "$SEC5"
printf '%s\n' "$SEC8"
}
FULL_PACKET="$(_assemble_full)"
# Truncation loop: shrink diff -> failure output -> source slices -> gate
# artifacts, in that order, until under ceiling or all optional sections
# are exhausted. Each shrink halves the section's line budget and marks it.
_shrink_marker() {
local label="$1"
printf '[truncated: %s section shrunk to fit the packet ceiling]\n' "$label"
}
_truncate_lines() {
local text="$1"
local keep_lines="$2"
local head_half=$((keep_lines / 2))
local tail_half=$((keep_lines - head_half))
{
printf '%s\n' "$text" | head -n "$head_half"
printf '...\n'
printf '%s\n' "$text" | tail -n "$tail_half"
}
}
_attempt=0
while [[ "${#FULL_PACKET}" -gt "$PACKET_CEILING_BYTES" ]] && [[ "$_attempt" -lt 4 ]]; do
case "$_attempt" in
0)
SEC7="$(_truncate_lines "$SEC7" 40)
$(_shrink_marker "current diff")"
;;
1)
SEC6="$(_truncate_lines "$SEC6" 40)
$(_shrink_marker "exact failure output")"
;;
2)
SEC5="$(_truncate_lines "$SEC5" 20)
$(_shrink_marker "source + test slices")"
;;
3)
SEC8="$(_shrink_marker "process / gate artifacts")"
;;
esac
FULL_PACKET="$(_assemble_full)"
_attempt=$((_attempt + 1))
done
# Final hard clamp: if still over ceiling after exhausting all four optional
# sections, byte-truncate the whole assembled optional tail rather than
# ship an oversized packet across the argv/env hop. Sections 1-4 (the core)
# are NEVER touched here — only bytes beyond them.
if [[ "${#FULL_PACKET}" -gt "$PACKET_CEILING_BYTES" ]]; then
_keep=$((PACKET_CEILING_BYTES - CORE_BYTES - 64))
[[ $_keep -lt 0 ]] && _keep=0
_tail="$(printf '%s' "$FULL_PACKET" | tail -c "$_keep")"
FULL_PACKET="$CORE_PACKET
[truncated: packet exceeded ceiling after per-section shrink; remaining sections clamped]
$_tail"
fi
CONTEXT_TEXT="$FULL_PACKET"
# Fixed same-harness-stronger-model mapping — deliberately not configurable
# (see header + D1). One harness is installed today; the second opinion is
# a stronger tier of it, not a different vendor.
ADVISOR_HARNESS=""
ADVISOR_MODEL=""
case "$HARNESS" in
claude_code)
ADVISOR_HARNESS="claude_code"
ADVISOR_MODEL="opus"
;;
esac
ADVISOR_EFFORT="high"
if [[ ! -x "$CODEGEN_CALL" ]]; then
printf 'codegen-advise: codegen-call not found or not executable: %s\n' "$CODEGEN_CALL" >&2
exit 127
fi
ENVELOPE="$("$CODEGEN_CALL" \
--harness="$ADVISOR_HARNESS" \
--model="$ADVISOR_MODEL" \
--effort="$ADVISOR_EFFORT" \
--system-prompt="@$SYSTEM_PROMPT_FILE" \
--json-schema="@$JSON_SCHEMA_FILE" \
-- "$CONTEXT_TEXT")"
STATUS="$(printf '%s' "$ENVELOPE" | jq -r '.result.status // empty' 2>/dev/null || true)"
if [[ -z "$STATUS" ]]; then
printf 'codegen-advise: codegen-call produced no parseable envelope\n' >&2
exit 1
fi
if [[ "$STATUS" != "success" ]]; then
REASON="$(printf '%s' "$ENVELOPE" | jq -r '.result.reason // "no reason given"' 2>/dev/null || echo "no reason given")"
printf 'codegen-advise: advisor call did not succeed (status=%s): %s\n' "$STATUS" "$REASON" >&2
exit 1
fi
VALUE="$(printf '%s' "$ENVELOPE" | jq -c '.result.value' 2>/dev/null || true)"
if [[ -z "$VALUE" || "$VALUE" == "null" ]]; then
printf 'codegen-advise: advisor call succeeded but returned no structured value\n' >&2
exit 1
fi
printf '%s\n' "$VALUE"