@visual-z/xeno 0.7.2 → 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.
- package/CHANGELOG.md +43 -0
- package/README.md +36 -9
- package/docs/foundations.md +1 -1
- package/package.json +3 -1
- package/src/apps/cli.ts +4 -3
- package/src/apps/commands.ts +8 -0
- package/src/apps/goal-flow.ts +70 -0
- package/src/apps/render.ts +0 -2
- package/src/apps/session-hub.ts +6 -5
- package/src/apps/session-lifecycle.ts +64 -0
- package/src/apps/shell.ts +89 -34
- package/src/apps/startup.ts +32 -31
- package/src/apps/ui/app.ts +38 -6
- package/src/apps/ui/control-model.ts +18 -1
- package/src/apps/ui/surface.ts +2 -0
- package/src/apps/update-check.ts +1 -1
- package/src/core/acceptance.ts +41 -9
- package/src/core/blind-checks.ts +72 -0
- package/src/core/branch-receipt.ts +3 -1
- package/src/core/checkpoints.ts +21 -0
- package/src/core/churn.ts +2 -2
- package/src/core/completion-review.ts +18 -11
- package/src/core/continuation.ts +11 -10
- package/src/core/control-view.ts +5 -8
- package/src/core/deadline.ts +2 -1
- package/src/core/derive.ts +0 -7
- package/src/core/environment.ts +2 -2
- package/src/core/event-validation.ts +19 -6
- package/src/core/events.ts +41 -32
- package/src/core/evidence-ladder.ts +62 -0
- package/src/core/evidence-links.ts +0 -10
- package/src/core/evidence.ts +1 -2
- package/src/core/export.ts +6 -11
- package/src/core/goal.ts +200 -0
- package/src/core/history.ts +1 -11
- package/src/core/json-event.ts +0 -6
- package/src/core/outcome.ts +0 -1
- package/src/core/program-status.ts +11 -2
- package/src/core/progress.ts +14 -8
- package/src/core/project.ts +6 -16
- package/src/core/read-bounds.ts +11 -2
- package/src/core/read-observation.ts +24 -0
- package/src/core/request-budget.ts +13 -3
- package/src/core/request-state.ts +178 -19
- package/src/core/response-recovery.ts +2 -1
- package/src/core/session-entry.ts +8 -3
- package/src/core/session-exit.ts +19 -3
- package/src/core/session-format.ts +169 -0
- package/src/core/settings.ts +7 -27
- package/src/core/stall.ts +30 -7
- package/src/core/stop-report.ts +4 -3
- package/src/core/transcript.ts +22 -2
- package/src/runtime/blind-check.ts +36 -0
- package/src/runtime/counter-check.ts +74 -0
- package/src/runtime/exec-sessions.ts +11 -0
- package/src/runtime/foundation.ts +46 -8
- package/src/runtime/loop.ts +239 -430
- package/src/runtime/oauth.ts +1 -3
- package/src/runtime/provider-request.ts +2 -1
- package/src/runtime/runtime-target.ts +2 -2
- package/src/runtime/session-control.ts +4 -4
- package/src/runtime/tools.ts +44 -42
- package/src/runtime/version.ts +12 -0
- package/src/runtime/workspace-lifecycle.ts +31 -0
- package/src/store/blind-checks.ts +32 -0
- package/src/store/checkpoint-history.ts +1 -12
- package/src/store/file-index.ts +88 -7
- package/src/store/log.ts +25 -27
- package/src/store/recovery.ts +1 -3
- package/src/store/replay.ts +2 -2
- package/src/store/restore.ts +10 -202
- package/src/store/search.ts +7 -5
- package/src/store/session-archive.ts +0 -1
- package/src/store/sessions.ts +31 -33
- package/src/store/workspace-clone.ts +42 -0
- package/src/store/workspace.ts +10 -157
- package/src/tui/ui/status.ts +6 -2
- package/src/core/benchmark.ts +0 -195
- package/src/core/evaluation-coverage.ts +0 -74
- package/src/core/experiment.ts +0 -173
- package/src/core/harness-comparison.ts +0 -90
- package/src/core/harness-diagnostics.ts +0 -206
- package/src/core/native-eval.ts +0 -25
- package/src/core/policy-evaluation.ts +0 -55
- package/src/core/query-eval-contract.ts +0 -14
- package/src/core/recall-analysis.ts +0 -90
- package/src/core/recall-trial.ts +0 -153
- package/src/core/recovery-corpus.ts +0 -17
- package/src/core/recovery-evaluation.ts +0 -60
- package/src/core/tool-mode-protocol.ts +0 -25
- package/src/core/trial-lifecycle.ts +0 -14
- package/src/core/window-trial.ts +0 -65
- package/src/index.ts +0 -10
- package/src/runtime/benchmark.ts +0 -187
- package/src/runtime/harness-comparison.ts +0 -376
- package/src/runtime/harness-eval.ts +0 -462
- package/src/runtime/native-eval-fixtures.ts +0 -33
- package/src/runtime/native-eval.ts +0 -146
- package/src/runtime/query-eval-verifier.ts +0 -28
- package/src/runtime/recall-trial.ts +0 -226
- package/src/runtime/recovery-coverage.ts +0 -71
- package/src/runtime/recovery-evidence.ts +0 -76
- package/src/runtime/recovery-fixtures.ts +0 -116
- package/src/runtime/tool-mode-eval.ts +0 -146
- package/src/runtime/trial-control.ts +0 -45
- package/src/runtime/window-trial.ts +0 -141
- package/src/store/git-branch.ts +0 -35
- package/src/tui/ui/testing.ts +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# xeno changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 — 2026-10-10
|
|
4
|
+
|
|
5
|
+
xeno now proves completion with receipts, and proves the receipts test the change. Two changes need attention when upgrading:
|
|
6
|
+
- Sessions saved by 0.7.x and earlier no longer open. They are left on disk unmodified; start a new session with `--fresh`.
|
|
7
|
+
- A message no longer stops after 40 model requests by default. The stall watchdog ends a run that stops producing new evidence; pass `--max-turns N` for a hard cap.
|
|
8
|
+
|
|
9
|
+
Verification-first (research doc appendix S):
|
|
10
|
+
- `/goal <objective>` sets an explicit goal.
|
|
11
|
+
- The model drafts acceptance criteria in one read-only request: it may read the workspace, but cannot change it or run commands. You accept the criteria one by one.
|
|
12
|
+
- A draft that does not parse goes back to the model once with the reasons. A goal that cannot be checked here is reported with the model's reason, and no goal is set.
|
|
13
|
+
- Work continues until every accepted check has a fresh passing receipt.
|
|
14
|
+
- A check that fails the same way twice against an unchanged workspace is reported as possibly broken instead of being rerun. An accepted check is never rewritten.
|
|
15
|
+
- A reported blocker, a spent budget, the stall watchdog, or two goal checks answered without acting stop the loop and leave the goal active.
|
|
16
|
+
- `/goal` shows its status and `/goal clear` ends it.
|
|
17
|
+
- Passing goal checks are counter-checked. Each one is re-run in a copy-on-write clone with the request's changes undone, including changes made by shell commands; the undo comes from a snapshot taken when the goal request starts. A check that still passes is named as not testing the change.
|
|
18
|
+
- Guards: a command criterion with `"guard": true` protects what the goal must not break, such as "the tests are unchanged". It must pass before and after the work, and the counter-check expects it to pass without the changes too.
|
|
19
|
+
- Drafts propose guards, and the checklist and `/goal` mark them.
|
|
20
|
+
- A guard that held no longer counts as a check that does not test the change.
|
|
21
|
+
- `--blind-checks <dir>` adds hidden checks from outside the workspace. They run on a clone of the final result after the goal's checks pass, and a pass the model could have seen is reported as exposed.
|
|
22
|
+
- Every request now shows its evidence ladder in the sidebar and `/control`: ran, tests the change, independent, human accepted.
|
|
23
|
+
- Without a contract, a request now reports `not_applicable`, `unchecked`, or `checked` after its last change, instead of a constant `unverified`.
|
|
24
|
+
- A blocker reported by the model is listed but no longer changes the status or skips the completion check.
|
|
25
|
+
- A failed `/resume` no longer leaves the current session's integrations closed.
|
|
26
|
+
- xeno's state directory ignores itself, so `git status` and scope checks no longer see session files.
|
|
27
|
+
|
|
28
|
+
From a session review (research doc appendix S.6):
|
|
29
|
+
- There is no request cap per message by default, as in Claude Code, Codex, Gemini CLI and OpenCode. The stall watchdog stops a run after 12 exchanges without new evidence. `--max-turns N` and agent budgets still set a hard cap with a reserved final answer.
|
|
30
|
+
- A command that exits nonzero but prints something new counts as progress. An exchange that repeats the previous one exactly earns nothing from output that only changes with timing.
|
|
31
|
+
- A read, query or lsp call under a newly found subdirectory AGENTS.md runs, and the rules come with its result instead of costing a retry. Changes are still held back until the rules have been read. A batch of calls gets each file once.
|
|
32
|
+
- grep and find skip git-ignored files inside a Git work tree and are no longer cut off after the first 20,000 files. A path that names an ignored directory is still searched.
|
|
33
|
+
- A read too large for one result stops at a line boundary and names the next line. It no longer hides the middle while claiming the end of the file.
|
|
34
|
+
- An out-of-range `yield_time_ms` is clamped to 100–30000 ms instead of refusing the command, and the receipt names the wait actually taken.
|
|
35
|
+
- Exiting prints `resume with: xeno --resume <id>` instead of the session's full path when that id resolves from the current directory.
|
|
36
|
+
|
|
37
|
+
Breaking:
|
|
38
|
+
- Sessions written by earlier versions are refused with their location and are left unmodified. Start a new session with `--fresh`.
|
|
39
|
+
- Full-snapshot workspace tracking is gone; journaled checkpoints are the only file history.
|
|
40
|
+
|
|
41
|
+
Maintenance:
|
|
42
|
+
- Evaluation code moved to `eval/` and out of the npm package (351 → 322 files) and the default gate.
|
|
43
|
+
- The version now lives only in `package.json`.
|
|
44
|
+
- Eight settings that nothing applied were removed.
|
|
45
|
+
|
|
3
46
|
## 0.7.2 — 2026-10-10
|
|
4
47
|
|
|
5
48
|
- `/update` now installs the newer release when xeno was installed with bun or npm, instead of only printing the command. The status line shows the target version, elapsed time against a 3-minute limit and the package manager's latest output; success asks for a restart, and a failure gives the reason and the command to run by hand. Automatic checks still never install, and `xeno update --check` still only checks.
|
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ is ever summarized by a model, and nothing is ever rewritten in place.
|
|
|
17
17
|
messages[] frozen system prefix + handoff + this node's events
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
The on-disk session format will still change; `0.x` is that statement, and
|
|
21
|
+
`xeno --version` reports the installed release. Published as `@visual-z/xeno`; the license is `UNLICENSED` (readable and installable, no reuse granted).
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
@@ -72,7 +72,7 @@ checks are reported separately from model accuracy.
|
|
|
72
72
|
The explicit-policy follow-up separates segmented, continuous and adaptive context
|
|
73
73
|
views. Its budget-limited result and negative cases are recorded in the same report;
|
|
74
74
|
neither segmentation nor the experimental adaptive rule has established a quality advantage.
|
|
75
|
-
`bun run scripts/compare-harness.ts smoke --output /absolute/new/smoke-directory`
|
|
75
|
+
`bun run eval/scripts/compare-harness.ts smoke --output /absolute/new/smoke-directory`
|
|
76
76
|
also exercises xeno-windowed, xeno-continuous and an installed Pi CLI with shared
|
|
77
77
|
confined tools, without paid model calls. The comparison Pi CLI runs on Node through
|
|
78
78
|
an isolated local tool bridge; xeno's providers use the pinned Pi AI runtime described below.
|
|
@@ -85,8 +85,8 @@ This checks a controlled configuration, not stock Pi or an official leaderboard.
|
|
|
85
85
|
The new single-factor protocol separately compares tool profiles, the carried
|
|
86
86
|
working set, and strict verification. It runs four isolated scenarios through
|
|
87
87
|
real sessions and grades files, fresh command evidence, and pending human review.
|
|
88
|
-
Use `bun scripts/eval-harness.ts smoke --output /absolute/new/eval-directory`,
|
|
89
|
-
then `bun scripts/eval-harness.ts audit --output /absolute/new/eval-directory`.
|
|
88
|
+
Use `bun eval/scripts/eval-harness.ts smoke --output /absolute/new/eval-directory`,
|
|
89
|
+
then `bun eval/scripts/eval-harness.ts audit --output /absolute/new/eval-directory`.
|
|
90
90
|
Smoke has no paid requests and reports synthetic usage. `plan` and `run` require
|
|
91
91
|
the same explicit numeric `--budget`; inputs, source, schemas, model settings,
|
|
92
92
|
and paired order are frozen before spending. Started directories cannot be
|
|
@@ -123,11 +123,34 @@ xeno [-m <model>] [--provider <name>] [--api-key <key>]
|
|
|
123
123
|
verification state and diagnostic progress across segment cuts and restarts.
|
|
124
124
|
Short tasks need no `note` calls to associate evidence. Without a declared
|
|
125
125
|
acceptance contract, successful commands remain evidence rather than verified
|
|
126
|
-
task completion.
|
|
126
|
+
task completion. A command's new output counts as progress for the stall
|
|
127
|
+
watchdog whatever its exit code; an exchange that repeats the previous one
|
|
128
|
+
exactly earns none from output that only changes with timing.
|
|
129
|
+
- `/goal <objective>` sets an explicit goal:
|
|
130
|
+
- The model first drafts acceptance criteria in a single read-only request: it may read the workspace (read, find, grep) but cannot change it or run commands. Each criterion is a command that should fail until the goal is done, a human check, or a guard.
|
|
131
|
+
- A guard (`"guard": true` on a command) protects what the goal must not break, such as "the test file is unchanged" or "only plan.md changed". It passes before the work and must still pass after it. Guards are marked in the checklist.
|
|
132
|
+
- A draft that does not parse goes back to the model once with the reasons. If the model says the goal cannot be checked in this workspace, it gives the reason and no goal is set.
|
|
133
|
+
- You accept the criteria one by one: a checklist in the TUI, or `/goal accept all|1,3` in line mode.
|
|
134
|
+
- Work then starts from the objective and continues automatically until every accepted criterion has a fresh passing receipt (and human criteria have your `/accept`).
|
|
135
|
+
- It stops early, with the goal still active, when the model reports a blocker, answers two goal checks without acting, a `--max-turns` budget runs out, or the stall watchdog fires.
|
|
136
|
+
- `/goal` shows the goal and the status of each criterion; `/goal clear` ends it.
|
|
137
|
+
- "Met" comes from receipts and your decisions, never from the model saying it is done.
|
|
138
|
+
- When the accepted checks pass, each one is counter-checked: it is re-run in a disposable copy-on-write clone of the workspace with this request's file changes undone.
|
|
139
|
+
- It reports whether the check fails without the changes (it tests them) or still passes (it does not).
|
|
140
|
+
- A guard is expected to pass there. If it does, it "held" and does not count against the evidence ladder. If it fails there, it "did not hold": the check actually tests the change, and it counts as such.
|
|
141
|
+
- If every check is a guard that held, the ladder says that no check tests the change.
|
|
142
|
+
- The undo comes from a snapshot taken when the goal request starts (a private Git directory and index; your HEAD, index and stash are untouched), so changes made by shell commands are undone too.
|
|
143
|
+
- If the starting state cannot be rebuilt exactly, the result is "unavailable", for example outside a Git work tree when a shell command wrote files that no checkpoint recorded.
|
|
144
|
+
- The real workspace is never touched, and the counter-check never blocks the goal.
|
|
145
|
+
- `--blind-checks <dir>` adds hidden checks: a directory outside the workspace holding `blind.json` (`{"version":1,"checks":[{"id","description","command"}]}`) and any fixtures.
|
|
146
|
+
- The checks are frozen when the session starts and never shown to the model.
|
|
147
|
+
- When the goal's checks pass, they run against a clone of the final workspace, with the directory copied in as `.xeno-blind/`.
|
|
148
|
+
- The whole log is scanned first, and a pass after the model named the directory or read one of its files is reported as `exposed`, not independent.
|
|
127
149
|
- `/acceptance` shows declared command checks and human criteria. `/accept ID`
|
|
128
150
|
records your acceptance of a human criterion; subsequent edits invalidate it.
|
|
129
151
|
Use `--acceptance-file` or a trusted project's `.xeno/acceptance.json` with
|
|
130
152
|
`{ "version": 1, "id": "task", "criteria": [{ "id": "tests", "kind": "command", "description": "tests pass", "command": "bun test" }] }`.
|
|
153
|
+
Add `"guard": true` to a command criterion that must keep passing rather than one that shows the work was done.
|
|
131
154
|
The declaration is frozen in the session. A normal execution exit does not
|
|
132
155
|
imply that human acceptance has been granted.
|
|
133
156
|
A contract may additionally declare `targets`, each with `id`, `build`
|
|
@@ -372,7 +395,9 @@ Shipped, not planned:
|
|
|
372
395
|
`branch`, `resolve`, `grep`, `find`, `ls`, `history`, `query`. `grep` / `find` use `rg` /
|
|
373
396
|
`fd` when those binaries are on `PATH`, otherwise a TypeScript walk;
|
|
374
397
|
both paths share one output format. They do not follow symlinks out of
|
|
375
|
-
the workspace.
|
|
398
|
+
the workspace. Inside a Git work tree they skip git-ignored files, as
|
|
399
|
+
Claude Code, OpenCode and Pi do; a path that names an ignored directory is
|
|
400
|
+
still searched.
|
|
376
401
|
- `history` searches the active ancestry, retrieves original events, and pages
|
|
377
402
|
immutable full observations. `query` filters and computes aggregates over
|
|
378
403
|
complete CSV, JSON, JSONL or text files, with source hashes. It evaluates a
|
|
@@ -506,7 +531,9 @@ An empty `write_stdin` in an agent session waits for new output, command exit,
|
|
|
506
531
|
hard timeout, cancellation or user steering. The process owner holds that wait;
|
|
507
532
|
quiet commands do not consume model requests through periodic polling.
|
|
508
533
|
Sending input and low-level process polling keep their bounded yield contract.
|
|
509
|
-
|
|
534
|
+
There is no request cap by default, as in Claude Code, Codex, Gemini CLI and
|
|
535
|
+
OpenCode: the stall watchdog stops a run after 12 exchanges without new
|
|
536
|
+
evidence. For `--max-turns N`, budgets above one reserve the last request for a text answer.
|
|
510
537
|
Tools stay frozen and are disabled for that request, with host enforcement.
|
|
511
538
|
Unsupported protocols or a failed final response use an evidence-based stop
|
|
512
539
|
report inside the same budget. `/usage` labels provider durations as model time.
|
|
@@ -632,7 +659,7 @@ diagnostic and 100 consecutive derivations with retained state. The cost formula
|
|
|
632
659
|
models input caching at the stated rates; it omits task quality and cannot prove
|
|
633
660
|
30k/40k is globally optimal. These legacy bounds retain their 5k work floor.
|
|
634
661
|
|
|
635
|
-
`scripts/ab-suite.ts` now separates node, replay, round-preserving truncation and
|
|
662
|
+
`eval/scripts/ab-suite.ts` now separates node, replay, round-preserving truncation and
|
|
636
663
|
observation masking. It uses a seeded shuffled schedule and includes output cost
|
|
637
664
|
and failed outcomes. Historical A/B figures predate these corrections and cannot
|
|
638
665
|
establish superiority over the corrected baselines. No new live-model result is
|
package/docs/foundations.md
CHANGED
|
@@ -71,7 +71,7 @@ apply_patch 参数为 `{patchText,expected_sha256?}`,语法使用 `*** Begin P
|
|
|
71
71
|
|
|
72
72
|
format 参数为 `{paths:[...]}`,只执行配置中的 literal argv;`$FILE` 替换为绝对路径。每条命令经真实拥有者、审批、退出码与检查点执行,不会在 edit 后暗中格式化。读到新的文件版本后再生成后续编辑。
|
|
73
73
|
|
|
74
|
-
启动从 Git 根到 cwd 加载目录规范。首次碰到目标文件的更深层 AGENTS.md / AGENTS.override.md
|
|
74
|
+
启动从 Git 根到 cwd 加载目录规范。首次碰到目标文件的更深层 AGENTS.md / AGENTS.override.md 时:read / query / lsp 照常执行,规范(带来源和 scope)附在同一个结果之后;write / edit / apply_patch / format 先返回指令回执,原操作未执行,模型读到后可显式重试。同一批调用里一份规范只下发一次,后到的修改只指向先附上规范的那次调用。规范追加到原始日志并按内容指纹去重,保留冻结前缀和旧事件。
|
|
75
75
|
|
|
76
76
|
## Diff、Review 与 LSP
|
|
77
77
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@visual-z/xeno",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Segmented append-only agent log with pure context and handoff projections",
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
"typecheck": "bun run scripts/typecheck.ts",
|
|
33
33
|
"check": "bun run scripts/check-boundaries.ts",
|
|
34
34
|
"gate": "bun test && bun run typecheck && bun run scripts/check-boundaries.ts",
|
|
35
|
+
"eval:test": "bun test ./eval/test",
|
|
36
|
+
"gate:full": "bun run gate && bun run eval:test",
|
|
35
37
|
"verify-red": "bun run scripts/verify-red.ts",
|
|
36
38
|
"regen-golden": "bun run scripts/regen-golden.ts",
|
|
37
39
|
"measure": "bun run scripts/measure-handoff.ts",
|
package/src/apps/cli.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { sessionControl } from "../runtime/session-control.ts";
|
|
|
24
24
|
import { listenControl } from "./control-transport.ts";
|
|
25
25
|
import { runAppServer } from "./app-server.ts";
|
|
26
26
|
import { runManualUpdate } from "./update-check.ts";
|
|
27
|
+
import { XENO_VERSION } from "../runtime/version.ts";
|
|
28
|
+
import { closeAppSession } from "./session-lifecycle.ts";
|
|
27
29
|
import { runAcpServer } from "./acp-server.ts";
|
|
28
30
|
import { runReview, reviewScopeFromText } from "./review.ts";
|
|
29
31
|
|
|
@@ -42,8 +44,7 @@ try {
|
|
|
42
44
|
printCliHelp();
|
|
43
45
|
} else if (args.version) {
|
|
44
46
|
if (args.extensionArgv) throw new Error("Unknown flags for --version");
|
|
45
|
-
|
|
46
|
-
console.log(pkg.version);
|
|
47
|
+
console.log(XENO_VERSION);
|
|
47
48
|
} else if (args.exportFile) {
|
|
48
49
|
if (args.extensionArgv) throw new Error("Unknown flags for offline export");
|
|
49
50
|
console.log(`Exported ${await exportHtmlFile(args.exportFile, args.prompt || undefined, args.workspace)}`);
|
|
@@ -70,7 +71,7 @@ try {
|
|
|
70
71
|
} else if (reviewMode) {
|
|
71
72
|
input.restore();
|
|
72
73
|
try { await runReview(app, reviewScopeFromText(args.prompt)); }
|
|
73
|
-
finally { await
|
|
74
|
+
finally { await closeAppSession(app); }
|
|
74
75
|
} else {
|
|
75
76
|
if (args.print) {
|
|
76
77
|
// With a prompt already given, stdin is optional context: wait briefly for it.
|
package/src/apps/commands.ts
CHANGED
|
@@ -31,6 +31,8 @@ export type Command =
|
|
|
31
31
|
| { kind: "branches"; action?: "wait" | "cancel" | "status"; node?: number }
|
|
32
32
|
| { kind: "processes" }
|
|
33
33
|
| { kind: "accept"; criterion: string }
|
|
34
|
+
| { kind: "goal"; action: "status" } | { kind: "goal"; action: "clear" } | { kind: "goal"; action: "accept"; pick: string }
|
|
35
|
+
| { kind: "goal"; action: "set"; objective: string }
|
|
34
36
|
| { kind: "model"; pattern?: string }
|
|
35
37
|
| { kind: "scoped-models" }
|
|
36
38
|
| { kind: "login"; provider?: string }
|
|
@@ -85,6 +87,7 @@ export const COMMANDS = [
|
|
|
85
87
|
{ name: "branches", description: "Inspect or join owned background work", argumentHint: "[wait|cancel|status node]", busy: "idle" },
|
|
86
88
|
{ name: "processes", description: "Show process observations and available handles", busy: "run" },
|
|
87
89
|
{ name: "accept", description: "Record your acceptance of a human criterion", argumentHint: "<criterion ID>", busy: "idle" },
|
|
90
|
+
{ name: "goal", description: "Set a goal: the model drafts checks, you accept them, work continues until they pass", argumentHint: "<objective> | clear | accept all|1,3", busy: "idle" },
|
|
88
91
|
{ name: "export", description: "Export HTML or JSONL", argumentHint: "[file]", busy: "idle" },
|
|
89
92
|
{ name: "import", description: "Import a portable JSONL session", argumentHint: "<file>", busy: "idle" },
|
|
90
93
|
{ name: "debug", description: "Write local terminal diagnostics", argumentHint: "[file]", busy: "run" },
|
|
@@ -213,6 +216,11 @@ export function parseCommand(input: string, catalog?: SlashCatalog): Command {
|
|
|
213
216
|
? { kind: "branches", action, node } : { kind: "unknown", input: text };
|
|
214
217
|
}
|
|
215
218
|
case "/accept": return { kind: "accept", criterion: arg };
|
|
219
|
+
case "/goal":
|
|
220
|
+
if (!arg) return { kind: "goal", action: "status" };
|
|
221
|
+
if (arg === "clear") return { kind: "goal", action: "clear" };
|
|
222
|
+
if (rest[0] === "accept") return { kind: "goal", action: "accept", pick: rest.slice(1).join(" ") };
|
|
223
|
+
return { kind: "goal", action: "set", objective: arg };
|
|
216
224
|
case "/model":
|
|
217
225
|
return arg.length > 0 ? { kind: "model", pattern: arg } : { kind: "model" };
|
|
218
226
|
case "/provider":
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The drafting half of /goal shared by the line shell and the TUI (S.2 / S.5):
|
|
3
|
+
* one read-only request, then the reply parsed into candidate criteria.
|
|
4
|
+
* Nothing here decides what is accepted; that is the user's pick.
|
|
5
|
+
*/
|
|
6
|
+
import { draftReply, GOAL_DRAFT_RETRIES, goalDraftRequest, goalDraftRetry, parseGoalDraft } from "../core/goal.ts";
|
|
7
|
+
import type { AcceptanceCriterion } from "../core/acceptance.ts";
|
|
8
|
+
import type { RequestOptions } from "../runtime/loop.ts";
|
|
9
|
+
import type { App } from "./startup.ts";
|
|
10
|
+
|
|
11
|
+
export type PendingGoal = { objective: string; draft: string; criteria: AcceptanceCriterion[] };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Runs the draft request and reads the candidates from its reply; null when
|
|
15
|
+
* there is nothing to accept. `runDraft` answers false for a draft that was
|
|
16
|
+
* not sent or was cancelled. A draft that does not validate goes back to the
|
|
17
|
+
* model once with the parser's reasons (GOAL_DRAFT_RETRIES); a reported
|
|
18
|
+
* blocker is an answer and does not.
|
|
19
|
+
*/
|
|
20
|
+
export async function draftGoal(app: App, objective: string,
|
|
21
|
+
runDraft: (prompt: string, options: RequestOptions) => Promise<boolean>): Promise<PendingGoal | null> {
|
|
22
|
+
const C = app.io.palette;
|
|
23
|
+
let prompt = goalDraftRequest(objective);
|
|
24
|
+
for (let attempt = 0; ; attempt++) {
|
|
25
|
+
const mark = app.session.state.events.at(-1)?.id;
|
|
26
|
+
if (!await runDraft(prompt, { readOnly: true })) return null;
|
|
27
|
+
const events = app.session.state.events;
|
|
28
|
+
// Only what this run recorded. A segment cut meanwhile removes the mark, and then the whole segment is new.
|
|
29
|
+
const reply = draftReply(events.slice(events.findIndex(event => event.id === mark) + 1));
|
|
30
|
+
if (!reply) { app.io.say(C.yellow(" the goal draft produced no reply; the goal was not set")); return null; }
|
|
31
|
+
const parsed = parseGoalDraft(reply.text);
|
|
32
|
+
for (const error of parsed.errors) app.io.say(C.yellow(` draft ${error}`));
|
|
33
|
+
if (parsed.errors.length > 0 && !parsed.blocked && attempt < GOAL_DRAFT_RETRIES) {
|
|
34
|
+
app.io.say(C.dim(" the draft goes back to the model once to be corrected"));
|
|
35
|
+
prompt = goalDraftRetry(parsed.errors);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (parsed.criteria.length === 0) {
|
|
39
|
+
app.io.say(C.yellow(parsed.blocked ? ` the model says this goal cannot be checked here: ${parsed.blocked}. The goal was not set`
|
|
40
|
+
: " the draft has no usable criteria; the goal was not set. Run /goal again or rephrase the objective"));
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return { objective, draft: reply.id, criteria: parsed.criteria };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* One drafted criterion as the user decides on it, the same in the line shell
|
|
49
|
+
* and the TUI checklist: what runs, and whether it is a guard -- a check that
|
|
50
|
+
* must keep passing rather than one that shows the work was done (S.7).
|
|
51
|
+
*/
|
|
52
|
+
export function criterionSummary(item: AcceptanceCriterion): string {
|
|
53
|
+
return item.kind === "human" ? `human · ${item.description}` : `${item.guard ? "guard · " : ""}${item.command} · ${item.description}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `all`, or 1-based positions such as `1,3`. Unknown positions are refused, never skipped. */
|
|
57
|
+
export function pickCriteria(criteria: readonly AcceptanceCriterion[], pick: string): { ok: true; ids: string[] } | { ok: false; error: string } {
|
|
58
|
+
const text = pick.trim();
|
|
59
|
+
if (text === "all") return { ok: true, ids: criteria.map(item => item.id) };
|
|
60
|
+
const positions = text.split(/[\s,]+/).filter(Boolean);
|
|
61
|
+
if (positions.length === 0) return { ok: false, error: "name the criteria to accept: /goal accept all, or /goal accept 1,3" };
|
|
62
|
+
const ids: string[] = [];
|
|
63
|
+
for (const position of positions) {
|
|
64
|
+
const index = Number(position);
|
|
65
|
+
if (!Number.isSafeInteger(index) || index < 1 || index > criteria.length) return { ok: false, error: `no drafted criterion ${position}` };
|
|
66
|
+
const id = criteria[index - 1]!.id;
|
|
67
|
+
if (!ids.includes(id)) ids.push(id);
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, ids };
|
|
70
|
+
}
|
package/src/apps/render.ts
CHANGED
package/src/apps/session-hub.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { readCursor, writeCursor } from "../store/log.ts";
|
|
|
6
6
|
import { SessionHub } from "../runtime/session-hub.ts";
|
|
7
7
|
import { sessionControl, type SessionControl } from "../runtime/session-control.ts";
|
|
8
8
|
import { openSessionFor, sessionDirFor, type App } from "./startup.ts";
|
|
9
|
+
import { appSessionFields, closeAppSession } from "./session-lifecycle.ts";
|
|
9
10
|
import { interactiveHooks } from "./shell.ts";
|
|
10
11
|
import { parseMcpServers } from "../core/mcp-config.ts";
|
|
11
12
|
import { reloadAppResources } from "./resources.ts";
|
|
@@ -68,9 +69,8 @@ export function createAppSessionHub(initial: App): SessionHub {
|
|
|
68
69
|
askSlot: {}, catalog: initial.catalog, auth: initial.auth, credentialOf: initial.credentialOf, effects: initial.effects,
|
|
69
70
|
interactive: false, projectTrusted: initial.projectTrusted === true, settings: initial.ui?.settings ?? {},
|
|
70
71
|
fallback: initial.providerConfig });
|
|
71
|
-
const app: App = { ...initial, args, ...opened,
|
|
72
|
-
|
|
73
|
-
foundation: opened.resources.foundation, projectTrusted: opened.resources.trusted, askSlot: opened.session.config.tools.askSlot ?? {} };
|
|
72
|
+
const app: App = { ...initial, args, ...appSessionFields(opened), logDir, askSlot: opened.session.config.tools.askSlot ?? {} };
|
|
73
|
+
if (!opened.resources.foundation) delete app.foundation;
|
|
74
74
|
const control: SessionControl = sessionControl(app.session);
|
|
75
75
|
try {
|
|
76
76
|
app.askSlot.current = request => control.ask(request);
|
|
@@ -79,7 +79,7 @@ export function createAppSessionHub(initial: App): SessionHub {
|
|
|
79
79
|
apps.set(control.threadId, app);
|
|
80
80
|
return control;
|
|
81
81
|
} catch (error) {
|
|
82
|
-
await
|
|
82
|
+
await closeAppSession(app).catch(() => {}); throw error;
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
85
|
const result = construct();
|
|
@@ -89,7 +89,8 @@ export function createAppSessionHub(initial: App): SessionHub {
|
|
|
89
89
|
},
|
|
90
90
|
dispose: async control => {
|
|
91
91
|
const app = apps.get(control.threadId);
|
|
92
|
-
|
|
92
|
+
if (app) await closeAppSession(app); else await control.session.dispose();
|
|
93
|
+
apps.delete(control.threadId);
|
|
93
94
|
},
|
|
94
95
|
});
|
|
95
96
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place an App gains, swaps and drops its session.
|
|
3
|
+
*
|
|
4
|
+
* Startup, the line and full-screen shells, and the app-server hub each used
|
|
5
|
+
* to carry their own copy of this: the field mapping from an opened session,
|
|
6
|
+
* the paired session/extension shutdown, and the swap. The copies drifted --
|
|
7
|
+
* resource reload restored released integrations on failure while a session
|
|
8
|
+
* switch did not, so a refused /resume left the current session's services
|
|
9
|
+
* closed (refactor-session-lifecycle).
|
|
10
|
+
*/
|
|
11
|
+
import type { App, openSessionFor } from "./startup.ts";
|
|
12
|
+
|
|
13
|
+
type Opened = Awaited<ReturnType<typeof openSessionFor>>;
|
|
14
|
+
type SessionFields = Pick<App, "session" | "providerConfig" | "credential" | "workspace" | "blobDir" | "system" | "loadedContext" |
|
|
15
|
+
"slash" | "ui" | "resources" | "projectTrusted" | "extensions" | "foundation">;
|
|
16
|
+
|
|
17
|
+
/** Everything an App holds that belongs to its current session, from one opened session. */
|
|
18
|
+
export function appSessionFields(opened: Opened): SessionFields {
|
|
19
|
+
return {
|
|
20
|
+
session: opened.session, providerConfig: opened.providerConfig, credential: opened.credential,
|
|
21
|
+
workspace: opened.workspace, blobDir: opened.blobDir, system: opened.system,
|
|
22
|
+
loadedContext: opened.resources.loadedContext, slash: opened.resources.slash, ui: opened.resources.ui,
|
|
23
|
+
resources: opened.resources.index, projectTrusted: opened.resources.trusted,
|
|
24
|
+
extensions: opened.resources.extensions,
|
|
25
|
+
...(opened.resources.foundation ? { foundation: opened.resources.foundation } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Session first: its last events may still reach extension hooks. */
|
|
30
|
+
export async function closeAppSession(app: Pick<App, "session" | "extensions">): Promise<void> {
|
|
31
|
+
await app.session.dispose();
|
|
32
|
+
await app.extensions?.dispose();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Moves an App to the session at `logDir`, or leaves it exactly as it was.
|
|
37
|
+
*
|
|
38
|
+
* The new session opens before the current one closes, so a refused or broken
|
|
39
|
+
* target never strands the user without a session. Integrations are released
|
|
40
|
+
* first because a new generation must not overlap an unresolved callback of
|
|
41
|
+
* the current one (Q.13); every failure after that restores them, the same
|
|
42
|
+
* rule reloadAppResources follows.
|
|
43
|
+
*/
|
|
44
|
+
export async function switchAppSession(app: App, logDir: string, open: (app: App, logDir: string) => Promise<Opened>): Promise<void> {
|
|
45
|
+
app.session.cancel();
|
|
46
|
+
await app.session.whenIdle();
|
|
47
|
+
const extensions = app.extensions;
|
|
48
|
+
if (extensions && !await extensions.releaseIntegrations()) {
|
|
49
|
+
extensions.restoreIntegrations();
|
|
50
|
+
throw Error("An integration still owns unfinished work or unconfirmed cleanup. Wait for it to settle before switching sessions");
|
|
51
|
+
}
|
|
52
|
+
let opened: Opened;
|
|
53
|
+
try { opened = await open(app, logDir); }
|
|
54
|
+
catch (error) { extensions?.restoreIntegrations(); throw error; }
|
|
55
|
+
try { await closeAppSession(app); }
|
|
56
|
+
catch (error) {
|
|
57
|
+
// The current session could not close cleanly; do not also strand the new one.
|
|
58
|
+
await closeAppSession({ session: opened.session, extensions: opened.resources.extensions }).catch(() => {});
|
|
59
|
+
extensions?.restoreIntegrations();
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
Object.assign(app, appSessionFields(opened), { logDir });
|
|
63
|
+
if (!opened.resources.foundation) delete app.foundation;
|
|
64
|
+
}
|