Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 169 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,42 @@ Skills live in `.agents/skills/`. Invoke them by name (e.g., `/office-hours`).
| `/unfreeze` | Remove directory edit restrictions. |
| `/gstack-upgrade` | Update gstack to the latest version. |

## OpenCode Integration

gstack now includes built-in support for OpenCode, allowing you to use gstack skills within the OpenCode interface.

### How to Use gstack Skills in OpenCode

1. **Access via Command Dialog**: Press `Ctrl+K` in OpenCode to open the command dialog
2. **Find gstack Commands**: Type `gstack:` to see all available gstack skills
3. **Select a Skill**: Choose from the list (e.g., `gstack:office-hours`, `gstack:plan-ceo-review`)
4. **Provide Input**: Optionally provide arguments when prompted
5. **See Results**: Watch the skill execute and return results to your OpenCode session
6. **Continue Working**: Refine results through natural conversation in OpenCode

### Available gstack Commands in OpenCode

- `gstack:office-hours` - Start here. Reframes your product idea before you write code.
- `gstack:plan-ceo-review` - CEO-level review: find the 10-star product in the request.
- `gstack:plan-eng-review` - Lock architecture, data flow, edge cases, and tests.
- `gstack:design-consultation` - Build a complete design system from scratch.
- `gstack:review` - Pre-landing PR review. Finds bugs that pass CI but break in prod.
- `gstack:debug` - Systematic root-cause debugging. No fixes without investigation.
- `gstack:ship` - Run tests, review, push, open PR. One command.
- `gstack:qa` - Open a real browser, find bugs, fix them, re-verify.

### Technical Details

These commands leverage OpenCode's custom command system which:
- Supports named arguments (`$ARGUMENT_NAME`)
- Can invoke any available tool (Read, Write, Bash, etc.)
- Integrates with OpenCode's agent system
- Provides rich descriptions and usage hints in the command dialog

The gstack skills are implemented as OpenCode command files located in:
- `opencode/commands/gstack/` (project-specific)
- Or copied to `~/.config/opencode/commands/gstack/` (user-wide) during setup

## Build commands

```bash
Expand All @@ -41,9 +77,140 @@ bun run gen:skill-docs # regenerate SKILL.md files from templates
bun run skill:check # health dashboard for all skills
```

## Key conventions
### Test commands

```bash
# Run all tests (excluding slow E2E tests)
bun test

# Run specific test suites
bun run test:evals # LLM evaluation tests
bun run test:e2e # End-to-end tests
bun run test:codex # Codex-specific E2E tests
bun run test:gemini # Gemini-specific E2E tests
bun run test:audit # Audit compliance tests

# Run a single test file
bun test test/skill-parser.test.ts

# Run a single test function (if supported by test runner)
bun test test/skill-parser.test.ts -t "extracts \$B commands"
```

## Code style guidelines

### Language & formatting

- **Primary language**: TypeScript with ES modules (`"type": "module"` in package.json)
- **Formatter**: No explicit formatter configured; follow existing code patterns
- **Line length**: Aim for 80-100 characters; use judgment for readability
- **Indentation**: 2 spaces (not tabs)
- **Semicolons**: Required (follow existing code)
- **Quotes**: Single quotes for strings, double quotes only when needed (e.g., JSX attributes)
- **File naming**: `.ts` for TypeScript files, `.test.ts` for test files
- **Directory organization**: Feature-based grouping (browse/, design/, scripts/, etc.)

### Imports

- **Order**: Built-in modules → external packages → internal modules
- **Syntax**:
- Named imports: `import { fs } from 'fs';`
- Default imports: `import fs from 'fs';` (when appropriate)
- Namespace imports: `import * as fs from 'fs';`
- **Path aliases**: Use relative paths (`./helpers/util`) or absolute from project root
- **Bun-specific**: Use `bun:test` for testing imports: `import { describe, test, expect } from 'bun:test';`

### Types

