@tarcisiopgs/lisa 1.30.0 → 1.31.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 +45 -4
- package/dist/{chunk-PBPVDKID.js → chunk-45ZNECZ5.js} +148 -2
- package/dist/{chunk-IMIDFVMI.js → chunk-RQTH257A.js} +167 -3
- package/dist/{chunk-5GZPXARF.js → chunk-YTHUJQKB.js} +3 -164
- package/dist/index.js +238 -54
- package/dist/{kanban-OHFQGK3R.js → kanban-TXO3LZON.js} +77 -21
- package/dist/{loop-TTRI7MFZ.js → loop-INGME5Y3.js} +2 -2
- package/dist/{tui-bridge-LIQLGTZ2.js → tui-bridge-KSE7MJDX.js} +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,8 @@ If something fails — pre-push hooks, quota limits, stuck processes — Lisa ha
|
|
|
40
40
|
- **7 issue trackers** — Linear, GitHub Issues, GitLab Issues, Jira, Trello, Plane, Shortcut
|
|
41
41
|
- **8 AI agents** — Claude Code, Gemini CLI, GitHub Copilot CLI, Cursor Agent, Aider, Goose, OpenCode, Codex
|
|
42
42
|
- **AI planning** — describe a goal, the AI brainstorms with you, decomposes it into issues with dependencies, created in your tracker
|
|
43
|
-
- **Language-aware** —
|
|
43
|
+
- **Language-aware** — responds in the same language you write your goal in
|
|
44
|
+
- **Spec compliance** — LLM-verified acceptance criteria check before PR creation, with auto-retry
|
|
44
45
|
- **Concurrent execution** — process multiple issues in parallel, each in its own worktree
|
|
45
46
|
- **Multi-repo** — plans across repos, creates one PR per repo in the correct order
|
|
46
47
|
- **Model fallback** — chain models; transient errors (429, quota, timeout) auto-switch to the next
|
|
@@ -101,6 +102,9 @@ lisa plan --no-brainstorm "goal" # skip brainstorming, decompose directly
|
|
|
101
102
|
lisa plan --yes "goal" # skip confirmations (CI/scripts)
|
|
102
103
|
lisa init # create .lisa/config.yaml interactively
|
|
103
104
|
lisa status # show session stats
|
|
105
|
+
lisa config --show # print current config
|
|
106
|
+
lisa config --get loop.cooldown # query a specific value
|
|
107
|
+
lisa config --set loop.cooldown=5 # set nested config values
|
|
104
108
|
lisa doctor # diagnose setup issues (config, provider, env, git)
|
|
105
109
|
lisa context refresh # regenerate project context
|
|
106
110
|
lisa feedback --pr URL # inject PR review feedback into guardrails
|
|
@@ -235,6 +239,11 @@ proof_of_work:
|
|
|
235
239
|
validation:
|
|
236
240
|
require_acceptance_criteria: true
|
|
237
241
|
|
|
242
|
+
spec_compliance:
|
|
243
|
+
enabled: true
|
|
244
|
+
max_retries: 1 # retry agent to fix unmet criteria (default: 1)
|
|
245
|
+
block_on_failure: true # skip PR when criteria aren't met (default: false)
|
|
246
|
+
|
|
238
247
|
ci_monitor:
|
|
239
248
|
enabled: true
|
|
240
249
|
max_retries: 3 # fix attempts on CI failure
|
|
@@ -244,10 +253,34 @@ ci_monitor:
|
|
|
244
253
|
|
|
245
254
|
progress_comments:
|
|
246
255
|
enabled: true # post real-time status on issues
|
|
256
|
+
|
|
257
|
+
hooks:
|
|
258
|
+
before_run: "./scripts/setup.sh"
|
|
259
|
+
after_run: "./scripts/cleanup.sh"
|
|
260
|
+
timeout: 60000 # ms, default 60000
|
|
247
261
|
```
|
|
248
262
|
|
|
249
263
|
</details>
|
|
250
264
|
|
|
265
|
+
## Validation Pipeline
|
|
266
|
+
|
|
267
|
+
After the agent implements an issue, Lisa runs a multi-stage validation pipeline before creating a PR:
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
Agent implements → Proof of Work (lint/test/typecheck) → Spec Compliance → PR
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Proof of Work** runs configured shell commands (lint, typecheck, test). If any fail, the agent is re-invoked with the error output to fix the issue.
|
|
274
|
+
|
|
275
|
+
**Spec Compliance** extracts acceptance criteria from the issue description (`- [ ]` checklists) and asks the LLM to verify each one against the git diff. The result is a structured JSON with met/not-met verdicts and evidence. If criteria are unmet, the agent is re-invoked to fix them. Results are appended to the PR body as a Markdown table:
|
|
276
|
+
|
|
277
|
+
| Criterion | Status | Evidence |
|
|
278
|
+
|-----------|--------|----------|
|
|
279
|
+
| Returns 429 on rate limit | Met | Rate limit middleware returns 429 |
|
|
280
|
+
| Headers include X-RateLimit | Not Met | No header injection found |
|
|
281
|
+
|
|
282
|
+
Both stages support `max_retries` and `block_on_failure` — when blocking is enabled, the PR is skipped entirely on failure.
|
|
283
|
+
|
|
251
284
|
## Writing Good Issues
|
|
252
285
|
|
|
253
286
|
Issue quality = PR quality. Lisa validates issues and skips vague ones (labeling them `needs-spec`).
|
|
@@ -290,15 +323,23 @@ The real-time Kanban board shows issue progress, streams provider output, and de
|
|
|
290
323
|
| `m` | Merge PR (warns if CI not passed) |
|
|
291
324
|
| `Esc` | Back to board |
|
|
292
325
|
|
|
293
|
-
**Plan
|
|
326
|
+
**Plan chat**
|
|
327
|
+
|
|
328
|
+
| Key | Action |
|
|
329
|
+
|-----|--------|
|
|
330
|
+
| `↵` | Send message |
|
|
331
|
+
| `↑` `↓` | Scroll chat history |
|
|
332
|
+
| `Esc` | Cancel |
|
|
333
|
+
|
|
334
|
+
**Plan review**
|
|
294
335
|
|
|
295
336
|
| Key | Action |
|
|
296
337
|
|-----|--------|
|
|
297
|
-
| `↵` |
|
|
338
|
+
| `↵` | View issue detail |
|
|
298
339
|
| `e` | Edit issue in $EDITOR |
|
|
299
340
|
| `d` | Delete issue |
|
|
300
341
|
| `a` | Approve and create issues |
|
|
301
|
-
| `Esc` |
|
|
342
|
+
| `Esc` | Back |
|
|
302
343
|
|
|
303
344
|
In CLI mode, the plan wizard also offers **Regenerate with feedback** — describe what to change and the AI regenerates the entire plan incorporating your feedback.
|
|
304
345
|
|
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
analyzeProject,
|
|
5
5
|
appendPlatformAttribution,
|
|
6
6
|
appendPlatformProofOfWork,
|
|
7
|
+
appendPlatformSpecCompliance,
|
|
8
|
+
buildCompliancePrompt,
|
|
9
|
+
buildComplianceRecoveryPrompt,
|
|
7
10
|
buildContinuationPrompt,
|
|
8
11
|
buildImplementPrompt,
|
|
9
12
|
buildNativeWorktreePrompt,
|
|
@@ -15,14 +18,18 @@ import {
|
|
|
15
18
|
createSource,
|
|
16
19
|
detectPackageManager,
|
|
17
20
|
detectTestRunner,
|
|
21
|
+
extractAcceptanceCriteria,
|
|
18
22
|
getContextPath,
|
|
23
|
+
getFullDiff,
|
|
19
24
|
isCompleteProviderExhaustion,
|
|
20
25
|
isProofOfWorkEnabled,
|
|
26
|
+
isSpecComplianceEnabled,
|
|
27
|
+
parseComplianceResponse,
|
|
21
28
|
readContext,
|
|
22
29
|
resolveModels,
|
|
23
30
|
runValidationCommands,
|
|
24
31
|
runWithFallback
|
|
25
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-RQTH257A.js";
|
|
26
33
|
import {
|
|
27
34
|
divider,
|
|
28
35
|
error,
|
|
@@ -225,6 +232,7 @@ function loadConfig(cwd = process.cwd()) {
|
|
|
225
232
|
const rawProofOfWork = parsed.proof_of_work;
|
|
226
233
|
const rawReconciliation = parsed.reconciliation;
|
|
227
234
|
const rawCiMonitor = parsed.ci_monitor;
|
|
235
|
+
const rawSpecCompliance = parsed.spec_compliance;
|
|
228
236
|
const rawProgress = parsed.progress_comments;
|
|
229
237
|
const config = {
|
|
230
238
|
...DEFAULT_CONFIG,
|
|
@@ -265,6 +273,11 @@ function loadConfig(cwd = process.cwd()) {
|
|
|
265
273
|
poll_timeout: rawCiMonitor.poll_timeout,
|
|
266
274
|
block_on_failure: rawCiMonitor.block_on_failure
|
|
267
275
|
} : void 0,
|
|
276
|
+
spec_compliance: rawSpecCompliance ? {
|
|
277
|
+
enabled: rawSpecCompliance.enabled ?? false,
|
|
278
|
+
max_retries: rawSpecCompliance.max_retries,
|
|
279
|
+
block_on_failure: rawSpecCompliance.block_on_failure
|
|
280
|
+
} : void 0,
|
|
268
281
|
progress_comments: rawProgress ? { enabled: rawProgress.enabled ?? false } : void 0,
|
|
269
282
|
provider_options: {
|
|
270
283
|
...DEFAULT_CONFIG.provider_options || {},
|
|
@@ -1175,7 +1188,7 @@ async function startInfra(issueId, cwd, config) {
|
|
|
1175
1188
|
function startReconciliationMonitor(source, issueId, config) {
|
|
1176
1189
|
return source && config.reconciliation?.enabled ? startReconciliation(source, issueId, config.reconciliation, config.source_config) : null;
|
|
1177
1190
|
}
|
|
1178
|
-
async function runProofOfWork(config, issue, models, cwd, logFile, workspace, lifecycleEnv,
|
|
1191
|
+
async function runProofOfWork(config, issue, models, cwd, logFile, workspace, lifecycleEnv, _result) {
|
|
1179
1192
|
if (!isProofOfWorkEnabled(config.proof_of_work)) return {};
|
|
1180
1193
|
const pow = config.proof_of_work;
|
|
1181
1194
|
let retriesLeft = pow?.max_retries ?? 2;
|
|
@@ -1229,6 +1242,90 @@ async function runProofOfWork(config, issue, models, cwd, logFile, workspace, li
|
|
|
1229
1242
|
}
|
|
1230
1243
|
}
|
|
1231
1244
|
}
|
|
1245
|
+
async function runSpecCompliance(config, issue, models, cwd, baseBranch, logFile, workspace, lifecycleEnv) {
|
|
1246
|
+
if (!isSpecComplianceEnabled(config.spec_compliance)) return {};
|
|
1247
|
+
const criteria = extractAcceptanceCriteria(issue.description);
|
|
1248
|
+
if (criteria.length === 0) {
|
|
1249
|
+
warn(`No extractable acceptance criteria for ${issue.id}. Skipping spec compliance.`);
|
|
1250
|
+
return {};
|
|
1251
|
+
}
|
|
1252
|
+
const sc = config.spec_compliance;
|
|
1253
|
+
let retriesLeft = sc?.max_retries ?? 1;
|
|
1254
|
+
while (true) {
|
|
1255
|
+
if (reconciliationSet.has(issue.id)) {
|
|
1256
|
+
reconciliationSet.delete(issue.id);
|
|
1257
|
+
warn(`Issue ${issue.id} was closed/cancelled during spec compliance. Skipping.`);
|
|
1258
|
+
return { reconciled: true };
|
|
1259
|
+
}
|
|
1260
|
+
startSpinner(`${issue.id} \u2014 checking spec compliance...`);
|
|
1261
|
+
const diff = await getFullDiff(cwd, baseBranch);
|
|
1262
|
+
if (!diff) {
|
|
1263
|
+
stopSpinner();
|
|
1264
|
+
warn(`No diff available for spec compliance on ${issue.id}. Skipping.`);
|
|
1265
|
+
return {};
|
|
1266
|
+
}
|
|
1267
|
+
const compliancePrompt = buildCompliancePrompt(issue, criteria, diff);
|
|
1268
|
+
const checkResult = await runWithFallback(
|
|
1269
|
+
models,
|
|
1270
|
+
compliancePrompt,
|
|
1271
|
+
buildRunOptions(config, issue, cwd, logFile, workspace, lifecycleEnv)
|
|
1272
|
+
);
|
|
1273
|
+
stopSpinner();
|
|
1274
|
+
if (!checkResult.success) {
|
|
1275
|
+
warn(`Spec compliance check failed to run for ${issue.id}. Proceeding without it.`);
|
|
1276
|
+
return {};
|
|
1277
|
+
}
|
|
1278
|
+
const compliance = parseComplianceResponse(checkResult.output);
|
|
1279
|
+
if (!compliance) {
|
|
1280
|
+
warn(
|
|
1281
|
+
`Could not parse spec compliance response for ${issue.id}. Proceeding without it.`
|
|
1282
|
+
);
|
|
1283
|
+
return {};
|
|
1284
|
+
}
|
|
1285
|
+
if (compliance.passed) {
|
|
1286
|
+
ok(`Spec compliance passed for ${issue.id}: ${compliance.summary}`);
|
|
1287
|
+
return { result: compliance };
|
|
1288
|
+
}
|
|
1289
|
+
const unmet = compliance.criteria.filter((c) => !c.met);
|
|
1290
|
+
warn(
|
|
1291
|
+
`Spec compliance failed for ${issue.id}: ${compliance.summary} (${unmet.length} unmet)`
|
|
1292
|
+
);
|
|
1293
|
+
if (retriesLeft <= 0) {
|
|
1294
|
+
if (sc?.block_on_failure) {
|
|
1295
|
+
error(
|
|
1296
|
+
`Spec compliance failed after max retries for ${issue.id}. Blocking PR creation.`
|
|
1297
|
+
);
|
|
1298
|
+
return { result: compliance, blocked: true };
|
|
1299
|
+
}
|
|
1300
|
+
warn(
|
|
1301
|
+
`Spec compliance failed after max retries for ${issue.id}. Creating PR with failures noted.`
|
|
1302
|
+
);
|
|
1303
|
+
return { result: compliance };
|
|
1304
|
+
}
|
|
1305
|
+
retriesLeft--;
|
|
1306
|
+
warn(
|
|
1307
|
+
`Re-invoking agent to fix unmet criteria for ${issue.id} (${retriesLeft} retries left)...`
|
|
1308
|
+
);
|
|
1309
|
+
const recoveryPrompt = buildComplianceRecoveryPrompt(issue, unmet);
|
|
1310
|
+
startSpinner(`${issue.id} \u2014 fixing unmet criteria...`);
|
|
1311
|
+
const recoveryResult = await runWithFallback(
|
|
1312
|
+
models,
|
|
1313
|
+
recoveryPrompt,
|
|
1314
|
+
buildRunOptions(config, issue, cwd, logFile, workspace, lifecycleEnv)
|
|
1315
|
+
);
|
|
1316
|
+
stopSpinner();
|
|
1317
|
+
if (!recoveryResult.success) {
|
|
1318
|
+
if (sc?.block_on_failure) {
|
|
1319
|
+
error(`Spec compliance recovery failed for ${issue.id}. Blocking PR creation.`);
|
|
1320
|
+
return { result: compliance, blocked: true };
|
|
1321
|
+
}
|
|
1322
|
+
warn(
|
|
1323
|
+
`Spec compliance recovery failed for ${issue.id}. Creating PR with failures noted.`
|
|
1324
|
+
);
|
|
1325
|
+
return { result: compliance };
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1232
1329
|
|
|
1233
1330
|
// src/loop/recovery.ts
|
|
1234
1331
|
async function injectRejectedPrFeedback(workspace, issueId, prUrls) {
|
|
@@ -2794,6 +2891,29 @@ async function runManualWorktreeSession(config, issue, logFile, session, models,
|
|
|
2794
2891
|
if (validationResults) {
|
|
2795
2892
|
await reporter.update("validating", "Validation passed");
|
|
2796
2893
|
}
|
|
2894
|
+
const complianceResult = await runSpecCompliance(
|
|
2895
|
+
config,
|
|
2896
|
+
issue,
|
|
2897
|
+
models,
|
|
2898
|
+
worktreePath,
|
|
2899
|
+
baseBranch,
|
|
2900
|
+
logFile,
|
|
2901
|
+
workspace,
|
|
2902
|
+
lifecycleEnv
|
|
2903
|
+
);
|
|
2904
|
+
if (complianceResult.reconciled) {
|
|
2905
|
+
await cleanupWorktree(repoPath, worktreePath);
|
|
2906
|
+
return failureResult(result.providerUsed, result);
|
|
2907
|
+
}
|
|
2908
|
+
if (complianceResult.blocked) {
|
|
2909
|
+
error(
|
|
2910
|
+
`Skipping PR for ${issue.id} \u2014 spec compliance failed with block_on_failure enabled.`
|
|
2911
|
+
);
|
|
2912
|
+
await reporter.fail("Spec compliance failed");
|
|
2913
|
+
await executeHook("before_remove", config.hooks, worktreePath, hookEnv);
|
|
2914
|
+
await cleanupWorktree(repoPath, worktreePath);
|
|
2915
|
+
return failureResult(result.providerUsed, result);
|
|
2916
|
+
}
|
|
2797
2917
|
const manifest = readManifestFile(manifestPath);
|
|
2798
2918
|
let prUrl = manifest?.prUrl;
|
|
2799
2919
|
if (!prUrl) {
|
|
@@ -2850,6 +2970,9 @@ async function runManualWorktreeSession(config, issue, logFile, session, models,
|
|
|
2850
2970
|
if (validationResults) {
|
|
2851
2971
|
await appendPlatformProofOfWork(prUrl, validationResults, config.platform);
|
|
2852
2972
|
}
|
|
2973
|
+
if (complianceResult.result) {
|
|
2974
|
+
await appendPlatformSpecCompliance(prUrl, complianceResult.result, config.platform);
|
|
2975
|
+
}
|
|
2853
2976
|
if (isCiMonitorEnabled(config.ci_monitor)) {
|
|
2854
2977
|
const manifestBranch = manifest?.branch ?? branchName;
|
|
2855
2978
|
const ciResult = await monitorCi(
|
|
@@ -3308,6 +3431,26 @@ async function runBranchSession(config, issue, logFile, session, models, source,
|
|
|
3308
3431
|
if (validationResults) {
|
|
3309
3432
|
await reporter.update("validating", "Validation passed");
|
|
3310
3433
|
}
|
|
3434
|
+
const complianceResult = await runSpecCompliance(
|
|
3435
|
+
config,
|
|
3436
|
+
issue,
|
|
3437
|
+
models,
|
|
3438
|
+
workspace,
|
|
3439
|
+
config.base_branch,
|
|
3440
|
+
logFile,
|
|
3441
|
+
workspace,
|
|
3442
|
+
lifecycleEnv
|
|
3443
|
+
);
|
|
3444
|
+
if (complianceResult.reconciled) {
|
|
3445
|
+
return failureResult(result.providerUsed, result);
|
|
3446
|
+
}
|
|
3447
|
+
if (complianceResult.blocked) {
|
|
3448
|
+
error(
|
|
3449
|
+
`Skipping PR for ${issue.id} \u2014 spec compliance failed with block_on_failure enabled.`
|
|
3450
|
+
);
|
|
3451
|
+
await reporter.fail("Spec compliance failed");
|
|
3452
|
+
return failureResult(result.providerUsed, result);
|
|
3453
|
+
}
|
|
3311
3454
|
const manifest = readManifestFile(manifestPath);
|
|
3312
3455
|
try {
|
|
3313
3456
|
unlinkSync3(manifestPath);
|
|
@@ -3330,6 +3473,9 @@ async function runBranchSession(config, issue, logFile, session, models, source,
|
|
|
3330
3473
|
if (validationResults) {
|
|
3331
3474
|
await appendPlatformProofOfWork(prUrl, validationResults, config.platform);
|
|
3332
3475
|
}
|
|
3476
|
+
if (complianceResult.result) {
|
|
3477
|
+
await appendPlatformSpecCompliance(prUrl, complianceResult.result, config.platform);
|
|
3478
|
+
}
|
|
3333
3479
|
if (isCiMonitorEnabled(config.ci_monitor)) {
|
|
3334
3480
|
const manifestBranch = manifest?.branch;
|
|
3335
3481
|
if (manifestBranch) {
|
|
@@ -2745,6 +2745,150 @@ function isProofOfWorkEnabled(config) {
|
|
|
2745
2745
|
return !!(config?.enabled && config.commands.length > 0);
|
|
2746
2746
|
}
|
|
2747
2747
|
|
|
2748
|
+
// src/session/spec-compliance.ts
|
|
2749
|
+
import { execa } from "execa";
|
|
2750
|
+
function extractAcceptanceCriteria(description) {
|
|
2751
|
+
const criteria = [];
|
|
2752
|
+
const checklistRegex = /^[\t ]*- \[ \]\s*(.+)$/gm;
|
|
2753
|
+
let match;
|
|
2754
|
+
match = checklistRegex.exec(description);
|
|
2755
|
+
while (match) {
|
|
2756
|
+
if (match[1]) criteria.push(match[1].trim());
|
|
2757
|
+
match = checklistRegex.exec(description);
|
|
2758
|
+
}
|
|
2759
|
+
if (criteria.length > 0) return criteria;
|
|
2760
|
+
const headerRegex = /(?:acceptance criteria|critérios de aceite|expected behavior)[:\s]*\n/i;
|
|
2761
|
+
const headerMatch = headerRegex.exec(description);
|
|
2762
|
+
if (headerMatch) {
|
|
2763
|
+
const afterHeader = description.slice(headerMatch.index + headerMatch[0].length);
|
|
2764
|
+
const lines = afterHeader.split("\n");
|
|
2765
|
+
for (const line of lines) {
|
|
2766
|
+
const trimmed = line.trim();
|
|
2767
|
+
if (trimmed.startsWith("#") || trimmed.startsWith("---")) break;
|
|
2768
|
+
const listMatch = /^[-*]\s+(.+)$/.exec(trimmed);
|
|
2769
|
+
if (listMatch?.[1]) {
|
|
2770
|
+
criteria.push(listMatch[1].trim());
|
|
2771
|
+
}
|
|
2772
|
+
const numberedMatch = /^\d+[.)]\s+(.+)$/.exec(trimmed);
|
|
2773
|
+
if (numberedMatch?.[1]) {
|
|
2774
|
+
criteria.push(numberedMatch[1].trim());
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
return criteria;
|
|
2779
|
+
}
|
|
2780
|
+
async function getFullDiff(cwd, baseBranch, maxChars = 3e4) {
|
|
2781
|
+
try {
|
|
2782
|
+
const { stdout } = await execa("git", ["diff", `${baseBranch}..HEAD`], {
|
|
2783
|
+
cwd,
|
|
2784
|
+
reject: false
|
|
2785
|
+
});
|
|
2786
|
+
const diff = stdout.trim();
|
|
2787
|
+
if (diff.length <= maxChars) return diff;
|
|
2788
|
+
return `${diff.slice(0, maxChars)}
|
|
2789
|
+
|
|
2790
|
+
[... diff truncated at ${maxChars} characters ...]`;
|
|
2791
|
+
} catch {
|
|
2792
|
+
return "";
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
function buildCompliancePrompt(issue, criteria, diff) {
|
|
2796
|
+
const criteriaList = criteria.map((c, i) => `${i + 1}. ${c}`).join("\n");
|
|
2797
|
+
return `You are a spec compliance validator. Your ONLY task is to check if an implementation satisfies the acceptance criteria. Do NOT modify any files or run any commands.
|
|
2798
|
+
|
|
2799
|
+
## Issue
|
|
2800
|
+
${issue.id}: ${issue.title}
|
|
2801
|
+
|
|
2802
|
+
## Acceptance Criteria
|
|
2803
|
+
${criteriaList}
|
|
2804
|
+
|
|
2805
|
+
## Implementation (git diff)
|
|
2806
|
+
\`\`\`diff
|
|
2807
|
+
${diff}
|
|
2808
|
+
\`\`\`
|
|
2809
|
+
|
|
2810
|
+
## Task
|
|
2811
|
+
For each acceptance criterion above, determine if the implementation (git diff) satisfies it.
|
|
2812
|
+
|
|
2813
|
+
Respond with ONLY a valid JSON object \u2014 no markdown fences, no explanation, no other text:
|
|
2814
|
+
|
|
2815
|
+
{
|
|
2816
|
+
"criteria": [
|
|
2817
|
+
{ "criterion": "the criterion text", "met": true, "evidence": "brief explanation of how it's met" },
|
|
2818
|
+
{ "criterion": "the criterion text", "met": false, "evidence": "what is missing or wrong" }
|
|
2819
|
+
],
|
|
2820
|
+
"summary": "X/Y criteria met",
|
|
2821
|
+
"passed": false
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
IMPORTANT:
|
|
2825
|
+
- Do NOT create, edit, or modify any files.
|
|
2826
|
+
- Do NOT run any shell commands.
|
|
2827
|
+
- Do NOT create branches or commits.
|
|
2828
|
+
- ONLY output the JSON object above.`;
|
|
2829
|
+
}
|
|
2830
|
+
function buildComplianceRecoveryPrompt(issue, unmetCriteria) {
|
|
2831
|
+
const unmetList = unmetCriteria.map((c, i) => `${i + 1}. **${c.criterion}**
|
|
2832
|
+
Reason: ${c.evidence}`).join("\n\n");
|
|
2833
|
+
return `You are continuing work on issue ${issue.id}: "${issue.title}".
|
|
2834
|
+
|
|
2835
|
+
Your implementation was checked against the acceptance criteria and the following were NOT met:
|
|
2836
|
+
|
|
2837
|
+
${unmetList}
|
|
2838
|
+
|
|
2839
|
+
Fix ONLY the unmet criteria above. Commit and push your changes.
|
|
2840
|
+
|
|
2841
|
+
IMPORTANT:
|
|
2842
|
+
- Do NOT create a new branch \u2014 you are already on the correct branch.
|
|
2843
|
+
- Fix ONLY the unmet criteria listed above.
|
|
2844
|
+
- Commit and push your fixes.
|
|
2845
|
+
- Do NOT create a PR \u2014 that will be handled separately.`;
|
|
2846
|
+
}
|
|
2847
|
+
function parseComplianceResponse(output) {
|
|
2848
|
+
const jsonPatterns = [
|
|
2849
|
+
// Direct JSON object
|
|
2850
|
+
/\{[\s\S]*"criteria"[\s\S]*\}/,
|
|
2851
|
+
// Inside markdown code fence
|
|
2852
|
+
/```(?:json)?\s*(\{[\s\S]*"criteria"[\s\S]*\})\s*```/
|
|
2853
|
+
];
|
|
2854
|
+
for (const pattern of jsonPatterns) {
|
|
2855
|
+
const match = pattern.exec(output);
|
|
2856
|
+
if (match) {
|
|
2857
|
+
const jsonStr = match[1] ?? match[0];
|
|
2858
|
+
try {
|
|
2859
|
+
const parsed = JSON.parse(jsonStr);
|
|
2860
|
+
if (Array.isArray(parsed.criteria)) {
|
|
2861
|
+
const allMet = parsed.criteria.every((c) => c.met);
|
|
2862
|
+
return {
|
|
2863
|
+
criteria: parsed.criteria,
|
|
2864
|
+
passed: allMet,
|
|
2865
|
+
summary: parsed.summary || `${parsed.criteria.filter((c) => c.met).length}/${parsed.criteria.length} criteria met`
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
} catch {
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
return null;
|
|
2873
|
+
}
|
|
2874
|
+
function isSpecComplianceEnabled(config) {
|
|
2875
|
+
return config?.enabled === true;
|
|
2876
|
+
}
|
|
2877
|
+
function formatSpecCompliance(result) {
|
|
2878
|
+
const lines = ["", "---", "## Spec Compliance", ""];
|
|
2879
|
+
lines.push(`**${result.summary}**`);
|
|
2880
|
+
lines.push("");
|
|
2881
|
+
lines.push("| Criterion | Status | Evidence |");
|
|
2882
|
+
lines.push("|-----------|--------|----------|");
|
|
2883
|
+
for (const c of result.criteria) {
|
|
2884
|
+
const status = c.met ? "Met" : "Not Met";
|
|
2885
|
+
const evidence = c.evidence.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
2886
|
+
const criterion = c.criterion.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
2887
|
+
lines.push(`| ${criterion} | ${status} | ${evidence} |`);
|
|
2888
|
+
}
|
|
2889
|
+
return lines.join("\n");
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2748
2892
|
// src/prompt.ts
|
|
2749
2893
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
2750
2894
|
import { join as join5, resolve } from "path";
|
|
@@ -3117,7 +3261,7 @@ function isDirectory(path) {
|
|
|
3117
3261
|
}
|
|
3118
3262
|
|
|
3119
3263
|
// src/git/bitbucket.ts
|
|
3120
|
-
import { execa } from "execa";
|
|
3264
|
+
import { execa as execa2 } from "execa";
|
|
3121
3265
|
var API_URL3 = "https://api.bitbucket.org/2.0";
|
|
3122
3266
|
var REQUEST_TIMEOUT_MS2 = 3e4;
|
|
3123
3267
|
var PROVIDER_DISPLAY_NAMES = {
|
|
@@ -3206,7 +3350,7 @@ async function appendPrBody2(prUrl, content) {
|
|
|
3206
3350
|
}
|
|
3207
3351
|
|
|
3208
3352
|
// src/git/gitlab.ts
|
|
3209
|
-
import { execa as
|
|
3353
|
+
import { execa as execa3 } from "execa";
|
|
3210
3354
|
var REQUEST_TIMEOUT_MS3 = 3e4;
|
|
3211
3355
|
var PROVIDER_DISPLAY_NAMES2 = {
|
|
3212
3356
|
claude: "Claude Code",
|
|
@@ -3312,6 +3456,19 @@ async function appendPlatformProofOfWork(prUrl, results, platform2) {
|
|
|
3312
3456
|
} catch {
|
|
3313
3457
|
}
|
|
3314
3458
|
}
|
|
3459
|
+
async function appendPlatformSpecCompliance(prUrl, result, platform2) {
|
|
3460
|
+
const section = formatSpecCompliance(result);
|
|
3461
|
+
try {
|
|
3462
|
+
if (platform2 === "gitlab") {
|
|
3463
|
+
await appendMrBody(prUrl, section);
|
|
3464
|
+
} else if (platform2 === "bitbucket") {
|
|
3465
|
+
await appendPrBody2(prUrl, section);
|
|
3466
|
+
} else {
|
|
3467
|
+
await appendPrBody(prUrl, section);
|
|
3468
|
+
}
|
|
3469
|
+
} catch {
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3315
3472
|
function buildPrCreateInstruction(platform2, targetBranch) {
|
|
3316
3473
|
const base = targetBranch ? ` --base ${targetBranch}` : "";
|
|
3317
3474
|
if (platform2 === "gitlab") {
|
|
@@ -3939,12 +4096,19 @@ export {
|
|
|
3939
4096
|
readContext,
|
|
3940
4097
|
WATCH_POLL_INTERVAL_MS,
|
|
3941
4098
|
resolveModels,
|
|
3942
|
-
analyzeProject,
|
|
3943
4099
|
runValidationCommands,
|
|
3944
4100
|
buildValidationRecoveryPrompt,
|
|
3945
4101
|
isProofOfWorkEnabled,
|
|
4102
|
+
extractAcceptanceCriteria,
|
|
4103
|
+
getFullDiff,
|
|
4104
|
+
buildCompliancePrompt,
|
|
4105
|
+
buildComplianceRecoveryPrompt,
|
|
4106
|
+
parseComplianceResponse,
|
|
4107
|
+
isSpecComplianceEnabled,
|
|
4108
|
+
analyzeProject,
|
|
3946
4109
|
appendPlatformAttribution,
|
|
3947
4110
|
appendPlatformProofOfWork,
|
|
4111
|
+
appendPlatformSpecCompliance,
|
|
3948
4112
|
detectPackageManager,
|
|
3949
4113
|
detectTestRunner,
|
|
3950
4114
|
buildImplementPrompt,
|