oh-my-customcode 1.1.47 → 1.1.49
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 -6
- package/dist/cli/index.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/.claude/agents/agora-runner.md +114 -0
- package/templates/.claude/hooks/scripts/agent-teams-advisor.sh +6 -1
- package/templates/.claude/hooks/scripts/r007-r008-drift-advisor.sh +41 -3
- package/templates/.claude/hooks/scripts/session-env-check.sh +29 -3
- package/templates/.claude/rules/MAY-optimization.md +6 -0
- package/templates/.claude/rules/MUST-agent-design.md +2 -0
- package/templates/.claude/rules/MUST-completion-verification.md +33 -2
- package/templates/.claude/rules/MUST-enforcement-policy.md +3 -3
- package/templates/.claude/rules/MUST-intent-transparency.md +7 -2
- package/templates/.claude/rules/MUST-orchestrator-coordination.md +50 -5
- package/templates/.claude/rules/MUST-parallel-execution.md +56 -7
- package/templates/.claude/rules/MUST-sync-verification.md +21 -2
- package/templates/.claude/rules/MUST-tool-identification.md +22 -4
- package/templates/.claude/skills/agora/SKILL.md +325 -0
- package/templates/.claude/skills/agora/scripts/agora.sh +761 -0
- package/templates/.claude/skills/agora/scripts/anonymize.sh +492 -0
- package/templates/.claude/skills/agora/scripts/judge.sh +427 -0
- package/templates/.claude/skills/agora/scripts/response-schema.json +26 -0
- package/templates/.claude/skills/agora/scripts/reviewers.sh +312 -0
- package/templates/.claude/skills/agora/scripts/verdict-schema.json +34 -0
- package/templates/.claude/skills/hada-scout/SKILL.md +1 -1
- package/templates/.claude/skills/help/SKILL.md +1 -1
- package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +47 -6
- package/templates/.claude/skills/sauron-watch/SKILL.md +1 -1
- package/templates/.claude/skills/status/SKILL.md +3 -3
- package/templates/.claude/skills/token-efficiency-audit/SKILL.md +1 -1
- package/templates/.github/workflows/wiki-sync.yml +1 -1
- package/templates/CLAUDE.md +3 -3
- package/templates/CLAUDE.md.en +3 -3
- package/templates/CLAUDE.md.ko +3 -3
- package/templates/README.md +5 -5
- package/templates/guides/agent-eval/README.md +1 -1
- package/templates/manifest.json +3 -3
- package/templates/workflows/auto-dev.yaml +47 -6
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# agora.sh — entry point, round loop, session state, stop decision.
|
|
3
|
+
# Spec: docs/superpowers/plans/2026-08-15-agora-anonymous-consensus-design.md
|
|
4
|
+
#
|
|
5
|
+
# -e is intentionally NOT set: run_round's exit code (including reviewers.sh
|
|
6
|
+
# /anonymize.sh/judge.sh propagated failures) must be explicitly captured and
|
|
7
|
+
# returned by its callers (start_session's loop, main()'s --round dispatch),
|
|
8
|
+
# not silently abort the whole interpreter mid-function on the first failing
|
|
9
|
+
# command substitution inside run_round/init_session/generate_report.
|
|
10
|
+
set -uo pipefail
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# decide_stop — PURE function (spec §9).
|
|
14
|
+
# Reads a state.json document on stdin, writes exactly one of
|
|
15
|
+
# CONSENSUS | STALLED | MAX_ROUNDS | USER | CONTINUE on stdout.
|
|
16
|
+
# Touches neither the filesystem nor the network so bun test can call it directly.
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
decide_stop() {
|
|
19
|
+
jq -r '
|
|
20
|
+
def last_round: (.history | length) as $n | if $n == 0 then null else .history[$n - 1] end;
|
|
21
|
+
def quiet($i):
|
|
22
|
+
($i > 0)
|
|
23
|
+
and (.history[$i].new_findings == 0)
|
|
24
|
+
and (.history[$i].max_severity == .history[$i - 1].max_severity);
|
|
25
|
+
|
|
26
|
+
(.history | length) as $n
|
|
27
|
+
| if (.stop == "USER") then "USER"
|
|
28
|
+
elif ($n > 0
|
|
29
|
+
and (last_round.consensus == "UNANIMOUS")
|
|
30
|
+
and (last_round.verdict == "BUILD" or last_round.verdict == "BUILD_WITH_CHANGES"))
|
|
31
|
+
then "CONSENSUS"
|
|
32
|
+
elif ($n >= 3 and quiet($n - 1) and quiet($n - 2)) then "STALLED"
|
|
33
|
+
elif ($n > 0 and (.round >= .max_rounds)) then "MAX_ROUNDS"
|
|
34
|
+
else "CONTINUE"
|
|
35
|
+
end
|
|
36
|
+
'
|
|
37
|
+
}
|
|
38
|
+
# --- end decide_stop ---
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# stop_vocabulary — the set of codes decide_stop can actually emit, READ OUT OF
|
|
42
|
+
# decide_stop's own body rather than restated here.
|
|
43
|
+
#
|
|
44
|
+
# A hardcoded copy would drift the moment a stop condition is added, renamed or
|
|
45
|
+
# removed, and the drift would be SILENT in the worst direction: --set-stop
|
|
46
|
+
# would keep accepting a code decide_stop no longer produces (so report.md
|
|
47
|
+
# names a reason the engine cannot reach), or reject one it does (so a legit
|
|
48
|
+
# stop cannot be recorded at all). Reading the literals from the function body
|
|
49
|
+
# makes decide_stop the single source of truth for the vocabulary, exactly as
|
|
50
|
+
# it is for the decision itself.
|
|
51
|
+
#
|
|
52
|
+
# Matches only the literals in EMITTING position (`then "X"` / `else "X"`), not
|
|
53
|
+
# every uppercase string in the body — the guard expressions also mention
|
|
54
|
+
# verdict/consensus values (UNANIMOUS, BUILD, BUILD_WITH_CHANGES) which are not
|
|
55
|
+
# stop codes.
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
stop_vocabulary() {
|
|
58
|
+
declare -f decide_stop \
|
|
59
|
+
| grep -oE '(then|else)[[:space:]]+"[A-Z_]+"' \
|
|
60
|
+
| grep -oE '"[A-Z_]+"' \
|
|
61
|
+
| tr -d '"' \
|
|
62
|
+
| sort -u
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
66
|
+
REVIEWERS_SH="$SCRIPT_DIR/reviewers.sh"
|
|
67
|
+
ANONYMIZE_SH="$SCRIPT_DIR/anonymize.sh"
|
|
68
|
+
JUDGE_SH="$SCRIPT_DIR/judge.sh"
|
|
69
|
+
|
|
70
|
+
AGORA_OUTPUT_ROOT="${AGORA_OUTPUT_ROOT:-.claude/outputs/sessions}"
|
|
71
|
+
|
|
72
|
+
topic_slug() {
|
|
73
|
+
printf '%s' "$1" | tr -cs '[:alnum:]' '-' | cut -c1-32 | sed 's/-*$//'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# resolve_abs_dir <dir> — normalizes a session directory to an ABSOLUTE path.
|
|
78
|
+
# The Produces contract requires --start's stdout to carry the session dir's
|
|
79
|
+
# absolute path; AGORA_OUTPUT_ROOT defaults to a RELATIVE path
|
|
80
|
+
# (.claude/outputs/sessions), and agora-runner (Task 8, spec §12) may invoke
|
|
81
|
+
# --round/--gate/--report from a different cwd than the one --start ran in —
|
|
82
|
+
# a relative path threaded through would silently resolve against the wrong
|
|
83
|
+
# cwd there. Every entry point that receives or produces a session dir
|
|
84
|
+
# normalizes it here rather than trusting the caller to have already done so.
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
resolve_abs_dir() {
|
|
87
|
+
local dir="$1"
|
|
88
|
+
( cd "$dir" 2>/dev/null && pwd ) || {
|
|
89
|
+
printf 'agora.sh: session dir not found: %s\n' "$dir" >&2
|
|
90
|
+
return 66
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# update_state_json <session_dir> <context> <jq-arg>... — the ONE guarded way
|
|
96
|
+
# this script rewrites state.json.
|
|
97
|
+
#
|
|
98
|
+
# `set -e` is deliberately off in this file (see the header), so the obvious
|
|
99
|
+
# spelling of this operation —
|
|
100
|
+
# jq ... "$dir/state.json" > "$tmp" && mv "$tmp" "$dir/state.json"
|
|
101
|
+
# followed by anything at all — swallows its own failure: a broken jq snaps the
|
|
102
|
+
# && chain and execution simply falls through to the next statement, which in
|
|
103
|
+
# run_round was `return 0`. That reported SUCCESS for a round whose result was
|
|
104
|
+
# never recorded, and every consumer downstream then read a stale document
|
|
105
|
+
# (decide_stop reads .round, so MAX_ROUNDS/STALLED could never fire again; and
|
|
106
|
+
# generate_report reads verdict/round-<stale>.json, so report.md came out
|
|
107
|
+
# empty). state.json is the session's only durable record, so every writer
|
|
108
|
+
# funnels through here rather than repeating — and re-fumbling — the guard.
|
|
109
|
+
#
|
|
110
|
+
# <context> is a short caller tag that names WHICH write failed, since the four
|
|
111
|
+
# callers fail for different reasons and the operator needs to tell them apart.
|
|
112
|
+
#
|
|
113
|
+
# Exit 73 (EX_CANTCREAT, "output file cannot be created") on any failure: the
|
|
114
|
+
# session artifact the caller is about to read could not be written. It is
|
|
115
|
+
# distinct from every code already in use across this skill — 64 usage,
|
|
116
|
+
# 65 vendor id, 66 missing input, 68 judge config, 4 judge CLI, 3 too few
|
|
117
|
+
# reviewers, 2 anonymize mapping, 124 timeout — and all four callers want the
|
|
118
|
+
# orchestrator to react identically: stop, do not consume the artifact, do not
|
|
119
|
+
# re-run the round (the vendors are already billed).
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
update_state_json() {
|
|
122
|
+
local dir="$1" ctx="$2"; shift 2
|
|
123
|
+
|
|
124
|
+
local tmp
|
|
125
|
+
tmp=$(mktemp) || {
|
|
126
|
+
printf '[agora] %s: could not create a staging file for state.json\n' "$ctx" >&2
|
|
127
|
+
return 73
|
|
128
|
+
}
|
|
129
|
+
# anonymize.sh's convention: the staging file must not outlive this function
|
|
130
|
+
# on ANY path. Without it every failed update leaks a 0-byte file into
|
|
131
|
+
# $TMPDIR (measured), because the `mv` that would have consumed it never ran.
|
|
132
|
+
trap "rm -f '$tmp'" RETURN
|
|
133
|
+
|
|
134
|
+
local rc=0
|
|
135
|
+
jq "$@" "$dir/state.json" > "$tmp" \
|
|
136
|
+
&& mv "$tmp" "$dir/state.json" \
|
|
137
|
+
&& chmod 644 "$dir/state.json" \
|
|
138
|
+
|| rc=$?
|
|
139
|
+
|
|
140
|
+
if [ "$rc" -ne 0 ]; then
|
|
141
|
+
printf '[agora] %s: state.json update FAILED (rc=%s) — %s/state.json is unchanged\n' \
|
|
142
|
+
"$ctx" "$rc" "$dir" >&2
|
|
143
|
+
return 73
|
|
144
|
+
fi
|
|
145
|
+
return 0
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# init_session <topic> <max_rounds> <mode> — creates the session artifact
|
|
150
|
+
# tree (spec §4) and the initial state.json, prints the session dir on
|
|
151
|
+
# stdout. Env overrides: AGORA_SESSION_EPOCH (determinism for tests/audit),
|
|
152
|
+
# AGORA_OUTPUT_ROOT (artifact root, default .claude/outputs/sessions per R006).
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
init_session() {
|
|
155
|
+
local topic="$1" max_rounds="$2" mode="$3"
|
|
156
|
+
local epoch="${AGORA_SESSION_EPOCH:-$(date +%s)}"
|
|
157
|
+
local day; day=$(date -u +%Y-%m-%d)
|
|
158
|
+
local hms; hms=$(date -u +%H%M%S)
|
|
159
|
+
local dir="$AGORA_OUTPUT_ROOT/$day/agora-$(topic_slug "$topic")-$hms"
|
|
160
|
+
|
|
161
|
+
mkdir -p "$dir/SEALED/raw" "$dir/SEALED/mapping" "$dir/anon" "$dir/verdict"
|
|
162
|
+
dir=$(resolve_abs_dir "$dir") || return 66
|
|
163
|
+
jq -n --argjson mr "$max_rounds" --arg m "$mode" --arg t "$topic" --arg e "$epoch" \
|
|
164
|
+
'{round: 0, max_rounds: $mr, mode: $m, topic: $t, epoch: $e, attachments: [], history: [], stop: null}' \
|
|
165
|
+
> "$dir/state.json"
|
|
166
|
+
chmod 644 "$dir/state.json"
|
|
167
|
+
printf '%s' "$dir"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# build_reviewer_prompt <session_dir> <round> <out_file> [agenda_json] —
|
|
172
|
+
# spec §10: R1 is a blank-slate independent review (topic + attachments
|
|
173
|
+
# only, no agenda, no prior_rounds, no draft); R2+ injects the previous
|
|
174
|
+
# round's agenda, draft and an anonymized summary of the previous round's
|
|
175
|
+
# opinions.
|
|
176
|
+
#
|
|
177
|
+
# agenda_json is the CALLER's already-combined agenda (judge agenda + any
|
|
178
|
+
# --extra-agenda from the gate's `e` option, spec §10) — this function does
|
|
179
|
+
# NOT re-derive it from verdict/round-N.json itself, so there is exactly one
|
|
180
|
+
# place (run_round) that decides what "this round's agenda" means; a second
|
|
181
|
+
# independent derivation here would drift the moment --extra-agenda exists.
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
build_reviewer_prompt() {
|
|
184
|
+
local dir="$1" round="$2" out="$3" agenda_json="${4:-[]}"
|
|
185
|
+
local topic; topic=$(jq -r '.topic' "$dir/state.json")
|
|
186
|
+
local attachments; attachments=$(jq -r '.attachments[]?' "$dir/state.json")
|
|
187
|
+
|
|
188
|
+
{
|
|
189
|
+
printf '당신은 익명 상호검증 리뷰어입니다.\n\n'
|
|
190
|
+
printf '주제: %s\n\n' "$topic"
|
|
191
|
+
if [ -n "$attachments" ]; then
|
|
192
|
+
printf '첨부 문서:\n'
|
|
193
|
+
printf '%s\n' "$attachments" | while IFS= read -r a; do
|
|
194
|
+
# An unreadable attachment used to emit its `--- <path> ---` header
|
|
195
|
+
# and then nothing, with no diagnostic anywhere. That is not a silent
|
|
196
|
+
# skip, it is a silent LIE: a heading with nothing under it reads to
|
|
197
|
+
# the reviewer as "this document is empty", not "this document could
|
|
198
|
+
# not be read" — and the two lead to opposite conclusions about the
|
|
199
|
+
# design under review. All three vendors are billed for the round
|
|
200
|
+
# either way, so the wrong reading is paid for at full price.
|
|
201
|
+
#
|
|
202
|
+
# Marked in the prompt AND reported on stderr, because the two
|
|
203
|
+
# channels reach different audiences and neither covers the other:
|
|
204
|
+
# the marker reaches the reviewers, who are the ones actually misled,
|
|
205
|
+
# and it persists in SEALED/raw/round-N.prompt.txt as the durable
|
|
206
|
+
# record of what was really sent; stderr reaches the operator, whose
|
|
207
|
+
# typo it usually is, but scrolls past unseen in an unattended run.
|
|
208
|
+
#
|
|
209
|
+
# Deliberately NOT an abort, unlike start_session's exit 73 when the
|
|
210
|
+
# attachment LIST cannot be recorded. That failure is total (no
|
|
211
|
+
# attachment survives the dropped write, so the round would review
|
|
212
|
+
# nothing) and lands before any vendor runs. This one is per-file and
|
|
213
|
+
# partial: the other attachments are intact, and build_reviewer_prompt
|
|
214
|
+
# runs on EVERY round, so a document moved or deleted mid-session
|
|
215
|
+
# would kill a session at round 4 that has already paid for three
|
|
216
|
+
# rounds of reviewers and judges. Once the reviewer is no longer
|
|
217
|
+
# misinformed, whether a partial document set is still worth reviewing
|
|
218
|
+
# is an operator judgement — made at the gate, with the warning in
|
|
219
|
+
# hand — not an invariant worth destroying a paid-up session over.
|
|
220
|
+
if [ -f "$a" ] && [ -r "$a" ]; then
|
|
221
|
+
printf -- '--- %s ---\n' "$a"
|
|
222
|
+
cat "$a"
|
|
223
|
+
else
|
|
224
|
+
printf -- '--- %s (읽을 수 없어 본문을 싣지 못했습니다 — 내용이 비어 있다는 뜻이 아닙니다) ---\n' "$a"
|
|
225
|
+
printf '[agora] attachment could not be read; its body is NOT in the reviewer prompt: %s\n' "$a" >&2
|
|
226
|
+
fi
|
|
227
|
+
done
|
|
228
|
+
printf '\n'
|
|
229
|
+
fi
|
|
230
|
+
# spec §10: round 1 is the only genuinely independent review — no frame injected.
|
|
231
|
+
if [ "$round" -gt 1 ]; then
|
|
232
|
+
local prev=$(( round - 1 ))
|
|
233
|
+
printf '직전 라운드 의제:\n'
|
|
234
|
+
jq -r '.[]? | " - " + .' <<< "$agenda_json"
|
|
235
|
+
printf '\n직전 라운드 통합 초안:\n%s\n\n' "$(jq -r '.draft // ""' "$dir/verdict/round-$prev.json")"
|
|
236
|
+
printf '직전 라운드 익명 의견 요약:\n%s\n\n' "$(jq -c '.reviewers | map({label, overall})' "$dir/anon/round-$prev.json")"
|
|
237
|
+
fi
|
|
238
|
+
printf '아래 JSON 스키마를 정확히 따르는 JSON 객체 하나만 출력하십시오.\n'
|
|
239
|
+
printf '"counter" 는 자기 주장에 대한 반론이며 빈 문자열이 될 수 없습니다.\n\n'
|
|
240
|
+
cat "$SCRIPT_DIR/response-schema.json"
|
|
241
|
+
} > "$out"
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# run_round --session-dir <dir> --round <N> [--extra-agenda <json-array>] —
|
|
246
|
+
# the agora-runner delegation unit (spec §12: "라운드 1개 = 위임 1건"). Drives
|
|
247
|
+
# reviewers.sh → anonymize.sh → judge.sh for exactly one round, then updates
|
|
248
|
+
# state.json.
|
|
249
|
+
#
|
|
250
|
+
# judge.sh's exit 68 is a CONFIGURATION error (its own verdict-schema.json is
|
|
251
|
+
# unreadable) — NOT a vendor/CLI failure like exit 4 (every rotation model
|
|
252
|
+
# failed). It is consumed here as an immediate hard stop with its own
|
|
253
|
+
# diagnostic line and is never retried: retrying would fail identically all
|
|
254
|
+
# three times (the schema file does not become readable by trying again) and
|
|
255
|
+
# would only burn wall-clock. Every other non-zero code from
|
|
256
|
+
# reviewers.sh/anonymize.sh/judge.sh is propagated as-is.
|
|
257
|
+
#
|
|
258
|
+
# No path here is ever exported — reviewers.sh/anonymize.sh/judge.sh receive
|
|
259
|
+
# every path they need as an explicit CLI flag, never via inherited
|
|
260
|
+
# environment, so judge.sh's "no route back to who said what" claim (spec §4)
|
|
261
|
+
# is not undermined by this loop leaking a sealed-path env var to it.
|
|
262
|
+
#
|
|
263
|
+
# --extra-agenda is the gate's `e` option (spec §10): user agenda items are
|
|
264
|
+
# APPENDED after the judge's own agenda, never overwriting it — the judge's
|
|
265
|
+
# REQ-6 agenda-setting authority must survive a user append. Concatenation
|
|
266
|
+
# happens ONCE here and the combined array is threaded into BOTH the
|
|
267
|
+
# reviewer prompt (build_reviewer_prompt) and anonymize.sh's --agenda (which
|
|
268
|
+
# becomes what the judge itself reads next round) — a single source of truth
|
|
269
|
+
# for "this round's agenda", not two independent re-derivations that could
|
|
270
|
+
# drift from each other.
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
run_round() {
|
|
273
|
+
local dir='' round='' extra_agenda='[]'
|
|
274
|
+
while [ "$#" -gt 0 ]; do
|
|
275
|
+
case "$1" in
|
|
276
|
+
--session-dir) dir="$2"; shift 2 ;;
|
|
277
|
+
--round) round="$2"; shift 2 ;;
|
|
278
|
+
--extra-agenda) extra_agenda="$2"; shift 2 ;;
|
|
279
|
+
*) printf 'agora.sh: unknown round option %s\n' "$1" >&2; return 64 ;;
|
|
280
|
+
esac
|
|
281
|
+
done
|
|
282
|
+
[ -n "$dir" ] && [ -n "$round" ] || { printf 'agora.sh: --session-dir and --round required\n' >&2; return 64; }
|
|
283
|
+
|
|
284
|
+
# --extra-agenda must be a JSON array BEFORE any reviewer/vendor is
|
|
285
|
+
# invoked. Left unvalidated, a malformed value (the gate's `e` option is
|
|
286
|
+
# free text assembled by the caller into JSON — one stray quote breaks it)
|
|
287
|
+
# makes the `jq -c --argjson extra ... '. + $extra'` combine below fail,
|
|
288
|
+
# collapsing agenda to an empty string; build_reviewer_prompt then silently
|
|
289
|
+
# renders a blank agenda section, all three vendors still get called (and
|
|
290
|
+
# billed), and only anonymize.sh's later `--agenda ''` failure surfaces the
|
|
291
|
+
# problem — after the cost was already paid. Fail fast here instead.
|
|
292
|
+
if ! jq -e 'type == "array"' >/dev/null 2>&1 <<< "$extra_agenda"; then
|
|
293
|
+
printf 'agora.sh: --extra-agenda must be a JSON array, got: %s\n' "$extra_agenda" >&2
|
|
294
|
+
return 64
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
dir=$(resolve_abs_dir "$dir") || return 66
|
|
298
|
+
|
|
299
|
+
local round_start; round_start=$(date +%s)
|
|
300
|
+
|
|
301
|
+
local epoch; epoch=$(jq -r '.epoch' "$dir/state.json")
|
|
302
|
+
local topic; topic=$(jq -r '.topic' "$dir/state.json")
|
|
303
|
+
local attachments; attachments=$(jq -c '.attachments' "$dir/state.json")
|
|
304
|
+
local agenda='[]'
|
|
305
|
+
if [ "$round" -gt 1 ]; then
|
|
306
|
+
agenda=$(jq -c '.agenda // []' "$dir/verdict/round-$(( round - 1 )).json")
|
|
307
|
+
fi
|
|
308
|
+
agenda=$(jq -c --argjson extra "$extra_agenda" '. + $extra' <<< "$agenda")
|
|
309
|
+
|
|
310
|
+
local prompt_file="$dir/SEALED/raw/round-$round.prompt.txt"
|
|
311
|
+
mkdir -p "$dir/SEALED/raw/round-$round"
|
|
312
|
+
build_reviewer_prompt "$dir" "$round" "$prompt_file" "$agenda"
|
|
313
|
+
|
|
314
|
+
bash "$REVIEWERS_SH" --run --session-dir "$dir" --round "$round" --prompt-file "$prompt_file" || return $?
|
|
315
|
+
|
|
316
|
+
bash "$ANONYMIZE_SH" --build \
|
|
317
|
+
--session-dir "$dir" --round "$round" --seed "agora-$epoch-r$round" \
|
|
318
|
+
--topic "$topic" --attachments "$attachments" --agenda "$agenda" || return $?
|
|
319
|
+
|
|
320
|
+
bash "$JUDGE_SH" --run \
|
|
321
|
+
--anon-file "$dir/anon/round-$round.json" \
|
|
322
|
+
--out-file "$dir/verdict/round-$round.json" \
|
|
323
|
+
--round "$round"
|
|
324
|
+
local judge_rc=$?
|
|
325
|
+
if [ "$judge_rc" -eq 68 ]; then
|
|
326
|
+
printf '[agora] judge.sh reported a configuration error (verdict schema unreadable) for round %s — aborting, not retrying\n' \
|
|
327
|
+
"$round" >&2
|
|
328
|
+
return 68
|
|
329
|
+
elif [ "$judge_rc" -ne 0 ]; then
|
|
330
|
+
return "$judge_rc"
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
local round_end; round_end=$(date +%s)
|
|
334
|
+
local elapsed=$(( round_end - round_start ))
|
|
335
|
+
|
|
336
|
+
local v="$dir/verdict/round-$round.json"
|
|
337
|
+
local max_sev
|
|
338
|
+
max_sev=$(jq -r '
|
|
339
|
+
([.unresolved[]?.severity] | map({CRITICAL:4, HIGH:3, MEDIUM:2, LOW:1}[.] // 0) | max // 0) as $m
|
|
340
|
+
| if $m == 4 then "CRITICAL" elif $m == 3 then "HIGH"
|
|
341
|
+
elif $m == 2 then "MEDIUM" elif $m == 1 then "LOW" else "NONE" end' "$v")
|
|
342
|
+
|
|
343
|
+
# By the time we get here the three vendors AND the judge have already run
|
|
344
|
+
# and been billed, so a silently-dropped write is the most expensive failure
|
|
345
|
+
# in this script — see update_state_json for why the bare && chain that used
|
|
346
|
+
# to live here reported success for a round it never recorded.
|
|
347
|
+
#
|
|
348
|
+
# new_findings is the realistic trigger: it reaches --argjson as raw model
|
|
349
|
+
# output, so a judge that answers "three" instead of 3 breaks the write.
|
|
350
|
+
# judge.sh's validate_verdict now type-checks this field against
|
|
351
|
+
# verdict-schema.json before any verdict file is written, so a wrong-typed
|
|
352
|
+
# value should not reach here at all — this guard is the layer BEHIND that
|
|
353
|
+
# one, and it stays because it covers the two ways that check stops
|
|
354
|
+
# applying: verdict-schema.json loosening new_findings' declared type (the
|
|
355
|
+
# check is schema-driven, so it would silently stop testing it without any
|
|
356
|
+
# code change), and a verdict file arriving by some route other than
|
|
357
|
+
# judge.sh --run. It is extracted first purely so the failure diagnostic
|
|
358
|
+
# can quote the offending value.
|
|
359
|
+
local nf; nf=$(jq -r '.new_findings' "$v")
|
|
360
|
+
|
|
361
|
+
update_state_json "$dir" "round $round" \
|
|
362
|
+
--argjson r "$round" \
|
|
363
|
+
--arg verdict "$(jq -r '.verdict' "$v")" \
|
|
364
|
+
--arg consensus "$(jq -r '.consensus' "$v")" \
|
|
365
|
+
--argjson nf "$nf" \
|
|
366
|
+
--arg sev "$max_sev" \
|
|
367
|
+
--argjson elapsed "$elapsed" \
|
|
368
|
+
'.round = $r
|
|
369
|
+
| .history += [{round: $r, verdict: $verdict, consensus: $consensus,
|
|
370
|
+
new_findings: $nf, max_severity: $sev, tokens: 0, elapsed_secs: $elapsed}]' \
|
|
371
|
+
|| {
|
|
372
|
+
printf '[agora] round %s ran but was NOT recorded (reviewers and judge are already billed). Most likely cause: %s carries a non-numeric "new_findings" (observed: %s). Re-running this round would bill the vendors again.\n' \
|
|
373
|
+
"$round" "$v" "$nf" >&2
|
|
374
|
+
return 73
|
|
375
|
+
}
|
|
376
|
+
return 0
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
# ---------------------------------------------------------------------------
|
|
380
|
+
# gate_display <session_dir> <round> — spec §10 gate block. Labels only,
|
|
381
|
+
# vendors are never named here (they surface for the first time in
|
|
382
|
+
# report.md — spec §4's single anonymity-lifting point). Also surfaces the
|
|
383
|
+
# round's wall-clock duration: judge rotation alone is a worst case of
|
|
384
|
+
# ~15 minutes (3 slots x AGORA_TIMEOUT_SECS default 300s), on top of the
|
|
385
|
+
# reviewer fan-out, so the user should not be left blind to how long a round
|
|
386
|
+
# actually took even though this script does not enforce a round-level
|
|
387
|
+
# timeout policy of its own.
|
|
388
|
+
# ---------------------------------------------------------------------------
|
|
389
|
+
gate_display() {
|
|
390
|
+
local dir="$1" round="$2"
|
|
391
|
+
dir=$(resolve_abs_dir "$dir") || return 66
|
|
392
|
+
local v="$dir/verdict/round-$round.json"
|
|
393
|
+
local a="$dir/anon/round-$round.json"
|
|
394
|
+
local max_rounds; max_rounds=$(jq -r '.max_rounds' "$dir/state.json")
|
|
395
|
+
local elapsed
|
|
396
|
+
elapsed=$(jq -r --argjson r "$round" '.history[] | select(.round == $r) | (.elapsed_secs // 0)' "$dir/state.json")
|
|
397
|
+
|
|
398
|
+
printf -- '─── Agora Round %s/%s ───────────────────────────────\n' "$round" "$max_rounds"
|
|
399
|
+
printf '심판: (모델 로테이션 #%s)\n' "$(( (round - 1) % 3 + 1 ))"
|
|
400
|
+
printf 'Consensus: %-16s Verdict: %s\n' "$(jq -r '.consensus' "$v")" "$(jq -r '.verdict' "$v")"
|
|
401
|
+
printf '리뷰어: %s\n' "$(jq -r '[.reviewers[] | .label + " " + .overall] | join(" · ")' "$a")"
|
|
402
|
+
printf '신규 지적: %s건 최고 심각도: %s\n' \
|
|
403
|
+
"$(jq -r '.new_findings' "$v")" \
|
|
404
|
+
"$(jq -r --argjson r "$round" '.history[] | select(.round == $r) | .max_severity' "$dir/state.json")"
|
|
405
|
+
printf '라운드 소요: %s초 (참고: 심판 로테이션 3슬롯 순차 재시도 시 최악 약 15분)\n' "$elapsed"
|
|
406
|
+
printf '\n해소됨(%s)\n' "$(jq -r '.resolved | length' "$v")"
|
|
407
|
+
jq -r '.resolved[]? | " " + .id + " " + .resolution' "$v"
|
|
408
|
+
printf '\n미해소(%s)\n' "$(jq -r '.unresolved | length' "$v")"
|
|
409
|
+
jq -r '.unresolved[]? | " " + .id + " [" + .severity + "] " + .positions' "$v"
|
|
410
|
+
printf '\n다음 라운드 의제\n'
|
|
411
|
+
jq -r '.agenda[]? | " - " + .' "$v"
|
|
412
|
+
printf '\n[c] 계속 [s] 중단하고 보고서 [e] 의제 추가 후 계속\n'
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
# ---------------------------------------------------------------------------
|
|
416
|
+
# record_stop <session_dir> <code> — writes .stop, the field report.md prints
|
|
417
|
+
# as "종료 사유".
|
|
418
|
+
#
|
|
419
|
+
# Without this, .stop had no reachable writer on the gated path at all, and
|
|
420
|
+
# gated is the PRIMARY path (spec §12: one round = one agora-runner
|
|
421
|
+
# delegation; --auto is the discouraged, token-expensive alternative). The two
|
|
422
|
+
# existing writers were both inside start_session, which in gated mode returns
|
|
423
|
+
# after round 1 — so every session the orchestrator drove to a normal
|
|
424
|
+
# CONSENSUS / STALLED / MAX_ROUNDS ending, and every user `s` at the gate,
|
|
425
|
+
# landed on generate_report's `.stop // "UNKNOWN"` fallback and reported
|
|
426
|
+
# 종료 사유: UNKNOWN. decide_stop could NAME the ending; nothing could record it.
|
|
427
|
+
#
|
|
428
|
+
# The accepted vocabulary comes from stop_vocabulary(), i.e. out of
|
|
429
|
+
# decide_stop's own body — never a second hardcoded list here.
|
|
430
|
+
#
|
|
431
|
+
# CONTINUE is rejected on purpose even though decide_stop emits it: it is the
|
|
432
|
+
# "no stop happened" sentinel, so persisting it into .stop would assert an
|
|
433
|
+
# ending that did not occur, and report.md would print 종료 사유: CONTINUE for a
|
|
434
|
+
# session that is still open. A caller that sees CONTINUE should run another
|
|
435
|
+
# round, not record it.
|
|
436
|
+
#
|
|
437
|
+
# Deliberately session-scoped (no --round): .stop describes HOW the session
|
|
438
|
+
# ended, .round already records WHERE. Writing is idempotent, so a re-issued
|
|
439
|
+
# code is harmless.
|
|
440
|
+
# ---------------------------------------------------------------------------
|
|
441
|
+
record_stop() {
|
|
442
|
+
local dir="$1" code="$2"
|
|
443
|
+
local vocab; vocab=$(stop_vocabulary)
|
|
444
|
+
local settable; settable=$(grep -vx 'CONTINUE' <<< "$vocab")
|
|
445
|
+
|
|
446
|
+
if [ "$code" = 'CONTINUE' ]; then
|
|
447
|
+
printf 'agora.sh: CONTINUE means the session has NOT stopped — it cannot be recorded as .stop. Run another round instead.\n' >&2
|
|
448
|
+
return 64
|
|
449
|
+
fi
|
|
450
|
+
if ! grep -qx -- "$code" <<< "$vocab"; then
|
|
451
|
+
printf 'agora.sh: unknown stop code %s — accepted: %s\n' \
|
|
452
|
+
"$code" "$(printf '%s' "$settable" | tr '\n' ' ')" >&2
|
|
453
|
+
return 64
|
|
454
|
+
fi
|
|
455
|
+
|
|
456
|
+
dir=$(resolve_abs_dir "$dir") || return 66
|
|
457
|
+
update_state_json "$dir" "stop=$code" --arg s "$code" '.stop = $s' \
|
|
458
|
+
|| {
|
|
459
|
+
printf '[agora] the session ending (%s) could not be recorded — report.md would say UNKNOWN, so do not generate it yet\n' \
|
|
460
|
+
"$code" >&2
|
|
461
|
+
return 73
|
|
462
|
+
}
|
|
463
|
+
return 0
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
# ---------------------------------------------------------------------------
|
|
467
|
+
# generate_report <session_dir> — spec §4: the ONE place anonymity is
|
|
468
|
+
# lifted. Reads SEALED/mapping/round-N.json (never read by any other
|
|
469
|
+
# function in this script apart from report generation). Staged to a temp
|
|
470
|
+
# file and moved into place only once fully assembled, so a mid-write
|
|
471
|
+
# failure never leaves a partial report.md at the final path.
|
|
472
|
+
# ---------------------------------------------------------------------------
|
|
473
|
+
generate_report() {
|
|
474
|
+
local dir="$1"
|
|
475
|
+
dir=$(resolve_abs_dir "$dir") || return 66
|
|
476
|
+
local out="$dir/report.md"
|
|
477
|
+
local stop; stop=$(jq -r '.stop // "UNKNOWN"' "$dir/state.json")
|
|
478
|
+
local last; last=$(jq -r '.round' "$dir/state.json")
|
|
479
|
+
|
|
480
|
+
local tmp
|
|
481
|
+
tmp=$(mktemp) || {
|
|
482
|
+
printf '[agora] report: could not create a staging file for report.md\n' >&2
|
|
483
|
+
return 73
|
|
484
|
+
}
|
|
485
|
+
trap "rm -f '$tmp'" RETURN
|
|
486
|
+
|
|
487
|
+
{
|
|
488
|
+
printf '# Agora 합의 보고서\n\n'
|
|
489
|
+
printf -- '- 주제: %s\n' "$(jq -r '.topic' "$dir/state.json")"
|
|
490
|
+
printf -- '- 종료 사유: %s\n' "$stop"
|
|
491
|
+
printf -- '- 총 라운드: %s\n\n' "$last"
|
|
492
|
+
if [ "$stop" = "MAX_ROUNDS" ]; then
|
|
493
|
+
printf '> **합의 없음 · 분기 결정 필요** — 상한 도달로 종료되었습니다.\n\n'
|
|
494
|
+
elif [ "$stop" = "STALLED" ]; then
|
|
495
|
+
printf '> **정체로 조기 종료** — 잔존 쟁점을 그대로 보고하며 결론을 강제하지 않습니다.\n\n'
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
printf '## 최종 통합 초안\n\n%s\n\n' "$(jq -r '.draft // ""' "$dir/verdict/round-$last.json")"
|
|
499
|
+
|
|
500
|
+
printf '## 라운드별 참여자 (익명 해제)\n\n'
|
|
501
|
+
local n
|
|
502
|
+
for (( n = 1; n <= last; n++ )); do
|
|
503
|
+
printf '### 라운드 %s\n\n' "$n"
|
|
504
|
+
jq -r '.map | to_entries[] | "- " + .key + " → " + .value' "$dir/SEALED/mapping/round-$n.json"
|
|
505
|
+
printf '\n'
|
|
506
|
+
done
|
|
507
|
+
|
|
508
|
+
printf '## 잔존 쟁점\n\n'
|
|
509
|
+
jq -r '.unresolved[]? | "- " + .id + " [" + .severity + "] " + .positions' "$dir/verdict/round-$last.json"
|
|
510
|
+
printf '\n'
|
|
511
|
+
} > "$tmp"
|
|
512
|
+
|
|
513
|
+
# The report is the session's deliverable and the ONE artifact that lifts
|
|
514
|
+
# anonymity — a dropped move here left the caller believing a report existed
|
|
515
|
+
# at "$out" when nothing had been written, which is exactly the class of
|
|
516
|
+
# silent failure that made the round-state bug expensive. Exit 73 for the
|
|
517
|
+
# same reason as update_state_json: the output file could not be created.
|
|
518
|
+
local rc=0
|
|
519
|
+
mv "$tmp" "$out" && chmod 644 "$out" || rc=$?
|
|
520
|
+
if [ "$rc" -ne 0 ]; then
|
|
521
|
+
printf '[agora] report: could not write %s (rc=%s) — no report was produced; the session dir may be read-only\n' \
|
|
522
|
+
"$out" "$rc" >&2
|
|
523
|
+
return 73
|
|
524
|
+
fi
|
|
525
|
+
return 0
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
# ---------------------------------------------------------------------------
|
|
529
|
+
# start_session <topic> [--attach <path>]... [--max-rounds <N>] [--auto]
|
|
530
|
+
# Creates the session, then drives the round loop:
|
|
531
|
+
# - mode auto: runs rounds until decide_stop() says stop, then reports.
|
|
532
|
+
# - mode gated: runs exactly ONE round, prints the gate, and returns — the
|
|
533
|
+
# orchestrator drives subsequent rounds via `--round <N> --session-dir`.
|
|
534
|
+
# ---------------------------------------------------------------------------
|
|
535
|
+
start_session() {
|
|
536
|
+
local topic='' max_rounds=5 mode='gated'
|
|
537
|
+
local attachments=()
|
|
538
|
+
topic="$1"; shift
|
|
539
|
+
while [ "$#" -gt 0 ]; do
|
|
540
|
+
case "$1" in
|
|
541
|
+
--attach) attachments+=("$2"); shift 2 ;;
|
|
542
|
+
--max-rounds) max_rounds="$2"; shift 2 ;;
|
|
543
|
+
--auto) mode='auto'; shift ;;
|
|
544
|
+
*) printf 'agora.sh: unknown start option %s\n' "$1" >&2; return 64 ;;
|
|
545
|
+
esac
|
|
546
|
+
done
|
|
547
|
+
|
|
548
|
+
# Declared first, assigned second, on purpose: `local dir=$(init_session ...)`
|
|
549
|
+
# would mask init_session's exit status behind `local`'s own (always 0), so a
|
|
550
|
+
# session dir that could not be created would sail on as an empty string and
|
|
551
|
+
# surface later as a baffling error against "/state.json".
|
|
552
|
+
local dir
|
|
553
|
+
dir=$(init_session "$topic" "$max_rounds" "$mode") || return $?
|
|
554
|
+
if [ "${#attachments[@]}" -gt 0 ]; then
|
|
555
|
+
local att_json; att_json=$(printf '%s\n' "${attachments[@]}" | jq -R . | jq -sc .)
|
|
556
|
+
# Hard-fail rather than proceed: build_reviewer_prompt reads .attachments
|
|
557
|
+
# from state.json, so a dropped write here does not merely lose metadata —
|
|
558
|
+
# it silently sends all three vendors a review WITHOUT the documents the
|
|
559
|
+
# user attached, and bills the round for reviewing the wrong material.
|
|
560
|
+
# Better to abort before any vendor is invoked.
|
|
561
|
+
update_state_json "$dir" 'attachments' --argjson a "$att_json" '.attachments = $a' \
|
|
562
|
+
|| {
|
|
563
|
+
printf '[agora] attachments were not recorded — aborting before any round runs, since reviewers would otherwise be billed for a review with no attachments\n' >&2
|
|
564
|
+
return 73
|
|
565
|
+
}
|
|
566
|
+
fi
|
|
567
|
+
|
|
568
|
+
local round=1 stop='CONTINUE' rc
|
|
569
|
+
while [ "$round" -le "$max_rounds" ]; do
|
|
570
|
+
rc=0
|
|
571
|
+
run_round --session-dir "$dir" --round "$round" || rc=$?
|
|
572
|
+
[ "$rc" -eq 0 ] || return "$rc"
|
|
573
|
+
|
|
574
|
+
stop=$(decide_stop < "$dir/state.json")
|
|
575
|
+
if [ "$stop" != "CONTINUE" ]; then
|
|
576
|
+
# Same guard as every other .stop writer (record_stop): if this write is
|
|
577
|
+
# dropped, the loop still breaks and generate_report still runs — and the
|
|
578
|
+
# report would name the ending UNKNOWN while claiming to be complete.
|
|
579
|
+
update_state_json "$dir" "stop=$stop" --arg s "$stop" '.stop = $s' \
|
|
580
|
+
|| {
|
|
581
|
+
printf '[agora] the session ended as %s but that could not be recorded — refusing to generate a report that would name the reason UNKNOWN\n' \
|
|
582
|
+
"$stop" >&2
|
|
583
|
+
return 73
|
|
584
|
+
}
|
|
585
|
+
break
|
|
586
|
+
fi
|
|
587
|
+
|
|
588
|
+
# In gated mode the orchestrator drives the gate; the script yields after one round.
|
|
589
|
+
#
|
|
590
|
+
# The gate goes to STDERR, not stdout: the Produces contract (see
|
|
591
|
+
# resolve_abs_dir) reserves --start's stdout for the session dir alone, and
|
|
592
|
+
# the documented `dir=$(agora.sh --start ...)` idiom captures stdout
|
|
593
|
+
# wholesale — with the gate on stdout, $dir came back as 17 lines of gate
|
|
594
|
+
# block with the path glued on the end (measured), i.e. not a usable path.
|
|
595
|
+
# stderr also keeps the block visible when --start is delegated to
|
|
596
|
+
# agora-runner, whose structured return value has no field to carry it.
|
|
597
|
+
# The standalone --gate subcommand keeps its block on STDOUT by contrast:
|
|
598
|
+
# rendering the gate is that subcommand's entire product, not a side
|
|
599
|
+
# channel next to a machine-read value.
|
|
600
|
+
if [ "$mode" = 'gated' ]; then
|
|
601
|
+
gate_display "$dir" "$round" >&2
|
|
602
|
+
printf '%s\n' "$dir"
|
|
603
|
+
return 0
|
|
604
|
+
fi
|
|
605
|
+
round=$(( round + 1 ))
|
|
606
|
+
done
|
|
607
|
+
|
|
608
|
+
# Propagate, do not swallow: printing the session dir and returning 0 after a
|
|
609
|
+
# failed report is the same "success reported for work that did not happen"
|
|
610
|
+
# shape as the round-state bug — the caller would go read a report.md that
|
|
611
|
+
# was never written.
|
|
612
|
+
generate_report "$dir" || return $?
|
|
613
|
+
printf '%s\n' "$dir"
|
|
614
|
+
return 0
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
main() {
|
|
618
|
+
case "${1:---help}" in
|
|
619
|
+
--decide-stop)
|
|
620
|
+
decide_stop
|
|
621
|
+
;;
|
|
622
|
+
--start)
|
|
623
|
+
shift
|
|
624
|
+
start_session "$@"
|
|
625
|
+
;;
|
|
626
|
+
--round)
|
|
627
|
+
# CLI syntax is documented (Produces contract) as
|
|
628
|
+
# `agora.sh --round <N> --session-dir <dir>`: the round number is the
|
|
629
|
+
# very next token after --round, not a `--round <N>` flag pair at this
|
|
630
|
+
# level. Reconstruct it into the --round/--session-dir flag pair
|
|
631
|
+
# run_round's own parser expects, in either order the caller supplied
|
|
632
|
+
# the remaining flags.
|
|
633
|
+
shift
|
|
634
|
+
local round_n="${1:-}"; shift || true
|
|
635
|
+
run_round --round "$round_n" "$@"
|
|
636
|
+
;;
|
|
637
|
+
--gate)
|
|
638
|
+
shift
|
|
639
|
+
local gd='' gr=''
|
|
640
|
+
while [ "$#" -gt 0 ]; do
|
|
641
|
+
case "$1" in
|
|
642
|
+
--session-dir) gd="$2"; shift 2 ;;
|
|
643
|
+
--round) gr="$2"; shift 2 ;;
|
|
644
|
+
*) printf 'agora.sh: unknown gate option %s\n' "$1" >&2; return 64 ;;
|
|
645
|
+
esac
|
|
646
|
+
done
|
|
647
|
+
[ -n "$gd" ] && [ -n "$gr" ] || {
|
|
648
|
+
printf 'agora.sh: --gate requires --session-dir and --round\n' >&2
|
|
649
|
+
return 64
|
|
650
|
+
}
|
|
651
|
+
gate_display "$gd" "$gr"
|
|
652
|
+
;;
|
|
653
|
+
--set-stop)
|
|
654
|
+
# `--set-stop <CODE> --session-dir <dir>` mirrors --round's shape: the
|
|
655
|
+
# value is the token right after the subcommand, not a flag pair.
|
|
656
|
+
shift
|
|
657
|
+
local sc="${1:-}"
|
|
658
|
+
case "$sc" in
|
|
659
|
+
''|--*)
|
|
660
|
+
printf 'agora.sh: --set-stop requires a stop code, e.g. --set-stop CONSENSUS --session-dir <dir>\n' >&2
|
|
661
|
+
return 64
|
|
662
|
+
;;
|
|
663
|
+
esac
|
|
664
|
+
shift
|
|
665
|
+
local sd=''
|
|
666
|
+
while [ "$#" -gt 0 ]; do
|
|
667
|
+
case "$1" in
|
|
668
|
+
--session-dir) sd="$2"; shift 2 ;;
|
|
669
|
+
*) printf 'agora.sh: unknown set-stop option %s\n' "$1" >&2; return 64 ;;
|
|
670
|
+
esac
|
|
671
|
+
done
|
|
672
|
+
[ -n "$sd" ] || {
|
|
673
|
+
printf 'agora.sh: --set-stop requires --session-dir\n' >&2
|
|
674
|
+
return 64
|
|
675
|
+
}
|
|
676
|
+
record_stop "$sd" "$sc"
|
|
677
|
+
;;
|
|
678
|
+
--report)
|
|
679
|
+
shift
|
|
680
|
+
local rd=''
|
|
681
|
+
while [ "$#" -gt 0 ]; do
|
|
682
|
+
case "$1" in
|
|
683
|
+
--session-dir) rd="$2"; shift 2 ;;
|
|
684
|
+
*) printf 'agora.sh: unknown report option %s\n' "$1" >&2; return 64 ;;
|
|
685
|
+
esac
|
|
686
|
+
done
|
|
687
|
+
[ -n "$rd" ] || { printf 'agora.sh: --report requires --session-dir\n' >&2; return 64; }
|
|
688
|
+
generate_report "$rd"
|
|
689
|
+
;;
|
|
690
|
+
--help | -h)
|
|
691
|
+
cat <<'USAGE'
|
|
692
|
+
Usage:
|
|
693
|
+
agora.sh --decide-stop Read state.json on stdin, print the stop code.
|
|
694
|
+
agora.sh --start "<topic>" [--attach <path>]... [--max-rounds <N>] [--auto]
|
|
695
|
+
Create a session and drive the round loop.
|
|
696
|
+
agora.sh --round <N> --session-dir <dir> [--extra-agenda <json-array>]
|
|
697
|
+
Run exactly one round (agora-runner delegation unit).
|
|
698
|
+
--extra-agenda is the gate's `e` option (spec §10):
|
|
699
|
+
it is APPENDED after the judge's own agenda, never
|
|
700
|
+
overwriting it.
|
|
701
|
+
agora.sh --gate --session-dir <dir> --round <N> Render the label-only gate block for a round.
|
|
702
|
+
agora.sh --set-stop <CODE> --session-dir <dir> Record HOW the session ended, into .stop.
|
|
703
|
+
<CODE> is one of decide_stop's own stop codes —
|
|
704
|
+
CONSENSUS, STALLED, MAX_ROUNDS, USER (the gate's
|
|
705
|
+
`s` choice). CONTINUE is rejected: it means the
|
|
706
|
+
session has NOT stopped. The accepted set is read
|
|
707
|
+
from decide_stop at runtime, and an invalid code
|
|
708
|
+
prints it.
|
|
709
|
+
agora.sh --report --session-dir <dir> Regenerate report.md from the sealed mapping.
|
|
710
|
+
|
|
711
|
+
Which entry points WRITE (delegate these to agora-runner, spec §12):
|
|
712
|
+
--start, --round, --set-stop, --report all write into the session dir.
|
|
713
|
+
--decide-stop and --gate are read-only. --set-stop in particular mutates
|
|
714
|
+
state.json, so the orchestrator does not run it directly.
|
|
715
|
+
|
|
716
|
+
Channel contract:
|
|
717
|
+
--start prints ONLY the session dir on stdout (`dir=$(agora.sh --start ...)`
|
|
718
|
+
is the intended idiom); its gate block goes to stderr. The standalone --gate
|
|
719
|
+
subcommand prints its block on stdout, since that block is its whole product.
|
|
720
|
+
|
|
721
|
+
Gated-mode contract (spec §12, "라운드 1개 = 위임 1건"):
|
|
722
|
+
--start WITHOUT --auto runs exactly ROUND 1 and returns — it does not loop
|
|
723
|
+
further within that one invocation. What it does next depends on round 1's
|
|
724
|
+
OWN stop decision (measured behavior, not caller-driven in this branch):
|
|
725
|
+
- If round 1 already satisfies a stop condition (CONSENSUS / STALLED /
|
|
726
|
+
MAX_ROUNDS / USER), --start writes .stop and generates report.md
|
|
727
|
+
ITSELF before returning — the gate is NOT rendered in this case.
|
|
728
|
+
- Otherwise --start renders the round-1 gate and returns WITHOUT writing
|
|
729
|
+
.stop or generating report.md.
|
|
730
|
+
Every round after round 1 is driven by the CALLER (agora-runner / the
|
|
731
|
+
orchestrator), one round per delegation, through the standalone --round
|
|
732
|
+
entry point:
|
|
733
|
+
1. bash agora.sh --round <N> --session-dir <dir> Advance one round.
|
|
734
|
+
2. bash agora.sh --decide-stop < state.json Check the stop code yourself.
|
|
735
|
+
3. bash agora.sh --gate --session-dir <dir> --round <N>
|
|
736
|
+
Show the round's gate (skip if you already stopped).
|
|
737
|
+
4. bash agora.sh --set-stop <CODE> --session-dir <dir>
|
|
738
|
+
REQUIRED whenever step 2 returned anything other
|
|
739
|
+
than CONTINUE: record that exact code before
|
|
740
|
+
reporting. Also used with USER when the user picks
|
|
741
|
+
`s` at the gate. .stop has no other writer on this
|
|
742
|
+
path, so skipping it makes report.md say
|
|
743
|
+
종료 사유: UNKNOWN no matter how cleanly the
|
|
744
|
+
session actually ended.
|
|
745
|
+
5. bash agora.sh --report --session-dir <dir> Generate report.md once you decide to stop.
|
|
746
|
+
--round on its own NEVER calls decide_stop, NEVER writes .stop, NEVER
|
|
747
|
+
renders a gate, and NEVER generates report.md — not even when <N> reaches
|
|
748
|
+
or exceeds max_rounds (--round does not check max_rounds at all). Those
|
|
749
|
+
steps above are the caller's responsibility for every round after round 1
|
|
750
|
+
in gated mode. (--start --auto performs all of this internally, across
|
|
751
|
+
every round, and needs none of the above.)
|
|
752
|
+
USAGE
|
|
753
|
+
;;
|
|
754
|
+
*)
|
|
755
|
+
printf 'agora.sh: unknown option %s\n' "$1" >&2
|
|
756
|
+
return 64
|
|
757
|
+
;;
|
|
758
|
+
esac
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
main "$@"
|