@prestyj/cli 5.30.2 → 5.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.
Files changed (142) 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 +6 -0
  8. package/dist/app-sidecar/goal-routes.js +360 -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 +37 -27
  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/prompt-routes.js +1 -2
  16. package/dist/app-sidecar/prompt-routes.js.map +1 -1
  17. package/dist/app-sidecar/route-context.d.ts +12 -0
  18. package/dist/app-sidecar/run-control-routes.js +2 -1
  19. package/dist/app-sidecar/run-control-routes.js.map +1 -1
  20. package/dist/app-sidecar/session-routes.js +41 -0
  21. package/dist/app-sidecar/session-routes.js.map +1 -1
  22. package/dist/app-sidecar/session-types.d.ts +2 -0
  23. package/dist/app-sidecar/workspace-routes.js +13 -95
  24. package/dist/app-sidecar/workspace-routes.js.map +1 -1
  25. package/dist/app-sidecar.js +219 -67
  26. package/dist/app-sidecar.js.map +1 -1
  27. package/dist/cli/shared.js +1 -1
  28. package/dist/cli/shared.js.map +1 -1
  29. package/dist/core/acceptance-checks.js +17 -4
  30. package/dist/core/acceptance-checks.js.map +1 -1
  31. package/dist/core/agent-notifications.d.ts +1 -1
  32. package/dist/core/agent-session.d.ts +90 -5
  33. package/dist/core/agent-session.js +185 -63
  34. package/dist/core/agent-session.js.map +1 -1
  35. package/dist/core/ask-user.d.ts +11 -0
  36. package/dist/core/ask-user.js +6 -1
  37. package/dist/core/ask-user.js.map +1 -1
  38. package/dist/core/auth-providers.d.ts +2 -80
  39. package/dist/core/auth-providers.js +4 -183
  40. package/dist/core/auth-providers.js.map +1 -1
  41. package/dist/core/cache-diagnostics.d.ts +1 -1
  42. package/dist/core/cache-diagnostics.js +2 -1
  43. package/dist/core/cache-diagnostics.js.map +1 -1
  44. package/dist/core/compaction/tool-result-pruner.d.ts +10 -0
  45. package/dist/core/compaction/tool-result-pruner.js +12 -0
  46. package/dist/core/compaction/tool-result-pruner.js.map +1 -1
  47. package/dist/core/context-files.d.ts +8 -0
  48. package/dist/core/context-files.js +46 -0
  49. package/dist/core/context-files.js.map +1 -0
  50. package/dist/core/custom-commands.d.ts +6 -0
  51. package/dist/core/custom-commands.js +17 -0
  52. package/dist/core/custom-commands.js.map +1 -1
  53. package/dist/core/env-delta.d.ts +9 -1
  54. package/dist/core/env-delta.js +20 -0
  55. package/dist/core/env-delta.js.map +1 -1
  56. package/dist/core/goal-summary.d.ts +32 -0
  57. package/dist/core/goal-summary.js +104 -0
  58. package/dist/core/goal-summary.js.map +1 -0
  59. package/dist/core/goals-store.d.ts +208 -0
  60. package/dist/core/goals-store.js +521 -0
  61. package/dist/core/goals-store.js.map +1 -0
  62. package/dist/core/local-endpoint-store.js +15 -11
  63. package/dist/core/local-endpoint-store.js.map +1 -1
  64. package/dist/core/plan-handoff.d.ts +8 -0
  65. package/dist/core/plan-handoff.js +11 -0
  66. package/dist/core/plan-handoff.js.map +1 -1
  67. package/dist/core/project-discovery.d.ts +7 -0
  68. package/dist/core/project-discovery.js +9 -3
  69. package/dist/core/project-discovery.js.map +1 -1
  70. package/dist/core/radio.d.ts +2 -3
  71. package/dist/core/radio.js.map +1 -1
  72. package/dist/core/session-manager.d.ts +7 -1
  73. package/dist/core/session-manager.js +17 -7
  74. package/dist/core/session-manager.js.map +1 -1
  75. package/dist/core/session-storage.d.ts +3 -0
  76. package/dist/core/session-storage.js +8 -2
  77. package/dist/core/session-storage.js.map +1 -1
  78. package/dist/core/steroids.d.ts +8 -0
  79. package/dist/core/steroids.js +19 -3
  80. package/dist/core/steroids.js.map +1 -1
  81. package/dist/core/subagent-manager.d.ts +51 -0
  82. package/dist/core/subagent-manager.js +103 -16
  83. package/dist/core/subagent-manager.js.map +1 -1
  84. package/dist/core/subagent-store.js +2 -1
  85. package/dist/core/subagent-store.js.map +1 -1
  86. package/dist/goals-agent/goal-team.d.ts +2 -0
  87. package/dist/goals-agent/goal-team.js +83 -0
  88. package/dist/goals-agent/goal-team.js.map +1 -0
  89. package/dist/goals-agent/goals-agent.d.ts +90 -0
  90. package/dist/goals-agent/goals-agent.js +246 -0
  91. package/dist/goals-agent/goals-agent.js.map +1 -0
  92. package/dist/index.js +1 -1
  93. package/dist/index.js.map +1 -1
  94. package/dist/interactive.js +4 -3
  95. package/dist/interactive.js.map +1 -1
  96. package/dist/modes/acp-autopilot.d.ts +27 -0
  97. package/dist/modes/acp-autopilot.js +54 -0
  98. package/dist/modes/acp-autopilot.js.map +1 -0
  99. package/dist/modes/acp-goals.d.ts +48 -0
  100. package/dist/modes/acp-goals.js +103 -0
  101. package/dist/modes/acp-goals.js.map +1 -0
  102. package/dist/modes/acp-mode.d.ts +128 -3
  103. package/dist/modes/acp-mode.js +1082 -52
  104. package/dist/modes/acp-mode.js.map +1 -1
  105. package/dist/modes/subagent-worker-mode.d.ts +14 -0
  106. package/dist/modes/subagent-worker-mode.js +32 -1
  107. package/dist/modes/subagent-worker-mode.js.map +1 -1
  108. package/dist/system-prompt.d.ts +8 -0
  109. package/dist/system-prompt.js +21 -9
  110. package/dist/system-prompt.js.map +1 -1
  111. package/dist/tools/bash-read-evidence.d.ts +13 -0
  112. package/dist/tools/bash-read-evidence.js +48 -1
  113. package/dist/tools/bash-read-evidence.js.map +1 -1
  114. package/dist/tools/grep-scan.d.ts +96 -0
  115. package/dist/tools/grep-scan.js +283 -0
  116. package/dist/tools/grep-scan.js.map +1 -0
  117. package/dist/tools/grep.d.ts +10 -0
  118. package/dist/tools/grep.js +418 -10
  119. package/dist/tools/grep.js.map +1 -1
  120. package/dist/tools/index.d.ts +7 -1
  121. package/dist/tools/index.js +10 -2
  122. package/dist/tools/index.js.map +1 -1
  123. package/dist/tools/read-tracker.d.ts +6 -12
  124. package/dist/tools/read-tracker.js +21 -36
  125. package/dist/tools/read-tracker.js.map +1 -1
  126. package/dist/tools/read.js +2 -2
  127. package/dist/tools/read.js.map +1 -1
  128. package/dist/tools/subagent-control.js +22 -3
  129. package/dist/tools/subagent-control.js.map +1 -1
  130. package/dist/tools/write.js +110 -8
  131. package/dist/tools/write.js.map +1 -1
  132. package/dist/ui/components/ActivityIndicator.d.ts +5 -5
  133. package/dist/ui/components/InputArea.d.ts +2 -2
  134. package/dist/ui/components/InputArea.js +3 -3
  135. package/dist/ui/components/InputArea.js.map +1 -1
  136. package/dist/ui/components/ToolExecution.d.ts +2 -2
  137. package/dist/ui/hooks/useContextCompaction.js +38 -31
  138. package/dist/ui/hooks/useContextCompaction.js.map +1 -1
  139. package/dist/utils/format.d.ts +0 -4
  140. package/dist/utils/format.js +0 -7
  141. package/dist/utils/format.js.map +1 -1
  142. package/package.json +4 -4
