@prestyj/cli 5.30.2 → 5.30.3

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.
Files changed (116) hide show
  1. package/dist/app-sidecar/app-settings.d.ts +6 -2
  2. package/dist/app-sidecar/app-settings.js +39 -20
  3. package/dist/app-sidecar/app-settings.js.map +1 -1
  4. package/dist/app-sidecar/git-target.d.ts +17 -0
  5. package/dist/app-sidecar/git-target.js +31 -0
  6. package/dist/app-sidecar/git-target.js.map +1 -0
  7. package/dist/app-sidecar/goal-routes.d.ts +26 -0
  8. package/dist/app-sidecar/goal-routes.js +445 -0
  9. package/dist/app-sidecar/goal-routes.js.map +1 -0
  10. package/dist/app-sidecar/history-routes.d.ts +13 -0
  11. package/dist/app-sidecar/history-routes.js +36 -25
  12. package/dist/app-sidecar/history-routes.js.map +1 -1
  13. package/dist/app-sidecar/mcp-routes.js +1 -2
  14. package/dist/app-sidecar/mcp-routes.js.map +1 -1
  15. package/dist/app-sidecar/route-context.d.ts +12 -0
  16. package/dist/app-sidecar/run-control-routes.js +2 -1
  17. package/dist/app-sidecar/run-control-routes.js.map +1 -1
  18. package/dist/app-sidecar/session-routes.js +41 -0
  19. package/dist/app-sidecar/session-routes.js.map +1 -1
  20. package/dist/app-sidecar/session-types.d.ts +2 -0
  21. package/dist/app-sidecar/workspace-routes.js +13 -95
  22. package/dist/app-sidecar/workspace-routes.js.map +1 -1
  23. package/dist/app-sidecar.js +213 -60
  24. package/dist/app-sidecar.js.map +1 -1
  25. package/dist/cli/shared.js +1 -1
  26. package/dist/cli/shared.js.map +1 -1
  27. package/dist/core/acceptance-checks.js +17 -4
  28. package/dist/core/acceptance-checks.js.map +1 -1
  29. package/dist/core/agent-notifications.d.ts +1 -1
  30. package/dist/core/agent-session.d.ts +83 -5
  31. package/dist/core/agent-session.js +174 -59
  32. package/dist/core/agent-session.js.map +1 -1
  33. package/dist/core/ask-user.d.ts +11 -0
  34. package/dist/core/ask-user.js +6 -1
  35. package/dist/core/ask-user.js.map +1 -1
  36. package/dist/core/auth-providers.d.ts +2 -80
  37. package/dist/core/auth-providers.js +4 -183
  38. package/dist/core/auth-providers.js.map +1 -1
  39. package/dist/core/cache-diagnostics.d.ts +1 -1
  40. package/dist/core/cache-diagnostics.js +2 -1
  41. package/dist/core/cache-diagnostics.js.map +1 -1
  42. package/dist/core/compaction/tool-result-pruner.d.ts +10 -0
  43. package/dist/core/compaction/tool-result-pruner.js +12 -0
  44. package/dist/core/compaction/tool-result-pruner.js.map +1 -1
  45. package/dist/core/context-files.d.ts +8 -0
  46. package/dist/core/context-files.js +46 -0
  47. package/dist/core/context-files.js.map +1 -0
  48. package/dist/core/env-delta.d.ts +9 -1
  49. package/dist/core/env-delta.js +20 -0
  50. package/dist/core/env-delta.js.map +1 -1
  51. package/dist/core/goals-store.d.ts +208 -0
  52. package/dist/core/goals-store.js +521 -0
  53. package/dist/core/goals-store.js.map +1 -0
  54. package/dist/core/local-endpoint-store.js +15 -11
  55. package/dist/core/local-endpoint-store.js.map +1 -1
  56. package/dist/core/project-discovery.d.ts +7 -0
  57. package/dist/core/project-discovery.js +9 -3
  58. package/dist/core/project-discovery.js.map +1 -1
  59. package/dist/core/radio.d.ts +2 -3
  60. package/dist/core/radio.js.map +1 -1
  61. package/dist/core/session-manager.d.ts +7 -1
  62. package/dist/core/session-manager.js +17 -7
  63. package/dist/core/session-manager.js.map +1 -1
  64. package/dist/core/session-storage.d.ts +3 -0
  65. package/dist/core/session-storage.js +8 -2
  66. package/dist/core/session-storage.js.map +1 -1
  67. package/dist/core/steroids.d.ts +8 -0
  68. package/dist/core/steroids.js +19 -3
  69. package/dist/core/steroids.js.map +1 -1
  70. package/dist/core/subagent-manager.d.ts +51 -0
  71. package/dist/core/subagent-manager.js +103 -16
  72. package/dist/core/subagent-manager.js.map +1 -1
  73. package/dist/core/subagent-store.js +2 -1
  74. package/dist/core/subagent-store.js.map +1 -1
  75. package/dist/goals-agent/goal-team.d.ts +2 -0
  76. package/dist/goals-agent/goal-team.js +83 -0
  77. package/dist/goals-agent/goal-team.js.map +1 -0
  78. package/dist/goals-agent/goals-agent.d.ts +90 -0
  79. package/dist/goals-agent/goals-agent.js +246 -0
  80. package/dist/goals-agent/goals-agent.js.map +1 -0
  81. package/dist/index.js +1 -1
  82. package/dist/index.js.map +1 -1
  83. package/dist/interactive.js +4 -3
  84. package/dist/interactive.js.map +1 -1
  85. package/dist/modes/subagent-worker-mode.d.ts +14 -0
  86. package/dist/modes/subagent-worker-mode.js +32 -1
  87. package/dist/modes/subagent-worker-mode.js.map +1 -1
  88. package/dist/system-prompt.d.ts +8 -0
  89. package/dist/system-prompt.js +21 -9
  90. package/dist/system-prompt.js.map +1 -1
  91. package/dist/tools/bash-read-evidence.d.ts +13 -0
  92. package/dist/tools/bash-read-evidence.js +48 -1
  93. package/dist/tools/bash-read-evidence.js.map +1 -1
  94. package/dist/tools/index.d.ts +7 -1
  95. package/dist/tools/index.js +10 -2
  96. package/dist/tools/index.js.map +1 -1
  97. package/dist/tools/read-tracker.d.ts +6 -12
  98. package/dist/tools/read-tracker.js +21 -36
  99. package/dist/tools/read-tracker.js.map +1 -1
  100. package/dist/tools/read.js +2 -2
  101. package/dist/tools/read.js.map +1 -1
  102. package/dist/tools/subagent-control.js +22 -3
  103. package/dist/tools/subagent-control.js.map +1 -1
  104. package/dist/tools/write.js +110 -8
  105. package/dist/tools/write.js.map +1 -1
  106. package/dist/ui/components/ActivityIndicator.d.ts +5 -5
  107. package/dist/ui/components/InputArea.d.ts +2 -2
  108. package/dist/ui/components/InputArea.js +3 -3
  109. package/dist/ui/components/InputArea.js.map +1 -1
  110. package/dist/ui/components/ToolExecution.d.ts +2 -2
  111. package/dist/ui/hooks/useContextCompaction.js +38 -31
  112. package/dist/ui/hooks/useContextCompaction.js.map +1 -1
  113. package/dist/utils/format.d.ts +0 -4
  114. package/dist/utils/format.js +0 -7
  115. package/dist/utils/format.js.map +1 -1
  116. package/package.json +4 -4
