@visual-z/xeno 0.7.1 → 0.8.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.
Files changed (112) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +39 -12
  3. package/docs/foundations.md +1 -1
  4. package/docs/install.md +18 -5
  5. package/package.json +3 -1
  6. package/src/apps/cli.ts +4 -3
  7. package/src/apps/commands.ts +9 -1
  8. package/src/apps/goal-flow.ts +70 -0
  9. package/src/apps/render.ts +0 -2
  10. package/src/apps/session-hub.ts +6 -5
  11. package/src/apps/session-lifecycle.ts +64 -0
  12. package/src/apps/shell.ts +90 -35
  13. package/src/apps/startup.ts +33 -32
  14. package/src/apps/ui/app.ts +47 -10
  15. package/src/apps/ui/control-model.ts +18 -1
  16. package/src/apps/ui/surface.ts +2 -0
  17. package/src/apps/update-check.ts +34 -4
  18. package/src/core/acceptance.ts +41 -9
  19. package/src/core/blind-checks.ts +72 -0
  20. package/src/core/branch-receipt.ts +3 -1
  21. package/src/core/checkpoints.ts +21 -0
  22. package/src/core/churn.ts +2 -2
  23. package/src/core/completion-review.ts +18 -11
  24. package/src/core/continuation.ts +11 -10
  25. package/src/core/control-view.ts +5 -8
  26. package/src/core/deadline.ts +2 -1
  27. package/src/core/derive.ts +0 -7
  28. package/src/core/environment.ts +2 -2
  29. package/src/core/event-validation.ts +19 -6
  30. package/src/core/events.ts +41 -32
  31. package/src/core/evidence-ladder.ts +62 -0
  32. package/src/core/evidence-links.ts +0 -10
  33. package/src/core/evidence.ts +1 -2
  34. package/src/core/export.ts +6 -11
  35. package/src/core/goal.ts +200 -0
  36. package/src/core/history.ts +1 -11
  37. package/src/core/json-event.ts +0 -6
  38. package/src/core/outcome.ts +0 -1
  39. package/src/core/program-status.ts +11 -2
  40. package/src/core/progress.ts +14 -8
  41. package/src/core/project.ts +6 -16
  42. package/src/core/read-bounds.ts +11 -2
  43. package/src/core/read-observation.ts +24 -0
  44. package/src/core/request-budget.ts +13 -3
  45. package/src/core/request-state.ts +178 -19
  46. package/src/core/response-recovery.ts +2 -1
  47. package/src/core/session-entry.ts +8 -3
  48. package/src/core/session-exit.ts +19 -3
  49. package/src/core/session-format.ts +169 -0
  50. package/src/core/settings.ts +7 -27
  51. package/src/core/stall.ts +30 -7
  52. package/src/core/stop-report.ts +4 -3
  53. package/src/core/transcript.ts +22 -2
  54. package/src/core/update-check.ts +40 -0
  55. package/src/runtime/blind-check.ts +36 -0
  56. package/src/runtime/counter-check.ts +74 -0
  57. package/src/runtime/exec-sessions.ts +11 -0
  58. package/src/runtime/foundation.ts +46 -8
  59. package/src/runtime/loop.ts +239 -430
  60. package/src/runtime/oauth.ts +1 -3
  61. package/src/runtime/provider-request.ts +2 -1
  62. package/src/runtime/runtime-target.ts +2 -2
  63. package/src/runtime/self-update.ts +74 -0
  64. package/src/runtime/session-control.ts +4 -4
  65. package/src/runtime/tools.ts +44 -42
  66. package/src/runtime/version.ts +12 -0
  67. package/src/runtime/workspace-lifecycle.ts +31 -0
  68. package/src/store/blind-checks.ts +32 -0
  69. package/src/store/checkpoint-history.ts +1 -12
  70. package/src/store/file-index.ts +88 -7
  71. package/src/store/log.ts +25 -27
  72. package/src/store/recovery.ts +1 -3
  73. package/src/store/replay.ts +2 -2
  74. package/src/store/restore.ts +10 -202
  75. package/src/store/search.ts +7 -5
  76. package/src/store/session-archive.ts +0 -1
  77. package/src/store/sessions.ts +31 -33
  78. package/src/store/workspace-clone.ts +42 -0
  79. package/src/store/workspace.ts +10 -157
  80. package/src/tui/ui/screen.ts +5 -4
  81. package/src/tui/ui/status.ts +6 -2
  82. package/src/core/benchmark.ts +0 -195
  83. package/src/core/evaluation-coverage.ts +0 -74
  84. package/src/core/experiment.ts +0 -173
  85. package/src/core/harness-comparison.ts +0 -90
  86. package/src/core/harness-diagnostics.ts +0 -206
  87. package/src/core/native-eval.ts +0 -25
  88. package/src/core/policy-evaluation.ts +0 -55
  89. package/src/core/query-eval-contract.ts +0 -14
  90. package/src/core/recall-analysis.ts +0 -90
  91. package/src/core/recall-trial.ts +0 -153
  92. package/src/core/recovery-corpus.ts +0 -17
  93. package/src/core/recovery-evaluation.ts +0 -60
  94. package/src/core/tool-mode-protocol.ts +0 -25
  95. package/src/core/trial-lifecycle.ts +0 -14
  96. package/src/core/window-trial.ts +0 -65
  97. package/src/index.ts +0 -10
  98. package/src/runtime/benchmark.ts +0 -187
  99. package/src/runtime/harness-comparison.ts +0 -376
  100. package/src/runtime/harness-eval.ts +0 -462
  101. package/src/runtime/native-eval-fixtures.ts +0 -33
  102. package/src/runtime/native-eval.ts +0 -146
  103. package/src/runtime/query-eval-verifier.ts +0 -28
  104. package/src/runtime/recall-trial.ts +0 -226
  105. package/src/runtime/recovery-coverage.ts +0 -71
  106. package/src/runtime/recovery-evidence.ts +0 -76
  107. package/src/runtime/recovery-fixtures.ts +0 -116
  108. package/src/runtime/tool-mode-eval.ts +0 -146
  109. package/src/runtime/trial-control.ts +0 -45
  110. package/src/runtime/window-trial.ts +0 -141
  111. package/src/store/git-branch.ts +0 -35
  112. package/src/tui/ui/testing.ts +0 -5
