mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 08:23:59 +01:00
* fix(eval-plan): per-arm pilot projection and --seat-metered arms (#3653) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): guard tolerates Claude Code's cwd-reset footer and recovers after an unreadable init (#3651) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (journal reader strips the cwd-reset footer; an init with no text block no longer locks the session) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: journal reader skips a failed or unreadable init, as invocation() does Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(autoplan): --ceo-only runs the CEO review alone with zero questions (GST-537) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(eval-plan): all-seat pilot, single-attempt arm and --seat-metered CLI wiring Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(eval-plan): summed deviation adds arm deviations, matching the summed band (arms share rows) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (test pins an unpriced seat-metered arm) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (seat-metered declaration fixed per --out, price row still required, a real charge settles as spent) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: classify autoplan-ceo-only in the Codex host metadata Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: a seat-metered arm that settles a charge is projected as a priced arm Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: select autoplan-ceo-only on the skills it runs; say it needs Claude Code Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(skills): /eval-plan --seat-metered and per-arm projection Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(eval-plan): skill prose stays under its context budget and carries no $N token Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: run autoplan-ceo-only in the marathon lane Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(skills): a seat-metered arm leaves the seat set once it settles a charge Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: changelog entry for /autoplan --ceo-only Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(autoplan): init_failed covers unreadable init output and the cwd-reset line (#3651) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: count the autoplan-ceo-only paid file in the shard inventory Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(autoplan): #3651 tests pass the test-code type ratchet Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(dx): CAP finding titles carry a one-line description (GBRA-223 claim 2) The DX error contract told both autoplan DX voices to title capped findings exactly CAP4:<path>:<item>, so gates, tasks and reconcile saw a bare code. The title is now CAP4:<path>:<item> <what fails>; the grader already parses that shape. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): reconcile pairs same-file findings that carry no line (GBRA-223 claim 1) Plan reviews name files, rarely lines, and reconcile associated by location only when both findings had a line, so findings both voices raised on one file were filed new and native-only. A same-file pair with a missing line is now associated as unverified (basis file), never confirmed; a CAP code no longer counts in title comparison. DRIVER.md names submit --reconcile. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): open Critical and High findings reach the gate as apply items (GBRA-223 claim 3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): a spend cap binds on every unattended host; --estimate-usd persists; DRIVER names --usage-usd (GBRA-223 claim 4) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): run binds complete api results, names a truncated response, --max-tokens; DRIVER documents run (GBRA-223 claim 6) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * feat(autoplan): carry an item-by-item answer when the later gate asks nothing new (GBRA-223 claim 7) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(dx): trim the compact error contract so plan-devex-review stays under its size pin Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (p1b applies finding fixes, api attempts reserve no host default, cross-file title pairing, paired severity counted) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: review fixes (priced api reservations, no file-only twins, two-pass pairing, partial replies keep p1-dependent defaults open, outside refusal degrades) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * P7: record whose words answered a gate (--words-by agent:<id>), never carry an agent's own words, import each row under its own answerer (GEVA-2) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: review pass 2 (p1b needs --rec-hash after recommend; api calls send the output limit they reserve for) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * changelog: fragment for the /autoplan field-report wave (GST-672) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs: captain record notes --words-by agent attribution Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test: follow finding apply items in approval-class and Captain flow tests; bind run-api limit to DEFAULT_MAX_TOKENS Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * wave autoplan: lower the gemini plan-devex-review pin to its exact size after #3781's DX trim Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): CAP titles on different paths never match by title in reconcile or confirm rounds (wave review) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): a truncated api call settles its cost and a retry is admitted afresh against the cap Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): an applied finding stays open until the revised plan is reviewed; a shared location alone never twins two findings Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(approval): a plan gate answered in an agent's own words is not owner approval Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(gate): a new answer replaces the earlier answer's authorship and words Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): a failed api call with no usage keeps its attempt open; reservations price the bound prompt (wave fix-check) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): a spend cap fails closed: only a call refused before sending keeps its attempt; a sent call with unknown usage settles at its reservation Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(autoplan): the 16384 output default applies to both providers; changelog for the wave's integrator fixes Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(autoplan): how run settles a failed api call; changelog scopes it to the native voice Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(autoplan): a spend cap fails closed on every exit of run; an unpriced model reserves the default once a cap is set Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(autoplan): every sent call settles; an unpriced model under a cap reserves the default Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(autoplan): DRIVER and help say a cap reserves in any session kind and how run settles a sent call Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
109 lines
5.6 KiB
TypeScript
109 lines
5.6 KiB
TypeScript
/**
|
|
* The Capy Captain flow end to end on the simulated Capy host, through the
|
|
* CLIs only: an autoplan run on a plan with the four brief sections reaches
|
|
* its gate and prints the Capy RELAY line; the Captain brief carries the
|
|
* gate's rec_hash; a relayed `rec` answered with that hash approves; the
|
|
* approval record imports the owner's exact words once; and its ## Approvals
|
|
* table is mirrored to GSTACK_APPROVALS_DIR. The negative control: the same
|
|
* run without GSTACK_CAPY prints no RELAY line.
|
|
*/
|
|
import { afterAll, describe, expect, test } from 'bun:test';
|
|
import { spawnSync } from 'node:child_process';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { ROOT, recCarrying, resultOf } from './helpers/autoplan-run-fixture';
|
|
import { capyRunner, phaseVia, writePlan } from './helpers/autoplan-capy';
|
|
|
|
const r = capyRunner();
|
|
afterAll(() => r.cleanup());
|
|
|
|
const PLAN = `# Retry backoff for the webhook worker
|
|
|
|
## ELI5
|
|
|
|
A failed webhook now waits longer before each retry, so the partner stops dropping them.
|
|
|
|
## Before → after
|
|
|
|
Before: a failed webhook retries every second until it is dropped. After: retries back off from 500 ms to 30 s.
|
|
|
|
## Why it matters
|
|
|
|
Bursty retries hit the partner's rate limit and drop about 2% of deliveries.
|
|
|
|
## What caused this work
|
|
|
|
Issue #41: "webhooks vanish under load".
|
|
|
|
## Big decisions
|
|
|
|
1. Exponential backoff with jitter rather than a fixed delay. Would flip if the partner publishes a retry-after header.
|
|
|
|
## Implementation plan
|
|
|
|
Add exponential backoff to the webhook worker's retry loop in lib/worker.ts: base 500 ms, factor 2, cap 30 s, jitter 10%.
|
|
Persist the attempt count on the job row so a restarted worker resumes the schedule. Tests cover the cap, the jitter bounds and the resume path.
|
|
|
|
## Review record
|
|
`;
|
|
|
|
const bin = (name: string, args: string[], extra: Record<string, string> = {}, cwd = ROOT) =>
|
|
spawnSync('bun', [path.join(ROOT, 'bin', name), ...args], { cwd, env: { ...r.env, ...extra }, encoding: 'utf8', timeout: 120_000 });
|
|
|
|
describe('Capy Captain flow', () => {
|
|
test('gate → RELAY line → Captain brief with rec_hash → relayed answer → approval record and mirror', () => {
|
|
const dir = path.join(r.host.base, 'captain');
|
|
const plan = writePlan(dir, PLAN);
|
|
const out = path.join(dir, 'run');
|
|
expect(r.ap(['next', '--out', out, '--plan', plan, '--no-ui', '--no-dx']).status).toBe(0);
|
|
phaseVia(r, out, 'ceo');
|
|
const eng = phaseVia(r, out, 'eng');
|
|
const gateOut = eng.outside.stdout + eng.next.stdout;
|
|
expect(gateOut).toContain('status=gate_pending');
|
|
expect(gateOut).toContain(`RELAY: gstack-owner-brief render --captain --out ${out} `);
|
|
|
|
const brief = bin('gstack-owner-brief', ['render', '--captain', '--out', out, '--json']);
|
|
expect(brief.status, brief.stderr).toBe(0);
|
|
const j = JSON.parse(brief.stdout);
|
|
expect(j.missing).toEqual([]);
|
|
expect(j.brief).toStartWith('**Decision needed:** approve the plan "Retry backoff for the webhook worker".');
|
|
for (const h of ['### Before → after', '### Why it matters', '### What caused this work', '### Big decisions', '### What we need from you']) expect(j.brief).toContain(h);
|
|
|
|
const reply = recCarrying(out, 'rec');
|
|
const answer = r.ap(['answer', '--out', out, '--gate-rev', String(j.gate_rev), '--reply', reply, '--rec-hash', j.rec_hash, '--by', 'agent:GSTA-1', '--owner-reply', reply]);
|
|
expect(answer.status, answer.stderr + answer.stdout).toBe(0);
|
|
expect(answer.stdout).toMatch(/^APPROVED: gate_rev=1/m);
|
|
|
|
const repo = path.join(r.host.base, 'repo');
|
|
fs.mkdirSync(repo, { recursive: true });
|
|
spawnSync('git', ['init', '-q', '-b', 'fix/backoff'], { cwd: repo, timeout: 30_000 });
|
|
const mirror = path.join(r.host.base, 'drive-approvals');
|
|
const env = { GSTACK_APPROVALS_DIR: mirror, GSTACK_PROJECT_SLUG: 'acme-worker' };
|
|
const imp = bin('gstack-approvals', ['import', '--autoplan', out, '--by', 'Garry'], env, repo);
|
|
expect(imp.status, imp.stderr).toBe(0);
|
|
expect(imp.stdout).toMatch(/^APPROVAL_IMPORTED: n=1 skipped=0 /);
|
|
expect(bin('gstack-approvals', ['import', '--autoplan', out, '--by', 'Garry'], env, repo).stdout).toMatch(/^APPROVAL_IMPORTED: n=0 skipped=1 /);
|
|
const table = bin('gstack-approvals', ['render'], env, repo).stdout;
|
|
expect(table).toContain('Approve the plan "Retry backoff for the webhook worker"');
|
|
expect(table).toContain(`“${reply}” (Garry, relayed by agent:GSTA-1) | approved |`);
|
|
expect(fs.readdirSync(path.join(mirror, 'acme-worker')).length).toBe(1);
|
|
});
|
|
|
|
test('without GSTACK_CAPY the gate prints no RELAY line', () => {
|
|
const dir = path.join(r.host.base, 'plain');
|
|
const plan = writePlan(dir, PLAN);
|
|
const out = path.join(dir, 'run');
|
|
expect(r.ap(['next', '--out', out, '--plan', plan, '--no-ui', '--no-dx']).status).toBe(0);
|
|
phaseVia(r, out, 'ceo');
|
|
expect(r.ap(['next', '--out', out]).status).toBe(0);
|
|
for (const voice of ['native', 'outside']) expect(r.review(path.join(out, `eng-${voice}-prompt.md`)).status).toBe(0);
|
|
const run = JSON.parse(fs.readFileSync(path.join(out, 'run.json'), 'utf8'));
|
|
const submit = (voice: string, model: string) => r.ap(['submit', '--out', out, '--phase', 'eng', '--voice', voice, '--result', resultOf(out, 'eng', voice as 'native' | 'outside'), '--model', model, '--attempt', run.phases.eng[voice].attempt, '--runner', 'host-subagent'], { GSTACK_CAPY: '' });
|
|
expect(submit('native', 'anthropic/claude-opus-5-5').status).toBe(0);
|
|
const last = submit('outside', 'openai/gpt-6.1-sol');
|
|
expect(last.stdout).toContain('status=gate_pending');
|
|
expect(last.stdout).toMatch(/^GATE: gate_rev=1 /m);
|
|
expect(last.stdout).not.toContain('RELAY: gstack-owner-brief');
|
|
});
|
|
});
|