@warpmetrics/coder 0.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.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/bin/init.js +178 -0
- package/defaults/agent-implement.yml +74 -0
- package/defaults/agent-revise.yml +81 -0
- package/defaults/scripts/check-revision-limit.js +47 -0
- package/defaults/scripts/pipeline-outcome.js +33 -0
- package/defaults/scripts/pipeline-start.js +40 -0
- package/defaults/scripts/pipeline.js +110 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WarpMetrics
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @warpmetrics/coder
|
|
2
|
+
|
|
3
|
+
Agent pipeline for implementing GitHub issues with Claude Code. Label an issue with `agent` and it gets implemented, reviewed, and revised automatically.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @warpmetrics/coder init
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This will:
|
|
12
|
+
1. Set up `ANTHROPIC_API_KEY` and `WARPMETRICS_API_KEY` as GitHub secrets
|
|
13
|
+
2. Add two workflow files to `.github/workflows/`
|
|
14
|
+
3. Add pipeline scripts to `.github/scripts/`
|
|
15
|
+
4. Register outcome classifications with WarpMetrics
|
|
16
|
+
|
|
17
|
+
## How It Works
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Issue labeled "agent"
|
|
21
|
+
→ agent-implement.yml runs Claude Code Action
|
|
22
|
+
→ Claude reads the issue, creates a branch, implements, opens PR
|
|
23
|
+
→ warp-review reviews the PR (if installed)
|
|
24
|
+
→ agent-revise.yml applies feedback and pushes fixes
|
|
25
|
+
→ Loop until approved or revision limit (3) reached
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Every step is instrumented with [WarpMetrics](https://warpmetrics.com) — you get runs, groups, and outcomes tracking the full pipeline.
|
|
29
|
+
|
|
30
|
+
## Workflows
|
|
31
|
+
|
|
32
|
+
### agent-implement.yml
|
|
33
|
+
|
|
34
|
+
Triggered when an issue is labeled `agent`. Creates a branch `agent/issue-{number}`, implements the issue, and opens a PR.
|
|
35
|
+
|
|
36
|
+
### agent-revise.yml
|
|
37
|
+
|
|
38
|
+
Triggered when `github-actions[bot]` submits a review with comments (i.e., warp-review feedback). Applies the review feedback and pushes to the same branch. Stops after 3 revision attempts.
|
|
39
|
+
|
|
40
|
+
## Outcome Classifications
|
|
41
|
+
|
|
42
|
+
| Name | Classification |
|
|
43
|
+
|------|---------------|
|
|
44
|
+
| PR Created | success |
|
|
45
|
+
| Fixes Applied | success |
|
|
46
|
+
| Issue Understood | success |
|
|
47
|
+
| Needs Clarification | neutral |
|
|
48
|
+
| Needs Human | neutral |
|
|
49
|
+
| Implementation Failed | failure |
|
|
50
|
+
| Tests Failed | failure |
|
|
51
|
+
| Revision Failed | failure |
|
|
52
|
+
| Max Retries | failure |
|
|
53
|
+
|
|
54
|
+
## Pairing with warp-review
|
|
55
|
+
|
|
56
|
+
For the full implement → review → revise loop, install [warp-review](https://github.com/warpmetrics/warp-review):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx @warpmetrics/review init
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
package/bin/init.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createInterface } from 'readline';
|
|
4
|
+
import { existsSync, mkdirSync, copyFileSync, readdirSync } from 'fs';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const defaultsDir = join(__dirname, '..', 'defaults');
|
|
11
|
+
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
|
|
14
|
+
function ask(question) {
|
|
15
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function log(msg) {
|
|
19
|
+
console.log(msg);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
log('');
|
|
24
|
+
log(' warp-coder \u2014 Agent pipeline for implementing GitHub issues');
|
|
25
|
+
log('');
|
|
26
|
+
|
|
27
|
+
// 1. Anthropic API key
|
|
28
|
+
const anthropicKey = await ask(' ? Anthropic API key: ');
|
|
29
|
+
if (!anthropicKey.startsWith('sk-ant-')) {
|
|
30
|
+
log(' \u26a0 Warning: key doesn\'t start with sk-ant- \u2014 make sure this is a valid Anthropic API key');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. WarpMetrics API key
|
|
34
|
+
const wmKey = await ask(' ? WarpMetrics API key (get one at warpmetrics.com/app/api-keys): ');
|
|
35
|
+
if (!wmKey.startsWith('wm_')) {
|
|
36
|
+
log(' \u26a0 Warning: key doesn\'t start with wm_ \u2014 make sure this is a valid WarpMetrics API key');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
log('');
|
|
40
|
+
|
|
41
|
+
// 3. Set GitHub secrets
|
|
42
|
+
let ghAvailable = false;
|
|
43
|
+
try {
|
|
44
|
+
execSync('gh --version', { stdio: 'ignore' });
|
|
45
|
+
ghAvailable = true;
|
|
46
|
+
} catch {
|
|
47
|
+
ghAvailable = false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (ghAvailable) {
|
|
51
|
+
log(' Setting GitHub secrets...');
|
|
52
|
+
try {
|
|
53
|
+
execSync('gh secret set ANTHROPIC_API_KEY', { input: anthropicKey, stdio: ['pipe', 'ignore', 'ignore'] });
|
|
54
|
+
log(' \u2713 ANTHROPIC_API_KEY set');
|
|
55
|
+
} catch (e) {
|
|
56
|
+
log(` \u2717 Failed to set ANTHROPIC_API_KEY: ${e.message}`);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
execSync('gh secret set WARPMETRICS_API_KEY', { input: wmKey, stdio: ['pipe', 'ignore', 'ignore'] });
|
|
60
|
+
log(' \u2713 WARPMETRICS_API_KEY set');
|
|
61
|
+
} catch (e) {
|
|
62
|
+
log(` \u2717 Failed to set WARPMETRICS_API_KEY: ${e.message}`);
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
log(' gh (GitHub CLI) not found. Set these secrets manually:');
|
|
66
|
+
log('');
|
|
67
|
+
log(' gh secret set ANTHROPIC_API_KEY');
|
|
68
|
+
log(' gh secret set WARPMETRICS_API_KEY');
|
|
69
|
+
log(' (gh will prompt for the value interactively)');
|
|
70
|
+
}
|
|
71
|
+
log('');
|
|
72
|
+
|
|
73
|
+
// 4. Copy workflows
|
|
74
|
+
await copyWorkflow('agent-implement.yml');
|
|
75
|
+
await copyWorkflow('agent-revise.yml');
|
|
76
|
+
|
|
77
|
+
// 5. Copy scripts
|
|
78
|
+
await copyScripts();
|
|
79
|
+
|
|
80
|
+
log('');
|
|
81
|
+
|
|
82
|
+
// 6. Register outcome classifications
|
|
83
|
+
log(' Registering outcome classifications with WarpMetrics...');
|
|
84
|
+
const classifications = [
|
|
85
|
+
{ name: 'PR Created', classification: 'success' },
|
|
86
|
+
{ name: 'Fixes Applied', classification: 'success' },
|
|
87
|
+
{ name: 'Issue Understood', classification: 'success' },
|
|
88
|
+
{ name: 'Needs Clarification', classification: 'neutral' },
|
|
89
|
+
{ name: 'Needs Human', classification: 'neutral' },
|
|
90
|
+
{ name: 'Implementation Failed', classification: 'failure' },
|
|
91
|
+
{ name: 'Tests Failed', classification: 'failure' },
|
|
92
|
+
{ name: 'Revision Failed', classification: 'failure' },
|
|
93
|
+
{ name: 'Max Retries', classification: 'failure' },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
let classOk = true;
|
|
97
|
+
for (const { name, classification } of classifications) {
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch(`https://api.warpmetrics.com/v1/outcomes/classifications/${encodeURIComponent(name)}`, {
|
|
100
|
+
method: 'PUT',
|
|
101
|
+
headers: {
|
|
102
|
+
Authorization: `Bearer ${wmKey}`,
|
|
103
|
+
'Content-Type': 'application/json',
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify({ classification }),
|
|
106
|
+
});
|
|
107
|
+
if (!res.ok) {
|
|
108
|
+
classOk = false;
|
|
109
|
+
console.warn(` \u26a0 Failed to set classification ${name}: ${res.status}`);
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
classOk = false;
|
|
113
|
+
console.warn(` \u26a0 Failed to set classification ${name}: ${e.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (classOk) {
|
|
117
|
+
log(' \u2713 Outcomes configured');
|
|
118
|
+
} else {
|
|
119
|
+
log(' \u26a0 Some classifications failed \u2014 you can set them manually in the WarpMetrics dashboard');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 7. Check for warp-review
|
|
123
|
+
log('');
|
|
124
|
+
if (!existsSync('.github/workflows/warp-review.yml')) {
|
|
125
|
+
log(' \u26a0 warp-review not found. Install it for automated code reviews on agent PRs:');
|
|
126
|
+
log(' npx @warpmetrics/review init');
|
|
127
|
+
} else {
|
|
128
|
+
log(' \u2713 warp-review detected \u2014 agent PRs will be reviewed automatically');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 8. Print next steps
|
|
132
|
+
log('');
|
|
133
|
+
log(' Done! Next steps:');
|
|
134
|
+
log(' 1. git add .github/workflows .github/scripts');
|
|
135
|
+
log(' 2. git commit -m "Add warp-coder agent pipeline"');
|
|
136
|
+
log(' 3. Label a GitHub issue with "agent" to trigger implementation');
|
|
137
|
+
log(' 4. View pipeline analytics at https://app.warpmetrics.com');
|
|
138
|
+
log('');
|
|
139
|
+
|
|
140
|
+
rl.close();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function copyWorkflow(filename) {
|
|
144
|
+
const dest = `.github/workflows/${filename}`;
|
|
145
|
+
if (existsSync(dest)) {
|
|
146
|
+
const overwrite = await ask(` ${filename} already exists. Overwrite? (y/N): `);
|
|
147
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
148
|
+
log(` Skipping ${filename}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
mkdirSync('.github/workflows', { recursive: true });
|
|
153
|
+
copyFileSync(join(defaultsDir, filename), dest);
|
|
154
|
+
log(` \u2713 ${dest} created`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function copyScripts() {
|
|
158
|
+
const scriptsDir = '.github/scripts';
|
|
159
|
+
if (existsSync(scriptsDir)) {
|
|
160
|
+
const overwrite = await ask(' .github/scripts/ already exists. Overwrite? (y/N): ');
|
|
161
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
162
|
+
log(' Skipping scripts');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
mkdirSync(scriptsDir, { recursive: true });
|
|
167
|
+
const srcDir = join(defaultsDir, 'scripts');
|
|
168
|
+
for (const file of readdirSync(srcDir)) {
|
|
169
|
+
copyFileSync(join(srcDir, file), join(scriptsDir, file));
|
|
170
|
+
}
|
|
171
|
+
log(` \u2713 .github/scripts/ created (${readdirSync(srcDir).length} files)`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
main().catch(err => {
|
|
175
|
+
console.error('init failed:', err.message);
|
|
176
|
+
process.exitCode = 1;
|
|
177
|
+
rl.close();
|
|
178
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
name: Agent — Implement Issue
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issues:
|
|
5
|
+
types: [labeled]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
pull-requests: write
|
|
10
|
+
issues: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
implement:
|
|
14
|
+
if: github.event.label.name == 'agent'
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
timeout-minutes: 30
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: 22
|
|
24
|
+
|
|
25
|
+
- name: Start pipeline
|
|
26
|
+
run: node .github/scripts/pipeline-start.js
|
|
27
|
+
env:
|
|
28
|
+
WARPMETRICS_API_KEY: ${{ secrets.WARPMETRICS_API_KEY }}
|
|
29
|
+
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
30
|
+
STEP: implement
|
|
31
|
+
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
32
|
+
ISSUE_TITLE: ${{ github.event.issue.title }}
|
|
33
|
+
|
|
34
|
+
- name: Implement issue
|
|
35
|
+
id: agent
|
|
36
|
+
uses: anthropics/claude-code-action@v1
|
|
37
|
+
with:
|
|
38
|
+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
39
|
+
direct_prompt: |
|
|
40
|
+
You are working on the repository ${{ github.repository }}.
|
|
41
|
+
|
|
42
|
+
Implement the following GitHub issue:
|
|
43
|
+
|
|
44
|
+
**#${{ github.event.issue.number }}: ${{ github.event.issue.title }}**
|
|
45
|
+
|
|
46
|
+
${{ github.event.issue.body }}
|
|
47
|
+
|
|
48
|
+
Steps:
|
|
49
|
+
1. Read the codebase to understand relevant context
|
|
50
|
+
2. Create a new branch: agent/issue-${{ github.event.issue.number }}
|
|
51
|
+
3. Implement the changes
|
|
52
|
+
4. Run tests to verify nothing is broken
|
|
53
|
+
5. Commit with a clear message
|
|
54
|
+
6. Push the branch and open a pull request
|
|
55
|
+
7. Include "Closes #${{ github.event.issue.number }}" in the PR body
|
|
56
|
+
|
|
57
|
+
If the issue is unclear or you cannot implement it, explain what is missing.
|
|
58
|
+
|
|
59
|
+
- name: Record outcome
|
|
60
|
+
if: always()
|
|
61
|
+
run: node .github/scripts/pipeline-outcome.js
|
|
62
|
+
env:
|
|
63
|
+
WARPMETRICS_API_KEY: ${{ secrets.WARPMETRICS_API_KEY }}
|
|
64
|
+
STEP: implement
|
|
65
|
+
STATUS: ${{ steps.agent.outcome }}
|
|
66
|
+
|
|
67
|
+
- name: Comment on failure
|
|
68
|
+
if: failure()
|
|
69
|
+
run: |
|
|
70
|
+
gh issue comment ${{ github.event.issue.number }} \
|
|
71
|
+
--body "Agent failed to implement this issue. See the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})."
|
|
72
|
+
gh issue edit ${{ github.event.issue.number }} --add-label "agent-failed" --remove-label "agent"
|
|
73
|
+
env:
|
|
74
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
name: Agent — Revise PR
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request_review:
|
|
5
|
+
types: [submitted]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
pull-requests: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
revise:
|
|
13
|
+
# Only trigger when warp-review (bot) submits a review with comments
|
|
14
|
+
# and the PR was created by the agent (has "agent" label on the linked issue)
|
|
15
|
+
if: >
|
|
16
|
+
github.event.review.user.login == 'github-actions[bot]' &&
|
|
17
|
+
github.event.review.state == 'COMMENTED'
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
timeout-minutes: 20
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
with:
|
|
24
|
+
ref: ${{ github.event.pull_request.head.ref }}
|
|
25
|
+
|
|
26
|
+
- uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: 22
|
|
29
|
+
|
|
30
|
+
- name: Check revision limit
|
|
31
|
+
id: check
|
|
32
|
+
run: node .github/scripts/check-revision-limit.js
|
|
33
|
+
env:
|
|
34
|
+
WARPMETRICS_API_KEY: ${{ secrets.WARPMETRICS_API_KEY }}
|
|
35
|
+
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
36
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
37
|
+
|
|
38
|
+
- name: Start pipeline
|
|
39
|
+
if: steps.check.outputs.should_revise == 'true'
|
|
40
|
+
run: node .github/scripts/pipeline-start.js
|
|
41
|
+
env:
|
|
42
|
+
WARPMETRICS_API_KEY: ${{ secrets.WARPMETRICS_API_KEY }}
|
|
43
|
+
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
44
|
+
STEP: revise
|
|
45
|
+
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
46
|
+
|
|
47
|
+
- name: Apply review feedback
|
|
48
|
+
id: agent
|
|
49
|
+
if: steps.check.outputs.should_revise == 'true'
|
|
50
|
+
uses: anthropics/claude-code-action@v1
|
|
51
|
+
with:
|
|
52
|
+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
53
|
+
direct_prompt: |
|
|
54
|
+
You are working on PR #${{ github.event.pull_request.number }} in ${{ github.repository }}.
|
|
55
|
+
|
|
56
|
+
A code review has been submitted with comments. Your job:
|
|
57
|
+
|
|
58
|
+
1. Read all review comments on this PR
|
|
59
|
+
2. Apply the suggested fixes
|
|
60
|
+
3. Run tests to make sure everything passes
|
|
61
|
+
4. Commit the fixes with a message like "Address review feedback"
|
|
62
|
+
5. Push to the same branch
|
|
63
|
+
|
|
64
|
+
Do NOT open a new PR — just push to the existing branch.
|
|
65
|
+
|
|
66
|
+
- name: Record outcome
|
|
67
|
+
if: always() && steps.check.outputs.should_revise == 'true'
|
|
68
|
+
run: node .github/scripts/pipeline-outcome.js
|
|
69
|
+
env:
|
|
70
|
+
WARPMETRICS_API_KEY: ${{ secrets.WARPMETRICS_API_KEY }}
|
|
71
|
+
STEP: revise
|
|
72
|
+
STATUS: ${{ steps.agent.outcome }}
|
|
73
|
+
|
|
74
|
+
- name: Request human review
|
|
75
|
+
if: steps.check.outputs.should_revise == 'false'
|
|
76
|
+
run: |
|
|
77
|
+
gh pr comment ${{ github.event.pull_request.number }} \
|
|
78
|
+
--body "Agent has reached the revision limit (${{ steps.check.outputs.revision_count }} attempts). Requesting human review."
|
|
79
|
+
gh pr edit ${{ github.event.pull_request.number }} --add-label "needs-human"
|
|
80
|
+
env:
|
|
81
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Agent pipeline — check if we should attempt another revision.
|
|
2
|
+
// Queries WarpMetrics for previous "revise" runs on this PR.
|
|
3
|
+
// Sets GitHub Actions outputs: should_revise, revision_count.
|
|
4
|
+
|
|
5
|
+
import { appendFileSync } from 'fs';
|
|
6
|
+
import { findRuns } from './pipeline.js';
|
|
7
|
+
|
|
8
|
+
const apiKey = process.env.WARPMETRICS_API_KEY;
|
|
9
|
+
const prNumber = process.env.PR_NUMBER;
|
|
10
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
11
|
+
const MAX_REVISIONS = 3;
|
|
12
|
+
|
|
13
|
+
function setOutput(key, value) {
|
|
14
|
+
if (process.env.GITHUB_OUTPUT) {
|
|
15
|
+
appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`);
|
|
16
|
+
}
|
|
17
|
+
console.log(`${key}=${value}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!apiKey) {
|
|
21
|
+
setOutput('should_revise', 'true');
|
|
22
|
+
setOutput('revision_count', '0');
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const runs = await findRuns(apiKey, 'agent-pipeline');
|
|
28
|
+
const revisions = runs.filter(r =>
|
|
29
|
+
r.opts?.step === 'revise' &&
|
|
30
|
+
r.opts?.pr_number === String(prNumber) &&
|
|
31
|
+
r.opts?.repo === repo
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const count = revisions.length;
|
|
35
|
+
const shouldRevise = count < MAX_REVISIONS;
|
|
36
|
+
|
|
37
|
+
setOutput('should_revise', String(shouldRevise));
|
|
38
|
+
setOutput('revision_count', String(count));
|
|
39
|
+
|
|
40
|
+
console.log(shouldRevise
|
|
41
|
+
? `Revision ${count + 1}/${MAX_REVISIONS}`
|
|
42
|
+
: `Revision limit reached (${count}/${MAX_REVISIONS})`);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.warn(`Revision check failed: ${err.message} — allowing revision`);
|
|
45
|
+
setOutput('should_revise', 'true');
|
|
46
|
+
setOutput('revision_count', '0');
|
|
47
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Agent pipeline — record an outcome after the agent step.
|
|
2
|
+
|
|
3
|
+
import { generateId, sendEvents, loadState } from './pipeline.js';
|
|
4
|
+
|
|
5
|
+
const apiKey = process.env.WARPMETRICS_API_KEY;
|
|
6
|
+
const status = process.env.STATUS; // "success" or "failure"
|
|
7
|
+
const step = process.env.STEP;
|
|
8
|
+
|
|
9
|
+
if (!apiKey) process.exit(0);
|
|
10
|
+
|
|
11
|
+
const state = loadState();
|
|
12
|
+
if (!state) {
|
|
13
|
+
console.warn('No pipeline state — skipping outcome');
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const names = {
|
|
18
|
+
implement: { success: 'PR Created', failure: 'Implementation Failed' },
|
|
19
|
+
revise: { success: 'Fixes Applied', failure: 'Revision Failed' },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const name = names[step]?.[status] || `${step}: ${status}`;
|
|
23
|
+
const id = generateId('oc');
|
|
24
|
+
const now = new Date().toISOString();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await sendEvents(apiKey, {
|
|
28
|
+
outcomes: [{ id, refId: state.groupId, name, opts: { status, step }, timestamp: now }],
|
|
29
|
+
});
|
|
30
|
+
console.log(`Outcome: ${name} (${id})`);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.warn(`Failed to record outcome: ${err.message}`);
|
|
33
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Agent pipeline — create a WarpMetrics run + group before the agent step.
|
|
2
|
+
|
|
3
|
+
import { generateId, sendEvents, registerClassifications, saveState } from './pipeline.js';
|
|
4
|
+
|
|
5
|
+
const apiKey = process.env.WARPMETRICS_API_KEY;
|
|
6
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
7
|
+
const step = process.env.STEP; // "implement" or "revise"
|
|
8
|
+
const issueNumber = process.env.ISSUE_NUMBER || null;
|
|
9
|
+
const issueTitle = process.env.ISSUE_TITLE || null;
|
|
10
|
+
const prNumber = process.env.PR_NUMBER || null;
|
|
11
|
+
|
|
12
|
+
if (!apiKey) {
|
|
13
|
+
console.log('WARPMETRICS_API_KEY not set — skipping');
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const runId = generateId('run');
|
|
18
|
+
const groupId = generateId('grp');
|
|
19
|
+
const now = new Date().toISOString();
|
|
20
|
+
|
|
21
|
+
const opts = { repo, step };
|
|
22
|
+
if (issueNumber) opts.issue = issueNumber;
|
|
23
|
+
if (issueTitle) opts.title = issueTitle;
|
|
24
|
+
if (prNumber) opts.pr_number = prNumber;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await sendEvents(apiKey, {
|
|
28
|
+
runs: [{ id: runId, label: 'agent-pipeline', opts, refId: null, timestamp: now }],
|
|
29
|
+
groups: [{ id: groupId, label: step, opts: { triggered_at: now }, timestamp: now }],
|
|
30
|
+
links: [{ parentId: runId, childId: groupId, type: 'group', timestamp: now }],
|
|
31
|
+
});
|
|
32
|
+
console.log(`Pipeline run=${runId} group=${groupId} step=${step}`);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.warn(`Failed to start pipeline: ${err.message}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
saveState({ runId, groupId, step });
|
|
38
|
+
|
|
39
|
+
// Register outcome classifications on first run (idempotent)
|
|
40
|
+
await registerClassifications(apiKey).catch(() => {});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Agent pipeline — shared helpers for WarpMetrics instrumentation.
|
|
2
|
+
// Zero external dependencies — uses Node built-ins + global fetch.
|
|
3
|
+
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
6
|
+
|
|
7
|
+
const API_URL = 'https://api.warpmetrics.com';
|
|
8
|
+
const STATE_FILE = '.pipeline-state.json';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// ID generation
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export function generateId(prefix) {
|
|
15
|
+
const t = Date.now().toString(36).padStart(10, '0');
|
|
16
|
+
const r = crypto.randomUUID().replace(/-/g, '').slice(0, 16);
|
|
17
|
+
return `wm_${prefix}_${t}${r}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// WarpMetrics Events API (same wire format as @warpmetrics/warp)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export async function sendEvents(apiKey, batch) {
|
|
25
|
+
const full = {
|
|
26
|
+
runs: batch.runs || [],
|
|
27
|
+
groups: batch.groups || [],
|
|
28
|
+
calls: batch.calls || [],
|
|
29
|
+
links: batch.links || [],
|
|
30
|
+
outcomes: batch.outcomes || [],
|
|
31
|
+
acts: batch.acts || [],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const raw = JSON.stringify(full);
|
|
35
|
+
const body = JSON.stringify({ d: Buffer.from(raw, 'utf-8').toString('base64') });
|
|
36
|
+
|
|
37
|
+
const res = await fetch(`${API_URL}/v1/events`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: {
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
Authorization: `Bearer ${apiKey}`,
|
|
42
|
+
},
|
|
43
|
+
body,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const text = await res.text().catch(() => '');
|
|
48
|
+
throw new Error(`Events API ${res.status}: ${text}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// WarpMetrics Query API
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
export async function findRuns(apiKey, label, { limit = 20 } = {}) {
|
|
57
|
+
const params = new URLSearchParams({ label, limit: String(limit) });
|
|
58
|
+
const res = await fetch(`${API_URL}/v1/runs?${params}`, {
|
|
59
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) return [];
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
return data.data || [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Outcome classifications (idempotent PUT)
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
export async function registerClassifications(apiKey) {
|
|
71
|
+
const items = [
|
|
72
|
+
{ name: 'PR Created', classification: 'success' },
|
|
73
|
+
{ name: 'Fixes Applied', classification: 'success' },
|
|
74
|
+
{ name: 'Issue Understood', classification: 'success' },
|
|
75
|
+
{ name: 'Needs Clarification', classification: 'neutral' },
|
|
76
|
+
{ name: 'Needs Human', classification: 'neutral' },
|
|
77
|
+
{ name: 'Implementation Failed', classification: 'failure' },
|
|
78
|
+
{ name: 'Tests Failed', classification: 'failure' },
|
|
79
|
+
{ name: 'Revision Failed', classification: 'failure' },
|
|
80
|
+
{ name: 'Max Retries', classification: 'failure' },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
for (const { name, classification } of items) {
|
|
84
|
+
try {
|
|
85
|
+
await fetch(`${API_URL}/v1/outcomes/classifications/${encodeURIComponent(name)}`, {
|
|
86
|
+
method: 'PUT',
|
|
87
|
+
headers: {
|
|
88
|
+
Authorization: `Bearer ${apiKey}`,
|
|
89
|
+
'Content-Type': 'application/json',
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify({ classification }),
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
// Best-effort
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Cross-step state
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
export function saveState(state) {
|
|
104
|
+
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function loadState() {
|
|
108
|
+
if (!existsSync(STATE_FILE)) return null;
|
|
109
|
+
return JSON.parse(readFileSync(STATE_FILE, 'utf-8'));
|
|
110
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@warpmetrics/coder",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Agent pipeline for implementing GitHub issues with Claude Code. Powered by WarpMetrics.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"warp-coder": "./bin/init.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"release:patch": "npm version patch && git push origin main --tags",
|
|
11
|
+
"release:minor": "npm version minor && git push origin main --tags"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"defaults/",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/warpmetrics/warp-coder"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"agent",
|
|
26
|
+
"claude-code",
|
|
27
|
+
"github-action",
|
|
28
|
+
"warpmetrics",
|
|
29
|
+
"llm",
|
|
30
|
+
"code-generation"
|
|
31
|
+
]
|
|
32
|
+
}
|