@visual-z/xeno 0.8.3 → 0.9.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 +24 -0
- package/README.md +31 -746
- package/bin/xeno.exe +26 -0
- package/install.cjs +88 -0
- package/package.json +18 -48
- package/docs/app-server.md +0 -115
- package/docs/foundations.md +0 -88
- package/docs/install.md +0 -244
- package/docs/providers.md +0 -172
- package/docs/research/foundation-clients-2026-10-09.md +0 -27
- package/docs/research/foundation-editing-2026-10-09.md +0 -56
- package/docs/research/foundation-network-2026-10-09.md +0 -37
- package/docs/research/foundation-workflow-2026-10-09.md +0 -37
- package/src/apps/acp-server.ts +0 -41
- package/src/apps/agent-profiles.ts +0 -19
- package/src/apps/app-server.ts +0 -30
- package/src/apps/approve.ts +0 -49
- package/src/apps/cli.ts +0 -99
- package/src/apps/commands.ts +0 -319
- package/src/apps/completions.ts +0 -87
- package/src/apps/control-transport.ts +0 -90
- package/src/apps/export-cli.ts +0 -24
- package/src/apps/goal-flow.ts +0 -70
- package/src/apps/image-draft.ts +0 -46
- package/src/apps/input.ts +0 -312
- package/src/apps/model-catalog.ts +0 -69
- package/src/apps/preferences.ts +0 -20
- package/src/apps/project-trust.ts +0 -9
- package/src/apps/provider-connect.ts +0 -346
- package/src/apps/question-ui.ts +0 -27
- package/src/apps/render.ts +0 -181
- package/src/apps/resources.ts +0 -66
- package/src/apps/review.ts +0 -109
- package/src/apps/session-hub.ts +0 -96
- package/src/apps/session-lifecycle.ts +0 -64
- package/src/apps/shell.ts +0 -2066
- package/src/apps/startup-input.ts +0 -135
- package/src/apps/startup.ts +0 -1677
- package/src/apps/ui/app.ts +0 -1713
- package/src/apps/ui/control-model.ts +0 -43
- package/src/apps/ui/extension-host.ts +0 -194
- package/src/apps/ui/image-cache.ts +0 -121
- package/src/apps/ui/palette.ts +0 -88
- package/src/apps/ui/project.ts +0 -127
- package/src/apps/ui/standalone.ts +0 -148
- package/src/apps/ui/status-model.ts +0 -68
- package/src/apps/ui/surface.ts +0 -91
- package/src/apps/ui/theme.ts +0 -86
- package/src/apps/update-check.ts +0 -112
- package/src/apps/workflow.ts +0 -58
- package/src/core/acceptance.ts +0 -139
- package/src/core/action-defaults.ts +0 -58
- package/src/core/activity-status.ts +0 -52
- package/src/core/agent-profile.ts +0 -130
- package/src/core/attachment-input.ts +0 -17
- package/src/core/blind-checks.ts +0 -72
- package/src/core/boundary.ts +0 -174
- package/src/core/branch-receipt.ts +0 -26
- package/src/core/catalog-merge.ts +0 -19
- package/src/core/checkpoints.ts +0 -85
- package/src/core/churn.ts +0 -61
- package/src/core/command-artifacts.ts +0 -94
- package/src/core/command-contract.ts +0 -151
- package/src/core/completion-action.ts +0 -20
- package/src/core/completion-review.ts +0 -98
- package/src/core/context-detail.ts +0 -37
- package/src/core/context-policy.ts +0 -133
- package/src/core/context-window.ts +0 -54
- package/src/core/continuation.ts +0 -130
- package/src/core/control-protocol.ts +0 -69
- package/src/core/control-view.ts +0 -39
- package/src/core/cycle.ts +0 -17
- package/src/core/data-query.ts +0 -201
- package/src/core/deadline.ts +0 -40
- package/src/core/derive.ts +0 -516
- package/src/core/draft-images.ts +0 -56
- package/src/core/edit-plan.ts +0 -200
- package/src/core/environment.ts +0 -181
- package/src/core/event-validation.ts +0 -220
- package/src/core/events.ts +0 -670
- package/src/core/evidence-ladder.ts +0 -62
- package/src/core/evidence-links.ts +0 -68
- package/src/core/evidence.ts +0 -240
- package/src/core/execution.ts +0 -18
- package/src/core/exhaustive.ts +0 -18
- package/src/core/export.ts +0 -179
- package/src/core/extension-flags.ts +0 -14
- package/src/core/external-effects.ts +0 -488
- package/src/core/failure-recovery.ts +0 -173
- package/src/core/file-precondition.ts +0 -78
- package/src/core/file-references.ts +0 -35
- package/src/core/frontmatter.ts +0 -213
- package/src/core/git-status.ts +0 -29
- package/src/core/goal-view.ts +0 -64
- package/src/core/goal.ts +0 -200
- package/src/core/handoff.ts +0 -479
- package/src/core/history-query.ts +0 -139
- package/src/core/history.ts +0 -237
- package/src/core/image-limits.ts +0 -72
- package/src/core/images.ts +0 -46
- package/src/core/integration-health.ts +0 -34
- package/src/core/json-event.ts +0 -131
- package/src/core/json-pointer.ts +0 -46
- package/src/core/keymap.ts +0 -153
- package/src/core/lsp-framing.ts +0 -34
- package/src/core/mcp-config.ts +0 -68
- package/src/core/model-picker.ts +0 -64
- package/src/core/model-preferences.ts +0 -28
- package/src/core/model-replay.ts +0 -22
- package/src/core/model-scope.ts +0 -53
- package/src/core/note-guidance.ts +0 -34
- package/src/core/oauth-fields.ts +0 -44
- package/src/core/observation-view.ts +0 -152
- package/src/core/outcome.ts +0 -44
- package/src/core/overflow.ts +0 -59
- package/src/core/packages.ts +0 -47
- package/src/core/patch.ts +0 -130
- package/src/core/path-suggestions.ts +0 -59
- package/src/core/permissions.ts +0 -410
- package/src/core/process-receipt.ts +0 -105
- package/src/core/process-state.ts +0 -37
- package/src/core/program-status.ts +0 -40
- package/src/core/progress.ts +0 -150
- package/src/core/project.ts +0 -431
- package/src/core/prompt-policy.ts +0 -46
- package/src/core/prompts.ts +0 -111
- package/src/core/question.ts +0 -94
- package/src/core/queue.ts +0 -78
- package/src/core/read-bounds.ts +0 -24
- package/src/core/read-observation.ts +0 -106
- package/src/core/recovery-sources.ts +0 -74
- package/src/core/regex-dialect.ts +0 -25
- package/src/core/request-budget.ts +0 -23
- package/src/core/request-progress.ts +0 -87
- package/src/core/request-state.ts +0 -273
- package/src/core/resource-selection.ts +0 -36
- package/src/core/response-recovery.ts +0 -59
- package/src/core/resume.ts +0 -59
- package/src/core/review.ts +0 -68
- package/src/core/ripgrep-release.ts +0 -64
- package/src/core/runtime-target.ts +0 -94
- package/src/core/scheduling.ts +0 -8
- package/src/core/search-observation.ts +0 -97
- package/src/core/secrets.ts +0 -188
- package/src/core/session-archive.ts +0 -4
- package/src/core/session-browser.ts +0 -82
- package/src/core/session-entry.ts +0 -61
- package/src/core/session-exit.ts +0 -42
- package/src/core/session-format.ts +0 -169
- package/src/core/session-position.ts +0 -26
- package/src/core/session-tree.ts +0 -147
- package/src/core/settings.ts +0 -192
- package/src/core/skills.ts +0 -165
- package/src/core/sql-query.ts +0 -211
- package/src/core/stall.ts +0 -128
- package/src/core/stop-report.ts +0 -68
- package/src/core/text-normalize.ts +0 -81
- package/src/core/theme.ts +0 -166
- package/src/core/thinking.ts +0 -26
- package/src/core/tokens.ts +0 -151
- package/src/core/tool-contract.ts +0 -153
- package/src/core/tool-discovery.ts +0 -87
- package/src/core/tool-failure.ts +0 -81
- package/src/core/tool-input.ts +0 -44
- package/src/core/transcript.ts +0 -108
- package/src/core/tui-mode.ts +0 -29
- package/src/core/update-check.ts +0 -157
- package/src/core/version.ts +0 -55
- package/src/core/web-content.ts +0 -80
- package/src/core/work-state.ts +0 -154
- package/src/core/workflow-mode.ts +0 -34
- package/src/core/working-set.ts +0 -119
- package/src/core/yaml.ts +0 -282
- package/src/runtime/acp-agent.ts +0 -289
- package/src/runtime/agent-profiles.ts +0 -21
- package/src/runtime/auth.ts +0 -367
- package/src/runtime/blind-check.ts +0 -36
- package/src/runtime/branch-jobs.ts +0 -36
- package/src/runtime/catalog.ts +0 -285
- package/src/runtime/clipboard.ts +0 -125
- package/src/runtime/command-artifacts.ts +0 -64
- package/src/runtime/connection-reuse.ts +0 -40
- package/src/runtime/control-client.ts +0 -149
- package/src/runtime/control-connection.ts +0 -139
- package/src/runtime/counter-check.ts +0 -74
- package/src/runtime/environment-tools.ts +0 -60
- package/src/runtime/evidence-notes.ts +0 -35
- package/src/runtime/exec-sessions.ts +0 -456
- package/src/runtime/execution-control.ts +0 -40
- package/src/runtime/extension-loader.ts +0 -37
- package/src/runtime/extension-ui.ts +0 -47
- package/src/runtime/extensions.ts +0 -370
- package/src/runtime/formatter-tool.ts +0 -38
- package/src/runtime/formatter.ts +0 -50
- package/src/runtime/foundation.ts +0 -116
- package/src/runtime/history-cache.ts +0 -57
- package/src/runtime/history-tools.ts +0 -168
- package/src/runtime/images.ts +0 -80
- package/src/runtime/integrations.ts +0 -216
- package/src/runtime/loop.ts +0 -3001
- package/src/runtime/lsp-client.ts +0 -201
- package/src/runtime/mcp-client.ts +0 -264
- package/src/runtime/mcp-oauth.ts +0 -168
- package/src/runtime/mcp-schema.ts +0 -51
- package/src/runtime/model-config.ts +0 -232
- package/src/runtime/model-data.LICENSE +0 -27
- package/src/runtime/model-data.ts +0 -21
- package/src/runtime/model-discovery.ts +0 -208
- package/src/runtime/model-replay.ts +0 -10
- package/src/runtime/model-scope.ts +0 -20
- package/src/runtime/model-sources.ts +0 -151
- package/src/runtime/model.ts +0 -686
- package/src/runtime/models-fetch.ts +0 -57
- package/src/runtime/oauth-device.ts +0 -86
- package/src/runtime/oauth.ts +0 -853
- package/src/runtime/open-image.ts +0 -42
- package/src/runtime/openai-responses.ts +0 -304
- package/src/runtime/openai.ts +0 -697
- package/src/runtime/output-schema.ts +0 -26
- package/src/runtime/package-manager.ts +0 -125
- package/src/runtime/patch-tool.ts +0 -38
- package/src/runtime/permission-resources.ts +0 -18
- package/src/runtime/pi-provider.ts +0 -423
- package/src/runtime/program-status.ts +0 -38
- package/src/runtime/progress.ts +0 -26
- package/src/runtime/provider-access.ts +0 -21
- package/src/runtime/provider-connect.ts +0 -109
- package/src/runtime/provider-presets.ts +0 -98
- package/src/runtime/provider-protocols.ts +0 -29
- package/src/runtime/provider-request.ts +0 -87
- package/src/runtime/provider-tool-policy.ts +0 -33
- package/src/runtime/provider.ts +0 -580
- package/src/runtime/question-tool.ts +0 -62
- package/src/runtime/retry.ts +0 -447
- package/src/runtime/review-tool.ts +0 -48
- package/src/runtime/rich-text.ts +0 -36
- package/src/runtime/runtime-target.ts +0 -149
- package/src/runtime/secrets.ts +0 -79
- package/src/runtime/self-update.ts +0 -74
- package/src/runtime/session-control.ts +0 -265
- package/src/runtime/session-hub.ts +0 -85
- package/src/runtime/stream-idle.ts +0 -80
- package/src/runtime/tool-broker.ts +0 -137
- package/src/runtime/tool-discovery.ts +0 -58
- package/src/runtime/tool-queue.ts +0 -77
- package/src/runtime/tools.ts +0 -1820
- package/src/runtime/turn-effects.ts +0 -31
- package/src/runtime/update-check.ts +0 -48
- package/src/runtime/usage.ts +0 -253
- package/src/runtime/version.ts +0 -12
- package/src/runtime/web-http.ts +0 -56
- package/src/runtime/web-tools.ts +0 -88
- package/src/runtime/working-notes.ts +0 -60
- package/src/runtime/workspace-lifecycle.ts +0 -141
- package/src/store/acceptance.ts +0 -15
- package/src/store/agent-profile-files.ts +0 -13
- package/src/store/atomic.ts +0 -31
- package/src/store/auth-store.ts +0 -50
- package/src/store/blind-checks.ts +0 -32
- package/src/store/blobs.ts +0 -143
- package/src/store/checkpoint-history.ts +0 -32
- package/src/store/command-artifacts.ts +0 -96
- package/src/store/command.ts +0 -111
- package/src/store/config.ts +0 -69
- package/src/store/context-files.ts +0 -436
- package/src/store/file-checkpoints.ts +0 -108
- package/src/store/file-index.ts +0 -174
- package/src/store/foundation-config.ts +0 -24
- package/src/store/git-checkpoints.ts +0 -67
- package/src/store/git-status.ts +0 -15
- package/src/store/history-search.ts +0 -19
- package/src/store/log.ts +0 -725
- package/src/store/lsp-document.ts +0 -30
- package/src/store/mcp-credentials.ts +0 -85
- package/src/store/models.ts +0 -60
- package/src/store/observations.ts +0 -224
- package/src/store/packages.ts +0 -98
- package/src/store/patch.ts +0 -92
- package/src/store/permission-paths.ts +0 -34
- package/src/store/process-receipts.ts +0 -34
- package/src/store/query-source.ts +0 -49
- package/src/store/recovery.ts +0 -44
- package/src/store/replay.ts +0 -76
- package/src/store/resource-index.ts +0 -170
- package/src/store/resources.ts +0 -285
- package/src/store/restore.ts +0 -110
- package/src/store/review.ts +0 -109
- package/src/store/runtime-target.ts +0 -64
- package/src/store/search.ts +0 -728
- package/src/store/secrets.ts +0 -44
- package/src/store/serial.ts +0 -13
- package/src/store/session-archive.ts +0 -165
- package/src/store/session-browser.ts +0 -68
- package/src/store/session-position.ts +0 -27
- package/src/store/sessions.ts +0 -525
- package/src/store/source-cancellation.ts +0 -6
- package/src/store/tool-binaries.ts +0 -90
- package/src/store/trust.ts +0 -82
- package/src/store/update-cache.ts +0 -36
- package/src/store/user-settings.ts +0 -30
- package/src/store/workspace-clone.ts +0 -42
- package/src/store/workspace-observation.ts +0 -113
- package/src/store/workspace.ts +0 -115
- package/src/store/worktree.ts +0 -278
- package/src/tui/ansi-spans.ts +0 -126
- package/src/tui/diff.ts +0 -160
- package/src/tui/exhaustive.ts +0 -14
- package/src/tui/fuzzy.ts +0 -75
- package/src/tui/style.ts +0 -208
- package/src/tui/ui/atomic-draft.ts +0 -121
- package/src/tui/ui/canvas.ts +0 -96
- package/src/tui/ui/colors.ts +0 -70
- package/src/tui/ui/composer.ts +0 -200
- package/src/tui/ui/control-panel.ts +0 -25
- package/src/tui/ui/control-sidebar.ts +0 -49
- package/src/tui/ui/dialog.ts +0 -398
- package/src/tui/ui/draft.ts +0 -23
- package/src/tui/ui/duration.ts +0 -16
- package/src/tui/ui/edge.ts +0 -186
- package/src/tui/ui/image-size.ts +0 -44
- package/src/tui/ui/ink.ts +0 -210
- package/src/tui/ui/items.ts +0 -42
- package/src/tui/ui/keys.ts +0 -40
- package/src/tui/ui/layout.ts +0 -74
- package/src/tui/ui/list-model.ts +0 -86
- package/src/tui/ui/list-view.ts +0 -172
- package/src/tui/ui/mascot.ts +0 -67
- package/src/tui/ui/messages.ts +0 -518
- package/src/tui/ui/motion.ts +0 -28
- package/src/tui/ui/paths.ts +0 -10
- package/src/tui/ui/pictures.ts +0 -108
- package/src/tui/ui/screen.ts +0 -548
- package/src/tui/ui/status.ts +0 -403
- package/src/tui/ui/styled.ts +0 -85
- package/src/tui/ui/tool-card.ts +0 -150
- package/src/tui/width.ts +0 -567
- package/src/vendor/pi-rich/LICENSE.upstream +0 -21
- package/src/vendor/pi-rich/UPSTREAM.json +0 -17
- package/src/vendor/pi-rich/clipboard-command.ts +0 -44
- package/src/vendor/pi-rich/exif-orientation.ts +0 -182
- package/src/vendor/pi-rich/html.ts +0 -51
- package/src/vendor/pi-rich/image-convert.ts +0 -49
- package/src/vendor/pi-rich/image-resize-core.ts +0 -164
- package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
- package/src/vendor/pi-rich/image-resize.ts +0 -123
- package/src/vendor/pi-rich/mime.ts +0 -116
- package/src/vendor/pi-rich/photon.ts +0 -139
- package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/** R4 / D8: a read descriptor names immutable version premises; it grants no authority. */
|
|
2
|
-
import type { ObservationReceipt } from "./read-observation.ts";
|
|
3
|
-
import { isObservationArtifact } from "./observation-view.ts";
|
|
4
|
-
import type { LogEvent } from "./events.ts";
|
|
5
|
-
|
|
6
|
-
export interface FileIdentity { host: string; workspace: string; resource: string }
|
|
7
|
-
export interface FileReadDescriptor {
|
|
8
|
-
kind: "file_read";
|
|
9
|
-
version: 1;
|
|
10
|
-
source: ObservationReceipt["source"];
|
|
11
|
-
selection: ObservationReceipt["selection"];
|
|
12
|
-
}
|
|
13
|
-
export interface FilePrecondition {
|
|
14
|
-
source: "read_handle" | "expected_sha256" | "session" | "create_only" | "unobserved";
|
|
15
|
-
expected: string | null;
|
|
16
|
-
read_handle?: string;
|
|
17
|
-
}
|
|
18
|
-
export type FilePreconditionDecision = { ok: true; precondition: FilePrecondition } |
|
|
19
|
-
{ ok: false; code: "invalid_input" | "version_mismatch"; message: string; precondition?: FilePrecondition };
|
|
20
|
-
|
|
21
|
-
const record = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
22
|
-
const natural = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
23
|
-
export const isFileDigest = (value: unknown): value is string => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
24
|
-
|
|
25
|
-
/** Preserve chronology across path aliases; handoff anchors intentionally keep their legacy spelling. */
|
|
26
|
-
export function latestObservedFileVersion(events: readonly LogEvent[], resource: string, identify: (path: string) => string): string | undefined {
|
|
27
|
-
for (let index = events.length - 1; index >= 0; index--) {
|
|
28
|
-
const event = events[index]!;
|
|
29
|
-
// P.3: multi-file Patch and captured commands observe each actual resulting version.
|
|
30
|
-
if (event.t === "checkpoint" && event.phase === "applied") {
|
|
31
|
-
for (const [path, change] of Object.entries(event.changes))
|
|
32
|
-
if (identify(path) === resource) return change.after?.sha256;
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
if (event.t !== "tool_result") continue;
|
|
36
|
-
const effect = event.effect;
|
|
37
|
-
if ((effect?.kind === "read" || effect?.kind === "edit") && identify(effect.path) === resource)
|
|
38
|
-
return effect.kind === "read" ? effect.sha256 : effect.sha256_after;
|
|
39
|
-
}
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function isFileReadDescriptor(value: unknown): value is FileReadDescriptor {
|
|
44
|
-
if (!record(value) || value["kind"] !== "file_read" || value["version"] !== 1) return false;
|
|
45
|
-
const source = value["source"], selection = value["selection"];
|
|
46
|
-
return record(source) && ["host", "workspace", "resource"].every(key => typeof source[key] === "string" && source[key] !== "") &&
|
|
47
|
-
isFileDigest(source["sha256"]) && isObservationArtifact(source["raw"]) && source["raw"].sha256 === source["sha256"] &&
|
|
48
|
-
record(selection) && natural(selection["start"]) && selection["start"] >= 1 && natural(selection["end"]) &&
|
|
49
|
-
natural(selection["total_lines"]) && selection["start"] <= selection["end"] && selection["end"] <= selection["total_lines"];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Explicit premises take precedence; an omitted premise may use the complete log projection. */
|
|
53
|
-
export function checkFilePrecondition(identity: FileIdentity, current: string | null, input: {
|
|
54
|
-
descriptor?: FileReadDescriptor; read_handle?: string; expected_sha256?: string; observed?: string; create_only?: boolean;
|
|
55
|
-
}): FilePreconditionDecision {
|
|
56
|
-
const invalid = (message: string): FilePreconditionDecision => ({ ok: false, code: "invalid_input", message });
|
|
57
|
-
if (input.expected_sha256 !== undefined && !isFileDigest(input.expected_sha256)) return invalid("expected_sha256 must be a lowercase SHA-256 digest");
|
|
58
|
-
if (input.observed !== undefined && !isFileDigest(input.observed)) return invalid("the observed file version is invalid; inspect the original log");
|
|
59
|
-
if (input.create_only && (input.read_handle !== undefined || input.expected_sha256 !== undefined))
|
|
60
|
-
return invalid("create_only cannot be combined with read_handle or expected_sha256");
|
|
61
|
-
if ((input.read_handle === undefined) !== (input.descriptor === undefined)) return invalid("read_handle requires its verified descriptor");
|
|
62
|
-
if (input.descriptor) {
|
|
63
|
-
if (!isFileReadDescriptor(input.descriptor) || !isFileDigest(input.read_handle)) return invalid("invalid read_handle descriptor");
|
|
64
|
-
for (const field of ["host", "workspace", "resource"] as const)
|
|
65
|
-
if (input.descriptor.source[field] !== identity[field]) return invalid(`read_handle names another ${field}; read the intended file here`);
|
|
66
|
-
if (input.expected_sha256 !== undefined && input.expected_sha256 !== input.descriptor.source.sha256)
|
|
67
|
-
return invalid("read_handle and expected_sha256 name different versions");
|
|
68
|
-
}
|
|
69
|
-
const precondition: FilePrecondition = input.create_only ? { source: "create_only", expected: null }
|
|
70
|
-
: input.descriptor ? { source: "read_handle", expected: input.descriptor.source.sha256, read_handle: input.read_handle! }
|
|
71
|
-
: input.expected_sha256 !== undefined ? { source: "expected_sha256", expected: input.expected_sha256 }
|
|
72
|
-
: input.observed !== undefined ? { source: "session", expected: input.observed }
|
|
73
|
-
: { source: "unobserved", expected: null };
|
|
74
|
-
if (precondition.source !== "unobserved" && precondition.expected !== current)
|
|
75
|
-
return { ok: false, code: "version_mismatch", precondition,
|
|
76
|
-
message: `file version changed; expected ${precondition.expected ?? "absent"}, current sha256: ${current ?? "absent"}. Read the current file before retrying.` };
|
|
77
|
-
return { ok: true, precondition };
|
|
78
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/** Explicit whitespace-delimited @ references; code literals and emails remain text. */
|
|
2
|
-
export interface FileReference { readonly start: number; readonly end: number; readonly path: string }
|
|
3
|
-
|
|
4
|
-
export function fileReferences(text: string): FileReference[] {
|
|
5
|
-
const refs: FileReference[] = [];
|
|
6
|
-
let codeTicks = 0;
|
|
7
|
-
for (let i = 0; i < text.length; i++) {
|
|
8
|
-
if (text[i] === "\\") { i++; continue; }
|
|
9
|
-
if (text[i] === "`") {
|
|
10
|
-
let end = i;
|
|
11
|
-
while (text[end] === "`") end++;
|
|
12
|
-
const count = end - i;
|
|
13
|
-
if (codeTicks === 0) codeTicks = count;
|
|
14
|
-
else if (count === codeTicks) codeTicks = 0;
|
|
15
|
-
i = end - 1;
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
18
|
-
if (codeTicks > 0 || text[i] !== "@" || (i > 0 && !/\s/.test(text[i - 1]!))) continue;
|
|
19
|
-
const start = i;
|
|
20
|
-
let end = i + 1;
|
|
21
|
-
let path: string;
|
|
22
|
-
if (text[end] === '"') {
|
|
23
|
-
const close = text.indexOf('"', end + 1);
|
|
24
|
-
if (close < 0 || text.slice(end, close).includes("\n")) continue;
|
|
25
|
-
path = text.slice(end + 1, close);
|
|
26
|
-
end = close + 1;
|
|
27
|
-
} else {
|
|
28
|
-
while (end < text.length && !/\s/.test(text[end]!)) end++;
|
|
29
|
-
path = text.slice(start + 1, end);
|
|
30
|
-
}
|
|
31
|
-
if (path.length > 0) refs.push({ start, end, path });
|
|
32
|
-
i = end - 1;
|
|
33
|
-
}
|
|
34
|
-
return refs;
|
|
35
|
-
}
|
package/src/core/frontmatter.ts
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Restricted YAML frontmatter reader.
|
|
3
|
-
*
|
|
4
|
-
* Why hand-written instead of a YAML package: the emitter in yaml.ts already
|
|
5
|
-
* made that call for writes (AGENTS.md cites it as the example). This is the
|
|
6
|
-
* read side of the same choice. A full parser would pull in aliases, anchors
|
|
7
|
-
* and tags on a path that reads user and project files.
|
|
8
|
-
*
|
|
9
|
-
* Supported: `key: value`, quoted strings, booleans, and `[a, b]` arrays.
|
|
10
|
-
* Nested mappings and multiline blocks are named and skipped; siblings stay.
|
|
11
|
-
* The function does not throw — a broken header must not take down startup.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export type FrontmatterValue = string | boolean | readonly string[];
|
|
15
|
-
|
|
16
|
-
export interface FrontmatterParse {
|
|
17
|
-
readonly fields: { readonly [key: string]: FrontmatterValue };
|
|
18
|
-
readonly body: string;
|
|
19
|
-
readonly diagnostics: readonly string[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const KEY = /^([A-Za-z_][A-Za-z0-9_-]*)\s*:\s*(.*)$/;
|
|
23
|
-
const BOOL = /^(?:true|false)$/i;
|
|
24
|
-
|
|
25
|
-
export function parseFrontmatter(text: string): FrontmatterParse {
|
|
26
|
-
try {
|
|
27
|
-
return parseInner(text);
|
|
28
|
-
} catch (err) {
|
|
29
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
30
|
-
return { fields: {}, body: text, diagnostics: [`frontmatter: ${message}`] };
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function parseInner(text: string): FrontmatterParse {
|
|
35
|
-
const normalized = stripBom(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
36
|
-
const extracted = extract(normalized);
|
|
37
|
-
if (extracted.yaml === null) {
|
|
38
|
-
return { fields: {}, body: extracted.body, diagnostics: [] };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const fields: { [key: string]: FrontmatterValue } = {};
|
|
42
|
-
const diagnostics: string[] = [];
|
|
43
|
-
const lines = extracted.yaml.split("\n");
|
|
44
|
-
|
|
45
|
-
for (let i = 0; i < lines.length; i++) {
|
|
46
|
-
const line = lines[i]!;
|
|
47
|
-
if (line.trim().length === 0 || line.trimStart().startsWith("#")) continue;
|
|
48
|
-
|
|
49
|
-
if (/^\s/.test(line)) {
|
|
50
|
-
diagnostics.push(`frontmatter: indented line is not a supported field`);
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const match = KEY.exec(line);
|
|
55
|
-
if (!match) {
|
|
56
|
-
diagnostics.push(`frontmatter: unparseable line ${JSON.stringify(line)}`);
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const key = match[1]!;
|
|
61
|
-
const raw = match[2] ?? "";
|
|
62
|
-
|
|
63
|
-
if (isNestedMarker(raw)) {
|
|
64
|
-
diagnostics.push(`frontmatter: nested mapping "${key}" is not supported`);
|
|
65
|
-
i = skipIndented(lines, i);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const parsed = parseValue(raw);
|
|
70
|
-
if (parsed === undefined) {
|
|
71
|
-
diagnostics.push(`frontmatter: nested mapping "${key}" is not supported`);
|
|
72
|
-
i = skipIndented(lines, i);
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
fields[key] = parsed;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return { fields, body: extracted.body, diagnostics };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function extract(text: string): { yaml: string | null; body: string } {
|
|
82
|
-
if (!text.startsWith("---")) return { yaml: null, body: text };
|
|
83
|
-
const lines = text.split("\n");
|
|
84
|
-
if (lines[0] !== "---") return { yaml: null, body: text };
|
|
85
|
-
let end = -1;
|
|
86
|
-
for (let i = 1; i < lines.length; i++) {
|
|
87
|
-
if (lines[i] === "---") {
|
|
88
|
-
end = i;
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (end < 0) return { yaml: null, body: text };
|
|
93
|
-
return {
|
|
94
|
-
yaml: lines.slice(1, end).join("\n"),
|
|
95
|
-
body: lines.slice(end + 1).join("\n").replace(/^\n+/, "").replace(/\s+$/, ""),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function isNestedMarker(raw: string): boolean {
|
|
100
|
-
const value = raw.trim();
|
|
101
|
-
return value.length === 0 || value === "|" || value === "|-" || value === "|+" ||
|
|
102
|
-
value === ">" || value === ">-" || value === ">+";
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function parseValue(raw: string): FrontmatterValue | undefined {
|
|
106
|
-
const trimmed = raw.trim();
|
|
107
|
-
if (trimmed.startsWith("{")) return undefined;
|
|
108
|
-
if (trimmed.startsWith("[")) return parseFlowArray(trimmed);
|
|
109
|
-
if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
|
|
110
|
-
const quoted = parseQuoted(trimmed);
|
|
111
|
-
return quoted ?? trimmed;
|
|
112
|
-
}
|
|
113
|
-
const unquoted = stripInlineComment(trimmed);
|
|
114
|
-
if (BOOL.test(unquoted)) return unquoted.toLowerCase() === "true";
|
|
115
|
-
return unquoted;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Flow arrays of scalars only. A mapping inside the brackets is a nested
|
|
120
|
-
* structure we refuse, so the caller can name the field and keep going.
|
|
121
|
-
*/
|
|
122
|
-
function parseFlowArray(raw: string): readonly string[] | undefined {
|
|
123
|
-
if (!raw.startsWith("[") || !raw.endsWith("]")) return undefined;
|
|
124
|
-
const inner = raw.slice(1, -1).trim();
|
|
125
|
-
if (inner.length === 0) return [];
|
|
126
|
-
if (inner.includes("{") || inner.includes("[") || /(?:^|,)\s*[A-Za-z_][A-Za-z0-9_-]*\s*:/.test(inner)) {
|
|
127
|
-
return undefined;
|
|
128
|
-
}
|
|
129
|
-
const items: string[] = [];
|
|
130
|
-
let buf = "";
|
|
131
|
-
let quote: string | null = null;
|
|
132
|
-
for (let i = 0; i < inner.length; i++) {
|
|
133
|
-
const ch = inner[i]!;
|
|
134
|
-
if (quote) {
|
|
135
|
-
if (quote === "'" && ch === "'" && inner[i + 1] === "'") {
|
|
136
|
-
buf += "'";
|
|
137
|
-
i++;
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
if (ch === quote && (quote === "'" || inner[i - 1] !== "\\")) {
|
|
141
|
-
quote = null;
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
buf += ch;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (ch === '"' || ch === "'") {
|
|
148
|
-
quote = ch;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (ch === ",") {
|
|
152
|
-
items.push(buf.trim());
|
|
153
|
-
buf = "";
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
buf += ch;
|
|
157
|
-
}
|
|
158
|
-
items.push(buf.trim());
|
|
159
|
-
return items;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function parseQuoted(raw: string): string | undefined {
|
|
163
|
-
const q = raw[0];
|
|
164
|
-
if (q !== '"' && q !== "'") return undefined;
|
|
165
|
-
let out = "";
|
|
166
|
-
for (let i = 1; i < raw.length; i++) {
|
|
167
|
-
const ch = raw[i]!;
|
|
168
|
-
if (q === "'" && ch === "'" && raw[i + 1] === "'") {
|
|
169
|
-
out += "'";
|
|
170
|
-
i++;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (ch === q) {
|
|
174
|
-
const rest = raw.slice(i + 1).trim();
|
|
175
|
-
if (rest.length > 0 && !rest.startsWith("#")) return undefined;
|
|
176
|
-
return out;
|
|
177
|
-
}
|
|
178
|
-
if (q === '"' && ch === "\\" && i + 1 < raw.length) {
|
|
179
|
-
const next = raw[i + 1]!;
|
|
180
|
-
out += next === "n" ? "\n" : next === "t" ? "\t" : next;
|
|
181
|
-
i++;
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
out += ch;
|
|
185
|
-
}
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function stripInlineComment(value: string): string {
|
|
190
|
-
const idx = value.indexOf(" #");
|
|
191
|
-
return (idx >= 0 ? value.slice(0, idx) : value).trim();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function skipIndented(lines: readonly string[], i: number): number {
|
|
195
|
-
let j = i + 1;
|
|
196
|
-
while (j < lines.length) {
|
|
197
|
-
const next = lines[j]!;
|
|
198
|
-
if (next.trim().length === 0) {
|
|
199
|
-
j++;
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
if (/^\s/.test(next)) {
|
|
203
|
-
j++;
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
return j - 1;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function stripBom(text: string): string {
|
|
212
|
-
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
213
|
-
}
|
package/src/core/git-status.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/** Counts from Git porcelain v2, distinct from agent tool effects. No path decoding needed. */
|
|
2
|
-
export interface GitStatus {
|
|
3
|
-
readonly branch: string | null;
|
|
4
|
-
readonly staged: number;
|
|
5
|
-
readonly changed: number;
|
|
6
|
-
readonly untracked: number;
|
|
7
|
-
readonly conflicted: number;
|
|
8
|
-
readonly ahead: number;
|
|
9
|
-
readonly behind: number;
|
|
10
|
-
}
|
|
11
|
-
export function parseGitStatus(output: string): GitStatus {
|
|
12
|
-
let branch: string | null = null, staged = 0, changed = 0, untracked = 0, conflicted = 0, ahead = 0, behind = 0;
|
|
13
|
-
const records = output.split("\0");
|
|
14
|
-
for (let index = 0; index < records.length; index++) {
|
|
15
|
-
const line = records[index]!;
|
|
16
|
-
if (line.startsWith("# branch.head ")) branch = line.slice(14).trim();
|
|
17
|
-
else if (line.startsWith("# branch.ab ")) {
|
|
18
|
-
const match = /^# branch.ab \+(\d+) -(\d+)$/.exec(line.trim());
|
|
19
|
-
if (match) { ahead = Number(match[1]); behind = Number(match[2]); }
|
|
20
|
-
} else if (line.startsWith("? ")) untracked++;
|
|
21
|
-
else if (/^[12u] [A-Z?.]{2} /.test(line)) {
|
|
22
|
-
if (line[2] !== ".") staged++;
|
|
23
|
-
if (line[3] !== ".") changed++;
|
|
24
|
-
if (line[0] === "u") conflicted++;
|
|
25
|
-
if (line[0] === "2") index++; // Rename's next NUL field is a path, never a status record.
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return { branch, staged, changed, untracked, conflicted, ahead, behind };
|
|
29
|
-
}
|
package/src/core/goal-view.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What the sidebar shows about an explicit goal (feat-sidebar-goal-and-speed),
|
|
3
|
-
* derived from the log alone so it reads the same after a restart.
|
|
4
|
-
*
|
|
5
|
-
* The block exists only while a /goal is set, and for one message after it is
|
|
6
|
-
* met. Its verdicts are the request ledger's (D11); this only chooses words:
|
|
7
|
-
* the phase answers "do I need to act, and is it still running", and each
|
|
8
|
-
* accepted check says what it needs from the user.
|
|
9
|
-
*/
|
|
10
|
-
import type { LogEvent } from "./events.ts";
|
|
11
|
-
import type { RequestCriterion, RequestState } from "./request-state.ts";
|
|
12
|
-
|
|
13
|
-
export type GoalPhase = "drafting" | "working" | "needs_you" | "met" | "stopped";
|
|
14
|
-
export type GoalCheck = { id: string; kind: RequestCriterion["kind"]; status: RequestCriterion["status"]; guard?: true; exit?: number };
|
|
15
|
-
/**
|
|
16
|
-
* `startedAt` is when the goal was set; `endedAt` when it was met, absent while
|
|
17
|
-
* it runs. Times, not a duration, so a caller can cache this per log length and
|
|
18
|
-
* read the clock only when it paints.
|
|
19
|
-
*/
|
|
20
|
-
export type GoalStatus = { phase: GoalPhase; objective: string; checks: GoalCheck[]; startedAt: number; endedAt?: number; requests: number };
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The goal block for the end of an ancestry, or null when no goal applies.
|
|
24
|
-
* `running` is whether a request is in flight now; `drafting` is the objective
|
|
25
|
-
* of a /goal whose criteria are still being drafted.
|
|
26
|
-
*/
|
|
27
|
-
export function goalStatus(events: readonly LogEvent[], state: RequestState | undefined,
|
|
28
|
-
live: { running: boolean; drafting?: string }): GoalStatus | null {
|
|
29
|
-
if (live.drafting !== undefined) return { phase: "drafting", objective: live.drafting, checks: [], startedAt: 0, requests: 0 };
|
|
30
|
-
const lastSet = events.findLastIndex(event => event.t === "goal" && event.action === "set");
|
|
31
|
-
if (lastSet < 0) return null;
|
|
32
|
-
const set = events[lastSet]!;
|
|
33
|
-
if (set.t !== "goal" || set.action !== "set") return null;
|
|
34
|
-
const after = events.slice(lastSet + 1);
|
|
35
|
-
// The last clear decides: a met goal the user then cleared is gone like any other cleared goal.
|
|
36
|
-
const clear = after.findLastIndex(event => event.t === "goal" && event.action === "clear");
|
|
37
|
-
const cleared = clear < 0 ? undefined : after[clear];
|
|
38
|
-
// A goal the user cleared is gone; a met goal stays for one summary, until the next message opens a new request.
|
|
39
|
-
if (cleared?.t === "goal" && cleared.action === "clear" && cleared.reason !== "met") return null;
|
|
40
|
-
const recorded = cleared !== undefined;
|
|
41
|
-
if (recorded && after.slice(clear + 1).some(event => event.t === "user" && event.request === undefined)) return null;
|
|
42
|
-
// Only the goal's own request carries its contract; the ledger of any other request says nothing about it.
|
|
43
|
-
const own = state?.acceptance?.contract_id === set.contract.id && state.acceptance.contract_event !== undefined
|
|
44
|
-
&& events.some((event, index) => index > lastSet && event.id === state.acceptance!.contract_event) ? state : undefined;
|
|
45
|
-
// The ledger is the verdict (D11): verified is met even before the clear is recorded -- an /accept outside a
|
|
46
|
-
// request, a log written before /accept recorded it -- and its clock stops at the last recorded fact.
|
|
47
|
-
const met = recorded || own?.status === "verified";
|
|
48
|
-
const end = recorded ? Date.parse(cleared.at) : met ? Date.parse(after.at(-1)?.at ?? set.at) : undefined;
|
|
49
|
-
const checks: GoalCheck[] = set.contract.criteria.map(item => {
|
|
50
|
-
const entry = own?.criteria.find(criterion => criterion.id === item.id);
|
|
51
|
-
const run = item.kind === "command" ? own?.commands.findLast(command => command.command === item.command) : undefined;
|
|
52
|
-
return { id: item.id, kind: item.kind, status: entry?.status ?? (item.kind === "human" ? "awaiting_human" : "not_run"),
|
|
53
|
-
...(item.kind === "command" && item.guard ? { guard: true as const } : {}),
|
|
54
|
-
...(entry?.status === "fail" && run ? { exit: run.exit } : {}) };
|
|
55
|
-
});
|
|
56
|
-
return { phase: met ? "met" : live.running ? "working" : own?.status === "awaiting_human" ? "needs_you" : "stopped",
|
|
57
|
-
objective: set.objective, checks, startedAt: Date.parse(set.at), ...(end === undefined ? {} : { endedAt: end }),
|
|
58
|
-
requests: (recorded ? after.slice(0, clear) : after).filter(event => event.t === "assistant").length };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** The words the sidebar and the input edge use for a phase. */
|
|
62
|
-
export function goalPhaseLabel(phase: GoalPhase): string {
|
|
63
|
-
return phase === "needs_you" ? "needs you" : phase;
|
|
64
|
-
}
|
package/src/core/goal.ts
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Explicit goals (research doc S.2 / S.5).
|
|
3
|
-
*
|
|
4
|
-
* The model drafts acceptance criteria for a goal; the user accepts them one
|
|
5
|
-
* by one; only the accepted ones become the goal's contract. Whether the goal
|
|
6
|
-
* is met is never the model's call: it is the request ledger's status, which
|
|
7
|
-
* only command receipts and the user's own acceptance decisions move (D11).
|
|
8
|
-
* The draft is itself only a claim -- it parses into candidates, and a
|
|
9
|
-
* candidate that does not validate is reported and sent back to the model to
|
|
10
|
-
* correct, never repaired by guessing.
|
|
11
|
-
*/
|
|
12
|
-
import { ACCEPTANCE_COMMAND_LIMIT, parseAcceptanceContract, type AcceptanceContract, type AcceptanceCriterion } from "./acceptance.ts";
|
|
13
|
-
import type { GoalEvent, LogEvent } from "./events.ts";
|
|
14
|
-
import type { RequestState } from "./request-state.ts";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Assumed, not measured: enough for a goal's distinct checks, few enough that
|
|
18
|
-
* a person reads and decides on each one rather than accepting a wall.
|
|
19
|
-
*/
|
|
20
|
-
export const GOAL_CRITERIA_LIMIT = 8;
|
|
21
|
-
|
|
22
|
-
export function goalDraftRequest(objective: string): string {
|
|
23
|
-
return [
|
|
24
|
-
`Goal: ${objective}`,
|
|
25
|
-
"",
|
|
26
|
-
"Do not start the work yet. Draft the acceptance criteria that would show this goal is met, for the user to accept or reject one by one.",
|
|
27
|
-
"You may read the workspace first (read, find, grep) to learn its real files and commands. You cannot change anything or run commands in this step.",
|
|
28
|
-
`Then reply with only a JSON object: {"criteria":[{"id":"short-id","kind":"command","description":"what it shows","command":"exact shell command"}]}, at most ${GOAL_CRITERIA_LIMIT} items.`,
|
|
29
|
-
"- kind \"command\": a command that exits 0 only when the criterion holds, and fails on the workspace as it is now if the goal is not yet done. Prefer checks that exercise new inputs over checks that only replay an example.",
|
|
30
|
-
"- \"guard\": true on a command for what the goal must not break or change, such as files to leave untouched or behaviour that must keep working. A guard passes now and must still pass when the goal is done; every command without it must fail now.",
|
|
31
|
-
"- kind \"human\" (no command): only for what a person must judge, such as tone or visual quality.",
|
|
32
|
-
"- ids: letters, digits, dot, dash, underscore, colon; unique.",
|
|
33
|
-
"If you cannot tell how to check something, make it a human criterion rather than inventing a command.",
|
|
34
|
-
"If the goal cannot be checked or achieved in this workspace at all, reply {\"criteria\": [], \"blocked\": \"the specific reason\"}.",
|
|
35
|
-
].join("\n");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type GoalDraft = { criteria: AcceptanceCriterion[]; errors: string[]; blocked?: string };
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Assumed, not measured: one correction. Instructor's reask and Pydantic AI's
|
|
42
|
-
* output retries likewise send the validation errors back for a bounded number
|
|
43
|
-
* of attempts. The slip this exists for -- a real GLM draft missing its
|
|
44
|
-
* closing brackets -- needs one; a model that fails the same schema twice is
|
|
45
|
-
* not helped by a third full request, and the user can still redraft.
|
|
46
|
-
*/
|
|
47
|
-
export const GOAL_DRAFT_RETRIES = 1;
|
|
48
|
-
|
|
49
|
-
/** The correction request: the parser's own reasons, and the form the reply must take. */
|
|
50
|
-
export function goalDraftRetry(errors: readonly string[]): string {
|
|
51
|
-
return [
|
|
52
|
-
"Harness draft check: your draft could not be used as written.",
|
|
53
|
-
...errors.map(error => `- ${error}`),
|
|
54
|
-
"Reply again with only the complete corrected JSON object in the same form, keeping the criteria that were valid. Do not start the work.",
|
|
55
|
-
].join("\n");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The draft among the events one draft run recorded: the last assistant reply
|
|
60
|
-
* of the first request they open. A follow-up the user queued meanwhile opens
|
|
61
|
-
* its own request and is never read as the draft; a run that recorded no
|
|
62
|
-
* request has no draft.
|
|
63
|
-
*/
|
|
64
|
-
export function draftReply(recorded: readonly LogEvent[]): { id: string; text: string } | null {
|
|
65
|
-
const opens = (event: LogEvent) => event.t === "user" && event.request === undefined;
|
|
66
|
-
const start = recorded.findIndex(opens);
|
|
67
|
-
if (start < 0) return null;
|
|
68
|
-
const next = recorded.findIndex((event, index) => index > start && opens(event));
|
|
69
|
-
const reply = recorded.slice(start, next < 0 ? undefined : next).findLast(event => event.t === "assistant");
|
|
70
|
-
return reply?.t === "assistant" ? { id: reply.id, text: reply.blocks.flatMap(block => block.type === "text" ? [block.text] : []).join("\n") } : null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** The first JSON object in a reply: a fenced block when present, else the outermost braces. */
|
|
74
|
-
function jsonObject(text: string): unknown {
|
|
75
|
-
const fenced = /```(?:json)?\s*\n([\s\S]*?)\n```/.exec(text)?.[1];
|
|
76
|
-
const candidate = fenced ?? text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1);
|
|
77
|
-
if (!candidate.trim()) throw Error("the reply holds no JSON object");
|
|
78
|
-
return JSON.parse(candidate);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}$/;
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Why one drafted item is not a criterion, in words the user can act on; null
|
|
85
|
-
* when it is one. A human item's empty or null `command`, and a `guard` of
|
|
86
|
-
* false or null, are only a model's way of saying "none" and are dropped
|
|
87
|
-
* before this runs.
|
|
88
|
-
*/
|
|
89
|
-
function draftItemError(item: Record<string, unknown>, seen: ReadonlySet<string>): string | null {
|
|
90
|
-
const { id, kind, description, command, guard } = item;
|
|
91
|
-
if (typeof id !== "string" || !id.trim()) return "has no id";
|
|
92
|
-
if (!ID.test(id)) return `id ${JSON.stringify(id)} may use only letters, digits, dot, dash, underscore and colon (at most 80)`;
|
|
93
|
-
if (seen.has(id)) return `repeats id ${id}`;
|
|
94
|
-
if (typeof description !== "string" || !description.trim()) return "has no description";
|
|
95
|
-
if (description.length > 300) return "has a description longer than 300 characters";
|
|
96
|
-
if (kind === "human") return command !== undefined ? "is a human criterion with a command; make it a command criterion or drop the command"
|
|
97
|
-
: guard !== undefined ? "is a human criterion marked as a guard; a guard needs a command" : null;
|
|
98
|
-
if (kind !== "command") return `has kind ${JSON.stringify(kind)}; use "command" or "human"`;
|
|
99
|
-
if (guard !== undefined && guard !== true) return `has guard ${JSON.stringify(guard)}; use true or leave it out`;
|
|
100
|
-
if (typeof command !== "string" || !command.trim()) return "needs a command";
|
|
101
|
-
if (command.length > ACCEPTANCE_COMMAND_LIMIT) return `has a command longer than ${ACCEPTANCE_COMMAND_LIMIT} characters`;
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function parseGoalDraft(text: string): GoalDraft {
|
|
106
|
-
let value: unknown;
|
|
107
|
-
try { value = jsonObject(text); }
|
|
108
|
-
catch (error) { return { criteria: [], errors: [`the draft is not readable JSON: ${error instanceof Error ? error.message : String(error)}`] }; }
|
|
109
|
-
const object = value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
110
|
-
const items = object?.["criteria"];
|
|
111
|
-
if (!Array.isArray(items)) return { criteria: [], errors: ["the draft has no criteria array"] };
|
|
112
|
-
const blockedText = object?.["blocked"];
|
|
113
|
-
const blocked = typeof blockedText === "string" && blockedText.trim() ? blockedText.trim().slice(0, 500) : undefined;
|
|
114
|
-
const criteria: AcceptanceCriterion[] = [], errors: string[] = [], seen = new Set<string>();
|
|
115
|
-
items.forEach((raw, index) => {
|
|
116
|
-
if (criteria.length >= GOAL_CRITERIA_LIMIT) { errors.push(`criterion ${index + 1}: more than ${GOAL_CRITERIA_LIMIT} criteria`); return; }
|
|
117
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) { errors.push(`criterion ${index + 1} is not an object`); return; }
|
|
118
|
-
const item = { ...(raw as Record<string, unknown>) };
|
|
119
|
-
if (item["kind"] === "human" && (item["command"] === null || typeof item["command"] === "string" && !item["command"].trim())) delete item["command"];
|
|
120
|
-
if (item["guard"] === false || item["guard"] === null) delete item["guard"];
|
|
121
|
-
const problem = draftItemError(item, seen);
|
|
122
|
-
if (problem) { errors.push(`criterion ${index + 1} ${problem}`); return; }
|
|
123
|
-
// The one validator for criteria: a draft item is accepted exactly when a contract would accept it.
|
|
124
|
-
try {
|
|
125
|
-
const [criterion] = parseAcceptanceContract({ version: 1, id: "goal", criteria: [{ id: item["id"], kind: item["kind"], description: item["description"],
|
|
126
|
-
...(item["kind"] === "command" ? { command: item["command"], ...(item["guard"] === true ? { guard: true } : {}) } : {}) }] }).criteria;
|
|
127
|
-
seen.add(criterion!.id); criteria.push(criterion!);
|
|
128
|
-
} catch (error) { errors.push(`criterion ${index + 1}: ${error instanceof Error ? error.message : String(error)}`); }
|
|
129
|
-
});
|
|
130
|
-
return { criteria, errors, ...(blocked ? { blocked } : {}) };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Only accepted criteria, in draft order. Accepting none is not a goal. */
|
|
134
|
-
export function goalContract(criteria: readonly AcceptanceCriterion[], accepted: readonly string[]): AcceptanceContract {
|
|
135
|
-
const chosen = criteria.filter(item => accepted.includes(item.id));
|
|
136
|
-
if (chosen.length === 0) throw Error("a goal needs at least one accepted criterion");
|
|
137
|
-
return parseAcceptanceContract({ version: 1, id: "goal", scope: "request", criteria: chosen });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export type ActiveGoal = Extract<GoalEvent, { action: "set" }>;
|
|
141
|
-
|
|
142
|
-
/** The goal in force at the end of an ancestry: the last set, unless a clear followed it. */
|
|
143
|
-
export function activeGoal(events: readonly LogEvent[]): ActiveGoal | null {
|
|
144
|
-
const last = events.findLast(event => event.t === "goal");
|
|
145
|
-
return last?.t === "goal" && last.action === "set" ? last : null;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Assumed, not measured: two goal checks answered without any tool use means
|
|
150
|
-
* the model is talking past the goal, and more requests will not change that.
|
|
151
|
-
* Claude Code's /goal stops on the same signal ("no tool use for several
|
|
152
|
-
* turns in a row"). Too low would stop a model that needed one more reply to
|
|
153
|
-
* plan; too high spends requests on replies that do nothing.
|
|
154
|
-
*/
|
|
155
|
-
export const GOAL_IDLE_LIMIT = 2;
|
|
156
|
-
|
|
157
|
-
/** Goal checks since the last tool result: each one the model answered without acting. */
|
|
158
|
-
export function idleGoalChecks(events: readonly LogEvent[]): number {
|
|
159
|
-
let count = 0;
|
|
160
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
161
|
-
const event = events[i]!;
|
|
162
|
-
if (event.t === "tool_result" || event.t === "user" && event.request === undefined) break;
|
|
163
|
-
if (event.t === "guidance" && event.reason === "goal") count++;
|
|
164
|
-
}
|
|
165
|
-
return count;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export type GoalDecision =
|
|
169
|
-
| { action: "met" }
|
|
170
|
-
| { action: "await_human" }
|
|
171
|
-
| { action: "stop"; reason: string }
|
|
172
|
-
| { action: "continue"; text: string };
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* What a goal does when the model ends a turn. Reads the request ledger, so
|
|
176
|
-
* the loop and every goal view agree. A reported blocker stops the loop --
|
|
177
|
-
* spending more requests against a stated blocker is the user's call -- but
|
|
178
|
-
* leaves the goal active: the statement is not evidence it cannot be met.
|
|
179
|
-
*/
|
|
180
|
-
export function goalDecision(state: RequestState, remainingRequests: number, idleChecks = 0): GoalDecision {
|
|
181
|
-
if (state.status === "verified") return { action: "met" };
|
|
182
|
-
if (state.status === "awaiting_human") return { action: "await_human" };
|
|
183
|
-
if (state.reported_blockers.length > 0) return { action: "stop", reason: `the model reported a blocker: ${state.reported_blockers.at(-1)!.reason}` };
|
|
184
|
-
if (remainingRequests < 1) return { action: "stop", reason: "the request budget is spent" };
|
|
185
|
-
if (idleChecks >= GOAL_IDLE_LIMIT) return { action: "stop", reason: `the model answered ${idleChecks} goal checks without acting` };
|
|
186
|
-
const missing = state.criteria.filter(item => item.kind === "command" && item.status !== "pass");
|
|
187
|
-
// An unchanged workspace failing the same way twice: rerunning is pointless,
|
|
188
|
-
// and the accepted check may be what is wrong. The model may not rewrite it;
|
|
189
|
-
// only the user can redraft a goal (S.2).
|
|
190
|
-
const stuck = missing.filter(item => (item.repeated_failures ?? 0) >= 2), rerun = missing.filter(item => !stuck.includes(item));
|
|
191
|
-
return { action: "continue", text: [
|
|
192
|
-
`Harness goal check: the goal is not met yet. Unmet: ${state.unmet.join("; ")}.`,
|
|
193
|
-
...(rerun.length ? ["Run each missing check exactly, as a separate invocation:",
|
|
194
|
-
...rerun.map(item => `- ${item.id}${item.guard ? " (a guard: it must keep passing)" : ""}: ${JSON.stringify(item.command)}`)] : []),
|
|
195
|
-
...stuck.map(item => `- ${item.id} failed ${item.repeated_failures} times in a row against an unchanged workspace. Do not rerun it as is: change the work, ` +
|
|
196
|
-
"or, if the check itself may itself be broken (a syntax error, a missing tool, or a test of the wrong thing), record which check is broken and why " +
|
|
197
|
-
"with note(kind='blocked') and stop. Do not rewrite or replace an accepted check; the user can redraft the goal with /goal."),
|
|
198
|
-
"Keep working toward the goal. If something outside your control blocks it, record the specific blocker with note(kind='blocked') and stop.",
|
|
199
|
-
].join("\n") };
|
|
200
|
-
}
|