@visual-z/xeno 0.8.4 → 0.9.1
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 +19 -0
- package/README.md +31 -746
- package/bin/xeno.exe +26 -0
- package/install.cjs +88 -0
- package/package.json +18 -48
- package/docs/app-server.md +0 -115
- package/docs/foundations.md +0 -88
- package/docs/install.md +0 -244
- package/docs/providers.md +0 -172
- package/docs/research/foundation-clients-2026-10-09.md +0 -27
- package/docs/research/foundation-editing-2026-10-09.md +0 -56
- package/docs/research/foundation-network-2026-10-09.md +0 -37
- package/docs/research/foundation-workflow-2026-10-09.md +0 -37
- package/src/apps/acp-server.ts +0 -41
- package/src/apps/agent-profiles.ts +0 -19
- package/src/apps/app-server.ts +0 -30
- package/src/apps/approve.ts +0 -49
- package/src/apps/cli.ts +0 -99
- package/src/apps/commands.ts +0 -319
- package/src/apps/completions.ts +0 -87
- package/src/apps/control-transport.ts +0 -90
- package/src/apps/export-cli.ts +0 -24
- package/src/apps/goal-flow.ts +0 -70
- package/src/apps/image-draft.ts +0 -46
- package/src/apps/input.ts +0 -312
- package/src/apps/model-catalog.ts +0 -69
- package/src/apps/preferences.ts +0 -20
- package/src/apps/project-trust.ts +0 -9
- package/src/apps/provider-connect.ts +0 -346
- package/src/apps/question-ui.ts +0 -27
- package/src/apps/render.ts +0 -181
- package/src/apps/resources.ts +0 -66
- package/src/apps/review.ts +0 -109
- package/src/apps/session-hub.ts +0 -96
- package/src/apps/session-lifecycle.ts +0 -64
- package/src/apps/shell.ts +0 -2071
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1818
- package/src/apps/ui/control-model.ts +0 -43
- package/src/apps/ui/extension-host.ts +0 -194
- package/src/apps/ui/image-cache.ts +0 -121
- package/src/apps/ui/palette.ts +0 -88
- package/src/apps/ui/project.ts +0 -127
- package/src/apps/ui/standalone.ts +0 -148
- package/src/apps/ui/status-model.ts +0 -68
- package/src/apps/ui/surface.ts +0 -91
- package/src/apps/ui/theme.ts +0 -86
- package/src/apps/update-check.ts +0 -112
- package/src/apps/workflow.ts +0 -58
- package/src/core/acceptance.ts +0 -139
- package/src/core/action-defaults.ts +0 -58
- package/src/core/activity-status.ts +0 -52
- package/src/core/agent-profile.ts +0 -130
- package/src/core/attachment-input.ts +0 -17
- package/src/core/blind-checks.ts +0 -72
- package/src/core/boundary.ts +0 -174
- package/src/core/branch-receipt.ts +0 -26
- package/src/core/catalog-merge.ts +0 -19
- package/src/core/checkpoints.ts +0 -85
- package/src/core/churn.ts +0 -61
- package/src/core/command-artifacts.ts +0 -94
- package/src/core/command-contract.ts +0 -151
- package/src/core/completion-action.ts +0 -20
- package/src/core/completion-review.ts +0 -98
- package/src/core/context-detail.ts +0 -37
- package/src/core/context-policy.ts +0 -133
- package/src/core/context-window.ts +0 -54
- package/src/core/continuation.ts +0 -130
- package/src/core/control-protocol.ts +0 -69
- package/src/core/control-view.ts +0 -39
- package/src/core/cycle.ts +0 -17
- package/src/core/data-query.ts +0 -201
- package/src/core/deadline.ts +0 -40
- package/src/core/derive.ts +0 -516
- package/src/core/draft-images.ts +0 -56
- package/src/core/edit-plan.ts +0 -200
- package/src/core/environment.ts +0 -181
- package/src/core/event-validation.ts +0 -220
- package/src/core/events.ts +0 -670
- package/src/core/evidence-ladder.ts +0 -62
- package/src/core/evidence-links.ts +0 -68
- package/src/core/evidence.ts +0 -240
- package/src/core/execution.ts +0 -18
- package/src/core/exhaustive.ts +0 -18
- package/src/core/export.ts +0 -179
- package/src/core/extension-flags.ts +0 -14
- package/src/core/external-effects.ts +0 -488
- package/src/core/failure-recovery.ts +0 -173
- package/src/core/file-precondition.ts +0 -78
- package/src/core/file-references.ts +0 -35
- package/src/core/frontmatter.ts +0 -213
- package/src/core/git-status.ts +0 -29
- package/src/core/goal-view.ts +0 -64
- package/src/core/goal.ts +0 -200
- package/src/core/handoff.ts +0 -479
- package/src/core/history-query.ts +0 -139
- package/src/core/history.ts +0 -237
- package/src/core/image-limits.ts +0 -72
- package/src/core/images.ts +0 -46
- package/src/core/integration-health.ts +0 -34
- package/src/core/json-event.ts +0 -131
- package/src/core/json-pointer.ts +0 -46
- package/src/core/keymap.ts +0 -153
- package/src/core/lsp-framing.ts +0 -34
- package/src/core/mcp-config.ts +0 -68
- package/src/core/model-picker.ts +0 -64
- package/src/core/model-preferences.ts +0 -28
- package/src/core/model-replay.ts +0 -22
- package/src/core/model-scope.ts +0 -53
- package/src/core/note-guidance.ts +0 -34
- package/src/core/oauth-fields.ts +0 -44
- package/src/core/observation-view.ts +0 -152
- package/src/core/outcome.ts +0 -44
- package/src/core/overflow.ts +0 -59
- package/src/core/packages.ts +0 -47
- package/src/core/patch.ts +0 -130
- package/src/core/path-suggestions.ts +0 -59
- package/src/core/permissions.ts +0 -410
- package/src/core/process-receipt.ts +0 -105
- package/src/core/process-state.ts +0 -37
- package/src/core/program-status.ts +0 -40
- package/src/core/progress.ts +0 -150
- package/src/core/project.ts +0 -431
- package/src/core/prompt-policy.ts +0 -46
- package/src/core/prompts.ts +0 -111
- package/src/core/question.ts +0 -94
- package/src/core/queue.ts +0 -78
- package/src/core/read-bounds.ts +0 -24
- package/src/core/read-observation.ts +0 -106
- package/src/core/recovery-sources.ts +0 -74
- package/src/core/regex-dialect.ts +0 -25
- package/src/core/request-budget.ts +0 -23
- package/src/core/request-progress.ts +0 -87
- package/src/core/request-state.ts +0 -273
- package/src/core/resource-selection.ts +0 -36
- package/src/core/response-recovery.ts +0 -59
- package/src/core/resume.ts +0 -59
- package/src/core/review.ts +0 -68
- package/src/core/ripgrep-release.ts +0 -64
- package/src/core/runtime-target.ts +0 -94
- package/src/core/scheduling.ts +0 -8
- package/src/core/search-observation.ts +0 -97
- package/src/core/secrets.ts +0 -188
- package/src/core/session-archive.ts +0 -4
- package/src/core/session-browser.ts +0 -82
- package/src/core/session-entry.ts +0 -61
- package/src/core/session-exit.ts +0 -42
- package/src/core/session-format.ts +0 -169
- package/src/core/session-position.ts +0 -26
- package/src/core/session-tree.ts +0 -147
- package/src/core/settings.ts +0 -192
- package/src/core/skills.ts +0 -165
- package/src/core/sql-query.ts +0 -211
- package/src/core/stall.ts +0 -128
- package/src/core/stop-report.ts +0 -68
- package/src/core/text-normalize.ts +0 -81
- package/src/core/theme.ts +0 -166
- package/src/core/thinking.ts +0 -26
- package/src/core/tokens.ts +0 -151
- package/src/core/tool-contract.ts +0 -153
- package/src/core/tool-discovery.ts +0 -87
- package/src/core/tool-failure.ts +0 -81
- package/src/core/tool-input.ts +0 -44
- package/src/core/transcript.ts +0 -108
- package/src/core/tui-mode.ts +0 -29
- package/src/core/update-check.ts +0 -157
- package/src/core/version.ts +0 -55
- package/src/core/web-content.ts +0 -80
- package/src/core/work-state.ts +0 -154
- package/src/core/workflow-mode.ts +0 -34
- package/src/core/working-set.ts +0 -119
- package/src/core/yaml.ts +0 -282
- package/src/runtime/acp-agent.ts +0 -289
- package/src/runtime/agent-profiles.ts +0 -21
- package/src/runtime/auth.ts +0 -367
- package/src/runtime/blind-check.ts +0 -36
- package/src/runtime/branch-jobs.ts +0 -36
- package/src/runtime/catalog.ts +0 -285
- package/src/runtime/clipboard.ts +0 -125
- package/src/runtime/command-artifacts.ts +0 -64
- package/src/runtime/connection-reuse.ts +0 -40
- package/src/runtime/control-client.ts +0 -149
- package/src/runtime/control-connection.ts +0 -139
- package/src/runtime/counter-check.ts +0 -74
- package/src/runtime/environment-tools.ts +0 -60
- package/src/runtime/evidence-notes.ts +0 -35
- package/src/runtime/exec-sessions.ts +0 -456
- package/src/runtime/execution-control.ts +0 -40
- package/src/runtime/extension-loader.ts +0 -37
- package/src/runtime/extension-ui.ts +0 -47
- package/src/runtime/extensions.ts +0 -370
- package/src/runtime/formatter-tool.ts +0 -38
- package/src/runtime/formatter.ts +0 -50
- package/src/runtime/foundation.ts +0 -116
- package/src/runtime/history-cache.ts +0 -57
- package/src/runtime/history-tools.ts +0 -168
- package/src/runtime/images.ts +0 -80
- package/src/runtime/integrations.ts +0 -216
- package/src/runtime/loop.ts +0 -3108
- package/src/runtime/lsp-client.ts +0 -201
- package/src/runtime/mcp-client.ts +0 -264
- package/src/runtime/mcp-oauth.ts +0 -168
- package/src/runtime/mcp-schema.ts +0 -51
- package/src/runtime/model-config.ts +0 -232
- package/src/runtime/model-data.LICENSE +0 -27
- package/src/runtime/model-data.ts +0 -21
- package/src/runtime/model-discovery.ts +0 -208
- package/src/runtime/model-replay.ts +0 -10
- package/src/runtime/model-scope.ts +0 -20
- package/src/runtime/model-sources.ts +0 -151
- package/src/runtime/model.ts +0 -686
- package/src/runtime/models-fetch.ts +0 -57
- package/src/runtime/oauth-device.ts +0 -86
- package/src/runtime/oauth.ts +0 -853
- package/src/runtime/open-image.ts +0 -42
- package/src/runtime/openai-responses.ts +0 -304
- package/src/runtime/openai.ts +0 -697
- package/src/runtime/output-schema.ts +0 -26
- package/src/runtime/package-manager.ts +0 -125
- package/src/runtime/patch-tool.ts +0 -38
- package/src/runtime/permission-resources.ts +0 -18
- package/src/runtime/pi-provider.ts +0 -423
- package/src/runtime/program-status.ts +0 -38
- package/src/runtime/progress.ts +0 -26
- package/src/runtime/provider-access.ts +0 -21
- package/src/runtime/provider-connect.ts +0 -109
- package/src/runtime/provider-presets.ts +0 -98
- package/src/runtime/provider-protocols.ts +0 -29
- package/src/runtime/provider-request.ts +0 -87
- package/src/runtime/provider-tool-policy.ts +0 -33
- package/src/runtime/provider.ts +0 -580
- package/src/runtime/question-tool.ts +0 -62
- package/src/runtime/retry.ts +0 -447
- package/src/runtime/review-tool.ts +0 -48
- package/src/runtime/rich-text.ts +0 -36
- package/src/runtime/runtime-target.ts +0 -149
- package/src/runtime/secrets.ts +0 -79
- package/src/runtime/self-update.ts +0 -74
- package/src/runtime/session-control.ts +0 -265
- package/src/runtime/session-hub.ts +0 -85
- package/src/runtime/stream-idle.ts +0 -80
- package/src/runtime/tool-broker.ts +0 -137
- package/src/runtime/tool-discovery.ts +0 -58
- package/src/runtime/tool-queue.ts +0 -77
- package/src/runtime/tools.ts +0 -1820
- package/src/runtime/turn-effects.ts +0 -31
- package/src/runtime/update-check.ts +0 -48
- package/src/runtime/usage.ts +0 -253
- package/src/runtime/version.ts +0 -12
- package/src/runtime/web-http.ts +0 -56
- package/src/runtime/web-tools.ts +0 -88
- package/src/runtime/working-notes.ts +0 -60
- package/src/runtime/workspace-lifecycle.ts +0 -141
- package/src/store/acceptance.ts +0 -15
- package/src/store/agent-profile-files.ts +0 -13
- package/src/store/atomic.ts +0 -31
- package/src/store/auth-store.ts +0 -50
- package/src/store/blind-checks.ts +0 -32
- package/src/store/blobs.ts +0 -143
- package/src/store/checkpoint-history.ts +0 -32
- package/src/store/command-artifacts.ts +0 -96
- package/src/store/command.ts +0 -111
- package/src/store/config.ts +0 -69
- package/src/store/context-files.ts +0 -436
- package/src/store/file-checkpoints.ts +0 -108
- package/src/store/file-index.ts +0 -174
- package/src/store/foundation-config.ts +0 -24
- package/src/store/git-checkpoints.ts +0 -67
- package/src/store/git-status.ts +0 -15
- package/src/store/history-search.ts +0 -19
- package/src/store/log.ts +0 -725
- package/src/store/lsp-document.ts +0 -30
- package/src/store/mcp-credentials.ts +0 -85
- package/src/store/models.ts +0 -60
- package/src/store/observations.ts +0 -224
- package/src/store/packages.ts +0 -98
- package/src/store/patch.ts +0 -92
- package/src/store/permission-paths.ts +0 -34
- package/src/store/process-receipts.ts +0 -34
- package/src/store/query-source.ts +0 -49
- package/src/store/recovery.ts +0 -44
- package/src/store/replay.ts +0 -76
- package/src/store/resource-index.ts +0 -170
- package/src/store/resources.ts +0 -285
- package/src/store/restore.ts +0 -110
- package/src/store/review.ts +0 -109
- package/src/store/runtime-target.ts +0 -64
- package/src/store/search.ts +0 -728
- package/src/store/secrets.ts +0 -44
- package/src/store/serial.ts +0 -13
- package/src/store/session-archive.ts +0 -165
- package/src/store/session-browser.ts +0 -68
- package/src/store/session-position.ts +0 -27
- package/src/store/sessions.ts +0 -525
- package/src/store/source-cancellation.ts +0 -6
- package/src/store/tool-binaries.ts +0 -90
- package/src/store/trust.ts +0 -82
- package/src/store/update-cache.ts +0 -36
- package/src/store/user-settings.ts +0 -30
- package/src/store/workspace-clone.ts +0 -42
- package/src/store/workspace-observation.ts +0 -113
- package/src/store/workspace.ts +0 -115
- package/src/store/worktree.ts +0 -278
- package/src/tui/ansi-spans.ts +0 -126
- package/src/tui/diff.ts +0 -160
- package/src/tui/exhaustive.ts +0 -14
- package/src/tui/fuzzy.ts +0 -75
- package/src/tui/style.ts +0 -208
- package/src/tui/ui/atomic-draft.ts +0 -121
- package/src/tui/ui/branch-panel.ts +0 -47
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -199
- package/src/tui/ui/control-panel.ts +0 -25
- package/src/tui/ui/control-sidebar.ts +0 -49
- package/src/tui/ui/dialog.ts +0 -398
- package/src/tui/ui/draft.ts +0 -23
- package/src/tui/ui/duration.ts +0 -16
- package/src/tui/ui/edge.ts +0 -186
- package/src/tui/ui/image-size.ts +0 -44
- package/src/tui/ui/ink.ts +0 -210
- package/src/tui/ui/items.ts +0 -42
- package/src/tui/ui/keys.ts +0 -40
- package/src/tui/ui/layout.ts +0 -74
- package/src/tui/ui/list-model.ts +0 -86
- package/src/tui/ui/list-view.ts +0 -172
- package/src/tui/ui/mascot.ts +0 -67
- package/src/tui/ui/messages.ts +0 -518
- package/src/tui/ui/motion.ts +0 -28
- package/src/tui/ui/paths.ts +0 -10
- package/src/tui/ui/pictures.ts +0 -108
- package/src/tui/ui/prompt-history.ts +0 -60
- package/src/tui/ui/screen.ts +0 -579
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
package/src/store/command.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/** O.1: own the process group, preserve partial observations, and drain before returning. */
|
|
2
|
-
import { captureCommandObservation } from "./observations.ts";
|
|
3
|
-
import { configDir } from "./config.ts";
|
|
4
|
-
import { xenoBinDir } from "./tool-binaries.ts";
|
|
5
|
-
import { commandArgv, commandEnvironment, COMMAND_TERMINATION_GRACE_MS } from "../core/command-contract.ts";
|
|
6
|
-
import { ToolFault, NOT_STARTED, failureDiagnostic } from "../core/tool-failure.ts";
|
|
7
|
-
|
|
8
|
-
interface CommandCaptureInput {
|
|
9
|
-
cwd: string;
|
|
10
|
-
outputStore: string;
|
|
11
|
-
maxOutputBytes: number;
|
|
12
|
-
timeoutMs: number;
|
|
13
|
-
signal?: AbortSignal;
|
|
14
|
-
onOutput?: (chunk: Uint8Array) => void;
|
|
15
|
-
/** Bounded probes never transfer background children to the caller. */
|
|
16
|
-
cleanupOnExit?: boolean;
|
|
17
|
-
/** Stop capture after the crossing chunk; command cancellation still owns process cleanup. */
|
|
18
|
-
outputLimitBytes?: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function observeCommand(input: CommandCaptureInput & { command: string }) {
|
|
22
|
-
if (input.signal?.aborted) {
|
|
23
|
-
const empty = () => new ReadableStream<Uint8Array>({ start: controller => controller.close() });
|
|
24
|
-
const captured = await captureCommandObservation(input.outputStore, empty(), empty(), Promise.resolve(130), input.maxOutputBytes);
|
|
25
|
-
return { ...captured, wallMs: 0, stopped: "cancelled" as const, cleanup: undefined };
|
|
26
|
-
}
|
|
27
|
-
return executeCaptured(input, commandArgv(input.command), false);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Native host API: argv is never interpreted by a shell and each pipe is recoverable. */
|
|
31
|
-
export async function observeArgvCommand(input: CommandCaptureInput & { argv: readonly string[] }) {
|
|
32
|
-
if (!Array.isArray(input.argv) || input.argv.length === 0 || !input.argv[0] ||
|
|
33
|
-
input.argv.some(value => typeof value !== "string" || value.includes("\0"))) {
|
|
34
|
-
throw new ToolFault("invalid_input", "native argv must contain a program and literal string arguments without NUL", NOT_STARTED);
|
|
35
|
-
}
|
|
36
|
-
if (input.signal?.aborted) throw new ToolFault("cancelled", "native command was cancelled before it started", NOT_STARTED);
|
|
37
|
-
const captured = await executeCaptured(input, [...input.argv], true);
|
|
38
|
-
if (captured.channels.length !== 2) throw new Error("missing native stdout/stderr artifacts");
|
|
39
|
-
return { ...captured, stdout: captured.channels[0]!, stderr: captured.channels[1]! };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function executeCaptured(input: CommandCaptureInput, argv: string[], native: boolean) {
|
|
43
|
-
const started = performance.now();
|
|
44
|
-
const proc = (() => {
|
|
45
|
-
try { return Bun.spawn(argv, {
|
|
46
|
-
cwd: input.cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe", detached: true,
|
|
47
|
-
// Preserve the trusted native API's inherited environment; shell tools
|
|
48
|
-
// retain their explicit noninteractive policy and login-shell startup.
|
|
49
|
-
env: native ? process.env : commandEnvironment(process.env, xenoBinDir(configDir())),
|
|
50
|
-
}); } catch (error) {
|
|
51
|
-
const os = (error as { code?: string }).code;
|
|
52
|
-
throw new ToolFault(os === "ENOENT" || os === "ENOTDIR" ? "environment_missing" : os === "EACCES" || os === "EPERM" ? "permission_denied" : "execution_failed", String(error), NOT_STARTED);
|
|
53
|
-
}
|
|
54
|
-
})();
|
|
55
|
-
let stopped: "cancelled" | "timeout" | undefined;
|
|
56
|
-
let escalation: ReturnType<typeof setTimeout> | undefined;
|
|
57
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
58
|
-
let exited = false, cleaned = false;
|
|
59
|
-
let cleanupError: unknown;
|
|
60
|
-
const kill = (signal: "SIGTERM" | "SIGKILL") => {
|
|
61
|
-
try {
|
|
62
|
-
// POSIX detached children lead their own group; never signal the caller's group.
|
|
63
|
-
if (process.platform === "win32") proc.kill(signal);
|
|
64
|
-
else process.kill(-proc.pid, signal);
|
|
65
|
-
} catch (error) {
|
|
66
|
-
if ((error as { code?: string }).code !== "ESRCH") throw error;
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
const cleanup = () => {
|
|
70
|
-
if (cleaned) return;
|
|
71
|
-
cleaned = true;
|
|
72
|
-
try { kill("SIGKILL"); } catch (error) { cleanupError = error; }
|
|
73
|
-
};
|
|
74
|
-
const stop = (reason: "cancelled" | "timeout") => {
|
|
75
|
-
if (exited || stopped !== undefined) return;
|
|
76
|
-
stopped = reason;
|
|
77
|
-
kill("SIGTERM");
|
|
78
|
-
escalation = setTimeout(cleanup, COMMAND_TERMINATION_GRACE_MS);
|
|
79
|
-
};
|
|
80
|
-
const exit = proc.exited.then(code => {
|
|
81
|
-
exited = true;
|
|
82
|
-
clearTimeout(timer); clearTimeout(escalation);
|
|
83
|
-
// Clean once at the observed exit, never after slow archival when a PID
|
|
84
|
-
// might have been reused. Legacy successful bash background starts remain.
|
|
85
|
-
if (native || input.cleanupOnExit || stopped !== undefined) cleanup();
|
|
86
|
-
return code;
|
|
87
|
-
});
|
|
88
|
-
const abort = () => stop("cancelled");
|
|
89
|
-
input.signal?.addEventListener("abort", abort, { once: true });
|
|
90
|
-
timer = setTimeout(() => stop("timeout"), input.timeoutMs);
|
|
91
|
-
if (input.signal?.aborted) abort();
|
|
92
|
-
try {
|
|
93
|
-
const captured = await captureCommandObservation(input.outputStore, proc.stdout, proc.stderr,
|
|
94
|
-
exit, input.maxOutputBytes, input.onOutput, native, input.outputLimitBytes);
|
|
95
|
-
// Parent exit and archived output remain facts even if group cleanup
|
|
96
|
-
// cannot be confirmed. This is execution uncertainty, not user denial.
|
|
97
|
-
const cleanupDiagnostic = cleanupError === undefined ? undefined : failureDiagnostic("unknown_effect",
|
|
98
|
-
`process-group cleanup was not confirmed: ${String(cleanupError)}`, { execution: "started", effects: "possible" });
|
|
99
|
-
return { ...captured, wallMs: performance.now() - started, ...(stopped ? { stopped } : {}), cleanup: cleanupDiagnostic };
|
|
100
|
-
} catch (error) {
|
|
101
|
-
cleanup();
|
|
102
|
-
await proc.exited;
|
|
103
|
-
throw error;
|
|
104
|
-
} finally {
|
|
105
|
-
clearTimeout(timer);
|
|
106
|
-
clearTimeout(escalation);
|
|
107
|
-
input.signal?.removeEventListener("abort", abort);
|
|
108
|
-
// A descendant can close stdio yet ignore SIGTERM; completing capture is not proof it stopped.
|
|
109
|
-
if (stopped !== undefined) cleanup();
|
|
110
|
-
}
|
|
111
|
-
}
|
package/src/store/config.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The user's configuration, which lives with the user and not with the project.
|
|
3
|
-
*
|
|
4
|
-
* `~/.xeno/` rather than the workspace's `.xeno/`. The split is not cosmetic:
|
|
5
|
-
* the log and the blob store belong to one piece of work and should travel
|
|
6
|
-
* with it, while a credential belongs to a person. A key written into the
|
|
7
|
-
* workspace would be committed, would be seen by the snapshot scan that backs
|
|
8
|
-
* `/back`, and would ship with the project.
|
|
9
|
-
*
|
|
10
|
-
* This layer only reads and writes files. It knows nothing about providers,
|
|
11
|
-
* priority, or what a key means -- that is `runtime/auth.ts`, and keeping the
|
|
12
|
-
* two apart is what lets the priority rules be tested without a filesystem.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
16
|
-
import { homedir } from "node:os";
|
|
17
|
-
import { join } from "node:path";
|
|
18
|
-
|
|
19
|
-
export function configDir(): string {
|
|
20
|
-
// XENO_CONFIG_DIR exists for tests and for anyone running two configurations
|
|
21
|
-
// side by side. Read here rather than threaded through every caller, because
|
|
22
|
-
// this is the one place that turns "the user" into a path.
|
|
23
|
-
return process.env["XENO_CONFIG_DIR"] ?? join(homedir(), ".xeno");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function authPath(): string {
|
|
27
|
-
return join(configDir(), "auth.json");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function settingsPath(): string {
|
|
31
|
-
return join(configDir(), "settings.json");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Reads a JSON config, treating "absent" and "unreadable" as empty.
|
|
36
|
-
*
|
|
37
|
-
* A missing config file is the normal first-run state, not an error. A
|
|
38
|
-
* corrupt one is reported to the caller as absent plus a reason, because
|
|
39
|
-
* silently starting with default settings after someone hand-edited a file
|
|
40
|
-
* is how a typo becomes an hour of confusion.
|
|
41
|
-
*/
|
|
42
|
-
export async function readJsonFile<T>(
|
|
43
|
-
path: string,
|
|
44
|
-
): Promise<{ value: T | null; error: string | null }> {
|
|
45
|
-
let text: string;
|
|
46
|
-
try {
|
|
47
|
-
text = await readFile(path, "utf8");
|
|
48
|
-
} catch {
|
|
49
|
-
return { value: null, error: null };
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
return { value: JSON.parse(text) as T, error: null };
|
|
53
|
-
} catch (err) {
|
|
54
|
-
return { value: null, error: `${path} is not valid JSON: ${(err as Error).message}` };
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Writes a config file, 0600 for the one that holds secrets.
|
|
60
|
-
*
|
|
61
|
-
* `chmod` after the write rather than a `mode` on the open, because an
|
|
62
|
-
* existing file keeps its old permissions when reopened -- so a file created
|
|
63
|
-
* before this rule existed would stay world-readable and nothing would say so.
|
|
64
|
-
*/
|
|
65
|
-
export async function writeJsonFile(path: string, value: unknown, secret: boolean): Promise<void> {
|
|
66
|
-
await mkdir(configDir(), { recursive: true });
|
|
67
|
-
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
68
|
-
if (secret) await chmod(path, 0o600);
|
|
69
|
-
}
|
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project context files (AGENTS.md and friends) and the system prompt they
|
|
3
|
-
* join.
|
|
4
|
-
*
|
|
5
|
-
* Two halves, kept apart because they have different reasons to change:
|
|
6
|
-
*
|
|
7
|
-
* DISCOVERY is I/O. It walks directories, reads bytes, skips what it
|
|
8
|
-
* cannot, and truncates what does not fit. It runs once at process
|
|
9
|
-
* start.
|
|
10
|
-
*
|
|
11
|
-
* ASSEMBLY is a pure function of those bytes. Same files in, same
|
|
12
|
-
* string out. That is what makes the ordering rules testable without
|
|
13
|
-
* a filesystem, and what makes `frozen.system` a value rather than a
|
|
14
|
-
* live view of disk.
|
|
15
|
-
*
|
|
16
|
-
* Re-reading these files at a node boundary would change `system` and
|
|
17
|
-
* invalidate the prefix cache for the whole conversation (D6). They are
|
|
18
|
-
* frozen at startup; an explicit reload checkpoints a changed prefix.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { readFile, stat, lstat } from "node:fs/promises";
|
|
22
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
23
|
-
import { assertNever } from "../core/exhaustive.ts";
|
|
24
|
-
import { blobAddress } from "./blobs.ts";
|
|
25
|
-
import { gitBytes } from "./workspace-observation.ts";
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Per-file cap. Assumed, not measured: a rule file larger than this is a
|
|
29
|
-
* document someone should split, not a prompt prefix. Hitting it truncates
|
|
30
|
-
* that file and names it in the prompt; the omitted rules are simply not
|
|
31
|
-
* followed, which is the failure mode the notice exists to make visible.
|
|
32
|
-
*/
|
|
33
|
-
export const MAX_FILE_BYTES = 64 * 1024;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Cap on the sum of loaded context files. Assumed, not measured: 256KB of
|
|
37
|
-
* instructions already crowds out the task. Hitting it truncates the file
|
|
38
|
-
* that overflowed (and names it); later files may be omitted, so a huge
|
|
39
|
-
* farther file can starve a nearer one.
|
|
40
|
-
*/
|
|
41
|
-
export const MAX_TOTAL_BYTES = 256 * 1024;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Directories walked from cwd toward the workspace root, including cwd.
|
|
45
|
-
* Assumed, not measured: a 32-level nest is not a project layout, it is a
|
|
46
|
-
* pathologically generated tree. Hitting it drops the farther (workspace)
|
|
47
|
-
* files -- the walk starts at cwd so nearer rules are kept.
|
|
48
|
-
*/
|
|
49
|
-
export const MAX_WALK_DEPTH = 32;
|
|
50
|
-
|
|
51
|
-
const AGENTS_CANDIDATES = ["AGENTS.override.md", "AGENTS.md", "CLAUDE.md"] as const;
|
|
52
|
-
|
|
53
|
-
export type ContextRole = "system" | "agents" | "append";
|
|
54
|
-
|
|
55
|
-
export interface LoadedFile {
|
|
56
|
-
path: string;
|
|
57
|
-
content: string;
|
|
58
|
-
/** Original size in UTF-8 bytes, before any truncation. */
|
|
59
|
-
bytes: number;
|
|
60
|
-
truncated: boolean;
|
|
61
|
-
role: ContextRole;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export type ContextNotice =
|
|
65
|
-
| { kind: "loaded"; path: string; truncated: boolean }
|
|
66
|
-
| { kind: "skipped"; path: string; reason: string };
|
|
67
|
-
|
|
68
|
-
export interface LoadedContext {
|
|
69
|
-
/** Replacement for the builtin system prompt, if any. */
|
|
70
|
-
system: LoadedFile | null;
|
|
71
|
-
/** AGENTS.md chain, far (global) to near (cwd). */
|
|
72
|
-
agents: LoadedFile[];
|
|
73
|
-
/** APPEND_SYSTEM.md then `--append-system-prompt`, in append order. */
|
|
74
|
-
appends: LoadedFile[];
|
|
75
|
-
notices: ContextNotice[];
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface LoadOptions {
|
|
79
|
-
workspace: string;
|
|
80
|
-
cwd: string;
|
|
81
|
-
/** `~/.xeno` or `XENO_CONFIG_DIR`. Injected so tests never touch the real home. */
|
|
82
|
-
configDir: string;
|
|
83
|
-
noContextFiles: boolean;
|
|
84
|
-
appendSystemPrompt: readonly string[];
|
|
85
|
-
/** Raw `--system-prompt` value. Omitted when the flag was not passed. */
|
|
86
|
-
systemPrompt?: string;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
class Budget {
|
|
90
|
-
remaining = MAX_TOTAL_BYTES;
|
|
91
|
-
|
|
92
|
-
fit(text: string, bytes: number): { content: string; truncated: boolean } {
|
|
93
|
-
const cap = Math.min(MAX_FILE_BYTES, this.remaining);
|
|
94
|
-
if (bytes <= cap) {
|
|
95
|
-
this.remaining -= bytes;
|
|
96
|
-
return { content: text, truncated: false };
|
|
97
|
-
}
|
|
98
|
-
const content = truncateUtf8(text, cap);
|
|
99
|
-
this.remaining = Math.max(0, this.remaining - Buffer.byteLength(content, "utf8"));
|
|
100
|
-
return { content, truncated: true };
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Walk from cwd up to the workspace root, nearest first, then reverse so
|
|
106
|
-
* assembly sees farther files first. cwd outside the workspace contributes
|
|
107
|
-
* only the workspace itself -- the session is rooted there, and climbing
|
|
108
|
-
* into unrelated trees would load someone else's rules.
|
|
109
|
-
*/
|
|
110
|
-
function projectDirectories(workspace: string, cwd: string): string[] {
|
|
111
|
-
const root = resolve(workspace);
|
|
112
|
-
const start = resolve(cwd);
|
|
113
|
-
if (!isInside(start, root)) return [root];
|
|
114
|
-
|
|
115
|
-
const fromCwd: string[] = [];
|
|
116
|
-
let dir = start;
|
|
117
|
-
for (let i = 0; i < MAX_WALK_DEPTH; i++) {
|
|
118
|
-
fromCwd.push(dir);
|
|
119
|
-
if (dir === root) break;
|
|
120
|
-
const parent = dirname(dir);
|
|
121
|
-
if (parent === dir) break;
|
|
122
|
-
dir = parent;
|
|
123
|
-
}
|
|
124
|
-
return fromCwd.reverse();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function isInside(child: string, parent: string): boolean {
|
|
128
|
-
const rel = relative(parent, child);
|
|
129
|
-
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function truncateUtf8(text: string, maxBytes: number): string {
|
|
133
|
-
if (maxBytes <= 0) return "";
|
|
134
|
-
const buf = Buffer.from(text, "utf8");
|
|
135
|
-
if (buf.byteLength <= maxBytes) return text;
|
|
136
|
-
let end = maxBytes;
|
|
137
|
-
// Do not split a multibyte sequence: a dangling continuation byte is
|
|
138
|
-
// not valid UTF-8, and the model would see a replacement character in
|
|
139
|
-
// the middle of a rule.
|
|
140
|
-
while (end > 0 && (buf[end]! & 0xc0) === 0x80) end--;
|
|
141
|
-
return buf.subarray(0, end).toString("utf8");
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
type ReadResult =
|
|
145
|
-
| { status: "missing" }
|
|
146
|
-
| { status: "ok"; text: string; bytes: number }
|
|
147
|
-
| { status: "skip"; reason: string };
|
|
148
|
-
|
|
149
|
-
async function tryRead(path: string): Promise<ReadResult> {
|
|
150
|
-
let buf: Buffer;
|
|
151
|
-
try {
|
|
152
|
-
buf = await readFile(path);
|
|
153
|
-
} catch (err) {
|
|
154
|
-
const code = (err as { code?: string }).code;
|
|
155
|
-
if (code === "ENOENT") return { status: "missing" };
|
|
156
|
-
if (code === "EACCES") return { status: "skip", reason: "permission denied" };
|
|
157
|
-
if (code === "EISDIR") return { status: "skip", reason: "is a directory" };
|
|
158
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
159
|
-
return { status: "skip", reason: message };
|
|
160
|
-
}
|
|
161
|
-
try {
|
|
162
|
-
const text = new TextDecoder("utf-8", { fatal: true }).decode(buf);
|
|
163
|
-
return { status: "ok", text, bytes: buf.byteLength };
|
|
164
|
-
} catch {
|
|
165
|
-
return { status: "skip", reason: "not valid UTF-8" };
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function absorbText(
|
|
170
|
-
path: string,
|
|
171
|
-
text: string,
|
|
172
|
-
role: ContextRole,
|
|
173
|
-
bytes: number,
|
|
174
|
-
budget: Budget,
|
|
175
|
-
notices: ContextNotice[],
|
|
176
|
-
): LoadedFile {
|
|
177
|
-
const fitted = budget.fit(text, bytes);
|
|
178
|
-
const file: LoadedFile = {
|
|
179
|
-
path,
|
|
180
|
-
content: fitted.content,
|
|
181
|
-
bytes,
|
|
182
|
-
truncated: fitted.truncated,
|
|
183
|
-
role,
|
|
184
|
-
};
|
|
185
|
-
notices.push({ kind: "loaded", path, truncated: file.truncated });
|
|
186
|
-
return file;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async function absorbFile(
|
|
190
|
-
path: string,
|
|
191
|
-
role: ContextRole,
|
|
192
|
-
budget: Budget,
|
|
193
|
-
notices: ContextNotice[],
|
|
194
|
-
): Promise<LoadedFile | null> {
|
|
195
|
-
const got = await tryRead(path);
|
|
196
|
-
if (got.status === "missing") return null;
|
|
197
|
-
if (got.status === "skip") {
|
|
198
|
-
notices.push({ kind: "skipped", path, reason: got.reason });
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
return absorbText(path, got.text, role, got.bytes, budget, notices);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* `--system-prompt` is text, or a file if the argument names one that
|
|
206
|
-
* exists. An existing path that cannot be read is skipped (and reported)
|
|
207
|
-
* rather than silently becoming the path string as the prompt.
|
|
208
|
-
*/
|
|
209
|
-
async function absorbSystemArg(
|
|
210
|
-
value: string,
|
|
211
|
-
cwd: string,
|
|
212
|
-
budget: Budget,
|
|
213
|
-
notices: ContextNotice[],
|
|
214
|
-
): Promise<LoadedFile | null> {
|
|
215
|
-
const candidate = resolve(cwd, value);
|
|
216
|
-
let file = false;
|
|
217
|
-
try {
|
|
218
|
-
file = (await stat(candidate)).isFile();
|
|
219
|
-
} catch {
|
|
220
|
-
file = false;
|
|
221
|
-
}
|
|
222
|
-
if (file) return absorbFile(candidate, "system", budget, notices);
|
|
223
|
-
return absorbText("--system-prompt", value, "system", Buffer.byteLength(value, "utf8"), budget, notices);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* One file per directory: override, else AGENTS.md, else CLAUDE.md.
|
|
228
|
-
*
|
|
229
|
-
* An unreadable chosen file does not fall through to the next name -- the
|
|
230
|
-
* directory had a preference, it just could not be honoured. A name that
|
|
231
|
-
* is missing or is a directory is not a preference, so the next candidate
|
|
232
|
-
* is tried.
|
|
233
|
-
*/
|
|
234
|
-
async function absorbAgentsAt(
|
|
235
|
-
dir: string,
|
|
236
|
-
budget: Budget,
|
|
237
|
-
notices: ContextNotice[],
|
|
238
|
-
): Promise<LoadedFile | null> {
|
|
239
|
-
for (const name of AGENTS_CANDIDATES) {
|
|
240
|
-
const path = join(dir, name);
|
|
241
|
-
const got = await tryRead(path);
|
|
242
|
-
if (got.status === "missing") continue;
|
|
243
|
-
if (got.status === "skip") {
|
|
244
|
-
if (got.reason === "is a directory") continue;
|
|
245
|
-
notices.push({ kind: "skipped", path, reason: got.reason });
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
return absorbText(path, got.text, "agents", got.bytes, budget, notices);
|
|
249
|
-
}
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export async function loadContext(opts: LoadOptions): Promise<LoadedContext> {
|
|
254
|
-
const budget = new Budget();
|
|
255
|
-
const notices: ContextNotice[] = [];
|
|
256
|
-
const agents: LoadedFile[] = [];
|
|
257
|
-
const appends: LoadedFile[] = [];
|
|
258
|
-
|
|
259
|
-
let system: LoadedFile | null = null;
|
|
260
|
-
if (opts.systemPrompt !== undefined) {
|
|
261
|
-
// The flag is an explicit choice. A missing/unreadable file does not
|
|
262
|
-
// fall through to SYSTEM.md -- that would make a typo in the path
|
|
263
|
-
// silently restore the project prompt.
|
|
264
|
-
system = await absorbSystemArg(opts.systemPrompt, opts.cwd, budget, notices);
|
|
265
|
-
} else {
|
|
266
|
-
system =
|
|
267
|
-
(await absorbFile(join(opts.workspace, ".xeno", "SYSTEM.md"), "system", budget, notices)) ??
|
|
268
|
-
(await absorbFile(join(opts.configDir, "SYSTEM.md"), "system", budget, notices));
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (!opts.noContextFiles) {
|
|
272
|
-
const globalAgents = await absorbFile(join(opts.configDir, "AGENTS.md"), "agents", budget, notices);
|
|
273
|
-
if (globalAgents) agents.push(globalAgents);
|
|
274
|
-
for (const dir of projectDirectories(opts.workspace, opts.cwd)) {
|
|
275
|
-
const file = await absorbAgentsAt(dir, budget, notices);
|
|
276
|
-
if (file) agents.push(file);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const globalAppend = await absorbFile(join(opts.configDir, "APPEND_SYSTEM.md"), "append", budget, notices);
|
|
281
|
-
if (globalAppend) appends.push(globalAppend);
|
|
282
|
-
const projectAppend = await absorbFile(
|
|
283
|
-
join(opts.workspace, ".xeno", "APPEND_SYSTEM.md"),
|
|
284
|
-
"append",
|
|
285
|
-
budget,
|
|
286
|
-
notices,
|
|
287
|
-
);
|
|
288
|
-
if (projectAppend) appends.push(projectAppend);
|
|
289
|
-
for (const text of opts.appendSystemPrompt) {
|
|
290
|
-
appends.push(
|
|
291
|
-
absorbText(
|
|
292
|
-
"--append-system-prompt",
|
|
293
|
-
text,
|
|
294
|
-
"append",
|
|
295
|
-
Buffer.byteLength(text, "utf8"),
|
|
296
|
-
budget,
|
|
297
|
-
notices,
|
|
298
|
-
),
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return { system, agents, appends, notices };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const CONTEXT_INTRO =
|
|
306
|
-
"# Project context\n\n" +
|
|
307
|
-
"These files were loaded at startup. Later files are closer to the working " +
|
|
308
|
-
"directory and take precedence.";
|
|
309
|
-
|
|
310
|
-
function truncationLine(file: LoadedFile): string {
|
|
311
|
-
const included = Buffer.byteLength(file.content, "utf8");
|
|
312
|
-
const omitted = Math.max(0, file.bytes - included);
|
|
313
|
-
return `[truncated: ${file.path} exceeded a size limit; ${omitted} bytes omitted]`;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function escapeAttr(value: string): string {
|
|
317
|
-
return value.replace(/&/g, "&").replace(/"/g, """);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function renderFile(file: LoadedFile): string {
|
|
321
|
-
const inner = file.truncated ? `${file.content}\n${truncationLine(file)}` : file.content;
|
|
322
|
-
return `<context_file path="${escapeAttr(file.path)}">\n${inner}\n</context_file>`;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* File contents in, prompt string out. The path of every loaded file is in
|
|
327
|
-
* the result so the model can cite which rule it is following, and a
|
|
328
|
-
* truncated file is named so a silent cut cannot hide.
|
|
329
|
-
*/
|
|
330
|
-
export function assembleSystemPrompt(builtin: string, loaded: LoadedContext): string {
|
|
331
|
-
const parts: string[] = [];
|
|
332
|
-
if (loaded.system !== null) {
|
|
333
|
-
parts.push(loaded.system.content);
|
|
334
|
-
if (loaded.system.truncated) parts.push(truncationLine(loaded.system));
|
|
335
|
-
} else {
|
|
336
|
-
parts.push(builtin);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const files = [...loaded.agents, ...loaded.appends];
|
|
340
|
-
if (files.length > 0) {
|
|
341
|
-
parts.push(CONTEXT_INTRO);
|
|
342
|
-
for (const file of files) parts.push(renderFile(file));
|
|
343
|
-
}
|
|
344
|
-
return parts.join("\n\n");
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
export function formatNotice(notice: ContextNotice): string {
|
|
348
|
-
switch (notice.kind) {
|
|
349
|
-
case "loaded":
|
|
350
|
-
return notice.truncated
|
|
351
|
-
? `context ${notice.path} (truncated)`
|
|
352
|
-
: `context ${notice.path}`;
|
|
353
|
-
case "skipped":
|
|
354
|
-
return `context skipped ${notice.path} (${notice.reason})`;
|
|
355
|
-
default:
|
|
356
|
-
return assertNever(notice, "formatNotice");
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/** Startup uses the actual repository instruction chain; loadContext remains explicit/compatible. */
|
|
361
|
-
export async function resolveContextWorkspace(cwd: string): Promise<string> {
|
|
362
|
-
try {
|
|
363
|
-
const bytes = await gitBytes(cwd, ["--no-pager", "rev-parse", "--show-prefix"]);
|
|
364
|
-
const prefix = new TextDecoder("utf-8", { fatal: true }).decode(bytes).replace(/\n$/, "");
|
|
365
|
-
// Git canonicalizes macOS /var to /private/var. Ascending the known prefix
|
|
366
|
-
// keeps root and cwd in the same spelling, preserving the instruction chain.
|
|
367
|
-
let root = resolve(cwd);
|
|
368
|
-
const levels = prefix.split("/").filter(Boolean).length;
|
|
369
|
-
for (let level = 0; level < levels; level++) root = dirname(root);
|
|
370
|
-
return root;
|
|
371
|
-
} catch { return resolve(cwd); }
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
export interface TargetInstruction extends LoadedFile { scope: string; key: string }
|
|
375
|
-
export interface TargetInstructionOptions {
|
|
376
|
-
workspace: string;
|
|
377
|
-
paths: readonly string[];
|
|
378
|
-
/** Digests from frozen startup files and durable instruction events, never model claims. */
|
|
379
|
-
known?: ReadonlySet<string>;
|
|
380
|
-
noContextFiles?: boolean;
|
|
381
|
-
/** Admission runs before instruction bytes are captured. */
|
|
382
|
-
canRead?: (path: string) => Promise<boolean>;
|
|
383
|
-
}
|
|
384
|
-
export const instructionKey = (file: Pick<LoadedFile, "path" | "content">): string =>
|
|
385
|
-
`${resolve(file.path)}:${blobAddress(file.content)}`;
|
|
386
|
-
|
|
387
|
-
/** Target paths are explicit. No recursive scan, sibling instruction loading or prefix surgery. */
|
|
388
|
-
export async function discoverTargetInstructions(options: TargetInstructionOptions): Promise<{ files: TargetInstruction[]; notices: ContextNotice[] }> {
|
|
389
|
-
if (options.noContextFiles) return { files: [], notices: [] };
|
|
390
|
-
const root = resolve(options.workspace), directories = new Set<string>();
|
|
391
|
-
for (const path of options.paths) {
|
|
392
|
-
const absolute = resolve(root, path);
|
|
393
|
-
// No workspace scope applies outside it, and another project's AGENTS.md
|
|
394
|
-
// is not this workspace's authority. Whether the target may be touched at
|
|
395
|
-
// all is the path rules' decision; throwing here pre-empted it with an
|
|
396
|
-
// internal step's name (outside-path-instructions card).
|
|
397
|
-
if (!isInside(absolute, root)) continue;
|
|
398
|
-
for (const directory of projectDirectories(root, dirname(absolute))) directories.add(directory);
|
|
399
|
-
}
|
|
400
|
-
const ordered = [...directories].sort((a, b) => a.split(sep).length - b.split(sep).length || a.localeCompare(b));
|
|
401
|
-
const files: TargetInstruction[] = [], notices: ContextNotice[] = [], budget = new Budget();
|
|
402
|
-
for (const directory of ordered) {
|
|
403
|
-
// A target directory's symlink must not import another project's ambient authority.
|
|
404
|
-
let safe = true, current = root;
|
|
405
|
-
for (const part of relative(root, directory).split(sep).filter(Boolean)) {
|
|
406
|
-
current = join(current, part);
|
|
407
|
-
try { const info = await lstat(current); if (info.isSymbolicLink() || !info.isDirectory()) { safe = false; break; } }
|
|
408
|
-
catch (error) { if ((error as { code?: string }).code !== "ENOENT") throw error; }
|
|
409
|
-
}
|
|
410
|
-
if (!safe) { notices.push({ kind: "skipped", path: directory, reason: "instruction scope crosses a symlink or non-directory" }); continue; }
|
|
411
|
-
for (const name of AGENTS_CANDIDATES) {
|
|
412
|
-
const path = join(directory, name);
|
|
413
|
-
try {
|
|
414
|
-
const info = await lstat(path);
|
|
415
|
-
if (info.isDirectory()) continue;
|
|
416
|
-
if (info.isSymbolicLink() || !info.isFile()) { notices.push({ kind: "skipped", path, reason: "instruction file must be ordinary, not a symlink" }); break; }
|
|
417
|
-
} catch (error) { if ((error as { code?: string }).code === "ENOENT") continue; throw error; }
|
|
418
|
-
if (options.canRead && !await options.canRead(path)) { notices.push({ kind: "skipped", path, reason: "instruction read permission is unavailable" }); break; }
|
|
419
|
-
const got = await tryRead(path);
|
|
420
|
-
if (got.status !== "ok") { if (got.status === "skip") notices.push({ kind: "skipped", path, reason: got.reason }); break; }
|
|
421
|
-
const file = absorbText(path, got.text, "agents", got.bytes, budget, notices), key = instructionKey(file);
|
|
422
|
-
if (!options.known?.has(key)) files.push({ ...file, scope: directory, key });
|
|
423
|
-
break;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return { files, notices };
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/** Append as a durable in-context event; closer scope overrides only files under that directory. */
|
|
430
|
-
export function renderTargetInstructions(files: readonly TargetInstruction[]): string {
|
|
431
|
-
if (!files.length) return "";
|
|
432
|
-
return "# Instructions discovered for the current file targets\n\n" +
|
|
433
|
-
"Apply each file only within its directory scope. Deeper directories take precedence. " +
|
|
434
|
-
"This observed version supersedes earlier versions at the same path; other scopes remain unchanged.\n\n" +
|
|
435
|
-
files.map(file => `<instruction_scope directory="${escapeAttr(file.scope)}" key="${escapeAttr(file.key)}">\n${renderFile(file)}\n</instruction_scope>`).join("\n\n");
|
|
436
|
-
}
|