A solo design pick ships a brittle ticket. A small jury forces you to surface the hard rule, spot when "labeling problem" is really a data gap, and converge to ready instead of refining.
When to convene
Caucus is for design questions with real divergence, where the goal is a ready ticket. Skip it for one-line facts (one agent), pure execution (just do it), or where there's only one sane option.
Composition: two advocates + one skeptic
Two advocates pick majority-wins and call it consensus. Add an impartial chair/skeptic whose job is failure modes and a decision criterion. Three is convention, not doctrine — two advocates + one chair works; one advocate + one skeptic works; three advocates doesn't.
The three prompts share one SCENARIO
Each agent gets the same verified facts (IDs, code-line refs, observed outcomes). Letting each agent self-research drifts the facts and you can't synthesize. Then each gets a distinct lens and a boundary:
SCENARIO (verified): ...
CONTEXT (read these files, these are the related issues, decision-maker's steer): ...
YOUR LENS: [extend-X | separate-track | skeptic/chair]
BOUNDARY: read-only, no code, ≤500 words, return VERDICT line.
Use VERDICT as the last line so synthesis can grep it. Pick lens names that frame the choice (extend-#122 advocate, separate-track advocate, skeptic/chair) — the framing shapes the output.
Run the three concurrently in background; wait for all three before synthesizing. Sequential loses the wall-clock and the parallel disagreement.
Synthesize
- Consensus → hard rule. Whatever all three agree on goes in as a non-negotiable constraint, not a soft preference.
- Divergence → chair's criterion + compatibility points. The skeptic's job is to name when each advocate breaks. Often the advocates are compatible once you apply the criterion (e.g. fallback rule + quarantine path, gated by data-first verification).
- Lock → ready. The whole point of the caucus is to converge. If you converged, open
ready and release the lock. Don't re-refine.
Traps
- Treating caucus as solution. Three agents return positions; you synthesize. Letting them agree with each other is a fragile consensus.
- No skeptic. Two advocates ship majority-wins.
- No shared SCENARIO. Facts diverge, synthesis collapses.
- No VERDICT line. Agent writes an essay, you can't grep.
- Caucus for execution. Locating a bug, writing a doc, picking a flag value — none of these need a jury.
- "Three" as dogma. Two advocates + one chair is the minimum useful shape. Three advocates is the maximum useless shape.
The synthesis rule that matters most: when the chair says "this is a data problem disguised as a labeling problem," that's the reframe — verify the data first, then design the fallback. Without the skeptic, you'd have built a clever rule on top of an incomplete export and shipped proxy labels.
Same task — "show me what this branch changed" — but the two tools take opposite dot conventions. Get it backwards and your review fills with commits the author never touched.
The diff: use three dots
git diff origin/main...HEAD
Three-dot diff is git diff $(git merge-base origin/main HEAD) HEAD — it diffs against the branch point, not the tip of main. That's exactly what you want: the author's delta and nothing else.
The nice property: it's immune to origin/main moving forward. New commits landing on main after the branch point aren't ancestors of HEAD, so they don't shift the merge-base. The diff stays clean.
The trap is a stale base, not a newer one. If your local main is older than the branch point, the merge-base slides back to an older ancestor and the diff swallows unrelated upstream commits — making the author look like they changed far more than they did. So fetch first:
git fetch origin
git diff origin/main...HEAD
Want to see the branch point itself? git merge-base origin/main HEAD.
The log: use two dots
git log origin/main..HEAD
Two-dot log = commits reachable from HEAD but not from origin/main = the branch's own commits, exactly.
Don't reach for three dots here out of habit — git log A...B is the symmetric difference, so it also lists the commits main picked up that HEAD doesn't have. That's the noise you were trying to avoid.
So: diff three-dot, log two-dot. Different tools, opposite defaults, same job.
VARCHAR2(4000) means 4000 bytes, not characters. Most people know this. What's less obvious: VARCHAR2(4000 CHAR) doesn't guarantee 4000 characters either.
Under the default MAX_STRING_SIZE=STANDARD, the hard column cap is 4000 bytes regardless of whether you declared BYTE or CHAR. In AL32UTF8, a Chinese character takes ~3 bytes, so VARCHAR2(4000 CHAR) on a column storing CJK text will fail once the actual byte count exceeds 4000 — around ~1333 characters in.
To actually store 4000 CJK characters in a single VARCHAR2, the instance needs MAX_STRING_SIZE=EXTENDED (12c+), which raises the limit to 32767 bytes. This is not the default — not even in 19c — and it's a one-way migration that requires running utl32k.sql in upgrade mode. Oracle keeps it off by default precisely because it changes data dictionary behavior and breaks compatibility.
Quick check for your instance:
SELECT value FROM v$parameter WHERE name = 'max_string_size';
STANDARD = 4000-byte ceiling. EXTENDED = 32767-byte ceiling.
In development, your frontend runs on localhost:5173 and your API server on localhost:3000. The browser blocks cross-origin requests — that's CORS. Vite's dev proxy solves this by forwarding /api/* requests to the backend, making them look same-origin to the browser:
// vite.config.ts
server: {
proxy: {
'/api': 'http://localhost:3000'
}
}
In production this proxy disappears. The built frontend is just static files (HTML/JS/CSS) — no port, no process. Nginx or a CDN serves them, and reverse-proxies /api/* to the backend the same way Vite did in dev:
user → Nginx :80
├── /api/* → backend :3000
└── /* → dist/ static files
One port from the user's perspective, no CORS issue. The backend port is always real and needed; the frontend "port" only exists during development because Vite's dev server is a live process.