- **Type definitions**: Prefer interfaces over types for object shapes
- **Explicit typing**:
- Function parameters and return values should be typed
- Avoid `any`; use `unknown` when type is truly unknown
- Use generics for reusable components
- **Nullable types**: Explicitly mark with `| null` or use strict null checks
- **Type inference**: Trust TypeScript inference for simple cases

### Naming conventions

- **Variables & functions**: camelCase (e.g., `fetchUserData`)
- **Classes & types**: PascalCase (e.g., `BrowserManager`)
- **Constants**: UPPER_SNAKE_CASE (e.g., `MAX_START_WAIT`)
- **Files**: kebab-case (e.g., `skill-parser.test.ts`)
- **Private members**: Prefix with underscore only if truly internal (`_internalMethod`)
- **Boolean variables**: Use is/has/can prefixes (e.g., `isEnabled`, `hasError`)

### Error handling

- **Synchronous code**: Use try/catch for recoverable errors
- **Asynchronous code**:
- Prefer try/catch with async/await
- For promises: `.then(result => ...).catch(error => handleError(error))`
- **Validation**: Validate inputs early; throw descriptive errors
- **Logging**: Use console.error for unexpected conditions; avoid console.log in libraries
- **User-facing errors**: Provide clear, actionable messages
- **Error types**: Consider creating custom error classes for domain-specific errors

### Documentation

- **JSDoc**: Use for all public APIs and complex functions
- **File headers**: Include purpose and flow description (see existing files)
- **Complex logic**: Add inline comments explaining why, not what
- **TODO comments**: Use `// TODO:` for tracking future work
- **Magic numbers**: Replace with named constants with explanations

### Testing patterns

- **Test files**: Name as `[feature].test.ts` alongside implementation or in `test/` directory
- **Test structure**:
- `describe()` for test suites
- `test()` for individual test cases
- `beforeAll()/afterAll()` for suite setup/teardown
- `beforeEach()/afterEach()` for test isolation
- **Assertions**: Use `expect()` from `bun:test`
- **Mocking**:
- Manual mocks for simple cases
- Temporary directories for file system tests (`os.tmpdir()`)
- Child process testing with `spawnSync` for CLI commands
- **E2E tests**:
- Mark with `.e2e.test.ts` suffix
- Use test servers for HTTP testing
- Clean up resources in `afterAll()`

### Specific patterns in this codebase

- **Configuration**: Use `resolveConfig()` pattern for loading settings
- **Process detection**: Check `process.platform` for OS-specific behavior
- **Constants**: Define timeouts, limits, and magic values as constants at top of file
- **HTTP servers**: Use consistent patterns for starting/stopping test servers
- **File operations**: Always check existence before reading/writing; use synchronous versions in CLI scripts for simplicity
- **CLI args**: Parse with `process.argv.slice(2)` or use parsing libraries for complex interfaces
- **Environment**: Use `process.env` for configuration; provide defaults and validation

## Safety guidelines

- **Destructive operations**: Always confirm before running commands like `rm -rf`, `DROP TABLE`, or force pushes
- **File modifications**: Prefer editing existing files over creating new ones unless explicitly required
- **Branch protection**: Never force push to main/master branches
- **Secret handling**: Never log or commit secrets, keys, or credentials
- **Testing**: Run relevant tests before considering work complete
- **Build verification**: Ensure `bun run build` succeeds after changes

## Agent-specific instructions

When operating as an agent in this repository:

1. **Start with understanding**: Read related files before making changes
2. **Follow existing patterns**: Match the coding style of the file you're editing
3. **Test thoroughly**: Run relevant unit tests and verify manually when appropriate
4. **Document changes**: Update comments and JSDoc when modifying behavior
5. **Consider edge cases**: Think about error conditions and input validation
6. **Keep changes focused**: Make minimal, purposeful changes
7. **Verify build**: Ensure `bun run build` still works after your changes
8. **Respect conventions**: Follow the established patterns for imports, naming, and error handling

## Documentation

