@visual-z/xeno 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -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 +105 -13
- 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-view.ts +64 -0
- 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 +15 -2
- 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/openai.ts +2 -2
- package/src/runtime/pi-provider.ts +3 -3
- package/src/runtime/provider-request.ts +2 -1
- package/src/runtime/provider.ts +33 -3
- package/src/runtime/retry.ts +9 -2
- 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/usage.ts +28 -0
- 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/duration.ts +16 -0
- package/src/tui/ui/layout.ts +4 -4
- package/src/tui/ui/screen.ts +13 -7
- package/src/tui/ui/status.ts +99 -17
- 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/src/apps/shell.ts
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* by scripts, while the interactive app projects the durable transcript.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { criterionSummary, draftGoal, pickCriteria, type PendingGoal } from "./goal-flow.ts";
|
|
10
|
+
import { closeAppSession, switchAppSession } from "./session-lifecycle.ts";
|
|
9
11
|
import { basename, dirname, join, resolve } from "node:path";
|
|
10
12
|
import { tmpdir } from "node:os";
|
|
11
13
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
12
14
|
import { assertNever } from "../core/exhaustive.ts";
|
|
15
|
+
import type { CounterVerdict } from "../core/acceptance.ts";
|
|
13
16
|
import type { LogEvent } from "../core/events.ts";
|
|
14
17
|
import type { Skill } from "../core/skills.ts";
|
|
15
18
|
import { workReadiness } from "../core/work-state.ts";
|
|
@@ -29,7 +32,7 @@ import {
|
|
|
29
32
|
type ContextDecision, type ContextLimits, type ContextPolicy, type GaugeLevel,
|
|
30
33
|
} from "../core/context-policy.ts";
|
|
31
34
|
import { anchoredCount, estimateTokens, type Anchor, type TokenCounter } from "../core/tokens.ts";
|
|
32
|
-
import type { RestorePoint, RewindMode } from "../runtime/loop.ts";
|
|
35
|
+
import type { RestorePoint, RewindMode, RequestOptions } from "../runtime/loop.ts";
|
|
33
36
|
import { createProvider, providerSessionId, findModelByPattern, groupedModels, loadCatalog } from "../runtime/catalog.ts";
|
|
34
37
|
import { outputTokenBudget, type Effort, type ModelSpec } from "../runtime/model.ts";
|
|
35
38
|
import { scopedModels } from "../runtime/model-scope.ts";
|
|
@@ -335,10 +338,10 @@ async function fileIndexFor(app: App, refresh = false): Promise<FileIndex> {
|
|
|
335
338
|
|
|
336
339
|
export async function runPrompt(app: App, prompt: string): Promise<void> {
|
|
337
340
|
await bindExtensions(app);
|
|
338
|
-
if (app.shutdownRequested) { await
|
|
341
|
+
if (app.shutdownRequested) { await closeAppSession(app); return; }
|
|
339
342
|
let ok: boolean;
|
|
340
343
|
try { ok = await run(app, prompt); }
|
|
341
|
-
finally { await
|
|
344
|
+
finally { await closeAppSession(app); }
|
|
342
345
|
printUsage(app);
|
|
343
346
|
// One-shot mode is scriptable, so a failed turn has to be a failed exit.
|
|
344
347
|
if (!ok) process.exit(1);
|
|
@@ -387,8 +390,7 @@ async function runLines(app: App): Promise<void> {
|
|
|
387
390
|
if (typeof outcome === "object") awaitingKeyFor = outcome.login;
|
|
388
391
|
}
|
|
389
392
|
await cancelPendingOAuth(app);
|
|
390
|
-
await app
|
|
391
|
-
await app.extensions?.dispose();
|
|
393
|
+
await closeAppSession(app);
|
|
392
394
|
printUsage(app);
|
|
393
395
|
}
|
|
394
396
|
|
|
@@ -408,7 +410,8 @@ async function runTui(app: App, releaseEarlyTty?: () => void): Promise<void> {
|
|
|
408
410
|
*/
|
|
409
411
|
export const interactiveHooks = {
|
|
410
412
|
handleLine: (app: App, text: string, ui: Ui) => handleLine(app, text, ui),
|
|
411
|
-
run: (app: App, prompt: string, signal: AbortSignal | undefined, ui: Ui | undefined, images: readonly ImageRef[] = [], skill?: Skill
|
|
413
|
+
run: (app: App, prompt: string, signal: AbortSignal | undefined, ui: Ui | undefined, images: readonly ImageRef[] = [], skill?: Skill, options?: RequestOptions) =>
|
|
414
|
+
run(app, prompt, signal, ui, images, skill, options),
|
|
412
415
|
parseLine: (app: App, text: string) => parseLine(app, text),
|
|
413
416
|
bindExtensions: (app: App, ui?: Ui) => bindExtensions(app, ui),
|
|
414
417
|
extensionContext: (app: App, ui?: Ui) => extensionContext(app, ui),
|
|
@@ -503,6 +506,9 @@ async function execute(app: App, command: Command, tui: Ui | null): Promise<Outc
|
|
|
503
506
|
await app.session.acceptCriterion(command.criterion);
|
|
504
507
|
await printAcceptance(app);
|
|
505
508
|
return "continue";
|
|
509
|
+
case "goal":
|
|
510
|
+
await goalCommand(app, command, tui ?? undefined);
|
|
511
|
+
return "continue";
|
|
506
512
|
case "help":
|
|
507
513
|
printHelp(app);
|
|
508
514
|
return "continue";
|
|
@@ -693,6 +699,70 @@ async function handleLine(app: App, text: string, tui: Ui): Promise<Outcome> {
|
|
|
693
699
|
return execute(app, parseLine(app, text), tui);
|
|
694
700
|
}
|
|
695
701
|
|
|
702
|
+
/**
|
|
703
|
+
* S.2 / S.5. Line mode keeps a drafted goal until `/goal accept` names the
|
|
704
|
+
* criteria to keep; the TUI asks with a checklist instead.
|
|
705
|
+
*/
|
|
706
|
+
const pendingGoals = new WeakMap<App, PendingGoal>();
|
|
707
|
+
|
|
708
|
+
async function goalCommand(app: App, command: Extract<Command, { kind: "goal" }>, tui?: Ui): Promise<void> {
|
|
709
|
+
const C = app.io.palette;
|
|
710
|
+
switch (command.action) {
|
|
711
|
+
case "status": await printGoal(app); return;
|
|
712
|
+
case "clear": app.io.say(await app.session.clearGoal() ? C.green(" goal cleared") : C.dim(" no active goal")); return;
|
|
713
|
+
case "set": {
|
|
714
|
+
if (tui) { tui.startGoal(command.objective); return; }
|
|
715
|
+
const drafted = await draftGoal(app, command.objective, (prompt, options) => run(app, prompt, undefined, undefined, [], undefined, options));
|
|
716
|
+
if (!drafted) return;
|
|
717
|
+
pendingGoals.set(app, drafted);
|
|
718
|
+
app.io.say(C.bold("\ndrafted criteria") + C.dim(" (accept with /goal accept all, or /goal accept 1,3)"));
|
|
719
|
+
drafted.criteria.forEach((item, index) => app.io.say(` ${index + 1}. ${item.id} · ${criterionSummary(item)}`));
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
case "accept": {
|
|
723
|
+
const drafted = pendingGoals.get(app);
|
|
724
|
+
if (!drafted) { app.io.say(C.yellow(" no drafted goal; run /goal <objective> first")); return; }
|
|
725
|
+
const picked = pickCriteria(drafted.criteria, command.pick);
|
|
726
|
+
if (!picked.ok) { app.io.say(C.yellow(` ${picked.error}`)); return; }
|
|
727
|
+
pendingGoals.delete(app);
|
|
728
|
+
await app.session.setGoal({ ...drafted, accepted: picked.ids });
|
|
729
|
+
app.io.say(C.green(` goal set with ${picked.ids.length} accepted criteria`));
|
|
730
|
+
await run(app, drafted.objective);
|
|
731
|
+
await printGoal(app);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
default: return assertNever(command, "goal command");
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function printGoal(app: App): Promise<void> {
|
|
739
|
+
const C = app.io.palette, goal = await app.session.goal();
|
|
740
|
+
if (!goal) { app.io.say(C.dim(" no active goal; /goal <objective> drafts one")); return; }
|
|
741
|
+
const request = await app.session.requestState();
|
|
742
|
+
const current = request?.acceptance?.contract_id === goal.contract.id ? request : undefined;
|
|
743
|
+
app.io.say(C.bold("\ngoal") + ` ${goal.objective}`);
|
|
744
|
+
for (const item of goal.contract.criteria) {
|
|
745
|
+
const criterion = current?.criteria.find(entry => entry.id === item.id), status = criterion?.status ?? "not_run";
|
|
746
|
+
const guard = item.kind === "command" && item.guard === true;
|
|
747
|
+
app.io.say(` ${status === "pass" || status === "accepted" ? C.green("✓") : C.yellow("○")} ${item.id}${guard ? " (guard)" : ""} · ${status} · ${item.description}` +
|
|
748
|
+
(criterion?.counter ? counterSuffix(C, criterion.counter) : ""));
|
|
749
|
+
}
|
|
750
|
+
for (const blocker of current?.reported_blockers ?? []) app.io.say(C.yellow(` reported blocker: ${blocker.reason}`));
|
|
751
|
+
if (current && current.evidence.independent !== "not_applicable") app.io.say(` blind checks ${current.evidence.independent}`);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** One counter-check verdict after a criterion, in the words of its role (S.7). */
|
|
755
|
+
function counterSuffix(C: App["io"]["palette"], verdict: CounterVerdict): string {
|
|
756
|
+
switch (verdict) {
|
|
757
|
+
case "vacuous": return C.yellow(" · also passes without the changes; it does not test them");
|
|
758
|
+
case "discriminating": return C.green(" · fails without the changes");
|
|
759
|
+
case "held": return C.green(" · held before the changes too");
|
|
760
|
+
case "not_held": return C.dim(" · did not hold before the changes; it tests them rather than guarding");
|
|
761
|
+
case "unavailable": return C.dim(" · counter-check unavailable");
|
|
762
|
+
default: return assertNever(verdict, "counter verdict");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
696
766
|
function printUsage(app: App): void {
|
|
697
767
|
const C = app.io.palette;
|
|
698
768
|
const t = app.session.totals;
|
|
@@ -756,7 +826,10 @@ async function printState(app: App): Promise<void> {
|
|
|
756
826
|
app.io.say(` command evidence ${request.commands.filter(command => command.status === "pass").length} current passes / ${request.commands.length} receipts`);
|
|
757
827
|
app.io.say(` observations ${request.progress.observations.length} new; artifact changes ${request.progress.artifact_changes.length}; acceptance advances ${request.progress.acceptance_advances.length}`);
|
|
758
828
|
app.io.say(` diagnostics ${request.progress.new_diagnostics.length} new; unchanged retries ${request.progress.unchanged_retries.length}; unknown ${request.progress.unknown_diagnostics.length}`);
|
|
759
|
-
|
|
829
|
+
app.io.say(` evidence ran ${request.evidence.ran} · discriminating ${request.evidence.discriminating} · independent ${request.evidence.independent} · human ${request.evidence.human}`);
|
|
830
|
+
for (const gap of request.unmet) app.io.say(` unmet ${gap}`);
|
|
831
|
+
for (const blocker of request.reported_blockers) app.io.say(` reported blocker ${blocker.reason}`);
|
|
832
|
+
if (!request.acceptance) app.io.say(" task verification no declared contract; a fresh check shows only that the changes ran, not that the request was met");
|
|
760
833
|
}
|
|
761
834
|
if (s.handoff) {
|
|
762
835
|
const h = s.handoff;
|
|
@@ -805,7 +878,7 @@ async function printAcceptance(app: App): Promise<void> {
|
|
|
805
878
|
app.io.say(` build ${target.build_event ?? "none"}; instance ${target.binding ?? "unknown"}; observation ${target.observation ?? "none"}; ${target.reason ?? "start the declared target after its build"}`);
|
|
806
879
|
}
|
|
807
880
|
for (const item of state?.criteria ?? []) {
|
|
808
|
-
app.io.say(` ${item.id}: ${item.status} · ${item.description}`);
|
|
881
|
+
app.io.say(` ${item.id}${item.kind === "command" && item.guard ? " (guard)" : ""}: ${item.status} · ${item.description}`);
|
|
809
882
|
if (item.kind === "command") app.io.say(` command: ${item.command}; evidence: ${item.evidence?.event_id ?? "none"}`);
|
|
810
883
|
else if (item.status !== "accepted") app.io.say(` /accept ${item.id} records your acceptance${item.target ? ` of the observed target ${item.target}` : ""}`);
|
|
811
884
|
}
|
|
@@ -841,8 +914,7 @@ async function applySettingsCommand(
|
|
|
841
914
|
app.io.say(app.io.palette.bold("\nsettings"));
|
|
842
915
|
app.io.say(` theme ${s?.theme ?? "dark"}`);
|
|
843
916
|
app.io.say(` thinking ${s?.thinking ?? app.providerConfig.effort}`);
|
|
844
|
-
app.io.say(
|
|
845
|
-
app.io.say(app.io.palette.dim(" /settings <theme|thinking|statusBar> <value> to change.\n"));
|
|
917
|
+
app.io.say(app.io.palette.dim(" /settings <theme|thinking> <value> to change; /settings alone opens every option.\n"));
|
|
846
918
|
return "continue";
|
|
847
919
|
}
|
|
848
920
|
|
|
@@ -858,7 +930,7 @@ async function commitSetting(app: App, field: string, value: string, tui: Ui | n
|
|
|
858
930
|
if (effort) await applyThinking(app, effort, tui);
|
|
859
931
|
return;
|
|
860
932
|
}
|
|
861
|
-
const parsed = settingPatch(
|
|
933
|
+
const parsed = settingPatch(field, value);
|
|
862
934
|
if (parsed.diagnostics.length) { app.io.say(C.yellow(parsed.diagnostics.join("\n"))); return; }
|
|
863
935
|
if (key === "theme") {
|
|
864
936
|
const diagnostics: string[] = [];
|
|
@@ -1639,25 +1711,7 @@ async function resumeSession(app: App, idOrPath: string, tui?: Ui): Promise<void
|
|
|
1639
1711
|
|
|
1640
1712
|
async function adoptLogDir(app: App, logDir: string): Promise<boolean> {
|
|
1641
1713
|
try {
|
|
1642
|
-
app
|
|
1643
|
-
if (app.extensions && !await app.extensions.releaseIntegrations()) throw Error("An integration still owns unfinished work or unconfirmed cleanup. Wait for it to settle before switching sessions.");
|
|
1644
|
-
const opened = await openSessionFor({ ...app, logDir, fallback: app.providerConfig, interactive: true });
|
|
1645
|
-
await app.session.dispose();
|
|
1646
|
-
await app.extensions?.dispose();
|
|
1647
|
-
app.session = opened.session;
|
|
1648
|
-
app.providerConfig = opened.providerConfig;
|
|
1649
|
-
app.credential = opened.credential;
|
|
1650
|
-
app.logDir = logDir;
|
|
1651
|
-
app.workspace = opened.workspace;
|
|
1652
|
-
app.blobDir = opened.blobDir;
|
|
1653
|
-
app.system = opened.system;
|
|
1654
|
-
app.loadedContext = opened.resources.loadedContext;
|
|
1655
|
-
app.slash = opened.resources.slash;
|
|
1656
|
-
app.ui = opened.resources.ui;
|
|
1657
|
-
app.resources = opened.resources.index;
|
|
1658
|
-
app.projectTrusted = opened.resources.trusted;
|
|
1659
|
-
app.extensions = opened.resources.extensions;
|
|
1660
|
-
app.foundation = opened.resources.foundation;
|
|
1714
|
+
await switchAppSession(app, logDir, (current, target) => openSessionFor({ ...current, logDir: target, fallback: current.providerConfig, interactive: true }));
|
|
1661
1715
|
} catch (err) {
|
|
1662
1716
|
app.io.say(app.io.palette.red(` ${describeError(err)}`));
|
|
1663
1717
|
return false;
|
|
@@ -1793,7 +1847,7 @@ async function nameSession(app: App, name: string, tui?: Ui): Promise<void> {
|
|
|
1793
1847
|
}
|
|
1794
1848
|
|
|
1795
1849
|
/** Runs a prompt. Returns false if the turn failed, having already said why. */
|
|
1796
|
-
async function run(app: App, prompt: string, signal?: AbortSignal, tui?: Ui, images: readonly ImageRef[] = [], skill?: Skill): Promise<boolean> {
|
|
1850
|
+
async function run(app: App, prompt: string, signal?: AbortSignal, tui?: Ui, images: readonly ImageRef[] = [], skill?: Skill, options?: RequestOptions): Promise<boolean> {
|
|
1797
1851
|
if (app.extensions && !initializedExtensions.has(app.extensions)) await bindExtensions(app, tui);
|
|
1798
1852
|
for (const result of await app.extensions?.emit("input", { text: prompt, images, source: "interactive" }) ?? []) {
|
|
1799
1853
|
if (result["action"] === "handled") return true;
|
|
@@ -1873,7 +1927,7 @@ async function run(app: App, prompt: string, signal?: AbortSignal, tui?: Ui, ima
|
|
|
1873
1927
|
return false;
|
|
1874
1928
|
}
|
|
1875
1929
|
try {
|
|
1876
|
-
await runTurn(app, input, signal, tui, skill);
|
|
1930
|
+
await runTurn(app, input, signal, tui, skill, options);
|
|
1877
1931
|
return true;
|
|
1878
1932
|
} catch (error) {
|
|
1879
1933
|
tui?.discardStream();
|
|
@@ -1895,6 +1949,7 @@ export async function runTurn(
|
|
|
1895
1949
|
signal?: AbortSignal,
|
|
1896
1950
|
tui?: TurnSurface,
|
|
1897
1951
|
skill?: Skill,
|
|
1952
|
+
options?: RequestOptions,
|
|
1898
1953
|
): Promise<void> {
|
|
1899
1954
|
// A pipe cannot unwrite bytes. Non-tty / -p therefore does not stream:
|
|
1900
1955
|
// deltas and discard are ignored, and the final block prints once.
|
|
@@ -1914,7 +1969,7 @@ export async function runTurn(
|
|
|
1914
1969
|
}
|
|
1915
1970
|
await registry?.emit("agent_start");
|
|
1916
1971
|
await registry?.emit("turn_start", { turnIndex: 0 });
|
|
1917
|
-
try { for await (const e of skill ? app.session.sendSkill(skill, prompt, signal) : sessionControl(app.session).send(prompt, signal, pull)) {
|
|
1972
|
+
try { for await (const e of skill ? app.session.sendSkill(skill, prompt, signal) : sessionControl(app.session).send(prompt, signal, pull, options)) {
|
|
1918
1973
|
// The log retains every response, but a script's final answer must be the
|
|
1919
1974
|
// accepted last response. A rejected JSON document cannot be unwritten.
|
|
1920
1975
|
if (e.kind === "turn_start" || e.kind === "discard") finalText = [];
|
|
@@ -1943,7 +1998,7 @@ export async function runTurn(
|
|
|
1943
1998
|
if (e.kind === "discard") tui.discardStream();
|
|
1944
1999
|
await tui.syncTranscript();
|
|
1945
2000
|
if (["assistant_text", "thinking", "tool_call", "tool_result", "done", "cut"].includes(e.kind)) tui.commitStream();
|
|
1946
|
-
if (e.kind === "
|
|
2001
|
+
if (e.kind === "stop_and_ask") {
|
|
1947
2002
|
for (const line of formatEvent(e, app.io.palette, Math.max(20, (process.stdout.columns ?? 80) - 1), {}, app.ui?.style)) app.io.say(line);
|
|
1948
2003
|
}
|
|
1949
2004
|
}
|
package/src/apps/startup.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { loadBlindChecks } from "../store/blind-checks.ts";
|
|
2
|
+
import type { BlindChecks } from "../core/blind-checks.ts";
|
|
3
|
+
import { appSessionFields } from "./session-lifecycle.ts";
|
|
1
4
|
import { release } from "node:os";
|
|
2
5
|
import { codingWorkingProtocol } from "../core/continuation.ts";
|
|
3
6
|
import { taskGuidance } from "../core/prompt-policy.ts";
|
|
@@ -40,6 +43,7 @@ import { readModelSources, readCatalogCacheText, writeCatalogCache, catalogCache
|
|
|
40
43
|
import { refreshRemoteCatalog } from "../runtime/models-fetch.ts";
|
|
41
44
|
import { readCursor, writeCursor, type Cursor } from "../store/log.ts";
|
|
42
45
|
import { getBlob } from "../store/blobs.ts";
|
|
46
|
+
import { parseSessionContext } from "../core/session-format.ts";
|
|
43
47
|
import { resolveLogDir, findSessionByPrefix, listSessions, sessionsDir, type ResolveLogOpts } from "../store/sessions.ts";
|
|
44
48
|
import { recoverCut } from "../store/recovery.ts";
|
|
45
49
|
import { planSessionEntry, restoreSessionModel } from "../core/session-entry.ts";
|
|
@@ -111,12 +115,12 @@ export function sessionEnvironment(workspace: string): SessionEnvironment {
|
|
|
111
115
|
* Session policy a CLI coding session differs from a benchmark run in.
|
|
112
116
|
* Benchmarks construct their own SessionConfig and keep the core defaults.
|
|
113
117
|
*/
|
|
114
|
-
export function cliSessionConfig(args: Args): { continuationAfter: number; workingSet: true;
|
|
118
|
+
export function cliSessionConfig(args: Args): { continuationAfter: number; workingSet: true; strictVerification: true; timeBudgetMs?: number } {
|
|
115
119
|
// Never within a request. A real Kimi K3 run with the reminder at reply six
|
|
116
120
|
// spent half its requests on notes after the work was already verified
|
|
117
121
|
// (fix-cli-continuation-noise). The reminder still fires where it earns its
|
|
118
122
|
// place: a resumed handoff that cannot continue, and boundary preparation.
|
|
119
|
-
return { continuationAfter: Infinity, workingSet: true,
|
|
123
|
+
return { continuationAfter: Infinity, workingSet: true, strictVerification: true, ...(args.timeBudgetMs === undefined ? {} : { timeBudgetMs: args.timeBudgetMs }) };
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
/**
|
|
@@ -190,6 +194,8 @@ export interface Args {
|
|
|
190
194
|
/** New sessions default to coding; restored context keeps its frozen schemas. */
|
|
191
195
|
toolProfile?: ToolProfile;
|
|
192
196
|
acceptanceFile?: string;
|
|
197
|
+
/** feat-blind-checks: a directory outside the workspace holding blind.json and its fixtures. */
|
|
198
|
+
blindChecks?: string;
|
|
193
199
|
excludeTools: string[];
|
|
194
200
|
noTools: boolean;
|
|
195
201
|
/** `--no-builtin-tools`. False when the flag was not passed. */
|
|
@@ -288,6 +294,7 @@ const KNOWN_FLAGS: readonly string[] = [
|
|
|
288
294
|
"--tools", "-t",
|
|
289
295
|
"--tool-profile",
|
|
290
296
|
"--acceptance-file",
|
|
297
|
+
"--blind-checks",
|
|
291
298
|
"--exclude-tools", "-xt",
|
|
292
299
|
"--no-tools", "-nt",
|
|
293
300
|
"--no-builtin-tools",
|
|
@@ -359,7 +366,9 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
|
|
|
359
366
|
let updateExtensions = false;
|
|
360
367
|
const args: Args = {
|
|
361
368
|
cliFlags: argv.filter(value => value.startsWith("-")),
|
|
362
|
-
|
|
369
|
+
// No default request cap (S.6 item 1): the stall watchdog ends a run that
|
|
370
|
+
// stops producing evidence; --max-turns or an agent budget sets a hard cap.
|
|
371
|
+
workspace: process.cwd(), logDir: "", maxTurns: Infinity, prompt: "",
|
|
363
372
|
// Overridable so a node boundary can be forced in a short session --
|
|
364
373
|
// otherwise cutting is only reachable after tens of thousands of tokens,
|
|
365
374
|
// which makes the most interesting path the least exercised one.
|
|
@@ -443,6 +452,10 @@ export function parseArgs(argv: string[], options: { deferExtensions?: boolean;
|
|
|
443
452
|
else if (a === "--system-prompt") args.systemPrompt = argv[++i] ?? "";
|
|
444
453
|
else if (a === "--append-system-prompt") args.appendSystemPrompt.push(argv[++i] ?? "");
|
|
445
454
|
else if (a === "--tools" || a === "-t") args.tools = parseToolList(argv[++i] ?? "");
|
|
455
|
+
else if (a === "--blind-checks") {
|
|
456
|
+
const path = argv[++i]; if (!path || path.startsWith("--")) throw new Error("--blind-checks requires a directory outside the workspace");
|
|
457
|
+
args.blindChecks = path;
|
|
458
|
+
}
|
|
446
459
|
else if (a === "--acceptance-file") {
|
|
447
460
|
const path = argv[++i]; if (!path || path.startsWith("--")) throw new Error("--acceptance-file requires a JSON path");
|
|
448
461
|
args.acceptanceFile = path;
|
|
@@ -942,6 +955,7 @@ export function createSessionFor(opts: {
|
|
|
942
955
|
extensions?: ExtensionRegistry;
|
|
943
956
|
foundation?: FoundationRuntime;
|
|
944
957
|
acceptanceContract?: AcceptanceContract;
|
|
958
|
+
blindChecks?: BlindChecks;
|
|
945
959
|
skills?: readonly Skill[];
|
|
946
960
|
}): Session {
|
|
947
961
|
const { args, workspace, logDir, blobDir, system, providerConfig, credential, io, askSlot } = opts;
|
|
@@ -975,6 +989,7 @@ export function createSessionFor(opts: {
|
|
|
975
989
|
...cliSessionConfig(args),
|
|
976
990
|
...(opts.skills ? { skills: opts.skills } : {}),
|
|
977
991
|
...(opts.acceptanceContract ? { acceptanceContract: opts.acceptanceContract } : {}),
|
|
992
|
+
...(opts.blindChecks ? { blindChecks: opts.blindChecks } : {}),
|
|
978
993
|
// Explicit legacy thresholds still select that experiment unless overridden.
|
|
979
994
|
contextPolicy: args.contextPolicy ?? (args.trigger > 0 ? "segmented" : DEFAULT_CONTEXT_POLICY),
|
|
980
995
|
...(args.trigger > 0
|
|
@@ -1115,26 +1130,16 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1115
1130
|
if (priorCursor?.context_ref) {
|
|
1116
1131
|
const savedContext = await getBlob(join(opts.logDir, "artifacts"), priorCursor.context_ref);
|
|
1117
1132
|
if (!savedContext) throw Error("missing frozen session context");
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
if (metadata.workflowToolProfile !== "foundation") throw Error("unsupported saved workflow tool profile");
|
|
1126
|
-
if (!opts.args.cliFlags?.some(flag => ["--tool-profile", "--tools", "-t", "--no-tools", "--no-builtin-tools"].includes(flag))) opts.args.toolProfile = "foundation";
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
if (metadata && typeof metadata === "object" && "agentProfile" in metadata && metadata.agentProfile !== undefined) {
|
|
1130
|
-
const role = metadata.agentProfile;
|
|
1131
|
-
if (!role || typeof role !== "object" || !("name" in role) || typeof role.name !== "string" || !/^[a-z][a-z0-9_-]{0,63}$/.test(role.name) || !("workflowExplicit" in role) || typeof role.workflowExplicit !== "boolean") throw Error("unsupported saved agent profile selector");
|
|
1132
|
-
if (!opts.args.cliFlags?.includes("--agent")) opts.args.agent = role.name;
|
|
1133
|
-
if (role.workflowExplicit && !opts.args.cliFlags?.includes("--workflow") && "workflowMode" in metadata) {
|
|
1134
|
-
if (metadata.workflowMode !== "build" && metadata.workflowMode !== "plan") throw Error("unsupported saved workflow mode");
|
|
1135
|
-
opts.args.workflow = metadata.workflowMode; opts.args.cliFlags = [...opts.args.cliFlags ?? [], "--workflow"];
|
|
1133
|
+
// S.3: the same parser Session.start uses; there is one reader of this format.
|
|
1134
|
+
const saved = parseSessionContext(JSON.parse(savedContext));
|
|
1135
|
+
const explicit = (...flags: string[]) => opts.args.cliFlags?.some(flag => flags.includes(flag)) === true;
|
|
1136
|
+
if (saved.agentProfile) {
|
|
1137
|
+
if (!explicit("--agent")) opts.args.agent = saved.agentProfile.name;
|
|
1138
|
+
if (saved.agentProfile.workflowExplicit && saved.workflowMode && !explicit("--workflow")) {
|
|
1139
|
+
opts.args.workflow = saved.workflowMode; opts.args.cliFlags = [...opts.args.cliFlags ?? [], "--workflow"];
|
|
1136
1140
|
}
|
|
1137
|
-
}
|
|
1141
|
+
} else if (saved.workflowMode && !explicit("--workflow")) opts.args.workflow = saved.workflowMode;
|
|
1142
|
+
if (saved.workflowToolProfile && !explicit("--tool-profile", "--tools", "-t", "--no-tools", "--no-builtin-tools")) opts.args.toolProfile = "foundation";
|
|
1138
1143
|
}
|
|
1139
1144
|
const workspace = priorCursor?.workspace ?? opts.workspace;
|
|
1140
1145
|
if (opts.args.pathRoot) {
|
|
@@ -1196,6 +1201,8 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1196
1201
|
const oauth = (model: ModelSpec, got: Resolved) => got.ok && storedIsOAuth(opts.auth, model.provider) && opts.args.apiKey.length === 0;
|
|
1197
1202
|
const acceptanceContract = priorCursor === null
|
|
1198
1203
|
? await loadAcceptanceContract(workspace, resources.trusted, opts.args.acceptanceFile) : undefined;
|
|
1204
|
+
// Frozen at session start like the contract; a resumed session reads its own copy from the saved context.
|
|
1205
|
+
const blindChecks = priorCursor === null && opts.args.blindChecks ? await loadBlindChecks(opts.args.blindChecks, workspace) : undefined;
|
|
1199
1206
|
const session = createSessionFor({
|
|
1200
1207
|
...opts,
|
|
1201
1208
|
system: resources.system,
|
|
@@ -1203,6 +1210,7 @@ export async function openSessionFor(opts: OpenSessionOptions): Promise<{
|
|
|
1203
1210
|
foundation: resources.foundation,
|
|
1204
1211
|
skills: [...resources.slash.skills.values()],
|
|
1205
1212
|
...(acceptanceContract ? { acceptanceContract } : {}),
|
|
1213
|
+
...(blindChecks ? { blindChecks } : {}),
|
|
1206
1214
|
workspace: priorCursor?.workspace ?? opts.workspace,
|
|
1207
1215
|
blobDir: priorCursor?.blob_dir ?? (priorCursor?.workspace && priorCursor.workspace !== opts.workspace
|
|
1208
1216
|
? statePaths(priorCursor.workspace, opts.args.stateDir).blobs : opts.blobDir),
|
|
@@ -1429,7 +1437,6 @@ export async function startup(args: Args, input?: StartupInput): Promise<App> {
|
|
|
1429
1437
|
sessionOpts = { explicit: picked };
|
|
1430
1438
|
}
|
|
1431
1439
|
const resolved = await resolveLogDir(workspace, sessionOpts);
|
|
1432
|
-
if (resolved.legacyNotice !== null && !args.logDir) io.say(C.yellow(` ${resolved.legacyNotice}`));
|
|
1433
1440
|
logDir = resolved.logDir;
|
|
1434
1441
|
} catch (err) {
|
|
1435
1442
|
const detail = err instanceof Error ? err.message : String(err);
|
|
@@ -1483,13 +1490,7 @@ export async function startup(args: Args, input?: StartupInput): Promise<App> {
|
|
|
1483
1490
|
tools: visible.map((t) => t.name),
|
|
1484
1491
|
});
|
|
1485
1492
|
|
|
1486
|
-
return {
|
|
1487
|
-
args, session, workspace: opened.workspace, logDir, blobDir: opened.blobDir, system: opened.system, providerConfig, credential,
|
|
1488
|
-
auth, effects, loadedContext: opened.resources.loadedContext, credentialOf, catalog, io, askSlot,
|
|
1489
|
-
ui: opened.resources.ui, slash: opened.resources.slash, resources: opened.resources.index, projectTrusted: opened.resources.trusted,
|
|
1490
|
-
extensions: opened.resources.extensions,
|
|
1491
|
-
foundation: opened.resources.foundation,
|
|
1492
|
-
};
|
|
1493
|
+
return { args, logDir, auth, effects, credentialOf, catalog, io, askSlot, ...appSessionFields(opened) };
|
|
1493
1494
|
}
|
|
1494
1495
|
|
|
1495
1496
|
/** The banner's words for the shared limits; the loop acts at exactly these numbers. */
|
|
@@ -1596,7 +1597,7 @@ export function printCliHelp(flags?: ReadonlyMap<string, ExtensionFlag>): void {
|
|
|
1596
1597
|
" [--tool-profile coding|readonly|research|data|foundation|full]\n" +
|
|
1597
1598
|
" [--workflow build|plan] [--agent name]\n" +
|
|
1598
1599
|
" [--output-schema schema.json] [--output-last-message file]\n" +
|
|
1599
|
-
" [--acceptance-file contract.json]\n" +
|
|
1600
|
+
" [--acceptance-file contract.json] [--blind-checks dir]\n" +
|
|
1600
1601
|
" [--control-socket /private/directory/control.sock]\n" +
|
|
1601
1602
|
" [--no-builtin-tools]\n" +
|
|
1602
1603
|
" [--allow-tools names] [--yolo|-y] [--approve|--no-approve]\n" +
|
package/src/apps/ui/app.ts
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
* either rebuilt from the log (transcript) or deliberately lost on exit
|
|
11
11
|
* (draft queue that was never sent, scroll position).
|
|
12
12
|
*/
|
|
13
|
+
import { ladderView } from "./control-model.ts";
|
|
14
|
+
import { criterionSummary, draftGoal } from "../goal-flow.ts";
|
|
15
|
+
import type { RequestOptions } from "../../runtime/loop.ts";
|
|
16
|
+
import { closeAppSession } from "../session-lifecycle.ts";
|
|
13
17
|
import { homedir } from "node:os";
|
|
14
18
|
import { basename, dirname, resolve } from "node:path";
|
|
15
19
|
import { fileURLToPath } from "node:url";
|
|
@@ -29,7 +33,7 @@ import { DISPLAY_SETTINGS, settingValue, settingValues } from "../../core/settin
|
|
|
29
33
|
import { THINKING_LEVELS, asThinkingLevel, supportedThinkingLevels } from "../../core/thinking.ts";
|
|
30
34
|
import { builtinTheme } from "../../core/theme.ts";
|
|
31
35
|
import { projectTranscript, type TranscriptItem } from "../../core/transcript.ts";
|
|
32
|
-
import { DEFAULT_FULLSCREEN_EXIT_OUTPUT, sessionExitStatus } from "../../core/session-exit.ts";
|
|
36
|
+
import { DEFAULT_FULLSCREEN_EXIT_OUTPUT, resumeCommand, sessionExitStatus } from "../../core/session-exit.ts";
|
|
33
37
|
import { lastAssistantText } from "../../core/export.ts";
|
|
34
38
|
import type { PermissionRequest } from "../../core/permissions.ts";
|
|
35
39
|
import type { ApprovalChoice } from "../../runtime/tools.ts";
|
|
@@ -37,7 +41,8 @@ import { attachImageBytes, attachImageFile } from "../../runtime/images.ts";
|
|
|
37
41
|
import { readClipboard, writeClipboard, type ClipboardContent } from "../../runtime/clipboard.ts";
|
|
38
42
|
import { highlightCode, transformMermaid, warmSyntaxLanguages } from "../../runtime/rich-text.ts";
|
|
39
43
|
import { SOURCE_PROVIDERS } from "../../runtime/model-data.ts";
|
|
40
|
-
import { SUBSCRIPTION_OPTIONS, subscriptionById, loginGitHubCopilot, startCallbackServer
|
|
44
|
+
import { SUBSCRIPTION_OPTIONS, subscriptionById, loginGitHubCopilot, startCallbackServer } from "../../runtime/oauth.ts";
|
|
45
|
+
import { XENO_VERSION } from "../../runtime/version.ts";
|
|
41
46
|
import { reportedCost } from "../../runtime/usage.ts";
|
|
42
47
|
import { UpdateChecks } from "../update-check.ts";
|
|
43
48
|
import { updateNotice } from "../../core/update-check.ts";
|
|
@@ -46,9 +51,12 @@ import { isUpgrade } from "../../core/version.ts";
|
|
|
46
51
|
import type { Effort } from "../../runtime/model.ts";
|
|
47
52
|
import { readNode, readCursor, setSessionLabel } from "../../store/log.ts";
|
|
48
53
|
import { readNodeHistory } from "../../store/restore.ts";
|
|
54
|
+
import { sessionsDir } from "../../store/sessions.ts";
|
|
49
55
|
import { readGitStatus } from "../../store/git-status.ts";
|
|
50
56
|
import type { GitStatus } from "../../core/git-status.ts";
|
|
51
57
|
import { activityStatus } from "../../core/activity-status.ts";
|
|
58
|
+
import { goalStatus } from "../../core/goal-view.ts";
|
|
59
|
+
import { generationSpeed } from "../../runtime/usage.ts";
|
|
52
60
|
import { executionOutcome, type ExecutionOutcome } from "../../core/outcome.ts";
|
|
53
61
|
import { projectTrust } from "../../store/trust.ts";
|
|
54
62
|
import { discoverResources, setResourceOverride } from "../../store/resource-index.ts";
|
|
@@ -155,6 +163,14 @@ export class InteractiveApp implements Ui {
|
|
|
155
163
|
private gitRun: Promise<void> | undefined;
|
|
156
164
|
private gitAgain = false;
|
|
157
165
|
private attention: ControlSnapshot["waiting"] = null;
|
|
166
|
+
/** The objective of a /goal whose criteria are being drafted. */
|
|
167
|
+
private drafting: string | undefined;
|
|
168
|
+
/** Index of this turn's first usage sample; generation speed reads from here. */
|
|
169
|
+
private turnUsageStart = 0;
|
|
170
|
+
/** Where this process began counting a goal's tokens: usage samples are kept per process, not in the log. */
|
|
171
|
+
private goalUsage: { goal: string; start: number } | undefined;
|
|
172
|
+
/** The last goal view and what it was derived from; see goalAndChecks. */
|
|
173
|
+
private goalCache: { key: string; view: import("../../core/goal-view.ts").GoalStatus | null; goal?: string } | undefined;
|
|
158
174
|
private lastOutcome: ExecutionOutcome | undefined;
|
|
159
175
|
private paintTimer: ReturnType<typeof setTimeout> | undefined;
|
|
160
176
|
/** Handoff-derived status (sidebar / top bar), refreshed single-flight and debounced (DoD 9). */
|
|
@@ -588,20 +604,61 @@ export class InteractiveApp implements Ui {
|
|
|
588
604
|
...(budget ? { context: { used: budget.used, trigger: budget.trigger, estimated: budget.estimated, segment: app.session.state.node, ...(spec.contextWindow ? { window: spec.contextWindow } : {}) } } : {}),
|
|
589
605
|
...(t.requests ? { usage: { input: t.prompt_tokens, output: t.output_tokens, cacheRate: t.cache_hit_rate,
|
|
590
606
|
// Under half a cent reads as $0.00, which says nothing (OpenCode #15903; Grok's status line hides it the same way).
|
|
591
|
-
cost: cost.kind === "usd" && cost.amount >= 0.005 ? `~$${cost.amount.toFixed(2)}` : cost.kind === "partial" && cost.known >= 0.005 ? `≥$${cost.known.toFixed(2)}` : null
|
|
607
|
+
cost: cost.kind === "usd" && cost.amount >= 0.005 ? `~$${cost.amount.toFixed(2)}` : cost.kind === "partial" && cost.known >= 0.005 ? `≥$${cost.known.toFixed(2)}` : null,
|
|
608
|
+
...speedOf(app.session.state.usage.slice(this.turnUsageStart)) } } : {}),
|
|
592
609
|
tools: { running: tools.filter(tool => tool.status === "pending").length, done: tools.filter(tool => tool.status === "ok").length, failed: tools.filter(tool => tool.status === "error").length },
|
|
593
610
|
...(this.derived ?? {}),
|
|
594
|
-
...
|
|
595
|
-
status: this.controlSnapshot.goal.state.status,
|
|
596
|
-
execution: this.inflight ? "running" : this.controlSnapshot.goal.state.execution?.reason ?? "idle",
|
|
597
|
-
observations: this.controlSnapshot.goal.state.progress.observations.length,
|
|
598
|
-
retries: this.controlSnapshot.goal.state.progress.unchanged_retries.length } } : {}),
|
|
611
|
+
...this.goalAndChecks(),
|
|
599
612
|
files: this.files,
|
|
600
613
|
resources: { contextFiles: app.loadedContext.agents.map(file => basename(file.path)), skills: app.slash?.skills.size ?? 0, extensions: app.extensions?.loaded.length ?? 0 },
|
|
601
614
|
...(extension ? { extension } : {}),
|
|
602
615
|
};
|
|
603
616
|
}
|
|
604
617
|
|
|
618
|
+
/**
|
|
619
|
+
* The sidebar's evidence: an explicit goal's block, or -- without one -- a
|
|
620
|
+
* Checks line for a request that changed files or declared a contract
|
|
621
|
+
* (feat-sidebar-goal-and-speed). Both read the request ledger the control
|
|
622
|
+
* snapshot already derived. The goal view is a scan of the whole log, so it
|
|
623
|
+
* is cached per log length; only its clock is read on every paint.
|
|
624
|
+
*/
|
|
625
|
+
private goalAndChecks(): Pick<StatusModel, "goal" | "checks"> {
|
|
626
|
+
const state = this.controlSnapshot?.goal?.state, session = this.app.session;
|
|
627
|
+
const key = `${this.app.logDir}:${session.state.node}:${this.ancestors.length}:${session.state.events.length}:${this.inflight}:${this.drafting ?? ""}:${state?.request.event ?? ""}:${state?.status ?? ""}`;
|
|
628
|
+
if (this.goalCache?.key !== key) {
|
|
629
|
+
const events = [...this.ancestors, ...session.state.events];
|
|
630
|
+
const set = events.findLast(event => event.t === "goal" && event.action === "set");
|
|
631
|
+
this.goalCache = { key, view: goalStatus(events, state, { running: this.inflight, ...(this.drafting !== undefined ? { drafting: this.drafting } : {}) }),
|
|
632
|
+
...(set ? { goal: set.id } : {}) };
|
|
633
|
+
}
|
|
634
|
+
const view = this.goalCache.view;
|
|
635
|
+
if (view) {
|
|
636
|
+
// The session's own clock stamps the log, so a running goal's time is read on it too.
|
|
637
|
+
const now = Date.parse(session.clock.now());
|
|
638
|
+
const since = this.goalCache.goal ? session.state.usage.slice(this.goalUsageStart(this.goalCache.goal)) : [];
|
|
639
|
+
const tokens = since.reduce((total, sample) => total + sample.input_tokens + sample.cache_creation_input_tokens + sample.cache_read_input_tokens + sample.output_tokens, 0);
|
|
640
|
+
return { goal: { phase: view.phase, objective: view.objective, checks: view.checks, requests: view.requests,
|
|
641
|
+
elapsedMs: view.phase === "drafting" ? 0 : Math.max(0, (view.endedAt ?? now) - view.startedAt),
|
|
642
|
+
...(state && view.phase !== "drafting" ? ladderView(state) : {}), ...(tokens ? { tokens } : {}) } };
|
|
643
|
+
}
|
|
644
|
+
if (state?.acceptance) {
|
|
645
|
+
// A declared contract without /goal: how many of its criteria hold; /acceptance lists them.
|
|
646
|
+
const items = state.acceptance.criteria, passed = items.filter(item => item.status === "pass" || item.status === "accepted").length;
|
|
647
|
+
return { checks: { status: passed === items.length ? "pass" : "partial", command: `${passed}/${items.length} passed · /acceptance` } };
|
|
648
|
+
}
|
|
649
|
+
if (!state?.changed) return {};
|
|
650
|
+
const fresh = state.criteria.find(item => item.kind === "fresh_check");
|
|
651
|
+
const run = fresh?.command ? state.commands.findLast(command => command.command === fresh.command) : state.commands.at(-1);
|
|
652
|
+
if (!fresh || fresh.status === "not_run" || !run) return { checks: { status: "unchecked" } };
|
|
653
|
+
return { checks: { status: fresh.status === "pass" ? "pass" : fresh.status === "stale" ? "stale" : "fail", command: run.command, exit: run.exit } };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Usage samples belong to this process; a goal's spend counts those recorded after this process first saw it. */
|
|
657
|
+
private goalUsageStart(goal: string): number {
|
|
658
|
+
if (this.goalUsage?.goal !== goal) this.goalUsage = { goal, start: this.app.session.state.usage.length };
|
|
659
|
+
return this.goalUsage.start;
|
|
660
|
+
}
|
|
661
|
+
|
|
605
662
|
private writeTitle(title: string): void {
|
|
606
663
|
if (title === this.writtenTitle || this.disposed) return;
|
|
607
664
|
this.writtenTitle = title;
|
|
@@ -851,16 +908,44 @@ export class InteractiveApp implements Ui {
|
|
|
851
908
|
this.paint();
|
|
852
909
|
}
|
|
853
910
|
|
|
854
|
-
|
|
911
|
+
/**
|
|
912
|
+
* S.2 / S.5. The draft runs as an ordinary, visible and cancellable turn;
|
|
913
|
+
* its checklist opens once the reply is in. Only the checked criteria
|
|
914
|
+
* become the goal, and the goal's own request starts from the objective.
|
|
915
|
+
*/
|
|
916
|
+
startGoal(objective: string): void {
|
|
917
|
+
this.track(async () => {
|
|
918
|
+
// The draft request's own text is an instruction to the model, not the goal; the sidebar shows the objective.
|
|
919
|
+
this.drafting = objective;
|
|
920
|
+
const pending = await draftGoal(this.app, objective, async (prompt, options) => {
|
|
921
|
+
await this.runPromptTurn(prompt, [], undefined, options);
|
|
922
|
+
// A cancelled draft is not a draft to accept, nor one to send back.
|
|
923
|
+
return this.lastOutcome?.status !== "cancelled";
|
|
924
|
+
}).finally(() => { this.drafting = undefined; this.paint(); });
|
|
925
|
+
if (!pending) return;
|
|
926
|
+
this.push({ kind: "list", title: `Accept the checks for: ${objective}`, multi: true, checked: pending.criteria.map(item => item.id), search: false,
|
|
927
|
+
items: pending.criteria.map(item => ({ value: item.id, label: item.id, description: criterionSummary(item) })),
|
|
928
|
+
onSubmitChecked: async ids => {
|
|
929
|
+
if (ids.length === 0) { this.notice("No criteria accepted; the goal was not set.", "warning"); return; }
|
|
930
|
+
await this.app.session.setGoal({ ...pending, accepted: ids });
|
|
931
|
+
this.notice(`Goal set with ${ids.length} accepted ${ids.length === 1 ? "check" : "checks"}.`, "success");
|
|
932
|
+
this.turn = this.runPromptTurn(objective).catch(error => this.notice(describeError(error), "error"));
|
|
933
|
+
} });
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private async runPromptTurn(text: string, images: readonly ImageRef[] = [], skill?: import("../../core/skills.ts").Skill, options?: RequestOptions): Promise<void> {
|
|
855
938
|
this.abort = new AbortController();
|
|
856
939
|
this.inflight = true;
|
|
857
940
|
this.lastOutcome = undefined;
|
|
941
|
+
// Speed is per turn: only this turn's model requests count (feat-sidebar-goal-and-speed).
|
|
942
|
+
this.turnUsageStart = this.app.session.state.usage.length;
|
|
858
943
|
this.programResult = false;
|
|
859
944
|
this.program?.update(programStatus({ running: true }));
|
|
860
945
|
this.executionKind = text.startsWith("!") ? "bash" : "model";
|
|
861
946
|
this.paint();
|
|
862
947
|
try {
|
|
863
|
-
const sent = await this.hooks.run(this.app, text, this.abort.signal, this, images, skill);
|
|
948
|
+
const sent = await this.hooks.run(this.app, text, this.abort.signal, this, images, skill, options);
|
|
864
949
|
if (!sent && !this.app.credential.ok) {
|
|
865
950
|
this.programResult = true; this.program?.update(programStatus({ running: false, waiting: "auth" }));
|
|
866
951
|
}
|
|
@@ -1109,7 +1194,7 @@ export class InteractiveApp implements Ui {
|
|
|
1109
1194
|
const detachInput = control.attachInput(phase => this.pullQueued(phase));
|
|
1110
1195
|
const unsubscribe = control.subscribe(notice => {
|
|
1111
1196
|
if (this.disposed) return;
|
|
1112
|
-
if (notice.method === "turn/started") { this.lastOutcome = undefined; this.program?.update(programStatus({ running: true })); }
|
|
1197
|
+
if (notice.method === "turn/started") { this.lastOutcome = undefined; this.turnUsageStart = this.app.session.state.usage.length; this.program?.update(programStatus({ running: true })); }
|
|
1113
1198
|
if (notice.method === "turn/error") {
|
|
1114
1199
|
this.lastOutcome = executionOutcome("execution_error");
|
|
1115
1200
|
this.programResult = true; this.program?.update({ state: "error", msg: "Turn failed; inspect the session" });
|
|
@@ -1558,7 +1643,7 @@ export async function runInteractive(app: App, hooks: Hooks, releaseEarlyTty?: (
|
|
|
1558
1643
|
await ui.closed;
|
|
1559
1644
|
await ui.settle();
|
|
1560
1645
|
} finally {
|
|
1561
|
-
try { await
|
|
1646
|
+
try { await closeAppSession(app); await hooks.cancelPendingOAuth(app); }
|
|
1562
1647
|
finally {
|
|
1563
1648
|
ui.dispose();
|
|
1564
1649
|
if (!renderer) r.destroy();
|
|
@@ -1582,7 +1667,8 @@ async function printExit(app: App): Promise<void> {
|
|
|
1582
1667
|
}
|
|
1583
1668
|
}
|
|
1584
1669
|
process.stdout.write(`\n ${history.ok ? sessionExitStatus(events) : "session saved · history unavailable; inspect before continuing"}\n`);
|
|
1585
|
-
|
|
1670
|
+
const path = resolve(app.logDir);
|
|
1671
|
+
process.stdout.write(`\n resume with: ${resumeCommand({ id: basename(path), directory: dirname(path), path }, resolve(sessionsDir(process.cwd())))}\n`);
|
|
1586
1672
|
}
|
|
1587
1673
|
|
|
1588
1674
|
/** The session-start rule first, then the projection (refactor-tui-dock-layout DoD 7). */
|
|
@@ -1591,6 +1677,12 @@ function projectWithStart(items: ReturnType<typeof projectItems>, clock: string
|
|
|
1591
1677
|
}
|
|
1592
1678
|
|
|
1593
1679
|
/** "~/Projects/workspace/xeno-tui" → "~/P/w/xeno-tui": every directory but the last cut to its first letter. */
|
|
1680
|
+
/** The turn's speed when one can be measured; nothing otherwise, so the row never shows a made-up figure. */
|
|
1681
|
+
function speedOf(samples: readonly import("../../runtime/usage.ts").UsageSample[]): { speed?: number } {
|
|
1682
|
+
const speed = generationSpeed(samples);
|
|
1683
|
+
return speed === undefined ? {} : { speed };
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1594
1686
|
export function shortPath(path: string): string {
|
|
1595
1687
|
const parts = path.split("/");
|
|
1596
1688
|
return parts.map((part, i) => i === parts.length - 1 || part === "~" || part === "" ? part : part.startsWith(".") ? part.slice(0, 2) : part.slice(0, 1)).join("/");
|