@tarcisiopgs/lisa 1.30.0 → 1.32.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
@@ -48,6 +49,7 @@ If something fails — pre-push hooks, quota limits, stuck processes — Lisa ha
48
49
  - **CI monitoring** — polls CI after PR creation, re-invokes the agent to fix failures automatically
49
50
  - **Progress comments** — posts real-time status updates on issues as Lisa works through stages
50
51
  - **Context enrichment** — greps for issue-related files and surfaces them in the agent prompt
52
+ - **PR reviewers & assignees** — auto-request reviews and assign PRs via config; `self` keyword resolves to the authenticated user
51
53
  - **Self-healing** — orphan recovery on startup, push failure retry, stuck process detection
52
54
  - **Guardrails** — past failures are injected into future prompts to avoid repeating mistakes
53
55
  - **Project context** — auto-generates `.lisa/context.md` with your stack, conventions, and constraints
@@ -101,6 +103,9 @@ lisa plan --no-brainstorm "goal" # skip brainstorming, decompose directly
101
103
  lisa plan --yes "goal" # skip confirmations (CI/scripts)
102
104
  lisa init # create .lisa/config.yaml interactively
103
105
  lisa status # show session stats
106
+ lisa config --show # print current config
107
+ lisa config --get loop.cooldown # query a specific value
108
+ lisa config --set loop.cooldown=5 # set nested config values
104
109
  lisa doctor # diagnose setup issues (config, provider, env, git)
105
110
  lisa context refresh # regenerate project context
106
111
  lisa feedback --pr URL # inject PR review feedback into guardrails
@@ -235,6 +240,11 @@ proof_of_work:
235
240
  validation:
236
241
  require_acceptance_criteria: true
237
242
 
243
+ spec_compliance:
244
+ enabled: true
245
+ max_retries: 1 # retry agent to fix unmet criteria (default: 1)
246
+ block_on_failure: true # skip PR when criteria aren't met (default: false)
247
+
238
248
  ci_monitor:
239
249
  enabled: true
240
250
  max_retries: 3 # fix attempts on CI failure
@@ -244,10 +254,41 @@ ci_monitor:
244
254
 
245
255
  progress_comments:
246
256
  enabled: true # post real-time status on issues
