@tarcisiopgs/lisa 1.12.1 → 1.12.2
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 +81 -10
- package/dist/chunk-DGL33SDZ.js +88 -0
- package/dist/chunk-NXGXGHS3.js +152 -0
- package/dist/chunk-OYQ6TOAG.js +74 -0
- package/dist/guardrails-KI5NEJVE.js +26 -0
- package/dist/index.js +971 -399
- package/dist/{kanban-CY62TCOA.js → kanban-THY72UON.js} +66 -40
- package/dist/paths-HQQDKACV.js +23 -0
- package/dist/pr-feedback-DGHNP3E7.js +11 -0
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
Lisa is an autonomous issue resolver that turns your backlog into pull requests — no babysitting required.
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
+
<p align="center">
|
|
16
|
+
<a href="https://www.npmjs.com/package/@tarcisiopgs/lisa"><img src="https://img.shields.io/npm/v/@tarcisiopgs/lisa.svg" alt="npm version" /></a>
|
|
17
|
+
<a href="https://www.npmjs.com/package/@tarcisiopgs/lisa"><img src="https://img.shields.io/npm/dm/@tarcisiopgs/lisa.svg" alt="npm downloads" /></a>
|
|
18
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT" /></a>
|
|
19
|
+
<img src="https://img.shields.io/node/v/%40tarcisiopgs%2Flisa" alt="Node.js version" />
|
|
20
|
+
</p>
|
|
21
|
+
|
|
15
22
|
---
|
|
16
23
|
|
|
17
24
|
## Quickstart
|
|
@@ -24,6 +31,8 @@ lisa run
|
|
|
24
31
|
|
|
25
32
|
That's it. Lisa picks up the next labeled issue, implements it, pushes a branch, opens a pull request, and moves the ticket to "In Review" — all without you touching it.
|
|
26
33
|
|
|
34
|
+
`lisa init` offers pre-defined templates for the most common source + provider combinations (GitHub Issues + Claude, Linear + Claude, Jira + Claude, and more). Select a template to pre-fill sensible defaults, or choose "Configure manually" for full control.
|
|
35
|
+
|
|
27
36
|
## Try it safely first
|
|
28
37
|
|
|
29
38
|
Before letting Lisa touch real issues, verify your configuration with `--dry-run`. No issues will be fetched, no code will be written, no PRs will be created.
|
|
@@ -53,11 +62,11 @@ Lisa follows a deterministic pipeline:
|
|
|
53
62
|
└─────────┘ └──────────┘ └───────────┘ └──────────┘ └────┘ └────────┘
|
|
54
63
|
```
|
|
55
64
|
|
|
56
|
-
1. **Fetch** — Pulls the next issue from
|
|
65
|
+
1. **Fetch** — Pulls the next issue from your project tracker matching the configured label, team, and project. Issues are sorted by priority. Blocked issues are skipped.
|
|
57
66
|
2. **Activate** — Moves the issue to `in_progress` so your team knows it's being worked on.
|
|
58
67
|
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 tests, and commits.
|
|
59
68
|
4. **Validate** — If the agent's tests pass and pre-push hooks succeed, the branch is pushed. If hooks fail, Lisa re-invokes the agent with the error output and retries.
|
|
60
|
-
5. **PR** — Pushes the branch and creates a pull request referencing the original issue.
|
|
69
|
+
5. **PR** — Pushes the branch and creates a pull request (or merge request) referencing the original issue.
|
|
61
70
|
6. **Update** — Moves the issue to the `done` status and removes the pickup label.
|
|
62
71
|
7. **Next** — Picks the next issue. When there are no more matching issues, Lisa stops.
|
|
63
72
|
|
|
@@ -71,7 +80,29 @@ Lisa follows a deterministic pipeline:
|
|
|
71
80
|
- **Self-healing** — Orphan issues stuck in "In Progress" are recovered on startup. Pre-push failures trigger the agent to fix and retry.
|
|
72
81
|
- **Guardrails** — Past failures are logged and injected into future prompts so the agent avoids repeating mistakes.
|
|
73
82
|
|
|
74
|
-
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Issue Sources
|
|
86
|
+
|
|
87
|
+
Lisa integrates with **7 project trackers** out of the box:
|
|
88
|
+
|
|
89
|
+
| Source | Key | Notes |
|
|
90
|
+
|--------|-----|-------|
|
|
91
|
+
| Linear | `linear` | GraphQL API — priority-sorted, project-scoped, blocker-aware |
|
|
92
|
+
| GitHub Issues | `github-issues` | Native GitHub issues with label-based pickup |
|
|
93
|
+
| Jira | `jira` | Atlassian Jira via REST API (cloud and self-hosted) |
|
|
94
|
+
| GitLab Issues | `gitlab-issues` | GitLab issues with label transitions |
|
|
95
|
+
| Trello | `trello` | Card-based workflow via Trello REST API |
|
|
96
|
+
| Plane | `plane` | Open-source project management (cloud and self-hosted) |
|
|
97
|
+
| Shortcut | `shortcut` | Story-based tracking (formerly Clubhouse) |
|
|
98
|
+
|
|
99
|
+
Each source supports label filtering, priority ordering, and status transitions out of the box. See [Source-Specific Fields](#source-specific-fields) for configuration details.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## AI Providers
|
|
104
|
+
|
|
105
|
+
Lisa supports **8 AI coding agents** as implementation backends:
|
|
75
106
|
|
|
76
107
|
| Provider | Key | Command |
|
|
77
108
|
|----------|-----|---------|
|
|
@@ -102,6 +133,33 @@ models:
|
|
|
102
133
|
- claude-haiku-4-5 # fallback 2
|
|
103
134
|
```
|
|
104
135
|
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## PR Delivery
|
|
139
|
+
|
|
140
|
+
Lisa delivers pull requests and merge requests to **4 platforms**:
|
|
141
|
+
|
|
142
|
+
| Platform | Key | Auth | Output |
|
|
143
|
+
|----------|-----|------|--------|
|
|
144
|
+
| GitHub CLI | `cli` | `gh auth login` | Pull Request |
|
|
145
|
+
| GitHub API | `token` | `GITHUB_TOKEN` | Pull Request |
|
|
146
|
+
| GitLab | `gitlab` | `GITLAB_TOKEN` | Merge Request |
|
|
147
|
+
| Bitbucket | `bitbucket` | `BITBUCKET_TOKEN` + `BITBUCKET_USERNAME` | Pull Request |
|
|
148
|
+
|
|
149
|
+
Set `github` in your config (or pass `--github` at runtime) to select the delivery method:
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
github: gitlab # or: cli, token, bitbucket
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
lisa run --github bitbucket # override at runtime
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Each platform appends a `🤖 Resolved by lisa` attribution comment to the PR/MR after creation.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
105
163
|
## Install
|
|
106
164
|
|
|
107
165
|
```bash
|
|
@@ -111,8 +169,11 @@ npm install -g @tarcisiopgs/lisa
|
|
|
111
169
|
## Environment Variables
|
|
112
170
|
|
|
113
171
|
```bash
|
|
114
|
-
#
|
|
115
|
-
export GITHUB_TOKEN=""
|
|
172
|
+
# PR/MR delivery — set one based on your platform
|
|
173
|
+
export GITHUB_TOKEN="" # GitHub — or have `gh` CLI authenticated (github: cli or token)
|
|
174
|
+
export GITLAB_TOKEN="" # GitLab MR creation (github: gitlab)
|
|
175
|
+
export BITBUCKET_TOKEN="" # Bitbucket PR creation (github: bitbucket)
|
|
176
|
+
export BITBUCKET_USERNAME="" # Bitbucket username (required with BITBUCKET_TOKEN)
|
|
116
177
|
|
|
117
178
|
# Required when source = linear
|
|
118
179
|
export LINEAR_API_KEY=""
|
|
@@ -129,7 +190,7 @@ export PLANE_WORKSPACE="" # optional; fallback when team is not set in config
|
|
|
129
190
|
# Required when source = shortcut
|
|
130
191
|
export SHORTCUT_API_TOKEN=""
|
|
131
192
|
|
|
132
|
-
# Required when source = gitlab-issues
|
|
193
|
+
# Required when source = gitlab-issues or github = gitlab
|
|
133
194
|
export GITLAB_TOKEN=""
|
|
134
195
|
export GITLAB_BASE_URL="" # optional; defaults to https://gitlab.com
|
|
135
196
|
|
|
@@ -140,6 +201,10 @@ export GITHUB_TOKEN="" # same token used for PR creation
|
|
|
140
201
|
export JIRA_BASE_URL="" # e.g. https://yourcompany.atlassian.net
|
|
141
202
|
export JIRA_EMAIL="" # Atlassian account email
|
|
142
203
|
export JIRA_API_TOKEN="" # Atlassian API token
|
|
204
|
+
|
|
205
|
+
# Optional — anonymous crash reporting (disabled by default)
|
|
206
|
+
export LISA_TELEMETRY=1 # enable anonymous crash/error reporting
|
|
207
|
+
export LISA_NO_TELEMETRY=1 # disable reporting (overrides LISA_TELEMETRY and config)
|
|
143
208
|
```
|
|
144
209
|
|
|
145
210
|
## Commands
|
|
@@ -157,15 +222,16 @@ export JIRA_API_TOKEN="" # Atlassian API token
|
|
|
157
222
|
| `lisa run --provider NAME` | Override AI provider |
|
|
158
223
|
| `lisa run --source NAME` | Override issue source |
|
|
159
224
|
| `lisa run --label NAME` | Override label filter |
|
|
160
|
-
| `lisa run --github METHOD` | Override
|
|
225
|
+
| `lisa run --github METHOD` | Override PR platform (`cli`, `token`, `gitlab`, or `bitbucket`) |
|
|
161
226
|
| `lisa run --no-bell` | Disable terminal bell on issue completion/failure |
|
|
162
|
-
| `lisa init` | Create `.lisa/config.yaml` interactively |
|
|
227
|
+
| `lisa init` | Create `.lisa/config.yaml` interactively (offers pre-defined templates) |
|
|
163
228
|
| `lisa config` | Edit config interactively |
|
|
164
229
|
| `lisa config --show` | Print current config as JSON |
|
|
165
230
|
| `lisa config --set key=value` | Set a single config value |
|
|
166
231
|
| `lisa status` | Show session stats |
|
|
167
232
|
| `lisa issue get <id>` | Fetch full issue details as JSON (for use inside worktrees) |
|
|
168
233
|
| `lisa issue done <id> --pr-url <url>` | Complete issue, attach PR, update status, remove label |
|
|
234
|
+
| `lisa feedback --pr <url> --issue <id>` | Inject PR review comments from a closed-without-merge PR into guardrails |
|
|
169
235
|
|
|
170
236
|
## TUI
|
|
171
237
|
|
|
@@ -219,7 +285,7 @@ source_config:
|
|
|
219
285
|
in_progress: In Progress
|
|
220
286
|
done: In Review
|
|
221
287
|
|
|
222
|
-
github: cli # "cli" (gh)
|
|
288
|
+
github: cli # "cli" (gh), "token" (GITHUB_TOKEN), "gitlab" (GITLAB_TOKEN), or "bitbucket" (BITBUCKET_TOKEN)
|
|
223
289
|
workspace: .
|
|
224
290
|
base_branch: main
|
|
225
291
|
|
|
@@ -249,6 +315,10 @@ overseer:
|
|
|
249
315
|
# Optional — validate issue spec before accepting
|
|
250
316
|
validation:
|
|
251
317
|
require_acceptance_criteria: true # skip issues without detectable acceptance criteria (default: true)
|
|
318
|
+
|
|
319
|
+
# Optional — anonymous crash/error reporting (disabled by default)
|
|
320
|
+
telemetry:
|
|
321
|
+
enabled: true # set via `lisa init` prompt or LISA_TELEMETRY=1; override with LISA_NO_TELEMETRY=1
|
|
252
322
|
```
|
|
253
323
|
|
|
254
324
|
### Source-Specific Fields
|
|
@@ -365,7 +435,8 @@ Lisa detects blocked issues using the `completedBlockerIds` from your issue trac
|
|
|
365
435
|
- **Orphan recovery** — On startup, Lisa scans for issues stuck in `in_progress` from interrupted runs and reverts them to `pick_from`.
|
|
366
436
|
- **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.
|
|
367
437
|
- **Signal handling** — SIGINT/SIGTERM gracefully revert the active issue to its previous status before exiting.
|
|
368
|
-
- **Guardrails** — Failed sessions are logged
|
|
438
|
+
- **Guardrails** — Failed sessions and rejected PR reviews are logged and injected into future prompts so the agent avoids repeating the same mistakes.
|
|
439
|
+
- **PR feedback injection** — When a PR created by Lisa is closed without merge, review comments are automatically captured and injected as guardrails for the next run of the same issue. Use `lisa feedback --pr <url> --issue <id>` to inject feedback manually.
|
|
369
440
|
|
|
370
441
|
### Overseer
|
|
371
442
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/git/pr-feedback.ts
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
function parsePrUrl(prUrl) {
|
|
6
|
+
const match = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
7
|
+
if (!match) return null;
|
|
8
|
+
return { owner: match[1] ?? "", repo: match[2] ?? "", prNumber: match[3] ?? "" };
|
|
9
|
+
}
|
|
10
|
+
async function fetchPrFeedback(prUrl) {
|
|
11
|
+
const parsed = parsePrUrl(prUrl);
|
|
12
|
+
if (!parsed) {
|
|
13
|
+
throw new Error(`Invalid GitHub PR URL: ${prUrl}`);
|
|
14
|
+
}
|
|
15
|
+
const { owner, repo, prNumber } = parsed;
|
|
16
|
+
const { stdout: prJson } = await execa("gh", [
|
|
17
|
+
"pr",
|
|
18
|
+
"view",
|
|
19
|
+
prUrl,
|
|
20
|
+
"--json",
|
|
21
|
+
"title,state,mergedAt"
|
|
22
|
+
]);
|
|
23
|
+
const { title, state, mergedAt } = JSON.parse(prJson);
|
|
24
|
+
let prState;
|
|
25
|
+
if (mergedAt) {
|
|
26
|
+
prState = "merged";
|
|
27
|
+
} else if (state === "CLOSED") {
|
|
28
|
+
prState = "closed";
|
|
29
|
+
} else {
|
|
30
|
+
prState = "open";
|
|
31
|
+
}
|
|
32
|
+
const { stdout: reviewsJson } = await execa("gh", [
|
|
33
|
+
"api",
|
|
34
|
+
`/repos/${owner}/${repo}/pulls/${prNumber}/reviews`
|
|
35
|
+
]);
|
|
36
|
+
const rawReviews = JSON.parse(reviewsJson);
|
|
37
|
+
const reviews = rawReviews.filter((r) => r.body.trim()).map((r) => ({
|
|
38
|
+
author: r.user.login,
|
|
39
|
+
state: r.state,
|
|
40
|
+
body: r.body,
|
|
41
|
+
submittedAt: r.submitted_at
|
|
42
|
+
}));
|
|
43
|
+
const { stdout: commentsJson } = await execa("gh", [
|
|
44
|
+
"api",
|
|
45
|
+
`/repos/${owner}/${repo}/pulls/${prNumber}/comments`
|
|
46
|
+
]);
|
|
47
|
+
const rawComments = JSON.parse(commentsJson);
|
|
48
|
+
const comments = rawComments.map((c) => ({
|
|
49
|
+
author: c.user.login,
|
|
50
|
+
body: c.body,
|
|
51
|
+
path: c.path,
|
|
52
|
+
line: c.line,
|
|
53
|
+
createdAt: c.created_at
|
|
54
|
+
}));
|
|
55
|
+
return { prUrl, title, state: prState, reviews, comments };
|
|
56
|
+
}
|
|
57
|
+
function formatPrFeedbackEntry(feedback, issueId, date) {
|
|
58
|
+
const lines = [
|
|
59
|
+
`## PR Feedback for Issue ${issueId} (${date})`,
|
|
60
|
+
`- PR: ${feedback.prUrl}`,
|
|
61
|
+
`- Title: ${feedback.title}`,
|
|
62
|
+
`- Status: Closed without merge`
|
|
63
|
+
];
|
|
64
|
+
if (feedback.reviews.length > 0) {
|
|
65
|
+
lines.push("- Reviews:");
|
|
66
|
+
lines.push("```");
|
|
67
|
+
for (const review of feedback.reviews) {
|
|
68
|
+
lines.push(`[${review.author}] ${review.state}: ${review.body}`);
|
|
69
|
+
}
|
|
70
|
+
lines.push("```");
|
|
71
|
+
}
|
|
72
|
+
if (feedback.comments.length > 0) {
|
|
73
|
+
lines.push("- Inline comments:");
|
|
74
|
+
lines.push("```");
|
|
75
|
+
for (const comment of feedback.comments) {
|
|
76
|
+
const location = comment.path ? ` (${comment.path}${comment.line ? `:${comment.line}` : ""})` : "";
|
|
77
|
+
lines.push(`[${comment.author}]${location}: ${comment.body}`);
|
|
78
|
+
}
|
|
79
|
+
lines.push("```");
|
|
80
|
+
}
|
|
81
|
+
return lines.join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
parsePrUrl,
|
|
86
|
+
fetchPrFeedback,
|
|
87
|
+
formatPrFeedbackEntry
|
|
88
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getGuardrailsPath
|
|
4
|
+
} from "./chunk-OYQ6TOAG.js";
|
|
5
|
+
|
|
6
|
+
// src/session/guardrails.ts
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
var LEGACY_GUARDRAILS_FILE = ".lisa/guardrails.md";
|
|
10
|
+
var MAX_ENTRIES = 20;
|
|
11
|
+
var CONTEXT_LINES = 20;
|
|
12
|
+
var writeLock = Promise.resolve();
|
|
13
|
+
function guardrailsPath(cwd) {
|
|
14
|
+
return getGuardrailsPath(cwd);
|
|
15
|
+
}
|
|
16
|
+
function migrateGuardrails(cwd) {
|
|
17
|
+
const legacyPath = join(cwd, LEGACY_GUARDRAILS_FILE);
|
|
18
|
+
if (!existsSync(legacyPath)) return;
|
|
19
|
+
const cachePath = getGuardrailsPath(cwd);
|
|
20
|
+
if (existsSync(cachePath)) return;
|
|
21
|
+
const cacheDir = dirname(cachePath);
|
|
22
|
+
if (!existsSync(cacheDir)) {
|
|
23
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
copyFileSync(legacyPath, cachePath);
|
|
26
|
+
}
|
|
27
|
+
function readGuardrails(cwd) {
|
|
28
|
+
const path = getGuardrailsPath(cwd);
|
|
29
|
+
if (!existsSync(path)) return "";
|
|
30
|
+
try {
|
|
31
|
+
return readFileSync(path, "utf-8");
|
|
32
|
+
} catch {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function buildGuardrailsSection(cwd) {
|
|
37
|
+
const content = readGuardrails(cwd);
|
|
38
|
+
if (!content.trim()) return "";
|
|
39
|
+
return `
|
|
40
|
+
## Guardrails \u2014 Avoid these known pitfalls
|
|
41
|
+
|
|
42
|
+
${content}
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
function extractContext(output) {
|
|
46
|
+
const lines = output.trim().split("\n");
|
|
47
|
+
return lines.slice(-CONTEXT_LINES).join("\n");
|
|
48
|
+
}
|
|
49
|
+
function extractErrorType(output) {
|
|
50
|
+
if (/429|rate.?limit|quota/i.test(output)) return "Rate limit / quota exceeded";
|
|
51
|
+
if (/ETIMEDOUT|ECONNREFUSED|ECONNRESET|ENOTFOUND/.test(output)) return "Network error";
|
|
52
|
+
if (/timeout|timed?\s*out/i.test(output)) return "Timeout";
|
|
53
|
+
const exitMatch = output.match(/exit code[:\s]+(\d+)/i);
|
|
54
|
+
if (exitMatch) return `Exit code ${exitMatch[1]}`;
|
|
55
|
+
if (/exit(?:ed)? with/i.test(output)) return "Non-zero exit code";
|
|
56
|
+
return "Unknown error";
|
|
57
|
+
}
|
|
58
|
+
function appendEntry(dir, entry) {
|
|
59
|
+
writeLock = writeLock.then(() => appendEntrySync(dir, entry)).catch(() => {
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function appendEntrySync(dir, entry) {
|
|
63
|
+
const path = guardrailsPath(dir);
|
|
64
|
+
const guardrailsDir = dirname(path);
|
|
65
|
+
if (!existsSync(guardrailsDir)) {
|
|
66
|
+
mkdirSync(guardrailsDir, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
const existing = existsSync(path) ? readFileSync(path, "utf-8") : "";
|
|
69
|
+
const newEntryText = formatEntry(entry);
|
|
70
|
+
let content;
|
|
71
|
+
if (!existing.trim()) {
|
|
72
|
+
content = `# Guardrails \u2014 Li\xE7\xF5es aprendidas
|
|
73
|
+
|
|
74
|
+
${newEntryText}`;
|
|
75
|
+
} else {
|
|
76
|
+
const header = extractHeader(existing);
|
|
77
|
+
const entries = splitEntries(existing);
|
|
78
|
+
entries.push(newEntryText);
|
|
79
|
+
const rotated = entries.length > MAX_ENTRIES ? entries.slice(-MAX_ENTRIES) : entries;
|
|
80
|
+
content = `${header}
|
|
81
|
+
|
|
82
|
+
${rotated.join("\n\n")}`;
|
|
83
|
+
}
|
|
84
|
+
writeFileSync(path, content, "utf-8");
|
|
85
|
+
}
|
|
86
|
+
function appendRawEntry(dir, entryText) {
|
|
87
|
+
writeLock = writeLock.then(() => appendRawEntrySync(dir, entryText)).catch(() => {
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function appendRawEntrySync(dir, entryText) {
|
|
91
|
+
const path = guardrailsPath(dir);
|
|
92
|
+
const guardrailsDir = dirname(path);
|
|
93
|
+
if (!existsSync(guardrailsDir)) {
|
|
94
|
+
mkdirSync(guardrailsDir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
const existing = existsSync(path) ? readFileSync(path, "utf-8") : "";
|
|
97
|
+
let content;
|
|
98
|
+
if (!existing.trim()) {
|
|
99
|
+
content = `# Guardrails \u2014 Li\xE7\xF5es aprendidas
|
|
100
|
+
|
|
101
|
+
${entryText}`;
|
|
102
|
+
} else {
|
|
103
|
+
const header = extractHeader(existing);
|
|
104
|
+
const entries = splitEntries(existing);
|
|
105
|
+
entries.push(entryText);
|
|
106
|
+
const rotated = entries.length > MAX_ENTRIES ? entries.slice(-MAX_ENTRIES) : entries;
|
|
107
|
+
content = `${header}
|
|
108
|
+
|
|
109
|
+
${rotated.join("\n\n")}`;
|
|
110
|
+
}
|
|
111
|
+
writeFileSync(path, content, "utf-8");
|
|
112
|
+
}
|
|
113
|
+
function formatEntry(entry) {
|
|
114
|
+
return [
|
|
115
|
+
`## Issue ${entry.issueId} (${entry.date})`,
|
|
116
|
+
`- Provider: ${entry.provider}`,
|
|
117
|
+
`- Erro: ${entry.errorType}`,
|
|
118
|
+
`- Contexto:`,
|
|
119
|
+
"```",
|
|
120
|
+
entry.context,
|
|
121
|
+
"```"
|
|
122
|
+
].join("\n");
|
|
123
|
+
}
|
|
124
|
+
function extractHeader(content) {
|
|
125
|
+
const firstEntry = content.search(/^## /m);
|
|
126
|
+
if (firstEntry === -1) return content.trim();
|
|
127
|
+
return content.slice(0, firstEntry).trim();
|
|
128
|
+
}
|
|
129
|
+
function splitEntries(content) {
|
|
130
|
+
const positions = [];
|
|
131
|
+
const regex = /^## /gm;
|
|
132
|
+
for (const match of content.matchAll(regex)) {
|
|
133
|
+
positions.push(match.index);
|
|
134
|
+
}
|
|
135
|
+
return positions.map((start, i) => {
|
|
136
|
+
const end = positions[i + 1] ?? content.length;
|
|
137
|
+
return content.slice(start, end).trim();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export {
|
|
142
|
+
guardrailsPath,
|
|
143
|
+
migrateGuardrails,
|
|
144
|
+
readGuardrails,
|
|
145
|
+
buildGuardrailsSection,
|
|
146
|
+
extractContext,
|
|
147
|
+
extractErrorType,
|
|
148
|
+
appendEntry,
|
|
149
|
+
appendEntrySync,
|
|
150
|
+
appendRawEntry,
|
|
151
|
+
appendRawEntrySync
|
|
152
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/paths.ts
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "fs";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join, resolve } from "path";
|
|
8
|
+
var MAX_LOG_FILES = 20;
|
|
9
|
+
function projectHash(cwd) {
|
|
10
|
+
const absolute = resolve(cwd);
|
|
11
|
+
return createHash("sha256").update(absolute).digest("hex").slice(0, 12);
|
|
12
|
+
}
|
|
13
|
+
function getCacheDir(cwd) {
|
|
14
|
+
const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
|
|
15
|
+
return join(base, "lisa", projectHash(cwd));
|
|
16
|
+
}
|
|
17
|
+
function getLogsDir(cwd) {
|
|
18
|
+
return join(getCacheDir(cwd), "logs");
|
|
19
|
+
}
|
|
20
|
+
function getGuardrailsPath(cwd) {
|
|
21
|
+
return join(getCacheDir(cwd), "guardrails.md");
|
|
22
|
+
}
|
|
23
|
+
function getManifestPath(cwd, issueId) {
|
|
24
|
+
if (issueId) {
|
|
25
|
+
const safe = issueId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
26
|
+
return join(getCacheDir(cwd), `manifest-${safe}.json`);
|
|
27
|
+
}
|
|
28
|
+
return join(getCacheDir(cwd), "manifest.json");
|
|
29
|
+
}
|
|
30
|
+
function getPrCachePath(cwd) {
|
|
31
|
+
return join(getCacheDir(cwd), "pr-cache.json");
|
|
32
|
+
}
|
|
33
|
+
function getPlanPath(cwd, issueId) {
|
|
34
|
+
if (issueId) {
|
|
35
|
+
const safe = issueId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
36
|
+
return join(getCacheDir(cwd), `plan-${safe}.json`);
|
|
37
|
+
}
|
|
38
|
+
return join(getCacheDir(cwd), "plan.json");
|
|
39
|
+
}
|
|
40
|
+
function ensureCacheDir(cwd) {
|
|
41
|
+
const dir = getCacheDir(cwd);
|
|
42
|
+
if (!existsSync(dir)) {
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function rotateLogFiles(cwd) {
|
|
47
|
+
const logsDir = getLogsDir(cwd);
|
|
48
|
+
if (!existsSync(logsDir)) return;
|
|
49
|
+
const files = readdirSync(logsDir).filter((f) => f.endsWith(".log")).map((f) => ({
|
|
50
|
+
name: f,
|
|
51
|
+
path: join(logsDir, f),
|
|
52
|
+
mtime: statSync(join(logsDir, f)).mtimeMs
|
|
53
|
+
})).sort((a, b) => a.mtime - b.mtime);
|
|
54
|
+
const excess = files.length - MAX_LOG_FILES;
|
|
55
|
+
if (excess <= 0) return;
|
|
56
|
+
for (const file of files.slice(0, excess)) {
|
|
57
|
+
try {
|
|
58
|
+
unlinkSync(file.path);
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
projectHash,
|
|
66
|
+
getCacheDir,
|
|
67
|
+
getLogsDir,
|
|
68
|
+
getGuardrailsPath,
|
|
69
|
+
getManifestPath,
|
|
70
|
+
getPrCachePath,
|
|
71
|
+
getPlanPath,
|
|
72
|
+
ensureCacheDir,
|
|
73
|
+
rotateLogFiles
|
|
74
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
appendEntry,
|
|
4
|
+
appendEntrySync,
|
|
5
|
+
appendRawEntry,
|
|
6
|
+
appendRawEntrySync,
|
|
7
|
+
buildGuardrailsSection,
|
|
8
|
+
extractContext,
|
|
9
|
+
extractErrorType,
|
|
10
|
+
guardrailsPath,
|
|
11
|
+
migrateGuardrails,
|
|
12
|
+
readGuardrails
|
|
13
|
+
} from "./chunk-NXGXGHS3.js";
|
|
14
|
+
import "./chunk-OYQ6TOAG.js";
|
|
15
|
+
export {
|
|
16
|
+
appendEntry,
|
|
17
|
+
appendEntrySync,
|
|
18
|
+
appendRawEntry,
|
|
19
|
+
appendRawEntrySync,
|
|
20
|
+
buildGuardrailsSection,
|
|
21
|
+
extractContext,
|
|
22
|
+
extractErrorType,
|
|
23
|
+
guardrailsPath,
|
|
24
|
+
migrateGuardrails,
|
|
25
|
+
readGuardrails
|
|
26
|
+
};
|