chainlesschain 0.162.30 → 0.162.31

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 (172) hide show
  1. package/package.json +2 -2
  2. package/src/assets/web-panel/assets/{AIOps-CsNttUU7.js → AIOps-BqWP6FKu.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-lgohjckQ.js → ActionButton-CXwMgOvX.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-ccV3LAca.js → Analytics-DAebZ4IY.js} +3 -3
  5. package/src/assets/web-panel/assets/AppLayout-CYsqYoME.js +9 -0
  6. package/src/assets/web-panel/assets/{Audit-B1gFM5U9.js → Audit-BbTtX1Nf.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-BeWE3ERo.js → Backup-DgqY2Eb-.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-CDkPsNG2.js → BaseInput-Cq2ZuSoO.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-ztb9ia6e.js → Chat-D2kqpUyO.js} +5 -5
  10. package/src/assets/web-panel/assets/ChatBubbleRenderer-C-svYkrC.js +1 -0
  11. package/src/assets/web-panel/assets/{Checkbox-BcfRBlIY.js → Checkbox-_9swHpyc.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-DOs99xkr.js → Codegen-Cr9YbCPl.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-D1X6tYlj.js → Col--wdpCMxx.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-DTksIWtz.js → Community-DuFcVnLu.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-DIJtAYBO.js → Compact-1yzYeT04.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-BBf7LF_k.js → Compliance-Dq3aU9Df.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-UBPXQ40s.js → Cowork-CrWcnIg8.js} +2 -2
  18. package/src/assets/web-panel/assets/{Cron-CkRm1jPB.js → Cron-Bh6fKZ0h.js} +2 -2
  19. package/src/assets/web-panel/assets/{Crosschain-qALlTl7e.js → Crosschain-8ofPaWVW.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-CqyqVS6E.js → DID-D3EiYm3w.js} +2 -2
  21. package/src/assets/web-panel/assets/Dashboard-BFjEdFne.js +3 -0
  22. package/src/assets/web-panel/assets/{Dropdown-Cb5UzbSZ.js → Dropdown-pYVPcP6O.js} +1 -1
  23. package/src/assets/web-panel/assets/{EmailListRenderer-CarBq8Fk.js → EmailListRenderer-zBPodwJ1.js} +1 -1
  24. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CSiGXaZz.js → FamilyGuardDashboard-CyQTW6PW.js} +1 -1
  25. package/src/assets/web-panel/assets/{Federation-DUxhVoBN.js → Federation-Ctaq3zYq.js} +1 -1
  26. package/src/assets/web-panel/assets/{FormItemContext-BoMQpkhx.js → FormItemContext-CWYJCLq1.js} +1 -1
  27. package/src/assets/web-panel/assets/{GenericCardRenderer-DTVqC_CX.js → GenericCardRenderer-B1g6t9R9.js} +1 -1
  28. package/src/assets/web-panel/assets/{Git-C_XuPtK5.js → Git-DH-v8iwd.js} +2 -2
  29. package/src/assets/web-panel/assets/{Governance-BZyqlqz-.js → Governance-jZxXvOs5.js} +1 -1
  30. package/src/assets/web-panel/assets/{Inference-DdZVUimI.js → Inference-D07LRghn.js} +1 -1
  31. package/src/assets/web-panel/assets/{KnowledgeGraph-IzZ-jnCn.js → KnowledgeGraph-DnGtRZhx.js} +1 -1
  32. package/src/assets/web-panel/assets/{Logs-koTK6eNc.js → Logs-D2pM9C4W.js} +2 -2
  33. package/src/assets/web-panel/assets/{Marketplace-6zpJ1L8n.js → Marketplace-UyIO7C7r.js} +1 -1
  34. package/src/assets/web-panel/assets/{McpTools-Ywc4IVks.js → McpTools-Bf1gvZPf.js} +3 -3
  35. package/src/assets/web-panel/assets/{Memory-C_zB9dUa.js → Memory-C1bWj4RN.js} +2 -2
  36. package/src/assets/web-panel/assets/{MobileBridge-Nc05r24L.js → MobileBridge-C_Ot1H_a.js} +2 -2
  37. package/src/assets/web-panel/assets/{MobileProjects-BJGxL526.js → MobileProjects-zr-PpsT_.js} +1 -1
  38. package/src/assets/web-panel/assets/{Mtc-Im7SIcz1.js → Mtc-CnzFUz5J.js} +4 -4
  39. package/src/assets/web-panel/assets/{MtcAudit-BFFzvzMD.js → MtcAudit-CAAh99wz.js} +2 -2
  40. package/src/assets/web-panel/assets/{Multisig-CcNEbycq.js → Multisig-D6IAg6HE.js} +3 -3
  41. package/src/assets/web-panel/assets/{NLProgramming-CDH6OTXN.js → NLProgramming-BFMarxb0.js} +1 -1
  42. package/src/assets/web-panel/assets/{Notes-Dqg3QXcU.js → Notes-BRp9ro3t.js} +3 -3
  43. package/src/assets/web-panel/assets/{NotificationSettings-CDVmK1eU.js → NotificationSettings-C0Au3Cxb.js} +1 -1
  44. package/src/assets/web-panel/assets/OrderTableRenderer-ISp6btRY.js +1 -0
  45. package/src/assets/web-panel/assets/{Organization-DJb9bRQS.js → Organization-DYoxLBRX.js} +4 -4
  46. package/src/assets/web-panel/assets/{Overflow-CK7Q5dje.js → Overflow-rO8JJWGJ.js} +1 -1
  47. package/src/assets/web-panel/assets/{P2P-CJIyYfwc.js → P2P-DJleeXIK.js} +2 -2
  48. package/src/assets/web-panel/assets/{PdhVaultBrowser-uqRULcuw.js → PdhVaultBrowser-DM5qghFp.js} +3 -3
  49. package/src/assets/web-panel/assets/{Permissions-Crvwt6bq.js → Permissions-D5v4Beya.js} +4 -4
  50. package/src/assets/web-panel/assets/{PersonalDataHub-DcN5OWzg.js → PersonalDataHub-c2ZTX0Pv.js} +2 -2
  51. package/src/assets/web-panel/assets/{Pipeline-DfWJvvJW.js → Pipeline-Crrkyhpz.js} +1 -1
  52. package/src/assets/web-panel/assets/{Privacy-DepD0S3v.js → Privacy-DZVyrJKa.js} +1 -1
  53. package/src/assets/web-panel/assets/{ProjectInit-B7OKhH27.js → ProjectInit-DKg7J0gz.js} +2 -2
  54. package/src/assets/web-panel/assets/{ProjectSettings-BJ4ueRFv.js → ProjectSettings-3ndmTvVH.js} +2 -2
  55. package/src/assets/web-panel/assets/{Projects-Dl_hPdhU.js → Projects-ll5wnj2L.js} +1 -1
  56. package/src/assets/web-panel/assets/{Providers-Dl0FT1S3.js → Providers-BeqBVMhB.js} +1 -1
  57. package/src/assets/web-panel/assets/{QuickAsk-V2hYLhfp.js → QuickAsk-DKAAxzuA.js} +1 -1
  58. package/src/assets/web-panel/assets/{Recommend-8Kaiodgv.js → Recommend-Byu7IGei.js} +1 -1
  59. package/src/assets/web-panel/assets/{Reputation-CsxB3JGg.js → Reputation-BKhWAmCu.js} +1 -1
  60. package/src/assets/web-panel/assets/{Row-6-x7tEYq.js → Row-BFtn11O6.js} +1 -1
  61. package/src/assets/web-panel/assets/{RssFeed-Buv6f5tw.js → RssFeed-D5a0PT0k.js} +2 -2
  62. package/src/assets/web-panel/assets/{Search-ABrDz84n.js → Search-DAkuaZNe.js} +1 -1
  63. package/src/assets/web-panel/assets/{Security-DqOJmz18.js → Security-C79Ml2Ms.js} +4 -4
  64. package/src/assets/web-panel/assets/{Services-Cq4Tda3q.js → Services-BBk_jH6-.js} +2 -2
  65. package/src/assets/web-panel/assets/{Skeleton-n74QlyYq.js → Skeleton-Cy0VvL0M.js} +1 -1
  66. package/src/assets/web-panel/assets/Skills-OQNky3uI.js +1 -0
  67. package/src/assets/web-panel/assets/{Sla-hwRgJ99Z.js → Sla-CbX1f8xN.js} +1 -1
  68. package/src/assets/web-panel/assets/{SpeechSettings-B6Bs6_-8.js → SpeechSettings-BIkoUjws.js} +1 -1
  69. package/src/assets/web-panel/assets/{SyncSettings-CTp2dZ0z.js → SyncSettings-DG6Swk7G.js} +2 -2
  70. package/src/assets/web-panel/assets/{Tasks-D70Lis6S.js → Tasks-C9R8sgyi.js} +1 -1
  71. package/src/assets/web-panel/assets/{Templates-Cags0ssw.js → Templates-AaJPeCIz.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tenant-BxCMzzGt.js → Tenant-jVFRofww.js} +1 -1
  73. package/src/assets/web-panel/assets/{Terminal-v05SDqHd.js → Terminal-DHBMzfK6.js} +2 -2
  74. package/src/assets/web-panel/assets/{TimelineRenderer-BLUDHbBL.js → TimelineRenderer-9RFfOHSI.js} +1 -1
  75. package/src/assets/web-panel/assets/{Tokens-D-xKLJYv.js → Tokens-ZTfwuABF.js} +1 -1
  76. package/src/assets/web-panel/assets/{Trigger-B47tVIbH.js → Trigger-Xo7uZNQs.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trust-DmRU9kfs.js → Trust-C0cTPYvn.js} +1 -1
  78. package/src/assets/web-panel/assets/{UkeySign-DzgSGs-c.js → UkeySign-DmMKio71.js} +1 -1
  79. package/src/assets/web-panel/assets/{VideoEditing-C6qu58up.js → VideoEditing-DP7B-oGT.js} +1 -1
  80. package/src/assets/web-panel/assets/{Wallet-Dh8ZWx8f.js → Wallet-B1kZDARo.js} +4 -4
  81. package/src/assets/web-panel/assets/{WebAuthn-DFHOVuAY.js → WebAuthn-Bo5kBx27.js} +5 -5
  82. package/src/assets/web-panel/assets/{WorkflowEditor-B_fyQ3Y_.js → WorkflowEditor-DGI9SNHH.js} +1 -1
  83. package/src/assets/web-panel/assets/{chat-BR-WxnCQ.js → chat-y97W1CIG.js} +1 -1
  84. package/src/assets/web-panel/assets/{colors-C-6RysQe.js → colors-DtTNo0sH.js} +1 -1
  85. package/src/assets/web-panel/assets/{compact-item-B_9_SCKN.js → compact-item-D0q0exuS.js} +1 -1
  86. package/src/assets/web-panel/assets/{createContext-D6rklIbE.js → createContext-D7pLFs2I.js} +1 -1
  87. package/src/assets/web-panel/assets/devWarning-BDK34w0I.js +1 -0
  88. package/src/assets/web-panel/assets/{hasIn-BrotgSvd.js → hasIn-CXjG5B2j.js} +1 -1
  89. package/src/assets/web-panel/assets/{index-DkpDFJRn.js → index-1dwtkcJv.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-DfqUsPl2.js → index-4mWZhCzz.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-BaLhL3Tj.js → index-6np5ESBM.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-DigjvHuo.js → index-8jxbZupG.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-MCmNzIC7.js → index-B3y_4OdG.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-CWmJukRW.js → index-B4dPdrvC.js} +1 -1
  95. package/src/assets/web-panel/assets/index-B6SaRuCI.js +1 -0
  96. package/src/assets/web-panel/assets/index-B9ekWb3I.js +1 -0
  97. package/src/assets/web-panel/assets/{index-GzuCTHVZ.js → index-BJUf19Wd.js} +3 -3
  98. package/src/assets/web-panel/assets/{index-PzM_GlKb.js → index-BO644Q4S.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-CTQkYbir.js → index-BPXhU-jp.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-BsDNNDBN.js → index-BU944DeT.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-Bwkg_EJk.js → index-B_hjkMtX.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-DMnomft7.js → index-BdhEYW2a.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CJ70GAW2.js → index-BgmvrPJH.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-DTh0fWI4.js → index-BgyrM0UN.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-CAfRNHna.js → index-BnLrbXDA.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-CCWzUY8K.js → index-BqVjUN8b.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-kkjq_hwC.js → index-BzCPx1cq.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CK8YwdNd.js → index-CFsPe2N7.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-D6KqyxG1.js → index-CKrbutAQ.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Bv9BrnD2.js → index-CSdhC7Qo.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-CJgp_QFo.js → index-Cbqu804A.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-rWiOF7Iu.js → index-CkGFqlYX.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CrGp-4E2.js → index-Ct6xtKkc.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Cn5ghmbB.js → index-DY6KLlgG.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BbRl_gIW.js → index-D_4WcI1V.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DTpElYJs.js → index-DjCawXk1.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-MBOwmoOi.js → index-Dr45Nm9V.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-B2yXH6vy.js → index-EaIfumgW.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DTCUOKu9.js → index-POaFzYGS.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BIiCIC2j.js → index-TrBGgrwG.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CdDmzoPE.js → index-YWOEx3rP.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-Cn21XmDt.js → index-_3wPBMKt.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CsWVDOd2.js → index-aarO4HT9.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CTpxOc5s.js → index-bVJvqDAz.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-B85rQNYG.js → index-gFLQe31v.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-ZehgEQYa.js → index-kvV0f4tV.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-E_5VXq8H.js → index-qoB3whR9.js} +1 -1
  128. package/src/assets/web-panel/assets/{initDefaultProps-C2v_L5na.js → initDefaultProps-BnXISaAa.js} +1 -1
  129. package/src/assets/web-panel/assets/{motion-DNDqGbfr.js → motion-ChY7C0zJ.js} +1 -1
  130. package/src/assets/web-panel/assets/{move-xvpQ_6hJ.js → move-ByFZMFM5.js} +1 -1
  131. package/src/assets/web-panel/assets/{omit-Cb0FsfrO.js → omit-BYeliY1H.js} +1 -1
  132. package/src/assets/web-panel/assets/{pickAttrs-BxhYpnum.js → pickAttrs-B9dcAKnu.js} +1 -1
  133. package/src/assets/web-panel/assets/{placementArrow-B3soaW4h.js → placementArrow-D3F_txz7.js} +1 -1
  134. package/src/assets/web-panel/assets/{responsiveObserve-B-eRSLvd.js → responsiveObserve-ClkwY7wS.js} +1 -1
  135. package/src/assets/web-panel/assets/{slide--cM2ZOx-.js → slide-BNgy2Eea.js} +1 -1
  136. package/src/assets/web-panel/assets/{statusUtils-DjBhfi8Q.js → statusUtils-Bv3heMCD.js} +1 -1
  137. package/src/assets/web-panel/assets/{styleChecker-C30mMh8o.js → styleChecker-DVdlHbQm.js} +1 -1
  138. package/src/assets/web-panel/assets/{useFlexGapSupport-f7y2Qlzs.js → useFlexGapSupport-alrRY5BK.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFs-iTCXoLoZ.js → useFs-CcVh0-Vu.js} +1 -1
  140. package/src/assets/web-panel/assets/{usePersonalDataHub-BH0RXmVF.js → usePersonalDataHub-CkkHPhyq.js} +1 -1
  141. package/src/assets/web-panel/assets/{vnode-DQtmeDXM.js → vnode-DWi0X9WN.js} +1 -1
  142. package/src/assets/web-panel/assets/{zoom-vw50zkLZ.js → zoom-DCbqxxLH.js} +1 -1
  143. package/src/assets/web-panel/index.html +1 -1
  144. package/src/commands/agent.js +306 -1
  145. package/src/commands/ask.js +35 -1
  146. package/src/commands/checkpoint.js +239 -0
  147. package/src/commands/cost.js +114 -0
  148. package/src/commands/session.js +22 -2
  149. package/src/index.js +4 -0
  150. package/src/lib/file-checkpoint.js +300 -0
  151. package/src/lib/llm-pricing.js +227 -0
  152. package/src/lib/personal-data-hub-wiring.js +30 -0
  153. package/src/lib/recent-session.js +72 -0
  154. package/src/lib/session-picker.js +68 -0
  155. package/src/repl/agent-repl.js +58 -2
  156. package/src/repl/chat-repl.js +16 -1
  157. package/src/runtime/agent-core.js +68 -31
  158. package/src/runtime/fallback-model.js +109 -0
  159. package/src/runtime/file-ref-expander.js +258 -0
  160. package/src/runtime/headless-runner.js +576 -0
  161. package/src/runtime/headless-stream.js +302 -0
  162. package/src/runtime/policies/agent-policy.js +6 -0
  163. package/src/runtime/quiet-stdout.js +35 -0
  164. package/src/runtime/system-prompt.js +60 -0
  165. package/src/assets/web-panel/assets/AppLayout-B0hl5cPk.js +0 -9
  166. package/src/assets/web-panel/assets/ChatBubbleRenderer-Dlw_6n3M.js +0 -1
  167. package/src/assets/web-panel/assets/Dashboard-XlMpT7K_.js +0 -3
  168. package/src/assets/web-panel/assets/OrderTableRenderer-Bg0bkfjR.js +0 -1
  169. package/src/assets/web-panel/assets/Skills-CC0iozL5.js +0 -1
  170. package/src/assets/web-panel/assets/devWarning-BiN5HELJ.js +0 -1
  171. package/src/assets/web-panel/assets/index-BhxiT2LJ.js +0 -1
  172. package/src/assets/web-panel/assets/index-DBNSZ2oz.js +0 -1
