impel-cli 0.20.43 → 0.20.45
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/README.md +10 -0
- package/package.json +1 -1
- package/src/agents.js +29 -14
- package/src/apps.js +4 -0
- package/src/commands/apps.js +11 -5
- package/src/commands/converge.js +7 -26
- package/src/commands/setup.js +15 -22
- package/src/commands/tasks.js +10 -2
- package/src/commands/tenantRecoveryReport.js +101 -0
- package/src/managedProfileVersion.js +1 -1
- package/src/providerReadiness.js +4 -2
- package/src/provisioning.js +29 -1
- package/src/verbatimRelay.js +10 -2
package/README.md
CHANGED
|
@@ -188,6 +188,16 @@ impel claude --agent research-agent --print "answer this question"
|
|
|
188
188
|
impel codex --agent research-agent exec "answer this question"
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
+
Claude profiles also generate `/ask-<agent>` commands for synchronized,
|
|
192
|
+
read-only agents that support direct answers. The generated command invokes
|
|
193
|
+
the exact tenant-bound agent, attributes the response, and returns its answer
|
|
194
|
+
verbatim; the ordinary relay subagent remains available. To suppress these
|
|
195
|
+
additive commands and remove previously generated ones on the next sync, run:
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
IMPEL_NATIVE_SLASH_COMMANDS=0 impel agents sync claude
|
|
199
|
+
```
|
|
200
|
+
|
|
191
201
|
Both managed paths verify the selected tenant manifest and generated adapter
|
|
192
202
|
bytes, resolve the canonical native agent name, and omit the normal parent
|
|
193
203
|
specialist instructions. Claude Code receives the explicit 2.1.220 opt-in and
|
package/package.json
CHANGED
package/src/agents.js
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
claudeFaithfulCompletionGuidance,
|
|
47
47
|
claudeVerbatimCompletionGuidance,
|
|
48
48
|
customAgentVerbatimDescriptionLead,
|
|
49
|
+
VERBATIM_NO_EXTERNAL_SOURCE_CONSTRAINT,
|
|
49
50
|
usesVerbatimRelay,
|
|
50
51
|
} from "./verbatimRelay.js";
|
|
51
52
|
import { renameWithWindowsRetry } from "./windowsFs.js";
|
|
@@ -64,7 +65,7 @@ export const NATIVE_AGENT_RESUME_TOOL = "resume_native_agent_run";
|
|
|
64
65
|
export const NATIVE_AGENT_RECOVER_TOOL = "recover_native_agent_runs";
|
|
65
66
|
export const NATIVE_AGENT_CONTINUATION_SCHEMA = "impel.native-agent-continuation.v1";
|
|
66
67
|
export const MANAGED_AGENT_MCP_SERVER = "impel_agent";
|
|
67
|
-
export const MANAGED_AGENT_MANIFEST_VERSION =
|
|
68
|
+
export const MANAGED_AGENT_MANIFEST_VERSION = 24;
|
|
68
69
|
export const IMPEL_NATIVE_PARENT_DIRECT_ENV = "IMPEL_NATIVE_PARENT_DIRECT";
|
|
69
70
|
export const IMPEL_NATIVE_SLASH_COMMANDS_ENV = "IMPEL_NATIVE_SLASH_COMMANDS";
|
|
70
71
|
|
|
@@ -88,8 +89,11 @@ function eagerNativeAgentTransportEnabled() {
|
|
|
88
89
|
return process.env.IMPEL_NATIVE_EAGER_TRANSPORT !== "0";
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
function enabledProfileFlag(environment, name) {
|
|
92
|
-
|
|
92
|
+
function enabledProfileFlag(environment, name, { defaultEnabled = false } = {}) {
|
|
93
|
+
const configured = String(environment?.[name] || "").toLowerCase();
|
|
94
|
+
if (["1", "true"].includes(configured)) return true;
|
|
95
|
+
if (["0", "false"].includes(configured)) return false;
|
|
96
|
+
return defaultEnabled;
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
export function nativeParentDirectEnabled(environment = process.env) {
|
|
@@ -97,7 +101,11 @@ export function nativeParentDirectEnabled(environment = process.env) {
|
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
export function nativeSlashCommandsEnabled(environment = process.env) {
|
|
100
|
-
|
|
104
|
+
// This surface is additive: the relay agent remains installed. Keep the
|
|
105
|
+
// explicit false literals as a symmetric rollback for managed profiles.
|
|
106
|
+
return enabledProfileFlag(environment, IMPEL_NATIVE_SLASH_COMMANDS_ENV, {
|
|
107
|
+
defaultEnabled: true,
|
|
108
|
+
});
|
|
101
109
|
}
|
|
102
110
|
const MAX_RETIRED_AGENT_BINDINGS = 50;
|
|
103
111
|
const MAX_NATIVE_AGENT_STATE_BYTES = 512 * 1024;
|
|
@@ -919,6 +927,7 @@ function claudeParentDirectInstructions(tenantId, agent) {
|
|
|
919
927
|
`Call ${nativeToolName(NATIVE_AGENT_ANSWER_TOOL)} exactly once with question set to the user's complete request. Do not spawn a relay subagent and do not perform the request yourself.`,
|
|
920
928
|
`If the answer returns an object whose schema is exactly ${JSON.stringify(NATIVE_AGENT_CONTINUATION_SCHEMA)}, pass that complete object unchanged as the handle to ${nativeToolName(NATIVE_AGENT_RESUME_TOOL)} until terminal; after a continuation exists, never call the answer tool again.`,
|
|
921
929
|
`On success, present the attribution line ${JSON.stringify(attribution)}, followed by the returned finalText verbatim with no rewriting, Markdown changes, or independent synthesis. On failure, attribute the failure to the same named managed agent and do not invent a replacement answer.`,
|
|
930
|
+
...(usesVerbatimRelay(agent) ? [VERBATIM_NO_EXTERNAL_SOURCE_CONSTRAINT] : []),
|
|
922
931
|
].join(" ");
|
|
923
932
|
}
|
|
924
933
|
|
|
@@ -1124,6 +1133,7 @@ export function renderManagedAgents(client, tenantId, agents, invocation = null,
|
|
|
1124
1133
|
policyFingerprint: nativeAgentPolicyFingerprint(agent),
|
|
1125
1134
|
retired: false,
|
|
1126
1135
|
name,
|
|
1136
|
+
...(usesVerbatimRelay(agent) ? { verbatimRelay: true } : {}),
|
|
1127
1137
|
fileName: parentDirect ? null : `${fileStem}${extension}`,
|
|
1128
1138
|
contents,
|
|
1129
1139
|
...(claudeRendered ? { launchDefinition: claudeRendered.launchDefinition } : {}),
|
|
@@ -1167,6 +1177,7 @@ function renderClaudeSlashCommand(tenantId, agent, commandName, invocation) {
|
|
|
1167
1177
|
"",
|
|
1168
1178
|
`The JSON below came from managed agent ${JSON.stringify(agent.title)} (${agent.agentId}), not from the host model.`,
|
|
1169
1179
|
`Run the fixed command exactly once. If ok is true, output the attribution line ${JSON.stringify(attribution)} followed by finalText verbatim. If ok is false, attribute the reported error to the same managed agent. Do not perform, rewrite, or independently answer the task.`,
|
|
1180
|
+
...(usesVerbatimRelay(agent) ? [VERBATIM_NO_EXTERNAL_SOURCE_CONSTRAINT] : []),
|
|
1170
1181
|
"",
|
|
1171
1182
|
`!\`${bashCommand}\``,
|
|
1172
1183
|
"",
|
|
@@ -1669,22 +1680,23 @@ function profileIsFresh(profile, tenantId, now, ttlMs) {
|
|
|
1669
1680
|
if (!Array.isArray(manifest.files) || !manifest.contentDigests
|
|
1670
1681
|
|| typeof manifest.contentDigests !== "object"
|
|
1671
1682
|
|| Array.isArray(manifest.contentDigests)) return false;
|
|
1683
|
+
const profileEnvironment = profile.environment ?? process.env;
|
|
1672
1684
|
const expectedDirectProfiles = profile.client === "codex" && profile.directProfiles !== false;
|
|
1673
1685
|
const expectedDirectCodeMode = profile.client === "codex" && profile.directCodeMode !== false;
|
|
1674
1686
|
const expectedNativeParentDirect = profile.client === "claude"
|
|
1675
|
-
&& (profile.nativeParentDirect ?? nativeParentDirectEnabled());
|
|
1687
|
+
&& (profile.nativeParentDirect ?? nativeParentDirectEnabled(profileEnvironment));
|
|
1676
1688
|
const expectedNativeSlashCommands = profile.client === "claude"
|
|
1677
|
-
&& (profile.nativeSlashCommands ?? nativeSlashCommandsEnabled());
|
|
1689
|
+
&& (profile.nativeSlashCommands ?? nativeSlashCommandsEnabled(profileEnvironment));
|
|
1678
1690
|
const expectedNativeIntercept = profile.client === "claude"
|
|
1679
|
-
&& (profile.nativeIntercept ?? nativeInterceptEnabled());
|
|
1691
|
+
&& (profile.nativeIntercept ?? nativeInterceptEnabled(profileEnvironment));
|
|
1680
1692
|
const expectedNativeInterceptTimeoutMs = expectedNativeIntercept
|
|
1681
|
-
? (profile.nativeInterceptTimeoutMs ?? nativeInterceptTimeoutMs())
|
|
1693
|
+
? (profile.nativeInterceptTimeoutMs ?? nativeInterceptTimeoutMs(profileEnvironment))
|
|
1682
1694
|
: null;
|
|
1683
1695
|
const expectedTelemetryEnvironment = nativeAgentTelemetryEnvironment(
|
|
1684
|
-
|
|
1696
|
+
profileEnvironment,
|
|
1685
1697
|
);
|
|
1686
1698
|
const expectedBenchmark = nativeBenchmarkHeaderValue(
|
|
1687
|
-
|
|
1699
|
+
profileEnvironment,
|
|
1688
1700
|
) !== null;
|
|
1689
1701
|
if (manifest.directProfiles !== expectedDirectProfiles
|
|
1690
1702
|
|| manifest.directCodeMode !== expectedDirectCodeMode
|
|
@@ -1729,11 +1741,13 @@ export function syncAgentProfile({
|
|
|
1729
1741
|
agents,
|
|
1730
1742
|
directProfiles = client === "codex",
|
|
1731
1743
|
directCodeMode = client === "codex",
|
|
1732
|
-
nativeParentDirect = client === "claude" && nativeParentDirectEnabled(),
|
|
1733
|
-
nativeSlashCommands = client === "claude" && nativeSlashCommandsEnabled(),
|
|
1734
|
-
nativeIntercept = client === "claude" && nativeInterceptEnabled(),
|
|
1735
|
-
nativeInterceptTimeoutMs: interceptTimeoutMs = nativeIntercept ? nativeInterceptTimeoutMs() : null,
|
|
1736
1744
|
environment = process.env,
|
|
1745
|
+
nativeParentDirect = client === "claude" && nativeParentDirectEnabled(environment),
|
|
1746
|
+
nativeSlashCommands = client === "claude" && nativeSlashCommandsEnabled(environment),
|
|
1747
|
+
nativeIntercept = client === "claude" && nativeInterceptEnabled(environment),
|
|
1748
|
+
nativeInterceptTimeoutMs: interceptTimeoutMs = nativeIntercept
|
|
1749
|
+
? nativeInterceptTimeoutMs(environment)
|
|
1750
|
+
: null,
|
|
1737
1751
|
invocation = null,
|
|
1738
1752
|
now = Date.now(),
|
|
1739
1753
|
nativeAgentRunsRoot = path.join(CONFIG_DIR, "native-agent-runs"),
|
|
@@ -1966,6 +1980,7 @@ export function syncAgentProfile({
|
|
|
1966
1980
|
policyFingerprint: agent.policyFingerprint,
|
|
1967
1981
|
retired: agent.retired,
|
|
1968
1982
|
name: agent.name,
|
|
1983
|
+
...(agent.verbatimRelay ? { verbatimRelay: true } : {}),
|
|
1969
1984
|
...(client === "claude" ? {
|
|
1970
1985
|
...(agent.parentLaunchDefinition
|
|
1971
1986
|
? { launchMode: "parent-direct", parentLaunchDefinition: agent.parentLaunchDefinition }
|
package/src/apps.js
CHANGED
|
@@ -302,6 +302,10 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
302
302
|
// 35: enable the Code Mode host in managed desktop profiles (pinned Codex
|
|
303
303
|
// fails closed on code_mode_only models without it) and serve CLI model
|
|
304
304
|
// catalogs from the shared registry so efforts and tiers cannot drift.
|
|
305
|
+
// 36: install attributed /ask-<agent> slash commands for read-only direct
|
|
306
|
+
// answers in managed Claude profiles by default.
|
|
307
|
+
// 37: forbid external retrieval and citation edits on every opt-in
|
|
308
|
+
// verbatim-relay surface.
|
|
305
309
|
export { CURRENT_CONFIG_VERSION };
|
|
306
310
|
|
|
307
311
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
package/src/commands/apps.js
CHANGED
|
@@ -52,7 +52,6 @@ import {
|
|
|
52
52
|
ensureWindowsClaudeApp,
|
|
53
53
|
ensureWindowsChatGPTApp,
|
|
54
54
|
findWindowsClaudeApp,
|
|
55
|
-
findWindowsChatGPTApp,
|
|
56
55
|
findManagedWindowsChatGPTApp,
|
|
57
56
|
launchWindowsClaudeApp,
|
|
58
57
|
pinnedWindowsChatGPTApp,
|
|
@@ -424,8 +423,11 @@ export async function reconcileWindowsTenantApps({
|
|
|
424
423
|
// missing vendor app) must not abort the other target's work: install
|
|
425
424
|
// recovery approves one vendor app at a time, and that approved install
|
|
426
425
|
// has to run even while its sibling stays missing.
|
|
426
|
+
// `reason` distinguishes a real installer failure from the synthetic decline
|
|
427
|
+
// recorded when a caller (install recovery) approved only the sibling app, so
|
|
428
|
+
// callers can attribute each failure to the product that actually failed.
|
|
427
429
|
const failed = [];
|
|
428
|
-
const fail = (target, error) => failed.push({ target, error });
|
|
430
|
+
const fail = (target, error, reason = "install-failed") => failed.push({ target, error, reason });
|
|
429
431
|
const hasFailed = (target) => failed.some((failure) => failure.target === target);
|
|
430
432
|
|
|
431
433
|
for (const target of actionTargets) {
|
|
@@ -437,7 +439,11 @@ export async function reconcileWindowsTenantApps({
|
|
|
437
439
|
if (!preparedTargets.has(target)) {
|
|
438
440
|
const confirmed = await confirmVendorInstall(target, { platform: "win32", mode });
|
|
439
441
|
if (!confirmed) {
|
|
440
|
-
fail(
|
|
442
|
+
fail(
|
|
443
|
+
target,
|
|
444
|
+
`${label} vendor ${mode === "update" ? "update" : "installation"} requires confirmation`,
|
|
445
|
+
"confirmation-required",
|
|
446
|
+
);
|
|
441
447
|
continue;
|
|
442
448
|
}
|
|
443
449
|
const vendor = await ensure({ update: mode === "update" }, { environment, homeDir });
|
|
@@ -568,13 +574,13 @@ export async function reconcileMacTenantApps({
|
|
|
568
574
|
// target so the sibling app still converges (install recovery approves one
|
|
569
575
|
// vendor app at a time).
|
|
570
576
|
const failed = [];
|
|
571
|
-
const fail = (target, error) => failed.push({ target, error });
|
|
577
|
+
const fail = (target, error, reason = "install-failed") => failed.push({ target, error, reason });
|
|
572
578
|
const hasFailed = (target) => failed.some((failure) => failure.target === target);
|
|
573
579
|
for (const target of actionTargets.filter((target) => !preparedTargets.has(target))) {
|
|
574
580
|
if (vendorPaths[target]) continue;
|
|
575
581
|
const confirmed = await confirmVendorInstall(target, { platform: "darwin", mode });
|
|
576
582
|
if (!confirmed) {
|
|
577
|
-
fail(target, `${target} vendor installation requires confirmation
|
|
583
|
+
fail(target, `${target} vendor installation requires confirmation`, "confirmation-required");
|
|
578
584
|
continue;
|
|
579
585
|
}
|
|
580
586
|
const result = await io.ensureVendor(target, { homeDir });
|
package/src/commands/converge.js
CHANGED
|
@@ -6,12 +6,12 @@ import {
|
|
|
6
6
|
printReconciliationSummary,
|
|
7
7
|
reconcileAllTenants,
|
|
8
8
|
selectDefaultTenant,
|
|
9
|
-
vendorAppTargetReady,
|
|
10
9
|
} from "../provisioning.js";
|
|
11
10
|
import { fetchTenants } from "../tenants.js";
|
|
12
11
|
import { promptText } from "../prompt.js";
|
|
13
12
|
import { describeCliFailure, preparePlatformClis } from "../platformSetup.js";
|
|
14
13
|
import { restoreNativeProfiles } from "./use.js";
|
|
14
|
+
import { createTenantRecoveryReportTracker } from "./tenantRecoveryReport.js";
|
|
15
15
|
|
|
16
16
|
const RETRYABLE_TENANT_DISCOVERY_ERROR = /could not reach|request timed out|fetch failed|ECONNRESET|ECONNABORTED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|network error|socket/iu;
|
|
17
17
|
|
|
@@ -19,17 +19,6 @@ function confirmed(answer) {
|
|
|
19
19
|
return /^(?:y|yes)$/iu.test(String(answer || "").trim());
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function mergeTenantReport(report, replacement) {
|
|
23
|
-
const byId = new Map(replacement.tenants.map((tenant) => [tenant.tenantId, tenant]));
|
|
24
|
-
report.tenants = report.tenants.map((tenant) => byId.get(tenant.tenantId) || tenant);
|
|
25
|
-
report.passed = report.tenants.every((tenant) => (
|
|
26
|
-
["ready", "unavailable"].includes(tenant.cli)
|
|
27
|
-
&& ["ready", "unavailable"].includes(tenant.apps)
|
|
28
|
-
&& ["ready", "unavailable"].includes(tenant.shell)
|
|
29
|
-
));
|
|
30
|
-
return report;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
22
|
export async function cmdConverge(argv = [], overrides = {}) {
|
|
34
23
|
const skipApps = argv.includes("--skip-apps");
|
|
35
24
|
const skipClis = argv.includes("--skip-clis");
|
|
@@ -264,12 +253,14 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
264
253
|
if (!tenant) continue;
|
|
265
254
|
let retryReport = null;
|
|
266
255
|
let retryPassed = false;
|
|
256
|
+
const recoveryReport = createTenantRecoveryReportTracker(report);
|
|
267
257
|
const retryTenant = async (confirmInstall) => {
|
|
268
258
|
retryReport = await run([tenant], !skipApps, confirmInstall);
|
|
269
259
|
retryPassed = retryReport.passed;
|
|
270
260
|
return retryPassed;
|
|
271
261
|
};
|
|
272
262
|
const retrySafeState = async () => {
|
|
263
|
+
recoveryReport.clearVendorAppAttempt();
|
|
273
264
|
const profileReport = await run([tenant], false, async () => false);
|
|
274
265
|
const profile = profileReport.tenants[0];
|
|
275
266
|
const current = report.tenants.find((candidate) => candidate.tenantId === tenant.id);
|
|
@@ -321,23 +312,13 @@ export async function cmdConverge(argv = [], overrides = {}) {
|
|
|
321
312
|
// sibling app still fails: the goal check keeps demanding full
|
|
322
313
|
// tenant readiness, so recovery can approve one app at a time.
|
|
323
314
|
installVendorApp: async (target) => {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|| (target === "codex" && product === "chatgpt")
|
|
328
|
-
));
|
|
329
|
-
const installed = vendorAppTargetReady(retryReport, target);
|
|
330
|
-
return {
|
|
331
|
-
installed,
|
|
332
|
-
error: installed
|
|
333
|
-
? null
|
|
334
|
-
: retryReport?.tenants?.[0]?.errors?.join("; ")
|
|
335
|
-
|| `The ${target} vendor app did not reach a verified state.`,
|
|
336
|
-
};
|
|
315
|
+
const attempt = recoveryReport.vendorAppAttempt(target);
|
|
316
|
+
await retryTenant(attempt.confirm);
|
|
317
|
+
return attempt.outcome(retryReport);
|
|
337
318
|
},
|
|
338
319
|
},
|
|
339
320
|
}, overrides.recoveryOverrides || {});
|
|
340
|
-
if (retryReport)
|
|
321
|
+
if (retryReport) report = recoveryReport.merge(retryReport);
|
|
341
322
|
}
|
|
342
323
|
}
|
|
343
324
|
printReconciliationSummary(report);
|
package/src/commands/setup.js
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
printReconciliationSummary,
|
|
17
17
|
reconcileAllTenants,
|
|
18
18
|
selectDefaultTenant,
|
|
19
|
-
vendorAppTargetReady,
|
|
20
19
|
} from "../provisioning.js";
|
|
21
20
|
import { describeCliFailure, preparePlatformClis } from "../platformSetup.js";
|
|
22
21
|
import { runInstallRecovery } from "../installRecovery/engine.js";
|
|
@@ -27,7 +26,9 @@ import {
|
|
|
27
26
|
cacheProviderReadiness,
|
|
28
27
|
probeGatewayProviderReadiness,
|
|
29
28
|
providerAvailable,
|
|
29
|
+
PROVIDER_READINESS_TIMEOUTS_MS,
|
|
30
30
|
} from "../providerReadiness.js";
|
|
31
|
+
import { createTenantRecoveryReportTracker } from "./tenantRecoveryReport.js";
|
|
31
32
|
|
|
32
33
|
const HELP = brandedText(`impel setup - prepare every accessible Impel tenant
|
|
33
34
|
|
|
@@ -57,7 +58,13 @@ export function resolveTenantChoice(listing, { requested = null, answer = null,
|
|
|
57
58
|
return selectDefaultTenant(listing, { currentTenantId });
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
export async function probeGateway(
|
|
61
|
+
export async function probeGateway(
|
|
62
|
+
gatewayUrl,
|
|
63
|
+
pat,
|
|
64
|
+
tenantId,
|
|
65
|
+
fetchImpl = fetch,
|
|
66
|
+
timeoutsMs = PROVIDER_READINESS_TIMEOUTS_MS,
|
|
67
|
+
) {
|
|
61
68
|
return probeGatewayProviderReadiness(
|
|
62
69
|
gatewayUrl,
|
|
63
70
|
tenantCredential(pat, tenantId),
|
|
@@ -112,12 +119,6 @@ function recomputeReport(report) {
|
|
|
112
119
|
return report;
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
function mergeTenantReport(report, replacement) {
|
|
116
|
-
const byId = new Map(replacement.tenants.map((tenant) => [tenant.tenantId, tenant]));
|
|
117
|
-
report.tenants = report.tenants.map((tenant) => byId.get(tenant.tenantId) || tenant);
|
|
118
|
-
return recomputeReport(report);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
122
|
function confirmed(answer) {
|
|
122
123
|
return /^(?:y|yes)$/iu.test(String(answer || "").trim());
|
|
123
124
|
}
|
|
@@ -416,12 +417,14 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
416
417
|
if (!tenant) continue;
|
|
417
418
|
let retryReport = null;
|
|
418
419
|
let retryPassed = false;
|
|
420
|
+
const recoveryReport = createTenantRecoveryReportTracker(report);
|
|
419
421
|
const retryTenant = async (confirmInstall) => {
|
|
420
422
|
retryReport = await verifyConvergence(!flags["skip-apps"], [tenant], confirmInstall);
|
|
421
423
|
retryPassed = retryReport.passed;
|
|
422
424
|
return retryPassed;
|
|
423
425
|
};
|
|
424
426
|
const retrySafeState = async () => {
|
|
427
|
+
recoveryReport.clearVendorAppAttempt();
|
|
425
428
|
const profileReport = await verifyConvergence(false, [tenant], async () => false);
|
|
426
429
|
const profile = profileReport.tenants[0];
|
|
427
430
|
const current = report.tenants.find((candidate) => candidate.tenantId === tenant.id);
|
|
@@ -474,23 +477,13 @@ export async function cmdSetup(argv, overrides = {}) {
|
|
|
474
477
|
// sibling app still fails: the goal check keeps demanding full
|
|
475
478
|
// tenant readiness, so recovery can approve one app at a time.
|
|
476
479
|
installVendorApp: async (target) => {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|| (target === "codex" && product === "chatgpt")
|
|
481
|
-
));
|
|
482
|
-
const installed = vendorAppTargetReady(retryReport, target);
|
|
483
|
-
return {
|
|
484
|
-
installed,
|
|
485
|
-
error: installed
|
|
486
|
-
? null
|
|
487
|
-
: retryReport?.tenants?.[0]?.errors?.join("; ")
|
|
488
|
-
|| `The ${target} vendor app did not reach a verified state.`,
|
|
489
|
-
};
|
|
480
|
+
const attempt = recoveryReport.vendorAppAttempt(target);
|
|
481
|
+
await retryTenant(attempt.confirm);
|
|
482
|
+
return attempt.outcome(retryReport);
|
|
490
483
|
},
|
|
491
484
|
},
|
|
492
485
|
}, overrides.recoveryOverrides || {});
|
|
493
|
-
if (retryReport)
|
|
486
|
+
if (retryReport) report = recoveryReport.merge(retryReport);
|
|
494
487
|
}
|
|
495
488
|
}
|
|
496
489
|
recomputeReport(report);
|
package/src/commands/tasks.js
CHANGED
|
@@ -10,7 +10,15 @@ import {
|
|
|
10
10
|
PRODUCT_ACCESS_WORKSPACE,
|
|
11
11
|
} from "../tenants.js";
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
export const TASK_PROGRESS_VALUES = [
|
|
14
|
+
"none",
|
|
15
|
+
"todo",
|
|
16
|
+
"progress",
|
|
17
|
+
"review",
|
|
18
|
+
"done",
|
|
19
|
+
"cancelled",
|
|
20
|
+
];
|
|
21
|
+
const VALID_PROGRESS = new Set(TASK_PROGRESS_VALUES);
|
|
14
22
|
const VALID_PRIORITY = new Set(["none", "urgent", "high", "medium", "low"]);
|
|
15
23
|
const VALID_LABELS = new Set(["feature", "bug", "engineering", "design", "product"]);
|
|
16
24
|
|
|
@@ -35,7 +43,7 @@ Options:
|
|
|
35
43
|
--description <markdown> Ticket body markdown.
|
|
36
44
|
--description-file <path> Read ticket body markdown from a file.
|
|
37
45
|
--description-mode replace|append Description update mode. Default: replace.
|
|
38
|
-
--progress none|todo|progress|review|done
|
|
46
|
+
--progress none|todo|progress|review|done|cancelled
|
|
39
47
|
--priority none|urgent|high|medium|low
|
|
40
48
|
--labels <csv> Full label set, e.g. feature,engineering. Empty clears labels.
|
|
41
49
|
--assignee <id|none> Assignee member id, or "none".
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recomputeTenantAggregates,
|
|
3
|
+
vendorAppTargetReady,
|
|
4
|
+
} from "../provisioning.js";
|
|
5
|
+
|
|
6
|
+
function reportPassed(tenants) {
|
|
7
|
+
return tenants.every((tenant) => (
|
|
8
|
+
["ready", "unavailable"].includes(tenant.cli)
|
|
9
|
+
&& ["ready", "unavailable"].includes(tenant.apps)
|
|
10
|
+
&& ["ready", "unavailable"].includes(tenant.shell)
|
|
11
|
+
));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function mergeTenantReports(report, replacement, products) {
|
|
15
|
+
const byId = new Map(replacement.tenants.map((tenant) => [tenant.tenantId, tenant]));
|
|
16
|
+
const tenants = report.tenants.map((tenant) => {
|
|
17
|
+
const attempted = byId.get(tenant.tenantId);
|
|
18
|
+
if (!attempted || !products) return attempted || tenant;
|
|
19
|
+
|
|
20
|
+
const clients = { ...attempted.clients };
|
|
21
|
+
const declined = [];
|
|
22
|
+
const restored = [];
|
|
23
|
+
for (const product of ["claude", "codex"]) {
|
|
24
|
+
if (products.includes(product)) continue;
|
|
25
|
+
const attempt = clients[product];
|
|
26
|
+
const prior = tenant.clients[product];
|
|
27
|
+
if (!prior || attempt?.appErrorReason !== "confirmation-required") continue;
|
|
28
|
+
if (attempt.appError) declined.push(attempt.appError);
|
|
29
|
+
clients[product] = {
|
|
30
|
+
...attempt,
|
|
31
|
+
app: prior.app,
|
|
32
|
+
shell: prior.shell,
|
|
33
|
+
appError: prior.appError,
|
|
34
|
+
appErrorReason: prior.appErrorReason,
|
|
35
|
+
};
|
|
36
|
+
if (prior.appError) restored.push(prior.appError);
|
|
37
|
+
}
|
|
38
|
+
if (!declined.length && !restored.length) return attempted;
|
|
39
|
+
|
|
40
|
+
const kept = attempted.errors.filter((error) => !declined.includes(error));
|
|
41
|
+
// recomputeTenantAggregates mutates its argument, so give it a fresh
|
|
42
|
+
// tenant and client map rather than the replacement report's tenant.
|
|
43
|
+
return recomputeTenantAggregates({
|
|
44
|
+
...attempted,
|
|
45
|
+
clients,
|
|
46
|
+
errors: [...new Set([...kept, ...restored])],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
return { ...report, tenants, passed: reportPassed(tenants) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function vendorAppOutcome(retryReport, target, products) {
|
|
53
|
+
const installed = vendorAppTargetReady(retryReport, target);
|
|
54
|
+
if (installed) return { installed: true, error: null };
|
|
55
|
+
|
|
56
|
+
const attempted = retryReport?.tenants?.[0];
|
|
57
|
+
const clients = attempted?.clients;
|
|
58
|
+
const attributed = Object.values(clients || {}).some((client) => client.appError);
|
|
59
|
+
// Reports without per-client attribution (no vendor step ran, or an older
|
|
60
|
+
// shape) still fall back to the tenant's complete error list.
|
|
61
|
+
const errors = attributed
|
|
62
|
+
? products.map((product) => clients?.[product]?.appError).filter(Boolean)
|
|
63
|
+
: attempted?.errors || [];
|
|
64
|
+
return {
|
|
65
|
+
installed: false,
|
|
66
|
+
error: errors.join("; ") || `The ${target} vendor app did not reach a verified state.`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Keep one tenant-recovery retry's product attribution and report merge in
|
|
72
|
+
* sync. A targeted vendor retry auto-declines its sibling, so the retry's
|
|
73
|
+
* outcome must use only the requested product while the final report restores
|
|
74
|
+
* the sibling's earlier, better-known state.
|
|
75
|
+
*/
|
|
76
|
+
export function createTenantRecoveryReportTracker(report) {
|
|
77
|
+
let attemptedProducts = null;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
clearVendorAppAttempt() {
|
|
81
|
+
attemptedProducts = null;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
vendorAppAttempt(target) {
|
|
85
|
+
const products = target === "all" ? ["claude", "codex"] : [target];
|
|
86
|
+
attemptedProducts = products;
|
|
87
|
+
return {
|
|
88
|
+
confirm: (product) => (
|
|
89
|
+
target === "all"
|
|
90
|
+
|| product === target
|
|
91
|
+
|| (target === "codex" && product === "chatgpt")
|
|
92
|
+
),
|
|
93
|
+
outcome: (retryReport) => vendorAppOutcome(retryReport, target, products),
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
merge(replacement) {
|
|
98
|
+
return mergeTenantReports(report, replacement, attemptedProducts);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// Fleet generation shared by managed-profile writers and upstream clients.
|
|
2
2
|
// Keep this isolated from apps.js so latency-sensitive transports do not load
|
|
3
3
|
// desktop bundle machinery just to identify their managed config contract.
|
|
4
|
-
export const CURRENT_CONFIG_VERSION =
|
|
4
|
+
export const CURRENT_CONFIG_VERSION = 37;
|
package/src/providerReadiness.js
CHANGED
|
@@ -3,6 +3,7 @@ import { redactSecretText } from "./config.js";
|
|
|
3
3
|
const PROVIDERS = new Set(["claude", "codex"]);
|
|
4
4
|
const POOL_STATES = new Set(["healthy", "exhausted", "rate_limited", "expired", "no_seat"]);
|
|
5
5
|
export const PROVIDER_READINESS_CACHE_MS = 60_000;
|
|
6
|
+
export const PROVIDER_READINESS_TIMEOUTS_MS = Object.freeze([5_000, 10_000]);
|
|
6
7
|
|
|
7
8
|
function providersFromPayload(payload) {
|
|
8
9
|
if (!Array.isArray(payload?.data)) return { error: "gateway model catalog has no model data array" };
|
|
@@ -57,7 +58,8 @@ async function probeGatewayOnce(gatewayUrl, credential, tenantId, fetchImpl, tim
|
|
|
57
58
|
let payload;
|
|
58
59
|
try {
|
|
59
60
|
payload = await response.json();
|
|
60
|
-
} catch {
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error?.name === "AbortError") throw error;
|
|
61
63
|
return { ...result, healthy: false, error: "gateway model catalog was not valid JSON" };
|
|
62
64
|
}
|
|
63
65
|
const catalog = providersFromPayload(payload);
|
|
@@ -87,7 +89,7 @@ export async function probeGatewayProviderReadiness(
|
|
|
87
89
|
credential,
|
|
88
90
|
tenantId,
|
|
89
91
|
fetchImpl = fetch,
|
|
90
|
-
timeoutsMs =
|
|
92
|
+
timeoutsMs = PROVIDER_READINESS_TIMEOUTS_MS,
|
|
91
93
|
) {
|
|
92
94
|
let probe = { reachable: false, error: "not probed" };
|
|
93
95
|
for (const timeoutMs of timeoutsMs) {
|
package/src/provisioning.js
CHANGED
|
@@ -280,7 +280,16 @@ export async function reconcileAllTenants({
|
|
|
280
280
|
? "ready"
|
|
281
281
|
: failedTargets.has("chatgpt") ? "failed" : "unsupported";
|
|
282
282
|
result.apps = failedTargets.size ? "failed" : supported.size ? "ready" : "unavailable";
|
|
283
|
-
|
|
283
|
+
// Record each failure on its own client too. `errors` keeps every
|
|
284
|
+
// message for the summary line; the per-client facets let a targeted
|
|
285
|
+
// retry tell its own failure apart from an auto-declined sibling's.
|
|
286
|
+
for (const failure of failedApps) {
|
|
287
|
+
const message = redactSecretText(failure.error);
|
|
288
|
+
const client = failure.target === "claude" ? "claude" : "codex";
|
|
289
|
+
result.clients[client].appError = message;
|
|
290
|
+
result.clients[client].appErrorReason = failure.reason || "install-failed";
|
|
291
|
+
result.errors.push(message);
|
|
292
|
+
}
|
|
284
293
|
} catch (error) {
|
|
285
294
|
result.apps = "failed";
|
|
286
295
|
for (const client of Object.values(result.clients)) {
|
|
@@ -360,6 +369,25 @@ export function vendorAppTargetReady(report, target) {
|
|
|
360
369
|
return converged(target === "claude" ? clients.claude : clients.codex);
|
|
361
370
|
}
|
|
362
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Re-derive a tenant's aggregate app/shell/status fields from its per-client
|
|
374
|
+
* facets. Used after a partial merge swaps one product's facets back to an
|
|
375
|
+
* earlier, better-known state.
|
|
376
|
+
*/
|
|
377
|
+
export function recomputeTenantAggregates(tenant) {
|
|
378
|
+
const clients = Object.values(tenant.clients);
|
|
379
|
+
const collapse = (states) => (
|
|
380
|
+
states.includes("failed") ? "failed"
|
|
381
|
+
: states.includes("ready") ? "ready"
|
|
382
|
+
: states.includes("pending") ? "pending" : "unavailable"
|
|
383
|
+
);
|
|
384
|
+
tenant.apps = collapse(clients.map((client) => client.app));
|
|
385
|
+
tenant.shell = collapse(clients.map((client) => client.shell));
|
|
386
|
+
const states = clients.flatMap((client) => [client.cli, client.app, client.shell]);
|
|
387
|
+
tenant.status = states.includes("failed") ? "failed" : states.includes("pending") ? "partial" : "ready";
|
|
388
|
+
return tenant;
|
|
389
|
+
}
|
|
390
|
+
|
|
363
391
|
export function printReconciliationSummary(report, log = console.log) {
|
|
364
392
|
log("Tenant readiness:");
|
|
365
393
|
for (const tenant of report.tenants) {
|
package/src/verbatimRelay.js
CHANGED
|
@@ -13,6 +13,12 @@ export const VERBATIM_SPAWN_REQUIREMENT = 'fork_turns="none"';
|
|
|
13
13
|
export const VERBATIM_FINAL_TEXT_CONSTRAINTS =
|
|
14
14
|
"no preface, rewriting, Markdown changes, or independent synthesis";
|
|
15
15
|
|
|
16
|
+
// A relayed answer is authoritative only if nothing else reached it. Hosts that
|
|
17
|
+
// can browse will otherwise blend their own retrieval into the Sources section.
|
|
18
|
+
export const VERBATIM_NO_EXTERNAL_SOURCE_CONSTRAINT =
|
|
19
|
+
"Never search the web, browse, open URLs, or consult any source outside this agent for the request, "
|
|
20
|
+
+ "and never add, merge, reorder, or replace the sources and citations it returns.";
|
|
21
|
+
|
|
16
22
|
export function usesVerbatimRelay(agent) {
|
|
17
23
|
return agent?.verbatimRelay === true;
|
|
18
24
|
}
|
|
@@ -21,7 +27,8 @@ export function parentVerbatimRelayAppendix() {
|
|
|
21
27
|
return (
|
|
22
28
|
`When an explicit custom agent's catalog-derived description declares ${VERBATIM_RELAY_OPT_IN_MARKER}, ` +
|
|
23
29
|
`spawn it with ${VERBATIM_SPAWN_REQUIREMENT} and relay its finalText verbatim with ${VERBATIM_FINAL_TEXT_CONSTRAINTS}; ` +
|
|
24
|
-
|
|
30
|
+
`preserve Sources sections and citations exactly. ${VERBATIM_NO_EXTERNAL_SOURCE_CONSTRAINT} ` +
|
|
31
|
+
"Start one child, then use one blocking wait sized for the child budget; " +
|
|
25
32
|
`on Codex, call wait_agent exactly once with timeout_ms=${VERBATIM_RELAY_PARENT_WAIT_MS}. Do not send follow-ups, short-poll the child, or synthesize while it is running. ` +
|
|
26
33
|
"Custom agents without that declaration keep the default delegation behavior."
|
|
27
34
|
);
|
|
@@ -37,7 +44,8 @@ export function customAgentVerbatimDescriptionLead() {
|
|
|
37
44
|
export function adapterCallerSpawnGuidance(clientLabel) {
|
|
38
45
|
return (
|
|
39
46
|
`Callers must spawn this explicit custom ${clientLabel} agent with ${VERBATIM_SPAWN_REQUIREMENT} ` +
|
|
40
|
-
"and must relay your result verbatim; this is caller guidance and cannot enforce host spawn behavior."
|
|
47
|
+
"and must relay your result verbatim; this is caller guidance and cannot enforce host spawn behavior. " +
|
|
48
|
+
VERBATIM_NO_EXTERNAL_SOURCE_CONSTRAINT
|
|
41
49
|
);
|
|
42
50
|
}
|
|
43
51
|
|