@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/core/edit-plan.ts
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* O.3: exact edits remain exact; a loose match is never silent.
|
|
3
|
-
*
|
|
4
|
-
* planEdit is the original single exact replacement and stays as it was.
|
|
5
|
-
* planEdits is what the tool uses: several edits, matched on the view read
|
|
6
|
-
* shows (see text-normalize.ts), with a loose fallback that is reported as
|
|
7
|
-
* such in the result rather than applied quietly.
|
|
8
|
-
*/
|
|
9
|
-
import { editDistance, fromView, normalizeLoose, similarity, toLF, toView } from "./text-normalize.ts";
|
|
10
|
-
|
|
11
|
-
export function lineDiff(before: string, after: string): { diff: string; firstLine: number } {
|
|
12
|
-
const a = before.split("\n"), b = after.split("\n");
|
|
13
|
-
let prefix = 0, suffix = 0;
|
|
14
|
-
while (prefix < a.length && prefix < b.length && a[prefix] === b[prefix]) prefix++;
|
|
15
|
-
while (suffix < a.length - prefix && suffix < b.length - prefix && a[a.length - suffix - 1] === b[b.length - suffix - 1]) suffix++;
|
|
16
|
-
const removed = a.slice(prefix, a.length - suffix), added = b.slice(prefix, b.length - suffix);
|
|
17
|
-
return { firstLine: prefix + 1,
|
|
18
|
-
diff: before === after ? "(content unchanged)" : `@@ -${prefix + 1},${removed.length} +${prefix + 1},${added.length} @@\n` +
|
|
19
|
-
[...removed.map(line => `-${line}`), ...added.map(line => `+${line}`)].join("\n") };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function planEdit(before: string, oldText: string, newText: string, all: boolean):
|
|
23
|
-
{ ok: true; after: string; replacements: number; diff: string; firstLine: number } |
|
|
24
|
-
{ ok: false; code: "invalid_input" | "not_found" | "ambiguous_edit"; occurrences: number } {
|
|
25
|
-
if (!oldText) return { ok: false, code: "invalid_input", occurrences: 0 };
|
|
26
|
-
const occurrences = before.split(oldText).length - 1;
|
|
27
|
-
if (occurrences === 0) return { ok: false, code: "not_found", occurrences };
|
|
28
|
-
if (occurrences > 1 && !all) return { ok: false, code: "ambiguous_edit", occurrences };
|
|
29
|
-
// split/join avoids replacement-string interpolation of literal $&, $` or $'.
|
|
30
|
-
const at = before.indexOf(oldText);
|
|
31
|
-
const after = all ? before.split(oldText).join(newText) : before.slice(0, at) + newText + before.slice(at + oldText.length);
|
|
32
|
-
const a = before.split("\n"), b = after.split("\n");
|
|
33
|
-
let prefix = 0, suffix = 0;
|
|
34
|
-
while (prefix < a.length && prefix < b.length && a[prefix] === b[prefix]) prefix++;
|
|
35
|
-
while (suffix < a.length - prefix && suffix < b.length - prefix && a[a.length - suffix - 1] === b[b.length - suffix - 1]) suffix++;
|
|
36
|
-
const removed = a.slice(prefix, a.length - suffix), added = b.slice(prefix, b.length - suffix);
|
|
37
|
-
return { ok: true, after, replacements: all ? occurrences : 1, firstLine: prefix + 1,
|
|
38
|
-
diff: before === after ? "(content unchanged)" : `@@ -${prefix + 1},${removed.length} +${prefix + 1},${added.length} @@\n` +
|
|
39
|
-
[...removed.map(line => `-${line}`), ...added.map(line => `+${line}`)].join("\n") };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
export interface EditSpec { old_string: string; new_string: string; replace_all?: boolean }
|
|
44
|
-
|
|
45
|
-
export interface ClosestMatch {
|
|
46
|
-
/** 1-indexed, inclusive, in the view read shows. */
|
|
47
|
-
startLine: number;
|
|
48
|
-
endLine: number;
|
|
49
|
-
score: number;
|
|
50
|
-
/** First line of old_string that differs from the file, and the file's line there. */
|
|
51
|
-
expected: string;
|
|
52
|
-
actual: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export type EditsPlan =
|
|
56
|
-
| { ok: true; after: string; replacements: number; fuzzy: boolean; diff: string; firstLine: number }
|
|
57
|
-
| { ok: false; code: "invalid_input" | "not_found" | "ambiguous_edit"; edit: number; occurrences: number;
|
|
58
|
-
message: string; closest?: ClosestMatch };
|
|
59
|
-
|
|
60
|
-
/** A replacement in view coordinates: [start, end) becomes text. */
|
|
61
|
-
interface Span { start: number; end: number; text: string; edit: number }
|
|
62
|
-
|
|
63
|
-
function occurrences(haystack: string, needle: string): number[] {
|
|
64
|
-
const found: number[] = [];
|
|
65
|
-
for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) found.push(at);
|
|
66
|
-
return found;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Offsets of each line start in `text`, plus a sentinel one past the end. */
|
|
70
|
-
function lineStarts(text: string): number[] {
|
|
71
|
-
const starts = [0];
|
|
72
|
-
for (let i = 0; i < text.length; i++) if (text[i] === "\n") starts.push(i + 1);
|
|
73
|
-
starts.push(text.length + 1);
|
|
74
|
-
return starts;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const lineOf = (starts: readonly number[], offset: number) => {
|
|
78
|
-
let lo = 0, hi = starts.length - 2;
|
|
79
|
-
while (lo < hi) { const mid = (lo + hi + 1) >> 1; if (starts[mid]! <= offset) lo = mid; else hi = mid - 1; }
|
|
80
|
-
return lo;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Loose matches mapped back onto whole original lines.
|
|
85
|
-
*
|
|
86
|
-
* normalizeLoose keeps the line count, so a match starting on normalized line
|
|
87
|
-
* i covers original lines i..j. Those lines -- and only those -- are replaced
|
|
88
|
-
* by their normalized form with the substitution applied. Rewriting the whole
|
|
89
|
-
* normalized file instead would quietly straighten every quote in it.
|
|
90
|
-
*/
|
|
91
|
-
function looseSpans(text: string, spec: { old: string; next: string; all: boolean }, edit: number):
|
|
92
|
-
{ spans: Span[] } | { count: number } {
|
|
93
|
-
const loose = normalizeLoose(text);
|
|
94
|
-
const needle = normalizeLoose(spec.old);
|
|
95
|
-
if (needle.trim().length === 0) return { count: 0 };
|
|
96
|
-
const found = occurrences(loose, needle);
|
|
97
|
-
if (found.length === 0 || (found.length > 1 && !spec.all)) return { count: found.length };
|
|
98
|
-
const looseStarts = lineStarts(loose), starts = lineStarts(text);
|
|
99
|
-
const spans: Span[] = [];
|
|
100
|
-
for (const at of found) {
|
|
101
|
-
const first = lineOf(looseStarts, at), last = lineOf(looseStarts, at + needle.length - 1);
|
|
102
|
-
const from = looseStarts[first]!, to = looseStarts[last + 1]! - 1;
|
|
103
|
-
const segment = loose.slice(from, to);
|
|
104
|
-
const local = at - from;
|
|
105
|
-
spans.push({ start: starts[first]!, end: starts[last + 1]! - 1, edit,
|
|
106
|
-
text: segment.slice(0, local) + spec.next + segment.slice(local + needle.length) });
|
|
107
|
-
}
|
|
108
|
-
return { spans };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Where old_string most nearly occurs, for a failure message that lets the
|
|
113
|
-
* model fix its copy instead of rereading the file (omp reports the same:
|
|
114
|
-
* `crates/pi-edit/src/fuzzy.rs`). Candidates are anchored on lines of
|
|
115
|
-
* old_string that occur verbatim (trimmed); failing that, on the most similar
|
|
116
|
-
* single line, which is only computed for files small enough to afford it.
|
|
117
|
-
*/
|
|
118
|
-
export function closestMatch(text: string, old: string): ClosestMatch | undefined {
|
|
119
|
-
const lines = text.split("\n");
|
|
120
|
-
const want = old.split("\n");
|
|
121
|
-
if (want.length > lines.length) return undefined;
|
|
122
|
-
const clip = (line: string) => line.trim().slice(0, 200);
|
|
123
|
-
const starts = new Set<number>();
|
|
124
|
-
const index = new Map<string, number[]>();
|
|
125
|
-
lines.forEach((line, i) => { const key = clip(line); if (key.length >= 4) index.set(key, [...(index.get(key) ?? []), i]); });
|
|
126
|
-
want.forEach((line, k) => {
|
|
127
|
-
for (const i of index.get(clip(line)) ?? []) if (i - k >= 0 && i - k + want.length <= lines.length) starts.add(i - k);
|
|
128
|
-
});
|
|
129
|
-
if (starts.size === 0 && lines.length <= 5_000) {
|
|
130
|
-
const anchor = want.findIndex((line) => clip(line).length > 0);
|
|
131
|
-
if (anchor === -1) return undefined;
|
|
132
|
-
const target = clip(want[anchor]!).slice(0, 80);
|
|
133
|
-
let best = -1, bestScore = 0;
|
|
134
|
-
lines.forEach((line, i) => {
|
|
135
|
-
const candidate = clip(line).slice(0, 80);
|
|
136
|
-
if (Math.abs(candidate.length - target.length) > target.length / 2) return;
|
|
137
|
-
const score = similarity(candidate, target);
|
|
138
|
-
if (score > bestScore && i - anchor >= 0 && i - anchor + want.length <= lines.length) { best = i - anchor; bestScore = score; }
|
|
139
|
-
});
|
|
140
|
-
if (best >= 0) starts.add(best);
|
|
141
|
-
}
|
|
142
|
-
let result: ClosestMatch | undefined;
|
|
143
|
-
const block = want.join("\n").slice(0, 2_000);
|
|
144
|
-
for (const start of [...starts].slice(0, 20)) {
|
|
145
|
-
const window = lines.slice(start, start + want.length);
|
|
146
|
-
const score = 1 - editDistance(window.join("\n").slice(0, 2_000), block) / Math.max(1, Math.max(block.length, window.join("\n").slice(0, 2_000).length));
|
|
147
|
-
if (result !== undefined && score <= result.score) continue;
|
|
148
|
-
const differs = want.findIndex((line, k) => line !== window[k]);
|
|
149
|
-
result = { startLine: start + 1, endLine: start + want.length, score,
|
|
150
|
-
expected: want[Math.max(0, differs)] ?? "", actual: window[Math.max(0, differs)] ?? "" };
|
|
151
|
-
}
|
|
152
|
-
return result !== undefined && result.score >= 0.5 ? result : undefined;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Several edits, all located in the original before any is applied, so one
|
|
157
|
-
* edit can never match text another just wrote. All succeed or none do.
|
|
158
|
-
*/
|
|
159
|
-
export function planEdits(raw: string, edits: readonly EditSpec[]): EditsPlan {
|
|
160
|
-
const view = toView(raw);
|
|
161
|
-
const text = view.text;
|
|
162
|
-
if (edits.length === 0) return { ok: false, code: "invalid_input", edit: 0, occurrences: 0, message: "no edits given" };
|
|
163
|
-
const spans: Span[] = [];
|
|
164
|
-
let fuzzy = false;
|
|
165
|
-
for (let e = 0; e < edits.length; e++) {
|
|
166
|
-
const spec = { old: toLF(edits[e]!.old_string), next: toLF(edits[e]!.new_string), all: edits[e]!.replace_all === true };
|
|
167
|
-
const which = edits.length > 1 ? `edit ${e + 1}: ` : "";
|
|
168
|
-
if (spec.old.length === 0) return { ok: false, code: "invalid_input", edit: e, occurrences: 0, message: `${which}old_string must not be empty` };
|
|
169
|
-
const exact = occurrences(text, spec.old);
|
|
170
|
-
if (exact.length > 1 && !spec.all) {
|
|
171
|
-
return { ok: false, code: "ambiguous_edit", edit: e, occurrences: exact.length,
|
|
172
|
-
message: `${which}old_string appears ${exact.length} times; add surrounding lines to make it unique or pass replace_all` };
|
|
173
|
-
}
|
|
174
|
-
if (exact.length > 0) {
|
|
175
|
-
for (const at of exact) spans.push({ start: at, end: at + spec.old.length, text: spec.next, edit: e });
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
const loose = looseSpans(text, spec, e);
|
|
179
|
-
if ("spans" in loose) { spans.push(...loose.spans); fuzzy = true; continue; }
|
|
180
|
-
if (loose.count > 1) {
|
|
181
|
-
return { ok: false, code: "ambiguous_edit", edit: e, occurrences: loose.count,
|
|
182
|
-
message: `${which}old_string has no exact match and ${loose.count} matches ignoring whitespace and quote style; add surrounding lines to make it unique` };
|
|
183
|
-
}
|
|
184
|
-
const closest = closestMatch(text, spec.old);
|
|
185
|
-
return { ok: false, code: "not_found", edit: e, occurrences: 0,
|
|
186
|
-
message: `${which}old_string not found` + (closest === undefined ? "; read the current file before retrying" : ""),
|
|
187
|
-
...(closest === undefined ? {} : { closest }) };
|
|
188
|
-
}
|
|
189
|
-
spans.sort((a, b) => a.start - b.start);
|
|
190
|
-
for (let i = 1; i < spans.length; i++) {
|
|
191
|
-
if (spans[i]!.start < spans[i - 1]!.end) {
|
|
192
|
-
return { ok: false, code: "invalid_input", edit: spans[i]!.edit, occurrences: 0,
|
|
193
|
-
message: `edits ${spans[i - 1]!.edit + 1} and ${spans[i]!.edit + 1} overlap; merge them into one edit` };
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
let after = text;
|
|
197
|
-
for (let i = spans.length - 1; i >= 0; i--) after = after.slice(0, spans[i]!.start) + spans[i]!.text + after.slice(spans[i]!.end);
|
|
198
|
-
if (after === text) return { ok: false, code: "invalid_input", edit: 0, occurrences: 0, message: "no change: new_string produces the current content" };
|
|
199
|
-
return { ok: true, after: fromView(view, after), replacements: spans.length, fuzzy, ...lineDiff(text, after) };
|
|
200
|
-
}
|
package/src/core/environment.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import type { ArtifactRef, LogEvent } from "./events.ts";
|
|
2
|
-
import { isObservationArtifact } from "./observation-view.ts";
|
|
3
|
-
import { workspaceChanged } from "./checkpoints.ts";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Facts about the machine, fixed at session start.
|
|
7
|
-
*
|
|
8
|
-
* Without them the model spends its first requests discovering what one
|
|
9
|
-
* `uname` / `pwd` / `git status` would have told it (Meta-Harness and
|
|
10
|
-
* LangChain's LocalContext middleware inject the same snapshot for that
|
|
11
|
-
* reason). They go into the frozen prefix, which a resumed session reads
|
|
12
|
-
* back from its saved context rather than rebuilding, so a date that has
|
|
13
|
-
* moved on by the next day does not change the cached prefix.
|
|
14
|
-
*/
|
|
15
|
-
export interface SessionEnvironment {
|
|
16
|
-
platform: string;
|
|
17
|
-
shell: string;
|
|
18
|
-
cwd: string;
|
|
19
|
-
/** Local calendar date, YYYY-MM-DD. */
|
|
20
|
-
date: string;
|
|
21
|
-
git: boolean;
|
|
22
|
-
/**
|
|
23
|
-
* Whether `rg` resolves in tool shells at session start. Models reach for
|
|
24
|
-
* it by habit; without this line a missing one costs an exit-127 request.
|
|
25
|
-
* Optional so environments recorded before it existed still render.
|
|
26
|
-
*/
|
|
27
|
-
ripgrep?: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function environmentSection(env: SessionEnvironment): string {
|
|
31
|
-
return [
|
|
32
|
-
"Environment (recorded at session start):",
|
|
33
|
-
`- Platform: ${env.platform}`,
|
|
34
|
-
`- Shell: ${env.shell}`,
|
|
35
|
-
`- Working directory: ${env.cwd}`,
|
|
36
|
-
`- Date: ${env.date}`,
|
|
37
|
-
`- Git repository: ${env.git ? "yes" : "no"}`,
|
|
38
|
-
...(env.ripgrep === undefined ? [] : [`- ripgrep (rg) in the shell: ${env.ripgrep ? "yes" : "no — use the grep and find tools"}`]),
|
|
39
|
-
].join("\n");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** R6: these are operational assumptions, not measured freshness guarantees. */
|
|
43
|
-
export const ENVIRONMENT_TTL_MS = 5 * 60_000;
|
|
44
|
-
export const ENVIRONMENT_TIMEOUT_MS = 5_000;
|
|
45
|
-
export const ENVIRONMENT_OUTPUT_BYTES = 256 * 1024;
|
|
46
|
-
export const ENVIRONMENT_PARSE_BYTES = 64 * 1024;
|
|
47
|
-
/** Version reports are metadata; verbose command output remains in the original artifact. */
|
|
48
|
-
export const ENVIRONMENT_VERSION_BYTES = 4096;
|
|
49
|
-
export type EnvironmentProbe = "version" | "python";
|
|
50
|
-
export type EnvironmentSource = { host: string; workspace: string; cwd: string; runtime: string; inherited_sha256: string };
|
|
51
|
-
export type PythonCapability = { executable: string; prefix: string; base_prefix: string; version: string;
|
|
52
|
-
implementation: string; isolated: true; features: { tarfile_extractall_filter: boolean } };
|
|
53
|
-
export type EnvironmentReceipt = {
|
|
54
|
-
kind: "environment"; version: 1;
|
|
55
|
-
query: { program: string; probe: EnvironmentProbe };
|
|
56
|
-
source: EnvironmentSource;
|
|
57
|
-
observed_at: number; expires_at: number;
|
|
58
|
-
state: "observed" | "missing" | "opaque" | "failed" | "unknown";
|
|
59
|
-
shell: { executable: string; version: string; cwd: string; path_sha256: string } | null;
|
|
60
|
-
resolution: { kind: string; executable: string | null } | null;
|
|
61
|
-
reported_version: string | null;
|
|
62
|
-
python?: PythonCapability;
|
|
63
|
-
evidence: { output: ArtifactRef; exit: number; complete: boolean };
|
|
64
|
-
reason?: string;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const quote = (text: string) => "'" + text.replaceAll("'", "'\\''") + "'";
|
|
68
|
-
const marker = "\0XENO_ENVIRONMENT_V1\0";
|
|
69
|
-
const PYTHON_CAPABILITY_PROBE = [
|
|
70
|
-
"import inspect,json,platform,sys,tarfile",
|
|
71
|
-
"print(json.dumps({'executable':sys.executable,'prefix':sys.prefix,'base_prefix':sys.base_prefix,'version':platform.python_version(),'implementation':platform.python_implementation(),'isolated':bool(sys.flags.isolated),'features':{'tarfile_extractall_filter':'filter' in inspect.signature(tarfile.TarFile.extractall).parameters}},sort_keys=True))",
|
|
72
|
-
].join(";");
|
|
73
|
-
|
|
74
|
-
/** Program names are data, including paths with quotes/newlines; no caller code is evaluated. */
|
|
75
|
-
export function environmentProbeCommand(program: unknown, probe: unknown = "version"): string {
|
|
76
|
-
if (typeof program !== "string" || !program || program.includes("\0") || new TextEncoder().encode(program).length > 4096) throw new Error("program must be a nonempty name or path up to 4096 bytes without NUL");
|
|
77
|
-
if (probe !== "version" && probe !== "python") throw new Error("probe must be version or python");
|
|
78
|
-
return [
|
|
79
|
-
"builtin set +e", `xeno_probe_program=${quote(program)}`,
|
|
80
|
-
'xeno_probe_kind=$(builtin type -t -- "$xeno_probe_program" 2>/dev/null)',
|
|
81
|
-
'xeno_probe_path=""',
|
|
82
|
-
'if [ "$xeno_probe_kind" = file ]; then xeno_probe_path=$(builtin type -P -- "$xeno_probe_program" 2>/dev/null); fi',
|
|
83
|
-
// A single frame precedes the program output, which remains untrusted data.
|
|
84
|
-
`builtin printf '\\0XENO_ENVIRONMENT_V1\\0%s\\0%s\\0%s\\0%s\\0%s\\0%s\\0%s\\0%s\\0' "$xeno_probe_program" ${quote(probe)} "$xeno_probe_kind" "$xeno_probe_path" "$BASH" "$BASH_VERSION" "$PWD" "$PATH"`,
|
|
85
|
-
'if [ -z "$xeno_probe_kind" ]; then builtin exit 127; fi',
|
|
86
|
-
'if [ "$xeno_probe_kind" != file ] || [ -z "$xeno_probe_path" ]; then builtin exit 125; fi',
|
|
87
|
-
// Replace the discovery shell: the managed group leader and exit belong
|
|
88
|
-
// to the probed program, not an extra shell waiting for its child (R1).
|
|
89
|
-
probe === "python" ? `builtin exec -- "$xeno_probe_path" -I -c ${quote(PYTHON_CAPABILITY_PROBE)}` : 'builtin exec -- "$xeno_probe_path" --version',
|
|
90
|
-
].join("\n");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface EnvironmentFrame { program: string; probe: EnvironmentProbe; kind: string; executable: string | null;
|
|
94
|
-
shell: string; shell_version: string; cwd: string; path: string; output: string }
|
|
95
|
-
export function parseEnvironmentFrame(text: string, program: string, probe: EnvironmentProbe): EnvironmentFrame | null {
|
|
96
|
-
const start = text.indexOf(marker);
|
|
97
|
-
if (start < 0) return null;
|
|
98
|
-
const fields: string[] = [];
|
|
99
|
-
let at = start + marker.length;
|
|
100
|
-
for (let i = 0; i < 8; i++) {
|
|
101
|
-
const end = text.indexOf("\0", at);
|
|
102
|
-
if (end < 0) return null;
|
|
103
|
-
fields.push(text.slice(at, end)); at = end + 1;
|
|
104
|
-
}
|
|
105
|
-
if (fields[0] !== program || fields[1] !== probe || !["", "file", "function", "alias", "builtin", "keyword"].includes(fields[2]!) ||
|
|
106
|
-
!fields[4] || !fields[5] || !fields[6] || (fields[2] === "file" && !fields[3])) return null;
|
|
107
|
-
return { program, probe, kind: fields[2]!, executable: fields[3] || null, shell: fields[4]!, shell_version: fields[5]!, cwd: fields[6]!, path: fields[7]!, output: text.slice(at) };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const object = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
111
|
-
const digest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
112
|
-
const strings = (value: Record<string, unknown>, keys: readonly string[]) => keys.every(key => typeof value[key] === "string" && value[key] !== "");
|
|
113
|
-
export function isPythonCapability(value: unknown): value is PythonCapability {
|
|
114
|
-
return object(value) && strings(value, ["executable", "prefix", "base_prefix", "version", "implementation"]) && value["isolated"] === true &&
|
|
115
|
-
object(value["features"]) && typeof value["features"]["tarfile_extractall_filter"] === "boolean";
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** D11: neither a version-looking line nor an interrupted exit zero proves a capability. */
|
|
119
|
-
export function environmentCapability(frame: EnvironmentFrame | null, exit: number, capture: {
|
|
120
|
-
complete: boolean; stopped?: "cancelled" | "timeout"; limited?: "output" | "parse";
|
|
121
|
-
}): Pick<EnvironmentReceipt, "state" | "reported_version" | "python" | "reason"> {
|
|
122
|
-
const unknown = (reason: string) => ({ state: "unknown" as const, reported_version: null, reason });
|
|
123
|
-
if (capture.limited) return unknown(`probe exceeded its ${capture.limited} budget; inspect the original output`);
|
|
124
|
-
if (capture.stopped) return unknown(`probe ${capture.stopped}; inspect partial effects before continuing`);
|
|
125
|
-
if (!capture.complete || !frame) return unknown("probe output is incomplete or its environment frame is invalid");
|
|
126
|
-
if (frame.kind === "" && exit === 127) return { state: "missing", reported_version: null, reason: "program was not found in the command environment" };
|
|
127
|
-
if (frame.kind !== "file") return { state: "opaque", reported_version: null, reason: "shell resolution is not an executable file; specify its exact path" };
|
|
128
|
-
if (exit !== 0) return { state: "failed", reported_version: null, reason: "program exited unsuccessfully; a version-like line is not capability evidence" };
|
|
129
|
-
const output = frame.output.trim();
|
|
130
|
-
if (!output || new TextEncoder().encode(output).length > ENVIRONMENT_VERSION_BYTES) return unknown("version report is empty or exceeds the metadata budget");
|
|
131
|
-
if (frame.probe === "version") return { state: "observed", reported_version: output };
|
|
132
|
-
try {
|
|
133
|
-
const value: unknown = JSON.parse(output);
|
|
134
|
-
if (isPythonCapability(value)) return { state: "observed", reported_version: value.version, python: value };
|
|
135
|
-
} catch { /* The complete untrusted report remains available in the artifact. */ }
|
|
136
|
-
return unknown("Python did not return a valid isolated capability report");
|
|
137
|
-
}
|
|
138
|
-
export function isEnvironmentReceipt(value: unknown): value is EnvironmentReceipt {
|
|
139
|
-
if (!object(value) || value["kind"] !== "environment" || value["version"] !== 1 || !object(value["query"]) ||
|
|
140
|
-
!strings(value["query"], ["program"]) || !["version", "python"].includes(String(value["query"]["probe"]))) return false;
|
|
141
|
-
const source = value["source"], evidence = value["evidence"], shell = value["shell"], resolution = value["resolution"];
|
|
142
|
-
if (!object(source) || !strings(source, ["host", "workspace", "cwd", "runtime"]) || !digest(source["inherited_sha256"]) ||
|
|
143
|
-
!object(evidence) || !isObservationArtifact(evidence["output"]) || !Number.isInteger(evidence["exit"]) || typeof evidence["complete"] !== "boolean") return false;
|
|
144
|
-
const observed = value["observed_at"], expires = value["expires_at"];
|
|
145
|
-
if (typeof observed !== "number" || !Number.isSafeInteger(observed) || observed < 0 || expires !== observed + ENVIRONMENT_TTL_MS ||
|
|
146
|
-
!["observed", "missing", "opaque", "failed", "unknown"].includes(String(value["state"])) ||
|
|
147
|
-
!(value["reported_version"] === null || typeof value["reported_version"] === "string") ||
|
|
148
|
-
!(value["reason"] === undefined || typeof value["reason"] === "string")) return false;
|
|
149
|
-
if (shell !== null && (!object(shell) || !strings(shell, ["executable", "version", "cwd"]) || !digest(shell["path_sha256"]))) return false;
|
|
150
|
-
if (resolution !== null && (!object(resolution) || typeof resolution["kind"] !== "string" ||
|
|
151
|
-
!(resolution["executable"] === null || typeof resolution["executable"] === "string"))) return false;
|
|
152
|
-
if (value["python"] !== undefined && !isPythonCapability(value["python"])) return false;
|
|
153
|
-
if (value["state"] === "observed" && (evidence["exit"] !== 0 || !evidence["complete"] || !object(shell) || !object(resolution) ||
|
|
154
|
-
resolution["kind"] !== "file" || !resolution["executable"] || !value["reported_version"] ||
|
|
155
|
-
value["query"]["probe"] === "python" && value["python"] === undefined)) return false;
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export type EnvironmentValidity = { state: "recorded" | "expired" | "scope_changed" | "invalidated" | "unconfirmed";
|
|
160
|
-
reason: string; event?: string };
|
|
161
|
-
/** Recomputed on consumption: recent observations are never guarantees about a future invocation. */
|
|
162
|
-
export function environmentValidity(receipt: EnvironmentReceipt, source: EnvironmentSource, now: number, after: readonly LogEvent[]): EnvironmentValidity {
|
|
163
|
-
for (const event of after) {
|
|
164
|
-
if (event.t === "user_bash" || workspaceChanged(event)) {
|
|
165
|
-
return { state: "invalidated", reason: "later command or workspace change requires environment revalidation", event: event.id };
|
|
166
|
-
}
|
|
167
|
-
if (event.t !== "tool_result") continue;
|
|
168
|
-
if (isEnvironmentReceipt(event.data) && event.data.query.program === receipt.query.program && event.data.query.probe === receipt.query.probe) {
|
|
169
|
-
return { state: "invalidated", reason: "a later probe supersedes this observation, including a failed probe", event: event.id };
|
|
170
|
-
}
|
|
171
|
-
if (event.effect?.kind === "edit" || event.effect?.kind === "command" || event.diagnostic?.code === "environment_missing" ||
|
|
172
|
-
event.diagnostic?.effects === "possible" && event.diagnostic.execution !== "not_started") {
|
|
173
|
-
return { state: "invalidated", reason: "later execution or mutation requires environment revalidation", event: event.id };
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
const keys: readonly (keyof EnvironmentSource)[] = ["host", "workspace", "cwd", "runtime", "inherited_sha256"];
|
|
177
|
-
if (keys.some(key => source[key] !== receipt.source[key])) return { state: "scope_changed", reason: "host, workspace, directory, runtime, or inherited environment changed" };
|
|
178
|
-
if (!Number.isFinite(now) || now < receipt.observed_at || now >= receipt.expires_at) return { state: "expired", reason: "observation time is outside its validity interval" };
|
|
179
|
-
if (receipt.state !== "observed") return { state: "unconfirmed", reason: "the probe did not establish this capability" };
|
|
180
|
-
return { state: "recorded", reason: "recent observation only; login startup files or external changes may require another probe" };
|
|
181
|
-
}
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
/** D8/D11: malformed durable data is corruption, never a valid empty event. */
|
|
2
|
-
import type { LogEvent, NodeRef } from "./events.ts";
|
|
3
|
-
import { isExecutionOutcome } from "./outcome.ts";
|
|
4
|
-
import { isJsonValue, isToolDiagnostic } from "./tool-contract.ts";
|
|
5
|
-
import { validImageRef } from "./images.ts";
|
|
6
|
-
import { parseAcceptanceContract } from "./acceptance.ts";
|
|
7
|
-
import { validFileChanges } from "./checkpoints.ts";
|
|
8
|
-
import { isObservationReceipt } from "./read-observation.ts";
|
|
9
|
-
import { isSearchReceipt } from "./search-observation.ts";
|
|
10
|
-
import { isProcessReceipt } from "./process-receipt.ts";
|
|
11
|
-
import { isObservationArtifact } from "./observation-view.ts";
|
|
12
|
-
import { isCommandArtifactReceipt } from "./command-artifacts.ts";
|
|
13
|
-
import { isRuntimeTargetReceipt } from "./runtime-target.ts";
|
|
14
|
-
|
|
15
|
-
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
16
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
17
|
-
}
|
|
18
|
-
const strings = (value: unknown): boolean => Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
19
|
-
const optionalString = (value: unknown): boolean => value === undefined || typeof value === "string";
|
|
20
|
-
const natural = (value: unknown): boolean => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
21
|
-
const eventRef = (value: unknown): boolean => isRecord(value) && natural(value["node"]) && typeof value["event"] === "string" && value["event"] !== "";
|
|
22
|
-
export function validNodeRef(value: unknown): value is NodeRef {
|
|
23
|
-
return isRecord(value) && Number.isSafeInteger(value["node"]) && Number(value["node"]) >= 0 &&
|
|
24
|
-
typeof value["log_ref"] === "string" && typeof value["sha256"] === "string" &&
|
|
25
|
-
(value["at_event"] === undefined || typeof value["at_event"] === "string");
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** A label is a short phrase or class name; anything longer or with control characters is not one the harness wrote. */
|
|
29
|
-
function validFailureCause(value: unknown): boolean {
|
|
30
|
-
return isRecord(value) && (value["source"] === "provider" || value["source"] === "harness") &&
|
|
31
|
-
typeof value["label"] === "string" && /^[^\u0000-\u001f\u007f]{1,64}$/.test(value["label"]) &&
|
|
32
|
-
(value["status"] === undefined || Number.isSafeInteger(value["status"])) &&
|
|
33
|
-
(value["retries"] === undefined || natural(value["retries"]));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function isLogEvent(value: unknown): value is LogEvent {
|
|
37
|
-
if (!isRecord(value) || typeof value["id"] !== "string" || value["id"].length === 0 || typeof value["at"] !== "string") return false;
|
|
38
|
-
if (value["outcome"] !== undefined && !isExecutionOutcome(value["outcome"])) return false;
|
|
39
|
-
if (value["work_id"] !== undefined && !(typeof value["work_id"] === "string" && value["work_id"].trim().length > 0)) return false;
|
|
40
|
-
const carried = value["carried_from"];
|
|
41
|
-
if (carried !== undefined && !(isRecord(carried) && natural(carried["node"]) && typeof carried["event"] === "string")) return false;
|
|
42
|
-
const has = (...keys: string[]): boolean => keys.every((key) => typeof value[key] === "string");
|
|
43
|
-
const images = value["images"];
|
|
44
|
-
if (images !== undefined && (!Array.isArray(images) || !images.every(validImageRef))) return false;
|
|
45
|
-
switch (value["t"]) {
|
|
46
|
-
case "tool_invocation": return has("call_id", "name") && value["call_id"] !== "" && value["name"] !== "" &&
|
|
47
|
-
(value["origin"] === undefined || value["origin"] === "extension") &&
|
|
48
|
-
isObservationArtifact(value["input_ref"]) && (value["schema_ref"] === null || isObservationArtifact(value["schema_ref"])) &&
|
|
49
|
-
(value["input"] === undefined || isJsonValue(value["input"])) && optionalString(value["parent_call_id"]) &&
|
|
50
|
-
(value["parent_ref"] === undefined ? value["parent_call_id"] === undefined : eventRef(value["parent_ref"]) && value["parent_call_id"] !== undefined) &&
|
|
51
|
-
value["parent_call_id"] !== "" && value["parent_call_id"] !== value["call_id"];
|
|
52
|
-
case "checkpoint": return has("operation") && ["edit", "git", "restore"].includes(String(value["source"])) &&
|
|
53
|
-
["prepared", "applied", "unavailable"].includes(String(value["phase"])) && validFileChanges(value["changes"]) &&
|
|
54
|
-
optionalString(value["tree"]) && optionalString(value["reason"]);
|
|
55
|
-
case "workspace_observation": return value["scope"] === "source-v1" && typeof value["changed"] === "boolean" && optionalString(value["reason"]) &&
|
|
56
|
-
(value["baseline"] === undefined || value["baseline"] === true) &&
|
|
57
|
-
(value["fingerprint"] === undefined || typeof value["fingerprint"] === "string" && /^[a-f0-9]{64}$/.test(value["fingerprint"]));
|
|
58
|
-
case "process_exit": {
|
|
59
|
-
const output = value["output"];
|
|
60
|
-
const receipt = value["receipt"];
|
|
61
|
-
if (receipt !== undefined && (!isProcessReceipt(receipt) || receipt.replayed || receipt.running !== false ||
|
|
62
|
-
receipt.session_id !== value["session"] || receipt.exit !== value["exit"] || !isRecord(output) ||
|
|
63
|
-
receipt.artifacts !== undefined && receipt.artifacts.command !== value["command"] ||
|
|
64
|
-
receipt.output === undefined || receipt.output.sha256 !== output["sha256"] || receipt.output.bytes !== output["bytes"])) return false;
|
|
65
|
-
return has("command") && natural(value["session"]) && Number(value["session"]) > 0 && Number.isInteger(value["exit"]) &&
|
|
66
|
-
value["reason"] === "disposed" && isRecord(output) && typeof output["path"] === "string" &&
|
|
67
|
-
typeof output["sha256"] === "string" && /^[a-f0-9]{64}$/.test(output["sha256"]) && natural(output["bytes"]);
|
|
68
|
-
}
|
|
69
|
-
case "approval": {
|
|
70
|
-
const user = ["once", "command", "tool", "deny"].includes(String(value["decision"]));
|
|
71
|
-
const rule = value["decision"] === "allow" || value["decision"] === "deny";
|
|
72
|
-
return has("call_id", "tool") && value["call_id"] !== "" && value["tool"] !== "" &&
|
|
73
|
-
optionalString(value["command"]) && optionalString(value["rule"]) &&
|
|
74
|
-
(value["source"] === "user" ? user : value["source"] === "no_prompt" ? value["decision"] === "deny" :
|
|
75
|
-
["session_rule", "config", "auto"].includes(String(value["source"])) && rule);
|
|
76
|
-
}
|
|
77
|
-
case "skill_loaded": return has("name", "path", "sha256", "artifact") &&
|
|
78
|
-
["sha256", "artifact"].every(key => /^[a-f0-9]{64}$/.test(String(value[key]))) &&
|
|
79
|
-
["inline", "fork", "read"].includes(String(value["mode"])) && ["user", "model"].includes(String(value["origin"]));
|
|
80
|
-
case "acceptance_contract": {
|
|
81
|
-
try { parseAcceptanceContract(value["contract"]); return has("request"); } catch { return false; }
|
|
82
|
-
}
|
|
83
|
-
case "acceptance_decision": return has("request", "contract_id", "criterion") && value["actor"] === "user" &&
|
|
84
|
-
(value["target_binding"] === undefined ? value["target_observation"] === undefined : has("target_binding", "target_observation"));
|
|
85
|
-
case "target_observation": return isRuntimeTargetReceipt(value["observation"]);
|
|
86
|
-
case "counter_check": return has("request", "criterion", "command") && (value["guard"] === undefined || value["guard"] === true) &&
|
|
87
|
-
(Number.isSafeInteger(value["exit"]) ? value["reason"] === undefined : typeof value["reason"] === "string" && value["exit"] === undefined);
|
|
88
|
-
case "blind_check": return has("request", "check") && typeof value["exposed"] === "boolean" && optionalString(value["exposure"]) &&
|
|
89
|
-
(Number.isSafeInteger(value["exit"]) ? value["reason"] === undefined : typeof value["reason"] === "string" && value["exit"] === undefined);
|
|
90
|
-
case "goal": {
|
|
91
|
-
if (value["action"] === "clear") return value["reason"] === "met" || value["reason"] === "user";
|
|
92
|
-
if (value["action"] !== "set" || !has("objective", "draft") || !strings(value["accepted"]) || !strings(value["rejected"])) return false;
|
|
93
|
-
try {
|
|
94
|
-
const contract = parseAcceptanceContract(value["contract"]), accepted = value["accepted"] as string[];
|
|
95
|
-
return accepted.length > 0 && contract.criteria.map(item => item.id).join("\0") === accepted.join("\0");
|
|
96
|
-
} catch { return false; }
|
|
97
|
-
}
|
|
98
|
-
case "extension": return has("customType", "content") && typeof value["display"] === "boolean" && typeof value["inContext"] === "boolean" && (value["data"] === undefined || isJsonValue(value["data"]));
|
|
99
|
-
case "user": return has("text") && (value["request"] === undefined || typeof value["request"] === "string" && value["request"] !== "");
|
|
100
|
-
case "outcome": return has("reason") && isExecutionOutcome(value["outcome"]) && (value["failure"] === undefined || validFailureCause(value["failure"]));
|
|
101
|
-
case "user_bash": {
|
|
102
|
-
const ref = value["content_ref"];
|
|
103
|
-
const outcome = value["outcome"];
|
|
104
|
-
if (!isExecutionOutcome(outcome) || !["tool_completed", "tool_failed", "cancelled", "timeout"].includes(outcome.reason)) return false;
|
|
105
|
-
if (outcome.reason === "tool_completed" && value["exit"] !== 0) return false;
|
|
106
|
-
if (outcome.reason === "tool_failed" && value["exit"] === 0) return false;
|
|
107
|
-
return has("command", "content") && Number.isInteger(value["exit"]) && typeof value["excludeFromContext"] === "boolean" &&
|
|
108
|
-
isRecord(ref) && typeof ref["sha256"] === "string" &&
|
|
109
|
-
typeof ref["path"] === "string" && natural(ref["bytes"]);
|
|
110
|
-
}
|
|
111
|
-
case "guidance": return has("text") && ["continuation", "repetition", "budget", "boundary", "stall", "output_recovery", "completion_review", "churn", "deadline", "runtime_acceptance", "goal", "secret", "branch"].includes(String(value["reason"])) && strings(value["sources"]);
|
|
112
|
-
case "interrupt": return has("user_id");
|
|
113
|
-
case "assistant": return (value["stop_reason"] === null || optionalString(value["stop_reason"])) &&
|
|
114
|
-
(value["replay_key"] === undefined || typeof value["replay_key"] === "string" && /^[a-f0-9]{64}$/.test(value["replay_key"])) &&
|
|
115
|
-
Array.isArray(value["blocks"]) && value["blocks"].every((b: unknown) => {
|
|
116
|
-
if (!isRecord(b)) return false;
|
|
117
|
-
if (b["type"] === "text") return typeof b["text"] === "string";
|
|
118
|
-
if (b["type"] === "thinking") return typeof b["text"] === "string" && "raw" in b;
|
|
119
|
-
return b["type"] === "tool_use" && typeof b["call_id"] === "string" && typeof b["name"] === "string" && "input" in b;
|
|
120
|
-
});
|
|
121
|
-
case "tool_result": {
|
|
122
|
-
if (!optionalString(value["parent_call_id"]) || value["parent_call_id"] === "" || value["parent_call_id"] === value["call_id"]) return false;
|
|
123
|
-
if (value["origin"] !== undefined && value["origin"] !== "extension") return false;
|
|
124
|
-
if (value["invocation_ref"] !== undefined && !eventRef(value["invocation_ref"])) return false;
|
|
125
|
-
const observation = value["observation"];
|
|
126
|
-
if (observation !== undefined && (!isObservationReceipt(observation) ||
|
|
127
|
-
!isRecord(value["content_ref"]) || value["content_ref"]["sha256"] !== observation.artifact.sha256 ||
|
|
128
|
-
value["content_ref"]["bytes"] !== observation.artifact.bytes || value["content_sha256"] !== observation.artifact.sha256)) return false;
|
|
129
|
-
if (value["data"] !== undefined && !isJsonValue(value["data"])) return false;
|
|
130
|
-
if (value["data_ref"] !== undefined && (!isObservationArtifact(value["data_ref"]) || value["data"] !== undefined)) return false;
|
|
131
|
-
if (isRecord(value["data"]) && value["data"]["kind"] === "process") {
|
|
132
|
-
const process = value["data"];
|
|
133
|
-
if (!isProcessReceipt(process)) return false;
|
|
134
|
-
if (process.output && (!isRecord(value["content_ref"]) || value["content_ref"]["sha256"] !== process.output.sha256 ||
|
|
135
|
-
value["content_ref"]["bytes"] !== process.output.bytes || value["content_sha256"] !== process.output.sha256)) return false;
|
|
136
|
-
if (process.replayed && value["effect"] !== undefined) return false;
|
|
137
|
-
if (value["effect"] !== undefined && (!isRecord(value["effect"]) || value["effect"]["kind"] !== "command" ||
|
|
138
|
-
process.running !== false || value["effect"]["exit"] !== process.exit ||
|
|
139
|
-
JSON.stringify(value["effect"]["artifacts"]) !== JSON.stringify(process.artifacts))) return false;
|
|
140
|
-
}
|
|
141
|
-
if (isObservationReceipt(observation) && observation.descriptor !== undefined &&
|
|
142
|
-
(!isRecord(value["data"]) || value["data"]["read_handle"] !== observation.descriptor.sha256)) return false;
|
|
143
|
-
if (isRecord(value["data"]) && value["data"]["kind"] === "search") {
|
|
144
|
-
const search = value["data"];
|
|
145
|
-
if (!isSearchReceipt(search) || !isRecord(value["content_ref"]) || value["content_ref"]["sha256"] !== search.artifact.sha256 ||
|
|
146
|
-
value["content_ref"]["bytes"] !== search.artifact.bytes || value["content_sha256"] !== search.artifact.sha256) return false;
|
|
147
|
-
}
|
|
148
|
-
if (value["diagnostic"] !== undefined && !isToolDiagnostic(value["diagnostic"])) return false;
|
|
149
|
-
if (value["warnings"] !== undefined && (!Array.isArray(value["warnings"]) || !value["warnings"].every(isToolDiagnostic))) return false;
|
|
150
|
-
const instructions = value["instructions"];
|
|
151
|
-
if (instructions !== undefined && (!isRecord(instructions) || !Array.isArray(instructions["keys"]) || instructions["keys"].length === 0 ||
|
|
152
|
-
!instructions["keys"].every(key => typeof key === "string" && key.length > 0) ||
|
|
153
|
-
typeof instructions["text"] !== "string" || instructions["text"].length === 0)) return false;
|
|
154
|
-
if (!has("call_id", "content") || typeof value["ok"] !== "boolean") return false;
|
|
155
|
-
if (!optionalString(value["content_sha256"])) return false;
|
|
156
|
-
if (!optionalString(value["observation_sha256"])) return false;
|
|
157
|
-
const artifact = value["content_ref"];
|
|
158
|
-
if (artifact !== undefined && !(isRecord(artifact) && typeof artifact["sha256"] === "string" &&
|
|
159
|
-
typeof artifact["path"] === "string" && natural(artifact["bytes"]))) return false;
|
|
160
|
-
const effect = value["effect"];
|
|
161
|
-
if (effect === undefined) return true;
|
|
162
|
-
if (!isRecord(effect)) return false;
|
|
163
|
-
if (effect["kind"] === "command") return typeof effect["cmd"] === "string" && Number.isInteger(effect["exit"]) &&
|
|
164
|
-
optionalString(effect["gate"]) && optionalString(effect["workspace_sha256"]) &&
|
|
165
|
-
(effect["artifacts"] === undefined || isCommandArtifactReceipt(effect["artifacts"]) &&
|
|
166
|
-
effect["artifacts"].exit === effect["exit"] && effect["artifacts"].command === effect["cmd"]) &&
|
|
167
|
-
(effect["workspace_scope"] === undefined || effect["workspace_scope"] === "source-v1") &&
|
|
168
|
-
(effect["workspace_observed"] === undefined || typeof effect["workspace_observed"] === "boolean");
|
|
169
|
-
if (effect["kind"] === "read") return typeof effect["path"] === "string" && typeof effect["sha256"] === "string" &&
|
|
170
|
-
(effect["ranges"] === undefined || (Array.isArray(effect["ranges"]) && effect["ranges"].every((r: unknown) =>
|
|
171
|
-
Array.isArray(r) && r.length === 2 && r.every((n) => Number.isSafeInteger(n)) && r[0] >= 1 && r[1] >= r[0])));
|
|
172
|
-
return effect["kind"] === "edit" && typeof effect["path"] === "string" && typeof effect["sha256_after"] === "string" &&
|
|
173
|
-
(effect["sha256_before"] === null || typeof effect["sha256_before"] === "string");
|
|
174
|
-
}
|
|
175
|
-
case "cut": return has("handoff_ref") && (value["working_set_version"] === undefined || value["working_set_version"] === 2 || value["working_set_version"] === 3) && ["trigger", "hard_limit", "model_checkpoint"].includes(String(value["reason"])) &&
|
|
176
|
-
(value["guidance_tools"] === undefined || Array.isArray(value["guidance_tools"]) && value["guidance_tools"].every(item => typeof item === "string")) &&
|
|
177
|
-
optionalString(value["retained_from"]) && optionalString(value["next_context_ref"]) &&
|
|
178
|
-
(value["next_node"] === undefined || natural(value["next_node"])) && optionalString(value["next_model"]) &&
|
|
179
|
-
(value["handoff_tokens"] === undefined || (typeof value["handoff_tokens"] === "number" && Number.isFinite(value["handoff_tokens"]) && value["handoff_tokens"] > 0)) &&
|
|
180
|
-
(value["predecessor"] === null || validNodeRef(value["predecessor"]));
|
|
181
|
-
case "claim": {
|
|
182
|
-
const q = value["evidence"];
|
|
183
|
-
return has("what") && isRecord(q) && (typeof q["gate"] === "string" || typeof q["command"] === "string");
|
|
184
|
-
}
|
|
185
|
-
case "decision": return has("chose", "because") && (value["rejected"] === undefined || strings(value["rejected"]));
|
|
186
|
-
case "pitfall": return has("tried", "failed_because");
|
|
187
|
-
case "blocked": return has("what", "reason");
|
|
188
|
-
case "question": return has("text");
|
|
189
|
-
case "next_action": return has("what", "next_action") && (value["depends_on"] === undefined || strings(value["depends_on"]));
|
|
190
|
-
case "work_dependencies": return has("work_id", "what") && strings(value["depends_on"]);
|
|
191
|
-
case "work_owner": return has("work_id", "what", "owner") && ["claim", "release"].includes(String(value["operation"]));
|
|
192
|
-
case "evidence_link": return has("work", "what", "source") && optionalString(value["resource"]) && optionalString(value["version"]) &&
|
|
193
|
-
optionalString(value["supersedes"]) && (value["observed_version"] === null || optionalString(value["observed_version"]));
|
|
194
|
-
case "work_status": return has("work_id", "what") && ["unblock", "reopen", "cancel"].includes(String(value["operation"])) &&
|
|
195
|
-
optionalString(value["reason"]) && (value["operation"] === "cancel" ||
|
|
196
|
-
(typeof value["next_action"] === "string" && value["next_action"].trim().length > 0));
|
|
197
|
-
case "rewind": return ["both", "files", "context"].includes(String(value["mode"])) && validNodeRef(value["from"]) &&
|
|
198
|
-
(value["files_from"] === undefined || validNodeRef(value["files_from"])) &&
|
|
199
|
-
has("prompt") && strings(value["restored"]) && strings(value["removed"]) && strings(value["unrecoverable"]) && strings(value["divergence"]);
|
|
200
|
-
case "spawn": {
|
|
201
|
-
const input = value["input"];
|
|
202
|
-
if (input !== undefined) {
|
|
203
|
-
if (!isRecord(input) || typeof input["source_event"] !== "string" || !strings(input["tools"]) ||
|
|
204
|
-
!Number.isSafeInteger(input["max_turns"]) || Number(input["max_turns"]) < 1 || !isRecord(input["workspace"])) return false;
|
|
205
|
-
const workspace = input["workspace"];
|
|
206
|
-
if (!(workspace["kind"] === "shared_readonly" || (workspace["kind"] === "git_tree" &&
|
|
207
|
-
typeof workspace["head"] === "string" && typeof workspace["tree"] === "string"))) return false;
|
|
208
|
-
}
|
|
209
|
-
return (value["background"] === undefined || value["background"] === true) && validNodeRef(value["branch"]) && has("purpose") && (value["workspace"] === undefined ||
|
|
210
|
-
(isRecord(value["workspace"]) && typeof value["workspace"]["path"] === "string" && typeof value["workspace"]["branch"] === "string"));
|
|
211
|
-
}
|
|
212
|
-
case "joined": return validNodeRef(value["branch"]) && has("handoff_ref", "summary") && optionalString(value["model"]) &&
|
|
213
|
-
(value["receipt_ref"] === undefined || typeof value["receipt_ref"] === "string" && /^[a-f0-9]{64}$/.test(value["receipt_ref"])) &&
|
|
214
|
-
(value["tip"] === undefined || validNodeRef(value["tip"]));
|
|
215
|
-
case "adopt": return validNodeRef(value["branch"]) && (value["next_node"] === undefined || natural(value["next_node"])) &&
|
|
216
|
-
(value["applied"] === undefined || value["applied"] === true);
|
|
217
|
-
case "discard": return validNodeRef(value["branch"]) && has("reason");
|
|
218
|
-
default: return false;
|
|
219
|
-
}
|
|
220
|
-
}
|