@songsid/agend 2.1.9 → 2.1.10-beta.2

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 (101) hide show
  1. package/README.md +8 -3
  2. package/README.zh-TW.md +11 -5
  3. package/dist/backend/antigravity.js +2 -1
  4. package/dist/backend/antigravity.js.map +1 -1
  5. package/dist/backend/codex.d.ts +13 -0
  6. package/dist/backend/codex.js +74 -2
  7. package/dist/backend/codex.js.map +1 -1
  8. package/dist/backend/kiro.d.ts +81 -5
  9. package/dist/backend/kiro.js +370 -36
  10. package/dist/backend/kiro.js.map +1 -1
  11. package/dist/backend/types.d.ts +10 -0
  12. package/dist/backend/types.js +13 -0
  13. package/dist/backend/types.js.map +1 -1
  14. package/dist/channel/adapters/telegram.js +4 -3
  15. package/dist/channel/adapters/telegram.js.map +1 -1
  16. package/dist/channel/ipc-bridge.js +10 -3
  17. package/dist/channel/ipc-bridge.js.map +1 -1
  18. package/dist/channel/mcp-operation-response.js +5 -4
  19. package/dist/channel/mcp-operation-response.js.map +1 -1
  20. package/dist/channel/mcp-server.d.ts +1 -8
  21. package/dist/channel/mcp-server.js +10 -0
  22. package/dist/channel/mcp-server.js.map +1 -1
  23. package/dist/channel/types.d.ts +7 -0
  24. package/dist/cli.d.ts +1 -1
  25. package/dist/cli.js +93 -10
  26. package/dist/cli.js.map +1 -1
  27. package/dist/config-validator.js +13 -0
  28. package/dist/config-validator.js.map +1 -1
  29. package/dist/config.js +26 -0
  30. package/dist/config.js.map +1 -1
  31. package/dist/daemon.d.ts +68 -0
  32. package/dist/daemon.js +211 -27
  33. package/dist/daemon.js.map +1 -1
  34. package/dist/delivery-outbox.js +18 -23
  35. package/dist/delivery-outbox.js.map +1 -1
  36. package/dist/fleet-manager.d.ts +59 -1
  37. package/dist/fleet-manager.js +208 -35
  38. package/dist/fleet-manager.js.map +1 -1
  39. package/dist/forged-envelope-scan.js +2 -5
  40. package/dist/forged-envelope-scan.js.map +1 -1
  41. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
  42. package/dist/instance-lifecycle.d.ts +25 -5
  43. package/dist/instance-lifecycle.js +68 -33
  44. package/dist/instance-lifecycle.js.map +1 -1
  45. package/dist/locale.js +174 -4
  46. package/dist/locale.js.map +1 -1
  47. package/dist/logger.js +19 -16
  48. package/dist/logger.js.map +1 -1
  49. package/dist/login-controller.d.ts +38 -0
  50. package/dist/login-controller.js +227 -6
  51. package/dist/login-controller.js.map +1 -1
  52. package/dist/login-flows.d.ts +7 -0
  53. package/dist/login-flows.js +1 -0
  54. package/dist/login-flows.js.map +1 -1
  55. package/dist/muse-usage-relay.js +3 -2
  56. package/dist/muse-usage-relay.js.map +1 -1
  57. package/dist/node-warnings.d.ts +1 -0
  58. package/dist/node-warnings.js +26 -0
  59. package/dist/node-warnings.js.map +1 -0
  60. package/dist/outbound-handlers.js +14 -16
  61. package/dist/outbound-handlers.js.map +1 -1
  62. package/dist/pause-marker.js +3 -2
  63. package/dist/pause-marker.js.map +1 -1
  64. package/dist/private-dir.d.ts +54 -0
  65. package/dist/private-dir.js +140 -0
  66. package/dist/private-dir.js.map +1 -0
  67. package/dist/routing-engine.d.ts +17 -2
  68. package/dist/routing-engine.js +48 -5
  69. package/dist/routing-engine.js.map +1 -1
  70. package/dist/service-installer.d.ts +46 -0
  71. package/dist/service-installer.js +224 -4
  72. package/dist/service-installer.js.map +1 -1
  73. package/dist/setup-wizard.js +3 -1
  74. package/dist/setup-wizard.js.map +1 -1
  75. package/dist/tunnel/cloudflared.d.ts +23 -5
  76. package/dist/tunnel/cloudflared.js +32 -13
  77. package/dist/tunnel/cloudflared.js.map +1 -1
  78. package/dist/tunnel/manager.d.ts +6 -0
  79. package/dist/tunnel/manager.js +60 -22
  80. package/dist/tunnel/manager.js.map +1 -1
  81. package/dist/tunnel/public-fetch.d.ts +30 -0
  82. package/dist/tunnel/public-fetch.js +156 -0
  83. package/dist/tunnel/public-fetch.js.map +1 -0
  84. package/dist/tunnel/types.d.ts +8 -2
  85. package/dist/tunnel/types.js +8 -2
  86. package/dist/tunnel/types.js.map +1 -1
  87. package/dist/types.d.ts +24 -0
  88. package/dist/ui/settings.html +2 -2
  89. package/dist/ui/view.html +14 -0
  90. package/dist/ui/web-terminal/terminal.html +1 -0
  91. package/dist/wake-coordinator.d.ts +0 -15
  92. package/dist/wake-coordinator.js +19 -3
  93. package/dist/wake-coordinator.js.map +1 -1
  94. package/dist/web-host-guard.d.ts +47 -0
  95. package/dist/web-host-guard.js +75 -0
  96. package/dist/web-host-guard.js.map +1 -0
  97. package/dist/web-terminal-http.d.ts +34 -0
  98. package/dist/web-terminal-http.js +63 -0
  99. package/dist/web-terminal-http.js.map +1 -1
  100. package/package.json +1 -1
  101. package/templates/systemd.service.ejs +17 -1
