@qwen-code/qwen-code 0.19.2 → 0.19.3-nightly.20260630.e00fe6a27

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 (286) hide show
  1. package/bundled/extension-creator/SKILL.md +357 -0
  2. package/bundled/loop/SKILL.md +21 -7
  3. package/bundled/loop/SKILL.test.ts +41 -0
  4. package/bundled/loop/loop-task-file.test.ts +1064 -0
  5. package/bundled/loop/loop-task-file.ts +404 -0
  6. package/bundled/loop/loop-tick-resolver.test.ts +665 -0
  7. package/bundled/loop/loop-tick-resolver.ts +347 -0
  8. package/bundled/qc-helper/SKILL.md +26 -19
  9. package/bundled/qc-helper/docs/configuration/auth.md +11 -11
  10. package/bundled/qc-helper/docs/configuration/model-providers.md +13 -13
  11. package/bundled/qc-helper/docs/configuration/settings.md +76 -75
  12. package/bundled/qc-helper/docs/features/channels/plugins.md +1 -1
  13. package/bundled/qc-helper/docs/features/commands.md +108 -77
  14. package/bundled/qc-helper/docs/features/hooks.md +6 -1
  15. package/bundled/qc-helper/docs/features/memory.md +52 -0
  16. package/bundled/qc-helper/docs/qwen-serve.md +24 -2
  17. package/bundled/qc-helper/docs/support/tos-privacy.md +16 -3
  18. package/bundled/qc-helper/docs/support/troubleshooting.md +6 -0
  19. package/chunks/{MaxSizedBox-7RERNLFK.js → MaxSizedBox-T5X5MN5Q.js} +29 -27
  20. package/chunks/{StandaloneSessionPicker-S22XBSN6.js → StandaloneSessionPicker-P3E3WTOQ.js} +42 -37
  21. package/chunks/{acpAgent-4MCETHF2.js → acpAgent-QEP6DXFW.js} +864 -175
  22. package/chunks/{agent-K3ENVYSB.js → agent-UDANDUF7.js} +24 -23
  23. package/chunks/{agent-headless-HO3V3A3A.js → agent-headless-KYVSZHSO.js} +24 -23
  24. package/chunks/{anthropicContentGenerator-E3TQ5XJJ.js → anthropicContentGenerator-KWSJAQ7B.js} +33 -12
  25. package/chunks/{artifact-tool-QA4KRO3B.js → artifact-tool-D2X7G3C3.js} +2 -2
  26. package/chunks/{askUserQuestion-HMWXW6VN.js → askUserQuestion-UGETICJQ.js} +2 -2
  27. package/chunks/{bridge-4LV2HBH2.js → bridge-X42LXPTA.js} +31 -30
  28. package/chunks/{ca-7WC7ELX6.js → ca-IUFWESZL.js} +1 -0
  29. package/chunks/{chunk-JXQ23SNT.js → chunk-2JKEQWDZ.js} +623 -78
  30. package/chunks/{chunk-2TJ3YN6Q.js → chunk-2RJA6J57.js} +416 -401
  31. package/chunks/{chunk-3UALQ56H.js → chunk-3227DZO4.js} +1 -1
  32. package/chunks/{chunk-BGUDAULQ.js → chunk-3CPCDXFR.js} +5 -5
  33. package/chunks/{chunk-5RVSM6NW.js → chunk-3HUHAQF3.js} +1 -1
  34. package/chunks/{chunk-D5E5TMYH.js → chunk-3M4W4U5G.js} +16 -2
  35. package/chunks/{chunk-DBZUXSSV.js → chunk-4X7NIEQJ.js} +3 -3
  36. package/chunks/chunk-5GOVVXEV.js +73 -0
  37. package/chunks/{chunk-2BAQ3ACS.js → chunk-5SM4VT6V.js} +405 -5
  38. package/chunks/{chunk-NYOHZYHM.js → chunk-5XQ4A26L.js} +45 -3
  39. package/chunks/{chunk-CI5O6SQL.js → chunk-6FKEVYCD.js} +4 -4
  40. package/chunks/{chunk-ZD3NW5KL.js → chunk-7LM4XHPE.js} +63 -9
  41. package/chunks/chunk-7UNP2XM7.js +840 -0
  42. package/chunks/{chunk-OQHQHJRK.js → chunk-ARYCHZ4B.js} +151 -4775
  43. package/chunks/chunk-BC3C4F3Q.js +40 -0
  44. package/chunks/{chunk-CGEGHF2C.js → chunk-BKXBARJZ.js} +1 -1
  45. package/chunks/chunk-BMFMJINR.js +45 -0
  46. package/chunks/chunk-BOJWHRMX.js +11273 -0
  47. package/chunks/{chunk-CGOMICBE.js → chunk-BSZEQNUA.js} +34 -1
  48. package/chunks/{chunk-OIC6S73E.js → chunk-BUUQ5HPX.js} +1 -1
  49. package/chunks/{chunk-2BIF2NVV.js → chunk-BVSVIN45.js} +2 -2
  50. package/chunks/{chunk-TOEGRAVX.js → chunk-BX5YO7UW.js} +17 -5
  51. package/chunks/{chunk-4W4T77MO.js → chunk-BXVWV4GI.js} +109 -6
  52. package/chunks/{chunk-VQRYPFCV.js → chunk-CYHVPQYT.js} +10 -460
  53. package/chunks/{chunk-PRYMDRBV.js → chunk-DDMOU62Q.js} +1 -1
  54. package/chunks/{chunk-S566HKUS.js → chunk-DHXWNSJA.js} +15 -1
  55. package/chunks/{chunk-F6XNTYNE.js → chunk-E6XQPQXW.js} +30 -307
  56. package/chunks/{chunk-QBZ22PQX.js → chunk-EWALXUAD.js} +3 -1
  57. package/chunks/{chunk-EL5IHQON.js → chunk-EWYQFWYF.js} +6 -27
  58. package/chunks/{chunk-HG6YKEBI.js → chunk-F2DOT6UZ.js} +161 -30
  59. package/chunks/{chunk-JTYRZODB.js → chunk-FOOIZCWL.js} +16 -7
  60. package/chunks/{chunk-EX4W6ZYG.js → chunk-G5G4TFZ7.js} +1 -1
  61. package/chunks/{chunk-EW6C27XX.js → chunk-GEHVIWAT.js} +9 -3
  62. package/chunks/chunk-HEVBZSXG.js +4923 -0
  63. package/chunks/chunk-HFQ2XSE7.js +690 -0
  64. package/chunks/{chunk-PF523XJX.js → chunk-HPGTGMP3.js} +1 -1
  65. package/chunks/{chunk-OTBK43JR.js → chunk-HYCC3XM3.js} +1 -1
  66. package/chunks/{chunk-TMBPDHXD.js → chunk-I4H7PRWQ.js} +1 -1
  67. package/chunks/{chunk-EEEZIVFJ.js → chunk-JOLIREYR.js} +1 -1
  68. package/chunks/{chunk-TZM5JIAX.js → chunk-JYPQTRA3.js} +10 -7
  69. package/chunks/chunk-K5XU6E4T.js +167 -0
  70. package/chunks/{chunk-BS64PXY3.js → chunk-KFVU7ODN.js} +1 -1
  71. package/chunks/{chunk-3RJ3ZPPR.js → chunk-KKQWC6WS.js} +7 -7
  72. package/chunks/{chunk-3QFE4OTE.js → chunk-LBW7NW7X.js} +4 -4
  73. package/chunks/{chunk-Z7XXFPGP.js → chunk-LDWTYE33.js} +1 -1
  74. package/chunks/{chunk-EXMX5YHG.js → chunk-LP5E4YIH.js} +1 -1
  75. package/chunks/{chunk-ZUAXI2OA.js → chunk-M2U5NM24.js} +188 -11366
  76. package/chunks/{chunk-I5T6PIBQ.js → chunk-MBV2CTRL.js} +1010 -925
  77. package/chunks/{chunk-5JZLXFPD.js → chunk-MTHMXSNT.js} +11 -11
  78. package/chunks/{chunk-FPACXT7T.js → chunk-MTJVHQDP.js} +534 -164
  79. package/chunks/chunk-NDOC6M5S.js +481 -0
  80. package/chunks/{chunk-HGNSRCRB.js → chunk-NL4C3H6C.js} +1 -1
  81. package/chunks/{chunk-GZCJS5WH.js → chunk-O2VBQ3Y2.js} +2 -2
  82. package/chunks/{chunk-UN3C6GOR.js → chunk-PPFWEV7G.js} +10 -108
  83. package/chunks/{chunk-RS4HRC75.js → chunk-QI2CFVRF.js} +85 -8
  84. package/chunks/{chunk-XU5P23GN.js → chunk-QLDPRILK.js} +3 -3
  85. package/chunks/chunk-QYUE6W3T.js +123 -0
  86. package/chunks/{chunk-USGMPVOY.js → chunk-R3LHDWF2.js} +21 -12
  87. package/chunks/{chunk-BNXANYLH.js → chunk-RD6YJJJ6.js} +81 -1
  88. package/chunks/{chunk-PXFFKNCO.js → chunk-RIP623NO.js} +4877 -2469
  89. package/chunks/{chunk-XKBP7M7A.js → chunk-RYBSUGOR.js} +142 -30
  90. package/chunks/{chunk-BG4BIP7F.js → chunk-SBBN4BWR.js} +4 -2
  91. package/chunks/{chunk-T2VDEFUU.js → chunk-SKQZZVAV.js} +2 -2
  92. package/chunks/{chunk-5R4SBCX6.js → chunk-SL5MOGQH.js} +3 -3
  93. package/chunks/chunk-SMXULRAN.js +37 -0
  94. package/chunks/{chunk-WA5OJGFL.js → chunk-SS6WJMQK.js} +363 -76
  95. package/chunks/{chunk-UAARVVBI.js → chunk-TNWFHGN6.js} +1 -1
  96. package/chunks/{chunk-I2RFAW6Q.js → chunk-TT6Q3EN4.js} +7 -5
  97. package/chunks/chunk-UAKXW3DN.js +30 -0
  98. package/chunks/{chunk-C4K3FEQ2.js → chunk-UX4SGCGN.js} +32 -3
  99. package/chunks/{chunk-J6AW7O5D.js → chunk-WHDQZHRU.js} +2 -2
  100. package/chunks/{chunk-WGAEWQTF.js → chunk-WN6K47NR.js} +1 -2
  101. package/chunks/{chunk-UAARJR2G.js → chunk-WQT5SOI6.js} +2 -2
  102. package/chunks/{chunk-VYFMN7C6.js → chunk-WUMVIRTN.js} +225 -37
  103. package/chunks/{chunk-R36VMVEP.js → chunk-XJMKOTVV.js} +5 -5
  104. package/chunks/{chunk-TXVGVZYR.js → chunk-XJYUPHRC.js} +3 -3
  105. package/chunks/{chunk-2JD55C6G.js → chunk-Y6YMRS3U.js} +2 -2
  106. package/chunks/{chunk-LLFFKXZZ.js → chunk-YALKC62D.js} +56 -29
  107. package/chunks/{chunk-GYZQA2FO.js → chunk-YLZJCUH6.js} +1 -1
  108. package/chunks/chunk-YTFBURQD.js +313 -0
  109. package/chunks/{chunk-KBXLIVWQ.js → chunk-ZFPRYHWR.js} +2 -2
  110. package/chunks/{chunk-X6EDWN44.js → chunk-ZP6UKOPB.js} +1 -1
  111. package/chunks/{computer-use-6BBRNT6S.js → computer-use-LF2S6LUZ.js} +103 -49
  112. package/chunks/{contextCommand-UJAJ6N57.js → contextCommand-VPX5UG55.js} +26 -25
  113. package/chunks/{cron-create-4KQE6YLH.js → cron-create-SKEY55RG.js} +4 -4
  114. package/chunks/{cron-delete-THME66UP.js → cron-delete-I3NAT32Z.js} +6 -6
  115. package/chunks/{cron-list-7A54SJ5N.js → cron-list-6XRRL6TH.js} +5 -5
  116. package/chunks/{daemon-status-provider-CSL3BUOY.js → daemon-status-provider-5JAWZS4V.js} +34 -34
  117. package/chunks/{de-ACHALDED.js → de-EOW5PVHF.js} +1 -0
  118. package/chunks/{dist-BD27IA3L.js → dist-4FCNM6KJ.js} +2 -2
  119. package/chunks/{dist-OD2FY543.js → dist-O3H4PABL.js} +2 -2
  120. package/chunks/{dist-BOOH5JWN.js → dist-SCAAUWPS.js} +18 -4
  121. package/chunks/{dist-QHJRBCMP.js → dist-UKPWQJCL.js} +44 -2
  122. package/chunks/{dist-QFFZWACU.js → dist-VD7HXTML.js} +20 -9
  123. package/chunks/{earlyInputCapture-X6KMCSHD.js → earlyInputCapture-7FRQMU4P.js} +25 -24
  124. package/chunks/{edit-7J6WXARF.js → edit-REKJDIRL.js} +68 -27
  125. package/chunks/{en-NCUBOJOY.js → en-HPWEJBTR.js} +8 -0
  126. package/chunks/{enter-worktree-KULCKJPJ.js → enter-worktree-GBKUDCD3.js} +24 -23
  127. package/chunks/{enterPlanMode-H2CIOLMH.js → enterPlanMode-WB3Y4FGT.js} +24 -23
  128. package/chunks/{errors-6G2JYR54.js → errors-2XJDYO3M.js} +26 -25
  129. package/chunks/{exit-worktree-ECBWBVC5.js → exit-worktree-KKC7O2ZV.js} +24 -23
  130. package/chunks/{exitPlanMode-DQV56GTW.js → exitPlanMode-7HMS3TF7.js} +24 -23
  131. package/chunks/{fast-path-35JJFS3C.js → fast-path-XVNIYWGG.js} +4 -4
  132. package/chunks/{fast-path-settings-TSVI53EB.js → fast-path-settings-UQLRFDQP.js} +3 -2
  133. package/chunks/{fr-GVKZ27MF.js → fr-BN5NKSAC.js} +1 -0
  134. package/chunks/{gemini-NDTG7WAX.js → gemini-U5TPU3IQ.js} +210 -104
  135. package/chunks/{geminiContentGenerator-XTG6SFLA.js → geminiContentGenerator-ZVWRHXOO.js} +4 -4
  136. package/chunks/{glob-SJ2JBZK4.js → glob-FEJWIH2W.js} +24 -23
  137. package/chunks/{grep-BRMWFEWR.js → grep-KMP25IIA.js} +24 -23
  138. package/chunks/{headlessSafetyWarnings-LJ6FJX77.js → headlessSafetyWarnings-5DEDET4M.js} +24 -23
  139. package/chunks/{initializer-P5AXF7PV.js → initializer-FJTACLOH.js} +31 -28
  140. package/chunks/{ja-FKZ2WZ4D.js → ja-7DEUB2TS.js} +1 -0
  141. package/chunks/{keychain-token-storage-CSYMNRPP.js → keychain-token-storage-YLMDLKXT.js} +2 -2
  142. package/chunks/{list-6COK34BP.js → list-SCQXYJUW.js} +32 -29
  143. package/chunks/{loadedSettingsAdapter-XIUTN245.js → loadedSettingsAdapter-P6KPNAG7.js} +30 -27
  144. package/chunks/{loop-wakeup-33GPXJLA.js → loop-wakeup-FYEQEJUA.js} +7 -7
  145. package/chunks/{ls-BYQY7RVV.js → ls-R5WP4BGZ.js} +4 -4
  146. package/chunks/{lsp-X54RO4IV.js → lsp-GSICJMMW.js} +2 -2
  147. package/chunks/{monitor-DH7KFSJV.js → monitor-JU3HHHVY.js} +24 -23
  148. package/chunks/nonInteractiveCli-IZEKUHU3.js +100 -0
  149. package/chunks/{notebook-edit-D4R4ZLMA.js → notebook-edit-YHY3Z2QQ.js} +52 -23
  150. package/chunks/{openaiContentGenerator-F74IPQ6E.js → openaiContentGenerator-4WUVA3Q2.js} +13 -12
  151. package/chunks/{pt-SY5NCFGR.js → pt-TNZRNLQB.js} +1 -0
  152. package/chunks/{qwenContentGenerator-ATTK5P4D.js → qwenContentGenerator-MVYYR44B.js} +26 -25
  153. package/chunks/{qwenOAuth2-K4AASPCV.js → qwenOAuth2-6HNGHALT.js} +5 -4
  154. package/chunks/{read-file-767G23OY.js → read-file-NABEO2X4.js} +8 -8
  155. package/chunks/{read-mcp-resource-I5HCZJ6X.js → read-mcp-resource-DQQW7H7T.js} +2 -2
  156. package/chunks/{ripGrep-4OMMGSWX.js → ripGrep-YXLK6UTX.js} +24 -23
  157. package/chunks/{ru-FWNXJG4F.js → ru-NIAUBKCN.js} +1 -0
  158. package/chunks/{run-qwen-serve-REKSTYRA.js → run-qwen-serve-4B5QBMLQ.js} +144 -41
  159. package/chunks/{scheduler-UNTFDQLG.js → scheduler-XAKIYBDL.js} +24 -23
  160. package/chunks/{send-message-2DAFXEYP.js → send-message-EE4TWKVU.js} +2 -2
  161. package/chunks/{serve-KZ56QB5S.js → serve-4RXVLUKD.js} +30 -28
  162. package/chunks/{server-BV5TZUMU.js → server-7PA5QGIA.js} +11037 -8747
  163. package/chunks/{session-OIBXFSGD.js → session-WNVWRYAH.js} +194 -57
  164. package/chunks/{settings-WJTCBIRA.js → settings-YJH32TFF.js} +30 -27
  165. package/chunks/{shell-WC52IBHW.js → shell-YFLABOS5.js} +24 -23
  166. package/chunks/{skill-U7F7JR4M.js → skill-N3J7FUAY.js} +38 -11
  167. package/chunks/{spawnChannel-EI2E5BCW.js → spawnChannel-D6WVMFZJ.js} +26 -25
  168. package/chunks/{src-4F6FBZFI.js → src-IU5GDBEH.js} +88 -28
  169. package/chunks/{startInteractiveUI-SFRVB3CE.js → startInteractiveUI-QIPQTRT4.js} +2784 -2474
  170. package/chunks/{syntheticOutput-PPS3HDVA.js → syntheticOutput-T5YHQ6S2.js} +3 -3
  171. package/chunks/{task-create-OUYCA553.js → task-create-BLY7RHXX.js} +6 -6
  172. package/chunks/{task-list-WAX7I2IA.js → task-list-3QUYNVYR.js} +5 -5
  173. package/chunks/{task-stop-CAIL6FNH.js → task-stop-U4RATK2V.js} +2 -2
  174. package/chunks/{task-update-AHEO5OUT.js → task-update-U54O7KJX.js} +6 -6
  175. package/chunks/{team-create-3WC7CIIW.js → team-create-TP4CLLM3.js} +24 -23
  176. package/chunks/{team-delete-HGHFG23B.js → team-delete-LKBYPASD.js} +5 -5
  177. package/chunks/{theme-manager-DICFCT3P.js → theme-manager-N2TBIBZT.js} +25 -24
  178. package/chunks/{todoWrite-IFBMN3FH.js → todoWrite-OYXKTVYN.js} +4 -4
  179. package/chunks/{tool-search-L5K7HLER.js → tool-search-GBBSICYB.js} +101 -16
  180. package/chunks/trustedFolders-F7TBU5CD.js +76 -0
  181. package/chunks/types-QX5C3CHJ.js +12 -0
  182. package/chunks/{validateNonInterActiveAuth-GCH4Z6KI.js → validateNonInterActiveAuth-EQDQPSMI.js} +55 -50
  183. package/chunks/{web-fetch-B5IIQ5TT.js → web-fetch-PCVCML2X.js} +29 -15
  184. package/chunks/{workflow-OB3AND7I.js → workflow-ANJ4FYBK.js} +32 -30
  185. package/chunks/{workspace-providers-status-ZIR5EPOO.js → workspace-providers-status-462QQKQM.js} +32 -29
  186. package/chunks/workspace-service-VTG4G6CW.js +75 -0
  187. package/chunks/{write-file-SEXBNDZM.js → write-file-3P3WMQQW.js} +63 -27
  188. package/chunks/{zh-RBEGQVQT.js → zh-5EUYXASN.js} +8 -0
  189. package/chunks/{zh-TW-QGAVRXT3.js → zh-TW-TR3XYGE5.js} +8 -0
  190. package/cli-entry.js +20 -9
  191. package/cli.js +6 -6
  192. package/locales/ca.js +2 -0
  193. package/locales/de.js +2 -0
  194. package/locales/en.js +14 -0
  195. package/locales/fr.js +2 -0
  196. package/locales/ja.js +1 -0
  197. package/locales/pt.js +2 -0
  198. package/locales/ru.js +2 -0
  199. package/locales/zh-TW.js +12 -0
  200. package/locales/zh.js +12 -0
  201. package/node_modules/@qwen-code/audio-capture/dist/index.d.ts +35 -0
  202. package/node_modules/@qwen-code/audio-capture/dist/index.js +48 -0
  203. package/node_modules/@qwen-code/audio-capture/dist/index.js.map +1 -0
  204. package/node_modules/@qwen-code/audio-capture/dist/platform.d.ts +7 -0
  205. package/node_modules/@qwen-code/audio-capture/dist/platform.js +18 -0
  206. package/node_modules/@qwen-code/audio-capture/dist/platform.js.map +1 -0
  207. package/node_modules/@qwen-code/audio-capture/node_modules/node-gyp-build/LICENSE +21 -0
  208. package/node_modules/@qwen-code/audio-capture/node_modules/node-gyp-build/README.md +58 -0
  209. package/node_modules/@qwen-code/audio-capture/node_modules/node-gyp-build/SECURITY.md +5 -0
  210. package/node_modules/@qwen-code/audio-capture/node_modules/node-gyp-build/bin.js +84 -0
  211. package/node_modules/@qwen-code/audio-capture/node_modules/node-gyp-build/build-test.js +19 -0
  212. package/node_modules/@qwen-code/audio-capture/node_modules/node-gyp-build/index.js +6 -0
  213. package/node_modules/@qwen-code/audio-capture/node_modules/node-gyp-build/node-gyp-build.js +207 -0
  214. package/node_modules/@qwen-code/audio-capture/node_modules/node-gyp-build/optional.js +7 -0
  215. package/node_modules/@qwen-code/audio-capture/node_modules/node-gyp-build/package.json +43 -0
  216. package/node_modules/@qwen-code/audio-capture/package.json +30 -0
  217. package/node_modules/@qwen-code/audio-capture/prebuilds/darwin-arm64/@qwen-code+audio-capture.node +0 -0
  218. package/node_modules/@qwen-code/audio-capture/prebuilds/darwin-x64/@qwen-code+audio-capture.node +0 -0
  219. package/node_modules/@qwen-code/audio-capture/prebuilds/linux-arm64/@qwen-code+audio-capture.node +0 -0
  220. package/node_modules/@qwen-code/audio-capture/prebuilds/linux-x64/@qwen-code+audio-capture.node +0 -0
  221. package/node_modules/@qwen-code/audio-capture/prebuilds/win32-x64/@qwen-code+audio-capture.node +0 -0
  222. package/package.json +18 -6
  223. package/patches/chrome-devtools-mcp+1.4.0.patch +31 -0
  224. package/postinstall.js +50 -0
  225. package/web-shell/assets/{arc-D_ae_Y9k.js → arc-DIemsO_F.js} +1 -1
  226. package/web-shell/assets/{architectureDiagram-3BPJPVTR-BMLRjppa.js → architectureDiagram-3BPJPVTR-CTlUvIQv.js} +1 -1
  227. package/web-shell/assets/{blockDiagram-GPEHLZMM-qVDQC8KL.js → blockDiagram-GPEHLZMM-DG0jmIZK.js} +1 -1
  228. package/web-shell/assets/{c4Diagram-AAUBKEIU-CxWS6MBd.js → c4Diagram-AAUBKEIU-WljDBi55.js} +1 -1
  229. package/web-shell/assets/channel-CbzRbdpA.js +1 -0
  230. package/web-shell/assets/{chunk-2J33WTMH-BtNuY5Yp.js → chunk-2J33WTMH-8nc6onr2.js} +1 -1
  231. package/web-shell/assets/{chunk-4BX2VUAB-BU-x368d.js → chunk-4BX2VUAB-EopXix8Z.js} +1 -1
  232. package/web-shell/assets/{chunk-55IACEB6-DRgTLvBu.js → chunk-55IACEB6-BE4Jz3N3.js} +1 -1
  233. package/web-shell/assets/{chunk-727SXJPM-D2aElv7q.js → chunk-727SXJPM-C_eErgDN.js} +1 -1
  234. package/web-shell/assets/{chunk-AQP2D5EJ-BwHCMRQ0.js → chunk-AQP2D5EJ-DFtVW-Is.js} +1 -1
  235. package/web-shell/assets/{chunk-FMBD7UC4-BMyPH-ku.js → chunk-FMBD7UC4-CJPyxIg9.js} +1 -1
  236. package/web-shell/assets/{chunk-ND2GUHAM-BNKTstUf.js → chunk-ND2GUHAM-Bk-xgj6M.js} +1 -1
  237. package/web-shell/assets/{chunk-QZHKN3VN-gD3vsFId.js → chunk-QZHKN3VN-p3Y4yaFB.js} +1 -1
  238. package/web-shell/assets/classDiagram-4FO5ZUOK-BFdyQ52h.js +1 -0
  239. package/web-shell/assets/classDiagram-v2-Q7XG4LA2-BFdyQ52h.js +1 -0
  240. package/web-shell/assets/{cose-bilkent-S5V4N54A-BG9FAsXE.js → cose-bilkent-S5V4N54A-BCMO6Js3.js} +1 -1
  241. package/web-shell/assets/{dagre-BM42HDAG-Cy1sR7om.js → dagre-BM42HDAG-DUMrwASf.js} +1 -1
  242. package/web-shell/assets/default-aEtf0G7-.svg +1 -0
  243. package/web-shell/assets/{diagram-2AECGRRQ-Bxi1I_Vu.js → diagram-2AECGRRQ-C5d7qJbL.js} +1 -1
  244. package/web-shell/assets/{diagram-5GNKFQAL-BGCJjO6y.js → diagram-5GNKFQAL-CjLzMhbT.js} +1 -1
  245. package/web-shell/assets/{diagram-KO2AKTUF-CMTeLXp7.js → diagram-KO2AKTUF-RCoNWeAm.js} +1 -1
  246. package/web-shell/assets/{diagram-LMA3HP47-BN1wyAGh.js → diagram-LMA3HP47-KThXjaNV.js} +1 -1
  247. package/web-shell/assets/{diagram-OG6HWLK6-UYSG_zcp.js → diagram-OG6HWLK6-DKsC2BWz.js} +1 -1
  248. package/web-shell/assets/{erDiagram-TEJ5UH35-DqXL08lw.js → erDiagram-TEJ5UH35-DJ2ukmq0.js} +1 -1
  249. package/web-shell/assets/{flowDiagram-I6XJVG4X-BPJ9SFji.js → flowDiagram-I6XJVG4X-zzqaARRX.js} +1 -1
  250. package/web-shell/assets/{ganttDiagram-6RSMTGT7-BEd9Pnms.js → ganttDiagram-6RSMTGT7-kFUg-gqT.js} +1 -1
  251. package/web-shell/assets/{gitGraphDiagram-PVQCEYII-DrNRH3oi.js → gitGraphDiagram-PVQCEYII-C6JnMVzD.js} +1 -1
  252. package/web-shell/assets/index-B3XUm7dw.js +713 -0
  253. package/web-shell/assets/index-CHmzAWys.css +5 -0
  254. package/web-shell/assets/{infoDiagram-5YYISTIA-CFYBrkVI.js → infoDiagram-5YYISTIA-XqPVhp89.js} +1 -1
  255. package/web-shell/assets/insert-DmRbh9Rv.svg +1 -0
  256. package/web-shell/assets/{ishikawaDiagram-YF4QCWOH-Dxsddth7.js → ishikawaDiagram-YF4QCWOH-D2k8sGpf.js} +1 -1
  257. package/web-shell/assets/{journeyDiagram-JHISSGLW-DxaDK80t.js → journeyDiagram-JHISSGLW-CQEST1GE.js} +1 -1
  258. package/web-shell/assets/{kanban-definition-UN3LZRKU-Q0jQCOct.js → kanban-definition-UN3LZRKU-DZ7kqVBC.js} +1 -1
  259. package/web-shell/assets/{linear-DGGM2kcP.js → linear-B6C_6q_E.js} +1 -1
  260. package/web-shell/assets/{mermaid.core-ByoxnWut.js → mermaid.core-CsyNP1qt.js} +5 -5
  261. package/web-shell/assets/{mindmap-definition-RKZ34NQL-C0ZseHsi.js → mindmap-definition-RKZ34NQL-B1VDIzJq.js} +1 -1
  262. package/web-shell/assets/{pieDiagram-4H26LBE5-MQXNhl5A.js → pieDiagram-4H26LBE5-DN0E7Jwu.js} +1 -1
  263. package/web-shell/assets/{quadrantDiagram-W4KKPZXB-DIR4ee2A.js → quadrantDiagram-W4KKPZXB-UKTQdHKm.js} +1 -1
  264. package/web-shell/assets/queue-DWpnpFgW.svg +1 -0
  265. package/web-shell/assets/{requirementDiagram-4Y6WPE33-BTR0UdYQ.js → requirementDiagram-4Y6WPE33-BRGceQvN.js} +1 -1
  266. package/web-shell/assets/{sankeyDiagram-5OEKKPKP-sfnhMELN.js → sankeyDiagram-5OEKKPKP-2DDyu41v.js} +1 -1
  267. package/web-shell/assets/{sequenceDiagram-3UESZ5HK-DXczHOHH.js → sequenceDiagram-3UESZ5HK-CT5iE8ZU.js} +1 -1
  268. package/web-shell/assets/{stateDiagram-AJRCARHV-DdE_KQFH.js → stateDiagram-AJRCARHV-CqMQ_s4G.js} +1 -1
  269. package/web-shell/assets/stateDiagram-v2-BHNVJYJU-CamhV5pM.js +1 -0
  270. package/web-shell/assets/{timeline-definition-PNZ67QCA-DIc61uE8.js → timeline-definition-PNZ67QCA-Di37QSZU.js} +1 -1
  271. package/web-shell/assets/{vennDiagram-CIIHVFJN-BF7ODmxB.js → vennDiagram-CIIHVFJN-DCXvpz5S.js} +1 -1
  272. package/web-shell/assets/{wardley-L42UT6IY-3pc1MEaw.js → wardley-L42UT6IY-CpUCk6OX.js} +1 -1
  273. package/web-shell/assets/{wardleyDiagram-YWT4CUSO-9jnPcZuP.js → wardleyDiagram-YWT4CUSO-D01pVEBg.js} +1 -1
  274. package/web-shell/assets/{xychartDiagram-2RQKCTM6-DcthaZK6.js → xychartDiagram-2RQKCTM6-DOu5JDoz.js} +1 -1
  275. package/web-shell/index.html +2 -2
  276. package/chunks/chunk-B6HQGTYR.js +0 -410
  277. package/chunks/chunk-R2BXK7SX.js +0 -13
  278. package/chunks/chunk-RKIZXJMB.js +0 -13
  279. package/chunks/nonInteractiveCli-7575LBXP.js +0 -91
  280. package/chunks/workspace-service-OM5D4OI2.js +0 -60
  281. package/web-shell/assets/channel-DYPNiTww.js +0 -1
  282. package/web-shell/assets/classDiagram-4FO5ZUOK-p_g25Tni.js +0 -1
  283. package/web-shell/assets/classDiagram-v2-Q7XG4LA2-p_g25Tni.js +0 -1
  284. package/web-shell/assets/index-CpOXWDPh.js +0 -714
  285. package/web-shell/assets/index-oVK7qhre.css +0 -5
  286. package/web-shell/assets/stateDiagram-v2-BHNVJYJU-BHUSEndU.js +0 -1
