@pellux/goodvibes-tui 2.1.0 → 2.1.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 +7 -1
- package/README.md +17 -16
- package/docs/README.md +4 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +17 -18
- package/src/input/command-reference.ts +2 -2
- package/src/verification/live-verifier.ts +0 -10
- package/src/version.ts +1 -1
- package/src/verification/verification-ledger.ts +0 -709
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [2.1.1] - 2026-11-02
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Restore local llama.cpp replies, including DeepSeek responses after tool calls, by excluding the active assistant rendering block from model requests. This release uses SDK 2.1.1 and daemon 1.29.1 and adds a terminal regression check for the outgoing conversation.
|
|
12
|
+
|
|
7
13
|
## [2.1.0] - 2026-09-30
|
|
8
14
|
|
|
9
15
|
### Added
|
|
@@ -1754,7 +1760,7 @@ First release of the best-in-class program: a full UX-first review of the codeba
|
|
|
1754
1760
|
- `bun run tsc --noEmit --pretty false`
|
|
1755
1761
|
- `bun run smoke:daemon`
|
|
1756
1762
|
- local `npm pack` install smoke with `GOODVIBES_ASSET_SOURCE_DIR=dist`
|
|
1757
|
-
-
|
|
1763
|
+
- The compiled `goodvibes-linux-x64 --version` command from a separate test workspace.
|
|
1758
1764
|
|
|
1759
1765
|
---
|
|
1760
1766
|
|
package/README.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/mgd34msu/goodvibes-tui)
|
|
6
6
|
|
|
7
7
|
GoodVibes is a terminal console for coding and operations work with an AI model. You run `goodvibes` in a project directory and get a full-screen terminal app where you talk to a model that can read and edit your files, run shell commands, search the web, and hand work off to background agents, asking your permission before anything that writes or executes.
|
|
8
8
|
|
|
9
9
|
It talks to many model providers (OpenAI, Anthropic, Gemini, Bedrock, Copilot, OpenRouter and other OpenAI-compatible gateways, plus local servers like Ollama and LM Studio that `/scan` finds on your machine and LAN), keeps its settings, sessions, and secrets on your own machine, and shows you the token count and running cost of every turn. Background work opens in large modals over the conversation: running agents, changed files and diffs, tokens and cost.
|
|
10
10
|
|
|
11
|
-
<img src="docs/assets/splash.png" alt="GoodVibes starting up in a terminal
|
|
11
|
+
<img src="docs/assets/splash.png" alt="GoodVibes 2.1.0 starting up in a terminal. A thin header reads GoodVibes 2.1.0 on the left and the git branch main with a changes dot and the model gpt-6.1-sol on the right. Centered below is a block-letter GOODVIBES wordmark in a cyan-to-violet gradient between two rules, the tagline, the version v2.1.0 with terminal AI assistant, the active model openai:gpt-6.1-sol with 29 tools, the working directory /tmp/orbit-watch, and a hint line reading Ctrl+P commands / ? help / F2 agents. At the bottom is an empty input box reading Ask anything, or type / for commands and @ for files, and a status line showing the normal permission mode, the directory and branch, a cost of zero dollars, an empty context bar at 0 percent of 1.1M, and ctrl+p menu." width="900">
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -47,21 +47,21 @@ Deeper install notes are in [docs/getting-started.md](docs/getting-started.md):
|
|
|
47
47
|
|
|
48
48
|
### The conversation loop
|
|
49
49
|
|
|
50
|
-
<img src="docs/assets/conversation.png" alt="Two conversation turns
|
|
50
|
+
<img src="docs/assets/conversation.png" alt="Two conversation turns. The user asks the model to search for nextPass and list every file that references it; the reply shows one collapsed find nextPass tool row with a green checkmark and the summary 2 matches in 2 files, then the answer listing src/index.ts and src/passes.ts. In the second turn the user asks, without running anything, for three things the tracker still needs, and the model answers with three short bullets about orbital-data refresh, pass-prediction validation, and alerts before upcoming passes. Each user message sits in a shaded block and each reply carries a small header with the model name, tool count, and elapsed time. The status line below the input shows the normal mode, the directory /tmp/orbit-watch, an estimated cost of about 3 cents, and the context bar at 1 percent, 14.4k of 1.1M tokens." width="900">
|
|
51
51
|
|
|
52
52
|
Tool calls stream inline as the model makes them, then their results fold into a collapsible group so a long working session stays readable. Expand any group to see what actually came back. Assistant messages render markdown, syntax-highlighted code, and inline diffs, and any block can be collapsed, bookmarked, copied, or saved to a file.
|
|
53
53
|
|
|
54
|
-
The
|
|
54
|
+
The status line under the input keeps a running total: the estimated cost so far and how much of the context window is used. `/usage` breaks that down into fresh input tokens, cache reads, and output tokens. When a model's price is not in the catalog, the cost is reported as unavailable rather than guessed.
|
|
55
55
|
|
|
56
56
|
### Permissions and workspace trust
|
|
57
57
|
|
|
58
|
-
<img src="docs/assets/workspace-trust.png" alt="A modal titled New workspace
|
|
58
|
+
<img src="docs/assets/workspace-trust.png" alt="A modal over the dimmed conversation, titled New workspace: choose a trust level, with an esc key on its right. Two options are listed: Trust this workspace, full capability, all tools may run, which is highlighted; and Keep restricted (read-only), explore safely, writes and commands are denied until trusted. The hint row reads up and down to move, Enter to choose. Behind it, the transcript shows the model starting exec mkdir -p build and waiting." width="820">
|
|
59
59
|
|
|
60
|
-
The first time
|
|
60
|
+
The first time the model wants to write or run something in a new directory, GoodVibes asks how much it is allowed to do there. Restricted is read-only. The model can look around, but writes and commands are refused until you trust the workspace.
|
|
61
61
|
|
|
62
|
-
<img src="docs/assets/permission-prompt.png" alt="
|
|
62
|
+
<img src="docs/assets/permission-prompt.png" alt="A Shell Execution Approval modal with an amber top edge over the dimmed conversation. The header reads EXECUTE and the session id. A shaded row shows the command mkdir -p build, followed by a red high risk tag beside Execute shell command. Three choices sit in a row, Allow once highlighted, Allow for session, and Deny, with d details on the right. The hint row reads: arrows to choose, Enter to confirm, y allow once, 1-4 remember, n deny, or type a reason to deny. Behind the modal the transcript line reads Waiting for your approval, exec, mkdir -p build." width="900">
|
|
63
63
|
|
|
64
|
-
After that, the default permission mode is `prompt`: writes, edits, shell commands, network fetches, agent spawns, and MCP calls each stop and ask. The prompt shows what will run
|
|
64
|
+
After that, the default permission mode is `prompt`: writes, edits, shell commands, network fetches, agent spawns, and MCP calls each stop and ask. The prompt shows what will run and its assessed risk; `d` expands the details, including the working directory, what it can affect, and the raw arguments. You can allow it once, deny it (optionally typing a reason the model sees), or remember the decision at whichever scope fits: this exact command, this command shape, this tool for the whole project, or just for the rest of the session.
|
|
65
65
|
|
|
66
66
|
Four other modes are available when prompting is not what you want:
|
|
67
67
|
|
|
@@ -74,7 +74,7 @@ Four other modes are available when prompting is not what you want:
|
|
|
74
74
|
|
|
75
75
|
### Models and providers
|
|
76
76
|
|
|
77
|
-
<img src="docs/assets/model-picker.png" alt="
|
|
77
|
+
<img src="docs/assets/model-picker.png" alt="The Models modal opened with /model. Tabs across the top name the routing targets: Main Chat, selected, then Helper (off), Tool LLM (off), TTS, and Embeddings. Below is a search field and a count reading available only, 2,574 of 3,214. The left column lists models grouped by provider, here abacusai, with each model's context window; the right pane describes the highlighted model, its provider, the current Main Chat model openai:gpt-6.1-sol, its status configured, its context window and capability tags, and the filter shortcuts for price, capability, availability, benchmark sort, and grouping. The footer reads Enter use for Main Chat, tab next target, ctrl+f pin, ctrl+r refresh catalog, and 2550 more." width="900">
|
|
78
78
|
|
|
79
79
|
Models come from a live catalog, so the picker lists far more than a hardcoded set, filterable by search, price, capability, and availability. Separate roles route independently: your main chat model, a cheaper helper model for grunt work, a tool model, a TTS model, and an embeddings model can each point somewhere different.
|
|
80
80
|
|
|
@@ -82,13 +82,13 @@ The `synthetic` provider groups the same model across every backend that serves
|
|
|
82
82
|
|
|
83
83
|
### The Agents control room
|
|
84
84
|
|
|
85
|
-
<img src="docs/assets/fleet-panel.png" alt="The
|
|
85
|
+
<img src="docs/assets/fleet-panel.png" alt="The Agents modal opened with F2, headed 0 running, the session's own cost, and the fleet cost. The filter field holds the word healthy and shows 3 of 8 rows. Under a running heading the list shows three watchers, CI watch poller, Daemon heartbeat, and Runtime heartbeat, each marked healthy with its elapsed time. The detail pane on the right describes the selected CI watch poller: watcher id ci-watch-poller, state idle, elapsed time, model unknown, tokens not applicable, cost unpriced, the headline CI watch poller, healthy, its activity phase, and approvals. The hint row reads Enter open full screen, s steer, ctrl+x stop, n new agent, v archive, a archive one, f follow." width="900">
|
|
86
86
|
|
|
87
|
-
Work that is not conversation opens in modals instead of scrolling past in the transcript. Agents, reached with `F2`, is the live control room for agents, WRFC chains, workstreams, watchers, scheduled jobs and hosted sessions: what is running, for how long, at what token cost, and what it is waiting on. You can open a running agent to watch its live tail or steer it, back out with `Esc` without stopping it, stop it with `Ctrl+X` (it asks first), pause and resume, and archive finished work. Usage (`/usage`) shows context pressure, tokens and cost; Changes (`/changes`) shows changed files with a tinted diff, a semantic summary, hunk staging and review comments; Notifications (`/notifications`) keeps the history.
|
|
87
|
+
Work that is not conversation opens in modals instead of scrolling past in the transcript. Agents, reached with `F2`, is the live control room for agents, WRFC chains, workstreams, watchers, scheduled jobs and hosted sessions: what is running, for how long, at what token cost, and what it is waiting on. You can open a running agent to watch its live tail or steer it, back out with `Esc` without stopping it, stop it with `Ctrl+X` (it asks first), pause and resume, and archive finished work. Usage (`/usage`) shows context pressure, tokens and cost; Changes (`/changes`) shows changed files with a tinted diff, a semantic summary, hunk staging and review comments; Notifications (`/notifications`) keeps the history.
|
|
88
88
|
|
|
89
89
|
### Everything else is a command
|
|
90
90
|
|
|
91
|
-
<img src="docs/assets/help-overlay.png" alt="
|
|
91
|
+
<img src="docs/assets/help-overlay.png" alt="The Help: Commands modal opened with the question-mark key, with a Type to filter field and a count of 160 items. Commands are grouped under category headings: provider accounts with /accounts and its review, panel, show, routes, and repair subcommands, highlighted; agents with /agents, which opens Agents and lists or hosts third-party coding agents over ACP; experience with /approval; platform access with /auth and its long list of subcommands; local runtime with /bookmarks; experience with /bootstrap; and branches with /branch. The hint row reads up and down to move, Enter to run, and 153 more." width="820">
|
|
92
92
|
|
|
93
93
|
Press `?` for a searchable, categorized list of every slash command with its arguments and description. The same list is generated into [docs/commands-reference.md](docs/commands-reference.md).
|
|
94
94
|
|
|
@@ -109,7 +109,7 @@ Press `?` for a searchable, categorized list of every slash command with its arg
|
|
|
109
109
|
| `Ctrl+X` | Inside an agent or process view: stop it (press twice to confirm) |
|
|
110
110
|
| `Ctrl+C` | Clear input, cancel a running turn. Press twice to quit |
|
|
111
111
|
|
|
112
|
-
Most bindings are customizable in `~/.goodvibes/tui/keybindings.json`, and `/keybindings` shows what is currently bound. Five keys are fixed and stay out of that file: `F2` (
|
|
112
|
+
Most bindings are customizable in `~/.goodvibes/tui/keybindings.json`, and `/keybindings` shows what is currently bound. Five keys are fixed and stay out of that file: `F2` (Agents), `Shift+Tab` (permission-mode cycle), `Esc` (leave the current mode), `?` (help), and `@` (file picker). The full reference is in [docs/tools-and-commands.md](docs/tools-and-commands.md).
|
|
113
113
|
|
|
114
114
|
---
|
|
115
115
|
|
|
@@ -213,12 +213,13 @@ bun run dev
|
|
|
213
213
|
| Command | Does |
|
|
214
214
|
| --- | --- |
|
|
215
215
|
| `bun run dev` | Run the TUI from source |
|
|
216
|
-
| `bun run test` | Run the
|
|
216
|
+
| `bun run test:changed` | Run the test files your change affects (since `origin/main`) |
|
|
217
|
+
| `bun run test` | Run the whole suite through the parallel per-file runner (CI runs it on every push) |
|
|
217
218
|
| `bun run build` | Compile `src/main.ts` into `dist/goodvibes` |
|
|
218
219
|
|
|
219
220
|
The compiled binary is the TUI entrypoint. With `daemon.enabled` on (the default) it adopts a running standalone GoodVibes daemon over loopback, and when a daemon is installed as a service but stopped, it starts that service once and waits for it to come online. It never embeds or constructs a daemon of its own. The control plane, HTTP listener (`danger.httpListener`), and web surface are all hosted by the daemon; the TUI configures them and reports their bindings.
|
|
220
221
|
|
|
221
|
-
Tests live under `src/test/`, mirroring the source tree
|
|
222
|
+
Tests live under `src/test/`, mirroring the source tree; byte-exact golden renderer frames are part of that suite. End-to-end tests under `src/test/e2e/` drive the compiled binary in a real terminal against a scripted model. CI also checks import cycles and layer boundaries (`scripts/check-architecture.ts`), and the release gates hold the performance budgets (`scripts/perf-baseline.json`). [Testing and validation](docs/testing-and-validation.md) says what runs where.
|
|
222
223
|
|
|
223
224
|
Some decisions worth knowing before you read the source:
|
|
224
225
|
|
|
@@ -248,7 +249,7 @@ src/
|
|
|
248
249
|
├── cli/ flag parsing, management verbs, doctor, launch-time self-update
|
|
249
250
|
├── tools/, mcp/ TUI-local tool guards, MCP runtime reload
|
|
250
251
|
├── audio/, export/ voice capture, wake word, playback and speech routing; gist upload
|
|
251
|
-
├── verification/, widget/ live verifier
|
|
252
|
+
├── verification/, widget/ live verifier, the terminal widget module
|
|
252
253
|
├── utils/, scripts/ formatting and clipboard helpers, message processing script
|
|
253
254
|
└── test/ the suite, mirroring the tree above
|
|
254
255
|
```
|
package/docs/README.md
CHANGED
|
@@ -64,8 +64,11 @@ This directory contains the current product documentation for `goodvibes-tui`.
|
|
|
64
64
|
- [Release and publishing](release-and-publishing.md)
|
|
65
65
|
Release validation, GitHub CD, compiled binary releases, npm distribution, and the GitHub Packages mirror.
|
|
66
66
|
|
|
67
|
+
- [Testing and validation](testing-and-validation.md)
|
|
68
|
+
What runs while you work, on every push, and before a release; the test layers; the end-to-end tests against the built binary.
|
|
69
|
+
|
|
67
70
|
- [Local verification](verification/local-verification.md)
|
|
68
|
-
|
|
71
|
+
GoodVibes home audits, compiled CLI probes, and authenticated daemon probes against your own machine.
|
|
69
72
|
|
|
70
73
|
- [Changelog](../CHANGELOG.md)
|
|
71
74
|
Current release history. Older per-version release-note files remain under `docs/releases/` for historical releases, but `CHANGELOG.md` is the canonical current stream.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Terminal-native GoodVibes client for coding, operations, knowledge and channel workflows; a pure client of the GoodVibes daemon.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
|
@@ -36,10 +36,13 @@
|
|
|
36
36
|
"build:all-shell": "bun run build:linux-x64 && bun run build:linux-arm64 && bun run build:macos-x64 && bun run build:macos-arm64 && bun run build:windows",
|
|
37
37
|
"typecheck": "bunx tsc --noEmit",
|
|
38
38
|
"typecheck:test": "bunx tsc --noEmit -p tsconfig.test.json",
|
|
39
|
-
"typecheck:coverage": "bun run scripts/typecheck-coverage-rule.ts",
|
|
40
39
|
"test": "bun run scripts/run-tests.ts",
|
|
41
|
-
"test:
|
|
42
|
-
"
|
|
40
|
+
"test:changed": "bun run scripts/run-tests.ts --changed=origin/main",
|
|
41
|
+
"test:e2e": "bun test --timeout=300000 src/test/e2e",
|
|
42
|
+
"test:e2e:fast": "bun test --timeout=120000 src/test/e2e/startup.e2e.test.ts src/test/e2e/first-turn.e2e.test.ts",
|
|
43
|
+
"test:e2e:release": "bun test --timeout=300000 src/test/e2e/daemon-adoption.e2e.test.ts src/test/e2e/wrfc-chain.e2e.test.ts src/test/e2e/turn-notification.e2e.test.ts src/test/e2e/resize.e2e.test.ts",
|
|
44
|
+
"version": "bun run scripts/release-prepare.ts --no-bump",
|
|
45
|
+
"release:prepare": "bun run scripts/release-prepare.ts",
|
|
43
46
|
"preinstall": "sh scripts/check-bun.sh",
|
|
44
47
|
"postinstall": "bun scripts/postinstall.js",
|
|
45
48
|
"postbuild": "bun scripts/postinstall.js --no-download",
|
|
@@ -63,11 +66,7 @@
|
|
|
63
66
|
"audit:home": "bun run scripts/audit-goodvibes-home.ts",
|
|
64
67
|
"foundation:artifacts": "bun run scripts/export-foundation-artifacts.ts",
|
|
65
68
|
"docs:commands": "bun run scripts/generate-command-reference.ts",
|
|
66
|
-
"verification:
|
|
67
|
-
"verification:live": "bun run scripts/verify-live.ts",
|
|
68
|
-
"eval:gate": "bun run scripts/eval-gate.ts",
|
|
69
|
-
"eval:gate:verbose": "bun run scripts/eval-gate.ts --verbose",
|
|
70
|
-
"eval:baseline": "bun run scripts/eval-gate.ts --save-baseline"
|
|
69
|
+
"verification:live": "bun run scripts/verify-live.ts"
|
|
71
70
|
},
|
|
72
71
|
"license": "MIT",
|
|
73
72
|
"repository": {
|
|
@@ -96,18 +95,18 @@
|
|
|
96
95
|
"@anthropic-ai/bedrock-sdk": "^0.29.1",
|
|
97
96
|
"@anthropic-ai/sdk": "^0.92.0",
|
|
98
97
|
"@ast-grep/napi": "^0.42.0",
|
|
99
|
-
"@pellux/goodvibes-sdk": "2.1.
|
|
100
|
-
"@pellux/goodvibes-terminal-shell": "2.1.
|
|
98
|
+
"@pellux/goodvibes-sdk": "2.1.1",
|
|
99
|
+
"@pellux/goodvibes-terminal-shell": "2.1.1",
|
|
101
100
|
"bash-language-server": "^5.6.0",
|
|
102
101
|
"fuse.js": "^7.1.0",
|
|
103
|
-
"@pellux/goodvibes-daemon": "1.29.
|
|
102
|
+
"@pellux/goodvibes-daemon": "1.29.1",
|
|
104
103
|
"graphql": "^16.13.2",
|
|
105
104
|
"jszip": "^3.10.1",
|
|
106
105
|
"node-edge-tts": "^1.2.10",
|
|
107
106
|
"onnxruntime-web": "^1.27.0",
|
|
108
107
|
"openai": "6.46.0",
|
|
109
108
|
"pyright": "^1.1.408",
|
|
110
|
-
"simple-git": "^
|
|
109
|
+
"simple-git": "^4.0.2",
|
|
111
110
|
"sql.js": "^1.14.1",
|
|
112
111
|
"sqlite-vec": "^0.1.9",
|
|
113
112
|
"tree-sitter-css": "^0.25.0",
|
|
@@ -121,15 +120,15 @@
|
|
|
121
120
|
"zustand": "^5.0.12"
|
|
122
121
|
},
|
|
123
122
|
"devDependencies": {
|
|
124
|
-
"@pellux/goodvibes-toolchain": "2.1.
|
|
123
|
+
"@pellux/goodvibes-toolchain": "2.1.1",
|
|
125
124
|
"@types/bun": "^1.3.10",
|
|
126
125
|
"typescript": "^5.9.3"
|
|
127
126
|
},
|
|
128
127
|
"optionalDependencies": {
|
|
129
|
-
"@pellux/goodvibes-tui-linux-x64": "2.
|
|
130
|
-
"@pellux/goodvibes-tui-linux-arm64": "2.
|
|
131
|
-
"@pellux/goodvibes-tui-darwin-x64": "2.
|
|
132
|
-
"@pellux/goodvibes-tui-darwin-arm64": "2.
|
|
128
|
+
"@pellux/goodvibes-tui-linux-x64": "2.1.1",
|
|
129
|
+
"@pellux/goodvibes-tui-linux-arm64": "2.1.1",
|
|
130
|
+
"@pellux/goodvibes-tui-darwin-x64": "2.1.1",
|
|
131
|
+
"@pellux/goodvibes-tui-darwin-arm64": "2.1.1"
|
|
133
132
|
},
|
|
134
133
|
"trustedDependencies": [],
|
|
135
134
|
"overrides": {
|
|
@@ -53,8 +53,8 @@ export function renderCommandReferenceMarkdown(entries: readonly CategorizedComm
|
|
|
53
53
|
lines.push('<!-- GENERATED FILE: DO NOT EDIT BY HAND.');
|
|
54
54
|
lines.push(' Regenerate with `bun run docs:commands`.');
|
|
55
55
|
lines.push(' Source of truth: the slash-command registry (src/input/commands.ts).');
|
|
56
|
-
lines.push('
|
|
57
|
-
lines.push('
|
|
56
|
+
lines.push(' Every build (scripts/prebuild.ts) and `bun run release:prepare`');
|
|
57
|
+
lines.push(' rewrite it, so it is current at each version bump. -->');
|
|
58
58
|
lines.push('');
|
|
59
59
|
lines.push('# Command Reference');
|
|
60
60
|
lines.push('');
|
|
@@ -2,7 +2,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { auditGoodVibesHome } from '../config/goodvibes-home-audit.ts';
|
|
5
|
-
import { buildVerificationLedger } from './verification-ledger.ts';
|
|
6
5
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
7
6
|
|
|
8
7
|
export type LiveVerificationStatus = 'pass' | 'warn' | 'fail' | 'skip';
|
|
@@ -232,15 +231,6 @@ export async function buildLiveVerificationReport(options: LiveVerificationOptio
|
|
|
232
231
|
const token = options.token ?? readDaemonToken(homeDir);
|
|
233
232
|
const checks: LiveVerificationCheck[] = [];
|
|
234
233
|
|
|
235
|
-
const ledger = buildVerificationLedger(projectRoot);
|
|
236
|
-
checks.push({
|
|
237
|
-
id: 'verification-ledger',
|
|
238
|
-
title: 'Verification inventory ledger',
|
|
239
|
-
status: ledger.totals.localSignalPercent >= 90 ? 'pass' : 'fail',
|
|
240
|
-
summary: `${ledger.totals.localSignalPercent}% local verification signal across ${ledger.totals.total} inventory items.`,
|
|
241
|
-
detail: `${ledger.totals.localBehaviorPercent}% local behavior verified; ${ledger.totals.externalOutcomeRequired} item(s) require external outcomes.`,
|
|
242
|
-
});
|
|
243
|
-
|
|
244
234
|
const audit = await auditGoodVibesHome({ homeDir });
|
|
245
235
|
const staleCandidates = audit.settings?.staleCandidates?.length ?? 0;
|
|
246
236
|
checks.push({
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '2.
|
|
9
|
+
let _version = '2.1.1';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
// Only trust a version read from OUR OWN package.json. A Bun single-file
|
|
@@ -1,709 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { CONFIG_SCHEMA } from '@pellux/goodvibes-sdk/platform/config';
|
|
4
|
-
import { FEATURE_SETTINGS } from '@pellux/goodvibes-sdk/platform/runtime/state';
|
|
5
|
-
import { featureEnablementWrite, GOODVIBES_CLI_CATALOG } from '@pellux/goodvibes-terminal-shell';
|
|
6
|
-
import { CommandRegistry } from '../input/command-registry.ts';
|
|
7
|
-
import { registerBuiltinCommands } from '../input/commands.ts';
|
|
8
|
-
|
|
9
|
-
export interface VerificationLedgerArea {
|
|
10
|
-
readonly area: string;
|
|
11
|
-
readonly total: number;
|
|
12
|
-
readonly localSignalVerified: number;
|
|
13
|
-
readonly localBehaviorVerified: number;
|
|
14
|
-
readonly externalOutcomeRequired: number;
|
|
15
|
-
readonly notes: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface VerificationLedger {
|
|
19
|
-
readonly generatedAt: string;
|
|
20
|
-
readonly areas: readonly VerificationLedgerArea[];
|
|
21
|
-
readonly totals: {
|
|
22
|
-
readonly total: number;
|
|
23
|
-
readonly localSignalVerified: number;
|
|
24
|
-
readonly localBehaviorVerified: number;
|
|
25
|
-
readonly externalOutcomeRequired: number;
|
|
26
|
-
readonly localSignalPercent: number;
|
|
27
|
-
readonly localBehaviorPercent: number;
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Baseline count of `Settings schema and persistence` items with real local
|
|
33
|
-
* behavior verification, as measured when this ledger was first authored. New
|
|
34
|
-
* schema keys added since then are accounted for explicitly (see
|
|
35
|
-
* FEATURE_KNOB_LOCAL_SETTINGS) rather than folded into this number, so the
|
|
36
|
-
* baseline stays an auditable fixed point.
|
|
37
|
-
*/
|
|
38
|
-
const SETTINGS_BEHAVIOR_BASELINE = 184;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Config keys promoted into CONFIG_SCHEMA for the flag-gated feature knobs
|
|
42
|
-
* (SDK commit f5c4af31, "config: add schema keys for flag-gated feature
|
|
43
|
-
* knobs"). Adding these 28 keys raised the settings inventory `total` without a
|
|
44
|
-
* matching rise in behavior-verified coverage, which dropped
|
|
45
|
-
* `localBehaviorPercent` below its floor. Each key is a local, non-external
|
|
46
|
-
* setting whose full persistence behavior, schema default, set → write →
|
|
47
|
-
* reload round-trip, and reset-to-default, is exercised by
|
|
48
|
-
* `src/test/verification/feature-knob-settings-persistence.test.ts`. They are
|
|
49
|
-
* therefore genuinely behavior-verified locally and counted as such here.
|
|
50
|
-
*/
|
|
51
|
-
/**
|
|
52
|
-
* The daemon's own mailbox and calendar keys (`surfaces.email.*`,
|
|
53
|
-
* `surfaces.calendar.*`), promoted into CONFIG_SCHEMA so the settings modal,
|
|
54
|
-
* which renders from that schema, can actually show them. Before, the
|
|
55
|
-
* daemon's handlers named these keys in their own error messages
|
|
56
|
-
* ("Set surfaces.calendar.caldavUrl and surfaces.calendar.caldavUser") while
|
|
57
|
-
* the UI that told an operator to set them could not display one of them.
|
|
58
|
-
*
|
|
59
|
-
* Declaring 25 keys raised the settings inventory `total` with no matching
|
|
60
|
-
* behavior coverage, which dropped `localBehaviorPercent` below its floor,
|
|
61
|
-
* the same arrival shape as the two lists above.
|
|
62
|
-
*
|
|
63
|
-
* THIS CONSTANT IS NOT A DIAL. Every key below is counted because
|
|
64
|
-
* `src/test/verification/daemon-mailbox-settings-persistence.test.ts` runs the
|
|
65
|
-
* same four-part persistence contract the other counted sets are counted for:
|
|
66
|
-
* schema default exposure, `set()` through the key's own validator to disk,
|
|
67
|
-
* reload into a fresh ConfigManager with read-back equality, and
|
|
68
|
-
* reset-to-default that also survives reload. That test also asserts this list
|
|
69
|
-
* is exactly the `surfaces.email.*` + `surfaces.calendar.*` schema key set and
|
|
70
|
-
* overlaps neither other counted set, so nothing is double-counted and nothing
|
|
71
|
-
* drifts in uncounted.
|
|
72
|
-
*
|
|
73
|
-
* PER-KEY EVIDENCE. All 42 are read by a LIVE consumer, which is why they were
|
|
74
|
-
* declared at all: the SDK's mail and calendar gateway compositions
|
|
75
|
-
* (platform/email/surface-config.ts and platform/calendar/caldav-gateway-config.ts)
|
|
76
|
-
* resolve every one of them when the daemon serves `email.*` and `calendar.*`,
|
|
77
|
-
* and the `surfaces.email.inbound.*` block is read by the daemon's inbound mail
|
|
78
|
-
* reader, the poller/IDLE path that makes incoming mail visible at all.
|
|
79
|
-
* The five password keys (email.password, email.imapPassword,
|
|
80
|
-
* email.imap.password, email.smtp.password, calendar.caldavPassword)
|
|
81
|
-
* additionally resolve through the daemon secret tier rather than from config,
|
|
82
|
-
* and carry no secret value in config themselves. That is now enforced on the
|
|
83
|
-
* write side too: all five are in `config/secret-config.ts`'s
|
|
84
|
-
* SECRET_CONFIG_KEYS, so the settings modal and `/config set` route an entered
|
|
85
|
-
* value into the secret store and leave only a `goodvibes://` reference in
|
|
86
|
-
* config, rather than writing the password itself into a settings file.
|
|
87
|
-
*/
|
|
88
|
-
export const DAEMON_MAILBOX_LOCAL_SETTINGS = [
|
|
89
|
-
'surfaces.email.host',
|
|
90
|
-
'surfaces.email.user',
|
|
91
|
-
'surfaces.email.username',
|
|
92
|
-
'surfaces.email.from',
|
|
93
|
-
'surfaces.email.password',
|
|
94
|
-
'surfaces.email.imapHost',
|
|
95
|
-
'surfaces.email.imapPort',
|
|
96
|
-
'surfaces.email.imapUser',
|
|
97
|
-
'surfaces.email.imapPassword',
|
|
98
|
-
'surfaces.email.imap.host',
|
|
99
|
-
'surfaces.email.imap.port',
|
|
100
|
-
'surfaces.email.imap.user',
|
|
101
|
-
'surfaces.email.imap.password',
|
|
102
|
-
'surfaces.email.imap.secure',
|
|
103
|
-
'surfaces.email.imap.mailbox',
|
|
104
|
-
'surfaces.email.imap.draftsMailbox',
|
|
105
|
-
'surfaces.email.smtp.host',
|
|
106
|
-
'surfaces.email.smtp.port',
|
|
107
|
-
'surfaces.email.smtp.password',
|
|
108
|
-
'surfaces.email.smtp.secure',
|
|
109
|
-
// Inbound mail, the poller/IDLE reader the daemon runs to actually READ mail,
|
|
110
|
-
// which arrived with the platform runtime. These are counted for exactly the
|
|
111
|
-
// reason the keys above are: the same four-part persistence contract runs over
|
|
112
|
-
// every one of them in
|
|
113
|
-
// src/test/verification/daemon-mailbox-settings-persistence.test.ts, and the
|
|
114
|
-
// list is asserted there to be the complete surfaces.email.*/surfaces.calendar.*
|
|
115
|
-
// schema set. Arriving uncounted is what dropped localBehaviorPercent through
|
|
116
|
-
// its floor when the schema grew.
|
|
117
|
-
'surfaces.email.inbound.enabled',
|
|
118
|
-
'surfaces.email.inbound.accounts',
|
|
119
|
-
'surfaces.email.inbound.source',
|
|
120
|
-
'surfaces.email.inbound.gmailPollSecondsExpecting',
|
|
121
|
-
'surfaces.email.inbound.gmailPollSecondsIdle',
|
|
122
|
-
'surfaces.email.inbound.mode',
|
|
123
|
-
'surfaces.email.inbound.pollIntervalSeconds',
|
|
124
|
-
'surfaces.email.inbound.idleReissueMinutes',
|
|
125
|
-
'surfaces.email.inbound.reconnect.maxBackoffSeconds',
|
|
126
|
-
'surfaces.email.inbound.notice.route',
|
|
127
|
-
'surfaces.email.inbound.notice.mode',
|
|
128
|
-
'surfaces.email.inbound.expectationWindowMinutes',
|
|
129
|
-
'surfaces.email.inbound.dedupTtlMinutes',
|
|
130
|
-
'surfaces.email.inbound.retentionDays',
|
|
131
|
-
'surfaces.email.inbound.maxRecords',
|
|
132
|
-
'surfaces.email.inbound.capabilityRecheckMinutes',
|
|
133
|
-
'surfaces.email.inbound.onInsufficientCapability',
|
|
134
|
-
'surfaces.calendar.caldavUrl',
|
|
135
|
-
'surfaces.calendar.caldavUser',
|
|
136
|
-
'surfaces.calendar.caldavPassword',
|
|
137
|
-
'surfaces.calendar.defaultCalendarId',
|
|
138
|
-
'surfaces.calendar.calendars',
|
|
139
|
-
] as const;
|
|
140
|
-
|
|
141
|
-
export const FEATURE_KNOB_LOCAL_SETTINGS = [
|
|
142
|
-
'provider.optimizerMode',
|
|
143
|
-
'provider.optimizerPinnedModel',
|
|
144
|
-
'permissions.divergenceThreshold',
|
|
145
|
-
'permissions.maxDivergenceRecords',
|
|
146
|
-
'tools.overflowSpillBackend',
|
|
147
|
-
'notifications.burstWindowMs',
|
|
148
|
-
'notifications.burstThreshold',
|
|
149
|
-
'notifications.burstCooldownMs',
|
|
150
|
-
'fetch.sanitizeMode',
|
|
151
|
-
'fetch.trustedHosts',
|
|
152
|
-
'fetch.blockedHosts',
|
|
153
|
-
'security.tokenAudit.rotationCadenceDays',
|
|
154
|
-
'security.tokenAudit.rotationWarningDays',
|
|
155
|
-
'security.tokenAudit.managed',
|
|
156
|
-
'integrations.delivery.maxRetries',
|
|
157
|
-
'integrations.delivery.initialDelayMs',
|
|
158
|
-
'integrations.delivery.maxDelayMs',
|
|
159
|
-
'integrations.delivery.maxDlqSize',
|
|
160
|
-
'integrations.delivery.sloEnforced',
|
|
161
|
-
'policy.bundleSource',
|
|
162
|
-
'policy.bundlePath',
|
|
163
|
-
'agents.passiveInjection.budgetTokens',
|
|
164
|
-
'agents.passiveInjection.relevanceFloor',
|
|
165
|
-
'agents.passiveInjection.codeLimit',
|
|
166
|
-
'agents.contextCompactThreshold',
|
|
167
|
-
'runtime.toolBudget.maxMs',
|
|
168
|
-
'runtime.toolBudget.maxTokens',
|
|
169
|
-
'runtime.toolBudget.maxCostUsd',
|
|
170
|
-
] as const;
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Config keys added by the paired-device and trigger-family domains in SDK
|
|
174
|
-
* 1.14.0. Like FEATURE_KNOB_LOCAL_SETTINGS above, these arrived as new
|
|
175
|
-
* CONFIG_SCHEMA entries that raised the settings inventory `total` with no
|
|
176
|
-
* matching behavior coverage, dropping `localBehaviorPercent` below its floor.
|
|
177
|
-
*
|
|
178
|
-
* THIS CONSTANT IS NOT A DIAL. Each key below is counted because a test that
|
|
179
|
-
* exercises it was actually written:
|
|
180
|
-
* `src/test/verification/device-and-trigger-settings-persistence.test.ts` runs,
|
|
181
|
-
* for every key in this list, the same four-part persistence contract the
|
|
182
|
-
* feature-knob keys are counted for, schema default exposure, `set()` through
|
|
183
|
-
* the key's own validator to disk, reload into a fresh ConfigManager with
|
|
184
|
-
* read-back equality, and reset-to-default that also survives reload. That test
|
|
185
|
-
* additionally asserts this list is exactly the non-enablement key set of both
|
|
186
|
-
* domains and overlaps neither of the other counted sets, so nothing here is
|
|
187
|
-
* double-counted and nothing drifts in uncounted.
|
|
188
|
-
*
|
|
189
|
-
* PER-KEY EVIDENCE, what is verified, and how live the key is TODAY.
|
|
190
|
-
* "persistence" below means the four-part contract above; the second column
|
|
191
|
-
* records whether anything in this tree (TUI or the pinned SDK) reads the key,
|
|
192
|
-
* audited by tracing each key to its consumers.
|
|
193
|
-
*
|
|
194
|
-
* watchers.triggers.backoffLadderMs persistence · live (trigger supervisor retry ladder)
|
|
195
|
-
* watchers.triggers.breakerStrikes persistence · live (strike breaker)
|
|
196
|
-
* watchers.triggers.defaultCheckIntervalMs persistence · live (condition-check scheduler)
|
|
197
|
-
* watchers.triggers.probeTimeoutMs persistence · live (probe execution)
|
|
198
|
-
* watchers.triggers.maxConcurrentChecks persistence · live (check concurrency cap)
|
|
199
|
-
* watchers.triggers.observationRingSize persistence · live (observation ring)
|
|
200
|
-
* watchers.triggers.runHistoryLimit persistence · live (run history retention)
|
|
201
|
-
* watchers.triggers.runHistoryTtlHours persistence · live (run history sweep)
|
|
202
|
-
* watchers.triggers.eventLogLimit persistence · live (event log retention)
|
|
203
|
-
* watchers.triggers.eventLogTtlHours persistence · live (event log sweep)
|
|
204
|
-
* watchers.triggers.sweepIntervalMs persistence · live (housekeeping cadence)
|
|
205
|
-
* watchers.triggers.supervisionTickMs persistence · live (supervision loop tick)
|
|
206
|
-
* watchers.triggers.streamQueueLimit persistence · live (stream watcher queue)
|
|
207
|
-
* watchers.triggers.streamBatchLines persistence · live (stream batching)
|
|
208
|
-
* watchers.triggers.streamBatchIntervalMs persistence · live (stream batching)
|
|
209
|
-
* watchers.triggers.onExitMaxDurationMs persistence · live (on-exit trigger duration cap)
|
|
210
|
-
* watchers.triggers.onExitStdin persistence · live (on-exit stdin posture)
|
|
211
|
-
* watchers.triggers.outputTailBytes persistence · live (captured output tail)
|
|
212
|
-
* device.capabilities.allowAlwaysOffer persistence · behavior (durable-grant offer by sensitivity)
|
|
213
|
-
* device.capabilities.requestTimeoutSeconds persistence · behavior (dispatch, wire payload and prompt deadline)
|
|
214
|
-
* device.location.precision persistence · behavior (precise fix refused / never granted)
|
|
215
|
-
* device.clipboard.readMode persistence · behavior (clipboard read refused / never granted)
|
|
216
|
-
* device.capture.retentionHours persistence · behavior (capture TTL, swept off disk at expiry)
|
|
217
|
-
* device.capture.maxArtifacts persistence · behavior (capture count cap at the sweep)
|
|
218
|
-
* device.capture.sweepIntervalMinutes persistence · behavior (period of the sweep that reaps)
|
|
219
|
-
* device.grants.expiryDays persistence · behavior (grant TTL, then asks again)
|
|
220
|
-
* device.grants.maxPerNode persistence · behavior (per-node grant cap at the sweep)
|
|
221
|
-
* device.grants.auditRetentionDays persistence · behavior (grant ledger retention)
|
|
222
|
-
* device.nodes.maxPaired persistence · live (enforced at the pairing path)
|
|
223
|
-
*
|
|
224
|
-
* On `device.nodes.maxPaired`: the SDK enforces it where a device pairs,
|
|
225
|
-
* PairingTokenManager.mint refuses a NEW node at the cap with
|
|
226
|
-
* PairingLimitReachedError (code DEVICE_NODES_MAX_PAIRED, carrying the setting
|
|
227
|
-
* name, the cap and the live count), mapped to HTTP 409 by the pairing and
|
|
228
|
-
* pairing-handoff routes. An already-paired node supersedes its own record
|
|
229
|
-
* instead of being refused, migration mints off the legacy shared token are
|
|
230
|
-
* deliberately exempt, lowering the cap unpairs nobody, and a non-positive or
|
|
231
|
-
* non-finite value reads as no cap so a broken setting cannot lock the owner
|
|
232
|
-
* out. The cap is read per mint, so a change applies without a restart. What is
|
|
233
|
-
* counted HERE is still only the persistence contract this repo's test
|
|
234
|
-
* exercises, the enforcement itself is SDK-side and SDK-tested, and is recorded
|
|
235
|
-
* in this column so the "how live is it" audit stays accurate, not to claim
|
|
236
|
-
* coverage this repo did not write.
|
|
237
|
-
*
|
|
238
|
-
* On the other ten `device.*` rows: they are live now. This app composes the
|
|
239
|
-
* platform's device posture runtime (src/runtime/device-posture-composition.ts)
|
|
240
|
-
* on the same distributed runtime phones pair onto and the same approval broker
|
|
241
|
-
* every other confirmation rides, registers the `phone` tool on it, and binds the
|
|
242
|
-
* devices.* verbs to it, so each of these keys is read, per request and per
|
|
243
|
-
* sweep, by the DeviceCapabilityService / DeviceGrantStore /
|
|
244
|
-
* DeviceCaptureArtifactStore / DeviceHousekeeper that serve them. They were dark
|
|
245
|
-
* before that: the policy struct carried matching defaults and named the keys in
|
|
246
|
-
* its refusal messages, but nothing mapped configuration into it in either tree.
|
|
247
|
-
* The mapping now lives in the SDK, so every daemon host honours it rather than
|
|
248
|
-
* only whichever consumer had written its own.
|
|
249
|
-
*
|
|
250
|
-
* The second column above is upgraded to "behavior" on the strength of
|
|
251
|
-
* `src/test/verification/device-posture-behavior.test.ts`, which drives THIS
|
|
252
|
-
* app's composition with a real ConfigManager over a temp home and the platform's
|
|
253
|
-
* real stores, stubs only the peer transport and the approval bridge, and asserts
|
|
254
|
-
* per key at two values that the observable outcome differs, refusal code, how
|
|
255
|
-
* authority was established, what reached the transport, what the prompt was
|
|
256
|
-
* given, and what survives on disk after a sweep. It also invokes the `phone`
|
|
257
|
-
* tool and the devices.* verbs off the composed runtime graph, because a mapping
|
|
258
|
-
* nothing composes is the same defect in a different place.
|
|
259
|
-
*
|
|
260
|
-
* STILL NOT COUNTED, FOR A DIFFERENT REASON NOW: the 24 non-enablement
|
|
261
|
-
* `voice.wake.*` keys from the same release. When this list was written they were
|
|
262
|
-
* excluded because nothing captured audio at all. That is no longer true, this
|
|
263
|
-
* terminal opens a recorder subprocess (src/audio/capture.ts), runs the SDK
|
|
264
|
-
* detector over it through onnxruntime-web (src/audio/wake-inference.ts), and
|
|
265
|
-
* hands both a confirmed wake's utterance and a push-to-talk recording to the
|
|
266
|
-
* daemon's `voice.stt` verb. Every one of those rows is read by
|
|
267
|
-
* resolveWakeRuntimeSettings and reaches this surface's runtime.
|
|
268
|
-
*
|
|
269
|
-
* They stay out of the NUMERATOR here because this constant counts exactly one
|
|
270
|
-
* thing, keys whose four-part PERSISTENCE contract is exercised by
|
|
271
|
-
* device-and-trigger-settings-persistence.test.ts, and that test does not cover
|
|
272
|
-
* them. The behaviour they now drive is covered instead by the capture and wake
|
|
273
|
-
* tests under src/test/audio/, which assert the frame path, the detection
|
|
274
|
-
* chain, the disabled-means-no-capture rule, the supervisor's restart and latch,
|
|
275
|
-
* and the recorder argv. Counting them in both places would double-count; adding
|
|
276
|
-
* them here without writing their persistence round-trip would overclaim. So the
|
|
277
|
-
* number this list raises coverage by is unchanged, and the reason it excludes
|
|
278
|
-
* them is recorded above rather than left reading as "the feature does nothing".
|
|
279
|
-
*
|
|
280
|
-
* Every wake row is now in force on this surface, with one condition stated at
|
|
281
|
-
* every surfacing: `voice.wake.vadThreshold` above 0 needs the speech gate's own
|
|
282
|
-
* pinned artifact provisioned (`/voice wake setup` fetches it with the models),
|
|
283
|
-
* and until it is, startup BLOCKS with that reason rather than scoring frames
|
|
284
|
-
* ungated behind a row that says they are screened. `voice.wake.noiseSuppression:
|
|
285
|
-
* speex` runs here: the filter is a WebAssembly module the SDK carries, applied
|
|
286
|
-
* by the wrapper the listener and the push-to-talk session put around this
|
|
287
|
-
* surface's capture opener.
|
|
288
|
-
*/
|
|
289
|
-
export const DEVICE_AND_TRIGGER_LOCAL_SETTINGS = [
|
|
290
|
-
'watchers.triggers.backoffLadderMs',
|
|
291
|
-
'watchers.triggers.breakerStrikes',
|
|
292
|
-
'watchers.triggers.defaultCheckIntervalMs',
|
|
293
|
-
'watchers.triggers.probeTimeoutMs',
|
|
294
|
-
'watchers.triggers.maxConcurrentChecks',
|
|
295
|
-
'watchers.triggers.observationRingSize',
|
|
296
|
-
'watchers.triggers.runHistoryLimit',
|
|
297
|
-
'watchers.triggers.runHistoryTtlHours',
|
|
298
|
-
'watchers.triggers.eventLogLimit',
|
|
299
|
-
'watchers.triggers.eventLogTtlHours',
|
|
300
|
-
'watchers.triggers.sweepIntervalMs',
|
|
301
|
-
'watchers.triggers.supervisionTickMs',
|
|
302
|
-
'watchers.triggers.streamQueueLimit',
|
|
303
|
-
'watchers.triggers.streamBatchLines',
|
|
304
|
-
'watchers.triggers.streamBatchIntervalMs',
|
|
305
|
-
'watchers.triggers.onExitMaxDurationMs',
|
|
306
|
-
'watchers.triggers.onExitStdin',
|
|
307
|
-
'watchers.triggers.outputTailBytes',
|
|
308
|
-
'device.capabilities.allowAlwaysOffer',
|
|
309
|
-
'device.capabilities.requestTimeoutSeconds',
|
|
310
|
-
'device.location.precision',
|
|
311
|
-
'device.clipboard.readMode',
|
|
312
|
-
'device.capture.retentionHours',
|
|
313
|
-
'device.capture.maxArtifacts',
|
|
314
|
-
'device.capture.sweepIntervalMinutes',
|
|
315
|
-
'device.grants.expiryDays',
|
|
316
|
-
'device.grants.maxPerNode',
|
|
317
|
-
'device.grants.auditRetentionDays',
|
|
318
|
-
'device.nodes.maxPaired',
|
|
319
|
-
] as const;
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Every feature's enablement settings key whose on/off writes round-trip
|
|
323
|
-
* through a REAL on-disk ConfigManager in
|
|
324
|
-
* `src/test/verification/../input/settings-modal-flag-persistence.test.ts`
|
|
325
|
-
* ("every feature with an off position round-trips both directions"). These
|
|
326
|
-
* are first-class schema keys under the dissolved feature model, so they are
|
|
327
|
-
* genuinely behavior-verified locally. Keys already counted in the
|
|
328
|
-
* feature-knob list are excluded to keep the sum honest.
|
|
329
|
-
*/
|
|
330
|
-
const ENABLEMENT_KEYS_BEHAVIOR_VERIFIED = new Set(
|
|
331
|
-
FEATURE_SETTINGS
|
|
332
|
-
.filter((feature) => featureEnablementWrite(feature.id, true) !== null && featureEnablementWrite(feature.id, false) !== null)
|
|
333
|
-
.map((feature) => feature.enablement.key)
|
|
334
|
-
.filter((key) => !(FEATURE_KNOB_LOCAL_SETTINGS as readonly string[]).includes(key)),
|
|
335
|
-
).size;
|
|
336
|
-
|
|
337
|
-
/** Settings with real local behavior verification: the authored baseline plus the persistence-tested feature-knob, device/trigger, and enablement keys. */
|
|
338
|
-
/**
|
|
339
|
-
* Config keys added by the LAN group-key layer: the automatic group-key
|
|
340
|
-
* rotation interval, the dual-generation acceptance window around a rotation,
|
|
341
|
-
* the discovery beacon interval, and the roster gossip interval.
|
|
342
|
-
*
|
|
343
|
-
* THIS CONSTANT IS NOT A DIAL, for the same reason as the two above. Each key
|
|
344
|
-
* is counted because a test that exercises it was actually written:
|
|
345
|
-
* `src/test/verification/cluster-group-settings-persistence.test.ts` runs the
|
|
346
|
-
* same four-part persistence contract, schema default exposure, `set()`
|
|
347
|
-
* through the key's own validator to disk, reload into a fresh ConfigManager
|
|
348
|
-
* with read-back equality, and reset-to-default that survives reload.
|
|
349
|
-
*
|
|
350
|
-
* PER-KEY EVIDENCE. All four have a LIVE consumer in this build, which is more
|
|
351
|
-
* than several of the keys counted above can claim:
|
|
352
|
-
* cluster.keyRotationHours , read by resolveClusterGroupSettings and
|
|
353
|
-
* compared against the current key's age in
|
|
354
|
-
* the group runtime's rotation check.
|
|
355
|
-
* cluster.keyRotationGraceMinutes, sets how long the previous generation
|
|
356
|
-
* stays accepted after a scheduled
|
|
357
|
-
* rotation; drives the keyring's accepted
|
|
358
|
-
* generation set.
|
|
359
|
-
* cluster.beaconSeconds , the discovery beacon timer interval, and
|
|
360
|
-
* the basis of the "recently heard from"
|
|
361
|
-
* window that decides which member mints a
|
|
362
|
-
* rotation.
|
|
363
|
-
* cluster.rosterGossipSeconds , how often the member list is shared.
|
|
364
|
-
*
|
|
365
|
-
* `cluster.enabled` is NOT here: it is a feature enablement key and the ledger
|
|
366
|
-
* counts those in its own set.
|
|
367
|
-
*/
|
|
368
|
-
export const CLUSTER_GROUP_LOCAL_SETTINGS = [
|
|
369
|
-
'cluster.keyRotationHours',
|
|
370
|
-
'cluster.keyRotationGraceMinutes',
|
|
371
|
-
'cluster.beaconSeconds',
|
|
372
|
-
'cluster.rosterGossipSeconds',
|
|
373
|
-
] as const;
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Config keys for the daemon's payment capability: the master switch, which
|
|
377
|
-
* card to use by default, the settlement currency, CVV handling, the six
|
|
378
|
-
* budget knobs, the shipping-tier preference, the fourteen billing/shipping
|
|
379
|
-
* address sub-fields, the two approval/veto windows, and the notify-channel
|
|
380
|
-
* list. Card MATERIAL (number, expiry, CVV, cardholder name) is intentionally
|
|
381
|
-
* NOT here, see the SDK's schema-domain-payments.ts header and this repo's
|
|
382
|
-
* own input/payments-config.ts: that material lives write-only in the daemon
|
|
383
|
-
* secret store, never in CONFIG_SCHEMA, so there is nothing for a persistence
|
|
384
|
-
* contract to count.
|
|
385
|
-
*
|
|
386
|
-
* THIS CONSTANT IS NOT A DIAL, for the same reason as the sets above. Every
|
|
387
|
-
* key below is counted because
|
|
388
|
-
* `src/test/verification/payments-settings-persistence.test.ts` runs the same
|
|
389
|
-
* four-part persistence contract the other counted sets are counted for:
|
|
390
|
-
* schema default exposure, `set()` through the key's own validator to disk,
|
|
391
|
-
* reload into a fresh ConfigManager with read-back equality, and
|
|
392
|
-
* reset-to-default that also survives reload. That test also asserts this
|
|
393
|
-
* list is exactly the CONFIG_SCHEMA's `payments.*` key set, so a future SDK
|
|
394
|
-
* addition under this domain is caught (fails the inventory-integrity test)
|
|
395
|
-
* rather than silently uncounted.
|
|
396
|
-
*
|
|
397
|
-
* PER-KEY EVIDENCE. All 28 are read by the daemon's own payment capability
|
|
398
|
-
* (platform/payments, platform/control-plane/routes/payments.ts): the budget
|
|
399
|
-
* keys size the daily/overage/tolerance pools, the window keys size the
|
|
400
|
-
* veto/approval timers, cvvHandling and shipping.preferredTier select the
|
|
401
|
-
* enum behaviors `platform/payments` documents, and the address sub-fields
|
|
402
|
-
* are read directly by the daemon's shipping-address-required refusal and the
|
|
403
|
-
* card-issuer address-verification path. `enabled` and `defaultCardId` gate
|
|
404
|
-
* and select, respectively, whether/which card a purchase uses.
|
|
405
|
-
*/
|
|
406
|
-
export const PAYMENTS_LOCAL_SETTINGS = [
|
|
407
|
-
'payments.enabled',
|
|
408
|
-
'payments.defaultCardId',
|
|
409
|
-
'payments.currency',
|
|
410
|
-
'payments.cvvHandling',
|
|
411
|
-
'payments.budget.dailyItem',
|
|
412
|
-
'payments.budget.dailyOverage',
|
|
413
|
-
'payments.budget.perPurchaseCeilingEnabled',
|
|
414
|
-
'payments.budget.perPurchaseCeiling',
|
|
415
|
-
'payments.budget.overageToleranceEnabled',
|
|
416
|
-
'payments.budget.overageToleranceDailyAllowance',
|
|
417
|
-
'payments.shipping.preferredTier',
|
|
418
|
-
'payments.billingAddress.name',
|
|
419
|
-
'payments.billingAddress.line1',
|
|
420
|
-
'payments.billingAddress.line2',
|
|
421
|
-
'payments.billingAddress.city',
|
|
422
|
-
'payments.billingAddress.region',
|
|
423
|
-
'payments.billingAddress.postalCode',
|
|
424
|
-
'payments.billingAddress.country',
|
|
425
|
-
'payments.shippingAddress.name',
|
|
426
|
-
'payments.shippingAddress.line1',
|
|
427
|
-
'payments.shippingAddress.line2',
|
|
428
|
-
'payments.shippingAddress.city',
|
|
429
|
-
'payments.shippingAddress.region',
|
|
430
|
-
'payments.shippingAddress.postalCode',
|
|
431
|
-
'payments.shippingAddress.country',
|
|
432
|
-
'payments.windows.vetoMinutes',
|
|
433
|
-
'payments.windows.approvalMinutes',
|
|
434
|
-
'payments.majorRetailersAdditional',
|
|
435
|
-
'payments.majorRetailersExcluded',
|
|
436
|
-
'payments.ebayMinSellerFeedbackCount',
|
|
437
|
-
'payments.ebayMinSellerPositivePercent',
|
|
438
|
-
'payments.notifyChannels',
|
|
439
|
-
] as const;
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Connector settings, the daemon's Google-backed mail/calendar keys plus the
|
|
443
|
-
* connected-host dial switch and the hosted-turn routing switch, declared as
|
|
444
|
-
* described schema rows by platform runtime 2.0.8 (before that the connector
|
|
445
|
-
* keys were cast onto the live config invisibly and the settings surface
|
|
446
|
-
* answered "Unknown setting" for keys the daemon really reads). Counted for
|
|
447
|
-
* exactly the reason DAEMON_MAILBOX_LOCAL_SETTINGS is: the same four-part
|
|
448
|
-
* persistence contract runs over every one of them in
|
|
449
|
-
* src/test/verification/connector-settings-persistence.test.ts, and that test
|
|
450
|
-
* asserts this list is the complete schema set for the email, calendar and
|
|
451
|
-
* google prefixes
|
|
452
|
-
* plus the two switches. Arriving uncounted is what dropped
|
|
453
|
-
* localBehaviorPercent through its floor when the schema grew. The three
|
|
454
|
-
* secret-valued keys hold goodvibes://secrets/ references, never values,
|
|
455
|
-
* same note as the mailbox passwords above.
|
|
456
|
-
*/
|
|
457
|
-
export const CONNECTOR_LOCAL_SETTINGS = [
|
|
458
|
-
'email.enabled',
|
|
459
|
-
'email.imapHost',
|
|
460
|
-
'email.imapPort',
|
|
461
|
-
'email.imapSecurity',
|
|
462
|
-
'email.smtpHost',
|
|
463
|
-
'email.smtpPort',
|
|
464
|
-
'email.smtpSecurity',
|
|
465
|
-
'email.username',
|
|
466
|
-
'email.passwordRef',
|
|
467
|
-
'email.smtpPasswordRef',
|
|
468
|
-
'email.fromAddress',
|
|
469
|
-
'email.mailbox',
|
|
470
|
-
'email.draftsMailbox',
|
|
471
|
-
'calendar.google.clientId',
|
|
472
|
-
'calendar.google.clientSecretRef',
|
|
473
|
-
'calendar.google.icsUrl',
|
|
474
|
-
'calendar.microsoft.clientId',
|
|
475
|
-
'calendar.microsoft.clientSecretRef',
|
|
476
|
-
'google.oauth.projectId',
|
|
477
|
-
'google.oauth.publishingStatus',
|
|
478
|
-
'google.oauth.refreshToken',
|
|
479
|
-
'google.credentials.migratedFrom',
|
|
480
|
-
'daemon.connectedHost.enabled',
|
|
481
|
-
'hostedSessions.routeConversationTurns',
|
|
482
|
-
] as const;
|
|
483
|
-
|
|
484
|
-
const SETTINGS_BEHAVIOR_VERIFIED = SETTINGS_BEHAVIOR_BASELINE
|
|
485
|
-
+ FEATURE_KNOB_LOCAL_SETTINGS.length
|
|
486
|
-
+ DEVICE_AND_TRIGGER_LOCAL_SETTINGS.length
|
|
487
|
-
+ CLUSTER_GROUP_LOCAL_SETTINGS.length
|
|
488
|
-
+ DAEMON_MAILBOX_LOCAL_SETTINGS.length
|
|
489
|
-
+ PAYMENTS_LOCAL_SETTINGS.length
|
|
490
|
-
+ CONNECTOR_LOCAL_SETTINGS.length
|
|
491
|
-
+ ENABLEMENT_KEYS_BEHAVIOR_VERIFIED;
|
|
492
|
-
|
|
493
|
-
const EXTERNAL_SLASH_COMMANDS = new Set([
|
|
494
|
-
'auth',
|
|
495
|
-
'bridge',
|
|
496
|
-
'cloudflare',
|
|
497
|
-
'health',
|
|
498
|
-
'listener',
|
|
499
|
-
'login',
|
|
500
|
-
'logout',
|
|
501
|
-
'mcp',
|
|
502
|
-
'notify',
|
|
503
|
-
'pair',
|
|
504
|
-
'qrcode',
|
|
505
|
-
'remote',
|
|
506
|
-
'remote-env',
|
|
507
|
-
'remote-setup',
|
|
508
|
-
'runner-pool',
|
|
509
|
-
'scan',
|
|
510
|
-
'secrets',
|
|
511
|
-
'services',
|
|
512
|
-
'subscription',
|
|
513
|
-
'teleport',
|
|
514
|
-
'tts',
|
|
515
|
-
'tunnel',
|
|
516
|
-
'voice',
|
|
517
|
-
]);
|
|
518
|
-
|
|
519
|
-
const EXTERNAL_CLI_COMMANDS = new Set([
|
|
520
|
-
'bridge',
|
|
521
|
-
'listener',
|
|
522
|
-
'pair',
|
|
523
|
-
'remote',
|
|
524
|
-
'run',
|
|
525
|
-
'serve',
|
|
526
|
-
'service',
|
|
527
|
-
'tui',
|
|
528
|
-
'web',
|
|
529
|
-
]);
|
|
530
|
-
|
|
531
|
-
const ONBOARDING_CAPABILITIES = [
|
|
532
|
-
'local-tui-only',
|
|
533
|
-
'browser-access',
|
|
534
|
-
'network-access',
|
|
535
|
-
'webhook-events',
|
|
536
|
-
'external-integrations',
|
|
537
|
-
'cloudflare-batch',
|
|
538
|
-
] as const;
|
|
539
|
-
|
|
540
|
-
const EXTERNAL_SURFACES = [
|
|
541
|
-
'bluebubbles',
|
|
542
|
-
'discord',
|
|
543
|
-
'googleChat',
|
|
544
|
-
'homeassistant',
|
|
545
|
-
'imessage',
|
|
546
|
-
'matrix',
|
|
547
|
-
'mattermost',
|
|
548
|
-
'msteams',
|
|
549
|
-
'ntfy',
|
|
550
|
-
'signal',
|
|
551
|
-
'slack',
|
|
552
|
-
'telegram',
|
|
553
|
-
'webhook',
|
|
554
|
-
'whatsapp',
|
|
555
|
-
] as const;
|
|
556
|
-
|
|
557
|
-
function percent(numerator: number, denominator: number): number {
|
|
558
|
-
return denominator === 0 ? 0 : Math.round((numerator / denominator) * 1000) / 10;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function listSlashCommands(): string[] {
|
|
562
|
-
const registry = new CommandRegistry();
|
|
563
|
-
registerBuiltinCommands(registry);
|
|
564
|
-
return registry.getAll().map((command) => command.name);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Built-in modals and views: every config-modal surface builtin-modals.ts
|
|
569
|
-
* registers, plus every kit view modal the shell's view openers construct
|
|
570
|
-
* (Agents, Usage, Changes, Notifications, the masked password prompt).
|
|
571
|
-
*/
|
|
572
|
-
function countBuiltinModals(root: string): number {
|
|
573
|
-
const surfaces = readFileSync(join(root, 'src', 'views', 'builtin-modals.ts'), 'utf8');
|
|
574
|
-
const openers = readFileSync(join(root, 'src', 'shell', 'view-openers.ts'), 'utf8');
|
|
575
|
-
const surfaceCount = [...surfaces.matchAll(/registerModalSurface\(/g)].length;
|
|
576
|
-
const viewModals = new Set([...openers.matchAll(/new ([A-Za-z]+Modal)\(/g)].map((m) => m[1]));
|
|
577
|
-
return surfaceCount + viewModals.size;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* The CLI command words this terminal actually accepts, read off the catalog
|
|
582
|
-
* the parser is driven by rather than scraped out of a source file, the
|
|
583
|
-
* ledger counts what ships, and a catalog entry IS what ships.
|
|
584
|
-
*/
|
|
585
|
-
function listCliCommands(): string[] {
|
|
586
|
-
return GOODVIBES_CLI_CATALOG.commands
|
|
587
|
-
.map((spec) => spec.name as string)
|
|
588
|
-
.filter((command) => command !== 'unknown');
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
export function buildVerificationLedger(root: string): VerificationLedger {
|
|
592
|
-
const slashCommandNames = listSlashCommands();
|
|
593
|
-
const cliCommandNames = listCliCommands();
|
|
594
|
-
const slashCommands = slashCommandNames.length;
|
|
595
|
-
const modals = countBuiltinModals(root);
|
|
596
|
-
const cliCommands = cliCommandNames.length;
|
|
597
|
-
const featureFlags = FEATURE_SETTINGS.length;
|
|
598
|
-
const settings = CONFIG_SCHEMA.length;
|
|
599
|
-
const externalSlashCommands = slashCommandNames.filter((command) => EXTERNAL_SLASH_COMMANDS.has(command)).length;
|
|
600
|
-
const externalCliCommands = cliCommandNames.filter((command) => EXTERNAL_CLI_COMMANDS.has(command)).length;
|
|
601
|
-
|
|
602
|
-
const areas: VerificationLedgerArea[] = [
|
|
603
|
-
{
|
|
604
|
-
area: 'Settings schema and persistence',
|
|
605
|
-
total: settings,
|
|
606
|
-
localSignalVerified: settings,
|
|
607
|
-
localBehaviorVerified: SETTINGS_BEHAVIOR_VERIFIED,
|
|
608
|
-
externalOutcomeRequired: settings - SETTINGS_BEHAVIOR_VERIFIED,
|
|
609
|
-
notes: 'Every schema setting can be validated for schema/default/load/write/location; external side effects remain separate.',
|
|
610
|
-
},
|
|
611
|
-
{
|
|
612
|
-
area: 'Feature flags',
|
|
613
|
-
total: featureFlags,
|
|
614
|
-
localSignalVerified: featureFlags,
|
|
615
|
-
localBehaviorVerified: featureFlags - 4,
|
|
616
|
-
externalOutcomeRequired: 4,
|
|
617
|
-
notes: 'All flags can be loaded/toggled; a small surface/service subset still requires live external behavior.',
|
|
618
|
-
},
|
|
619
|
-
{
|
|
620
|
-
area: 'Slash commands',
|
|
621
|
-
total: slashCommands,
|
|
622
|
-
localSignalVerified: slashCommands,
|
|
623
|
-
localBehaviorVerified: slashCommands - externalSlashCommands,
|
|
624
|
-
externalOutcomeRequired: externalSlashCommands,
|
|
625
|
-
notes: 'Every command can be routed and invoked with a fake context; external/provider/device commands need live outcome checks.',
|
|
626
|
-
},
|
|
627
|
-
{
|
|
628
|
-
area: 'Built-in modals and views',
|
|
629
|
-
total: modals,
|
|
630
|
-
localSignalVerified: modals,
|
|
631
|
-
localBehaviorVerified: modals,
|
|
632
|
-
externalOutcomeRequired: 0,
|
|
633
|
-
notes: 'Modals and views can be rendered and input-tested against fake read models and real cached state.',
|
|
634
|
-
},
|
|
635
|
-
{
|
|
636
|
-
area: 'Top-level CLI commands',
|
|
637
|
-
total: cliCommands,
|
|
638
|
-
localSignalVerified: cliCommands,
|
|
639
|
-
localBehaviorVerified: cliCommands - externalCliCommands,
|
|
640
|
-
externalOutcomeRequired: externalCliCommands,
|
|
641
|
-
notes: 'Parser/help/status/package behavior is local; long-running TUI/service/remote flows require process or external checks.',
|
|
642
|
-
},
|
|
643
|
-
{
|
|
644
|
-
area: 'External surfaces',
|
|
645
|
-
total: EXTERNAL_SURFACES.length,
|
|
646
|
-
localSignalVerified: EXTERNAL_SURFACES.length,
|
|
647
|
-
localBehaviorVerified: 2,
|
|
648
|
-
externalOutcomeRequired: EXTERNAL_SURFACES.length - 2,
|
|
649
|
-
notes: 'Config/readiness can be local for all surfaces; real message delivery is external for most surfaces.',
|
|
650
|
-
},
|
|
651
|
-
{
|
|
652
|
-
area: 'Onboarding capability bundles',
|
|
653
|
-
total: ONBOARDING_CAPABILITIES.length,
|
|
654
|
-
localSignalVerified: ONBOARDING_CAPABILITIES.length,
|
|
655
|
-
localBehaviorVerified: 5,
|
|
656
|
-
externalOutcomeRequired: 1,
|
|
657
|
-
notes: 'Wizard state derivation/apply can be local; Cloudflare provisioning remains external.',
|
|
658
|
-
},
|
|
659
|
-
];
|
|
660
|
-
|
|
661
|
-
const total = areas.reduce((sum, area) => sum + area.total, 0);
|
|
662
|
-
const localSignalVerified = areas.reduce((sum, area) => sum + area.localSignalVerified, 0);
|
|
663
|
-
const localBehaviorVerified = areas.reduce((sum, area) => sum + area.localBehaviorVerified, 0);
|
|
664
|
-
const externalOutcomeRequired = areas.reduce((sum, area) => sum + area.externalOutcomeRequired, 0);
|
|
665
|
-
|
|
666
|
-
return {
|
|
667
|
-
generatedAt: new Date().toISOString(),
|
|
668
|
-
areas,
|
|
669
|
-
totals: {
|
|
670
|
-
total,
|
|
671
|
-
localSignalVerified,
|
|
672
|
-
localBehaviorVerified,
|
|
673
|
-
externalOutcomeRequired,
|
|
674
|
-
localSignalPercent: percent(localSignalVerified, total),
|
|
675
|
-
localBehaviorPercent: percent(localBehaviorVerified, total),
|
|
676
|
-
},
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
export function renderVerificationLedgerMarkdown(ledger: VerificationLedger): string {
|
|
681
|
-
const lines = [
|
|
682
|
-
'# GoodVibes Verification Ledger',
|
|
683
|
-
'',
|
|
684
|
-
`Generated: ${ledger.generatedAt}`,
|
|
685
|
-
'',
|
|
686
|
-
'| Area | Total | Local verification signal | Local behavior | External outcome required | Notes |',
|
|
687
|
-
'|---|---:|---:|---:|---:|---|',
|
|
688
|
-
...ledger.areas.map((area) => [
|
|
689
|
-
`| ${area.area}`,
|
|
690
|
-
area.total,
|
|
691
|
-
area.localSignalVerified,
|
|
692
|
-
area.localBehaviorVerified,
|
|
693
|
-
area.externalOutcomeRequired,
|
|
694
|
-
area.notes,
|
|
695
|
-
].join(' | ') + ' |'),
|
|
696
|
-
'',
|
|
697
|
-
'## Totals',
|
|
698
|
-
'',
|
|
699
|
-
`- Total inventory items: ${ledger.totals.total}`,
|
|
700
|
-
`- Local verification signal: ${ledger.totals.localSignalVerified} (${ledger.totals.localSignalPercent}%)`,
|
|
701
|
-
`- Local behavior verified: ${ledger.totals.localBehaviorVerified} (${ledger.totals.localBehaviorPercent}%)`,
|
|
702
|
-
`- External outcome required: ${ledger.totals.externalOutcomeRequired}`,
|
|
703
|
-
'',
|
|
704
|
-
'Local verification signal means the item can be exercised through schema, routing, persistence, render, readiness, daemon, CLI, or real-state checks without relying on an external SaaS/device outcome.',
|
|
705
|
-
'Local behavior verified means the behavior can be completed locally with in-process, CLI, daemon, tmux, or real persisted state.',
|
|
706
|
-
'',
|
|
707
|
-
];
|
|
708
|
-
return `${lines.join('\n')}\n`;
|
|
709
|
-
}
|