mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +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>
192 lines
7.3 KiB
TypeScript
192 lines
7.3 KiB
TypeScript
import { beforeAll, afterAll, expect } from 'bun:test';
|
|
import { JUDGE_MS, CAPTURE_MS } from './helpers/eval-budgets';
|
|
import { runSkillTest } from './helpers/session-runner';
|
|
import {
|
|
ROOT, runId,
|
|
describeIfSelected, testConcurrentIfSelected,
|
|
copyDirSync, logCost, recordE2E,
|
|
createEvalCollector, finalizeEvalCollector,
|
|
} from './helpers/e2e-helpers';
|
|
import { spawnSync } from 'child_process';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
|
|
const evalCollector = createEvalCollector('e2e-plan-tune');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// /plan-tune E2E: verify the skill recognizes plain-English intent and hits
|
|
// the right binary paths without CLI subcommand syntax.
|
|
//
|
|
// This is a gate-tier test — if /plan-tune requires memorized subcommands or
|
|
// fails on plain English, that is a regression of the core v1 DX promise.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describeIfSelected('PlanTune E2E', ['plan-tune-inspect'], () => {
|
|
let workDir: string;
|
|
let gstackHome: string;
|
|
let slug: string;
|
|
|
|
beforeAll(() => {
|
|
workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-plan-tune-'));
|
|
gstackHome = path.join(workDir, '.gstack-home');
|
|
|
|
const run = (cmd: string, args: string[]) =>
|
|
spawnSync(cmd, args, { cwd: workDir, stdio: 'pipe', timeout: 5000 });
|
|
run('git', ['init', '-b', 'main']);
|
|
run('git', ['config', 'user.email', 'test@test.com']);
|
|
run('git', ['config', 'user.name', 'Test']);
|
|
fs.writeFileSync(path.join(workDir, 'README.md'), '# test\n');
|
|
run('git', ['add', '.']);
|
|
run('git', ['commit', '-m', 'initial']);
|
|
|
|
// Copy the /plan-tune skill (extract the flow section only — full template
|
|
// is ~45KB and includes preamble boilerplate the agent doesn't need).
|
|
copyDirSync(path.join(ROOT, 'plan-tune'), path.join(workDir, 'plan-tune'));
|
|
|
|
// Copy required bins — the skill references these by path.
|
|
const binDir = path.join(workDir, 'bin');
|
|
fs.mkdirSync(binDir, { recursive: true });
|
|
for (const script of [
|
|
'gstack-slug',
|
|
'gstack-config',
|
|
'gstack-state-root.sh',
|
|
'gstack-remote-identity.sh',
|
|
'gstack-question-log',
|
|
'gstack-question-preference',
|
|
'gstack-developer-profile',
|
|
'gstack-builder-profile',
|
|
]) {
|
|
const src = path.join(ROOT, 'bin', script);
|
|
if (fs.existsSync(src)) {
|
|
fs.copyFileSync(src, path.join(binDir, script));
|
|
fs.chmodSync(path.join(binDir, script), 0o755);
|
|
}
|
|
}
|
|
|
|
// gstack-developer-profile --derive imports from scripts/ — copy those too.
|
|
const scriptsDir = path.join(workDir, 'scripts');
|
|
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
for (const src of ['question-registry.ts', 'psychographic-signals.ts', 'archetypes.ts', 'one-way-doors.ts']) {
|
|
fs.copyFileSync(path.join(ROOT, 'scripts', src), path.join(scriptsDir, src));
|
|
}
|
|
|
|
// Compute slug the same way the binary does (basename fallback).
|
|
slug = path.basename(workDir).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
|
|
// Seed a few question-log entries so "review questions" has something to show.
|
|
const projectDir = path.join(gstackHome, 'projects', slug);
|
|
fs.mkdirSync(projectDir, { recursive: true });
|
|
const entries = [
|
|
{
|
|
ts: '2026-04-10T10:00:00Z',
|
|
skill: 'plan-ceo-review',
|
|
question_id: 'plan-ceo-review-mode',
|
|
question_summary: 'Which review mode?',
|
|
category: 'routing',
|
|
door_type: 'two-way',
|
|
options_count: 4,
|
|
user_choice: 'expand',
|
|
recommended: 'selective',
|
|
followed_recommendation: false,
|
|
session_id: 's1',
|
|
},
|
|
{
|
|
ts: '2026-04-11T10:00:00Z',
|
|
skill: 'ship',
|
|
question_id: 'ship-test-failure-triage',
|
|
question_summary: 'Test failed',
|
|
category: 'approval',
|
|
door_type: 'one-way',
|
|
options_count: 3,
|
|
user_choice: 'fix-now',
|
|
recommended: 'fix-now',
|
|
followed_recommendation: true,
|
|
session_id: 's2',
|
|
},
|
|
{
|
|
ts: '2026-04-12T10:00:00Z',
|
|
skill: 'ship',
|
|
question_id: 'ship-changelog-voice-polish',
|
|
question_summary: 'Polish changelog voice',
|
|
category: 'approval',
|
|
door_type: 'two-way',
|
|
options_count: 2,
|
|
user_choice: 'skip',
|
|
recommended: 'accept',
|
|
followed_recommendation: false,
|
|
session_id: 's3',
|
|
},
|
|
];
|
|
fs.writeFileSync(
|
|
path.join(projectDir, 'question-log.jsonl'),
|
|
entries.map((e) => JSON.stringify(e)).join('\n') + '\n',
|
|
);
|
|
|
|
// Pre-set question_tuning=true so the skill doesn't enter the first-time setup flow.
|
|
const cfgDir = path.join(gstackHome);
|
|
fs.mkdirSync(cfgDir, { recursive: true });
|
|
fs.writeFileSync(path.join(cfgDir, 'config.yaml'), 'question_tuning: true\n');
|
|
});
|
|
|
|
afterAll(() => {
|
|
try { fs.rmSync(workDir, { recursive: true, force: true }); } catch {}
|
|
finalizeEvalCollector(evalCollector);
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Plain-English intent: "review my questions"
|
|
// -------------------------------------------------------------------------
|
|
testConcurrentIfSelected('plan-tune-inspect', async () => {
|
|
const result = await runSkillTest({
|
|
prompt: `Read ./plan-tune/SKILL.md for the /plan-tune skill instructions.
|
|
|
|
The user has invoked /plan-tune and says: "Review the questions I've been asked recently."
|
|
|
|
IMPORTANT:
|
|
- Use GSTACK_HOME="${gstackHome}" as an environment variable for all bin calls.
|
|
- Replace any ~/.claude/skills/gstack/bin/ references with ./bin/ (relative path).
|
|
- Replace any ~/.claude/skills/gstack/scripts/ references with ./scripts/.
|
|
- Do NOT use AskUserQuestion.
|
|
- Do NOT implement code changes.
|
|
- Route the user's intent to the right section of the skill (Review question log).
|
|
- Show them the logged questions with counts and the follow/override ratio.`,
|
|
workingDirectory: workDir,
|
|
maxTurns: 15,
|
|
allowedTools: ['Bash', 'Read', 'Grep', 'Glob'],
|
|
timeout: JUDGE_MS,
|
|
testName: 'plan-tune-inspect',
|
|
runId,
|
|
});
|
|
|
|
logCost('/plan-tune review', result);
|
|
|
|
const output = result.output.toLowerCase();
|
|
|
|
// Agent must have surfaced at least 2 of the 3 logged question_ids
|
|
const mentionsCEO = output.includes('plan-ceo-review-mode') || output.includes('review mode');
|
|
const mentionsShipTest = output.includes('ship-test-failure-triage') || output.includes('test failed');
|
|
const mentionsChangelog = output.includes('changelog') || output.includes('ship-changelog-voice-polish');
|
|
const foundCount = [mentionsCEO, mentionsShipTest, mentionsChangelog].filter(Boolean).length;
|
|
|
|
// Agent should note override behavior (user overrode CEO review and changelog polish)
|
|
const noticedOverride =
|
|
output.includes('overrid') ||
|
|
output.includes('skip') ||
|
|
output.includes('expand');
|
|
|
|
const exitOk = ['success', 'error_max_turns'].includes(result.exitReason);
|
|
|
|
recordE2E(evalCollector, '/plan-tune', 'Plan-tune inspection flow (plain English)', result, {
|
|
passed: exitOk && foundCount >= 2,
|
|
});
|
|
|
|
expect(exitOk).toBe(true);
|
|
expect(foundCount).toBeGreaterThanOrEqual(2);
|
|
|
|
if (!noticedOverride) {
|
|
console.warn('Agent did not surface override/skip behavior from the log');
|
|
}
|
|
}, CAPTURE_MS);
|
|
});
|