@songsid/agend 2.3.2-beta.9 → 2.3.3-beta.1

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 (285) hide show
  1. package/README.md +6 -0
  2. package/README.zh-TW.md +6 -0
  3. package/dist/agent-cli-instructions.md +2 -2
  4. package/dist/agent-cli.js +48 -0
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +6 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +21 -0
  9. package/dist/agent-whats-new.js +53 -21
  10. package/dist/agent-whats-new.js.map +1 -1
  11. package/dist/auth-api.js +2 -0
  12. package/dist/auth-api.js.map +1 -1
  13. package/dist/backend/claude-code.d.ts +13 -1
  14. package/dist/backend/claude-code.js +43 -2
  15. package/dist/backend/claude-code.js.map +1 -1
  16. package/dist/backend/codex-session-lookup.d.ts +15 -0
  17. package/dist/backend/codex-session-lookup.js +58 -0
  18. package/dist/backend/codex-session-lookup.js.map +1 -1
  19. package/dist/backend/codex.js +15 -4
  20. package/dist/backend/codex.js.map +1 -1
  21. package/dist/backend/kiro-classic-archive.d.ts +57 -0
  22. package/dist/backend/kiro-classic-archive.js +622 -0
  23. package/dist/backend/kiro-classic-archive.js.map +1 -0
  24. package/dist/backend/kiro-classic-links.d.ts +24 -0
  25. package/dist/backend/kiro-classic-links.js +135 -0
  26. package/dist/backend/kiro-classic-links.js.map +1 -0
  27. package/dist/backend/kiro-identity.d.ts +100 -1
  28. package/dist/backend/kiro-identity.js +262 -31
  29. package/dist/backend/kiro-identity.js.map +1 -1
  30. package/dist/backend/kiro-migration-verify.d.ts +10 -0
  31. package/dist/backend/kiro-migration-verify.js +52 -0
  32. package/dist/backend/kiro-migration-verify.js.map +1 -0
  33. package/dist/backend/kiro-reconcile-store.d.ts +27 -0
  34. package/dist/backend/kiro-reconcile-store.js +351 -0
  35. package/dist/backend/kiro-reconcile-store.js.map +1 -0
  36. package/dist/backend/kiro-v2-store-worker.js +35 -1
  37. package/dist/backend/kiro-v2-store-worker.js.map +1 -1
  38. package/dist/backend/kiro-v2-store.d.ts +31 -1
  39. package/dist/backend/kiro-v2-store.js +22 -12
  40. package/dist/backend/kiro-v2-store.js.map +1 -1
  41. package/dist/backend/kiro.d.ts +124 -6
  42. package/dist/backend/kiro.js +553 -39
  43. package/dist/backend/kiro.js.map +1 -1
  44. package/dist/backend/types.d.ts +109 -4
  45. package/dist/backend/types.js +9 -0
  46. package/dist/backend/types.js.map +1 -1
  47. package/dist/cache-service.d.ts +13 -1
  48. package/dist/cache-service.js +45 -2
  49. package/dist/cache-service.js.map +1 -1
  50. package/dist/channel/ipc-timeouts.js +3 -0
  51. package/dist/channel/ipc-timeouts.js.map +1 -1
  52. package/dist/channel/mcp-tools.js +40 -5
  53. package/dist/channel/mcp-tools.js.map +1 -1
  54. package/dist/channel/tool-router.js +2 -1
  55. package/dist/channel/tool-router.js.map +1 -1
  56. package/dist/classic-channel-manager.d.ts +6 -0
  57. package/dist/classic-channel-manager.js +25 -0
  58. package/dist/classic-channel-manager.js.map +1 -1
  59. package/dist/cli.js +52 -6
  60. package/dist/cli.js.map +1 -1
  61. package/dist/config-validator.d.ts +7 -1
  62. package/dist/config-validator.js +20 -9
  63. package/dist/config-validator.js.map +1 -1
  64. package/dist/config.js +2 -0
  65. package/dist/config.js.map +1 -1
  66. package/dist/cross-instance-envelope.d.ts +10 -5
  67. package/dist/cross-instance-envelope.js +20 -17
  68. package/dist/cross-instance-envelope.js.map +1 -1
  69. package/dist/daemon.d.ts +125 -63
  70. package/dist/daemon.js +694 -330
  71. package/dist/daemon.js.map +1 -1
  72. package/dist/dashboard-permissions.d.ts +15 -4
  73. package/dist/dashboard-permissions.js +49 -23
  74. package/dist/dashboard-permissions.js.map +1 -1
  75. package/dist/delivery-outbox.d.ts +27 -22
  76. package/dist/delivery-outbox.js +109 -161
  77. package/dist/delivery-outbox.js.map +1 -1
  78. package/dist/delivery-outbox.test.js +10 -25
  79. package/dist/delivery-outbox.test.js.map +1 -1
  80. package/dist/doctor.js +2 -1
  81. package/dist/doctor.js.map +1 -1
  82. package/dist/fleet-control-audit.d.ts +1 -1
  83. package/dist/fleet-control-audit.js +3 -0
  84. package/dist/fleet-control-audit.js.map +1 -1
  85. package/dist/fleet-manager.d.ts +94 -15
  86. package/dist/fleet-manager.js +597 -133
  87. package/dist/fleet-manager.js.map +1 -1
  88. package/dist/forged-envelope-scan.d.ts +9 -0
  89. package/dist/forged-envelope-scan.js +30 -5
  90. package/dist/forged-envelope-scan.js.map +1 -1
  91. package/dist/general-instance.d.ts +24 -0
  92. package/dist/general-instance.js +53 -0
  93. package/dist/general-instance.js.map +1 -1
  94. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +1 -1
  95. package/dist/general-knowledge/skills/docker-install/SKILL.md +25 -0
  96. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -3
  97. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
  98. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +34 -0
  99. package/dist/general-knowledge/skills/session-management/SKILL.md +2 -2
  100. package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -1
  101. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +32 -0
  102. package/dist/general-knowledge/skills/web-appearance/SKILL.md +32 -0
  103. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +57 -17
  104. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +57 -17
  105. package/dist/instance-config-impact.js +3 -2
  106. package/dist/instance-config-impact.js.map +1 -1
  107. package/dist/instance-lifecycle.d.ts +4 -0
  108. package/dist/instance-lifecycle.js +28 -5
  109. package/dist/instance-lifecycle.js.map +1 -1
  110. package/dist/instructions.js +6 -7
  111. package/dist/instructions.js.map +1 -1
  112. package/dist/kiro-migrate.d.ts +19 -2
  113. package/dist/kiro-migrate.js +133 -35
  114. package/dist/kiro-migrate.js.map +1 -1
  115. package/dist/kiro-reconcile.d.ts +21 -0
  116. package/dist/kiro-reconcile.js +88 -0
  117. package/dist/kiro-reconcile.js.map +1 -0
  118. package/dist/kiro-session-proof.d.ts +41 -0
  119. package/dist/kiro-session-proof.js +89 -0
  120. package/dist/kiro-session-proof.js.map +1 -0
  121. package/dist/kiro-tui-forensic.d.ts +19 -0
  122. package/dist/kiro-tui-forensic.js +124 -0
  123. package/dist/kiro-tui-forensic.js.map +1 -0
  124. package/dist/locale.js +202 -16
  125. package/dist/locale.js.map +1 -1
  126. package/dist/log-service.js +3 -3
  127. package/dist/log-service.js.map +1 -1
  128. package/dist/needs-you.d.ts +3 -1
  129. package/dist/needs-you.js +4 -2
  130. package/dist/needs-you.js.map +1 -1
  131. package/dist/office-levels.d.ts +64 -0
  132. package/dist/office-levels.js +66 -0
  133. package/dist/office-levels.js.map +1 -0
  134. package/dist/office-share-page.d.ts +3 -1
  135. package/dist/office-share-page.js +8 -3
  136. package/dist/office-share-page.js.map +1 -1
  137. package/dist/office-share-service.d.ts +6 -3
  138. package/dist/office-share-service.js +12 -8
  139. package/dist/office-share-service.js.map +1 -1
  140. package/dist/office-share.d.ts +45 -8
  141. package/dist/office-share.js +75 -15
  142. package/dist/office-share.js.map +1 -1
  143. package/dist/outbound-handlers.js +21 -2
  144. package/dist/outbound-handlers.js.map +1 -1
  145. package/dist/outbound-schemas.d.ts +47 -2
  146. package/dist/outbound-schemas.js +42 -3
  147. package/dist/outbound-schemas.js.map +1 -1
  148. package/dist/public-web-gateway.js +1 -1
  149. package/dist/public-web-gateway.js.map +1 -1
  150. package/dist/quickstart-api.d.ts +39 -1
  151. package/dist/quickstart-api.js +115 -17
  152. package/dist/quickstart-api.js.map +1 -1
  153. package/dist/remote-card-answer.d.ts +83 -0
  154. package/dist/remote-card-answer.js +208 -0
  155. package/dist/remote-card-answer.js.map +1 -0
  156. package/dist/remote-card-prompt.d.ts +38 -0
  157. package/dist/remote-card-prompt.js +86 -0
  158. package/dist/remote-card-prompt.js.map +1 -0
  159. package/dist/scheduler/task-board.d.ts +44 -17
  160. package/dist/scheduler/task-board.js +95 -36
  161. package/dist/scheduler/task-board.js.map +1 -1
  162. package/dist/sd-notify.d.ts +13 -14
  163. package/dist/sd-notify.js +62 -28
  164. package/dist/sd-notify.js.map +1 -1
  165. package/dist/service-installer.d.ts +13 -0
  166. package/dist/service-installer.js +15 -0
  167. package/dist/service-installer.js.map +1 -1
  168. package/dist/service-owner.d.ts +25 -0
  169. package/dist/service-owner.js +43 -2
  170. package/dist/service-owner.js.map +1 -1
  171. package/dist/settings-api.d.ts +2 -1
  172. package/dist/settings-api.js +19 -26
  173. package/dist/settings-api.js.map +1 -1
  174. package/dist/settings-change.js +10 -5
  175. package/dist/settings-change.js.map +1 -1
  176. package/dist/settings-effect.js +7 -6
  177. package/dist/settings-effect.js.map +1 -1
  178. package/dist/settings-instance-patch.js +1 -1
  179. package/dist/settings-instance-patch.js.map +1 -1
  180. package/dist/setup-form.d.ts +2 -2
  181. package/dist/setup-form.js +139 -93
  182. package/dist/setup-form.js.map +1 -1
  183. package/dist/setup-i18n.d.ts +25 -0
  184. package/dist/setup-i18n.js +226 -0
  185. package/dist/setup-i18n.js.map +1 -0
  186. package/dist/task-board-waker.d.ts +40 -1
  187. package/dist/task-board-waker.js +115 -9
  188. package/dist/task-board-waker.js.map +1 -1
  189. package/dist/tmux-control.d.ts +1 -1
  190. package/dist/tmux-control.js +3 -2
  191. package/dist/tmux-control.js.map +1 -1
  192. package/dist/tmux-manager.js +18 -16
  193. package/dist/tmux-manager.js.map +1 -1
  194. package/dist/tmux-read.d.ts +2 -0
  195. package/dist/tmux-read.js +7 -1
  196. package/dist/tmux-read.js.map +1 -1
  197. package/dist/tool-permissions.js +18 -0
  198. package/dist/tool-permissions.js.map +1 -1
  199. package/dist/topic-commands.d.ts +2 -0
  200. package/dist/topic-commands.js +2 -1
  201. package/dist/topic-commands.js.map +1 -1
  202. package/dist/transcript-sources.d.ts +1 -0
  203. package/dist/transcript-sources.js +20 -3
  204. package/dist/transcript-sources.js.map +1 -1
  205. package/dist/types.d.ts +11 -5
  206. package/dist/ui/backend-signin.js +4 -3
  207. package/dist/ui/chat-store.js +3 -2
  208. package/dist/ui/chat-strings.js +2 -2
  209. package/dist/ui/fleet-office-card.js +114 -0
  210. package/dist/ui/fleet-office-editor.js +81 -9
  211. package/dist/ui/fleet-office-layout.js +56 -11
  212. package/dist/ui/fleet-office-levels.js +105 -0
  213. package/dist/ui/fleet-office-model.js +9 -1
  214. package/dist/ui/fleet-office-pack.d.ts +15 -3
  215. package/dist/ui/fleet-office-pack.js +177 -23
  216. package/dist/ui/fleet-office-skin.js +39 -3
  217. package/dist/ui/fleet-office-store.js +74 -3
  218. package/dist/ui/fleet-office-view.js +8 -0
  219. package/dist/ui/fleet-office-zones.js +102 -19
  220. package/dist/ui/fleet-office.js +308 -44
  221. package/dist/ui/fleet-org.js +2 -0
  222. package/dist/ui/fleet-task-card.js +2 -2
  223. package/dist/ui/fleet-task-common.js +30 -7
  224. package/dist/ui/fleet-task-work.js +2 -2
  225. package/dist/ui/fleet-tasks.js +10 -11
  226. package/dist/ui/office-share-dialog.js +31 -7
  227. package/dist/ui/office-share-page.js +1 -0
  228. package/dist/ui/panel-chat.js +14 -2
  229. package/dist/ui/panel-log.js +6 -6
  230. package/dist/ui/panel-needs.js +1 -1
  231. package/dist/ui/panel-settings.js +16 -8
  232. package/dist/ui/settings-appearance.js +3 -35
  233. package/dist/ui/settings-discord.js +3 -2
  234. package/dist/ui/settings-guests.js +17 -7
  235. package/dist/ui/settings-model.js +17 -9
  236. package/dist/ui/settings-registry.js +1 -1
  237. package/dist/ui/settings-strings.js +18 -6
  238. package/dist/ui/settings-token.js +3 -2
  239. package/dist/ui/settings-wizard.js +25 -9
  240. package/dist/ui/shared/app-i18n.js +2 -2
  241. package/dist/ui/shared/app-stream.js +1 -1
  242. package/dist/ui/shared/app.css +70 -2
  243. package/dist/ui/shared/setup-host-text.js +107 -0
  244. package/dist/ui/structured-signin.js +89 -38
  245. package/dist/update-check.d.ts +13 -4
  246. package/dist/update-check.js +17 -4
  247. package/dist/update-check.js.map +1 -1
  248. package/dist/usage-rollup.d.ts +34 -3
  249. package/dist/usage-rollup.js +56 -3
  250. package/dist/usage-rollup.js.map +1 -1
  251. package/dist/web-api.d.ts +10 -1
  252. package/dist/web-api.js +207 -17
  253. package/dist/web-api.js.map +1 -1
  254. package/dist/web-auth.js +1 -1
  255. package/dist/web-auth.js.map +1 -1
  256. package/dist/web-experimental.d.ts +6 -2
  257. package/dist/web-experimental.js +8 -4
  258. package/dist/web-experimental.js.map +1 -1
  259. package/dist/web-office-acts.d.ts +5 -2
  260. package/dist/web-office-acts.js +9 -4
  261. package/dist/web-office-acts.js.map +1 -1
  262. package/dist/web-office-pack-read.d.ts +35 -0
  263. package/dist/web-office-pack-read.js +252 -0
  264. package/dist/web-office-pack-read.js.map +1 -0
  265. package/dist/web-office-pack-tools.d.ts +241 -0
  266. package/dist/web-office-pack-tools.js +785 -0
  267. package/dist/web-office-pack-tools.js.map +1 -0
  268. package/dist/web-office-skins.d.ts +68 -4
  269. package/dist/web-office-skins.js +324 -33
  270. package/dist/web-office-skins.js.map +1 -1
  271. package/dist/web-office-store.d.ts +47 -4
  272. package/dist/web-office-store.js +124 -23
  273. package/dist/web-office-store.js.map +1 -1
  274. package/dist/web-settings-patch.d.ts +3 -2
  275. package/dist/web-settings-patch.js +7 -5
  276. package/dist/web-settings-patch.js.map +1 -1
  277. package/dist/workflow-templates/default.md +3 -3
  278. package/dist/working-recovery.d.ts +0 -2
  279. package/dist/working-recovery.js +2 -23
  280. package/dist/working-recovery.js.map +1 -1
  281. package/package.json +2 -2
  282. package/templates/windows-shortcuts.ps1 +18 -17
  283. package/dist/upgrade-notices.d.ts +0 -17
  284. package/dist/upgrade-notices.js +0 -93
  285. package/dist/upgrade-notices.js.map +0 -1