@@ -12,15 +12,27 @@ import { LspManager, parseLspConfig } from "./lsp-client.ts";
12
12
  import { validateFormatters } from "./formatter.ts";
13
13
  import { createFormatterTool } from "./formatter-tool.ts";
14
14
  import { permissionResourceAction } from "./permission-resources.ts";
15
- import type { ToolContext, ToolDefinition, ToolResult } from "./tools.ts";
15
+ import type { InstructionCheck, ToolContext, ToolDefinition } from "./tools.ts";
16
16
  import { McpManager } from "./mcp-client.ts";
17
17
  import { parseMcpServers } from "../core/mcp-config.ts";
18
18
  import type { ExtensionRegistry } from "./extensions.ts";
19
19
  import { join } from "node:path";
20
20
  import { getBlob } from "../store/blobs.ts";
21
21
 
22
+ /**
23
+ * Calls that only observe run and carry newly discovered rules (S.6 item 3:
24
+ * OpenCode, Gemini CLI and Claude Code do the same). A change is still held
25
+ * back until the rules have been read: the reason for blocking first was never
26
+ * to notify after a modification (foundation-editing research).
27
+ */
28
+ const OBSERVING_TOOLS = new Set(["read", "query", "lsp"]);
29
+ /** Assumed: more batches than any interleaving of live sessions sharing this runtime. */
30
+ const RECENT_BATCHES = 16;
31
+
22
32
  export class FoundationRuntime {
23
33
  readonly questionSlot: QuestionSlot;
34
+ /** Per batch (assistant event id): instruction key -> the call that carried it. */
35
+ private readonly batches = new Map<string, Map<string, string>>();
24
36
  readonly mcp: McpManager;
25
37
  readonly tools: ToolDefinition[];
26
38
  readonly permissions: PermissionRule[];
@@ -47,14 +59,32 @@ export class FoundationRuntime {
47
59
  const discovered = await this.mcp.discover();
48
60
  for (const integration of discovered.integrations) registry.api("xeno-mcp").registerIntegration(integration);
49
61
  }
50
- async instructions(ctx: ToolContext, name: string, input: unknown, noContextFiles: boolean): Promise<ToolResult | null> {
62
+ /** Calls are prepared one at a time in their batch, so "first to discover carries the rules" is deterministic. */
63
+ private claims(batch: string): Map<string, string> {
64
+ let claimed = this.batches.get(batch);
65
+ if (claimed === undefined) {
66
+ claimed = new Map();
67
+ this.batches.set(batch, claimed);
68
+ for (const old of this.batches.keys()) if (this.batches.size > RECENT_BATCHES) this.batches.delete(old);
69
+ }
70
+ return claimed;
71
+ }
72
+ async instructions(ctx: ToolContext, name: string, input: unknown, noContextFiles: boolean): Promise<InstructionCheck | null> {
51
73
  if (noContextFiles || !["read", "write", "edit", "apply_patch", "query", "format", "lsp"].includes(name) || !input || typeof input !== "object") return null;
52
74
  const args = input as Record<string, unknown>;
53
75
  const paths = name === "apply_patch" ? patchPaths(parsePatch(String(args["patchText"]))) : typeof args["path"] === "string" ? [args["path"]] : Array.isArray(args["paths"]) ? args["paths"].filter((path): path is string => typeof path === "string") : [];
54
76
  if (!paths.length) return null;
77
+ const blocked = (content: string): InstructionCheck => ({ kind: "block", result: { ok: false, content } });
55
78
  const known = new Set<string>();
56
- for (const event of await ctx.historyEvents?.() ?? []) {
57
- if (event.t !== "tool_result") continue;
79
+ const history = await ctx.historyEvents?.() ?? [];
80
+ // Rules count as read only once the model has seen them: results of this
81
+ // same batch may already be committed, but they reach the model together
82
+ // with this call's result. Within the batch, claims decide instead.
83
+ const batch = history.findLast(event => event.t === "assistant");
84
+ const sameBatch = new Set(batch?.t === "assistant" ? batch.blocks.flatMap(block => block.type === "tool_use" ? [block.call_id] : []) : []);
85
+ for (const event of history) {
86
+ if (event.t !== "tool_result" || sameBatch.has(event.call_id)) continue;
87
+ for (const key of event.instructions?.keys ?? []) known.add(key);
58
88
  let data: unknown = event.data;
59
89
  if (event.data_ref && ctx.outputStore) {
60
90
  const saved = await getBlob(ctx.outputStore, event.data_ref.sha256);
@@ -67,12 +97,20 @@ export class FoundationRuntime {
67
97
  }
68
98
  const found = await discoverTargetInstructions({ workspace: ctx.root, paths, known,
69
99
  canRead: async path => await permissionResourceAction(ctx.root, path, ctx.rules ?? [], ctx.sessionRules ?? []) === "allow" });
70
- if (found.notices.some(notice => notice.kind === "skipped" && notice.reason === "instruction read permission is unavailable")) return { ok: false, content: "Scoped instructions require read permission before this operation." };
100
+ if (found.notices.some(notice => notice.kind === "skipped" && notice.reason === "instruction read permission is unavailable")) return blocked("Scoped instructions require read permission before this operation.");
71
101
  const files = found.files.filter(file => !ctx.frozenSystem?.().includes(file.content));
72
102
  if (!files.length) return null;
73
- for (const file of files) if (await permissionResourceAction(ctx.root, file.path, ctx.rules ?? [], ctx.sessionRules ?? []) !== "allow") return { ok: false, content: "Scoped instructions require read permission before this operation." };
74
- return { ok: false, content: "Scoped instructions were discovered. Read and follow them before deciding whether to retry the original operation. No original operation was executed.\n\n" + renderTargetInstructions(files),
75
- data: { kind: "scoped_instructions", keys: files.map(file => file.key), files: files.map(file => ({ path: file.path, scope: file.scope, key: file.key, content: file.content, truncated: file.truncated })) } };
103
+ for (const file of files) if (await permissionResourceAction(ctx.root, file.path, ctx.rules ?? [], ctx.sessionRules ?? []) !== "allow") return blocked("Scoped instructions require read permission before this operation.");
104
+ // Once per batch: a later call in the same assistant message does not receive the same rules again.
105
+ const claimed = this.claims(batch?.id ?? "");
106
+ const fresh = files.filter(file => !claimed.has(file.key));
107
+ const carriers = [...new Set(files.flatMap(file => claimed.get(file.key) ?? []))];
108
+ for (const file of fresh) claimed.set(file.key, ctx.callId ?? name);
109
+ if (OBSERVING_TOOLS.has(name)) return fresh.length ? { kind: "attach", instructions: { keys: fresh.map(file => file.key), text: renderTargetInstructions(fresh) } } : null;
110
+ const earlier = carriers.length ? `Scoped instructions for this target were attached to the result of ${carriers.join(", ")} in this batch. ` : "";
111
+ if (!fresh.length) return blocked(`${earlier}No original operation was executed. Follow them, then retry.`);
112
+ return { kind: "block", result: { ok: false, content: `Scoped instructions were discovered. ${earlier}Read and follow them before deciding whether to retry the original operation. No original operation was executed.\n\n` + renderTargetInstructions(fresh),
113
+ data: { kind: "scoped_instructions", keys: fresh.map(file => file.key), files: fresh.map(file => ({ path: file.path, scope: file.scope, key: file.key, content: file.content, truncated: file.truncated })) } } };
76
114
  }
77
115
  async dispose(): Promise<void> { await Promise.all([this.lsp.dispose(), this.mcp.dispose()]); }
78
116
  }