Merge remote-tracking branch 'origin/main' into garrytan/codex-compat-wave2

# Conflicts:
#	.agents/skills/gstack-autoplan/SKILL.md
#	.agents/skills/gstack-benchmark/SKILL.md
#	.agents/skills/gstack-browse/SKILL.md
#	.agents/skills/gstack-canary/SKILL.md
#	.agents/skills/gstack-design-consultation/SKILL.md
#	.agents/skills/gstack-design-review/SKILL.md
#	.agents/skills/gstack-document-release/SKILL.md
#	.agents/skills/gstack-investigate/SKILL.md
#	.agents/skills/gstack-land-and-deploy/SKILL.md
#	.agents/skills/gstack-office-hours/SKILL.md
#	.agents/skills/gstack-plan-ceo-review/SKILL.md
#	.agents/skills/gstack-plan-design-review/SKILL.md
#	.agents/skills/gstack-plan-eng-review/SKILL.md
#	.agents/skills/gstack-qa-only/SKILL.md
#	.agents/skills/gstack-qa/SKILL.md
#	.agents/skills/gstack-retro/SKILL.md
#	.agents/skills/gstack-review/SKILL.md
#	.agents/skills/gstack-setup-browser-cookies/SKILL.md
#	.agents/skills/gstack-setup-deploy/SKILL.md
#	.agents/skills/gstack-ship/SKILL.md
#	.agents/skills/gstack/SKILL.md
#	.github/workflows/skill-docs.yml
#	README.md
This commit is contained in:
Garry Tan
2026-03-22 17:55:14 -07:00
62 changed files with 8691 additions and 337 deletions
+76
View File
@@ -0,0 +1,76 @@
/**
* Shared fixture for test coverage audit E2E tests.
*
* Creates a Node.js project with billing source code that has intentional
* test coverage gaps: processPayment has happy-path-only tests,
* refundPayment has no tests at all.
*
* Used by: ship-coverage-audit E2E, review-coverage-audit E2E
*/
import * as fs from 'fs';
import * as path from 'path';
import { spawnSync } from 'child_process';
export function createCoverageAuditFixture(dir: string): void {
// Create a Node.js project WITH test framework but coverage gaps
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
name: 'test-coverage-app',
version: '1.0.0',
type: 'module',
scripts: { test: 'echo "no tests yet"' },
devDependencies: { vitest: '^1.0.0' },
}, null, 2));
// Create vitest config
fs.writeFileSync(path.join(dir, 'vitest.config.ts'),
`import { defineConfig } from 'vitest/config';\nexport default defineConfig({ test: {} });\n`);
fs.writeFileSync(path.join(dir, 'VERSION'), '0.1.0.0\n');
fs.writeFileSync(path.join(dir, 'CHANGELOG.md'), '# Changelog\n');
// Create source file with multiple code paths
fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
fs.writeFileSync(path.join(dir, 'src', 'billing.ts'), `
export function processPayment(amount: number, currency: string) {
if (amount <= 0) throw new Error('Invalid amount');
if (currency !== 'USD' && currency !== 'EUR') throw new Error('Unsupported currency');
return { status: 'success', amount, currency };
}
export function refundPayment(paymentId: string, reason: string) {
if (!paymentId) throw new Error('Payment ID required');
if (!reason) throw new Error('Reason required');
return { status: 'refunded', paymentId, reason };
}
`);
// Create a test directory with ONE test (partial coverage)
fs.mkdirSync(path.join(dir, 'test'), { recursive: true });
fs.writeFileSync(path.join(dir, 'test', 'billing.test.ts'), `
import { describe, test, expect } from 'vitest';
import { processPayment } from '../src/billing';
describe('processPayment', () => {
test('processes valid payment', () => {
const result = processPayment(100, 'USD');
expect(result.status).toBe('success');
});
// GAP: no test for invalid amount
// GAP: no test for unsupported currency
// GAP: refundPayment not tested at all
});
`);
// Init git repo with main branch
const run = (cmd: string, args: string[]) =>
spawnSync(cmd, args, { cwd: dir, stdio: 'pipe', timeout: 5000 });
run('git', ['init', '-b', 'main']);
run('git', ['config', 'user.email', 'test@test.com']);
run('git', ['config', 'user.name', 'Test']);
run('git', ['add', '.']);
run('git', ['commit', '-m', 'initial commit']);
// Create feature branch
run('git', ['checkout', '-b', 'feature/billing']);
}
+146 -2
View File
@@ -457,6 +457,150 @@ describe('REVIEW_DASHBOARD resolver', () => {
});
});
// ─── Test Coverage Audit Resolver Tests ─────────────────────
describe('TEST_COVERAGE_AUDIT placeholders', () => {
const planSkill = fs.readFileSync(path.join(ROOT, 'plan-eng-review', 'SKILL.md'), 'utf-8');
const shipSkill = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
const reviewSkill = fs.readFileSync(path.join(ROOT, 'review', 'SKILL.md'), 'utf-8');
test('all three modes share codepath tracing methodology', () => {
const sharedPhrases = [
'Trace data flow',
'Diagram the execution',
'Quality scoring rubric',
'★★★',
'★★',
'GAP',
];
for (const phrase of sharedPhrases) {
expect(planSkill).toContain(phrase);
expect(shipSkill).toContain(phrase);
expect(reviewSkill).toContain(phrase);
}
// Plan mode traces the plan, not a git diff
expect(planSkill).toContain('Trace every codepath in the plan');
expect(planSkill).not.toContain('git diff origin');
// Ship and review modes trace the diff
expect(shipSkill).toContain('Trace every codepath changed');
expect(reviewSkill).toContain('Trace every codepath changed');
});
test('all three modes include E2E decision matrix', () => {
for (const skill of [planSkill, shipSkill, reviewSkill]) {
expect(skill).toContain('E2E Test Decision Matrix');
expect(skill).toContain('→E2E');
expect(skill).toContain('→EVAL');
}
});
test('all three modes include regression rule', () => {
for (const skill of [planSkill, shipSkill, reviewSkill]) {
expect(skill).toContain('REGRESSION RULE');
expect(skill).toContain('IRON RULE');
}
});
test('all three modes include test framework detection', () => {
for (const skill of [planSkill, shipSkill, reviewSkill]) {
expect(skill).toContain('Test Framework Detection');
expect(skill).toContain('CLAUDE.md');
}
});
test('plan mode adds tests to plan + includes test plan artifact', () => {
expect(planSkill).toContain('Add missing tests to the plan');
expect(planSkill).toContain('eng-review-test-plan');
expect(planSkill).toContain('Test Plan Artifact');
});
test('ship mode auto-generates tests + includes before/after count', () => {
expect(shipSkill).toContain('Generate tests for uncovered paths');
expect(shipSkill).toContain('Before/after test count');
expect(shipSkill).toContain('30 code paths max');
expect(shipSkill).toContain('ship-test-plan');
});
test('review mode generates via Fix-First + gaps are INFORMATIONAL', () => {
expect(reviewSkill).toContain('Fix-First');
expect(reviewSkill).toContain('INFORMATIONAL');
expect(reviewSkill).toContain('Step 4.75');
expect(reviewSkill).toContain('subsumes the "Test Gaps" category');
});
test('plan mode does NOT include ship-specific content', () => {
expect(planSkill).not.toContain('Before/after test count');
expect(planSkill).not.toContain('30 code paths max');
expect(planSkill).not.toContain('ship-test-plan');
});
test('review mode does NOT include test plan artifact', () => {
expect(reviewSkill).not.toContain('Test Plan Artifact');
expect(reviewSkill).not.toContain('eng-review-test-plan');
expect(reviewSkill).not.toContain('ship-test-plan');
});
// Regression guard: ship output contains key phrases from before the refactor
test('ship SKILL.md regression guard — key phrases preserved', () => {
const regressionPhrases = [
'100% coverage is the goal',
'ASCII coverage diagram',
'processPayment',
'refundPayment',
'billing.test.ts',
'checkout.e2e.ts',
'COVERAGE:',
'QUALITY:',
'GAPS:',
'Code paths:',
'User flows:',
];
for (const phrase of regressionPhrases) {
expect(shipSkill).toContain(phrase);
}
});
});
// --- {{TEST_FAILURE_TRIAGE}} resolver tests ---
describe('TEST_FAILURE_TRIAGE resolver', () => {
const shipSkill = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
test('contains all 4 triage steps', () => {
expect(shipSkill).toContain('Step T1: Classify each failure');
expect(shipSkill).toContain('Step T2: Handle in-branch failures');
expect(shipSkill).toContain('Step T3: Handle pre-existing failures');
expect(shipSkill).toContain('Step T4: Execute the chosen action');
});
test('T1 includes classification criteria (in-branch vs pre-existing)', () => {
expect(shipSkill).toContain('In-branch');
expect(shipSkill).toContain('Likely pre-existing');
expect(shipSkill).toContain('git diff origin/');
});
test('T3 branches on REPO_MODE (solo vs collaborative)', () => {
expect(shipSkill).toContain('REPO_MODE');
expect(shipSkill).toContain('solo');
expect(shipSkill).toContain('collaborative');
});
test('solo mode offers fix-now, TODO, and skip options', () => {
expect(shipSkill).toContain('Investigate and fix now');
expect(shipSkill).toContain('Add as P0 TODO');
expect(shipSkill).toContain('Skip');
});
test('collaborative mode offers blame + assign option', () => {
expect(shipSkill).toContain('Blame + assign GitHub issue');
expect(shipSkill).toContain('gh issue create');
});
test('defaults ambiguous failures to in-branch (safety)', () => {
expect(shipSkill).toContain('When ambiguous, default to in-branch');
});
});
// --- {{PLAN_FILE_REVIEW_REPORT}} resolver tests ---
describe('PLAN_FILE_REVIEW_REPORT resolver', () => {
@@ -652,11 +796,11 @@ describe('Codex generation (--host codex)', () => {
test('Codex review step stripped from Codex-host ship and review', () => {
const shipContent = fs.readFileSync(path.join(AGENTS_DIR, '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(AGENTS_DIR, 'gstack-review', 'SKILL.md'), 'utf-8');
expect(reviewContent).not.toContain('codex review --base');
expect(reviewContent).not.toContain('Investigate and fix');
expect(reviewContent).not.toContain('CODEX_REVIEWS');
});
test('--host codex --dry-run freshness', () => {
+187
View File
@@ -0,0 +1,187 @@
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";
import { spawnSync } from "child_process";
// Import normalizeRemoteUrl for unit testing
// We test the script end-to-end via CLI and normalizeRemoteUrl via import
const scriptPath = join(import.meta.dir, "..", "bin", "gstack-global-discover.ts");
describe("gstack-global-discover", () => {
describe("normalizeRemoteUrl", () => {
// Dynamically import to test the exported function
let normalizeRemoteUrl: (url: string) => string;
beforeEach(async () => {
const mod = await import("../bin/gstack-global-discover.ts");
normalizeRemoteUrl = mod.normalizeRemoteUrl;
});
test("strips .git suffix", () => {
expect(normalizeRemoteUrl("https://github.com/user/repo.git")).toBe(
"https://github.com/user/repo"
);
});
test("converts SSH to HTTPS", () => {
expect(normalizeRemoteUrl("git@github.com:user/repo.git")).toBe(
"https://github.com/user/repo"
);
});
test("converts SSH without .git to HTTPS", () => {
expect(normalizeRemoteUrl("git@github.com:user/repo")).toBe(
"https://github.com/user/repo"
);
});
test("lowercases host", () => {
expect(normalizeRemoteUrl("https://GitHub.COM/user/repo")).toBe(
"https://github.com/user/repo"
);
});
test("SSH and HTTPS for same repo normalize to same URL", () => {
const ssh = normalizeRemoteUrl("git@github.com:garrytan/gstack.git");
const https = normalizeRemoteUrl("https://github.com/garrytan/gstack.git");
const httpsNoDotGit = normalizeRemoteUrl("https://github.com/garrytan/gstack");
expect(ssh).toBe(https);
expect(https).toBe(httpsNoDotGit);
});
test("handles local: URLs consistently", () => {
const result = normalizeRemoteUrl("local:/tmp/my-repo");
// local: gets parsed as a URL scheme — the important thing is consistency
expect(result).toContain("/tmp/my-repo");
});
test("handles GitLab SSH URLs", () => {
expect(normalizeRemoteUrl("git@gitlab.com:org/project.git")).toBe(
"https://gitlab.com/org/project"
);
});
});
describe("CLI", () => {
test("--help exits 0 and prints usage", () => {
const result = spawnSync("bun", ["run", scriptPath, "--help"], {
encoding: "utf-8",
timeout: 10000,
});
expect(result.status).toBe(0);
expect(result.stderr).toContain("--since");
});
test("no args exits 1 with error", () => {
const result = spawnSync("bun", ["run", scriptPath], {
encoding: "utf-8",
timeout: 10000,
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("--since is required");
});
test("invalid window format exits 1", () => {
const result = spawnSync("bun", ["run", scriptPath, "--since", "abc"], {
encoding: "utf-8",
timeout: 10000,
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("Invalid window format");
});
test("--since 7d produces valid JSON", () => {
const result = spawnSync(
"bun",
["run", scriptPath, "--since", "7d", "--format", "json"],
{ encoding: "utf-8", timeout: 30000 }
);
expect(result.status).toBe(0);
const json = JSON.parse(result.stdout);
expect(json).toHaveProperty("window", "7d");
expect(json).toHaveProperty("repos");
expect(json).toHaveProperty("total_sessions");
expect(json).toHaveProperty("total_repos");
expect(json).toHaveProperty("tools");
expect(Array.isArray(json.repos)).toBe(true);
});
test("--since 7d --format summary produces readable output", () => {
const result = spawnSync(
"bun",
["run", scriptPath, "--since", "7d", "--format", "summary"],
{ encoding: "utf-8", timeout: 30000 }
);
expect(result.status).toBe(0);
expect(result.stdout).toContain("Window: 7d");
expect(result.stdout).toContain("Sessions:");
expect(result.stdout).toContain("Repos:");
});
test("--since 1h returns results (may be empty)", () => {
const result = spawnSync(
"bun",
["run", scriptPath, "--since", "1h", "--format", "json"],
{ encoding: "utf-8", timeout: 30000 }
);
expect(result.status).toBe(0);
const json = JSON.parse(result.stdout);
expect(json.total_sessions).toBeGreaterThanOrEqual(0);
});
});
describe("discovery output structure", () => {
test("repos have required fields", () => {
const result = spawnSync(
"bun",
["run", scriptPath, "--since", "30d", "--format", "json"],
{ encoding: "utf-8", timeout: 30000 }
);
expect(result.status).toBe(0);
const json = JSON.parse(result.stdout);
for (const repo of json.repos) {
expect(repo).toHaveProperty("name");
expect(repo).toHaveProperty("remote");
expect(repo).toHaveProperty("paths");
expect(repo).toHaveProperty("sessions");
expect(Array.isArray(repo.paths)).toBe(true);
expect(repo.paths.length).toBeGreaterThan(0);
expect(repo.sessions).toHaveProperty("claude_code");
expect(repo.sessions).toHaveProperty("codex");
expect(repo.sessions).toHaveProperty("gemini");
}
});
test("tools summary matches repo data", () => {
const result = spawnSync(
"bun",
["run", scriptPath, "--since", "30d", "--format", "json"],
{ encoding: "utf-8", timeout: 30000 }
);
const json = JSON.parse(result.stdout);
// Total sessions should equal sum across tools
const toolTotal =
json.tools.claude_code.total_sessions +
json.tools.codex.total_sessions +
json.tools.gemini.total_sessions;
expect(json.total_sessions).toBe(toolTotal);
});
test("deduplicates Conductor workspaces by remote", () => {
const result = spawnSync(
"bun",
["run", scriptPath, "--since", "30d", "--format", "json"],
{ encoding: "utf-8", timeout: 30000 }
);
const json = JSON.parse(result.stdout);
// Check that no two repos share the same normalized remote
const remotes = json.repos.map((r: any) => r.remote);
const uniqueRemotes = new Set(remotes);
expect(remotes.length).toBe(uniqueRemotes.size);
});
});
});
+9 -3
View File
@@ -70,7 +70,7 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
'plan-eng-review-artifact': ['plan-eng-review/**'],
// Ship
'ship-base-branch': ['ship/**'],
'ship-base-branch': ['ship/**', 'bin/gstack-repo-mode'],
'ship-local-workflow': ['ship/**', 'scripts/gen-skill-docs.ts'],
// Setup browser cookies
@@ -80,6 +80,9 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
'retro': ['retro/**'],
'retro-base-branch': ['retro/**'],
// Global discover
'global-discover': ['bin/gstack-global-discover.ts', 'test/global-discover.test.ts'],
// Document-release
'document-release': ['document-release/**'],
@@ -95,8 +98,11 @@ export const E2E_TOUCHFILES: Record<string, string[]> = {
'gemini-review-findings': ['review/**', '.agents/skills/gstack-review/**', 'test/helpers/gemini-session-runner.ts'],
// Ship coverage audit
'ship-coverage-audit': ['ship/**'],
// Coverage audit (shared fixture) + triage
'ship-coverage-audit': ['ship/**', 'test/fixtures/coverage-audit-fixture.ts', 'bin/gstack-repo-mode'],
'review-coverage-audit': ['review/**', 'test/fixtures/coverage-audit-fixture.ts'],
'plan-eng-coverage-audit': ['plan-eng-review/**', 'test/fixtures/coverage-audit-fixture.ts'],
'ship-triage': ['ship/**', 'bin/gstack-repo-mode'],
// Design
'design-consultation-core': ['design-consultation/**'],
File diff suppressed because it is too large Load Diff
+70 -2
View File
@@ -241,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) {
@@ -557,6 +558,7 @@ describe('v0.4.1 preamble features', () => {
'benchmark/SKILL.md',
'land-and-deploy/SKILL.md',
'setup-deploy/SKILL.md',
'cso/SKILL.md',
];
for (const skill of skillsWithPreamble) {
@@ -835,7 +837,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`, () => {
@@ -993,6 +995,15 @@ describe('gstack-slug', () => {
expect(lines[0]).toMatch(/^SLUG=.+/);
expect(lines[1]).toMatch(/^BRANCH=.+/);
});
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 Bootstrap validation ---
@@ -1319,10 +1330,12 @@ describe('Codex skill', () => {
test('codex-host ship/review do NOT contain adversarial review step', () => {
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');
});
@@ -1450,3 +1463,58 @@ 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('generated SKILL.md contains See Something Say Something section', () => {
const content = fs.readFileSync(path.join(ROOT, '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');
});
});