@visual-z/xeno 0.8.4 → 0.9.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 +8 -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
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/** O.9: references to facts, never a child's self-assessed completion. */
|
|
2
|
-
import type { LogEvent, NodeRef } from "./events.ts";
|
|
3
|
-
import type { Handoff } from "./handoff.ts";
|
|
4
|
-
import type { ExecutionOutcome } from "./outcome.ts";
|
|
5
|
-
import { deriveRequestState } from "./request-state.ts";
|
|
6
|
-
|
|
7
|
-
export function deriveBranchReceipt(input: {
|
|
8
|
-
source: NodeRef; outcome: ExecutionOutcome; handoff: Handoff;
|
|
9
|
-
nodes: readonly { node: number; events: readonly LogEvent[] }[];
|
|
10
|
-
}) {
|
|
11
|
-
const entries = input.nodes.flatMap(part => part.events.map(event => ({ node: part.node, event })));
|
|
12
|
-
const commands = entries.flatMap(({ node, event }) => event.t === "tool_result" && event.effect?.kind === "command"
|
|
13
|
-
? [{ node, event: event.id, exit: event.effect.exit }] : []);
|
|
14
|
-
const changes = entries.flatMap(({ node, event }) => event.t === "tool_result" && event.effect?.kind === "edit"
|
|
15
|
-
? [{ node, event: event.id, path: event.effect.path }] : []);
|
|
16
|
-
const observations = entries.flatMap(({ node, event }) => event.t === "tool_result" ? [{ node, event: event.id }] : []);
|
|
17
|
-
const unfinished = (input.handoff.state.work_items ?? []).filter(item => item.status === "active" || item.status === "blocked")
|
|
18
|
-
.map(item => ({ work_id: item.id, status: item.status, next_action: item.next_action ?? null }));
|
|
19
|
-
return { version: 1, source: input.source, execution: input.outcome,
|
|
20
|
-
// S.1: the child's request ledger, the same verdict any request view shows.
|
|
21
|
-
verification: deriveRequestState(input.nodes)?.status ?? "not_applicable",
|
|
22
|
-
task_acceptance: "not_assessed", commands, changes, observations, unfinished,
|
|
23
|
-
...(input.handoff.processes?.length ? { processes: input.handoff.processes } : {}),
|
|
24
|
-
unresolved_branches: input.handoff.branches.filter(b => b.resolution === null).map(b => b.node),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { modelKey } from "./session-position.ts";
|
|
2
|
-
|
|
3
|
-
/** D13: identity and selection are independent of provider SDKs and source order. */
|
|
4
|
-
export function mergeCatalogs<T extends { provider: string; id: string }>(...layers: readonly (readonly T[])[]): T[] {
|
|
5
|
-
const models = new Map<string, T>();
|
|
6
|
-
for (const layer of layers) for (const model of layer) models.set(modelKey(model), model);
|
|
7
|
-
return [...models.values()];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Provider-local allow/deny patterns use literal characters plus '*' and '?'. */
|
|
11
|
-
export function matchesModelPattern(id: string, pattern: string): boolean {
|
|
12
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
13
|
-
return new RegExp(`^${escaped}$`, "u").test(id);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function modelEnabled(id: string, allow?: readonly string[], deny?: readonly string[]): boolean {
|
|
17
|
-
return (allow === undefined || allow.some(pattern => matchesModelPattern(id, pattern)))
|
|
18
|
-
&& !(deny?.some(pattern => matchesModelPattern(id, pattern)) ?? false);
|
|
19
|
-
}
|
package/src/core/checkpoints.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/** P.3: recovery describes observed file transitions, never a complete disk image. */
|
|
2
|
-
export type FileVersion = { sha256: string; mode: number } | null;
|
|
3
|
-
export type FileChange = { before: FileVersion; after: FileVersion };
|
|
4
|
-
export type FileChanges = Record<string, FileChange>;
|
|
5
|
-
export interface CheckpointEvent {
|
|
6
|
-
t: "checkpoint";
|
|
7
|
-
id: string;
|
|
8
|
-
at: string;
|
|
9
|
-
operation: string;
|
|
10
|
-
source: "edit" | "git" | "restore";
|
|
11
|
-
phase: "prepared" | "applied" | "unavailable";
|
|
12
|
-
changes: FileChanges;
|
|
13
|
-
tree?: string;
|
|
14
|
-
reason?: string;
|
|
15
|
-
}
|
|
16
|
-
export interface WorkspaceObservationEvent {
|
|
17
|
-
t: "workspace_observation";
|
|
18
|
-
id: string;
|
|
19
|
-
at: string;
|
|
20
|
-
/** P.3: this scope excludes ignored/build output and is not a backup. */
|
|
21
|
-
fingerprint?: string;
|
|
22
|
-
scope: "source-v1";
|
|
23
|
-
changed: boolean;
|
|
24
|
-
reason?: string;
|
|
25
|
-
/** The workspace as a goal request began: the state a counter-check must revert to exactly. */
|
|
26
|
-
baseline?: true;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function sameVersion(a: FileVersion, b: FileVersion): boolean {
|
|
30
|
-
return a === null || b === null ? a === b : a.sha256 === b.sha256 && a.mode === b.mode;
|
|
31
|
-
}
|
|
32
|
-
export function changedPaths(changes: FileChanges): string[] {
|
|
33
|
-
return Object.keys(changes).filter(path => !sameVersion(changes[path]!.before, changes[path]!.after)).sort();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Undo the abandoned suffix then replay the selected suffix, touching only their paths. */
|
|
37
|
-
export function planFileTransition(from: readonly CheckpointEvent[], to: readonly CheckpointEvent[]): FileChanges {
|
|
38
|
-
const left = from.filter(event => event.phase === "applied");
|
|
39
|
-
const right = to.filter(event => event.phase === "applied");
|
|
40
|
-
let common = 0;
|
|
41
|
-
while (common < left.length && common < right.length && left[common]!.id === right[common]!.id) common++;
|
|
42
|
-
const changes: FileChanges = Object.create(null) as FileChanges;
|
|
43
|
-
const apply = (path: string, before: FileVersion, after: FileVersion) => {
|
|
44
|
-
const existing = changes[path];
|
|
45
|
-
if (existing && !sameVersion(existing.after, before)) throw new Error(`discontinuous file history: ${path}; inspect intervening external edits`);
|
|
46
|
-
changes[path] = { before: existing ? existing.before : before, after };
|
|
47
|
-
};
|
|
48
|
-
for (const event of left.slice(common).reverse()) for (const path of changedPaths(event.changes)) {
|
|
49
|
-
const change = event.changes[path]!;
|
|
50
|
-
apply(path, change.after, change.before);
|
|
51
|
-
}
|
|
52
|
-
for (const event of right.slice(common)) for (const path of changedPaths(event.changes)) {
|
|
53
|
-
const change = event.changes[path]!;
|
|
54
|
-
apply(path, change.before, change.after);
|
|
55
|
-
}
|
|
56
|
-
return Object.fromEntries(changedPaths(changes).map(path => [path, changes[path]!])) as FileChanges;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function validFileChanges(value: unknown): value is FileChanges {
|
|
60
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
61
|
-
const version = (v: unknown): boolean => v === null || typeof v === "object" && !Array.isArray(v) &&
|
|
62
|
-
typeof (v as Record<string, unknown>)["sha256"] === "string" && /^[a-f0-9]{64}$/.test(String((v as Record<string, unknown>)["sha256"])) &&
|
|
63
|
-
Number.isInteger((v as Record<string, unknown>)["mode"]) && Number((v as Record<string, unknown>)["mode"]) >= 0 && Number((v as Record<string, unknown>)["mode"]) <= 0o777;
|
|
64
|
-
return Object.entries(value).every(([path, change]: [string, unknown]) => path.length > 0 && typeof change === "object" && change !== null &&
|
|
65
|
-
version((change as Record<string, unknown>)["before"]) && version((change as Record<string, unknown>)["after"]));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* The one test for "this event changed the workspace": a real edit, an
|
|
70
|
-
* applied checkpoint that moved a file, or an observation that saw a change.
|
|
71
|
-
* Acceptance, request evidence and environment validity all ask exactly this
|
|
72
|
-
* question and must not answer it three different ways (S.1).
|
|
73
|
-
*/
|
|
74
|
-
export function workspaceChanged(event: import("./events.ts").LogEvent): boolean {
|
|
75
|
-
return event.t === "tool_result" && event.effect?.kind === "edit" && event.effect.sha256_before !== event.effect.sha256_after ||
|
|
76
|
-
event.t === "checkpoint" && event.phase === "applied" && changedPaths(event.changes).length > 0 ||
|
|
77
|
-
event.t === "workspace_observation" && event.changed;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** The transition back: every path's before and after swapped. Applying it undoes `changes`. */
|
|
81
|
-
export function invertFileChanges(changes: FileChanges): FileChanges {
|
|
82
|
-
const inverted: FileChanges = Object.create(null) as FileChanges;
|
|
83
|
-
for (const [path, change] of Object.entries(changes)) inverted[path] = { before: change.after, after: change.before };
|
|
84
|
-
return inverted;
|
|
85
|
-
}
|
package/src/core/churn.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Two loops the repetition check cannot see.
|
|
3
|
-
*
|
|
4
|
-
* `continuationGuidance` flags an action/observation cycle that repeats
|
|
5
|
-
* exactly. A real rut rarely does: a rerun test prints a new timestamp, a
|
|
6
|
-
* build a new temp path, and every observation digest differs -- so neither
|
|
7
|
-
* that check nor D9 stall counting fires while the model retries one idea.
|
|
8
|
-
* These two keys come from facts the harness recorded itself, not from
|
|
9
|
-
* comparing output: the exact command with its exit code, and the path of
|
|
10
|
-
* each successful edit (LangChain's harness write-up uses the same edit
|
|
11
|
-
* count; opencode's doom_loop misses exactly these near-repeats).
|
|
12
|
-
*
|
|
13
|
-
* Advice only, once per request: nothing is blocked and the stop rules are
|
|
14
|
-
* unchanged.
|
|
15
|
-
*/
|
|
16
|
-
import type { GuidanceEvent, LogEvent } from "./events.ts";
|
|
17
|
-
import { currentRequestEvents } from "./request-state.ts";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Assumed, not measured. Three failures leaves room for "change, retry,
|
|
21
|
-
* still failing" -- normal debugging -- before calling it a loop; eight edits
|
|
22
|
-
* of one file sits above a typical refactor. Too low costs one extra notice;
|
|
23
|
-
* too high falls back to today's behaviour.
|
|
24
|
-
*/
|
|
25
|
-
export const CHURN_FAILURES = 3;
|
|
26
|
-
export const CHURN_EDITS = 8;
|
|
27
|
-
|
|
28
|
-
export function churnReview(events: readonly LogEvent[]): Pick<GuidanceEvent, "reason" | "text" | "sources"> | null {
|
|
29
|
-
const current = currentRequestEvents(events);
|
|
30
|
-
if (current.some((event) => event.t === "guidance" && event.reason === "churn")) return null;
|
|
31
|
-
const failures = new Map<string, { count: number; last: string }>();
|
|
32
|
-
const edits = new Map<string, { count: number; last: string }>();
|
|
33
|
-
for (const event of current) {
|
|
34
|
-
if (event.t !== "tool_result" || event.effect === undefined) continue;
|
|
35
|
-
const effect = event.effect;
|
|
36
|
-
if (effect.kind === "command") {
|
|
37
|
-
// R5 handles structured failures with version/change premises; keep this path for legacy logs.
|
|
38
|
-
if (event.diagnostic?.execution !== undefined) continue;
|
|
39
|
-
const key = effect.cmd.trim();
|
|
40
|
-
if (effect.exit === 0) { failures.delete(key); continue; }
|
|
41
|
-
const seen = { count: (failures.get(key)?.count ?? 0) + 1, last: event.id };
|
|
42
|
-
failures.set(key, seen);
|
|
43
|
-
if (seen.count >= CHURN_FAILURES) return {
|
|
44
|
-
reason: "churn", sources: [event.id],
|
|
45
|
-
text: `Harness: \`${key}\` failed ${seen.count} times in this request without succeeding. ` +
|
|
46
|
-
"Retrying the same approach is unlikely to change the result. Read the end of the last failure, " +
|
|
47
|
-
"state the cause you believe it has, and test that belief with a different step before running it again.",
|
|
48
|
-
};
|
|
49
|
-
} else if (effect.kind === "edit") {
|
|
50
|
-
const seen = { count: (edits.get(effect.path)?.count ?? 0) + 1, last: event.id };
|
|
51
|
-
edits.set(effect.path, seen);
|
|
52
|
-
if (seen.count >= CHURN_EDITS) return {
|
|
53
|
-
reason: "churn", sources: [event.id],
|
|
54
|
-
text: `Harness: ${effect.path} was edited ${seen.count} times in this request. ` +
|
|
55
|
-
"Repeated small edits usually mean the underlying cause is not yet understood. Re-read the file and the " +
|
|
56
|
-
"failing evidence, then make one change that addresses the cause.",
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/** R8 / D8 / D11: an explicit observation scope is not a claim of build causality. */
|
|
2
|
-
import type { ArtifactRef } from "./events.ts";
|
|
3
|
-
import { isObservationArtifact } from "./observation-view.ts";
|
|
4
|
-
|
|
5
|
-
// Assumed observation budgets keep a model-supplied plan bounded before execution.
|
|
6
|
-
export const MAX_COMMAND_ARTIFACTS = 16;
|
|
7
|
-
export const MAX_ARTIFACT_PLAN_BYTES = 16 * 1024;
|
|
8
|
-
export const MAX_COMMAND_ARTIFACT_BYTES = 512 * 1024 * 1024;
|
|
9
|
-
export const COMMAND_ARTIFACT_SCOPE = "source-v1-minus-artifacts";
|
|
10
|
-
export type CommandArtifactPlan = { paths: string[]; output_roots: string[] };
|
|
11
|
-
export type ArtifactFileIdentity = { device: string; inode: string; mtime_ns: string; ctime_ns: string; mode: number };
|
|
12
|
-
export type ArtifactFile = { state: "observed"; sha256: string; bytes: number; identity: ArtifactFileIdentity } |
|
|
13
|
-
{ state: "missing" | "unsafe" | "changed" | "unknown"; reason: string };
|
|
14
|
-
export type ArtifactChange = "created" | "modified" | "unchanged" | "unknown";
|
|
15
|
-
export type CommandArtifactOutput = { path: string; before: ArtifactFile; after: ArtifactFile; change: ArtifactChange };
|
|
16
|
-
export type CommandArtifactReceipt = {
|
|
17
|
-
kind: "command_artifacts"; version: 1;
|
|
18
|
-
state: "pending" | "observed" | "changed" | "unknown";
|
|
19
|
-
plan: CommandArtifactPlan;
|
|
20
|
-
source: { host: string; workspace: string; cwd: string; runtime: string };
|
|
21
|
-
command: string; exit: number | null;
|
|
22
|
-
started_at: string; observed_at: string | null;
|
|
23
|
-
inputs: { scope: typeof COMMAND_ARTIFACT_SCOPE; exclusions: string[]; before: string | null; after: string | null };
|
|
24
|
-
outputs: CommandArtifactOutput[];
|
|
25
|
-
reasons: string[];
|
|
26
|
-
manifest?: ArtifactRef;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
30
|
-
const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
31
|
-
const covers = (root: string, path: string) => root === path || path.startsWith(root + "/");
|
|
32
|
-
export function validArtifactPath(value: unknown): value is string {
|
|
33
|
-
return typeof value === "string" && value.length > 0 && new TextEncoder().encode(value).length <= 4096 &&
|
|
34
|
-
!/[\x00-\x1f\x7f\\:]/.test(value) && value.split("/").every(part => part !== "" && part !== "." && part !== ".." && part !== ".git" && part !== ".xeno");
|
|
35
|
-
}
|
|
36
|
-
export function parseCommandArtifactPlan(value: unknown): CommandArtifactPlan {
|
|
37
|
-
if (!object(value) || Object.keys(value).some(key => !["paths", "output_roots"].includes(key))) throw new Error("artifacts needs paths and optional output_roots only");
|
|
38
|
-
const paths = value["paths"], roots = value["output_roots"] ?? paths;
|
|
39
|
-
for (const list of [paths, roots]) {
|
|
40
|
-
if (!Array.isArray(list) || list.length < 1 || list.length > MAX_COMMAND_ARTIFACTS || !list.every(validArtifactPath) || new Set(list).size !== list.length)
|
|
41
|
-
throw new Error("artifacts needs 1..16 unique workspace-relative file paths; no traversal or protected paths");
|
|
42
|
-
}
|
|
43
|
-
const plan = { paths: [...paths as string[]].sort(), output_roots: [...roots as string[]].sort() };
|
|
44
|
-
if (new TextEncoder().encode(JSON.stringify(plan)).length > MAX_ARTIFACT_PLAN_BYTES) throw new Error("artifact plan exceeds 16 KiB");
|
|
45
|
-
if (plan.paths.some((path, i) => plan.paths.some((other, j) => i !== j && covers(path, other)))) throw new Error("artifact file paths cannot contain one another");
|
|
46
|
-
if (plan.output_roots.some((root, i) => !plan.paths.some(path => covers(root, path)) || plan.output_roots.some((other, j) => i !== j && covers(root, other))) ||
|
|
47
|
-
plan.paths.some(path => !plan.output_roots.some(root => covers(root, path)))) throw new Error("output_roots must be nonoverlapping and cover exactly the requested artifact paths");
|
|
48
|
-
return plan;
|
|
49
|
-
}
|
|
50
|
-
export function artifactChange(before: ArtifactFile, after: ArtifactFile): ArtifactChange {
|
|
51
|
-
if (after.state !== "observed") return "unknown";
|
|
52
|
-
if (before.state === "missing") return "created";
|
|
53
|
-
if (before.state !== "observed") return "unknown";
|
|
54
|
-
return before.sha256 === after.sha256 && before.bytes === after.bytes && sameArtifactIdentity(before.identity, after.identity) ? "unchanged" : "modified";
|
|
55
|
-
}
|
|
56
|
-
export function sameArtifactIdentity(a: ArtifactFileIdentity, b: ArtifactFileIdentity): boolean {
|
|
57
|
-
return a.device === b.device && a.inode === b.inode && a.mtime_ns === b.mtime_ns && a.ctime_ns === b.ctime_ns && a.mode === b.mode;
|
|
58
|
-
}
|
|
59
|
-
export function commandArtifactState(receipt: Pick<CommandArtifactReceipt, "exit" | "inputs" | "outputs" | "reasons">): CommandArtifactReceipt["state"] {
|
|
60
|
-
if (receipt.exit === null) return "pending";
|
|
61
|
-
if (receipt.inputs.before !== null && receipt.inputs.after !== null && receipt.inputs.before !== receipt.inputs.after ||
|
|
62
|
-
receipt.outputs.some(output => output.after.state === "changed")) return "changed";
|
|
63
|
-
if (receipt.exit !== 0 || receipt.reasons.length > 0 || receipt.inputs.before === null || receipt.inputs.after === null ||
|
|
64
|
-
receipt.outputs.length === 0 || receipt.outputs.some(output => output.after.state !== "observed")) return "unknown";
|
|
65
|
-
return "observed";
|
|
66
|
-
}
|
|
67
|
-
export function isArtifactFile(value: unknown): value is ArtifactFile {
|
|
68
|
-
if (!object(value)) return false;
|
|
69
|
-
if (value["state"] !== "observed") return ["missing", "unsafe", "changed", "unknown"].includes(String(value["state"])) && typeof value["reason"] === "string" && value["reason"].length > 0;
|
|
70
|
-
const id = value["identity"];
|
|
71
|
-
return digest(value["sha256"]) && Number.isSafeInteger(value["bytes"]) && Number(value["bytes"]) >= 0 && Number(value["bytes"]) <= MAX_COMMAND_ARTIFACT_BYTES &&
|
|
72
|
-
object(id) && ["device", "inode", "mtime_ns", "ctime_ns"].every(key => typeof id[key] === "string" && /^-?\d+$/.test(id[key])) &&
|
|
73
|
-
Number.isInteger(id["mode"]) && Number(id["mode"]) >= 0 && Number(id["mode"]) <= 0o777;
|
|
74
|
-
}
|
|
75
|
-
export function isCommandArtifactReceipt(value: unknown): value is CommandArtifactReceipt {
|
|
76
|
-
if (!object(value) || value["kind"] !== "command_artifacts" || value["version"] !== 1 || typeof value["command"] !== "string" || !value["command"].trim() ||
|
|
77
|
-
typeof value["started_at"] !== "string" || (value["exit"] !== null && !Number.isInteger(value["exit"]))) return false;
|
|
78
|
-
let plan: CommandArtifactPlan;
|
|
79
|
-
try { plan = parseCommandArtifactPlan(value["plan"]); } catch { return false; }
|
|
80
|
-
if (!object(value["plan"]) || JSON.stringify(value["plan"]["paths"]) !== JSON.stringify(plan.paths) ||
|
|
81
|
-
JSON.stringify(value["plan"]["output_roots"]) !== JSON.stringify(plan.output_roots)) return false;
|
|
82
|
-
const source = value["source"], inputs = value["inputs"], outputs = value["outputs"], reasons = value["reasons"];
|
|
83
|
-
if (!object(source) || !["host", "workspace", "cwd", "runtime"].every(key => typeof source[key] === "string" && source[key] !== "") ||
|
|
84
|
-
!object(inputs) || inputs["scope"] !== COMMAND_ARTIFACT_SCOPE || !Array.isArray(inputs["exclusions"]) ||
|
|
85
|
-
!inputs["exclusions"].every(path => typeof path === "string" && path.length > 0) || new Set(inputs["exclusions"]).size !== inputs["exclusions"].length ||
|
|
86
|
-
!plan.output_roots.every(root => (inputs["exclusions"] as unknown[]).includes(root)) ||
|
|
87
|
-
!["before", "after"].every(key => inputs[key] === null || digest(inputs[key])) || !Array.isArray(outputs) ||
|
|
88
|
-
!Array.isArray(reasons) || !reasons.every(reason => typeof reason === "string" && reason.length > 0)) return false;
|
|
89
|
-
if (value["exit"] === null) return value["state"] === "pending" && value["observed_at"] === null && inputs["after"] === null && outputs.length === 0 && value["manifest"] === undefined;
|
|
90
|
-
if (typeof value["observed_at"] !== "string" || !value["observed_at"] || outputs.length !== plan.paths.length || !outputs.every((item, index) =>
|
|
91
|
-
object(item) && item["path"] === plan.paths[index] && isArtifactFile(item["before"]) && isArtifactFile(item["after"]) && item["change"] === artifactChange(item["before"], item["after"]))) return false;
|
|
92
|
-
if (value["manifest"] !== undefined && !isObservationArtifact(value["manifest"])) return false;
|
|
93
|
-
return value["state"] === commandArtifactState(value as CommandArtifactReceipt) && (value["state"] !== "observed" || isObservationArtifact(value["manifest"]));
|
|
94
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/** R1 / D11: all command entrances agree on invocation and observed termination. */
|
|
2
|
-
import { executionOutcome } from "./outcome.ts";
|
|
3
|
-
import type { ToolDiagnostic } from "./tool-contract.ts";
|
|
4
|
-
|
|
5
|
-
/** Operational grace, not a measured optimum: ignored SIGTERM cannot defeat cancellation. */
|
|
6
|
-
export const COMMAND_TERMINATION_GRACE_MS = 200;
|
|
7
|
-
|
|
8
|
-
/** A fresh login Bash per command; interactive stdin does not change shell success semantics. */
|
|
9
|
-
export function commandArgv(command: string): string[] {
|
|
10
|
-
return ["bash", "-o", "pipefail", "-lc", command];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Preserve inherited values except documented noninteractive defaults; never mutate the host.
|
|
15
|
-
*
|
|
16
|
-
* `appendPath` is xeno's own tool directory (store/tool-binaries.ts). It goes
|
|
17
|
-
* last, so anything the user installed wins and xeno only fills a gap.
|
|
18
|
-
*/
|
|
19
|
-
export function commandEnvironment(base: Readonly<Record<string, string | undefined>>, appendPath?: string): Record<string, string> {
|
|
20
|
-
const env: Record<string, string> = {};
|
|
21
|
-
for (const [key, value] of Object.entries(base)) if (value !== undefined) env[key] = value;
|
|
22
|
-
if (appendPath !== undefined && appendPath.length > 0 && !(env["PATH"] ?? "").split(":").includes(appendPath)) {
|
|
23
|
-
env["PATH"] = env["PATH"] ? `${env["PATH"]}:${appendPath}` : appendPath;
|
|
24
|
-
}
|
|
25
|
-
Object.assign(env, {
|
|
26
|
-
TERM: "dumb", PAGER: "cat", GIT_PAGER: "cat", NO_COLOR: "1",
|
|
27
|
-
GIT_TERMINAL_PROMPT: "0", DEBIAN_FRONTEND: "noninteractive",
|
|
28
|
-
});
|
|
29
|
-
if (env["LANG"] === undefined || env["LANG"] === "") env["LANG"] = "C.UTF-8";
|
|
30
|
-
return env;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Codex's `max_output_tokens` as an inline byte budget for one call. It can
|
|
35
|
-
* only lower the session ceiling: full output is still captured and
|
|
36
|
-
* recoverable, so this shapes the preview, never the receipt.
|
|
37
|
-
*
|
|
38
|
-
* Assumed, not measured: 4 bytes per token rounds the ASCII estimate in
|
|
39
|
-
* tokens.ts (about 3.6) up, so the model never gets less than it asked for.
|
|
40
|
-
* The 1 KiB floor keeps room for the header and recovery hint (about 300
|
|
41
|
-
* bytes); below it a preview shows almost no output and forces a re-read.
|
|
42
|
-
*/
|
|
43
|
-
export const OUTPUT_BYTES_PER_TOKEN = 4;
|
|
44
|
-
export const MIN_OUTPUT_BYTES = 1_024;
|
|
45
|
-
export function outputBytesForTokens(tokens: unknown, ceiling: number): number {
|
|
46
|
-
if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens <= 0) return ceiling;
|
|
47
|
-
return Math.min(ceiling, Math.max(MIN_OUTPUT_BYTES, Math.floor(tokens) * OUTPUT_BYTES_PER_TOKEN));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Model seconds and host milliseconds meet once; a yield is never a command deadline. */
|
|
51
|
-
export function commandTimeout(requestedSeconds: number | undefined, defaultMs: number, maximumMs = defaultMs): number {
|
|
52
|
-
return Math.min(requestedSeconds === undefined ? defaultMs : requestedSeconds * 1000, maximumMs);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Programs whose exit 1 is an answer rather than a fault (POSIX: grep's 1 is
|
|
57
|
-
* "no match" and >1 an error; diff and cmp use 1 for "differ"; test for
|
|
58
|
-
* false). Reporting them as failures tells the model to inspect and retry an
|
|
59
|
-
* answer it already holds -- the same reading Claude Code's command semantics
|
|
60
|
-
* give these programs.
|
|
61
|
-
*/
|
|
62
|
-
const NEGATIVE_ANSWERS: Readonly<Record<string, string>> = {
|
|
63
|
-
grep: "no match", egrep: "no match", fgrep: "no match", rg: "no match",
|
|
64
|
-
diff: "files differ", cmp: "files differ", test: "condition false", "[": "condition false",
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The final simple command of `command` and the separator before it, or
|
|
69
|
-
* null when this scanner cannot read the command with certainty --
|
|
70
|
-
* unbalanced quotes, subshells, compound statements, substitutions or a
|
|
71
|
-
* background `&`. Only top-level `;`, newline, `&&`, `||` and `|` split.
|
|
72
|
-
*/
|
|
73
|
-
function finalStage(command: string): { separator: string; words: string[] } | null {
|
|
74
|
-
let quote: "'" | "\"" | null = null, start = 0, separator = "";
|
|
75
|
-
for (let i = 0; i < command.length; i++) {
|
|
76
|
-
const c = command[i]!;
|
|
77
|
-
if (quote !== null) {
|
|
78
|
-
if (c === "\\" && quote === "\"") i++;
|
|
79
|
-
else if (c === quote) quote = null;
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
if (c === "\\") { i++; continue; }
|
|
83
|
-
if (c === "'" || c === "\"") { quote = c; continue; }
|
|
84
|
-
if (c === "`" || c === "(" || c === ")" || c === "{" || c === "}" || (c === "$" && command[i + 1] === "(")) return null;
|
|
85
|
-
const two = command.slice(i, i + 2);
|
|
86
|
-
if (two === "&&" || two === "||") { separator = two; start = i + 2; i++; continue; }
|
|
87
|
-
if (c === ";" || c === "\n" || c === "|") { separator = c; start = i + 1; continue; }
|
|
88
|
-
if (c === "&" && command[i + 1] !== ">" && command[i - 1] !== ">") return null;
|
|
89
|
-
}
|
|
90
|
-
if (quote !== null) return null;
|
|
91
|
-
return { separator, words: command.slice(start).trim().split(/\s+/).filter(word => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(word)) };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const programName = (words: readonly string[]) => (words[0] ?? "").replace(/^.*\//, "");
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* What exit 1 of the command's final simple command means, or null.
|
|
98
|
-
*
|
|
99
|
-
* Only the last segment after a top-level `;`, newline, `||` or `|` is read.
|
|
100
|
-
* After `&&` the 1 may come from the command that short-circuited, so it is
|
|
101
|
-
* not classified. Under pipefail an earlier pipeline stage can also set the
|
|
102
|
-
* status; the exit is kept in the receipt either way, and the label states
|
|
103
|
-
* the program's meaning rather than claiming success. Anything this scanner
|
|
104
|
-
* cannot read with certainty -- unbalanced quotes, subshells, compound
|
|
105
|
-
* statements, substitutions -- yields null.
|
|
106
|
-
*/
|
|
107
|
-
export function negativeAnswer(command: string, exit: number): string | null {
|
|
108
|
-
if (exit !== 1) return null;
|
|
109
|
-
const stage = finalStage(command);
|
|
110
|
-
if (stage === null || stage.separator === "&&") return null;
|
|
111
|
-
const program = programName(stage.words);
|
|
112
|
-
if (program === "git") return stage.words[1] === "grep" ? "no match" : null;
|
|
113
|
-
return Object.hasOwn(NEGATIVE_ANSWERS, program) ? NEGATIVE_ANSWERS[program]! : null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Readers that may stop reading before their input ends (`head -n`, `sed Nq`,
|
|
118
|
-
* `awk '{exit}'`, `grep -m` / `-q`), closing the pipe on the writer before it.
|
|
119
|
-
*/
|
|
120
|
-
const EARLY_READERS: ReadonlySet<string> = new Set(["head", "sed", "awk", "gawk", "mawk", "grep", "egrep", "fgrep", "rg"]);
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* What exit 141 means when an early reader ends the pipeline, or null.
|
|
124
|
-
*
|
|
125
|
-
* Measured (pipe-early-close card): under `bash -o pipefail`, `seq 1 1000000
|
|
126
|
-
* | head -1` exits 141 -- pipefail reports the rightmost nonzero stage, here
|
|
127
|
-
* the writer that SIGPIPE killed after head closed its input, while every
|
|
128
|
-
* stage to its right, the final reader included, exited 0. The output the
|
|
129
|
-
* command asked for is complete. Only a final stage joined by a top-level
|
|
130
|
-
* `|` is read: after `;`, `&&` or `||` the 141 is the last command's own.
|
|
131
|
-
*/
|
|
132
|
-
export function earlyCloseAnswer(command: string, exit: number): string | null {
|
|
133
|
-
if (exit !== 141) return null;
|
|
134
|
-
const stage = finalStage(command);
|
|
135
|
-
if (stage === null || stage.separator !== "|") return null;
|
|
136
|
-
const program = programName(stage.words);
|
|
137
|
-
return EARLY_READERS.has(program) ? `${program} closed the pipe early; the earlier stage's SIGPIPE is not a failure` : null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** The program's own meaning of a nonzero exit that is an answer, not a fault. */
|
|
141
|
-
export function commandAnswer(command: string, exit: number): string | null {
|
|
142
|
-
return negativeAnswer(command, exit) ?? earlyCloseAnswer(command, exit);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** A trap can exit zero after cancellation: successful exit alone does not erase the stop. */
|
|
146
|
-
export function commandCompletion(exit: number, stopped?: "cancelled" | "timeout", cleanup?: ToolDiagnostic, command?: string) {
|
|
147
|
-
const answered = exit === 0 || command !== undefined && commandAnswer(command, exit) !== null;
|
|
148
|
-
return { ok: stopped === undefined && answered && cleanup === undefined,
|
|
149
|
-
outcome: executionOutcome(stopped ?? (answered && cleanup === undefined ? "tool_completed" : "tool_failed")),
|
|
150
|
-
...(cleanup ? { warnings: [cleanup] } : {}) };
|
|
151
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { assertNever } from "./exhaustive.ts";
|
|
2
|
-
|
|
3
|
-
/** D19: confirmation follows the selected completion, never the unfinished draft. */
|
|
4
|
-
export function completionAction(kind: "command" | "argument" | "path", key: "enter" | "tab", requiresArgument: boolean): "insert" | "submit" {
|
|
5
|
-
if (key === "tab") return "insert";
|
|
6
|
-
switch (kind) {
|
|
7
|
-
case "command": return requiresArgument ? "insert" : "submit";
|
|
8
|
-
case "argument": return "submit";
|
|
9
|
-
case "path": return "insert";
|
|
10
|
-
default: return assertNever(kind, "completion kind");
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** Match the parser's exact identity before discoverable prefixes and substrings. */
|
|
15
|
-
export function commandCompletionRank(name: string, query: string): number {
|
|
16
|
-
const canonical = name.toLowerCase(), typed = query.toLowerCase();
|
|
17
|
-
if (canonical === typed) return 0;
|
|
18
|
-
if (canonical.startsWith(typed)) return 1;
|
|
19
|
-
return typed.length > 1 && canonical.includes(typed) ? 2 : Number.POSITIVE_INFINITY;
|
|
20
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A one-time reconciliation when the model ends a request.
|
|
3
|
-
*
|
|
4
|
-
* The model deciding it is done is a claim like any other (D11), and the
|
|
5
|
-
* failure modes that end a Terminal-Bench run early all look like a normal
|
|
6
|
-
* final reply: an edit nobody ran, a question handed back to a user who is
|
|
7
|
-
* not there, or nothing at all. ForgeCode found a programmatic check at this
|
|
8
|
-
* point was its largest single gain and that a prompt instruction alone did
|
|
9
|
-
* not reproduce it. The check here only appends a notice; the model still
|
|
10
|
-
* decides, and a second finish is respected -- an honest blocker must not
|
|
11
|
-
* turn into a loop.
|
|
12
|
-
*/
|
|
13
|
-
import type { GuidanceEvent, LogEvent } from "./events.ts";
|
|
14
|
-
import type { AcceptanceState } from "./acceptance.ts";
|
|
15
|
-
import { deriveRequestState, type RequestState } from "./request-state.ts";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Wording that hands the task back instead of finishing it. Ported from
|
|
19
|
-
* waner `crates/waner_core/src/turn/premature_stop.rs`, including its window:
|
|
20
|
-
* the first 400 and last 300 characters, where an opening refusal or a
|
|
21
|
-
* closing offer sits. The same words in the middle of a long report are
|
|
22
|
-
* usually a quotation or a description, not a stop.
|
|
23
|
-
*/
|
|
24
|
-
const HAND_BACK = [
|
|
25
|
-
"i can't proceed", "i cannot proceed", "i can't continue", "i cannot continue",
|
|
26
|
-
"giving up", "stopping here", "unable to complete", "i'm unable to", "i am unable to",
|
|
27
|
-
"let me know if you", "would you like me to", "do you want me to", "shall i ",
|
|
28
|
-
"我无法继续", "无法完成", "需要你确认", "请确认是否", "是否需要我", "要不要我",
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
export function looksLikeHandBack(text: string): boolean {
|
|
32
|
-
const chars = [...text.trim()];
|
|
33
|
-
const window = `${chars.slice(0, 400).join("")}\n${chars.slice(Math.max(0, chars.length - 300)).join("")}`.toLowerCase();
|
|
34
|
-
return HAND_BACK.some((pattern) => window.includes(pattern));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function completionReview(
|
|
38
|
-
events: readonly LogEvent[],
|
|
39
|
-
remainingRequests: number,
|
|
40
|
-
options?: { strictVerification?: boolean; acceptance?: AcceptanceState },
|
|
41
|
-
): Pick<GuidanceEvent, "reason" | "text" | "sources"> | null {
|
|
42
|
-
const state = deriveRequestState([{ node: 0, events }], { strict: options?.strictVerification === true });
|
|
43
|
-
const decision = state ? requestCompletion(state, remainingRequests, options) : { action: "finish" as const };
|
|
44
|
-
return decision.action === "continue" ? decision.guidance : null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export type RequestCompletion = { action: "finish" } |
|
|
48
|
-
{ action: "continue"; guidance: Pick<GuidanceEvent, "reason" | "text" | "sources"> };
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Q23 / S.1: loop termination reads the same ledger as every request view.
|
|
52
|
-
* A reported blocker does not end the check early: the model's statement is
|
|
53
|
-
* not evidence (D11), and the check still runs only once, so an honest
|
|
54
|
-
* blocker restated at the second finish is respected.
|
|
55
|
-
*/
|
|
56
|
-
export function requestCompletion(state: RequestState, remainingRequests: number,
|
|
57
|
-
options?: { acceptance?: AcceptanceState }): RequestCompletion {
|
|
58
|
-
const finish = { action: "finish" as const };
|
|
59
|
-
if (remainingRequests < 1 || state.reviewed) return finish;
|
|
60
|
-
const final = state.final;
|
|
61
|
-
if (!final || final.has_tools || state.execution?.reason !== "model_end" && state.execution?.reason !== "unknown") return finish;
|
|
62
|
-
const text = final.text;
|
|
63
|
-
|
|
64
|
-
const gaps: string[] = [];
|
|
65
|
-
const acceptance = options?.acceptance ?? state.acceptance;
|
|
66
|
-
const missingCommands = acceptance?.status === "needs_work" ? acceptance.criteria.flatMap(item =>
|
|
67
|
-
item.kind === "command" && item.status !== "pass" ? [{ id: item.id, command: item.command, status: item.status }] : []) : [];
|
|
68
|
-
const freshCheck = acceptance ? undefined : state.criteria.find(item => item.kind === "fresh_check");
|
|
69
|
-
if (state.used_tools && text.length === 0) gaps.push("the final reply is empty");
|
|
70
|
-
// Strict verification counts only configured gates (O.11); say so, since a successful `cat` would otherwise look like a check.
|
|
71
|
-
if (freshCheck && freshCheck.status !== "pass") gaps.push(freshCheck.gate_only
|
|
72
|
-
? "files were changed but no configured verification gate (a test, typecheck, lint, build or check command) has passed since the last edit; reading or printing files does not count, and a gate counts only when run as plain commands joined by && (trailing output redirects to fixed files are fine); ;, ||, pipes, $() and variables hide it"
|
|
73
|
-
: "files were changed but no command has succeeded since the last edit");
|
|
74
|
-
if (acceptance?.status === "needs_work") gaps.push(`declared acceptance criteria need evidence: ${[
|
|
75
|
-
...missingCommands.map(item => `${item.id}=${item.status}`),
|
|
76
|
-
...(acceptance.targets ?? []).filter(target => target.status !== "ready").map(target => `target ${target.id}=${target.status}`),
|
|
77
|
-
].join(", ")}`);
|
|
78
|
-
if (text.length > 0 && looksLikeHandBack(text) && acceptance?.status !== "awaiting_human")
|
|
79
|
-
gaps.push("the reply deferred the request; check whether authorized work remains or a specific blocker requires the user");
|
|
80
|
-
if (gaps.length === 0) return finish;
|
|
81
|
-
// D11 / O.11: a declared command needs its exact receipt even when the
|
|
82
|
-
// request has no recorded edit. Compound shell success is not that receipt.
|
|
83
|
-
const next = missingCommands.length > 0
|
|
84
|
-
? "Obtain a fresh successful receipt for each missing command. Run each command exactly as a separate invocation without adding other commands:\n" +
|
|
85
|
-
missingCommands.map(item => `- ${item.id}: ${JSON.stringify(item.command)}`).join("\n") +
|
|
86
|
-
"\nIf verification cannot run, report the specific blocker. "
|
|
87
|
-
: !state.changed
|
|
88
|
-
? "Use original sources for research and full inputs for calculations; do not invent command evidence. Finish with the result or the specific blocker. "
|
|
89
|
-
: "Run the command that verifies the result, or finish with the specific blocker that prevents verification. ";
|
|
90
|
-
return { action: "continue", guidance: {
|
|
91
|
-
reason: "completion_review",
|
|
92
|
-
sources: [state.request.event, final.source.event],
|
|
93
|
-
text: `Harness completion check: ${gaps.join("; ")}. ` +
|
|
94
|
-
`Re-read the original request (event ${state.request.event}) and check each requirement against evidence in this session. ` +
|
|
95
|
-
next + (acceptance?.criteria.some(item => item.kind === "human" && item.status !== "accepted") ? "Required human acceptance remains pending. " : "") +
|
|
96
|
-
"This check runs once per request.",
|
|
97
|
-
} };
|
|
98
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/** O.8: a read-only explanation of the same projection that is dispatched. */
|
|
2
|
-
import { projectContext, type ProjectInput } from "./project.ts";
|
|
3
|
-
import { anchoredCount, estimateTokens, type Anchor, type TokenCounter } from "./tokens.ts";
|
|
4
|
-
import { historyItems, HISTORY_SECTIONS } from "./history.ts";
|
|
5
|
-
import { contextLimits, type ContextPolicy } from "./context-policy.ts";
|
|
6
|
-
import type { BoundaryConfig } from "./boundary.ts";
|
|
7
|
-
|
|
8
|
-
export function contextDetail(input: ProjectInput & {
|
|
9
|
-
anchor: Anchor | null; counter?: TokenCounter; policy: ContextPolicy;
|
|
10
|
-
contextWindow: number | undefined; outputReserve: number; boundary?: Partial<BoundaryConfig>;
|
|
11
|
-
}) {
|
|
12
|
-
const counter = input.counter ?? estimateTokens;
|
|
13
|
-
const projection = projectContext({ ...input, trace: true });
|
|
14
|
-
const base = counter.count([], "", []);
|
|
15
|
-
const system = counter.count([], projection.system, []);
|
|
16
|
-
const tools = counter.count([], projection.system, projection.tools);
|
|
17
|
-
const floor = counter.count(projection.messages.slice(0, input.handoff ? 1 : 0), projection.system, projection.tools);
|
|
18
|
-
const estimated = counter.count(projection.messages, projection.system, projection.tools);
|
|
19
|
-
const anchored = anchoredCount(projection.messages, projection.system, input.anchor, counter, projection.tools);
|
|
20
|
-
const sections = input.handoff ? HISTORY_SECTIONS.map(section => {
|
|
21
|
-
const retained = historyItems(input.handoff!, section).length;
|
|
22
|
-
const index = input.handoff!.history?.sections[section];
|
|
23
|
-
return { section, retained, omitted: index?.offset ?? 0, total: index?.total ?? retained,
|
|
24
|
-
recovery: index && index.offset > 0 ? { node: input.handoff!.history!.source.node, section, offset: 0 } : null };
|
|
25
|
-
}) : [];
|
|
26
|
-
return {
|
|
27
|
-
version: projection.provenance!.version,
|
|
28
|
-
countKind: counter.kind,
|
|
29
|
-
components: { framing: base, system: system - base, tools: tools - system, handoff: floor - tools, messages: estimated - floor },
|
|
30
|
-
estimatedTotal: estimated, used: anchored.tokens,
|
|
31
|
-
anchorCorrection: anchored.tokens - estimated,
|
|
32
|
-
measuredThrough: anchored.exactUpTo,
|
|
33
|
-
limits: contextLimits(input), policy: input.policy,
|
|
34
|
-
sources: projection.provenance!.messages, omittedEvents: projection.provenance!.omitted,
|
|
35
|
-
sections, violations: projection.violations,
|
|
36
|
-
};
|
|
37
|
-
}
|