@@ -5,7 +5,7 @@ import { flagUntrustedToolResult } from "./injection-detect.js";
5
5
  import { COMPLETION_REVIEW_STATE_KIND, } from "./completion-review.js";
6
6
  import { SlashCommandRegistry, createBuiltinCommands, } from "./slash-commands.js";
7
7
  import { PROMPT_COMMANDS, getPromptCommand } from "./prompt-commands.js";
8
- import { loadCustomCommands } from "./custom-commands.js";
8
+ import { loadWorkspaceCustomCommands } from "./custom-commands.js";
9
9
  import { expandPromptCommand } from "./prompt-command-expansion.js";
10
10
  import { SettingsManager } from "./settings-manager.js";
11
11
  import { AuthStorage } from "./auth-storage.js";
@@ -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) }];
@@ -530,7 +561,6 @@ export class AgentSession {
530
561
  const helpCmd = this.slashCommands.get("help");
531
562
  if (helpCmd) {
532
563
  const registry = this.slashCommands;
533
- const cwd = this.cwd;
534
564
  helpCmd.execute = async () => {
535
565
  const all = registry.getAll();
536
566
  const lines = all.map((c) => ` /${c.name}${c.aliases.length ? ` (${c.aliases.map((a) => "/" + a).join(", ")})` : ""} — ${c.description}`);
@@ -540,7 +570,7 @@ export class AgentSession {
540
570
  lines.push(` /${cmd.name}${cmd.aliases.length ? ` (${cmd.aliases.map((a) => "/" + a).join(", ")})` : ""} — ${cmd.description}`);
541
571
  }
542
572
  }
543
- const customCmds = await loadCustomCommands(cwd);
573
+ const customCmds = await this.loadCustomCommands();
544
574
  if (customCmds.length > 0) {
545
575
  lines.push("", "Custom commands:");
546
576
  for (const cmd of customCmds)
@@ -748,7 +778,7 @@ export class AgentSession {
748
778
  return null;
749
779
  // EZ Coder alone can resolve its prompt-template and project commands.
750
780
  const builtinPromptCmd = coderCommands ? getPromptCommand(parsed.name) : undefined;
751
- const customCmds = coderCommands ? await loadCustomCommands(this.cwd) : [];
781
+ const customCmds = coderCommands ? await this.loadCustomCommands() : [];
752
782
  const customPromptCmd = !builtinPromptCmd
753
783
  ? customCmds.find((c) => c.name === parsed.name)
754
784
  : undefined;
@@ -786,6 +816,7 @@ export class AgentSession {
786
816
  if (slash?.kind === "template") {
787
817
  // Prompt templates remain human-originated: the user invoked the command.
788
818
  const userMessage = { role: "user", content: slash.fullPrompt, provenance };
819
+ await this.announceNewLanguages();
789
820
  this.messages.push(userMessage);
790
821
  await this.persistMessage(userMessage);
791
822
  this.lastPersistedIndex = this.messages.length;
@@ -802,6 +833,7 @@ export class AgentSession {
802
833
  }
803
834
  // Push user message
804
835
  const userMessage = { role: "user", content, provenance };
836
+ await this.announceNewLanguages();
805
837
  this.messages.push(userMessage);
806
838
  await this.persistMessage(userMessage);
807
839
  this.lastPersistedIndex = this.messages.length;
@@ -826,6 +858,7 @@ export class AgentSession {
826
858
  content: parts,
827
859
  provenance: { source: "human", kind: "prompt", visibility: "transcript" },
828
860
  };
861
+ await this.announceNewLanguages();
829
862
  this.messages.push(userMessage);
830
863
  await this.persistMessage(userMessage);
831
864
  this.lastPersistedIndex = this.messages.length;
@@ -1713,36 +1746,6 @@ export class AgentSession {
1713
1746
  if (!force) {
1714
1747
  if (!this.settingsManager.get("autoCompact"))
1715
1748
  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
1749
  const contextWindow = getContextWindow(this.model, {
1747
1750
  provider: this.provider,
1748
1751
  accountId,
@@ -1755,10 +1758,48 @@ export class AgentSession {
1755
1758
  accountId,
1756
1759
  approvedPlanPath: this.approvedPlanPath,
1757
1760
  });
1758
- const activeTokens = calculateActiveContextTokens(messages, {
1759
- usage,
1760
- pendingMessages,
1761
- });
1761
+ // Provider usage when there is some, else an estimate of the history.
1762
+ const measureActiveTokens = () => {
1763
+ let usage = transformOpts.usage;
1764
+ let pendingMessages = transformOpts.pendingMessages;
1765
+ if (!usage && this.providerContext) {
1766
+ const anchorIndex = messages.lastIndexOf(this.providerContext.anchor);
1767
+ if (anchorIndex >= 0) {
1768
+ usage = this.providerContext.usage;
1769
+ pendingMessages = messages.slice(anchorIndex + 1);
1770
+ }
1771
+ else {
1772
+ this.providerContext = null;
1773
+ }
1774
+ }
1775
+ return calculateActiveContextTokens(messages, { usage, pendingMessages });
1776
+ };
1777
+ let activeTokens = measureActiveTokens();
1778
+ // Cheap stale-tool-output pruning before the expensive LLM
1779
+ // compaction check, once the context is big enough for the freed
1780
+ // tokens to be worth a cache rewrite. In-place mutation preserves
1781
+ // anchors; drop the retained usage afterwards since it counted the
1782
+ // pruned content.
1783
+ if (shouldPruneToolResults(activeTokens, policy.targetTokens)) {
1784
+ const pruneResult = pruneStaleToolResults(messages);
1785
+ if (pruneResult.pruned) {
1786
+ this.cacheDiagnostics.noteEdit("tool_prune", pruneResult.freedTokens);
1787
+ this.providerContext = null;
1788
+ log("INFO", "compaction", "Pruned stale tool outputs", {
1789
+ prunedResults: String(pruneResult.prunedResults),
1790
+ compactedToolCalls: String(pruneResult.compactedToolCalls),
1791
+ freedTokens: String(pruneResult.freedTokens),
1792
+ });
1793
+ // The turn's own usage also counted the pruned content — estimate
1794
+ // the (now smaller) history so the freed tokens actually defer
1795
+ // the LLM compaction.
1796
+ activeTokens = calculateActiveContextTokens(messages, {
1797
+ pendingMessages: transformOpts.pendingMessages,
1798
+ });
1799
+ }
1800
+ }
1801
+ if (Date.now() < this.compactionRetryAfter)
1802
+ return messages;
1762
1803
  // An approved plan runs as ONE run, so step boundaries are only
1763
1804
  // visible here, between model steps — not after the run ends.
1764
1805
  const planStep = this.observePlanStepProgress(messages, contextWindow, activeTokens);
@@ -2510,6 +2551,9 @@ export class AgentSession {
2510
2551
  // new session's /history, and get re-persisted into the new file by the
2511
2552
  // next compaction — the cross-session duplicate-marker propagation bug.
2512
2553
  this.markers.clear();
2554
+ // Detect languages before the first render so the opening prompt already
2555
+ // carries their packs and only a genuinely new language needs an update.
2556
+ this.refreshActiveLanguages();
2513
2557
  const basePrompt = await this.buildBasePrompt(false, undefined);
2514
2558
  this.baseSystemPrompt = basePrompt;
2515
2559
  this.messages = [{ role: "system", content: this.withSystemPromptTail(basePrompt) }];
@@ -2713,6 +2757,19 @@ export class AgentSession {
2713
2757
  .map((m) => m.text)
2714
2758
  .join("\n\n");
2715
2759
  }
2760
+ /**
2761
+ * Tell the agent a new fact at its next turn, as a short hidden message
2762
+ * appended after the cached prompt (the same channel as a finished worker),
2763
+ * so a mid-session change reaches it without re-rendering the prompt. A
2764
+ * later note with the same id replaces one not yet delivered.
2765
+ */
2766
+ notify(id, text) {
2767
+ this.notifications.enqueue("goal", id, text, { terminal: true });
2768
+ }
2769
+ /** This session's sub-agents (workers), newest-state, including ones restored after a restart. */
2770
+ listSubAgents() {
2771
+ return this.subAgentManager?.list() ?? [];
2772
+ }
2716
2773
  /** Snapshot of background processes (bash run_in_background), newest-state. */
2717
2774
  listBackgroundProcesses() {
2718
2775
  return this.processManager?.list() ?? [];
@@ -2780,6 +2837,14 @@ export class AgentSession {
2780
2837
  getAdditionalRoots() {
2781
2838
  return [...this.additionalRoots];
2782
2839
  }
2840
+ /**
2841
+ * The custom `.ezcoder/commands` slash commands this session can run: the
2842
+ * project's, then those of every added root (a goal's attached repos too).
2843
+ * Read fresh, so a new command file is picked up without a restart.
2844
+ */
2845
+ loadCustomCommands() {
2846
+ return loadWorkspaceCustomCommands([this.cwd, ...this.additionalRoots]);
2847
+ }
2783
2848
  /**
2784
2849
  * Add another workspace root. Tools already accept absolute paths, so this
2785
2850
  * only widens the write guard and tells the model the root exists. Rebuilding
@@ -2811,7 +2876,7 @@ export class AgentSession {
2811
2876
  return relative.startsWith("..") || path.isAbsolute(relative);
2812
2877
  });
2813
2878
  this.additionalRoots.push(resolved);
2814
- await this.rebuildSystemPromptInPlace();
2879
+ await this.refreshPromptForRoots();
2815
2880
  return { ok: true, root: resolved };
2816
2881
  }
2817
2882
  /** Remove an exact root previously added with `/add-dir`. */
@@ -2822,9 +2887,20 @@ export class AgentSession {
2822
2887
  return { ok: false, error: `Not an additional workspace root: ${resolved}` };
2823
2888
  }
2824
2889
  this.additionalRoots.splice(index, 1);
2825
- await this.rebuildSystemPromptInPlace();
2890
+ await this.refreshPromptForRoots();
2826
2891
  return { ok: true, root: resolved };
2827
2892
  }
2893
+ /**
2894
+ * Name a root change in the prompt only while there is no conversation yet.
2895
+ * Mid-conversation the env-delta note tells the model instead: the roots sit
2896
+ * in the Environment section, and rewriting it would invalidate every cached
2897
+ * message after it (a goal attaching a folder re-sent ~75k tokens).
2898
+ */
2899
+ async refreshPromptForRoots() {
2900
+ if (this.messages.some((message) => message.role !== "system"))
2901
+ return;
2902
+ await this.rebuildSystemPromptInPlace();
2903
+ }
2828
2904
  /**
2829
2905
  * Names to advertise as available-on-demand. A tool the model already
2830
2906
  * promoted lives in `this.tools` and carries its own schema, so it drops out
@@ -2865,6 +2941,8 @@ export class AgentSession {
2865
2941
  return this.customSystemPrompt;
2866
2942
  const toolNames = this.tools.map((tool) => tool.name);
2867
2943
  const deferredToolNames = this.deferredToolNamesForPrompt(toolNames);
2944
+ // Whatever is rendered now is what the model has been told.
2945
+ this.announcedLanguages = new Set(this.activeLanguages);
2868
2946
  if (this.agentPrompt !== undefined) {
2869
2947
  return buildSubAgentSystemPrompt(this.agentPrompt, {
2870
2948
  cwd: this.cwd,
@@ -2874,29 +2952,73 @@ export class AgentSession {
2874
2952
  role: this.opts.agentRole,
2875
2953
  environment: this.recordRenderedEnvironment(),
2876
2954
  contextLimits: this.contextLimits,
2955
+ ...(this.languagesApply() ? { activeLanguages: this.activeLanguages } : {}),
2877
2956
  });
2878
2957
  }
2879
2958
  return buildSystemPrompt(this.cwd, this.skills, planMode, approvedPlanPath, toolNames, this.activeLanguages, this.provider, this.recordRenderedEnvironment(), deferredToolNames, this.contextLimits);
2880
2959
  }
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
2960
  /**
2887
2961
  * Bring the system prompt to the exact state the next provider request will
2888
2962
  * send. Shared by real runs and {@link prewarm} so both see one prefix.
2963
+ *
2964
+ * Languages are re-detected at each task boundary. Before the first reply
2965
+ * nothing is cached worth keeping, so a new language simply rebuilds the
2966
+ * prompt. After that the prompt is a cached prefix and is left alone: the
2967
+ * new language is announced by {@link announceNewLanguages} instead.
2889
2968
  */
2890
2969
  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())
2970
+ if (this.refreshActiveLanguages() && !this.hasReply()) {
2895
2971
  await this.rebuildSystemPromptInPlace();
2972
+ }
2896
2973
  this.refreshSystemPromptTail();
2897
2974
  }
2975
+ hasReply() {
2976
+ return this.messages.some((m) => m.role === "assistant");
2977
+ }
2978
+ /**
2979
+ * Give the model style packs for languages that appeared after its prompt
2980
+ * was cached (e.g. the previous task scaffolded a TypeScript project), as a
2981
+ * hidden message appended to the history rather than a prompt re-render that
2982
+ * would throw away the whole cache. Runs just before a new user message is
2983
+ * added, so the user's request stays the last thing the model reads and the
2984
+ * update is saved in order with the rest of the conversation.
2985
+ */
2986
+ async announceNewLanguages() {
2987
+ if (!this.hasReply())
2988
+ return; // the next request rebuilds the prompt instead
2989
+ this.refreshActiveLanguages();
2990
+ const fresh = new Set([...this.activeLanguages].filter((id) => !this.announcedLanguages.has(id)));
2991
+ if (fresh.size === 0)
2992
+ return;
2993
+ for (const id of fresh)
2994
+ this.announcedLanguages.add(id);
2995
+ const update = buildLanguageUpdateMessage(fresh, this.cwd);
2996
+ if (!update)
2997
+ return;
2998
+ this.messages.push(update);
2999
+ await this.persistMessage(update);
3000
+ this.lastPersistedIndex = this.messages.length;
3001
+ log("INFO", "language", "Appended style packs for new languages", {
3002
+ added: [...fresh].join(","),
3003
+ });
3004
+ }
3005
+ /**
3006
+ * Whether this session renders language packs: the standard prompt, and a
3007
+ * code-writing child (a sub-agent prompt with project context). A verbatim
3008
+ * custom prompt never does, nor do user-facing specialist agents (Motion,
3009
+ * goals) or recon children without project context.
3010
+ */
3011
+ languagesApply() {
3012
+ if (this.customSystemPrompt)
3013
+ return false;
3014
+ if (this.agentPrompt === undefined)
3015
+ return true;
3016
+ return ((this.opts.agentRole ?? "subagent") === "subagent" &&
3017
+ (this.opts.agentContext ?? "project") === "project");
3018
+ }
3019
+ /** Add newly detected project languages. Returns true when the set grew. */
2898
3020
  refreshActiveLanguages() {
2899
- if (this.customSystemPrompt || this.agentPrompt !== undefined)
3021
+ if (!this.languagesApply())
2900
3022
  return false;
2901
3023
  let grew = false;
2902
3024
  try {