@epoch-agent/infra 0.1.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/LICENSE +219 -0
- package/README.md +99 -0
- package/dist/index.d.ts +2468 -0
- package/dist/index.js +3099 -0
- package/dist/locales/en.yaml +2757 -0
- package/dist/locales/zh.yaml +2932 -0
- package/package.json +51 -0
|
@@ -0,0 +1,2757 @@
|
|
|
1
|
+
# epoch-agent message catalog —— the one and only translation.
|
|
2
|
+
#
|
|
3
|
+
# zh.yaml is the baseline / source of truth; this file mirrors it key for key.
|
|
4
|
+
# Adding a key here without adding it there (or vice versa) turns the
|
|
5
|
+
# consistency test red, and so does dropping a `{placeholder}`.
|
|
6
|
+
#
|
|
7
|
+
# There is no third catalog, and none is planned —— see the scope statement at
|
|
8
|
+
# the top of .agents/plans/40-i18n-plan.md before adding one.
|
|
9
|
+
|
|
10
|
+
i18n:
|
|
11
|
+
locales_missing: 'locales directory not found; UI text will show as key paths'
|
|
12
|
+
catalog_missing: 'locales/{file} is missing; {lang} text falls back to the default language'
|
|
13
|
+
catalog_invalid: 'locales/{file} is not valid YAML ({reason}); fell back to built-in defaults'
|
|
14
|
+
|
|
15
|
+
# Value checks for flags that take a value (cli/src/commands/run-overrides.ts).
|
|
16
|
+
# Every one of these must say what to type next — the valid values, or "the task
|
|
17
|
+
# is a positional argument". At the moment of the typo the user is not missing
|
|
18
|
+
# "this value is wrong", they are missing "so how do I write it".
|
|
19
|
+
flags:
|
|
20
|
+
provider_unknown: 'unknown --provider: {value}'
|
|
21
|
+
permission_unknown: 'unknown --permission: {value}'
|
|
22
|
+
choices: 'valid values: {choices}'
|
|
23
|
+
model_not_a_name: '{flag} does not look like a model name: {value}'
|
|
24
|
+
model_not_a_name_hint: 'Model ids are ASCII (letters, digits and - . _ : / @) with no spaces and no CJK. To send it to the model, pass it as a positional argument: epoch "{value}"'
|
|
25
|
+
base_url_invalid: '--base-url is not a valid http(s) URL: {value}'
|
|
26
|
+
base_url_invalid_hint: 'It should look like https://api.example.com/v1. To send it to the model, pass it as a positional argument: epoch "{value}"'
|
|
27
|
+
|
|
28
|
+
# `--help` text for individual subcommands. A whole command moves at once —
|
|
29
|
+
# never just the newly added flag: half-catalog, half-hardcoded help renders as
|
|
30
|
+
# half English, half Chinese on a non-Chinese machine. Only `epoch web` has moved
|
|
31
|
+
# so far (2026-08-14, the change that added --json).
|
|
32
|
+
cli:
|
|
33
|
+
web:
|
|
34
|
+
summary: 'Start the local Web UI server (opens a browser by default)'
|
|
35
|
+
opt_port: 'Port to listen on (default {port}; 0 = let the OS pick)'
|
|
36
|
+
opt_host: 'Address to bind (default {host}; a non-loopback address requires --token)'
|
|
37
|
+
opt_token: 'Auth token (a fresh random one per start by default)'
|
|
38
|
+
opt_no_open: 'Do not open a browser'
|
|
39
|
+
opt_json: 'Print one JSON line on stdout (url / host / port / token) and route every other message to stderr. For host programs spawning the CLI; pair it with --port 0 so no port has to be hardcoded'
|
|
40
|
+
config:
|
|
41
|
+
summary: 'Manage provider and model configuration'
|
|
42
|
+
keys: 'provider / model / permission / shell / models.utility / budget.<field>'
|
|
43
|
+
keys_unset: 'model / permission / shell / models.utility / budget.<field>'
|
|
44
|
+
value: 'Value'
|
|
45
|
+
show: 'Show the current configuration (apiKey and other secrets are masked)'
|
|
46
|
+
opt_raw: 'Do not mask; print as-is'
|
|
47
|
+
path: 'Print the config file path (use it to confirm where multi-profile writes land)'
|
|
48
|
+
get: 'Read a single setting; exits 1 when unset'
|
|
49
|
+
set: 'Set a single setting'
|
|
50
|
+
unset: 'Remove a setting and fall back to the default'
|
|
51
|
+
secret: 'Show the credential storage backend; --export dumps credentials back to .env (plaintext, asks twice)'
|
|
52
|
+
opt_export: 'Write credentials back to ~/.epoch/.env (plaintext!)'
|
|
53
|
+
schema: 'Write the settings JSON Schema into <project root>/.epoch/schemas/ for editor completion and validation'
|
|
54
|
+
schema_paste_hint: 'Add this line at the top of .epoch/settings.json (relative path, no network):'
|
|
55
|
+
# The sandbox section of `epoch doctor` (plan 46 PR-2, cli/src/commands/status.ts).
|
|
56
|
+
# The sandbox is a *probed* value, not a configured one, so every line here comes
|
|
57
|
+
# from a real probe. `code_exec` and `terminal` get separate lines: they share the
|
|
58
|
+
# backends but not the boundary, and collapsing them into one "sandbox: enabled"
|
|
59
|
+
# is exactly the hole plan 46 exists to close.
|
|
60
|
+
doctor:
|
|
61
|
+
sandbox_head: 'Sandbox:'
|
|
62
|
+
sandbox_absent_tools: 'Neither code_exec nor terminal got OS-level isolation — no usable backend was detected on this machine'
|
|
63
|
+
sandbox_backend: 'Backend: {backend} ({platform})'
|
|
64
|
+
sandbox_code_exec: 'code_exec: {detail}'
|
|
65
|
+
sandbox_terminal: 'terminal ({mode}): enforcement {enforcement}'
|
|
66
|
+
sandbox_partial_devnull: 'Why partial: a POSIX shell needs /dev/null writable or it cannot even start'
|
|
67
|
+
sandbox_writable: 'Directories terminal may write to under workspace-write:'
|
|
68
|
+
sandbox_covers: 'Tools inside the sandbox: {tools}'
|
|
69
|
+
sandbox_excludes: 'Not in the sandbox (easily mistaken for being in it): {tools}'
|
|
70
|
+
# What to say once that list is empty (the normal case after plan 46 PR-4/PR-5).
|
|
71
|
+
# The second half is NOT optional: this is the one line on this screen that could
|
|
72
|
+
# be read as "terminal is safe now", and this tier only buys an OS-enforced
|
|
73
|
+
# *write* boundary — reads and network are both wide open.
|
|
74
|
+
sandbox_excludes_none: 'All three terminal paths (pipe / background / PTY) are inside the sandbox. Note it only governs writes: reads and network are unrestricted'
|
|
75
|
+
|
|
76
|
+
# The `epoch schedule` command family (plan 45 §6). Move a command's help text
|
|
77
|
+
# over **as a whole**: half catalog and half hardcoded reads as half English
|
|
78
|
+
# and half Chinese on a non-Chinese machine.
|
|
79
|
+
schedule:
|
|
80
|
+
summary: 'Manage scheduled tasks (the OS fires them; epoch does not need to stay open)'
|
|
81
|
+
add: 'Create a scheduled task and register it with the OS scheduler'
|
|
82
|
+
list: 'List all scheduled tasks'
|
|
83
|
+
show: 'Show every field of a task, including "what it will do without asking"'
|
|
84
|
+
enable: 'Enable a task'
|
|
85
|
+
disable: 'Disable a task (it stays in the OS scheduler, just stopped)'
|
|
86
|
+
rm: 'Delete a task and unregister it from the OS scheduler'
|
|
87
|
+
run: 'Run it once right now — no need to wait for the next trigger to find out it is broken'
|
|
88
|
+
logs: 'Show the last few runs'
|
|
89
|
+
fire: 'Invoked by the OS scheduler. You can run it too, but it is non-interactive: anything needing approval is recorded as an IOU'
|
|
90
|
+
doctor: 'Check that the tasks in the database and the ones registered with the OS still match'
|
|
91
|
+
arg_id: 'Task id (a leading prefix is enough)'
|
|
92
|
+
opt_name: 'Task name'
|
|
93
|
+
opt_prompt: 'The prompt — what the agent should do when it fires'
|
|
94
|
+
opt_daily: 'Every day at this time (HH:mm, 24-hour)'
|
|
95
|
+
opt_weekly: 'These weekdays, comma separated (MON,WED,FRI); needs --at'
|
|
96
|
+
opt_monthly: 'These days of the month, comma separated (1,15); needs --at'
|
|
97
|
+
opt_every: 'By interval: 5m/10m/15m/20m/30m/1h/2h/3h/4h/6h/8h/12h, anchored at midnight'
|
|
98
|
+
opt_once: 'Date for a one-shot run (YYYY-MM-DD); needs --at'
|
|
99
|
+
opt_at: 'Time of day (HH:mm). Required by --weekly / --monthly / --once'
|
|
100
|
+
opt_start: 'Start of the active window (inclusive)'
|
|
101
|
+
opt_end: 'End of the active window (inclusive)'
|
|
102
|
+
opt_workdir: 'Working directory. Defaults to ~/.epoch/automation/<id>/; it never inherits the current directory'
|
|
103
|
+
opt_model: 'Model for this task; omit to use the default'
|
|
104
|
+
opt_permission: 'Permission level — the same four words as the chat pill: plan / default / acceptEdits / bypass'
|
|
105
|
+
opt_allow_tool: 'Skip approval for this tool while unattended; repeatable'
|
|
106
|
+
opt_allow_operation: 'Skip approval for this operation type while unattended; repeatable'
|
|
107
|
+
opt_allow_rule: 'Skip approval for operations matching this rule while unattended; repeatable (e.g. terminal(git status))'
|
|
108
|
+
opt_max_turns: 'Maximum turns (default 20 — tighter than the interactive 50)'
|
|
109
|
+
opt_budget: 'Required: the most this single run may spend, in USD'
|
|
110
|
+
opt_timeout: 'Wall-clock timeout in minutes (default 15) — the only ruler that catches a hung command'
|
|
111
|
+
opt_disabled: 'Create it stopped instead of enabling it right away'
|
|
112
|
+
opt_bypass_ack: 'Acknowledge that under "skip all checks" the task will do anything without asking'
|
|
113
|
+
opt_fix: "Append this run's IOUs straight into the task allowlist"
|
|
114
|
+
opt_limit: 'How many recent runs to show'
|
|
115
|
+
opt_tail: 'Print the body of the most recent recording'
|
|
116
|
+
opt_home: 'Data directory for this run. Processes started by the OS scheduler cannot see EPOCH_HOME / EPOCH_PROFILE, so registration bakes this into the command line'
|
|
117
|
+
opt_repair: 'Re-register from the database (the database is the source of truth)'
|
|
118
|
+
opt_prune: 'Unregister OS tasks that no longer exist in the database'
|
|
119
|
+
|
|
120
|
+
# Marketplace catalog could not be fetched (core/src/plugin/marketplace.ts).
|
|
121
|
+
# `fetch_failed_private` is its own entry rather than an appended hint: a private
|
|
122
|
+
# repo answers the raw URL with 404, which looks exactly like a typo — that
|
|
123
|
+
# sentence is the most valuable part of this error.
|
|
124
|
+
# Startup diagnostic: the plan approved in the resumed session is back in context
|
|
125
|
+
# (runtime/src/build.ts). The plan outlives the process, but the card that showed
|
|
126
|
+
# it was drawn by the previous one — without this line the user sees an empty
|
|
127
|
+
# screen and a model that inexplicably follows some outline.
|
|
128
|
+
plan:
|
|
129
|
+
restored: 'Restored the plan you approved last time'
|
|
130
|
+
|
|
131
|
+
# The two startup diagnostics for session search (runtime/src/tools.ts).
|
|
132
|
+
# The "all workspaces" line is only printed when the switch is on — that is a
|
|
133
|
+
# decision to widen what the model can read, and the user should see it in
|
|
134
|
+
# `epoch doctor`. The default (off) prints nothing; one more line would be noise.
|
|
135
|
+
session_search:
|
|
136
|
+
unavailable: 'session store unavailable; session_search not registered'
|
|
137
|
+
all_workspaces: 'cross-workspace search enabled (config.sessionSearch.allWorkspaces)'
|
|
138
|
+
|
|
139
|
+
# Server-side error bodies for `epoch web` (packages/server). The server can call
|
|
140
|
+
# t() because runtime re-exports it — server may not depend on infra directly.
|
|
141
|
+
web:
|
|
142
|
+
session_store_unavailable: 'session persistence is unavailable (SQLite did not start); this action cannot be performed'
|
|
143
|
+
title_required: 'title must be a non-empty string'
|
|
144
|
+
session_cooled: 'this session was cooled down (too many active sessions); reopen it and send again'
|
|
145
|
+
# `POST /api/sessions` could not build a session (the multi-session factory, plan 30 §6.3).
|
|
146
|
+
# 503 rather than 500: the service itself is fine, one of its dependencies is not
|
|
147
|
+
# configured yet. A 500 sends people off to restart the server; the actual next step
|
|
148
|
+
# is to configure a key — so the sentence has to say that.
|
|
149
|
+
session_unavailable: 'cannot create a new session: this process has no usable model provider (configure an API key and retry, or run epoch model)'
|
|
150
|
+
# `POST /api/sessions` was given an agent name this process does not know
|
|
151
|
+
# (the base for decision 20 ②). 400 rather than 404: the URL is fine, a value in
|
|
152
|
+
# the body is not (same shape as `bad-workspace`). And never silently fall back to
|
|
153
|
+
# "no role" — same criterion as `--agent`: the user named an identity, so running an
|
|
154
|
+
# agent with a different tool set is the worst possible outcome.
|
|
155
|
+
# The name has to appear in the sentence: the browser's copy of the agent list may
|
|
156
|
+
# be stale (the user just deleted a .md), and without the name the screen only says
|
|
157
|
+
# "no such agent" while the list in front of them still shows it.
|
|
158
|
+
session_unknown_role: 'no agent named "{role}" — the list on the capabilities page may be stale; refresh and pick again'
|
|
159
|
+
workspace_must_be_string: 'workspace must be a non-empty string (an absolute path on the server); omit the field entirely to bind no workspace'
|
|
160
|
+
workspace_locked: 'this session is already bound to a workspace and cannot be changed ({detail}). Start a new session to use another directory — switching mid-session would make the relative paths in earlier tool calls point at a different directory, with nothing in the record saying where the switch happened'
|
|
161
|
+
# The only refusal of `GET /api/sessions/:id/workspace` (decision 18, 2026-08-15).
|
|
162
|
+
#
|
|
163
|
+
# ⚠️ It is **not** the same thing as "this session uses no workspace", and that
|
|
164
|
+
# distinction is the whole reason the endpoint exists: bindings live in process
|
|
165
|
+
# memory and are never persisted, so a session left over from an earlier process
|
|
166
|
+
# has no record here. Answering "not bound" would put "no workspace" on screen —
|
|
167
|
+
# a lie about a session that spent yesterday working inside a real repository.
|
|
168
|
+
# The code reuses `not-live-session` (same as `rewind_not_live`): the user's next
|
|
169
|
+
# step is likewise "bring this session back into this process".
|
|
170
|
+
workspace_not_live: 'this process does not hold that session, so it cannot say where it is bound — workspace bindings live in process memory and are never persisted (bring the session back into this process to get an answer)'
|
|
171
|
+
# The refusals of the new `POST /api/sessions/:id/workspace` (plan 55 PR-1, 2026-08-17).
|
|
172
|
+
#
|
|
173
|
+
# The first two are about the **shape of the request body** (exactly one of two
|
|
174
|
+
# branches), which is why they are separate from `workspace_must_be_string`, which
|
|
175
|
+
# is about the path: that one's next step is "try another path", these two are
|
|
176
|
+
# "change which fields you sent". "Both branches given" must never pick one and run
|
|
177
|
+
# it — the two branches lead to two entirely different sessions.
|
|
178
|
+
workspace_bind_both: 'give either root or none, not both: the first means "bind to this directory", the second means "explicitly use no workspace" — they are mutually exclusive'
|
|
179
|
+
workspace_bind_neither: 'give either root (an absolute path on the server) or none: true (explicitly use no workspace)'
|
|
180
|
+
# ⚠️ This restates decision 18's "a binding cannot be changed" on the new endpoint,
|
|
181
|
+
# and it has to cover **both** "already bound to a directory" and "already chose no
|
|
182
|
+
# workspace". The latter looks like an empty slot, but it is a state the user
|
|
183
|
+
# explicitly picked (a member of the "Tasks" group in decision 18's sidebar).
|
|
184
|
+
# Letting it be changed means giving a workspace to a session that said it wanted
|
|
185
|
+
# none — after its earlier relative paths were already resolved somewhere else.
|
|
186
|
+
workspace_already_decided: "this session's workspace is already settled (either bound to a directory or explicitly set to none) and cannot be changed — start a new session to use another directory"
|
|
187
|
+
# The two refusals of `deferWorkspace` on `POST /api/sessions` (plan 55 PR-1).
|
|
188
|
+
# Both are **no silent downgrade**, same criterion as `workspace_must_be_string`
|
|
189
|
+
# above: quietly dropping a field the client explicitly sent hands it back a
|
|
190
|
+
# session that is not the one it thinks it asked for.
|
|
191
|
+
workspace_defer_with_root: 'give either workspace or deferWorkspace, not both: the first means "bind it at creation time", the second means "create it now and pick the directory later"'
|
|
192
|
+
workspace_defer_needs_create: 'deferWorkspace requires create: true — a request without create means "give me the existing session", whose workspace was settled at assembly time'
|
|
193
|
+
# The four refusals of `GET /api/workspaces/dirs` (plan 55 PR-2, 2026-08-17).
|
|
194
|
+
#
|
|
195
|
+
# ⚠️ **"no permission" and "does not exist" get separate sentences AND separate
|
|
196
|
+
# status codes** (403 / 404): this is the one place on this endpoint that must be
|
|
197
|
+
# split — the user's next step differs completely (fix permissions or use another
|
|
198
|
+
# entry point vs. go find where that directory actually is). Merged, the UI can
|
|
199
|
+
# only offer one hint that is wrong in both directions. All four carry the path:
|
|
200
|
+
# this is a local UI, and being specific beats being vague.
|
|
201
|
+
dirs_denied: 'no permission to list this directory: {path} (it is there, but this server process cannot read it)'
|
|
202
|
+
dirs_missing: 'no such path: {path}'
|
|
203
|
+
dirs_not_absolute: 'the directory to list must be an absolute path: {path} — there is no "current directory" on this route'
|
|
204
|
+
dirs_not_a_directory: 'that is a file, not a directory: {path} (pick the directory it lives in)'
|
|
205
|
+
# The five refusals of `POST /api/workspaces` (plan 55 PR-3, 2026-08-17).
|
|
206
|
+
#
|
|
207
|
+
# ⚠️ **"already exists" is a 409, not a reuse** — the most important line here.
|
|
208
|
+
# Pressing "New" and getting **somebody else's directory** is the most expensive
|
|
209
|
+
# mistake on this route: it may hold a CLAUDE.md, a git repo, all sorts of things,
|
|
210
|
+
# while the user believes they got an empty one. So the sentence has to offer both
|
|
211
|
+
# ways out (rename, or go pick it).
|
|
212
|
+
mkdir_exists: 'that directory already exists: {path} — pick another name, or go back to the list and select it ("New" never reuses an existing directory)'
|
|
213
|
+
# Say "no permission" outright rather than translating it into "could not create":
|
|
214
|
+
# the user's next step is either a different location or a permission change, and
|
|
215
|
+
# both start with knowing it was a permission problem.
|
|
216
|
+
mkdir_denied: 'no permission to create a directory here: {path}'
|
|
217
|
+
mkdir_parent_missing: 'the parent directory does not exist, so {path} cannot be created — create it first (this route never creates parents for you)'
|
|
218
|
+
mkdir_parent_not_absolute: 'the location to create in must be an absolute path: {parent}'
|
|
219
|
+
# A single segment only. Creating two levels (a/b) is two operations — accepting
|
|
220
|
+
# a/b would not cost one `recursive` flag, it would make "did that create one
|
|
221
|
+
# directory or two" something you have to read the implementation to know.
|
|
222
|
+
mkdir_bad_name: 'the directory name must be a single segment with no path separators, and cannot be . or ..: {name}'
|
|
223
|
+
# An errno we do not recognise. **Treat it as a 500** — calling an unfamiliar
|
|
224
|
+
# failure "bad path" sends the user off to fix an input that was fine.
|
|
225
|
+
mkdir_failed: 'could not create that directory: {path}'
|
|
226
|
+
# The two refusals of `POST /api/sessions/:id/plan` (the write half of plan 35).
|
|
227
|
+
#
|
|
228
|
+
# ⚠️ **`plan_mode_bootstrap_only` was deleted on 2026-08-16** (together with the
|
|
229
|
+
# `permission_` and `model_` members of the same family). It said "only the
|
|
230
|
+
# session opened first can enter plan mode", and plan mode became one per
|
|
231
|
+
# session in that round (plan 30 §14) — the 409 gate is gone, so the sentence
|
|
232
|
+
# can never be sent again. **Keeping it would be worse than deleting it**: a
|
|
233
|
+
# string with no call site can only be found to be false by a full-text search.
|
|
234
|
+
plan_mode_unavailable: 'this process has no plan mode (the permission layer did not start)'
|
|
235
|
+
plan_mode_bad_action: 'action must be one of {actions}'
|
|
236
|
+
# The two refusals of `POST /api/sessions/:id/permission` (decision 20 ③, 2026-08-15).
|
|
237
|
+
#
|
|
238
|
+
# ⚠️ `permission_bootstrap_only` was deleted on 2026-08-16, same reason as the
|
|
239
|
+
# plan one above. Worth recording: the old comment here argued they should stay
|
|
240
|
+
# **two keys** because "when the two scopes are narrowed independently, a merged
|
|
241
|
+
# sentence would start lying on one of the two paths". That call was right —
|
|
242
|
+
# the narrowing just arrived more tidily than expected: both went on one day.
|
|
243
|
+
permission_unavailable: 'this process has no permission layer, so the level cannot be changed (what you see is the value from the config file)'
|
|
244
|
+
# ⚠️ **This sentence must not mention a level being disabled by managed
|
|
245
|
+
# settings** — that is not a bad request but a legal one refused by policy on
|
|
246
|
+
# this machine, and it travels as 200 + `ok:false` (see
|
|
247
|
+
# `WirePermissionSetResponse`). This one only says "that is not a level name".
|
|
248
|
+
permission_bad_level: 'level must be one of {levels}'
|
|
249
|
+
# The four refusals of checkpoint rewind (the web half of plan 27).
|
|
250
|
+
#
|
|
251
|
+
# `not_live` is **a different thing** from the entry above, and only since
|
|
252
|
+
# 2026-08-15 can the two be told apart: this one says "this process does not
|
|
253
|
+
# hold a runtime for that session" (left over from an earlier process, or
|
|
254
|
+
# cooled down). Checkpoints are **per session** — as long as this process still
|
|
255
|
+
# holds it, it can be rewound. The user's next step is therefore "bring this
|
|
256
|
+
# session back", the opposite of the plan / model one.
|
|
257
|
+
rewind_not_live: 'this process does not hold that session, so it cannot be rewound — a rewind has to come from the process running it (bring the session back first)'
|
|
258
|
+
rewind_bad_turn: 'turnIndex must be a non-negative integer (take it from the checkpoint list)'
|
|
259
|
+
rewind_bad_scope: 'scope must be one of {scopes}'
|
|
260
|
+
# This one does **not** degrade silently to "overwrite nothing": a client that
|
|
261
|
+
# misspells it would believe the overwrite succeeded while the user's own edits
|
|
262
|
+
# are in fact still there. "Overwrite everything" is deliberately inexpressible here.
|
|
263
|
+
rewind_bad_overwrite: 'overwrite must be a list of file paths (the ones the user confirmed one by one); omit the field entirely if there is nothing to overwrite'
|
|
264
|
+
rewind_no_checkpoint: 'no checkpoint for turn {turn}'
|
|
265
|
+
# The five refusals of `POST /api/sessions/:id/settings` (the write half of plan 43 §7).
|
|
266
|
+
#
|
|
267
|
+
# ⚠️ There is deliberately **no** `not_live` entry here, unlike plan / rewind above:
|
|
268
|
+
# those two change the state of the session running in this process, while this one
|
|
269
|
+
# changes **a config file on disk** — which has nothing to do with which session is
|
|
270
|
+
# running. An extra gate would only stop people editing settings from a history session.
|
|
271
|
+
settings_write_bad_request: 'the body needs key (the dotted path of the setting), layer (one of {layers}) and value (string / number / boolean)'
|
|
272
|
+
settings_write_unknown_key: 'that key cannot be edited here. This path writes model and maxTurns only — every other setting has just the built-in and user layers, and putting it in a settings file does nothing'
|
|
273
|
+
settings_write_bad_value: "that value is outside this setting's domain (maxTurns must be a positive integer, for one), so nothing was written"
|
|
274
|
+
# Not overwriting is deliberate: a broken file is something the user is in the
|
|
275
|
+
# middle of editing, and stringifying a fresh one over it would delete that work.
|
|
276
|
+
# This sentence carries the path because the only next step is to edit it by hand.
|
|
277
|
+
settings_write_unwritable: 'this layer does not recognise how {path} is written (or the file is already broken), so it was left alone — please edit that file yourself'
|
|
278
|
+
settings_write_io: 'writing {path} failed: possibly no write permission, or the directory could not be created'
|
|
279
|
+
# The three rejections of `POST /api/sessions/:id/model` (plan 26, the web half).
|
|
280
|
+
#
|
|
281
|
+
# ⚠️ Only the ones where the *request itself* does not stand up live here.
|
|
282
|
+
# "cannot switch" (no credential / no image support / window too small) is
|
|
283
|
+
# **not here and must not be**: those three are a 200 + `ok:false` payload,
|
|
284
|
+
# and what travels the wire is a **code plus parameters** — the wording lives
|
|
285
|
+
# in the browser's two catalogs (`web.ui.model.rej_*`). The criterion is in
|
|
286
|
+
# the file header of `server/src/model.ts`.
|
|
287
|
+
#
|
|
288
|
+
# ⚠️ `model_bootstrap_only` and `model_unavailable` were both deleted on
|
|
289
|
+
# 2026-08-16. The first mirrors the plan/permission ones (model selection is
|
|
290
|
+
# one per session now, so the 409 gate is gone); the second was the body of the
|
|
291
|
+
# 503 `no-model-control` gate, and that gate is gone too — when the provider
|
|
292
|
+
# layer fails to come up this process builds **no sessions at all**, so the
|
|
293
|
+
# first gate answers 404. Criterion in the file header of `server/src/model.ts`.
|
|
294
|
+
model_bad_request: 'model must be a non-empty string (a model name, or provider/model); pass null to go back to the configured one'
|
|
295
|
+
# The single kind of rejection each of the two capability-page action endpoints
|
|
296
|
+
# has (plan 56).
|
|
297
|
+
#
|
|
298
|
+
# Both **must carry the name**, same reason as `session_unknown_role` above: the
|
|
299
|
+
# list the browser is holding may already be stale (mcp.json edited, a skill
|
|
300
|
+
# directory moved away, while this tab was opened before that). Without the name
|
|
301
|
+
# the screen just says "no such thing" while the list still shows it.
|
|
302
|
+
#
|
|
303
|
+
# ⚠️ For both, the next step is a **refresh**, not a retry — they say "the list
|
|
304
|
+
# you are holding has drifted from this process". "That attempt did not succeed"
|
|
305
|
+
# is a different thing: 200 + `ok:false`, worded in the browser's own catalogues
|
|
306
|
+
# (criterion on `WireMcpReconnectResponse`).
|
|
307
|
+
mcp_unknown_server: 'This process has no MCP server named "{name}" — connections are built from mcp.json at startup, so the list you are holding may be stale. Refresh and look again.'
|
|
308
|
+
skill_unknown: 'No skill named "{name}" — the capability page list may be stale. Refresh and look again.'
|
|
309
|
+
# The only rejection `POST /api/providers/:type/models` has. NOT "could not
|
|
310
|
+
# discover models" — that has four tiers and all of them are 200 + a `status`
|
|
311
|
+
# code, worded in the browser's own catalog (`web.ui.model.probe_*`).
|
|
312
|
+
provider_unknown: 'No provider named "{name}" is supported — the selectable set is a fixed list, so the one you are holding may be stale. Refresh and look again.'
|
|
313
|
+
|
|
314
|
+
# The five rejections of `POST /api/mcp` (2026-08-18). Worded like the mkdir
|
|
315
|
+
# trio (`mkdir_bad_name` and friends), **not** like the `/api/schedules` family:
|
|
316
|
+
# that one turns business validation into 200 + a list of **codes**, on the
|
|
317
|
+
# criterion "the same check should read as two different sentences on the CLI
|
|
318
|
+
# and in a form" — and this path has **no CLI counterpart** (there is no
|
|
319
|
+
# `epoch mcp add` anywhere in the repo).
|
|
320
|
+
#
|
|
321
|
+
# ⚠️ The {detail} in `mcp_add_unwritable` is **the underlying sentence verbatim**
|
|
322
|
+
# ("the top level is not a JSON object", "EACCES: permission denied", …); this
|
|
323
|
+
# layer does not rewrite it. It is the only thing on screen that can say what is
|
|
324
|
+
# actually wrong with the user's mcp.json.
|
|
325
|
+
#
|
|
326
|
+
# ⚠️ The name-clash line must spell out that **all three sources count** (the
|
|
327
|
+
# file, the host, plugins) — otherwise the user stares at an error about a name
|
|
328
|
+
# that appears nowhere in their own mcp.json.
|
|
329
|
+
#
|
|
330
|
+
# ⚠️ `mcp_add_lan_exposed` is the only line in this group with security meaning.
|
|
331
|
+
# Its three hard requirements are the same as `role_add_lan_exposed` (say why /
|
|
332
|
+
# say the way forward / never phrase it as "insufficient permissions"), but the
|
|
333
|
+
# **why half is not the same sentence, so do not copy it**: there it is "the body
|
|
334
|
+
# enters the context of every future turn", here it is "adding one writes a
|
|
335
|
+
# command that gets launched, and it governs every future epoch process on this
|
|
336
|
+
# machine". Copying says half of it.
|
|
337
|
+
#
|
|
338
|
+
# ⚠️ It is also **not shared** with `mcp_config_lan_exposed` (those three are
|
|
339
|
+
# deliberately shared): that sentence is about refusing even to *read*, because
|
|
340
|
+
# the env in that file is cleartext, and this path never sends a single byte of
|
|
341
|
+
# that file back. The two ways forward do coincide — because they really are the
|
|
342
|
+
# same two.
|
|
343
|
+
mcp_add_lan_exposed: 'MCP servers cannot be added in this mode — the server was bound outside the loopback interface with --host, so we cannot tell whether the browser sending this request is on this machine. Adding one writes "which executable to launch" into mcp.json; that server is started right away, and every future epoch run on this machine (including the CLI and the TUI) starts it too. Two ways forward: edit that file directly on the machine running the server, or drop --host and restart.'
|
|
344
|
+
mcp_add_bad_transport: 'Transport must be one of stdio / sse / http'
|
|
345
|
+
mcp_add_bad_name: 'The server name "{name}" cannot be used — it becomes part of the tool name verbatim, so only names starting with a letter or digit and made of letters, digits, _ and - are accepted (48 max). A hand-written mcp.json is not subject to this; only names added from the UI are.'
|
|
346
|
+
mcp_add_taken: 'There is already an MCP server named "{name}" (it may be in your mcp.json, or it may come from the host or a plugin) — pick another name. Nothing is overwritten and nothing is auto-renamed here: overwriting would delete the block you wrote by hand, and renaming would make the tool names disagree with what you typed.'
|
|
347
|
+
mcp_add_unwritable: 'Nothing was written to mcp.json — {detail}. Go and look at that file first.'
|
|
348
|
+
|
|
349
|
+
# `GET` / `PUT /api/mcp/config` and `POST /api/mcp/config/apply` (2026-08-18).
|
|
350
|
+
#
|
|
351
|
+
# ⚠️ There is **no** "not valid JSON" line in this group — that sentence is
|
|
352
|
+
# forwarded verbatim from runtime's `mcp_config.invalid_json` (it carries the
|
|
353
|
+
# character position from JSON.parse, and this layer cannot do better).
|
|
354
|
+
#
|
|
355
|
+
# ⚠️ `mcp_config_lan_exposed` is **shared by all three endpoints**, including the
|
|
356
|
+
# GET: the way forward is the same for all three, and three copies of one
|
|
357
|
+
# decision would drift. It must say why even *reading* is refused — the env in
|
|
358
|
+
# that file is cleartext, and the same file decides which executable is launched.
|
|
359
|
+
#
|
|
360
|
+
# ⚠️ `mcp_config_stale` must make clear that the next step is **not** a retry.
|
|
361
|
+
#
|
|
362
|
+
# ⚠️ `mcp_config_no_servers` guards against wiping out every MCP server with one
|
|
363
|
+
# slip, so it names the most common cause (`mcpServers` instead of `servers`) and
|
|
364
|
+
# says how to really empty the list.
|
|
365
|
+
mcp_config_lan_exposed: 'mcp.json can neither be viewed nor edited in this mode — the server was bound outside the loopback interface with --host, so we cannot tell whether the browser sending this request is on this machine. Environment variables in that file are stored in cleartext (they may hold secrets), and the same file decides which executable gets launched. Two ways forward: edit that file directly on the machine running the server, or drop --host and restart.'
|
|
366
|
+
mcp_config_unreadable: 'mcp.json could not be read — {detail}. Go and look at that file first.'
|
|
367
|
+
mcp_config_bad_body: 'This request body is wrong: text and revision must both be strings, and revision must be the one from the last read.'
|
|
368
|
+
mcp_config_stale: 'Nothing was written — the mcp.json on disk is no longer the one you read (your own editor may have just saved it, or the "Add server" form may have just inserted one). Writing over it would erase that change, so this stopped here: read it again and see what is on disk now.'
|
|
369
|
+
mcp_config_unwritable: 'Nothing was written to mcp.json — {detail}. Go and look at that file first.'
|
|
370
|
+
mcp_config_no_servers: 'Nothing was touched — the mcp.json on disk parses to zero servers and it comes with diagnostics: {detail}. Applying that would disconnect every MCP server you have, so this stopped here. The most common cause is writing the key as mcpServers instead of servers. If you really do want an empty list, write "servers": {} and press again.'
|
|
371
|
+
|
|
372
|
+
# `POST /api/roles` ("create an identity", 2026-08-18).
|
|
373
|
+
#
|
|
374
|
+
# ⚠️ Only **two** strings here, while that endpoint has six rejection branches —
|
|
375
|
+
# the other four (name / duplicate / tool names / not writable) are forwarded
|
|
376
|
+
# verbatim from the runtime side (`role_write.*`). Writing a second copy here
|
|
377
|
+
# would give the same rejection two wordings, and they would drift.
|
|
378
|
+
#
|
|
379
|
+
# ⚠️ `role_add_lan_exposed` is the whole point of that endpoint. Three hard
|
|
380
|
+
# requirements on the wording:
|
|
381
|
+
# 1. say **why** (the text enters the context of every future turn, and in this
|
|
382
|
+
# mode we cannot tell who is sending the request);
|
|
383
|
+
# 2. say **the way out** (write the file on that machine / restart without
|
|
384
|
+
# --host) — saying only "not allowed" leaves the user concluding it is broken;
|
|
385
|
+
# 3. **do not phrase it as "insufficient permissions"**: their credentials are
|
|
386
|
+
# perfectly fine; it is this path that is closed in this mode.
|
|
387
|
+
role_add_lan_exposed: 'Identities cannot be created in this mode — the server was bound outside the loopback interface with --host, so we cannot tell whether the browser sending this request is on this machine. The body of an identity enters the context of every future turn (and its description enters the tool description of every delegation), so this path is only open under the default loopback binding. Two ways forward: go to the machine running the server and put a .md file in ~/.epoch/agents/, or drop --host and restart.'
|
|
388
|
+
role_add_bad_field: 'That field cannot be accepted: {detail}'
|
|
389
|
+
|
|
390
|
+
# The nine `/api/schedules*` endpoints (plan 45 PR-3).
|
|
391
|
+
#
|
|
392
|
+
# ⚠️ **This group only has two kinds: "this JSON is unreadable" and "the thing
|
|
393
|
+
# at this URL is not here".** Not one business validation lives here — an empty
|
|
394
|
+
# name, a 7-minute interval, "skip every check" without a workspace: those go
|
|
395
|
+
# out as 200 + `issues` (a list of **codes**), worded in the browser's own
|
|
396
|
+
# catalogue (`web.ui.auto.issue.*`). Two criteria: server-side Chinese never
|
|
397
|
+
# goes over the wire (same as `wire-settings`); and the same validation
|
|
398
|
+
# **ought to read differently** in the CLI and in the form — the CLI says
|
|
399
|
+
# "--budget is required", and there is no --budget in a form.
|
|
400
|
+
schedule_unknown: 'No scheduled task with id {id} — the list you are holding may be stale (another tab or the command line may have deleted it). Refresh and look again.'
|
|
401
|
+
schedule_run_unknown: 'This task has no run {runId} in its history — run history is capped (the most recent 20 per task), and older ones get pruned.'
|
|
402
|
+
schedule_bad_field: 'The {field} field has the wrong type (this JSON is unreadable — it is not that the value is unreasonable).'
|
|
403
|
+
schedule_bad_permission: 'Unknown permission level — this route only accepts the levels on the permission pill.'
|
|
404
|
+
schedule_bad_trigger: 'Cannot read this trigger: kind must be cron / interval / once, and each one must carry its own fields.'
|
|
405
|
+
schedule_bad_operation: 'allowOperations contains an unrecognised operation type. Only these are accepted: {types}'
|
|
406
|
+
schedule_create_incomplete: 'Creating a scheduled task needs at least these five: name / prompt / permission / trigger / maxBudgetUsd.'
|
|
407
|
+
# ⚠️ This line is the wire-side face of the one deliberate piece of friction in
|
|
408
|
+
# the whole plan (§3.5): the server **does not fill in a default**. A fake
|
|
409
|
+
# default would make "woke up to $40" our fault, so a client that forgets to
|
|
410
|
+
# send it gets a 400 instead.
|
|
411
|
+
schedule_budget_required: 'maxBudgetUsd is required and has no default — a fake default would make "woke up to $40" our fault.'
|
|
412
|
+
schedule_fix_needs_run: 'runId is required: the IOUs hang off one particular run, and the rules are read from that row in the database (rules echoed back in the request body are not accepted).'
|
|
413
|
+
|
|
414
|
+
# ---- Browser UI strings (plan 43 §2.9) ----
|
|
415
|
+
#
|
|
416
|
+
# This group is a different animal from the ones above: those are error bodies
|
|
417
|
+
# the server generates and sends over the wire; these are strings the frontend
|
|
418
|
+
# itself hard-codes. Vite compiles them into the web bundle at build time
|
|
419
|
+
# (`packages/web/vite/locales.ts`), and web's own `t()` reads them.
|
|
420
|
+
#
|
|
421
|
+
# ⚠️ After switching to English, the server-generated half (the entries above,
|
|
422
|
+
# diagnostic details, permission denial reasons) still follows the language of
|
|
423
|
+
# the `epoch web` process. A Chinese-locale server plus an English browser is
|
|
424
|
+
# therefore half-and-half. That is the **expected shape** for this round
|
|
425
|
+
# (plan 43, acceptance 31), not a missed translation.
|
|
426
|
+
ui:
|
|
427
|
+
conn:
|
|
428
|
+
connecting: 'Connecting'
|
|
429
|
+
open: 'Connected'
|
|
430
|
+
reconnecting: 'Reconnecting'
|
|
431
|
+
closed: 'Disconnected'
|
|
432
|
+
|
|
433
|
+
topbar:
|
|
434
|
+
untitled: 'Untitled session'
|
|
435
|
+
resyncing: 'Catching up'
|
|
436
|
+
# ⚠️ Used in two places: the aria-label/title of the topbar toggle, and the
|
|
437
|
+
# aria-label of the inspector `<aside>` (at ≤1180px it is a drawer, and
|
|
438
|
+
# focus lands on that root when it slides in). The toggle and the thing it
|
|
439
|
+
# toggles deliberately share one name.
|
|
440
|
+
inspector: 'Inspector'
|
|
441
|
+
# The sidebar toggle plus the sidebar `<aside>` aria-label — same as above.
|
|
442
|
+
# ⚠️ That toggle **only exists at ≤860px** (on wide screens the sidebar is a
|
|
443
|
+
# permanent column, so pressing it would change nothing on screen — decision
|
|
444
|
+
# 20 ①); the criterion is on `TopbarProps.sidebar` in `shell/topbar.tsx`.
|
|
445
|
+
# "Sessions sidebar", not "Menu": that column holds the session list, new
|
|
446
|
+
# session, capabilities, settings and usage, and "menu" would both fail to
|
|
447
|
+
# say what is in it and collide with the `.menu` popover primitive.
|
|
448
|
+
sidebar: 'Sessions sidebar'
|
|
449
|
+
# The line next to the model name in the topbar (plan 30 §10.4 item 2 → §6).
|
|
450
|
+
#
|
|
451
|
+
# ⚠️ 2026-08-16: both the key and the sentence changed. The old one
|
|
452
|
+
# (`model_one_per_process`) said "there is one model selection per process,
|
|
453
|
+
# go back to the session you opened first" — model selection is now one per
|
|
454
|
+
# session, so that half is a **lie**, and it goes away together with the
|
|
455
|
+
# code it quoted (`bootstrap-session-only`).
|
|
456
|
+
#
|
|
457
|
+
# What survives is the second half of the old sentence, and it was always
|
|
458
|
+
# the harmful one: this cell falls back to `config.model` (the default for
|
|
459
|
+
# this launch) only when **this process is not running that session**, and
|
|
460
|
+
# then it cannot say which model that session actually used.
|
|
461
|
+
# ⚠️ Never say "switch models elsewhere": that session is not running at
|
|
462
|
+
# all, so there is nowhere to switch it.
|
|
463
|
+
model_not_live: 'This session is not running in this process; the name here is the default model for this launch — it may not be the one it actually used.'
|
|
464
|
+
|
|
465
|
+
# The model-switch menu in the topbar (plan 26, the web half).
|
|
466
|
+
#
|
|
467
|
+
# ⚠️ This group is the *wording of the rejections*; what travels the wire is
|
|
468
|
+
# a code plus parameters (`WireModelRejection` / `WireModelCaveat`) — the
|
|
469
|
+
# server does not send a single word of prose. The criterion is in the file
|
|
470
|
+
# header of `server/src/model.ts`. The three rejections each say their own
|
|
471
|
+
# thing and must never collapse into one "cannot switch": their next steps
|
|
472
|
+
# are completely different (go configure a key / pick a model that reads
|
|
473
|
+
# images / clear the context first), and saying that next step is the entire
|
|
474
|
+
# point of these sentences.
|
|
475
|
+
model:
|
|
476
|
+
# This menu changes which model **this session is using right now** — not
|
|
477
|
+
# the settings-page value, which is the default for the next start-up.
|
|
478
|
+
label: 'Model this session is using right now'
|
|
479
|
+
trigger: '{model} — open to switch model'
|
|
480
|
+
current: '{provider} · in use'
|
|
481
|
+
reset: 'Back to the configured one'
|
|
482
|
+
# **There is no model list**; the criterion is in the file header of
|
|
483
|
+
# `shell/model-menu.tsx`: the set of names a provider accepts is open, so
|
|
484
|
+
# a closed list would omit exactly the ones users need to type by hand.
|
|
485
|
+
placeholder: 'Type a model name, then Enter'
|
|
486
|
+
rej_no_credential: 'no usable credential for {provider}, cannot switch. Set {env}, or run epoch model to configure it'
|
|
487
|
+
# The `envVar: null` case: that provider has no agreed environment variable
|
|
488
|
+
# name, and leaving the placeholder in would send the user off to set a
|
|
489
|
+
# variable literally called "null".
|
|
490
|
+
rej_no_credential_bare: 'no usable credential for {provider}, cannot switch. Run epoch model to configure it'
|
|
491
|
+
rej_images: '{model} does not read images, and this session already has some. Switching does not clear the context, so the next turn would be rejected by the provider'
|
|
492
|
+
rej_context: 'context is already at {used}, past the {model} window ({window}). Start a fresh session, or pick a model with a bigger window'
|
|
493
|
+
cav_context_tight: 'switched, but context is already at {used} — close to the {model} window ({window}), so it may not last many turns'
|
|
494
|
+
cav_no_tools: 'switched, but {model} does not support tool calls — it can only chat, and cannot read or write files'
|
|
495
|
+
# Provider picker + model suggestions (`web/src/model/picker.tsx`, shared by
|
|
496
|
+
# both surfaces).
|
|
497
|
+
#
|
|
498
|
+
# ⚠️ These are the five "what do we draw when discovery comes up short" tiers.
|
|
499
|
+
# NONE of them may be merged into one "no models available" — the next step
|
|
500
|
+
# differs per tier: wait / go configure a key / check the network / just type.
|
|
501
|
+
pick_provider: 'Provider'
|
|
502
|
+
pick_provider_none: 'Pick one…'
|
|
503
|
+
provider_no_key: '{label} (no key configured)'
|
|
504
|
+
probe_probing: 'Asking {provider} which models it has… no need to wait — you can just type one above.'
|
|
505
|
+
probe_no_key: 'No key configured for {provider}, so its model list cannot be fetched. Set {env}, or run epoch model.'
|
|
506
|
+
probe_no_key_bare: 'No key configured for {provider}, so its model list cannot be fetched. Run epoch model to set one up.'
|
|
507
|
+
probe_failed_cache: 'Could not reach {provider} (network / bad key / that base URL has no /models). Below is what was fetched last time, which may be stale — you can also just type a name.'
|
|
508
|
+
probe_failed_static: 'Could not reach {provider} (network / bad key / that base URL has no /models). Below is the built-in list, which may be stale — you can also just type a name.'
|
|
509
|
+
probe_empty: '{provider} returned no model names at all, and the built-in list is empty too — just type one.'
|
|
510
|
+
probe_ok: 'Got {count} from {provider}'
|
|
511
|
+
# ⚠️ This line is the VISIBLE form of "the model tier is not a closed list".
|
|
512
|
+
# Without it, a column of names reads as the complete set.
|
|
513
|
+
suggest_note: 'These are fetched suggestions, not the full set — new or self-hosted models will not be here. You can still just type one.'
|
|
514
|
+
refresh: 'Re-fetch'
|
|
515
|
+
refresh_hint: 'Bypass the cache and ask this provider again'
|
|
516
|
+
|
|
517
|
+
# The top row of the sidebar plus the workspace menu hanging off it (plan 30
|
|
518
|
+
# acceptance 8).
|
|
519
|
+
newsession:
|
|
520
|
+
# ⚠️ No "⌘N" chip, even though the design draft has one: browsers keep
|
|
521
|
+
# ⌘N / Ctrl+N for themselves (new window), the keydown never reaches the
|
|
522
|
+
# page, and preventDefault cannot stop it. Drawing an unpressable shortcut
|
|
523
|
+
# chip is the same thing as drawing a button that does nothing (decision
|
|
524
|
+
# 20 ①). Full reasoning in session/new-session.tsx.
|
|
525
|
+
open: 'New session'
|
|
526
|
+
# ✅ 2026-08-17 (plan 55 PR-1): the workspace menu that used to hang off this
|
|
527
|
+
# row **moved wholesale to the composer's bottom bar** (where decision 18 draws
|
|
528
|
+
# it), so its five lines moved with it, under
|
|
529
|
+
# `web.ui.composer.workspace_pick*` — `menu_label` / `here_current` /
|
|
530
|
+
# `no_workspace` / `no_workspace_detail` / `list_note`, wording unchanged.
|
|
531
|
+
#
|
|
532
|
+
# ⚠️ **Do not add new workspace copy here.** This row is now a plain action
|
|
533
|
+
# (one click gives you a new session) and says nothing about the directory —
|
|
534
|
+
# that is the bottom bar's job. Full reasoning in session/new-session.tsx.
|
|
535
|
+
|
|
536
|
+
# The directory picker (plan 55 PR-2, 2026-08-17) — the second-level view inside
|
|
537
|
+
# the workspace menu. This is what "Open a local folder" in the draft opens.
|
|
538
|
+
dirpick:
|
|
539
|
+
back: 'Back'
|
|
540
|
+
# The anchor screen. **Not "root"**: it lists three groups of "places you
|
|
541
|
+
# probably want" (home / the server process's cwd / known workspaces), not the
|
|
542
|
+
# filesystem root. Never make people start crawling from `/` — that is the one
|
|
543
|
+
# thing this kind of picker always gets shouted at for.
|
|
544
|
+
anchors: 'Common places'
|
|
545
|
+
up: 'Up one level'
|
|
546
|
+
# Deep directories genuinely take time, so draw this line instead of leaving a
|
|
547
|
+
# gap — a gap reads as "there is nothing here".
|
|
548
|
+
loading: 'Loading…'
|
|
549
|
+
# "This level has no subdirectories" is true, and it has a next step (pick this
|
|
550
|
+
# level itself).
|
|
551
|
+
empty: 'No subdirectories here — pick this one, or type a path below'
|
|
552
|
+
# ⚠️ **Truncation has to be said out loud**: silently cutting the list reads as
|
|
553
|
+
# "this is all of it", and the user will conclude their directory is not here.
|
|
554
|
+
# Reasoning lives on the server's `omitted` field.
|
|
555
|
+
omitted: '{count} more not listed at this level (1000 max per level) — type a path below to reach them'
|
|
556
|
+
# The "show hidden directories" checkbox (2026-08-18). ⚠️ Two strings, because
|
|
557
|
+
# they do not say the same thing: the plain one says "this is the rule this list
|
|
558
|
+
# follows", the one with the count adds a **checkable fact** — N were just held
|
|
559
|
+
# back at this level. Same reasoning as `omitted` above: silent filtering reads
|
|
560
|
+
# as "this machine has no such directory", and `~/.claude` is a directory people
|
|
561
|
+
# really do want to pick.
|
|
562
|
+
#
|
|
563
|
+
# ⚠️ Wording is "hidden directories", not "dot directories"/"dotfiles": the
|
|
564
|
+
# former is the word Finder and Explorer already taught the user. And **not
|
|
565
|
+
# "hidden files"** — this endpoint lists no files at all.
|
|
566
|
+
show_hidden: 'Show hidden directories (starting with .)'
|
|
567
|
+
show_hidden_count: 'Show hidden directories (starting with ., {count} here)'
|
|
568
|
+
# An unreadable row is **drawn but not clickable**. This goes in the title: a
|
|
569
|
+
# greyed button only says "you can't", not "why" — and the why comes from file
|
|
570
|
+
# permissions on the machine running the server.
|
|
571
|
+
unreadable: 'no permission to enter this directory: {path}'
|
|
572
|
+
into: 'Enter {name}'
|
|
573
|
+
# "I have worked here before". ⚠️ It is **not** "this is a git repository" —
|
|
574
|
+
# that would need a git call, and this level may hold a thousand directories.
|
|
575
|
+
known: 'visited'
|
|
576
|
+
# The second entry point. It costs nothing and covers two things you cannot
|
|
577
|
+
# click to: deep paths, and the case where the user already knows exactly where
|
|
578
|
+
# they want to go. (Dot directories used to be on that list too; since
|
|
579
|
+
# 2026-08-18 the checkbox above covers them — typing a path only occurs to
|
|
580
|
+
# someone who already knows what is missing.)
|
|
581
|
+
# ⚠️ **No pre-validation on this side** — validation belongs to the server's
|
|
582
|
+
# existing bind() gate; pre-validating means two copies of the same rule, and
|
|
583
|
+
# they always drift.
|
|
584
|
+
manual_label: 'Or type an absolute path'
|
|
585
|
+
manual_hint: 'e.g. /Users/you/work/repo'
|
|
586
|
+
manual_go: 'Use this'
|
|
587
|
+
# "New workspace" (plan 55 PR-3). The draft makes this a modal; here it is a
|
|
588
|
+
# row at the bottom of this view. Reasoning is decision 5 (the whole draft has
|
|
589
|
+
# exactly one modal left, and this is it): the picker is already open, and
|
|
590
|
+
# "where to create it" is precisely the level on screen — another overlay
|
|
591
|
+
# would just cover it up.
|
|
592
|
+
new_label: 'Create a directory here'
|
|
593
|
+
new_hint: 'e.g. my-project'
|
|
594
|
+
new_go: 'Create'
|
|
595
|
+
# ⚠️ This line has to be on screen **before** the button is pressed. The draft
|
|
596
|
+
# modal used to say "a newly created directory is trusted by default"; that
|
|
597
|
+
# half was ruled out (plan 55 §2.6, two criteria: the trust write path is
|
|
598
|
+
# deliberately kept off the wire; and "it is a freshly created empty
|
|
599
|
+
# directory" does not hold up — empty is only true right now, and the very
|
|
600
|
+
# first thing to happen is an agent writing into it). What the user expects
|
|
601
|
+
# about trust as they press this is exactly what this line corrects.
|
|
602
|
+
new_trust_note: 'A new directory is treated exactly like an existing one: untrusted — its .epoch/settings.json and CLAUDE.md will not take effect. Run epoch trust add to grant trust.'
|
|
603
|
+
|
|
604
|
+
# The new-session screen (design draft tpl-blank). ⚠️ A blank screen is an
|
|
605
|
+
# invitation, not an "no data" notice — so when messages cannot be sent it
|
|
606
|
+
# draws nothing at all. Reasoning in session/blank.tsx.
|
|
607
|
+
#
|
|
608
|
+
# ⚠️ 2026-08-18: this screen shrank to **just `title`**. The eyebrow, the
|
|
609
|
+
# lede, the three seeds and the session-scope note are all gone — the
|
|
610
|
+
# ledger for each is in the file header of session/blank.tsx. Short version:
|
|
611
|
+
# the three seeds were "too prescriptive" (the user's words), narrowing
|
|
612
|
+
# "what is this agent for" down to "it edits code", on the very first
|
|
613
|
+
# screen someone sees. **Do not add them back.**
|
|
614
|
+
blank:
|
|
615
|
+
title: 'What would you like to do?'
|
|
616
|
+
|
|
617
|
+
sidebar:
|
|
618
|
+
search: 'Search sessions'
|
|
619
|
+
capability: 'Identities · Skills · Connectors'
|
|
620
|
+
# The fourth destination in the top-level nav (plan 45 §7.1). **Placement
|
|
621
|
+
# is product hierarchy** (decision 14): buried in settings, scheduled tasks
|
|
622
|
+
# read as a config option.
|
|
623
|
+
automation: 'Automation'
|
|
624
|
+
settings: 'Settings'
|
|
625
|
+
session: 'Session'
|
|
626
|
+
usage_session: 'Session usage'
|
|
627
|
+
usage_run: 'Last run usage'
|
|
628
|
+
cost: 'Spend'
|
|
629
|
+
cost_of: '~${spent} / ${limit}'
|
|
630
|
+
cost_est: 'Estimate'
|
|
631
|
+
tasks: 'Background tasks'
|
|
632
|
+
tasks_of: '{running} running / {total}'
|
|
633
|
+
diagnostic: '{module}: {detail}'
|
|
634
|
+
|
|
635
|
+
slist:
|
|
636
|
+
# Three groups; the test is "what can this row actually say", not importance.
|
|
637
|
+
# ⚠️ 'Tasks' is decision 18's `none` state ("explicitly chose no workspace") —
|
|
638
|
+
# not a catch-all bucket. Sessions left over from an earlier process used to
|
|
639
|
+
# land here too, which announced a binding decision they never made.
|
|
640
|
+
tasks: 'Tasks'
|
|
641
|
+
spaces: 'Spaces'
|
|
642
|
+
# Third group (2026-08-18): the rows whose wire field says `unknown`.
|
|
643
|
+
#
|
|
644
|
+
# ⚠️ 2026-08-19 this group narrowed: the test moved from "`live: false` with
|
|
645
|
+
# no workspace" to the explicit `unknown` state on the wire, so it now holds
|
|
646
|
+
# only rows older than session-DB v7 (both columns NULL). Sessions that were
|
|
647
|
+
# bound to a space — or that explicitly chose no workspace — go back to their
|
|
648
|
+
# space / to Tasks after a restart. The line below is unchanged; it is now
|
|
649
|
+
# more accurate than it was.
|
|
650
|
+
unknown: 'Earlier sessions'
|
|
651
|
+
# Not decoration — this line is the whole reason the group exists. Same
|
|
652
|
+
# wording as the composer's cell (`composer.workspace_unknown_hint`).
|
|
653
|
+
unknown_hint: "This process can't say where these worked — not that they have none"
|
|
654
|
+
awaiting: 'Needs you'
|
|
655
|
+
running: 'Running'
|
|
656
|
+
# Shared by the sidebar row and the cross-session bar — same field
|
|
657
|
+
# (`WireSessionSummary.updatedAt`), so one string, not two.
|
|
658
|
+
# Was 'Started at' until 2026-08-16; session DB v4 added `updated_at`.
|
|
659
|
+
# Reads "last active", NOT "waiting for" — see the zh catalog for why.
|
|
660
|
+
updated_at: 'Last active'
|
|
661
|
+
count: '{count} sessions'
|
|
662
|
+
empty: 'No sessions yet'
|
|
663
|
+
no_match: 'No matching sessions'
|
|
664
|
+
|
|
665
|
+
# ---- Row overflow: delete / rename (acceptance 15 / 16, 2026-08-15) ----
|
|
666
|
+
acts: 'Actions for “{title}”'
|
|
667
|
+
rename: 'Rename'
|
|
668
|
+
rename_label: 'Session title'
|
|
669
|
+
rename_do: 'Save title'
|
|
670
|
+
delete: 'Delete session'
|
|
671
|
+
delete_ask: 'Delete “{title}”?'
|
|
672
|
+
# Says what goes with it, not "are you sure" — the last clause is the point:
|
|
673
|
+
# the files it changed stay on disk, they just can't be rewound any more
|
|
674
|
+
delete_what: 'Its conversation, its checkpoints and its workspace binding all go, and none of it comes back — the files it changed stay on disk, but they can no longer be rewound.'
|
|
675
|
+
delete_running: 'It has a turn running right now; deleting aborts that turn.'
|
|
676
|
+
# "abandoned", not "denied" — hub.unregister() calls relay.abandon(), so the
|
|
677
|
+
# tool calls waiting on those requests never receive a denial
|
|
678
|
+
delete_awaiting: '{count} request(s) are still waiting on you; deleting abandons them rather than denying them.'
|
|
679
|
+
delete_do: 'Delete'
|
|
680
|
+
cancel: 'Cancel'
|
|
681
|
+
err_gone: 'That session is already gone.'
|
|
682
|
+
err_title: 'The title cannot be empty.'
|
|
683
|
+
err_no_store: 'The session store on this machine did not start, so sessions cannot be renamed or deleted.'
|
|
684
|
+
|
|
685
|
+
inspector:
|
|
686
|
+
patch: 'Patch'
|
|
687
|
+
output: 'Output'
|
|
688
|
+
artifact: 'Artifacts'
|
|
689
|
+
plan: 'Plan'
|
|
690
|
+
close: 'Collapse'
|
|
691
|
+
no_patch: 'No file changes in this turn yet'
|
|
692
|
+
locate: 'Locate'
|
|
693
|
+
locate_folded: 'That row is not on the timeline right now — most likely a fold bar has it collapsed. Expand that section and this button works.'
|
|
694
|
+
|
|
695
|
+
output:
|
|
696
|
+
title: 'Background tasks'
|
|
697
|
+
loading: 'Loading tasks…'
|
|
698
|
+
empty: 'This session has no background tasks right now'
|
|
699
|
+
st_running: 'Running'
|
|
700
|
+
st_exited: 'Finished, exit code {code}'
|
|
701
|
+
st_killed: 'Stopped'
|
|
702
|
+
st_failed: 'Failed to start'
|
|
703
|
+
dur: '{value} elapsed'
|
|
704
|
+
out_bytes: '{bytes} of output'
|
|
705
|
+
no_output: 'This task has produced no output yet'
|
|
706
|
+
omitted: 'Only the tail is shown; {bytes} before it were not sent.'
|
|
707
|
+
truncated: 'This task’s output went over the cap, so the beginning was dropped — it is gone, here and everywhere else.'
|
|
708
|
+
sampled: 'For tasks still running this is a single sample: it is re-read when the turn state changes, so anything produced in between waits for the next read.'
|
|
709
|
+
|
|
710
|
+
art:
|
|
711
|
+
files: 'Workspace changes'
|
|
712
|
+
files_note: 'Files this session created or modified in the workspace.'
|
|
713
|
+
files_empty: 'This session has not changed any file in the workspace'
|
|
714
|
+
no_space: 'This session has no workspace, so there are no workspace changes to show. Sessions left over from a previous process land here too — workspace binding only lives in process memory.'
|
|
715
|
+
loading: 'Loading the list…'
|
|
716
|
+
thin: 'Rows replayed after a refresh only carry the path and the write count: file contents are not persisted, so they cannot be opened.'
|
|
717
|
+
created: 'created'
|
|
718
|
+
changed: 'changed'
|
|
719
|
+
lines: '{count} lines'
|
|
720
|
+
writes: 'written {count} times'
|
|
721
|
+
tools: 'Tool artifacts'
|
|
722
|
+
tools_note: 'Binaries produced by tools: screenshots, audio, downloads.'
|
|
723
|
+
tools_empty: 'None in this turn. Today only MCP tools produce them (image / audio / resource.blob).'
|
|
724
|
+
tool_gate: 'They pass four gates before entering the context: capability, size, spooling, cost. On a model that takes no images, falling back is the main path — files are still listed here, but only a sentence about them reaches the model.'
|
|
725
|
+
back: 'Back to list'
|
|
726
|
+
readonly: 'read-only'
|
|
727
|
+
copy_path: 'Copy path'
|
|
728
|
+
by_created: 'created by {tool}'
|
|
729
|
+
by_changed: 'changed by {tool}'
|
|
730
|
+
kind_binary: 'binary'
|
|
731
|
+
kind_charset: 'encoding unknown'
|
|
732
|
+
opaque_binary: 'This looks like binary: it carries NUL bytes, or unprintable characters take up a large share of it. Laid out as text it is a screenful of replacement characters, so the content is not shown here.'
|
|
733
|
+
opaque_charset: 'This content is not UTF-8 — it may be GBK, UTF-16 or another encoding, and it may just as well be binary. The raw bytes never reach the browser (a patch frame carries an already-decoded string), so which one it is cannot be told from here. Not knowing means not guessing, and not drawing it.'
|
|
734
|
+
lossy: '{count} byte sequences did not decode. This file is most likely not UTF-8, and the original text at those spots is gone for good.'
|
|
735
|
+
clip_lines: 'Only the first {shown} lines are shown; this file has {total} lines.'
|
|
736
|
+
clip_chars: 'Cut off on line {line} — that line is too long. This file has {total} lines.'
|
|
737
|
+
# ⚠️ 2026-08-18 trimmed — see the zh comment for the ledger.
|
|
738
|
+
binary_note: 'These files cannot be viewed here — open them with the program your system uses.'
|
|
739
|
+
|
|
740
|
+
plan:
|
|
741
|
+
empty: 'No approved plan in this session yet'
|
|
742
|
+
empty_hint: 'A plan the model hands in has to be approved once on the panel above the composer; only the approved one stays here.'
|
|
743
|
+
loading: 'Loading the plan…'
|
|
744
|
+
note: 'This is the one currently in effect — a revised plan replaces it wholesale, no history is kept here. To see how it evolved, look for the exit_plan_mode calls on the timeline.'
|
|
745
|
+
|
|
746
|
+
banner:
|
|
747
|
+
dismiss: 'Got it'
|
|
748
|
+
session_gone: 'This conversation is gone on the server — most likely it restarted, and a restart starts a fresh session. This one is not coming back, but you can pick up in the new one.'
|
|
749
|
+
session_gone_open: 'Go to the current session'
|
|
750
|
+
session_gone_stuck: 'This conversation is gone on the server, and right now it cannot even tell us which session is still usable — the server process has most likely stopped. Check on it in your terminal.'
|
|
751
|
+
|
|
752
|
+
thread:
|
|
753
|
+
thinking_label: 'Reasoning'
|
|
754
|
+
# The two-line header on an assistant turn (`.mhead`, decision 8's *first*
|
|
755
|
+
# moment, 2026-08-15). It sat undrawn for two months because it needs two
|
|
756
|
+
# true things — who is running, and how much this turn will cost — and a
|
|
757
|
+
# top-level session had no role while the estimate under-counted Chinese by
|
|
758
|
+
# 2–2.7x. Both premises were fixed in this round.
|
|
759
|
+
#
|
|
760
|
+
# The number reads `~12k`, not `12,431` (as drawn): the footer's number is a
|
|
761
|
+
# measured value reported by the provider, this one is computed *before*
|
|
762
|
+
# sending, and printing it to the last digit is false precision.
|
|
763
|
+
head_estimate: 'expected {tokens} tokens'
|
|
764
|
+
# This badge must not be removed. The comment in the mockup's index.html says
|
|
765
|
+
# "swapping in a real tokenizer comes first if you want this badge gone" — that
|
|
766
|
+
# is a necessary condition, not a sufficient one; do not read it backwards. The
|
|
767
|
+
# sufficient condition is in the same mockup's thread.css on `.est`: tokens have
|
|
768
|
+
# three moments and only the pre-run one is a guess. A real tokenizer fixed how
|
|
769
|
+
# far off the number is, not whether it is a guess.
|
|
770
|
+
head_est_badge: 'estimate'
|
|
771
|
+
live_thinking: 'Thinking'
|
|
772
|
+
live_running: 'Generating a reply'
|
|
773
|
+
live_tool: 'Running a tool'
|
|
774
|
+
live_waiting: 'Waiting for the model'
|
|
775
|
+
live_approval: 'Waiting for your decision'
|
|
776
|
+
live_question: 'Waiting for your answer'
|
|
777
|
+
live_aborting: 'Stopping…'
|
|
778
|
+
live_spent: '{tokens} tokens so far'
|
|
779
|
+
queued: '{count} queued'
|
|
780
|
+
stop: 'Stop'
|
|
781
|
+
# Fold bar (2026-08-14). The three counts are why this row exists — see zh.yaml
|
|
782
|
+
fold_tools: '{count} calls'
|
|
783
|
+
fold_files: '{count} files'
|
|
784
|
+
fold_thoughts: '{count} thoughts'
|
|
785
|
+
fold_show: 'Show'
|
|
786
|
+
fold_hide: 'Hide'
|
|
787
|
+
gate: '{tool} is stopped here waiting for your decision. The chain below cannot pass this gate until then.'
|
|
788
|
+
tool_pending: 'Running'
|
|
789
|
+
tool_done: 'Done'
|
|
790
|
+
tool_error: 'Failed'
|
|
791
|
+
art_in: 'In the model context'
|
|
792
|
+
art_out: 'Not in the model context'
|
|
793
|
+
art_unknown: 'This stream does not say whether it entered the context'
|
|
794
|
+
art_tokens: '≈{count} tokens'
|
|
795
|
+
art_out_note: 'The model only received the line below; it never got the artifact itself.'
|
|
796
|
+
art_unknown_note: 'The gate verdict is not carried on this stream, so this is not guessing at it.'
|
|
797
|
+
art_no_file: 'Not spooled to disk — this artifact cannot be opened.'
|
|
798
|
+
art_download: 'Download {name}'
|
|
799
|
+
art_download_any: 'Download this artifact'
|
|
800
|
+
expand: 'Expand {count} more lines'
|
|
801
|
+
collapse: 'Collapse'
|
|
802
|
+
todo_title: 'Delivery checklist'
|
|
803
|
+
todo_count: '{done} / {total} done'
|
|
804
|
+
patch_of: 'Patch {path}'
|
|
805
|
+
patch_too_big: 'Too large to diff line by line; only the line counts are reported'
|
|
806
|
+
# The fourth state of a patch card: one side is not displayable text. Unlike
|
|
807
|
+
# the read-only preview (one whole file), a patch is a comparison — and the
|
|
808
|
+
# comparison itself goes wrong when one side decoded lossily, so this state
|
|
809
|
+
# reports no +N −M either. Full rationale in web/src/thread/patch.tsx.
|
|
810
|
+
patch_opaque_old: 'The version before this change cannot be drawn; the one after it is normal text.'
|
|
811
|
+
patch_opaque_new: 'The version after this change cannot be drawn.'
|
|
812
|
+
patch_opaque_both: 'Neither side of this change can be drawn.'
|
|
813
|
+
code: 'Code'
|
|
814
|
+
copy_code: 'Copy code'
|
|
815
|
+
copied: 'Copied'
|
|
816
|
+
note_model: 'Model: {model}'
|
|
817
|
+
note_compression: 'Context compressed ({count} messages)'
|
|
818
|
+
note_summary: 'Session ended after {duration}'
|
|
819
|
+
|
|
820
|
+
act_copy: 'Copy this reply'
|
|
821
|
+
act_rerun: 'Run this turn again'
|
|
822
|
+
act_quote: 'Quote this turn in the composer'
|
|
823
|
+
act_inspect: 'Open in the inspector'
|
|
824
|
+
facts_tokens: '{tokens} tokens'
|
|
825
|
+
facts_token_label: 'tokens'
|
|
826
|
+
facts_unknown: 'not reported'
|
|
827
|
+
facts_failover: 'failover'
|
|
828
|
+
facts_tools: '{count} tool calls'
|
|
829
|
+
facts_no_tools: 'no tools'
|
|
830
|
+
# The fifth action (the web half of plan 27). Text, not an icon: it is not an
|
|
831
|
+
# instant action (it opens a preview, you pick a scope and tick conflicts one
|
|
832
|
+
# by one), and it is the only one on this row that deletes files.
|
|
833
|
+
act_rewind: 'Rewind to before this turn'
|
|
834
|
+
# The mark a finished rewind leaves on the timeline. Note that "which turn"
|
|
835
|
+
# is NOT a field on WireRewindResponse — the quoted text comes from the
|
|
836
|
+
# timeline's own history (the turn the button was attached to), never from
|
|
837
|
+
# the response. Never write "turn N": turnIndex is a lazily assigned
|
|
838
|
+
# checkpoint number, not an ordinal of conversation turns.
|
|
839
|
+
rewound_to: 'Rewound to before the turn “{text}”'
|
|
840
|
+
rewound_here: 'Rewound to before this point'
|
|
841
|
+
# See the zh notes: this pair is for the failed branch only — the two lines
|
|
842
|
+
# above would be a lie there, and this mark sits on the causal timeline.
|
|
843
|
+
rewound_failed: 'Tried to rewind to before "{text}" — it did not go through'
|
|
844
|
+
rewound_failed_here: 'Tried to rewind to before this point — it did not go through'
|
|
845
|
+
# `title` for the little cancellation-reason chip (`.todo__why`). The reason
|
|
846
|
+
# text itself is the model's own words and never passes through here — this
|
|
847
|
+
# key only gives that text an identity.
|
|
848
|
+
#
|
|
849
|
+
# It is needed because a screen reader runs the two runs of text together:
|
|
850
|
+
# "Bump vitest while we're here you said don't touch versions" sounds like
|
|
851
|
+
# part of the task name. Visually the chip's box (and the fact that it
|
|
852
|
+
# trails a struck-through item) already says what it is; audibly nothing does.
|
|
853
|
+
#
|
|
854
|
+
# When there is no reason the whole chip is omitted, so this string never
|
|
855
|
+
# appears — we do not fall back to "no reason given" (decision 20 ①).
|
|
856
|
+
todo_why: 'Cancellation reason'
|
|
857
|
+
|
|
858
|
+
# "Nearly full" — the third of decision 8's three moments (2026-08-16), sitting
|
|
859
|
+
# after the "estimate" badge on the turn head. Four criteria are in the file
|
|
860
|
+
# header of web/src/thread/turn-head.tsx.
|
|
861
|
+
#
|
|
862
|
+
# Neither string may be written as a prediction ("will compact next turn"):
|
|
863
|
+
# the engine's numerator is the input+output accumulated step by step across
|
|
864
|
+
# this run, which is always >= the prompt estimate judged here. So the badge
|
|
865
|
+
# being lit does mean the engine is past the line, but the badge being absent
|
|
866
|
+
# does NOT mean nothing will be compacted. A prediction would make "no badge"
|
|
867
|
+
# read as "no compaction", which is false.
|
|
868
|
+
#
|
|
869
|
+
# Neither string may carry a number either. The `~64k` to its left is the very
|
|
870
|
+
# quantity being judged; the line itself (128,000 × 50% = 64,000) is printed by
|
|
871
|
+
# the settings screen.
|
|
872
|
+
head_ctx_line: 'at compaction line'
|
|
873
|
+
# Auto-compaction turned off, or the compressor never came up — for a user past
|
|
874
|
+
# the line the consequence is word for word the same: no automatic remedy. So it
|
|
875
|
+
# is the same sentence with a suffix, not a separate sentence.
|
|
876
|
+
head_ctx_off: 'at compaction line · no auto-compaction'
|
|
877
|
+
|
|
878
|
+
# The rewind preview panel (the web half of plan 27). The interaction is copied
|
|
879
|
+
# from the TUI's RewindPicker line by line — two hosts disagreeing on the
|
|
880
|
+
# defaults for this is the real risk.
|
|
881
|
+
rewind:
|
|
882
|
+
title: 'Rewind to before this turn'
|
|
883
|
+
loading: 'Reading this turn’s checkpoint…'
|
|
884
|
+
act_restore: 'restore'
|
|
885
|
+
act_delete: 'delete'
|
|
886
|
+
act_recreate: 'recreate'
|
|
887
|
+
act_skip: 'leave'
|
|
888
|
+
files_head: 'Will touch {count} file(s)'
|
|
889
|
+
files_none: 'Nothing to change (the files already match the snapshot)'
|
|
890
|
+
conflicts_head: '{count} file(s) changed after the agent left them — none are overwritten by default'
|
|
891
|
+
conflict_hint: 'Ticked ones get overwritten — those are your edits, not the agent’s'
|
|
892
|
+
drift_changed: 'changed since'
|
|
893
|
+
drift_unknown: 'old checkpoint, not sure'
|
|
894
|
+
scope_legend: 'What to rewind'
|
|
895
|
+
scope_files: 'Files only (the conversation stays, so it can try again)'
|
|
896
|
+
scope_conversation: 'Conversation only (the files stay)'
|
|
897
|
+
scope_both: 'Both'
|
|
898
|
+
scope_conversation_off: 'not supported by this checkpoint'
|
|
899
|
+
terminal_note: 'Only edits made by file tools are covered; files changed by commands run in the terminal are not'
|
|
900
|
+
incomplete: 'This checkpoint is incomplete (some files were never captured), so only part of it can be restored'
|
|
901
|
+
conversation_warn: 'I understand: rewinding the conversation really deletes the messages after this point — it is not undoable, and they are not merely hidden'
|
|
902
|
+
run: 'Rewind'
|
|
903
|
+
running: 'Rewinding…'
|
|
904
|
+
cancel: 'Cancel'
|
|
905
|
+
close: 'Got it'
|
|
906
|
+
failed: 'Rewind failed: {reason}'
|
|
907
|
+
failed_atomic: 'Not a single byte of the workspace was changed — the rewind is atomic, so a failure means nothing was touched.'
|
|
908
|
+
done_restored: '{count} file(s) restored'
|
|
909
|
+
done_deleted: '{count} file(s) deleted'
|
|
910
|
+
done_recreated: '{count} file(s) recreated'
|
|
911
|
+
done_nothing: 'Nothing to change (the files already matched the snapshot)'
|
|
912
|
+
done_skipped: '{count} of your own edits were not overwritten'
|
|
913
|
+
done_messages: '{count} message(s) deleted from the conversation (not undoable)'
|
|
914
|
+
|
|
915
|
+
dock:
|
|
916
|
+
title_permission: 'Needs your confirmation'
|
|
917
|
+
title_plan: 'Plan awaiting approval'
|
|
918
|
+
title_question: 'Needs a choice from you'
|
|
919
|
+
queue: '{count} more queued behind this'
|
|
920
|
+
queue_questions: '{count} more sets of questions'
|
|
921
|
+
|
|
922
|
+
act_file_read: 'read a file'
|
|
923
|
+
act_file_write: 'write a file'
|
|
924
|
+
act_command: 'run a command'
|
|
925
|
+
act_network: 'make a network request'
|
|
926
|
+
act_code_exec: 'run code in the sandbox'
|
|
927
|
+
|
|
928
|
+
fact_action: 'Action'
|
|
929
|
+
fact_target: 'Target'
|
|
930
|
+
fact_detail: 'Detail'
|
|
931
|
+
fact_reason: 'Reason'
|
|
932
|
+
target_none: '(none)'
|
|
933
|
+
|
|
934
|
+
allow_once: 'Allow once'
|
|
935
|
+
allow_session: 'Always allow this session'
|
|
936
|
+
allow_session_hint: 'until you quit'
|
|
937
|
+
allow_always: 'Allow permanently'
|
|
938
|
+
allow_always_hint: 'writes approvals.json'
|
|
939
|
+
deny: 'Deny'
|
|
940
|
+
|
|
941
|
+
plan_execute: 'Approve and execute'
|
|
942
|
+
plan_readonly: 'Approve, but stay read-only'
|
|
943
|
+
plan_revise: 'Let me revise it'
|
|
944
|
+
plan_execute_hint: 'permission returns to {level}'
|
|
945
|
+
plan_readonly_hint: "I'll do it myself"
|
|
946
|
+
plan_readonly_forced: 'you asked for read-only throughout'
|
|
947
|
+
note_label: 'Say what to change:'
|
|
948
|
+
note_placeholder: 'e.g. only touch core for now, leave tui alone'
|
|
949
|
+
|
|
950
|
+
q_free: 'None of these — my own answer:'
|
|
951
|
+
q_free_placeholder: '(optional) filling this in makes it the answer'
|
|
952
|
+
q_submit: 'Submit'
|
|
953
|
+
q_submit_partial: '{count} unanswered; it will be told you skipped them'
|
|
954
|
+
q_skip: 'Skip'
|
|
955
|
+
q_skip_hint: 'the model picks a default and says which'
|
|
956
|
+
|
|
957
|
+
crossbar:
|
|
958
|
+
approval_one: '“{title}” is waiting on one approval'
|
|
959
|
+
approval_n: '“{title}” is waiting on {count} approvals'
|
|
960
|
+
question_one: '“{title}” is waiting on one question'
|
|
961
|
+
question_n: '“{title}” is waiting on {count} questions'
|
|
962
|
+
cold: 'cooled'
|
|
963
|
+
cold_hint: 'the server is not holding it right now; opening it replays history'
|
|
964
|
+
more: '{count} more'
|
|
965
|
+
go: 'Go there'
|
|
966
|
+
# The caption for the age cell on the bar (2026-08-16). It used to read
|
|
967
|
+
# "Last active" (shared with the sidebar's `slist.updated_at`); it now reads
|
|
968
|
+
# the moment the session got blocked (`WireSessionSummary.pendingSince`), so
|
|
969
|
+
# the caption changed with it and the two cells no longer share a key.
|
|
970
|
+
# ⚠️ No placeholder — this is the title on the number, not a sentence around
|
|
971
|
+
# it. The number is `relativeTime()`'s "3 hours ago"; see zh.yaml for the
|
|
972
|
+
# judgement, including why the grain stops at minutes and not the mock's 2:07.
|
|
973
|
+
waited: 'Waiting since'
|
|
974
|
+
|
|
975
|
+
toast:
|
|
976
|
+
# Neutral on purpose — it covers both "finished cleanly" (green check) and
|
|
977
|
+
# "we don't know how it ended" (no sign at all). See zh.yaml for the judgement.
|
|
978
|
+
done: '“{title}” finished its run'
|
|
979
|
+
# The failure half (2026-08-15). One line per FinishReason, not one shared
|
|
980
|
+
# "didn't finish": hitting a budget cap and being refused by the model call
|
|
981
|
+
# for completely different follow-ups, and telling them apart without
|
|
982
|
+
# opening the session is the whole point of this corner.
|
|
983
|
+
max_turns: '“{title}” ran out of turns before finishing'
|
|
984
|
+
stalled: '“{title}” was going in circles and stopped'
|
|
985
|
+
aborted: '“{title}” was stopped mid-run'
|
|
986
|
+
filtered: '“{title}” was refused by the model'
|
|
987
|
+
budget: '“{title}” hit the budget cap and stopped early'
|
|
988
|
+
failed: '“{title}” hit an error'
|
|
989
|
+
go: 'View'
|
|
990
|
+
|
|
991
|
+
composer:
|
|
992
|
+
label: 'Message'
|
|
993
|
+
placeholder_idle: 'Say something… (Enter to send, Shift+Enter for a newline)'
|
|
994
|
+
placeholder_running: 'This turn is still running; what you send queues behind it'
|
|
995
|
+
placeholder_offline: 'Disconnected from the server, reconnecting…'
|
|
996
|
+
placeholder_approval: 'Waiting for you to confirm the action above'
|
|
997
|
+
placeholder_question: 'Waiting for you to answer the question above'
|
|
998
|
+
placeholder_no_session: 'No session yet'
|
|
999
|
+
|
|
1000
|
+
# Four sentences for "you cannot write" (2026-08-15). ⚠️ One per cause,
|
|
1001
|
+
# do NOT merge them: this used to be a single "history only" line, which is
|
|
1002
|
+
# a concrete falsehood for the other three. Every line has to carry the
|
|
1003
|
+
# next step — see `READONLY_KEY` in `composer/index.tsx`
|
|
1004
|
+
#
|
|
1005
|
+
# This one matters most: it is neither a history session nor one left over
|
|
1006
|
+
# by a previous process — it is a freshly created session on a machine that
|
|
1007
|
+
# is not configured yet. So the line also has to block the wrong next step
|
|
1008
|
+
# (opening another session helps nothing: with no provider there is no live
|
|
1009
|
+
# session anywhere). Wording matches the server's 503 for session creation
|
|
1010
|
+
# (`web.session_unavailable` above) — same cause, likely seen both ways
|
|
1011
|
+
placeholder_no_provider: 'No model provider is available on this machine — another session will not help; configure an API key (or run epoch model)'
|
|
1012
|
+
# Left over from an earlier process AND unable to say where it worked
|
|
1013
|
+
# (rows created before session store v7). Since 2026-08-19 "left over from
|
|
1014
|
+
# an earlier process" alone no longer lands here: when the workspace
|
|
1015
|
+
# decision was persisted, sending one message revives it server-side and
|
|
1016
|
+
# the composer stays writable (rationale in web's `state/read-only.ts`).
|
|
1017
|
+
# The wording narrowed with it — "history session, read only" would now be
|
|
1018
|
+
# a concrete lie for the majority that can be resumed.
|
|
1019
|
+
placeholder_history: 'This conversation cannot say where it used to work, so it cannot be picked back up — read it, or start a new one'
|
|
1020
|
+
# The session is gone server-side. The exit lives on the banner, not here —
|
|
1021
|
+
# this line only has to stop claiming it is a history session
|
|
1022
|
+
placeholder_gone: 'This conversation is gone on the server; there is nothing to send to'
|
|
1023
|
+
# The only hint beside the button, and it only shows while a turn is running
|
|
1024
|
+
# (decision 22 ②: the hint changes, the button never does). The idle one used
|
|
1025
|
+
# to read 'to send' right next to a button labelled Send; `placeholder_idle`
|
|
1026
|
+
# above already says Enter sends, so it went away on 2026-08-18
|
|
1027
|
+
hint_queue: 'to queue'
|
|
1028
|
+
send: 'Send'
|
|
1029
|
+
# Slash-command completion panel. This is the listbox's accessible name —
|
|
1030
|
+
# screen readers announce it; nothing on screen shows it
|
|
1031
|
+
commands_label: 'Custom slash commands'
|
|
1032
|
+
# Heading above the expansion warnings. The warnings themselves are NOT
|
|
1033
|
+
# here: they are engine text that varies with the user's own command file,
|
|
1034
|
+
# so there is no finite set of codes to translate. See
|
|
1035
|
+
# `WireCommandExpansion.warnings`
|
|
1036
|
+
command_warnings: 'A few things to know about how /{name} expanded:'
|
|
1037
|
+
|
|
1038
|
+
attach: 'Add an image'
|
|
1039
|
+
remove_attach: 'Remove {name}'
|
|
1040
|
+
# The drop zone. Says what letting go will do, not that you are dragging —
|
|
1041
|
+
# the user already knows the latter, they are the one doing it
|
|
1042
|
+
drop_hint: 'Drop to add the image to this message'
|
|
1043
|
+
# Placeholder while the file is being read. It sits inside a chip next to
|
|
1044
|
+
# the file name, so it has to stay short
|
|
1045
|
+
reading: 'reading…'
|
|
1046
|
+
# Manual dismissal for the rejected list. Deliberately NOT a toast that
|
|
1047
|
+
# disappears after three seconds: the user is mid-sentence, and an error
|
|
1048
|
+
# that vanishes half-read is an error that was never said
|
|
1049
|
+
reject_dismiss: 'Dismiss these'
|
|
1050
|
+
no_workspace: 'No workspace'
|
|
1051
|
+
# The third state (2026-08-15): **we asked, and this process says it cannot tell**.
|
|
1052
|
+
# ⚠️ Do not merge it with the line above — "No workspace" is a definite
|
|
1053
|
+
# statement, and the bug this cell had for a whole round was exactly that:
|
|
1054
|
+
# stating something definite (the bootstrap session's directory) when it did
|
|
1055
|
+
# not know. There is only one cause: the session was left over from an earlier
|
|
1056
|
+
# process and this process has not picked it back up (the server answers 409
|
|
1057
|
+
# `not-live-session`). "Not asked yet" is not this state — then the cell is
|
|
1058
|
+
# not drawn at all.
|
|
1059
|
+
#
|
|
1060
|
+
# ⚠️ 2026-08-19 the wording changed: the old line claimed "workspace bindings
|
|
1061
|
+
# are not persisted", which is now false. Session DB v7 stores the user's
|
|
1062
|
+
# original decision, and the sidebar puts the session back in its own space.
|
|
1063
|
+
# What is not persisted is the binding itself — it runs the trust gate and
|
|
1064
|
+
# only happens when this process actually picks the session back up — so this
|
|
1065
|
+
# cell still cannot say what it is bound to *right now*. ⚠️ Do not rewire this
|
|
1066
|
+
# cell to read the stored decision: it renders trust state and the instruction
|
|
1067
|
+
# file list right next to it, and neither has been computed yet.
|
|
1068
|
+
workspace_unknown: 'Workspace unknown'
|
|
1069
|
+
workspace_unknown_hint: 'This session was left over from an earlier process and has not been picked back up here — so this process cannot say what it is bound to right now'
|
|
1070
|
+
# 2026-08-16 this cell became the door to the workspace-diff screen (plan 30
|
|
1071
|
+
# PR-3). ⚠️ It sits on its own line **after the absolute path**, so it does not
|
|
1072
|
+
# repeat the path — it only names the destination. A hand cursor cannot say what
|
|
1073
|
+
# clicking does, and a second ago this cell was just a path.
|
|
1074
|
+
workspace_diff_hint: 'Open to see what has changed in this directory'
|
|
1075
|
+
|
|
1076
|
+
# The fourth state (plan 55 PR-1, 2026-08-17): **created, no directory picked
|
|
1077
|
+
# yet** — this cell draws a picker, exactly where decision 18 puts it.
|
|
1078
|
+
#
|
|
1079
|
+
# ⚠️ Four of the five lines below **moved here from `web.ui.newsession.*`**
|
|
1080
|
+
# (the picker moved from the sidebar row to this cell); the wording is
|
|
1081
|
+
# unchanged. Moving the keys rather than referencing them in place: a key's
|
|
1082
|
+
# prefix in this repo says **where it is drawn**, and someone editing these
|
|
1083
|
+
# lines would go looking for a sidebar menu that no longer exists.
|
|
1084
|
+
#
|
|
1085
|
+
# The trigger. **Says "pick", not "none picked"**: the other three states in
|
|
1086
|
+
# this cell state facts, this one is an action, and the verb is the clearest
|
|
1087
|
+
# difference between them.
|
|
1088
|
+
workspace_pick: 'Pick a workspace'
|
|
1089
|
+
# The menu has no visible title; this is the name a screen reader announces.
|
|
1090
|
+
workspace_pick_label: 'Which workspace this session works in'
|
|
1091
|
+
# Decision 7: "two things at once in the same repository is the norm".
|
|
1092
|
+
workspace_here_current: '{name} (current)'
|
|
1093
|
+
# The detail line of the "no workspace" item. Its title reuses `no_workspace`
|
|
1094
|
+
# above — the menu item and the text this cell shows afterwards must match
|
|
1095
|
+
# word for word.
|
|
1096
|
+
#
|
|
1097
|
+
# ⚠️ This corrects the design draft's blank-screen line ("you can still chat
|
|
1098
|
+
# without picking one; to touch files pick one below"), which is false: with
|
|
1099
|
+
# no binding the AgentLoop runs in the workDir captured at assembly time
|
|
1100
|
+
# (runtime/src/session-factory.ts). No workspace is not a sandbox that cannot
|
|
1101
|
+
# touch files.
|
|
1102
|
+
#
|
|
1103
|
+
# The second sentence is new in 2026-08-17: this choice **is final** (the
|
|
1104
|
+
# server answers 409 for `none`, same as an already-bound directory). Without
|
|
1105
|
+
# it users would read this as a setting they can revisit.
|
|
1106
|
+
workspace_none_detail: 'Runs in the directory the server process started in — not a sandbox that cannot touch files. This choice is final.'
|
|
1107
|
+
# Where the list comes from and where it ends. "New workspace" and "Open a
|
|
1108
|
+
# local folder" are in the draft but not in PR-1 (plan 55 PR-2 / PR-3).
|
|
1109
|
+
workspace_pick_note: 'The list is the directories recently used on this machine. To start a session somewhere new, run epoch there once first.'
|
|
1110
|
+
# "Open a local folder…" (plan 55 PR-2, 2026-08-17) — the draft menu's 4th item.
|
|
1111
|
+
#
|
|
1112
|
+
# ⚠️ The previous round ruled this item out as "no exit for this shape in a
|
|
1113
|
+
# browser", because the browser cannot produce a server-side absolute path.
|
|
1114
|
+
# **The first half is right; the second half conflated "let the browser pick"
|
|
1115
|
+
# with "let the server tell the browser which directories exist"** — the latter
|
|
1116
|
+
# is an ordinary read endpoint.
|
|
1117
|
+
#
|
|
1118
|
+
# The detail line names **which machine** it lists. That is not filler under
|
|
1119
|
+
# `--host 0.0.0.0`: the machine in front of the user and the one running the
|
|
1120
|
+
# agent need not be the same one.
|
|
1121
|
+
workspace_browse: 'Open a local folder…'
|
|
1122
|
+
workspace_browse_detail: 'Walk down to a directory, or type an absolute path (lists directories on the machine running epoch)'
|
|
1123
|
+
permission: '{level} permissions'
|
|
1124
|
+
|
|
1125
|
+
# The plan-mode switch (the web entry point for plan 35, 2026-08-15).
|
|
1126
|
+
# ⚠️ `plan_mode_why` is the only full sentence on this row, and the question
|
|
1127
|
+
# it has to answer is "why can't I write files" — a chip that only says
|
|
1128
|
+
# "Plan mode" cannot answer it
|
|
1129
|
+
plan_mode: 'Plan mode'
|
|
1130
|
+
plan_on_hint: 'Enter plan mode: the model researches read-only and hands you a plan to approve; it touches no files meanwhile'
|
|
1131
|
+
plan_off_hint: 'Leave plan mode and go back to the permission level you were on'
|
|
1132
|
+
plan_mode_why: 'Read-only: the model can look and search but cannot change files; you get an approval when it hands in the plan'
|
|
1133
|
+
reject_not_image: '{name} is not an image; this round only takes images'
|
|
1134
|
+
reject_too_big: '{name} is {size} MB, over the {max} MB limit'
|
|
1135
|
+
reject_unreadable: '{name} could not be read'
|
|
1136
|
+
# A folder was dropped. "Not an image" would be a true statement that sends
|
|
1137
|
+
# the user the wrong way — told about a `shots/` full of screenshots it reads
|
|
1138
|
+
# as "images are not accepted here". Only the drop layer knows (see drop.tsx)
|
|
1139
|
+
reject_directory: '{name} is a folder — open it and drag the images inside'
|
|
1140
|
+
# This message is too big to send. The limit is not the UI's number: it is the
|
|
1141
|
+
# server's request-body limit, and going over it destroys the connection there.
|
|
1142
|
+
# So this has to be said *before* send is pressed, with both exits named
|
|
1143
|
+
too_big: 'This message is {size}; one message carries at most {max}. Drop an image, or shorten the pasted text'
|
|
1144
|
+
|
|
1145
|
+
# The identity chip on that row (decision 20 ② / plan 57 §3.6, 2026-08-17).
|
|
1146
|
+
#
|
|
1147
|
+
# The chip prints the identity name itself and nothing else, so both of these
|
|
1148
|
+
# are the half it cannot say out loud: what it is, and how to cancel it.
|
|
1149
|
+
#
|
|
1150
|
+
# ⚠️ Both carry `{role}`: a screen-reader user walking the control list only
|
|
1151
|
+
# hears "remove", and cannot tell this chip from an attachment chip next to it.
|
|
1152
|
+
#
|
|
1153
|
+
# `expert_chip_title` must say **every following message**, not "this one" —
|
|
1154
|
+
# that is the one semantic difference between this chip and every other chip
|
|
1155
|
+
# in the app (it sticks until the × is pressed).
|
|
1156
|
+
expert_chip_title: 'Every following message is answered by "{role}" — press the × on the left to cancel'
|
|
1157
|
+
# The **session-level** identity cell on the composer footer (2026-08-18).
|
|
1158
|
+
# ⚠️ Different thing from the chip above; this title exists to say so —
|
|
1159
|
+
# both render as just a name, and without it people look for an × here
|
|
1160
|
+
# (there is none: the identity cannot be changed mid-session).
|
|
1161
|
+
identity_title: 'This session runs as "{role}". It was set when the session was created and cannot be changed.'
|
|
1162
|
+
expert_remove: 'Stop letting "{role}" take over'
|
|
1163
|
+
|
|
1164
|
+
# The **permission-level** menu on the row outside the box (decision 20 ③, 2026-08-15).
|
|
1165
|
+
#
|
|
1166
|
+
# ⚠️ Its own `perm` group rather than folded into `composer` above: these lines
|
|
1167
|
+
# belong to the **permission model itself** (the five-level table in
|
|
1168
|
+
# `core/src/permission/by-level.ts`), not to the composer. If the settings-page
|
|
1169
|
+
# segmented control ever gets a write endpoint it uses the same sentences —
|
|
1170
|
+
# the design mock uses one wording in both places, and two copies drifting
|
|
1171
|
+
# apart shows up as one level described two different ways on two screens.
|
|
1172
|
+
#
|
|
1173
|
+
# ⚠️ **The raw value still prints on the chip** (`composer.permission`); these
|
|
1174
|
+
# are the sentences **inside the menu**. No overlap: the chip carries the word
|
|
1175
|
+
# the user would type after `--permission`, the menu says what it opens up.
|
|
1176
|
+
perm:
|
|
1177
|
+
# The menu's accessible name. Screen readers announce it; it is not on screen
|
|
1178
|
+
menu_label: 'Permission level'
|
|
1179
|
+
# The five levels and their one-line explanations. **The first four are taken
|
|
1180
|
+
# verbatim from the design mock** (design/web-ui/index.html, #permMenu);
|
|
1181
|
+
# `auto` follows the table in core/src/permission/by-level.ts — the mock has
|
|
1182
|
+
# no such entry, and why five are drawn here is argued in the file header of
|
|
1183
|
+
# composer/permission-menu.tsx
|
|
1184
|
+
lvl_plan: 'Read-only'
|
|
1185
|
+
lvl_plan_detail: 'It can look and think. To touch files or run commands it hands you a plan first'
|
|
1186
|
+
lvl_default: 'Ask when needed'
|
|
1187
|
+
lvl_default_detail: 'Anything the rule lists mark ask gets asked, one by one. The default'
|
|
1188
|
+
lvl_accept_edits: 'Auto-accept edits'
|
|
1189
|
+
lvl_accept_edits_detail: 'File edits stop asking; commands still ask'
|
|
1190
|
+
# ⚠️ This line has to name the actual difference from the level above: not
|
|
1191
|
+
# "a bit more automatic" but **files outside the workspace and the network
|
|
1192
|
+
# open up too**, leaving only non-read-only commands behind a prompt
|
|
1193
|
+
lvl_auto: 'Trust its judgement'
|
|
1194
|
+
lvl_auto_detail: 'Reading, writing and network stop asking, outside the workspace too; commands still ask for the ones that change things'
|
|
1195
|
+
# ⚠️ "the whole rail turns red, deliberately uncomfortable" is the mock's own
|
|
1196
|
+
# wording — **do not soften it**: it is this level's only brake, and getting
|
|
1197
|
+
# this level wrong is expensive
|
|
1198
|
+
lvl_bypass: 'Skip every check'
|
|
1199
|
+
lvl_bypass_detail: 'Nothing is ever asked. The whole rail turns red, deliberately uncomfortable'
|
|
1200
|
+
# That level does not exist on this machine (managed `disableBypassPermissionsMode`).
|
|
1201
|
+
#
|
|
1202
|
+
# ⚠️ This sentence **must give both the why and where to look**, per the mock's
|
|
1203
|
+
# `.btn--pick[data-locked]` note: greying an item only says "not clickable",
|
|
1204
|
+
# and the why comes from a machine-level managed-settings.json the user cannot
|
|
1205
|
+
# find anywhere else in the UI. Where that file lives and which other switches
|
|
1206
|
+
# it turns on belong to the security centre screen — so this points there
|
|
1207
|
+
# instead of reciting them inside a 12px caption
|
|
1208
|
+
blocked_managed: 'This machine has enterprise managed settings, and they turn off the "Skip every check" level. See Settings › Security centre for details'
|
|
1209
|
+
|
|
1210
|
+
fatal:
|
|
1211
|
+
unauthorized: 'No access credential'
|
|
1212
|
+
unauthorized_hint: 'Go back to the terminal running epoch web and reopen the tokenised URL it printed.'
|
|
1213
|
+
offline: 'Cannot reach the local service'
|
|
1214
|
+
|
|
1215
|
+
settings:
|
|
1216
|
+
title: 'Settings'
|
|
1217
|
+
back: 'Back to session'
|
|
1218
|
+
security: 'Security'
|
|
1219
|
+
pane_model: 'Model & provider'
|
|
1220
|
+
pane_memory: 'Memory & context'
|
|
1221
|
+
pane_budget: 'Budget'
|
|
1222
|
+
# The bodies of these two panes are `web.ui.tools.*` / `web.ui.about.*` below.
|
|
1223
|
+
# "Tools" sits right after the security centre (`PANES` in
|
|
1224
|
+
# `settings/index.tsx`): that screen is the rules themselves, this one is what
|
|
1225
|
+
# those rules add up to for each individual tool right now
|
|
1226
|
+
pane_tools: 'Tools'
|
|
1227
|
+
pane_about: 'About'
|
|
1228
|
+
appearance: 'Appearance'
|
|
1229
|
+
appearance_lede: 'Affects this browser on this machine only. It is not written to config.yaml and does not follow an account — we have no accounts.'
|
|
1230
|
+
# ⚠️ 2026-08-18 trimmed — see the zh comment for the ledger.
|
|
1231
|
+
appearance_note: 'Light and dark only. Colour carries meaning in this design (state, syntax, additions and deletions, permissions), so it is not user-configurable.'
|
|
1232
|
+
theme: 'Theme'
|
|
1233
|
+
theme_field: 'Colour scheme'
|
|
1234
|
+
theme_system: 'Follow system'
|
|
1235
|
+
theme_light: 'Light'
|
|
1236
|
+
theme_dark: 'Dark'
|
|
1237
|
+
theme_hint: 'When following the system it tracks prefers-color-scheme live, no reload needed. The system is currently {now}.'
|
|
1238
|
+
lang: 'Language'
|
|
1239
|
+
lang_field: 'Interface language'
|
|
1240
|
+
lang_system: 'Follow browser'
|
|
1241
|
+
lang_zh: '中文'
|
|
1242
|
+
lang_en: 'English'
|
|
1243
|
+
lang_hint: 'This only switches the frontend own strings. Diagnostics and messages returned by the server follow the language of the epoch web process, so they stay in Chinese after you switch to English.'
|
|
1244
|
+
|
|
1245
|
+
# Setting provenance and editing (plan 43 §7): the model & provider /
|
|
1246
|
+
# memory & context / budget panes.
|
|
1247
|
+
#
|
|
1248
|
+
# ⚠️ The rule for this whole block used to be **do not promise it can be
|
|
1249
|
+
# edited** (there was not one write control). Since 2026-08-15 the model and
|
|
1250
|
+
# maxTurns rows really are editable, so the rule is now two:
|
|
1251
|
+
#
|
|
1252
|
+
# 1. The rows that still cannot be edited keep pointing at where to change
|
|
1253
|
+
# them rather than at a control on this page — they are the vast majority
|
|
1254
|
+
# (SettingsFileSchema allows only three keys).
|
|
1255
|
+
# 2. For the two that can, every sentence has to state the consequence:
|
|
1256
|
+
# whether the write will be overridden, when it takes effect, and which
|
|
1257
|
+
# value this process is still using. Drop any one of those and the reader
|
|
1258
|
+
# concludes that "saved" means "that is the value now", which is false.
|
|
1259
|
+
set:
|
|
1260
|
+
no_session: 'No session yet. The two project-level settings layers follow the workspace, and the workspace is bound per session — with no session there is no "which .epoch/settings.json" to answer.'
|
|
1261
|
+
loading: 'Reading setting provenance…'
|
|
1262
|
+
unset: 'Not set'
|
|
1263
|
+
why: 'Why this value'
|
|
1264
|
+
chain_win: 'wins'
|
|
1265
|
+
g_other: 'Other'
|
|
1266
|
+
# "There is a list of keys a project can override" **has to be on screen**.
|
|
1267
|
+
# Without it, a reader sees that other rows have no chain and concludes
|
|
1268
|
+
# "that row is not built yet", then writes a key into .epoch/settings.json
|
|
1269
|
+
# that will never take effect.
|
|
1270
|
+
overridable: 'Only these keys can be overridden by the project, project-local, command-line or managed layers: {keys}. Every other setting has just two layers, built-in and user — putting one in .epoch/settings.json does nothing, and startup diagnostics will report it as an unknown config key.'
|
|
1271
|
+
overridable_perm: 'The permissions entry on that list is three lists (allow / ask / deny) merged as a union rather than an override, so its provenance is per rule — see Security › Permission rules.'
|
|
1272
|
+
# The managed-lock line **says why it cannot be changed** rather than
|
|
1273
|
+
# greying a control out: greying only says "you cannot click this", and the
|
|
1274
|
+
# why is not discoverable anywhere else — it comes from a machine-level file.
|
|
1275
|
+
managed_locked: 'This one is set by the managed settings on this machine; changing it at the user or project level has no effect. That file is not writable by an ordinary user — ask an administrator.'
|
|
1276
|
+
|
|
1277
|
+
# ---- Editing a row (the write half of plan 43 §7) ----
|
|
1278
|
+
#
|
|
1279
|
+
# Only the model and maxTurns rows draw these controls; every other row
|
|
1280
|
+
# does not even get the "Edit" button.
|
|
1281
|
+
edit: 'Edit'
|
|
1282
|
+
cancel: 'Cancel'
|
|
1283
|
+
save: 'Save'
|
|
1284
|
+
new_value: 'New value'
|
|
1285
|
+
write_layer: 'Write to'
|
|
1286
|
+
# The dirty chip. It is the entire cost of the "nothing counts until you
|
|
1287
|
+
# press save" rule — without it a reader assumes what they typed is already
|
|
1288
|
+
# on disk.
|
|
1289
|
+
dirty: 'Unsaved'
|
|
1290
|
+
# ⚠️ These two are the most important sentences on this screen, and they are
|
|
1291
|
+
# on it **before** save is pressed. Saying "that last one got overridden"
|
|
1292
|
+
# afterwards is correctly ordered and completely useless.
|
|
1293
|
+
# The two are worded separately because the next step differs: one is to
|
|
1294
|
+
# write a higher layer, the other is to trust the directory.
|
|
1295
|
+
futile_shadowed: 'Writing to {layer} will have no effect: {by} sets this too, and it ranks higher, so on load it overrides what you write here. To make it take effect, write to {by} or above.'
|
|
1296
|
+
futile_untrusted: 'This workspace is not trusted yet, so settings in {layer} are dropped wholesale on load (deny rules are the one exception) — writing there does nothing. Trust the directory under Security › Workspace & trust first, or write to the user layer instead: it does not go through the trust gate.'
|
|
1297
|
+
# The three sentences of the receipt. The second one (what this process is
|
|
1298
|
+
# still using) cannot be dropped: the row above still prints the old value,
|
|
1299
|
+
# and without it the reader thinks the UI failed to refresh.
|
|
1300
|
+
wrote: 'Written to {layer}: {value}'
|
|
1301
|
+
wrote_stale: 'This process read the config once at startup and never re-reads it, so it is still using {value} — restarting epoch web is what switches it to the value you just wrote. Starting a new session will not do it either; the config is one per process.'
|
|
1302
|
+
|
|
1303
|
+
# ---- Model & provider ----
|
|
1304
|
+
# ⚠️ 2026-08-18 trimmed. See the zh comment: two of the three sentences
|
|
1305
|
+
# explained how this screen draws itself. The source-layer badge on each row
|
|
1306
|
+
# says the same thing without a sentence pointing at it.
|
|
1307
|
+
lede_model: 'The model and provider this process is using right now. To change: edit ~/.epoch/config.yaml, or run epoch model.'
|
|
1308
|
+
k_model: 'Main model'
|
|
1309
|
+
k_models_utility: 'Utility model'
|
|
1310
|
+
h_models_utility: 'Used for chores like compacting context and titling sessions. Unset means everything goes to the main model.'
|
|
1311
|
+
k_fallback_model: 'Fallback model'
|
|
1312
|
+
k_fallback_providers: 'Fallback chain'
|
|
1313
|
+
h_fallback_providers: 'Tried in this order when the main provider is unreachable.'
|
|
1314
|
+
|
|
1315
|
+
# ---- Memory & context ----
|
|
1316
|
+
lede_memory: 'The first two decide how much fits in a turn and how many steps it may take; the last two decide what happens when it no longer fits. Setting a context window larger than the model really has buys you a 400.'
|
|
1317
|
+
k_context_length: 'Context window'
|
|
1318
|
+
# ⚠️ Reworded 2026-08-17: the old line ("a configured value wins") had it
|
|
1319
|
+
# backwards. Assembly uses `getCapability(config.model).contextLength`;
|
|
1320
|
+
# this row only takes over when that lookup throws (the catch arm of
|
|
1321
|
+
# runtime's `resolveModelCapability()`). Read literally, the old line told
|
|
1322
|
+
# people they could raise this number to compress less often — it never
|
|
1323
|
+
# takes effect, and the compression line below has always been computed
|
|
1324
|
+
# from the window the engine actually compares against, not from this row.
|
|
1325
|
+
h_context_length: 'Only used as a fallback when the model metadata cannot be read; when it can, metadata wins and a configured value has no effect. The compression line below uses the window the engine actually runs on.'
|
|
1326
|
+
k_max_turns: 'Max steps per turn'
|
|
1327
|
+
h_max_turns: 'How many tool calls the model may make in one turn. On reaching the cap it stops and finishes its answer.'
|
|
1328
|
+
k_compression_enabled: 'Compact when nearly full'
|
|
1329
|
+
h_compression_enabled: 'Compaction rewrites earlier messages into a summary; the originals stay in the session store and the model can search them back. A note is left in the thread afterwards.'
|
|
1330
|
+
k_compression_threshold: 'Compact at'
|
|
1331
|
+
h_compression_threshold: 'Compaction starts once the prompt reaches this fraction of the context window. Between 0 and 1.'
|
|
1332
|
+
|
|
1333
|
+
# The auto-compaction block (`settings/context-memory.tsx`).
|
|
1334
|
+
# ⚠️ Same rule as the security centre: state the consequence, not the status word.
|
|
1335
|
+
ctx_line: 'With this configuration, compaction starts once the prompt reaches {tokens} tokens — {percent}% of the {window}-token context window.'
|
|
1336
|
+
ctx_line_caveat: 'That is the line itself. Just after a compaction, or once several compactions in a row have failed, crossing it will not compact yet.'
|
|
1337
|
+
ctx_off: 'Auto-compaction is off. When a long session hits the context window there is no automatic rescue — that turn simply fails, and the context does not shrink on its own. Manual /compact is unaffected by this switch and still works.'
|
|
1338
|
+
ctx_no_switch: 'These two rows cannot be changed here, same as the two above: only three keys can be overridden by project-level config (the list below), and compaction is not one of them — otherwise a repository could quietly let long sessions opened inside it run into the window. Change it by editing ~/.epoch/config.yaml yourself:'
|
|
1339
|
+
ctx_restart: 'Restart the epoch web process for this to take effect — it reads the config once at startup and never re-reads it.'
|
|
1340
|
+
|
|
1341
|
+
# ---- Budget ----
|
|
1342
|
+
lede_budget: 'Amounts only appear in the UI once a cap is configured. With no cap these rows read "Not set", which is also the default.'
|
|
1343
|
+
k_budget_max_cost: 'Spend cap'
|
|
1344
|
+
k_budget_max_tokens: 'Token cap'
|
|
1345
|
+
h_budget_max_tokens: 'With both rulers configured, the gate reports whichever is tighter.'
|
|
1346
|
+
k_budget_warn_at: 'Warn at'
|
|
1347
|
+
h_budget_warn_at: 'Percent. Warns once, on crossing — not every turn.'
|
|
1348
|
+
k_budget_unknown_pricing: 'When the model has no pricing data'
|
|
1349
|
+
h_budget_unknown_pricing: 'block stops the call, warn lets it through with a warning. With no pricing data the cost is "unknown", not 0, and the two must be handled separately.'
|
|
1350
|
+
|
|
1351
|
+
# The security centre (plan 42 PR-3 / decision 16): sandbox + permissions +
|
|
1352
|
+
# workspace trust + policy, on one screen.
|
|
1353
|
+
#
|
|
1354
|
+
# ⚠️ The rule for this whole block is **state the consequence, not the
|
|
1355
|
+
# status word**. "Untrusted" tells a reader nothing; "nothing in its .epoch/
|
|
1356
|
+
# is read" does. The reason this screen exists is to unfold the four
|
|
1357
|
+
# heaviest, least-visible subsystems — unfolding them into jargon is not
|
|
1358
|
+
# unfolding them.
|
|
1359
|
+
sec:
|
|
1360
|
+
# ⚠️ Reworded 2026-08-17. The old line claimed every item here is changed
|
|
1361
|
+
# in a config file or on the command line — the permission mode is not.
|
|
1362
|
+
# Its control sits on the composer row of this very UI
|
|
1363
|
+
# (`composer/permission-menu.tsx`, decision 20 ③). Taken literally, the
|
|
1364
|
+
# old line sent people to `config.yaml`, where the key decides which mode
|
|
1365
|
+
# the *next* session starts in, not the one in front of them.
|
|
1366
|
+
# ⚠️ 2026-08-18 trimmed. The old text spent two sentences on this page's
|
|
1367
|
+
# read/write nature and its own index. Someone opening "Security" wants to
|
|
1368
|
+
# know what the agent can touch. The 2026-08-17 note above still holds
|
|
1369
|
+
# (the permission mode's entry point is special) — it now lives in
|
|
1370
|
+
# `perm_where`, right next to that field.
|
|
1371
|
+
lede: 'What this agent can touch right now, and what is holding it back.'
|
|
1372
|
+
no_session: 'No session yet. Three things here are bound per session: the workspace, its trust verdict, and the project-level policy directory that follows that trust — with no session there is no "which one" to answer.'
|
|
1373
|
+
loading: 'Reading security status…'
|
|
1374
|
+
|
|
1375
|
+
# ---- The four status cards ----
|
|
1376
|
+
card_sandbox: 'Sandbox'
|
|
1377
|
+
card_perm: 'Permission mode'
|
|
1378
|
+
card_space: 'Workspaces'
|
|
1379
|
+
card_rules: 'Active rules'
|
|
1380
|
+
perm_d_managed: 'Locked by the managed layer; it cannot be changed on this machine.'
|
|
1381
|
+
# ⚠️ Reworded 2026-08-17. "The result of merging every config layer" only
|
|
1382
|
+
# held while nobody had touched the mode since the session opened. After
|
|
1383
|
+
# one switch — or one trip in and out of plan mode — it pinned a config
|
|
1384
|
+
# provenance onto a *runtime* value, and `settings/security-perm.tsx`
|
|
1385
|
+
# item 3 says verbatim that this is worse than pinning none.
|
|
1386
|
+
perm_d_local: 'The mode this session is running under right now — not the one written on disk.'
|
|
1387
|
+
space_none: 'None known'
|
|
1388
|
+
space_all_trusted: '{count} · all trusted'
|
|
1389
|
+
space_untrusted: '{count} · {untrusted} untrusted'
|
|
1390
|
+
space_at: 'Currently bound to {root}'
|
|
1391
|
+
space_unbound: 'This session has no workspace.'
|
|
1392
|
+
rules_n: '{count} permission · {policy} policy'
|
|
1393
|
+
rules_none: 'None at all'
|
|
1394
|
+
|
|
1395
|
+
# ---- Sandbox ----
|
|
1396
|
+
g_sandbox: 'Sandbox'
|
|
1397
|
+
sb_field: 'Isolation backend'
|
|
1398
|
+
sb_os: 'OS-level isolation ({backend})'
|
|
1399
|
+
sb_process: 'Process isolation only ({platform})'
|
|
1400
|
+
# ⚠️ When probing fails, **say probing failed**. Writing "enabled" here is
|
|
1401
|
+
# the most expensive lie on this screen
|
|
1402
|
+
sb_absent: 'Not probed'
|
|
1403
|
+
sb_absent_d: 'The isolation layer itself did not come up, so it cannot even report what it found. This is not the same as "process isolation only".'
|
|
1404
|
+
sb_absent_long: 'The isolation layer did not come up, so it cannot even report what it found. This is not the same as "process isolation only" — that is a known conclusion, and this is an unknown.'
|
|
1405
|
+
# The last three are **three different next steps**. `backend-missing`
|
|
1406
|
+
# deliberately does not interpolate {backend}: its value there is none,
|
|
1407
|
+
# and "install none" would send the reader down a hole
|
|
1408
|
+
sb_os_isolated: '{backend} on {platform} really does hold it in; there is nothing more to explain here.'
|
|
1409
|
+
sb_backend_unavailable: '{platform} should have an implementation, but the probe failed — the backend is there and will not start.'
|
|
1410
|
+
sb_backend_missing: '{platform} has an implementation but the dependency is missing: Linux needs bubblewrap, or the kernel may have disabled unprivileged user namespaces.'
|
|
1411
|
+
sb_platform_unsupported: '{platform} has no implementation at all (Windows would need AppContainer and Node has no binding), so installing anything will not help.'
|
|
1412
|
+
sb_covers: 'Tools that go through the sandbox'
|
|
1413
|
+
# ⚠️ `sb_covers_hint` dropped 2026-08-18. It explained that the list comes
|
|
1414
|
+
# from the server rather than being hardcoded in the browser — a note for
|
|
1415
|
+
# whoever maintains this screen, not for whoever reads it. The rule now
|
|
1416
|
+
# lives in the comments of settings/security-sandbox.tsx.
|
|
1417
|
+
sb_covers_none: 'No tool goes through the isolation layer.'
|
|
1418
|
+
sb_excludes: 'Explicitly outside the sandbox'
|
|
1419
|
+
sb_excludes_none: 'No tool is called out as being outside.'
|
|
1420
|
+
# ⚠️ 2026-08-18 trimmed — see the zh comment for the ledger.
|
|
1421
|
+
sb_excludes_why: 'The terminal does not go through the sandbox — the dangerous-command table and the permission gate hold it back instead. "Sandbox on" does not cover it.'
|
|
1422
|
+
|
|
1423
|
+
# ---- Permission mode ----
|
|
1424
|
+
g_perm: 'Permission mode'
|
|
1425
|
+
perm_field: 'Current mode'
|
|
1426
|
+
perm_unknown: 'unreadable'
|
|
1427
|
+
# ⚠️ 2026-08-18 trimmed; the "who overrides whom" half moved to the rule
|
|
1428
|
+
# table's own lede, where it is that table's first fact rather than this
|
|
1429
|
+
# field's second one.
|
|
1430
|
+
perm_hint: 'What to do by default when in doubt.'
|
|
1431
|
+
# ⚠️ Rewritten 2026-08-17 (plan 56 §1.3). The previous wording was "merging
|
|
1432
|
+
# the config layers discards provenance and leaves only a value" — **that is
|
|
1433
|
+
# now false**: the source layer is available, and the "Default" row below
|
|
1434
|
+
# carries it. The real reason was always a different one; here it is.
|
|
1435
|
+
# ⚠️ 2026-08-18: dropped. It explained why a row has *no* badge — a question
|
|
1436
|
+
# nobody on screen was asking. The rule it protected (never hang a config
|
|
1437
|
+
# source layer on the runtime row) now lives in the comments of
|
|
1438
|
+
# `settings/security-perm.tsx`.
|
|
1439
|
+
managed_field: 'Managed settings'
|
|
1440
|
+
managed_on: 'This machine has a managed settings file, and it overrides every other layer.'
|
|
1441
|
+
# **`present: false` is the norm**, so this branch is a flat statement
|
|
1442
|
+
# rather than a warning — a UI that defaults to looking "locked by IT"
|
|
1443
|
+
# makes readers think the mode cannot be changed at all
|
|
1444
|
+
managed_off: 'This machine has no managed settings. That is the norm.'
|
|
1445
|
+
managed_rules_only: 'The managed layer turned on "managed permission rules only": the three lists from every other layer are discarded wholesale.'
|
|
1446
|
+
managed_bypass_off: 'The managed layer disabled the "skip all checks" mode.'
|
|
1447
|
+
|
|
1448
|
+
# ---- Permission rules ----
|
|
1449
|
+
g_rules: 'Permission rules'
|
|
1450
|
+
rules_empty: 'There are no permission rules at all; every decision falls through to the mode above.'
|
|
1451
|
+
rules_no_exec: 'An allow rule that permits arbitrary code execution (terminal(python:*) and the like) is rejected with a warning at load time: it would route around the entire permission system.'
|
|
1452
|
+
shadow_lede: 'Some rules are covered by others:'
|
|
1453
|
+
shadow_shadowed: '{rule} never takes effect — {by} covers it entirely.'
|
|
1454
|
+
shadow_partial: '{rule} has a hole punched in it by {by}. This is often deliberate.'
|
|
1455
|
+
shadow_redundant: '{rule} adds nothing — {by} in the same list is already broader.'
|
|
1456
|
+
# Layer numbers copied verbatim from core/src/config/sources.ts
|
|
1457
|
+
layer_builtin: '① built-in'
|
|
1458
|
+
layer_plugin: '①ᐟ⁵ plugin'
|
|
1459
|
+
layer_user: '② user'
|
|
1460
|
+
# Environment variables are **not** in the layer table in the docs, because
|
|
1461
|
+
# they are not a settings layer: they override config.yaml during config
|
|
1462
|
+
# loading, in the same slot as the user level — so project level beats them.
|
|
1463
|
+
layer_env: 'environment'
|
|
1464
|
+
layer_project: '③ project'
|
|
1465
|
+
layer_project_local: '④ project-local'
|
|
1466
|
+
layer_cli: '⑤ command line'
|
|
1467
|
+
layer_managed: '∞ managed'
|
|
1468
|
+
# ⚠️ Both catalogs hold the same value here on purpose: `unknown` is not
|
|
1469
|
+
# "could not find it", it has an exact meaning — the rule is written in
|
|
1470
|
+
# config.yaml itself and belongs to no settings layer. Printing the
|
|
1471
|
+
# filename is far more useful than "unknown"; opening it is the next step
|
|
1472
|
+
layer_unknown: 'config.yaml'
|
|
1473
|
+
|
|
1474
|
+
# ---- Workspace and trust ----
|
|
1475
|
+
g_space: 'Workspace and trust'
|
|
1476
|
+
known_field: 'Known workspaces'
|
|
1477
|
+
known_empty: 'No directory has been asked about trust yet.'
|
|
1478
|
+
known_hint: 'Trust follows the directory, not the session: bind one directory to two sessions and there is still only one trust verdict.'
|
|
1479
|
+
ws_current: 'current'
|
|
1480
|
+
# Four cases, because level and trusted can disagree, and the disagreement
|
|
1481
|
+
# is exactly what the user needs. "You refused it" and "never asked" lead
|
|
1482
|
+
# to completely different next steps, so they cannot collapse into one
|
|
1483
|
+
ws_trusted: 'Trusted; its .epoch/ is read.'
|
|
1484
|
+
ws_gate_off: 'The trust gate is off entirely (trust.enabled is false), so it counts as trusted.'
|
|
1485
|
+
ws_refused: 'You refused it. Nothing in its .epoch/ is read.'
|
|
1486
|
+
ws_unknown: 'Never asked. Its .epoch/ is read only after epoch trust add.'
|
|
1487
|
+
bound_field: 'Current workspace'
|
|
1488
|
+
unbound: 'This session has no workspace — a real state, not a pending lookup. Relative paths have no base, and project-level config, roles and skills are all left unloaded.'
|
|
1489
|
+
main_field: 'Main root {name}'
|
|
1490
|
+
main_hint: 'Relative paths resolve against the main root only. Project-level .epoch/settings.json, agents/, skills/ and policies/ are discovered here and nowhere else.'
|
|
1491
|
+
loaded_files: 'Convention files loaded:'
|
|
1492
|
+
loaded_none: 'This root has no convention files.'
|
|
1493
|
+
skipped_line: 'These convention files exist, and not a word of them was read.'
|
|
1494
|
+
skipped_why: 'They go into the system prompt, so they pass the trust gate first. Trust this directory and they will load:'
|
|
1495
|
+
extra_field: 'Extra directories'
|
|
1496
|
+
layer_add_dir: '--add-dir'
|
|
1497
|
+
extra_none: 'No extra directories.'
|
|
1498
|
+
extra_desc: 'path allowance only'
|
|
1499
|
+
# ⚠️ 2026-08-18 trimmed — see the zh comment for the ledger.
|
|
1500
|
+
extra_hint: 'These only widen what can be read and written. They are not a second workspace: relative paths, project config, agents and skills all resolve against the main root only.'
|
|
1501
|
+
unloaded_extra: 'The extra directories contain convention files, and none of them are loaded:'
|
|
1502
|
+
unloaded_extra_way: 'This differs from the main root: trusting will not load them either, because "convention files in extra directories need their own trust verdict" has not been built. To make them count, make that directory the main root, or fold their content into the main root convention file.'
|
|
1503
|
+
trust_only_deny: 'In an untrusted repository only the deny list of its permission rules applies — it can subtract, never add.'
|
|
1504
|
+
trust_not_permission: 'Trust does not change the permission mode. It answers only "may files in this directory influence my instructions", never "should this action ask you" — the rule table above always decides that.'
|
|
1505
|
+
|
|
1506
|
+
# ---- Policy rules ----
|
|
1507
|
+
g_policy: 'Policy rules'
|
|
1508
|
+
policy_lede: 'A different thing from the permission rule table above: permission rules govern "should this ask me", policy rules govern "what does this tool touching this path count as" — and a deny can carry a sentence you wrote to the user.'
|
|
1509
|
+
policy_none: 'No policy directory takes part in the scan.'
|
|
1510
|
+
policy_user: 'User level, follows this machine.'
|
|
1511
|
+
policy_project: 'Project level, follows the current workspace — another session means another repository.'
|
|
1512
|
+
policy_n: '{count} rules'
|
|
1513
|
+
policy_skipped: '{dir} was not loaded at all — that workspace is untrusted.'
|
|
1514
|
+
policy_skipped_why: 'Not "deny only": not one rule was read.'
|
|
1515
|
+
policy_asym: 'This is asymmetric with permissions, which is worth stating outright: permissions keep deny when untrusted, policy is dropped wholesale. Within one [[rule]] the tool name, path pattern and decision are bound together and deny_message is shown to the user verbatim — leaving a "deny still applies" opening would mean rewriting the semantics of somebody else rules one by one.'
|
|
1516
|
+
|
|
1517
|
+
# ---- Audit trail ----
|
|
1518
|
+
#
|
|
1519
|
+
# The sections above answer "what are the rules"; this one answers "what
|
|
1520
|
+
# actually happened under them".
|
|
1521
|
+
# NOTE: with zero entries the whole section is not drawn (decision 20 ①),
|
|
1522
|
+
# which is why there is no empty-state string here — it is not missing.
|
|
1523
|
+
g_audit: 'What it decided'
|
|
1524
|
+
audit_lede: 'Verdicts made by the permission layer this run, newest first.'
|
|
1525
|
+
# This line is the most important one in the section — do not drop it. It
|
|
1526
|
+
# states what this ledger records and what it does not.
|
|
1527
|
+
# ⚠️ 2026-08-18 trimmed — see the zh comment for the ledger.
|
|
1528
|
+
audit_scope: 'Only decisions the permission layer actually made — not every risky action.'
|
|
1529
|
+
audit_col_at: 'Time'
|
|
1530
|
+
audit_col_tool: 'Tool'
|
|
1531
|
+
audit_col_type: 'Type'
|
|
1532
|
+
audit_col_target: 'Target'
|
|
1533
|
+
audit_col_outcome: 'Verdict'
|
|
1534
|
+
audit_col_code: 'Reason'
|
|
1535
|
+
audit_granted: 'Allowed'
|
|
1536
|
+
audit_denied: 'Blocked'
|
|
1537
|
+
audit_dropped: '{dropped} earlier entries have been evicted — this table keeps at most 500. What you see is not all of it.'
|
|
1538
|
+
audit_ephemeral: 'This ledger lives only inside the running server process: never written to disk, never exported, never sent to telemetry. It is gone when the process exits, so it is not a complete history.'
|
|
1539
|
+
# Twelve reason codes. Each has to answer "where do I look next", rather
|
|
1540
|
+
# than restating the code in words.
|
|
1541
|
+
audit_code_dangerous_command: 'Matched the dangerous-command list'
|
|
1542
|
+
audit_code_sensitive_path: 'Matched the sensitive-path list'
|
|
1543
|
+
audit_code_obfuscated: 'Command is obfuscated and nobody could confirm it non-interactively'
|
|
1544
|
+
audit_code_rule_match: 'Matched a permission rule'
|
|
1545
|
+
audit_code_policy_rule: 'Matched a policy rule'
|
|
1546
|
+
audit_code_approval_cache: 'You already made the same decision for this target'
|
|
1547
|
+
audit_code_sandbox_isolation: 'Decided by the measured sandbox isolation level'
|
|
1548
|
+
audit_code_level_default: 'No rule matched, so it fell through to the permission level'
|
|
1549
|
+
audit_code_headless_preauthorized: 'Non-interactive, matched the pre-authorization list'
|
|
1550
|
+
audit_code_headless_unauthorized: 'Non-interactive, not pre-authorized'
|
|
1551
|
+
audit_code_user_approved: 'You approved it'
|
|
1552
|
+
audit_code_user_denied: 'You denied it'
|
|
1553
|
+
|
|
1554
|
+
# ---- Appended 2026-08-16 ----
|
|
1555
|
+
#
|
|
1556
|
+
# At the tail of this group rather than beside perm_hint: this round may only
|
|
1557
|
+
# append to web.ui.sec.*, and inserting mid-group collides with other branches.
|
|
1558
|
+
# On-screen order is decided by settings/security-perm.tsx, not by line order.
|
|
1559
|
+
#
|
|
1560
|
+
# This line landed once "should the mode cell grow a segmented control" was
|
|
1561
|
+
# actually decided. The answer is **no** (argued in full in the file header of
|
|
1562
|
+
# settings/security-perm.tsx, "three things deliberately not drawn" #1: that
|
|
1563
|
+
# control already lives on the composer row, and the segmented control the
|
|
1564
|
+
# design mock puts on the settings screen answers a different value).
|
|
1565
|
+
# This screen's shape for a control it does not draw is "state the fact, then
|
|
1566
|
+
# name the real path to change it" — this is that sentence.
|
|
1567
|
+
#
|
|
1568
|
+
# ⚠️ The "takes effect immediately, not written to disk" half cannot be dropped:
|
|
1569
|
+
# every other row on this page answers "what is on disk / what happens next
|
|
1570
|
+
# launch". Without it the user reads a mode switch as setting a new default
|
|
1571
|
+
# (decision 20 ④).
|
|
1572
|
+
# ⚠️ 2026-08-18 rewritten; it now covers **both rows** (and absorbed
|
|
1573
|
+
# `perm_two_rows`). "Takes effect immediately" must stay — every other row on
|
|
1574
|
+
# this page answers "what is on disk", so without it a mode switch reads as
|
|
1575
|
+
# "I changed the default".
|
|
1576
|
+
perm_where: 'Change the top one from the row under the composer — it takes effect immediately. The bottom one is where new sessions start: edit ~/.epoch/config.yaml and restart.'
|
|
1577
|
+
|
|
1578
|
+
# ---- Appended 2026-08-17 (plan 56 §1.3) ----
|
|
1579
|
+
#
|
|
1580
|
+
# The permission mode goes from one cell to two rows. ⚠️ **The second row is
|
|
1581
|
+
# always drawn, not "only when the two differ"** — drawing it conditionally
|
|
1582
|
+
# means users never learn there are two values here, and the first time it
|
|
1583
|
+
# shows up they read it as something having gone wrong.
|
|
1584
|
+
#
|
|
1585
|
+
# "Now / Default" are short enough to align on a fixed-width column, and that
|
|
1586
|
+
# alignment is the whole point: the eye has to spot the difference at a glance.
|
|
1587
|
+
# Do not expand them into "Current mode / Default mode".
|
|
1588
|
+
perm_now: 'Now'
|
|
1589
|
+
perm_default: 'Default'
|
|
1590
|
+
# Printed only when the two really differ — it explains **this particular
|
|
1591
|
+
# difference**, it is not one of the two rows. Drawn unconditionally it would
|
|
1592
|
+
# be a lie on the vast majority of (never-switched) sessions
|
|
1593
|
+
perm_now_changed: '(changed in this session)'
|
|
1594
|
+
# ⚠️ `perm_two_rows` dropped 2026-08-18. Both things it said now have closer
|
|
1595
|
+
# owners: the two words above name the rows, and `perm_where` covers where to
|
|
1596
|
+
# change each. Full reasoning in `settings/security-perm.tsx`.
|
|
1597
|
+
|
|
1598
|
+
# The capability screen (plan 42 PR-1).
|
|
1599
|
+
#
|
|
1600
|
+
# `g_*` are the source-layer group titles, shared by all three panes;
|
|
1601
|
+
# `g_*_roles` / `g_*_skills` / `g_*_conn` are that group's note, written
|
|
1602
|
+
# **per pane** — what a user looks for in the note line is always "where on
|
|
1603
|
+
# disk does this group live", and the three panes do not share a directory.
|
|
1604
|
+
cap:
|
|
1605
|
+
title: 'Capabilities'
|
|
1606
|
+
back: 'Back to session'
|
|
1607
|
+
experts: 'Identities'
|
|
1608
|
+
skills: 'Skills'
|
|
1609
|
+
connectors: 'Connectors'
|
|
1610
|
+
search: 'Search names, descriptions, tool names'
|
|
1611
|
+
no_session: 'No session yet. These three panes follow the session — the "Project" layer depends on which workspace it is bound to.'
|
|
1612
|
+
loading: 'Loading…'
|
|
1613
|
+
|
|
1614
|
+
experts_lede: 'An identity is a prompt, a tool set and a turn limit. The main agent hands work off with delegate_task; each sub-agent runs under its own identity in a separate context and only hands back the conclusion.'
|
|
1615
|
+
skills_lede: 'Skills are progressively disclosed: only the one line below enters the context, and the body is read only when the model decides it is relevant. So the number at the end of each row is what it costs you every turn.'
|
|
1616
|
+
connectors_lede: 'Connectors are where tools come from. The built-in ones ship with the program and cannot fail to connect; the MCP ones are yours to configure, which is why they carry a state.'
|
|
1617
|
+
|
|
1618
|
+
g_builtin: 'Built-in'
|
|
1619
|
+
g_plugin: 'Plugins'
|
|
1620
|
+
g_host: 'Host'
|
|
1621
|
+
g_user: 'Mine'
|
|
1622
|
+
g_project: 'Project'
|
|
1623
|
+
g_mcp: 'MCP'
|
|
1624
|
+
|
|
1625
|
+
g_builtin_roles: 'Present out of the box, nothing to configure. Any of them can be overridden by a same-named entry in a layer below.'
|
|
1626
|
+
g_plugin_roles: 'Brought in by installed plugins, from each plugin own agents/ directory.'
|
|
1627
|
+
g_user_roles: '.md files under ~/.epoch/agents/ — one set per machine, shared by every project.'
|
|
1628
|
+
g_project_roles: '.md files under this repository .epoch/agents/. They travel with the repository and override same-named entries above.'
|
|
1629
|
+
g_host_roles: 'Shipped by the application this engine is embedded in; their names carry the prefix that application chose. They never show up in epoch plugin list — you did not install them, and only that application own UI can manage them.'
|
|
1630
|
+
|
|
1631
|
+
g_user_skills: 'One directory per skill under ~/.epoch/skills/, with the body in SKILL.md.'
|
|
1632
|
+
g_plugin_skills: 'Brought in by installed plugins; their names are prefixed with the plugin name.'
|
|
1633
|
+
g_project_skills: 'From this repository .epoch/skills/. They travel with the repository.'
|
|
1634
|
+
g_host_skills: 'Shipped by the application this engine is embedded in; their names carry the prefix that application chose. They are upgraded together with that application, so they are read-only here.'
|
|
1635
|
+
|
|
1636
|
+
g_builtin_conn: 'Tool plugins compiled into the program. Only the tool count is shown, not every name — spelling out thirty-odd of them would push the MCP servers below off the screen.'
|
|
1637
|
+
g_mcp_note: 'What you configured in ~/.epoch/mcp.json. Their tool names carry an mcp__<server>__ prefix.'
|
|
1638
|
+
g_host_conn: 'Shipped by the application this engine is embedded in. They are not in your mcp.json, so you can neither edit nor remove them — only that application own UI can. Their names carry the prefix it chose, so their tool names start with mcp__<prefix>__<server>__.'
|
|
1639
|
+
# The plugin group (plan 44 PR-3). It differs from the row above in exactly
|
|
1640
|
+
# one way, and that difference is the whole reason this string exists: you
|
|
1641
|
+
# *can* do something about these. Name both commands, or it reads the same
|
|
1642
|
+
# as the "you cannot touch this" group.
|
|
1643
|
+
g_plugin_conn: 'Brought in by an installed plugin, from the mcp.json at its own root. Names are prefixed with the plugin name, so their tool names start with mcp__<plugin>__<server>__. You cannot edit their contents, but epoch plugin list tells you which plugin brought them and epoch plugin disable turns the whole thing off.'
|
|
1644
|
+
|
|
1645
|
+
project_locked_roles: 'This workspace is not trusted yet, so we never read its .epoch/agents/ — which means we cannot even tell you how many there are. Trust it and come back.'
|
|
1646
|
+
locked_line: 'This workspace is not trusted yet; none of its skills were read.'
|
|
1647
|
+
locked_why: 'This is not "read then disabled" — that directory was never opened at all, which is why not even a count can be listed here.'
|
|
1648
|
+
locked_gate: 'What the gate stops: you clone a repository, and whatever it carries in .epoch/ should not reach the model context before you have looked at it.'
|
|
1649
|
+
|
|
1650
|
+
summon: 'Use'
|
|
1651
|
+
overrides_builtin: 'Overrides the built-in of the same name'
|
|
1652
|
+
stat_tools: 'Tools'
|
|
1653
|
+
stat_turns: 'Turn limit'
|
|
1654
|
+
unlimited: 'unrestricted'
|
|
1655
|
+
turns_global: 'global'
|
|
1656
|
+
toolset: 'Tool set'
|
|
1657
|
+
toolset_n: 'Tool set ({count})'
|
|
1658
|
+
unlimited_hint: 'It declares no tools, so it gets every tool available this round.'
|
|
1659
|
+
list_sep: ', '
|
|
1660
|
+
narrowed: 'Some tools declared by {role} were outside the range of the layer above and have been dropped: {cut}. A tool allowlist can only narrow, never widen.'
|
|
1661
|
+
|
|
1662
|
+
skill_tokens: '{tokens} tokens'
|
|
1663
|
+
no_toggle: 'There is no "turn this skill off" switch: as long as the file is in the directory, the line above enters the context. To drop one, move its directory away.'
|
|
1664
|
+
|
|
1665
|
+
# The skill-body screen (plan 56 §1.2).
|
|
1666
|
+
#
|
|
1667
|
+
# ⚠️ `skill_view_no_count` is the most important line on that screen, and it
|
|
1668
|
+
# is **always drawn**: the thing users most easily get wrong is "did opening
|
|
1669
|
+
# this count as using it?". It did not — that route does not touch the stats,
|
|
1670
|
+
# and the score only counts times the model actually used it. It says "will
|
|
1671
|
+
# not affect its score" rather than "does not count": the latter is our
|
|
1672
|
+
# implementation word, the former is the consequence the user cares about.
|
|
1673
|
+
# ---- Import skills (plan 42 §六, 2026-08-18) ----
|
|
1674
|
+
# ⚠️ import_hint_local is the most important line on this screen, and it is
|
|
1675
|
+
# always drawn, above the input: the mistake users make here is assuming
|
|
1676
|
+
# this is an upload box. The full rationale for not accepting uploads is in
|
|
1677
|
+
# core/src/skill/import.ts.
|
|
1678
|
+
import_entry: 'Import a skill'
|
|
1679
|
+
import_title: 'Import a skill'
|
|
1680
|
+
import_lede: 'Import from the machine running the server. Accepts a folder containing SKILL.md, a two-level category/name skill tree, a .zip of either, or a single .md with frontmatter.'
|
|
1681
|
+
# ⚠️ 2026-08-18 trimmed — see the zh comment for the ledger.
|
|
1682
|
+
import_hint_local: 'Give a path on the machine running the server — this route does not accept uploads.'
|
|
1683
|
+
import_placeholder: '/Users/you/skills/my-skill'
|
|
1684
|
+
import_path_label: 'Absolute path to import'
|
|
1685
|
+
import_look: 'See what is there'
|
|
1686
|
+
import_browse: 'Browse directories'
|
|
1687
|
+
import_browse_off: 'Collapse'
|
|
1688
|
+
import_pick_here: 'Use this directory'
|
|
1689
|
+
import_count: 'Will import {count} skill(s) ({form})'
|
|
1690
|
+
import_form_markdown: 'a single .md'
|
|
1691
|
+
import_form_skill-dir: 'one skill folder'
|
|
1692
|
+
import_form_tree: 'a skill tree'
|
|
1693
|
+
import_form_archive: 'an archive'
|
|
1694
|
+
import_files: '{files} extra file(s) · {bytes} bytes'
|
|
1695
|
+
import_no_desc: '(no description — it will not appear usefully in <available_skills>, so the model is unlikely to pick it)'
|
|
1696
|
+
import_conflict_refuse: ' ⚠️ A user-level skill of this name exists; this one will not be imported'
|
|
1697
|
+
import_conflict_shadowed: ' ⚠️ A project-level skill of this name exists; importing it has no effect (project-level wins)'
|
|
1698
|
+
import_confirm: 'Import these {count}'
|
|
1699
|
+
import_issues: 'Worth a look:'
|
|
1700
|
+
import_skipped: 'Skipped:'
|
|
1701
|
+
import_done: 'Imported {count}, skipped {skipped}'
|
|
1702
|
+
import_failed: 'Nothing can be imported from that path'
|
|
1703
|
+
import_again: 'Import another'
|
|
1704
|
+
skill_view: 'View body'
|
|
1705
|
+
skill_back: 'Back to skills'
|
|
1706
|
+
skill_view_no_count: 'Looking at this will not affect its score — that tally only counts times the model actually used it.'
|
|
1707
|
+
# The truncation line goes **above** the body: finding out halfway through
|
|
1708
|
+
# that you are not reading all of it is too late
|
|
1709
|
+
skill_truncated: 'This SKILL.md is large ({bytes} bytes in total); below is only the first stretch of it. Open the file directly to read the whole thing.'
|
|
1710
|
+
skill_bytes: '{bytes} bytes in total'
|
|
1711
|
+
|
|
1712
|
+
tool_count: '{count} tools'
|
|
1713
|
+
# ⚠️ Changed 2026-08-18: this used to read "Add one to ~/.epoch/mcp.json, or
|
|
1714
|
+
# run epoch mcp add" — and **`epoch mcp add` does not exist anywhere in the
|
|
1715
|
+
# repo** (`epoch mcp` only has list / status / login / logout). A line that
|
|
1716
|
+
# tells people to run something that cannot be run. It now points at the
|
|
1717
|
+
# button this screen actually has.
|
|
1718
|
+
mcp_empty: 'No MCP server configured yet. Use "Add server" above, or edit ~/.epoch/mcp.json directly.'
|
|
1719
|
+
mcp_connected: 'Connected'
|
|
1720
|
+
mcp_failed: 'Unreachable'
|
|
1721
|
+
mcp_login: 'Login required'
|
|
1722
|
+
mcp_retries: 'Retried {count} times.'
|
|
1723
|
+
mcp_failed_why: 'Its tools are entirely unavailable this round. After fixing mcp.json you have to restart epoch web for it to reconnect.'
|
|
1724
|
+
mcp_login_why: 'It is reachable but not authorised — reconnecting will not help however many times you try. Run this in a terminal:'
|
|
1725
|
+
|
|
1726
|
+
# The reconnect button (plan 56 §1.1).
|
|
1727
|
+
#
|
|
1728
|
+
# ⚠️ `mcp_reconnect_scope` is **always drawn, right next to the button** —
|
|
1729
|
+
# this screen is session-scoped (the capability page fetches by :id) while
|
|
1730
|
+
# MCP connections are process-wide. The user has to know **before pressing**
|
|
1731
|
+
# that this touches other sessions, not be told afterwards (decision 20 ①:
|
|
1732
|
+
# what is said about an action belongs where the action is taken).
|
|
1733
|
+
#
|
|
1734
|
+
# ⚠️ It also spells out that reconnecting does not re-read mcp.json: underneath
|
|
1735
|
+
# it goes through the client built at startup. So this cures "the other end
|
|
1736
|
+
# restarted / the network hiccuped", not "I just edited the config" — the
|
|
1737
|
+
# latter still needs a restart, which is exactly what mcp_failed_why says.
|
|
1738
|
+
mcp_reconnect: 'Reconnect'
|
|
1739
|
+
mcp_reconnecting: 'Reconnecting…'
|
|
1740
|
+
mcp_reconnect_scope: 'This affects every session in this process, not just the one you are looking at — the MCP connection is shared by the whole process. It reconnects using the mcp.json read at startup, so if you changed the config you still need a restart.'
|
|
1741
|
+
mcp_reconnect_after_login: 'Run the login above first, then press here — new credentials only take effect after a reconnect.'
|
|
1742
|
+
|
|
1743
|
+
# The "Add server" screen (2026-08-18).
|
|
1744
|
+
#
|
|
1745
|
+
# ⚠️ `mcp_add_scope` is **always drawn, right next to the submit button** —
|
|
1746
|
+
# same criterion as `mcp_reconnect_scope` above, but **this sentence has to
|
|
1747
|
+
# reach one notch further**: reconnecting changes one connection in this
|
|
1748
|
+
# process, adding changes the **on-disk ~/.epoch/mcp.json**, which every
|
|
1749
|
+
# future epoch process on this machine (CLI / TUI / other hosts) will see.
|
|
1750
|
+
# Copying the reconnect wording would say only half of it.
|
|
1751
|
+
#
|
|
1752
|
+
# ⚠️ `mcp_add_env_hint` says something unflattering but true: env lands on
|
|
1753
|
+
# disk in plaintext. That is what mcp.json has always been; this path does
|
|
1754
|
+
# not make it worse, and does not quietly make it better either — hiding it
|
|
1755
|
+
# would let users believe a secret typed into the UI got encrypted.
|
|
1756
|
+
#
|
|
1757
|
+
# ⚠️ `mcp_add_offline` is **not a failure**: the config is on disk, the other
|
|
1758
|
+
# end just is not up. Calling it a failure makes users try again, and trying
|
|
1759
|
+
# again runs straight into "that name is taken".
|
|
1760
|
+
#
|
|
1761
|
+
# ⚠️ `mcp_add_locked`: when the server is bound outside the loopback
|
|
1762
|
+
# interface, the top of this group shows **this paragraph, not a greyed-out
|
|
1763
|
+
# button** (the "Add server" button is not drawn at all). Three hard
|
|
1764
|
+
# requirements identical to `role_add_locked`, but the **why half belongs to
|
|
1765
|
+
# this path alone**:
|
|
1766
|
+
# 1. why — adding one writes "which executable to launch" into mcp.json,
|
|
1767
|
+
# and it governs every future epoch process on this machine (that half
|
|
1768
|
+
# is already in `mcp_add_scope`; **do not drop it here**);
|
|
1769
|
+
# 2. two ways forward (edit that file on that machine / drop --host and
|
|
1770
|
+
# restart) — "not allowed" alone leaves the user concluding it is broken;
|
|
1771
|
+
# 3. never phrase it as "insufficient permissions": their credentials are
|
|
1772
|
+
# fine; it is this path that is closed in this mode.
|
|
1773
|
+
#
|
|
1774
|
+
# ⚠️ **Do not copy role_add_locked** (that one is about writing into the
|
|
1775
|
+
# system prompt), and do not copy mcp_cfg_locked either (that one is about
|
|
1776
|
+
# refusing even to read, because env is cleartext). Two of these three appear
|
|
1777
|
+
# on screen at the same time, so a copied sentence becomes a half-true one.
|
|
1778
|
+
mcp_add: 'Add server'
|
|
1779
|
+
mcp_add_title: 'Add an MCP server'
|
|
1780
|
+
mcp_add_name: 'Server name'
|
|
1781
|
+
mcp_add_name_hint: 'Becomes part of the tool names (mcp__<name>__…), so only letters, digits, _ and - are accepted.'
|
|
1782
|
+
mcp_add_transport: 'Transport'
|
|
1783
|
+
mcp_add_command: 'Executable'
|
|
1784
|
+
mcp_add_args: 'Arguments (one per line)'
|
|
1785
|
+
mcp_add_url: 'URL'
|
|
1786
|
+
mcp_add_env: 'Environment variables (one KEY=value per line)'
|
|
1787
|
+
mcp_add_env_hint: 'These lines are written to mcp.json in plaintext. Do not put secrets here — that path is epoch mcp login, where credentials are stored encrypted.'
|
|
1788
|
+
mcp_add_scope: 'This is written to ~/.epoch/mcp.json — not just this conversation, and not just this process: every future epoch run on this machine (including the CLI and the TUI) will see it. It is connected right away, so its tools are usable this round.'
|
|
1789
|
+
mcp_add_submit: 'Add it'
|
|
1790
|
+
mcp_add_submitting: 'Connecting…'
|
|
1791
|
+
mcp_add_cancel: 'Cancel'
|
|
1792
|
+
mcp_add_done: 'Written to {path}.'
|
|
1793
|
+
mcp_add_offline: 'The config was added, but this server is unreachable right now — the card above says why. If the config is right, press Reconnect once the other end is up.'
|
|
1794
|
+
mcp_add_locked: 'MCP servers cannot be added from the UI in this mode: the server was bound outside the loopback interface with --host, and we cannot confirm your browser is on the machine running it. Adding one writes "which executable to launch" into mcp.json — that server is started right away, and every future epoch run on this machine (including the CLI and the TUI) starts it too, not just this conversation. To add one: edit ~/.epoch/mcp.json directly on that machine, or drop --host and restart.'
|
|
1795
|
+
|
|
1796
|
+
# The "see its config" screen — the raw mcp.json editor (2026-08-18).
|
|
1797
|
+
#
|
|
1798
|
+
# ⚠️ `mcp_cfg_scope` is **always drawn, above the buttons** (same criterion as
|
|
1799
|
+
# mcp_add_scope), but it has to say **one thing more**: this screen can delete
|
|
1800
|
+
# a server, and deleting one silently takes away capability other sessions are
|
|
1801
|
+
# relying on. So it separates the two actions — save touches the file on disk,
|
|
1802
|
+
# apply touches every session in this process.
|
|
1803
|
+
#
|
|
1804
|
+
# ⚠️ `mcp_cfg_saved` must **always be followed** by one of `mcp_cfg_not_live` /
|
|
1805
|
+
# `mcp_cfg_apply_dirty`: "Written to X" on its own reads as "it took effect",
|
|
1806
|
+
# and this request never touched a single connection in this process.
|
|
1807
|
+
#
|
|
1808
|
+
# ⚠️ `mcp_cfg_locked` follows the three hard requirements of role_add_locked
|
|
1809
|
+
# (why / way forward / no blame), but for a **different reason**: the env in
|
|
1810
|
+
# that file is cleartext, and the file also decides what gets executed.
|
|
1811
|
+
#
|
|
1812
|
+
# ⚠️ The five action lines are **explanations, not labels**: after pressing
|
|
1813
|
+
# Apply the user must see at a glance which server was disconnected and which
|
|
1814
|
+
# was left alone. `mcp_cfg_act_skipped` also has to state the timing gap.
|
|
1815
|
+
mcp_cfg_open: 'See its config'
|
|
1816
|
+
mcp_cfg_title: 'Edit mcp.json'
|
|
1817
|
+
mcp_cfg_path: 'Config file path:'
|
|
1818
|
+
mcp_cfg_dirty: 'Unsaved'
|
|
1819
|
+
mcp_cfg_loading: 'Reading that file…'
|
|
1820
|
+
mcp_cfg_absent: 'This file does not exist yet — the lines below are a template, not your content. It is only created for real when you press Save.'
|
|
1821
|
+
mcp_cfg_editor: 'Contents of mcp.json'
|
|
1822
|
+
mcp_cfg_locked: 'mcp.json can neither be viewed nor edited in this mode — the server was bound outside the loopback interface with --host, so we cannot tell whether the browser sending this request is on this machine. Environment variables in that file are stored in cleartext (they may hold secrets), and the same file decides which executable gets launched. Two ways forward: edit that file directly on the machine running the server, or drop --host and restart.'
|
|
1823
|
+
mcp_cfg_reload: 'Read it again (discards your current edits)'
|
|
1824
|
+
mcp_cfg_scope: 'There are two actions on this screen and they change different things. Save only writes ~/.epoch/mcp.json — every future epoch run on this machine (including the CLI and the TUI) reads it. Apply to this process is what touches the live connections, and that affects every session in this process, not just the one you are looking at. Once a server is removed, sessions using it can no longer call its tools from the next turn on.'
|
|
1825
|
+
mcp_cfg_save: 'Save'
|
|
1826
|
+
mcp_cfg_saving: 'Writing…'
|
|
1827
|
+
mcp_cfg_apply: 'Apply to this process'
|
|
1828
|
+
mcp_cfg_applying: 'Applying…'
|
|
1829
|
+
mcp_cfg_apply_dirty: 'Save first — Apply reads what is on disk, and your version has not been written yet.'
|
|
1830
|
+
mcp_cfg_close: 'Collapse'
|
|
1831
|
+
mcp_cfg_saved: 'Written to {path}.'
|
|
1832
|
+
mcp_cfg_not_live: 'The servers in this process are still running with the config read at startup — press "Apply to this process", or let it take effect on the next start.'
|
|
1833
|
+
mcp_cfg_applied: 'MCP in this process has been rearranged to match what is on disk:'
|
|
1834
|
+
mcp_cfg_applied_none: 'Nothing changed — what is on disk matches the servers running in this process verbatim.'
|
|
1835
|
+
mcp_cfg_act_added: 'newly connected'
|
|
1836
|
+
mcp_cfg_act_reconnected: 'config changed, reconnected with the new one'
|
|
1837
|
+
mcp_cfg_act_removed: 'disconnected; its tools are unavailable from this turn on'
|
|
1838
|
+
mcp_cfg_act_skipped: 'this name is taken by the host or a plugin, so it was not added in this process — your server wins on the next start'
|
|
1839
|
+
mcp_cfg_act_unchanged: 'untouched (config unchanged)'
|
|
1840
|
+
|
|
1841
|
+
# The "create an identity" screen (2026-08-18) — the last of the three
|
|
1842
|
+
# columns to get a write path.
|
|
1843
|
+
#
|
|
1844
|
+
# ⚠️ `role_add_locked` is the important one: when the server is bound outside
|
|
1845
|
+
# the loopback interface, the top of this column shows **this paragraph, not a
|
|
1846
|
+
# greyed-out button**. Three hard requirements:
|
|
1847
|
+
# 1. say **why** (the text enters the context of every future turn, and in
|
|
1848
|
+
# this mode we cannot confirm the browser is on that machine);
|
|
1849
|
+
# 2. say **the way out** (write the file on that machine / drop --host and
|
|
1850
|
+
# restart) — saying only "not allowed" leaves the user concluding it is
|
|
1851
|
+
# broken;
|
|
1852
|
+
# 3. **do not phrase it as "insufficient permissions"**: their credentials
|
|
1853
|
+
# are perfectly fine; it is this path that is closed in this mode.
|
|
1854
|
+
#
|
|
1855
|
+
# ⚠️ `role_add_scope` is **always drawn, right above the submit button** (same
|
|
1856
|
+
# as mcp_add_scope). It says **one extra thing that only this path has**: the
|
|
1857
|
+
# text being written enters the model's context. Without that half sentence,
|
|
1858
|
+
# users read "create an identity" as "add a local config entry".
|
|
1859
|
+
#
|
|
1860
|
+
# ⚠️ `role_add_desc_hint` must spell out that this field is **written for the
|
|
1861
|
+
# main agent**. Otherwise users write a self-introduction, the main agent
|
|
1862
|
+
# never delegates to the identity, and nothing on screen explains why.
|
|
1863
|
+
#
|
|
1864
|
+
# ⚠️ `role_add_tools_hint` must spell out that **empty = unrestricted**. An
|
|
1865
|
+
# empty list means "none" elsewhere on this screen (the env field above);
|
|
1866
|
+
# here it means the opposite.
|
|
1867
|
+
role_add: 'New identity'
|
|
1868
|
+
role_add_title: 'Create an identity'
|
|
1869
|
+
role_add_name: 'Name'
|
|
1870
|
+
role_add_name_hint: 'Lowercase letters, digits and hyphens only. It is both the file name and the word used to delegate work.'
|
|
1871
|
+
role_add_desc: 'When to delegate to it'
|
|
1872
|
+
role_add_desc_hint: 'This sentence is written for the main agent, not as a self-introduction — it decides whether to hand work over based on it, so describe when it should be used. It also enters the tool description of every delegation.'
|
|
1873
|
+
role_add_prompt: 'Body'
|
|
1874
|
+
role_add_prompt_hint: 'The passage this identity reads while it works. Leaving it empty is fine — then it only changes the identity line and the tool boundary.'
|
|
1875
|
+
role_add_tools: 'Tool whitelist (one per line)'
|
|
1876
|
+
role_add_tools_hint: 'Empty = unrestricted, it gets every tool available this round. Listing names restricts it to those. The names must match the ones on Settings › Tools.'
|
|
1877
|
+
role_add_turns: 'Turn limit'
|
|
1878
|
+
role_add_turns_hint: 'Empty = follow the global setting. 1 to 200.'
|
|
1879
|
+
role_add_scope: 'This creates a .md file in ~/.epoch/agents/ — not just for this conversation, and not just for this process: every future epoch run on this machine (including the CLI and the TUI) will see it. ⚠️ Both passages above enter the model context ("when to delegate to it" on every single turn), so what you write here carries the same weight as what you say in a conversation.'
|
|
1880
|
+
role_add_submit: 'Create it'
|
|
1881
|
+
role_add_submitting: 'Writing…'
|
|
1882
|
+
role_add_cancel: 'Cancel'
|
|
1883
|
+
role_add_done: 'Written to {path} — go to that file to keep editing the body.'
|
|
1884
|
+
role_add_locked: 'Identities cannot be created from the UI in this mode: the server was bound outside the loopback interface with --host, and we cannot confirm your browser is on the machine running it. The body of an identity enters the context of every future turn (and its "when to delegate to it" line enters the tool description of every delegation), so this path is only open under the default loopback binding. To add one: put a .md file in ~/.epoch/agents/ on that machine, or drop --host and restart.'
|
|
1885
|
+
|
|
1886
|
+
copy_cmd: 'Copy'
|
|
1887
|
+
|
|
1888
|
+
trusted_at: 'Trusted {root}'
|
|
1889
|
+
untrusted_at: 'Not trusted {root}'
|
|
1890
|
+
no_workspace: 'This session has no workspace bound'
|
|
1891
|
+
|
|
1892
|
+
summon_prefill: 'Have {role} do: '
|
|
1893
|
+
|
|
1894
|
+
open_session: 'New session'
|
|
1895
|
+
open_session_here: 'Start a new session run by {role} in {root}; the conversation you are in stays exactly where it is'
|
|
1896
|
+
open_session_nows: 'Start a new session run by {role}, with no workspace bound either; the conversation you are in stays exactly where it is'
|
|
1897
|
+
open_session_failed: 'Could not start the session: {detail}'
|
|
1898
|
+
|
|
1899
|
+
# The card's third action (plan 57 §3.6, 2026-08-17): the **per-message** one —
|
|
1900
|
+
# no prefill, no new session, it just pins this card to the chip on the
|
|
1901
|
+
# composer. Which gate each of the three paths takes is documented on
|
|
1902
|
+
# `ExpertsProps.canSummon`.
|
|
1903
|
+
#
|
|
1904
|
+
# The verb is picked to match the handoff marker written into the history
|
|
1905
|
+
# (`agent_role.handoff`, "From this message on, \"x\" takes over."): whoever
|
|
1906
|
+
# presses this button reads the same words in the transcript a moment later,
|
|
1907
|
+
# and with two different verbs they cannot tell that line was their own doing.
|
|
1908
|
+
take_over: 'Let it take over'
|
|
1909
|
+
# Three words cannot carry the consequence, so title / aria-label spell it out.
|
|
1910
|
+
# **"this conversation is untouched" may not be dropped**: of the three actions
|
|
1911
|
+
# on this screen one edits the composer and one starts a whole new session,
|
|
1912
|
+
# and this is the only one where nothing happens at all
|
|
1913
|
+
take_over_title: 'From your next message on, "{role}" answers; this conversation is untouched'
|
|
1914
|
+
# Already this one. The button stays, turns into a state and greys out — this
|
|
1915
|
+
# screen covers the conversation entirely, so the chip is nowhere in sight and
|
|
1916
|
+
# "which one is taking over" can only be said here
|
|
1917
|
+
take_over_on: 'Taking over'
|
|
1918
|
+
# ⚠️ The second half is **required**: the only way out is the × on the chip in
|
|
1919
|
+
# the composer (the single such exit in the app), and it is invisible from this
|
|
1920
|
+
# screen. Without it the user hunts this card for a switch that is not there
|
|
1921
|
+
take_over_on_title: '"{role}" is taking over the messages that follow — to cancel, go back to the conversation and press the × on the chip above the composer'
|
|
1922
|
+
|
|
1923
|
+
# The workspace-diff screen (plan 30 PR-3, 2026-08-16).
|
|
1924
|
+
#
|
|
1925
|
+
# ⚠️ None of the five notes may be merged — the server itself keeps them as five
|
|
1926
|
+
# distinct codes. These three pairs are the tempting ones, and each pair points
|
|
1927
|
+
# the user at a completely different next step:
|
|
1928
|
+
#
|
|
1929
|
+
# - `no_workspace` (this session has no directory) vs `no_changes` (the directory
|
|
1930
|
+
# has nothing in it): the first calls for starting a session bound to a
|
|
1931
|
+
# directory, the second means "that is the expected answer".
|
|
1932
|
+
# - `no_workspace_history` (this process has no record for this session) vs
|
|
1933
|
+
# `no_changes`: the first is **we cannot answer**, the second is **there is
|
|
1934
|
+
# genuinely nothing**.
|
|
1935
|
+
# - `not_a_git_repo` is **not an error** — it means the source changed (falling
|
|
1936
|
+
# back to "paths this session touched"), so it legitimately shows up alongside
|
|
1937
|
+
# the other codes.
|
|
1938
|
+
diff:
|
|
1939
|
+
title: 'Workspace changes'
|
|
1940
|
+
back: 'Back to session'
|
|
1941
|
+
reload: 'Read again'
|
|
1942
|
+
# Say plainly that nothing moves until you press it: this screen does not poll
|
|
1943
|
+
# (rationale in the header of workspace/diff.tsx)
|
|
1944
|
+
reload_hint: 'Read this directory again — this screen does not refresh itself'
|
|
1945
|
+
loading: 'Reading…'
|
|
1946
|
+
# ⚠️ This may only say "the request never landed". Every other failure (no
|
|
1947
|
+
# workspace bound, not a git repo, a read that blew up) comes back as a 200
|
|
1948
|
+
# with a note, and never reaches this branch
|
|
1949
|
+
failed: 'Could not read this session’s workspace changes — the request never reached the local server.'
|
|
1950
|
+
pick: 'Pick a file on the left.'
|
|
1951
|
+
# The git source is **wider than "what this session changed"**, and that has to
|
|
1952
|
+
# be said out loud: seeing a file you just edited yourself, the first reading is
|
|
1953
|
+
# "the model touched it"
|
|
1954
|
+
scope_git: 'This lists every uncommitted change in the directory relative to git HEAD — including your own edits and other programs’, not just this session’s.'
|
|
1955
|
+
status_added: 'added'
|
|
1956
|
+
status_modified: 'modified'
|
|
1957
|
+
status_deleted: 'deleted'
|
|
1958
|
+
|
|
1959
|
+
note_no_changes: 'This directory has no uncommitted changes.'
|
|
1960
|
+
# Always shows up together with the checkpoint fallback, so it has to finish the
|
|
1961
|
+
# thought: what are you looking at instead?
|
|
1962
|
+
note_not_a_git_repo: 'This directory is not a git repository, so what is listed here is the paths this session touched through the file tools.'
|
|
1963
|
+
# "this process does not know" — **not** "nothing changed"
|
|
1964
|
+
note_no_workspace_history: 'This process holds no file record for this session (most likely it was left behind by an earlier process), so the changed files cannot be listed.'
|
|
1965
|
+
note_no_workspace: 'This session has no workspace bound, so there is no directory to look at. Changing directory means starting a new session.'
|
|
1966
|
+
note_truncated: 'Too many files changed; only the first {count} are listed here.'
|
|
1967
|
+
|
|
1968
|
+
# The three "cannot be drawn" cases. **The short label and the why are two
|
|
1969
|
+
# separate entries** — the label has to fit the end of a file-tree row, the why
|
|
1970
|
+
# has to be long enough to actually explain.
|
|
1971
|
+
#
|
|
1972
|
+
# ⚠️ Why `omit_binary`’s why is not here: it is the same thing the read-only
|
|
1973
|
+
# preview says, and the single source of truth for that is
|
|
1974
|
+
# `web.ui.art.opaque_binary` above (wired up in inspector/content.ts).
|
|
1975
|
+
omit_binary: 'binary'
|
|
1976
|
+
omit_binary_sized: 'binary, {bytes}'
|
|
1977
|
+
omit_too_large: 'too large'
|
|
1978
|
+
omit_too_large_sized: 'too large, {bytes}'
|
|
1979
|
+
omit_too_large_why: 'This file is over the 512 KB per-file limit, so neither side’s contents were fetched — that is why there is no diff here, not because nothing changed.'
|
|
1980
|
+
# ⚠️ This has to name **which side** is missing, and why the current contents are
|
|
1981
|
+
# not substituted for it: doing that would draw a "nothing changed" diff, which
|
|
1982
|
+
# is exactly what the server flags this case to avoid
|
|
1983
|
+
omit_no_snapshot: 'no before-state'
|
|
1984
|
+
omit_no_snapshot_why: 'This directory is not a git repository, and no snapshot was kept of how the file looked before this session touched it — so all we know is that it changed, not which lines.'
|
|
1985
|
+
|
|
1986
|
+
# Settings pane six, "Tools" (`settings/tools.tsx`,
|
|
1987
|
+
# `GET /api/sessions/:id/tools`).
|
|
1988
|
+
#
|
|
1989
|
+
# This screen answers exactly one question: **of the tools sent to the model
|
|
1990
|
+
# this turn, who is blocking each one right now**.
|
|
1991
|
+
#
|
|
1992
|
+
# ⚠️ Every string in this group has to hold the same line: the three axes
|
|
1993
|
+
# (rule → level → sandbox) **do not look at arguments**. The safety baseline
|
|
1994
|
+
# (sensitive paths, dangerous commands) runs ahead of them and can only speak
|
|
1995
|
+
# once a real call arrives carrying real arguments. So `axes` is not a
|
|
1996
|
+
# disclaimer, it is the **definition** of this table — without it, users take
|
|
1997
|
+
# the table to mean "will this call go through", which is not a question it
|
|
1998
|
+
# can answer.
|
|
1999
|
+
#
|
|
2000
|
+
# One more: the four `v_*` words go through the catalog, while the security
|
|
2001
|
+
# centre's rule table prints its four raw in English. Deliberately different —
|
|
2002
|
+
# that screen prints the token you type into a config file, this one prints a
|
|
2003
|
+
# conclusion, and the fourth case ("depends") appears in no config file at all
|
|
2004
|
+
# (rationale on VERDICT_LABEL in tools.tsx).
|
|
2005
|
+
tools:
|
|
2006
|
+
no_session: 'No session yet, so this table cannot be computed — the verdicts are worked out against one session’s current permission level. Start a session and come back.'
|
|
2007
|
+
loading: 'Reading…'
|
|
2008
|
+
lede: 'Every tool sent to the model this turn: where it came from, what it does, how many times this session called it, and who is blocking it right now.'
|
|
2009
|
+
axes: '"Who is blocking it" is computed on three axes: the parenthesis-free rules in the permission table first, then the permission level, and for code execution one more pass over the sandbox. None of the three looks at arguments — the sensitive-path and dangerous-command layer sits ahead of them and can only decide once a real call arrives with real arguments. So whether one specific call gets blocked is not something this table can answer.'
|
|
2010
|
+
# This screen reads once and then stops (the ⚠️ in the header of tools.tsx),
|
|
2011
|
+
# so "which level this was computed against" has to be printed: without it a
|
|
2012
|
+
# stale table and a current one look exactly alike
|
|
2013
|
+
level_line: 'Computed against the "{level}" level. This screen reads once and stops — after changing the level from the composer, you have to leave this section and come back for it to recompute.'
|
|
2014
|
+
level_unknown: 'This session’s permission level could not be read, so the "who is blocking it" column could not be computed this time.'
|
|
2015
|
+
# Two causes, two sentences. Merged into one, the first (start another
|
|
2016
|
+
# session and it is fine) reads as the second (this process is broken)
|
|
2017
|
+
ungraded_no_session: 'This session’s permission level could not be read (most likely the session was left behind by an earlier process), so nothing below is graded — only the tools themselves are listed.'
|
|
2018
|
+
ungraded_no_layer: 'This process did not send the permission verdicts down (its permission layer never came up), so nothing below is graded — only the tools themselves are listed.'
|
|
2019
|
+
# With no verdicts the table is not grouped by type: the type comes down the
|
|
2020
|
+
# same path and falls back to "command" when the server cannot read it, so
|
|
2021
|
+
# grouping by it would paint an "every tool is a command" fiction
|
|
2022
|
+
g_all: 'All tools'
|
|
2023
|
+
|
|
2024
|
+
# The five operation types. Groups run loosest to tightest (read → write →
|
|
2025
|
+
# command → network → code execution); rationale on TYPE_ORDER in tools.tsx
|
|
2026
|
+
t_file_read: 'Read files'
|
|
2027
|
+
t_file_write: 'Write files'
|
|
2028
|
+
t_command: 'Run commands'
|
|
2029
|
+
t_network: 'Network access'
|
|
2030
|
+
t_code_exec: 'Execute code'
|
|
2031
|
+
# The engine added a type this build has not claimed yet — dropping those
|
|
2032
|
+
# rows looks exactly like "nothing happened", so they land here instead
|
|
2033
|
+
# (same rationale as orphans in the config panes)
|
|
2034
|
+
t_other: 'Other'
|
|
2035
|
+
|
|
2036
|
+
col_tool: 'Tool'
|
|
2037
|
+
col_source: 'Source'
|
|
2038
|
+
col_desc: 'What it does'
|
|
2039
|
+
col_gate: 'Who blocks it'
|
|
2040
|
+
col_calls: 'Calls'
|
|
2041
|
+
|
|
2042
|
+
v_allow: 'allowed'
|
|
2043
|
+
v_ask: 'asks first'
|
|
2044
|
+
v_deny: 'denied'
|
|
2045
|
+
# The fourth case: at this level the verdict for this tool depends on the
|
|
2046
|
+
# arguments. That is a truth, not a warning — hence the neutral colour
|
|
2047
|
+
# rather than warning yellow (rationale in the header of settings-tools.css)
|
|
2048
|
+
v_depends: 'depends'
|
|
2049
|
+
# The fifth: no `gate` at all. "It is blocked" and "we cannot tell" have to
|
|
2050
|
+
# be two different sentences
|
|
2051
|
+
v_none: 'no verdict'
|
|
2052
|
+
|
|
2053
|
+
# The "who decided" half-sentence, three axes and three phrasings. Merging
|
|
2054
|
+
# them leaves nothing to explain with (rationale in the JSDoc on
|
|
2055
|
+
# `WireToolGateAxis`)
|
|
2056
|
+
by_none: 'not graded'
|
|
2057
|
+
by_sandbox: 'sandbox isolation'
|
|
2058
|
+
by_level: '{level} level'
|
|
2059
|
+
# Fallback for a missing rule string. Inventing one has a very concrete
|
|
2060
|
+
# cost: the user goes looking for it in their own config file and it is not
|
|
2061
|
+
# there
|
|
2062
|
+
by_rule: 'a rule'
|
|
2063
|
+
cond: '{count} argument-scoped rules could change this'
|
|
2064
|
+
|
|
2065
|
+
# The only exit from this pane. It points at "where to change it", not
|
|
2066
|
+
# "click here to change it": the security centre is read-only too, and the
|
|
2067
|
+
# real path to adding a rule runs through a config file — `perm_where` says
|
|
2068
|
+
# that, and it is not copied a second time here
|
|
2069
|
+
how_to_block: 'To block a tool, add a deny rule to the permission table. Rules only count once they are in a config file, and this interface never writes config — the security centre shows which rules exist today and which layer each came from.'
|
|
2070
|
+
goto_rules: 'Open the rule table'
|
|
2071
|
+
mcp_note: 'Tools from MCP servers are in this table too, named mcp__<server>__<tool>. The Source column says which server each one came from.'
|
|
2072
|
+
|
|
2073
|
+
# Settings pane seven, "About" (`settings/about.tsx`). Its data rides down on
|
|
2074
|
+
# the first-paint `GET /api/config`, so this pane has no sessionId — and
|
|
2075
|
+
# therefore no "no session yet" state.
|
|
2076
|
+
#
|
|
2077
|
+
# It exists for one reason: **when something goes wrong, screenshotting this
|
|
2078
|
+
# screen beats describing the symptoms**. Every entry follows from that — a
|
|
2079
|
+
# field earns its place if pasting it into an issue saves a round trip.
|
|
2080
|
+
about:
|
|
2081
|
+
loading: 'Reading…'
|
|
2082
|
+
lede: 'When something goes wrong, paste a screenshot of this into the issue — it beats describing the symptoms.'
|
|
2083
|
+
|
|
2084
|
+
g_machine: 'This machine'
|
|
2085
|
+
k_version: 'Version'
|
|
2086
|
+
# Three facts on one line rather than three: this line exists to be copied
|
|
2087
|
+
# out whole
|
|
2088
|
+
version_line: 'epoch {version} · Node {node} · {platform}'
|
|
2089
|
+
k_home: 'Data directory'
|
|
2090
|
+
home_hint: 'Sessions, config and logs all live here. Set EPOCH_HOME to move the lot somewhere else.'
|
|
2091
|
+
k_db: 'Session database'
|
|
2092
|
+
# The most common answer to "where did my sessions go" is on this line: a
|
|
2093
|
+
# different profile
|
|
2094
|
+
db_hint: 'Every session’s record is in this one file. It follows the data directory and the profile — switching profile means switching to an entirely separate set of data.'
|
|
2095
|
+
k_profile: 'Profile'
|
|
2096
|
+
# The wire omits this key entirely when it equals the default, so the row is
|
|
2097
|
+
# not drawn. Its presence is itself the information: your paths differ from
|
|
2098
|
+
# everyone else’s
|
|
2099
|
+
profile_hint: 'Chosen with EPOCH_PROFILE or --profile. The default profile does not draw this row — seeing it here means the two paths above differ from everyone else’s.'
|
|
2100
|
+
|
|
2101
|
+
g_diag: 'Startup diagnostics'
|
|
2102
|
+
diag_lede: 'What this process checked as it came up. The sidebar only draws the ones with a problem; this is all four kinds.'
|
|
2103
|
+
# No entries is **not** "all clear": a healthy module still records an ok.
|
|
2104
|
+
# The two have to be two different sentences
|
|
2105
|
+
diag_none: 'This process sent no diagnostics at all. That is not the same as "all clear" — a module that self-checked records an ok even when nothing is wrong.'
|
|
2106
|
+
# ⚠️ `detail` is server-generated prose and follows the server process’s own
|
|
2107
|
+
# language: with the browser switched to English this line stays Chinese.
|
|
2108
|
+
# Expected (acceptance 31), same rationale as the sidebar. Chinese uses a
|
|
2109
|
+
# full-width colon with no space, English a half-width one with a space —
|
|
2110
|
+
# which is the whole reason this key exists
|
|
2111
|
+
diag_line: '{module}: {detail}'
|
|
2112
|
+
|
|
2113
|
+
# No update check, but the route to upgrading still has to be named: the
|
|
2114
|
+
# command depends on how you installed, and the code that can tell lives in
|
|
2115
|
+
# the CLI layer, out of the web server’s reach. So this gives a command you
|
|
2116
|
+
# can type, not a button that spins and then has nothing to say
|
|
2117
|
+
# (decision 20 ①)
|
|
2118
|
+
# ⚠️ 2026-08-18 trimmed — see the zh comment for the ledger.
|
|
2119
|
+
update_note: 'Run this in a terminal: it checks for a new version and tells you how to upgrade:'
|
|
2120
|
+
|
|
2121
|
+
# The Automation screen (plan 45 PR-3). Two tabs: scheduled tasks / run history.
|
|
2122
|
+
#
|
|
2123
|
+
# ⚠️ This section deliberately does NOT hold the eight outcomes, the frequency
|
|
2124
|
+
# summaries, the weekdays, or the four permission-level words — those live in
|
|
2125
|
+
# `schedule.*` and `web.ui.perm.lvl_*`, shared with the CLI and the composer
|
|
2126
|
+
# pill. Criterion: the same thing must not read two ways on two paths (full
|
|
2127
|
+
# reasoning in `packages/web/src/automation/labels.ts`).
|
|
2128
|
+
auto:
|
|
2129
|
+
title: 'Automation'
|
|
2130
|
+
back: 'Back to session'
|
|
2131
|
+
tab_tasks: 'Scheduled tasks'
|
|
2132
|
+
tab_runs: 'Run history'
|
|
2133
|
+
loading: 'Loading…'
|
|
2134
|
+
dismiss: 'Got it'
|
|
2135
|
+
|
|
2136
|
+
# The banner at the top. The slot is kept from the screenshot, but the
|
|
2137
|
+
# wording is swapped for the truth — WorkBuddy's "do not shut down your
|
|
2138
|
+
# computer or quit the client" is exactly what we are here to remove (§2.4).
|
|
2139
|
+
# The three variants are probed from `capability`, not hard-coded.
|
|
2140
|
+
notice_os: 'You do not need to keep epoch running — the OS scheduler wakes these tasks up. But occurrences due while the machine is off, or while nobody is logged in, are skipped and never caught up.'
|
|
2141
|
+
notice_unsupported: 'This platform cannot register scheduled tasks with the OS scheduler yet (Windows and macOS only for now). Tasks can be created, but nothing will fire them.'
|
|
2142
|
+
# ⚠️ This line is the one thing §2.6 really has to hold: when the host has
|
|
2143
|
+
# no headless entry point, say **"quit the app and it stops"** out loud.
|
|
2144
|
+
# Never dress it up as "enabled".
|
|
2145
|
+
# ⚠️ No markdown here — plain text, see the zh catalog.
|
|
2146
|
+
notice_host: 'This app has no headless entry point the OS scheduler can wake. Scheduled tasks only run while the app is open: quit the app and they stop.'
|
|
2147
|
+
|
|
2148
|
+
# Empty state. **One sentence and one button; no template cards** (the six
|
|
2149
|
+
# templates belong to PR-4).
|
|
2150
|
+
empty_title: 'Start your first automation'
|
|
2151
|
+
empty_hint: "Have the agent do one thing for you on a schedule: sweep the repo, round up this week's PRs, scan for stale docs. Each run leaves something you can read."
|
|
2152
|
+
new: 'New task'
|
|
2153
|
+
|
|
2154
|
+
mark_host: 'Runs only while the app is open'
|
|
2155
|
+
never_run: 'Never run'
|
|
2156
|
+
next_none: 'Will not fire again'
|
|
2157
|
+
next_at: 'Next {when}'
|
|
2158
|
+
last_at: 'Last {status} · {when}'
|
|
2159
|
+
run_now: 'Run once now'
|
|
2160
|
+
enable: 'Enable'
|
|
2161
|
+
disable: 'Disable'
|
|
2162
|
+
edit: 'Edit'
|
|
2163
|
+
remove: 'Delete'
|
|
2164
|
+
# ⚠️ This confirmation must say the OS registration goes with it: that state
|
|
2165
|
+
# lives outside epoch, and the user can see it in Task Scheduler / LaunchAgents.
|
|
2166
|
+
remove_confirm: 'Delete "{name}"? Its registration in the OS scheduler goes with it. Run recordings are kept.'
|
|
2167
|
+
|
|
2168
|
+
# ---- Form ----
|
|
2169
|
+
form_new: 'New scheduled task'
|
|
2170
|
+
form_edit: 'Edit scheduled task'
|
|
2171
|
+
f_name: 'Name'
|
|
2172
|
+
f_prompt: 'Prompt'
|
|
2173
|
+
f_prompt_hint: 'What the agent should do when it fires. Say what it should leave behind — nobody will be there to ask.'
|
|
2174
|
+
f_workdir: 'Workspace (optional)'
|
|
2175
|
+
f_workdir_hint: 'Absolute path on the server'
|
|
2176
|
+
# ⚠️ "Optional" still has to land on a **definite** directory: leaving it
|
|
2177
|
+
# empty does not mean "the server process cwd" — the cwd the OS scheduler
|
|
2178
|
+
# hands us is not ours to choose.
|
|
2179
|
+
f_workdir_note: "Left empty, it runs in ~/.epoch/automation/<task id>/. It does **not** inherit any terminal's current directory."
|
|
2180
|
+
f_workdir_default: '~/.epoch/automation/<task id>/'
|
|
2181
|
+
f_model: 'Model (optional)'
|
|
2182
|
+
f_model_hint: 'Empty uses the configured one'
|
|
2183
|
+
f_kind: 'Frequency'
|
|
2184
|
+
k_daily: 'Daily'
|
|
2185
|
+
k_weekly: 'Weekly'
|
|
2186
|
+
k_monthly: 'Monthly'
|
|
2187
|
+
k_interval: 'Every N'
|
|
2188
|
+
k_once: 'Once'
|
|
2189
|
+
f_at: 'Time'
|
|
2190
|
+
f_date: 'Date'
|
|
2191
|
+
f_every: 'Interval'
|
|
2192
|
+
f_weekdays: 'Days of week'
|
|
2193
|
+
f_days: 'Days of month'
|
|
2194
|
+
f_days_hint: '1, 15'
|
|
2195
|
+
f_days_note: 'The 31st does not fire in short months — that is the native behaviour on both platforms.'
|
|
2196
|
+
f_start: 'Active from (optional)'
|
|
2197
|
+
f_end: 'Active until (optional)'
|
|
2198
|
+
f_permission: 'Permission level'
|
|
2199
|
+
# ⚠️ The §3.4 rule-3 acknowledgement. **Name the workspace in it** — an
|
|
2200
|
+
# "I understand" with no object acknowledges nothing.
|
|
2201
|
+
f_bypass_ack: 'I understand: this task will do anything without asking inside {workDir}, including deleting files and running arbitrary commands.'
|
|
2202
|
+
f_turns: 'Max turns'
|
|
2203
|
+
f_timeout: 'Timeout (minutes)'
|
|
2204
|
+
f_timeout_note: 'The only ruler that catches "a command hung" — turns and spend both stop growing there.'
|
|
2205
|
+
f_budget: 'Max spend per run (USD)'
|
|
2206
|
+
# ⚠️ This line explains the one deliberate piece of friction in the whole
|
|
2207
|
+
# plan (§3.5 / §12.2). A required field with no reason behind it just gets
|
|
2208
|
+
# a random number typed into it — and this friction exists precisely to
|
|
2209
|
+
# make the user think about that number once, at creation time.
|
|
2210
|
+
# **Do not soften it.**
|
|
2211
|
+
f_budget_why: 'Required, and we give no default: nobody is watching this task, and a fake default would make "woke up to $40" our fault.'
|
|
2212
|
+
save_and_run: 'Save and run once'
|
|
2213
|
+
save_only: 'Save only'
|
|
2214
|
+
cancel: 'Cancel'
|
|
2215
|
+
blocked_budget: 'Enter a budget above 0 to save'
|
|
2216
|
+
blocked_bypass: 'Tick the acknowledgement to save'
|
|
2217
|
+
|
|
2218
|
+
# ---- Unattended allowlist ----
|
|
2219
|
+
allow_title: 'This task will do these without asking'
|
|
2220
|
+
allow_lede: 'The permission level answers "does this need a human"; this list answers "it does, but nobody is there — which ones can go through anyway". Listed one by one so you can see them before you save.'
|
|
2221
|
+
# ⚠️ These two read **opposite** reasons, and the read-only one carries an
|
|
2222
|
+
# extra half sentence: what it denies is not a missing grant, so it never
|
|
2223
|
+
# becomes an IOU and adding a rule will never be consulted.
|
|
2224
|
+
allow_off_plan: 'The read-only level has no authorisation surface at all, so no list is needed. ⚠️ What it denies is **not a missing grant**: it never becomes an IOU, and adding rules here will not help — change the level instead.'
|
|
2225
|
+
allow_off_bypass: 'Under "skip every check" this list means nothing: the task will do anything without asking. Leaving a list that looks live would make you think you still have a boundary.'
|
|
2226
|
+
allow_ops: 'Whole operation classes'
|
|
2227
|
+
allow_tools: 'Whole tools (one per line)'
|
|
2228
|
+
allow_tools_hint: 'terminal'
|
|
2229
|
+
allow_rules: 'Rules (one per line)'
|
|
2230
|
+
allow_rules_hint: "terminal(git status)\nfile_write(./reports/**)\nmcp__github__*"
|
|
2231
|
+
allow_rules_note: 'A connector goes in as mcp__<server>__*; "only write this directory" goes in as file_write(<dir>/**).'
|
|
2232
|
+
allow_scope: 'This list applies to this task only and is not affected by the global non-interactive allowlist.'
|
|
2233
|
+
|
|
2234
|
+
# ---- Registration outcome ----
|
|
2235
|
+
reg_warn_source: "What got registered is this checkout's path — move or delete the repository and this task will fail."
|
|
2236
|
+
reg_unsupported: 'This platform cannot register scheduled tasks yet (Windows and macOS only for now), so nothing will fire it.'
|
|
2237
|
+
reg_npx: 'An epoch run through npx cannot register scheduled tasks: its entry point lives in a temporary cache, and one cache clear kills the task.'
|
|
2238
|
+
reg_no_runner: 'This app has no headless entry point the OS scheduler can wake, so this task only runs while the app is open.'
|
|
2239
|
+
reg_backend_error: 'The OS scheduler reported an error: {detail}'
|
|
2240
|
+
|
|
2241
|
+
# ---- Run history ----
|
|
2242
|
+
runs_empty_none: 'No runs yet — there is not a single scheduled task. Create one in the "Scheduled tasks" tab.'
|
|
2243
|
+
runs_empty_never: 'No runs yet. It will fire on its own when due; if you cannot wait, hit "Run once now" over in the tasks tab.'
|
|
2244
|
+
run_manual: 'manual'
|
|
2245
|
+
run_meta: '{turns} turns · ${cost}'
|
|
2246
|
+
run_orphan: '(task deleted)'
|
|
2247
|
+
just_ran: 'Just ran: {status}'
|
|
2248
|
+
replay_show: 'View recording'
|
|
2249
|
+
replay_hide: 'Hide recording'
|
|
2250
|
+
replay_loading: 'Loading recording…'
|
|
2251
|
+
replay_none: 'No recording for this one (no session was started, or retention has pruned it).'
|
|
2252
|
+
replay_truncated: 'This recording is long; only the first {count} frames are drawn.'
|
|
2253
|
+
|
|
2254
|
+
# ---- IOUs ----
|
|
2255
|
+
# ⚠️ This block and `allow_off_plan` above are **deliberately two different
|
|
2256
|
+
# sentences**: the next step for an IOU is adding a rule (the button
|
|
2257
|
+
# below), while a level-decided denial would never consult that rule — the
|
|
2258
|
+
# only meaningful move there is changing the level.
|
|
2259
|
+
pending_header: '{count} operations should have asked you, but nobody was there (IOUs)'
|
|
2260
|
+
pending_rule: '→ {rule}'
|
|
2261
|
+
pending_no_rule: '(no exact rule can be derived for this one)'
|
|
2262
|
+
pending_fix: 'Add to the allowlist'
|
|
2263
|
+
pending_fixed: '✓ Added {count}: {rules}'
|
|
2264
|
+
pending_fixed_none: 'Nothing was added — no exact rule can be derived for these, and hard-coding one would only put a dead rule in the list.'
|
|
2265
|
+
|
|
2266
|
+
# ---- Validation results (the server sends codes; this is this screen''s wording) ----
|
|
2267
|
+
#
|
|
2268
|
+
# ⚠️ **Deliberately not shared with `schedule.issue.*`**: that group names
|
|
2269
|
+
# command-line flags (`--budget`, `--max-turns`), and there is no such
|
|
2270
|
+
# thing in a form. The criterion was written down in core's
|
|
2271
|
+
# `schedule/types.ts` long before this screen existed.
|
|
2272
|
+
issue:
|
|
2273
|
+
name_empty: 'Give it a name'
|
|
2274
|
+
prompt_empty: 'Say what it should do when it fires — a scheduled task with no prompt does nothing'
|
|
2275
|
+
permission_unknown: 'Unknown permission level: {detail}'
|
|
2276
|
+
work_dir_missing: 'This directory does not exist: {detail}'
|
|
2277
|
+
budget_missing: 'Enter a number above 0 (currently {detail})'
|
|
2278
|
+
max_turns_invalid: 'Must be a whole number between 1 and 1000 (currently {detail})'
|
|
2279
|
+
timeout_invalid: 'Must be positive and at most 24 hours (currently {detail} ms)'
|
|
2280
|
+
trigger_time_invalid: 'The time must be written HH:mm (currently {detail})'
|
|
2281
|
+
trigger_weekdays_empty: 'Pick at least one day'
|
|
2282
|
+
trigger_days_invalid: 'Days of month must be between 1 and 31 ({detail})'
|
|
2283
|
+
trigger_interval_invalid: 'That interval will not work ({detail} minutes) — only divisors of 60 and whole hours are allowed, because macOS can only enumerate times and an interval that does not divide evenly drifts at each hour boundary'
|
|
2284
|
+
trigger_once_date_invalid: 'The date must be written YYYY-MM-DD and must be a day that exists (currently {detail})'
|
|
2285
|
+
trigger_once_past: 'That moment has already passed; saving it would never fire ({detail})'
|
|
2286
|
+
date_range_invalid: 'The active range is wrong: {detail}'
|
|
2287
|
+
date_range_on_once: 'A run-once task cannot carry an active range — it is a date already'
|
|
2288
|
+
rule_invalid: '{detail}'
|
|
2289
|
+
bypass_needs_workdir: 'This level must be bound to a workspace — "skip every check" plus an undefined directory is the literal meaning of "unbounded"'
|
|
2290
|
+
bypass_workdir_too_broad: 'The workspace for this level cannot be the home directory or the filesystem root (currently {detail})'
|
|
2291
|
+
bypass_needs_limits: 'Under this level, budget / turns / timeout must all have values'
|
|
2292
|
+
bypass_not_confirmed: 'This level needs the acknowledgement ticked first'
|
|
2293
|
+
|
|
2294
|
+
# Workspaces (decision 18: one bound per session). Both groups are user-facing:
|
|
2295
|
+
# `recent_*` goes into startup diagnostics, `bind_*` goes straight into the
|
|
2296
|
+
# 400 / 409 body of `POST /api/sessions`.
|
|
2297
|
+
workspace:
|
|
2298
|
+
recent_unreadable: 'could not read the recent-workspaces list {path} ({reason}); treating it as empty for this run'
|
|
2299
|
+
recent_not_object: 'the recent-workspaces list {path} is not a JSON object at the top level; treating it as empty'
|
|
2300
|
+
recent_bad_version: 'the recent-workspaces list {path} declares version {version}, this build only understands {expected}; treating it as empty'
|
|
2301
|
+
recent_not_array: '`workspaces` in {path} is not an array; treating the list as empty'
|
|
2302
|
+
recent_dropped: 'the recent-workspaces list {path} had {count} malformed entries, which were ignored'
|
|
2303
|
+
recent_write_failed: 'could not write the recent-workspaces list {path}: {reason}'
|
|
2304
|
+
bind_not_absolute: 'a workspace must be an absolute path: {path}'
|
|
2305
|
+
bind_missing: 'no such directory: {path}'
|
|
2306
|
+
bind_not_a_directory: 'not a directory: {path}'
|
|
2307
|
+
bind_locked: 'session {sessionId} is already bound to {current} and cannot be changed to {next}'
|
|
2308
|
+
# The persisted copy did not make it (session DB v7, 2026-08-19). This does NOT
|
|
2309
|
+
# block the bind — the workspace is bound and this run works fine; what is lost
|
|
2310
|
+
# is "still knowing where it worked after a restart". So the wording has to say
|
|
2311
|
+
# what failed AND what follows from it: next launch the user sees that session
|
|
2312
|
+
# fall into "Earlier sessions", and they need to be able to connect the two.
|
|
2313
|
+
decision_write_failed: "could not record session {sessionId}'s workspace decision in the session database ({reason}) — this run is unaffected, but after a restart it will not know which space it belongs to"
|
|
2314
|
+
|
|
2315
|
+
# Host preset (runtime/src/host-preset.ts, plan 54 §6). The whole group goes into
|
|
2316
|
+
# the startup diagnostics.
|
|
2317
|
+
#
|
|
2318
|
+
# There is exactly one criterion behind the wording: **"configured but had no
|
|
2319
|
+
# effect" is the only way this feature fails**, and before this group it failed
|
|
2320
|
+
# in complete silence — the host passes `hostPreset`, then sees a bare
|
|
2321
|
+
# `Provider: no usable credential`, with nothing anywhere saying "but I did
|
|
2322
|
+
# preset one". So every line answers "what happened to this item": written
|
|
2323
|
+
# (which file, which keys), not written (the user already has it), or broken
|
|
2324
|
+
# (why).
|
|
2325
|
+
#
|
|
2326
|
+
# The `kept` lines are **ok, not warn**: not overwriting the user's choice is
|
|
2327
|
+
# this feature working correctly, not a problem.
|
|
2328
|
+
host_preset:
|
|
2329
|
+
wrote: 'wrote {path}: {keys}'
|
|
2330
|
+
kept: 'the user already configured these in {path}, left untouched: {keys}'
|
|
2331
|
+
write_failed: 'could not write {path} ({reason}); this item did not take effect'
|
|
2332
|
+
home_unwritable: 'could not create the data directory {path} ({reason}); no part of the preset took effect'
|
|
2333
|
+
section_inline: '{section} in {path} uses inline (flow) syntax, and inserting fields would corrupt it — that section was not preset'
|
|
2334
|
+
nothing_to_do: 'every item in the preset was already in place; nothing was written'
|
|
2335
|
+
secret_stored: 'the API key went to the injected secret store ({name}), never to disk'
|
|
2336
|
+
secret_kept: 'the secret store already holds {name}; the preset key did not overwrite it'
|
|
2337
|
+
secret_failed: 'could not store the API key in the secret store ({name}: {reason}); there is no usable credential this run'
|
|
2338
|
+
secret_not_needed: '{provider} does not take an API key; the one in the preset was ignored'
|
|
2339
|
+
mcp_wrote: 'wrote these MCP servers to {path}: {names}'
|
|
2340
|
+
mcp_kept: '{path} already has MCP servers with these names, left untouched: {names}'
|
|
2341
|
+
mcp_unreadable: 'could not read {path} ({reason}); none of the preset MCP servers were written — that file is yours and we will not overwrite it'
|
|
2342
|
+
mcp_no_servers: '{path} has no `servers` section (did you write `mcpServers`?); none of the preset MCP servers were written'
|
|
2343
|
+
workspaces_wrote: 'added to the known-workspaces list: {roots}'
|
|
2344
|
+
workspaces_kept: 'these are already in the known-workspaces list; their order was left alone: {roots}'
|
|
2345
|
+
workspace_not_absolute: 'a known workspace must be an absolute path, skipped: {path}'
|
|
2346
|
+
|
|
2347
|
+
# Diagnostics for loading `~/.epoch/keybindings.json` (core/src/config/keybindings.ts).
|
|
2348
|
+
# All three mean "this line of yours did not take effect", so each must name the key
|
|
2349
|
+
# involved — the user needs that exact string to go and fix the file. Key names are
|
|
2350
|
+
# spelled the way the config spells them (`ctrl+l`, not `Ctrl+L`) so they can be
|
|
2351
|
+
# copy-pasted.
|
|
2352
|
+
keybindings:
|
|
2353
|
+
bad_chord: '{key} is not a recognizable key; this binding was ignored'
|
|
2354
|
+
reserved: '{key} is reserved and cannot be rebound (it is the escape hatch when things hang); this binding was ignored'
|
|
2355
|
+
conflict: '{key} is already bound to {first}; this binding was ignored'
|
|
2356
|
+
|
|
2357
|
+
marketplace:
|
|
2358
|
+
fetch_failed: 'failed to fetch {url}: HTTP {status}'
|
|
2359
|
+
fetch_failed_private: 'failed to fetch {url}: HTTP {status}. Either the address is wrong or it is private — marketplace catalogs are fetched without any credentials; for a private one, clone it locally and run epoch plugin marketplace add ./that-directory'
|
|
2360
|
+
fetch_error: 'failed to fetch {url}: {reason}'
|
|
2361
|
+
|
|
2362
|
+
# Runtime text for scheduled tasks (plan 45). The command help lives above under
|
|
2363
|
+
# `cli.schedule`.
|
|
2364
|
+
#
|
|
2365
|
+
# Two hard rules about wording:
|
|
2366
|
+
# 1. The four permission words are **verbatim the chat pill's** (design note 20)
|
|
2367
|
+
# — the user learned them once already; a second vocabulary means learning
|
|
2368
|
+
# two names for the same thing
|
|
2369
|
+
# 2. "skipped" and "failed" never share a mark. Showing "the previous run was
|
|
2370
|
+
# still going, so this one was skipped" as a red cross sends the user
|
|
2371
|
+
# hunting for a failure that never happened
|
|
2372
|
+
schedule:
|
|
2373
|
+
list_header: 'Scheduled tasks:'
|
|
2374
|
+
list_empty: 'No scheduled tasks yet.'
|
|
2375
|
+
list_empty_hint: 'Create one: epoch schedule add --name "daily sweep" --prompt "…" --daily 09:00 --budget 0.50'
|
|
2376
|
+
col_next: 'next'
|
|
2377
|
+
col_last: 'last'
|
|
2378
|
+
disabled_mark: 'disabled'
|
|
2379
|
+
yes: 'yes'
|
|
2380
|
+
no: 'no'
|
|
2381
|
+
field_enabled: 'Enabled'
|
|
2382
|
+
field_trigger: 'Frequency'
|
|
2383
|
+
field_window: 'Active window'
|
|
2384
|
+
field_next: 'Next run'
|
|
2385
|
+
field_workdir: 'Working directory'
|
|
2386
|
+
field_model: 'Model'
|
|
2387
|
+
field_permission: 'Permission level'
|
|
2388
|
+
field_limits: 'Three rulers'
|
|
2389
|
+
field_backend: 'Registered in'
|
|
2390
|
+
field_prompt: 'Prompt'
|
|
2391
|
+
field_last_run: 'Last run: {when}'
|
|
2392
|
+
limits_value: 'at most {turns} turns · at most {budget} USD · killed after {minutes} minutes'
|
|
2393
|
+
allowlist_header: 'This task will do the following without asking:'
|
|
2394
|
+
allowlist_empty: '(the list is empty — anything needing approval becomes an IOU and the run ends)'
|
|
2395
|
+
allowlist_scope_note: 'This list applies to this task only; the global non-interactive allowlist does not affect it.'
|
|
2396
|
+
allowlist_bypass: 'Permission level is "skip all checks": the list means nothing here — the task will do anything without asking.'
|
|
2397
|
+
allowlist_readonly: 'Permission level is "read-only": zero authorization surface by construction, no list needed.'
|
|
2398
|
+
created: '✓ Created and registered: {name} ({id})'
|
|
2399
|
+
created_next: 'Run it once now and find out what authorization it is missing: epoch schedule run {id}'
|
|
2400
|
+
ask_name: 'Task name'
|
|
2401
|
+
ask_prompt: 'What should the agent do when it fires'
|
|
2402
|
+
ask_at: 'What time each day (HH:mm)'
|
|
2403
|
+
ask_budget: 'Most this single run may spend, in USD'
|
|
2404
|
+
ask_permission: 'Permission level'
|
|
2405
|
+
permission:
|
|
2406
|
+
plan: 'read-only'
|
|
2407
|
+
default: 'ask when needed'
|
|
2408
|
+
accept_edits: 'auto-accept edits'
|
|
2409
|
+
bypass: 'skip all checks'
|
|
2410
|
+
auto: 'auto-allow'
|
|
2411
|
+
trigger:
|
|
2412
|
+
daily: 'every day at {at}'
|
|
2413
|
+
weekly: 'every {days} at {at}'
|
|
2414
|
+
monthly: 'day {days} of every month at {at}'
|
|
2415
|
+
every_minutes: 'every {n} minutes'
|
|
2416
|
+
every_hours: 'every {n} hours'
|
|
2417
|
+
once: '{date} {at} (once)'
|
|
2418
|
+
weekday:
|
|
2419
|
+
sun: 'Sun'
|
|
2420
|
+
mon: 'Mon'
|
|
2421
|
+
tue: 'Tue'
|
|
2422
|
+
wed: 'Wed'
|
|
2423
|
+
thu: 'Thu'
|
|
2424
|
+
fri: 'Fri'
|
|
2425
|
+
sat: 'Sat'
|
|
2426
|
+
status:
|
|
2427
|
+
ok: 'ok'
|
|
2428
|
+
failed: 'failed'
|
|
2429
|
+
denied: 'missing authorization'
|
|
2430
|
+
timeout: 'timed out'
|
|
2431
|
+
budget: 'limit reached'
|
|
2432
|
+
skipped_overlap: 'skipped (previous run still going)'
|
|
2433
|
+
skipped_window: 'skipped (outside the active window)'
|
|
2434
|
+
missed: 'missed'
|
|
2435
|
+
drift:
|
|
2436
|
+
missing_in_os: 'in the database, not in the OS (registration lost)'
|
|
2437
|
+
orphan_in_os: 'in the OS, not in the database (uninstall leftover)'
|
|
2438
|
+
spec_mismatch: 'in both, but they disagree'
|
|
2439
|
+
never_registered: 'the database row claims it was registered but carries no OS identifier'
|
|
2440
|
+
issue:
|
|
2441
|
+
name_empty: 'the name cannot be empty'
|
|
2442
|
+
prompt_empty: 'the prompt cannot be empty — a scheduled task without one does nothing'
|
|
2443
|
+
permission_unknown: 'unknown permission level: {detail}'
|
|
2444
|
+
work_dir_missing: 'working directory does not exist: {detail}'
|
|
2445
|
+
budget_missing: '--budget is required and must be positive (got {detail})'
|
|
2446
|
+
max_turns_invalid: '--max-turns must be an integer between 1 and 1000 (got {detail})'
|
|
2447
|
+
timeout_invalid: '--timeout must be positive and at most 24 hours (got {detail} ms)'
|
|
2448
|
+
trigger_time_invalid: 'the time must be HH:mm (got {detail})'
|
|
2449
|
+
trigger_weekdays_empty: 'the weekly schedule needs at least one day'
|
|
2450
|
+
trigger_days_invalid: 'days of the month must be between 1 and 31 ({detail})'
|
|
2451
|
+
trigger_interval_invalid: 'the interval must divide 60 or be a whole number of hours (got {detail} minutes) — launchd can only enumerate instants, so an interval that does not divide 60 drifts at every hour boundary'
|
|
2452
|
+
trigger_once_date_invalid: 'the date must be YYYY-MM-DD and must be a day that exists (got {detail})'
|
|
2453
|
+
trigger_once_past: 'that moment has already passed; saving it would never fire ({detail})'
|
|
2454
|
+
date_range_invalid: 'bad active window: {detail}'
|
|
2455
|
+
date_range_on_once: 'a one-shot task cannot carry an active window — it is a single date already'
|
|
2456
|
+
rule_invalid: '{detail}'
|
|
2457
|
+
bypass_needs_workdir: '"skip all checks" must be bound to a workspace — that level plus an undecided directory is the literal meaning of "unbounded"'
|
|
2458
|
+
bypass_workdir_too_broad: 'the workspace for "skip all checks" cannot be your home or the filesystem root (got {detail})'
|
|
2459
|
+
bypass_needs_limits: 'under "skip all checks" the budget, turn and timeout limits are all mandatory'
|
|
2460
|
+
bypass_not_confirmed: '"skip all checks" needs an explicit acknowledgement: add --i-understand-bypass'
|
|
2461
|
+
run_start: 'Running: {name}'
|
|
2462
|
+
run_meta: '{turns} turns · {cost} USD{manual}'
|
|
2463
|
+
run_manual: ' · manual'
|
|
2464
|
+
fire_missing: 'no task {id} in the database; did nothing'
|
|
2465
|
+
fire_disabled: '{id} is disabled; did nothing'
|
|
2466
|
+
pending_header: '{n} operations were denied because nobody was there to ask (IOUs):'
|
|
2467
|
+
pending_line: '{tool} {target}'
|
|
2468
|
+
pending_fix: 'add this rule: {rule}'
|
|
2469
|
+
fix_hint: 'Add these to the allowlist so the next run does not stall: epoch schedule run {id} --fix'
|
|
2470
|
+
fix_done: '✓ Appended {n} rules to the allowlist'
|
|
2471
|
+
fix_none: 'nothing could be appended automatically (no exact rule can be derived from these)'
|
|
2472
|
+
logs_empty: 'No runs recorded yet.'
|
|
2473
|
+
logs_recording: '── most recent recording ({path})'
|
|
2474
|
+
enabled_done: '✓ Enabled: {name}'
|
|
2475
|
+
disabled_done: '✓ Disabled (the task stays in the OS scheduler, just stopped): {name}'
|
|
2476
|
+
removed: '✓ Deleted and unregistered: {name}'
|
|
2477
|
+
removed_logs_kept: 'The recordings are kept in {dir}/; delete them yourself if you want them gone'
|
|
2478
|
+
doctor_clean: '✓ Checked {n}; the database and the OS agree'
|
|
2479
|
+
doctor_found: 'Found {n} disagreements:'
|
|
2480
|
+
doctor_hint: '--repair re-registers from the database (the database is the source of truth); --prune removes OS orphans'
|
|
2481
|
+
doctor_unsupported: 'Scheduled tasks are not supported on this platform yet (Windows and macOS for now)'
|
|
2482
|
+
doctor_repaired: '✓ Re-registered: {name}'
|
|
2483
|
+
doctor_repair_failed: '✗ Could not re-register: {name} ({detail})'
|
|
2484
|
+
doctor_pruned: '✓ Unregistered: {id}'
|
|
2485
|
+
doctor_prune_failed: '✗ Could not unregister: {id} ({detail})'
|
|
2486
|
+
err_not_found: 'no task {id}'
|
|
2487
|
+
err_not_found_hint: 'Run epoch schedule list to see what exists'
|
|
2488
|
+
err_ambiguous: 'the prefix {id} matches more than one task; be more specific:'
|
|
2489
|
+
err_invalid: 'This task has problems and nothing was saved:'
|
|
2490
|
+
err_no_trigger: 'a frequency is required: --daily / --weekly / --monthly / --every / --once'
|
|
2491
|
+
err_many_triggers: 'only one frequency may be given'
|
|
2492
|
+
err_at_required: '--weekly / --monthly / --once need --at HH:mm'
|
|
2493
|
+
err_weekday: 'unrecognized weekday: {value}'
|
|
2494
|
+
err_every: 'unknown --every: {value}'
|
|
2495
|
+
err_every_hint: 'Only these (they are exactly what both platforms can express): {values}'
|
|
2496
|
+
err_permission: 'unknown --permission: {value}'
|
|
2497
|
+
err_operation: 'unknown --allow-operation: {value}'
|
|
2498
|
+
err_budget: '--budget must be a positive number: {value}'
|
|
2499
|
+
err_budget_hint: 'There is deliberately no default: a fake one would make "woke up to 40 dollars" our fault'
|
|
2500
|
+
err_missing_flags: 'required values are missing and there is no terminal here to ask'
|
|
2501
|
+
err_missing_flags_hint: 'At minimum give --name --prompt --budget and a frequency (--daily and friends)'
|
|
2502
|
+
err_npx: 'An epoch run through npx cannot register scheduled tasks — its entry point lives in a temporary cache, so one cache clean breaks the task'
|
|
2503
|
+
err_npx_hint: 'Install it first: npm i -g @epoch-agent/cli'
|
|
2504
|
+
err_no_host_runner: 'This host has no headless entry point the OS scheduler can invoke, so scheduled tasks only run while the app is open'
|
|
2505
|
+
err_no_host_runner_hint: 'To make them run after the app is closed, the host must provide scheduleRunner (see docs/EMBEDDING.md)'
|
|
2506
|
+
err_platform_unsupported: 'Registering scheduled tasks is not supported on this platform yet (Windows and macOS for now)'
|
|
2507
|
+
err_backend: 'the OS scheduler reported an error: {detail}'
|
|
2508
|
+
warn_source_entry: '⚠ The path registered points at this checkout; moving or deleting the repo will break this task'
|
|
2509
|
+
|
|
2510
|
+
# Skill scanning (core/src/skill/scan.ts). See the zh catalog for the reasoning:
|
|
2511
|
+
# the four source labels are separate entries because the sentence mentions two
|
|
2512
|
+
# of them and the two languages order them differently.
|
|
2513
|
+
skill:
|
|
2514
|
+
scope_user: 'user-level'
|
|
2515
|
+
scope_project: 'project-level'
|
|
2516
|
+
scope_plugin: 'plugin'
|
|
2517
|
+
scope_host: 'host-injected'
|
|
2518
|
+
scope_override: 'The {scope} skill "{name}" shadows a same-named {previous} skill'
|
|
2519
|
+
# ---- Import from a local directory (plan 42 §六, 2026-08-18) ----
|
|
2520
|
+
# The gate rationale lives in core/src/skill/import.ts: this path accepts a
|
|
2521
|
+
# server-local **path**, never uploaded bytes. Every string below answers
|
|
2522
|
+
# "which one did not make it, and why" — silent skipping is the worst failure
|
|
2523
|
+
# mode here, because the user walks away believing the import worked.
|
|
2524
|
+
import_no_system: 'The skill system did not start this run, so there is nowhere to import into. Check the Skill entries in the startup diagnostics'
|
|
2525
|
+
import_not_absolute: 'An absolute path is required — a relative one resolves against the server process cwd, which you cannot see from the browser'
|
|
2526
|
+
import_missing: 'Cannot find {path}'
|
|
2527
|
+
import_empty: 'No importable skill under {path} (expected a .md with frontmatter, a folder containing SKILL.md, or either of those zipped up)'
|
|
2528
|
+
import_stale: 'That directory changed after you previewed it — import cancelled. Take another look, then confirm'
|
|
2529
|
+
import_conflict: 'A user-level skill named "{name}" already exists; this one was not imported (no overwriting). To replace it, delete the copy under ~/.epoch/skills first'
|
|
2530
|
+
import_dup_in_batch: 'More than one skill in this batch is named "{name}"; only the first was kept — skill names are globally unique, different categories still collide'
|
|
2531
|
+
import_skipped_entry: '{entry} is not something a skill directory may contain (only SKILL.md plus the references / templates / scripts / assets subdirectories); skipped just this entry'
|
|
2532
|
+
import_skipped_special: '{entry} is not a regular file (a symlink or similar) and was skipped — files in a skill directory do get read'
|
|
2533
|
+
import_too_many_files: 'This skill carries too many files; the rest were not copied — check that you pointed at a skill directory and not a whole repository'
|
|
2534
|
+
import_read_failed: 'Read failed: {reason}'
|
|
2535
|
+
import_write_failed: 'Write failed: {reason}'
|
|
2536
|
+
import_bad_name_empty: 'The skill name is empty'
|
|
2537
|
+
import_bad_name_long: 'The skill name is too long (over {max} characters); it has to work as a directory name'
|
|
2538
|
+
import_bad_name_dots: 'A skill name cannot be . or ..'
|
|
2539
|
+
import_bad_name_char: 'The skill name "{name}" contains characters that cannot be used in a directory name (< > : " / \ | ? * and control characters). The colon in particular separates the plugin/host prefix — a user-level skill taking it would masquerade as a plugin skill'
|
|
2540
|
+
import_bad_name_trailing: 'The skill name "{name}" ends with a dot or a space — on Windows such a directory can be created but not opened'
|
|
2541
|
+
|
|
2542
|
+
# Capabilities injected by an embedding host (runtime/src/build.ts, plan 44 §2.1).
|
|
2543
|
+
# All of these go into the startup diagnostics — see the zh catalog for why each
|
|
2544
|
+
# one has to name the offending entry.
|
|
2545
|
+
host_capabilities:
|
|
2546
|
+
loaded: '{namespace}: {roles} identity/identities, {skillDirs} skill directory/directories, {mcpServers} MCP server(s) (the first two get the {namespace}: prefix, servers get {namespace}__ — a server name ends up inside a tool name, and tool names cannot contain a colon)'
|
|
2547
|
+
bad_role_name: 'Identity "{name}" has an invalid name (lowercase letters, digits and hyphens only, and it may not start with a hyphen); that one was skipped, the rest were injected as usual'
|
|
2548
|
+
duplicate_role: 'Identity "{name}" appears more than once in this batch; only the first one was kept'
|
|
2549
|
+
empty_namespace: 'The namespace is empty, so nothing was injected — without it there is no prefix to apply, and an unprefixed injection can shadow a built-in role'
|
|
2550
|
+
bad_namespace: 'The namespace "{namespace}" is invalid (lowercase letters, digits and hyphens only, and it may not start with a hyphen); nothing was injected'
|
|
2551
|
+
skill_dir_relative: 'The skill directory {path} is not an absolute path and was skipped — relative paths resolve against the engine process cwd, which the host does not control'
|
|
2552
|
+
# Three more from plan 44 PR-2 (the connector row). Server names are validated
|
|
2553
|
+
# more strictly than mcp.json ones because they end up verbatim inside a tool
|
|
2554
|
+
# name — say so, or the host only sees "my server did not come up".
|
|
2555
|
+
bad_mcp_name: 'MCP server "{name}" has an invalid name (lowercase letters, digits and hyphens only, and it may not start with a hyphen); that one was skipped, the rest were injected as usual. These names are validated more strictly than the ones in mcp.json because a server name ends up verbatim inside a tool name'
|
|
2556
|
+
duplicate_mcp: 'MCP server "{name}" appears more than once in this batch; only the first one was kept'
|
|
2557
|
+
mcp_name_taken: 'MCP server "{name}" collides with one already present in the user mcp.json; the host-injected one was skipped — the line the user wrote themselves wins'
|
|
2558
|
+
|
|
2559
|
+
# Startup diagnostic for the agent role table (wireAgentRoles in
|
|
2560
|
+
# runtime/src/build.ts, plan 18). Assembled segment by segment because the
|
|
2561
|
+
# segments depend on what is present — see the zh catalog for the reasoning.
|
|
2562
|
+
agent_roles:
|
|
2563
|
+
tally: '{total} available ({parts}, same-named ones merged)'
|
|
2564
|
+
tally_builtin: '{count} built-in'
|
|
2565
|
+
tally_user: '{count} user'
|
|
2566
|
+
tally_project: '{count} project'
|
|
2567
|
+
tally_plugin: '{count} from plugins'
|
|
2568
|
+
tally_host: '{count} from the host'
|
|
2569
|
+
|
|
2570
|
+
# Per-message identity handoff marker (plan 57 §2.3 / §3.5).
|
|
2571
|
+
# ⚠️ These two are the body only — the "the system is speaking" prefix is NOT
|
|
2572
|
+
# here. It lives in packages/protocol/src/system-note.ts (SYSTEM_NOTE_PREFIX) and
|
|
2573
|
+
# runtime/src/agent-session.ts prepends it. Do not add any prefix back into these
|
|
2574
|
+
# two sentences: that yields a doubled prefix, and nothing anywhere goes red.
|
|
2575
|
+
# See the zh catalog for the reasoning.
|
|
2576
|
+
#
|
|
2577
|
+
# ⚠️ `handoff_cleared` says "identity" where it used to say the old word, and the
|
|
2578
|
+
# zh twin now repeats 「身份」 twice in one sentence — that is deliberate. Before
|
|
2579
|
+
# the rename each half of that sentence used a different noun for the same thing,
|
|
2580
|
+
# which read as two. Do not swap either one back out for a synonym.
|
|
2581
|
+
agent_role:
|
|
2582
|
+
handoff: 'From this message on, "{role}" takes over.'
|
|
2583
|
+
handoff_cleared: 'From this message on, no identity is pinned; back to this session default.'
|
|
2584
|
+
|
|
2585
|
+
# User-facing strings for the plugin side (plan 44 PR-3 / PR-4).
|
|
2586
|
+
#
|
|
2587
|
+
# Two groups, kept together because they must agree with each other: `mcp_*` are
|
|
2588
|
+
# problems the pre-install preview reports while scanning mcp.json
|
|
2589
|
+
# (core/src/plugin/inventory.ts); `n_*` are the numbers `epoch plugin list`
|
|
2590
|
+
# prints afterwards (cli/src/commands/plugin.ts).
|
|
2591
|
+
#
|
|
2592
|
+
# `n_*` are deliberately terse — they are joined with ` · ` onto the line that
|
|
2593
|
+
# already carries the plugin name and version. Use --verbose for the breakdown.
|
|
2594
|
+
plugin:
|
|
2595
|
+
mcp_not_json: 'mcp.json is not valid JSON ({reason}); the whole file was ignored'
|
|
2596
|
+
mcp_not_object: 'The top level of mcp.json must be an object; the whole file was ignored'
|
|
2597
|
+
# Anyone copying a config over from Claude Desktop hits this one. The wording
|
|
2598
|
+
# deliberately matches parseMcpConfig in plugin-mcp — same mistake, one answer.
|
|
2599
|
+
mcp_wrong_key: 'Found an `mcpServers` field; epoch reads `servers`, please rename the key'
|
|
2600
|
+
mcp_no_servers: 'mcp.json has no `servers` field, so there are no MCP servers to use'
|
|
2601
|
+
mcp_servers_not_object: '`servers` must be an object (server name → config); the whole file was ignored'
|
|
2602
|
+
# The ⚠ is here for the same reason as the hook row: these start subprocesses.
|
|
2603
|
+
# Deliberately not phrased as "a security risk" — a plugin that can carry hooks
|
|
2604
|
+
# could already start arbitrary subprocesses. This line answers "how many more
|
|
2605
|
+
# processes will run on my machine once this is installed".
|
|
2606
|
+
preview_mcp: '⚠ {count} MCP server(s): {names} — each is a separate process, started when epoch starts'
|
|
2607
|
+
n_commands: '{count} cmd'
|
|
2608
|
+
n_roles: '{count} roles'
|
|
2609
|
+
n_skills: '{count} skills'
|
|
2610
|
+
n_hooks: '{count} hooks'
|
|
2611
|
+
n_deny: '{count} deny'
|
|
2612
|
+
n_mcp: '{count} MCP'
|
|
2613
|
+
brings_nothing: '(brings nothing)'
|
|
2614
|
+
|
|
2615
|
+
# Startup diagnostics for plugin-provided MCP servers
|
|
2616
|
+
# (runtime/src/plugin-mcp-servers.ts, plan 44 PR-3).
|
|
2617
|
+
#
|
|
2618
|
+
# Same reasoning as the host_capabilities group: "it was brought in but never
|
|
2619
|
+
# came up" is the only failure mode here, and neither the plugin author nor the
|
|
2620
|
+
# user is watching — so name the offending server and say why these names are
|
|
2621
|
+
# validated more strictly than the ones in mcp.json.
|
|
2622
|
+
# Where the text scanner gave up while splicing a server into the user's
|
|
2623
|
+
# hand-written mcp.json (runtime/src/mcp-config-write.ts, 2026-08-18).
|
|
2624
|
+
#
|
|
2625
|
+
# ⚠️ These five **are our own diagnoses, so they get translated**; the other half
|
|
2626
|
+
# of the same path (`EACCES` / `ENOENT` and friends) is Node's own wording and is
|
|
2627
|
+
# **forwarded verbatim, untranslated**. Both land in the {detail} of
|
|
2628
|
+
# `web.mcp_add_unwritable`. That is not an inconsistency — the criterion is
|
|
2629
|
+
# "who said this sentence".
|
|
2630
|
+
mcp_write:
|
|
2631
|
+
not_object: 'the top level is not a JSON object'
|
|
2632
|
+
top_unreadable: 'the top-level object cannot be read through (most likely a missing comma or quote)'
|
|
2633
|
+
servers_not_object: '`servers` is not an object'
|
|
2634
|
+
servers_unreadable: 'the `servers` object cannot be read through'
|
|
2635
|
+
mcp_servers_key: 'this config uses the `mcpServers` key while epoch reads `servers`; rename the key first, then add'
|
|
2636
|
+
invalid: 'the configuration is not valid'
|
|
2637
|
+
|
|
2638
|
+
# Writing mcp.json in full (runtime/src/mcp-config-file.ts, 2026-08-18).
|
|
2639
|
+
#
|
|
2640
|
+
# ⚠️ One string only, and it is the **one** content problem that stops a save:
|
|
2641
|
+
# not valid JSON. Do not add "missing command" / "unknown key" here — those save
|
|
2642
|
+
# just fine and travel in the response's issues (see protocol's
|
|
2643
|
+
# WireMcpConfigIssue).
|
|
2644
|
+
#
|
|
2645
|
+
# ⚠️ {why} is the raw English sentence thrown by JSON.parse ("Unexpected token }
|
|
2646
|
+
# in JSON at position 42") and is **forwarded verbatim, never translated**: it is
|
|
2647
|
+
# the only thing on screen that says which character is wrong.
|
|
2648
|
+
mcp_config:
|
|
2649
|
+
invalid_json: 'Nothing was written — this content is not valid JSON: {why}'
|
|
2650
|
+
|
|
2651
|
+
# Creating a role definition file (core's `agent-role/create.ts`, 2026-08-18).
|
|
2652
|
+
#
|
|
2653
|
+
# ⚠️ `roundtrip` is the important one here: it says "the file I generated cannot be
|
|
2654
|
+
# read back by our own loader" — meaning **not a single byte was written**, rather
|
|
2655
|
+
# than "it was written badly". The wording must point at "reword it / write the file
|
|
2656
|
+
# by hand", not at "try again".
|
|
2657
|
+
#
|
|
2658
|
+
# ⚠️ `taken` must carry the reason for not overwriting and not auto-renaming, same
|
|
2659
|
+
# judgement as `web.mcp_add_taken`: the body of a role file is prose the user wrote,
|
|
2660
|
+
# so overwriting deletes it; and the name becomes the word used to delegate work
|
|
2661
|
+
# (the `role` argument of delegate_task).
|
|
2662
|
+
role_write:
|
|
2663
|
+
bad_name: 'The identity name "{name}" cannot be used — lowercase letters, digits and hyphens only, and it may not start with a hyphen. This name is both the file name and the word used to delegate work (the `role` argument of delegate_task, the value of --agent), which is why it is validated strictly.'
|
|
2664
|
+
taken: 'There is already an identity named "{name}" — pick another name. Nothing is overwritten and nothing is auto-renamed here: overwriting would delete the prose you wrote in that file, and renaming would make the word used to delegate work disagree with what you typed.'
|
|
2665
|
+
roundtrip: 'Nothing was written — our own loader cannot read back the file that was generated ({why}). Most likely a passage in the description or the body breaks the YAML; reword it and try again, or write the file by hand in ~/.epoch/agents/.'
|
|
2666
|
+
roundtrip_generic: 'it reads back as nothing'
|
|
2667
|
+
# ⚠️ The duplicate sentence must name **which layer** holds the name: colliding with
|
|
2668
|
+
# a built-in leaves only "pick another name", while colliding with a project-level
|
|
2669
|
+
# one also lets the user edit that file in their own repo. Just saying "name taken"
|
|
2670
|
+
# would make someone who collided with `general` go looking for an invisible file
|
|
2671
|
+
# in their own ~/.epoch/agents/.
|
|
2672
|
+
dup_in_table: 'There is already an identity named "{name}" (from {layer}), so that name is unavailable — pick another. Nothing is overwritten and nothing is auto-renamed here: overwriting a built-in identity would also replace the default target for delegated work, and renaming would make the word used to delegate work disagree with what you typed.'
|
|
2673
|
+
layer_builtin: 'the built-in set'
|
|
2674
|
+
layer_user: 'your own ~/.epoch/agents/'
|
|
2675
|
+
layer_project: "this workspace's .epoch/agents/"
|
|
2676
|
+
layer_plugin: 'an installed plugin'
|
|
2677
|
+
layer_host: 'the application embedding this engine'
|
|
2678
|
+
unknown_tools: 'These tool names do not exist: {tools}. They are not in the tool table — fix the typo, or take a look at Settings › Tools if you are unsure what is available. (Writing them anyway would not fail: it would leave a warning in the next startup diagnostics and silently give this identity fewer tools than you asked for.)'
|
|
2679
|
+
blocked_tools: 'No identity can be given {tools} — those are a hard floor (they prevent recursive delegation and privilege escalation), not something this whitelist got wrong. Remove them from the tools field.'
|
|
2680
|
+
|
|
2681
|
+
plugin_mcp:
|
|
2682
|
+
unreadable: 'Could not read {path} ({reason}); none of the MCP servers this plugin carries were started'
|
|
2683
|
+
bad_name: 'MCP server "{name}" from plugin {plugin} has an invalid name (lowercase letters, digits and hyphens only, and it may not start with a hyphen); that one was skipped, the rest are unaffected. These names are validated more strictly than the ones in ~/.epoch/mcp.json because a server name ends up verbatim inside a tool name sent to the model — one invalid name gets the entire tool table rejected'
|
|
2684
|
+
name_taken: 'MCP server "{name}" collides with one that is already present (from the user mcp.json or from the host); the plugin one was skipped — whichever came first wins'
|
|
2685
|
+
|
|
2686
|
+
# Long-task goals (plan 52) — every string the `/goal` command family prints.
|
|
2687
|
+
#
|
|
2688
|
+
# ## Why this group is in the catalog while the `blocked` reason text is not
|
|
2689
|
+
#
|
|
2690
|
+
# Different readers; the criterion lives in the file header of
|
|
2691
|
+
# `core/src/goal/wording.ts`. This group is **UI** (what the user sees after
|
|
2692
|
+
# typing `/goal`); that one is **record + model input** (it lands in the goals
|
|
2693
|
+
# table and in the system prompt). If the latter followed the UI language, two
|
|
2694
|
+
# consecutive blocked records in one session could be in two languages — and
|
|
2695
|
+
# they are meant to be grepped.
|
|
2696
|
+
#
|
|
2697
|
+
# ## Every refusal must say what to type next
|
|
2698
|
+
#
|
|
2699
|
+
# `refuse_busy` names `/goal clear`, `refuse_no_goal` says how to create one,
|
|
2700
|
+
# `refuse_bad_budget` gives the range. Without that half sentence the user is
|
|
2701
|
+
# left with a "no" plus a reason they cannot act on, and retypes the same thing.
|
|
2702
|
+
goal:
|
|
2703
|
+
usage: |-
|
|
2704
|
+
/goal <objective> set a completion criterion for this long task ({rounds}-round budget by default)
|
|
2705
|
+
/goal show the current goal, rounds used, and whether it is blocked
|
|
2706
|
+
/goal edit <objective> reword it (rounds and phase are left alone)
|
|
2707
|
+
/goal budget <n> change the round budget (1 to {max})
|
|
2708
|
+
/goal pause / resume hold / release
|
|
2709
|
+
/goal done <evidence> report completion yourself; evidence is required
|
|
2710
|
+
/goal clear drop the goal
|
|
2711
|
+
none: |-
|
|
2712
|
+
This session has no goal yet.
|
|
2713
|
+
|
|
2714
|
+
A goal answers "**what counts as done here, and how many rounds may it
|
|
2715
|
+
take**" — useful for long tasks, especially ones you hand off and walk away
|
|
2716
|
+
from: hitting the budget leaves a record of where it got stuck instead of
|
|
2717
|
+
just "the model went quiet".
|
|
2718
|
+
|
|
2719
|
+
{usage}
|
|
2720
|
+
status: |-
|
|
2721
|
+
Goal {objective}
|
|
2722
|
+
Phase {phase}
|
|
2723
|
+
Rounds {used} / {max}{extra}
|
|
2724
|
+
phase_active: 'running'
|
|
2725
|
+
phase_paused: 'on hold (rounds do not advance)'
|
|
2726
|
+
phase_blocked: 'blocked'
|
|
2727
|
+
phase_complete: 'complete'
|
|
2728
|
+
extra_blocked: |-
|
|
2729
|
+
|
|
2730
|
+
Blocked [{code}] {message}
|
|
2731
|
+
Once that is handled, `/goal resume` releases it; `/goal budget <n>` buys more rounds.
|
|
2732
|
+
extra_complete: |-
|
|
2733
|
+
|
|
2734
|
+
Evidence {evidence}
|
|
2735
|
+
To start the next thing, just `/goal <new objective>` — a completed goal is replaced.
|
|
2736
|
+
extra_paused: |-
|
|
2737
|
+
|
|
2738
|
+
`/goal resume` releases it.
|
|
2739
|
+
extra_active: ''
|
|
2740
|
+
created: 'Goal set: {objective} ({max}-round budget). Each message you send is one round; running out turns it into "blocked", not "complete".'
|
|
2741
|
+
edited: 'Goal reworded to: {objective}. Rounds and phase untouched ({used}/{max}) — to start over, `/goal clear` and create a new one.'
|
|
2742
|
+
budget_set: 'Budget is now {max} rounds ({used} used).'
|
|
2743
|
+
budget_set_exhausted: 'Budget is now {max} rounds but {used} are already used — it just turned blocked. Raise the budget to keep going.'
|
|
2744
|
+
paused: 'On hold. Rounds no longer advance; the goal itself is kept.'
|
|
2745
|
+
resumed: 'Released, running against the goal again ({used}/{max}).'
|
|
2746
|
+
completed: 'Recorded as complete. Evidence: {evidence}'
|
|
2747
|
+
cleared: 'Goal cleared.'
|
|
2748
|
+
clear_noop: 'There was no goal to clear.'
|
|
2749
|
+
startup_blocked: '⚠ The previous goal is blocked [{code}]: {message} (`/goal` for details, `/goal resume` to release)'
|
|
2750
|
+
refuse_busy: 'There is already an unfinished goal ({phase}). A session holds one goal at a time — `/goal clear` first, or `/goal edit` the current one.'
|
|
2751
|
+
refuse_no_goal: 'This session has no goal. Set one with `/goal <objective>`.'
|
|
2752
|
+
refuse_empty_objective: 'The objective cannot be empty. Write one sentence describing what counts as done, e.g. `/goal wire the inspector panel to background task output`.'
|
|
2753
|
+
refuse_objective_too_long: 'That objective is too long (max {max} characters). It is a **completion criterion**, not the plan itself — steps belong in `/plan`.'
|
|
2754
|
+
refuse_bad_budget: 'The budget must be an integer between 1 and {max}.'
|
|
2755
|
+
refuse_empty_evidence: 'Reporting completion requires evidence: `/goal done <why this counts as done>`. If you cannot state it, it is not done.'
|
|
2756
|
+
refuse_wrong_phase: 'That cannot be done from the current phase ({phase}). Run `/goal` to see where it stands.'
|
|
2757
|
+
refuse_unavailable: 'Goal storage is unavailable (the session database did not come up); see `/diagnostics`.'
|