@visual-z/xeno 0.4.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +41 -10
  3. package/docs/app-server.md +35 -4
  4. package/docs/foundations.md +88 -0
  5. package/docs/install.md +23 -2
  6. package/docs/providers.md +4 -0
  7. package/docs/research/foundation-clients-2026-10-09.md +27 -0
  8. package/docs/research/foundation-editing-2026-10-09.md +56 -0
  9. package/docs/research/foundation-network-2026-10-09.md +37 -0
  10. package/docs/research/foundation-workflow-2026-10-09.md +37 -0
  11. package/package.json +10 -2
  12. package/src/apps/acp-server.ts +41 -0
  13. package/src/apps/agent-profiles.ts +19 -0
  14. package/src/apps/app-server.ts +4 -2
  15. package/src/apps/cli.ts +18 -2
  16. package/src/apps/commands.ts +20 -0
  17. package/src/apps/control-transport.ts +3 -2
  18. package/src/apps/model-catalog.ts +57 -31
  19. package/src/apps/preferences.ts +20 -0
  20. package/src/apps/question-ui.ts +27 -0
  21. package/src/apps/resources.ts +63 -25
  22. package/src/apps/review.ts +109 -0
  23. package/src/apps/session-hub.ts +84 -0
  24. package/src/apps/shell.ts +88 -30
  25. package/src/apps/startup-input.ts +7 -2
  26. package/src/apps/startup.ts +180 -25
  27. package/src/apps/ui/app.ts +74 -91
  28. package/src/apps/ui/surface.ts +1 -0
  29. package/src/apps/update-check.ts +71 -0
  30. package/src/apps/workflow.ts +58 -0
  31. package/src/core/agent-profile.ts +130 -0
  32. package/src/core/completion-review.ts +13 -4
  33. package/src/core/control-protocol.ts +8 -1
  34. package/src/core/file-precondition.ts +6 -0
  35. package/src/core/lsp-framing.ts +34 -0
  36. package/src/core/mcp-config.ts +68 -0
  37. package/src/core/model-preferences.ts +28 -0
  38. package/src/core/patch.ts +130 -0
  39. package/src/core/permissions.ts +156 -11
  40. package/src/core/question.ts +94 -0
  41. package/src/core/review.ts +68 -0
  42. package/src/core/settings.ts +1 -1
  43. package/src/core/update-check.ts +24 -5
  44. package/src/core/web-content.ts +80 -0
  45. package/src/core/workflow-mode.ts +34 -0
  46. package/src/runtime/acp-agent.ts +289 -0
  47. package/src/runtime/agent-profiles.ts +21 -0
  48. package/src/runtime/catalog.ts +10 -3
  49. package/src/runtime/control-client.ts +149 -0
  50. package/src/runtime/control-connection.ts +39 -4
  51. package/src/runtime/extensions.ts +20 -2
  52. package/src/runtime/formatter-tool.ts +38 -0
  53. package/src/runtime/formatter.ts +50 -0
  54. package/src/runtime/foundation.ts +78 -0
  55. package/src/runtime/integrations.ts +8 -2
  56. package/src/runtime/loop.ts +68 -10
  57. package/src/runtime/lsp-client.ts +201 -0
  58. package/src/runtime/mcp-client.ts +264 -0
  59. package/src/runtime/mcp-oauth.ts +168 -0
  60. package/src/runtime/mcp-schema.ts +51 -0
  61. package/src/runtime/oauth.ts +1 -1
  62. package/src/runtime/output-schema.ts +26 -0
  63. package/src/runtime/patch-tool.ts +38 -0
  64. package/src/runtime/permission-resources.ts +11 -0
  65. package/src/runtime/provider.ts +2 -0
  66. package/src/runtime/question-tool.ts +62 -0
  67. package/src/runtime/review-tool.ts +48 -0
  68. package/src/runtime/session-control.ts +60 -2
  69. package/src/runtime/session-hub.ts +85 -0
  70. package/src/runtime/tool-discovery.ts +4 -3
  71. package/src/runtime/tools.ts +48 -16
  72. package/src/runtime/update-check.ts +3 -2
  73. package/src/runtime/web-http.ts +56 -0
  74. package/src/runtime/web-tools.ts +88 -0
  75. package/src/runtime/workspace-lifecycle.ts +14 -1
  76. package/src/store/agent-profile-files.ts +13 -0
  77. package/src/store/context-files.ts +77 -1
  78. package/src/store/file-checkpoints.ts +5 -4
  79. package/src/store/foundation-config.ts +24 -0
  80. package/src/store/lsp-document.ts +30 -0
  81. package/src/store/mcp-credentials.ts +85 -0
  82. package/src/store/patch.ts +92 -0
  83. package/src/store/permission-paths.ts +34 -0
  84. package/src/store/resources.ts +13 -22
  85. package/src/store/review.ts +109 -0
  86. package/src/store/search.ts +48 -9
  87. package/src/store/user-settings.ts +30 -0
  88. package/src/tui/ui/control-panel.ts +7 -5
  89. package/src/tui/ui/control-sidebar.ts +49 -0
  90. package/src/tui/ui/screen.ts +107 -11