- SKILL.md files are **generated** from `.tmpl` templates. Edit the template, not the output.
- Run `bun run gen:skill-docs --host codex` to regenerate Codex-specific output.
- The browse binary provides headless browser access. Use `$B <command>` in skills.
- Safety skills (careful, freeze, guard) use inline advisory prose — always confirm before destructive operations.
- Safety skills (careful, freeze, guard) use inline advisory prose — always confirm before destructive operations.
98 changes: 98 additions & 0 deletions bin/gstack-opencode-setup
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env node

/**
* gstack-opencode-setup - Installs gstack OpenCode integration globally
*
* This script copies the gstack OpenCode command files to the user's OpenCode
* commands directory so they're available in any OpenCode instance.
*/

import fs from 'fs';
import path from 'path';

function log(message) {
console.log(`[gstack-opencode-setup] ${message}`);
}

function error(message) {
console.error(`[gstack-opencode-setup] ERROR: ${message}`);
process.exit(1);
}

try {
// Get the directory where this script is located
const scriptDir = path.dirname(process.argv[1]);
const gstackRoot = path.resolve(scriptDir, '..');

// Source directory: gstack's opencode commands
const sourceDir = path.join(gstackRoot, 'opencode', 'commands', 'gstack');

// Determine OpenCode commands directory
let opencodeCommandsDir;

if (process.platform === 'win32') {
// Windows: %USERPROFILE%\.opencode\commands
const userProfile = process.env.USERPROFILE;
if (!userProfile) {
error('USERPROFILE environment variable not found');
}
opencodeCommandsDir = path.join(userProfile, '.opencode', 'commands', 'gstack');
} else {
// Linux/macOS: ~/.opencode/commands or $XDG_CONFIG_HOME/opencode/commands
const xdgConfigHome = process.env.XDG_CONFIG_HOME ||
path.join(process.env.HOME, '.config');
opencodeCommandsDir = path.join(xdgConfigHome, 'opencode', 'commands', 'gstack');
}

log(`Source directory: ${sourceDir}`);
log(`Target directory: ${opencodeCommandsDir}`);

// Check if source directory exists
if (!fs.existsSync(sourceDir)) {
error(`Source directory not found: ${sourceDir}`);
}

// Create target directory if it doesn't exist
if (!fs.existsSync(opencodeCommandsDir)) {
log(`Creating directory: ${opencodeCommandsDir}`);
fs.mkdirSync(opencodeCommandsDir, { recursive: true });
}

// Copy all .md files from source to target
const files = fs.readdirSync(sourceDir);
let copiedCount = 0;

for (const file of files) {
if (path.extname(file) === '.md') {
const sourceFile = path.join(sourceDir, file);
const targetFile = path.join(opencodeCommandsDir, file);

fs.copyFileSync(sourceFile, targetFile);
log(`Copied: ${file}`);
copiedCount++;
}
}

// Also copy the README if it exists
const readmeSource = path.join(sourceDir, '..', 'README.md');
if (fs.existsSync(readmeSource)) {
const readmeTarget = path.join(opencodeCommandsDir, 'README.md');
fs.copyFileSync(readmeSource, readmeTarget);
log('Copied: README.md');
copiedCount++;
}

log(`Successfully installed ${copiedCount} gstack OpenCode commands to ${opencodeCommandsDir}`);
log('');
log('To use gstack skills in OpenCode:');
log('1. Open OpenCode');
log('2. Press Ctrl+K to open the command dialog');
log('3. Type "gstack:" to see all available gstack commands');
log('4. Select a command (e.g., "gstack:office-hours")');
log('5. Optionally provide input when prompted');
log('');
log('Note: These commands will be available in ALL OpenCode instances.');

} catch (err) {
error(`Failed to install gstack OpenCode integration: ${err.message}`);
}
1 change: 1 addition & 0 deletions opencode
Submodule opencode added at 73ee49
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"license": "MIT",
"type": "module",
"bin": {
"browse": "./browse/dist/browse"
"browse": "./browse/dist/browse",
"gstack-opencode-setup": "./bin/gstack-opencode-setup"
},
"scripts": {
"build": "bun run gen:skill-docs; bun run gen:skill-docs --host codex; bun build --compile browse/src/cli.ts --outfile browse/dist/browse && bun build --compile browse/src/find-browse.ts --outfile browse/dist/find-browse && bun build --compile design/src/cli.ts --outfile design/dist/design && bun build --compile bin/gstack-global-discover.ts --outfile bin/gstack-global-discover && bash browse/scripts/build-node-server.sh && git rev-parse HEAD > browse/dist/.version && git rev-parse HEAD > design/dist/.version && rm -f .*.bun-build || true",
Expand Down Expand Up @@ -56,4 +57,4 @@
"devDependencies": {
"@anthropic-ai/sdk": "^0.78.0"
}
}
}
19 changes: 19 additions & 0 deletions packages/adapters/opencode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@paperclipai/adapter-opencode",
"version": "0.0.1",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts",
"./server": "./src/server/index.ts",
"./ui": "./src/ui/index.ts",
"./cli": "./src/cli/index.ts"
},
"dependencies": {
"@paperclipai/adapter-utils": "workspace:*",
"picocolors": "^1.1.1"
},
"devDependencies": {
"typescript": "^5.7.3"
}
}
34 changes: 34 additions & 0 deletions packages/adapters/opencode/src/cli/format-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import pc from 'picocolors';