@@ -65,6 +65,9 @@ import {
65
65
  agentLoop as coreAgentLoop,
66
66
  formatToolArgs,
67
67
  } from "../runtime/agent-core.js";
68
+ import { expandFileRefs } from "../runtime/file-ref-expander.js";
69
+ import { composeSystemPrompt } from "../runtime/system-prompt.js";
70
+ import { makeFallbackChatFn } from "../runtime/fallback-model.js";
68
71
 
69
72
  /**
70
73
  * Reference to the runtime DB for hook execution (set during startAgentRepl)
@@ -150,6 +153,26 @@ export async function startAgentRepl(options = {}) {
150
153
  let provider = options.provider || "ollama";
151
154
  const baseUrl = options.baseUrl || "http://localhost:11434";
152
155
  const apiKey = options.apiKey || null;
156
+ // Extra workspace roots (--add-dir): advertised in the system prompt and
157
+ // spanned by search_files.
158
+ const additionalDirectories = Array.isArray(options.additionalDirectories)
159
+ ? options.additionalDirectories
160
+ : [];
161
+
162
+ // --fallback-model: retry a turn's LLM call once on a backup model when the
163
+ // primary errors out (overload / network). Built once; passed into every
164
+ // agentLoop call via chatFn. Undefined when no fallback configured.
165
+ const _fallbackChatFn = options.fallbackModel
166
+ ? makeFallbackChatFn({
167
+ fallbackModel: options.fallbackModel,
168
+ onFallback: ({ from, to, error }) =>
169
+ logger.info(
170
+ chalk.yellow(
171
+ `[fallback] model "${from}" failed (${error}); retrying with "${to}"`,
172
+ ),
173
+ ),
174
+ })
175
+ : undefined;
153
176
 
154
177
  // Bootstrap runtime (best-effort, DB not required)
155
178
  let db = null;
@@ -299,7 +322,18 @@ export async function startAgentRepl(options = {}) {
299
322
  }
300
323
 
301
324
  const messages = [
302
- { role: "system", content: buildSystemPrompt(process.cwd()) },
325
+ {
326
+ role: "system",
327
+ // --system-prompt replaces the built-in prompt; --append-system-prompt
328
+ // extends it (parity with the headless runners).
329
+ content: composeSystemPrompt(
330
+ buildSystemPrompt(process.cwd(), { additionalDirectories }),
331
+ {
332
+ systemPrompt: options.systemPrompt,
333
+ appendSystemPrompt: options.appendSystemPrompt,
334
+ },
335
+ ),
336
+ },
303
337
  ];
304
338
 
305
339
  // Deep Agents Deploy Phase 1 — load agent bundle if --bundle provided.
@@ -1418,8 +1452,28 @@ export async function startAgentRepl(options = {}) {
1418
1452
  logger.verbose(`[hook] prompt rewritten by UserPromptSubmit hook`);
1419
1453
  }
1420
1454
 
1455
+ // Expand @path file references into context blocks (Claude-Code parity),
1456
+ // so `review @src/x.js` injects the file contents. Typo'd paths are warned
1457
+ // about and left as-is.
1458
+ let userContent = effectivePrompt;
1459
+ try {
1460
+ const fileRefs = expandFileRefs(effectivePrompt, { cwd: process.cwd() });
1461
+ userContent = fileRefs.prompt;
1462
+ for (const w of fileRefs.warnings) {
1463
+ logger.info(chalk.yellow(`[@ref] ${w}`));
1464
+ }
1465
+ if (fileRefs.refs.length > 0) {
1466
+ const summary = fileRefs.refs
1467
+ .map((r) => `${r.rel}${r.kind === "dir" ? "/" : ""}`)
1468
+ .join(", ");
1469
+ logger.verbose(`[@ref] injected: ${summary}`);
1470
+ }
1471
+ } catch (err) {
1472
+ logger.verbose(`[@ref] expansion skipped: ${err.message}`);
1473
+ }
1474
+
1421
1475
  // Add user message
1422
- messages.push({ role: "user", content: effectivePrompt });
1476
+ messages.push({ role: "user", content: userContent });
1423
1477
 
1424
1478
  // Slot-filling: detect intent and fill missing parameters interactively
1425
1479
  try {
@@ -1476,9 +1530,11 @@ export async function startAgentRepl(options = {}) {
1476
1530
  iterationBudget,
1477
1531
  sessionId,
1478
1532
  cwd: process.cwd(),
1533
+ additionalDirectories,
1479
1534
  prepareCall: defaultPrepareCall,
1480
1535
  approvalGate: _approvalGate,
1481
1536
  mcpClient: _bundleMcpClient || undefined,
1537
+ chatFn: _fallbackChatFn,
1482
1538
  });
1483
1539
 
1484
1540
  if (sessionId && usageEvents?.length) {
@@ -12,6 +12,7 @@ import readline from "readline";
12
12
  import chalk from "chalk";
13
13
  import { logger } from "../lib/logger.js";
14
14
  import { BUILT_IN_PROVIDERS } from "../lib/llm-providers.js";
15
+ import { expandFileRefs } from "../runtime/file-ref-expander.js";
15
16
  import {
16
17
  streamOllama,
17
18
  streamOpenAI,
@@ -153,8 +154,22 @@ export async function startChatRepl(options = {}) {
153
154
  }
154
155
  }
155
156
 
157
+ // Expand @path file references into context blocks (Claude-Code parity),
158
+ // so `summarize @notes.md` injects the file. The model sees the expanded
159
+ // content; the JSONL log keeps the original line for readability.
160
+ let userContent = trimmed;
161
+ try {
162
+ const fileRefs = expandFileRefs(trimmed, { cwd: process.cwd() });
163
+ userContent = fileRefs.prompt;
164
+ for (const w of fileRefs.warnings) {
165
+ logger.info(chalk.yellow(`[@ref] ${w}`));
166
+ }
167
+ } catch (err) {
168
+ logger.verbose?.(`[@ref] expansion skipped: ${err.message}`);
169
+ }
170
+
156
171
  // Add user message
157
- messages.push({ role: "user", content: trimmed });
172
+ messages.push({ role: "user", content: userContent });
158
173
 
159
174
  // Stream the response
160
175
  process.stdout.write(chalk.blue("ai> "));
@@ -338,9 +338,12 @@ function _buildPersonaPrompt(persona, envLines, cwd) {
338
338
  * 4. Default hardcoded prompt → fallback when no persona
339
339
  *
340
340
  * @param {string} [cwd] - working directory
341
+ * @param {object} [opts]
342
+ * @param {string[]} [opts.additionalDirectories] - extra workspace roots
343
+ * (absolute paths) the agent may read/search/edit beyond `cwd`.
341
344
  * @returns {string} complete system prompt
342
345
  */