@@ -1,11 +1,22 @@
1
1
  import { type CliBackend, type CliBackendConfig, type ErrorPattern, type StartupDialog, type RuntimeDialog } from "./types.js";
2
- export declare const KIRO_REQUIRE_MCP_MIN = "1.25.0";
3
2
  export declare const KIRO_LEGACY_UI_MIN = "1.27.0";
4
3
  export declare const KIRO_EFFORT_FLAG_MIN = "2.6.0";
4
+ /** Oldest kiro-cli AgEnD claims: older ones still launch, with a warning. */
5
+ export declare const KIRO_SUPPORTED_MIN = "2.21.0";
6
+ /**
7
+ * Newest kiro-cli run live under AgEnD. Above it the launch flags come from
8
+ * the binary's own --help instead of the version table, and the operator is
9
+ * told the version is unverified.
10
+ */
11
+ export declare const KIRO_TESTED_MAX = "2.27.0";
5
12
  export interface KiroCliCompatibility {
6
13
  version?: string;
7
- supportsRequireMcpStartup: boolean;
8
14
  supportsLegacyUi: boolean;
15
+ supportsTui: boolean;
16
+ /** `--v3` exists, i.e. this binary has an engine AgEnD must not drift into. */
17
+ supportsV3: boolean;
18
+ /** Values `--agent-engine` accepts; null when the flag does not exist. */
19
+ agentEngines: readonly string[] | null;
9
20
  supportsEffortFlag: boolean;
10
21
  source: "version" | "help" | "unknown";
11
22
  }
@@ -19,8 +30,61 @@ type KiroProbeRunner = (binaryPath: string, args: string[]) => string;
19
30
  * AgEnD must not pick a replacement on the user's behalf.
20
31
  */
21
32
  export declare function kiroUnavailableModelPickerActive(pane: string): boolean;
22
- /** Probe once per backend construction, falling back from semver to --help. */
33
+ interface KiroLaunchPromptSpec {
34
+ /** Text that identifies the prompt; the LAST occurrence heads it. */
35
+ header: string;
36
+ /** Option labels, in screen order (matched as a row prefix). */
37
+ options: readonly string[];
38
+ }
39
+ export interface KiroLaunchPromptState {
40
+ /** The prompt is the current interactive region of the pane. */
41
+ active: boolean;
42
+ /** Index into `options` of the one row with a recognised cursor; null = unknown. */
43
+ cursor: number | null;
44
+ }
45
+ /**
46
+ * Whether a kiro launch prompt owns the pane right now, and where its cursor
47
+ * is. Bottom-anchored on purpose: below the header come only its description,
48
+ * then exactly its option rows in order, then at most a short key-hint footer.
49
+ * A quoted copy of the prompt in a transcript has the reply and the composer
50
+ * row (`12% !>`) below it, so it is not active and never receives a key.
51
+ *
52
+ * The cursor is known only when exactly one option row carries a glyph AgEnD
53
+ * recognises; any other glyph, none, or two is "unknown" — still active, so
54
+ * it is held for a human, never answered.
55
+ */
56
+ export declare function kiroLaunchPromptState(pane: string, spec: KiroLaunchPromptSpec): KiroLaunchPromptState;
57
+ /**
58
+ * The values `chat --help` lists for --agent-engine, or null without the flag.
59
+ * clap prints them as `[possible values: v2, v1, v3]` a few lines below the
60
+ * flag; the quoted names in its description are the fallback.
61
+ */
62
+ export declare function parseKiroAgentEngines(help: string): string[] | null;
63
+ /**
64
+ * Probe once per binary generation. A version AgEnD has run (<= TESTED_MAX)
65
+ * is answered from the table without a second CLI call; anything newer, or a
66
+ * version string that does not parse, is answered by the binary's own
67
+ * `chat --help` — 3.0 included, which is exactly where the table stops being
68
+ * evidence.
69
+ */
23
70
  export declare function probeKiroCliCompatibility(binaryPath: string, run?: KiroProbeRunner): KiroCliCompatibility;
71
+ export type KiroLaunchPlan = {
72
+ kind: "launch";
73
+ ui: "legacy" | "tui" | "v3";
74
+ flags: string[];
75
+ } | {
76
+ kind: "refuse";
77
+ reason: string;
78
+ };
79
+ /**
80
+ * Pin the UI AND the engine on every launch (#1109). A flag outranks kiro's
81
+ * persisted `chat.agentEngine`, which the V3 ease-in prompt writes for the
82
+ * whole machine and which kiro 3.0 may default to V3. An instance's
83
+ * conversation lives in its engine's store — classic sqlite, V2 JSON, V3 KAS —
84
+ * and moving between them forks it one way, so a launch AgEnD cannot pin to
85
+ * the instance's own engine is refused, never run on whatever kiro defaults to.
86
+ */
87
+ export declare function planKiroLaunch(ui: "legacy" | "tui" | "v3", compat: KiroCliCompatibility): KiroLaunchPlan;
24
88
  /** Process-level memo used by backend construction; exported for regression tests. */
