@visual-z/xeno 0.2.0 → 0.3.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 +15 -0
- package/README.md +106 -26
- package/docs/install.md +52 -9
- package/docs/providers.md +11 -0
- package/package.json +5 -4
- package/src/apps/approve.ts +3 -3
- package/src/apps/cli.ts +10 -11
- package/src/apps/commands.ts +38 -5
- package/src/apps/completions.ts +73 -19
- package/src/apps/image-draft.ts +46 -0
- package/src/apps/project-trust.ts +2 -42
- package/src/apps/provider-connect.ts +346 -0
- package/src/apps/render.ts +22 -47
- package/src/apps/resources.ts +5 -2
- package/src/apps/shell.ts +298 -1526
- package/src/apps/startup-input.ts +130 -0
- package/src/apps/startup.ts +143 -115
- package/src/apps/ui/app.ts +1383 -0
- package/src/apps/ui/extension-host.ts +194 -0
- package/src/apps/ui/image-cache.ts +78 -0
- package/src/apps/ui/palette.ts +88 -0
- package/src/apps/ui/project.ts +127 -0
- package/src/apps/ui/standalone.ts +146 -0
- package/src/apps/ui/status-model.ts +68 -0
- package/src/apps/ui/surface.ts +84 -0
- package/src/apps/ui/theme.ts +86 -0
- package/src/core/acceptance.ts +107 -0
- package/src/core/action-defaults.ts +47 -295
- package/src/core/benchmark.ts +2 -2
- package/src/core/branch-receipt.ts +24 -0
- package/src/core/checkpoints.ts +64 -0
- package/src/core/churn.ts +2 -0
- package/src/core/command-artifacts.ts +94 -0
- package/src/core/command-contract.ts +35 -0
- package/src/core/completion-review.ts +21 -5
- package/src/core/context-detail.ts +37 -0
- package/src/core/context-policy.ts +74 -1
- package/src/core/continuation.ts +17 -2
- package/src/core/derive.ts +29 -3
- package/src/core/draft-images.ts +56 -0
- package/src/core/environment.ts +145 -0
- package/src/core/evaluation-coverage.ts +74 -0
- package/src/core/event-validation.ts +80 -5
- package/src/core/events.ts +97 -3
- package/src/core/evidence-links.ts +7 -0
- package/src/core/evidence.ts +13 -3
- package/src/core/execution.ts +8 -2
- package/src/core/experiment.ts +133 -0
- package/src/core/export.ts +9 -0
- package/src/core/failure-recovery.ts +173 -0
- package/src/core/file-precondition.ts +72 -0
- package/src/core/handoff.ts +65 -4
- package/src/core/history-query.ts +38 -2
- package/src/core/history.ts +38 -1
- package/src/core/integration-health.ts +34 -0
- package/src/core/keymap.ts +8 -20
- package/src/core/model-picker.ts +64 -0
- package/src/core/native-eval.ts +25 -0
- package/src/core/note-guidance.ts +34 -0
- package/src/core/observation-view.ts +49 -8
- package/src/core/permissions.ts +24 -2
- package/src/core/process-receipt.ts +105 -0
- package/src/core/process-state.ts +37 -0
- package/src/core/project.ts +45 -18
- package/src/core/prompt-policy.ts +37 -0
- package/src/core/query-eval-contract.ts +14 -0
- package/src/core/read-observation.ts +82 -0
- package/src/core/recovery-corpus.ts +17 -0
- package/src/core/recovery-evaluation.ts +60 -0
- package/src/core/recovery-sources.ts +74 -0
- package/src/core/runtime-target.ts +94 -0
- package/src/core/search-observation.ts +97 -0
- package/src/core/session-tree.ts +147 -0
- package/src/core/settings.ts +26 -10
- package/src/core/skills.ts +44 -0
- package/src/core/sql-query.ts +211 -0
- package/src/core/theme.ts +36 -58
- package/src/core/tool-contract.ts +51 -2
- package/src/core/tool-discovery.ts +87 -0
- package/src/core/tool-failure.ts +60 -0
- package/src/core/tool-mode-protocol.ts +25 -0
- package/src/core/transcript.ts +19 -4
- package/src/core/trial-lifecycle.ts +14 -0
- package/src/core/tui-mode.ts +29 -0
- package/src/core/update-check.ts +97 -0
- package/src/core/version.ts +55 -0
- package/src/core/work-state.ts +49 -3
- package/src/core/working-set.ts +91 -0
- package/src/runtime/auth.ts +2 -2
- package/src/runtime/branch-jobs.ts +36 -0
- package/src/runtime/clipboard.ts +15 -4
- package/src/runtime/command-artifacts.ts +64 -0
- package/src/runtime/environment-tools.ts +60 -0
- package/src/runtime/evidence-notes.ts +4 -2
- package/src/runtime/exec-sessions.ts +206 -61
- package/src/runtime/execution-control.ts +1 -0
- package/src/runtime/extension-ui.ts +22 -30
- package/src/runtime/extensions.ts +95 -16
- package/src/runtime/harness-comparison.ts +27 -7
- package/src/runtime/harness-eval.ts +460 -0
- package/src/runtime/history-cache.ts +57 -0
- package/src/runtime/history-tools.ts +62 -12
- package/src/runtime/integrations.ts +210 -0
- package/src/runtime/loop.ts +1021 -268
- package/src/runtime/model-discovery.ts +208 -0
- package/src/runtime/native-eval-fixtures.ts +33 -0
- package/src/runtime/native-eval.ts +143 -0
- package/src/runtime/oauth.ts +1 -1
- package/src/runtime/provider-connect.ts +109 -0
- package/src/runtime/provider-presets.ts +87 -0
- package/src/runtime/query-eval-verifier.ts +28 -0
- package/src/runtime/recovery-coverage.ts +71 -0
- package/src/runtime/recovery-evidence.ts +76 -0
- package/src/runtime/recovery-fixtures.ts +116 -0
- package/src/runtime/runtime-target.ts +149 -0
- package/src/runtime/tool-broker.ts +137 -0
- package/src/runtime/tool-discovery.ts +57 -0
- package/src/runtime/tool-mode-eval.ts +146 -0
- package/src/runtime/tool-queue.ts +77 -0
- package/src/runtime/tools.ts +628 -183
- package/src/runtime/trial-control.ts +45 -0
- package/src/runtime/update-check.ts +47 -0
- package/src/runtime/window-trial.ts +2 -1
- package/src/runtime/working-notes.ts +33 -3
- package/src/runtime/workspace-lifecycle.ts +97 -0
- package/src/store/acceptance.ts +15 -0
- package/src/store/checkpoint-history.ts +43 -0
- package/src/store/command-artifacts.ts +96 -0
- package/src/store/command.ts +65 -37
- package/src/store/file-checkpoints.ts +104 -0
- package/src/store/file-index.ts +13 -1
- package/src/store/git-checkpoints.ts +67 -0
- package/src/store/observations.ts +61 -20
- package/src/store/process-receipts.ts +34 -0
- package/src/store/query-source.ts +13 -8
- package/src/store/recovery.ts +2 -3
- package/src/store/replay.ts +16 -4
- package/src/store/runtime-target.ts +64 -0
- package/src/store/search.ts +171 -132
- package/src/store/session-archive.ts +4 -0
- package/src/store/source-cancellation.ts +6 -0
- package/src/store/update-cache.ts +36 -0
- package/src/store/workspace-observation.ts +113 -0
- package/src/tui/ansi-spans.ts +126 -0
- package/src/tui/style.ts +19 -16
- package/src/tui/ui/atomic-draft.ts +121 -0
- package/src/tui/ui/canvas.ts +96 -0
- package/src/tui/ui/colors.ts +70 -0
- package/src/tui/ui/composer.ts +199 -0
- package/src/tui/ui/dialog.ts +379 -0
- package/src/tui/ui/draft.ts +23 -0
- package/src/tui/ui/edge.ts +186 -0
- package/src/tui/ui/image-size.ts +40 -0
- package/src/tui/ui/ink.ts +210 -0
- package/src/tui/ui/items.ts +40 -0
- package/src/tui/ui/keys.ts +40 -0
- package/src/tui/ui/layout.ts +74 -0
- package/src/tui/ui/list-model.ts +86 -0
- package/src/tui/ui/list-view.ts +172 -0
- package/src/tui/ui/mascot.ts +67 -0
- package/src/tui/ui/messages.ts +518 -0
- package/src/tui/ui/motion.ts +28 -0
- package/src/tui/ui/paths.ts +10 -0
- package/src/tui/ui/pictures.ts +78 -0
- package/src/tui/ui/screen.ts +421 -0
- package/src/tui/ui/status.ts +271 -0
- package/src/tui/ui/styled.ts +85 -0
- package/src/tui/ui/testing.ts +5 -0
- package/src/tui/ui/tool-card.ts +150 -0
- package/src/apps/extension-ui.ts +0 -330
- package/src/apps/resource-browser.ts +0 -75
- package/src/apps/session-browser.ts +0 -113
- package/src/apps/session-picker.ts +0 -55
- package/src/tui/ansi.ts +0 -67
- package/src/tui/autocomplete.ts +0 -29
- package/src/tui/chat-viewport.ts +0 -47
- package/src/tui/complete.ts +0 -87
- package/src/tui/editor.ts +0 -940
- package/src/tui/host.ts +0 -264
- package/src/tui/input.ts +0 -75
- package/src/tui/keys.ts +0 -378
- package/src/tui/markdown.ts +0 -17
- package/src/tui/render.ts +0 -153
- package/src/tui/select.ts +0 -258
- package/src/tui/settings.ts +0 -44
- package/src/tui/statusline.ts +0 -162
- package/src/tui/system-theme.ts +0 -700
- package/src/tui/terminal.ts +0 -30
- package/src/tui/text.ts +0 -52
- package/src/tui/toolview.ts +0 -386
- package/src/tui/transcript.ts +0 -123
- package/src/tui/tree.ts +0 -471
- package/src/vendor/pi-tui/LICENSE.upstream +0 -21
- package/src/vendor/pi-tui/UPSTREAM.json +0 -50
- package/src/vendor/pi-tui/VENDOR.md +0 -25
- package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
- package/src/vendor/pi-tui/autocomplete.ts +0 -860
- package/src/vendor/pi-tui/colors.ts +0 -367
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
- package/src/vendor/pi-tui/components/box.ts +0 -166
- package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
- package/src/vendor/pi-tui/components/editor.ts +0 -2472
- package/src/vendor/pi-tui/components/h-stack.ts +0 -44
- package/src/vendor/pi-tui/components/image.ts +0 -127
- package/src/vendor/pi-tui/components/input.ts +0 -494
- package/src/vendor/pi-tui/components/loader.ts +0 -101
- package/src/vendor/pi-tui/components/markdown.ts +0 -1021
- package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
- package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
- package/src/vendor/pi-tui/components/select-list.ts +0 -273
- package/src/vendor/pi-tui/components/settings-list.ts +0 -328
- package/src/vendor/pi-tui/components/spacer.ts +0 -28
- package/src/vendor/pi-tui/components/stack.ts +0 -154
- package/src/vendor/pi-tui/components/text.ts +0 -107
- package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
- package/src/vendor/pi-tui/components/v-stack.ts +0 -33
- package/src/vendor/pi-tui/editor-component.ts +0 -74
- package/src/vendor/pi-tui/fuzzy.ts +0 -138
- package/src/vendor/pi-tui/index.ts +0 -186
- package/src/vendor/pi-tui/keybindings.ts +0 -320
- package/src/vendor/pi-tui/keys.ts +0 -1401
- package/src/vendor/pi-tui/kill-ring.ts +0 -46
- package/src/vendor/pi-tui/latex.ts +0 -1506
- package/src/vendor/pi-tui/layout-node.ts +0 -51
- package/src/vendor/pi-tui/layout.ts +0 -449
- package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
- package/src/vendor/pi-tui/native/clipboard.h +0 -208
- package/src/vendor/pi-tui/native/darwin/README.md +0 -22
- package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
- package/src/vendor/pi-tui/native/linux/README.md +0 -23
- package/src/vendor/pi-tui/native/linux/build.sh +0 -65
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
- package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
- package/src/vendor/pi-tui/native/napi.h +0 -91
- package/src/vendor/pi-tui/native/win32/README.md +0 -31
- package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
- package/src/vendor/pi-tui/native-modifiers.ts +0 -13
- package/src/vendor/pi-tui/native-module-path.ts +0 -31
- package/src/vendor/pi-tui/native-platform.ts +0 -65
- package/src/vendor/pi-tui/oklab.ts +0 -233
- package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
- package/src/vendor/pi-tui/terminal-colors.ts +0 -91
- package/src/vendor/pi-tui/terminal-image.ts +0 -731
- package/src/vendor/pi-tui/terminal.ts +0 -554
- package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
- package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
- package/src/vendor/pi-tui/tui.ts +0 -1493
- package/src/vendor/pi-tui/undo-stack.ts +0 -28
- package/src/vendor/pi-tui/utils.ts +0 -1385
- package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
- package/src/vendor/pi-tui/word-navigation.ts +0 -117
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic version comparison (semver.org §11), without a dependency. Pure.
|
|
3
|
+
*
|
|
4
|
+
* Used to decide whether a published version is newer than the running one
|
|
5
|
+
* (docs/tasks/feat-update-check.md). Anything that does not parse is not a
|
|
6
|
+
* version, and an unparseable version never produces an update notice.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface Version {
|
|
10
|
+
readonly major: number;
|
|
11
|
+
readonly minor: number;
|
|
12
|
+
readonly patch: number;
|
|
13
|
+
/** Dot-separated pre-release identifiers; empty for a release. */
|
|
14
|
+
readonly prerelease: readonly (number | string)[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PATTERN = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
18
|
+
|
|
19
|
+
export function parseVersion(text: string): Version | null {
|
|
20
|
+
const match = PATTERN.exec(text.trim());
|
|
21
|
+
if (!match) return null;
|
|
22
|
+
const numbers = [match[1], match[2], match[3]].map(Number);
|
|
23
|
+
if (numbers.some(n => !Number.isSafeInteger(n))) return null;
|
|
24
|
+
const prerelease = match[4] ? match[4].split(".").map(part => /^\d+$/.test(part) ? Number(part) : part) : [];
|
|
25
|
+
return { major: numbers[0]!, minor: numbers[1]!, patch: numbers[2]!, prerelease };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** -1, 0 or 1, by §11: numbers, then a release outranks its pre-releases, then identifiers left to right. Build metadata is ignored. */
|
|
29
|
+
export function compareVersions(a: Version, b: Version): -1 | 0 | 1 {
|
|
30
|
+
for (const key of ["major", "minor", "patch"] as const) if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1;
|
|
31
|
+
const x = a.prerelease, y = b.prerelease;
|
|
32
|
+
if (!x.length || !y.length) return x.length === y.length ? 0 : x.length ? -1 : 1;
|
|
33
|
+
for (let i = 0; i < Math.min(x.length, y.length); i++) {
|
|
34
|
+
const p = x[i]!, q = y[i]!;
|
|
35
|
+
if (p === q) continue;
|
|
36
|
+
if (typeof p === "number" && typeof q === "number") return p < q ? -1 : 1;
|
|
37
|
+
// Numeric identifiers have lower precedence than alphanumeric ones.
|
|
38
|
+
if (typeof p === "number") return -1;
|
|
39
|
+
if (typeof q === "number") return 1;
|
|
40
|
+
return p < q ? -1 : 1;
|
|
41
|
+
}
|
|
42
|
+
return x.length === y.length ? 0 : x.length < y.length ? -1 : 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether `latest` should be offered to someone running `current`. Never a
|
|
47
|
+
* downgrade, never something unparseable, and a release is not nudged
|
|
48
|
+
* towards a pre-release (a pre-release user may move to the next release).
|
|
49
|
+
*/
|
|
50
|
+
export function isUpgrade(latest: string, current: string): boolean {
|
|
51
|
+
const next = parseVersion(latest), now = parseVersion(current);
|
|
52
|
+
if (!next || !now) return false;
|
|
53
|
+
if (next.prerelease.length && !now.prerelease.length) return false;
|
|
54
|
+
return compareVersions(next, now) > 0;
|
|
55
|
+
}
|
package/src/core/work-state.ts
CHANGED
|
@@ -15,8 +15,35 @@ export type WorkItem = {
|
|
|
15
15
|
evidence?: Evidence;
|
|
16
16
|
/** Reopening keeps provenance without carrying a second copy of old proof. */
|
|
17
17
|
previous_evidence?: string;
|
|
18
|
+
depends_on?: readonly string[];
|
|
19
|
+
owner?: string;
|
|
20
|
+
owner_event?: string;
|
|
18
21
|
};
|
|
19
22
|
|
|
23
|
+
export type WorkReadiness = { ready: boolean; reason: "ready" | "waiting" | "blocked" | "closed" | "owned" | "missing"; waiting_for: string[] };
|
|
24
|
+
export function workReadiness(items: readonly WorkItem[], id: string, actor?: string): WorkReadiness {
|
|
25
|
+
const item = items.find(work => work.id === id);
|
|
26
|
+
if (!item) return { ready: false, reason: "missing", waiting_for: [id] };
|
|
27
|
+
if (item.status === "completed" || item.status === "cancelled") return { ready: false, reason: "closed", waiting_for: [] };
|
|
28
|
+
const waiting = (item.depends_on ?? []).filter(dep => items.find(work => work.id === dep)?.status !== "completed");
|
|
29
|
+
const reason = item.status === "blocked" ? "blocked" : waiting.length ? "waiting" : item.owner && item.owner !== actor ? "owned" : "ready";
|
|
30
|
+
return { ready: reason === "ready", reason, waiting_for: [...waiting] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function dependencyError(items: readonly WorkItem[], id: string, dependencies: readonly string[]): string | null {
|
|
34
|
+
if (new Set(dependencies).size !== dependencies.length) return "duplicate work dependencies";
|
|
35
|
+
if (dependencies.includes(id)) return "work dependency cycle: self dependency";
|
|
36
|
+
if (dependencies.some(dep => !items.some(item => item.id === dep))) return "unknown work dependency";
|
|
37
|
+
const seen = new Set<string>(), pending = [...dependencies];
|
|
38
|
+
while (pending.length) {
|
|
39
|
+
const dep = pending.pop()!;
|
|
40
|
+
if (dep === id) return "work dependency cycle";
|
|
41
|
+
if (seen.has(dep)) continue;
|
|
42
|
+
seen.add(dep); pending.push(...items.find(item => item.id === dep)?.depends_on ?? []);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
20
47
|
/** O.2: sibling sessions may choose the same local label without aliasing work. */
|
|
21
48
|
export function scopedWorkId(namespace: string, requested: string, items: readonly WorkItem[]): string {
|
|
22
49
|
return items.some(item => item.id === requested) || requested.startsWith(`${namespace}:`)
|
|
@@ -26,6 +53,18 @@ export function scopedWorkId(namespace: string, requested: string, items: readon
|
|
|
26
53
|
export function workChangeError(items: readonly WorkItem[], event: LogEvent): string | null {
|
|
27
54
|
if (!("work_id" in event) || event.work_id === undefined) return null;
|
|
28
55
|
const item = items.find(item => item.id === event.work_id);
|
|
56
|
+
if ((event.t === "next_action" || event.t === "work_dependencies") && event.depends_on !== undefined) {
|
|
57
|
+
const error = dependencyError(items, event.work_id, event.depends_on);
|
|
58
|
+
if (error) return error;
|
|
59
|
+
}
|
|
60
|
+
if (event.t === "work_dependencies" && !item) return `unknown work_id ${event.work_id}`;
|
|
61
|
+
if (event.t === "work_owner") {
|
|
62
|
+
if (!item) return `unknown work_id ${event.work_id}`;
|
|
63
|
+
if (event.operation === "release") return item.owner === event.owner ? null : "only the recorded owner can release work";
|
|
64
|
+
const ready = workReadiness(items, item.id, event.owner);
|
|
65
|
+
return ready.ready ? null : `work ${item.id} is ${ready.reason}; cannot claim it`;
|
|
66
|
+
}
|
|
67
|
+
if (event.t === "claim" && item && (item.depends_on ?? []).some(dep => items.find(work => work.id === dep)?.status !== "completed")) return "work dependencies are not completed";
|
|
29
68
|
if (event.t === "work_status") {
|
|
30
69
|
// D15 carries post-fork instructions, not their whole history. A harness-
|
|
31
70
|
// authenticated transition may seed intent in the adopted branch, never proof.
|
|
@@ -51,13 +90,19 @@ export function deriveWorkItems(previous: readonly WorkItem[], events: readonly
|
|
|
51
90
|
const items = new Map(previous.map(item => [item.id, { ...item }]));
|
|
52
91
|
for (let index = 0; index < events.length; index++) {
|
|
53
92
|
const event = events[index]!;
|
|
54
|
-
if (
|
|
93
|
+
if ((event.t !== "next_action" && event.t !== "blocked" && event.t !== "work_status" && event.t !== "claim" && event.t !== "work_dependencies" && event.t !== "work_owner") ||
|
|
94
|
+
event.work_id === undefined || workChangeError([...items.values()], event)) continue;
|
|
55
95
|
const old = items.get(event.work_id);
|
|
56
|
-
const item: WorkItem = old ? { ...old, what: event.what, updated_at: event.id } : {
|
|
96
|
+
const item: WorkItem = old ? { ...old, what: event.t === "work_owner" || event.t === "work_dependencies" ? old.what : event.what, updated_at: event.id } : {
|
|
57
97
|
id: event.work_id, what: event.what, status: "active", verification: "not_run",
|
|
58
98
|
opened_at: event.id, updated_at: event.id,
|
|
59
99
|
};
|
|
60
|
-
if (event.t === "next_action") item.next_action = event.next_action;
|
|
100
|
+
if (event.t === "next_action") { item.next_action = event.next_action; if (event.depends_on !== undefined) item.depends_on = [...event.depends_on]; }
|
|
101
|
+
else if (event.t === "work_dependencies") item.depends_on = [...event.depends_on];
|
|
102
|
+
else if (event.t === "work_owner") {
|
|
103
|
+
if (event.operation === "claim") { item.owner = event.owner; item.owner_event = event.id; }
|
|
104
|
+
else { delete item.owner; delete item.owner_event; }
|
|
105
|
+
}
|
|
61
106
|
else if (event.t === "blocked") {
|
|
62
107
|
item.status = "blocked";
|
|
63
108
|
item.reason = event.reason;
|
|
@@ -89,6 +134,7 @@ export function deriveWorkItems(previous: readonly WorkItem[], events: readonly
|
|
|
89
134
|
} else { item.verification = "unverified"; item.reason = resolved.reason; }
|
|
90
135
|
}
|
|
91
136
|
items.set(item.id, item);
|
|
137
|
+
if (item.status === "completed" || item.status === "cancelled") { delete item.owner; delete item.owner_event; }
|
|
92
138
|
}
|
|
93
139
|
return [...items.values()].map(item => {
|
|
94
140
|
if (!item.evidence) return item;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** O.10: prioritize explicit intent and references without changing D20 arrays. */
|
|
2
|
+
import type { Handoff } from "./handoff.ts";
|
|
3
|
+
import type { NodeRef } from "./events.ts";
|
|
4
|
+
import { workReadiness, type WorkItem, type WorkReadiness } from "./work-state.ts";
|
|
5
|
+
import { emitYaml } from "./yaml.ts";
|
|
6
|
+
import { estimateTokens } from "./tokens.ts";
|
|
7
|
+
import { RECOVERY_SECTIONS, recoverySources, type RecoveryNodes, type RecoveryRanges, type RecoveryReference } from "./recovery-sources.ts";
|
|
8
|
+
import { recoveryInstructions } from "./prompt-policy.ts";
|
|
9
|
+
|
|
10
|
+
export type WorkingSet = {
|
|
11
|
+
version: 1 | 2; source: NodeRef;
|
|
12
|
+
work: (Pick<WorkItem, "id" | "what" | "status" | "next_action" | "reason" | "depends_on" | "owner"> & { readiness?: WorkReadiness })[];
|
|
13
|
+
references: ({ node?: number; event: string; kind: string; work?: string } & Partial<Omit<RecoveryReference, "node" | "event" | "kind" | "work">>)[];
|
|
14
|
+
omitted: { work: number; references: number };
|
|
15
|
+
recovery_sections?: RecoveryRanges;
|
|
16
|
+
recovery: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function workingSet(full: Handoff, budget: number): WorkingSet | null {
|
|
20
|
+
if (!full.predecessor) return null;
|
|
21
|
+
const items = (full.state.work_items ?? []).filter(item => item.status === "blocked" || item.status === "active")
|
|
22
|
+
.toSorted((a, b) => Number(b.status === "blocked") - Number(a.status === "blocked") || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
23
|
+
const active = new Set(items.map(item => item.id));
|
|
24
|
+
const references: WorkingSet["references"] = [
|
|
25
|
+
...(full.evidence_links ?? []).filter(link => active.has(link.work) && !link.superseded_by)
|
|
26
|
+
.map(link => ({ event: link.event, kind: "evidence", work: link.work })),
|
|
27
|
+
...full.decisions.filter(item => item.work_id && active.has(item.work_id) && item.source)
|
|
28
|
+
.map(item => ({ node: item.source!.node, event: item.source!.event_id, kind: "decision", work: item.work_id! })),
|
|
29
|
+
...full.pitfalls.filter(item => item.work_id && active.has(item.work_id) && item.source)
|
|
30
|
+
.map(item => ({ node: item.source!.node, event: item.source!.event_id, kind: "pitfall", work: item.work_id! })),
|
|
31
|
+
...(full.progress_evidence ?? []).toReversed().map(item => ({ node: item.source.node, event: item.source.event_id, kind: item.kind })),
|
|
32
|
+
];
|
|
33
|
+
const view: WorkingSet = { version: 1, source: full.predecessor, work: [], references: [],
|
|
34
|
+
omitted: { work: items.length, references: references.length }, recovery: "history(work_id=ID), history(event=ID), or history(node=source.node, section=...)" };
|
|
35
|
+
const fits = () => budget === Infinity || estimateTokens.count([], emitYaml(view)) <= budget;
|
|
36
|
+
if (!fits()) return null;
|
|
37
|
+
for (const item of items) {
|
|
38
|
+
const { id, what, status, next_action, reason, depends_on, owner } = item;
|
|
39
|
+
view.work.push({ id, what, status, ...(next_action ? { next_action } : {}), ...(reason ? { reason } : {}),
|
|
40
|
+
...(depends_on ? { depends_on, readiness: workReadiness(full.state.work_items ?? [], id) } : {}), ...(owner ? { owner } : {}) });
|
|
41
|
+
view.omitted.work--;
|
|
42
|
+
if (!fits()) { view.work.pop(); view.omitted.work++; }
|
|
43
|
+
}
|
|
44
|
+
for (const reference of references) {
|
|
45
|
+
view.references.push(reference); view.omitted.references--;
|
|
46
|
+
if (!fits()) { view.references.pop(); view.omitted.references++; }
|
|
47
|
+
}
|
|
48
|
+
return view;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** R9: a new projection version keeps the original v1 ordering byte-stable. */
|
|
52
|
+
export function recoveryWorkingSet(full: Handoff, nodes: RecoveryNodes, budget: number, tools?: readonly string[]): WorkingSet | null {
|
|
53
|
+
const legacy = workingSet(full, Infinity);
|
|
54
|
+
if (!legacy) return null;
|
|
55
|
+
const sources = recoverySources(nodes);
|
|
56
|
+
const view: WorkingSet = { version: 2, source: legacy.source, work: [], references: [],
|
|
57
|
+
omitted: { work: legacy.work.length, references: legacy.references.length + RECOVERY_SECTIONS.reduce((sum, section) => sum + sources[section].length, 0) },
|
|
58
|
+
recovery_sections: Object.fromEntries(RECOVERY_SECTIONS.map(section => [section, { total: sources[section].length, omitted: sources[section].length }])) as RecoveryRanges,
|
|
59
|
+
recovery: tools === undefined ? "Read original user messages before continuing: history(node=source.node,recovery=users,offset=0). Other pages: failures, environment, next_actions; event=ID reads one original. Abandoned messages are cancelled; annotations are not user instructions. Observations are historical: recheck resource, host and current permissions before acting. history(work_id=ID) recovers work." : recoveryInstructions(tools) };
|
|
60
|
+
const fits = () => estimateTokens.count([], emitYaml(view)) <= budget;
|
|
61
|
+
if (!fits()) return null;
|
|
62
|
+
const queues = RECOVERY_SECTIONS.map(section => ({ section, items: [...sources[section]], at: 0 }));
|
|
63
|
+
// The latest and first live user messages anchor correction and initial scope;
|
|
64
|
+
// remaining user sources are newest first. Cancelled turns stay pageable.
|
|
65
|
+
const live = sources.users.filter(ref => !ref.abandoned), first = live.at(-1);
|
|
66
|
+
queues[0]!.items = [...live.slice(0, 1), ...(first && first !== live[0] ? [first] : []),
|
|
67
|
+
...live.slice(1, -1), ...sources.users.filter(ref => ref.abandoned)];
|
|
68
|
+
const takeReference = (reference: WorkingSet["references"][number], section?: keyof RecoveryRanges) => {
|
|
69
|
+
view.references.push(reference); view.omitted.references--;
|
|
70
|
+
if (section) view.recovery_sections![section].omitted--;
|
|
71
|
+
if (!fits()) {
|
|
72
|
+
view.references.pop(); view.omitted.references++;
|
|
73
|
+
if (section) view.recovery_sections![section].omitted++;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
// One source per role precedes work prose; round-robin growth thereafter
|
|
77
|
+
// prevents a long task list or repeated failures starving other categories.
|
|
78
|
+
for (const queue of queues) { const reference = queue.items[queue.at++]; if (reference) takeReference(reference, queue.section); }
|
|
79
|
+
let at = 0;
|
|
80
|
+
while (at < legacy.work.length || at < legacy.references.length || queues.some(queue => queue.at < queue.items.length)) {
|
|
81
|
+
const work = legacy.work[at];
|
|
82
|
+
if (work) {
|
|
83
|
+
view.work.push(work); view.omitted.work--;
|
|
84
|
+
if (!fits()) { view.work.pop(); view.omitted.work++; }
|
|
85
|
+
}
|
|
86
|
+
for (const queue of queues) { const reference = queue.items[queue.at++]; if (reference) takeReference(reference, queue.section); }
|
|
87
|
+
const reference = legacy.references[at++];
|
|
88
|
+
if (reference) takeReference(reference);
|
|
89
|
+
}
|
|
90
|
+
return view;
|
|
91
|
+
}
|
package/src/runtime/auth.ts
CHANGED
|
@@ -304,8 +304,8 @@ export function envLabel(provider: string): string {
|
|
|
304
304
|
*/
|
|
305
305
|
export function describeMissingCredential(provider: string): string {
|
|
306
306
|
return (
|
|
307
|
-
`no credential for ${provider}.
|
|
308
|
-
`or
|
|
307
|
+
`no credential for ${provider}. Run /login ${provider} to add a key, ` +
|
|
308
|
+
`or export ${envLabel(provider)}`
|
|
309
309
|
);
|
|
310
310
|
}
|
|
311
311
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** O.14: volatile ownership, never a replacement for the durable branch log. */
|
|
2
|
+
export class BranchJobs<T> {
|
|
3
|
+
private readonly jobs = new Map<number, { controller: AbortController; done: Promise<void>; value?: T; error?: string; detach(): void; ready: boolean }>();
|
|
4
|
+
|
|
5
|
+
get active(): number { return [...this.jobs.values()].filter(job => !job.ready).length; }
|
|
6
|
+
state(node: number): "running" | "ready" | undefined {
|
|
7
|
+
const job = this.jobs.get(node); return job ? job.ready ? "ready" : "running" : undefined;
|
|
8
|
+
}
|
|
9
|
+
nodes(): number[] { return [...this.jobs.keys()]; }
|
|
10
|
+
start(node: number, controller: AbortController, run: () => Promise<T>, detach: () => void): void {
|
|
11
|
+
if (this.jobs.has(node)) throw new Error("branch already owned");
|
|
12
|
+
const job: { controller: AbortController; done: Promise<void>; value?: T; error?: string; detach(): void; ready: boolean } = {
|
|
13
|
+
controller, ready: false, detach, done: Promise.resolve(),
|
|
14
|
+
};
|
|
15
|
+
this.jobs.set(node, job);
|
|
16
|
+
job.done = run().then(value => { job.value = value; }, () => { job.error = "branch finalization failed; inspect its original log"; })
|
|
17
|
+
.finally(() => { job.ready = true; job.detach(); });
|
|
18
|
+
}
|
|
19
|
+
cancel(node?: number): void {
|
|
20
|
+
for (const [id, job] of this.jobs) if (node === undefined || node === id) job.controller.abort();
|
|
21
|
+
}
|
|
22
|
+
async drain(node?: number): Promise<void> { await Promise.all([...this.jobs].filter(([id]) => node === undefined || node === id).map(([, job]) => job.done)); }
|
|
23
|
+
async wait(node: number, ms: number): Promise<{ value?: T; error?: string; running: boolean } | undefined> {
|
|
24
|
+
const job = this.jobs.get(node); if (!job) return undefined;
|
|
25
|
+
if (!Number.isFinite(ms) || ms < 0 || ms > 60_000) throw new RangeError("wait_ms must be between 0 and 60000");
|
|
26
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
27
|
+
try {
|
|
28
|
+
if (!job.ready && ms > 0) await Promise.race([job.done, new Promise<void>(resolve => { timer = setTimeout(resolve, ms); })]);
|
|
29
|
+
} finally { clearTimeout(timer); }
|
|
30
|
+
return { ...(job.value !== undefined ? { value: job.value } : {}), ...(job.error ? { error: job.error } : {}), running: !job.ready };
|
|
31
|
+
}
|
|
32
|
+
forget(node: number): void {
|
|
33
|
+
if (this.state(node) === "running") throw new Error("cannot forget a running branch");
|
|
34
|
+
this.jobs.delete(node);
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/runtime/clipboard.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Pi's files → image → text precedence (MIT, 02eed88) over the platform's own
|
|
3
|
+
* clipboard commands. The optional native helper went with the vendored TUI
|
|
4
|
+
* (D20); a caller may still inject one.
|
|
5
|
+
*/
|
|
2
6
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
|
|
7
|
+
|
|
8
|
+
export interface NativeClipboard {
|
|
9
|
+
setText?(text: string): Promise<void>;
|
|
10
|
+
getText?(): Promise<string | null | undefined>;
|
|
11
|
+
getImage?(): Promise<Uint8Array | null | undefined>;
|
|
12
|
+
getFilePaths?(): Promise<string[] | null | undefined>;
|
|
13
|
+
}
|
|
14
|
+
function getNativeClipboard(): NativeClipboard | null { return null; }
|
|
4
15
|
import { runClipboardCommand } from "../vendor/pi-rich/clipboard-command.ts";
|
|
5
16
|
|
|
6
17
|
export type ClipboardContent = { files: string[] } | { image: Uint8Array } | { text: string } | null;
|
|
@@ -104,9 +115,9 @@ export async function readClipboard(options: ClipboardOptions = {}): Promise<Cli
|
|
|
104
115
|
return bytes.length ? { text: bytes.toString("utf8") } : null;
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
|
-
const image = await native?.getImage();
|
|
118
|
+
const image = await native?.getImage?.();
|
|
108
119
|
if (image?.length) return { image };
|
|
109
|
-
const text = await native?.getText();
|
|
120
|
+
const text = await native?.getText?.();
|
|
110
121
|
if (text !== undefined) return text ? { text } : null;
|
|
111
122
|
const fallback = env["TERMUX_VERSION"] ? await run("termux-clipboard-get", []) : platform === "darwin" ? await run("pbpaste", []) : undefined;
|
|
112
123
|
if (fallback === undefined) throw new Error("Clipboard unavailable for this terminal session");
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** R8: one owner observes an admitted command once, including after a managed call yields. */
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { resolve, join } from "node:path";
|
|
4
|
+
import { artifactChange, commandArtifactState, parseCommandArtifactPlan, COMMAND_ARTIFACT_SCOPE, type CommandArtifactReceipt } from "../core/command-artifacts.ts";
|
|
5
|
+
import { observeArtifactInputs, observeArtifactFiles, artifactExclusions, archiveCommandArtifacts } from "../store/command-artifacts.ts";
|
|
6
|
+
import type { ToolContext } from "./tools.ts";
|
|
7
|
+
|
|
8
|
+
export type ArtifactCommandEnd = { exit: number; stopped?: "cancelled" | "timeout"; capture?: "complete" | "partial"; cleanup?: unknown };
|
|
9
|
+
export type CommandArtifactObserver = {
|
|
10
|
+
pending: CommandArtifactReceipt;
|
|
11
|
+
finish(end: ArtifactCommandEnd): Promise<CommandArtifactReceipt>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Syntactic validation precedes admission; no preflight disk probing is smuggled through a denied tool. */
|
|
15
|
+
export function prepareArtifactArguments(input: unknown): unknown {
|
|
16
|
+
if (input === null || typeof input !== "object" || Array.isArray(input) || !("artifacts" in input)) return input;
|
|
17
|
+
return { ...input, artifacts: parseCommandArtifactPlan(input.artifacts) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function beginCommandArtifacts(ctx: Pick<ToolContext, "root" | "hostId" | "ownedStatePaths" | "outputStore" | "signal">,
|
|
21
|
+
command: string, cwd: string, requested: unknown): Promise<CommandArtifactObserver | undefined> {
|
|
22
|
+
if (requested === undefined) return undefined;
|
|
23
|
+
const plan = parseCommandArtifactPlan(requested), exclusions = artifactExclusions(ctx.root, plan,
|
|
24
|
+
[...(ctx.ownedStatePaths ?? []), ctx.outputStore ?? join(ctx.root, ".xeno", "observations")]);
|
|
25
|
+
const beforeInputs = await observeArtifactInputs(ctx.root, exclusions, ctx.signal);
|
|
26
|
+
const before = await observeArtifactFiles(ctx.root, plan.paths, ctx.signal);
|
|
27
|
+
const pending: CommandArtifactReceipt = {
|
|
28
|
+
kind: "command_artifacts", version: 1, state: "pending", plan,
|
|
29
|
+
source: { host: ctx.hostId ?? hostname(), workspace: resolve(ctx.root), cwd: resolve(cwd), runtime: `bun/${Bun.version}` },
|
|
30
|
+
command, exit: null, started_at: new Date().toISOString(), observed_at: null,
|
|
31
|
+
inputs: { scope: COMMAND_ARTIFACT_SCOPE, exclusions, before: beforeInputs.sha256, after: null },
|
|
32
|
+
outputs: [], reasons: beforeInputs.reason ? [beforeInputs.reason] : [],
|
|
33
|
+
};
|
|
34
|
+
let completed: Promise<CommandArtifactReceipt> | undefined;
|
|
35
|
+
return { pending, finish(end) {
|
|
36
|
+
completed ??= (async () => {
|
|
37
|
+
// Observation failures retain the actual exit. Never retry a post-exit read against later files.
|
|
38
|
+
const reasons = [...pending.reasons];
|
|
39
|
+
if (end.exit !== 0) reasons.push("command exited unsuccessfully");
|
|
40
|
+
if (end.stopped) reasons.push(`command ${end.stopped}`);
|
|
41
|
+
if (end.capture === "partial") reasons.push("command output capture is partial");
|
|
42
|
+
if (end.cleanup) reasons.push("command cleanup is unconfirmed");
|
|
43
|
+
const after = await observeArtifactFiles(ctx.root, plan.paths, ctx.signal);
|
|
44
|
+
const afterInputs = await observeArtifactInputs(ctx.root, exclusions, ctx.signal);
|
|
45
|
+
if (afterInputs.reason) reasons.push(afterInputs.reason);
|
|
46
|
+
const receipt: CommandArtifactReceipt = { ...pending, exit: end.exit, observed_at: new Date().toISOString(),
|
|
47
|
+
inputs: { ...pending.inputs, after: afterInputs.sha256 },
|
|
48
|
+
outputs: plan.paths.map((path, i) => ({ path, before: before[i]!, after: after[i]!, change: artifactChange(before[i]!, after[i]!) })), reasons };
|
|
49
|
+
receipt.state = commandArtifactState(receipt);
|
|
50
|
+
try { receipt.manifest = await archiveCommandArtifacts(ctx.outputStore ?? join(ctx.root, ".xeno", "observations"), receipt); }
|
|
51
|
+
catch { receipt.reasons.push("artifact observation archive unavailable"); receipt.state = commandArtifactState(receipt); }
|
|
52
|
+
return receipt;
|
|
53
|
+
})();
|
|
54
|
+
return completed;
|
|
55
|
+
} };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function commandArtifactSummary(receipt: CommandArtifactReceipt | undefined): string {
|
|
59
|
+
if (!receipt) return "";
|
|
60
|
+
const changes = ["created", "modified", "unchanged", "unknown"].map(change => `${change}=${receipt.outputs.filter(output => output.change === change).length}`).join(", ");
|
|
61
|
+
return `\n[artifacts ${receipt.state}; ${changes}; reasons=${receipt.reasons.length}; ` +
|
|
62
|
+
(receipt.manifest ? `history artifact=${receipt.manifest.sha256}; ` : "") +
|
|
63
|
+
"command association only, not build causality or runtime acceptance]";
|
|
64
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** R6: observe through the same shell and process owner; never infer versions from the host PATH. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { ENVIRONMENT_OUTPUT_BYTES, ENVIRONMENT_PARSE_BYTES, ENVIRONMENT_TIMEOUT_MS, ENVIRONMENT_TTL_MS,
|
|
6
|
+
environmentCapability, parseEnvironmentFrame, type EnvironmentProbe, type EnvironmentReceipt, type EnvironmentSource } from "../core/environment.ts";
|
|
7
|
+
import { executionOutcome } from "../core/outcome.ts";
|
|
8
|
+
import { canonicalValue } from "../core/continuation.ts";
|
|
9
|
+
import { failureDiagnostic } from "../core/tool-failure.ts";
|
|
10
|
+
import { observeCommand } from "../store/command.ts";
|
|
11
|
+
import type { ToolContext, ToolResult } from "./tools.ts";
|
|
12
|
+
|
|
13
|
+
const hash = (text: string) => createHash("sha256").update(text).digest("hex");
|
|
14
|
+
/** Hash only resolution-related inherited inputs; never expose the full environment or its credentials. */
|
|
15
|
+
export function environmentSource(ctx: Pick<ToolContext, "root" | "hostId">, cwd: string): EnvironmentSource {
|
|
16
|
+
const keys = ["PATH", "HOME", "SHELL", "LANG", "BASH_ENV", "PYTHONHOME", "PYTHONPATH", "PYENV_VERSION", "VIRTUAL_ENV", "CONDA_PREFIX", "NVM_BIN"];
|
|
17
|
+
return { host: ctx.hostId ?? hostname(), workspace: resolve(ctx.root), cwd: resolve(cwd),
|
|
18
|
+
runtime: `bun/${Bun.version};${process.platform}/${process.arch};${process.execPath}`,
|
|
19
|
+
inherited_sha256: hash(canonicalValue(Object.fromEntries(keys.map(key => [key, process.env[key] ?? null])))) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function observeEnvironment(ctx: ToolContext, cwd: string, program: string, probe: EnvironmentProbe, command: string): Promise<ToolResult> {
|
|
23
|
+
const budget = new AbortController();
|
|
24
|
+
const signal = ctx.signal ? AbortSignal.any([ctx.signal, budget.signal]) : budget.signal;
|
|
25
|
+
let outputBytes = 0;
|
|
26
|
+
const source = environmentSource(ctx, cwd);
|
|
27
|
+
const captured = await observeCommand({ command, cwd, cleanupOnExit: true, outputLimitBytes: ENVIRONMENT_OUTPUT_BYTES,
|
|
28
|
+
outputStore: ctx.outputStore ?? join(ctx.root, ".xeno", "observations"),
|
|
29
|
+
maxOutputBytes: ENVIRONMENT_PARSE_BYTES, timeoutMs: Math.min(ENVIRONMENT_TIMEOUT_MS, ctx.timeoutMs, ctx.maxTimeoutMs ?? Infinity), signal,
|
|
30
|
+
onOutput: chunk => {
|
|
31
|
+
outputBytes += chunk.byteLength;
|
|
32
|
+
if (outputBytes > ENVIRONMENT_OUTPUT_BYTES) budget.abort();
|
|
33
|
+
ctx.onOutput?.(chunk);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const outputLimited = budget.signal.aborted;
|
|
37
|
+
const parseLimited = captured.ref.bytes > ENVIRONMENT_PARSE_BYTES;
|
|
38
|
+
const frame = parseEnvironmentFrame(new TextDecoder().decode(captured.head), program, probe);
|
|
39
|
+
const capability = captured.cleanup ? { state: "unknown" as const, reported_version: null, reason: captured.cleanup.message } : environmentCapability(frame, captured.exit, { complete: captured.capture === "complete",
|
|
40
|
+
...(captured.stopped ? { stopped: captured.stopped } : {}),
|
|
41
|
+
...(outputLimited ? { limited: "output" } : parseLimited ? { limited: "parse" } : {}) });
|
|
42
|
+
const observedAt = Date.now();
|
|
43
|
+
const receipt: EnvironmentReceipt = { kind: "environment", version: 1, query: { program, probe }, source,
|
|
44
|
+
observed_at: observedAt, expires_at: observedAt + ENVIRONMENT_TTL_MS, ...capability,
|
|
45
|
+
shell: frame ? { executable: frame.shell, version: frame.shell_version, cwd: frame.cwd, path_sha256: hash(frame.path) } : null,
|
|
46
|
+
resolution: frame ? { kind: frame.kind, executable: frame.executable } : null,
|
|
47
|
+
evidence: { output: captured.ref, exit: captured.exit, complete: captured.capture === "complete" } };
|
|
48
|
+
const status = outputLimited ? "tool_failed" : captured.stopped ?? (capability.state === "observed" ? "tool_completed" : "tool_failed");
|
|
49
|
+
const code = outputLimited ? "execution_failed" : captured.stopped ?? (capability.state === "missing" ? "environment_missing" : "execution_failed");
|
|
50
|
+
const text = JSON.stringify(receipt);
|
|
51
|
+
const bytes = new TextEncoder().encode(text);
|
|
52
|
+
// Raw output is the primary artifact. Structured data is separately preserved by the Session broker.
|
|
53
|
+
const content = bytes.length <= ctx.maxOutputBytes ? text :
|
|
54
|
+
`environment ${receipt.state}; exit ${captured.exit}; full receipt in data; original output: ${captured.ref.sha256}`;
|
|
55
|
+
return { ok: capability.state === "observed", content, content_ref: captured.ref, data: receipt,
|
|
56
|
+
...(captured.cleanup ? { warnings: [captured.cleanup] } : {}),
|
|
57
|
+
observation_sha256: hash(canonicalValue({ query: receipt.query, source, ...capability, shell: receipt.shell, resolution: receipt.resolution, evidence: receipt.evidence })),
|
|
58
|
+
effect: { kind: "command", cmd: command, exit: captured.exit }, outcome: executionOutcome(status),
|
|
59
|
+
...(capability.state === "observed" ? {} : { diagnostic: failureDiagnostic(code, capability.reason ?? "capability unconfirmed", { execution: "started", effects: "possible" }) }) };
|
|
60
|
+
}
|
|
@@ -5,14 +5,16 @@ import { historyEntries } from "../core/history-query.ts";
|
|
|
5
5
|
import { observedVersion, resourceKey } from "../core/evidence-links.ts";
|
|
6
6
|
import { scopedWorkId } from "../core/work-state.ts";
|
|
7
7
|
import { readNodeHistory } from "../store/restore.ts";
|
|
8
|
+
import { failureDiagnostic, NOT_STARTED, READ_ATTEMPTED } from "../core/tool-failure.ts";
|
|
8
9
|
import type { ToolResult } from "./tools.ts";
|
|
9
10
|
|
|
10
11
|
export async function resolveEvidenceNote(logDir: string, node: number, namespace: string, current: Handoff, event: EvidenceLinkEvent): Promise<{ event?: EvidenceLinkEvent; result: ToolResult }> {
|
|
11
12
|
const work = scopedWorkId(namespace, event.work, current.state.work_items ?? []);
|
|
12
|
-
const fail = (message: string) => ({ result: { ok: false, content: message
|
|
13
|
+
const fail = (message: string, attempted = false) => ({ result: { ok: false, content: message, data: { recorded: false },
|
|
14
|
+
diagnostic: failureDiagnostic(attempted ? "execution_failed" : "invalid_input", message, attempted ? READ_ATTEMPTED : NOT_STARTED) } });
|
|
13
15
|
if (!current.state.work_items?.some(item => item.id === work)) return fail("pin requires an existing work item");
|
|
14
16
|
const history = await readNodeHistory(logDir, node);
|
|
15
|
-
if (!history.ok) return fail(history.error);
|
|
17
|
+
if (!history.ok) return fail(history.error, true);
|
|
16
18
|
const entries = historyEntries(history.nodes);
|
|
17
19
|
const source = entries.find(entry => entry.event === event.source && entry.kind === "tool_result");
|
|
18
20
|
if (!source) return fail("pin source must be an original tool observation in the active ancestry");
|