257
+
258
+ pr:
259
+ reviewers: # auto-request reviews on every PR
260
+ - octocat
261
+ - hubot
262
+ assignees: # auto-assign PRs ("self" = authenticated user)
263
+ - self
264
+
265
+ hooks:
266
+ before_run: "./scripts/setup.sh"
267
+ after_run: "./scripts/cleanup.sh"
268
+ timeout: 60000 # ms, default 60000
247
269
  ```
248
270
 
249
271
  </details>
250
272
 
273
+ ## Validation Pipeline
274
+
275
+ After the agent implements an issue, Lisa runs a multi-stage validation pipeline before creating a PR:
276
+
277
+ ```
278
+ Agent implements → Proof of Work (lint/test/typecheck) → Spec Compliance → PR
279
+ ```
280
+
281
+ **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.
282
+
283
+ **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:
284
+
285
+ | Criterion | Status | Evidence |
286
+ |-----------|--------|----------|
287
+ | Returns 429 on rate limit | Met | Rate limit middleware returns 429 |
288
+ | Headers include X-RateLimit | Not Met | No header injection found |
289
+
290
+ Both stages support `max_retries` and `block_on_failure` — when blocking is enabled, the PR is skipped entirely on failure.
291
+
251
292
  ## Writing Good Issues
252
293
 
253
294
  Issue quality = PR quality. Lisa validates issues and skips vague ones (labeling them `needs-spec`).
@@ -290,15 +331,23 @@ The real-time Kanban board shows issue progress, streams provider output, and de
290
331
  | `m` | Merge PR (warns if CI not passed) |
291
332
  | `Esc` | Back to board |
292
333
 
293
- **Plan mode**
334
+ **Plan chat**
335
+
336
+ | Key | Action |
337
+ |-----|--------|
338
+ | `↵` | Send message |
339
+ | `↑` `↓` | Scroll chat history |
340
+ | `Esc` | Cancel |
341
+
342
+ **Plan review**
294
343
 
295
344
  | Key | Action |
296
345
  |-----|--------|
297
- | `↵` | Send message / view detail |
346
+ | `↵` | View issue detail |
298
347
  | `e` | Edit issue in $EDITOR |
299
348
  | `d` | Delete issue |
300
349
  | `a` | Approve and create issues |
301
- | `Esc` | Cancel / back |
350
+ | `Esc` | Back |
302
351
 
303
352
  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
353
 
@@ -4,6 +4,10 @@ import {
4
4
  analyzeProject,
5
5
  appendPlatformAttribution,
6
6
  appendPlatformProofOfWork,
7
+ appendPlatformSpecCompliance,
8
+ applyPrReviewersAndAssignees,
9
+ buildCompliancePrompt,
10
+ buildComplianceRecoveryPrompt,
7
11
  buildContinuationPrompt,
8
12
  buildImplementPrompt,
9
13
  buildNativeWorktreePrompt,
@@ -15,14 +19,18 @@ import {
15
19
  createSource,
16
20
  detectPackageManager,
17
21
  detectTestRunner,
22
+ extractAcceptanceCriteria,
18
23
  getContextPath,
24
+ getFullDiff,
19
25
  isCompleteProviderExhaustion,
20
26
  isProofOfWorkEnabled,
27
+ isSpecComplianceEnabled,
28
+ parseComplianceResponse,
21
29
  readContext,
22
30
  resolveModels,
23
31
  runValidationCommands,
24
32
  runWithFallback
25
- } from "./chunk-IMIDFVMI.js";
33
+ } from "./chunk-NHC3JG2C.js";
26
34
  import {
27
35
  divider,
28
36
  error,
@@ -185,6 +193,23 @@ function findConfigDir(startDir = process.cwd()) {
185
193
  dir = parent;
186
194
  }
187
195
  }
196
+ var USERNAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,38}$/;
197
+ function isValidPrUsername(s) {
198
+ return s === "self" || USERNAME_RE.test(s);
199
+ }
200
+ function isStringArray(v) {
201
+ return Array.isArray(v) && v.every((item) => typeof item === "string");
202
+ }
203
+ function parsePrConfig(raw) {
204
+ if (!raw) return void 0;
205
+ const reviewers = isStringArray(raw.reviewers) ? raw.reviewers.filter(isValidPrUsername) : void 0;
206
+ const assignees = isStringArray(raw.assignees) ? raw.assignees.filter(isValidPrUsername) : void 0;
207
+ if (!reviewers?.length && !assignees?.length) return void 0;
208
+ return {
209
+ reviewers: reviewers?.length ? reviewers : void 0,
210
+ assignees: assignees?.length ? assignees : void 0
211
+ };
212
+ }
188
213
  function loadConfig(cwd = process.cwd()) {
189
214
  const configPath = getConfigPath(cwd);
190
215
  if (!existsSync(configPath)) {
@@ -225,7 +250,9 @@ function loadConfig(cwd = process.cwd()) {
225
250
  const rawProofOfWork = parsed.proof_of_work;
226
251
  const rawReconciliation = parsed.reconciliation;
227
252
  const rawCiMonitor = parsed.ci_monitor;
253
+ const rawSpecCompliance = parsed.spec_compliance;
228
254
  const rawProgress = parsed.progress_comments;
255
+ const rawPr = parsed.pr;
229
256
  const config = {
230
257
  ...DEFAULT_CONFIG,
231
258
  ...parsedWithoutLogs,
@@ -265,7 +292,13 @@ function loadConfig(cwd = process.cwd()) {
265
292
  poll_timeout: rawCiMonitor.poll_timeout,
266
293
  block_on_failure: rawCiMonitor.block_on_failure
267
294
  } : void 0,
295
+ spec_compliance: rawSpecCompliance ? {
296
+ enabled: rawSpecCompliance.enabled ?? false,
297
+ max_retries: rawSpecCompliance.max_retries,
298
+ block_on_failure: rawSpecCompliance.block_on_failure
299
+ } : void 0,
268
300
  progress_comments: rawProgress ? { enabled: rawProgress.enabled ?? false } : void 0,
301
+ pr: parsePrConfig(rawPr),
269
302
  provider_options: {
270
303
  ...DEFAULT_CONFIG.provider_options || {},
271
304
  ...parsed.provider_options ?? {}
@@ -1175,7 +1208,7 @@ async function startInfra(issueId, cwd, config) {
1175
1208
  function startReconciliationMonitor(source, issueId, config) {
1176
1209
  return source && config.reconciliation?.enabled ? startReconciliation(source, issueId, config.reconciliation, config.source_config) : null;
1177
1210
  }
1178
- async function runProofOfWork(config, issue, models, cwd, logFile, workspace, lifecycleEnv, result) {
1211
+ async function runProofOfWork(config, issue, models, cwd, logFile, workspace, lifecycleEnv, _result) {
1179
1212
  if (!isProofOfWorkEnabled(config.proof_of_work)) return {};
1180
1213
  const pow = config.proof_of_work;
1181
1214
  let retriesLeft = pow?.max_retries ?? 2;
@@ -1229,6 +1262,90 @@ async function runProofOfWork(config, issue, models, cwd, logFile, workspace, li
1229
1262
  }
1230
1263
  }
1231
1264
  }
1265
+ async function runSpecCompliance(config, issue, models, cwd, baseBranch, logFile, workspace, lifecycleEnv) {
1266
+ if (!isSpecComplianceEnabled(config.spec_compliance)) return {};
1267
+ const criteria = extractAcceptanceCriteria(issue.description);
1268
+ if (criteria.length === 0) {
1269
+ warn(`No extractable acceptance criteria for ${issue.id}. Skipping spec compliance.`);
1270
+ return {};
1271
+ }
1272
+ const sc = config.spec_compliance;
1273
+ let retriesLeft = sc?.max_retries ?? 1;
1274
+ while (true) {
1275
+ if (reconciliationSet.has(issue.id)) {
1276
+ reconciliationSet.delete(issue.id);
1277
+ warn(`Issue ${issue.id} was closed/cancelled during spec compliance. Skipping.`);
1278
+ return { reconciled: true };
1279
+ }
1280
+ startSpinner(`${issue.id} \u2014 checking spec compliance...`);
1281
+ const diff = await getFullDiff(cwd, baseBranch);
1282
+ if (!diff) {
1283
+ stopSpinner();
1284
+ warn(`No diff available for spec compliance on ${issue.id}. Skipping.`);
1285
+ return {};
1286
+ }
1287
+ const compliancePrompt = buildCompliancePrompt(issue, criteria, diff);
1288
+ const checkResult = await runWithFallback(
1289
+ models,
1290
+ compliancePrompt,
1291
+ buildRunOptions(config, issue, cwd, logFile, workspace, lifecycleEnv)
1292
+ );
1293
+ stopSpinner();
1294
+ if (!checkResult.success) {
1295
+ warn(`Spec compliance check failed to run for ${issue.id}. Proceeding without it.`);
1296
+ return {};
1297
+ }
1298
+ const compliance = parseComplianceResponse(checkResult.output);
1299
+ if (!compliance) {
1300
+ warn(
1301
+ `Could not parse spec compliance response for ${issue.id}. Proceeding without it.`
1302
+ );
1303
+ return {};
1304
+ }
1305
+ if (compliance.passed) {
1306
+ ok(`Spec compliance passed for ${issue.id}: ${compliance.summary}`);
1307
+ return { result: compliance };
1308
+ }
1309
+ const unmet = compliance.criteria.filter((c) => !c.met);
1310
+ warn(
1311
+ `Spec compliance failed for ${issue.id}: ${compliance.summary} (${unmet.length} unmet)`
1312
+ );
1313
+ if (retriesLeft <= 0) {
1314
+ if (sc?.block_on_failure) {
1315
+ error(
1316
+ `Spec compliance failed after max retries for ${issue.id}. Blocking PR creation.`
1317
+ );
1318
+ return { result: compliance, blocked: true };
1319
+ }
1320
+ warn(
1321
+ `Spec compliance failed after max retries for ${issue.id}. Creating PR with failures noted.`
1322
+ );
1323
+ return { result: compliance };
1324
+ }
1325
+ retriesLeft--;
1326
+ warn(
1327
+ `Re-invoking agent to fix unmet criteria for ${issue.id} (${retriesLeft} retries left)...`
1328
+ );
1329
+ const recoveryPrompt = buildComplianceRecoveryPrompt(issue, unmet);
1330
+ startSpinner(`${issue.id} \u2014 fixing unmet criteria...`);
1331
+ const recoveryResult = await runWithFallback(
1332
+ models,
1333
+ recoveryPrompt,
1334
+ buildRunOptions(config, issue, cwd, logFile, workspace, lifecycleEnv)
1335
+ );
1336
+ stopSpinner();
1337
+ if (!recoveryResult.success) {
1338
+ if (sc?.block_on_failure) {
1339
+ error(`Spec compliance recovery failed for ${issue.id}. Blocking PR creation.`);
1340
+ return { result: compliance, blocked: true };
1341
+ }
1342
+ warn(
1343
+ `Spec compliance recovery failed for ${issue.id}. Creating PR with failures noted.`
1344
+ );
1345
+ return { result: compliance };
1346
+ }
1347
+ }
1348
+ }
1232
1349
 
1233
1350
  // src/loop/recovery.ts
1234
1351
  async function injectRejectedPrFeedback(workspace, issueId, prUrls) {
@@ -2480,6 +2597,7 @@ ${contResult.output}
2480
2597
  await executeHook("before_remove", config.hooks, worktreePath, hookEnv);
2481
2598
  await cleanupWorktree(repoPath, worktreePath);
2482
2599
  await appendPlatformAttribution(prUrl, result.providerUsed, config.platform);
2600
+ await applyPrReviewersAndAssignees(prUrl, config.pr, config.platform);
2483
2601
  ok(`Step ${stepNum} complete: ${repoPath} \u2014 PR: ${prUrl}`);
2484
2602
  return {
2485
2603
  success: true,
@@ -2649,6 +2767,7 @@ async function runNativeWorktreeSession(config, issue, logFile, session, models,
2649
2767
  }
2650
2768
  ok(`PR created by provider: ${prUrl}`);
2651
2769
  await appendPlatformAttribution(prUrl, result.providerUsed, config.platform);
2770
+ await applyPrReviewersAndAssignees(prUrl, config.pr, config.platform);
2652
2771
  if (isCiMonitorEnabled(config.ci_monitor)) {
2653
2772
  const manifestBranch = manifest?.branch;
2654
2773
  if (manifestBranch) {
@@ -2794,6 +2913,29 @@ async function runManualWorktreeSession(config, issue, logFile, session, models,
2794
2913
  if (validationResults) {
2795
2914
  await reporter.update("validating", "Validation passed");
2796
2915
  }
2916
+ const complianceResult = await runSpecCompliance(
2917
+ config,
2918
+ issue,
2919
+ models,
2920
+ worktreePath,
2921
+ baseBranch,
2922
+ logFile,
2923
+ workspace,
2924
+ lifecycleEnv
2925
+ );
2926
+ if (complianceResult.reconciled) {
2927
+ await cleanupWorktree(repoPath, worktreePath);
2928
+ return failureResult(result.providerUsed, result);
2929
+ }
2930
+ if (complianceResult.blocked) {
2931
+ error(
2932
+ `Skipping PR for ${issue.id} \u2014 spec compliance failed with block_on_failure enabled.`
2933
+ );
2934
+ await reporter.fail("Spec compliance failed");
2935
+ await executeHook("before_remove", config.hooks, worktreePath, hookEnv);
2936
+ await cleanupWorktree(repoPath, worktreePath);
2937
+ return failureResult(result.providerUsed, result);
2938
+ }
2797
2939
  const manifest = readManifestFile(manifestPath);
2798
2940
  let prUrl = manifest?.prUrl;
2799
2941
  if (!prUrl) {
@@ -2847,9 +2989,13 @@ async function runManualWorktreeSession(config, issue, logFile, session, models,
2847
2989
  }
2848
2990
  ok(`PR created by provider: ${prUrl}`);
2849
2991
  await appendPlatformAttribution(prUrl, result.providerUsed, config.platform);
2992
+ await applyPrReviewersAndAssignees(prUrl, config.pr, config.platform);
2850
2993
  if (validationResults) {
2851
2994
  await appendPlatformProofOfWork(prUrl, validationResults, config.platform);
2852
2995
  }
2996
+ if (complianceResult.result) {
2997
+ await appendPlatformSpecCompliance(prUrl, complianceResult.result, config.platform);
2998
+ }
2853
2999
  if (isCiMonitorEnabled(config.ci_monitor)) {
2854
3000
  const manifestBranch = manifest?.branch ?? branchName;
2855
3001
  const ciResult = await monitorCi(
@@ -3308,6 +3454,26 @@ async function runBranchSession(config, issue, logFile, session, models, source,
3308
3454
  if (validationResults) {
3309
3455
  await reporter.update("validating", "Validation passed");
3310
3456
  }
3457
+ const complianceResult = await runSpecCompliance(
3458
+ config,
3459
+ issue,
3460
+ models,
3461
+ workspace,
3462
+ config.base_branch,
3463
+ logFile,
3464
+ workspace,
3465
+ lifecycleEnv
3466
+ );
3467
+ if (complianceResult.reconciled) {
3468
+ return failureResult(result.providerUsed, result);
3469
+ }
3470
+ if (complianceResult.blocked) {
3471
+ error(
3472
+ `Skipping PR for ${issue.id} \u2014 spec compliance failed with block_on_failure enabled.`
3473
+ );
3474
+ await reporter.fail("Spec compliance failed");
3475
+ return failureResult(result.providerUsed, result);
3476
+ }
3311
3477
  const manifest = readManifestFile(manifestPath);
3312
3478
  try {
3313
3479
  unlinkSync3(manifestPath);
@@ -3327,9 +3493,13 @@ async function runBranchSession(config, issue, logFile, session, models, source,
3327
3493
  }
3328
3494
  ok(`PR created by provider: ${prUrl}`);
3329
3495
  await appendPlatformAttribution(prUrl, result.providerUsed, config.platform);
3496
+ await applyPrReviewersAndAssignees(prUrl, config.pr, config.platform);
3330
3497
  if (validationResults) {
3331
3498
  await appendPlatformProofOfWork(prUrl, validationResults, config.platform);
3332
3499
  }
3500
+ if (complianceResult.result) {
3501
+ await appendPlatformSpecCompliance(prUrl, complianceResult.result, config.platform);
3502
+ }
3333
3503
  if (isCiMonitorEnabled(config.ci_monitor)) {
3334
3504
  const manifestBranch = manifest?.branch;
3335
3505
  if (manifestBranch) {
@@ -4,7 +4,7 @@ import {
4
4
  readContext,
5
5
  resolveModels,
6
6
  runWithFallback
7
- } from "./chunk-IMIDFVMI.js";
7
+ } from "./chunk-NHC3JG2C.js";
8
8
  import {
9
9
  error,
10
10
  log,
@@ -208,164 +208,7 @@ _Depends on: ${depRefs}_`;
208
208
 
