Skip to content
Merged
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
48 changes: 28 additions & 20 deletions .github/workflows/pr-standards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,20 @@ jobs:

// Check if author is a team member or bot
if (login === 'opencode-agent[bot]') return;
const { data: file } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/TEAM_MEMBERS',
ref: 'main'
});
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
if (members.includes(login)) {
console.log(`Skipping: ${login} is a team member`);
return;
try {
const { data: file } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/TEAM_MEMBERS',
ref: 'main'
});
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
if (members.includes(login)) {
console.log(`Skipping: ${login} is a team member`);
return;
}
} catch (e) {
console.log('TEAM_MEMBERS file not found, skipping team member check');
}

const title = pr.title;
Expand Down Expand Up @@ -175,16 +179,20 @@ jobs:

// Check if author is a team member or bot
if (login === 'opencode-agent[bot]') return;
const { data: file } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/TEAM_MEMBERS',
ref: 'main'
});
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
if (members.includes(login)) {
console.log(`Skipping: ${login} is a team member`);
return;
try {
const { data: file } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/TEAM_MEMBERS',
ref: 'main'
});
const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean);
if (members.includes(login)) {
console.log(`Skipping: ${login} is a team member`);
return;
}
} catch (e) {
console.log('TEAM_MEMBERS file not found, skipping team member check');
}

const body = pr.body || '';
Expand Down
19 changes: 18 additions & 1 deletion docs/docs/configure/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

## Built-in Commands

altimate ships with three built-in slash commands:
altimate ships with four built-in slash commands:

| Command | Description |
|---------|-------------|
| `/init` | Create or update an AGENTS.md file with build commands and code style guidelines. |
| `/discover` | Scan your data stack and set up warehouse connections. Detects dbt projects, warehouse connections from profiles/Docker/env vars, installed tools, and config files. Walks you through adding and testing new connections, then indexes schemas. |
| `/review` | Review changes — accepts `commit`, `branch`, or `pr` as an argument (defaults to uncommitted changes). |
| `/feedback` | Submit product feedback as a GitHub issue. Guides you through title, category, description, and optional session context. |

### `/discover`

