mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-07 14:06:42 +02:00
Merge branch 'main' into garrytan/team-supabase-store
Brings in 55 commits from main (v0.12.x–v0.13.5.0): Factory Droid compat, prompt injection defense, user sovereignty, security audit, design binary, skill namespacing, modular resolvers, Chrome sidebar, and more. Conflict resolution: - .agents/ SKILL.md files: deleted (main moved to .factory/) - 8 .tmpl templates: accepted main (new features: CDP mode, design tools, global retro, parallelization, distribution checks, plan audits) - scripts/gen-skill-docs.ts: accepted main's modular resolver refactor - test/helpers/session-runner.ts: accepted main + layered back CostEntry tracking from team branch - Generated SKILL.md files: regenerated via bun run gen:skill-docs - Updated tests to match main's gstack-slug output (2 lines, no PROJECTS_DIR) and review log mechanism (gstack-review-log, not $BRANCH.jsonl) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+139
-29
@@ -99,6 +99,20 @@ describe('SKILL.md command validation', () => {
|
||||
const result = validateSkill(skill);
|
||||
expect(result.snapshotFlagErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('all $B commands in autoplan/SKILL.md are valid browse commands', () => {
|
||||
const skill = path.join(ROOT, 'autoplan', 'SKILL.md');
|
||||
if (!fs.existsSync(skill)) return;
|
||||
const result = validateSkill(skill);
|
||||
expect(result.invalid).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('all snapshot flags in autoplan/SKILL.md are valid', () => {
|
||||
const skill = path.join(ROOT, 'autoplan', 'SKILL.md');
|
||||
if (!fs.existsSync(skill)) return;
|
||||
const result = validateSkill(skill);
|
||||
expect(result.snapshotFlagErrors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Command registry consistency', () => {
|
||||
@@ -227,6 +241,7 @@ describe('Update check preamble', () => {
|
||||
'benchmark/SKILL.md',
|
||||
'land-and-deploy/SKILL.md',
|
||||
'setup-deploy/SKILL.md',
|
||||
'cso/SKILL.md',
|
||||
];
|
||||
|
||||
for (const skill of skillsWithUpdateCheck) {
|
||||
@@ -513,10 +528,12 @@ describe('TODOS-format.md reference consistency', () => {
|
||||
// --- v0.4.1 feature coverage: RECOMMENDATION format, session awareness, enum completeness ---
|
||||
|
||||
describe('v0.4.1 preamble features', () => {
|
||||
const skillsWithPreamble = [
|
||||
'SKILL.md', 'browse/SKILL.md', 'qa/SKILL.md',
|
||||
'qa-only/SKILL.md',
|
||||
'setup-browser-cookies/SKILL.md',
|
||||
// Tier 1 skills have core preamble only (no AskUserQuestion format)
|
||||
const tier1Skills = ['SKILL.md', 'browse/SKILL.md', 'setup-browser-cookies/SKILL.md', 'benchmark/SKILL.md'];
|
||||
|
||||
// Tier 2+ skills have AskUserQuestion format with RECOMMENDATION
|
||||
const tier2PlusSkills = [
|
||||
'qa/SKILL.md', 'qa-only/SKILL.md',
|
||||
'ship/SKILL.md', 'review/SKILL.md',
|
||||
'plan-ceo-review/SKILL.md', 'plan-eng-review/SKILL.md',
|
||||
'retro/SKILL.md',
|
||||
@@ -526,22 +543,25 @@ describe('v0.4.1 preamble features', () => {
|
||||
'design-consultation/SKILL.md',
|
||||
'document-release/SKILL.md',
|
||||
'canary/SKILL.md',
|
||||
'benchmark/SKILL.md',
|
||||
'land-and-deploy/SKILL.md',
|
||||
'setup-deploy/SKILL.md',
|
||||
'cso/SKILL.md',
|
||||
];
|
||||
|
||||
for (const skill of skillsWithPreamble) {
|
||||
const skillsWithPreamble = [...tier1Skills, ...tier2PlusSkills];
|
||||
|
||||
for (const skill of tier2PlusSkills) {
|
||||
test(`${skill} contains RECOMMENDATION format`, () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
|
||||
expect(content).toContain('RECOMMENDATION: Choose');
|
||||
expect(content).toContain('AskUserQuestion');
|
||||
});
|
||||
}
|
||||
|
||||
for (const skill of skillsWithPreamble) {
|
||||
test(`${skill} contains session awareness`, () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
|
||||
expect(content).toContain('_SESSIONS');
|
||||
expect(content).toContain('RECOMMENDATION');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -724,14 +744,8 @@ describe('Contributor mode preamble structure', () => {
|
||||
for (const skill of skillsWithPreamble) {
|
||||
test(`${skill} has 0-10 rating in contributor mode`, () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
|
||||
expect(content).toContain('0 to 10');
|
||||
expect(content).toContain('My rating');
|
||||
});
|
||||
|
||||
test(`${skill} has calibration example`, () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, skill), 'utf-8');
|
||||
expect(content).toContain('Calibration');
|
||||
expect(content).toContain('the bar');
|
||||
expect(content).toContain('0-10');
|
||||
expect(content).toContain('Rating');
|
||||
});
|
||||
|
||||
test(`${skill} has "what would make this a 10" field`, () => {
|
||||
@@ -807,7 +821,7 @@ describe('Completeness Principle in generated SKILL.md files', () => {
|
||||
'design-review/SKILL.md',
|
||||
'design-consultation/SKILL.md',
|
||||
'document-release/SKILL.md',
|
||||
];
|
||||
'cso/SKILL.md', ];
|
||||
|
||||
for (const skill of skillsWithPreamble) {
|
||||
test(`${skill} contains Completeness Principle section`, () => {
|
||||
@@ -817,17 +831,12 @@ describe('Completeness Principle in generated SKILL.md files', () => {
|
||||
});
|
||||
}
|
||||
|
||||
test('Completeness Principle includes compression table', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
test('Completeness Principle includes compression table in tier 2+ skills', () => {
|
||||
// Root is tier 1 (no completeness). Check tier 2+ skill.
|
||||
const content = fs.readFileSync(path.join(ROOT, 'cso', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('CC+gstack');
|
||||
expect(content).toContain('Compression');
|
||||
});
|
||||
|
||||
test('Completeness Principle includes anti-patterns', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('BAD:');
|
||||
expect(content).toContain('Anti-patterns');
|
||||
});
|
||||
});
|
||||
|
||||
// --- Part 7: Planted-bug fixture validation (A4) ---
|
||||
@@ -961,10 +970,37 @@ describe('gstack-slug', () => {
|
||||
test('output is eval-compatible (KEY=VALUE format)', () => {
|
||||
const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' });
|
||||
const lines = result.stdout.toString().trim().split('\n');
|
||||
expect(lines.length).toBe(3);
|
||||
expect(lines.length).toBe(2);
|
||||
expect(lines[0]).toMatch(/^SLUG=.+/);
|
||||
expect(lines[1]).toMatch(/^BRANCH=.+/);
|
||||
expect(lines[2]).toMatch(/^PROJECTS_DIR=.+/);
|
||||
});
|
||||
|
||||
test('output values contain only safe characters (no shell metacharacters)', () => {
|
||||
const result = Bun.spawnSync([SLUG_BIN], { cwd: ROOT, stdout: 'pipe', stderr: 'pipe' });
|
||||
const slug = result.stdout.toString().match(/SLUG=(.*)/)?.[1] ?? '';
|
||||
const branch = result.stdout.toString().match(/BRANCH=(.*)/)?.[1] ?? '';
|
||||
// Only alphanumeric, dot, dash, underscore are allowed (#133)
|
||||
expect(slug).toMatch(/^[a-zA-Z0-9._-]+$/);
|
||||
expect(branch).toMatch(/^[a-zA-Z0-9._-]+$/);
|
||||
});
|
||||
test('eval sets variables under bash with set -euo pipefail', () => {
|
||||
const result = Bun.spawnSync(
|
||||
['bash', '-c', 'set -euo pipefail; eval "$(./bin/gstack-slug 2>/dev/null)"; echo "SLUG=$SLUG"; echo "BRANCH=$BRANCH"'],
|
||||
{ cwd: ROOT, stdout: 'pipe', stderr: 'pipe' }
|
||||
);
|
||||
expect(result.exitCode).toBe(0);
|
||||
const output = result.stdout.toString();
|
||||
expect(output).toMatch(/^SLUG=.+/m);
|
||||
expect(output).toMatch(/^BRANCH=.+/m);
|
||||
});
|
||||
|
||||
test('no templates or bin scripts use source process substitution for gstack-slug', () => {
|
||||
const result = Bun.spawnSync(
|
||||
['grep', '-r', 'source <(.*gstack-slug', '--include=*.tmpl', '--include=gstack-review-*', '.'],
|
||||
{ cwd: ROOT, stdout: 'pipe', stderr: 'pipe' }
|
||||
);
|
||||
// grep returns exit code 1 when no matches found — that's what we want
|
||||
expect(result.stdout.toString().trim()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1275,7 +1311,7 @@ describe('Codex skill', () => {
|
||||
expect(content).toContain('fall back to the Claude adversarial subagent');
|
||||
// Review log uses new skill name
|
||||
expect(content).toContain('adversarial-review');
|
||||
expect(content).toContain('xhigh');
|
||||
expect(content).toContain('reasoning_effort="high"');
|
||||
expect(content).toContain('ADVERSARIAL REVIEW SYNTHESIS');
|
||||
});
|
||||
|
||||
@@ -1285,17 +1321,23 @@ describe('Codex skill', () => {
|
||||
expect(content).toContain('< 50');
|
||||
expect(content).toContain('200+');
|
||||
expect(content).toContain('adversarial-review');
|
||||
expect(content).toContain('xhigh');
|
||||
expect(content).toContain('reasoning_effort="high"');
|
||||
expect(content).toContain('Investigate and fix');
|
||||
});
|
||||
|
||||
test('codex-host ship/review do NOT contain adversarial review step', () => {
|
||||
// .agents/ is gitignored — generate on demand
|
||||
Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'codex'], {
|
||||
cwd: ROOT, stdout: 'pipe', stderr: 'pipe',
|
||||
});
|
||||
const shipContent = fs.readFileSync(path.join(ROOT, '.agents', 'skills', 'gstack-ship', 'SKILL.md'), 'utf-8');
|
||||
expect(shipContent).not.toContain('codex review --base');
|
||||
expect(shipContent).not.toContain('Investigate and fix');
|
||||
expect(shipContent).not.toContain('CODEX_REVIEWS');
|
||||
|
||||
const reviewContent = fs.readFileSync(path.join(ROOT, '.agents', 'skills', 'gstack-review', 'SKILL.md'), 'utf-8');
|
||||
expect(reviewContent).not.toContain('codex review --base');
|
||||
expect(reviewContent).not.toContain('codex_reviews');
|
||||
expect(reviewContent).not.toContain('CODEX_REVIEWS');
|
||||
expect(reviewContent).not.toContain('adversarial-review');
|
||||
expect(reviewContent).not.toContain('Investigate and fix');
|
||||
});
|
||||
@@ -1306,6 +1348,13 @@ describe('Codex skill', () => {
|
||||
expect(content).toContain('codex exec');
|
||||
});
|
||||
|
||||
test('/review persists a review-log entry for ship readiness', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'review', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('"skill":"review"');
|
||||
expect(content).toContain('"issues_found":N');
|
||||
expect(content).toContain('Persist Eng Review result');
|
||||
});
|
||||
|
||||
test('Review Readiness Dashboard includes Adversarial Review row', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('Adversarial');
|
||||
@@ -1362,6 +1411,11 @@ describe('Skill trigger phrases', () => {
|
||||
describe('Codex skill validation', () => {
|
||||
const AGENTS_DIR = path.join(ROOT, '.agents', 'skills');
|
||||
|
||||
// .agents/ is gitignored (v0.11.2.0) — generate on demand for tests
|
||||
Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'codex'], {
|
||||
cwd: ROOT, stdout: 'pipe', stderr: 'pipe',
|
||||
});
|
||||
|
||||
// Discover all Claude skills with templates (except /codex which is Claude-only)
|
||||
const CLAUDE_SKILLS_WITH_TEMPLATES = (() => {
|
||||
const skills: string[] = [];
|
||||
@@ -1423,3 +1477,59 @@ describe('Codex skill validation', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- Repo mode and test failure triage validation ---
|
||||
|
||||
describe('Repo mode preamble validation', () => {
|
||||
test('generated SKILL.md preamble contains REPO_MODE output', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('REPO_MODE:');
|
||||
expect(content).toContain('gstack-repo-mode');
|
||||
});
|
||||
|
||||
test('tier 3+ skills contain See Something Say Something section', () => {
|
||||
// Root SKILL.md is tier 1 (no Repo Mode). Check a tier 3 skill instead.
|
||||
const content = fs.readFileSync(path.join(ROOT, 'plan-ceo-review', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('See Something, Say Something');
|
||||
expect(content).toContain('REPO_MODE');
|
||||
expect(content).toContain('solo');
|
||||
expect(content).toContain('collaborative');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test failure triage in ship skill', () => {
|
||||
test('ship/SKILL.md contains Test Failure Ownership Triage', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('Test Failure Ownership Triage');
|
||||
});
|
||||
|
||||
test('ship/SKILL.md triage uses git diff for classification', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('git diff origin/<base>...HEAD --name-only');
|
||||
});
|
||||
|
||||
test('ship/SKILL.md triage has solo and collaborative paths', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('REPO_MODE');
|
||||
expect(content).toContain('solo');
|
||||
expect(content).toContain('collaborative');
|
||||
expect(content).toContain('Investigate and fix now');
|
||||
expect(content).toContain('Add as P0 TODO');
|
||||
});
|
||||
|
||||
test('ship/SKILL.md triage has GitHub issue assignment for collaborative mode', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('gh issue create');
|
||||
expect(content).toContain('--assignee');
|
||||
});
|
||||
|
||||
test('{{TEST_FAILURE_TRIAGE}} placeholder is fully resolved in ship/SKILL.md', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||
expect(content).not.toContain('{{TEST_FAILURE_TRIAGE}}');
|
||||
});
|
||||
|
||||
test('ship/SKILL.md uses in-branch language for stop condition', () => {
|
||||
const content = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||
expect(content).toContain('In-branch test failures');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user