triage-trip 0.1.0 → 0.1.4

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.
package/README.md CHANGED
@@ -1,146 +1,117 @@
1
- # `ai-blackbox`
1
+ Markdown
2
+ # triage-trip
2
3
 
3
- > Autonomous AI Regression Triage & Provenance CLI. Pinpoint breaking contract changes introduced by AI coding tools in seconds.
4
+ > Autonomous CLI to pinpoint and patch regressions introduced by AI code assistants (Cursor, Copilot).
4
5
 
5
- <p align="center">
6
- <img src="./assets/demo.gif" alt="AI-Blackbox Demo" width="700">
7
- </p>
8
- ---
6
+ ![triage-trip Demo](assets/demo.gif)
9
7
 
10
8
  ## The Problem
9
+ When you use AI coding assistants to refactor or draft features, errors often slip through:
10
+ - Subtle schema property renames (`userId` ➔ `user_id`).
11
+ - Dropped function arguments or breaking contract shifts.
12
+ - Regressions that aren't caught until 3 commits later.
11
13
 
12
- AI coding assistants (Cursor, GitHub Copilot, Windsurf) dramatically accelerate drafting and refactoring code. However, they frequently cause **silent contract drift**:
13
-
14
- * An AI renames an object key or subtle schema attribute (e.g., `userId` ➔ `user_id`).
15
- * Standard linters and compilers pass because the syntax is completely valid.
16
- * Downstream modules crash 2–3 commits later with cryptic runtime errors (`TypeError: Cannot read properties of undefined`).
17
- * Developers spend 20–30 minutes manually inspecting multi-file diffs, running `git bisect`, and searching for what changed.
18
-
19
- ---
20
-
21
- ## What `ai-blackbox` Does
22
-
23
- `ai-blackbox` connects the dots across your development environment to trace the exact lineage of a regression:
24
-
25
- ```text
26
- [ User Prompt in IDE ] ➔ [ AI Generated Diff ] ➔ [ Git Commit ] ➔ [ Runtime Error ]
27
- │
28
- ai-blackbox triage
29
- │
30
- ▼
31
- • Exact Root Cause & Commit
32
- • Attributed AI Prompt
33
- • Semantic Contract Shift
34
- • Clean Unified Diff Patch
14
+ Developers lose 30+ minutes digging through hundreds of lines of AI diffs trying to figure out which prompt broke the build.
35
15
 
36
- ```
37
-
38
- 1. **Inspects Git History:** Automatically reads uncommitted working changes and recent commits using `simple-git`.
39
- 2. **Extracts AI Session Provenance:** Scans local IDE storage (e.g., Cursor's local SQLite database `state.vscdb`) to retrieve recent prompts and user instructions.
40
- 3. **Triages Semantic Drift:** Correlates the runtime stack trace with recent AST modifications and prompts via the OpenAI API to deliver an instant diagnostic report.
16
+ ## How It Works
17
+ `triage-trip` connects runtime failures back to their origin:
18
+ 1. **Reads Local Git Stream:** Analyzes unstaged changes and recent commit diffs.
19
+ 2. **Inspects Cursor Provenance:** Reads local IDE session SQLite databases (`state.vscdb`) to identify the exact human prompt that generated the regression.
20
+ 3. **Isolates Semantic Shifts:** Determines contract and schema breaks.
21
+ 4. **Interactive In-Memory Patching:** Applies clean unified diffs directly to your working tree via `git apply` with a single confirmation.
41
22
 
42
23
  ---
43
24
 
44
- ## Installation
45
-
46
- ### Prerequisites
47
-
48
- * **Node.js** (v18 or higher, ESM native)
49
- * An active **OpenAI API Key**
50
-
51
- ### Local Setup
52
-
53
- ```bash
54
- # Clone the repository
55
- git clone https://github.com/your-username/ai-blackbox.git
56
- cd ai-blackbox
57
-
58
- # Install dependencies
59
- npm install
60
-
61
- # Link executable globally (optional for direct CLI access)
62
- npm link
63
-
64
- ```
25
+ ## Quick Start
65
26
 
66
- Set your OpenAI API key in your terminal environment or add it to a `.env` file:
27
+ Run instantly without global installation:
67
28
 
68
29
  ```bash
69
- export OPENAI_API_KEY="sk-..."
30
+ npx triage-trip triage "TypeError: Cannot read properties of undefined (reading 'toUpperCase')"
31
+ Or install globally:
32
+
33
+ Bash
34
+ npm install -g triage-trip
35
+ Environment Setup
36
+ Set your preferred LLM API key:
37
+
38
+ Bash
39
+ export ANTHROPIC_API_KEY="your-anthropic-key"
40
+ # or
41
+ export OPENAI_API_KEY="your-openai-key"
42
+ Usage
43
+ Diagnose an Error Trace
44
+ Bash
45
+ triage-trip triage "Cannot read properties of undefined (reading 'toUpperCase')"
46
+ Auto-Apply Patches Instantly
47
+ Skip the confirmation prompt to patch immediately:
48
+
49
+ Bash
50
+ triage-trip triage "ReferenceError: getUserProfile is not defined" --yes
51
+ Inspect More Commits
52
+ Bash
53
+ triage-trip triage "SyntaxError: Unexpected token" --commits 10
54
+ License
55
+ MIT
70
56
 
71
- ```
72
57
 
73
58
  ---
74
59
 
75
- ## Quickstart & Usage
76
-
77
- Run the triage engine directly against any runtime error message or stack trace:
60
+ ### Step 3: Commit and Publish
78
61
 
79
62
  ```bash
80
- # Via npm script
81
- node bin/cli.js triage "TypeError: Cannot read properties of undefined (reading 'toUpperCase')"
82
-
83
- # Or globally (if linked)
84
- ai-blackbox triage "TypeError: Cannot read properties of undefined (reading 'toUpperCase')"
85
-
86
- ```
63
+ # Stage the updated doc and new GIF
64
+ git add README.md assets/demo.gif package.json bin/cli.js
87
65
 
88
- ### Options
66
+ # Commit
67
+ git commit -m "docs: rebrand to triage-trip with new demo gif"
89
68
 
90
- * `<query_or_error>` *(required)*: The error message or failing test description.
91
- * `-c, --commits <number>`: Number of recent commits to analyze (default: `5`).
69
+ # Push to your remote
70
+ git push origin main
92
71
 
93
- ---
94
-
95
- ## Example Output
72
+ # Publish live on npm
73
+ npm publish --access public
96
74
 
97
- ```markdown
98
- ■ AI-Blackbox Triage Engine
99
75
 
100
- ✔ Root cause isolated.
76
+ ## Inspiration & Ecosystem
101
77
 
102
- 1. **Root Cause**: The error occurs in `app.js` at line 6, where `profile.userId` is accessed.
103
- The breaking change was introduced in commit `b83c577` ("refactor: update user schema"),
104
- which modified the `getUserProfile` function in `user.js`.
78
+ Large engineering organizations (like Uber with their internal autonomous debugging agents) have long relied on automated bisect and triage systems to catch regressions before they reach staging.
105
79
 
106
- 2. **Attributed Prompt**: "Rename userId to user_id across the returned profile object."
80
+ However, existing enterprise tooling wasn't built for the AI code assistant era. When an LLM refactors multi-file repositories in Cursor or Copilot, conventional stack traces don't tell you which human prompt caused the contract shift.
107
81
 
108
- 3. **The Shift**: The `getUserProfile` function now returns `user_id` instead of `userId`,
109
- causing `profile.userId` to evaluate to `undefined`.
82
+ `triage-trip` bridges that gap for every developer: a zero-overhead local CLI that brings automated root-cause isolation, Cursor prompt provenance, and in-memory patching right to your terminal.
110
83
 
111
- 4. **Patch**:
112
- --- a/app.js
113
- +++ b/app.js
114
- @@ -6,2 +6,2 @@
115
- - console.log(`Loading dashboard for user ID: ${profile.userId.toUpperCase()}`);
116
- + console.log(`Loading dashboard for user ID: ${profile.user_id.toUpperCase()}`);
84
+ ## Security & Privacy
85
+ - **Read-Only SQLite Access:** `triage-trip` reads local IDE databases (`state.vscdb`) in strict read-only mode (`readonly: true`). It never modifies editor settings or session files.
86
+ - **Direct LLM Communication:** Your repository context and diffs are sent directly to Anthropic or OpenAI using your own local environment API keys. No intermediate servers, telemetry trackers, or external logging daemons.
117
87
 
118
- ```
88
+ ## Compatibility
89
+ - **IDEs:** Cursor (Full prompt provenance support via local Composer storage), VS Code / Windsurf (Git diff & contract triage)
90
+ - **Platforms:** macOS, Linux, Windows (WSL / PowerShell)
91
+ - **Runtime:** Node.js `>= 18.0.0`
119
92
 
120
- ---
93
+ ## CLI Options
121
94
 
122
- ## Project Structure
95
+ | Flag | Default | Description |
96
+ | :--- | :--- | :--- |
97
+ | `-c, --commits <num>` | `5` | Number of recent commits to inspect alongside unstaged working diffs |
98
+ | `-y, --yes` | `false` | Automatically apply the proposed patch without an interactive prompt |
99
+ | `-V, --version` | | Display current version |
100
+ | `-h, --help` | | Display help and usage options |
123
101
 
124
- ```text
125
- ai-blackbox/
126
- ├── bin/
127
- │ └── cli.js # CLI commands & terminal output formatting (Commander, Ora, Picocolors)
128
- ├── src/
129
- │ ├── git.js # Working tree and commit diff extraction (simple-git)
130
- │ ├── cursor.js # Cursor SQLite database parser (better-sqlite3)
131
- │ └── triage.js # OpenAI triage diagnostic engine (GPT-4o)
132
- ├── package.json
133
- └── README.md
102
+ ## Development
134
103
 
135
- ```
104
+ ```bash
105
+ git clone [https://github.com/arules3/triage-trip.git](https://github.com/arules3/triage-trip.git)
106
+ cd triage-trip
107
+ npm install
108
+ npm link
136
109
 
137
- ---
138
110
 
139
- ## Roadmap
111
+ ## Current Status & Roadmap
140
112
 
141
- * [x] Git diff & commit stream extraction
142
- * [x] Local SQLite prompt extraction for Cursor Composer
143
- * [x] OpenAI regression diagnostic engine
144
- * [ ] Auto-patch application flag (`--fix` via `git apply`)
145
- * [ ] Multi-IDE adapters (GitHub Copilot Chat, Windsurf Cascade, Claude Code)
146
- * [ ] GitHub Action runner for automated PR failure triage
113
+ ### What's Working Today (`v0.1.0`)
114
+ - **Git Context Analysis:** Reads unstaged working tree diffs and recent commit logs.
115
+ - **Cursor Prompt Provenance:** Direct read-only extraction from local `state.vscdb` to link regressions to specific Composer prompts.
116
+ - **Root Cause Isolation:** LLM-assisted semantic contract shift detection.
117
+ - **Interactive Patching:** Clean, in-memory `git apply` with user confirmation prompts (`--yes` fallback).
package/bin/cli.js CHANGED
@@ -24,25 +24,32 @@ program
24
24
  .option('-c, --commits <number>', 'Number of recent commits to analyze', '5')
25
25
  .option('-y, --yes', 'Automatically apply the patch without asking', false)
26
26
  .action(async (errorInput, options) => {
27
- console.log(pc.bold(pc.cyan('\n■ AI-Blackbox Triage Engine\n')));
27
+ console.log(pc.bold(pc.cyan('\n■ Triage-Trip Engine\n')));
28
28
 
29
29
  const spinner = ora('Reading recent git changes & Cursor prompts...').start();
30
30
  let diagnosis = '';
31
-
31
+
32
32
  try {
33
33
  const commitCount = parseInt(options.commits, 10);
34
34
  const [changes, prompts] = await Promise.all([
35
- getRecentChanges(commitCount),
35
+ getRecentChanges(commitCount, errorInput),
36
36
  Promise.resolve().then(() => getRecentCursorPrompts(10))
37
37
  ]);
38
38
 
39
39
  spinner.text = 'Isolating structural contract shifts and AST regressions...';
40
- diagnosis = await triageRegression(errorInput, changes, prompts);
40
+ diagnosis = await triageRegression(errorInput, changes, prompts, () => {
41
+ // Stop spinner before the first streamed token so ora doesn't garble stdout
42
+ spinner.stop();
43
+ process.stdout.write('\n');
44
+ });
41
45
 
42
- spinner.succeed(pc.green('Root cause isolated.'));
43
- console.log('\n' + diagnosis + '\n');
46
+ console.log('\n' + pc.green('✓ Root cause isolated.') + '\n');
44
47
  } catch (err) {
45
- spinner.fail(pc.red('Triage failed: ' + err.message));
48
+ if (spinner.isSpinning) {
49
+ spinner.fail(pc.red('Triage failed: ' + err.message));
50
+ } else {
51
+ console.error(pc.red('\nTriage failed: ' + err.message));
52
+ }
46
53
  process.exit(1);
47
54
  }
48
55
 
@@ -60,16 +67,21 @@ program
60
67
  }
61
68
 
62
69
  if (shouldApply) {
63
- const patchSpinner = ora('Applying patch via git apply...').start();
70
+ const patchSpinner = ora('Applying patch to working tree...').start();
64
71
  try {
65
- await applyDiff(patch);
72
+ const res = await applyDiff(patch);
66
73
  patchSpinner.succeed(pc.green('Patch applied successfully to working tree.'));
74
+ if (res?.modifiedFiles && res.modifiedFiles.length > 0) {
75
+ console.log(pc.dim(`Modified: ${res.modifiedFiles.join(', ')}`));
76
+ }
67
77
  console.log(pc.dim('Run `git diff` to review or `git commit` to save the fix.\n'));
68
78
  } catch (err) {
69
- patchSpinner.fail(pc.red(err.message));
79
+ patchSpinner.fail(pc.red('Patch application failed: ' + err.message));
70
80
  }
71
81
  }
82
+ } else {
83
+ console.log(pc.dim('ℹ No executable patch block was detected in the diagnostic report.\n'));
72
84
  }
73
85
  });
74
86
 
75
- program.parse(process.argv);
87
+ program.parse(process.argv);
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "triage-trip",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
4
4
  "description": "Autonomous CLI to pinpoint and patch regressions introduced by AI code assistants",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
7
+ "test": "node test/patch.test.js && node test/simulation.test.js && node test/streaming.test.js",
8
+ "test:sim": "node test/simulation.test.js",
9
+ "test:stream": "node test/streaming.test.js"
8
10
  },
9
11
  "keywords": [
10
12
  "cursor",
package/src/git.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { simpleGit } from 'simple-git';
2
+ import fs from 'fs';
3
+ import path from 'path';
2
4
 
3
5
  const git = simpleGit();
4
6
 
@@ -13,7 +15,126 @@ function sanitizeDiff(diffStr, maxChars = 4000) {
13
15
  return diffStr;
14
16
  }
15
17
 
16
- export async function getRecentChanges(maxCommits = 3) {
18
+ const BINARY_EXTENSIONS = new Set([
19
+ '.gif', '.png', '.jpg', '.jpeg', '.cast', '.ico', '.pdf', '.zip',
20
+ '.tar', '.gz', '.db', '.sqlite', '.sqlite3', '.bin', '.wasm'
21
+ ]);
22
+
23
+ /**
24
+ * Collects active working copy text for relevant files with line numbers.
25
+ * Gives the LLM exact source of truth for current file state, eliminating hallucinated diffs.
26
+ */
27
+ function collectActiveFiles(status, commitDiffs, errorInput = '', maxFiles = 5) {
28
+ const candidates = new Set();
29
+
30
+ // 1. Extract file paths from error input / stack trace
31
+ if (errorInput) {
32
+ const fileMatches = errorInput.match(/(?:[a-zA-Z0-9_\-\.\/]+\/)?([a-zA-Z0-9_\-\.]+\.[a-zA-Z0-9]+)/g) || [];
33
+ for (const match of fileMatches) {
34
+ const cleanPath = match.replace(/^(?:file:\/\/\/|\/)/, '');
35
+ candidates.add(cleanPath);
36
+ candidates.add(path.basename(cleanPath));
37
+ }
38
+ }
39
+
40
+ // 2. Working tree files from git status
41
+ const statusFiles = [
42
+ ...(status.modified || []),
43
+ ...(status.staged || []),
44
+ ...(status.created || []),
45
+ ...(status.not_added || []),
46
+ ];
47
+ for (const f of statusFiles) candidates.add(f);
48
+
49
+ // 3. Files from latest commit diff
50
+ if (commitDiffs.length > 0 && commitDiffs[0].diff) {
51
+ const diffFiles = commitDiffs[0].diff.match(/(?:---|\+\+\+)\s+[ab]\/([^\s\n]+)/g) || [];
52
+ for (const df of diffFiles) {
53
+ const p = df.replace(/^(?:---|\+\+\+)\s+[ab]\//, '');
54
+ candidates.add(p);
55
+ }
56
+ }
57
+
58
+ const activeFiles = {};
59
+ let count = 0;
60
+
61
+ for (const candidate of candidates) {
62
+ if (count >= maxFiles) break;
63
+
64
+ // Resolve file
65
+ let fullPath = path.resolve(process.cwd(), candidate);
66
+ if (!fs.existsSync(fullPath)) {
67
+ // Try resolving by basename in workspace
68
+ const baseName = path.basename(candidate);
69
+ const found = findCandidateFile(process.cwd(), baseName);
70
+ if (found) fullPath = found;
71
+ else continue;
72
+ }
73
+
74
+ const ext = path.extname(fullPath).toLowerCase();
75
+ if (BINARY_EXTENSIONS.has(ext)) continue;
76
+ if (fullPath.includes('node_modules') || fullPath.includes('.git')) continue;
77
+
78
+ try {
79
+ const stat = fs.statSync(fullPath);
80
+ if (!stat.isFile() || stat.size > 50000) continue;
81
+
82
+ const content = fs.readFileSync(fullPath, 'utf8');
83
+ const relPath = path.relative(process.cwd(), fullPath);
84
+
85
+ const lines = content.split('\n');
86
+ const formatted = lines
87
+ .slice(0, 150)
88
+ .map((line, idx) => `${idx + 1}| ${line}`)
89
+ .join('\n');
90
+
91
+ activeFiles[relPath] = formatted;
92
+ count++;
93
+
94
+ // Also inspect relative local imports for JS/TS
95
+ if (['.js', '.ts', '.jsx', '.tsx', '.mjs'].includes(ext)) {
96
+ const importMatches = content.match(/(?:import|require)\s*\(?['"](\.[^'"]+)['"]/g) || [];
97
+ for (const imp of importMatches) {
98
+ const importRel = imp.match(/['"](\.[^'"]+)['"]/)?.[1];
99
+ if (importRel) {
100
+ const dir = path.dirname(fullPath);
101
+ let importedPath = path.resolve(dir, importRel);
102
+ if (!fs.existsSync(importedPath) && fs.existsSync(importedPath + '.js')) {
103
+ importedPath = importedPath + '.js';
104
+ }
105
+ if (fs.existsSync(importedPath) && !activeFiles[path.relative(process.cwd(), importedPath)]) {
106
+ candidates.add(path.relative(process.cwd(), importedPath));
107
+ }
108
+ }
109
+ }
110
+ }
111
+ } catch {}
112
+ }
113
+
114
+ return activeFiles;
115
+ }
116
+
117
+ function findCandidateFile(dir, baseName, depth = 0) {
118
+ if (depth > 4) return null;
119
+ const ignored = new Set(['node_modules', '.git', 'dist', 'build', '.cache', 'coverage']);
120
+
121
+ try {
122
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
123
+ for (const ent of entries) {
124
+ if (ent.isDirectory()) {
125
+ if (!ignored.has(ent.name)) {
126
+ const found = findCandidateFile(path.join(dir, ent.name), baseName, depth + 1);
127
+ if (found) return found;
128
+ }
129
+ } else if (ent.isFile() && ent.name === baseName) {
130
+ return path.join(dir, ent.name);
131
+ }
132
+ }
133
+ } catch {}
134
+ return null;
135
+ }
136
+
137
+ export async function getRecentChanges(maxCommits = 3, errorInput = '') {
17
138
  const isRepo = await git.checkIsRepo();
18
139
  if (!isRepo) {
19
140
  throw new Error('Not inside a valid Git repository.');
@@ -27,6 +148,9 @@ export async function getRecentChanges(maxCommits = 3) {
27
148
  ':!yarn.lock',
28
149
  ':!pnpm-lock.yaml',
29
150
  ':!bun.lockb',
151
+ ':!assets/*',
152
+ ':!*.gif',
153
+ ':!*.cast',
30
154
  ];
31
155
 
32
156
  const unstagedDiff = await git.diff(['--', '.', ...lockfileIgnores]);
@@ -58,10 +182,13 @@ export async function getRecentChanges(maxCommits = 3) {
58
182
  })
59
183
  );
60
184
 
185
+ const activeFileContents = collectActiveFiles(status, commitDiffs, errorInput);
186
+
61
187
  return {
62
188
  branch: status.current,
63
- untrackedFiles: status.not_added, // Tells the LLM if newly created files exist
189
+ untrackedFiles: status.not_added,
64
190
  workingDiff: sanitizeDiff(combinedWorkingDiff) || null,
65
191
  recentCommits: commitDiffs,
192
+ activeFileContents,
66
193
  };
67
- }
194
+ }