@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.
- package/CHANGELOG.md +20 -0
- package/README.md +41 -10
- package/docs/app-server.md +35 -4
- package/docs/foundations.md +88 -0
- package/docs/install.md +23 -2
- package/docs/providers.md +4 -0
- 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/preferences.ts +20 -0
- 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 +88 -30
- package/src/apps/startup-input.ts +7 -2
- package/src/apps/startup.ts +180 -25
- package/src/apps/ui/app.ts +74 -91
- 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/completion-review.ts +13 -4
- 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/model-preferences.ts +28 -0
- package/src/core/patch.ts +130 -0
- package/src/core/permissions.ts +156 -11
- package/src/core/question.ts +94 -0
- package/src/core/review.ts +68 -0
- package/src/core/settings.ts +1 -1
- 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 +10 -3
- 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 +48 -16
- 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/resources.ts +13 -22
- package/src/store/review.ts +109 -0
- package/src/store/search.ts +48 -9
- package/src/store/user-settings.ts +30 -0
- package/src/tui/ui/control-panel.ts +7 -5
- package/src/tui/ui/control-sidebar.ts +49 -0
- package/src/tui/ui/screen.ts +107 -11
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";
|
|
@@ -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`
|
|
235
|
-
*
|
|
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 === "--
|
|
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")
|
|
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(({
|
|
858
|
-
|
|
859
|
-
|
|
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:
|
|
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
|
|
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)),
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
|
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
|
-
"
|
|
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" +
|