mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-06 17:04:27 +01:00
* fix(land-and-deploy): never merge over red, pending or missing CI (#2995) /land-and-deploy gated only on required checks, so a repo with none (every GitHub Free private repo, any repo without branch protection) skipped the CI wait and could merge over red CI. The verdict now comes from bin/gstack-ci-gate, a tested helper bound to the head SHA: all checks by default, cancel/unknown buckets fail, NO_CHECKS needs explicit per-head approval, and both merge commands (including --auto) run only after a PASS for the same SHA they pin with --match-head-commit. Co-authored-by: harshitgavita-07 <harshit.gavita@gmail.com> * fix(slug): give nested-group projects their own state bucket and verify checkpoint identity (#3003, #2901) gstack-slug kept only the last two remote path segments, so customer-a/product/repo and customer-b/product/repo shared one bucket and /context-restore could present another project's checkpoint. - One canonical-remote rule (bash + TS, golden vectors) used by all six resolvers: lowercased host, no userinfo/port/.git, Azure DevOps and Bitbucket Server normalized. 3+ segment remotes get <last-two>-<16 hex>; two-segment remotes (all of GitHub) are unchanged. - Versioned sticky cache (v2:) keeps cache hits git-free. - context-save stamps the credential-free remote and root; context-restore only treats this project's checkpoints as latest, reports foreign/unverified ones first, and treats a different root with the same remote (Conductor handoff) as information. - gstack-slug --adopt-legacy copies only files the user confirms. - question-preference-hook reads the same versioned cache, so never-ask preferences fire; unknown identity asks instead of auto-deciding. * fix(gbrain): brain context loads, managed sync works, memory sync stops duplicating (#2883, #2985, #2922) - brain-context-load called 'gbrain list_pages' (an MCP tool name), so every skill's brain context was 0 bytes. It now uses 'gbrain list' with gbrain's real filter flags, and prints one line when queries fail. - The code-stage sync always passes --no-pull (managed gbrain >= 0.51 refuses without it); a gbrain that rejects the flag fails with an upgrade message and is never retried without it. stderr is streamed with a bounded tail and quoted in failure summaries. - /sync-gbrain gains --sources (and GSTACK_MEMORY_INGEST_SOURCES); by default it skips types already synced through a federated curated source, so curated pages stop being re-duplicated. Co-authored-by: Vitor Cepeda Lopes <vitor@vitorcepedalopes.com> Co-authored-by: harshitgavita-07 <harshit.gavita@gmail.com> * fix(codex,cso): clean Codex reviews stop failing; semgrep and trivy SARIF results import (#2769, #3011) - Codex review gate is PASS / FAIL / UNVERIFIED. Untagged output (what a clean 'codex review --base' prints) is UNVERIFIED, not FAIL, and never a false PASS; P2/P3-only output passes. - import-sarif resolves semgrep's undeclared %SRCROOT% and trivy's ROOTPATH (including git-URL targets) through explicit, captured mappings; declared bases must sit inside the source root or the run's checkout, URIs are percent-decoded, and rejected results are counted with rule, base and path instead of silently dropped. * fix: version strings are not IPs, failed update checks are not 'up to date', private security reporting (#2856, #2786, #2984) - The pre-push redact hook exempts a root VERSION file that is only a version string and the version token in CHANGELOG release headings; the engine allowlist is untouched. - gstack-update-check caches an unreadable remote as CHECK_FAILED with a short TTL instead of UP_TO_DATE; /gstack-upgrade never reports 'latest' after a failed check. - SECURITY.md points reporters at GitHub private vulnerability reporting. - pr-title-sync (pull_request_target, write token) pins actions/checkout to a reviewed SHA. * fix(autoplan): publication guard accepts real Claude Code journals (#3007, #3009, #2968, #2977, #2986, #3006) The guard required a session journal to start with a user message, but Claude Code starts it with SessionStart hook attachments (and a forked compacted session starts with a compact_boundary), so every phase was denied for anyone with a SessionStart hook. Windows path spellings (C:/, /c/, drive case) were rejected too. - Reader accepts the captured startup, compact, clear and fork shapes (claude 2.1.284 fixture), computes root uniqueness over every candidate, and returns typed reason codes instead of one string. - Hard denials (competing root, foreign cwd, sidechain, agent, cycle, malformed, identity) say what happened and how to run the reviews by hand; unflushed journals still retry. - A well-formed, stable journal whose root shape is unrecognized now warns ('phase publication was NOT verified') and continues, without overriding Claude Code's own permission check; no bypass switch. - Windows paths fold before validation, so '..' is still rejected. - Periodic drift canary runs the reader on fresh journals from the pinned claude. Co-authored-by: Charles M Berg <77517285+CharlesBerg85@users.noreply.github.com> Co-authored-by: laurenbdaniels-coder <264343335+laurenbdaniels-coder@users.noreply.github.com> Co-authored-by: Jordan Mayer <176445995+maydaycyber@users.noreply.github.com> Co-authored-by: elmesery-dev <elmesery.dev@gmail.com> * v1.91.13.0: severe bug fix wave release notes, stale gbrain verbs in docs * test: seed the versioned slug cache in the memory-injection deny case (#2901) The preference hook now resolves the project bucket from gstack-slug's v2 slug-cache entry instead of basename(cwd); this fixture still wrote preferences under the basename bucket without the entry the skill preamble always writes first. * fix: full-suite follow-ups for the fix wave - gstack-slug's --help heredoc is over 512B, so it gets the BASH_COMPAT=50 pipe-deadlock guard like the other bins. - Refresh the codex/factory ship goldens for the plan-gates slug change. - The periodic lane gains the autoplan journal drift canary (107 paid files); raise the eval-slices cap to 25 so every slice still starts at once until its recorded duration lets the planner pack it. * fix(plan-eng-review): clearer Step 0 sequencing, write routes and section outputs The workflow judge scored plan-eng-review's instructions at actionability 3.67 (bar 4) on text unchanged from main. Resolve the ambiguities it named together, without changing any gate or output: Step 0 names the Read's order and keeps one STOP; the pending-call, read-only, forbidden-write and failed-save rules become one Write routes block; header/label limits and grid checks are concrete; Performance gets a checklist; each section has a stated output shape; Lake Score is defined. Same total length (parity cap). --------- Co-authored-by: harshitgavita-07 <harshit.gavita@gmail.com> Co-authored-by: Vitor Cepeda Lopes <vitor@vitorcepedalopes.com> Co-authored-by: Charles M Berg <77517285+CharlesBerg85@users.noreply.github.com> Co-authored-by: laurenbdaniels-coder <264343335+laurenbdaniels-coder@users.noreply.github.com> Co-authored-by: Jordan Mayer <176445995+maydaycyber@users.noreply.github.com> Co-authored-by: elmesery-dev <elmesery.dev@gmail.com>
88 lines
5.2 KiB
TypeScript
88 lines
5.2 KiB
TypeScript
/**
|
|
* Autoplan journal drift canary (periodic tier, rule kind, ~$0.02, haiku).
|
|
*
|
|
* The /autoplan publication guard reads Claude Code's own session journal and
|
|
* must prove which records belong to the parent session. Claude Code has
|
|
* changed that journal's root shape before (SessionStart hook attachments,
|
|
* #3007/#2968; resumed forks that begin at a compact boundary, #2977), and each
|
|
* change bricked /autoplan for users until a release. This canary captures real
|
|
* journals from the pinned `claude` with a SessionStart hook installed, for
|
|
* startup, /compact and `--resume --fork-session`, and runs the guard's reader on
|
|
* them, so the next format change fails this lane instead of users.
|
|
*
|
|
* It never files issues; the periodic report job owns that.
|
|
*/
|
|
import { expect } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { JUDGE_MS } from './helpers/eval-budgets';
|
|
import { describeIfSelected, testIfSelected } from './helpers/e2e-helpers';
|
|
import { resolveClaudeCommand } from '../lib/claude-bin';
|
|
import { readOwnedClaudePublicTranscript, readPlanCountTranscript } from '../lib/claude-public-transcript';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const CANARY_MODEL = 'claude-haiku-4-5-20251001';
|
|
const PINNED = /@anthropic-ai\/claude-code@(\d+\.\d+\.\d+)/.exec(
|
|
fs.readFileSync(path.join(ROOT, '.github/docker/Dockerfile.ci'), 'utf8'))?.[1];
|
|
|
|
describeIfSelected('autoplan journal drift canary', ['autoplan-journal-drift'], () => {
|
|
testIfSelected('autoplan-journal-drift', async () => {
|
|
if (!process.env.ANTHROPIC_API_KEY) throw new Error('autoplan-journal-drift requires ANTHROPIC_API_KEY; refusing to skip');
|
|
const claude = resolveClaudeCommand();
|
|
if (!claude || !PINNED) throw new Error('autoplan-journal-drift needs the claude CLI and the Dockerfile.ci pin');
|
|
const base = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'autoplan-journal-drift-')));
|
|
try {
|
|
const config = path.join(base, 'home', '.claude'), project = path.join(base, 'project');
|
|
fs.mkdirSync(config, { recursive: true }); fs.mkdirSync(project);
|
|
// Two SessionStart hooks, one plain and one with additionalContext, the
|
|
// shape plugins install and #3007 reported.
|
|
fs.writeFileSync(path.join(config, 'settings.json'), JSON.stringify({ hooks: { SessionStart: [{ hooks: [
|
|
{ type: 'command', command: 'echo drift-canary-hook' },
|
|
{ type: 'command', command: `echo '${JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: 'drift canary' } })}'` },
|
|
] }] } }));
|
|
const env = { PATH: process.env.PATH ?? '', HOME: path.join(base, 'home'), ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
|
CLAUDE_CONFIG_DIR: config, DISABLE_AUTOUPDATER: '1', TERM: 'dumb' };
|
|
const claudeRun = (...args: string[]) => {
|
|
const child = spawnSync(claude.command, [...claude.argsPrefix, ...args], { cwd: project, env, encoding: 'utf8', timeout: 90_000 });
|
|
if (child.status !== 0) throw new Error(`claude ${args.join(' ')} exited ${child.status ?? child.signal}: ${child.stderr.slice(-400)}`);
|
|
return child.stdout;
|
|
};
|
|
expect(claudeRun('--version')).toContain(PINNED);
|
|
const journals = () => {
|
|
const projects = path.join(config, 'projects');
|
|
return fs.readdirSync(projects).flatMap(dir => fs.readdirSync(path.join(projects, dir))
|
|
.filter(name => name.endsWith('.jsonl')).map(name => path.join(projects, dir, name)));
|
|
};
|
|
const owned = (file: string) => readOwnedClaudePublicTranscript(file, project, path.basename(file, '.jsonl'));
|
|
const shape = (file: string) => fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line))
|
|
.filter(r => r.uuid).map(r => [r.type, r.subtype, r.attachment?.type].filter(Boolean).join(':')).slice(0, 6);
|
|
const expectOwned = (file: string, label: string) => {
|
|
const read = owned(file);
|
|
if (read.transcript.status !== 'ready')
|
|
throw new Error(`${label}: Claude Code ${PINNED} journal is not owned (${read.transcript.reason ?? read.transcript.status}); head ${JSON.stringify(shape(file))}`);
|
|
expect(read.events.some(e => e.kind === 'message')).toBe(true);
|
|
};
|
|
|
|
claudeRun('-p', 'Reply with the single word: ok', '--model', CANARY_MODEL, '--strict-mcp-config');
|
|
const [startup] = journals();
|
|
expect(journals()).toHaveLength(1);
|
|
expectOwned(startup!, 'startup');
|
|
expect(readPlanCountTranscript(config, project).status).toBe('ready');
|
|
const session = path.basename(startup!, '.jsonl');
|
|
|
|
claudeRun('-p', '/compact', '--resume', session, '--model', CANARY_MODEL, '--strict-mcp-config');
|
|
expect(fs.readFileSync(startup!, 'utf8')).toContain('"compact_boundary"');
|
|
expectOwned(startup!, 'compact');
|
|
|
|
claudeRun('-p', 'Reply with the single word: ok', '--resume', session, '--fork-session', '--model', CANARY_MODEL, '--strict-mcp-config');
|
|
const fork = journals().find(file => file !== startup);
|
|
expect(fork).toBeDefined();
|
|
expectOwned(fork!, 'fork after compact');
|
|
} finally {
|
|
fs.rmSync(base, { recursive: true, force: true });
|
|
}
|
|
}, JUDGE_MS * 3);
|
|
});
|