@@ -5,14 +5,17 @@ import { homedir } from "node:os";
5
5
  import { getAgendHome } from "../paths.js";
6
6
  import { credentialHomeSpec, credentialProfileHome, credentialProfileStoreHome, prepareCredentialProfileHome, resolveCredentialProfile, } from "./credential-profile.js";
7
7
  import { kiroAgentName, kiroFleetTag, writeKiroAgent, removeKiroAgent, writeSharedKiroMcpEntries, removeSharedKiroMcpEntries, writeTaggedKiroSteering, removeTaggedKiroSteering, } from "./kiro-agent.js";
8
- import { resolveKiroIdentity, listKiroV1Sessions, listKiroV2Sessions, confirmKiroAgentSwitch, kiroAgentConfirmed, forgetKiroIdentity, kiroIdentityNeedsStore, kiroRecordedConversation, kiroCanonicalDirectory, KiroIdentityError, kiroEngineFloor, } from "./kiro-identity.js";
8
+ import { resolveKiroIdentity, listKiroV1Sessions, listKiroV2Sessions, readKiroClassicLinks, confirmKiroAgentSwitch, kiroAgentConfirmed, forgetKiroIdentity, kiroIdentityNeedsStore, kiroRecordedConversation, kiroCanonicalDirectory, KiroIdentityError, kiroEngineFloor, kiroCarryCandidate, } from "./kiro-identity.js";
9
9
  import { readKiroLedger, kiroHasNoLaunchRecord } from "./kiro-engine-ledger.js";
10
10
  import { execFileSync } from "node:child_process";
11
11
  import { writeFileSync, chmodSync, statSync } from "node:fs";
12
- import { UnsupportedCliError, resolveBinary, shellQuote, validateEffort, validateModel, warnIfModelMismatch } from "./types.js";
12
+ import { UnsupportedCliError, ResumeBlockedError, resolveBinary, shellQuote, validateEffort, validateModel, warnIfModelMismatch } from "./types.js";
13
+ import { KIRO_SESSION_ID_COMMAND, kiroTitleCommand } from "../kiro-session-proof.js";
13
14
  import { PIE_CLASS } from "../tui-glyphs.js";