@@ -27,7 +27,7 @@ import { partitionToolsByTier } from "../tools/tool-tiers.js";
27
27
  import { formatImpactForVerification } from "./test-impact.js";
28
28
  import { buildProcessCompletionFollowUp } from "./process-gate.js";
29
29
  import { PLAN_SUBMISSION_NUDGE, shouldNudgePlanSubmission } from "./plan-submission-gate.js";
30
- import { buildSubAgentCompletionFollowUp } from "./subagent-manager.js";
30
+ import { buildSubAgentCompletionFollowUp, } from "./subagent-manager.js";
31
31
  import { applyAsyncSubagentPolicy } from "./subagent-policy.js";
32
32
  import { resolveResponsesLite, resolveStrictTools, } from "./codex-request-shape.js";
33
33
  import { subAgentDescription } from "../tools/subagent.js";
@@ -46,14 +46,14 @@ import { decidePlanStepCompaction, DEFAULT_CACHE_WRITE_READ_RATIO, PLAN_STEP_KEE
46
46
  import { extractPlanSteps, findCompletedMarkers } from "../utils/plan-steps.js";
47
47
  import { readFileSync } from "node:fs";
48
48
  import { clampThinkingForPlanMode } from "./thinking-level.js";
49
- import { pruneStaleToolResults } from "./compaction/tool-result-pruner.js";
49
+ import { pruneStaleToolResults, shouldPruneToolResults } from "./compaction/tool-result-pruner.js";
50
50
  import { discoverAgents } from "./agents.js";
51
51
  import { enhancePrompt } from "../utils/prompt-enhancer.js";
52
52
  import { detectLanguages, detectProjectStack } from "./language-detector.js";
53
53
  import { buildLoopBreakMessage } from "./loop-breaker.js";
54
54
  import { buildRegroundingMessage, requestTextForRegrounding } from "./regrounding.js";
55
55
  import { buildSemanticLoopJudgePrompt, buildSemanticLoopMessage, parseSemanticLoopVerdict, SEMANTIC_LOOP_JUDGE_TIMEOUT_MS, withJudgeTimeout, } from "./semantic-loop-check.js";
56
- import { buildEnvDeltaMessage } from "./env-delta.js";
56
+ import { buildEnvDeltaMessage, buildLanguageUpdateMessage } from "./env-delta.js";
57
57
  import { wrapSteeringText, buildNotificationSteeringText, STEERING_PREFIX } from "./steering.js";
58
58
  import { AgentNotificationQueue } from "./agent-notifications.js";
59
59
  import { VerificationGate, VERIFICATION_STATE_KIND } from "./verification-gate.js";
@@ -220,6 +220,9 @@ export class AgentSession {
220
220
  * for no benefit.
221
221
  */
222
222
  activeLanguages = new Set();
223
+ /** Languages the model has already been given guidance for, via the system
224
+ * prompt or an appended language update. */
225
+ announcedLanguages = new Set();
223
226
  /** Shared with the tool layer so plan-mode restrictions read live state. */
224
227
  planModeRef = { current: false };
225
228
  /** Path of the approved plan currently being implemented, or undefined. When
@@ -309,7 +312,10 @@ export class AgentSession {
309
312
  // Session manager. Agent-specific roots keep chat and coder histories isolated.
310
313
  this.sessionManager = new SessionManager(this.opts.sessionRootDir ?? paths.sessionsDir);
311
314
  const projectCustomization = this.opts.projectCustomization !== false;
312
- if (projectCustomization) {
315
+ // A sub-agent worker never creates them: the session that owns the
316
+ // project does. EZ Goals' workers work in the user's attached folders,
317
+ // which mustn't gain empty .ezcoder folders as a side effect.
318
+ if (projectCustomization && !this.opts.subagentWorker) {
313
319
  // Ensure project-local .ezcoder directories exist.
314
320
  const localEzDir = path.join(this.cwd, ".ezcoder");
315
321
  await fs.mkdir(path.join(localEzDir, "skills"), { recursive: true });
@@ -330,16 +336,20 @@ export class AgentSession {
330
336
  }
331
337
  // Discover agents and create tools (with sub-agent support). Chat sessions
332
338
  // can retain bundled workers without loading project/global customization.
333
- const agents = projectCustomization
334
- ? await discoverAgents({
335
- globalAgentsDir: paths.agentsDir,
336
- projectDir: this.cwd,
337
- })
338
- : this.opts.globalSubagents
339
- ? await discoverAgents({ globalAgentsDir: paths.agentsDir })
340
- : [];
339
+ const agents = this.opts.subAgents
340
+ ? [...this.opts.subAgents]
341
+ : projectCustomization
342
+ ? await discoverAgents({
343
+ globalAgentsDir: paths.agentsDir,
344
+ projectDir: this.cwd,
345
+ })
346
+ : this.opts.globalSubagents
347
+ ? await discoverAgents({ globalAgentsDir: paths.agentsDir })
348
+ : [];
341
349
  const { tools: builtInTools, processManager, rebuildReadTool, clearReadTracker, recordBashReads, lspManager, testImpact, debugManager, subAgentManager, } = await createTools(this.cwd, {
342
350
  agents,
351
+ shareWorkspaceRoots: this.opts.shareRootsWithSubAgents,
352
+ defaultSubAgent: this.opts.defaultSubAgent,
343
353
  skills: this.skills,
344
354
  contextLimits: this.contextLimits,
345
355
  provider: this.provider,
@@ -381,6 +391,7 @@ export class AgentSession {
381
391
  getBaseUrl: () => this.baseUrl,
382
392
  getCacheKey: () => this.getPromptCacheKey(),
383
393
  getMaxPerModel: () => this.settingsManager.get("subagentMaxPerModel"),
394
+ getChildModel: this.opts.getChildModel,
384
395
  // A persistent child is already the single allowed fan-out level. Blocking
385
396
  // `subagent` here created a timeout sandwich: the nested call could consume
386
397
  // the child's entire 10-minute turn budget, discarding all nested results.
@@ -408,6 +419,16 @@ export class AgentSession {
408
419
  // a hallucinated call can't mutate the repo — and buildSystemPrompt below is
409
420
  // fed the same filtered names so the Tools section matches exactly.
410
421
  this.tools = this.opts.allowedTools ? tools.filter((t) => this.isToolAllowed(t.name)) : tools;
422
+ const without = new Set(this.opts.withoutTools ?? []);
423
+ if (without.size > 0)
424
+ this.tools = this.tools.filter((t) => !without.has(t.name));
425
+ // A host may wrap built-ins (EZ Goals holds back its own editing once a
426
+ // goal is underway). Same names and schemas, so the cached tool block and
427
+ // the system prompt's Tools section are unchanged.
428
+ if (this.opts.wrapTool) {
429
+ const wrap = this.opts.wrapTool;
430
+ this.tools = this.tools.map((t) => wrap(t));
431
+ }
411
432
  // Both delegation tools carry the agent roster; when both survive the
412
433
  // allow-list, `subagent` points at `spawn_agent`'s copy instead.
413
434
  if (this.tools.some((t) => t.name === "spawn_agent")) {
@@ -422,7 +443,14 @@ export class AgentSession {
422
443
  // catalog must hold the wrapped tool so a promoted spawn still loads
423
444
  // wait_agent.
424
445
  this.promoteFollowUpTools();
425
- const { core, deferred } = partitionToolsByTier(this.tools);
446
+ const partition = partitionToolsByTier(this.tools);
447
+ // A session can keep chosen built-ins in its core set (EZ Goals
448
+ // orchestrates, so its agent tools are always at hand). Fixed for the
449
+ // session, so the cached tool block stays byte-stable.
450
+ const keep = new Set(this.opts.coreTools ?? []);
451
+ const deferred = partition.deferred.filter((t) => !keep.has(t.name));
452
+ // Everything not deferred, in the original relative order.
453
+ const core = keep.size > 0 ? this.tools.filter((t) => !deferred.includes(t)) : partition.core;
426
454
  if (deferred.length > 0) {
427
455
  // Append-only: `core` preserves the original relative order and
428
456
  // tool_search is pushed after it, so the serialized tool block that
@@ -454,6 +482,9 @@ export class AgentSession {
454
482
  else {
455
483
  await this.connectMcpServers();
456
484
  }
485
+ // Detect languages before the first render so the opening prompt already
486
+ // carries their packs and only a genuinely new language needs an update.
487
+ this.refreshActiveLanguages();
457
488
  const basePrompt = await this.buildBasePrompt(false, undefined);
458
489
  this.baseSystemPrompt = basePrompt;
459
490
  this.messages = [{ role: "system", content: this.withSystemPromptTail(basePrompt) }];
@@ -786,6 +817,7 @@ export class AgentSession {
786
817
  if (slash?.kind === "template") {
787
818
  // Prompt templates remain human-originated: the user invoked the command.
788
819
  const userMessage = { role: "user", content: slash.fullPrompt, provenance };
820
+ await this.announceNewLanguages();
789
821
  this.messages.push(userMessage);
790
822
  await this.persistMessage(userMessage);
791
823
  this.lastPersistedIndex = this.messages.length;
@@ -802,6 +834,7 @@ export class AgentSession {
802
834
  }
803
835
  // Push user message
804
836
  const userMessage = { role: "user", content, provenance };
837
+ await this.announceNewLanguages();
805
838
  this.messages.push(userMessage);
806
839
  await this.persistMessage(userMessage);
807
840
  this.lastPersistedIndex = this.messages.length;
@@ -826,6 +859,7 @@ export class AgentSession {
826
859
  content: parts,
827
860
  provenance: { source: "human", kind: "prompt", visibility: "transcript" },
828
861
  };
862
+ await this.announceNewLanguages();
829
863
  this.messages.push(userMessage);
830
864
  await this.persistMessage(userMessage);
831
865
  this.lastPersistedIndex = this.messages.length;
@@ -1713,36 +1747,6 @@ export class AgentSession {
1713
1747
  if (!force) {
1714
1748
  if (!this.settingsManager.get("autoCompact"))
1715
1749
  return messages;
1716
- // Cheap stale-tool-output pruning before the expensive LLM
1717
- // compaction check. In-place mutation preserves anchors; drop the
1718
- // retained usage afterwards since it counted the pruned content.
1719
- const pruneResult = pruneStaleToolResults(messages);
1720
- if (pruneResult.pruned) {
1721
- this.cacheDiagnostics.noteEdit("tool_prune", pruneResult.freedTokens);
1722
- this.providerContext = null;
1723
- log("INFO", "compaction", "Pruned stale tool outputs", {
1724
- prunedResults: String(pruneResult.prunedResults),
1725
- compactedToolCalls: String(pruneResult.compactedToolCalls),
1726
- freedTokens: String(pruneResult.freedTokens),
1727
- });
1728
- }
1729
- if (Date.now() < this.compactionRetryAfter)
1730
- return messages;
1731
- // The turn's own usage also counted the pruned content — after a
1732
- // prune, fall back to estimating the (now smaller) history so the
1733
- // freed tokens actually defer the LLM compaction.
1734
- let usage = pruneResult.pruned ? undefined : transformOpts.usage;
1735
- let pendingMessages = transformOpts.pendingMessages;
1736
- if (!usage && this.providerContext) {
1737
- const anchorIndex = messages.lastIndexOf(this.providerContext.anchor);
1738
- if (anchorIndex >= 0) {
1739
- usage = this.providerContext.usage;
1740
- pendingMessages = messages.slice(anchorIndex + 1);
1741
- }
1742
- else {
1743
- this.providerContext = null;
1744
- }
1745
- }
1746
1750
  const contextWindow = getContextWindow(this.model, {
1747
1751
  provider: this.provider,
1748
1752
  accountId,
@@ -1755,10 +1759,48 @@ export class AgentSession {
1755
1759
  accountId,
1756
1760
  approvedPlanPath: this.approvedPlanPath,
1757
1761
  });
1758
- const activeTokens = calculateActiveContextTokens(messages, {
1759
- usage,
1760
- pendingMessages,
1761
- });
1762
+ // Provider usage when there is some, else an estimate of the history.
1763
+ const measureActiveTokens = () => {
1764
+ let usage = transformOpts.usage;
1765
+ let pendingMessages = transformOpts.pendingMessages;
1766
+ if (!usage && this.providerContext) {
1767
+ const anchorIndex = messages.lastIndexOf(this.providerContext.anchor);
1768
+ if (anchorIndex >= 0) {
1769
+ usage = this.providerContext.usage;
1770
+ pendingMessages = messages.slice(anchorIndex + 1);
1771
+ }
1772
+ else {
1773
+ this.providerContext = null;
1774
+ }
1775
+ }
1776
+ return calculateActiveContextTokens(messages, { usage, pendingMessages });
1777
+ };
1778
+ let activeTokens = measureActiveTokens();
1779
+ // Cheap stale-tool-output pruning before the expensive LLM
1780
+ // compaction check, once the context is big enough for the freed
1781
+ // tokens to be worth a cache rewrite. In-place mutation preserves
1782
+ // anchors; drop the retained usage afterwards since it counted the
1783
+ // pruned content.
1784
+ if (shouldPruneToolResults(activeTokens, policy.targetTokens)) {
1785
+ const pruneResult = pruneStaleToolResults(messages);
1786
+ if (pruneResult.pruned) {
1787
+ this.cacheDiagnostics.noteEdit("tool_prune", pruneResult.freedTokens);
1788
+ this.providerContext = null;
1789
+ log("INFO", "compaction", "Pruned stale tool outputs", {
1790
+ prunedResults: String(pruneResult.prunedResults),
1791
+ compactedToolCalls: String(pruneResult.compactedToolCalls),
1792
+ freedTokens: String(pruneResult.freedTokens),
1793
+ });
1794
+ // The turn's own usage also counted the pruned content — estimate
1795
+ // the (now smaller) history so the freed tokens actually defer
1796
+ // the LLM compaction.
1797
+ activeTokens = calculateActiveContextTokens(messages, {
1798
+ pendingMessages: transformOpts.pendingMessages,
1799
+ });
1800
+ }
1801
+ }
1802
+ if (Date.now() < this.compactionRetryAfter)
1803
+ return messages;
1762
1804
  // An approved plan runs as ONE run, so step boundaries are only
1763
1805
  // visible here, between model steps — not after the run ends.
1764
1806
  const planStep = this.observePlanStepProgress(messages, contextWindow, activeTokens);
@@ -2510,6 +2552,9 @@ export class AgentSession {
2510
2552
  // new session's /history, and get re-persisted into the new file by the
2511
2553
  // next compaction — the cross-session duplicate-marker propagation bug.
2512
2554
  this.markers.clear();
2555
+ // Detect languages before the first render so the opening prompt already
2556
+ // carries their packs and only a genuinely new language needs an update.
2557
+ this.refreshActiveLanguages();
2513
2558
  const basePrompt = await this.buildBasePrompt(false, undefined);
2514
2559
  this.baseSystemPrompt = basePrompt;
2515
2560
  this.messages = [{ role: "system", content: this.withSystemPromptTail(basePrompt) }];
@@ -2713,6 +2758,19 @@ export class AgentSession {
2713
2758
  .map((m) => m.text)
2714
2759
  .join("\n\n");
2715
2760
  }
2761
+ /**
2762
+ * Tell the agent a new fact at its next turn, as a short hidden message
2763
+ * appended after the cached prompt (the same channel as a finished worker),
2764
+ * so a mid-session change reaches it without re-rendering the prompt. A
2765
+ * later note with the same id replaces one not yet delivered.
2766
+ */
2767
+ notify(id, text) {
2768
+ this.notifications.enqueue("goal", id, text, { terminal: true });
2769
+ }
2770
+ /** This session's sub-agents (workers), newest-state, including ones restored after a restart. */
2771
+ listSubAgents() {
2772
+ return this.subAgentManager?.list() ?? [];
2773
+ }
2716
2774
  /** Snapshot of background processes (bash run_in_background), newest-state. */
2717
2775
  listBackgroundProcesses() {
2718
2776
  return this.processManager?.list() ?? [];
@@ -2811,7 +2869,7 @@ export class AgentSession {
2811
2869
  return relative.startsWith("..") || path.isAbsolute(relative);
2812
2870
  });
2813
2871
  this.additionalRoots.push(resolved);
2814
- await this.rebuildSystemPromptInPlace();
2872
+ await this.refreshPromptForRoots();
2815
2873
  return { ok: true, root: resolved };
2816
2874
  }
2817
2875
  /** Remove an exact root previously added with `/add-dir`. */
@@ -2822,9 +2880,20 @@ export class AgentSession {
2822
2880
  return { ok: false, error: `Not an additional workspace root: ${resolved}` };
2823
2881
  }
2824
2882
  this.additionalRoots.splice(index, 1);
2825
- await this.rebuildSystemPromptInPlace();
2883
+ await this.refreshPromptForRoots();
2826
2884
  return { ok: true, root: resolved };
2827
2885
  }
