mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
v1.91.216.0 fix(sync-gbrain): refuse an active autopilot before touching the source list (#3360 part 2, port of #3362) (#3386)
* fix(sync-gbrain): refuse on an active autopilot before the code stage registers a source Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * docs(changelog): sync-gbrain autopilot refusal before source registration (#3360, port of #3362) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(sync-gbrain): the early autopilot refusal check matches #3377's pid-naming wording (#3360) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: Evgenii Lopatin <e75533@gmail.com> Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
6 files changed
+76
-10
No files matched your search
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## [1.91.216.0] - 2026-10-25
|
||||
|
||||
**/sync-gbrain refuses an active autopilot before it touches the brain's source list (#3360, part 2; ported from #3362 by @exGeni).** The code stage checked for a running `gbrain autopilot` only right before its destructive walk. By then it had already cleaned up legacy sources, renamed a source and run `gbrain sources add`, so a refused run still registered a source it never synced.
|
||||
|
||||
### Fixed
|
||||
- **`bin/gstack-gbrain-sync.ts`**: the code stage runs the autopilot check before Step 0a, so a refusal leaves the source list untouched. The check before the walk stays, because autopilot can start while the steps in between run. Dry-run, read-only and deny policies and the local-engine skip still return earlier and are unchanged. Which processes count as an autopilot (the machine-wide probe, #3360 part 1) is unchanged.
|
||||
|
||||
### Tests
|
||||
- `test/gstack-gbrain-sync.test.ts` runs the real script against a fake `gbrain` and a `pgrep` that reports an autopilot: the stage is refused and the fake saw no `sources add`, `rename`, `remove` or `sync`. On main the same run issues `sources add`.
|
||||
|
||||
## [1.91.215.0] - 2026-10-25
|
||||
|
||||
**An unattended `/ship` leaves its receipt in a file the parent can read, and the parent is told to read that file (GSTA-88).** Before, Step 18's handoff section wrote the ship receipt only to stdout, and `docs/unattended.md` told the parent to read it from the PR body with `gstack-ship-receipt read --pr <n> --json`. An unattended run must not push, so there is no PR: the parent got a usage error or `RECEIPT_MISSING`, or, if an older PR existed, read that PR's receipt with exit 0 and a stale `head`. Now the handoff writes `.gstack/tmp/ship-receipt.md`, the parent reads it with `read --body` in the child's checkout, and it compares the receipt's `head` to the branch SHA either way.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.91.215.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.91.216.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,
|
||||
|
||||
@@ -1225,6 +1225,18 @@ async function runCodeImport(args: CliArgs): Promise<StageResult> {
|
||||
return skipStageForLocalStatus("code", localStatus, t0);
|
||||
}
|
||||
|
||||
// #1734: refuse on an active autopilot BEFORE any source-metadata change
|
||||
// (Step 0b rename, Step 1 `sources add`), so a refused stage leaves the
|
||||
// brain's source list untouched. The check right before the destructive walk
|
||||
// below stays: autopilot can start while the steps in between run.
|
||||
const refusedAutopilot = (signal: string | null): StageResult => ({
|
||||
name: "code", ran: true, ok: false, duration_ms: Date.now() - t0,
|
||||
summary: `refused: gbrain autopilot active (${signal}). Stop autopilot, then re-run /sync-gbrain.`,
|
||||
detail: { source_id: sourceId, source_path: root, status: "refused-autopilot" },
|
||||
});
|
||||
const apEarly = detectAutopilot(gbrainEnv);
|
||||
if (apEarly.active) return refusedAutopilot(apEarly.signal);
|
||||
|
||||
// Step 0a: Best-effort cleanup of pre-pathhash legacy source (v1.x form).
|
||||
// Earlier /sync-gbrain versions registered `gstack-code-<slug>` (no path
|
||||
// suffix). On a multi-worktree repo, those collapsed onto a single id
|
||||
@@ -1308,13 +1320,7 @@ async function runCodeImport(args: CliArgs): Promise<StageResult> {
|
||||
// --allow-reclone. Both surface a visible reason and fail the stage so the
|
||||
// verdict shows ERR rather than silently skipping protection.
|
||||
const apBeforeWalk = detectAutopilot(gbrainEnv);
|
||||
if (apBeforeWalk.active) {
|
||||
return {
|
||||
name: "code", ran: true, ok: false, duration_ms: Date.now() - t0,
|
||||
summary: `refused: gbrain autopilot active (${apBeforeWalk.signal}). Stop autopilot, then re-run /sync-gbrain.`,
|
||||
detail: { source_id: sourceId, source_path: root, status: "refused-autopilot" },
|
||||
};
|
||||
}
|
||||
if (apBeforeWalk.active) return refusedAutopilot(apBeforeWalk.signal);
|
||||
const reclone = decideCodeSync(sourceId, gbrainEnv, args.allowReclone);
|
||||
if (!reclone.allow) {
|
||||
return {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.91.215",
|
||||
"version": "1.91.216",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -1139,3 +1139,53 @@ esac
|
||||
expect(r.stderr.length).toBeLessThan(64 * 1024);
|
||||
});
|
||||
});
|
||||
|
||||
// #1734: a refused code stage must leave the brain's source list untouched.
|
||||
describe("code stage autopilot refusal registers nothing (#1734)", () => {
|
||||
it.skipIf(process.platform === "win32")("refuses before `gbrain sources add` when an autopilot process is visible", () => {
|
||||
const home = makeTestHome();
|
||||
const gstackHome = join(home, ".gstack");
|
||||
const repo = mkdtempSync(join(tmpdir(), "gstack-ap-refuse-repo-"));
|
||||
const bindir = mkdtempSync(join(tmpdir(), "gstack-ap-refuse-bin-"));
|
||||
const commandLog = join(home, "gbrain-commands.log");
|
||||
mkdirSync(gstackHome, { recursive: true });
|
||||
mkdirSync(join(home, ".gbrain"), { recursive: true });
|
||||
writeFileSync(join(home, ".gbrain", "config.json"), JSON.stringify({ engine: "pglite", database_url: "pglite:///test" }));
|
||||
spawnSync("git", ["init", "--quiet", "-b", "main"], { cwd: repo, timeout: 30_000 });
|
||||
writeFileSync(join(bindir, "gbrain"), `#!/bin/sh
|
||||
printf '%s\\n' "$*" >> "$GSTACK_TEST_GBRAIN_LOG"
|
||||
case "$1 $2" in
|
||||
"--version ") echo 'gbrain 0.59.0.0' ;;
|
||||
"sources list") echo '{"sources":[]}' ;;
|
||||
"sources add"|"sources attach"|"sync --strategy") ;;
|
||||
*) echo "unexpected gbrain command: $*" >&2; exit 1 ;;
|
||||
esac
|
||||
`);
|
||||
chmodSync(join(bindir, "gbrain"), 0o755);
|
||||
writeFileSync(join(bindir, "pgrep"), "#!/bin/sh\necho 4242\nexit 0\n");
|
||||
chmodSync(join(bindir, "pgrep"), 0o755);
|
||||
const r = spawnSync("bun", [SCRIPT, "--code-only", "--quiet"], {
|
||||
encoding: "utf-8",
|
||||
timeout: 60000,
|
||||
cwd: repo,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: home,
|
||||
GSTACK_HOME: gstackHome,
|
||||
GBRAIN_HOME: "",
|
||||
GSTACK_TEST_GBRAIN_LOG: commandLog,
|
||||
PATH: `${bindir}:${process.env.PATH || ""}`,
|
||||
},
|
||||
});
|
||||
const commands = existsSync(commandLog) ? readFileSync(commandLog, "utf-8") : "";
|
||||
const state = JSON.parse(readFileSync(join(gstackHome, ".gbrain-sync-state.json"), "utf-8"));
|
||||
const code = state.last_stages.find((s: { name: string }) => s.name === "code");
|
||||
for (const d of [repo, bindir, home]) rmSync(d, { recursive: true, force: true });
|
||||
expect(r.status).toBe(1);
|
||||
expect(code.ok).toBe(false);
|
||||
expect(code.summary).toContain("refused: gbrain autopilot active (process:gbrain autopilot pid 4242 ");
|
||||
expect(code.summary).toContain("Stop autopilot, then re-run /sync-gbrain.");
|
||||
expect(commands).not.toMatch(/^sources (add|rename|remove) /m);
|
||||
expect(commands).not.toMatch(/^sync /m);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user