fix: pre-landing review fixes (answer gate covers ALREADY_BUMPED --version writes; typed session rows; never drift and --answer rejection cases)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-31 03:35:41 +00:00
1 parent 27e781bd00
commit e8ffc8b29c
2 files changed
+11 -4

No files matched your search

+3 -2
View File
@@ -98,9 +98,10 @@ export function version(argv: readonly string[], cwd: string): Report {
return r.stop(`BUMP_LEVEL needed (FRESH, ${stat || 'no diff'}): MICRO under 50 lines, PATCH 50+ with no feature signal, MINOR/MAJOR ask first (Step 12.2); rerun with --bump <level>, or with --keep-current to leave VERSION at ${j.baseVersion}`);
}
const answer = flag('--answer');
if (fresh && asked && policy?.bump !== 'patch' && !answer) {
if ((fresh ? asked : explicit) && policy?.bump !== 'patch' && !answer) {
const kind = bin(cwd, 'gstack-session-kind', [], 30_000).stdout.trim() || 'unknown';
if (kind !== 'interactive') return r.stop(`BUMP_ANSWER_REQUIRED (${kind} session, bump ask): Step 12's level is the owner's approval, and a brief's "take the recommended option" is not an answer. Ask: ${asked} (recommended), keep ${j.baseVersion} (--keep-current), or stop; then rerun with --bump ${asked} --answer "<their words>". A repo whose unit PRs never bump sets bump never; one that always takes PATCH sets bump patch`);
const pick = fresh ? `--bump ${asked}` : `--version ${explicit}`;
if (kind !== 'interactive') return r.stop(`BUMP_ANSWER_REQUIRED (${kind} session, bump ask): a version Step 12 writes is the owner's approval, and a brief's "take the recommended option" is not an answer. Ask: ${pick} (recommended), keep ${fresh ? j.baseVersion : j.currentVersion} (--keep-current), or stop; then rerun with ${pick} --answer "<their words>". A repo whose unit PRs never bump sets bump never; one that always takes PATCH sets bump patch`);
}
let q = queue(cwd, base, level, j.baseVersion);
if (q.json?.complete === false) q = queue(cwd, base, level, j.baseVersion);
+8 -2
View File
@@ -89,7 +89,8 @@ describe('version (Step 12): keeping VERSION and waiting for the bump answer (GS
// Value: protects=in a session no person answers (unattended, spawned, OpenClaw, headless) a bump under bump ask waits for the owner's words before anything is written; fails_when=the bin writes VERSION on a --bump the agent took as the recommended option (gstack-evals#89 wrote 0.0.2 before anyone was asked);
// why_new=GST-380; seam=gstack-session-kind's classification, gstack-version-bump write; no_claim=that the words came from the owner
test('an unattended --bump under bump ask stops with BUMP_ANSWER_REQUIRED and writes nothing until --answer', () => {
for (const env of [UNATTENDED, { GSTACK_SESSION_KIND: 'spawned' }, { OPENCLAW_SESSION: '1' }, { GSTACK_HEADLESS: '1' }]) {
const sessions: Record<string, string>[] = [UNATTENDED, { GSTACK_SESSION_KIND: 'spawned' }, { OPENCLAW_SESSION: '1' }, { GSTACK_HEADLESS: '1' }];
for (const env of sessions) {
const f = fx();
const r = f.run(RELEASE, ['version', '--base', 'main', '--bump', 'micro', '--why', 'small'], env);
expect([r.status, /^STOP: BUMP_ANSWER_REQUIRED/m.test(r.stdout), r.stdout.includes('--keep-current')]).toEqual([3, true, true]);
@@ -97,10 +98,12 @@ describe('version (Step 12): keeping VERSION and waiting for the bump answer (GS
}
const f = fx();
expect(f.run(RELEASE, ['version', '--base', 'main', '--bump', 'micro', '--answer', '--why', 'x'], UNATTENDED).status).toBe(2);
expect(f.run(RELEASE, ['version', '--base', 'main', '--bump', 'micro', '--answer', ' '], UNATTENDED).status).toBe(2);
for (const bad of [' ', 'a\nb', 'x'.repeat(201)]) expect(f.run(RELEASE, ['version', '--base', 'main', '--bump', 'micro', '--answer', bad], UNATTENDED).status).toBe(2);
untouched(f);
const ok = f.run(RELEASE, ['version', '--base', 'main', '--bump', 'micro', '--answer', 'micro is fine'], UNATTENDED);
expect([ok.status, read(f, 'VERSION')]).toEqual([0, '1.0.0.1\n']);
const rebump = f.run(RELEASE, ['version', '--base', 'main', '--bump', 'major', '--version', '2.0.0.0'], UNATTENDED);
expect([rebump.status, /^STOP: BUMP_ANSWER_REQUIRED/m.test(rebump.stdout), read(f, 'VERSION')]).toEqual([3, true, '1.0.0.1\n']);
const patch = fx({ '.gstack/ship-policy.json': '{"bump":"patch"}\n' });
expect([patch.run(RELEASE, ['version', '--base', 'main'], UNATTENDED).status, read(patch, 'VERSION')]).toEqual([0, '1.0.1.0\n']);
});
@@ -120,6 +123,9 @@ describe('version (Step 12): keeping VERSION and waiting for the bump answer (GS
const bumped = fx(policy, { VERSION: '1.0.0.1\n' });
const moved = bumped.run(RELEASE, ['version', '--base', 'main']);
expect([moved.status, moved.stdout.includes('STOP: VERSION moved from 1.0.0.0 to 1.0.0.1 but the policy says bump never')]).toEqual([3, true]);
const stale = fx(policy, { VERSION: '1.0.0.1\n', 'package.json': '{"name":"fx","version":"0.9.0"}\n' });
const drift = stale.run(RELEASE, ['version', '--base', 'main']);
expect([drift.status, drift.stdout.includes('but the policy says bump never'), drift.stdout.includes('REPAIR:')]).toEqual([3, true, false]);
});
// Value: protects=a --bump beside --keep-current or --version <current> is a contradiction the bin names instead of picking one; fails_when=it silently keeps or silently bumps;