@tarcisiopgs/lisa 0.9.5 → 1.0.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/README.md +106 -66
- package/dist/index.js +178 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
While the Ralphs of the world flooded GitHub with mindless agent loops — brute-forcing their way through issues with no context, no workflow awareness, and no regard for the mess they leave behind — Lisa takes a different approach. She reads the issue, understands the workspace, picks the right repo, creates the branch, validates her work, and opens the PR. Then she moves on to the next one. When there's nothing left to do, she stops.
|
|
8
8
|
|
|
9
|
-
Named after the smartest Simpson, Lisa is an autonomous issue resolver that connects your project tracker (Linear or Trello) to an AI coding agent (Claude Code, Gemini CLI, or OpenCode) and delivers pull requests via
|
|
9
|
+
Named after the smartest Simpson, Lisa is an autonomous issue resolver that connects your project tracker (Linear or Trello) to an AI coding agent (Claude Code, Gemini CLI, or OpenCode) and delivers pull requests via GitHub. No MCP servers. No prompt chains. No blind retries. Just structured, end-to-end execution.
|
|
10
10
|
|
|
11
11
|
## Why Lisa?
|
|
12
12
|
|
|
@@ -14,10 +14,12 @@ Most AI agent loops work like Ralph — they grab an issue, throw it at a model,
|
|
|
14
14
|
|
|
15
15
|
Lisa is deterministic. She follows a structured pipeline with clear stages (fetch, activate, implement, validate, PR, update) and stops when the work is done. This means:
|
|
16
16
|
|
|
17
|
-
- **Token efficiency** — Each issue gets one focused prompt with full context
|
|
18
|
-
- **Multi-repo awareness** — Lisa detects which repos the agent
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
17
|
+
- **Token efficiency** — Each issue gets one focused prompt with full context. No wasted retries, no speculative exploration, no idle polling.
|
|
18
|
+
- **Multi-repo awareness** — Lisa detects which repos the agent touched and creates one PR per repo. Monorepos with multiple packages just work.
|
|
19
|
+
- **Provider fallback** — Configure a chain of models (`claude → gemini → opencode`). Transient errors (429, quota, timeout) trigger the next provider; non-transient errors stop the chain.
|
|
20
|
+
- **Workflow integration** — Issues move through your board in real time (Backlog → In Progress → In Review). Your team always knows what's being worked on.
|
|
21
|
+
- **Self-healing** — Orphan issues (stuck in "In Progress" from interrupted runs) are automatically recovered on startup. Pre-push hook failures trigger the agent to fix and retry.
|
|
22
|
+
- **Guardrails** — Past failures are logged and injected into future prompts so the agent avoids repeating mistakes.
|
|
21
23
|
|
|
22
24
|
## Install
|
|
23
25
|
|
|
@@ -27,11 +29,9 @@ npm install -g @tarcisiopgs/lisa
|
|
|
27
29
|
|
|
28
30
|
## Environment Variables
|
|
29
31
|
|
|
30
|
-
Lisa calls external APIs directly. Set these in your shell profile (`~/.zshrc` or `~/.bashrc`):
|
|
31
|
-
|
|
32
32
|
```bash
|
|
33
|
-
# Required (
|
|
34
|
-
export GITHUB_TOKEN=""
|
|
33
|
+
# Required (at least one)
|
|
34
|
+
export GITHUB_TOKEN="" # or have `gh` CLI authenticated
|
|
35
35
|
|
|
36
36
|
# Required when source = linear
|
|
37
37
|
export LINEAR_API_KEY=""
|
|
@@ -41,24 +41,28 @@ export TRELLO_API_KEY=""
|
|
|
41
41
|
export TRELLO_TOKEN=""
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
The CLI will warn you if any required variable is missing.
|
|
45
|
-
|
|
46
44
|
## Quick Start
|
|
47
45
|
|
|
48
46
|
```bash
|
|
49
47
|
# Interactive setup
|
|
50
48
|
lisa init
|
|
51
49
|
|
|
52
|
-
# Run continuously
|
|
50
|
+
# Run continuously until all labeled issues are done
|
|
53
51
|
lisa run
|
|
54
52
|
|
|
55
53
|
# Single issue
|
|
56
54
|
lisa run --once
|
|
57
55
|
|
|
56
|
+
# Specific issue by identifier or URL
|
|
57
|
+
lisa run --issue INT-150
|
|
58
|
+
|
|
59
|
+
# Process up to N issues
|
|
60
|
+
lisa run --limit 5
|
|
61
|
+
|
|
58
62
|
# Preview without executing
|
|
59
63
|
lisa run --dry-run
|
|
60
64
|
|
|
61
|
-
# Override provider
|
|
65
|
+
# Override provider for a single run
|
|
62
66
|
lisa run --provider gemini --once
|
|
63
67
|
```
|
|
64
68
|
|
|
@@ -68,8 +72,13 @@ lisa run --provider gemini --once
|
|
|
68
72
|
|---------|-------------|
|
|
69
73
|
| `lisa run` | Run the agent loop |
|
|
70
74
|
| `lisa run --once` | Process a single issue |
|
|
75
|
+
| `lisa run --issue ID` | Process a specific issue by identifier or URL |
|
|
71
76
|
| `lisa run --limit N` | Process up to N issues |
|
|
72
77
|
| `lisa run --dry-run` | Preview without executing |
|
|
78
|
+
| `lisa run --provider NAME` | Override AI provider |
|
|
79
|
+
| `lisa run --label NAME` | Override label filter |
|
|
80
|
+
| `lisa run --json` | Output as JSON lines |
|
|
81
|
+
| `lisa run --quiet` | Suppress non-essential output |
|
|
73
82
|
| `lisa config` | Interactive config wizard |
|
|
74
83
|
| `lisa config --show` | Show current config |
|
|
75
84
|
| `lisa config --set key=value` | Set a config value |
|
|
@@ -86,106 +95,137 @@ lisa run --provider gemini --once
|
|
|
86
95
|
|
|
87
96
|
At least one provider must be installed and available in your PATH.
|
|
88
97
|
|
|
89
|
-
All providers
|
|
98
|
+
All providers use `child_process.spawn` with `sh -c`. Prompts are written to a temp file and passed via `$(cat file)` to avoid argument length limits. Output streams to both stdout and the session log file in real time.
|
|
90
99
|
|
|
91
|
-
|
|
100
|
+
### Fallback Chain
|
|
101
|
+
|
|
102
|
+
Configure multiple providers in the `models` array. Lisa tries them in order — transient errors (429, quota, timeout, network) trigger the next provider. Non-transient errors stop the chain immediately.
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
models:
|
|
106
|
+
- claude
|
|
107
|
+
- gemini
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If `models` is not set, Lisa uses the single `provider` field.
|
|
92
111
|
|
|
93
|
-
|
|
112
|
+
## Workflow Modes
|
|
94
113
|
|
|
95
|
-
### Branch
|
|
114
|
+
### Branch
|
|
96
115
|
|
|
97
116
|
The AI agent creates a branch directly in your current checkout, implements the changes, and pushes. Simple setup, works everywhere.
|
|
98
117
|
|
|
99
118
|
### Worktree
|
|
100
119
|
|
|
101
|
-
Lisa creates an isolated [git worktree](https://git-scm.com/docs/git-worktree) for each issue under `.worktrees/`. The
|
|
120
|
+
Lisa creates an isolated [git worktree](https://git-scm.com/docs/git-worktree) for each issue under `.worktrees/`. The agent works inside the worktree without touching your main checkout. After the PR is created, the worktree is cleaned up automatically.
|
|
121
|
+
|
|
122
|
+
In multi-repo workspaces, the agent selects the correct repository, creates the worktree, implements, and writes a `.lisa-manifest.json` with the repo path, branch name, and PR title. Lisa reads the manifest to push and create the PR.
|
|
102
123
|
|
|
103
124
|
Worktree mode is ideal when you want to keep working in the repo while Lisa resolves issues in the background.
|
|
104
125
|
|
|
105
126
|
## Configuration
|
|
106
127
|
|
|
107
|
-
Config lives in `.lisa/config.yaml
|
|
128
|
+
Config lives in `.lisa/config.yaml`. Run `lisa init` to create it interactively.
|
|
108
129
|
|
|
109
|
-
**Linear:**
|
|
110
130
|
```yaml
|
|
111
131
|
provider: claude
|
|
132
|
+
models:
|
|
133
|
+
- claude
|
|
134
|
+
- gemini
|
|
112
135
|
source: linear
|
|
113
|
-
workflow:
|
|
136
|
+
workflow: worktree
|
|
114
137
|
|
|
115
138
|
source_config:
|
|
116
139
|
team: Engineering
|
|
117
140
|
project: Web App
|
|
118
141
|
label: ready
|
|
119
|
-
pick_from:
|
|
142
|
+
pick_from: Backlog
|
|
120
143
|
in_progress: In Progress
|
|
121
144
|
done: In Review
|
|
122
145
|
|
|
123
|
-
github: cli
|
|
146
|
+
github: cli # "cli" (gh) or "token" (GITHUB_TOKEN)
|
|
124
147
|
workspace: .
|
|
148
|
+
base_branch: main
|
|
149
|
+
|
|
125
150
|
repos:
|
|
126
|
-
- name:
|
|
151
|
+
- name: my-api
|
|
152
|
+
path: ./api
|
|
153
|
+
base_branch: main
|
|
154
|
+
- name: my-app
|
|
127
155
|
path: ./app
|
|
128
|
-
|
|
156
|
+
base_branch: main
|
|
129
157
|
|
|
130
158
|
loop:
|
|
131
|
-
cooldown: 10
|
|
132
|
-
max_sessions: 0
|
|
159
|
+
cooldown: 10 # seconds between issues
|
|
160
|
+
max_sessions: 0 # 0 = unlimited
|
|
133
161
|
|
|
134
162
|
logs:
|
|
135
163
|
dir: .lisa/logs
|
|
136
|
-
format: text
|
|
137
|
-
```
|
|
164
|
+
format: text # "text" or "json"
|
|
138
165
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
source_config:
|
|
146
|
-
board: Product
|
|
147
|
-
pick_from: Backlog
|
|
148
|
-
label: ready
|
|
149
|
-
in_progress: In Progress
|
|
150
|
-
done: Code Review
|
|
151
|
-
|
|
152
|
-
github: cli
|
|
153
|
-
workspace: .
|
|
154
|
-
|
|
155
|
-
loop:
|
|
156
|
-
cooldown: 10
|
|
157
|
-
max_sessions: 0
|
|
158
|
-
|
|
159
|
-
logs:
|
|
160
|
-
dir: .lisa/logs
|
|
161
|
-
format: text
|
|
166
|
+
# Optional — kill stuck providers
|
|
167
|
+
overseer:
|
|
168
|
+
enabled: true
|
|
169
|
+
check_interval: 30 # seconds between git status checks
|
|
170
|
+
stuck_threshold: 300 # seconds without git changes before killing
|
|
162
171
|
```
|
|
163
172
|
|
|
164
|
-
### Source-
|
|
173
|
+
### Source-Specific Fields
|
|
165
174
|
|
|
166
175
|
| Field | Linear | Trello |
|
|
167
176
|
|-------|--------|--------|
|
|
168
|
-
| `team`
|
|
177
|
+
| `team` | Team name | Board name |
|
|
169
178
|
| `project` | Project name | — |
|
|
170
|
-
| `pick_from` | Status to pick issues from
|
|
179
|
+
| `pick_from` | Status to pick issues from | List to pick cards from |
|
|
171
180
|
| `label` | Label to filter issues | Label to filter cards |
|
|
172
|
-
| `in_progress` | In-progress status
|
|
173
|
-
| `done` | Destination status
|
|
181
|
+
| `in_progress` | In-progress status | In-progress column |
|
|
182
|
+
| `done` | Destination status after PR | Destination column after PR |
|
|
174
183
|
|
|
175
|
-
|
|
184
|
+
### Lifecycle Resources
|
|
176
185
|
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
For repos that need services running during implementation (databases, dev servers):
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
repos:
|
|
190
|
+
- name: my-api
|
|
191
|
+
path: ./api
|
|
192
|
+
base_branch: main
|
|
193
|
+
lifecycle:
|
|
194
|
+
resources:
|
|
195
|
+
- name: postgres
|
|
196
|
+
check_port: 5432
|
|
197
|
+
up: "docker compose up -d postgres"
|
|
198
|
+
down: "docker compose down"
|
|
199
|
+
startup_timeout: 30
|
|
200
|
+
setup:
|
|
201
|
+
- "npx prisma generate"
|
|
202
|
+
- "npx prisma db push"
|
|
179
203
|
```
|
|
180
204
|
|
|
205
|
+
Lisa starts resources before the agent runs, waits for the port to be ready, runs setup commands, then stops everything after the session.
|
|
206
|
+
|
|
181
207
|
## How It Works
|
|
182
208
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
209
|
+
```
|
|
210
|
+
┌─────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌────┐ ┌────────┐
|
|
211
|
+
│ Fetch │───▶│ Activate │───▶│ Implement │───▶│ Validate │───▶│ PR │───▶│ Update │
|
|
212
|
+
└─────────┘ └──────────┘ └───────────┘ └──────────┘ └────┘ └────────┘
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
1. **Fetch** — Pulls the next issue from Linear or Trello matching the configured label, team, and project. Issues are sorted by priority. Blocked issues (with unresolved dependencies) are skipped.
|
|
216
|
+
2. **Activate** — Moves the issue to `in_progress` so your team knows it's being worked on.
|
|
217
|
+
3. **Implement** — Builds a structured prompt with full issue context and sends it to the AI agent. The agent works in a worktree or branch, implements the change, runs validation, and commits.
|
|
218
|
+
4. **Validate** — Runs the project's test suite. If tests fail, the session is aborted and the issue reverts.
|
|
219
|
+
5. **PR** — Pushes the branch and creates a pull request referencing the original issue. If pre-push hooks fail, Lisa re-invokes the agent to fix the errors and retries (up to 2 recovery attempts).
|
|
220
|
+
6. **Update** — Moves the issue to the `done` status and removes the pickup label in a single atomic operation.
|
|
221
|
+
7. **Next** — Picks the next issue. When there are no more matching issues, Lisa stops.
|
|
222
|
+
|
|
223
|
+
### Recovery Mechanisms
|
|
224
|
+
|
|
225
|
+
- **Orphan recovery** — On startup, Lisa scans for issues stuck in `in_progress` from previous interrupted runs and reverts them to `pick_from`.
|
|
226
|
+
- **Push recovery** — If `git push` fails due to pre-push hooks (linter, typecheck, tests), Lisa re-invokes the agent with the error output and retries the push.
|
|
227
|
+
- **Signal handling** — SIGINT/SIGTERM gracefully revert the active issue to its previous status before exiting.
|
|
228
|
+
- **Guardrails** — Failed sessions are logged to `.lisa/guardrails.md` and injected into future prompts so the agent avoids repeating the same mistakes.
|
|
189
229
|
|
|
190
230
|
## License
|
|
191
231
|
|
package/dist/index.js
CHANGED
|
@@ -531,6 +531,16 @@ This project uses **${testRunner}** as its test runner.
|
|
|
531
531
|
- Do NOT skip writing tests \u2014 the PR will be blocked if tests are missing or failing.
|
|
532
532
|
`;
|
|
533
533
|
}
|
|
534
|
+
function buildPreCommitHookInstructions() {
|
|
535
|
+
return `
|
|
536
|
+
**Pre-commit hooks:**
|
|
537
|
+
If \`git commit\` fails due to a pre-commit hook (e.g. husky), read the error output carefully and fix the underlying issue:
|
|
538
|
+
- Linter/formatter failures \u2192 run the project's lint/format commands, then re-stage and retry the commit.
|
|
539
|
+
- Code generation errors (e.g. stale Prisma client) \u2192 run the required generation command (e.g. \`npx prisma generate\`), then re-stage and retry.
|
|
540
|
+
- Type errors \u2192 fix the type issues in the source files, then re-stage and retry.
|
|
541
|
+
Do NOT skip or bypass hooks (no \`--no-verify\`). Fix the root cause and retry.
|
|
542
|
+
`;
|
|
543
|
+
}
|
|
534
544
|
function buildReadmeInstructions() {
|
|
535
545
|
return `
|
|
536
546
|
**README.md Evaluation:**
|
|
@@ -565,6 +575,7 @@ function buildWorktreeMultiRepoPrompt(issue, config2) {
|
|
|
565
575
|
].join("\n");
|
|
566
576
|
}).join("\n\n");
|
|
567
577
|
const readmeBlock = buildReadmeInstructions();
|
|
578
|
+
const hookBlock = buildPreCommitHookInstructions();
|
|
568
579
|
const manifestPath = join(workspace, ".lisa-manifest.json");
|
|
569
580
|
return `You are an autonomous implementation agent working in a multi-repository workspace.
|
|
570
581
|
Your job is to determine the correct repository, create an English-named branch, implement the issue, commit, and write a manifest file.
|
|
@@ -610,7 +621,7 @@ ${repoBlock}
|
|
|
610
621
|
- Follow the implementation instructions exactly
|
|
611
622
|
- Verify each acceptance criteria (if present)
|
|
612
623
|
- Respect any stack or technical constraints (if present)
|
|
613
|
-
${readmeBlock}
|
|
624
|
+
${readmeBlock}${hookBlock}
|
|
614
625
|
5. **Validate**: Run the project's linter/typecheck/tests if available:
|
|
615
626
|
- Check \`package.json\` for lint, typecheck, check, or test scripts.
|
|
616
627
|
- Run whichever validation scripts exist (e.g., \`npm run lint\`, \`npm run typecheck\`, \`npm run test\`).
|
|
@@ -647,6 +658,7 @@ ${readmeBlock}
|
|
|
647
658
|
function buildWorktreePrompt(issue, testRunner) {
|
|
648
659
|
const testBlock = buildTestInstructions(testRunner ?? null);
|
|
649
660
|
const readmeBlock = buildReadmeInstructions();
|
|
661
|
+
const hookBlock = buildPreCommitHookInstructions();
|
|
650
662
|
return `You are an autonomous implementation agent. Your job is to implement a single
|
|
651
663
|
issue, validate it, commit, and push the branch.
|
|
652
664
|
|
|
@@ -670,7 +682,7 @@ ${issue.description}
|
|
|
670
682
|
- Follow the implementation instructions exactly
|
|
671
683
|
- Verify each acceptance criteria (if present)
|
|
672
684
|
- Respect any stack or technical constraints (if present)
|
|
673
|
-
${testBlock}${readmeBlock}
|
|
685
|
+
${testBlock}${readmeBlock}${hookBlock}
|
|
674
686
|
2. **Validate**: Run the project's linter/typecheck/tests if available:
|
|
675
687
|
- Check \`package.json\` (or equivalent) for lint, typecheck, check, or test scripts.
|
|
676
688
|
- Run whichever validation scripts exist (e.g., \`npm run lint\`, \`npm run typecheck\`).
|
|
@@ -711,6 +723,7 @@ function buildBranchPrompt(issue, config2, testRunner) {
|
|
|
711
723
|
const baseBranchInstruction = config2.repos.length > 0 ? "From the repo's base branch (listed above)" : `From \`${config2.base_branch}\``;
|
|
712
724
|
const testBlock = buildTestInstructions(testRunner ?? null);
|
|
713
725
|
const readmeBlock = buildReadmeInstructions();
|
|
726
|
+
const hookBlock = buildPreCommitHookInstructions();
|
|
714
727
|
const manifestPath = join(workspace, ".lisa-manifest.json");
|
|
715
728
|
return `You are an autonomous implementation agent. Your job is to implement a single
|
|
716
729
|
issue, validate it, commit, and push the branch.
|
|
@@ -741,7 +754,7 @@ ${repoEntries}
|
|
|
741
754
|
- Follow the implementation instructions exactly
|
|
742
755
|
- Verify each acceptance criteria (if present)
|
|
743
756
|
- Respect any stack or technical constraints (if present)
|
|
744
|
-
${testBlock}${readmeBlock}
|
|
757
|
+
${testBlock}${readmeBlock}${hookBlock}
|
|
745
758
|
4. **Validate**: Run the project's linter/typecheck/tests if available:
|
|
746
759
|
- Check \`package.json\` (or equivalent) for lint, typecheck, check, or test scripts.
|
|
747
760
|
- Run whichever validation scripts exist (e.g., \`npm run lint\`, \`npm run typecheck\`).
|
|
@@ -771,6 +784,32 @@ ${testBlock}${readmeBlock}
|
|
|
771
784
|
- Do NOT create pull requests \u2014 the caller handles that.
|
|
772
785
|
- Do NOT update the issue tracker \u2014 the caller handles that.`;
|
|
773
786
|
}
|
|
787
|
+
function buildPushRecoveryPrompt(hookErrors) {
|
|
788
|
+
return `The previous \`git push\` failed because a pre-push hook rejected the push.
|
|
789
|
+
Here is the full error output:
|
|
790
|
+
|
|
791
|
+
\`\`\`
|
|
792
|
+
${hookErrors}
|
|
793
|
+
\`\`\`
|
|
794
|
+
|
|
795
|
+
## Instructions
|
|
796
|
+
|
|
797
|
+
1. **Read the errors** above carefully and identify the root cause.
|
|
798
|
+
2. **Fix the issue** \u2014 common fixes include:
|
|
799
|
+
- Run linters/formatters (e.g. \`npm run lint -- --fix\`, \`npm run format\`)
|
|
800
|
+
- Run code generation (e.g. \`npx prisma generate\`, \`npm run codegen\`)
|
|
801
|
+
- Fix type errors in the source files
|
|
802
|
+
- Fix failing tests
|
|
803
|
+
3. **Amend the commit** so the fix is included:
|
|
804
|
+
\`\`\`
|
|
805
|
+
git add -A && git commit --amend --no-edit
|
|
806
|
+
\`\`\`
|
|
807
|
+
4. **Do NOT push** \u2014 the caller handles pushing after you finish.
|
|
808
|
+
5. **Do NOT create pull requests** \u2014 the caller handles that.
|
|
809
|
+
6. **Do NOT update the issue tracker** \u2014 the caller handles that.
|
|
810
|
+
|
|
811
|
+
Focus only on fixing the hook errors. Do not make unrelated changes.`;
|
|
812
|
+
}
|
|
774
813
|
|
|
775
814
|
// src/guardrails.ts
|
|
776
815
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
@@ -1449,6 +1488,56 @@ var LinearSource = class {
|
|
|
1449
1488
|
}
|
|
1450
1489
|
async attachPullRequest(_issueId, _prUrl) {
|
|
1451
1490
|
}
|
|
1491
|
+
async completeIssue(issueId, statusName, labelToRemove) {
|
|
1492
|
+
const issueData = await gql(
|
|
1493
|
+
`query($identifier: String!) {
|
|
1494
|
+
issue(id: $identifier) {
|
|
1495
|
+
id
|
|
1496
|
+
team { id }
|
|
1497
|
+
labels { nodes { id name } }
|
|
1498
|
+
}
|
|
1499
|
+
}`,
|
|
1500
|
+
{ identifier: issueId }
|
|
1501
|
+
);
|
|
1502
|
+
const statesData = await gql(
|
|
1503
|
+
`query($teamId: ID!) {
|
|
1504
|
+
workflowStates(filter: { team: { id: { eq: $teamId } } }) {
|
|
1505
|
+
nodes { id name }
|
|
1506
|
+
}
|
|
1507
|
+
}`,
|
|
1508
|
+
{ teamId: issueData.issue.team.id }
|
|
1509
|
+
);
|
|
1510
|
+
const state = statesData.workflowStates.nodes.find(
|
|
1511
|
+
(s) => s.name.toLowerCase() === statusName.toLowerCase()
|
|
1512
|
+
);
|
|
1513
|
+
if (!state) {
|
|
1514
|
+
const available = statesData.workflowStates.nodes.map((s) => s.name).join(", ");
|
|
1515
|
+
throw new Error(`Status "${statusName}" not found. Available: ${available}`);
|
|
1516
|
+
}
|
|
1517
|
+
const input = { stateId: state.id };
|
|
1518
|
+
if (labelToRemove) {
|
|
1519
|
+
const currentLabels = issueData.issue.labels.nodes;
|
|
1520
|
+
const filtered = currentLabels.filter(
|
|
1521
|
+
(l) => l.name.toLowerCase() !== labelToRemove.toLowerCase()
|
|
1522
|
+
);
|
|
1523
|
+
if (filtered.length !== currentLabels.length) {
|
|
1524
|
+
input.labelIds = filtered.map((l) => l.id);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
const mutationResult = await gql(
|
|
1528
|
+
`mutation($issueId: String!, $input: IssueUpdateInput!) {
|
|
1529
|
+
issueUpdate(id: $issueId, input: $input) {
|
|
1530
|
+
success
|
|
1531
|
+
}
|
|
1532
|
+
}`,
|
|
1533
|
+
{ issueId: issueData.issue.id, input }
|
|
1534
|
+
);
|
|
1535
|
+
if (!mutationResult.issueUpdate.success) {
|
|
1536
|
+
throw new Error(
|
|
1537
|
+
`issueUpdate returned success=false for ${issueId} (stateId: ${state.id}, stateName: ${state.name})`
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1452
1541
|
async removeLabel(issueId, labelName) {
|
|
1453
1542
|
const issueData = await gql(
|
|
1454
1543
|
`query($identifier: String!) {
|
|
@@ -1591,6 +1680,12 @@ var TrelloSource = class {
|
|
|
1591
1680
|
async attachPullRequest(cardId, prUrl) {
|
|
1592
1681
|
await trelloPost(`/cards/${cardId}/attachments`, `url=${encodeURIComponent(prUrl)}`);
|
|
1593
1682
|
}
|
|
1683
|
+
async completeIssue(cardId, listName, labelToRemove) {
|
|
1684
|
+
await this.updateStatus(cardId, listName);
|
|
1685
|
+
if (labelToRemove) {
|
|
1686
|
+
await this.removeLabel(cardId, labelToRemove);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1594
1689
|
async removeLabel(cardId, labelName) {
|
|
1595
1690
|
const card = await trelloGet(
|
|
1596
1691
|
`/cards/${cardId}`,
|
|
@@ -1792,6 +1887,58 @@ function cleanupManifest(dir) {
|
|
|
1792
1887
|
} catch {
|
|
1793
1888
|
}
|
|
1794
1889
|
}
|
|
1890
|
+
var MAX_PUSH_RETRIES = 2;
|
|
1891
|
+
var HOOK_ERROR_PATTERNS = [
|
|
1892
|
+
/husky - pre-push/i,
|
|
1893
|
+
/husky - pre-commit/i,
|
|
1894
|
+
/pre-push hook/i,
|
|
1895
|
+
/pre-commit hook/i,
|
|
1896
|
+
/hook declined/i,
|
|
1897
|
+
/hook.*failed/i,
|
|
1898
|
+
/hook.*exited with/i,
|
|
1899
|
+
/hook.*returned.*exit code/i
|
|
1900
|
+
];
|
|
1901
|
+
function isHookError(errorMessage) {
|
|
1902
|
+
return HOOK_ERROR_PATTERNS.some((pattern) => pattern.test(errorMessage));
|
|
1903
|
+
}
|
|
1904
|
+
async function pushWithRecovery(opts) {
|
|
1905
|
+
for (let attempt = 0; attempt <= MAX_PUSH_RETRIES; attempt++) {
|
|
1906
|
+
try {
|
|
1907
|
+
await execa3("git", ["push", "-u", "origin", opts.branch], { cwd: opts.cwd });
|
|
1908
|
+
return { success: true };
|
|
1909
|
+
} catch (err) {
|
|
1910
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1911
|
+
if (!isHookError(errorMessage)) {
|
|
1912
|
+
return { success: false, error: errorMessage };
|
|
1913
|
+
}
|
|
1914
|
+
if (attempt >= MAX_PUSH_RETRIES) {
|
|
1915
|
+
return {
|
|
1916
|
+
success: false,
|
|
1917
|
+
error: `Push hook failed after ${MAX_PUSH_RETRIES} recovery attempts: ${errorMessage}`
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
warn(
|
|
1921
|
+
`Push hook failed (attempt ${attempt + 1}/${MAX_PUSH_RETRIES}). Re-invoking provider to fix...`
|
|
1922
|
+
);
|
|
1923
|
+
const recoveryPrompt = buildPushRecoveryPrompt(errorMessage);
|
|
1924
|
+
const result = await runWithFallback(opts.models, recoveryPrompt, {
|
|
1925
|
+
logFile: opts.logFile,
|
|
1926
|
+
cwd: opts.cwd,
|
|
1927
|
+
guardrailsDir: opts.guardrailsDir,
|
|
1928
|
+
issueId: opts.issueId,
|
|
1929
|
+
overseer: opts.overseer
|
|
1930
|
+
});
|
|
1931
|
+
if (!result.success) {
|
|
1932
|
+
return {
|
|
1933
|
+
success: false,
|
|
1934
|
+
error: `Provider failed to fix push hook errors: ${result.output}`
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
ok("Provider finished recovery. Retrying push...");
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
return { success: false, error: "Push recovery exhausted retries" };
|
|
1941
|
+
}
|
|
1795
1942
|
function installSignalHandlers() {
|
|
1796
1943
|
const cleanup = async (signal) => {
|
|
1797
1944
|
if (shuttingDown) {
|
|
@@ -1991,22 +2138,16 @@ async function runLoop(config2, opts) {
|
|
|
1991
2138
|
warn(`Failed to attach PR: ${err instanceof Error ? err.message : String(err)}`);
|
|
1992
2139
|
}
|
|
1993
2140
|
}
|
|
1994
|
-
let statusUpdated = false;
|
|
1995
2141
|
try {
|
|
1996
2142
|
const doneStatus = config2.source_config.done;
|
|
1997
|
-
|
|
2143
|
+
const labelToRemove = opts.issueId ? void 0 : config2.source_config.label;
|
|
2144
|
+
await source.completeIssue(issue.id, doneStatus, labelToRemove);
|
|
1998
2145
|
ok(`Updated ${issue.id} status to "${doneStatus}"`);
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
error(`Failed to update status: ${err instanceof Error ? err.message : String(err)}`);
|
|
2002
|
-
}
|
|
2003
|
-
if (statusUpdated && !opts.issueId) {
|
|
2004
|
-
try {
|
|
2005
|
-
await source.removeLabel(issue.id, config2.source_config.label);
|
|
2006
|
-
ok(`Removed label "${config2.source_config.label}" from ${issue.id}`);
|
|
2007
|
-
} catch (err) {
|
|
2008
|
-
error(`Failed to remove label: ${err instanceof Error ? err.message : String(err)}`);
|
|
2146
|
+
if (labelToRemove) {
|
|
2147
|
+
ok(`Removed label "${labelToRemove}" from ${issue.id}`);
|
|
2009
2148
|
}
|
|
2149
|
+
} catch (err) {
|
|
2150
|
+
error(`Failed to complete issue: ${err instanceof Error ? err.message : String(err)}`);
|
|
2010
2151
|
}
|
|
2011
2152
|
activeCleanup = null;
|
|
2012
2153
|
if (opts.once) {
|
|
@@ -2158,12 +2299,17 @@ ${result.output}
|
|
|
2158
2299
|
);
|
|
2159
2300
|
}
|
|
2160
2301
|
}
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2302
|
+
const pushResult = await pushWithRecovery({
|
|
2303
|
+
branch: effectiveBranch,
|
|
2304
|
+
cwd: worktreePath,
|
|
2305
|
+
models,
|
|
2306
|
+
logFile,
|
|
2307
|
+
guardrailsDir: repoPath,
|
|
2308
|
+
issueId: issue.id,
|
|
2309
|
+
overseer: config2.overseer
|
|
2310
|
+
});
|
|
2311
|
+
if (!pushResult.success) {
|
|
2312
|
+
error(`Failed to push branch to remote: ${pushResult.error}`);
|
|
2167
2313
|
cleanupManifest(worktreePath);
|
|
2168
2314
|
await cleanupWorktree(repoPath, worktreePath);
|
|
2169
2315
|
return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
|
|
@@ -2248,12 +2394,17 @@ ${result.output}
|
|
|
2248
2394
|
cleanupManifest(workspace);
|
|
2249
2395
|
return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
|
|
2250
2396
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2397
|
+
const pushResult = await pushWithRecovery({
|
|
2398
|
+
branch: manifest.branch,
|
|
2399
|
+
cwd: effectiveCwd,
|
|
2400
|
+
models,
|
|
2401
|
+
logFile,
|
|
2402
|
+
guardrailsDir: manifest.repoPath,
|
|
2403
|
+
issueId: issue.id,
|
|
2404
|
+
overseer: config2.overseer
|
|
2405
|
+
});
|
|
2406
|
+
if (!pushResult.success) {
|
|
2407
|
+
error(`Failed to push branch to remote: ${pushResult.error}`);
|
|
2257
2408
|
if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
|
|
2258
2409
|
cleanupManifest(workspace);
|
|
2259
2410
|
return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
|