Expand All @@ -30,6 +31,22 @@ The recommended way to set up a new data engineering project. Run `/discover` in
/review pr # review the current pull request
```

### `/feedback`

Submit product feedback directly from the CLI. The agent walks you through:

1. **Title** — a short summary of your feedback
2. **Category** — bug, feature, improvement, or ux
3. **Description** — detailed explanation
4. **Session context** (opt-in) — includes working directory name and session ID for debugging

```
/feedback # start the guided feedback flow
/feedback dark mode support # pre-fill the description
```

Requires the `gh` CLI to be installed and authenticated (`gh auth login`).

## Custom Commands

Custom commands let you define reusable slash commands.
Expand Down
138 changes: 138 additions & 0 deletions packages/opencode/src/altimate/tools/feedback-submit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import z from "zod"
// Use Bun.$ (namespace access) instead of destructured $ to support test mocking
import Bun from "bun"
import os from "os"
import path from "path"
import { Tool } from "../../tool/tool"
import { Installation } from "@/installation"

const CATEGORY_LABELS = {
bug: "bug",
feature: "enhancement",
improvement: "improvement",
ux: "ux",
} satisfies Record<"bug" | "feature" | "improvement" | "ux", string>

export const FeedbackSubmitTool = Tool.define("feedback_submit", {
description:
"Submit user feedback as a GitHub issue to the altimate-code repository. " +
"Creates an issue with appropriate labels and metadata. " +
"Requires the `gh` CLI to be installed and authenticated.",
parameters: z.object({
title: z.string().trim().min(1).describe("A concise title for the feedback issue"),
category: z
.enum(["bug", "feature", "improvement", "ux"])
.describe("The category of feedback: bug, feature, improvement, or ux"),
description: z.string().trim().min(1).describe("Detailed description of the feedback"),
include_context: z
.boolean()
.optional()
.default(false)
.describe("Whether to include session context (working directory basename, platform info) in the issue body"),
}),
async execute(args, ctx) {
const ghNotInstalled = {
title: "Feedback submission failed",
metadata: { error: "gh_not_installed", issueUrl: "" },
output:
"The `gh` CLI is not installed. Please install it to submit feedback:\n" +
" - macOS: `brew install gh`\n" +
" - Linux: https://github.com/cli/cli/blob/trunk/docs/install_linux.md\n" +
" - Windows: `winget install GitHub.cli`\n\n" +
"Then authenticate with: `gh auth login`",
}

// Check if gh CLI is available
let ghVersion: string
try {
ghVersion = await Bun.$`gh --version`.quiet().nothrow().text()
} catch {
// ENOENT — gh binary not found on PATH
return ghNotInstalled
}
if (!ghVersion.trim().startsWith("gh version")) {
return ghNotInstalled
}

// Check if authenticated
let authStatus: { exitCode: number }
try {
authStatus = await Bun.$`gh auth status`.quiet().nothrow()
} catch {
return {
title: "Feedback submission failed",
metadata: { error: "gh_auth_check_failed", issueUrl: "" },
output:
"Failed to verify `gh` authentication status. Please check your installation with:\n" +
" `gh auth status`",
}
}
if (authStatus.exitCode !== 0) {
return {
title: "Feedback submission failed",
metadata: { error: "gh_not_authenticated", issueUrl: "" },
output:
"The `gh` CLI is not authenticated. Please run:\n" +
" `gh auth login`\n\n" +
"Then try submitting feedback again.",
}
}

// Collect metadata
const version = Installation.VERSION
const platform = process.platform
const arch = process.arch
const osRelease = os.release()

// Build issue body
let body = `${args.description}\n\n`
body += `---\n\n`
body += `### Metadata\n\n`
body += `| Field | Value |\n`
body += `|-------|-------|\n`
body += `| CLI Version | ${version} |\n`
body += `| Platform | ${platform} |\n`
body += `| Architecture | ${arch} |\n`
body += `| OS Release | ${osRelease} |\n`
body += `| Category | ${args.category} |\n`

if (args.include_context) {
const cwdBasename = path.basename(process.cwd()) || "unknown"
body += `| Working Directory | ${cwdBasename} |\n`
body += `| Session ID | ${ctx.sessionID} |\n`
}

// Build labels
const labels = ["user-feedback", "from-cli", CATEGORY_LABELS[args.category]]

// Create the issue
let issueResult: { stdout: Buffer; stderr: Buffer; exitCode: number }
try {
issueResult = await Bun.$`gh issue create --repo AltimateAI/altimate-code --title ${args.title} --body ${body} --label ${labels.join(",")}`.quiet().nothrow()
} catch {
return {
title: "Feedback submission failed",
metadata: { error: "issue_creation_failed", issueUrl: "" },
output: "Failed to create GitHub issue. The `gh` CLI encountered an unexpected error.\n\nPlease check your gh CLI installation and try again.",
}
}

const stdout = issueResult.stdout.toString().trim()
const stderr = issueResult.stderr.toString().trim()

if (issueResult.exitCode !== 0 || !stdout || !stdout.includes("github.com")) {
const errorDetail = stderr || stdout || "No output from gh CLI"
return {
title: "Feedback submission failed",
metadata: { error: "issue_creation_failed", issueUrl: "" },
output: `Failed to create GitHub issue.\n\n${errorDetail}\n\nPlease check your gh CLI authentication and try again.`,
}
}

return {
title: "Feedback submitted",
metadata: { error: "", issueUrl: stdout },
output: `Feedback submitted successfully!\n\nIssue URL: ${stdout}`,
}
},
})
11 changes: 11 additions & 0 deletions packages/opencode/src/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Identifier } from "../id/id"
import PROMPT_INITIALIZE from "./template/initialize.txt"
import PROMPT_DISCOVER from "./template/discover.txt"
import PROMPT_REVIEW from "./template/review.txt"
import PROMPT_FEEDBACK from "./template/feedback.txt"
import { MCP } from "../mcp"
import { Skill } from "../skill"
import { Log } from "../util/log"
Expand Down Expand Up @@ -57,6 +58,7 @@ export namespace Command {
INIT: "init",
DISCOVER: "discover",
REVIEW: "review",
FEEDBACK: "feedback",
} as const