@@ -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";
@@ -177,6 +195,8 @@ export interface Args {
177
195
  /** `--no-builtin-tools`. False when the flag was not passed. */
178
196
  noBuiltinTools: boolean;
179
197
  allowTools: string[];
198
+ /** `--yolo` / `-y`: skip tool approval for this process only. */
199
+ yolo: boolean;
180
200
  /** `--approve` true, `--no-approve` false. Omitted when neither. */
181
201
  approve?: boolean;
182
202
  configCommand?: boolean;
@@ -231,14 +251,15 @@ export interface Args {
231
251
  }
232
252
 
233
253
  /**
234
- * Names `parseArgs` actually accepts. `--refresh` is not here: it is only
235
- * 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
236
256
  * `--refresh` suggest itself while still being rejected.
237
257
  *
238
258
  * Keep in sync with the branches in `parseArgs`. A missing entry still
239
259
  * parses; it just cannot be offered as a suggestion.
240
260
  */
241
261
  const KNOWN_FLAGS: readonly string[] = [
262
+ "--workflow", "--agent", "--output-schema", "--output-last-message",
242
263
  "--checkpoint-shell",
243
264
  "--workspace", "-w",
244
265
  "--path-root",
@@ -271,6 +292,7 @@ const KNOWN_FLAGS: readonly string[] = [
271
292
  "--no-tools", "-nt",
272
293
  "--no-builtin-tools",
273
294
  "--allow-tools",
295
+ "--yolo", "-y",
274
296
  "--approve", "-a",
275
297
  "--no-approve", "-na",
276
298
  "--theme",
@@ -334,7 +356,9 @@ function rejectUnknownFlag(flag: string): never {
334
356
 
335
357
  export function parseArgs(argv: string[], options: { deferExtensions?: boolean; flags?: ReadonlyMap<string, ExtensionFlag> } = {}): Args {
336
358
  let selectedTheme: string | undefined;
359
+ let updateExtensions = false;
337
360
  const args: Args = {
361
+ cliFlags: argv.filter(value => value.startsWith("-")),
338
362
  workspace: process.cwd(), logDir: "", maxTurns: 40, prompt: "",
339
363
  // Overridable so a node boundary can be forced in a short session --
340
364
  // otherwise cutting is only reachable after tens of thousands of tokens,
@@ -343,7 +367,7 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
343
367
  model: "", provider: "", apiKey: "", listModels: false, modelsCommand: false, modelsRefresh: false, fresh: false,
344
368
  noContextFiles: false, appendSystemPrompt: [],
345
369
  systemPrompt: null,
346
- excludeTools: [], noTools: false, noBuiltinTools: false, allowTools: [],
370
+ excludeTools: [], noTools: false, noBuiltinTools: false, allowTools: [], yolo: false,
347
371
  print: false,
348
372
  theme: "",
349
373
  thinking: "",
@@ -359,7 +383,10 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
359
383
  console.error(`${a} requires a value`);
360
384
  process.exit(1);
361
385
  }
362
- 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;
363
390
  else if (a === "--workspace" || a === "-w") args.workspace = argv[++i] ?? args.workspace;
364
391
  else if (a === "--path-root") args.pathRoot = resolve(argv[++i] ?? "/");
365
392
  else if (a === "--state-dir") args.stateDir = resolve(argv[++i] ?? ".");
@@ -395,7 +422,8 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
395
422
  else if (a === "config" && i === 0) args.configCommand = true;
396
423
  else if ((a === "install" || a === "remove" || a === "update" || a === "list") && i === 0) args.packageCommand = {operation:a,sources:[]};
397
424
  else if ((a === "--local" || a === "-l") && (args.packageCommand || args.configCommand)) { args.local=true; if(args.packageCommand)args.packageCommand.local=true; }
398
- 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;
399
427
  else if (a === "--refresh" && args.modelsCommand) {
400
428
  args.modelsRefresh = true;
401
429
  }
@@ -421,13 +449,14 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
421
449
  }
422
450
  else if (a === "--tool-profile") {
423
451
  const value = argv[++i] ?? "";
424
- 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");
425
453
  args.toolProfile = value;
426
454
  }
427
455
  else if (a === "--exclude-tools" || a === "-xt") args.excludeTools = parseToolList(argv[++i] ?? "");
428
456
  else if (a === "--no-tools" || a === "-nt") args.noTools = true;
429
457
  else if (a === "--no-builtin-tools" || a === "-nbt") args.noBuiltinTools = true;
430
458
  else if (a === "--allow-tools") args.allowTools = parseToolList(argv[++i] ?? "");
459
+ else if (a === "--yolo" || a === "-y") args.yolo = true;
431
460
  else if (a === "--approve" || a === "-a") args.approve = true;
432
461
  else if (a === "--no-approve" || a === "-na") args.approve = false;
433
462
  else if (a === "--theme") {
@@ -492,6 +521,9 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
492
521
  else args.prompt += (args.prompt ? " " : "") + a;
493
522
  }
494
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
+ }
495
527
  return args;
496
528
  }
497
529
 
@@ -714,6 +746,7 @@ export interface AppIO {
714
746
  }
715
747
 
716
748
  export interface App {
749
+ foundation?: FoundationRuntime;
717
750
  args: Args;
718
751
  session: Session;
719
752
  workspace: string;
@@ -843,6 +876,11 @@ export function toolSelectionOf(args: Args) {
843
876
  };
844
877
  }
845
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
+
846
884
  /**
847
885
  * Schema assembly: every tool is tagged with a source here so
848
886
  * `--no-builtin-tools` can drop a category without a later cross-layer
@@ -853,10 +891,16 @@ export function toolSelectionOf(args: Args) {
853
891
  * the provider bills, and an extra field there would look like a tool
854
892
  * argument.
855
893
  */
856
- export function sessionToolCatalog(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = []) {
857
- const custom = [...extensions?.definitions().values() ?? []].map(({ name, description, input_schema }) => ({ name, description, input_schema, source: "extension" as const }));
858
- const available = [...TOOL_DEFINITIONS.filter(tool => !extensions?.tools.has(tool.name) && (!tool.optional || args.tools?.includes(tool.name))).map((t) => ({ ...t, source: "builtin" as const })), ...custom];
859
- const selected = selectTools(available, toolSelectionOf(args));
894
+ export function sessionToolCatalog(args: Args, extensions?: ExtensionRegistry, previous: readonly unknown[] = [], builtinDefaults?: readonly string[]) {
895
+ const custom = [...extensions?.definitions().values() ?? []].map(tool => ({ ...tool, source: extensions?.builtinTools.has(tool.name) ? "builtin" as const : "extension" as const }));
896
+ // O.7/D6: a cold app open uses saved capabilities as defaults, never today's profile.
897
+ // Explicit selection still wins; extension loading and current exclusions remain live.
898
+ const retained = builtinDefaults !== undefined && args.tools === undefined && args.toolProfile === undefined ? new Set(builtinDefaults) : undefined;
899
+ const available = [...TOOL_DEFINITIONS.filter(tool => !extensions?.tools.has(tool.name) &&
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));
860
904
  const deferred = selected.filter(tool => tool.source === "extension" && extensions?.tools.get(tool.name)?.exposure === "deferred")
861
905
  .map(({ name, description, input_schema }) => ({ name, description, input_schema }));
862
906
  const deferredNames = new Set(deferred.map(tool => tool.name));
@@ -896,6 +940,7 @@ export function createSessionFor(opts: {
896
940
  oauth?: boolean;
897
941
  effects?: ResolveEffects;
898
942
  extensions?: ExtensionRegistry;
943
+ foundation?: FoundationRuntime;
899
944
  acceptanceContract?: AcceptanceContract;
900
945
  skills?: readonly Skill[];
901
946
  }): Session {
@@ -906,10 +951,14 @@ export function createSessionFor(opts: {
906
951
  const tools: ToolContext = {
907
952
  root: workspace,
908
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) } : {}),
909
958
  ...(args.pathRoot === undefined ? {} : { pathRoot: args.pathRoot }),
910
959
  allow: names,
911
960
  deferredTools: deferred,
912
- rules: rulesWithAllowTools(args.allowTools),
961
+ rules: processPermissionRules(args, opts.foundation),
913
962
  sessionRules,
914
963
  askSlot,
915
964
  ...(opts.extensions ? { extensions: opts.extensions.definitions(), extensionHooks: opts.extensions.toolHooks() } : {}),
@@ -922,6 +971,7 @@ export function createSessionFor(opts: {
922
971
  shellCheckpoints: args.shellCheckpoints === true,
923
972
  tools,
924
973
  maxTurns: args.maxTurns,
974
+ ...(args.agent ? { agentProfile: { name: args.agent, workflowExplicit: args.cliFlags?.includes("--workflow") === true } } : {}),
925
975
  ...cliSessionConfig(args),
926
976
  ...(opts.skills ? { skills: opts.skills } : {}),
927
977
  ...(opts.acceptanceContract ? { acceptanceContract: opts.acceptanceContract } : {}),
@@ -983,14 +1033,44 @@ async function prepareExtensionPackages(args: Args, workspace: string): Promise<
983
1033
  args.extensionPaths=(args.extensionPaths??[]).filter(path=>!remote.includes(path));
984
1034
  }
985
1035
 
986
- export async function loadSessionResources(args: Args, workspace: string, io: AppIO, trustOverride?: boolean): Promise<{
987
- 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;
988
1038
  }> {
989
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);
990
1067
  await prepareExtensionPackages(args,workspace);
991
1068
  const index = await discoverResources({ ...args, workspace, trusted });
992
1069
  const extensions = new ExtensionRegistry({ reserved: COMMANDS.map(command => command.name), reservedFlags: KNOWN_FLAGS.map(flag => flag.replace(/^-+/, "")),
993
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);
994
1074
  await extensions.load(index);
995
1075
  if(args.extensionArgv) {
996
1076
  const validated = parseArgs(args.extensionArgv, { flags: extensions.flags, ...(args.help ? { deferExtensions: true } : {}) });
@@ -998,7 +1078,8 @@ export async function loadSessionResources(args: Args, workspace: string, io: Ap
998
1078
  args.prompt = validated.prompt; args.extensionFlags = validated.extensionFlags ?? {};
999
1079
  }
1000
1080
  const loaded = await loadUiResources({ ...args, workspace, trusted, ...(args.theme ? { theme: args.theme } : {}) }, index);
1001
- 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(),
1002
1083
  noContextFiles: args.noContextFiles, appendSystemPrompt: args.appendSystemPrompt,
1003
1084
  ...(args.systemPrompt === null ? {} : { systemPrompt: args.systemPrompt }) });
1004
1085
  for (const notice of loadedContext.notices) if (notice.kind === "skipped" || notice.truncated) io.say(io.palette.yellow(formatNotice(notice)));
@@ -1006,11 +1087,13 @@ export async function loadSessionResources(args: Args, workspace: string, io: Ap
1006
1087
  noPromptTemplates: args.noPromptTemplates, skillPaths: args.skillPaths, promptTemplatePaths: args.promptTemplatePaths }, index);
1007
1088
  const slash = resolveSlashCatalog(text.prompts, text.skills);
1008
1089
  for (const line of new Set([...loaded.diagnostics, ...text.diagnostics, ...slash.diagnostics])) io.say(io.palette.yellow(` ${line}`));
1009
- 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);
1010
1092
  const ui = toUiState(loaded, colorEnabled());
1011
1093
  // Also reached on /resume and /reload: `auto` is resolved once per process by the Tui (apps/shell.ts), not here.
1012
1094
  if (args.tuiMode) ui.settings = { ...ui.settings, tuiMode: args.tuiMode };
1013
- 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; }
1014
1097
  }
1015
1098
 
1016
1099
  /**
@@ -1022,24 +1105,56 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
1022
1105
  session: Session; priorCursor: Cursor | null; providerConfig: ProviderConfig; credential: Resolved;
1023
1106
  workspace: string; blobDir: string; system: string; resources: Awaited<ReturnType<typeof loadSessionResources>>;
1024
1107
  }> {
1108
+ const resourceSelector = () => JSON.stringify([opts.args.agent, opts.args.workflow, opts.args.toolProfile, opts.args.cliFlags]);
1109
+ const preparedSelector = resourceSelector();
1025
1110
  opts.startupPhase?.("Reading session history");
1026
1111
  const recorded = await readCursor(opts.logDir);
1027
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
+ }
1028
1139
  const workspace = priorCursor?.workspace ?? opts.workspace;
1029
1140
  if (opts.args.pathRoot) {
1030
1141
  const rel = relative(opts.args.pathRoot, workspace);
1031
1142
  if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) throw new Error("saved workspace is outside --path-root");
1032
1143
  }
1033
- 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();
1034
1146
  opts.startupPhase?.("Loading session resources");
1035
- const resources = opts.preparedResources?.workspace === workspace ? opts.preparedResources.value
1147
+ const resources = opts.preparedResources && reusePrepared ? opts.preparedResources.value
1036
1148
  : await loadSessionResources(opts.args, workspace, opts.io, workspace === opts.workspace ? opts.projectTrusted : undefined);
1149
+ let openedSession: Session | undefined;
1150
+ try {
1037
1151
  const interactive = opts.interactive && !opts.args.print && !opts.args.prompt && opts.args.mode !== "json";
1038
1152
  const named = [...opts.args.tools ?? [], ...opts.args.excludeTools, ...opts.args.allowTools];
1039
1153
  const unknown = unknownToolNames(named).filter(name => !resources.extensions.definitions().has(name));
1040
1154
  if (unknown.length) throw new Error(`unknown tool: ${unknown.join(", ")}`);
1041
1155
  const saved = priorCursor === null ? undefined : restoreSessionModel(priorCursor, opts.catalog);
1042
1156
  const flags = { ...opts.modelFlags ?? { model: "", provider: "" } };
1157
+ if (opts.args.agent) { flags.model = opts.args.model; flags.provider = opts.args.provider; }
1043
1158
  const parsedModel = flags.model ? scopedModels([flags.model], flags.provider ? opts.catalog.filter(model => model.provider === flags.provider) : opts.catalog, true).entries[0] : undefined;
1044
1159
  if (parsedModel) flags.model = modelKey(parsedModel.model);
1045
1160
  const explicit = flags.model.length > 0 || flags.provider.length > 0;
@@ -1085,6 +1200,7 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
1085
1200
  ...opts,
1086
1201
  system: resources.system,
1087
1202
  extensions: resources.extensions,
1203
+ foundation: resources.foundation,
1088
1204
  skills: [...resources.slash.skills.values()],
1089
1205
  ...(acceptanceContract ? { acceptanceContract } : {}),
1090
1206
  workspace: priorCursor?.workspace ?? opts.workspace,
@@ -1094,13 +1210,32 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
1094
1210
  credential: restoringCredential,
1095
1211
  ...(oauth(restoring, restoringCredential) ? { oauth: true } : {}),
1096
1212
  });
1213
+ openedSession = session;
1097
1214
  opts.startupPhase?.("Opening session history");
1098
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));
1099
1219
  resources.slash = { ...resources.slash, skills: new Map((session.config.skills ?? []).map(skill => [skill.name, skill])) };
1100
- if (resources.extensions.loaded.length || session.config.frozen.tools.some(tool =>
1101
- typeof tool === "object" && tool !== null && "name" in tool && typeof tool.name === "string" && unknownToolNames([tool.name]).length)) {
1102
- await session.reloadResources({ system: resources.system, tools: visibleSchemas(opts.args, resources.extensions, session.config.frozen.tools) });
1220
+ const restoredTools = sessionToolCatalog(opts.args, resources.extensions, session.config.frozen.tools,
1221
+ priorCursor === null ? undefined : declaredToolNames(session.config.frozen.tools));
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 =>
1223
+ typeof tool === "object" && tool !== null && "name" in tool && typeof tool.name === "string" && unknownToolNames([tool.name]).length);
1224
+ const explicitTools = requestedWorkflow !== undefined || opts.args.agent !== undefined || opts.args.tools !== undefined || opts.args.toolProfile !== undefined || opts.args.noTools ||
1225
+ opts.args.noBuiltinTools || opts.args.excludeTools.length > 0;
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);
1103
1233
  }
1234
+ // Admission follows this app's selected declaration; current process approval rules stay intact.
1235
+ // Keep this out of Session.start(): an SDK caller can intentionally supply a narrower allowlist.
1236
+ session.config.tools.allow = declaredToolNames(session.config.frozen.tools);
1237
+ session.config.tools.workflowMode = opts.args.workflow ?? "build";
1238
+ session.config.tools.deferredTools = restoredTools.deferred;
1104
1239
  if (restoring.id !== spec.id || restoring.provider !== spec.provider) {
1105
1240
  await session.switchModel(createProvider(providerConfig, credential.ok ? credential.key : "", retryHandler(opts.io),
1106
1241
  { sessionId: providerSessionId(session.config.logDir), ...(oauth(spec, credential) ? { oauth: true } : {}), ...(opts.effects ? { effects: opts.effects } : {}) }));
@@ -1111,6 +1246,11 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
1111
1246
  if (resources.ui.settings.scopedModels) for (const warning of scopedModels(resources.ui.settings.scopedModels, opts.catalog).diagnostics) opts.io.say(` ${warning}`);
1112
1247
  return { session, priorCursor, providerConfig, credential, workspace: session.config.tools.root,
1113
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
+ }
1114
1254
  }
1115
1255
 
1116
1256
  export async function startup(args: Args, input?: StartupInput): Promise<App> {
@@ -1348,6 +1488,7 @@ export async function startup(args: Args, input?: StartupInput): Promise<App> {
1348
1488
  auth, effects, loadedContext: opened.resources.loadedContext, credentialOf, catalog, io, askSlot,
1349
1489
  ui: opened.resources.ui, slash: opened.resources.slash, resources: opened.resources.index, projectTrusted: opened.resources.trusted,
1350
1490
  extensions: opened.resources.extensions,
1491
+ foundation: opened.resources.foundation,
1351
1492
  };
1352
1493
  }
1353
1494
 
@@ -1447,15 +1588,18 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
1447
1588
  " [-n|--name name] [--session-id id] [--version|-v]\n" +
1448
1589
  " [models --refresh]\n" +
1449
1590
  " [install|remove <source>... [--local]] [update [source] [--extensions]] [list]\n" +
1591
+ " [update --check] Check for a newer xeno without installing\n" +
1450
1592
  " [config [--local]] [--export session [output.html]] [--offline]\n" +
1451
1593
  " [--system-prompt <text-or-file>] [--append-system-prompt <text>]\n" +
1452
1594
  " [--no-context-files|-nc]\n" +
1453
1595
  " [--tools names] [--exclude-tools names] [--no-tools]\n" +
1454
- " [--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" +
1455
1599
  " [--acceptance-file contract.json]\n" +
1456
1600
  " [--control-socket /private/directory/control.sock]\n" +
1457
1601
  " [--no-builtin-tools]\n" +
1458
- " [--allow-tools names] [--approve|--no-approve]\n" +
1602
+ " [--allow-tools names] [--yolo|-y] [--approve|--no-approve]\n" +
1459
1603
  " [--theme <name|path>] [--use-theme name] [--no-themes] [--thinking level] [--models patterns]\n" +
1460
1604
  " [--skill <path>] [--no-skills]\n" +
1461
1605
  " [--prompt-template <path>] [--no-prompt-templates]\n" +
@@ -1464,6 +1608,8 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
1464
1608
  " [-p <prompt>] [prompt...]\n\n" +
1465
1609
  " xeno app-server [session/model flags] runs the experimental JSONL control interface.\n" +
1466
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" +
1467
1613
  " --control-socket attaches local adapters to the same live session as the TUI.\n" +
1468
1614
  " /control opens goal, progress and context panels; XENO_PROGRAM_STATUS=auto|1|0 controls terminal status.\n\n" +
1469
1615
  " Starts a new session by default; --fresh is an explicit alias. Nothing is deleted.\n" +
@@ -1479,9 +1625,18 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
1479
1625
  " --system-prompt replaces the built-in instructions (text, or a file that exists).\n" +
1480
1626
  " --append-system-prompt appends; repeatable. -nc skips AGENTS.md discovery.\n" +
1481
1627
  " --tools is a strict allowlist; excluded tools leave the schema.\n" +
1482
- " 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" +
1483
1635
  " --no-builtin-tools drops built-ins and keeps any other source; --tools still wins.\n" +
1484
- " bash asks before it runs. --allow-tools bash skips the ask for this process.\n" +
1636
+ " Shell entry points (exec_command, environment, bash, target) ask before they run.\n" +
1637
+ " --allow-tools exec_command skips asks for that tool for this process.\n" +
1638
+ " --yolo (-y) skips all tool approvals for this process, including resumed sessions.\n" +
1639
+ " Tool exclusions and path limits still apply; YOLO does not change project trust.\n" +
1485
1640
  " --time-budget <seconds> tells the model, once, when 75% of the run's wall-clock budget is used.\n" +
1486
1641
  " --state-dir <dir> keeps snapshots, observations and sessions out of the workspace.\n" +
1487
1642
  " --checkpoint-shell enables best-effort Git recovery for command changes; ignored files and external effects are excluded.\n" +