impel-cli 0.20.40 → 0.20.42

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.
package/src/cli.js CHANGED
@@ -21,6 +21,7 @@ import { cmdNuke } from "./commands/nuke.js";
21
21
  import { cmdExperimental } from "./commands/experimental.js";
22
22
  import { cmdConverge } from "./commands/converge.js";
23
23
  import { cmdRemote } from "./commands/remote.js";
24
+ import { cmdNative } from "./commands/native.js";
24
25
  import { ensureMacDeveloperTools } from "./macDeveloperTools.js";
25
26
  import { refuseElevatedMacExecution } from "./privileges.js";
26
27
 
@@ -42,6 +43,8 @@ Work:
42
43
  impel tenant list List accessible organizations
43
44
  impel tenant current Show the CLI's current organization
44
45
  impel tenant use <org> Select the organization for CLI launches
46
+ impel native answer --tenant <t> --agent <id> --prompt <task> [--json]
47
+ Ask the named managed agent with attribution
45
48
 
46
49
  Workspace:
47
50
  impel tasks list|get|create|update|delete Work with Impel tickets (aliases: task, ticket[s])
@@ -128,6 +131,9 @@ export async function main(argv) {
128
131
  case "remote":
129
132
  return cmdRemote(rest);
130
133
 
134
+ case "native":
135
+ return cmdNative(rest);
136
+
131
137
  case "status":
132
138
  return cmdStatus();
133
139
 
@@ -55,6 +55,7 @@ import {
55
55
  findWindowsChatGPTApp,
56
56
  findManagedWindowsChatGPTApp,
57
57
  launchWindowsClaudeApp,
58
+ pinnedWindowsChatGPTApp,
58
59
  launchWindowsChatGPTApp,
59
60
  removeManagedWindowsChatGPTApp,
60
61
  stageWindowsChatGPTApp,
@@ -280,6 +281,7 @@ const WINGET_EXIT_HINTS = new Map([
280
281
  [0x8A150014, "no package matched the winget query"],
281
282
  [0x8A15001B, "the Microsoft Store client is blocked by policy"],
282
283
  [0x8A15001C, "the Microsoft Store app is blocked by policy"],
284
+ [0x8A15002B, "a Store package is already installed but does not match the reviewed pin — its version likely moved past the pinned release"],
283
285
  ]);
284
286
 
285
287
  function windowsProcessFailure(result) {
@@ -380,7 +382,7 @@ export async function reconcileWindowsTenantApps({
380
382
  findClaudeApp: findWindowsClaudeApp,
381
383
  ensureClaudeApp: ensureWindowsClaudeApp,
382
384
  claudeUserData: windowsClaudeUserData,
383
- findChatGPTApp: findWindowsChatGPTApp,
385
+ findChatGPTApp: (environment) => pinnedWindowsChatGPTApp(environment),
384
386
  ensureChatGPTApp: ensureWindowsChatGPTApp,
385
387
  findManagedChatGPTApp: findManagedWindowsChatGPTApp,
386
388
  stageChatGPTApp: stageWindowsChatGPTApp,
@@ -664,7 +666,7 @@ export async function cmdWindowsApps(argv, overrides = {}) {
664
666
  ensureClaudeApp: ensureWindowsClaudeApp,
665
667
  launchClaudeApp: launchWindowsClaudeApp,
666
668
  claudeUserData: windowsClaudeUserData,
667
- findChatGPTApp: findWindowsChatGPTApp,
669
+ findChatGPTApp: (environment) => pinnedWindowsChatGPTApp(environment),
668
670
  ensureChatGPTApp: ensureWindowsChatGPTApp,
669
671
  findManagedChatGPTApp: findManagedWindowsChatGPTApp,
670
672
  launchChatGPTApp: launchWindowsChatGPTApp,
@@ -99,6 +99,32 @@ export function impelLaunchArguments(tool, argv, {
99
99
  if (!RUNTIME_BRAND.features.agents) return [...argv];
100
100
  if (tool === "claude") {
101
101
  if (claudeManagedAgent) {
102
+ if (claudeManagedAgent.parentDirect === true) {
103
+ const { parentLaunchDefinition, parentMcpConfigJson } = claudeManagedAgent;
104
+ if (!parentLaunchDefinition
105
+ || typeof parentLaunchDefinition.prompt !== "string"
106
+ || !parentLaunchDefinition.prompt
107
+ || !Array.isArray(parentLaunchDefinition.tools)
108
+ || parentLaunchDefinition.tools.length !== 2
109
+ || typeof parentMcpConfigJson !== "string"
110
+ || !parentMcpConfigJson) {
111
+ throw new Error("managed Claude parent-direct launch configuration is invalid");
112
+ }
113
+ const tools = parentLaunchDefinition.tools.join(",");
114
+ return [
115
+ ...IMPEL_CLAUDE_MANAGED_AGENT_ARGUMENTS,
116
+ "--mcp-config",
117
+ parentMcpConfigJson,
118
+ "--strict-mcp-config",
119
+ "--tools",
120
+ tools,
121
+ "--allowedTools",
122
+ tools,
123
+ "--append-system-prompt",
124
+ parentLaunchDefinition.prompt,
125
+ ...argv,
126
+ ];
127
+ }
102
128
  const { launchName, launchAgentsJson } = claudeManagedAgent;
103
129
  if (typeof launchName !== "string"
104
130
  || !launchName
@@ -198,14 +198,73 @@ export function runNativeAgentMcpServer({
198
198
  output = process.stdout,
199
199
  mode = "durable",
200
200
  telemetry = () => {},
201
+ environment = process.env,
202
+ random = Math.random,
203
+ answerToolDescription = null,
201
204
  }) {
202
205
  if (!["durable", "recovery", "answer"].includes(mode)) throw new Error("invalid native-agent MCP mode");
203
206
  const lines = readline.createInterface({ input, crlfDelay: Infinity });
204
207
  const active = new Map();
205
208
  const pending = new Set();
209
+ const needsUpstream = mode === "durable" || mode === "answer";
210
+ const prewarmController = new AbortController();
211
+ let prewarmTimer = null;
212
+ let prewarmStarted = false;
213
+ let refreshTimer = null;
214
+ let refreshInFlight = false;
206
215
  let progress = 0;
207
216
  const write = (value) => output.write(`${value}\n`);
208
217
 
218
+ function startPrewarm() {
219
+ if (!needsUpstream || prewarmStarted) return;
220
+ prewarmStarted = true;
221
+ if (prewarmTimer) {
222
+ clearTimeout(prewarmTimer);
223
+ prewarmTimer = null;
224
+ }
225
+ Promise.resolve()
226
+ .then(() => transport.prepareNewSession(prewarmController.signal, { prewarmed: true }))
227
+ .catch(() => {});
228
+ }
229
+
230
+ function schedulePrewarm() {
231
+ if (!needsUpstream || prewarmStarted || prewarmTimer) return;
232
+ const jitterMs = 50 + Math.min(
233
+ 200,
234
+ Math.floor(Math.max(0, Math.min(1, random())) * 201),
235
+ );
236
+ prewarmTimer = setTimeout(() => {
237
+ prewarmTimer = null;
238
+ startPrewarm();
239
+ }, jitterMs);
240
+ prewarmTimer.unref?.();
241
+ }
242
+
243
+ function refreshIntervalMs() {
244
+ const configured = environment?.IMPEL_NATIVE_PREWARM_REFRESH_MS;
245
+ if (configured === undefined || configured === "0") return 0;
246
+ if (!/^\d{1,9}$/u.test(configured)) return 0;
247
+ const parsed = Number(configured);
248
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : 0;
249
+ }
250
+
251
+ telemetry("local_server_started");
252
+ schedulePrewarm();
253
+ const refreshMs = needsUpstream ? refreshIntervalMs() : 0;
254
+ if (refreshMs > 0) {
255
+ refreshTimer = setInterval(() => {
256
+ if (refreshInFlight || prewarmController.signal.aborted) return;
257
+ refreshInFlight = true;
258
+ Promise.resolve()
259
+ .then(() => typeof transport.refreshPreparedSession === "function"
260
+ ? transport.refreshPreparedSession(prewarmController.signal, { prewarmed: true })
261
+ : transport.prepareNewSession(prewarmController.signal, { prewarmed: true }))
262
+ .catch(() => {})
263
+ .finally(() => { refreshInFlight = false; });
264
+ }, refreshMs);
265
+ refreshTimer.unref?.();
266
+ }
267
+
209
268
  function schedule(message) {
210
269
  const request = tasksJsonRpcRequestId(message);
211
270
  if (!request.valid) {
@@ -227,6 +286,7 @@ export function runNativeAgentMcpServer({
227
286
  let telemetryTool;
228
287
  try {
229
288
  if (message.method === "initialize") {
289
+ startPrewarm();
230
290
  write(nativeRpcResult(message.id, {
231
291
  protocolVersion: "2025-06-18",
232
292
  capabilities: { tools: {} },
@@ -239,8 +299,13 @@ export function runNativeAgentMcpServer({
239
299
  return;
240
300
  }
241
301
  if (message.method === "tools/list") {
302
+ const tools = nativeAgentCompositeTools({ mode });
303
+ if (mode === "answer" && typeof answerToolDescription === "string") {
304
+ const answerTool = tools.find(({ name }) => name === NATIVE_AGENT_ANSWER_TOOL);
305
+ if (answerTool) answerTool.description = answerToolDescription;
306
+ }
242
307
  write(nativeRpcResult(message.id, {
243
- tools: nativeAgentCompositeTools({ mode }),
308
+ tools,
244
309
  }));
245
310
  return;
246
311
  }
@@ -326,6 +391,9 @@ export function runNativeAgentMcpServer({
326
391
  });
327
392
  return new Promise((resolve) => {
328
393
  lines.on("close", async () => {
394
+ if (prewarmTimer) clearTimeout(prewarmTimer);
395
+ if (refreshTimer) clearInterval(refreshTimer);
396
+ prewarmController.abort();
329
397
  for (const controller of active.values()) controller.abort();
330
398
  await Promise.allSettled([...pending]);
331
399
  resolve();
@@ -340,6 +408,7 @@ export async function cmdMcp(argv = []) {
340
408
  "agent-id": { type: "string" },
341
409
  "scope-param": { type: "string" },
342
410
  "policy-fingerprint": { type: "string" },
411
+ "agent-title": { type: "string" },
343
412
  "recovery-only": { type: "boolean" },
344
413
  "answer-only": { type: "boolean" },
345
414
  });
@@ -361,7 +430,7 @@ export async function cmdMcp(argv = []) {
361
430
  if (nativeAgentTarget) {
362
431
  const expectedFlags = [
363
432
  "target", "tenant", "agent-id", "scope-param", "policy-fingerprint",
364
- "recovery-only", "answer-only",
433
+ "agent-title", "recovery-only", "answer-only",
365
434
  ];
366
435
  const unsupportedFlags = Object.keys(flags).filter((name) => !expectedFlags.includes(name));
367
436
  if (unsupportedFlags.length > 0 || positionals.length > 0) {
@@ -375,9 +444,18 @@ export async function cmdMcp(argv = []) {
375
444
  if (flags["recovery-only"] === true && flags["answer-only"] === true) {
376
445
  throw new Error("native-agent MCP cannot be both recovery-only and answer-only");
377
446
  }
447
+ if (Object.hasOwn(flags, "agent-title") && (
448
+ flags["answer-only"] !== true
449
+ || typeof flags["agent-title"] !== "string"
450
+ || !flags["agent-title"].trim()
451
+ || flags["agent-title"].length > 160
452
+ )) {
453
+ throw new Error("native-agent MCP --agent-title requires an answer-only binding");
454
+ }
378
455
  } else if (!tasksTarget && (Object.hasOwn(flags, "agent-id")
379
456
  || Object.hasOwn(flags, "scope-param")
380
457
  || Object.hasOwn(flags, "policy-fingerprint")
458
+ || Object.hasOwn(flags, "agent-title")
381
459
  || Object.hasOwn(flags, "recovery-only")
382
460
  || Object.hasOwn(flags, "answer-only"))) {
383
461
  throw new Error("native-agent binding flags require `--target native-agent`");
@@ -400,7 +478,6 @@ export async function cmdMcp(argv = []) {
400
478
  ? "recovery"
401
479
  : (flags["answer-only"] === true ? "answer" : "durable"),
402
480
  });
403
- telemetry("local_server_started");
404
481
  return runNativeAgentMcpServer({
405
482
  transport: new NativeAgentCompositeTransport({
406
483
  tenantId,
@@ -415,6 +492,12 @@ export async function cmdMcp(argv = []) {
415
492
  ? "recovery"
416
493
  : (flags["answer-only"] === true ? "answer" : "durable"),
417
494
  telemetry,
495
+ ...(flags["agent-title"] ? {
496
+ answerToolDescription: [
497
+ `Ask the managed agent ${JSON.stringify(flags["agent-title"])} (${flags["agent-id"]}) exactly once through this fixed binding.`,
498
+ `On success, present the returned finalText verbatim and explicitly attribute it to managed agent ${JSON.stringify(flags["agent-title"])}; do not spawn a relay subagent.`,
499
+ ].join(" "),
500
+ } : {}),
418
501
  });
419
502
  }
420
503
  const endpoint = tasksTarget ? null : `${gatewayUrl}/mcp`;
@@ -0,0 +1,285 @@
1
+ import crypto from "node:crypto";
2
+
3
+ import { parseFlags } from "../args.js";
4
+ import {
5
+ agentProfileRoot,
6
+ managedClaudeAnswerBindings,
7
+ resolveManagedClaudeAnswerBinding,
8
+ } from "../agents.js";
9
+ import {
10
+ loadConfig,
11
+ redactSecretText,
12
+ resolveDefaultGateway,
13
+ } from "../config.js";
14
+ import { extractAnswerFinalText } from "../directAnswer.js";
15
+ import { createNativeAgentTelemetry } from "../nativeAgentTelemetry.js";
16
+ import {
17
+ MANAGED_NATIVE_INTERCEPT_FLAG,
18
+ nativeInterceptTimeoutMs,
19
+ } from "../nativeInterception.js";
20
+ import {
21
+ NATIVE_AGENT_CONTINUATION_SCHEMA,
22
+ NATIVE_AGENT_HANDLE_SCHEMA,
23
+ NATIVE_AGENT_RESULT_SCHEMA,
24
+ NativeAgentCompositeTransport,
25
+ } from "../nativeAgentTransport.js";
26
+ import { normalizeTenantId, tenantCredential } from "../tenants.js";
27
+ import { readHookInput } from "../sessionCollector.js";
28
+
29
+ const ANSWER_SPEC = {
30
+ tenant: { type: "string" },
31
+ agent: { type: "string" },
32
+ prompt: { type: "string" },
33
+ json: { type: "boolean" },
34
+ };
35
+ const INTERCEPT_SPEC = {
36
+ tenant: { type: "string" },
37
+ [MANAGED_NATIVE_INTERCEPT_FLAG]: { type: "boolean" },
38
+ };
39
+
40
+ function abortError() {
41
+ const error = new Error("native-agent answer timed out or was cancelled");
42
+ error.name = "AbortError";
43
+ return error;
44
+ }
45
+
46
+ function throwIfAborted(signal) {
47
+ if (signal?.aborted) throw abortError();
48
+ }
49
+
50
+ function terminalAnswer(value) {
51
+ const direct = extractAnswerFinalText(value);
52
+ if (direct !== null) {
53
+ return { ok: true, finalText: direct, ...(value.runId ? { runId: value.runId } : {}) };
54
+ }
55
+ if (value?.schema === NATIVE_AGENT_RESULT_SCHEMA) {
56
+ if (value.status === "succeeded" && typeof value.finalText === "string") {
57
+ return { ok: true, finalText: value.finalText, ...(value.runId ? { runId: value.runId } : {}) };
58
+ }
59
+ return {
60
+ ok: false,
61
+ finalText: typeof value.output === "string" ? value.output : "",
62
+ ...(value.runId ? { runId: value.runId } : {}),
63
+ error: redactSecretText(
64
+ typeof value.error === "string"
65
+ ? value.error
66
+ : value.error?.message || "managed agent answer failed",
67
+ ),
68
+ };
69
+ }
70
+ return null;
71
+ }
72
+
73
+ /**
74
+ * Execute the same fixed-binding, idempotent transport used by the managed
75
+ * MCP adapter, including continuation/resume handling and telemetry.
76
+ */
77
+ export async function answerManagedNativeAgent({
78
+ tenantId,
79
+ agent,
80
+ prompt,
81
+ signal,
82
+ }, {
83
+ config = loadConfig(),
84
+ gatewayUrl = null,
85
+ createTransport = (options) => new NativeAgentCompositeTransport(options),
86
+ createTelemetry = createNativeAgentTelemetry,
87
+ } = {}) {
88
+ if (!config?.pat) {
89
+ throw new Error("not authenticated; run `impel setup` (or `impel auth`) first");
90
+ }
91
+ if (!agent || typeof agent !== "object") throw new Error("managed agent binding is invalid");
92
+ if (typeof prompt !== "string" || !prompt.trim() || prompt.length > 40_000) {
93
+ throw new Error("--prompt must contain between 1 and 40000 characters");
94
+ }
95
+ const normalizedTenant = normalizeTenantId(tenantId);
96
+ const telemetry = createTelemetry({
97
+ tenantId: normalizedTenant,
98
+ agentId: agent.agentId,
99
+ scopeParam: agent.scopeParam,
100
+ mode: "answer",
101
+ });
102
+ const transport = createTransport({
103
+ tenantId: normalizedTenant,
104
+ agentId: agent.agentId,
105
+ scopeParam: agent.scopeParam,
106
+ policyFingerprint: agent.policyFingerprint,
107
+ gatewayUrl: gatewayUrl || config.gatewayUrl || resolveDefaultGateway(),
108
+ credential: tenantCredential(config.pat, normalizedTenant),
109
+ telemetry,
110
+ });
111
+ const correlationId = crypto.randomUUID();
112
+ const startedAt = Date.now();
113
+ telemetry("local_tool_received", { correlationId, tool: "answer_native_agent" });
114
+ try {
115
+ throwIfAborted(signal);
116
+ let value = await transport.answer({ question: prompt, contextKeys: [] }, { signal });
117
+ for (;;) {
118
+ throwIfAborted(signal);
119
+ const terminal = terminalAnswer(value);
120
+ if (terminal) {
121
+ telemetry("local_tool_completed", {
122
+ correlationId,
123
+ tool: "answer_native_agent",
124
+ outcome: terminal.ok ? "succeeded" : "failed",
125
+ durationMs: Date.now() - startedAt,
126
+ durableRunCreated: Boolean(terminal.runId),
127
+ });
128
+ return terminal;
129
+ }
130
+ if (![NATIVE_AGENT_CONTINUATION_SCHEMA, NATIVE_AGENT_HANDLE_SCHEMA].includes(value?.schema)) {
131
+ throw new Error("managed agent answer returned an invalid transport result");
132
+ }
133
+ value = await transport.resume({ handle: value }, { signal });
134
+ }
135
+ } catch (error) {
136
+ telemetry("local_tool_completed", {
137
+ correlationId,
138
+ tool: "answer_native_agent",
139
+ outcome: error?.name === "AbortError" ? "cancelled" : "failed",
140
+ durationMs: Date.now() - startedAt,
141
+ });
142
+ throw error;
143
+ }
144
+ }
145
+
146
+ function answerJson(result) {
147
+ return JSON.stringify({
148
+ ok: result.ok === true,
149
+ finalText: typeof result.finalText === "string" ? result.finalText : "",
150
+ ...(result.runId ? { runId: result.runId } : {}),
151
+ ...(result.error ? { error: redactSecretText(result.error) } : {}),
152
+ });
153
+ }
154
+
155
+ function escapeRegex(value) {
156
+ return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
157
+ }
158
+
159
+ export function matchManagedNativeMention(prompt, bindings) {
160
+ if (typeof prompt !== "string" || !Array.isArray(bindings)) return null;
161
+ // Reject every prompt containing a second @mention, managed or otherwise.
162
+ // This keeps implicit/mixed delegation on the model-mediated path.
163
+ const mentions = prompt.match(/@[a-z0-9][a-z0-9_.:-]*/giu) || [];
164
+ if (mentions.length !== 1) return null;
165
+ for (const binding of bindings) {
166
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/u.test(binding?.name || "")) continue;
167
+ const match = new RegExp(`^@${escapeRegex(binding.name)}\\s+([\\s\\S]+)$`, "u").exec(prompt);
168
+ if (!match || !match[1].trim()) continue;
169
+ return { binding, task: match[1] };
170
+ }
171
+ return null;
172
+ }
173
+
174
+ export async function cmdNativeIntercept(argv, {
175
+ environment = process.env,
176
+ profileRoot = null,
177
+ readInput = readHookInput,
178
+ resolveBindings = managedClaudeAnswerBindings,
179
+ answer = answerManagedNativeAgent,
180
+ timeoutMs = null,
181
+ stdout = (line) => process.stdout.write(`${line}\n`),
182
+ } = {}) {
183
+ try {
184
+ const { flags, positionals } = parseFlags(argv, INTERCEPT_SPEC);
185
+ if (positionals.length > 0
186
+ || Object.keys(flags).some((name) => !Object.hasOwn(INTERCEPT_SPEC, name))
187
+ || flags[MANAGED_NATIVE_INTERCEPT_FLAG] !== true
188
+ || typeof flags.tenant !== "string"
189
+ || !flags.tenant.trim()) {
190
+ return null;
191
+ }
192
+ const tenantId = normalizeTenantId(flags.tenant);
193
+ const input = await readInput();
194
+ if (input.hook_event_name !== "UserPromptSubmit") return null;
195
+ const root = profileRoot || agentProfileRoot("claude", { environment });
196
+ const matched = matchManagedNativeMention(
197
+ input.prompt,
198
+ resolveBindings(root, tenantId),
199
+ );
200
+ if (!matched) return null;
201
+
202
+ const controller = new AbortController();
203
+ const hardTimeoutMs = timeoutMs ?? nativeInterceptTimeoutMs(environment);
204
+ let timeout;
205
+ const deadline = new Promise((resolve) => {
206
+ timeout = setTimeout(() => {
207
+ controller.abort();
208
+ resolve(null);
209
+ }, hardTimeoutMs);
210
+ });
211
+ let result;
212
+ try {
213
+ const answered = Promise.resolve(answer({
214
+ tenantId,
215
+ agent: matched.binding,
216
+ prompt: matched.task,
217
+ signal: controller.signal,
218
+ })).catch(() => null);
219
+ result = await Promise.race([answered, deadline]);
220
+ } finally {
221
+ clearTimeout(timeout);
222
+ }
223
+ if (!result?.ok || typeof result.finalText !== "string") return null;
224
+ stdout(JSON.stringify({
225
+ decision: "block",
226
+ reason: `Response from managed agent ${JSON.stringify(matched.binding.title)} (${matched.binding.agentId}):\n\n${result.finalText}`,
227
+ suppressOriginalPrompt: true,
228
+ }));
229
+ return result;
230
+ } catch {
231
+ // Interception is an optimization only. Invalid input, stale profiles,
232
+ // transport failures, and timeouts all fall through to Claude unchanged.
233
+ return null;
234
+ }
235
+ }
236
+
237
+ export async function cmdNative(argv, {
238
+ environment = process.env,
239
+ profileRoot = null,
240
+ resolveBinding = resolveManagedClaudeAnswerBinding,
241
+ answer = answerManagedNativeAgent,
242
+ stdout = (line) => console.log(line),
243
+ stderr = (line) => console.error(line),
244
+ } = {}) {
245
+ const [action, ...rest] = argv;
246
+ if (action === "intercept") {
247
+ return cmdNativeIntercept(rest, {
248
+ environment,
249
+ profileRoot,
250
+ stdout,
251
+ });
252
+ }
253
+ if (action !== "answer") {
254
+ throw new Error("native command expects `answer --tenant <tenant> --agent <id> --prompt <task> [--json]`");
255
+ }
256
+ const { flags, positionals } = parseFlags(rest, ANSWER_SPEC);
257
+ if (positionals.length > 0) throw new Error("native answer does not accept positional arguments");
258
+ for (const flag of ["tenant", "agent", "prompt"]) {
259
+ if (typeof flags[flag] !== "string" || !flags[flag].trim()) {
260
+ throw new Error(`native answer requires --${flag}`);
261
+ }
262
+ }
263
+ const tenantId = normalizeTenantId(flags.tenant);
264
+ const root = profileRoot || agentProfileRoot("claude", { environment });
265
+ let binding;
266
+ try {
267
+ binding = resolveBinding(root, tenantId, flags.agent);
268
+ const result = await answer({ tenantId, agent: binding, prompt: flags.prompt });
269
+ if (flags.json === true) stdout(answerJson(result));
270
+ else if (result.ok) {
271
+ stdout(`Response from managed agent ${JSON.stringify(binding.title)} (${binding.agentId}):\n\n${result.finalText}`);
272
+ } else {
273
+ stderr(`Managed agent ${JSON.stringify(binding.title)} (${binding.agentId}) failed: ${result.error}`);
274
+ }
275
+ if (!result.ok) process.exitCode = 1;
276
+ return result;
277
+ } catch (error) {
278
+ const message = redactSecretText(error?.message || error);
279
+ const result = { ok: false, finalText: "", error: message };
280
+ if (flags.json === true) stdout(answerJson(result));
281
+ else stderr(`Managed agent ${JSON.stringify(binding?.title || flags.agent)} failed: ${message}`);
282
+ process.exitCode = 1;
283
+ return result;
284
+ }
285
+ }
@@ -15,7 +15,7 @@ import {
15
15
  productAccessLabel,
16
16
  } from "../tenants.js";
17
17
  import { installedVersion, maybePrintUpdateNotice } from "../updates.js";
18
- import { windowsClaudeUserData } from "../windowsApps.js";
18
+ import { findManagedWindowsChatGPTApp, windowsClaudeUserData } from "../windowsApps.js";
19
19
  import { RUNTIME_BRAND } from "../runtimeBrand.js";
20
20
  import { cachedProviderReadiness, providerAvailable } from "../providerReadiness.js";
21
21
 
@@ -52,11 +52,16 @@ export function desktopReadiness(tenant, {
52
52
  states[client] = { app: "unsupported", shell: "unsupported" };
53
53
  continue;
54
54
  }
55
+ // A generated config alone proves nothing launchable on Windows: when the
56
+ // vendor ensure failed, config.toml can exist while no managed app was
57
+ // ever staged — reporting "ready" then hides the exact failure the user
58
+ // is debugging (IMP-474). Require the staged binary too.
55
59
  const appReady = platform === "darwin"
56
60
  ? existsSync(paths[product].launcher)
57
- : existsSync(product === "claude"
58
- ? path.join(paths.claude.userData, "configLibrary", `${CLAUDE_CONFIG_ID}.json`)
59
- : path.join(paths.chatgpt.codexHome, "config.toml"));
61
+ : product === "claude"
62
+ ? existsSync(path.join(paths.claude.userData, "configLibrary", `${CLAUDE_CONFIG_ID}.json`))
63
+ : existsSync(path.join(paths.chatgpt.codexHome, "config.toml"))
64
+ && Boolean(findManagedWindowsChatGPTApp(paths.root, { isFile: existsSync }));
60
65
  let shellReady = appReady && registered.has(product);
61
66
  if (platform === "win32" && appData) {
62
67
  const shortcutPath = path.win32.join(
@@ -0,0 +1,4 @@
1
+ // Fleet generation shared by managed-profile writers and upstream clients.
2
+ // Keep this isolated from apps.js so latency-sensitive transports do not load
3
+ // desktop bundle machinery just to identify their managed config contract.
4
+ export const CURRENT_CONFIG_VERSION = 35;
@@ -12,6 +12,7 @@ const SAFE_EVENTS = new Set([
12
12
  "local_tool_completed",
13
13
  "session_prepared",
14
14
  "upstream_request_completed",
15
+ "hedge_fired",
15
16
  "binding_completed",
16
17
  ]);
17
18
  const SAFE_TOOLS = new Set([
@@ -94,9 +95,12 @@ export function createNativeAgentTelemetry({
94
95
  outcome: SAFE_OUTCOMES.has(fields.outcome) ? fields.outcome : undefined,
95
96
  status: safeId(fields.status),
96
97
  durationMs: safeInteger(fields.durationMs),
98
+ elapsedMs: safeInteger(fields.elapsedMs),
97
99
  pollCount: safeInteger(fields.pollCount),
98
100
  handshakeReused: typeof fields.handshakeReused === "boolean" ? fields.handshakeReused : undefined,
99
101
  bindingReused: typeof fields.bindingReused === "boolean" ? fields.bindingReused : undefined,
102
+ prewarmed: typeof fields.prewarmed === "boolean" ? fields.prewarmed : undefined,
103
+ replayTerminal: typeof fields.replayTerminal === "boolean" ? fields.replayTerminal : undefined,
100
104
  durableRunCreated: typeof fields.durableRunCreated === "boolean" ? fields.durableRunCreated : undefined,
101
105
  };
102
106
  try {