25
89
  export declare function getCachedKiroCliCompatibility(binaryPath: string, run?: KiroProbeRunner): KiroCliCompatibility;
26
90
  /** Test-only reset for the process-level compatibility and warning memo. */
@@ -31,8 +95,10 @@ export declare class KiroBackend implements CliBackend {
31
95
  private instanceDir;
32
96
  readonly binaryName = "kiro-cli";
33
97
  private binaryPath;
34
- private readonly compatibility;
35
- private readonly compatibilityCacheKey?;
98
+ private compatibility;
99
+ private compatibilityCacheKey?;
100
+ /** Injected by tests: never re-probed. */
101
+ private readonly fixedCompatibility;
36
102
  private warnedUnsupportedEffort;
37
103
  /**
38
104
  * UI flavour and trust mode of the LAST command built. The Enter-drop
@@ -44,6 +110,8 @@ export declare class KiroBackend implements CliBackend {
44
110
  */
45
111
  private activeUi;
46
112
  private activeTrustAll;
113
+ /** Version-gate notice for the launch just built (consumeLaunchWarning). */
114
+ private launchWarning;
47
115
  constructor(instanceDir: string, compatibility?: KiroCliCompatibility);
48
116
  requiresDeliveryEnterRetry(): boolean;
49
117
  dropsEnterWhileBusy(): boolean;
@@ -69,6 +137,14 @@ export declare class KiroBackend implements CliBackend {
69
137
  * launched before this existed.
70
138
  */
71
139
  private withCredentialProfile;
140
+ /**
141
+ * P3 of #1109: tell the operator, once per binary generation, when the
142
+ * kiro-cli in use is outside the range AgEnD has run. Never blocks a launch:
143
+ * below KIRO_SUPPORTED_MIN it still works, and above KIRO_TESTED_MAX the
144
+ * flags were already checked against the binary's own --help.
145
+ */
146
+ private noteVersionGate;
147
+ consumeLaunchWarning(): string | null;
72
148
  private shouldWarnUnsupportedEffort;
73
149
  writeConfig(config: CliBackendConfig): void;
74
150
  getReadyPattern(): RegExp;
@@ -3,27 +3,43 @@ import { getAgendHome } from "../paths.js";
3
3
  import { credentialHomeSpec, credentialProfileHome, prepareCredentialProfileHome, resolveCredentialProfile, } from "./credential-profile.js";
4
4
  import { execFileSync } from "node:child_process";
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, unlinkSync, statSync } from "node:fs";
6
- import { resolveBinary, shellQuote, validateEffort, validateModel, warnIfModelMismatch } from "./types.js";
6
+ import { UnsupportedCliError, resolveBinary, shellQuote, validateEffort, validateModel, warnIfModelMismatch } from "./types.js";
7
7
  import { PIE_CLASS } from "../tui-glyphs.js";
8
8
  import { KIRO_EXPIRED_LOGIN_SCREEN } from "../login-flows.js";
9
+ import { t } from "../locale.js";
9
10
  // Kiro CLI feature gates. These are deliberately separate: the flags shipped
10
11
  // in different releases, so one broad "old Kiro" check would still crash some
11
- // supported versions with an unknown argument.
12
- // - 1.25.0: https://kiro.dev/changelog/cli/1-25/ (MCP startup checks)
13
- // - 1.27.0: verified against Kiro's archived 1.26.0/1.27.0 binaries. Before
14
- // this, classic was the only UI and neither --legacy-ui nor --classic existed.
12
+ // supported versions with an unknown argument. Every row below was read from
13
+ // `chat --help` of the archived release binary (tests/fixtures/kiro-help/).
14
+ // - 1.27.0: --tui / --legacy-ui. Before this, classic was the only UI and
15
+ // neither --legacy-ui nor --classic existed.
16
+ // - 2.3.0: --agent-engine, but its values were `rust` (default) | `kas`:
17
+ // `--agent-engine=v1` exits 2 there.
18
+ // - 2.4.0: --agent-engine v1|v2|kas, and `--legacy-ui` only with v1.
15
19
  // - 2.6.0: https://kiro.dev/changelog/cli/2-6/ (initial effort flag)
16
- export const KIRO_REQUIRE_MCP_MIN = "1.25.0";
20
+ // - 2.8.0: --agent-engine v1|v2|v3, plus --v3.
17
21
  export const KIRO_LEGACY_UI_MIN = "1.27.0";
18
22
  export const KIRO_EFFORT_FLAG_MIN = "2.6.0";
23
+ /** Oldest kiro-cli AgEnD claims: older ones still launch, with a warning. */
24
+ export const KIRO_SUPPORTED_MIN = "2.21.0";
25
+ /**
26
+ * Newest kiro-cli run live under AgEnD. Above it the launch flags come from
27
+ * the binary's own --help instead of the version table, and the operator is
28
+ * told the version is unverified.
29
+ */
30
+ export const KIRO_TESTED_MAX = "2.27.0";
19
31
  const UNKNOWN_KIRO_COMPATIBILITY = {
20
- supportsRequireMcpStartup: false,
21
32
  supportsLegacyUi: false,
33
+ supportsTui: false,
34
+ supportsV3: false,
35
+ agentEngines: null,
22
36
  supportsEffortFlag: false,
23
37
  source: "unknown",
24
38
  };
25
39
  const compatibilityCache = new Map();
26
40
  const warnedUnsupportedEffortCacheKeys = new Set();
41
+ /** Binary generations whose version-gate warning already went out. */
42
+ const warnedVersionGateCacheKeys = new Set();
27
43
  /**
28
44
  * A picker may change its model labels or credit wording between Kiro releases.
29
45
  * This broad test is only a safety hold: it must never select an option or
@@ -127,6 +143,123 @@ export function kiroUnavailableModelPickerActive(pane) {
127
143
  }
128
144
  return cursorCount === 1 && modelCount >= 1;
129
145
  }
146
+ const KIRO_PROMPT_CURSOR = /^[❯›>]$/;
147
+ const KIRO_PROMPT_FOOTER = /navigate|select|↑|↓|\benter\b|\besc\b/i;
148
+ /**
149
+ * Whether a kiro launch prompt owns the pane right now, and where its cursor
150
+ * is. Bottom-anchored on purpose: below the header come only its description,
151
+ * then exactly its option rows in order, then at most a short key-hint footer.
152
+ * A quoted copy of the prompt in a transcript has the reply and the composer
153
+ * row (`12% !>`) below it, so it is not active and never receives a key.
154
+ *
155
+ * The cursor is known only when exactly one option row carries a glyph AgEnD
156
+ * recognises; any other glyph, none, or two is "unknown" — still active, so
157
+ * it is held for a human, never answered.
158
+ */
159
+ export function kiroLaunchPromptState(pane, spec) {
160
+ const none = { active: false, cursor: null };
161
+ const rows = pane.replace(/\r/g, "").split("\n").map(row => row.replace(/\s+$/, ""));
162
+ let header = -1;
163
+ for (let i = rows.length - 1; i >= 0; i--) {
164
+ if (rows[i].includes(spec.header)) {
165
+ header = i;
166
+ break;
167
+ }
168
+ }
169
+ if (header < 0)
170
+ return none;
171
+ const body = rows.slice(header + 1);
172
+ const optionRow = (row, label) => {
173
+ const match = /^\s*(?:(\S)\s+)?(.*)$/.exec(row);
174
+ if (match && match[2].startsWith(label))
175
+ return { glyph: match[1] ?? null };
176
+ // No glyph: the label itself starts the row.
177
+ return row.trim().startsWith(label) ? { glyph: null } : null;
178
+ };
179
+ const first = body.findIndex(row => optionRow(row, spec.options[0]) !== null);
180
+ if (first < 0)
181
+ return none;
182
+ // A composer row anywhere below the header — before the options or after
183
+ // them — means this is quoted history with the input box back on screen.
184
+ // Checked before the footer test, whose keywords (enter/select/esc) a
185
+ // composer line can contain.
186
+ if (body.some(row => KIRO_COMPOSER_ROW.test(row)))
187
+ return none;
188
+ const optionRows = body.slice(first).filter(row => row.trim());
189
+ if (optionRows.length < spec.options.length)
190
+ return none;
191
+ const glyphs = [];
192
+ for (let i = 0; i < spec.options.length; i++) {
193
+ const hit = optionRow(optionRows[i], spec.options[i]);
194
+ if (!hit)
195
+ return none;
196
+ glyphs.push(hit.glyph);
197
+ }
198
+ const trailing = optionRows.slice(spec.options.length);
199
+ if (trailing.length > 2 || !trailing.every(row => KIRO_PROMPT_FOOTER.test(row)))
200
+ return none;
201
+ const marked = glyphs.map((g, i) => (g !== null ? i : -1)).filter(i => i >= 0);
202
+ if (marked.length !== 1 || !KIRO_PROMPT_CURSOR.test(glyphs[marked[0]]))
203
+ return { active: true, cursor: null };
204
+ return { active: true, cursor: marked[0] };
205
+ }
206
+ /** kiro's input row: legacy `12% !>` / `[agent] 3% λ !>`, or a bare `>`/`❯`. */
207
+ const KIRO_COMPOSER_ROW = /^\s*(?:\[[^\]]*\]\s*)?\d+%\s*\S{0,2}\s*!?\s*[❯>]|^\s*[!❯>]\s*$/;
208
+ /**
209
+ * Launch prompts that would move an instance off its engine (#1109). Text
210
+ * from the kiro-cli 2.27.0 binary (crates/chat-cli/src/launch/v3_ease_in.rs,
211
+ * auto_migrate.rs); not yet seen live — the V3 prompt is offered to 25% of
212
+ * internal users only.
213
+ *
214
+ * The first option of each is the one that switches, so it is never confirmed:
215
+ * a cursor verified on it gets ONE Down, and only a later capture that shows
216
+ * the cursor on the option that changes nothing gets Enter. Anything else —
217
+ * an unrecognised cursor, "Don't ask again" (saved for the whole machine), or
218
+ * a Down that did not move the cursor — is held for a human: deliveries stay
219
+ * blocked and the parked-dialog report fires.
220
+ */
221
+ const KIRO_V3_EASE_IN = {
222
+ header: "CLI 3.0 is becoming the default experience",
223
+ options: ["Switch to 3.0 and upgrade my configs", "Remind me later", "Don't ask again"],
224
+ };
225
+ const KIRO_AGENT_UPGRADE = {
226
+ header: "Your agent configs are still in the 2.0 format",
227
+ options: ["Enable auto-upgrade", "Not now"],
228
+ };
229
+ function kiroLaunchPromptDialogs(spec, name, key, keep) {
230
+ const pattern = new RegExp(spec.header.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
231
+ const state = (pane) => kiroLaunchPromptState(pane, spec);
232
+ const guarded = { pattern, blocksDelivery: true, inputBlocked: true, verifyAfterKeys: true };
233
+ return [
234
+ {
235
+ ...guarded,
236
+ keys: ["Down"],
237
+ description: `${name} — cursor on '${spec.options[0]}': one step down`,
238
+ isActive: pane => { const s = state(pane); return s.active && s.cursor === 0; },
239
+ autoResolutionKey: `${key}-step`,
240
+ },
241
+ {
242
+ ...guarded,
243
+ keys: ["Enter"],
244
+ description: `${name} — confirm '${spec.options[keep]}'`,
245
+ isActive: pane => { const s = state(pane); return s.active && s.cursor === keep; },
246
+ autoResolutionKey: `${key}-confirm`,
247
+ },
248
+ {
249
+ pattern,
250
+ keys: [],
251
+ holdOnly: true,
252
+ blocksDelivery: true,
253
+ inputBlocked: true,
254
+ description: `${name} — not on a choice AgEnD can make safely; holding for a human`,
255
+ isActive: pane => state(pane).active,
256
+ },
257
+ ];
258
+ }
259
+ const KIRO_ENGINE_PROMPT_DIALOGS = [
260
+ ...kiroLaunchPromptDialogs(KIRO_V3_EASE_IN, "Kiro V3 ease-in prompt", "kiro-v3-ease-in", 1),
261
+ ...kiroLaunchPromptDialogs(KIRO_AGENT_UPGRADE, "Kiro 3.0 agent-config upgrade prompt", "kiro-agent-upgrade", 1),
262
+ ];
130
263
  function parseSemver(value) {
131
264
  const match = value?.match(/\b(\d+)\.(\d+)\.(\d+)\b/);
132
265
  if (!match)
@@ -142,9 +275,83 @@ function versionAtLeast(version, minimum) {
142
275
  return true;
143
276
  }
144
277
  function helpAdvertisesFlag(help, flag) {
145
- return new RegExp(`^\\s*${flag}(?:[ =<]|$)`, "m").test(help);
278
+ // clap prints a short alias first when there is one: ` -r, --resume`.
279
+ return new RegExp(`^\\s*(?:-[A-Za-z0-9],\\s*)?${flag}(?:[ =<,]|$)`, "m").test(help);
146
280
  }
147
- /** Probe once per backend construction, falling back from semver to --help. */
281
+ /**
282
+ * The values `chat --help` lists for --agent-engine, or null without the flag.
283
+ * clap prints them as `[possible values: v2, v1, v3]` a few lines below the
284
+ * flag; the quoted names in its description are the fallback.
285
+ */
286
+ export function parseKiroAgentEngines(help) {
287
+ const lines = help.split("\n");
288
+ const at = lines.findIndex(line => KIRO_HELP_OPTION_ROW.test(line) && /--agent-engine\b/.test(line));
289
+ if (at < 0)
290
+ return null;
291
+ // Only this option's own description/value block: it ends at the next
292
+ // option row, so a neighbour's `[possible values: …]` is never read as ours.
293
+ let end = at + 1;
294
+ while (end < lines.length && !KIRO_HELP_OPTION_ROW.test(lines[end]))
295
+ end++;
296
+ const block = lines.slice(at, end);
297
+ for (const line of block) {
298
+ const listed = line.match(/\[possible values:\s*([^\]]+)\]/);
299
+ if (listed)
300
+ return listed[1].split(",").map(v => v.trim()).filter(Boolean);
301
+ }
302
+ const quoted = [...block.slice(1).join(" ").matchAll(/"([a-z0-9]+)"/gi)].map(m => m[1]);
303
+ // Present but without legible values: [] — nothing to pin to, so refused.
304
+ return [...new Set(quoted)];
305
+ }
306
+ /** A clap option row: ` -r, --resume`, ` --agent-engine <ENGINE>`. */
307
+ const KIRO_HELP_OPTION_ROW = /^\s*(?:-[A-Za-z0-9],\s*)?--[a-z][a-z0-9-]*/;
308
+ /**
309
+ * Whether `help` is kiro's `chat --help` at all. Every release from 1.26 to
310
+ * 2.27 prints the clap usage line and the --trust-all-tools / --resume options
311
+ * AgEnD launches with. An empty or truncated help proves nothing — in
312
+ * particular not that the binary is old enough to have no engine to pin.
313
+ */
314
+ function isKiroChatHelp(help) {
315
+ return /^Usage:\s+\S*kiro\S*\s+chat\b/m.test(help)
316
+ && helpAdvertisesFlag(help, "--trust-all-tools")
317
+ && helpAdvertisesFlag(help, "--resume");
318
+ }
319
+ /** What a released kiro-cli version accepts, per the table above. */
320
+ function compatibilityFromVersion(version, parsed) {
321
+ const has = (min) => versionAtLeast(parsed, min);
322
+ return {
323
+ version,
324
+ supportsLegacyUi: has(KIRO_LEGACY_UI_MIN),
325
+ supportsTui: has(KIRO_LEGACY_UI_MIN),
326
+ supportsV3: has("2.8.0"),
327
+ agentEngines: has("2.8.0") ? ["v2", "v1", "v3"]
328
+ : has("2.4.0") ? ["v2", "v1", "kas"]
329
+ : has("2.3.0") ? ["rust", "kas"]
330
+ : null,
331
+ supportsEffortFlag: has(KIRO_EFFORT_FLAG_MIN),
332
+ source: "version",
333
+ };
334
+ }
335
+ function compatibilityFromHelp(version, help) {
336
+ if (!isKiroChatHelp(help))
337
+ return { ...UNKNOWN_KIRO_COMPATIBILITY, version };
338
+ return {
339
+ version,
340
+ supportsLegacyUi: helpAdvertisesFlag(help, "--legacy-ui"),
341
+ supportsTui: helpAdvertisesFlag(help, "--tui"),
342
+ supportsV3: helpAdvertisesFlag(help, "--v3"),
343
+ agentEngines: parseKiroAgentEngines(help),
344
+ supportsEffortFlag: helpAdvertisesFlag(help, "--effort"),
345
+ source: "help",
346
+ };
347
+ }
348
+ /**
349
+ * Probe once per binary generation. A version AgEnD has run (<= TESTED_MAX)
350
+ * is answered from the table without a second CLI call; anything newer, or a
351
+ * version string that does not parse, is answered by the binary's own
352
+ * `chat --help` — 3.0 included, which is exactly where the table stops being
353
+ * evidence.
354
+ */
148
355
  export function probeKiroCliCompatibility(binaryPath, run = (binary, args) => execFileSync(binary, args, {
149
356
  encoding: "utf-8",
150
357
  timeout: 5000,
@@ -156,29 +363,87 @@ export function probeKiroCliCompatibility(binaryPath, run = (binary, args) => ex
156
363
  }
157
364
  catch { /* fall through to capability help */ }
158
365
  const parsed = parseSemver(version);
159
- if (parsed) {
160
- return {
161
- version,
162
- supportsRequireMcpStartup: versionAtLeast(parsed, KIRO_REQUIRE_MCP_MIN),
163
- supportsLegacyUi: versionAtLeast(parsed, KIRO_LEGACY_UI_MIN),
164
- supportsEffortFlag: versionAtLeast(parsed, KIRO_EFFORT_FLAG_MIN),
165
- source: "version",
166
- };
366
+ if (parsed && !versionAtLeast(parsed, nextPatch(KIRO_TESTED_MAX))) {
367
+ return compatibilityFromVersion(version, parsed);
167
368
  }
168
369
  try {
169
- const help = run(binaryPath, ["chat", "--help"]);
170
- return {
171
- version,
172
- supportsRequireMcpStartup: helpAdvertisesFlag(help, "--require-mcp-startup"),
173
- supportsLegacyUi: helpAdvertisesFlag(help, "--legacy-ui"),
174
- supportsEffortFlag: helpAdvertisesFlag(help, "--effort"),
175
- source: "help",
176
- };
370
+ return compatibilityFromHelp(version, run(binaryPath, ["chat", "--help"]));
177
371
  }
178
372
  catch {
373
+ // An untested 2.x whose help cannot be read is still a 2.x; a 3.x or an
374
+ // unidentified binary is not something to guess about.
375
+ if (parsed && !versionAtLeast(parsed, "3.0.0"))
376
+ return compatibilityFromVersion(version, parsed);
179
377
  return { ...UNKNOWN_KIRO_COMPATIBILITY, version };
180
378
  }
181
379
  }
380
+ function nextPatch(version) {
381
+ const [major, minor, patch] = parseSemver(version);
382
+ return `${major}.${minor}.${patch + 1}`;
383
+ }
384
+ /**
385
+ * Pin the UI AND the engine on every launch (#1109). A flag outranks kiro's
386
+ * persisted `chat.agentEngine`, which the V3 ease-in prompt writes for the
387
+ * whole machine and which kiro 3.0 may default to V3. An instance's
388
+ * conversation lives in its engine's store — classic sqlite, V2 JSON, V3 KAS —
389
+ * and moving between them forks it one way, so a launch AgEnD cannot pin to
390
+ * the instance's own engine is refused, never run on whatever kiro defaults to.
391
+ */
392
+ export function planKiroLaunch(ui, compat) {
393
+ const version = compat.version ?? "of unknown version";
394
+ if (ui === "v3")
395
+ return { kind: "launch", ui, flags: ["--v3"] };
396
+ const engines = compat.agentEngines;
397
+ // A binary with no engine choice at all (before 2.3) has nowhere to drift —
398
+ // but only a version AgEnD knows proves that. Read from --help, a missing
399
+ // selector is just a missing selector: a new release that dropped it must
400
+ // not be launched on its default.
401
+ const knownOld = compat.source === "version";
402
+ const singleEngine = knownOld && engines === null && !compat.supportsV3;
403
+ // 2.3 offered rust|kas with rust (the legacy engine) as its default. Only
404
+ // the version table may say "this is 2.3": a newer help that happens to
405
+ // list `rust` without v1/v2 is a new release, and its default is unknown.
406
+ const rustEra = knownOld && engines?.includes("rust") === true && !compat.supportsV3;
407
+ if (ui === "legacy") {
408
+ if (!compat.supportsLegacyUi) {
409
+ // Before 1.27 classic was the only UI: nothing to select.
410
+ if (!compat.supportsTui && singleEngine)
411
+ return { kind: "launch", ui, flags: [] };
412
+ return {
413
+ kind: "refuse",
414
+ reason: `kiro-cli ${version} no longer offers the legacy UI (--legacy-ui) this instance runs on. `
415
+ + "AgEnD will not start it on another UI or engine: its conversation would not come along (#1109). "
416
+ + "Install a kiro-cli 2.x to keep using it; moving instances to the terminal UI is #1110.",
417
+ };
418
+ }
419
+ if (engines?.includes("v1"))
420
+ return { kind: "launch", ui, flags: ["--legacy-ui", "--agent-engine=v1"] };
421
+ if (singleEngine || rustEra)
422
+ return { kind: "launch", ui, flags: ["--legacy-ui"] };
423
+ return {
424
+ kind: "refuse",
425
+ reason: `kiro-cli ${version} has --legacy-ui but no v1 agent engine to pin it to `
426
+ + `(--agent-engine accepts: ${engines?.join(", ") || "nothing AgEnD recognises"}). `
427
+ + "AgEnD will not let it pick an engine on its own (#1109).",
428
+ };
429
+ }
430
+ // tui
431
+ if (!compat.supportsTui) {
432
+ if (singleEngine)
433
+ return { kind: "launch", ui, flags: [] };
434
+ return { kind: "refuse", reason: `kiro-cli ${version} has no --tui flag; AgEnD cannot tell which UI it would start (#1109).` };
435
+ }
436
+ if (engines?.includes("v2"))
437
+ return { kind: "launch", ui, flags: ["--tui", "--agent-engine=v2"] };
438
+ if (singleEngine || rustEra)
439
+ return { kind: "launch", ui, flags: ["--tui"] };
440
+ return {
441
+ kind: "refuse",
442
+ reason: `kiro-cli ${version} has no v2 agent engine to pin the terminal UI to `
443
+ + `(--agent-engine accepts: ${engines?.join(", ") || "nothing AgEnD recognises"}). `
444
+ + "AgEnD will not let it pick an engine on its own (#1109).",
445
+ };
446
+ }
182
447
  function kiroBinaryCacheKey(binaryPath) {
183
448
  try {
184
449
  const stat = statSync(binaryPath);
@@ -193,12 +458,19 @@ function kiroBinaryCacheKey(binaryPath) {
193
458
  return `${binaryPath}\0unavailable`;
194
459
  }
195
460
  }
461
+ /**
462
+ * How long an "unknown" probe result is reused. A binary that exists but did
463
+ * not answer (a 5s timeout on a loaded host) refuses its launch, and the
464
+ * fleet's startup retry must get a fresh probe rather than the same miss.
465
+ */
466
+ const UNKNOWN_COMPATIBILITY_TTL_MS = 60_000;
196
467
  function cachedKiroCliCompatibility(binaryPath, run) {
197
468
  const cacheKey = kiroBinaryCacheKey(binaryPath);
198
469
  const cached = compatibilityCache.get(cacheKey);
199
- if (cached)
470
+ if (cached && (cached.compatibility.source !== "unknown" || Date.now() - cached.probedAt < UNKNOWN_COMPATIBILITY_TTL_MS)) {
200
471
  return cached;
201
- const entry = { cacheKey, compatibility: probeKiroCliCompatibility(binaryPath, run) };
472
+ }
473
+ const entry = { cacheKey, compatibility: probeKiroCliCompatibility(binaryPath, run), probedAt: Date.now() };
202
474
  compatibilityCache.set(cacheKey, entry);
203
475
  return entry;
204
476
  }
@@ -210,6 +482,7 @@ export function getCachedKiroCliCompatibility(binaryPath, run) {
210
482
  export function resetKiroCompatibilityCacheForTests() {
211
483
  compatibilityCache.clear();
212
484
  warnedUnsupportedEffortCacheKeys.clear();
485
+ warnedVersionGateCacheKeys.clear();
213
486
  }
214
487
  /** Startup budget for a `--resume` launch (60% first output, 40% ready). */
215
488
  export const KIRO_RESUME_STARTUP_BUDGET_MS = 60_000;
@@ -219,6 +492,8 @@ export class KiroBackend {
219
492
  binaryPath;
220
493
  compatibility;
221
494
  compatibilityCacheKey;
495
+ /** Injected by tests: never re-probed. */
496
+ fixedCompatibility;
222
497
  warnedUnsupportedEffort = false;
223
498
  /**
224
499
  * UI flavour and trust mode of the LAST command built. The Enter-drop
@@ -230,9 +505,12 @@ export class KiroBackend {
230
505
  */
231
506
  activeUi = "legacy";
232
507
  activeTrustAll = true;
508
+ /** Version-gate notice for the launch just built (consumeLaunchWarning). */
509
+ launchWarning = null;
233
510
  constructor(instanceDir, compatibility) {
234
511
  this.instanceDir = instanceDir;
235
512
  this.binaryPath = resolveBinary("kiro-cli");
513
+ this.fixedCompatibility = compatibility !== undefined;
236
514
  if (compatibility) {
237
515
  this.compatibility = compatibility;
238
516
  }
@@ -308,15 +586,32 @@ export class KiroBackend {
308
586
  }
309
587
  buildCommand(config) {
310
588
  const ui = config.kiroUi ?? "legacy";
589
+ // Every launch re-reads the binary generation: a crash-respawn reuses this
590
+ // backend, and kiro-cli may have replaced itself in place since the last
591
+ // launch (an auto-update to a release without this instance's engine).
592
+ // Cheap — a stat and a map lookup unless the binary changed.
593
+ if (!this.fixedCompatibility) {
594
+ const current = cachedKiroCliCompatibility(this.binaryPath);
595
+ this.compatibility = current.compatibility;
596
+ this.compatibilityCacheKey = current.cacheKey;
597
+ }
598
+ if (this.compatibility.source === "unknown") {
599
+ // Neither --version nor chat --help answered: a missing or wedged binary,
600
+ // or a loaded host timing out. Launching blind could start kiro on its
601
+ // default engine; fail this attempt and let the startup retry re-probe.
602
+ throw new Error(`kiro-cli at ${this.binaryPath} did not answer --version or chat --help, so AgEnD cannot pin its UI and engine; not launching (#1109)`);
603
+ }
604
+ const plan = planKiroLaunch(ui, this.compatibility);
605
+ if (plan.kind === "refuse")
606
+ throw new UnsupportedCliError(plan.reason);
311
607
  let cmd = `${this.binaryPath} chat`;
312
- if (ui === "legacy" && this.compatibility.supportsLegacyUi)
313
- cmd += " --legacy-ui";
314
- else if (ui === "v3")
315
- cmd += " --v3";
608
+ for (const flag of plan.flags)
609
+ cmd += ` ${flag}`;
610
+ this.noteVersionGate();
316
611
  // Record what is actually being launched for the delivery gate (see
317
- // dropsEnterWhileBusy): a "legacy" request on a binary without --legacy-ui
318
- // runs the default new TUI, not the legacy screen.
319
- this.activeUi = ui === "legacy" && this.compatibility.supportsLegacyUi ? "legacy" : ui === "v3" ? "v3" : "tui";
612
+ // dropsEnterWhileBusy): the legacy prompt row exists only under
613
+ // --legacy-ui; a binary from before 1.27 paints its own classic screen.
614
+ this.activeUi = plan.flags.includes("--legacy-ui") ? "legacy" : ui === "v3" ? "v3" : "tui";
320
615
  this.activeTrustAll = config.skipPermissions !== false;
321
616
  if (config.skipPermissions !== false)
322
617
  cmd += " --trust-all-tools";
@@ -351,8 +646,14 @@ export class KiroBackend {
351
646
  console.warn(`[agend] kiro-cli ${detected} does not support the --effort launch flag (requires >= ${KIRO_EFFORT_FLAG_MIN}); configured effort "${effort}" was not applied`);
352
647
  }
353
648
  }
354
- if (this.compatibility.supportsRequireMcpStartup)
355
- cmd += " --require-mcp-startup";
649
+ // Deliberately NOT `--require-mcp-startup` (#1111). Kiro has no per-server
650
+ // "required": the flag exits 3 when ANY enabled MCP server fails, and that
651
+ // includes the user's own servers from ~/.kiro/settings/mcp.json. One
652
+ // third-party server breaking on a kiro-cli update (outline, 2.27) took
653
+ // down every kiro instance at once. Like claude and codex, a broken
654
+ // third-party server now costs only its own tools; whether AgEnD's fleet
655
+ // server connected is the daemon's check (fleet MCP startup watch), which
656
+ // knows which server is which.
356
657
  return this.withCredentialProfile(config, cmd);
357
658
  }
358
659
  /**
@@ -371,6 +672,36 @@ export class KiroBackend {
371
672
  prepareCredentialProfileHome(spec, home);
372
673
  return `${spec.env}=${shellQuote(home)} ${cmd}`;
373
674
  }
675
+ /**
676
+ * P3 of #1109: tell the operator, once per binary generation, when the
677
+ * kiro-cli in use is outside the range AgEnD has run. Never blocks a launch:
678
+ * below KIRO_SUPPORTED_MIN it still works, and above KIRO_TESTED_MAX the
679
+ * flags were already checked against the binary's own --help.
680
+ */
681
+ noteVersionGate() {
682
+ const parsed = parseSemver(this.compatibility.version);
683
+ if (!parsed)
684
+ return;
685
+ const key = this.compatibilityCacheKey ?? `instance:${this.compatibility.version}`;
686
+ if (warnedVersionGateCacheKeys.has(key))
687
+ return;
688
+ let warning = null;
689
+ if (!versionAtLeast(parsed, KIRO_SUPPORTED_MIN)) {
690
+ warning = t("kiro.version_below_supported", this.compatibility.version, KIRO_SUPPORTED_MIN);
691
+ }
692
+ else if (versionAtLeast(parsed, nextPatch(KIRO_TESTED_MAX))) {
693
+ warning = t("kiro.version_untested", this.compatibility.version, KIRO_TESTED_MAX);
694
+ }
695
+ if (!warning)
696
+ return;
697
+ warnedVersionGateCacheKeys.add(key);
698
+ this.launchWarning = warning;
699
+ }
700
+ consumeLaunchWarning() {
701
+ const warning = this.launchWarning;
702
+ this.launchWarning = null;
703
+ return warning;
704
+ }
374
705
  shouldWarnUnsupportedEffort() {
375
706
  if (!this.compatibilityCacheKey) {
376
707
  if (this.warnedUnsupportedEffort)
@@ -631,6 +962,7 @@ export class KiroBackend {
631
962
  }
632
963
  getStartupDialogs() {
633
964
  return [
965
+ ...KIRO_ENGINE_PROMPT_DIALOGS,
634
966
  {
635
967
  // Kiro CLI --trust-all-tools now shows a confirmation prompt.
636
968
  // Default cursor is on "No, exit" — press Down then Enter to select "Yes, I accept".
@@ -642,6 +974,8 @@ export class KiroBackend {
642
974
  }
643
975
  getRuntimeDialogs() {
644
976
  return [
977
+ // The engine prompts can paint after the startup scan has moved on.
978
+ ...KIRO_ENGINE_PROMPT_DIALOGS,
645
979
  {
646
980
  // A service/model outage can drop Kiro into the interactive /model
647
981
  // picker. Its choices range across credit multipliers and capability