14
15
  import { KIRO_EXPIRED_LOGIN_SCREEN } from "../login-flows.js";
15
16
  import { t } from "../locale.js";
17
+ import { sharedKiroV2StoreLane } from "./kiro-v2-store.js";
18
+ import { KIRO_CLASSIC_LINKS_NOT_PREPARED } from "./kiro-classic-links.js";
16
19
  import { decideKiroModelError, readKiroKnownModels, readKiroModelRecords, readKiroV1SavedModel, readKiroV2SavedModel, writeKiroModelRecords, KIRO_FALLBACK_MODEL } from "./kiro-retired-model.js";
17
20
  import { recordKiroLaunch } from "./kiro-engine-ledger.js";
18
21
  import { forgetKiroV3Identity, resolveKiroV3Resume } from "./kiro-v3-identity.js";
@@ -49,10 +52,13 @@ export const KIRO_TESTED_MAX = "2.34.0";
49
52
  * queued message mid-tool — has been captured (tests/fixtures/kiro-steer-1405, 2026-10-08: 2.27.1 `--tui
50
53
  * --agent-engine=v2` and `--v3`, 2.28.0 `--tui --agent-engine=v2`; identical rows on all three). Any other version,
51
54
  * 2.28.0's v3, the legacy UI and an undetected version all deliver a steer as an ordinary message after the turn.
55
+ * 2.34.0 TUI was checked on the user-authorized #2598 D5 run (tests/fixtures/kiro-2.34.0-tui-d5):
56
+ * native steer during a read-only tool, a fleet message queued mid-tool, and a native queue entry in the busy turn.
52
57
  */
53
58
  export const KIRO_STEER_VERIFIED = {
54
59
  "2.27.1": ["tui", "v3"],
55
60
  "2.28.0": ["tui"],
61
+ "2.34.0": ["tui"],
56
62
  };
57
63
  /**
58
64
  * The TUI composer's interrupt mode, read off its own row: the LAST row starting at column 0 with `›` (transcript user
@@ -70,8 +76,10 @@ export const KIRO_STEER_VERIFIED = {
70
76
  * evidence of either an empty or a full box. The toggle key's label is whatever the user bound; it is read past, never
71
77
  * pressed.
72
78
  */
