gv-ai 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ Error: Oh My Zsh can't be loaded from: . You need to run zsh instead.
2
+ Here's the process tree:
3
+
4
+ PPID PID COMMAND
5
+ 1 520 /usr/sbin/cron
6
+ 520 60485 /usr/sbin/cron
7
+ 60485 60486 /bin/sh -c /Users/souvikmahapatra/projects/gv-ai/.gv-ai/scripts/orchestrator.sh >> /Users/souvikmahapatra/projects/gv-ai/.gv-ai/cron.log 2>&1
8
+ 60486 60487 /bin/bash /Users/souvikmahapatra/projects/gv-ai/.gv-ai/scripts/orchestrator.sh
9
+
10
+ /Users/souvikmahapatra/.zshrc: line 107: pyenv: command not found
11
+ /Users/souvikmahapatra/.zshrc: line 112: pyenv: command not found
12
+ /Users/souvikmahapatra/.zshrc: line 113: pyenv: command not found
13
+ /Users/souvikmahapatra/.zshrc: line 123: autoload: command not found
14
+ /Users/souvikmahapatra/.bun/_bun: line 922: syntax error near unexpected token `('
15
+ /Users/souvikmahapatra/.bun/_bun: line 922: ` local -a packages_full_path=(${global_node_modules}/*(N))'
16
+ /Users/souvikmahapatra/.bun/_bun: line 924: _alternative: command not found
17
+ /Users/souvikmahapatra/.bun/_bun: line 925: syntax error near unexpected token `}'
18
+ /Users/souvikmahapatra/.bun/_bun: line 925: `}'
19
+ /Users/souvikmahapatra/projects/gv-ai/.gv-ai/scripts/orchestrator.sh: line 35: export: ` SENTRY_PROJECTS: tal ai prod tal ai stag=': not a valid identifier
20
+ /Users/souvikmahapatra/projects/gv-ai/.gv-ai/scripts/orchestrator.sh: line 35: export: ` TELEGRAM_BOT_TOKEN: 7895808491:AAHWA31cob6g0oq1YPdLc2KjCXgFX4y6by0=': not a valid identifier
21
+ /Users/souvikmahapatra/projects/gv-ai/.gv-ai/scripts/orchestrator.sh: line 35: export: ` TELEGRAM_CHAT_ID: 1827747473=': not a valid identifier
22
+ 🚀 Starting Sentry Resolver Run: 20260118-075502
23
+ 📂 Project Root: /Users/souvikmahapatra/projects/gv-ai
24
+ 🔍 Fetching issues...
25
+ ⚠️ No issues.json created. Check /Users/souvikmahapatra/projects/gv-ai/.gv-ai/runs/20260118-075502/fetcher.log
@@ -0,0 +1,5 @@
1
+ {
2
+ "SENTRY_PROJECTS": "tal ai prod, tal ai stag",
3
+ "TELEGRAM_BOT_TOKEN": "7895808491:AAHWA31cob6g0oq1YPdLc2KjCXgFX4y6by0",
4
+ "TELEGRAM_CHAT_ID": "1827747473"
5
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "mcp": {
4
+ "sentry": {
5
+ "type": "remote",
6
+ "url": "https://mcp.sentry.dev/mcp",
7
+ "oauth": {}
8
+ },
9
+ "linear": {
10
+ "type": "local",
11
+ "command": [
12
+ "npx",
13
+ "-y",
14
+ "mcp-remote",
15
+ "https://mcp.linear.app/sse"
16
+ ],
17
+ "enabled": true
18
+ }
19
+ },
20
+ "agent": {
21
+ "gv-fetcher": {
22
+ "prompt": "{file:prompts/fetcher.md}",
23
+ "tools": {
24
+ "sentry_find_organizations": true,
25
+ "sentry_find_projects": true,
26
+ "sentry_search_issues": true,
27
+ "write": true,
28
+ "read": true
29
+ }
30
+ },
31
+ "gv-rca": {
32
+ "prompt": "{file:prompts/rca.md}",
33
+ "tools": {
34
+ "sentry_find_organizations": true,
35
+ "sentry_get_issue_details": true,
36
+ "sentry_analyze_issue_with_seer": true,
37
+ "read": true,
38
+ "notify": true
39
+ }
40
+ },
41
+ "gv-fixer": {
42
+ "prompt": "{file:prompts/fixer.md}",
43
+ "tools": {
44
+ "edit": true,
45
+ "bash": true,
46
+ "read": true,
47
+ "write": true,
48
+ "notify": true
49
+ }
50
+ }
51
+ },
52
+ "permission": {
53
+ "bash": "allow",
54
+ "edit": "allow",
55
+ "write": "allow",
56
+ "read": "allow"
57
+ }
58
+ }
@@ -0,0 +1,42 @@
1
+ tools:
2
+ bash: false
3
+ write: true
4
+ edit: false
5
+ sentry_find_organizations: true
6
+ sentry_find_projects: true
7
+ sentry_search_events: false
8
+ sentry_search_issues: true
9
+ ---
10
+
11
+ You are the **Issue Fetcher & Prioritizer Agent**.
12
+
13
+ ### TASK
14
+ Your goal is to identify the most critical Sentry issues for the requested projects and save them for processing.
15
+
16
+ ### INSTRUCTIONS
17
+ 1. **Identify Targets**:
18
+ * You have been provided a list of **Sentry Projects** in the prompt.
19
+ * Use your available Sentry tools to resolve the Organization and Project Slugs necessary to query them.
20
+ 2. **Query Sentry**:
21
+ * Search for unresolved issues (`is:unresolved level:error`) in these projects.
22
+ * Limit: 20 issues per project.
23
+ 3. **Prioritize**:
24
+ * Score issues (0-100) based on User Count, Event Frequency, and Age.
25
+ * Select the **Top 5** highest scoring issues.
26
+ 4. **Output**:
27
+ * Read `state.json` (if exists) and exclude IDs in `processed_ids`.
28
+ * Write the filtered list to `issues.json` in the current directory.
29
+
30
+ ### OUTPUT FORMAT (issues.json)
31
+ ```json
32
+ [
33
+ {
34
+ "id": "SENTRY-123",
35
+ "title": "Error Title",
36
+ "organization": "org-slug",
37
+ "project": "project-slug",
38
+ "priority_score": 95,
39
+ "reason": "High impact"
40
+ }
41
+ ]
42
+ ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ description: Implement code fixes and create Pull Requests
3
+ mode: primary
4
+ tools:
5
+ bash: true
6
+ write: true
7
+ edit: true
8
+ read: true
9
+ notify: true
10
+ ---
11
+
12
+ You are the **Fixer Agent**.
13
+
14
+ ### TASK
15
+ Implement the fix proposed in the RCA and submit a Pull Request.
16
+
17
+ ### INPUT
18
+ * Read `rca.md` in the current directory.
19
+ * The current directory is `.gv-ai/runs/<timestamp>/<issue_id>/`.
20
+
21
+ ### INSTRUCTIONS
22
+ 1. **Preparation**:
23
+ * Read `rca.md` to understand the plan.
24
+ * **Git**: Ensure you are on the base branch (usually `main` or `master`). Pull latest.
25
+ * **Branch**: Create a new branch: `fix/sentry-<issue_id>-<timestamp>`.
26
+ 2. **Implementation**:
27
+ * Use `edit` to modify the code.
28
+ * **Verify**: If possible, run a relevant test (or create a temporary test case) to ensure the fix works and doesn't break syntax.
29
+ * **Lint**: Run `npm run lint` (or equivalent) if available.
30
+ 3. **Commit & PR**:
31
+ * `git commit -am "fix(sentry): Resolve <issue_id> - <brief description>"`
32
+ * `git push -u origin <branch_name>`
33
+ * Create PR using `gh pr create`:
34
+ * Title: `fix: Resolve Sentry Issue <issue_id>`
35
+ * Body: Include summary from `rca.md`.
36
+ 4. **Notify**:
37
+ * Use `notify` tool: "✅ *Fix Implemented* for [Issue ID]\nPR: [PR URL]"
38
+ 5. **Artifact**:
39
+ * Write `fix_result.json` with the PR URL and status.
40
+
41
+ ### CONSTRAINTS
42
+ * Do NOT change unrelated code.
43
+ * If you cannot verify the fix (e.g., tests fail), abort and notify with a clear error message.
@@ -0,0 +1,47 @@
1
+ tools:
2
+ bash: true
3
+ write: true
4
+ edit: false
5
+ read: true
6
+ sentry_find_organizations: true
7
+ sentry_get_issue_details: true
8
+ sentry_get_issue_tag_values: true
9
+ sentry_analyze_issue_with_seer: true
10
+ notify: true
11
+ ---
12
+
13
+ You are the **RCA (Root Cause Analysis) Agent**.
14
+
15
+ ### TASK
16
+ Analyze the provided Sentry issue to determine the root cause and fixability.
17
+
18
+ ### INPUT
19
+ * **Issue ID** (e.g., `SENTRY-123`) provided in the prompt.
20
+ * **Context**: You are running in `.gv-ai/runs/<timestamp>/<issue_id>/`.
21
+
22
+ ### INSTRUCTIONS
23
+ 1. **Analyze Issue**:
24
+ * Use your tools (`sentry_get_issue_details`, `sentry_analyze_issue_with_seer`) to inspect the issue.
25
+ * If you need the Organization slug and it wasn't provided, use your tools to find it.
26
+ * **Verify Code**: Check the local codebase (`grep`/`read`) to confirm the file and line number from the stack trace exist.
27
+ 2. **Determine Root Cause**:
28
+ * Identify the logic error.
29
+ * Assess **Fixability** (High/Medium/Low).
30
+ 3. **Report**:
31
+ * Write `rca.md` with your findings.
32
+ 4. **Notify**:
33
+ * Send a Telegram summary using the `notify` tool.
34
+
35
+ ### OUTPUT FORMAT (rca.md)
36
+ ```markdown
37
+ # RCA: SENTRY-123
38
+ ## Root Cause
39
+ ...
40
+ ## Evidence
41
+ ...
42
+ ## Proposed Fix
43
+ ...
44
+ ## Assessment
45
+ - **Confidence**: High/Medium/Low
46
+ - **Action**: Fix / Ticket / Skip
47
+ ```
@@ -0,0 +1 @@
1
+ /Users/souvikmahapatra/projects/gv-ai/.gv-ai/scripts/orchestrator.sh: line 55: opencode: command not found
@@ -0,0 +1,115 @@
1
+ #!/bin/bash
2
+
3
+ # Ensure we have a path (Cron often has limited path)
4
+ export PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
5
+
6
+ # Try to source user profile for NVM/Node/etc
7
+ if [ -f "$HOME/.bashrc" ]; then source "$HOME/.bashrc"; fi
8
+ if [ -f "$HOME/.zshrc" ]; then source "$HOME/.zshrc"; fi
9
+ if [ -f "$HOME/.profile" ]; then source "$HOME/.profile"; fi
10
+
11
+ # Directory Setup
12
+ # Resolve the directory where this script resides (.gv-ai/scripts)
13
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
14
+ # Base dir is parent of scripts (.gv-ai)
15
+ BASE_DIR="$(dirname "$SCRIPT_DIR")"
16
+ # Project Root is parent of .gv-ai
17
+ PROJECT_ROOT="$(dirname "$BASE_DIR")"
18
+
19
+ RUN_ID=$(date +%Y%m%d-%H%M%S)
20
+ RUN_DIR="$BASE_DIR/runs/$RUN_ID"
21
+ STATE_FILE="$BASE_DIR/state.json"
22
+ ENV_FILE="$BASE_DIR/env.json"
23
+
24
+ mkdir -p "$RUN_DIR"
25
+
26
+ # Load Environment Variables from env.json
27
+ if [ -f "$ENV_FILE" ]; then
28
+ # Simple parser for flat JSON string/number values
29
+ while IFS='=' read -r key value; do
30
+ # Remove quotes and comma
31
+ key=$(echo "$key" | tr -d '",')
32
+ value=$(echo "$value" | tr -d '",')
33
+ # Export if not empty
34
+ if [[ ! -z "$key" && "$key" != "{" && "$key" != "}" ]]; then
35
+ export "$key=$value"
36
+ fi
37
+ done < <(cat "$ENV_FILE" | grep ":")
38
+ fi
39
+
40
+ # Export Config Dir for OpenCode to find custom tools
41
+ export OPENCODE_CONFIG_DIR="$BASE_DIR"
42
+
43
+ echo "🚀 Starting Sentry Resolver Run: $RUN_ID"
44
+ echo "📂 Project Root: $PROJECT_ROOT"
45
+
46
+ # ---------------------------------------------------------
47
+ # Phase 1: Fetch Issues
48
+ # ---------------------------------------------------------
49
+ echo "🔍 Fetching issues..."
50
+ cd "$RUN_DIR" || exit 1
51
+
52
+ # Run opencode from Project Root so it can access project files if needed
53
+ cd "$PROJECT_ROOT" || exit 1
54
+
55
+ opencode run \
56
+ --agent gv-fetcher \
57
+ --prompt "Fetch critical issues for projects: ${SENTRY_PROJECTS}. Write output to $RUN_DIR/issues.json" \
58
+ > "$RUN_DIR/fetcher.log" 2>&1
59
+
60
+ if [ ! -f "$RUN_DIR/issues.json" ]; then
61
+ echo "⚠️ No issues.json created. Check $RUN_DIR/fetcher.log"
62
+ exit 0
63
+ fi
64
+
65
+ # ---------------------------------------------------------
66
+ # Phase 2: Process Loop
67
+ # ---------------------------------------------------------
68
+ # Use node to parse the issues.json because bash JSON parsing is painful
69
+ node -e "
70
+ const fs = require('fs');
71
+ const issues = JSON.parse(fs.readFileSync('$RUN_DIR/issues.json', 'utf8'));
72
+ issues.forEach(issue => console.log(issue.id));
73
+ " > "$RUN_DIR/issue_list.txt"
74
+
75
+ while read -r ISSUE_ID; do
76
+ if [ -z "$ISSUE_ID" ]; then continue; fi
77
+
78
+ echo "⚙️ Processing Issue: $ISSUE_ID"
79
+ ISSUE_DIR="$RUN_DIR/$ISSUE_ID"
80
+ mkdir -p "$ISSUE_DIR"
81
+
82
+ # RCA Phase
83
+ echo " - Running RCA..."
84
+ opencode run \
85
+ --agent gv-rca \
86
+ --prompt "Perform RCA for $ISSUE_ID. Working directory: $ISSUE_DIR" \
87
+ > "$ISSUE_DIR/rca.log" 2>&1
88
+
89
+ if [ ! -f "$ISSUE_DIR/rca.md" ]; then
90
+ echo " ⚠️ RCA failed to produce report."
91
+ continue
92
+ fi
93
+
94
+ # Decision Check
95
+ # We look for 'Action: Fix' in the RCA markdown
96
+ ACTION=$(grep -i "Action:" "$ISSUE_DIR/rca.md" | head -n 1)
97
+
98
+ if [[ "$ACTION" == *"Fix"* ]]; then
99
+ echo " - Decision: FIX. Running Fixer..."
100
+ opencode run \
101
+ --agent gv-fixer \
102
+ --prompt "Fix $ISSUE_ID. RCA is at $ISSUE_DIR/rca.md" \
103
+ > "$ISSUE_DIR/fixer.log" 2>&1
104
+ else
105
+ echo " - Decision: SKIP/TICKET ($ACTION)"
106
+ fi
107
+
108
+ # Update State (Append to processed list)
109
+ # In a real impl, we'd read/write JSON properly. For now, we assume fetcher handles filtering.
110
+ # But we should append to a processed log.
111
+ echo "$ISSUE_ID processed at $(date)" >> "$BASE_DIR/processed_log.txt"
112
+
113
+ done < "$RUN_DIR/issue_list.txt"
114
+
115
+ echo "✅ Run Complete."
@@ -0,0 +1,51 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+
3
+ export default tool({
4
+ description: "Send a notification via Telegram",
5
+ args: {
6
+ message: tool.schema.string().describe("The message to send"),
7
+ },
8
+ async execute(args) {
9
+ const fs = await import("fs/promises");
10
+ const path = await import("path");
11
+
12
+ // Load secrets from .gv-ai/env.json (this file is in .gv-ai/tool/)
13
+ const envPath = path.join(__dirname, "..", "env.json");
14
+
15
+ let config;
16
+ try {
17
+ const envContent = await fs.readFile(envPath, "utf-8");
18
+ config = JSON.parse(envContent);
19
+ } catch {
20
+ return "Error: Could not load .gv-ai/env.json";
21
+ }
22
+
23
+ const { TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID } = config;
24
+ if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
25
+ return "Error: Telegram credentials missing in env.json";
26
+ }
27
+
28
+ const text = `🤖 *Tal Auto-Fixer*\n\n${args.message}`;
29
+
30
+ try {
31
+ const res = await fetch(
32
+ `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`,
33
+ {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json" },
36
+ body: JSON.stringify({
37
+ chat_id: TELEGRAM_CHAT_ID,
38
+ text,
39
+ parse_mode: "Markdown",
40
+ }),
41
+ }
42
+ );
43
+ const result = await res.json();
44
+ return result.ok
45
+ ? "Notification sent."
46
+ : `Telegram Error: ${result.description}`;
47
+ } catch (e: any) {
48
+ return `Network Error: ${e.message}`;
49
+ }
50
+ },
51
+ });
package/dist/cli.js CHANGED
@@ -5508,6 +5508,7 @@ var {
5508
5508
  var isUpKey = (key, keybindings = []) => key.name === "up" || keybindings.includes("vim") && key.name === "k" || keybindings.includes("emacs") && key.ctrl && key.name === "p";
5509
5509
  var isDownKey = (key, keybindings = []) => key.name === "down" || keybindings.includes("vim") && key.name === "j" || keybindings.includes("emacs") && key.ctrl && key.name === "n";
5510
5510
  var isSpaceKey = (key) => key.name === "space";
5511
+ var isBackspaceKey = (key) => key.name === "backspace";
5511
5512
  var isTabKey = (key) => key.name === "tab";
5512
5513
  var isNumberKey = (key) => "1234567890".includes(key.name);
5513
5514
  var isEnterKey = (key) => key.name === "enter" || key.name === "return";
@@ -6802,6 +6803,90 @@ var esm_default3 = createPrompt((config, done) => {
6802
6803
  const message = theme.style.message(config.message, status);
6803
6804
  return `${prefix} ${message}${defaultValue} ${formattedValue}`;
6804
6805
  });
6806
+ // node_modules/@inquirer/input/dist/esm/index.js
6807
+ var inputTheme = {
6808
+ validationFailureMode: "keep"
6809
+ };
6810
+ var esm_default4 = createPrompt((config, done) => {
6811
+ const { prefill = "tab" } = config;
6812
+ const theme = makeTheme(inputTheme, config.theme);
6813
+ const [status, setStatus] = useState("idle");
6814
+ const [defaultValue = "", setDefaultValue] = useState(config.default);
6815
+ const [errorMsg, setError] = useState();
6816
+ const [value, setValue] = useState("");
6817
+ const prefix = usePrefix({ status, theme });
6818
+ async function validate(value2) {
6819
+ const { required, pattern, patternError = "Invalid input" } = config;
6820
+ if (required && !value2) {
6821
+ return "You must provide a value";
6822
+ }
6823
+ if (pattern && !pattern.test(value2)) {
6824
+ return patternError;
6825
+ }
6826
+ if (typeof config.validate === "function") {
6827
+ return await config.validate(value2) || "You must provide a valid value";
6828
+ }
6829
+ return true;
6830
+ }
6831
+ useKeypress(async (key, rl) => {
6832
+ if (status !== "idle") {
6833
+ return;
6834
+ }
6835
+ if (isEnterKey(key)) {
6836
+ const answer = value || defaultValue;
6837
+ setStatus("loading");
6838
+ const isValid = await validate(answer);
6839
+ if (isValid === true) {
6840
+ setValue(answer);
6841
+ setStatus("done");
6842
+ done(answer);
6843
+ } else {
6844
+ if (theme.validationFailureMode === "clear") {
6845
+ setValue("");
6846
+ } else {
6847
+ rl.write(value);
6848
+ }
6849
+ setError(isValid);
6850
+ setStatus("idle");
6851
+ }
6852
+ } else if (isBackspaceKey(key) && !value) {
6853
+ setDefaultValue(undefined);
6854
+ } else if (isTabKey(key) && !value) {
6855
+ setDefaultValue(undefined);
6856
+ rl.clearLine(0);
6857
+ rl.write(defaultValue);
6858
+ setValue(defaultValue);
6859
+ } else {
6860
+ setValue(rl.line);
6861
+ setError(undefined);
6862
+ }
6863
+ });
6864
+ useEffect((rl) => {
6865
+ if (prefill === "editable" && defaultValue) {
6866
+ rl.write(defaultValue);
6867
+ setValue(defaultValue);
6868
+ }
6869
+ }, []);
6870
+ const message = theme.style.message(config.message, status);
6871
+ let formattedValue = value;
6872
+ if (typeof config.transformer === "function") {
6873
+ formattedValue = config.transformer(value, { isFinal: status === "done" });
6874
+ } else if (status === "done") {
6875
+ formattedValue = theme.style.answer(value);
6876
+ }
6877
+ let defaultStr;
6878
+ if (defaultValue && status !== "done" && !value) {
6879
+ defaultStr = theme.style.defaultAnswer(defaultValue);
6880
+ }
6881
+ let error = "";
6882
+ if (errorMsg) {
6883
+ error = theme.style.error(errorMsg);
6884
+ }
6885
+ return [
6886
+ [prefix, message, defaultStr, formattedValue].filter((v) => v !== undefined).join(" "),
6887
+ error
6888
+ ];
6889
+ });
6805
6890
  // node_modules/chalk/source/vendor/ansi-styles/index.js
6806
6891
  var ANSI_BACKGROUND_OFFSET = 10;
6807
6892
  var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
@@ -14910,8 +14995,258 @@ Step 3: Creating instruction files...
14910
14995
  }
14911
14996
  }
14912
14997
 
14998
+ // src/commands/sentry-resolver.ts
14999
+ import fs3 from "fs/promises";
15000
+ import path8 from "path";
15001
+ import { fileURLToPath as fileURLToPath3 } from "url";
15002
+ var __filename2 = fileURLToPath3(import.meta.url);
15003
+ var __dirname2 = path8.dirname(__filename2);
15004
+ async function sentryResolverInitCommand() {
15005
+ console.log(source_default.bold.cyan(`
15006
+ \uD83D\uDEE1️ GV-AI Sentry Resolver Setup
15007
+ `));
15008
+ console.log(source_default.dim(`This will configure an automated agent to triage and fix Sentry issues.
15009
+ `));
15010
+ try {
15011
+ const baseDir = path8.join(process.cwd(), ".gv-ai");
15012
+ try {
15013
+ await fs3.access(baseDir);
15014
+ console.log(source_default.yellow(`
15015
+ ⚠️ .gv-ai directory already exists.`));
15016
+ const shouldOverwrite = await esm_default3({
15017
+ message: "Do you want to overwrite the existing configuration? (This will reset env.json and prompts)",
15018
+ default: false
15019
+ });
15020
+ if (!shouldOverwrite) {
15021
+ console.log(source_default.yellow("Setup cancelled. Existing configuration preserved."));
15022
+ return;
15023
+ }
15024
+ } catch {}
15025
+ const sentryProjects = await esm_default4({ message: "Sentry Project Names (comma separated):" });
15026
+ const telegramToken = await esm_default4({ message: "Telegram Bot Token:" });
15027
+ const telegramChatId = await esm_default4({ message: "Telegram Chat ID:" });
15028
+ const config = {
15029
+ SENTRY_PROJECTS: sentryProjects,
15030
+ TELEGRAM_BOT_TOKEN: telegramToken,
15031
+ TELEGRAM_CHAT_ID: telegramChatId
15032
+ };
15033
+ const spinner = ora("Creating .gv-ai directory structure...").start();
15034
+ await fs3.mkdir(path8.join(baseDir, "runs"), { recursive: true });
15035
+ await fs3.mkdir(path8.join(baseDir, "scripts"), { recursive: true });
15036
+ await fs3.mkdir(path8.join(baseDir, "prompts"), { recursive: true });
15037
+ await fs3.mkdir(path8.join(baseDir, "tool"), { recursive: true });
15038
+ await fs3.writeFile(path8.join(baseDir, "env.json"), JSON.stringify(config, null, 2));
15039
+ spinner.succeed("Created .gv-ai/ and env.json");
15040
+ const templateSpinner = ora("Copying templates...").start();
15041
+ const templateDirCandidates = [
15042
+ path8.join(__dirname2, "../templates/sentry-resolver"),
15043
+ path8.join(__dirname2, "templates/sentry-resolver"),
15044
+ path8.join(__dirname2, "../src/templates/sentry-resolver")
15045
+ ];
15046
+ let templateDir = "";
15047
+ for (const dir of templateDirCandidates) {
15048
+ try {
15049
+ await fs3.access(dir);
15050
+ templateDir = dir;
15051
+ break;
15052
+ } catch {}
15053
+ }
15054
+ if (!templateDir) {
15055
+ throw new Error(`Could not locate templates. Searched: ${templateDirCandidates.join(", ")}`);
15056
+ }
15057
+ await fs3.copyFile(path8.join(templateDir, "prompts/fetcher.md"), path8.join(baseDir, "prompts/fetcher.md"));
15058
+ await fs3.copyFile(path8.join(templateDir, "prompts/rca.md"), path8.join(baseDir, "prompts/rca.md"));
15059
+ await fs3.copyFile(path8.join(templateDir, "prompts/fixer.md"), path8.join(baseDir, "prompts/fixer.md"));
15060
+ await fs3.copyFile(path8.join(templateDir, "scripts/orchestrator.sh"), path8.join(baseDir, "scripts/orchestrator.sh"));
15061
+ await fs3.chmod(path8.join(baseDir, "scripts/orchestrator.sh"), 493);
15062
+ await fs3.copyFile(path8.join(templateDir, "tool/notify.ts.template"), path8.join(baseDir, "tool/notify.ts"));
15063
+ templateSpinner.succeed("Templates copied");
15064
+ const configSpinner = ora("Generating bot configuration...").start();
15065
+ const botConfig = {
15066
+ $schema: "https://opencode.ai/config.json",
15067
+ mcp: {
15068
+ sentry: {
15069
+ type: "remote",
15070
+ url: "https://mcp.sentry.dev/mcp",
15071
+ oauth: {}
15072
+ },
15073
+ linear: {
15074
+ type: "local",
15075
+ command: ["npx", "-y", "mcp-remote", "https://mcp.linear.app/sse"],
15076
+ enabled: true
15077
+ }
15078
+ },
15079
+ agent: {
15080
+ "gv-fetcher": {
15081
+ prompt: "{file:prompts/fetcher.md}",
15082
+ tools: {
15083
+ sentry_find_organizations: true,
15084
+ sentry_find_projects: true,
15085
+ sentry_search_issues: true,
15086
+ write: true,
15087
+ read: true
15088
+ }
15089
+ },
15090
+ "gv-rca": {
15091
+ prompt: "{file:prompts/rca.md}",
15092
+ tools: {
15093
+ sentry_find_organizations: true,
15094
+ sentry_get_issue_details: true,
15095
+ sentry_analyze_issue_with_seer: true,
15096
+ read: true,
15097
+ notify: true
15098
+ }
15099
+ },
15100
+ "gv-fixer": {
15101
+ prompt: "{file:prompts/fixer.md}",
15102
+ tools: {
15103
+ edit: true,
15104
+ bash: true,
15105
+ read: true,
15106
+ write: true,
15107
+ notify: true
15108
+ }
15109
+ }
15110
+ },
15111
+ permission: {
15112
+ bash: "allow",
15113
+ edit: "allow",
15114
+ write: "allow",
15115
+ read: "allow"
15116
+ }
15117
+ };
15118
+ await fs3.writeFile(path8.join(baseDir, "opencode.json"), JSON.stringify(botConfig, null, 2));
15119
+ configSpinner.succeed("Bot configuration generated (.gv-ai/opencode.json)");
15120
+ const cronSpinner = ora("Setting up cron job...").start();
15121
+ const scriptPath = path8.join(baseDir, "scripts/orchestrator.sh");
15122
+ const cronSchedule = "0 */4 * * *";
15123
+ const cronJob = `${cronSchedule} ${scriptPath} >> ${path8.join(baseDir, "cron.log")} 2>&1`;
15124
+ try {
15125
+ let currentCrontab = "";
15126
+ try {
15127
+ const { stdout } = await execa("crontab", ["-l"]);
15128
+ currentCrontab = stdout;
15129
+ } catch (e) {}
15130
+ if (!currentCrontab.includes(scriptPath)) {
15131
+ const newCrontab = currentCrontab + (currentCrontab ? `
15132
+ ` : "") + cronJob + `
15133
+ `;
15134
+ await execa("sh", ["-c", `echo "${newCrontab}" | crontab -`]);
15135
+ cronSpinner.succeed("Cron job registered");
15136
+ } else {
15137
+ cronSpinner.info("Cron job already exists");
15138
+ }
15139
+ } catch (e) {
15140
+ cronSpinner.warn(`Failed to set crontab: ${e.message}. You may need to add it manually.`);
15141
+ console.log(source_default.dim(`Manual entry: ${cronJob}`));
15142
+ }
15143
+ try {
15144
+ const gitignorePath = path8.join(process.cwd(), ".gitignore");
15145
+ const gitignore = await fs3.readFile(gitignorePath, "utf-8").catch(() => "");
15146
+ const entry = `
15147
+ # GV-AI Automation
15148
+ .gv-ai/`;
15149
+ if (!gitignore.includes(".gv-ai")) {
15150
+ await fs3.appendFile(gitignorePath, entry + `
15151
+ `);
15152
+ console.log(source_default.green("Added .gv-ai/ to .gitignore"));
15153
+ }
15154
+ } catch (e) {}
15155
+ console.log(source_default.bold.green(`
15156
+ ✅ Initialization Complete!`));
15157
+ console.log(source_default.dim(`
15158
+ - Config: .gv-ai/env.json
15159
+ - Logs: .gv-ai/runs/
15160
+ - Cron: Runs every 5 minutes
15161
+
15162
+ To test manually:
15163
+ $ .gv-ai/scripts/orchestrator.sh
15164
+ `));
15165
+ } catch (error) {
15166
+ console.error(source_default.red(`
15167
+ ❌ Error: ${error.message}`));
15168
+ process.exit(1);
15169
+ }
15170
+ }
15171
+ async function sentryResolverCronSetCommand(value) {
15172
+ const baseDir = path8.join(process.cwd(), ".gv-ai");
15173
+ const scriptPath = path8.join(baseDir, "scripts/orchestrator.sh");
15174
+ const cronRegex = /^(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(\*|[0-9,\-\/]+)\s+(\*|[0-9,\-\/]+)\s+(\*|[0-9,\-\/]+)\s+(\*|[0-9,\-\/]+)\s+(\*|[0-9,\-\/]+)$/;
15175
+ let cronSchedule = "";
15176
+ if (value.endsWith("m")) {
15177
+ const mins = parseInt(value.slice(0, -1));
15178
+ if (isNaN(mins) || mins < 1 || mins > 59) {
15179
+ console.error(source_default.red(`❌ Invalid minutes: ${value}. Use format: 5m, 10m, 30m`));
15180
+ process.exit(1);
15181
+ }
15182
+ cronSchedule = `*/${mins} * * * *`;
15183
+ } else if (value.endsWith("h")) {
15184
+ const hours = parseInt(value.slice(0, -1));
15185
+ if (isNaN(hours) || hours < 1 || hours > 23) {
15186
+ console.error(source_default.red(`❌ Invalid hours: ${value}. Use format: 1h, 4h, 12h`));
15187
+ process.exit(1);
15188
+ }
15189
+ cronSchedule = `0 */${hours} * * *`;
15190
+ } else if (cronRegex.test(value)) {
15191
+ cronSchedule = value;
15192
+ } else {
15193
+ console.error(source_default.red(`❌ Invalid format. Use: 5m, 2h, or cron expression`));
15194
+ process.exit(1);
15195
+ }
15196
+ console.log(source_default.cyan(`
15197
+ ⏰ Setting cron to: ${cronSchedule}`));
15198
+ try {
15199
+ let currentCrontab = "";
15200
+ try {
15201
+ const { stdout } = await execa("crontab", ["-l"]);
15202
+ currentCrontab = stdout;
15203
+ } catch {}
15204
+ const cleanedCrontab = currentCrontab.split(`
15205
+ `).filter((line) => !line.includes(scriptPath)).join(`
15206
+ `);
15207
+ const cronJob = `${cronSchedule} ${scriptPath} >> ${path8.join(baseDir, "cron.log")} 2>&1`;
15208
+ const newCrontab = (cleanedCrontab ? cleanedCrontab + `
15209
+ ` : "") + cronJob + `
15210
+ `;
15211
+ await execa("sh", ["-c", `echo "${newCrontab}" | crontab -`]);
15212
+ console.log(source_default.green("✅ Cron job updated!"));
15213
+ } catch (e) {
15214
+ console.error(source_default.red(`❌ Failed to update cron: ${e.message}`));
15215
+ process.exit(1);
15216
+ }
15217
+ }
15218
+ async function sentryResolverCronDisableCommand() {
15219
+ const baseDir = path8.join(process.cwd(), ".gv-ai");
15220
+ const scriptPath = path8.join(baseDir, "scripts/orchestrator.sh");
15221
+ console.log(source_default.cyan(`
15222
+ \uD83D\uDED1 Disabling cron job...`));
15223
+ try {
15224
+ let currentCrontab = "";
15225
+ try {
15226
+ const { stdout } = await execa("crontab", ["-l"]);
15227
+ currentCrontab = stdout;
15228
+ } catch {}
15229
+ if (!currentCrontab.includes(scriptPath)) {
15230
+ console.log(source_default.yellow("Cron job not found."));
15231
+ return;
15232
+ }
15233
+ const cleanedCrontab = currentCrontab.split(`
15234
+ `).filter((line) => !line.includes(scriptPath)).join(`
15235
+ `);
15236
+ await execa("sh", ["-c", `echo "${cleanedCrontab}" | crontab -`]);
15237
+ console.log(source_default.green("✅ Cron job disabled!"));
15238
+ } catch (e) {
15239
+ console.error(source_default.red(`❌ Failed to disable cron: ${e.message}`));
15240
+ process.exit(1);
15241
+ }
15242
+ }
15243
+
14913
15244
  // src/cli.ts
14914
15245
  var program2 = new Command;
14915
15246
  program2.name("gv-ai").description("CLI tool to enforce Linear ticket tracking across AI coding tools").version("1.0.0");
14916
15247
  program2.command("project").description("Manage project configuration").command("init").description("Initialize GV AI project setup").action(initCommand);
15248
+ var sentryResolver = program2.command("sentry-resolver").description("Manage Sentry automated resolution");
15249
+ sentryResolver.command("init").description("Initialize Sentry Resolver bot").action(sentryResolverInitCommand);
15250
+ sentryResolver.command("cron").description("Manage cron schedule").command("set <interval>").description('Set cron interval (e.g., 5m, 2h, "*/10 * * * *")').action((interval) => sentryResolverCronSetCommand(interval));
15251
+ sentryResolver.command("disable").description("Disable the cron job").action(sentryResolverCronDisableCommand);
14917
15252
  program2.parse();
@@ -0,0 +1,4 @@
1
+ export declare function sentryResolverInitCommand(): Promise<void>;
2
+ export declare function sentryResolverCronSetCommand(value: string): Promise<void>;
3
+ export declare function sentryResolverCronDisableCommand(): Promise<void>;
4
+ //# sourceMappingURL=sentry-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentry-resolver.d.ts","sourceRoot":"","sources":["../../src/commands/sentry-resolver.ts"],"names":[],"mappings":"AAWA,wBAAsB,yBAAyB,kBAkO9C;AAED,wBAAsB,4BAA4B,CAAC,KAAK,EAAE,MAAM,iBAuD/D;AAED,wBAAsB,gCAAgC,kBA6BrD"}
@@ -0,0 +1,2 @@
1
+ export declare function showTemplateCommand(): Promise<void>;
2
+ //# sourceMappingURL=show-template.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"show-template.d.ts","sourceRoot":"","sources":["../../src/commands/show-template.ts"],"names":[],"mappings":"AAEA,wBAAsB,mBAAmB,kBAExC"}
@@ -0,0 +1,91 @@
1
+ export const AGENTS_MD_TEMPLATE = `# AGENTS.md
2
+
3
+ ## Linear Ticket Enforcement
4
+
5
+ **CRITICAL**: Before making ANY code changes (editing files, creating files, running commands that modify state), you MUST have a confirmed Linear ticket.
6
+
7
+ ### Pre-Implementation Gate
8
+
9
+ When the user requests implementation work:
10
+
11
+ 1. **Ask what they're working on** (if not clear from context)
12
+ 2. **Search Linear for relevant tickets** using Linear MCP tools
13
+ 3. **Fetch all Linear projects** to show available options
14
+ 4. **Present a numbered list** to the user:
15
+
16
+ \`\`\`
17
+ Found potential matches:
18
+ 1. [PROJ-123]: Ticket title here
19
+ 2. [PROJ-456]: Another ticket title
20
+
21
+ Or create new ticket in:
22
+ 3. Backend API (BACK)
23
+ 4. Platform Infrastructure (PLAT)
24
+ 5. Mobile App (MOB)
25
+
26
+ Which one? (enter number)
27
+ \`\`\`
28
+
29
+ 5. **Wait for user to select a number**
30
+ 6. **Confirm the selection** before proceeding with any implementation
31
+
32
+ ### Free Actions (No Ticket Required)
33
+
34
+ You may freely help with:
35
+ - Answering questions about code
36
+ - Explaining how code works
37
+ - Discussing approaches and solutions
38
+ - Debugging and analysis (without making changes)
39
+ - Code review and suggestions
40
+ - General brainstorming
41
+
42
+ ### Gated Actions (Ticket Required)
43
+
44
+ You MUST have a confirmed Linear ticket before:
45
+ - Creating new files
46
+ - Editing existing files
47
+ - Running commands that modify state (git commits, npm install, etc.)
48
+ - Creating branches or pull requests
49
+ - Deploying or publishing
50
+
51
+ ### During Implementation
52
+
53
+ Once a ticket is confirmed:
54
+ - Update the Linear ticket with progress comments as you work
55
+ - Set ticket status to "In Progress" when starting
56
+ - Link commits and branches to the ticket
57
+ - Keep the ticket updated with any blockers or decisions
58
+
59
+ ### PR Creation
60
+
61
+ When creating a pull request:
62
+ - Use Linear's branch naming convention (if available)
63
+ - Automatically link the PR to the associated ticket
64
+ - Update the ticket status to "In Review"
65
+ - Add a comment to the ticket with the PR link
66
+
67
+ ### Multi-Ticket Sessions
68
+
69
+ If work touches multiple features or areas:
70
+ - Identify all relevant tickets at the start
71
+ - Confirm with the user which tickets to track
72
+ - Update each ticket appropriately as you work on related code
73
+ - Keep tickets in sync with the work being done
74
+
75
+ ### Ad-hoc Tickets
76
+
77
+ When creating a new ad-hoc ticket:
78
+ - Ask the user which Linear project to create it in
79
+ - Use the user's description as the ticket title
80
+ - Automatically add an "ad-hoc" label
81
+ - Assign the ticket to the current user (if possible)
82
+ - Set the status to "In Progress"
83
+
84
+ ### Important Notes
85
+
86
+ - **Always confirm** before associating work with a ticket
87
+ - **Never guess** which ticket applies - always ask the user
88
+ - **Show ticket details** (title, description preview) when asking for confirmation
89
+ - **Handle errors gracefully** if Linear API is unavailable
90
+ - **Be patient** - the user may need time to find the right ticket
91
+ `;
@@ -0,0 +1,42 @@
1
+ tools:
2
+ bash: false
3
+ write: true
4
+ edit: false
5
+ sentry_find_organizations: true
6
+ sentry_find_projects: true
7
+ sentry_search_events: false
8
+ sentry_search_issues: true
9
+ ---
10
+
11
+ You are the **Issue Fetcher & Prioritizer Agent**.
12
+
13
+ ### TASK
14
+ Your goal is to identify the most critical Sentry issues for the requested projects and save them for processing.
15
+
16
+ ### INSTRUCTIONS
17
+ 1. **Identify Targets**:
18
+ * You have been provided a list of **Sentry Projects** in the prompt.
19
+ * Use your available Sentry tools to resolve the Organization and Project Slugs necessary to query them.
20
+ 2. **Query Sentry**:
21
+ * Search for unresolved issues (`is:unresolved level:error`) in these projects.
22
+ * Limit: 20 issues per project.
23
+ 3. **Prioritize**:
24
+ * Score issues (0-100) based on User Count, Event Frequency, and Age.
25
+ * Select the **Top 5** highest scoring issues.
26
+ 4. **Output**:
27
+ * Read `state.json` (if exists) and exclude IDs in `processed_ids`.
28
+ * Write the filtered list to `issues.json` in the current directory.
29
+
30
+ ### OUTPUT FORMAT (issues.json)
31
+ ```json
32
+ [
33
+ {
34
+ "id": "SENTRY-123",
35
+ "title": "Error Title",
36
+ "organization": "org-slug",
37
+ "project": "project-slug",
38
+ "priority_score": 95,
39
+ "reason": "High impact"
40
+ }
41
+ ]
42
+ ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ description: Implement code fixes and create Pull Requests
3
+ mode: primary
4
+ tools:
5
+ bash: true
6
+ write: true
7
+ edit: true
8
+ read: true
9
+ notify: true
10
+ ---
11
+
12
+ You are the **Fixer Agent**.
13
+
14
+ ### TASK
15
+ Implement the fix proposed in the RCA and submit a Pull Request.
16
+
17
+ ### INPUT
18
+ * Read `rca.md` in the current directory.
19
+ * The current directory is `.gv-ai/runs/<timestamp>/<issue_id>/`.
20
+
21
+ ### INSTRUCTIONS
22
+ 1. **Preparation**:
23
+ * Read `rca.md` to understand the plan.
24
+ * **Git**: Ensure you are on the base branch (usually `main` or `master`). Pull latest.
25
+ * **Branch**: Create a new branch: `fix/sentry-<issue_id>-<timestamp>`.
26
+ 2. **Implementation**:
27
+ * Use `edit` to modify the code.
28
+ * **Verify**: If possible, run a relevant test (or create a temporary test case) to ensure the fix works and doesn't break syntax.
29
+ * **Lint**: Run `npm run lint` (or equivalent) if available.
30
+ 3. **Commit & PR**:
31
+ * `git commit -am "fix(sentry): Resolve <issue_id> - <brief description>"`
32
+ * `git push -u origin <branch_name>`
33
+ * Create PR using `gh pr create`:
34
+ * Title: `fix: Resolve Sentry Issue <issue_id>`
35
+ * Body: Include summary from `rca.md`.
36
+ 4. **Notify**:
37
+ * Use `notify` tool: "✅ *Fix Implemented* for [Issue ID]\nPR: [PR URL]"
38
+ 5. **Artifact**:
39
+ * Write `fix_result.json` with the PR URL and status.
40
+
41
+ ### CONSTRAINTS
42
+ * Do NOT change unrelated code.
43
+ * If you cannot verify the fix (e.g., tests fail), abort and notify with a clear error message.
@@ -0,0 +1,47 @@
1
+ tools:
2
+ bash: true
3
+ write: true
4
+ edit: false
5
+ read: true
6
+ sentry_find_organizations: true
7
+ sentry_get_issue_details: true
8
+ sentry_get_issue_tag_values: true
9
+ sentry_analyze_issue_with_seer: true
10
+ notify: true
11
+ ---
12
+
13
+ You are the **RCA (Root Cause Analysis) Agent**.
14
+
15
+ ### TASK
16
+ Analyze the provided Sentry issue to determine the root cause and fixability.
17
+
18
+ ### INPUT
19
+ * **Issue ID** (e.g., `SENTRY-123`) provided in the prompt.
20
+ * **Context**: You are running in `.gv-ai/runs/<timestamp>/<issue_id>/`.
21
+
22
+ ### INSTRUCTIONS
23
+ 1. **Analyze Issue**:
24
+ * Use your tools (`sentry_get_issue_details`, `sentry_analyze_issue_with_seer`) to inspect the issue.
25
+ * If you need the Organization slug and it wasn't provided, use your tools to find it.
26
+ * **Verify Code**: Check the local codebase (`grep`/`read`) to confirm the file and line number from the stack trace exist.
27
+ 2. **Determine Root Cause**:
28
+ * Identify the logic error.
29
+ * Assess **Fixability** (High/Medium/Low).
30
+ 3. **Report**:
31
+ * Write `rca.md` with your findings.
32
+ 4. **Notify**:
33
+ * Send a Telegram summary using the `notify` tool.
34
+
35
+ ### OUTPUT FORMAT (rca.md)
36
+ ```markdown
37
+ # RCA: SENTRY-123
38
+ ## Root Cause
39
+ ...
40
+ ## Evidence
41
+ ...
42
+ ## Proposed Fix
43
+ ...
44
+ ## Assessment
45
+ - **Confidence**: High/Medium/Low
46
+ - **Action**: Fix / Ticket / Skip
47
+ ```
@@ -0,0 +1,115 @@
1
+ #!/bin/bash
2
+
3
+ # Ensure we have a path (Cron often has limited path)
4
+ export PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
5
+
6
+ # Try to source user profile for NVM/Node/etc
7
+ if [ -f "$HOME/.bashrc" ]; then source "$HOME/.bashrc"; fi
8
+ if [ -f "$HOME/.zshrc" ]; then source "$HOME/.zshrc"; fi
9
+ if [ -f "$HOME/.profile" ]; then source "$HOME/.profile"; fi
10
+
11
+ # Directory Setup
12
+ # Resolve the directory where this script resides (.gv-ai/scripts)
13
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
14
+ # Base dir is parent of scripts (.gv-ai)
15
+ BASE_DIR="$(dirname "$SCRIPT_DIR")"
16
+ # Project Root is parent of .gv-ai
17
+ PROJECT_ROOT="$(dirname "$BASE_DIR")"
18
+
19
+ RUN_ID=$(date +%Y%m%d-%H%M%S)
20
+ RUN_DIR="$BASE_DIR/runs/$RUN_ID"
21
+ STATE_FILE="$BASE_DIR/state.json"
22
+ ENV_FILE="$BASE_DIR/env.json"
23
+
24
+ mkdir -p "$RUN_DIR"
25
+
26
+ # Load Environment Variables from env.json
27
+ if [ -f "$ENV_FILE" ]; then
28
+ # Simple parser for flat JSON string/number values
29
+ while IFS='=' read -r key value; do
30
+ # Remove quotes and comma
31
+ key=$(echo "$key" | tr -d '",')
32
+ value=$(echo "$value" | tr -d '",')
33
+ # Export if not empty
34
+ if [[ ! -z "$key" && "$key" != "{" && "$key" != "}" ]]; then
35
+ export "$key=$value"
36
+ fi
37
+ done < <(cat "$ENV_FILE" | grep ":")
38
+ fi
39
+
40
+ # Export Config Dir for OpenCode to find custom tools
41
+ export OPENCODE_CONFIG_DIR="$BASE_DIR"
42
+
43
+ echo "🚀 Starting Sentry Resolver Run: $RUN_ID"
44
+ echo "📂 Project Root: $PROJECT_ROOT"
45
+
46
+ # ---------------------------------------------------------
47
+ # Phase 1: Fetch Issues
48
+ # ---------------------------------------------------------
49
+ echo "🔍 Fetching issues..."
50
+ cd "$RUN_DIR" || exit 1
51
+
52
+ # Run opencode from Project Root so it can access project files if needed
53
+ cd "$PROJECT_ROOT" || exit 1
54
+
55
+ opencode run \
56
+ --agent gv-fetcher \
57
+ --prompt "Fetch critical issues for projects: ${SENTRY_PROJECTS}. Write output to $RUN_DIR/issues.json" \
58
+ > "$RUN_DIR/fetcher.log" 2>&1
59
+
60
+ if [ ! -f "$RUN_DIR/issues.json" ]; then
61
+ echo "⚠️ No issues.json created. Check $RUN_DIR/fetcher.log"
62
+ exit 0
63
+ fi
64
+
65
+ # ---------------------------------------------------------
66
+ # Phase 2: Process Loop
67
+ # ---------------------------------------------------------
68
+ # Use node to parse the issues.json because bash JSON parsing is painful
69
+ node -e "
70
+ const fs = require('fs');
71
+ const issues = JSON.parse(fs.readFileSync('$RUN_DIR/issues.json', 'utf8'));
72
+ issues.forEach(issue => console.log(issue.id));
73
+ " > "$RUN_DIR/issue_list.txt"
74
+
75
+ while read -r ISSUE_ID; do
76
+ if [ -z "$ISSUE_ID" ]; then continue; fi
77
+
78
+ echo "⚙️ Processing Issue: $ISSUE_ID"
79
+ ISSUE_DIR="$RUN_DIR/$ISSUE_ID"
80
+ mkdir -p "$ISSUE_DIR"
81
+
82
+ # RCA Phase
83
+ echo " - Running RCA..."
84
+ opencode run \
85
+ --agent gv-rca \
86
+ --prompt "Perform RCA for $ISSUE_ID. Working directory: $ISSUE_DIR" \
87
+ > "$ISSUE_DIR/rca.log" 2>&1
88
+
89
+ if [ ! -f "$ISSUE_DIR/rca.md" ]; then
90
+ echo " ⚠️ RCA failed to produce report."
91
+ continue
92
+ fi
93
+
94
+ # Decision Check
95
+ # We look for 'Action: Fix' in the RCA markdown
96
+ ACTION=$(grep -i "Action:" "$ISSUE_DIR/rca.md" | head -n 1)
97
+
98
+ if [[ "$ACTION" == *"Fix"* ]]; then
99
+ echo " - Decision: FIX. Running Fixer..."
100
+ opencode run \
101
+ --agent gv-fixer \
102
+ --prompt "Fix $ISSUE_ID. RCA is at $ISSUE_DIR/rca.md" \
103
+ > "$ISSUE_DIR/fixer.log" 2>&1
104
+ else
105
+ echo " - Decision: SKIP/TICKET ($ACTION)"
106
+ fi
107
+
108
+ # Update State (Append to processed list)
109
+ # In a real impl, we'd read/write JSON properly. For now, we assume fetcher handles filtering.
110
+ # But we should append to a processed log.
111
+ echo "$ISSUE_ID processed at $(date)" >> "$BASE_DIR/processed_log.txt"
112
+
113
+ done < "$RUN_DIR/issue_list.txt"
114
+
115
+ echo "✅ Run Complete."
@@ -0,0 +1,51 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+
3
+ export default tool({
4
+ description: "Send a notification via Telegram",
5
+ args: {
6
+ message: tool.schema.string().describe("The message to send"),
7
+ },
8
+ async execute(args) {
9
+ const fs = await import("fs/promises");
10
+ const path = await import("path");
11
+
12
+ // Load secrets from .gv-ai/env.json (this file is in .gv-ai/tool/)
13
+ const envPath = path.join(__dirname, "..", "env.json");
14
+
15
+ let config;
16
+ try {
17
+ const envContent = await fs.readFile(envPath, "utf-8");
18
+ config = JSON.parse(envContent);
19
+ } catch {
20
+ return "Error: Could not load .gv-ai/env.json";
21
+ }
22
+
23
+ const { TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID } = config;
24
+ if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
25
+ return "Error: Telegram credentials missing in env.json";
26
+ }
27
+
28
+ const text = `🤖 *Tal Auto-Fixer*\n\n${args.message}`;
29
+
30
+ try {
31
+ const res = await fetch(
32
+ `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`,
33
+ {
34
+ method: "POST",
35
+ headers: { "Content-Type": "application/json" },
36
+ body: JSON.stringify({
37
+ chat_id: TELEGRAM_CHAT_ID,
38
+ text,
39
+ parse_mode: "Markdown",
40
+ }),
41
+ }
42
+ );
43
+ const result = await res.json();
44
+ return result.ok
45
+ ? "Notification sent."
46
+ : `Telegram Error: ${result.description}`;
47
+ } catch (e: any) {
48
+ return `Network Error: ${e.message}`;
49
+ }
50
+ },
51
+ });
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "gv-ai",
3
- "version": "1.0.0",
4
- "description": "Internal CLI tool for GV team - Linear ticket enforcement",
3
+ "version": "1.1.0",
4
+ "description": "Internal CLI tool for GV team - Linear ticket enforcement and automated Sentry issue resolution",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "gv-ai": "./dist/cli.js"
8
8
  },
9
9
  "scripts": {
10
- "build": "bun build src/cli.ts --outdir dist --target node --format esm && bun run build:types",
10
+ "build": "bun build src/cli.ts --outdir dist --target node --format esm && cp -r src/templates dist/ && bun run build:types",
11
11
  "build:types": "tsc --emitDeclarationOnly",
12
12
  "dev": "bun run src/cli.ts",
13
13
  "start": "bun run dist/cli.js",