diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cf730125..7dc2e26fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.95.4.0] - 2026-11-04 + +### 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.95.3.0] - 2026-11-04 **A crew thread no longer creates threads itself. It asks its Captain, so every new thread joins a crew from the start (R68). docs/captain.md also warns that `tools.capy.read` returns a cut-down view of long files.** diff --git a/VERSION b/VERSION index d1317fde6..61868db17 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.95.3.0 +1.95.4.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 00803de87..640cf2e75 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.95.3.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.95.4.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 a2aef2c50..47d27398d 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 3e84d6815..78ffc8d4d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.95.3", + "version": "1.95.4", "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);'); + }); +});