209
209
  // src/plan/prompt.ts
210
210
  import { resolve } from "path";
211
-
212
- // src/plan/language.ts
213
- var STOP_WORDS = {
214
- pt: /* @__PURE__ */ new Set([
215
- "n\xE3o",
216
- "tamb\xE9m",
217
- "j\xE1",
218
- "est\xE1",
219
- "s\xE3o",
220
- "nos",
221
- "das",
222
- "dos",
223
- "pelo",
224
- "pela",
225
- "uma",
226
- "nas",
227
- "aos",
228
- "essa",
229
- "esse",
230
- "isso",
231
- "aqui",
232
- "muito",
233
- "quando",
234
- "como",
235
- "mais",
236
- "ainda",
237
- "fazer",
238
- "deve",
239
- "pode",
240
- "cada",
241
- "todos",
242
- "todas",
243
- "entre",
244
- "ap\xF3s",
245
- "sobre",
246
- "seus",
247
- "suas",
248
- "desta",
249
- "deste",
250
- "onde",
251
- "apenas",
252
- // Common contractions (preposition + article)
253
- "na",
254
- "no",
255
- "da",
256
- "do",
257
- "ao",
258
- "num",
259
- "numa",
260
- "para",
261
- "com",
262
- "sem",
263
- "ou"
264
- ]),
265
- es: /* @__PURE__ */ new Set([
266
- "tambi\xE9n",
267
- "m\xE1s",
268
- "pero",
269
- "muy",
270
- "est\xE1",
271
- "est\xE1n",
272
- "puede",
273
- "todo",
274
- "esta",
275
- "este",
276
- "como",
277
- "cuando",
278
- "donde",
279
- "cada",
280
- "entre",
281
- "sobre",
282
- "despu\xE9s",
283
- "antes",
284
- "desde",
285
- "hasta",
286
- "seg\xFAn",
287
- "durante",
288
- "todos",
289
- "todas",
290
- "otro",
291
- "otra",
292
- "otros",
293
- "otras",
294
- "hacer",
295
- "debe",
296
- "aqu\xED",
297
- "ahora",
298
- "siempre",
299
- "nunca"
300
- ]),
301
- en: /* @__PURE__ */ new Set([
302
- "the",
303
- "is",
304
- "are",
305
- "was",
306
- "were",
307
- "been",
308
- "being",
309
- "have",
310
- "has",
311
- "had",
312
- "having",
313
- "does",
314
- "did",
315
- "will",
316
- "would",
317
- "could",
318
- "should",
319
- "might",
320
- "shall",
321
- "this",
322
- "that",
323
- "these",
324
- "those",
325
- "with",
326
- "from",
327
- "into",
328
- "through",
329
- "during",
330
- "before",
331
- "after",
332
- "above",
333
- "below",
334
- "between",
335
- "each",
336
- "every",
337
- "which",
338
- "when",
339
- "where",
340
- "while"
341
- ])
342
- };
343
- var LANGUAGE_NAMES = {
344
- pt: "Portuguese",
345
- es: "Spanish",
346
- en: "English"
347
- };
348
- function detectLanguage(text2) {
349
- const words = text2.toLowerCase().replace(/[^\p{L}\s]/gu, "").split(/\s+/).filter((w) => w.length > 1);
350
- if (words.length === 0) return "en";
351
- const scores = { pt: 0, es: 0, en: 0 };
352
- for (const word of words) {
353
- for (const [lang, stopWords] of Object.entries(STOP_WORDS)) {
354
- if (stopWords.has(word)) {
355
- scores[lang]++;
356
- }
357
- }
358
- }
359
- const best = Object.entries(scores).sort((a, b) => b[1] - a[1])[0];
360
- return best[1] > 0 ? best[0] : "en";
361
- }
362
- function languageName(code) {
363
- return LANGUAGE_NAMES[code] ?? "English";
364
- }
365
-
366
- // src/plan/prompt.ts
367
211
  function buildPlanningPrompt(goal, config, parentIssueDescription) {
368
- const language = detectLanguage(goal);
369
212
  const workspace = resolve(config.workspace);
370
213
  const contextMd = readContext(workspace);
371
214
  const contextBlock = buildContextMdBlock(contextMd);
@@ -379,6 +222,8 @@ ${parentIssueDescription}
379
222
  ` : "";
380
223
  return `You are a project planning agent. Your job is to decompose a high-level goal into atomic, implementable issues.
381
224
 
225
+ Always respond in the same language the user wrote their goal in.
226
+
382
227
  ## Goal
383
228
 
384
229
  ${goal}
@@ -399,10 +244,6 @@ For each issue, provide:
399
244
  - **order**: Integer (1-based) \u2014 execution order based on dependencies
400
245
  - **dependsOn**: Array of order numbers this issue depends on (empty if independent)
401
246
  ${config.repos.length > 1 ? "- **repo**: Name of the target repository from the list above (required for multi-repo)\n" : ""}
402
- ## Language
403
-
404
- Respond in ${languageName(language)}. Generate all issue titles, descriptions, and acceptance criteria in ${languageName(language)}.
405
-
406
247
  ## Rules
407
248
 
408
249
  1. Each issue MUST be self-contained and completable in a single session
@@ -751,8 +592,6 @@ function markdownToIssue(content, original) {
751
592
 
752
593
  export {
753
594
  CliError,
754
- detectLanguage,
755
- languageName,
756
595
  parseStructuredOutput,
757
596
  createPlanIssues,
758
597
  buildPlanningPrompt,