2886
+ /**
2887
+ * Name a root change in the prompt only while there is no conversation yet.
2888
+ * Mid-conversation the env-delta note tells the model instead: the roots sit
2889
+ * in the Environment section, and rewriting it would invalidate every cached
2890
+ * message after it (a goal attaching a folder re-sent ~75k tokens).
2891
+ */
2892
+ async refreshPromptForRoots() {
2893
+ if (this.messages.some((message) => message.role !== "system"))
2894
+ return;
2895
+ await this.rebuildSystemPromptInPlace();
2896
+ }
2828
2897
  /**
2829
2898
  * Names to advertise as available-on-demand. A tool the model already
2830
2899
  * promoted lives in `this.tools` and carries its own schema, so it drops out
@@ -2865,6 +2934,8 @@ export class AgentSession {
2865
2934
  return this.customSystemPrompt;
2866
2935
  const toolNames = this.tools.map((tool) => tool.name);
2867
2936
  const deferredToolNames = this.deferredToolNamesForPrompt(toolNames);
2937
+ // Whatever is rendered now is what the model has been told.
2938
+ this.announcedLanguages = new Set(this.activeLanguages);
2868
2939
  if (this.agentPrompt !== undefined) {
2869
2940
  return buildSubAgentSystemPrompt(this.agentPrompt, {
2870
2941
  cwd: this.cwd,
@@ -2874,29 +2945,73 @@ export class AgentSession {
2874
2945
  role: this.opts.agentRole,
2875
2946
  environment: this.recordRenderedEnvironment(),
2876
2947
  contextLimits: this.contextLimits,
2948
+ ...(this.languagesApply() ? { activeLanguages: this.activeLanguages } : {}),
2877
2949
  });
2878
2950
  }
2879
2951
  return buildSystemPrompt(this.cwd, this.skills, planMode, approvedPlanPath, toolNames, this.activeLanguages, this.provider, this.recordRenderedEnvironment(), deferredToolNames, this.contextLimits);
2880
2952
  }
2881
- /**
2882
- * Add newly detected project languages. Returns true when the set grew and
2883
- * the standard prompt therefore needs a rebuild. Custom and sub-agent
2884
- * prompts never render packs, so detection is skipped for them.
2885
- */
2886
2953
  /**
2887
2954
  * Bring the system prompt to the exact state the next provider request will
2888
2955
  * send. Shared by real runs and {@link prewarm} so both see one prefix.
2956
+ *
2957
+ * Languages are re-detected at each task boundary. Before the first reply
2958
+ * nothing is cached worth keeping, so a new language simply rebuilds the
2959
+ * prompt. After that the prompt is a cached prefix and is left alone: the
2960
+ * new language is announced by {@link announceNewLanguages} instead.
2889
2961
  */
