@visual-z/xeno 0.5.0 → 0.7.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +19 -4
  3. package/docs/app-server.md +32 -3
  4. package/docs/foundations.md +88 -0
  5. package/docs/install.md +23 -2
  6. package/docs/research/foundation-clients-2026-10-09.md +27 -0
  7. package/docs/research/foundation-editing-2026-10-09.md +56 -0
  8. package/docs/research/foundation-network-2026-10-09.md +37 -0
  9. package/docs/research/foundation-workflow-2026-10-09.md +37 -0
  10. package/package.json +10 -2
  11. package/src/apps/acp-server.ts +41 -0
  12. package/src/apps/agent-profiles.ts +19 -0
  13. package/src/apps/app-server.ts +4 -2
  14. package/src/apps/cli.ts +18 -2
  15. package/src/apps/commands.ts +20 -0
  16. package/src/apps/control-transport.ts +3 -2
  17. package/src/apps/model-catalog.ts +57 -31
  18. package/src/apps/question-ui.ts +27 -0
  19. package/src/apps/resources.ts +63 -25
  20. package/src/apps/review.ts +109 -0
  21. package/src/apps/session-hub.ts +95 -0
  22. package/src/apps/shell.ts +55 -1
  23. package/src/apps/startup.ts +157 -22
  24. package/src/apps/ui/app.ts +89 -38
  25. package/src/apps/ui/image-cache.ts +71 -28
  26. package/src/apps/ui/surface.ts +1 -0
  27. package/src/apps/update-check.ts +71 -0
  28. package/src/apps/workflow.ts +58 -0
  29. package/src/core/activity-status.ts +28 -0
  30. package/src/core/agent-profile.ts +130 -0
  31. package/src/core/control-protocol.ts +8 -1
  32. package/src/core/file-precondition.ts +6 -0
  33. package/src/core/git-status.ts +29 -0
  34. package/src/core/image-limits.ts +72 -0
  35. package/src/core/lsp-framing.ts +34 -0
  36. package/src/core/mcp-config.ts +68 -0
  37. package/src/core/patch.ts +130 -0
  38. package/src/core/permissions.ts +143 -4
  39. package/src/core/progress.ts +9 -0
  40. package/src/core/question.ts +94 -0
  41. package/src/core/request-budget.ts +13 -0
  42. package/src/core/review.ts +68 -0
  43. package/src/core/session-exit.ts +26 -0
  44. package/src/core/settings.ts +1 -1
  45. package/src/core/stall.ts +18 -6
  46. package/src/core/stop-report.ts +25 -0
  47. package/src/core/update-check.ts +24 -5
  48. package/src/core/web-content.ts +80 -0
  49. package/src/core/workflow-mode.ts +34 -0
  50. package/src/runtime/acp-agent.ts +289 -0
  51. package/src/runtime/agent-profiles.ts +21 -0
  52. package/src/runtime/catalog.ts +2 -2
  53. package/src/runtime/control-client.ts +149 -0
  54. package/src/runtime/control-connection.ts +39 -4
  55. package/src/runtime/exec-sessions.ts +40 -4
  56. package/src/runtime/extensions.ts +20 -2
  57. package/src/runtime/formatter-tool.ts +38 -0
  58. package/src/runtime/formatter.ts +50 -0
  59. package/src/runtime/foundation.ts +78 -0
  60. package/src/runtime/images.ts +6 -2
  61. package/src/runtime/integrations.ts +8 -2
  62. package/src/runtime/loop.ts +124 -21
  63. package/src/runtime/lsp-client.ts +201 -0
  64. package/src/runtime/mcp-client.ts +264 -0
  65. package/src/runtime/mcp-oauth.ts +168 -0
  66. package/src/runtime/mcp-schema.ts +51 -0
  67. package/src/runtime/oauth.ts +1 -1
  68. package/src/runtime/openai-responses.ts +6 -4
  69. package/src/runtime/openai.ts +6 -1
  70. package/src/runtime/output-schema.ts +26 -0
  71. package/src/runtime/patch-tool.ts +38 -0
  72. package/src/runtime/permission-resources.ts +11 -0
  73. package/src/runtime/pi-provider.ts +5 -4
  74. package/src/runtime/provider-tool-policy.ts +33 -0
  75. package/src/runtime/provider.ts +8 -1
  76. package/src/runtime/question-tool.ts +62 -0
  77. package/src/runtime/review-tool.ts +48 -0
  78. package/src/runtime/session-control.ts +61 -2
  79. package/src/runtime/session-hub.ts +85 -0
  80. package/src/runtime/tool-discovery.ts +4 -3
  81. package/src/runtime/tools.ts +44 -10
  82. package/src/runtime/update-check.ts +3 -2
  83. package/src/runtime/web-http.ts +56 -0
  84. package/src/runtime/web-tools.ts +88 -0
  85. package/src/runtime/workspace-lifecycle.ts +14 -1
  86. package/src/store/agent-profile-files.ts +13 -0
  87. package/src/store/blobs.ts +2 -1
  88. package/src/store/context-files.ts +77 -1
  89. package/src/store/file-checkpoints.ts +5 -4
  90. package/src/store/foundation-config.ts +24 -0
  91. package/src/store/git-status.ts +15 -0
  92. package/src/store/lsp-document.ts +30 -0
  93. package/src/store/mcp-credentials.ts +85 -0
  94. package/src/store/patch.ts +92 -0
  95. package/src/store/permission-paths.ts +34 -0
  96. package/src/store/review.ts +109 -0
  97. package/src/store/search.ts +48 -9
  98. package/src/tui/ui/layout.ts +7 -7
  99. package/src/tui/ui/messages.ts +1 -1
  100. package/src/tui/ui/pictures.ts +26 -9
  101. package/src/tui/ui/screen.ts +12 -8
  102. package/src/tui/ui/status.ts +32 -6
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";
@@ -707,7 +739,7 @@ function printUsage(app: App): void {
707
739
  } else {
708
740
  return assertNever(dollars, "printUsage");
709
741
  }
710
- app.io.say(` wall clock ${(t.ms / 1000).toFixed(1)}s`);
742
+ app.io.say(` model time ${(t.ms / 1000).toFixed(1)}s`);
711
743
  app.io.say("");
712
744
  }
713
745
 
@@ -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
  // ---------------------------------------------------------------------------
@@ -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` is not here: it is only
237
- * valid after the `models` subcommand, and listing it would make a top-level
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 === "--checkpoint-shell") args.shellCheckpoints = true;
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") { /* Package-only update; xeno never self-updates. */ }
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(({ name, description, input_schema }) => ({ name, description, input_schema, source: "extension" as const }));
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 })), ...custom];
867
- const selected = selectTools(available, { ...toolSelectionOf(args), ...(retained ? { profile: "full" as const } : {}) });
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: rulesWithAllowTools(args.allowTools, { yolo: args.yolo }),
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 loadedContext = await loadContext({ workspace, cwd: workspace, configDir: configDir(),
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)), loadedContext), text.skills);
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
- if (opts.preparedResources && opts.preparedResources.workspace !== workspace) await opts.preparedResources.value.extensions.dispose();
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?.workspace === workspace ? opts.preparedResources.value
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
- if (refreshResources || (priorCursor !== null && explicitTools)) {
1115
- await session.reloadResources({ system: refreshResources ? resources.system : session.config.frozen.system, tools: restoredTools.schemas });
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 use coding (10 tools); full exposes default built-ins.\n" +
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" +