diff --git a/CHANGELOG.md b/CHANGELOG.md index 32210d67f..c4fd10ee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.94.47.0] - 2026-11-03 + +### Fixed +- **The lean `/autoplan` reviewer sees the committed draft on a PR-branch checkout** (GST-1241). Draft capture took `origin/HEAD` as its base, and a Capy machine started on a PR branch points `origin/HEAD` at that branch, so the reviewer got `DRAFT: none (no code changes against origin/)`. It now resolves the base through `lib/default-base.ts`, the same resolver `bin/gstack-diff-scope` uses, which skips an `origin/HEAD` naming the checked-out branch and falls back to the PR base, the remote's live HEAD, then `origin/main`. + ## [1.94.46.0] - 2026-11-03 **`gstack-autoplan export --public` no longer carries the owner's own words (GST-1243).** GEVA's public bundle (GEVA-225, gstack 1.93.34.0) kept all 38 `owner_reply` fields in decisions.jsonl, and a sealed reviewer file (`ceo-native.a5.bound.md`) quoted an owner reply word for word. GEVA redacted both by hand. Garry's rule: nothing a person wrote leaves without their click. diff --git a/VERSION b/VERSION index e48a3683b..3bc4a6676 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.94.46.0 +1.94.47.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 94fd4a9e5..c0b1f157f 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.94.46.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.94.47.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/autoplan/DRIVER.md b/autoplan/DRIVER.md index 994d98564..3a1b4969a 100644 --- a/autoplan/DRIVER.md +++ b/autoplan/DRIVER.md @@ -20,7 +20,7 @@ The first `next` prints `SCOPE: ui=… (terms; visual; files) dx=… (detected) TODOS*, docs/** but not docs/*-policy.md, or other markdown outside a skill dir) runs CEO on the native voice only, then Eng. UI needs a rendered file path or a UI term next to a visual noun in the Implementation plan. To keep flags for later runs on the same plan, add the `SCOPE_PIN:` marker it prints (``) as the plan's first -line yourself; gstack never writes the plan. **Kinds (R65), first `next` only.** `--kind fix` reviews nothing: it prints `AUTOPLAN: not needed (bug fix; R65)`, a `PROOF:` line and `GSTACK_RESULT: skill=autoplan status=skipped run=-`, writes nothing and needs no `--out` or `--plan`; the PR still carries the bug's proof, a repro on current main or a failing test written first. `--kind feature`, the default for an unattended run on Capy (`GSTACK_CAPY=1`) that names no kind, is the lean run. Its `phase_order` is `eng` alone with one native reviewer, whose prompt merges the CEO and Eng lenses (Design or DX only when scope detects them), always asks "Any outside terms, licences or rights involved?" and shows the draft code. The draft is `git diff` from the merge base with `--base ` (default `origin/HEAD`), untracked files included and the run directory left out, written to the snapshot as `draft.diff` and hashed; with no code yet it says `DRAFT: none ()`. The first `next` prints `LEAN:` and each opened phase `DRAFT:`; the reviewer's line after INPUT must be `DRAFT: ` or `DRAFT: none`, or submit refuses it (`RESULT_RECEIPT_MISSING`). No outside voice, `--max-rounds 1`, no carry check and one gate revision: `revise --incorporates` applies the accepted changes without re-review and approves. `--full` brings back every phase and both voices; off Capy, and in an interactive session, the default stays full. +line yourself; gstack never writes the plan. **Kinds (R65), first `next` only.** `--kind fix` reviews nothing: it prints `AUTOPLAN: not needed (bug fix; R65)`, a `PROOF:` line and `GSTACK_RESULT: skill=autoplan status=skipped run=-`, writes nothing and needs no `--out` or `--plan`; the PR still carries the bug's proof, a repro on current main or a failing test written first. `--kind feature`, the default for an unattended run on Capy (`GSTACK_CAPY=1`) that names no kind, is the lean run. Its `phase_order` is `eng` alone with one native reviewer, whose prompt merges the CEO and Eng lenses (Design or DX only when scope detects them), always asks "Any outside terms, licences or rights involved?" and shows the draft code. The draft is `git diff` from the merge base with `--base ` (default: `gstack-diff-scope`'s base, never an `origin/HEAD` naming the checked-out branch), untracked files included and the run directory left out, written to the snapshot as `draft.diff` and hashed; with no code yet it says `DRAFT: none ()`. The first `next` prints `LEAN:` and each opened phase `DRAFT:`; the reviewer's line after INPUT must be `DRAFT: ` or `DRAFT: none`, or submit refuses it (`RESULT_RECEIPT_MISSING`). No outside voice, `--max-rounds 1`, no carry check and one gate revision: `revise --incorporates` applies the accepted changes without re-review and approves. `--full` brings back every phase and both voices; off Capy, and in an interactive session, the default stays full. ## The loop diff --git a/autoplan/DRIVER.md.tmpl b/autoplan/DRIVER.md.tmpl index 2831814f7..a5e67ebe2 100644 --- a/autoplan/DRIVER.md.tmpl +++ b/autoplan/DRIVER.md.tmpl @@ -17,7 +17,7 @@ The first `next` prints `SCOPE: ui=… (terms; visual; files) dx=… (detected) TODOS*, docs/** but not docs/*-policy.md, or other markdown outside a skill dir) runs CEO on the native voice only, then Eng. UI needs a rendered file path or a UI term next to a visual noun in the Implementation plan. To keep flags for later runs on the same plan, add the `SCOPE_PIN:` marker it prints (``) as the plan's first -line yourself; gstack never writes the plan. **Kinds (R65), first `next` only.** `--kind fix` reviews nothing: it prints `AUTOPLAN: not needed (bug fix; R65)`, a `PROOF:` line and `GSTACK_RESULT: skill=autoplan status=skipped run=-`, writes nothing and needs no `--out` or `--plan`; the PR still carries the bug's proof, a repro on current main or a failing test written first. `--kind feature`, the default for an unattended run on Capy (`GSTACK_CAPY=1`) that names no kind, is the lean run. Its `phase_order` is `eng` alone with one native reviewer, whose prompt merges the CEO and Eng lenses (Design or DX only when scope detects them), always asks "Any outside terms, licences or rights involved?" and shows the draft code. The draft is `git diff` from the merge base with `--base ` (default `origin/HEAD`), untracked files included and the run directory left out, written to the snapshot as `draft.diff` and hashed; with no code yet it says `DRAFT: none ()`. The first `next` prints `LEAN:` and each opened phase `DRAFT:`; the reviewer's line after INPUT must be `DRAFT: ` or `DRAFT: none`, or submit refuses it (`RESULT_RECEIPT_MISSING`). No outside voice, `--max-rounds 1`, no carry check and one gate revision: `revise --incorporates` applies the accepted changes without re-review and approves. `--full` brings back every phase and both voices; off Capy, and in an interactive session, the default stays full. +line yourself; gstack never writes the plan. **Kinds (R65), first `next` only.** `--kind fix` reviews nothing: it prints `AUTOPLAN: not needed (bug fix; R65)`, a `PROOF:` line and `GSTACK_RESULT: skill=autoplan status=skipped run=-`, writes nothing and needs no `--out` or `--plan`; the PR still carries the bug's proof, a repro on current main or a failing test written first. `--kind feature`, the default for an unattended run on Capy (`GSTACK_CAPY=1`) that names no kind, is the lean run. Its `phase_order` is `eng` alone with one native reviewer, whose prompt merges the CEO and Eng lenses (Design or DX only when scope detects them), always asks "Any outside terms, licences or rights involved?" and shows the draft code. The draft is `git diff` from the merge base with `--base ` (default: `gstack-diff-scope`'s base, never an `origin/HEAD` naming the checked-out branch), untracked files included and the run directory left out, written to the snapshot as `draft.diff` and hashed; with no code yet it says `DRAFT: none ()`. The first `next` prints `LEAN:` and each opened phase `DRAFT:`; the reviewer's line after INPUT must be `DRAFT: ` or `DRAFT: none`, or submit refuses it (`RESULT_RECEIPT_MISSING`). No outside voice, `--max-rounds 1`, no carry check and one gate revision: `revise --incorporates` applies the accepted changes without re-review and approves. `--full` brings back every phase and both voices; off Capy, and in an interactive session, the default stays full. ## The loop diff --git a/lib/autoplan-lean.ts b/lib/autoplan-lean.ts index d27efe67f..f16c939b5 100644 --- a/lib/autoplan-lean.ts +++ b/lib/autoplan-lean.ts @@ -16,6 +16,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { OUTSIDE_ROLE, sectionLoadFor, type Phase, type Scope } from './autoplan-prompts'; import { RunError, type RunState } from './autoplan-state'; +import { defaultBaseBranch, noDefaultBaseLine } from './default-base'; import { redactFindingSpans } from './redact-engine'; export const KINDS = ['fix', 'feature'] as const; @@ -66,8 +67,9 @@ export function captureDraft(cwd: string, dir: string, out: string, base?: strin const top = git(['rev-parse', '--show-toplevel']); if (top.status !== 0) return { base: base ?? 'origin/HEAD', none: 'not a git checkout' }; const root = top.stdout.trim(); - const origin = git(['rev-parse', '--abbrev-ref', 'origin/HEAD'], root); - const ref = base ?? (origin.status === 0 && origin.stdout.trim() ? origin.stdout.trim() : 'origin/main'); + const found = base ? undefined : defaultBaseBranch(root); + if (found && !found.branch) return { base: 'origin/HEAD', none: found.skipped ? noDefaultBaseLine(found.skipped) : 'no default base (no origin/HEAD, PR base, origin/main or origin/master); pass --base' }; + const ref = base ?? `${found!.remote ?? 'origin'}/${found!.branch}`; const mb = git(['merge-base', 'HEAD', ref], root); if (mb.status !== 0) return { base: ref, none: `no merge base with ${ref}` }; const mergeBase = mb.stdout.trim(); diff --git a/package.json b/package.json index 9c2fd6e48..3c8401771 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.94.46", + "version": "1.94.47", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/test/autoplan-lean.test.ts b/test/autoplan-lean.test.ts index 449b5b3f2..c2d565c31 100644 --- a/test/autoplan-lean.test.ts +++ b/test/autoplan-lean.test.ts @@ -16,6 +16,7 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { ROOT, SMALL_PLAN, autoplan, env, journal, readRun } from './helpers/autoplan-run-fixture'; import { capyRunner } from './helpers/autoplan-capy'; +import { captureDraft } from '../lib/autoplan-lean'; const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'ap-lean-')); afterAll(() => fs.rmSync(TMP, { recursive: true, force: true })); @@ -488,3 +489,42 @@ describe('draft capture edges', () => { expect(prompt.indexOf('IGNORE THE PLAN')).toBeLessThan(prompt.indexOf(`\n${open[1]}\n`, open.index + 1)); }); }); +describe('draft capture base on a PR-branch clone (GST-1241)', () => { + /** A clone of `feature`, its draft committed and pushed, whose origin/HEAD names `head` while origin's own HEAD stays main: a Capy machine started on a PR branch names that branch. */ + const clone = (name: string, head: string) => { + const src = repo(name, false); + fs.writeFileSync(path.join(src, 'lib', 'worker.ts'), 'export const retry = (n: number) => Math.min(30_000, 500 * 2 ** n);\n'); + git(src, 'commit', '-q', '-am', 'draft backoff'); + const bare = path.join(TMP, name, 'origin.git'); + git(TMP, 'clone', '-q', '--bare', src, bare); + git(bare, 'symbolic-ref', 'HEAD', 'refs/heads/main'); + const cwd = path.join(TMP, name, 'clone'); + git(TMP, 'clone', '-q', '-b', 'feature', bare, cwd); + git(cwd, 'remote', 'set-head', 'origin', head); + expect(git(cwd, 'rev-parse', '--abbrev-ref', 'origin/HEAD').trim()).toBe(`origin/${head}`); + return cwd; + }; + const capture = (name: string, cwd: string) => { + const dir = path.join(TMP, name, 'snap'); + const out = path.join(TMP, name, 'run'); + fs.mkdirSync(dir); + fs.mkdirSync(out); + const draft = captureDraft(cwd, dir, out); + return { draft, diff: draft.sha256 ? fs.readFileSync(path.join(dir, 'draft.diff'), 'utf8') : '' }; + }; + + test('origin/HEAD naming the PR branch still captures the committed draft against main', () => { + const { draft, diff } = capture('pr-head', clone('pr-head', 'feature')); + expect(draft.none).toBeUndefined(); + expect(draft.base).toBe('origin/main'); + expect(draft.files).toEqual(['lib/worker.ts']); + expect(diff).toContain('+export const retry = (n: number) => Math.min(30_000, 500 * 2 ** n);'); + }); + + test('control: origin/HEAD naming main captures the same draft', () => { + const main = capture('main-head', clone('main-head', 'main')); + expect(main.draft.base).toBe('origin/main'); + expect(main.draft.files).toEqual(['lib/worker.ts']); + expect(main.diff).toContain('+export const retry = (n: number) => Math.min(30_000, 500 * 2 ** n);'); + }); +});