@@ -0,0 +1,665 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Qwen
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import * as fs from 'node:fs/promises';
8
+ import * as os from 'node:os';
9
+ import * as path from 'node:path';
10
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
11
+ import {
12
+ LOOP_SENTINEL_CRON,
13
+ LOOP_SENTINEL_DYNAMIC,
14
+ LoopTickResolver,
15
+ detectLoopSentinel,
16
+ } from './loop-tick-resolver.js';
17
+ import { LOOP_TASK_FILE_MAX_BYTES } from './loop-task-file.js';
18
+
19
+ // Make only realpath observable; every other fs call stays real so the temp-dir
20
+ // fixtures keep working. The default impl calls through, so behavior is unchanged
21
+ // — the spy just lets a test count how often a boundary is re-resolved.
22
+ vi.mock('node:fs/promises', async (importActual) => {
23
+ const actual = await importActual<typeof import('node:fs/promises')>();
24
+ return { ...actual, realpath: vi.fn(actual.realpath) };
25
+ });
26
+
27
+ describe('detectLoopSentinel', () => {
28
+ it('recognizes the cron and dynamic sentinels exactly (after trim)', () => {
29
+ expect(detectLoopSentinel(LOOP_SENTINEL_CRON)).toBe('cron');
30
+ expect(detectLoopSentinel(LOOP_SENTINEL_DYNAMIC)).toBe('dynamic');
31
+ expect(detectLoopSentinel(` ${LOOP_SENTINEL_DYNAMIC}\n`)).toBe('dynamic');
32
+ });
33
+
34
+ it('returns null for non-sentinel prompts', () => {
35
+ expect(detectLoopSentinel('/loop check the deploy')).toBeNull();
36
+ expect(detectLoopSentinel('<<loop.md>> and more')).toBeNull();
37
+ expect(detectLoopSentinel('')).toBeNull();
38
+ });
39
+ });
40
+
41
+ describe('LoopTickResolver', () => {
42
+ let tempDir: string;
43
+ let projectRoot: string;
44
+ let homeDir: string;
45
+ let resolver: LoopTickResolver;
46
+
47
+ const projectFile = () => path.join(projectRoot, '.qwen', 'loop.md');
48
+ const homeFile = () => path.join(homeDir, '.qwen', 'loop.md');
49
+ const writeProject = (content: string) =>
50
+ fs
51
+ .mkdir(path.join(projectRoot, '.qwen'), { recursive: true })
52
+ .then(() => fs.writeFile(projectFile(), content));
53
+ const writeHome = (content: string) =>
54
+ fs
55
+ .mkdir(path.join(homeDir, '.qwen'), { recursive: true })
56
+ .then(() => fs.writeFile(homeFile(), content));
57
+
58
+ beforeEach(async () => {
59
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'loop-tick-'));
60
+ projectRoot = path.join(tempDir, 'project');
61
+ homeDir = path.join(tempDir, 'home');
62
+ await fs.mkdir(projectRoot, { recursive: true });
63
+ await fs.mkdir(homeDir, { recursive: true });
64
+ resolver = new LoopTickResolver({
65
+ projectRoot,
66
+ homeDir,
67
+ allowProjectFile: () => true,
68
+ });
69
+ });
70
+
71
+ afterEach(async () => {
72
+ // Reset realpath call history (keep the call-through impl) between tests.
73
+ vi.mocked(fs.realpath).mockClear();
74
+ await fs.rm(tempDir, { recursive: true, force: true });
75
+ });
76
+
77
+ it('ignores the project loop.md in an untrusted folder (allowProjectFile: false)', async () => {
78
+ // An untrusted folder's repo-controlled project loop.md must not be read,
79
+ // but the user-owned home loop.md still is.
80
+ await writeProject('- repo-controlled tasks');
81
+ await writeHome('- user tasks');
82
+ const untrusted = new LoopTickResolver({
83
+ projectRoot,
84
+ homeDir,
85
+ allowProjectFile: () => false,
86
+ });
87
+
88
+ const tick = await untrusted.resolve('cron');
89
+
90
+ expect(tick.full).toBe(true);
91
+ expect(tick.sourceLabel).toBe('home loop.md');
92
+ expect(tick.modelText).toContain('- user tasks');
93
+ expect(tick.modelText).not.toContain('- repo-controlled tasks');
94
+ });
95
+
96
+ it('treats a present project loop.md as absent when the folder is untrusted', async () => {
97
+ await writeProject('- repo-controlled tasks');
98
+ const untrusted = new LoopTickResolver({
99
+ projectRoot,
100
+ homeDir,
101
+ allowProjectFile: () => false,
102
+ });
103
+
104
+ const tick = await untrusted.resolve('cron');
105
+
106
+ expect(tick.full).toBe(false);
107
+ expect(tick.sourceLabel).toBeUndefined();
108
+ // A genuinely-absent tick is NOT flagged transient, so the echo says "not
109
+ // present" rather than "temporarily unavailable".
110
+ expect(tick.transientError).toBe(false);
111
+ expect(tick.modelText).toContain('loop.md is not currently present');
112
+ // The project candidate was never read (untrusted), so the absent message
113
+ // must not claim it was checked — only the home candidate is named, via a
114
+ // leak-safe label (this fixture's homeDir is a temp dir outside the real
115
+ // $HOME, so the absolute path must never reach the model text).
116
+ expect(tick.modelText).not.toContain('(project)');
117
+ expect(tick.modelText).toContain('(home)');
118
+ expect(tick.modelText).not.toContain(homeFile());
119
+ });
120
+
121
+ it('re-reads folder trust per tick: a trusted→untrusted flip stops reading the project file', async () => {
122
+ // allowProjectFile is a getter, not a snapshot: isTrustedFolder() can flip
123
+ // mid-session (IDE workspace-trust update) and the resolver outlives a tick.
124
+ // A resolver built while trusted must skip the repo-controlled project
125
+ // loop.md on the very next tick once trust flips — not keep reading it.
126
+ await writeProject('- repo-controlled tasks');
127
+ let trusted = true;
128
+ const flipping = new LoopTickResolver({
129
+ projectRoot,
130
+ homeDir,
131
+ allowProjectFile: () => trusted,
132
+ });
133
+
134
+ const trustedTick = await flipping.resolve('cron');
135
+ expect(trustedTick.full).toBe(true);
136
+ expect(trustedTick.sourceLabel).toBe('project loop.md');
137
+ expect(trustedTick.modelText).toContain('- repo-controlled tasks');
138
+ flipping.markDelivered();
139
+
140
+ // Trust revoked. With no user-owned home loop.md, the next tick must be a
141
+ // labelled no-op — the project file is no longer read by the SAME resolver.
142
+ trusted = false;
143
+ const untrustedTick = await flipping.resolve('cron');
144
+ expect(untrustedTick.full).toBe(false);
145
+ expect(untrustedTick.sourceLabel).toBeUndefined();
146
+ expect(untrustedTick.modelText).toContain(
147
+ 'loop.md is not currently present',
148
+ );
149
+ expect(untrustedTick.modelText).not.toContain('- repo-controlled tasks');
150
+ // Trust is revoked, so the project file was not read — don't claim it.
151
+ expect(untrustedTick.modelText).not.toContain('(project)');
152
+ });
153
+
154
+ it('delivers the full task block on first fire', async () => {
155
+ await writeProject('- ship the thing');
156
+
157
+ const tick = await resolver.resolve('dynamic');
158
+
159
+ expect(tick.full).toBe(true);
160
+ // sourceLabel is the relative label, never the absolute path — the model
161
+ // text (and this label) must not leak projectFile().
162
+ expect(tick.sourceLabel).toBe('project loop.md');
163
+ expect(tick.modelText).toContain(
164
+ '# /loop tick — loop.md tasks from project loop.md',
165
+ );
166
+ expect(tick.modelText).not.toContain(projectFile());
167
+ expect(tick.modelText).toContain('The user configured a loop-tasks file.');
168
+ expect(tick.modelText).toContain('- ship the thing');
169
+ // The full block carries the mode-specific pacing suffix (dynamic re-arm)...
170
+ expect(tick.modelText).toContain('(dynamic pacing)');
171
+ expect(tick.modelText).toContain('call LoopWakeup again');
172
+ // ...but NOT the "established earlier" reminder: the block is right here in
173
+ // this message, so that phrasing would contradict the INTRO above it.
174
+ expect(tick.modelText).not.toContain('established earlier');
175
+ // Exactly one H1 in the whole message (no duplicated tick heading).
176
+ expect(tick.modelText.match(/^# /gm)).toHaveLength(1);
177
+ });
178
+
179
+ it('delivers only the short reminder when content is unchanged', async () => {
180
+ await writeProject('- ship the thing');
181
+ await resolver.resolve('dynamic');
182
+ resolver.markDelivered();
183
+
184
+ const tick = await resolver.resolve('dynamic');
185
+
186
+ expect(tick.full).toBe(false);
187
+ // The unchanged branch still reports the resolved source so Session.ts can
188
+ // label it even when only the short reminder is sent.
189
+ expect(tick.sourceLabel).toBe('project loop.md');
190
+ expect(tick.modelText).not.toContain(
191
+ 'The user configured a loop-tasks file.',
192
+ );
193
+ // A subsequent tick DOES point back to the earlier full block — that
194
+ // reminder semantics is intact (only the first delivery omits it).
195
+ expect(tick.modelText).toContain('established earlier');
196
+ expect(tick.modelText).toContain(
197
+ '# /loop tick — loop.md tasks (dynamic pacing)',
198
+ );
199
+ });
200
+
201
+ it('commits content only on markDelivered, so an undelivered tick re-expands', async () => {
202
+ await writeProject('- tasks');
203
+ expect((await resolver.resolve('dynamic')).full).toBe(true);
204
+
205
+ // No markDelivered() — the block was never delivered (e.g. the tick was
206
+ // aborted before the send). The next tick must re-deliver the full block.
207
+ expect((await resolver.resolve('dynamic')).full).toBe(true);
208
+
209
+ resolver.markDelivered();
210
+ expect((await resolver.resolve('dynamic')).full).toBe(false);
211
+ });
212
+
213
+ it('re-delivers the full NEW block when an undelivered tick is followed by an edit', async () => {
214
+ // First tick resolved but ABORTED before delivery (no markDelivered), then the
215
+ // file is edited. Delivered content (#lastContent) is still null, so the second
216
+ // resolve must emit the FULL block with the NEW content — this is the
217
+ // #pendingContent-vs-#lastContent divergence path. If #pendingContent were
218
+ // committed eagerly on resolve(), the first tick would collapse to a short
219
+ // reminder (full=false), pointing the model at a block it never received.
220
+ await writeProject('- v1 tasks');
221
+ expect((await resolver.resolve('dynamic')).full).toBe(true);
222
+ // No markDelivered() — the first tick never reached the model.
223
+
224
+ await writeProject('- v2 edited tasks');
225
+ const tick = await resolver.resolve('dynamic');
226
+
227
+ expect(tick.full).toBe(true);
228
+ expect(tick.modelText).toContain('The user configured a loop-tasks file.');
229
+ expect(tick.modelText).toContain('- v2 edited tasks');
230
+ });
231
+
232
+ it('re-delivers the full block when loop.md is edited', async () => {
233
+ await writeProject('- v1');
234
+ await resolver.resolve('dynamic');
235
+ resolver.markDelivered();
236
+
237
+ await writeProject('- v2 edited');
238
+ const tick = await resolver.resolve('dynamic');
239
+
240
+ expect(tick.full).toBe(true);
241
+ expect(tick.modelText).toContain('- v2 edited');
242
+ });
243
+
244
+ it('re-delivers the full block after resetCache (compaction)', async () => {
245
+ await writeProject('- stable');
246
+ await resolver.resolve('dynamic');
247
+ resolver.markDelivered();
248
+ expect((await resolver.resolve('dynamic')).full).toBe(false);
249
+
250
+ resolver.resetCache();
251
+ const tick = await resolver.resolve('dynamic');
252
+
253
+ expect(tick.full).toBe(true);
254
+ expect(tick.modelText).toContain('- stable');
255
+ });
256
+
257
+ it('clears the boundary realpath cache on resetCache so it is re-resolved', async () => {
258
+ // The fs.realpath of the confinement boundary (projectRoot) is cached per
259
+ // resolver for the per-tick perf win. resetCache must invalidate it too —
260
+ // otherwise a long-lived process keeps a stale boundary after a /cd or symlink
261
+ // re-point. Prove projectRoot is re-resolved only after a reset.
262
+ await writeProject('- tasks');
263
+ const rootResolves = () =>
264
+ vi
265
+ .mocked(fs.realpath)
266
+ .mock.calls.filter((c) => String(c[0]) === projectRoot).length;
267
+
268
+ await resolver.resolve('cron');
269
+ expect(rootResolves()).toBe(1);
270
+ await resolver.resolve('cron');
271
+ expect(rootResolves()).toBe(1); // served from the instance cache, not re-resolved
272
+
273
+ resolver.resetCache();
274
+ await resolver.resolve('cron');
275
+ expect(rootResolves()).toBe(2); // cache cleared → boundary re-resolved
276
+ });
277
+
278
+ it('emits the absent reminder without poisoning the cache, then re-expands on recreate', async () => {
279
+ const absent = await resolver.resolve('dynamic');
280
+ expect(absent.full).toBe(false);
281
+ expect(absent.sourceLabel).toBeUndefined();
282
+ expect(absent.modelText).toContain('loop.md is not currently present');
283
+
284
+ await writeProject('- recreated tasks');
285
+ const tick = await resolver.resolve('dynamic');
286
+
287
+ expect(tick.full).toBe(true);
288
+ expect(tick.modelText).toContain('- recreated tasks');
289
+ });
290
+
291
+ it('gives the absent tick the same shared heading style (and dynamic suffix)', async () => {
292
+ const cron = await resolver.resolve('cron');
293
+ expect(cron.modelText).toContain('# /loop tick — loop.md absent\n');
294
+
295
+ const dyn = new LoopTickResolver({
296
+ projectRoot,
297
+ homeDir,
298
+ allowProjectFile: () => true,
299
+ });
300
+ const dynTick = await dyn.resolve('dynamic');
301
+ expect(dynTick.modelText).toContain(
302
+ '# /loop tick — loop.md absent (dynamic pacing)\n',
303
+ );
304
+ // The absent dynamic tail names the re-arm sentinel by interpolating the
305
+ // constant — asserting against LOOP_SENTINEL_DYNAMIC catches a future rename
306
+ // drift between the constant and the user-facing instruction.
307
+ expect(dynTick.modelText).toContain(LOOP_SENTINEL_DYNAMIC);
308
+ // Exactly one H1 — the heading isn't duplicated by the body.
309
+ expect(dynTick.modelText.match(/^# /gm)).toHaveLength(1);
310
+ });
311
+
312
+ it('resolve() honors an explicit allowProjectFile override over the getter', async () => {
313
+ // FIX 3: the caller captures folder-trust ONCE per tick and threads it in,
314
+ // so the per-tick getter is bypassed. Build a resolver whose getter would
315
+ // ALLOW the project file, but pass `false`: the repo-controlled project
316
+ // loop.md must be skipped on this tick, exactly as the getter-false path.
317
+ await writeProject('- repo-controlled tasks');
318
+ const threaded = new LoopTickResolver({
319
+ projectRoot,
320
+ homeDir,
321
+ allowProjectFile: () => true, // getter would allow...
322
+ });
323
+
324
+ const tick = await threaded.resolve('cron', false); // ...override forbids
325
+
326
+ expect(tick.full).toBe(false);
327
+ expect(tick.modelText).not.toContain('- repo-controlled tasks');
328
+ expect(tick.modelText).not.toContain('(project)');
329
+ expect(tick.modelText).toContain('(home)');
330
+ });
331
+
332
+ it('buildTransientErrorTick mirrors the absent tick with a re-arm and errno note', () => {
333
+ // FIX 4: a transient, non-whitelisted read error must NOT kill a dynamic
334
+ // loop. The degraded tick mirrors the absent path's re-arm + cache-clear, plus
335
+ // a note that the file was unreadable this tick, so the model still re-arms
336
+ // LoopWakeup and the loop survives.
337
+ const tick = resolver.buildTransientErrorTick('dynamic', true, 'EIO');
338
+
339
+ expect(tick.full).toBe(false);
340
+ // Flagged transient (file present, unreadable this tick) so the caller's echo
341
+ // can say "temporarily unavailable" rather than the genuinely-absent label.
342
+ expect(tick.transientError).toBe(true);
343
+ // The heading says "unavailable", NOT "absent"/"not present": the file exists,
344
+ // it just couldn't be read this tick, so the heading must mirror the body.
345
+ // Mutation guard: revert the heading to { absent: true } and these fail.
346
+ expect(tick.modelText).toContain(
347
+ '# /loop tick — loop.md unavailable (dynamic pacing)\n',
348
+ );
349
+ expect(tick.modelText).not.toContain('absent');
350
+ expect(tick.modelText).not.toContain('not present');
351
+ expect(tick.modelText).toContain('could not be read this tick (EIO)');
352
+ // The dynamic re-arm instruction (the literal sentinel) keeps the loop alive.
353
+ expect(tick.modelText).toContain(LOOP_SENTINEL_DYNAMIC);
354
+ // projectChecked=true names BOTH candidates (the set that was probed).
355
+ expect(tick.modelText).toContain('(project)');
356
+ expect(tick.modelText).toContain('(home)');
357
+ });
358
+
359
+ it('cron buildTransientErrorTick uses the cron tail and omits an unprobed project', () => {
360
+ // cron mode degrades only via its own next interval, but the tick text still
361
+ // uses the cron no-op tail (no LoopWakeup re-arm). With projectChecked=false
362
+ // (untrusted) the never-probed project candidate must NOT be named.
363
+ const tick = resolver.buildTransientErrorTick('cron', false, 'EACCES');
364
+
365
+ // Heading conveys "unavailable" (file exists, unreadable this tick), never
366
+ // the misleading "absent".
367
+ expect(tick.modelText).toContain('# /loop tick — loop.md unavailable');
368
+ expect(tick.modelText).not.toContain('absent');
369
+ expect(tick.modelText).toContain('could not be read this tick (EACCES)');
370
+ expect(tick.modelText).toContain('the recurring cron fires the next tick');
371
+ expect(tick.modelText).not.toContain(LOOP_SENTINEL_DYNAMIC);
372
+ expect(tick.modelText).not.toContain('(project)');
373
+ expect(tick.modelText).toContain('(home)');
374
+ });
375
+
376
+ it('a transient-error tick clears the change-detection cache so the next read re-delivers full', async () => {
377
+ // The degraded tick must behave like absent for caching: after it, a read of
378
+ // byte-identical content re-expands the FULL block rather than a dangling
379
+ // short reminder pointing at a block no longer guaranteed to be in context.
380
+ // Mutation guard: if buildTransientErrorTick doesn't clear the caches, the
381
+ // second resolve sees "unchanged" and returns a short reminder (full:false).
382
+ await writeProject('- tasks');
383
+ const full = await resolver.resolve('dynamic');
384
+ expect(full.full).toBe(true);
385
+ resolver.markDelivered();
386
+
387
+ resolver.buildTransientErrorTick('dynamic', true, 'EIO');
388
+
389
+ const next = await resolver.resolve('dynamic');
390
+ expect(next.full).toBe(true);
391
+ });
392
+
393
+ it('names the real home loop.md in the absent reminder (QWEN_HOME-aware, not a hardcoded ~/.qwen)', async () => {
394
+ // Regression: the absent body hardcoded `~/.qwen/loop.md (home)`, which is
395
+ // wrong once the global dir is relocated (QWEN_HOME). The resolver checks
396
+ // `<homeQwenDir>/loop.md`, but the label is MODEL-FACING, so a $QWEN_HOME
397
+ // outside $HOME (tildeifyPath no-op there) must read as the literal
398
+ // `$QWEN_HOME/loop.md`, never the raw absolute path it would otherwise leak.
399
+ const relocated = path.join(tempDir, 'relocated-qwen');
400
+ const prevQwenHome = process.env['QWEN_HOME'];
401
+ process.env['QWEN_HOME'] = relocated;
402
+ try {
403
+ const relocatedTick = await new LoopTickResolver({
404
+ projectRoot,
405
+ homeDir: relocated,
406
+ homeQwenDir: relocated,
407
+ allowProjectFile: () => true,
408
+ }).resolve('cron');
409
+
410
+ expect(relocatedTick.full).toBe(false);
411
+ expect(relocatedTick.modelText).toContain(
412
+ 'loop.md is not currently present',
413
+ );
414
+ expect(relocatedTick.modelText).toContain('$QWEN_HOME/loop.md (home)');
415
+ // The old hardcoded home location is gone; the project label stays relative.
416
+ expect(relocatedTick.modelText).not.toContain('~/.qwen/loop.md');
417
+ expect(relocatedTick.modelText).toContain('.qwen/loop.md (project)');
418
+ // Privacy: the raw absolute global dir never reaches the model text.
419
+ expect(relocatedTick.modelText).not.toContain(relocated);
420
+ } finally {
421
+ if (prevQwenHome === undefined) delete process.env['QWEN_HOME'];
422
+ else process.env['QWEN_HOME'] = prevQwenHome;
423
+ }
424
+
425
+ // Under the real OS home (the QWEN_HOME-unset case) the home prefix tilde-
426
+ // abbreviates, so the message reads `~/…/loop.md`, never the absolute $HOME.
427
+ const underHome = path.join(
428
+ os.homedir(),
429
+ `.qwen-loop-absent-${process.pid}`,
430
+ );
431
+ const homeTick = await new LoopTickResolver({
432
+ projectRoot,
433
+ homeDir: os.homedir(),
434
+ homeQwenDir: underHome,
435
+ allowProjectFile: () => true,
436
+ }).resolve('dynamic');
437
+
438
+ expect(homeTick.modelText).toContain(
439
+ `~/${path.basename(underHome)}/loop.md (home)`,
440
+ );
441
+ expect(homeTick.modelText).not.toContain(os.homedir());
442
+ });
443
+
444
+ it('homeLoopLabel never leaks an absolute $QWEN_HOME path outside $HOME (privacy)', async () => {
445
+ // The label is sent to the model/API. $QWEN_HOME may point OUTSIDE $HOME
446
+ // (supported relocation; common in containers/CI), where tildeifyPath is a
447
+ // no-op — so the resolved absolute dir must be swapped for the literal
448
+ // `$QWEN_HOME`. Mutation guard: revert homeLoopLabel to
449
+ // `tildeifyPath(join(homeQwenDir,'loop.md'))` and `outside` (the absolute
450
+ // path) reappears in BOTH assertions below, failing this test.
451
+ const outside = path.join(tempDir, 'srv-qwen-home');
452
+ const prevQwenHome = process.env['QWEN_HOME'];
453
+ process.env['QWEN_HOME'] = outside;
454
+ try {
455
+ const relocated = new LoopTickResolver({
456
+ projectRoot,
457
+ homeDir: outside,
458
+ homeQwenDir: outside,
459
+ allowProjectFile: () => true,
460
+ });
461
+ expect(relocated.homeLoopLabel()).toBe('$QWEN_HOME/loop.md');
462
+ const tick = await relocated.resolve('cron');
463
+ expect(tick.modelText).toContain('$QWEN_HOME/loop.md (home)');
464
+ expect(tick.modelText).not.toContain(outside);
465
+
466
+ // Defensive case: an out-of-$HOME global dir with $QWEN_HOME UNSET still
467
+ // never surfaces the absolute path — a generic placeholder is used.
468
+ delete process.env['QWEN_HOME'];
469
+ const generic = new LoopTickResolver({
470
+ projectRoot,
471
+ homeDir: outside,
472
+ homeQwenDir: outside,
473
+ allowProjectFile: () => true,
474
+ });
475
+ expect(generic.homeLoopLabel()).toBe('the configured global loop.md');
476
+ expect(generic.homeLoopLabel()).not.toContain(outside);
477
+ } finally {
478
+ if (prevQwenHome === undefined) delete process.env['QWEN_HOME'];
479
+ else process.env['QWEN_HOME'] = prevQwenHome;
480
+ }
481
+ });
482
+
483
+ it('homeLoopLabel keeps the separator when $QWEN_HOME has a trailing slash', async () => {
484
+ // Storage.getGlobalQwenDir() does NOT strip a trailing slash, so a
485
+ // `QWEN_HOME=/srv/qwen/` reaches homeQwenDir as `/srv/qwen/`. Slicing the
486
+ // joined loop.md path by the raw homeQwenDir length over-counts the trailing
487
+ // separator and garbles the label into `$QWEN_HOMEloop.md`. Mutation guard:
488
+ // revert the slice base to `homeQwenDir.length` and the first assertion below
489
+ // fails with the separator-less `$QWEN_HOMEloop.md`.
490
+ const outsideTrailing = path.join(tempDir, 'srv-qwen-home') + path.sep;
491
+ const prevQwenHome = process.env['QWEN_HOME'];
492
+ process.env['QWEN_HOME'] = outsideTrailing;
493
+ try {
494
+ const trailing = new LoopTickResolver({
495
+ projectRoot,
496
+ homeDir: outsideTrailing,
497
+ homeQwenDir: outsideTrailing,
498
+ allowProjectFile: () => true,
499
+ });
500
+ expect(trailing.homeLoopLabel()).toBe('$QWEN_HOME/loop.md');
501
+ // Never the raw absolute dir, and never the garbled separator-less form.
502
+ expect(trailing.homeLoopLabel()).not.toContain(outsideTrailing);
503
+ expect(trailing.homeLoopLabel()).not.toContain('$QWEN_HOMEloop.md');
504
+
505
+ // out-of-$HOME branch still behaves with QWEN_HOME UNSET: generic placeholder.
506
+ delete process.env['QWEN_HOME'];
507
+ const generic = new LoopTickResolver({
508
+ projectRoot,
509
+ homeDir: outsideTrailing,
510
+ homeQwenDir: outsideTrailing,
511
+ allowProjectFile: () => true,
512
+ });
513
+ expect(generic.homeLoopLabel()).toBe('the configured global loop.md');
514
+ } finally {
515
+ if (prevQwenHome === undefined) delete process.env['QWEN_HOME'];
516
+ else process.env['QWEN_HOME'] = prevQwenHome;
517
+ }
518
+
519
+ // under-$HOME branch still behaves with a trailing slash: tilde-abbreviated.
520
+ const underHomeTrailing =
521
+ path.join(os.homedir(), `.qwen-loop-trailing-${process.pid}`) + path.sep;
522
+ const underHome = new LoopTickResolver({
523
+ projectRoot,
524
+ homeDir: os.homedir(),
525
+ homeQwenDir: underHomeTrailing,
526
+ allowProjectFile: () => true,
527
+ });
528
+ expect(underHome.homeLoopLabel()).toBe(
529
+ `~/.qwen-loop-trailing-${process.pid}/loop.md`,
530
+ );
531
+ expect(underHome.homeLoopLabel()).not.toContain(os.homedir());
532
+ });
533
+
534
+ it('homeLoopLabel keeps the separator when $QWEN_HOME is the filesystem root', async () => {
535
+ // `QWEN_HOME=/` makes homeQwenDir the root, so homeLoopPath is
536
+ // path.join('/', 'loop.md') = '/loop.md', whose path.dirname is '/' (length 1).
537
+ // Slicing the joined path past that length drops the leading separator,
538
+ // garbling the label into the separator-less `$QWEN_HOMEloop.md`. Mutation
539
+ // guard: revert homeLoopLabel to the slice-by-dirname-length approach and the
540
+ // first assertion below fails with `$QWEN_HOMEloop.md`.
541
+ const root = path.sep; // the filesystem root ('/' on POSIX)
542
+ const prevQwenHome = process.env['QWEN_HOME'];
543
+ process.env['QWEN_HOME'] = root;
544
+ try {
545
+ const atRoot = new LoopTickResolver({
546
+ projectRoot,
547
+ homeDir: root,
548
+ homeQwenDir: root,
549
+ allowProjectFile: () => true,
550
+ });
551
+ expect(atRoot.homeLoopLabel()).toBe(`$QWEN_HOME${path.sep}loop.md`);
552
+ // The garbled, separator-less form must never appear.
553
+ expect(atRoot.homeLoopLabel()).not.toContain('$QWEN_HOMEloop.md');
554
+ } finally {
555
+ if (prevQwenHome === undefined) delete process.env['QWEN_HOME'];
556
+ else process.env['QWEN_HOME'] = prevQwenHome;
557
+ }
558
+ });
559
+
560
+ it('re-expands after delete→recreate even when the recreated content is identical', async () => {
561
+ await writeProject('- same tasks');
562
+ expect((await resolver.resolve('dynamic')).full).toBe(true);
563
+ resolver.markDelivered();
564
+ // Unchanged content → short reminder, as expected.
565
+ expect((await resolver.resolve('dynamic')).full).toBe(false);
566
+
567
+ // Delete → the absent tick clears the delivered-content memory.
568
+ await fs.rm(projectFile());
569
+ const absent = await resolver.resolve('dynamic');
570
+ expect(absent.full).toBe(false);
571
+ expect(absent.modelText).toContain('loop.md is not currently present');
572
+
573
+ // Recreate with byte-identical content. Absence was a state change, so the
574
+ // full block must re-expand rather than collapse to a dangling reminder.
575
+ await writeProject('- same tasks');
576
+ const tick = await resolver.resolve('dynamic');
577
+ expect(tick.full).toBe(true);
578
+ expect(tick.modelText).toContain('- same tasks');
579
+ });
580
+
581
+ it('uses mode-specific reminders; dynamic names the re-arm sentinel', async () => {
582
+ await writeProject('- tasks');
583
+
584
+ const cron = await resolver.resolve('cron');
585
+ expect(cron.modelText).toContain('do not call LoopWakeup from this tick');
586
+ expect(cron.modelText).not.toContain('(dynamic pacing)');
587
+
588
+ // Fresh resolver so 'dynamic' is also a first (full) delivery.
589
+ const dyn = new LoopTickResolver({
590
+ projectRoot,
591
+ homeDir,
592
+ allowProjectFile: () => true,
593
+ });
594
+ const dynTick = await dyn.resolve('dynamic');
595
+ expect(dynTick.modelText).toContain(LOOP_SENTINEL_DYNAMIC);
596
+ expect(dynTick.modelText).toContain('call LoopWakeup again');
597
+ });
598
+
599
+ it('appends the truncation warning on a line boundary for oversized files', async () => {
600
+ const line = 'task line padding padding padding\n';
601
+ const body = line.repeat(Math.ceil(LOOP_TASK_FILE_MAX_BYTES / line.length));
602
+ await writeProject(body);
603
+
604
+ const tick = await resolver.resolve('cron');
605
+
606
+ expect(tick.full).toBe(true);
607
+ const warning = `> WARNING: loop.md was truncated to ${LOOP_TASK_FILE_MAX_BYTES} bytes. Keep the task list concise.`;
608
+ expect(tick.modelText).toContain(`\n${warning}`);
609
+ // The body is trimmed back to a COMPLETE line — the warning never glues onto
610
+ // a half-line. Guards against cutToLastNewline regressing to a no-op (which
611
+ // would leave the body ending mid-line, e.g. "task line ").
612
+ const beforeWarning = tick.modelText.slice(
613
+ 0,
614
+ tick.modelText.indexOf(`\n${warning}`),
615
+ );
616
+ expect(beforeWarning.endsWith('task line padding padding padding')).toBe(
617
+ true,
618
+ );
619
+ });
620
+
621
+ it('keeps the body when the only newline is at index 0 (no empty truncated block)', async () => {
622
+ // A truncated file whose only newline is the leading byte: there is no
623
+ // complete line to keep, so cutting to the "last full line" would empty the
624
+ // body and leave the INTRO promising tasks that aren't there. The body must
625
+ // survive — guards cutToLastNewline against a `cut >= 0` regression that
626
+ // slices a position-0 newline down to "".
627
+ await writeProject('\n' + 'x'.repeat(LOOP_TASK_FILE_MAX_BYTES + 100));
628
+
629
+ const tick = await resolver.resolve('cron');
630
+
631
+ expect(tick.full).toBe(true);
632
+ const warning = `> WARNING: loop.md was truncated to ${LOOP_TASK_FILE_MAX_BYTES} bytes. Keep the task list concise.`;
633
+ expect(tick.modelText).toContain(`\n${warning}`);
634
+ // The x-run above the warning is non-empty; a `cut >= 0` regression would
635
+ // empty it, leaving only INTRO + warning.
636
+ const beforeWarning = tick.modelText.slice(
637
+ 0,
638
+ tick.modelText.indexOf(`\n${warning}`),
639
+ );
640
+ expect(beforeWarning).toContain('xxxxxxxxxx');
641
+ });
642
+
643
+ it('names the home loop.md in the header and re-expands when the source switches', async () => {
644
+ await writeProject('- project tasks');
645
+ const first = await resolver.resolve('cron');
646
+ resolver.markDelivered();
647
+ expect(first.full).toBe(true);
648
+ expect(first.sourceLabel).toBe('project loop.md');
649
+
650
+ // Project gone, home has DIFFERENT content → re-expand (cache keys on
651
+ // content, not path) and the header now names the home file.
652
+ await fs.rm(projectFile());
653
+ await writeHome('- home tasks');
654
+ const second = await resolver.resolve('cron');
655
+
656
+ expect(second.full).toBe(true);
657
+ expect(second.sourceLabel).toBe('home loop.md');
658
+ expect(second.modelText).toContain(
659
+ '# /loop tick — loop.md tasks from home loop.md',
660
+ );
661
+ // The absolute home path must not leak into the model-facing text.
662
+ expect(second.modelText).not.toContain(homeFile());
663
+ expect(second.modelText).toContain('- home tasks');
664
+ });
665
+ });