343
- export function buildSystemPrompt(cwd) {
346
+ export function buildSystemPrompt(cwd, opts = {}) {
344
347
  const dir = cwd || process.cwd();
345
348
 
346
349
  // Check for project persona
@@ -393,6 +396,19 @@ export function buildSystemPrompt(cwd) {
393
396
  // Non-critical
394
397
  }
395
398
 
399
+ // Advertise extra workspace roots (--add-dir) so the model knows it may
400
+ // reach beyond cwd and which absolute paths to use.
401
+ const extraDirs = Array.isArray(opts.additionalDirectories)
402
+ ? opts.additionalDirectories.filter(Boolean)
403
+ : [];
404
+ if (extraDirs.length > 0) {
405
+ prompt +=
406
+ `\n\n## Additional working directories\n` +
407
+ `Beyond the current working directory, you may read, search, and edit ` +
408
+ `files under these absolute roots. Pass absolute paths to access them:\n` +
409
+ extraDirs.map((d) => `- ${d}`).join("\n");
410
+ }
411
+
396
412
  return prompt;
397
413
  }
398
414
 
@@ -529,6 +545,7 @@ export async function executeTool(name, args, context = {}) {
529
545
  shellPolicyOverrides: context.shellPolicyOverrides || null,
530
546
  approvalGate: context.approvalGate || null,
531
547
  shellConfirm: context.shellConfirm || null,
548
+ additionalDirectories: context.additionalDirectories || null,
532
549
  });