73
- function readKiroComposer(pane) {
74
- const rows = pane.split("\n").map(row => row.replace(/\s+$/, ""));
79
+ /** #2682: the takeover hold's key (RuntimeDialog.pattern); kiroTakeoverActive decides, this never matches a pane. */
80
+ const KIRO_TAKEOVER_KEY = /(?!)kiro-takeover/;
81
+ /** The composer's `›` row in `rows` and how many typed-text rows continue below it; null when there is none. */
82
+ function findKiroComposerRow(rows) {
75
83
  let i = rows.length - 1;
76
84
  while (i >= 0 && (rows[i] === "" || /^[ \t]{20,}\S/.test(rows[i])))
77
85
  i--;
@@ -83,30 +91,47 @@ function readKiroComposer(pane) {
83
91
  continued++;
84
92
  }
85
93
  const row = rows[i];
86
- if (row === undefined || !/^›[ \t]+\S/.test(row))
94
+ return row !== undefined && /^›[ \t]+\S/.test(row) ? { index: i, continued } : null;
95
+ }
96
+ function readKiroComposer(pane) {
97
+ const rows = pane.split("\n").map(row => row.replace(/\s+$/, ""));
98
+ const at = findKiroComposerRow(rows);
99
+ if (!at)
87
100
  return null;
101
+ let row = rows[at.index];
88
102
  // A placeholder is one row: with continuation rows below it, this is typed text — unless its first row is another
89
- // mode's placeholder, which is then not ours to read.
90
- if (continued > 0) {
91
- if (/^›[ \t]+(?:Kiro is working\b|ask a question or describe a task\b|Goal (?:Active|Paused):|Editing queued message \d|Initializing\b|describe what "|running shell command\b)/.test(row))
92
- return null;
93
- return { mode: "text", text: row.replace(/^›[ \t]+/, "") };
103
+ // mode's placeholder. #2682: a narrow pane word-wraps a placeholder onto indented rows; joined, a COMPLETE known form
104
+ // reads as itself (an idle placeholder is then idle, not unreadable); anything else stays unreadable.
105
+ if (at.continued > 0) {
106
+ if (!/^›[ \t]+(?:Kiro is working\b|ask a question or describe a task\b|Goal (?:Active|Paused):|Editing queued message \d|Initializing\b|describe what "|running shell command\b)/.test(row)) {
107
+ return { mode: "text", text: row.replace(/^›[ \t]+/, "") };
108
+ }
109
+ row = [row, ...rows.slice(at.index + 1, at.index + 1 + at.continued).map(r => r.trim())].join(" ");
110
+ const mode = kiroPlaceholderMode(row);
111
+ return mode ? { mode, text: row.replace(/^›[ \t]+/, "") } : null;
94
112
  }
95
- const working = String.raw `^›[ \t]+Kiro is working(?:[ \t]+·[ \t]+[^·]+?)?[ \t]+·[ \t]+`;
96
113
  const text = row.replace(/^›[ \t]+/, "");
114
+ const mode = kiroPlaceholderMode(row);
115
+ if (mode)
116
+ return { mode, text };
117
+ // Other placeholders (and anything the ASCII glyph set paints) are not typed text.
118
+ if (/^›[ \t]+(?:Kiro is working\b|Goal (?:Active|Paused):|Editing queued message \d|Initializing\b|describe what "|running shell command\b|ask a question or describe a task (?:\.|enter\b))/.test(row))
119
+ return null;
120
+ return { mode: "text", text };
121
+ }
122
+ /** The empty composer's mode from its whole placeholder (`› …`, one row or wrapped rows joined); null when it is none. */
123
+ function kiroPlaceholderMode(row) {
124
+ const working = String.raw `^›[ \t]+Kiro is working(?:[ \t]+·[ \t]+[^·]+?)?[ \t]+·[ \t]+`;
97
125
  if (new RegExp(`${working}Type to steer[ \\t]+·[ \\t]+[^·]+?[ \\t]+to queue$`).test(row))
98
- return { mode: "steer", text };
126
+ return "steer";
99
127
  if (new RegExp(`${working}Type to queue(?:[ \\t]+·[ \\t]+[^·]+?[ \\t]+to steer)?$`).test(row))
100
- return { mode: "queue", text };
128
+ return "queue";
101
129
  const idle = String.raw `^›[ \t]+ask a question or describe a task`;
102
130
  if (new RegExp(`${idle}[ \\t]+↵$`).test(row)
103
131
  || new RegExp(`${idle}[ \\t]+↵[ \\t]+·[ \\t]+exit plan mode: shift\\+tab$`).test(row)
104
132
  || new RegExp(`${idle}[ \\t]+·[ \\t]+/tangent to go back[ \\t]+·[ \\t]+/tangent ls to view$`).test(row))
105
- return { mode: "idle", text };
106
- // Other placeholders (and anything the ASCII glyph set paints) are not typed text.
107
- if (/^›[ \t]+(?:Kiro is working\b|Goal (?:Active|Paused):|Editing queued message \d|Initializing\b|describe what "|running shell command\b|ask a question or describe a task (?:\.|enter\b))/.test(row))
108
- return null;
109
- return { mode: "text", text };
133
+ return "idle";
134
+ return null;
110
135
  }
111
136
  export function readKiroSteerComposer(pane) {
112
137
  return readKiroComposer(pane)?.mode ?? null;
@@ -116,6 +141,178 @@ export function readKiroComposerText(pane) {
116
141
  const c = readKiroComposer(pane);
117
142
  return c?.mode === "text" ? c.text : null;
118
143
  }
144
+ /**
145
+ * #2683: the composer as a one-line housekeeping command (`/session-id`, `/title …`) sees it: `""` when it is
146
+ * positively empty (the idle placeholder, nothing else), the typed text of a single-row draft, or null for anything
147
+ * else (busy, another mode, wrapped text, no composer). A typed slash command opens kiro's command menu below the box
148
+ * (live 2.34.0, tui and V3; tests/fixtures/kiro-session-id-2681):
149
+ *
150
+ * `› /session-id`
151
+ * `/session-id Print the current session ID`
152
+ * `──────…`
153
+ * ` esc to cancel · ↵ to select`
154
+ *
155
+ * and its `›` row is read past that menu. Enter there runs the highlighted entry, the command itself.
156
+ */
157
+ export function readKiroCommandComposer(pane) {
158
+ const rows = pane.split("\n").map(row => row.replace(/\s+$/, ""));
159
+ let i = rows.length - 1;
160
+ while (i >= 0 && (rows[i] === "" || /^[ \t]{20,}\S/.test(rows[i])))
161
+ i--;
162
+ if (i >= 2 && /^ esc to cancel · .*↵ to select$/.test(rows[i]) && /^─{20,}$/.test(rows[i - 1])) {
163
+ let r = i - 2, entries = 0;
164
+ while (r >= 0 && entries < 12 && /^\/[\w-]+ {2,}\S/.test(rows[r])) {
165
+ r--;
166
+ entries++;
167
+ }
168
+ const row = rows[r];
169
+ return entries > 0 && row !== undefined && /^›[ \t]+\/\S/.test(row) ? row.replace(/^›[ \t]+/, "") : null;
170
+ }
171
+ const at = findKiroComposerRow(rows);
172
+ if (!at || at.continued > 0)
173
+ return null;
174
+ const c = readKiroComposer(pane);
175
+ return c?.mode === "idle" ? "" : c?.mode === "text" ? c.text : null;
176
+ }
177
+ /**
178
+ * #2683: kiro's own answer to `/session-id`, where only that answer is painted: the alert at the bottom of the chat,
179
+ * on the rows right above the footer (the divider within 8 rows above the composer, and the trust banner's divider
180
+ * above that one when it is shown). Live 2.34.0, tui and V3 (tests/fixtures/kiro-session-id-2681):
181
+ *
182
+ * `• Session ID: <id>`
183
+ * ` Resume with: kiro-cli --resume-id <id>` (the same id)
184
+ *
185
+ * or `• Session ID: none` alone. It is a transient alert (10 s), not a turn. The same lines anywhere else (a quoted
186
+ * user row is ` › …`, an answer scrolls up) are not read; an agent answer that ends with them would sit in the same
187
+ * place, which is why the daemon also requires that the alert was not there before it typed the command. Null when no
188
+ * such alert is shown.
189
+ */
190
+ export function readKiroSessionIdAlert(pane) {
191
+ const rows = pane.split("\n").map(row => row.replace(/\s+$/, ""));
192
+ const at = findKiroComposerRow(rows);
193
+ if (!at)
194
+ return null;
195
+ let d = at.index - 1;
196
+ while (d >= 0 && at.index - d <= 8 && !/^─{20,}$/.test(rows[d]))
197
+ d--;
198
+ if (d < 0 || !/^─{20,}$/.test(rows[d]))
199
+ return null;
200
+ if (d >= 2 && /^ \S/.test(rows[d - 1]) && /^─{20,}$/.test(rows[d - 2]))
201
+ d -= 2; // ` Trust All Tools active …`
202
+ if (rows[d - 1] === "• Session ID: none")
203
+ return "none";
204
+ const id = /^ {2}Resume with: kiro-cli --resume-id ([A-Za-z0-9][A-Za-z0-9_.-]{0,199})$/.exec(rows[d - 1] ?? "")?.[1];
205
+ return id !== undefined && id !== "none" && rows[d - 2] === `• Session ID: ${id}` ? id : null;
206
+ }
207
+ /**
208
+ * #2676: a collapsed paste on the composer's row. The TUI shows a paste of more than 10 lines or 500 characters as one
209
+ * chip (2.34.0 tui.js: `n>1 ? "${n} lines" : "${r} chars"`, then `▸`, then the `Press Tab to expand` hint):
210
+ *
211
+ * `› 42 lines ▸ Press Tab to expand` → 42
212
+ * `› 657 chars ▸ Press Tab to expand` → 1 (one line, shown only by its length)
213
+ *
214
+ * `text` is the row's text (readKiroComposerText). Null when it is not such a chip.
215
+ */
216
+ export function readKiroCollapsedPasteLines(text) {
217
+ const m = /^(\d+) (lines|chars) ▸(?:[ \t]+Press Tab to expand)?$/.exec(text.trim());
218
+ if (!m)
219
+ return null;
220
+ return m[2] === "lines" ? Number(m[1]) : 1;
221
+ }
222
+ /**
223
+ * #2682: another program owns kiro's pane, so a paste and an Enter would go into it instead of the chat (captured on
224
+ * 2.34.0, tests/fixtures/kiro-2.34.0-takeover-2682). Read from kiro's own rows first:
225
+ * - a focused `/kvim` panel: `── Neovim · focused · Ctrl+\ to chat · :q to close ──`, or `· the agent is waiting on you:`
226
+ * (unfocused, `Neovim · Ctrl+\ to edit`, the composer has the keys again);
227
+ * - a running `!` command (tui, V3): its PTY takes every keystroke, while the composer still reads busy
228
+ * (`Kiro is working · Type to steer`) — the nearest turn above kiro's footer is ` › !…`, with only its indented
229
+ * output and `• Running...` below (kiroBusyTurn; a turn it cannot see is not called a takeover here, but no busy
230
+ * input is typed into it either: KiroBackend.busyTurnTakesInput);
231
+ * - an editor or pager kiro started (`/editor`, `/transcript`, a full-screen `!vim`/`!less`): no composer row, kiro's
232
+ * temp file `kiro-editor-XXXXXX/prompt.md` or `kiro-raw-XXXXXX/conversation.md` on screen;
233
+ * - legacy: a `!` command running from its prompt row (the bottom row is the prompt with `!…` after the marker: the
234
+ * command took the terminal before kiro painted anything else), and its `/editor` and `/reply` temp file
235
+ * `q_prompt_<uuid>.md` with no prompt row at the bottom;
236
+ * and, only where kiro's input row is not on screen (every engine; legacy's `/editor` and `/reply` run `$EDITOR`), the
237
+ * program's own bottom rows: vi/vim/nvim (`~` rows, its ruler, `-- INSERT --`), less (`:`, `(END)`), more
238
+ * (`--More--`), nano (`GNU nano` and `^X Exit`). `/fullscreen` is not a takeover: the same composer takes input.
239
+ */
240
+ export function kiroTakeoverActive(pane, ui) {
241
+ const rows = pane.split("\n").map(row => row.replace(/\s+$/, ""));
242
+ if (ui === "legacy") {
243
+ const prompt = kiroLegacyPromptAtBottom(rows);
244
+ if (prompt !== null)
245
+ return prompt.startsWith("!");
246
+ if (rows.some(row => /\/q_prompt_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.md\b/.test(row)))
247
+ return true;
248
+ return fullScreenProgramAtBottom(rows);
249
+ }
250
+ if (rows.some(row => /^─+ Neovim · (?:focused · |the agent is waiting on you: )/.test(row)))
251
+ return true;
252
+ const at = findKiroComposerRow(rows);
253
+ if (at) {
254
+ const mode = readKiroComposer(pane)?.mode;
255
+ return (mode === "steer" || mode === "queue") && kiroBusyTurn(rows, at.index) === "shell";
256
+ }
257
+ if (rows.some(row => /\/kiro-(?:editor|raw)-[A-Za-z0-9]{6}\/(?:prompt|conversation)\.md\b/.test(row)))
258
+ return true;
259
+ return fullScreenProgramAtBottom(rows);
260
+ }
261
+ /**
262
+ * #2682: the legacy UI's prompt row as the last non-blank row — `4% !>` (trust-all) or `4% >`, with an optional
263
+ * `[agent]` prefix and mode glyph, as getBottomReadyPattern — and the text after its marker (a placeholder hint, a
264
+ * draft, or a `!` command it is running); null when the bottom row is not the prompt.
265
+ */
266
+ export function kiroLegacyPromptAtBottom(rows) {
267
+ const last = [...rows].reverse().find(row => row.trim() !== "");
268
+ const m = last === undefined ? null : /^\s*(?:\[[^\]]*\]\s*)?\d+%\s*(?:[^\s\d%!❯>]{1,2}\s+)?(?:!\s?[❯>]|❯|>)(.*)$/.exec(last);
269
+ return m ? m[1].trim() : null;
270
+ }
271
+ /**
272
+ * #2682: what the busy turn nearest above kiro's footer is: "shell" — a `!` command still running (its ` › !` row with
273
+ * only its output and `• Running...` below), "agent" — positive evidence of the agent's turn first (another user row,
274
+ * one of kiro's bullets, its spinner), "unknown" — no footer, or nothing that says either up to the top of the pane.
275
+ * Everything else is passed over, never taken as evidence: the footer and its notes however they wrap, dividers, the
276
+ * tray and tips, indented output.
277
+ */
278
+ function kiroBusyTurn(rows, composer) {
279
+ let d = composer - 1;
280
+ while (d >= 0 && composer - d <= 8 && !/^─{20,}$/.test(rows[d]))
281
+ d--;
282
+ if (d < 0 || !/^─{20,}$/.test(rows[d]))
283
+ return "unknown"; // no footer divider within reach
284
+ for (let r = d - 1; r >= 0; r--) {
285
+ const row = rows[r];
286
+ if (/^ {2}› !/.test(row))
287
+ return "shell";
288
+ if (/^ {2}› /.test(row) || /^• (?!Running\.\.\.$)\S/.test(row) || /^\S[ \t]+(?:Thinking|Working)(?:\.{3}|…)/.test(row))
289
+ return "agent";
290
+ }
291
+ return "unknown";
292
+ }
293
+ /**
294
+ * #2682: whether a busy composer's typed input goes to kiro's own turn: only when that turn is seen to be the agent's.
295
+ * A `!` command's PTY would take it, and a turn it cannot see might be one.
296
+ */
297
+ export function kiroBusyTurnTakesInput(pane) {
298
+ const rows = pane.split("\n").map(row => row.replace(/\s+$/, ""));
299
+ const at = findKiroComposerRow(rows);
300
+ return at !== null && kiroBusyTurn(rows, at.index) === "agent";
301
+ }
302
+ /** #2682: a full-screen editor or pager's own bottom rows (vi/vim/nvim, less, more, nano), whoever started it. */
303
+ function fullScreenProgramAtBottom(rows) {
304
+ const shown = rows.filter(row => row.trim() !== "");
305
+ const last = shown[shown.length - 1]?.trim() ?? "";
306
+ if (last === ":" || /\(END\)$/.test(last) || /^--More--/.test(last))
307
+ return true; // less, more
308
+ if (/^-- (?:INSERT|REPLACE|VISUAL(?: LINE| BLOCK)?) --/.test(last))
309
+ return true; // vi modes
310
+ if (/\s\d+,\d+(?:-\d+)?\s+(?:All|Top|Bot|\d+%)$/.test(last))
311
+ return true; // vi ruler
312
+ if (shown.slice(-7, -1).filter(row => row === "~").length >= 3)
313
+ return true; // vi's empty lines
314
+ return shown.slice(-2).some(row => /\^X Exit/.test(row)) && rows.some(row => /^\s*GNU nano\b/.test(row)); // nano
315
+ }
119
316
  const UNKNOWN_KIRO_COMPATIBILITY = {
120
317
  supportsLegacyUi: false,
121
318
  supportsTui: false,
@@ -134,11 +331,12 @@ const warnedVersionGateCacheKeys = new Set();
134
331
  * This broad test is only a safety hold: it must never select an option or
135
332
  * claim a service outage. A ready prompt below the picker makes it history.
136
333
  */
334
+ const KIRO_MODEL_PICKER_HEADER = /^\s*Select model(?: \(type to search\):|:\s+type to search)\s*$/m;
137
335
  function kiroModelPickerAtTail(pane) {
138
336
  const lines = pane.replace(/\r/g, "").split("\n");
139
337
  let pickerIndex = -1;
140
338
  for (let index = lines.length - 1; index >= 0; index--) {
141
- if (/^\s*Select model \(type to search\):\s*$/.test(lines[index])) {
339
+ if (KIRO_MODEL_PICKER_HEADER.test(lines[index])) {
142
340
  pickerIndex = index;
143
341
  break;
144
342
  }
@@ -146,7 +344,8 @@ function kiroModelPickerAtTail(pane) {
146
344
  if (pickerIndex < 0)
147
345
  return false;
148
346
  const tail = lines.slice(pickerIndex + 1);
149
- if (tail.some(line => /^\s*\d+%[^\n]*[!❯>]\s+\S/.test(line)))
347
+ const composer = readKiroSteerComposer(tail.join("\n"));
348
+ if (tail.some(line => /^\s*\d+%[^\n]*[!❯>]\s+\S/.test(line)) || (composer !== null && composer !== "text"))
150
349
  return false;
151
350
  return tail.some(line => /^\s*[>❯›]\s*\*?\s*\S/.test(line));
152
351
  }
@@ -399,6 +598,137 @@ export function kiroQuestionCardActive(pane) {
399
598
  return false;
400
599
  return body.filter(row => /^\s*❯\s+\S/.test(row)).length === 1;
401
600
  }
601
+ /** #2675: the live card's rows — the header row, the body between it and the key hints' divider, and the key hints. */
602
+ function kiroCardRows(pane, headerAt) {
603
+ const rows = pane.replace(/\r/g, "").split("\n").map(row => row.trimEnd());
604
+ let last = rows.length - 1;
605
+ while (last >= 0 && !rows[last].trim())
606
+ last--;
607
+ const header = headerAt(rows, last);
608
+ return header < 0 ? null : { header: rows[header].trim(), body: rows.slice(header + 1, last - 1), footer: rows[last].trim(), width: rows[last - 1].trim().length };
609
+ }
610
+ const KIRO_QUESTION_OPTION_ROW = /^ (❯| ) (\d+)\. (.*?)( \(recommended\))?$/;
611
+ const KIRO_QUESTION_SUBOPTION_ROW = /^ (❯| ) \[(✓| )\] (.*)$/;
612
+ const KIRO_QUESTION_FREE_ROW = /^ (❯| ) (Type a different answer…|or type feedback to request changes)$/;
613
+ /**
614
+ * #2675: the live Question card (see kiroQuestionCardActive), read for a person to answer from the chat: the question,
615
+ * the numbered options (`(recommended)` and the description row under one), the typed-answer row, and where the `❯`
616
+ * is (tui.js `A0`; tests/fixtures/kiro-question-2592). On a sub-options page the rows are the `[✓]` boxes; while an
617
+ * answer is being typed the last row is `❯ <text>`. A row the shapes above do not account for is a wrapped title (the
618
+ * card's own indent) or description (deeper), joined to the row it continues; one before any option belongs to the
619
+ * question. Null when the card is not live, or when it is in a shape this reader does not know.
620
+ */
621
+ export function readKiroQuestionCard(pane) {
622
+ if (!kiroQuestionCardActive(pane))
623
+ return null;
624
+ const card = kiroCardRows(pane, (rows, last) => {
625
+ let top = last - 2;
626
+ while (top >= 0 && !/^─{3,}$/.test(rows[top].trim()))
627
+ top--;
628
+ return top + 1;
629
+ });
630
+ if (!card)
631
+ return null;
632
+ const page = /space to toggle/.test(card.footer) ? "suboptions" : "options";
633
+ const question = [];
634
+ const options = [];
635
+ let cursor = null, freeText = false, typed = null, freeRow = false;
636
+ for (const row of card.body) {
637
+ const option = page === "options" ? KIRO_QUESTION_OPTION_ROW.exec(row) : null;
638
+ const sub = page === "suboptions" ? KIRO_QUESTION_SUBOPTION_ROW.exec(row) : null;
639
+ const free = page === "options" ? KIRO_QUESTION_FREE_ROW.exec(row) : null;
640
+ if (freeRow)
641
+ return null; // nothing follows the typed-answer row
642
+ if (option && Number(option[2]) === options.length + 1) {
643
+ if (option[1] === "❯")
644
+ cursor = options.length;
645
+ options.push({ n: options.length + 1, title: option[3], recommended: option[4] !== undefined, description: null });
646
+ }
647
+ else if (sub) {
648
+ if (sub[1] === "❯")
649
+ cursor = options.length;
650
+ options.push({ n: options.length + 1, title: sub[3], recommended: false, description: null, checked: sub[2] === "✓" });
651
+ }
652
+ else if (free) {
653
+ freeText = true;
654
+ freeRow = true;
655
+ if (free[1] === "❯")
656
+ cursor = options.length;
657
+ }
658
+ else if (page === "options" && options.length > 0 && /^ ❯ /.test(row)) {
659
+ freeText = true;
660
+ freeRow = true;
661
+ typed = row.slice(3);
662
+ }
663
+ else if (!row.trim()) {
664
+ continue;
665
+ }
666
+ else if (options.length === 0) {
667
+ question.push(row.replace(/^ /, ""));
668
+ }
669
+ else {
670
+ const lastOption = options[options.length - 1];
671
+ if (/^ {5,}\S/.test(row))
672
+ lastOption.description = lastOption.description === null ? row.trim() : `${lastOption.description} ${row.trim()}`;
673
+ else if (lastOption.description === null)
674
+ lastOption.title = `${lastOption.title} ${row.trim()}`;
675
+ else
676
+ return null;
677
+ }
678
+ }
679
+ if (options.length === 0 || question.length === 0)
680
+ return null;
681
+ const typing = typed !== null;
682
+ if (typing ? cursor !== null : cursor === null)
683
+ return null;
684
+ const titles = options.map(option => `${option.n}. ${option.title}`).join("\n");
685
+ return {
686
+ kind: "question", identity: `${card.header}\n${question.join("\n")}\n${page === "suboptions" ? "sub" : "options"}\n${titles}`,
687
+ title: card.header, question: question.join("\n").trim(), page: typing ? "freetext" : page, options, freeText, cursor, typed, rowWidth: card.width,
688
+ };
689
+ }
690
+ const KIRO_MCP_INPUT_ACTIONS = new Set(["Submit", "Decline", "Open in browser", "Copy URL", "I'll open it myself", "Cancel"]);
691
+ /**
692
+ * #2675: the live MCP `Input requested` card (see kiroMcpInputRequestActive), read for the chat: the server, the body
693
+ * as painted, with the `URL:` row masked (a sign-in link stays in the pane), its action rows, and the `❯` row. Its
694
+ * identity is the server, the mode, the message's first row and the actions: moving or filling in a field keeps it.
695
+ */
696
+ export function readKiroMcpInputCard(pane) {
697
+ if (!kiroMcpInputRequestActive(pane))
698
+ return null;
699
+ const card = kiroCardRows(pane, (rows, last) => {
700
+ for (let index = last - 2; index >= 1; index--)
701
+ if (KIRO_MCP_INPUT_HEADER.test(rows[index]))
702
+ return index;
703
+ return -1;
704
+ });
705
+ if (!card)
706
+ return null;
707
+ // The URL row is masked, and so is every row after it up to the next action row: a long URL wraps (#2677 review).
708
+ let inUrl = false;
709
+ const body = card.body.slice(1).flatMap(row => {
710
+ if (/^\s*URL: /.test(row)) {
711
+ inUrl = true;
712
+ return [row.replace(/URL: .*$/, "URL: (shown in the instance pane)")];
713
+ }
714
+ if (inUrl && KIRO_MCP_INPUT_ACTIONS.has(row.replace(/^ ❯ /, " ").trim()))
715
+ inUrl = false;
716
+ return inUrl ? [] : [row];
717
+ });
718
+ const strip = (row) => row.replace(/^ ❯ /, " ").trim();
719
+ const cursorRow = body.find(row => /^\s*❯\s+\S/.test(row));
720
+ if (!cursorRow)
721
+ return null;
722
+ return {
723
+ kind: "input", server: card.header.replace(/^Input requested · /, ""),
724
+ mode: /ctrl\+s to submit/.test(card.footer) ? "form" : "url",
725
+ rows: body.map(row => row.replace(/^ ❯ /, " ").replace(/^ {3}/, "")).filter(row => row.trim()),
726
+ actions: body.map(strip).filter(row => KIRO_MCP_INPUT_ACTIONS.has(row)),
727
+ cursorRow: strip(cursorRow),
728
+ // The focused field paints extra rows (its value, its caret) and a typed value changes its row: neither is another request.
729
+ identity: `${card.header}\n${/ctrl\+s to submit/.test(card.footer) ? "form" : "url"}\n${strip(body[0] ?? "")}\n${body.map(strip).filter(row => KIRO_MCP_INPUT_ACTIONS.has(row)).join("|")}`,
730
+ };
731
+ }
402
732
  /**
403
733
  * Kiro's service-unavailable path opens the same picker as `/model`, but only
404
734
  * the former needs incident escalation. The runtime scanner and delivery gate
@@ -411,7 +741,7 @@ export function kiroUnavailableModelPickerActive(pane) {
411
741
  const lines = pane.replace(/\r/g, "").split("\n");
412
742
  let pickerIndex = -1;
413
743
  for (let i = lines.length - 1; i >= 0; i--) {
414
- if (/^\s*Select model \(type to search\):\s*$/.test(lines[i])) {
744
+ if (KIRO_MODEL_PICKER_HEADER.test(lines[i])) {
415
745
  pickerIndex = i;
416
746
  break;
417
747
  }
@@ -627,6 +957,13 @@ function versionAtLeast(version, minimum) {
627
957
  }
628
958
  return true;
629
959
  }
960
+ /** Verified carry/reconcile stays on supported 2.x and requires the same capabilities as launch. */
961
+ export function supportsVerifiedKiroMigration(compat) {
962
+ // `version` retains the complete --version line (e.g. "kiro-cli 2.34.0").
963
+ const version = parseSemver(compat.version);
964
+ return !!version && version[0] === 2 && versionAtLeast(version, "2.34.0")
965
+ && compat.supportsTui && compat.supportsInstanceAgent === true && compat.agentEngines?.includes("v2") === true;
966
+ }
630
967
  function helpAdvertisesFlag(help, flag) {
631
968
  // clap prints a short alias first when there is one: ` -r, --resume`.
632
969
  return new RegExp(`^\\s*(?:-[A-Za-z0-9],\\s*)?${flag}(?:[ =<,]|$)`, "m").test(help);
@@ -754,6 +1091,22 @@ function nextPatch(version) {
754
1091
  const [major, minor, patch] = parseSemver(version);
755
1092
  return `${major}.${minor}.${patch + 1}`;
756
1093
  }
1094
+ /** #2714: what a resume decision says about a compaction, for the log and the Log tab. */
1095
+ function kiroCompactionNotes(decision) {
1096
+ const notes = [];
1097
+ if (decision.followed?.length)
1098
+ notes.push(t("kiro.compaction_followed", decision.followed[0], decision.id));
1099
+ const kept = decision.kept;
1100
+ if (kept?.reason === "unreadable")
1101
+ notes.push(t("kiro.compaction_kept_unreadable", decision.id));
1102
+ else if (kept?.reason === "over-budget")
1103
+ notes.push(t("kiro.compaction_kept_over_budget", decision.id));
1104
+ else if (kept?.reason === "ambiguous")
1105
+ notes.push(t("kiro.compaction_kept_ambiguous", decision.id, kept.from, kept.candidates.join(", ")));
1106
+ else if (kept?.reason === "not-ours")
1107
+ notes.push(t("kiro.compaction_kept_not_ours", decision.id, kept.from, kept.candidate));
1108
+ return notes;
1109
+ }
757
1110
  /** #2598 B3: engines in the order a conversation can move between them (never back). */
758
1111
  const KIRO_ENGINE_RANK = { legacy: 1, tui: 2, v3: 3 };
759
1112
  /** Binary generations already told they cannot isolate (once each). */
@@ -838,6 +1191,34 @@ export function readActiveKiroAgent(pane, ui) {
838
1191
  }
839
1192
  return null;
840
1193
  }
1194
+ /** Model on Kiro 2.34's live footer (status, cwd, optional branch, blank, idle composer).
1195
+ * Separate from readActiveKiroAgent: do not widen the disabled agent-switch action's reader.
1196
+ */
1197
+ export function readActiveKiroModel(pane, expectedAgent) {
1198
+ if (readKiroSteerComposer(pane) !== "idle")
1199
+ return null;
1200
+ const rows = pane.split("\n").map(row => row.replace(/\s+$/, "")), at = findKiroComposerRow(rows);
1201
+ if (!at)
1202
+ return null;
1203
+ let cwd = false, branch = false;
1204
+ // Footer only. Never search backwards through arbitrary output or a model picker for a matching model.
1205
+ for (let i = at.index - 1; i >= Math.max(0, at.index - 8); i--) {
1206
+ const row = rows[i];
1207
+ if (!row.trim() || RULE_ROW.test(row))
1208
+ continue;
1209
+ if (!branch && /^\s*\([^()\r\n]{1,128}\)\s*$/.test(row)) {
1210
+ branch = true;
1211
+ continue;
1212
+ }
1213
+ if (!cwd && /^\s*(?:\/[^\x00-\x1f]*|~(?:\/[^\x00-\x1f]*)?)\s*$/.test(row)) {
1214
+ cwd = true;
1215
+ continue;
1216
+ }
1217
+ const status = /^\s*(\S+) · ([A-Za-z0-9_.:/-]{1,128}) · .+$/.exec(row);
1218
+ return cwd && status && (expectedAgent === undefined || status[1] === expectedAgent) ? status[2] : null;
1219
+ }
1220
+ return null;
1221
+ }
841
1222
  export function planKiroLaunch(ui, compat) {
842
1223
  const version = compat.version ?? "of unknown version";
843
1224
  if (ui === "v3")
@@ -968,6 +1349,9 @@ export const KIRO_RATE_LIMIT_SCREEN = new RegExp(KIRO_MESSAGE_LEAD + wrapToleran
968
1349
  /** #1636: kiro's monthly cap (MonthlyLimitReached branch); see getErrorPatterns. */
969
1350
  export const KIRO_REQUEST_LIMIT_SCREEN = new RegExp(KIRO_MESSAGE_LEAD + "(?:" + ["Hourly", "Daily", "Weekly", "Monthly"].map(wrapTolerant).join("|") + ")"
970
1351
  + wrapTolerant(" request limit reached") + "(?:[ \\t]*$|" + wrapTolerant(" - The limits reset on") + "\\b)", "m");
1352
+ /** #2598: Kiro 2.34 lists a carried UUID in both V2 and classic; this proves presence, not a missing session. */
1353
+ export const KIRO_AMBIGUOUS_RESUME_SCREEN = new RegExp(wrapTolerant('Error: Session id "') + '[A-Za-z0-9_.\\n-]{1,256}' + wrapTolerant('" is ambiguous (')
1354
+ + "\\d+" + wrapTolerant(" matches)."), "m");
971
1355
  /** Startup budget for a `--resume` launch (60% first output, 40% ready). */
972
1356
  export const KIRO_RESUME_STARTUP_BUDGET_MS = 60_000;
973
1357
  export class KiroBackend {
@@ -1008,9 +1392,12 @@ export class KiroBackend {
1008
1392
  /** #1218: the last launch built in full (cleared when a build starts, set only when it returns). */
1009
1393
  builtLaunch = null;
1010
1394
  preparedV2Store = null;
1395
+ /** #2738: a classic launch's compaction links, worked out in prepareLaunch on their worker lane (off the loop). */
1396
+ preparedClassicLinks = null;
1011
1397
  /** #2648: the saved model of the v2 conversation this instance has recorded, read in prepareLaunch (off the loop). */
1012
1398
  preparedV2Model = null;
1013
1399
  storePreparation = 0;
1400
+ preparedCarry = null;
1014
1401
  storeKey(config) {
1015
1402
  return JSON.stringify([config.instanceName, config.workingDirectory, kiroCanonicalDirectory(config.workingDirectory), config.kiroUi,
1016
1403
  resolveCredentialProfile(config.backendOptions), !!config.skipResume, getAgendHome(), kiroV2SessionsDir(), kiroCanonicalDirectory(kiroV2SessionsDir())]);
@@ -1021,22 +1408,63 @@ export class KiroBackend {
1021
1408
  }
1022
1409
  return this.preparedV2Store.store;
1023
1410
  }
1411
+ /** Not prepared for this launch: unproven, so the launch stays on the recorded conversation (never read here). */
1412
+ readPreparedClassicLinks(config) {
1413
+ return this.preparedClassicLinks?.key === this.storeKey(config) ? this.preparedClassicLinks.links
1414
+ : KIRO_CLASSIC_LINKS_NOT_PREPARED;
1415
+ }
1024
1416
  /** Metadata only: identity claims/config writes remain synchronous, after
1025
1417
  * the daemon rechecks its launch admission following this await. */
1026
1418
  async prepareLaunch(config) {
1027
1419
  const preparation = ++this.storePreparation;
1028
1420
  this.preparedV2Store = null;
1029
1421
  this.preparedV2Model = null;
1030
- if (!config || config.kiroUi !== "tui")
1422
+ this.preparedCarry = null;
1423
+ this.preparedClassicLinks = null;
1424
+ if (!config)
1031
1425
  return;
1032
1426
  if (!this.fixedCompatibility)
1033
1427
  this.compatibility = cachedKiroCliCompatibility(this.binaryPath).compatibility;
1034
1428
  const key = this.storeKey(config);
1429
+ const classic = planKiroLaunch(config.kiroUi ?? "legacy", this.compatibility);
1430
+ if (classic.kind === "launch" && classic.flags.includes("--agent-engine=v1") && this.compatibility.supportsInstanceAgent && !config.skipResume) {
1431
+ // #2738: whether the recorded conversation was compacted reads every transcript of the key — a GB on a
1432
+ // long-lived instance. Worked out here on the worker lane; writeConfig only looks the answer up.
1433
+ const links = await readKiroClassicLinks(config.workingDirectory, kiroV1DbPath(resolveCredentialProfile(config.backendOptions)));
1434
+ if (preparation === this.storePreparation && key === this.storeKey(config))
1435
+ this.preparedClassicLinks = { key, links };
1436
+ return;
1437
+ }
1438
+ if (config.kiroUi !== "tui")
1439
+ return;
1035
1440
  const plan = planKiroLaunch("tui", this.compatibility);
1036
1441
  if (plan.kind !== "launch" || !plan.flags.includes("--agent-engine=v2") || !this.compatibility.supportsInstanceAgent)
1037
1442
  return;
1038
1443
  const opts = { instance: config.instanceName, engine: "v2", workingDirectory: config.workingDirectory,
1039
1444
  credentialProfile: resolveCredentialProfile(config.backendOptions), skipResume: config.skipResume, agendHome: getAgendHome() };
1445
+ if (!config.skipResume && this.needsCarryPreparation()) {
1446
+ try {
1447
+ this.preparedCarry = { key, error: null };
1448
+ if (kiroCarryCandidate(opts)) {
1449
+ const reply = await sharedKiroV2StoreLane.read({ keys: [], sessionsDir: kiroV2SessionsDir(), carry: {
1450
+ ...opts, sessionsDir: kiroV2SessionsDir(), dbPath: kiroV1DbPath(opts.credentialProfile),
1451
+ } });
1452
+ if (preparation !== this.storePreparation || key !== this.storeKey(config))
1453
+ return;
1454
+ this.preparedCarry = { key, error: reply.kind === "unreadable" ? reply.detail : null };
1455
+ if (reply.kind === "unreadable")
1456
+ return;
1457
+ if (reply.carry?.archivePath)
1458
+ this.addLaunchWarning(t("kiro.archive_done", reply.carry.archivePath));
1459
+ if (reply.carry?.repaired)
1460
+ this.addLaunchWarning(t("kiro.carry_pin_repaired"));
1461
+ }
1462
+ }
1463
+ catch (err) {
1464
+ this.preparedCarry = { key, error: err.message };
1465
+ return;
1466
+ }
1467
+ }
1040
1468
  if (kiroIdentityNeedsStore(opts)) {
1041
1469
  const store = await listKiroV2Sessions(config.workingDirectory, kiroV2SessionsDir());
1042
1470
  if (preparation === this.storePreparation && key === this.storeKey(config))
@@ -1051,6 +1479,27 @@ export class KiroBackend {
1051
1479
  this.preparedV2Model = { key, id: recorded, model };
1052
1480
  }
1053
1481
  }
1482
+ needsCarryPreparation() {
1483
+ const v = /(?:^|\s)(\d+)\.(\d+)\.(\d+)/.exec(this.compatibility.version ?? "");
1484
+ return !!v && (Number(v[1]) > 2 || (Number(v[1]) === 2 && Number(v[2]) >= 34));
1485
+ }
1486
+ assertCarryPrepared(config) {
1487
+ if (config.kiroUi !== "tui" || config.skipResume || !this.needsCarryPreparation())
1488
+ return;
1489
+ const opts = { agendHome: getAgendHome(), instance: config.instanceName, workingDirectory: config.workingDirectory,
1490
+ credentialProfile: resolveCredentialProfile(config.backendOptions) };
1491
+ let required;
1492
+ try {
1493
+ required = !!kiroCarryCandidate(opts);
1494
+ }
1495
+ catch (err) {
1496
+ throw new ResumeBlockedError(t("kiro.archive_failed", err.message), t("kiro.archive_remedy"));
1497
+ }
1498
+ if (this.preparedCarry?.key === this.storeKey(config) && this.preparedCarry.error)
1499
+ throw new ResumeBlockedError(t("kiro.archive_failed", this.preparedCarry.error), t("kiro.archive_remedy"));
1500
+ if (required && this.preparedCarry?.key !== this.storeKey(config))
1501
+ throw new ResumeBlockedError(t("kiro.archive_failed", "carry preparation is missing"), t("kiro.archive_remedy"));
1502
+ }
1054
1503
  constructor(instanceDir, compatibility) {
1055
1504
  this.instanceDir = instanceDir;
1056
1505
  this.binaryPath = resolveBinary("kiro-cli");
@@ -1105,6 +1554,10 @@ export class KiroBackend {
1105
1554
  readSteerComposerText(pane) {
1106
1555
  return this.activeUi === "legacy" ? null : readKiroComposerText(pane);
1107
1556
  }
1557
+ /** #2676: the line count a collapsed paste chip on the composer's row names (readKiroCollapsedPasteLines). */
1558
+ readSteerComposerPasteLines(text) {
1559
+ return this.activeUi === "legacy" ? null : readKiroCollapsedPasteLines(text);
1560
+ }
1108
1561
  /**
1109
1562
  * Live evidence (kiro-cli 2.21.0, 2026-09-03): `--resume` prints nothing
1110
1563
  * until the conversation comes back from the backend ("Picking up where we
@@ -1116,6 +1569,18 @@ export class KiroBackend {
1116
1569
  getStartupBudgetMs(ctx) {
1117
1570
  return ctx.resume ? KIRO_RESUME_STARTUP_BUDGET_MS : undefined;
1118
1571
  }
1572
+ resumeBlockedPattern() { return KIRO_AMBIGUOUS_RESUME_SCREEN; }
1573
+ retainedResumeBlock() {
1574
+ const plan = this.activePlan;
1575
+ if (!plan || plan.engine !== "v2" || plan.mode !== "resume" || !plan.id || !this.activeInstance)
1576
+ return null;
1577
+ let carried = true;
1578
+ try {
1579
+ carried = kiroCarryCandidate({ agendHome: getAgendHome(), instance: this.activeInstance, workingDirectory: plan.cwd, credentialProfile: plan.credentialProfile })?.id === plan.id;
1580
+ }
1581
+ catch { /* lost ownership proof is also a hold, never authority to discard history */ }
1582
+ return carried ? { reason: t("kiro.carry_resume_failed"), remedy: t("kiro.archive_remedy") } : null;
1583
+ }
1119
1584
  retriesResumeOnStartupFailure() {
1120
1585
  // A resume miss is far more often "the backend was slow" than "the session
1121
1586
  // is broken"; one more attempt before abandoning the conversation is cheap.
@@ -1150,6 +1615,7 @@ export class KiroBackend {
1150
1615
  // V3 has a different engine and no verified completion signal yet (#1144).
1151
1616
  this.replyGuardOptedIn = false;
1152
1617
  this.builtLaunch = null;
1618
+ this.assertCarryPrepared(config);
1153
1619
  const ui = config.kiroUi ?? "legacy";
1154
1620
  // #2598 B3: never back onto an older engine than this instance has run on — that resumes the frozen copy and loses
1155
1621
  // everything done since. Before any probe: no kiro-cli flag or version changes the answer.
@@ -1188,6 +1654,8 @@ export class KiroBackend {
1188
1654
  const lp = prepared ? this.launchPlan : this.planInstanceLaunch(config);
1189
1655
  if (!prepared && lp.reason)
1190
1656
  this.addLaunchWarning(lp.reason);
1657
+ if (!prepared)
1658
+ this.quietLaunchNotes.push(...lp.notes ?? []);
1191
1659
  this.launchPlan = null;
1192
1660
  this.activePlan = lp;
1193
1661
  this.activeSpec = lp.mode === "legacy" ? null : this.agentSpec(config);
@@ -1324,6 +1792,8 @@ export class KiroBackend {
1324
1792
  this.launchWarning = null;
1325
1793
  return warning;
1326
1794
  }
1795
+ quietLaunchNotes = [];
1796
+ consumeQuietLaunchNotes() { const notes = this.quietLaunchNotes; this.quietLaunchNotes = []; return notes; }
1327
1797
  shouldWarnUnsupportedEffort() {
1328
1798
  if (!this.compatibilityCacheKey) {
1329
1799
  if (this.warnedUnsupportedEffort)
@@ -1337,6 +1807,7 @@ export class KiroBackend {
1337
1807
  return true;
1338
1808
  }
1339
1809
  writeConfig(config) {
1810
+ this.assertCarryPrepared(config);
1340
1811
  // WORKAROUND: kiro-cli ignores the "env" block of an MCP server entry — the server subprocess inherits the fleet
1341
1812
  // manager's env, with a stale AGEND_SOCKET_PATH from whichever daemon wrote last. Each server therefore runs
1342
1813
  // through a wrapper script that exports this instance's env and execs the real server.
@@ -1360,6 +1831,7 @@ export class KiroBackend {
1360
1831
  this.launchPlan = plan;
1361
1832
  if (plan.reason)
1362
1833
  this.addLaunchWarning(plan.reason);
1834
+ this.quietLaunchNotes.push(...plan.notes ?? []);
1363
1835
  const spec = this.agentSpec(config);
1364
1836
  const root = dirname(this.instanceDir);
1365
1837
  if (plan.mode === "legacy") {
@@ -1511,7 +1983,7 @@ export class KiroBackend {
1511
1983
  skipResume: config.skipResume,
1512
1984
  agendHome,
1513
1985
  readStore: () => engine === "v1"
1514
- ? listKiroV1Sessions(config.workingDirectory, kiroV1DbPath(credentialProfile))
1986
+ ? listKiroV1Sessions(config.workingDirectory, kiroV1DbPath(credentialProfile), this.readPreparedClassicLinks(config))
1515
1987
  : this.readPreparedV2Store(config),
1516
1988
  launchedBefore: () => kiroLaunchedHereBefore(config.instanceName, config.workingDirectory, credentialProfile, engine),
1517
1989
  upgradeAdoption: () => {
@@ -1532,10 +2004,12 @@ export class KiroBackend {
1532
2004
  }
1533
2005
  if (decision.mode === "legacy")
1534
2006
  return legacy(t("kiro.identity_legacy", decision.reason));
1535
- return decision.mode === "resume"
1536
- ? { ...base, mode: "resume", id: decision.id, agent, agentConfirmed: decision.agentConfirmed, reason: null }
1537
- : { ...base, mode: "fresh", agent, agentConfirmed: true,
1538
- reason: decision.notice ? t(`kiro.upgrade_${decision.notice.replaceAll("-", "_")}`) : null };
2007
+ if (decision.mode === "resume") {
2008
+ return { ...base, mode: "resume", id: decision.id, agent, agentConfirmed: decision.agentConfirmed, reason: null, notes: kiroCompactionNotes(decision) };
2009
+ }
2010
+ const reasons = [decision.notice ? t(`kiro.upgrade_${decision.notice.replaceAll("-", "_")}`) : null,
2011
+ decision.lost ? t("kiro.claim_lost", decision.lost.id, t(`kiro.claim_lost_${decision.lost.claim.replaceAll("-", "_")}`)) : null];
2012
+ return { ...base, mode: "fresh", agent, agentConfirmed: true, reason: reasons.filter(r => r !== null).join("\n") || null };
1539
2013
  }
1540
2014
  /**
1541
2015
  * After a resume as the instance's agent (#906 §3): what the daemon needs to read the active agent off the live
@@ -1578,16 +2052,6 @@ export class KiroBackend {
1578
2052
  },
1579
2053
  };
1580
2054
  }
1581
- /**
1582
- * An instance running as its own agent reads its instructions from the agent file's prompt (#906). Until a resumed
1583
- * conversation's switch is confirmed it still runs as its saved agent, reading the steering file (the default).
1584
- */
1585
- instructionsSource() {
1586
- const plan = this.activePlan;
1587
- if (plan?.mode === "resume" && !kiroResumeAgentSwitch.enabled)
1588
- return null; // its saved agent: the steering file
1589
- return plan && plan.mode !== "legacy" && plan.agent && plan.agentConfirmed ? `.kiro/agents/${plan.agent}.json (its "prompt" field)` : null;
1590
- }
1591
2055
  activeSpec = null;
1592
2056
  activeInstance = null;
1593
2057
  getReadyPattern() {
@@ -1827,7 +2291,7 @@ export class KiroBackend {
1827
2291
  // picker. Its choices range across credit multipliers and capability
1828
2292
  // tiers, so this must be a human decision, never an automatic Enter or
1829
2293
  // Escape. The ordinary /model menu alone is intentionally not matched.
1830
- pattern: /^\s*Select model \(type to search\):\s*$/m,
2294
+ pattern: KIRO_MODEL_PICKER_HEADER,
1831
2295
  keys: [],
1832
2296
  description: "Kiro model unavailable — choose a replacement model in the instance pane",
1833
2297
  blocksDelivery: true,
@@ -1839,7 +2303,7 @@ export class KiroBackend {
1839
2303
  // Any active /model picker owns stdin, even if a future Kiro release
1840
2304
  // changes the exact warning, credit wording or row layout. Keep this
1841
2305
  // after the verified-outage case: it never labels /model as an outage.
1842
- pattern: /^\s*Select model \(type to search\):\s*$/m,
2306
+ pattern: KIRO_MODEL_PICKER_HEADER,
1843
2307
  keys: [],
1844
2308
  description: "Kiro model picker — manual selection required",
1845
2309
  blocksDelivery: true,
@@ -1856,6 +2320,8 @@ export class KiroBackend {
1856
2320
  holdOnly: true,
1857
2321
  inputBlocked: true,
1858
2322
  isActive: kiroMcpInputRequestActive,
2323
+ requestIdentity: pane => readKiroMcpInputCard(pane)?.identity ?? null,
2324
+ remoteCard: readKiroMcpInputCard,
1859
2325
  },
1860
2326
  {
1861
2327
  // #2592: the agent's question to the user owns stdin until the person answers or cancels it.
@@ -1866,11 +2332,42 @@ export class KiroBackend {
1866
2332
  holdOnly: true,
1867
2333
  inputBlocked: true,
1868
2334
  isActive: kiroQuestionCardActive,
2335
+ requestIdentity: pane => readKiroQuestionCard(pane)?.identity ?? null,
2336
+ remoteCard: readKiroQuestionCard,
1869
2337
  },
1870
2338
  ...kiroTrustPromptDialogs(this.activeTrustAll),
1871
2339
  ...kiroWorkspaceTrustDialogs(this.activeTrustAll),
2340
+ {
2341
+ // #2682: an editor, a pager, a `!` command or a focused /kvim owns the pane: a paste and an Enter would go into
2342
+ // it. Last, so every dialog above keeps its own words; held for the person, never keyed.
2343
+ pattern: KIRO_TAKEOVER_KEY,
2344
+ keys: [],
2345
+ description: "Kiro's terminal is in use by another program (an editor, a pager or a ! command) — finish or close it in the instance pane",
2346
+ blocksDelivery: true,
2347
+ holdOnly: true,
2348
+ inputBlocked: true,
2349
+ isActive: pane => kiroTakeoverActive(pane, this.activeUi),
2350
+ },
1872
2351
  ];
1873
2352
  }
2353
+ /**
2354
+ * #2682: kiro's own input row, read positively, before an idle delivery. TUI and V3: the composer reads "idle", or
2355
+ * "text" (a draft, as before); busy, another mode (a goal, the tray being edited) or no composer at all — an unknown
2356
+ * full-screen program, a blank frame — is not a screen to type into. Legacy: its prompt row is the bottom row (`4% !>`,
2357
+ * or `4% >` without trust-all, live 2.34.0), not running a `!` command. Anything else: the delivery waits.
2358
+ */
2359
+ /** #2682: the busy composer's input reaches kiro's own turn (kiroBusyTurnTakesInput), not a `!` command's PTY. */
2360
+ busyTurnTakesInput(pane) {
2361
+ return this.activeUi !== "legacy" && kiroBusyTurnTakesInput(pane);
2362
+ }
2363
+ idleComposerReady(pane) {
2364
+ if (this.activeUi === "legacy") {
2365
+ const prompt = kiroLegacyPromptAtBottom(pane.split("\n").map(row => row.replace(/\s+$/, "")));
2366
+ return prompt !== null && !prompt.startsWith("!");
2367
+ }
2368
+ const mode = readKiroSteerComposer(pane);
2369
+ return mode === "idle" || mode === "text";
2370
+ }
1874
2371
  getContextUsage() {
1875
2372
  return null;
1876
2373
  }
@@ -1885,6 +2382,23 @@ export class KiroBackend {
1885
2382
  return null;
1886
2383
  return { engine: built.engine, sessionId: plan?.mode === "resume" && plan.id ? plan.id : null };
1887
2384
  }
2385
+ /**
2386
+ * M-2025: `/session-id` and `/title` exist on tui and V3 only. Legacy keeps
2387
+ * the tmux window name and the sqlite lookup.
2388
+ */
2389
+ sessionProof() {
2390
+ const ui = this.activeUi;
2391
+ if (ui !== "tui" && ui !== "v3")
2392
+ return null;
2393
+ const pinned = this.transcriptLaunch()?.sessionId ?? null;
2394
+ return {
2395
+ pinned,
2396
+ sessionIdCommand: KIRO_SESSION_ID_COMMAND,
2397
+ titleCommand: this.activeInstance ? kiroTitleCommand(this.activeInstance) : null,
2398
+ readComposer: readKiroCommandComposer,
2399
+ readSessionIdAlert: readKiroSessionIdAlert,
2400
+ };
2401
+ }
1888
2402
  getSessionId() {
1889
2403
  // Kiro manages sessions internally via SQLite keyed by working directory.
1890
2404
  // No external session ID needed — --resume handles it automatically.