const state = Instance.state(async () => {
Expand Down Expand Up @@ -91,6 +93,15 @@ export namespace Command {
subtask: true,
hints: hints(PROMPT_REVIEW),
},
[Default.FEEDBACK]: {
name: Default.FEEDBACK,
description: "submit product feedback as a GitHub issue",
source: "command",
get template() {
return PROMPT_FEEDBACK
},
hints: hints(PROMPT_FEEDBACK),
},
}

for (const [name, command] of Object.entries(cfg.command ?? {})) {
Expand Down
42 changes: 42 additions & 0 deletions packages/opencode/src/command/template/feedback.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
You are helping the user submit product feedback for altimate-code. Feedback is filed as a GitHub issue.

If $ARGUMENTS is provided, use it as the initial description and skip asking for a description. Still confirm the title and category before submitting.

Step 1 — Collect feedback details:

Ask the user for the following information. Collect each piece one at a time:

1. **Title**: A short summary of the feedback (one line).
2. **Category**: Ask the user to pick one:
- bug — Something is broken or not working as expected
- feature — A new capability or feature request
- improvement — An enhancement to existing functionality
- ux — Feedback on usability, flow, or developer experience
3. **Description**: A detailed explanation of the feedback. If $ARGUMENTS was provided, present it back and ask if they want to add anything or if it looks good.

Step 2 — Session context (opt-in):

Ask the user if they want to include session context with their feedback. Explain what this includes:
- Working directory name (basename only, not the full path)
- Session ID (for debugging correlation)
- No code, credentials, or personal data is included

If they opt in, set `include_context` to true when submitting.

Step 3 — Confirm and submit:

Show a summary of the feedback before submitting:
- **Title**: ...
- **Category**: ...
- **Description**: ...
- **Session context**: included / not included

Ask the user to confirm. If they confirm, call the `feedback_submit` tool with:
- `title`: the feedback title
- `category`: the selected category
- `description`: the full description
- `include_context`: true or false

Step 4 — Show result:

After submission, display the created GitHub issue URL to the user so they can track it. Thank them for the feedback.
2 changes: 2 additions & 0 deletions packages/opencode/src/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import { AltimateCoreParseDbtTool } from "../altimate/tools/altimate-core-parse-
import { AltimateCoreIsSafeTool } from "../altimate/tools/altimate-core-is-safe"
import { ProjectScanTool } from "../altimate/tools/project-scan"
import { DatamateManagerTool } from "../altimate/tools/datamate"
import { FeedbackSubmitTool } from "../altimate/tools/feedback-submit"
// altimate_change end

export namespace ToolRegistry {
Expand Down Expand Up @@ -263,6 +264,7 @@ export namespace ToolRegistry {
AltimateCoreIsSafeTool,
ProjectScanTool,
DatamateManagerTool,
FeedbackSubmitTool,
// altimate_change end
...custom,
]
Expand Down
7 changes: 4 additions & 3 deletions packages/opencode/test/bridge/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,10 @@ describe("Bridge.start integration", () => {
test("ensureEngine is called when bridge starts", async () => {
const { Bridge } = await import("../../src/altimate/bridge/client")

// /bin/echo exists and will spawn successfully but won't respond to
// the JSON-RPC ping, so start() will eventually fail on verification.
process.env.OPENCODE_PYTHON = "/bin/echo"
// process.execPath (the current Bun/Node binary) exists on all platforms.
// When spawned as a Python replacement it exits quickly without speaking
// JSON-RPC, so start() fails on the ping verification as expected.
process.env.OPENCODE_PYTHON = process.execPath

try {
await Bridge.call("ping", {} as any)
Expand Down
Loading
Loading