533
550
  } catch (err) {
534
551
  if (hookDb) {
@@ -598,6 +615,7 @@ async function executeToolInner(
598
615
  shellPolicyOverrides,
599
616
  approvalGate,
600
617
  shellConfirm,
618
+ additionalDirectories,
601
619
  },
602
620
  ) {
603
621
  const localToolDescriptor =
@@ -962,41 +980,52 @@ async function executeToolInner(
962
980
  }
963
981
 
964
982
  case "search_files": {
965
- const dir = args.directory ? path.resolve(cwd, args.directory) : cwd;
966
- try {
967
- if (args.content_search) {
968
- const cmd =
969
- process.platform === "win32"
970
- ? `findstr /s /i /n "${args.pattern}" *`
971
- : `grep -r -l -i "${args.pattern}" . --include="*" 2>/dev/null | head -20`;
972
- const output = execSync(cmd, {
973
- cwd: dir,
974
- encoding: "utf8",
975
- timeout: 10000,
976
- });
977
- return attachDescriptor({
978
- matches: output.trim().split("\n").slice(0, 20),
979
- });
980
- } else {
981
- const cmd =
982
- process.platform === "win32"
983
- ? `dir /s /b *${args.pattern}* 2>NUL`
984
- : `find . -name "*${args.pattern}*" -type f 2>/dev/null | head -20`;
983
+ // An explicit directory scopes the search to one root; otherwise span
984
+ // cwd plus any --add-dir roots so cross-package searches find matches.
985
+ const extraRoots = Array.isArray(additionalDirectories)
986
+ ? additionalDirectories.filter(Boolean)
987
+ : [];
988
+ const roots = args.directory
989
+ ? [path.resolve(cwd, args.directory)]
990
+ : [cwd, ...extraRoots];
991
+ const isContent = Boolean(args.content_search);
992
+ const cmd = isContent
993
+ ? process.platform === "win32"
994
+ ? `findstr /s /i /n "${args.pattern}" *`
995
+ : `grep -r -l -i "${args.pattern}" . --include="*" 2>/dev/null | head -20`
996
+ : process.platform === "win32"
997
+ ? `dir /s /b *${args.pattern}* 2>NUL`
998
+ : `find . -name "*${args.pattern}*" -type f 2>/dev/null | head -20`;
999
+
1000
+ const hits = [];
1001
+ const seen = new Set();
1002
+ for (const root of roots) {
1003
+ if (hits.length >= 20) break;
1004
+ try {
1005
+ if (!fs.existsSync(root)) continue;
985
1006
  const output = execSync(cmd, {
986
- cwd: dir,
1007
+ cwd: root,
987
1008
  encoding: "utf8",
988
1009
  timeout: 10000,
989
1010
  });
990
- return attachDescriptor({
991
- files: output.trim().split("\n").filter(Boolean).slice(0, 20),
992
- });
1011
+ for (const line of output.trim().split("\n")) {
1012
+ const v = line.trim();
1013
+ if (!v || seen.has(v)) continue;
1014
+ // Qualify with the root so multi-root results stay unambiguous.
1015
+ const labeled = roots.length > 1 ? `${root}: ${v}` : v;
1016
+ seen.add(v);
1017
+ hits.push(labeled);
1018
+ if (hits.length >= 20) break;
1019
+ }
1020
+ } catch {
1021
+ // No matches in this root — continue to the next.
993
1022
  }
994
- } catch {
995
- return attachDescriptor({
996
- files: [],
997
- message: "No matches found",
998
- });
999
1023
  }
1024
+
1025
+ if (hits.length === 0) {
1026
+ return attachDescriptor({ files: [], message: "No matches found" });
1027
+ }
1028
+ return attachDescriptor(isContent ? { matches: hits } : { files: hits });
1000
1029
  }
1001
1030
 
1002
1031
  case "list_dir": {
@@ -1608,9 +1637,16 @@ export async function chatWithTools(rawMessages, options) {
1608
1637
  } = options;
1609
1638
 
1610
1639
  const persona = _loadProjectPersona(options.cwd);
1640
+ // Merge the project-persona deny-list with any caller-supplied deny-list
1641
+ // (e.g. headless `--disallowed-tools`). Without this merge the caller's
1642
+ // deny-list is silently dropped and the tool stays callable.
1643
+ const mergedDisabledTools = [
1644
+ ...(Array.isArray(persona?.toolsDisabled) ? persona.toolsDisabled : []),
1645
+ ...(Array.isArray(options.disabledTools) ? options.disabledTools : []),
1646
+ ];
1611
1647
  const tools = getAgentToolDefinitions({
1612
1648
  names: options.enabledToolNames,
1613
- disabledTools: persona?.toolsDisabled,
1649
+ disabledTools: mergedDisabledTools,
1614
1650
  extraTools: [
1615
1651
  ...(options.hostManagedToolPolicy?.toolDefinitions || []),
1616
1652
  ...(options.extraToolDefinitions || []),
@@ -1842,6 +1878,7 @@ export async function* agentLoop(messages, options) {
1842
1878
  shellPolicyOverrides: options.shellPolicyOverrides || null,
1843
1879
  approvalGate: options.approvalGate || null,
1844
1880
  shellConfirm: options.shellConfirm || null,
1881
+ additionalDirectories: options.additionalDirectories || null,
1845
1882
  };
1846
1883
 
1847
1884
  throwIfAborted(signal);
@@ -0,0 +1,109 @@
1
+ /**
2
+ * `--fallback-model` support — Claude-Code parity for unattended runs.
3
+ *
4
+ * Wraps the agent loop's LLM call so a single request that fails with a
5
+ * *retryable* error (overload / rate-limit / transient network) is transparently
6
+ * re-issued once with a backup model. Because it sits at the chatFn seam
7
+ * (`agentLoop` uses `options.chatFn || chatWithTools`), fallback needs no changes
8
+ * to the runners — the wrapped fn is passed in via `options.chatFn`.
9
+ *
10
+ * Same-provider only: the fallback swaps `options.model`, keeping the configured
11
+ * provider / baseUrl / apiKey. Cross-provider fallback is a larger feature.
12
+ */
13
+
14
+ import { chatWithTools } from "./agent-core.js";
15
+
16
+ // Heuristics for "try again on a different model" — overloaded backends,
17
+ // rate limits, and transient connectivity. Deliberately conservative: a 4xx
18
+ // that is not 429 (bad request / auth) is NOT retried.
19
+ const RETRYABLE_PATTERNS = [
20
+ /overload/i,
21
+ /rate.?limit/i,
22
+ /too many requests/i,
23
+ /temporarily unavailable/i,
24
+ /\b429\b/,
25
+ /\b50[0234]\b/,
26
+ /\b529\b/,
27
+ /timeout/i,
28
+ /timed out/i,
29
+ /ETIMEDOUT/i,
30
+ /ECONNREFUSED/i,
31
+ /ECONNRESET/i,
32
+ /ENOTFOUND/i,
33
+ /EAI_AGAIN/i,
34
+ /socket hang up/i,
35
+ /fetch failed/i,
36
+ /network error/i,
37
+ ];
38
+
39
+ /**
40
+ * Decide whether an error from an LLM call warrants a fallback retry.
41
+ * @param {any} err
42
+ * @returns {boolean}
43
+ */
44
+ export function isRetryableModelError(err) {
45
+ if (!err) return false;
46
+ const status =
47
+ typeof err.status === "number"
48
+ ? err.status
49
+ : typeof err.statusCode === "number"
50
+ ? err.statusCode
51
+ : null;
52
+ if (status === 429) return true;
53
+ if (status !== null && status >= 500 && status <= 599) return true;
54
+
55
+ const parts = [
56
+ err.message,
57
+ typeof err.code === "string" ? err.code : "",
58
+ err.cause?.message,
59
+ err.cause?.code,
60
+ ]
61
+ .filter(Boolean)
62
+ .join(" ");
63
+ return RETRYABLE_PATTERNS.some((re) => re.test(parts));
64
+ }
65
+
66
+ /**
67
+ * Build a chatFn that retries once on the fallback model.
68
+ *
69
+ * @param {object} opts
70
+ * @param {string} opts.fallbackModel backup model name (required)
71
+ * @param {Function} [opts.baseChatFn=chatWithTools] underlying LLM call
72
+ * @param {Function} [opts.isRetryable] error predicate (testing seam)
73
+ * @param {Function} [opts.onFallback] notified ({from,to,error}) on retry
74
+ * @returns {Function} a (messages, options) => Promise<result> chatFn
75
+ */
76
+ export function makeFallbackChatFn(opts = {}) {
77
+ const fallbackModel = opts.fallbackModel;
78
+ const baseChatFn = opts.baseChatFn || chatWithTools;
79
+ const isRetryable = opts.isRetryable || isRetryableModelError;
80
+ const onFallback = opts.onFallback;
81
+
82
+ return async function chatWithFallback(messages, options = {}) {
83
+ try {
84
+ return await baseChatFn(messages, options);
85
+ } catch (err) {
86
+ const primaryModel = options.model;
87
+ // Skip a no-op retry when the fallback is the same model as the primary.
88
+ if (
89
+ !fallbackModel ||
90
+ fallbackModel === primaryModel ||
91
+ !isRetryable(err)
92
+ ) {
93
+ throw err;
94
+ }
95
+ if (typeof onFallback === "function") {
96
+ try {
97
+ onFallback({
98
+ from: primaryModel,
99
+ to: fallbackModel,
100
+ error: err?.message || String(err),
101
+ });
102
+ } catch {
103
+ // Notification is best-effort — never mask the retry.
104
+ }
105
+ }
106
+ return await baseChatFn(messages, { ...options, model: fallbackModel });
107
+ }
108
+ };
109
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * `@file` reference expander — Claude-Code-style prompt file injection.
3
+ *
4
+ * Scans a prompt for `@<path>` tokens and, for each one that resolves to a real
5
+ * file/dir under the working directory, appends the file contents (or a dir
6
+ * listing) as a `<referenced-files>` context block. The original `@token` stays
7
+ * in the prose so the message remains readable and the model knows what the
8
+ * user pointed at.
9
+ *
10
+ * Design notes:
11
+ * - A token only matches when `@` is at start-of-input or preceded by
12
+ * whitespace / an opening bracket-quote. That skips email addresses
13
+ * (`foo@bar.com`) and decorative `@` — the `@` there is preceded by a word
14
+ * char and never matches.
15
+ * - Non-existent paths are NOT expanded and are surfaced as warnings (so a
16
+ * typo'd path is visible rather than silently dropped). Decorative `@words`
17
+ * that happen not to be files simply produce no ref and no warning unless
18
+ * they look path-like (contain a slash or a dot-extension).
19
+ * - Files are read as UTF-8 (encoding.md rule). Binary files are skipped with
20
+ * a note. Oversized files are truncated with a marker.
21
+ * - All filesystem access goes through an injectable `deps` seam so unit tests
22
+ * never touch the real disk (mirrors the project's `_deps` philosophy).
23
+ */
24
+
25
+ import fsDefault from "fs";
26
+ import pathDefault from "path";
27
+
28
+ export const DEFAULT_MAX_BYTES = 100 * 1024; // 100 KB per referenced file
29
+ export const DEFAULT_MAX_DIR_ENTRIES = 200;
30
+
31
+ // `@` preceded by start / whitespace / opening bracket-quote, then a path run.
32
+ // We capture greedily up to the next whitespace or a closing bracket-quote and
33
+ // trim trailing sentence punctuation afterwards.
34
+ const TOKEN_RE = /(^|[\s("'`[{])@([^\s"'`)\]}]+)/g;
35
+
36
+ /** A raw token looks path-like if it has a directory separator or a file ext. */
37
+ function looksPathLike(raw) {
38
+ return /[\\/]/.test(raw) || /\.[A-Za-z0-9]+$/.test(raw);
39
+ }
40
+
41
+ /** Strip trailing sentence punctuation that is unlikely to be part of a path. */
42
+ function trimTrailingPunct(raw) {
43
+ return raw.replace(/[),;:!?'".]+$/g, "");
44
+ }
45
+
46
+ /**
47
+ * Find unique `@path` candidates in the text, in first-seen order.
48
+ * @returns {Array<{raw:string}>}
49
+ */
50
+ export function findFileRefTokens(text) {
51
+ const seen = new Set();
52
+ const out = [];
53
+ let m;
54
+ TOKEN_RE.lastIndex = 0;
55
+ while ((m = TOKEN_RE.exec(text)) !== null) {
56
+ const raw = m[2];
57
+ if (!raw || seen.has(raw)) continue;
58
+ seen.add(raw);
59
+ out.push({ raw });
60
+ }
61
+ return out;
62
+ }
63
+
64
+ /** True when a buffer/string looks binary (has a NUL in the sampled prefix). */
65
+ function looksBinary(buf) {
66
+ const sample = buf.slice(0, 8000);
67
+ for (let i = 0; i < sample.length; i++) {
68
+ if (sample[i] === 0) return true;
69
+ }
70
+ return false;
71
+ }
72
+
73
+ /**
74
+ * Resolve a single raw token to an injectable ref descriptor, or null when it
75
+ * does not resolve to an existing path. Tries the literal path first, then a
76
+ * trailing-punctuation-stripped variant (handles "see @config.json.").
77
+ */
78
+ function resolveRef(raw, { cwd, fs, path, maxBytes, maxDirEntries }) {
79
+ const candidates = [raw];
80
+ const trimmed = trimTrailingPunct(raw);
81
+ if (trimmed && trimmed !== raw) candidates.push(trimmed);
82
+
83
+ for (const cand of candidates) {
84
+ const abs = path.resolve(cwd, cand);
85
+ let stat;
86
+ try {
87
+ stat = fs.statSync(abs);
88
+ } catch {
89
+ continue; // not this candidate
90
+ }
91
+ if (stat.isDirectory()) {
92
+ let entries;
93
+ try {
94
+ entries = fs.readdirSync(abs, { withFileTypes: true });
95
+ } catch (err) {
96
+ return {
97
+ kind: "error",
98
+ raw: cand,
99
+ rel: cand,
100
+ message: `cannot read directory: ${err.message}`,
101
+ };
102
+ }
103
+ const names = entries
104
+ .slice(0, maxDirEntries)
105
+ .map((e) => (e.isDirectory() ? `${e.name}/` : e.name))
106
+ .sort();
107
+ const truncated = entries.length > maxDirEntries;
108
+ return {
109
+ kind: "dir",
110
+ raw: cand,
111
+ rel: cand,
112
+ entries: names,
113
+ total: entries.length,
114
+ truncated,
115
+ };
116
+ }
117
+ if (stat.isFile()) {
118
+ let buf;
119
+ try {
120
+ buf = fs.readFileSync(abs);
121
+ } catch (err) {
122
+ return {
123
+ kind: "error",
124
+ raw: cand,
125
+ rel: cand,
126
+ message: `cannot read file: ${err.message}`,
127
+ };
128
+ }
129
+ if (looksBinary(buf)) {
130
+ return {
131
+ kind: "binary",
132
+ raw: cand,
133
+ rel: cand,
134
+ bytes: stat.size,
135
+ };
136
+ }
137
+ const truncated = buf.length > maxBytes;
138
+ const content = (truncated ? buf.slice(0, maxBytes) : buf).toString(
139
+ "utf-8",
140
+ );
141
+ return {
142
+ kind: "file",
143
+ raw: cand,
144
+ rel: cand,
145
+ bytes: stat.size,
146
+ content,
147
+ truncated,
148
+ };
149
+ }
150
+ // Something exotic (socket/fifo) — treat as non-expandable.
151
+ return null;
152
+ }
153
+ return null;
154
+ }
155
+
156
+ function escapeAttr(s) {
157
+ return String(s).replace(/"/g, "&quot;");
158
+ }
159
+
160
+ /** Render the resolved refs into a single `<referenced-files>` block. */
161
+ function renderBlock(refs) {
162
+ const parts = [
163
+ '<referenced-files note="auto-injected from @path references">',
164
+ ];
165
+ for (const ref of refs) {
166
+ if (ref.kind === "file") {
167
+ const attrs =
168
+ `path="${escapeAttr(ref.rel)}" bytes="${ref.bytes}"` +
169
+ (ref.truncated
170
+ ? ` truncated="true" shown-bytes="${DEFAULT_MAX_BYTES}"`
171
+ : "");
172
+ parts.push(`<file ${attrs}>`);
173
+ parts.push(ref.content);
174
+ if (ref.truncated) {
175
+ parts.push(`\n… [truncated — file is ${ref.bytes} bytes]`);
176
+ }
177
+ parts.push("</file>");
178
+ } else if (ref.kind === "dir") {
179
+ const attrs =
180
+ `path="${escapeAttr(ref.rel)}" entries="${ref.total}"` +
181
+ (ref.truncated ? ` truncated="true"` : "");
182
+ parts.push(`<dir ${attrs}>`);
183
+ parts.push(ref.entries.join("\n"));
184
+ if (ref.truncated) {
185
+ parts.push(`… [truncated — ${ref.total} entries total]`);
186
+ }
187
+ parts.push("</dir>");
188
+ } else if (ref.kind === "binary") {
189
+ parts.push(
190
+ `<file path="${escapeAttr(ref.rel)}" bytes="${ref.bytes}" binary="true" note="binary file — contents omitted" />`,
191
+ );
192
+ }
193
+ }
194
+ parts.push("</referenced-files>");
195
+ return parts.join("\n");
196
+ }
197
+
198
+ /**
199
+ * Expand `@file` references in a prompt.
200
+ *
201
+ * @param {string} prompt
202
+ * @param {object} [opts]
203
+ * @param {string} [opts.cwd=process.cwd()]
204
+ * @param {number} [opts.maxBytes]
205
+ * @param {number} [opts.maxDirEntries]
206
+ * @param {object} [opts.deps] Injection seam: { fs, path }.
207
+ * @returns {{ prompt:string, refs:Array, warnings:string[] }}
208
+ * `prompt` is unchanged when nothing resolved; otherwise it is the
209
+ * original text + a trailing `<referenced-files>` block.
210
+ */
211
+ export function expandFileRefs(prompt, opts = {}) {
212
+ const text = typeof prompt === "string" ? prompt : "";
213
+ if (!text || !text.includes("@")) {
214
+ return { prompt: text, refs: [], warnings: [] };
215
+ }
216
+ const fs = opts.deps?.fs || fsDefault;
217
+ const path = opts.deps?.path || pathDefault;
218
+ const cwd = opts.cwd || process.cwd();
219
+ const maxBytes = Number.isFinite(opts.maxBytes)
220
+ ? opts.maxBytes
221
+ : DEFAULT_MAX_BYTES;
222
+ const maxDirEntries = Number.isFinite(opts.maxDirEntries)
223
+ ? opts.maxDirEntries
224
+ : DEFAULT_MAX_DIR_ENTRIES;
225
+
226
+ const tokens = findFileRefTokens(text);
227
+ const refs = [];
228
+ const warnings = [];
229
+
230
+ for (const { raw } of tokens) {
231
+ const ref = resolveRef(raw, { cwd, fs, path, maxBytes, maxDirEntries });
232
+ if (!ref) {
233
+ // Only warn for path-like tokens; bare @mentions are left alone.
234
+ if (looksPathLike(raw)) {
235
+ warnings.push(`@${raw} — no such file or directory (left as-is)`);
236
+ }
237
+ continue;
238
+ }
239
+ if (ref.kind === "error") {
240
+ warnings.push(`@${ref.raw} — ${ref.message} (left as-is)`);
241
+ continue;
242
+ }
243
+ refs.push(ref);
244
+ }
245
+
246
+ if (refs.length === 0) {
247
+ return { prompt: text, refs: [], warnings };
248
+ }
249
+
250
+ const block = renderBlock(refs);
251
+ return {
252
+ prompt: `${text}\n\n${block}`,
253
+ refs,
254
+ warnings,
255
+ };
256
+ }
257
+
258
+ export const _deps = { fs: fsDefault, path: pathDefault };