2890
2962
  async prepareSystemPromptForRequest() {
2891
- // Languages are re-detected at each task boundary so a project scaffolded
2892
- // during the previous turn gets its packs; the prompt is rebuilt only when
2893
- // the set grows, keeping the cached prefix stable otherwise.
2894
- if (this.refreshActiveLanguages())
2963
+ if (this.refreshActiveLanguages() && !this.hasReply()) {
2895
2964
  await this.rebuildSystemPromptInPlace();
2965
+ }
2896
2966
  this.refreshSystemPromptTail();
2897
2967
  }
2968
+ hasReply() {
2969
+ return this.messages.some((m) => m.role === "assistant");
2970
+ }
2971
+ /**
2972
+ * Give the model style packs for languages that appeared after its prompt
2973
+ * was cached (e.g. the previous task scaffolded a TypeScript project), as a
2974
+ * hidden message appended to the history rather than a prompt re-render that
2975
+ * would throw away the whole cache. Runs just before a new user message is
2976
+ * added, so the user's request stays the last thing the model reads and the
2977
+ * update is saved in order with the rest of the conversation.
2978
+ */
2979
+ async announceNewLanguages() {
2980
+ if (!this.hasReply())
2981
+ return; // the next request rebuilds the prompt instead
2982
+ this.refreshActiveLanguages();
2983
+ const fresh = new Set([...this.activeLanguages].filter((id) => !this.announcedLanguages.has(id)));
2984
+ if (fresh.size === 0)
2985
+ return;
2986
+ for (const id of fresh)
2987
+ this.announcedLanguages.add(id);
2988
+ const update = buildLanguageUpdateMessage(fresh, this.cwd);
2989
+ if (!update)
2990
+ return;
2991
+ this.messages.push(update);
2992
+ await this.persistMessage(update);
2993
+ this.lastPersistedIndex = this.messages.length;
2994
+ log("INFO", "language", "Appended style packs for new languages", {
2995
+ added: [...fresh].join(","),
2996
+ });
2997
+ }
2998
+ /**
2999
+ * Whether this session renders language packs: the standard prompt, and a
3000
+ * code-writing child (a sub-agent prompt with project context). A verbatim
3001
+ * custom prompt never does, nor do user-facing specialist agents (Motion,
3002
+ * goals) or recon children without project context.
3003
+ */
3004
+ languagesApply() {
3005
+ if (this.customSystemPrompt)
3006
+ return false;
3007
+ if (this.agentPrompt === undefined)
3008
+ return true;
3009
+ return ((this.opts.agentRole ?? "subagent") === "subagent" &&
3010
+ (this.opts.agentContext ?? "project") === "project");
3011
+ }
3012
+ /** Add newly detected project languages. Returns true when the set grew. */
2898
3013
  refreshActiveLanguages() {
2899
- if (this.customSystemPrompt || this.agentPrompt !== undefined)
3014
+ if (!this.languagesApply())
2900
3015
  return false;
2901
3016
  let grew = false;
2902
3017
  try {