@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 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** — detects your goal's language (pt/en/es) and generates issues in the same language
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 mode**
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
- | `↵` | Send message / view detail |
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` | Cancel / back |
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-IMIDFVMI.js";
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, result) {
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 execa2 } from "execa";
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,