/**
* Format a line of stdout from the OpenCode process for display in the terminal.
* This is used when running `paperclipai run --watch`.
*
* @param line - The line of stdout from the OpenCode process
* @param debug - Whether to enable debug output (unrecognized lines are shown in gray)
*/
export function formatOpenCodeStdoutEvent(line: string, debug: boolean): void {
// In this simple implementation, we just print the line as-is.
// We could try to parse the line to see if it's a known OpenCode output format,
// but for now we'll treat all lines as regular output.
console.log(line);

// If we wanted to do more sophisticated formatting, we could do something like:
// if (debug) {
// // In debug mode, we might want to show all lines, even if we don't understand them
// console.log(pc.gray(line));
// } else {
// // In non-debug mode, we might want to filter or style known lines
// // For example, if we knew that lines starting with "[INFO]" are info messages:
// if (line.startsWith('[INFO]')) {
// console.log(pc.blue(line));
// } else if (line.startsWith('[ERROR]')) {
// console.log(pc.red(line));
// } else {
// console.log(line);
// }
// }
}

// Note: The CLI adapter interface expects a function named `formatStdoutEvent`.
// We'll export it with that name in the index.ts file.
1 change: 1 addition & 0 deletions packages/adapters/opencode/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { formatOpenCodeStdoutEvent as formatStdoutEvent } from './format-event';
28 changes: 28 additions & 0 deletions packages/adapters/opencode/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const type = "opencode";
export const label = "OpenCode (local)";

export const models = [
{ id: "opencode", label: "OpenCode" },
];

export const agentConfigurationDoc = `# OpenCode agent configuration

Adapter: opencode

Use when:
- The agent needs to run OpenCode CLI locally on the host machine
- You want to use OpenCode's interactive TUI or non-interactive mode
- The task requires OpenCode-specific features (e.g. multiple AI providers, session management)

Don't use when:
- You need a simple one-shot script execution (use the "process" adapter instead)
- OpenCode CLI is not installed on the host
- You need to use a different agent runtime (e.g. Claude Code, Codex)

Core fields:
- cwd (string, required): absolute working directory for the OpenCode process
- model (string, optional): OpenCode model to use (default: claude-3.5-sonnet)
- timeoutSec (number, optional): timeout for each OpenCode invocation in seconds (default: 120)
- graceSec (number, optional): grace period for OpenCode to shut down after timeout (default: 15)
- sessionHistoryLimit (number, optional): maximum number of conversation turns to keep in history (default: 10)
`;
Loading