Harness spec 3.5.0: scheduled work on a linked computer
ci / check (push) Waiting to run

Accept scheduled work (remote.scheduled, off by default) said as limits.scheduled; a scheduled
turn is an agent session marked `scheduled` in the schedule's mode (Edit when none chosen, Plan
refused, lowered to remote.max_mode) with its Min/Max; the answer reports status ok, silent,
failed or cancelled and the final reply. Approvals wait the schedule's wait and go to the person
through the server, then count as denied. _lembas/monitor runs a watch command under the mode's
rules (one that would ask is refused) and answers a hash and an excerpt. Schedule routes of
/api/v2 accept the device's token. team_add under a sub-manager names it (added_under).
Conformance areas scheduled, silent, monitor; team cases changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
HomerandClaude Opus 5.5 committed 2026-11-10 12:11:56 +00:00
1 parent fe0fa62a51
commit e349d829a1
8 files changed
+639 -21

No files matched your search

+35 -1
View File
@@ -22,7 +22,7 @@ page *Harness-parity* in LLeMbas CLI's wiki.
| `permission/defaults.json` | The rules every session starts from | | `permission/defaults.json` | The rules every session starts from |
| `permission/modes.json` | The four modes — manual, edit, auto, plan — as what each does with each risk class | | `permission/modes.json` | The four modes — manual, edit, auto, plan — as what each does with each risk class |
| `models/schema.json` | Model metadata: context, output limit, efforts and the thinking switch, vision, tools, notes, capacity (`parallel`); `levels` — the effort levels a chat offers and what each sends | | `models/schema.json` | Model metadata: context, output limit, efforts and the thinking switch, vision, tools, notes, capacity (`parallel`); `levels` — the effort levels a chat offers and what each sends |
| `loop.json` | How one prompt runs: the step backstop, budgets, wrap-up, nudges, parallel calls, capacity; `jobs` — background work, its marks and limits; `subagents` — the model refusals, what names a model, the setting's label; `team` — a team's size, names, marks, refusals and texts; `progress` — the phases' texts; `context` — result ids, stubs, the output cap, the compaction table, the environment lines, the session notes | | `loop.json` | How one prompt runs: the step backstop, budgets, wrap-up, nudges, parallel calls, capacity; `jobs` — background work, its marks and limits; `subagents` — the model refusals, what names a model, the setting's label; `team` — a team's size, names, marks, refusals and texts; `progress` — the phases' texts; `context` — result ids, stubs, the output cap, the compaction table, the environment lines, the session notes; `scheduled` (3.5.0) — scheduled work on a linked computer: the setting, the modes, the approval wait, the sentinel, the watch command's limits and the refusals |
| `filesystem.md` | The file system a session works in: paths, node kinds, the four access levels, errors, listings, search and indexing by tokens, stacked instruction files, skills, memory | | `filesystem.md` | The file system a session works in: paths, node kinds, the four access levels, errors, listings, search and indexing by tokens, stacked instruction files, skills, memory |
| `context.md` | Context hygiene: what happens to a tool result when it is inserted, and never afterwards; the prefix that stays still; reasoning sent back during its turn; no absolute paths in the prompt | | `context.md` | Context hygiene: what happens to a tool result when it is inserted, and never afterwards; the prefix that stays still; reasoning sent back during its turn; no absolute paths in the prompt |
| `team.md` | Teams: a lead and the members it adds, the size setting, the room, who receives what, reports, a manager's refusals, capacity, stopping | | `team.md` | Teams: a lead and the members it adds, the size setting, the room, who receives what, reports, a manager's refusals, capacity, stopping |
@@ -71,6 +71,40 @@ page *Harness-parity* in LLeMbas CLI's wiki.
approval in plan mode and answers its `unavailable` text outside it. Their permission key is approval in plan mode and answers its `unavailable` text outside it. Their permission key is
`plan` (`permission/defaults.json`), which a rule written for the former `todo` key still reaches. `plan` (`permission/defaults.json`), which a rule written for the former `todo` key still reaches.
## 3.5.0
Written ahead of both implementations; the server vendors it as written, both carry it in the same
round (`acp.md` *Scheduled work on a linked computer*, `loop.json` `scheduled`).
- **Scheduled turns on a linked computer.** A schedule may run each time as an agent turn on a
linked device, in a folder of it. The device must allow it: **Accept scheduled work**
(`remote.scheduled`, off by default; `lembas service scheduled on|off`), said to the server as
`limits.scheduled` and judged again at every request — off, every scheduled request is refused
with one fixed text. The server opens an agent session marked `scheduled` (schedule, run, mode,
approval wait) with the schedule's Min/Max as an agent chat's, and prompts it with the run's own
text. **The mode** is the schedule's — Manual, Edit or Auto, **Edit when none was chosen**, so never
wider than Edit unless the person picked Auto; Plan refused — lowered to `remote.max_mode`. The
answer reports the run: `status` `ok`, `silent` (the `[SILENT]` sentinel on the first or last
line, the server's rule), `failed` or `cancelled`, and the final reply's `text`. A device that is
away is waited for like a busy model, then the run is missed; a refusal fails the run.
- **Approvals in a scheduled turn** go to the person through the server (a phone notification with
the card), carry `scheduled` on `session/request_permission`, and wait the schedule's wait
(default 30 minutes) instead of `remote.approval_timeout`; then they count as denied and the turn
goes on. `ask_user` and `plan_submit` have nobody to answer; `show` is not offered.
- **Monitor mode, the device's half**: `_lembas/monitor` runs a schedule's watch command in its
folder, with a timeout, under the mode's permission rules — **a command the mode would ask about
is refused** — and answers the output's hash (`sha256:` over the exit status and the bytes) and a
short excerpt; the server runs the model only when the hash changed, and records `unchanged`
otherwise.
- **Schedules over `/api/v2` with the device's token** (scope `link`): the CLI's `/schedule` lists,
creates from words (compile), pauses, resumes, runs now and reads history. A run may be
`unchanged`, may wait for the `device`; a schedule may say `runs_on` and `watch`.
- **`team_add` under a sub-manager** says where the report goes: `team.texts.added_under` and
`added_waiting_under` ("Its report goes to <parent> when it finishes").
- Capability `scheduled`. Conformance areas **`scheduled`** (the mode, the wait, the refusals),
**`silent`** (the sentinel and the stripped text) and **`monitor`** (the hash, and what a watch
command may run); `team` cases changed for `added_under`.
## 3.4.0 ## 3.4.0
Written ahead of both implementations; the server vendors it as written, both carry it in the same Written ahead of both implementations; the server vendors it as written, both carry it in the same
+1 -1
View File
@@ -1 +1 @@
3.4.0 3.5.0
+173 -3
View File
@@ -68,6 +68,7 @@ change to the protocol is a change here first, then in both projects.
| `progress` | spec 3.4.0 (`progress.md`): the `phase` event; `progress` in `_lembas/session/status` | | `progress` | spec 3.4.0 (`progress.md`): the `phase` event; `progress` in `_lembas/session/status` |
| `agents` | spec 3.4.0: the session's Subagents setting `{ min, max }` is read from `_meta.lembas.agents` on `session/new`, `session/prompt` and `_lembas/configure` (*Subagents in an agent chat*, below) | | `agents` | spec 3.4.0: the session's Subagents setting `{ min, max }` is read from `_meta.lembas.agents` on `session/new`, `session/prompt` and `_lembas/configure` (*Subagents in an agent chat*, below) |
| `tree` | spec 3.4.0 (`subagents.md` §5–§13): the `team` event; `_lembas/team`, `_lembas/team/transcript`, `_lembas/team/findings`, `_lembas/team/stop`; `subagent` on `session/request_permission` | | `tree` | spec 3.4.0 (`subagents.md` §5–§13): the `team` event; `_lembas/team`, `_lembas/team/transcript`, `_lembas/team/findings`, `_lembas/team/stop`; `subagent` on `session/request_permission` |
| `scheduled` | spec 3.5.0 (*Scheduled work on a linked computer*, below): `scheduled` in `_meta.lembas` of `session/new` and `session/prompt` read, and the run's outcome in the prompt's answer; `scheduled` on `session/request_permission`; `_lembas/monitor`; `limits.scheduled` in `_lembas/hello` and `initialize`. Listed by every build that has it, whatever the setting says: whether this device *accepts* scheduled work is `limits.scheduled` |
## Standard ACP, as used here ## Standard ACP, as used here
@@ -90,14 +91,15 @@ Result:
"_meta": { "lembas": { "_meta": { "lembas": {
"protocol": 3, "protocol": 3,
"harness_spec": "3.0.0", "harness_spec": "3.0.0",
"limits": { "roots": ["~/work"], "max_mode": "edit" }, "limits": { "roots": ["~/work"], "max_mode": "edit", "scheduled": false },
"connection": "example", "connection": "example",
"capabilities": ["directories", "configure", "…"] "capabilities": ["directories", "configure", "…"]
} } } }
} }
``` ```
`limits` is `null` on `serve --stdio`. `connection` is the device's name for the instance — the `limits` is `null` on `serve --stdio`. `limits.scheduled` (spec 3.5.0, capability `scheduled`): whether
this device accepts scheduled work now (`remote.scheduled`, *Scheduled work on a linked computer*). `connection` is the device's name for the instance — the
login's connection — or `null`; the `model` event and `announce` say directly whether a model is login's connection — or `null`; the `model` event and `announce` say directly whether a model is
the instance's (`instance`). the instance's (`instance`).
@@ -251,6 +253,10 @@ person may also answer at the terminal (a shared session), so the client does no
can say which — a background subagent may ask while the session itself is idle. `subagent` (capability `tree`, spec 3.4.0): can say which — a background subagent may ask while the session itself is idle. `subagent` (capability `tree`, spec 3.4.0):
`{ id, name, position }` when the call is a tree subagent's that nobody in the tree could allow (`subagents.md` §7) — `{ id, name, position }` when the call is a tree subagent's that nobody in the tree could allow (`subagents.md` §7) —
the card names it; the answers are the same as for the main agent's own call. the card names it; the answers are the same as for the main agent's own call.
`scheduled` (capability `scheduled`, spec 3.5.0): `{ schedule_id, run_id, wait_s }` when the call is
a scheduled turn's — nobody is at the chat, so the client puts the card to the person where they are
(LLeMbas: a notification on their phone carrying the card, answerable there and in every client),
and the device waits `wait_s` for it, not `remote.approval_timeout` (*Scheduled work*, below).
Answer: ACP's `{ outcome: { outcome: "selected", optionId } | { outcome: "cancelled" } }`, with Answer: ACP's `{ outcome: { outcome: "selected", optionId } | { outcome: "cancelled" } }`, with
optional `_meta.lembas.command` (the corrected command line, `once` only) or optional `_meta.lembas.command` (the corrected command line, `once` only) or
`_meta.lembas.feedback` (why it was denied). No answer within `remote.approval_timeout` is a denial. `_meta.lembas.feedback` (why it was denied). No answer within `remote.approval_timeout` is a denial.
@@ -306,6 +312,134 @@ is on the device, and gets it once the device links again. Over `/api/v2` the ch
the tree as it changes and the endpoints to read a subagent's transcript and findings, the same for the tree as it changes and the endpoints to read a subagent's transcript and findings, the same for
a chat's own subagents and for an agent chat's — so Web, Desktop and Android draw both alike. a chat's own subagents and for an agent chat's — so Web, Desktop and Android draw both alike.
## Scheduled work on a linked computer (spec 3.5.0)
A schedule (the server's: its rule, its runs, its history and notifications) may run **on a linked
computer**: each run is an agent turn on that device, in a folder of it, instead of a chat on the
server. The server decides *when*; the device decides *whether*, and *how far* — every rule an agent
chat has still holds, and the ones below are added. Numbers and texts: `loop.json` `scheduled`.
Conformance areas `scheduled`, `silent`, `monitor`.
### The device's setting: Accept scheduled work
- **`remote.scheduled`** in the device's global config, **off by default**; shown as **Accept
scheduled work** (`loop.json` `scheduled.setting`). Set from the command line with
`lembas service scheduled on|off` (also `lembas remote scheduled on|off`; `lembas remote list
--json` says `scheduled`), and by LLeMbas Desktop through the same command. It is global only: a
project's config cannot turn it on.
- It narrows remote work and widens nothing: a scheduled turn also needs `remote.enabled`, a folder
inside `remote.roots`, a trusted project where `remote.require_trust`, and a mode within
`remote.max_mode`, exactly as an agent chat does.
- The device says it in `limits.scheduled` (hello and `initialize`). Turning it on or off changes
the limits, so the device dials again once nothing works (as for any limit) and the client hears
it. The client may offer the device in a schedule's form only while `limits.scheduled` is true,
and says why not otherwise. The device judges it again at every `session/new`, `session/prompt`
and `_lembas/monitor` that is scheduled, whatever it said earlier.
- **Off** (or remote work off): every scheduled request is refused with `-32602`, the message
`loop.json` `scheduled.texts.off` word for word, and `data: { reason: "scheduled_off" }`. Not
retried: it is the person's setting. A folder the device does not accept is `-32602` with the
usual reason and `data: { reason: "folder" }`.
### Starting a scheduled turn
The client opens an agent session as for an agent chat (`session/new`: `cwd`, `lembas_model`,
`effort`, `_meta.lembas` with `agents` — the schedule's **Min/Max**, as an agent chat's — and the
`Context`), marked scheduled:
```json
"_meta": { "mode": "edit", "lembas_model": "qwen", "lembas": {
"agents": { "min": 1, "max": 4 },
"scheduled": { "schedule_id": "sched1", "run_id": "run7", "title": "Waybread check",
"mode": "edit", "approval_wait_s": 1800 } } }
```
and runs the turn with `session/prompt`, its `_meta.lembas` carrying the same `scheduled` object
(and a `turnId`, the run's — so a prompt sent again after a dropped link gets the run's answer
instead of a second run). A schedule that continues one conversation (`continue`) prompts the same
session again on its next run; a fresh run opens a new one. The prompt's text is the server's own
for a scheduled run — the instruction, the run's time, and for a schedule that notifies only when
it matters the `[SILENT]` hint — composed exactly as for a run on the server; the device adds nothing.
**The mode** (`scheduled.mode`; `_meta.mode` is ignored for a scheduled session): `manual`, `edit`
or `auto`, and **`edit` when absent** — so a run is never wider than Edit unless the person picked
Auto on that schedule. `plan` is refused (`scheduled.texts.plan`, `data: { reason: "plan" }`):
nobody is there to approve a plan. Then lowered to `remote.max_mode`, as every remote session is —
Auto runs only where both the schedule and the device allow it (`conformance/scheduled.json`). The
mode holds for the turn; neither `_lembas/configure` nor a command in the prompt widens it.
In a scheduled turn:
- **Approvals** go to the person through the client (`session/request_permission` with
`_meta.lembas.scheduled: { schedule_id, run_id, wait_s }`), and the device waits
`approval_wait_s` — the schedule's wait, 30 minutes by default, clamped to
`scheduled.approval_wait_seconds` — instead of `remote.approval_timeout`. Unanswered by then, the
call is denied with `scheduled.texts.denied` as the feedback the model reads (`{{minutes}}` the
wait in whole minutes), the card is settled (`_lembas/permission/settled`), and the turn goes on —
the model works around the refusal or fails. Not `patient`.
- **`ask_user` and `plan_submit`** have nobody to answer: the device answers them as it does with
no client (`ask_user` tells the model nobody can be asked; `plan_submit` saves the plan and says
to stop). `show` is not offered: nobody is looking.
- Subagents as in an agent chat (`agents`, the tree, findings); a subagent's call only the person
can allow is an approval like the main agent's, with `scheduled` beside `subagent`.
### How the turn ends and reports
`session/prompt`'s answer carries the run's outcome in `_meta.lembas.scheduled`:
```json
{ "stopReason": "end_turn", "_meta": { "lembas": { "turnId": "run7",
"scheduled": { "run_id": "run7", "status": "ok", "text": "All the waybread is wrapped.",
"error": null } } } }
```
- **`text`**: the turn's final reply — the text of the last assistant message the turn wrote, as
written (`[SILENT]` included). Model output: untrusted wherever it is shown.
- **`status`**: `cancelled` — the turn was cancelled (`session/cancel`: the person stopped it, or the
client's inactivity limit); `failed` — the turn ended on an error (`error` its text), or with no
reply text at all (`scheduled.texts.no_reply`); `silent` — the reply says the sentinel
`[SILENT]` on its first or last non-empty line (any case, a trailing full stop forgiven;
`conformance/silent.json`, the server's own rule for its runs); otherwise `ok`. A turn that hit the
step backstop or a budget and still wrote a reply is `ok`; `stopReason` says why it stopped.
- The client records the run from this (its history, its notification by the schedule's policy —
`silent` is never notified, a failure is told as a failure), and keeps the session as the run's
agent chat, whose events it received as for any agent chat. What the sentinel stripped looks like
is `silent.json`'s `stripped`.
### When the device is away, or refuses
- **Away** (not linked when the run is due, or the link drops before `session/new` is answered): the
client treats the device like a busy model — the run is `waiting` (`waiting_for: "device"`) and
tried again on the same steps a run waiting for a free model is, until the run's window closes;
then it is **missed**. A turn already running when the link drops is followed again by
`_lembas/session/status` and the turn id, as for any agent chat.
- **Refused** (`-32602` from a scheduled `session/new`, `session/prompt` or `_lembas/monitor`): the
run **fails** with the device's message, said in the run's history and in the failure
notification; `data.reason` (`scheduled_off`, `folder`, `plan`, `permission`) lets the client say
what to change. Not retried.
### Monitor mode: a command watched on the device
A schedule may **watch** something and run the model only when it changed. A URL is fetched by the
server and needs nothing from the device; a **command's output** is watched on the device with
`_lembas/monitor` (below): the client asks the device to run the watch command, compares the
answer's `hash` with the one the previous check stored, and when it is the same records the run as
`unchanged` — no model, nobody notified. Changed (or the first check), the model runs as the
schedule says — on the server, or as a scheduled turn on the device — told what changed through the
`excerpt`. A check that fails (timed out, refused) is a failed run.
The command runs under the **same permission rules as the schedule's mode**, with one difference:
**a command the mode would ask about is refused**, because a watch has nobody to ask. The device
judges the command line as `bash` would be judged in a session in that folder (the hardline, the
default rules, the global rules and — in a trusted project — the project's), and runs it only on
`allow`; `ask` is refused with `scheduled.texts.monitor_asks`, `deny` with
`scheduled.texts.monitor_denied`, both `-32602` with `data: { reason: "permission" }`
(`conformance/monitor.json`).
**The hash** (`monitor.json` `hash` cases): `sha256:` and the lowercase hex SHA-256 of the exit
status as decimal digits, a newline, and then the output's bytes exactly as read — standard output
and standard error as one stream, as the `bash` tool reads them. Nothing is normalised: a watch
command that prints a time changes every time, and should not.
## `_lembas/*` methods ## `_lembas/*` methods
Direction is who sends it. "→ result" marks a request; the others are notifications. Direction is who sends it. "→ result" marks a request; the others are notifications.
@@ -313,7 +447,9 @@ Direction is who sends it. "→ result" marks a request; the others are notifica
### `_lembas/hello` — agent → client ### `_lembas/hello` — agent → client
Sent once when the link opens, before `initialize`: Sent once when the link opens, before `initialize`:
`{ protocol, version, harness_spec, device: { host, platform }, limits: { roots, max_mode, require_trust } }`. `{ protocol, version, harness_spec, device: { host, platform }, limits: { roots, max_mode, require_trust, scheduled } }`
(`scheduled` since spec 3.5.0: whether the device accepts scheduled work; a change of it is a
change of the limits, below).
A device links whether its remote work is on or not, and with no roots at all. Off, or with no A device links whether its remote work is on or not, and with no roots at all. Off, or with no
roots, it says `limits.roots: []` here and in `initialize`, and refuses every `session/new`, every roots, it says `limits.roots: []` here and in `initialize`, and refuses every `session/new`, every
@@ -350,6 +486,22 @@ client hears the new limits.
| `phase` | `progress` — the progress object (`progress.md` §1), or `null` when nothing runs; `agent?` — the subagent's id when it is a subagent's, absent for the main agent's (capability `progress`). Sent when the phase changes, and for a change of its data at most four times a second. The existing `progress` event (the prompt's tokens read) is unchanged | | `phase` | `progress` — the progress object (`progress.md` §1), or `null` when nothing runs; `agent?` — the subagent's id when it is a subagent's, absent for the main agent's (capability `progress`). Sent when the phase changes, and for a change of its data at most four times a second. The existing `progress` event (the prompt's tokens read) is unchanged |
| `team` | `tree` — the session's subagent tree (`subagents.md` §12, the data) — whenever it changes, at most four times a second (capability `tree`). A client draws it and fetches nothing else to do so | | `team` | `tree` — the session's subagent tree (`subagents.md` §12, the data) — whenever it changes, at most four times a second (capability `tree`). A client draws it and fetches nothing else to do so |
### `_lembas/monitor` — client → agent → result (capability `scheduled`)
The watch command of a schedule in monitor mode (*Monitor mode*, above). Params
`{ cwd, command, mode?, timeout_s?, schedule_id?, run_id? }` — `cwd` absolute, judged as
`session/new`'s; `mode` the schedule's (`manual`, `edit` or `auto`; `edit` when absent; `plan`
refused), lowered to `remote.max_mode`; `timeout_s` defaulting to and clamped by
`loop.json` `scheduled.monitor.timeout_seconds`. Refused like any scheduled request when the device
does not accept scheduled work. The command runs as `bash -c` in `cwd`, in its own process group,
with no input; at the timeout the group is stopped.
Result `{ hash, exit, timed_out, excerpt, truncated, bytes, duration_ms }`: `hash` as above (`null`
when it timed out), `exit` the exit status (`null` on a signal or a timeout), `excerpt` the start of
the output decoded as UTF-8 (invalid bytes replaced), at most `scheduled.monitor.excerpt_chars`
characters, `truncated` whether there was more, `bytes` the output's length. The device keeps at
most `scheduled.monitor.output_max_bytes` of output and hashes all of it as it streams.
### `_lembas/directories` — client → agent → result ### `_lembas/directories` — client → agent → result
Params `{ path?: string }`. Without a path: `{ path: "", places: Dir[] }`, the remote roots and every Params `{ path?: string }`. Without a path: `{ path: "", places: Dir[] }`, the remote roots and every
@@ -682,6 +834,24 @@ Besides `/v1/models`, the chat completions, `/api/v1/instance` and `/mcp`:
older `library` scope counts as `memory`. Local memory files are not synced up; logged out, there older `library` scope counts as `memory`. Local memory files are not synced up; logged out, there
is no memory. is no memory.
- **Schedules** (spec 3.5.0): the schedule routes of `/api/v2` — `GET`/`POST /api/v2/schedules`,
`GET`/`PATCH`/`DELETE /api/v2/schedules/{id}`, `POST …/{id}/pause`, `/resume`, `/run`,
`GET …/{id}/runs`, `GET …/{id}/runs/{run}`, `POST /api/v2/schedules/compile` and `/preview` —
**accept the device's token** (scope `link`) as well as an app's (`app`), for the same person and
under the same `schedule.use` permission; nothing else of `/api/v2` does. The shapes are the
server's (its contract fixtures); what the CLI's `/schedule` relies on: a schedule's `id`,
`title`, `instruction`, `description`, `enabled`, `next_run`, `last_run` (`status`, `timing`,
`due_at`, `finished_at`, `preview`, `error.text`), `running`; a run's `id`, `status`, `due_at`,
`started_at`, `finished_at`, `model.name`, `preview`, `error`; compile's `ok`, `reason`, `title`,
`instruction`, `rule`, `description`, `next_runs`, `timezone`. A run's `status` may also be
**`unchanged`** (monitor mode: the watch saw nothing new; no model ran), and `waiting_for` may be
**`device`**. A schedule that runs on a linked computer has
**`runs_on: { device, device_name, cwd, mode, approval_wait_minutes }`** (else `null`); one that
watches has **`watch: { kind: "url", url } | { kind: "command", command, timeout_s }`** (else
`null`). Creating one with the device's token, `runs_on.device: "this"` names the calling device.
A server without these answers 401 or 403 to the device's token, and the CLI says the server is
older.
## Notes ## Notes
- `_lembas/files/search`'s `root` is the session's working directory, which `@path` mentions - `_lembas/files/search`'s `root` is the session's working directory, which `@path` mentions
+123
View File
@@ -0,0 +1,123 @@
{
"about": "Spec 3.5.0 (acp.md *Monitor mode*, `_lembas/monitor`): the watch command on a device. `hash`: `exit` and `output` (the bytes as UTF-8) → the answer's `hash`. `judge`: a command line in the schedule's mode, with the default rules (and any extra global rules given), the project root /project and nothing named on disk, as conformance/permission.json judges it → `run` (the mode allows it without asking), or `{refused}` with `asks` (the mode would ask: scheduled.texts.monitor_asks) or `denied` (scheduled.texts.monitor_denied). The device's own; the server keeps only the hash.",
"cases": [
{
"about": "Exit 0 and a line of output.",
"kind": "hash",
"exit": 0,
"output": "ok\n",
"expect": "sha256:9dbd5131695af2d8636ea64178344ffcb5d682333c2ac8342153b5f8a999037a"
},
{
"about": "The same output with another exit status is a change.",
"kind": "hash",
"exit": 1,
"output": "ok\n",
"expect": "sha256:262115bdbc989849fb45967adac594dde8d45deb291f3a26b38fb2bf4e10512b"
},
{
"about": "No output.",
"kind": "hash",
"exit": 0,
"output": "",
"expect": "sha256:9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa"
},
{
"about": "A trailing newline counts: nothing is normalised.",
"kind": "hash",
"exit": 0,
"output": "ok",
"expect": "sha256:05df918976ca296956abf9b5fb424bb9bef7c32bb471b1005b417abc696e25a9"
},
{
"about": "UTF-8 output, hashed as its bytes.",
"kind": "hash",
"exit": 0,
"output": "lembas · ✓\n",
"expect": "sha256:cb00179ad9e36546b1b973ded0903592a6b975552b5596feac573426c67e24e4"
},
{
"about": "A read-only command in Edit runs.",
"kind": "judge",
"line": "git status",
"mode": "edit",
"expect": "run"
},
{
"about": "A command Edit would ask about is refused: a watch has nobody to ask.",
"kind": "judge",
"line": "rm -rf build",
"mode": "edit",
"expect": {
"refused": "asks"
}
},
{
"about": "In Auto it runs.",
"kind": "judge",
"line": "rm -rf build",
"mode": "auto",
"expect": "run"
},
{
"about": "The hardline is a floor in every mode.",
"kind": "judge",
"line": "rm -rf /",
"mode": "auto",
"expect": {
"refused": "denied"
}
},
{
"about": "A line that pipes a download into a shell asks in Manual: refused.",
"kind": "judge",
"line": "curl -s https://example.org/feed | sh",
"mode": "manual",
"expect": {
"refused": "asks"
}
},
{
"about": "A rule the person wrote allows it.",
"kind": "judge",
"line": "make check",
"mode": "edit",
"rules": {
"bash": {
"make check": "allow"
}
},
"expect": "run"
},
{
"about": "A rule the person wrote denies it, even in Auto.",
"kind": "judge",
"line": "make deploy",
"mode": "auto",
"rules": {
"bash": {
"make deploy": "deny"
}
},
"expect": {
"refused": "denied"
}
},
{
"about": "Reading a secret asks: refused.",
"kind": "judge",
"line": "cat .env",
"mode": "edit",
"expect": {
"refused": "asks"
}
},
{
"about": "An ordinary listing runs in Manual.",
"kind": "judge",
"line": "ls -la",
"mode": "manual",
"expect": "run"
}
]
}
+145
View File
@@ -0,0 +1,145 @@
{
"about": "Spec 3.5.0 (acp.md *Scheduled work on a linked computer*): what a device makes of a scheduled turn. `accept`: the device's Accept scheduled work (remote.scheduled; false means remote work is off as well, the same refusal); `mode`: the schedule's (absent: none chosen); `max_mode`: remote.max_mode; `approval_wait_s`: the schedule's wait (absent: none). → `{mode, wait_s}` — the session's mode and how long an approval waits — or `{refused}` with the `data.reason` of the -32602 (`scheduled_off`, `plan`), whose message is loop.json scheduled.texts.off / .plan. Written from the rules; the device's other refusals (folder, trust) are the agent chat's.",
"cases": [
{
"about": "Off by default: refused, whatever else is asked.",
"accept": false,
"mode": "edit",
"max_mode": "edit",
"expect": {
"refused": "scheduled_off"
}
},
{
"about": "No mode chosen: Edit.",
"accept": true,
"max_mode": "edit",
"expect": {
"mode": "edit",
"wait_s": 1800
}
},
{
"about": "No mode chosen, the device allows Auto: still Edit — Auto only when the schedule says so.",
"accept": true,
"max_mode": "auto",
"expect": {
"mode": "edit",
"wait_s": 1800
}
},
{
"about": "Auto chosen and allowed.",
"accept": true,
"mode": "auto",
"max_mode": "auto",
"expect": {
"mode": "auto",
"wait_s": 1800
}
},
{
"about": "Auto chosen, the device allows Edit at most: lowered.",
"accept": true,
"mode": "auto",
"max_mode": "edit",
"expect": {
"mode": "edit",
"wait_s": 1800
}
},
{
"about": "Edit chosen, the device allows Manual at most: lowered.",
"accept": true,
"mode": "edit",
"max_mode": "manual",
"expect": {
"mode": "manual",
"wait_s": 1800
}
},
{
"about": "Manual chosen.",
"accept": true,
"mode": "manual",
"max_mode": "auto",
"expect": {
"mode": "manual",
"wait_s": 1800
}
},
{
"about": "Plan is refused: nobody is there to approve a plan.",
"accept": true,
"mode": "plan",
"max_mode": "auto",
"expect": {
"refused": "plan"
}
},
{
"about": "An unknown mode is read as the default, never as anything wider.",
"accept": true,
"mode": "unrestricted",
"max_mode": "auto",
"expect": {
"mode": "edit",
"wait_s": 1800
}
},
{
"about": "The device allows Plan at most: every scheduled turn is lowered to it.",
"accept": true,
"mode": "edit",
"max_mode": "plan",
"expect": {
"mode": "plan",
"wait_s": 1800
}
},
{
"about": "The wait, in seconds, as the schedule says.",
"accept": true,
"mode": "edit",
"max_mode": "edit",
"approval_wait_s": 600,
"expect": {
"mode": "edit",
"wait_s": 600
}
},
{
"about": "Below the floor: one minute.",
"accept": true,
"mode": "edit",
"max_mode": "edit",
"approval_wait_s": 5,
"expect": {
"mode": "edit",
"wait_s": 60
}
},
{
"about": "Above the ceiling: a day.",
"accept": true,
"mode": "edit",
"max_mode": "edit",
"approval_wait_s": 999999,
"expect": {
"mode": "edit",
"wait_s": 86400
}
},
{
"about": "A fraction is cut to whole seconds.",
"accept": true,
"mode": "edit",
"max_mode": "edit",
"approval_wait_s": 90.7,
"expect": {
"mode": "edit",
"wait_s": 90
}
}
]
}
+101
View File
@@ -0,0 +1,101 @@
{
"about": "Spec 3.5.0: whether a scheduled run's reply is `silent` — loop.json scheduled.sentinel on its first or last non-empty line, compared after trimming, any case, every trailing full stop forgiven — and the reply with every sentinel line taken out (`stripped`, the text a preview shows), trimmed. The server's rule for its own runs, and the device's for a scheduled turn.",
"cases": [
{
"about": "The sentinel alone.",
"text": "[SILENT]",
"expect": {
"silent": true,
"stripped": ""
}
},
{
"about": "Lower case, with a full stop.",
"text": "[silent].",
"expect": {
"silent": true,
"stripped": ""
}
},
{
"about": "On the last line, after a sentence.",
"text": "Nothing new since yesterday.\n[SILENT]",
"expect": {
"silent": true,
"stripped": "Nothing new since yesterday."
}
},
{
"about": "On the first line, before a sentence.",
"text": "[SILENT]\nChecked the feed: nothing.",
"expect": {
"silent": true,
"stripped": "Checked the feed: nothing."
}
},
{
"about": "In the middle: not silent (only the first or last line counts), and the line still goes from the preview.",
"text": "Two new posts.\n[SILENT]\nBoth about waybread.",
"expect": {
"silent": false,
"stripped": "Two new posts.\nBoth about waybread."
}
},
{
"about": "Inside a sentence: not silent.",
"text": "I would say [SILENT] but there is news: the build broke.",
"expect": {
"silent": false,
"stripped": "I would say [SILENT] but there is news: the build broke."
}
},
{
"about": "Blank lines around it are not lines.",
"text": "\n\n [SILENT] \n\n",
"expect": {
"silent": true,
"stripped": ""
}
},
{
"about": "A space before the full stop.",
"text": "All quiet.\n[SILENT] .",
"expect": {
"silent": true,
"stripped": "All quiet."
}
},
{
"about": "Without brackets: not the sentinel.",
"text": "SILENT",
"expect": {
"silent": false,
"stripped": "SILENT"
}
},
{
"about": "Empty: not silent.",
"text": "",
"expect": {
"silent": false,
"stripped": ""
}
},
{
"about": "An ordinary reply.",
"text": "The backup finished at 03:12; 4.2 GB.",
"expect": {
"silent": false,
"stripped": "The backup finished at 03:12; 4.2 GB."
}
},
{
"about": "Two full stops are forgiven too (every trailing one is).",
"text": "[SILENT]..",
"expect": {
"silent": true,
"stripped": ""
}
}
]
}
+16 -16
View File
@@ -1,5 +1,5 @@
{ {
"about": "Teams (team.md, spec 3.2.0; the main agent and its tree, subagents.md §5–§13, spec 3.4.0 — a member's team_add is now the lead's with `parent`, and `offered` lists 3.4.0's tools): one lead session's team, step by step, written from a reference model of the rules. `start`: the size setting `min` and `max`, `subagents` (enabled for the session; default true) and the chat model's `capacity` (models/schema.json: `parallel`, `single_session`; {} — no limit). The lead is `lead`; its first prompt is running as the case begins, and every agent is on the chat's model, and it is the only model there is (spec 3.3.0: other models are subagents.json's). Steps and what each answers: `offered` (`by` an agent, default lead) — `{tools, setting}`: the team tools listed for that agent, sorted, and whether the setting is shown; `call` (`by`, `tool`, `args`) — `{result, delivered}`: the call's answer word for word (null where an ordinary tool would run: the case looks only at whether it is refused), and what was delivered to whom by this step, `{name: [message, …]}`; `team_read` answers `{room, delivered}` instead, `room` being the messages it shows as `{n, from, to}` (the tree and the text format are loop.json team.read's, checked by each project's own tests); `task` runs a subagent that counts toward the size (and holds a slot) until `task_done` (`by`) — answers null; `finish` (`by`, `reply`, `reason`: stop by default, or steps, budget, `error — <text>`) — the agent's turn ends: a member's reply is its report — `{delivered}`; `set` (`min`, `max`, `capacity`: the person's setting or the model's metadata changed) — null; `person_stop` (`name`) — the person stopped that member in the UI — null; `user` — the person's message to the lead — `{note}`: the note on it (context.md §10), null when there is none; `state` — the team in tree order as `{name, manager, state}`: working, waiting (for a slot), finished, idle (the lead between turns). Delivering to a finished member or the idle lead wakes it (it is then working, or waiting when its model is full). Spec 3.4.0 adds: `start.mode` — the main agent's mode (default edit); `call` to team_whoami, team_finish, team_findings, team_request, team_decide, team_assign, team_replace, answered word for word; `context` (`by`, `used`, `window`) — its engine's context use — null; `context_full` (`by`) — its context reached team.context.pause_at: `{delivered}`; `started` (`by`) — `{text}`: what its latest turn was started with (a brief, or the messages that woke it); `tree` — `{size, open_slots, agents: [{id, name, parent, state, mode, request}], ended: [{id, name, state}]}`; `main_mode` (`mode`) — the main agent's mode changed — null. LLeMbas: services/team.py; CLI: team/.", "about": "Teams (team.md, spec 3.2.0; the main agent and its tree, subagents.md §5–§13, spec 3.4.0 — a member's team_add is now the lead's with `parent`, and `offered` lists 3.4.0's tools): one lead session's team, step by step, written from a reference model of the rules. `start`: the size setting `min` and `max`, `subagents` (enabled for the session; default true) and the chat model's `capacity` (models/schema.json: `parallel`, `single_session`; {} — no limit). The lead is `lead`; its first prompt is running as the case begins, and every agent is on the chat's model, and it is the only model there is (spec 3.3.0: other models are subagents.json's). Spec 3.5.0: a subagent added under a sub-manager is said to report to it (`added_under`). Steps and what each answers: `offered` (`by` an agent, default lead) — `{tools, setting}`: the team tools listed for that agent, sorted, and whether the setting is shown; `call` (`by`, `tool`, `args`) — `{result, delivered}`: the call's answer word for word (null where an ordinary tool would run: the case looks only at whether it is refused), and what was delivered to whom by this step, `{name: [message, …]}`; `team_read` answers `{room, delivered}` instead, `room` being the messages it shows as `{n, from, to}` (the tree and the text format are loop.json team.read's, checked by each project's own tests); `task` runs a subagent that counts toward the size (and holds a slot) until `task_done` (`by`) — answers null; `finish` (`by`, `reply`, `reason`: stop by default, or steps, budget, `error — <text>`) — the agent's turn ends: a member's reply is its report — `{delivered}`; `set` (`min`, `max`, `capacity`: the person's setting or the model's metadata changed) — null; `person_stop` (`name`) — the person stopped that member in the UI — null; `user` — the person's message to the lead — `{note}`: the note on it (context.md §10), null when there is none; `state` — the team in tree order as `{name, manager, state}`: working, waiting (for a slot), finished, idle (the lead between turns). Delivering to a finished member or the idle lead wakes it (it is then working, or waiting when its model is full). Spec 3.4.0 adds: `start.mode` — the main agent's mode (default edit); `call` to team_whoami, team_finish, team_findings, team_request, team_decide, team_assign, team_replace, answered word for word; `context` (`by`, `used`, `window`) — its engine's context use — null; `context_full` (`by`) — its context reached team.context.pause_at: `{delivered}`; `started` (`by`) — `{text}`: what its latest turn was started with (a brief, or the messages that woke it); `tree` — `{size, open_slots, agents: [{id, name, parent, state, mode, request}], ended: [{id, name, state}]}`; `main_mode` (`mode`) — the main agent's mode changed — null. LLeMbas: services/team.py; CLI: team/.",
"cases": [ "cases": [
{ {
"about": "Offered on a model that serves two requests at once: the lead has all four tools and the setting is shown.", "about": "Offered on a model that serves two requests at once: the lead has all four tools and the setting is shown.",
@@ -717,7 +717,7 @@
"delivered": {} "delivered": {}
}, },
{ {
"result": "Added db (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 4 agents.", "result": "Added db (owns a part) under backend: it is working. Its report goes to backend when it finishes. The team is 3 of 4 agents.",
"delivered": {} "delivered": {}
}, },
{ {
@@ -1637,7 +1637,7 @@
"delivered": {} "delivered": {}
}, },
{ {
"result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 6 agents.", "result": "Added routes (owns a part) under api: it is working. Its report goes to api when it finishes. The team is 3 of 6 agents.",
"delivered": {} "delivered": {}
}, },
{ {
@@ -1878,7 +1878,7 @@
"delivered": {} "delivered": {}
}, },
{ {
"result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 6 agents.", "result": "Added routes (owns a part) under api: it is working. Its report goes to api when it finishes. The team is 3 of 6 agents.",
"delivered": {} "delivered": {}
}, },
{ {
@@ -2155,11 +2155,11 @@
"delivered": {} "delivered": {}
}, },
{ {
"result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 8 agents.", "result": "Added routes (owns a part) under api: it is working. Its report goes to api when it finishes. The team is 3 of 8 agents.",
"delivered": {} "delivered": {}
}, },
{ {
"result": "Added db (owns a part): it is working. Its report comes to you when it finishes. The team is 4 of 8 agents.", "result": "Added db (owns a part) under api: it is working. Its report goes to api when it finishes. The team is 4 of 8 agents.",
"delivered": {} "delivered": {}
}, },
{ {
@@ -2464,7 +2464,7 @@
"delivered": {} "delivered": {}
}, },
{ {
"result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 5 agents.", "result": "Added routes (owns a part) under api: it is working. Its report goes to api when it finishes. The team is 3 of 5 agents.",
"delivered": {} "delivered": {}
}, },
{ {
@@ -2836,15 +2836,15 @@
"delivered": {} "delivered": {}
}, },
{ {
"result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 8 agents.", "result": "Added routes (owns a part) under api: it is working. Its report goes to api when it finishes. The team is 3 of 8 agents.",
"delivered": {} "delivered": {}
}, },
{ {
"result": "Added auth (owns a part): it is working. Its report comes to you when it finishes. The team is 4 of 8 agents.", "result": "Added auth (owns a part) under routes: it is working. Its report goes to routes when it finishes. The team is 4 of 8 agents.",
"delivered": {} "delivered": {}
}, },
{ {
"result": "Added db (owns a part): it is working. Its report comes to you when it finishes. The team is 5 of 8 agents.", "result": "Added db (owns a part) under api: it is working. Its report goes to api when it finishes. The team is 5 of 8 agents.",
"delivered": {} "delivered": {}
}, },
{ {
@@ -2890,7 +2890,7 @@
} }
], ],
{ {
"result": "Added auth (owns a part): it is working. Its report comes to you when it finishes. The team is 5 of 8 agents.", "result": "Added auth (owns a part) under api: it is working. Its report goes to api when it finishes. The team is 5 of 8 agents.",
"delivered": {} "delivered": {}
}, },
{ {
@@ -2968,7 +2968,7 @@
"delivered": {} "delivered": {}
}, },
{ {
"result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 6 agents.", "result": "Added routes (owns a part) under api: it is working. Its report goes to api when it finishes. The team is 3 of 6 agents.",
"delivered": {} "delivered": {}
}, },
{ {
@@ -3647,7 +3647,7 @@
"delivered": {} "delivered": {}
}, },
{ {
"result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 6 agents.", "result": "Added routes (owns a part) under api: it is working. Its report goes to api when it finishes. The team is 3 of 6 agents.",
"delivered": {} "delivered": {}
}, },
{ {
@@ -3978,7 +3978,7 @@
"delivered": {} "delivered": {}
}, },
{ {
"result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 6 agents.", "result": "Added routes (owns a part) under api: it is working. Its report goes to api when it finishes. The team is 3 of 6 agents.",
"delivered": {} "delivered": {}
}, },
{ {
@@ -4382,7 +4382,7 @@
"delivered": {} "delivered": {}
}, },
{ {
"result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 6 agents.", "result": "Added routes (owns a part) under api: it is working. Its report goes to api when it finishes. The team is 3 of 6 agents.",
"delivered": {} "delivered": {}
}, },
{ {
@@ -4843,7 +4843,7 @@
"delivered": {} "delivered": {}
}, },
{ {
"result": "Added routes (owns a part): it is working. Its report comes to you when it finishes. The team is 3 of 6 agents.", "result": "Added routes (owns a part) under api: it is working. Its report goes to api when it finishes. The team is 3 of 6 agents.",
"delivered": {} "delivered": {}
}, },
{ {
+45
View File
@@ -146,6 +146,8 @@
"texts": { "texts": {
"added": "Added {{name}} ({{role}}): it is working. Its report comes to you when it finishes. The team is {{size}} of {{max}} agents.", "added": "Added {{name}} ({{role}}): it is working. Its report comes to you when it finishes. The team is {{size}} of {{max}} agents.",
"added_waiting": "Added {{name}} ({{role}}): it waits for a free slot — the model serves {{parallel}} requests at once and all are busy — and starts when one frees. Its report comes to you when it finishes. The team is {{size}} of {{max}} agents.", "added_waiting": "Added {{name}} ({{role}}): it waits for a free slot — the model serves {{parallel}} requests at once and all are busy — and starts when one frees. Its report comes to you when it finishes. The team is {{size}} of {{max}} agents.",
"added_under": "Added {{name}} ({{role}}) under {{parent}}: it is working. Its report goes to {{parent}} when it finishes. The team is {{size}} of {{max}} agents.",
"added_waiting_under": "Added {{name}} ({{role}}) under {{parent}}: it waits for a free slot — the model serves {{parallel}} requests at once and all are busy — and starts when one frees. Its report goes to {{parent}} when it finishes. The team is {{size}} of {{max}} agents.",
"sent": "Sent to {{to}}.", "sent": "Sent to {{to}}.",
"stopped": "Stopped {{names}}. The team is {{size}} of {{max}} agents.", "stopped": "Stopped {{names}}. The team is {{size}} of {{max}} agents.",
"manager": "You lead a team: give this to a member (team_add or team_send).", "manager": "You lead a team: give this to a member (team_add or team_send).",
@@ -438,5 +440,48 @@
], ],
"what_max": 80, "what_max": 80,
"pace_per_second": 4 "pace_per_second": 4
},
"scheduled": {
"about": "Spec 3.5.0 (acp.md *Scheduled work on a linked computer*): a schedule's run as an agent turn on a linked computer, and the watch command of monitor mode. `setting` is how the device's switch is shown; `modes` the modes a schedule may choose (`default_mode` when it chose none), lowered to remote.max_mode; `approval_wait_seconds` how long an approval in a scheduled turn waits (the schedule's wait, clamped); `sentinel` the word a reply says when there is nothing to tell; `monitor` the watch command's limits. conformance/scheduled.json, silent.json, monitor.json.",
"setting": {
"key": "remote.scheduled",
"label": "Accept scheduled work",
"hint": "Let your schedules run here as agent turns, in the folders remote work may use. Off by default."
},
"modes": [
"manual",
"edit",
"auto"
],
"default_mode": "edit",
"approval_wait_seconds": {
"default": 1800,
"min": 60,
"max": 86400
},
"sentinel": "[SILENT]",
"statuses": [
"ok",
"silent",
"failed",
"cancelled"
],
"monitor": {
"timeout_seconds": {
"default": 60,
"min": 1,
"max": 600
},
"excerpt_chars": 2000,
"output_max_bytes": 1048576
},
"texts": {
"off": "This computer does not accept scheduled work. Turn on \"Accept scheduled work\" on it (lembas service scheduled on).",
"plan": "A scheduled run cannot use plan mode: nobody is there to approve a plan. Choose Manual, Edit or Auto.",
"denied": "Nobody answered this approval within {{minutes}} minutes, so it counts as denied.",
"monitor_asks": "The watch command would need approval in {{mode}} mode ({{reason}}), and a watch has nobody to ask. Allow the command in the project's permission rules, or choose a wider mode for the schedule.",
"monitor_denied": "The watch command is refused here: {{reason}}.",
"no_reply": "The run ended without a reply."
}
} }
} }