@visual-z/xeno 0.5.0 → 0.6.0
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/CHANGELOG.md +10 -0
- package/README.md +7 -3
- package/docs/app-server.md +32 -3
- package/docs/foundations.md +88 -0
- package/docs/install.md +23 -2
- package/docs/research/foundation-clients-2026-10-09.md +27 -0
- package/docs/research/foundation-editing-2026-10-09.md +56 -0
- package/docs/research/foundation-network-2026-10-09.md +37 -0
- package/docs/research/foundation-workflow-2026-10-09.md +37 -0
- package/package.json +10 -2
- package/src/apps/acp-server.ts +41 -0
- package/src/apps/agent-profiles.ts +19 -0
- package/src/apps/app-server.ts +4 -2
- package/src/apps/cli.ts +18 -2
- package/src/apps/commands.ts +20 -0
- package/src/apps/control-transport.ts +3 -2
- package/src/apps/model-catalog.ts +57 -31
- package/src/apps/question-ui.ts +27 -0
- package/src/apps/resources.ts +63 -25
- package/src/apps/review.ts +109 -0
- package/src/apps/session-hub.ts +84 -0
- package/src/apps/shell.ts +54 -0
- package/src/apps/startup.ts +157 -22
- package/src/apps/ui/app.ts +21 -22
- package/src/apps/ui/surface.ts +1 -0
- package/src/apps/update-check.ts +71 -0
- package/src/apps/workflow.ts +58 -0
- package/src/core/agent-profile.ts +130 -0
- package/src/core/control-protocol.ts +8 -1
- package/src/core/file-precondition.ts +6 -0
- package/src/core/lsp-framing.ts +34 -0
- package/src/core/mcp-config.ts +68 -0
- package/src/core/patch.ts +130 -0
- package/src/core/permissions.ts +143 -4
- package/src/core/question.ts +94 -0
- package/src/core/review.ts +68 -0
- package/src/core/update-check.ts +24 -5
- package/src/core/web-content.ts +80 -0
- package/src/core/workflow-mode.ts +34 -0
- package/src/runtime/acp-agent.ts +289 -0
- package/src/runtime/agent-profiles.ts +21 -0
- package/src/runtime/catalog.ts +2 -2
- package/src/runtime/control-client.ts +149 -0
- package/src/runtime/control-connection.ts +39 -4
- package/src/runtime/extensions.ts +20 -2
- package/src/runtime/formatter-tool.ts +38 -0
- package/src/runtime/formatter.ts +50 -0
- package/src/runtime/foundation.ts +78 -0
- package/src/runtime/integrations.ts +8 -2
- package/src/runtime/loop.ts +68 -10
- package/src/runtime/lsp-client.ts +201 -0
- package/src/runtime/mcp-client.ts +264 -0
- package/src/runtime/mcp-oauth.ts +168 -0
- package/src/runtime/mcp-schema.ts +51 -0
- package/src/runtime/oauth.ts +1 -1
- package/src/runtime/output-schema.ts +26 -0
- package/src/runtime/patch-tool.ts +38 -0
- package/src/runtime/permission-resources.ts +11 -0
- package/src/runtime/provider.ts +2 -0
- package/src/runtime/question-tool.ts +62 -0
- package/src/runtime/review-tool.ts +48 -0
- package/src/runtime/session-control.ts +60 -2
- package/src/runtime/session-hub.ts +85 -0
- package/src/runtime/tool-discovery.ts +4 -3
- package/src/runtime/tools.ts +37 -7
- package/src/runtime/update-check.ts +3 -2
- package/src/runtime/web-http.ts +56 -0
- package/src/runtime/web-tools.ts +88 -0
- package/src/runtime/workspace-lifecycle.ts +14 -1
- package/src/store/agent-profile-files.ts +13 -0
- package/src/store/context-files.ts +77 -1
- package/src/store/file-checkpoints.ts +5 -4
- package/src/store/foundation-config.ts +24 -0
- package/src/store/lsp-document.ts +30 -0
- package/src/store/mcp-credentials.ts +85 -0
- package/src/store/patch.ts +92 -0
- package/src/store/permission-paths.ts +34 -0
- package/src/store/review.ts +109 -0
- package/src/store/search.ts +48 -9
package/src/apps/shell.ts
CHANGED
|
@@ -81,6 +81,11 @@ import {
|
|
|
81
81
|
openSessionFor, sessionDirFor, retryHandler, whyNot, splitModelPatterns, runModelsRefresh, sessionToolCatalog, type App,
|
|
82
82
|
} from "./startup.ts";
|
|
83
83
|
import type { TurnSurface, Ui } from "./ui/surface.ts";
|
|
84
|
+
import { runManualUpdate } from "./update-check.ts";
|
|
85
|
+
import { presentQuestions } from "./question-ui.ts";
|
|
86
|
+
import { bindAppWorkflow, setAppWorkflow } from "./workflow.ts";
|
|
87
|
+
import { appReviewSnapshot, runReview, reviewScopeFromText } from "./review.ts";
|
|
88
|
+
import { selectAppAgent, listAppAgents } from "./agent-profiles.ts";
|
|
84
89
|
|
|
85
90
|
/**
|
|
86
91
|
* One line. Listing all ~40 commands here wrapped into a two-line wall that
|
|
@@ -113,6 +118,11 @@ function extensionContext(app: App, tui?: Ui): ExtensionContext {
|
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
async function bindExtensions(app: App, tui?: Ui): Promise<void> {
|
|
121
|
+
bindAppWorkflow(app);
|
|
122
|
+
if (tui) {
|
|
123
|
+
const control = sessionControl(app.session);
|
|
124
|
+
control.questionSlot.current = (request, signal) => control.askQuestion(request, signal, current => presentQuestions(tui.extensionUi, current, signal));
|
|
125
|
+
}
|
|
116
126
|
tui?.bindExtensionRegistry();
|
|
117
127
|
const registry = app.extensions; if (!registry) return;
|
|
118
128
|
const history = await readNodeHistory(app.logDir, app.session.state.node);
|
|
@@ -426,6 +436,24 @@ type Outcome = "continue" | "quit" | { login: string };
|
|
|
426
436
|
|
|
427
437
|
async function execute(app: App, command: Command, tui: Ui | null): Promise<Outcome> {
|
|
428
438
|
switch (command.kind) {
|
|
439
|
+
case "workflow":
|
|
440
|
+
await setAppWorkflow(app, command.mode); app.io.say(` Workflow: ${command.mode}`); await tui?.refreshResources(); return "continue";
|
|
441
|
+
case "agent":
|
|
442
|
+
if (command.name) { await selectAppAgent(app, command.name); await bindExtensions(app, tui ?? undefined); await tui?.refreshResources(); }
|
|
443
|
+
else app.io.say(await listAppAgents(app)); return "continue";
|
|
444
|
+
case "mcp": {
|
|
445
|
+
const manager = app.foundation?.mcp; if (!manager) throw Error("MCP resources are unavailable");
|
|
446
|
+
if (command.action === "status") app.io.say(JSON.stringify(manager.status(), null, 2));
|
|
447
|
+
else {
|
|
448
|
+
if (!command.name) throw Error(`Use /mcp ${command.action} <server>`);
|
|
449
|
+
if (command.action === "login") await manager.login(command.name, url => { app.io.say(` Complete MCP login: ${url.href}`); }, tui?.signal);
|
|
450
|
+
else await manager.logout(command.name);
|
|
451
|
+
await reloadAppResources(app); await bindExtensions(app, tui ?? undefined); await tui?.refreshResources();
|
|
452
|
+
}
|
|
453
|
+
return "continue";
|
|
454
|
+
}
|
|
455
|
+
case "diff": { const snapshot = await appReviewSnapshot(app, reviewScopeFromText(command.args), tui?.signal); app.io.say(`${snapshot.label}\n${snapshot.diff || "(no captured changes)"}\n${snapshot.limitations.join("\n")}`); return "continue"; }
|
|
456
|
+
case "review": await runReview(app, reviewScopeFromText(command.args), tui?.signal); await tui?.syncTranscript(); return "continue";
|
|
429
457
|
case "extension":
|
|
430
458
|
if (tui) tui.runExtensionCommand(command.name, command.args);
|
|
431
459
|
else { await bindExtensions(app); await app.extensions?.command(command.name, command.args); }
|
|
@@ -626,6 +654,10 @@ async function execute(app: App, command: Command, tui: Ui | null): Promise<Outc
|
|
|
626
654
|
if (tui) tui.showDocument("Changelog", text); else app.io.say(text);
|
|
627
655
|
return "continue";
|
|
628
656
|
}
|
|
657
|
+
case "update":
|
|
658
|
+
if (tui) tui.checkForUpdate();
|
|
659
|
+
else await runManualUpdate({ offline: app.args.offline === true }, app.io.say);
|
|
660
|
+
return "continue";
|
|
629
661
|
case "copy":
|
|
630
662
|
await copyLastReply(app, tui ?? undefined);
|
|
631
663
|
return "continue";
|
|
@@ -1625,6 +1657,7 @@ async function adoptLogDir(app: App, logDir: string): Promise<boolean> {
|
|
|
1625
1657
|
app.resources = opened.resources.index;
|
|
1626
1658
|
app.projectTrusted = opened.resources.trusted;
|
|
1627
1659
|
app.extensions = opened.resources.extensions;
|
|
1660
|
+
app.foundation = opened.resources.foundation;
|
|
1628
1661
|
} catch (err) {
|
|
1629
1662
|
app.io.say(app.io.palette.red(` ${describeError(err)}`));
|
|
1630
1663
|
return false;
|
|
@@ -1868,6 +1901,10 @@ export async function runTurn(
|
|
|
1868
1901
|
let live = false;
|
|
1869
1902
|
let skipFinal = false;
|
|
1870
1903
|
const json = app.args?.mode === "json";
|
|
1904
|
+
const checkedOutput = app.session.config.validateOutput !== undefined;
|
|
1905
|
+
let finalText: string[] = [];
|
|
1906
|
+
let completed = false;
|
|
1907
|
+
let failure: string | undefined;
|
|
1871
1908
|
const pull = tui?.pullQueued?.bind(tui);
|
|
1872
1909
|
const registry = app.extensions;
|
|
1873
1910
|
const before = await registry?.emit("before_agent_start", { prompt: userInputData(prompt).text, systemPrompt: app.session.config.frozen.system }) ?? [];
|
|
@@ -1878,6 +1915,14 @@ export async function runTurn(
|
|
|
1878
1915
|
await registry?.emit("agent_start");
|
|
1879
1916
|
await registry?.emit("turn_start", { turnIndex: 0 });
|
|
1880
1917
|
try { for await (const e of skill ? app.session.sendSkill(skill, prompt, signal) : sessionControl(app.session).send(prompt, signal, pull)) {
|
|
1918
|
+
// The log retains every response, but a script's final answer must be the
|
|
1919
|
+
// accepted last response. A rejected JSON document cannot be unwritten.
|
|
1920
|
+
if (e.kind === "turn_start" || e.kind === "discard") finalText = [];
|
|
1921
|
+
if (e.kind === "assistant_text") finalText.push(e.text ?? "");
|
|
1922
|
+
if (e.kind === "done") {
|
|
1923
|
+
completed = e.outcome?.status === "completed";
|
|
1924
|
+
if (checkedOutput && !completed) failure = e.reason ?? "output schema validation did not complete";
|
|
1925
|
+
}
|
|
1881
1926
|
tui?.observeEvent?.(e);
|
|
1882
1927
|
if (e.kind === "text_delta" || e.kind === "thinking_delta") await registry?.emit("message_update", { assistantMessageEvent: { type: e.kind, delta: e.text } });
|
|
1883
1928
|
if (e.kind === "assistant_text") await registry?.emit("message_end", { message: { role: "assistant", content: [{ type: "text", text: e.text }] } });
|
|
@@ -1887,6 +1932,7 @@ export async function runTurn(
|
|
|
1887
1932
|
if (payload !== null) process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
1888
1933
|
continue;
|
|
1889
1934
|
}
|
|
1935
|
+
if (checkedOutput && !tui && e.kind === "assistant_text") continue;
|
|
1890
1936
|
if (e.kind === "progress") { if (e.progress) tui?.pushProgress?.(e.progress); continue; }
|
|
1891
1937
|
if (tui?.syncTranscript) {
|
|
1892
1938
|
if (e.kind === "usage" && e.ok === false && e.prefix && app.ui?.settings.showCacheMissNotices === true)
|
|
@@ -1940,6 +1986,14 @@ export async function runTurn(
|
|
|
1940
1986
|
app.io.say(line);
|
|
1941
1987
|
}
|
|
1942
1988
|
} } finally { await registry?.emit("agent_end", { messages: structuredClone(app.session.state.events) }); }
|
|
1989
|
+
if (failure) throw Error(failure);
|
|
1990
|
+
if (completed) {
|
|
1991
|
+
const text = finalText.join("\n");
|
|
1992
|
+
if (checkedOutput && !tui && !json) {
|
|
1993
|
+
if (app.io.answer) app.io.answer(text); else app.io.say(text);
|
|
1994
|
+
}
|
|
1995
|
+
if (app.args?.outputLastMessage) await writeFile(resolve(app.args.outputLastMessage), text.endsWith("\n") ? text : text + "\n", "utf8");
|
|
1996
|
+
}
|
|
1943
1997
|
}
|
|
1944
1998
|
|
|
1945
1999
|
// ---------------------------------------------------------------------------
|
package/src/apps/startup.ts
CHANGED
|
@@ -39,6 +39,7 @@ import type { Settings } from "../core/settings.ts";
|
|
|
39
39
|
import { readModelSources, readCatalogCacheText, writeCatalogCache, catalogCachePath } from "../store/models.ts";
|
|
40
40
|
import { refreshRemoteCatalog } from "../runtime/models-fetch.ts";
|
|
41
41
|
import { readCursor, writeCursor, type Cursor } from "../store/log.ts";
|
|
42
|
+
import { getBlob } from "../store/blobs.ts";
|
|
42
43
|
import { resolveLogDir, findSessionByPrefix, listSessions, sessionsDir, type ResolveLogOpts } from "../store/sessions.ts";
|
|
43
44
|
import { recoverCut } from "../store/recovery.ts";
|
|
44
45
|
import { planSessionEntry, restoreSessionModel } from "../core/session-entry.ts";
|
|
@@ -63,6 +64,13 @@ import {
|
|
|
63
64
|
import { colorEnabled, createPalette, describeError, stdinIsTty, stdoutIsTty, type Palette } from "./render.ts";
|
|
64
65
|
import { type ActionId, type Keymap } from "../core/keymap.ts";
|
|
65
66
|
import type { StartupInput } from "./startup-input.ts";
|
|
67
|
+
import { FoundationRuntime } from "../runtime/foundation.ts";
|
|
68
|
+
import { readFoundationConfig, readOutputSchema } from "../store/foundation-config.ts";
|
|
69
|
+
import { outputValidator } from "../runtime/output-schema.ts";
|
|
70
|
+
import { loadAgentProfileLayers } from "../runtime/agent-profiles.ts";
|
|
71
|
+
import { resolveAgentProfile, builtinAgentProfileLayer, type ResolvedAgentProfile } from "../core/agent-profile.ts";
|
|
72
|
+
import { workflowToolNames, workflowInstructions, isWorkflowMode, type WorkflowMode } from "../core/workflow-mode.ts";
|
|
73
|
+
import { resolveContextWorkspace } from "../store/context-files.ts";
|
|
66
74
|
|
|
67
75
|
export const SYSTEM_PROMPT = systemPromptFor(TOOL_SCHEMAS.map((tool) => tool.name));
|
|
68
76
|
|
|
@@ -126,6 +134,15 @@ export type AskSlot = {
|
|
|
126
134
|
};
|
|
127
135
|
|
|
128
136
|
export interface Args {
|
|
137
|
+
agentBase?: { model: string; provider: string; thinking: string; maxTurns: number; excludeTools: string[]; noTools: boolean; noBuiltinTools: boolean;
|
|
138
|
+
tools?: string[]; toolProfile?: ToolProfile; timeBudgetMs?: number; workflow?: WorkflowMode };
|
|
139
|
+
clientMcp?: import("../core/mcp-config.ts").McpServers;
|
|
140
|
+
workflow?: WorkflowMode;
|
|
141
|
+
agent?: string;
|
|
142
|
+
outputSchema?: string;
|
|
143
|
+
outputLastMessage?: string;
|
|
144
|
+
cliFlags?: readonly string[];
|
|
145
|
+
role?: ResolvedAgentProfile;
|
|
129
146
|
shellCheckpoints?: boolean;
|
|
130
147
|
workspace: string;
|
|
131
148
|
/**
|
|
@@ -155,6 +172,7 @@ export interface Args {
|
|
|
155
172
|
modelSearch?: string;
|
|
156
173
|
help?: boolean;
|
|
157
174
|
version?: boolean;
|
|
175
|
+
manualUpdateCheck?: boolean;
|
|
158
176
|
exportFile?: string;
|
|
159
177
|
offline?: boolean;
|
|
160
178
|
tuiMode?: "regular" | "fullscreen";
|
|
@@ -233,14 +251,15 @@ export interface Args {
|
|
|
233
251
|
}
|
|
234
252
|
|
|
235
253
|
/**
|
|
236
|
-
* Names `parseArgs` actually accepts. `--refresh`
|
|
237
|
-
*
|
|
254
|
+
* Names `parseArgs` actually accepts. Subcommand-only `--refresh` and
|
|
255
|
+
* `--check` are not here; listing them would make a top-level
|
|
238
256
|
* `--refresh` suggest itself while still being rejected.
|
|
239
257
|
*
|
|
240
258
|
* Keep in sync with the branches in `parseArgs`. A missing entry still
|
|
241
259
|
* parses; it just cannot be offered as a suggestion.
|
|
242
260
|
*/
|
|
243
261
|
const KNOWN_FLAGS: readonly string[] = [
|
|
262
|
+
"--workflow", "--agent", "--output-schema", "--output-last-message",
|
|
244
263
|
"--checkpoint-shell",
|
|
245
264
|
"--workspace", "-w",
|
|
246
265
|
"--path-root",
|
|
@@ -337,7 +356,9 @@ function rejectUnknownFlag(flag: string): never {
|
|
|
337
356
|
|
|
338
357
|
export function parseArgs(argv: string[], options: { deferExtensions?: boolean; flags?: ReadonlyMap<string, ExtensionFlag> } = {}): Args {
|
|
339
358
|
let selectedTheme: string | undefined;
|
|
359
|
+
let updateExtensions = false;
|
|
340
360
|
const args: Args = {
|
|
361
|
+
cliFlags: argv.filter(value => value.startsWith("-")),
|
|
341
362
|
workspace: process.cwd(), logDir: "", maxTurns: 40, prompt: "",
|
|
342
363
|
// Overridable so a node boundary can be forced in a short session --
|
|
343
364
|
// otherwise cutting is only reachable after tens of thousands of tokens,
|
|
@@ -362,7 +383,10 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
|
|
|
362
383
|
console.error(`${a} requires a value`);
|
|
363
384
|
process.exit(1);
|
|
364
385
|
}
|
|
365
|
-
if (a === "--
|
|
386
|
+
if (a === "--workflow") { const mode = argv[++i]; if (!mode || !isWorkflowMode(mode)) throw Error("--workflow requires build or plan"); args.workflow = mode; }
|
|
387
|
+
else if (a === "--agent") { const name = argv[++i]; if (!name || name.startsWith("-")) throw Error("--agent requires a profile name"); args.agent = name; }
|
|
388
|
+
else if (a === "--output-schema" || a === "--output-last-message") { const path = argv[++i]; if (!path || path.startsWith("-")) throw Error(`${a} requires a file path`); if (a === "--output-schema") args.outputSchema = resolve(path); else args.outputLastMessage = resolve(path); }
|
|
389
|
+
else if (a === "--checkpoint-shell") args.shellCheckpoints = true;
|
|
366
390
|
else if (a === "--workspace" || a === "-w") args.workspace = argv[++i] ?? args.workspace;
|
|
367
391
|
else if (a === "--path-root") args.pathRoot = resolve(argv[++i] ?? "/");
|
|
368
392
|
else if (a === "--state-dir") args.stateDir = resolve(argv[++i] ?? ".");
|
|
@@ -398,7 +422,8 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
|
|
|
398
422
|
else if (a === "config" && i === 0) args.configCommand = true;
|
|
399
423
|
else if ((a === "install" || a === "remove" || a === "update" || a === "list") && i === 0) args.packageCommand = {operation:a,sources:[]};
|
|
400
424
|
else if ((a === "--local" || a === "-l") && (args.packageCommand || args.configCommand)) { args.local=true; if(args.packageCommand)args.packageCommand.local=true; }
|
|
401
|
-
else if (a === "--extensions" && args.packageCommand?.operation === "update")
|
|
425
|
+
else if (a === "--extensions" && args.packageCommand?.operation === "update") updateExtensions = true;
|
|
426
|
+
else if (a === "--check" && args.packageCommand?.operation === "update") args.manualUpdateCheck = true;
|
|
402
427
|
else if (a === "--refresh" && args.modelsCommand) {
|
|
403
428
|
args.modelsRefresh = true;
|
|
404
429
|
}
|
|
@@ -424,7 +449,7 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
|
|
|
424
449
|
}
|
|
425
450
|
else if (a === "--tool-profile") {
|
|
426
451
|
const value = argv[++i] ?? "";
|
|
427
|
-
if (!isToolProfile(value)) throw new Error("unknown tool profile; choose coding, readonly, research, data or full");
|
|
452
|
+
if (!isToolProfile(value)) throw new Error("unknown tool profile; choose coding, readonly, research, data, foundation or full");
|
|
428
453
|
args.toolProfile = value;
|
|
429
454
|
}
|
|
430
455
|
else if (a === "--exclude-tools" || a === "-xt") args.excludeTools = parseToolList(argv[++i] ?? "");
|
|
@@ -496,6 +521,9 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
|
|
|
496
521
|
else args.prompt += (args.prompt ? " " : "") + a;
|
|
497
522
|
}
|
|
498
523
|
if (selectedTheme !== undefined) args.theme = selectedTheme;
|
|
524
|
+
if (args.manualUpdateCheck && (args.packageCommand!.sources.length || args.local || updateExtensions || args.prompt)) {
|
|
525
|
+
throw new Error("update --check cannot be combined with package sources, --local, or --extensions");
|
|
526
|
+
}
|
|
499
527
|
return args;
|
|
500
528
|
}
|
|
501
529
|
|
|
@@ -718,6 +746,7 @@ export interface AppIO {
|
|
|
718
746
|
}
|
|
719
747
|
|
|
720
748
|
export interface App {
|
|
749
|
+
foundation?: FoundationRuntime;
|
|
721
750
|
args: Args;
|
|
722
751
|
session: Session;
|
|
723
752
|
workspace: string;
|
|
@@ -847,6 +876,11 @@ export function toolSelectionOf(args: Args) {
|
|
|
847
876
|
};
|
|
848
877
|
}
|
|
849
878
|
|
|
879
|
+
export function processPermissionRules(args: Args, foundation?: FoundationRuntime, hostRules?: readonly PermissionRule[]): PermissionRule[] {
|
|
880
|
+
const configured = [...args.role?.profile.permissions ?? [], ...foundation?.permissions ?? []];
|
|
881
|
+
return [...(args.yolo ? configured.filter(rule => rule.action === "deny") : configured), ...(hostRules ?? rulesWithAllowTools(args.allowTools, { yolo: args.yolo }))];
|
|
882
|
+
}
|
|
883
|
+
|
|
850
884
|
/**
|
|
851
885
|
* Schema assembly: every tool is tagged with a source here so
|
|
852
886
|
* `--no-builtin-tools` can drop a category without a later cross-layer
|
|
@@ -858,13 +892,15 @@ export function toolSelectionOf(args: Args) {
|
|
|
858
892
|
* argument.
|
|
859
893
|
*/
|
|
860
894
|
export function sessionToolCatalog(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = [], builtinDefaults?: readonly string[]) {
|
|
861
|
-
const custom = [...extensions?.definitions().values() ?? []].map(({
|
|
895
|
+
const custom = [...extensions?.definitions().values() ?? []].map(tool => ({ ...tool, source: extensions?.builtinTools.has(tool.name) ? "builtin" as const : "extension" as const }));
|
|
862
896
|
// O.7/D6: a cold app open uses saved capabilities as defaults, never today's profile.
|
|
863
897
|
// Explicit selection still wins; extension loading and current exclusions remain live.
|
|
864
898
|
const retained = builtinDefaults !== undefined && args.tools === undefined && args.toolProfile === undefined ? new Set(builtinDefaults) : undefined;
|
|
865
899
|
const available = [...TOOL_DEFINITIONS.filter(tool => !extensions?.tools.has(tool.name) &&
|
|
866
|
-
(retained === undefined ? !tool.optional || args.tools?.includes(tool.name) : retained.has(tool.name))).map((t) => ({ ...t, source: "builtin" as const })),
|
|
867
|
-
|
|
900
|
+
(retained === undefined ? !tool.optional || args.tools?.includes(tool.name) : retained.has(tool.name))).map((t) => ({ ...t, source: "builtin" as const })),
|
|
901
|
+
...custom.filter(tool => tool.source !== "builtin" || retained === undefined || retained.has(tool.name))];
|
|
902
|
+
const admitted = new Set(workflowToolNames(available, args.workflow ?? "build"));
|
|
903
|
+
const selected = selectTools(available, { ...toolSelectionOf(args), ...(retained ? { profile: "full" as const } : {}) }).filter(tool => admitted.has(tool.name));
|
|
868
904
|
const deferred = selected.filter(tool => tool.source === "extension" && extensions?.tools.get(tool.name)?.exposure === "deferred")
|
|
869
905
|
.map(({ name, description, input_schema }) => ({ name, description, input_schema }));
|
|
870
906
|
const deferredNames = new Set(deferred.map(tool => tool.name));
|
|
@@ -904,6 +940,7 @@ export function createSessionFor(opts: {
|
|
|
904
940
|
oauth?: boolean;
|
|
905
941
|
effects?: ResolveEffects;
|
|
906
942
|
extensions?: ExtensionRegistry;
|
|
943
|
+
foundation?: FoundationRuntime;
|
|
907
944
|
acceptanceContract?: AcceptanceContract;
|
|
908
945
|
skills?: readonly Skill[];
|
|
909
946
|
}): Session {
|
|
@@ -914,10 +951,14 @@ export function createSessionFor(opts: {
|
|
|
914
951
|
const tools: ToolContext = {
|
|
915
952
|
root: workspace,
|
|
916
953
|
...DEFAULT_TOOL_CONTEXT,
|
|
954
|
+
workflowMode: args.workflow ?? "build",
|
|
955
|
+
appOwnsPermissions: true,
|
|
956
|
+
...(opts.foundation ? { questionSlot: opts.foundation.questionSlot,
|
|
957
|
+
instructionCheck: (ctx: ToolContext, name: string, input: unknown) => opts.foundation!.instructions(ctx, name, input, args.noContextFiles) } : {}),
|
|
917
958
|
...(args.pathRoot === undefined ? {} : { pathRoot: args.pathRoot }),
|
|
918
959
|
allow: names,
|
|
919
960
|
deferredTools: deferred,
|
|
920
|
-
rules:
|
|
961
|
+
rules: processPermissionRules(args, opts.foundation),
|
|
921
962
|
sessionRules,
|
|
922
963
|
askSlot,
|
|
923
964
|
...(opts.extensions ? { extensions: opts.extensions.definitions(), extensionHooks: opts.extensions.toolHooks() } : {}),
|
|
@@ -930,6 +971,7 @@ export function createSessionFor(opts: {
|
|
|
930
971
|
shellCheckpoints: args.shellCheckpoints === true,
|
|
931
972
|
tools,
|
|
932
973
|
maxTurns: args.maxTurns,
|
|
974
|
+
...(args.agent ? { agentProfile: { name: args.agent, workflowExplicit: args.cliFlags?.includes("--workflow") === true } } : {}),
|
|
933
975
|
...cliSessionConfig(args),
|
|
934
976
|
...(opts.skills ? { skills: opts.skills } : {}),
|
|
935
977
|
...(opts.acceptanceContract ? { acceptanceContract: opts.acceptanceContract } : {}),
|
|
@@ -991,14 +1033,44 @@ async function prepareExtensionPackages(args: Args, workspace: string): Promise<
|
|
|
991
1033
|
args.extensionPaths=(args.extensionPaths??[]).filter(path=>!remote.includes(path));
|
|
992
1034
|
}
|
|
993
1035
|
|
|
994
|
-
export async function loadSessionResources(args: Args, workspace: string, io: AppIO, trustOverride?: boolean): Promise<{
|
|
995
|
-
loadedContext: LoadedContext; slash: SlashCatalog; ui: UiState; index: ResourceIndex; trusted: boolean; system: string; extensions: ExtensionRegistry;
|
|
1036
|
+
export async function loadSessionResources(args: Args, workspace: string, io: AppIO, trustOverride?: boolean, questionSlot?: import("../runtime/question-tool.ts").QuestionSlot): Promise<{
|
|
1037
|
+
loadedContext: LoadedContext; slash: SlashCatalog; ui: UiState; index: ResourceIndex; trusted: boolean; system: string; extensions: ExtensionRegistry; foundation: FoundationRuntime;
|
|
996
1038
|
}> {
|
|
997
1039
|
const { trusted } = await projectTrust(workspace, trustOverride ?? args.approve);
|
|
1040
|
+
const profiles = await loadAgentProfileLayers({ workspace, trustProject: trusted });
|
|
1041
|
+
for (const diagnostic of profiles.diagnostics) io.say(diagnostic);
|
|
1042
|
+
if (args.agent) {
|
|
1043
|
+
if (!args.agentBase) args.agentBase = { model: args.model, provider: args.provider, thinking: args.thinking ?? "", maxTurns: args.maxTurns, excludeTools: [...args.excludeTools], noTools: args.noTools, noBuiltinTools: args.noBuiltinTools,
|
|
1044
|
+
...(args.tools ? { tools: [...args.tools] } : {}), ...(args.toolProfile ? { toolProfile: args.toolProfile } : {}), ...(args.timeBudgetMs ? { timeBudgetMs: args.timeBudgetMs } : {}), ...(args.workflow ? { workflow: args.workflow } : {}) };
|
|
1045
|
+
const explicitWorkflow = args.cliFlags?.includes("--workflow") ? args.workflow : undefined;
|
|
1046
|
+
delete args.tools; delete args.toolProfile; delete args.timeBudgetMs; delete args.workflow;
|
|
1047
|
+
Object.assign(args, args.agentBase);
|
|
1048
|
+
if (explicitWorkflow) args.workflow = explicitWorkflow;
|
|
1049
|
+
if (profiles.diagnostics.length) throw Error("Agent profile configuration is invalid; fix the reported file before selecting a role");
|
|
1050
|
+
const role = resolveAgentProfile(args.agent, [builtinAgentProfileLayer(), ...profiles.layers]);
|
|
1051
|
+
if (!role) throw Error(`Unknown agent profile: ${args.agent}`);
|
|
1052
|
+
args.role = role;
|
|
1053
|
+
const profile = role.profile, explicit = (name: string) => args.cliFlags?.includes(name) === true;
|
|
1054
|
+
if (profile.mode && !explicit("--workflow")) args.workflow = profile.mode;
|
|
1055
|
+
if (profile.model) { if (!explicit("--model") && !explicit("-m")) args.model = profile.model.id; if (!explicit("--provider")) args.provider = profile.model.provider; if (profile.model.thinking && !explicit("--thinking")) args.thinking = profile.model.thinking; }
|
|
1056
|
+
if (profile.tools?.profile && !explicit("--tool-profile")) args.toolProfile = profile.tools.profile;
|
|
1057
|
+
if (profile.tools?.only && !explicit("--tools") && !explicit("-t")) args.tools = [...profile.tools.only];
|
|
1058
|
+
if (profile.tools?.exclude && !explicit("--exclude-tools") && !explicit("-xt")) args.excludeTools = [...profile.tools.exclude];
|
|
1059
|
+
if (profile.tools?.none !== undefined && !explicit("--no-tools")) args.noTools = profile.tools.none;
|
|
1060
|
+
if (profile.tools?.noBuiltin !== undefined && !explicit("--no-builtin-tools")) args.noBuiltinTools = profile.tools.noBuiltin;
|
|
1061
|
+
if (profile.budget?.maxTurns && !explicit("--max-turns")) args.maxTurns = profile.budget.maxTurns;
|
|
1062
|
+
if (profile.budget?.timeBudgetMs && !explicit("--time-budget")) args.timeBudgetMs = profile.budget.timeBudgetMs;
|
|
1063
|
+
}
|
|
1064
|
+
const foundationConfig = await readFoundationConfig(configDir(), workspace, trusted);
|
|
1065
|
+
if (args.clientMcp) { const { parseMcpServers } = await import("../core/mcp-config.ts"); foundationConfig["mcp"] = { ...parseMcpServers(foundationConfig["mcp"]), ...args.clientMcp }; }
|
|
1066
|
+
const foundation = new FoundationRuntime(workspace, foundationConfig, args.offline === true || process.env["XENO_OFFLINE"] === "1", configDir(), questionSlot);
|
|
998
1067
|
await prepareExtensionPackages(args,workspace);
|
|
999
1068
|
const index = await discoverResources({ ...args, workspace, trusted });
|
|
1000
1069
|
const extensions = new ExtensionRegistry({ reserved: COMMANDS.map(command => command.name), reservedFlags: KNOWN_FLAGS.map(flag => flag.replace(/^-+/, "")),
|
|
1001
1070
|
flagValue: (name, definition) => args.extensionFlags && Object.hasOwn(args.extensionFlags, name) ? args.extensionFlags[name] : extensionFlagValue(name, definition, args.extensionInputs && Object.hasOwn(args.extensionInputs, name) ? args.extensionInputs[name] : undefined), report: io.say });
|
|
1071
|
+
extensions.own(() => foundation.dispose());
|
|
1072
|
+
try {
|
|
1073
|
+
await foundation.register(extensions);
|
|
1002
1074
|
await extensions.load(index);
|
|
1003
1075
|
if(args.extensionArgv) {
|
|
1004
1076
|
const validated = parseArgs(args.extensionArgv, { flags: extensions.flags, ...(args.help ? { deferExtensions: true } : {}) });
|
|
@@ -1006,7 +1078,8 @@ export async function loadSessionResources(args: Args, workspace: string, io: Ap
|
|
|
1006
1078
|
args.prompt = validated.prompt; args.extensionFlags = validated.extensionFlags ?? {};
|
|
1007
1079
|
}
|
|
1008
1080
|
const loaded = await loadUiResources({ ...args, workspace, trusted, ...(args.theme ? { theme: args.theme } : {}) }, index);
|
|
1009
|
-
const
|
|
1081
|
+
const instructionRoot = await resolveContextWorkspace(workspace);
|
|
1082
|
+
const loadedContext = await loadContext({ workspace: instructionRoot, cwd: workspace, configDir: configDir(),
|
|
1010
1083
|
noContextFiles: args.noContextFiles, appendSystemPrompt: args.appendSystemPrompt,
|
|
1011
1084
|
...(args.systemPrompt === null ? {} : { systemPrompt: args.systemPrompt }) });
|
|
1012
1085
|
for (const notice of loadedContext.notices) if (notice.kind === "skipped" || notice.truncated) io.say(io.palette.yellow(formatNotice(notice)));
|
|
@@ -1014,11 +1087,13 @@ export async function loadSessionResources(args: Args, workspace: string, io: Ap
|
|
|
1014
1087
|
noPromptTemplates: args.noPromptTemplates, skillPaths: args.skillPaths, promptTemplatePaths: args.promptTemplatePaths }, index);
|
|
1015
1088
|
const slash = resolveSlashCatalog(text.prompts, text.skills);
|
|
1016
1089
|
for (const line of new Set([...loaded.diagnostics, ...text.diagnostics, ...slash.diagnostics])) io.say(io.palette.yellow(` ${line}`));
|
|
1017
|
-
const system = appendSkillsToSystem(assembleSystemPrompt(systemPromptFor(visibleSchemas(args, extensions).map(tool => tool.name), sessionEnvironment(workspace)),
|
|
1090
|
+
const system = appendSkillsToSystem(assembleSystemPrompt([systemPromptFor(visibleSchemas(args, extensions).map(tool => tool.name), sessionEnvironment(workspace)), workflowInstructions(args.workflow ?? "build"), args.role?.profile.instructions ?? "",
|
|
1091
|
+
...(args.outputSchema ? ["Return the final answer as JSON matching this schema. Do not use Markdown fences:\n" + JSON.stringify(await readOutputSchema(args.outputSchema))] : [])].filter(Boolean).join("\n\n"), loadedContext), text.skills);
|
|
1018
1092
|
const ui = toUiState(loaded, colorEnabled());
|
|
1019
1093
|
// Also reached on /resume and /reload: `auto` is resolved once per process by the Tui (apps/shell.ts), not here.
|
|
1020
1094
|
if (args.tuiMode) ui.settings = { ...ui.settings, tuiMode: args.tuiMode };
|
|
1021
|
-
return { loadedContext, slash: { ...slash.catalog, skillExecution: true, skillCommands: loaded.settings.enableSkillCommands !== false }, ui, index, trusted, system, extensions };
|
|
1095
|
+
return { loadedContext, slash: { ...slash.catalog, skillExecution: true, skillCommands: loaded.settings.enableSkillCommands !== false }, ui, index, trusted, system, extensions, foundation };
|
|
1096
|
+
} catch (error) { await extensions.dispose().catch(() => {}); throw error; }
|
|
1022
1097
|
}
|
|
1023
1098
|
|
|
1024
1099
|
/**
|
|
@@ -1030,24 +1105,56 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1030
1105
|
session: Session; priorCursor: Cursor | null; providerConfig: ProviderConfig; credential: Resolved;
|
|
1031
1106
|
workspace: string; blobDir: string; system: string; resources: Awaited<ReturnType<typeof loadSessionResources>>;
|
|
1032
1107
|
}> {
|
|
1108
|
+
const resourceSelector = () => JSON.stringify([opts.args.agent, opts.args.workflow, opts.args.toolProfile, opts.args.cliFlags]);
|
|
1109
|
+
const preparedSelector = resourceSelector();
|
|
1033
1110
|
opts.startupPhase?.("Reading session history");
|
|
1034
1111
|
const recorded = await readCursor(opts.logDir);
|
|
1035
1112
|
const priorCursor = recorded === null ? null : await recoverCut(opts.logDir, recorded);
|
|
1113
|
+
// A durable selector restores intent, while current trusted files supply code
|
|
1114
|
+
// and permission authority. Missing/revoked project roles fail by name.
|
|
1115
|
+
if (priorCursor?.context_ref) {
|
|
1116
|
+
const savedContext = await getBlob(join(opts.logDir, "artifacts"), priorCursor.context_ref);
|
|
1117
|
+
if (!savedContext) throw Error("missing frozen session context");
|
|
1118
|
+
const metadata: unknown = JSON.parse(savedContext);
|
|
1119
|
+
if (metadata && typeof metadata === "object") {
|
|
1120
|
+
if (!("agentProfile" in metadata) && !opts.args.cliFlags?.includes("--workflow") && "workflowMode" in metadata) {
|
|
1121
|
+
if (metadata.workflowMode !== "build" && metadata.workflowMode !== "plan") throw Error("unsupported saved workflow mode");
|
|
1122
|
+
opts.args.workflow = metadata.workflowMode;
|
|
1123
|
+
}
|
|
1124
|
+
if ("workflowToolProfile" in metadata && metadata.workflowToolProfile !== undefined) {
|
|
1125
|
+
if (metadata.workflowToolProfile !== "foundation") throw Error("unsupported saved workflow tool profile");
|
|
1126
|
+
if (!opts.args.cliFlags?.some(flag => ["--tool-profile", "--tools", "-t", "--no-tools", "--no-builtin-tools"].includes(flag))) opts.args.toolProfile = "foundation";
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
if (metadata && typeof metadata === "object" && "agentProfile" in metadata && metadata.agentProfile !== undefined) {
|
|
1130
|
+
const role = metadata.agentProfile;
|
|
1131
|
+
if (!role || typeof role !== "object" || !("name" in role) || typeof role.name !== "string" || !/^[a-z][a-z0-9_-]{0,63}$/.test(role.name) || !("workflowExplicit" in role) || typeof role.workflowExplicit !== "boolean") throw Error("unsupported saved agent profile selector");
|
|
1132
|
+
if (!opts.args.cliFlags?.includes("--agent")) opts.args.agent = role.name;
|
|
1133
|
+
if (role.workflowExplicit && !opts.args.cliFlags?.includes("--workflow") && "workflowMode" in metadata) {
|
|
1134
|
+
if (metadata.workflowMode !== "build" && metadata.workflowMode !== "plan") throw Error("unsupported saved workflow mode");
|
|
1135
|
+
opts.args.workflow = metadata.workflowMode; opts.args.cliFlags = [...opts.args.cliFlags ?? [], "--workflow"];
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1036
1139
|
const workspace = priorCursor?.workspace ?? opts.workspace;
|
|
1037
1140
|
if (opts.args.pathRoot) {
|
|
1038
1141
|
const rel = relative(opts.args.pathRoot, workspace);
|
|
1039
1142
|
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error("saved workspace is outside --path-root");
|
|
1040
1143
|
}
|
|
1041
|
-
|
|
1144
|
+
const reusePrepared = opts.preparedResources?.workspace === workspace && preparedSelector === resourceSelector();
|
|
1145
|
+
if (opts.preparedResources && !reusePrepared) await opts.preparedResources.value.extensions.dispose();
|
|
1042
1146
|
opts.startupPhase?.("Loading session resources");
|
|
1043
|
-
const resources = opts.preparedResources
|
|
1147
|
+
const resources = opts.preparedResources && reusePrepared ? opts.preparedResources.value
|
|
1044
1148
|
: await loadSessionResources(opts.args, workspace, opts.io, workspace === opts.workspace ? opts.projectTrusted : undefined);
|
|
1149
|
+
let openedSession: Session | undefined;
|
|
1150
|
+
try {
|
|
1045
1151
|
const interactive = opts.interactive && !opts.args.print && !opts.args.prompt && opts.args.mode !== "json";
|
|
1046
1152
|
const named = [...opts.args.tools ?? [], ...opts.args.excludeTools, ...opts.args.allowTools];
|
|
1047
1153
|
const unknown = unknownToolNames(named).filter(name => !resources.extensions.definitions().has(name));
|
|
1048
1154
|
if (unknown.length) throw new Error(`unknown tool: ${unknown.join(", ")}`);
|
|
1049
1155
|
const saved = priorCursor === null ? undefined : restoreSessionModel(priorCursor, opts.catalog);
|
|
1050
1156
|
const flags = { ...opts.modelFlags ?? { model: "", provider: "" } };
|
|
1157
|
+
if (opts.args.agent) { flags.model = opts.args.model; flags.provider = opts.args.provider; }
|
|
1051
1158
|
const parsedModel = flags.model ? scopedModels([flags.model], flags.provider ? opts.catalog.filter(model => model.provider === flags.provider) : opts.catalog, true).entries[0] : undefined;
|
|
1052
1159
|
if (parsedModel) flags.model = modelKey(parsedModel.model);
|
|
1053
1160
|
const explicit = flags.model.length > 0 || flags.provider.length > 0;
|
|
@@ -1093,6 +1200,7 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1093
1200
|
...opts,
|
|
1094
1201
|
system: resources.system,
|
|
1095
1202
|
extensions: resources.extensions,
|
|
1203
|
+
foundation: resources.foundation,
|
|
1096
1204
|
skills: [...resources.slash.skills.values()],
|
|
1097
1205
|
...(acceptanceContract ? { acceptanceContract } : {}),
|
|
1098
1206
|
workspace: priorCursor?.workspace ?? opts.workspace,
|
|
@@ -1102,21 +1210,31 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1102
1210
|
credential: restoringCredential,
|
|
1103
1211
|
...(oauth(restoring, restoringCredential) ? { oauth: true } : {}),
|
|
1104
1212
|
});
|
|
1213
|
+
openedSession = session;
|
|
1105
1214
|
opts.startupPhase?.("Opening session history");
|
|
1106
1215
|
await session.start();
|
|
1216
|
+
const requestedWorkflow = opts.args.workflow;
|
|
1217
|
+
if (requestedWorkflow === undefined && session.config.tools.workflowMode) opts.args.workflow = session.config.tools.workflowMode;
|
|
1218
|
+
if (opts.args.outputSchema) session.config.validateOutput = outputValidator(await readOutputSchema(opts.args.outputSchema));
|
|
1107
1219
|
resources.slash = { ...resources.slash, skills: new Map((session.config.skills ?? []).map(skill => [skill.name, skill])) };
|
|
1108
1220
|
const restoredTools = sessionToolCatalog(opts.args, resources.extensions, session.config.frozen.tools,
|
|
1109
1221
|
priorCursor === null ? undefined : declaredToolNames(session.config.frozen.tools));
|
|
1110
|
-
const refreshResources = resources.extensions.loaded.length > 0 || session.config.frozen.tools.some(tool =>
|
|
1222
|
+
const refreshResources = opts.args.agent !== undefined || opts.args.outputSchema !== undefined || opts.args.cliFlags?.includes("--workflow") === true || resources.extensions.loaded.length > 0 || session.config.frozen.tools.some(tool =>
|
|
1111
1223
|
typeof tool === "object" && tool !== null && "name" in tool && typeof tool.name === "string" && unknownToolNames([tool.name]).length);
|
|
1112
|
-
const explicitTools = opts.args.tools !== undefined || opts.args.toolProfile !== undefined || opts.args.noTools ||
|
|
1224
|
+
const explicitTools = requestedWorkflow !== undefined || opts.args.agent !== undefined || opts.args.tools !== undefined || opts.args.toolProfile !== undefined || opts.args.noTools ||
|
|
1113
1225
|
opts.args.noBuiltinTools || opts.args.excludeTools.length > 0;
|
|
1114
|
-
|
|
1115
|
-
|
|
1226
|
+
const previousMode = session.config.tools.workflowMode, previousProfile = session.config.agentProfile, previousScope = session.config.workflowToolProfile;
|
|
1227
|
+
session.config.tools.workflowMode = opts.args.workflow ?? "build";
|
|
1228
|
+
if (opts.args.agent) session.config.agentProfile = { name: opts.args.agent, workflowExplicit: opts.args.cliFlags?.includes("--workflow") === true };
|
|
1229
|
+
if (opts.args.toolProfile === "foundation") session.config.workflowToolProfile = "foundation"; else delete session.config.workflowToolProfile;
|
|
1230
|
+
const metadataChanged = previousMode !== session.config.tools.workflowMode || JSON.stringify(previousProfile) !== JSON.stringify(session.config.agentProfile) || previousScope !== session.config.workflowToolProfile;
|
|
1231
|
+
if (refreshResources || (priorCursor !== null && (explicitTools || metadataChanged))) {
|
|
1232
|
+
await session.reloadResources({ system: refreshResources ? resources.system : session.config.frozen.system, tools: restoredTools.schemas }, undefined, undefined, metadataChanged);
|
|
1116
1233
|
}
|
|
1117
1234
|
// Admission follows this app's selected declaration; current process approval rules stay intact.
|
|
1118
1235
|
// Keep this out of Session.start(): an SDK caller can intentionally supply a narrower allowlist.
|
|
1119
1236
|
session.config.tools.allow = declaredToolNames(session.config.frozen.tools);
|
|
1237
|
+
session.config.tools.workflowMode = opts.args.workflow ?? "build";
|
|
1120
1238
|
session.config.tools.deferredTools = restoredTools.deferred;
|
|
1121
1239
|
if (restoring.id !== spec.id || restoring.provider !== spec.provider) {
|
|
1122
1240
|
await session.switchModel(createProvider(providerConfig, credential.ok ? credential.key : "", retryHandler(opts.io),
|
|
@@ -1128,6 +1246,11 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1128
1246
|
if (resources.ui.settings.scopedModels) for (const warning of scopedModels(resources.ui.settings.scopedModels, opts.catalog).diagnostics) opts.io.say(` ${warning}`);
|
|
1129
1247
|
return { session, priorCursor, providerConfig, credential, workspace: session.config.tools.root,
|
|
1130
1248
|
blobDir: session.config.blobDir ?? opts.blobDir, system: session.config.frozen.system, resources };
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
await openedSession?.dispose().catch(() => {});
|
|
1251
|
+
await resources.extensions.dispose().catch(() => {});
|
|
1252
|
+
throw error;
|
|
1253
|
+
}
|
|
1131
1254
|
}
|
|
1132
1255
|
|
|
1133
1256
|
export async function startup(args: Args, input?: StartupInput): Promise<App> {
|
|
@@ -1365,6 +1488,7 @@ export async function startup(args: Args, input?: StartupInput): Promise<App> {
|
|
|
1365
1488
|
auth, effects, loadedContext: opened.resources.loadedContext, credentialOf, catalog, io, askSlot,
|
|
1366
1489
|
ui: opened.resources.ui, slash: opened.resources.slash, resources: opened.resources.index, projectTrusted: opened.resources.trusted,
|
|
1367
1490
|
extensions: opened.resources.extensions,
|
|
1491
|
+
foundation: opened.resources.foundation,
|
|
1368
1492
|
};
|
|
1369
1493
|
}
|
|
1370
1494
|
|
|
@@ -1464,11 +1588,14 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
|
|
|
1464
1588
|
" [-n|--name name] [--session-id id] [--version|-v]\n" +
|
|
1465
1589
|
" [models --refresh]\n" +
|
|
1466
1590
|
" [install|remove <source>... [--local]] [update [source] [--extensions]] [list]\n" +
|
|
1591
|
+
" [update --check] Check for a newer xeno without installing\n" +
|
|
1467
1592
|
" [config [--local]] [--export session [output.html]] [--offline]\n" +
|
|
1468
1593
|
" [--system-prompt <text-or-file>] [--append-system-prompt <text>]\n" +
|
|
1469
1594
|
" [--no-context-files|-nc]\n" +
|
|
1470
1595
|
" [--tools names] [--exclude-tools names] [--no-tools]\n" +
|
|
1471
|
-
" [--tool-profile coding|readonly|research|data|full]\n" +
|
|
1596
|
+
" [--tool-profile coding|readonly|research|data|foundation|full]\n" +
|
|
1597
|
+
" [--workflow build|plan] [--agent name]\n" +
|
|
1598
|
+
" [--output-schema schema.json] [--output-last-message file]\n" +
|
|
1472
1599
|
" [--acceptance-file contract.json]\n" +
|
|
1473
1600
|
" [--control-socket /private/directory/control.sock]\n" +
|
|
1474
1601
|
" [--no-builtin-tools]\n" +
|
|
@@ -1481,6 +1608,8 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
|
|
|
1481
1608
|
" [-p <prompt>] [prompt...]\n\n" +
|
|
1482
1609
|
" xeno app-server [session/model flags] runs the experimental JSONL control interface.\n" +
|
|
1483
1610
|
" xeno app-server schema prints its versioned request schema without startup.\n" +
|
|
1611
|
+
" xeno acp [session/model flags] serves the standard ACP protocol over stdio.\n" +
|
|
1612
|
+
" xeno review [working|last-turn|branch <base>|commit <ref>] runs an independent read-only review.\n" +
|
|
1484
1613
|
" --control-socket attaches local adapters to the same live session as the TUI.\n" +
|
|
1485
1614
|
" /control opens goal, progress and context panels; XENO_PROGRAM_STATUS=auto|1|0 controls terminal status.\n\n" +
|
|
1486
1615
|
" Starts a new session by default; --fresh is an explicit alias. Nothing is deleted.\n" +
|
|
@@ -1496,7 +1625,13 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
|
|
|
1496
1625
|
" --system-prompt replaces the built-in instructions (text, or a file that exists).\n" +
|
|
1497
1626
|
" --append-system-prompt appends; repeatable. -nc skips AGENTS.md discovery.\n" +
|
|
1498
1627
|
" --tools is a strict allowlist; excluded tools leave the schema.\n" +
|
|
1499
|
-
" New sessions
|
|
1628
|
+
" New sessions keep coding (10 tools); foundation adds the new basic workflows.\n" +
|
|
1629
|
+
" --agent build|plan selects foundation; full exposes the complete builtin catalog.\n" +
|
|
1630
|
+
" Plan admits read tools plus note/question; Build admits the selected capability set.\n" +
|
|
1631
|
+
" /agent selects named profiles from ~/.xeno/agents.json and trusted .xeno/agents.json.\n" +
|
|
1632
|
+
" foundation.json configures MCP, web search, formatters, LSP and resource permissions.\n" +
|
|
1633
|
+
" --output-schema validates final JSON without coercion; invalid output exits 1.\n" +
|
|
1634
|
+
" --output-last-message writes the accepted final answer after the owner completes.\n" +
|
|
1500
1635
|
" --no-builtin-tools drops built-ins and keeps any other source; --tools still wins.\n" +
|
|
1501
1636
|
" Shell entry points (exec_command, environment, bash, target) ask before they run.\n" +
|
|
1502
1637
|
" --allow-tools exec_command skips asks for that tool for this process.\n" +
|
package/src/apps/ui/app.ts
CHANGED
|
@@ -38,10 +38,9 @@ import { highlightCode, transformMermaid, warmSyntaxLanguages } from "../../runt
|
|
|
38
38
|
import { SOURCE_PROVIDERS } from "../../runtime/model-data.ts";
|
|
39
39
|
import { SUBSCRIPTION_OPTIONS, subscriptionById, loginGitHubCopilot, startCallbackServer, XENO_VERSION } from "../../runtime/oauth.ts";
|
|
40
40
|
import { reportedCost } from "../../runtime/usage.ts";
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
41
|
+
import { UpdateChecks } from "../update-check.ts";
|
|
42
|
+
import { updateNotice } from "../../core/update-check.ts";
|
|
43
43
|
import { isUpgrade } from "../../core/version.ts";
|
|
44
|
-
import { packageLocation, readUpdateCache, writeUpdateCache } from "../../store/update-cache.ts";
|
|
45
44
|
import type { Effort } from "../../runtime/model.ts";
|
|
46
45
|
import { readNode, readCursor, setSessionLabel } from "../../store/log.ts";
|
|
47
46
|
import { readNodeHistory } from "../../store/restore.ts";
|
|
@@ -178,8 +177,9 @@ export class InteractiveApp implements Ui {
|
|
|
178
177
|
};
|
|
179
178
|
/** Decoded pictures shared by every node that shows them (feat-tui-inline-images). */
|
|
180
179
|
private readonly images = new ImageCache(() => this.app.blobDir);
|
|
181
|
-
/**
|
|
182
|
-
private readonly
|
|
180
|
+
/** Shared update requests are aborted on close so quitting never waits on the registry. */
|
|
181
|
+
private readonly updateChecks = new UpdateChecks();
|
|
182
|
+
private manualUpdateRequested = false;
|
|
183
183
|
/** The terminal finished answering capability queries: picture support may have changed. */
|
|
184
184
|
private readonly capabilitiesChanged = (): void => this.applyPictures();
|
|
185
185
|
/** What the terminal reported (OSC 4/10/11); null until it answers, or forever if it never does. */
|
|
@@ -325,7 +325,7 @@ export class InteractiveApp implements Ui {
|
|
|
325
325
|
this.requestDerived();
|
|
326
326
|
this.paint(true);
|
|
327
327
|
// Never awaited: startup and exit do not wait on the network (feat-update-check DoD 3).
|
|
328
|
-
void this.
|
|
328
|
+
void this.checkForUpdateOnStartup();
|
|
329
329
|
}
|
|
330
330
|
|
|
331
331
|
/**
|
|
@@ -334,25 +334,24 @@ export class InteractiveApp implements Ui {
|
|
|
334
334
|
* and is shown now only while the session is still empty, so it never
|
|
335
335
|
* interrupts work. xeno names the update command and never runs it.
|
|
336
336
|
*/
|
|
337
|
-
private async
|
|
337
|
+
private async checkForUpdateOnStartup(): Promise<void> {
|
|
338
338
|
try {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
if (plan.notice && !this.disposed && !this.inflight) this.notice(plan.notice);
|
|
345
|
-
if (!plan.check) return;
|
|
346
|
-
const latest = await fetchLatestVersion(process.env["npm_config_registry"], { signal: this.updateRequest.signal });
|
|
347
|
-
// Quitting cut the request short: that is no answer, so the next start asks again.
|
|
348
|
-
if (this.disposed) return;
|
|
349
|
-
// A failed attempt is recorded too, so a closed network is not retried on every start.
|
|
350
|
-
const known = latest ?? cache?.latest;
|
|
351
|
-
await writeUpdateCache({ checkedAt: Date.now(), ...(known ? { latest: known } : {}) });
|
|
352
|
-
if (!plan.notice && latest && isUpgrade(latest, XENO_VERSION) && this.emptySession()) this.notice(updateNotice(latest, XENO_VERSION, install));
|
|
339
|
+
const { input, plan } = await this.updateChecks.prepare({ offline: this.app.args.offline === true, enabled: this.app.ui?.settings.updateCheck });
|
|
340
|
+
if (plan.notice && !this.manualUpdateRequested && !this.disposed && !this.inflight) this.notice(plan.notice);
|
|
341
|
+
if (!plan.check || this.manualUpdateRequested) return;
|
|
342
|
+
const latest = await this.updateChecks.refresh(input);
|
|
343
|
+
if (!this.manualUpdateRequested && !plan.notice && latest && isUpgrade(latest, XENO_VERSION) && this.emptySession()) this.notice(updateNotice(latest, XENO_VERSION, input.install));
|
|
353
344
|
} catch { /* an update hint is never worth an error */ }
|
|
354
345
|
}
|
|
355
346
|
|
|
347
|
+
checkForUpdate(): void {
|
|
348
|
+
// The explicit action owns its result even when it joins a startup request.
|
|
349
|
+
this.manualUpdateRequested = true;
|
|
350
|
+
void this.updateChecks.manual({ offline: this.app.args.offline === true }, text => {
|
|
351
|
+
if (!this.disposed) this.notice(text);
|
|
352
|
+
}).catch(() => { if (!this.disposed) this.notice("Could not check for xeno updates. Try /update again.", "error"); });
|
|
353
|
+
}
|
|
354
|
+
|
|
356
355
|
private emptySession(): boolean {
|
|
357
356
|
return !this.disposed && !this.inflight && !this.transcriptItems.some(item => item.kind === "user" || item.kind === "assistant" || item.kind === "tool" || item.kind === "shell");
|
|
358
357
|
}
|
|
@@ -394,7 +393,7 @@ export class InteractiveApp implements Ui {
|
|
|
394
393
|
this.renderer.off("palette", this.paletteChanged);
|
|
395
394
|
this.renderer.off("selection", this.selectionFinished);
|
|
396
395
|
this.renderer.off("capabilities", this.capabilitiesChanged);
|
|
397
|
-
this.
|
|
396
|
+
this.updateChecks.cancel();
|
|
398
397
|
this.images.dispose();
|
|
399
398
|
this.screen.dispose();
|
|
400
399
|
delete this.app.askSlot.current;
|
package/src/apps/ui/surface.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface Ui extends TurnSurface {
|
|
|
34
34
|
/** Notices and documents in the message stream. */
|
|
35
35
|
notice(text: string, tone?: "info" | "success" | "warning" | "error"): void;
|
|
36
36
|
showDocument(title: string, text: string): void;
|
|
37
|
+
checkForUpdate(): void;
|
|
37
38
|
toast(text: string, tone?: "info" | "success" | "warning" | "error"): void;
|
|
38
39
|
redraw(): void;
|
|
39
40
|
|