@xingular/harness 1.3.1-unstable.2 → 1.3.1-unstable.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/README.md +1 -1
- package/dist/cli.js +3680 -1947
- package/dist/sdk/cli.js +2 -0
- package/dist/sdk/commands/registry.d.ts +15 -0
- package/dist/sdk/commands/registry.js +2 -0
- package/dist/sdk/computer/screenshot.d.ts +32 -0
- package/dist/sdk/computer/screenshot.js +35 -0
- package/dist/sdk/context/attribution.d.ts +18 -0
- package/dist/sdk/context/attribution.js +295 -0
- package/dist/sdk/context/types.d.ts +94 -0
- package/dist/sdk/context/types.js +10 -0
- package/dist/sdk/index.d.ts +2 -1
- package/dist/sdk/index.js +2 -1
- package/dist/sdk/plugins/builtin.js +1 -0
- package/dist/sdk/prompt/compile.d.ts +42 -2
- package/dist/sdk/prompt/compile.js +87 -15
- package/dist/sdk/remote/addresses.d.ts +19 -0
- package/dist/sdk/remote/addresses.js +52 -0
- package/dist/sdk/remote/pairing.d.ts +59 -0
- package/dist/sdk/remote/pairing.js +160 -0
- package/dist/sdk/remote/seal.d.ts +28 -0
- package/dist/sdk/remote/seal.js +84 -0
- package/dist/sdk/remote/server.d.ts +28 -0
- package/dist/sdk/remote/server.js +158 -0
- package/dist/sdk/remote/service.d.ts +125 -0
- package/dist/sdk/remote/service.js +330 -0
- package/dist/sdk/remote/webClient.d.ts +4 -0
- package/dist/sdk/remote/webClient.js +444 -0
- package/dist/sdk/server/methods.js +3 -0
- package/dist/sdk/sessions/contracts.d.ts +4 -0
- package/dist/sdk/sessions/contracts.js +2 -0
- package/dist/sdk/sessions/planMode.d.ts +26 -1
- package/dist/sdk/sessions/planMode.js +48 -1
- package/dist/sdk/sessions/runner.d.ts +12 -0
- package/dist/sdk/sessions/runner.js +34 -2
- package/dist/sdk/sessions/sessions.js +1 -0
- package/dist/sdk/sessions/subagentService.d.ts +11 -1
- package/dist/sdk/sessions/subagentService.js +120 -38
- package/dist/sdk/sessions/subagents.d.ts +31 -0
- package/dist/sdk/sessions/subagents.js +45 -0
- package/dist/sdk/sessions/workSlice.d.ts +5 -9
- package/dist/sdk/sessions/workSlice.js +8 -149
- package/dist/sdk/terminal/client.d.ts +3 -1
- package/dist/sdk/terminal/client.js +15 -6
- package/dist/sdk/terminal/control.d.ts +1 -1
- package/dist/sdk/terminal/control.js +2 -2
- package/dist/sdk/terminal/controller.d.ts +22 -0
- package/dist/sdk/terminal/controller.js +104 -13
- package/dist/sdk/terminal/piExtension.js +106 -4
- package/dist/sdk/terminal/stallWatch.d.ts +13 -0
- package/dist/sdk/terminal/stallWatch.js +16 -0
- package/dist/sdk/terminal/standalone.d.ts +2 -0
- package/dist/sdk/terminal/standalone.js +9 -2
- package/dist/sdk/terminal/terminals.d.ts +14 -0
- package/dist/sdk/terminal/terminals.js +98 -2
- package/dist/sdk/terminal/tui/commandPages.d.ts +4 -1
- package/dist/sdk/terminal/tui/commandPages.js +12 -0
- package/dist/sdk/terminal/tui/commands.d.ts +3 -2
- package/dist/sdk/terminal/tui/commands.js +19 -5
- package/dist/sdk/terminal/tui/dialogs.d.ts +3 -2
- package/dist/sdk/terminal/tui/dialogs.js +4 -3
- package/dist/sdk/terminal/tui/keybindings.d.ts +1 -1
- package/dist/sdk/terminal/tui/keybindings.js +1 -1
- package/dist/sdk/terminal/tui/markdown.d.ts +16 -0
- package/dist/sdk/terminal/tui/markdown.js +55 -0
- package/dist/sdk/terminal/tui/modeChoices.d.ts +3 -2
- package/dist/sdk/terminal/tui/modeChoices.js +4 -4
- package/dist/sdk/terminal/tui/mouseReport.d.ts +30 -0
- package/dist/sdk/terminal/tui/mouseReport.js +80 -0
- package/dist/sdk/terminal/tui/pages/annotatePage.js +3 -2
- package/dist/sdk/terminal/tui/pages/browserPage.d.ts +1 -1
- package/dist/sdk/terminal/tui/pages/contextPage.d.ts +62 -0
- package/dist/sdk/terminal/tui/pages/contextPage.js +484 -0
- package/dist/sdk/terminal/tui/pages/remoteControlPage.d.ts +32 -0
- package/dist/sdk/terminal/tui/pages/remoteControlPage.js +170 -0
- package/dist/sdk/terminal/tui/pages/trajectoryPage.js +5 -4
- package/dist/sdk/terminal/tui/qr.d.ts +4 -0
- package/dist/sdk/terminal/tui/qr.js +35 -0
- package/dist/sdk/terminal/tui/runtime.js +8 -2
- package/dist/sdk/terminal/tui/search.js +3 -2
- package/dist/sdk/terminal/tui/sideQuestion.d.ts +1 -0
- package/dist/sdk/terminal/tui/sideQuestion.js +8 -3
- package/dist/sdk/terminal/tui/skillInvocation.d.ts +21 -2
- package/dist/sdk/terminal/tui/skillInvocation.js +31 -11
- package/dist/sdk/terminal/tui/status.d.ts +6 -0
- package/dist/sdk/terminal/tui/status.js +15 -1
- package/dist/sdk/terminal/tui/theme.d.ts +9 -0
- package/dist/sdk/terminal/tui/theme.js +9 -0
- package/dist/sdk/tools/defaultSkills.d.ts +19 -0
- package/dist/sdk/tools/defaultSkills.js +27 -1
- package/dist/sdk/tools/dispatch.js +8 -9
- package/dist/sdk/tools/spawnSubagent.d.ts +1 -1
- package/dist/sdk/tools/spawnSubagent.js +4 -2
- package/dist/terminal-opentui.js +1607 -492
- package/dist/terminal-pi.js +256 -144
- package/docs/README.md +1 -0
- package/docs/architecture/02-sessions-and-runs.md +55 -22
- package/docs/architecture/04-prompt.md +23 -4
- package/docs/architecture/07-tools-and-gate.md +4 -2
- package/docs/architecture/10-server-protocol.md +24 -0
- package/docs/architecture/11-terminal.md +52 -10
- package/docs/architecture/12-storage.md +5 -2
- package/docs/architecture/13-world-tools.md +17 -3
- package/docs/architecture/14-surfaces-and-parity.md +7 -3
- package/docs/architecture/15-remote-control.md +118 -0
- package/docs/architecture/README.md +3 -0
- package/docs/architecture/proposals/PROP-0049-context-attribution.md +409 -0
- package/docs/architecture/proposals/README.md +2 -1
- package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +4 -0
- package/docs/decisions/ADR-0048-required-skills-from-a-message.md +42 -0
- package/docs/decisions/ADR-0050-approved-window-screenshots.md +19 -0
- package/docs/decisions/ADR-0051-terminal-decision-wait.md +10 -0
- package/docs/decisions/ADR-0052-per-profile-subagent-budgets.md +25 -0
- package/docs/decisions/ADR-0053-no-repeated-work-guard.md +45 -0
- package/docs/decisions/ADR-0054-terminal-reply-heartbeat-and-stall.md +52 -0
- package/docs/decisions/ADR-0055-plan-output-repair.md +51 -0
- package/docs/decisions/ADR-0056-remote-control.md +53 -0
- package/docs/decisions/ADR-0057-structured-plan-outcomes.md +39 -0
- package/docs/decisions/ADR-0058-ask-user-only-plan-clarification.md +36 -0
- package/docs/parity.md +14 -10
- package/docs/releases/1.3.1-unstable.3.md +54 -0
- package/docs/releases/1.3.1-unstable.4.md +56 -0
- package/docs/terminal-tui.md +43 -18
- package/package.json +3 -1
|
@@ -97,6 +97,15 @@ A session that cannot open ends its run as `failed` and returns `nativeSessionId
|
|
|
97
97
|
- **Subagents.** `spawn_subagent` / `wait_subagents` for the model; `subagents.spawn`, `wait`,
|
|
98
98
|
`results` for the host (`sessions.spawnSubagent` is a compatibility alias). Child runs have
|
|
99
99
|
`relation: 'subagent'` and `parentRunId`. Nesting defaults to 1, at most 3 (`subagentDepth`).
|
|
100
|
+
A run also starts only a few children: by default at most four distinct child conversations, at
|
|
101
|
+
most two `worker`, three `explorer`, one of any other named profile and one with no profile. The
|
|
102
|
+
parent's `subagents` option overrides them (`maxChildren`, `maxByProfile`, `maxOther`,
|
|
103
|
+
`maxUnprofiled`), clamped to 0–50. The count covers this run and the runs it was continued from
|
|
104
|
+
(`resumedFromRunId`), so a work-slice continuation keeps the budget; it counts distinct
|
|
105
|
+
conversations, so continuing a stopped child uses no new slot, while a child whose startup
|
|
106
|
+
failed still counts. A concurrent spawn reserves its slot before it launches. A refused spawn is
|
|
107
|
+
an `invalid_request` whose message tells the model to do the work itself; no child run is
|
|
108
|
+
created.
|
|
100
109
|
The child's record carries `title`, the short name the parent gave it (PROP-0030).
|
|
101
110
|
`spawn_subagent { continue: <runId> }` (`subagents.spawn { continue }`) continues a child that
|
|
102
111
|
stopped before finishing as a new child run with `continueFrom`. It reopens the first link's
|
|
@@ -132,7 +141,29 @@ A session that cannot open ends its run as `failed` and returns `nativeSessionId
|
|
|
132
141
|
steps and verification. A small fix stays concise without omitting its
|
|
133
142
|
outcome or check; cross-cutting work uses ordered steps. It uses alternatives only when requested
|
|
134
143
|
or materially useful; contextual `uiSketch` questions clarify real choices.
|
|
135
|
-
|
|
144
|
+
**Outcome contract (ADR-0058, superseding the clarification portion of ADR-0057).** Classify a
|
|
145
|
+
new final reply as exactly one of `plan_ready`, `blocked`, or `invalid`. `plan_ready` uses the
|
|
146
|
+
existing complete `<proposed_plan>` block and is the only outcome that offers a plan card and
|
|
147
|
+
Implement. When a required user decision cannot be obtained because `ask_user` is unavailable,
|
|
148
|
+
`blocked` uses a nonempty `<plan_blocked>` block to report the blocker without asking a free-text
|
|
149
|
+
question or inventing the decision; it
|
|
150
|
+
is not a plan and never offers Implement. New final replies must not use `<plan_clarification>`
|
|
151
|
+
or free-text questions as a clarification channel. Missing, incomplete, conflicting or otherwise
|
|
152
|
+
malformed outcomes are `invalid`, not a guessed plan. When clarification is needed and `ask_user`
|
|
153
|
+
is available, it blocks the active run for the answer, then that same run continues research and
|
|
154
|
+
planning.
|
|
155
|
+
|
|
156
|
+
Server and terminal use the same outcome classification and one bounded, hidden repair turn
|
|
157
|
+
in the same eligible attended top-level plan run, only for `invalid`. The repair requests a valid
|
|
158
|
+
outcome without inventing decisions or changing the substance; it may call `ask_user` when
|
|
159
|
+
available, blocking the active run for the answer, and may return either valid outcome.
|
|
160
|
+
It is not a new user request, does not bypass the plan gate, and is never attempted for a
|
|
161
|
+
plan-research child, command, probe, improver, reflection, other unattended run, interruption,
|
|
162
|
+
error or work-slice termination. A second invalid result ends repair: retain the original reply,
|
|
163
|
+
complete the run with a warning and no Implement choice. Preserve the existing plan tag, card,
|
|
164
|
+
run events, tool permissions and cached prompt boundary; old transcripts, including legacy
|
|
165
|
+
clarification blocks and untagged replies, remain intact and visible, not silently promoted to
|
|
166
|
+
plans. Do not require clients to understand a new event type.
|
|
136
167
|
- **Compaction checkpoint** (PROP-0033, ADR-0032). After compaction, an active run's model context
|
|
137
168
|
receives a bounded, ephemeral checkpoint with the current user request and the latest validated
|
|
138
169
|
todo list or applicable plan instructions, when either is available to restore. It is re-added to
|
|
@@ -274,28 +305,28 @@ is approved but remains in progress; no completed cross-surface catalog is impli
|
|
|
274
305
|
checkpoint and leaves handoff calls callable; otherwise a plan-research child must stop with a
|
|
275
306
|
concise partial research report, and the runner derives a checkpoint from run evidence. A
|
|
276
307
|
non-handoff tool call after the refusal ends the reply. A budget-ended reply is `interrupted` with
|
|
277
|
-
terminal reason `slice_complete
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
reply
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
policy's slice cap, only after a slice that changed a file, passed a check or recorded a finished
|
|
290
|
-
item, and never after a checkpoint that is `blocked`. A run may turn the guard off with
|
|
291
|
-
`workSlice.enabled: false`, and a launch may set `XINGULAR_WORK_SLICE=off` for every run in that
|
|
292
|
-
process; with it off a reply has no boundary — no checkpoint, no continuation, no stall notes — and
|
|
293
|
-
ends only when the model stops or a person interrupts.
|
|
308
|
+
terminal reason `slice_complete` (ADR-0053).
|
|
309
|
+
No call is refused for repeating: identical calls, a Bash check that keeps failing and repeated
|
|
310
|
+
`edit`/`write` results on one path are allowed as often as the work needs, because a distinct read
|
|
311
|
+
or another edit is not evidence of a loop. `todo_write` counts toward the tool-call budget and is
|
|
312
|
+
otherwise unconstrained. Third-compaction thrash detection remains: three context compactions in
|
|
313
|
+
one reply end it with the same handoff request. Plan-research children without `checkpoint_write`
|
|
314
|
+
receive the configured bound without a new capability and do not auto-continue. A new run
|
|
315
|
+
continues it only up to the policy's slice cap, only after a slice that changed a file, passed a
|
|
316
|
+
check or recorded a finished item, and never after a checkpoint that is `blocked`. A run may turn
|
|
317
|
+
the guard off with `workSlice.enabled: false`, and a launch may set `XINGULAR_WORK_SLICE=off` for
|
|
318
|
+
every run in that process; with it off a reply has no boundary — no checkpoint, no continuation —
|
|
319
|
+
and ends only when the model stops or a person interrupts.
|
|
294
320
|
- **INV-RUN-14** A run event stream carries `checkpoint` (the handoff `checkpoint_write` records or
|
|
295
321
|
the harness derives from the run's own evidence when the model wrote none) and `heartbeat`
|
|
296
|
-
(steps, elapsed time, last tool, compactions, slice, and the call budget).
|
|
297
|
-
|
|
298
|
-
|
|
322
|
+
(steps, elapsed time, last tool, compactions, slice, and the call budget). Every live reply emits a
|
|
323
|
+
heartbeat every 30 seconds, terminal replies included. A heartbeat older than five minutes while
|
|
324
|
+
the owner process lives marks the run `interrupted` with reason `stalled`; a run that never emitted
|
|
325
|
+
one keeps the 30-minute silence rule. A reply whose heartbeat keeps arriving but which produces no
|
|
326
|
+
other event for fifteen minutes is a wedged turn, not a busy one: the terminal ends it through the
|
|
327
|
+
native abort and records `interrupted` with reason `stalled`, and grants a reply whose oldest
|
|
328
|
+
running tool has run for thirty minutes the same ending. An unanswered approval or question is
|
|
329
|
+
never cut short. The watchdog never restarts work (ADR-0054).
|
|
299
330
|
- **INV-RUN-12** A post-compaction checkpoint is a bounded context projection of the active request,
|
|
300
331
|
the latest validated todo list and applicable plan instructions. It is compiled by
|
|
301
332
|
`prompt/compile.ts`, supplied only to model requests in the active run, and never persists hidden
|
|
@@ -322,7 +353,9 @@ Decisions: [ADR-0002](../decisions/ADR-0002-native-subagent-lifecycle.md) (super
|
|
|
322
353
|
[ADR-0023](../decisions/ADR-0023-contextual-side-question-runs.md),
|
|
323
354
|
[ADR-0026](../decisions/ADR-0026-contextual-question-wireframes.md),
|
|
324
355
|
[ADR-0033](../decisions/ADR-0033-plan-mode-research-delegation.md),
|
|
325
|
-
[ADR-0039](../decisions/ADR-0039-progress-aware-stall-guards.md)
|
|
356
|
+
[ADR-0039](../decisions/ADR-0039-progress-aware-stall-guards.md),
|
|
357
|
+
[ADR-0053](../decisions/ADR-0053-no-repeated-work-guard.md),
|
|
358
|
+
[ADR-0054](../decisions/ADR-0054-terminal-reply-heartbeat-and-stall.md).
|
|
326
359
|
|
|
327
360
|
## Native session names (PROP-0010)
|
|
328
361
|
|
|
@@ -14,6 +14,7 @@ keeps provider caching effective across turns.
|
|
|
14
14
|
|---|---|
|
|
15
15
|
| `compile(env, caller, launch, variant)` → `ContextPack` | Builds the ordered, budgeted sections, including the configured data-root path in native file guidance. |
|
|
16
16
|
| `render(env, pack)` → `string` | One string for a session that takes a single prompt. |
|
|
17
|
+
| `renderSized(env, pack)` → `{ text, sizes, framing }` | The same string, plus the characters each section occupied and the framing around them. |
|
|
17
18
|
| `sessionLaunch(env, caller, launch, historyFile, variant)` → `{ system, update, pack }` | SDK sessions: stable prefix plus an update with only the dynamic sections that changed since the last delivered snapshot. |
|
|
18
19
|
| `deliveredSnapshot(sessionFile)` | Reads the last delivered snapshot on the active history branch; a compaction forces a fresh one. |
|
|
19
20
|
| `renderCompactionCheckpoint` | Compiles a bounded current-request, todo list and plan-state message for post-compaction model requests. |
|
|
@@ -26,9 +27,16 @@ A `ContextPack` holds `sections: Section[]` (each with `id`, `title`,
|
|
|
26
27
|
`body`, `sources`, `budget`, `size`, `cut`), `toolGroups`, `harness` (scope and fingerprint),
|
|
27
28
|
`profile`, `compiledAt` and `delivery` (`systemSectionIds`, `updatedSectionIds`).
|
|
28
29
|
|
|
30
|
+
`delivery.sizes` records what each delivered section occupied when it was rendered — `system` and
|
|
31
|
+
`update` maps by section id, plus the framing characters that belong to no section (the product
|
|
32
|
+
heading, the section headings, the context-update guidance). It is what lets a surface attribute the
|
|
33
|
+
context window without re-parsing a prompt whose bodies may contain their own headings. A pack
|
|
34
|
+
recorded before the compiler measured them has no `sizes`, and a reader then falls back to
|
|
35
|
+
`Section.size` and says the split is approximate.
|
|
36
|
+
|
|
29
37
|
## Section order
|
|
30
38
|
|
|
31
|
-
`SECTION_ORDER`: `harnessIdentity` 5, `system` 25, `sessionState` 30, `guardrails` 40, `memory` 45, `references` 50, `subagents` 64, `scripts` 66, `wiki` 80, `tools` 90, `reporting` 95. Application sections
|
|
39
|
+
`SECTION_ORDER`: `harnessIdentity` 5, `system` 25, `sessionState` 30, `guardrails` 40, `memory` 45, `references` 50, `requiredSkills` 52, `subagents` 64, `scripts` 66, `wiki` 80, `tools` 90, `reporting` 95. Application sections
|
|
32
40
|
sit among them by their `order`; plan mode uses `plan` at 25. Ties keep insertion order.
|
|
33
41
|
When a run may spawn, the dynamic `subagents` section lists every profile it can start (built-in,
|
|
34
42
|
global, project and variant c5 names) and replaces the variant's own list (PROP-0029). Normal-mode
|
|
@@ -49,6 +57,15 @@ in the frozen set: the name line appears only with `session_name`, and the todo
|
|
|
49
57
|
`sessionLaunch` and the changed-section rule; the terminal appends the same text as a request-time
|
|
50
58
|
custom message.
|
|
51
59
|
|
|
60
|
+
`required-skills` is a built-in dynamic section at order 52 when `Launch.requiredSkills` names any
|
|
61
|
+
skill. Named in a message as `$name`, a skill is required for that request: the compiler reads its
|
|
62
|
+
`SKILL.md` from the pinned variant (the built-in `plugin-authoring` skill fills in when the variant
|
|
63
|
+
has none) through `readSkillFile`, and the section carries the skill's text and the directive to
|
|
64
|
+
follow it. It is bounded like `references`, names an unavailable skill honestly, and changes no tool
|
|
65
|
+
or permission. Structured sessions deliver it through `sessionLaunch`; the terminal builds the same
|
|
66
|
+
text with `requiredSkillsText` from Pi's loaded skills and appends it as a hidden request-time custom
|
|
67
|
+
message. The section belongs to the request, so an identical one is delivered again.
|
|
68
|
+
|
|
52
69
|
The fixed `harness-identity` section says `You are an AI agent running in <productName>.` and uses
|
|
53
70
|
`env.productName` (default `Xingular Agent`). `Launch.standard: { cwd, project?: { name } | null }` requests shared role and working-folder
|
|
54
71
|
sections from `prompt/standard.ts`; the compiler supplies common connections, file and annotation
|
|
@@ -83,10 +100,12 @@ compile ──► pack ──► stable sections ──► system prompt (+ "Con
|
|
|
83
100
|
type xingular-context
|
|
84
101
|
```
|
|
85
102
|
|
|
86
|
-
- Dynamic sections are the built-in `references` and `wiki`, plus every application section with
|
|
103
|
+
- Dynamic sections are the built-in `references`, `required-skills` and `wiki`, plus every application section with
|
|
87
104
|
`dynamic: true`.
|
|
105
|
+
- The rendered sizes of both halves are recorded with the pack (`delivery.sizes`), so the context a run
|
|
106
|
+
was given can be attributed after the fact without replaying the compiler.
|
|
88
107
|
- An update lists only the dynamic sections that changed, and announces a withdrawn one.
|
|
89
|
-
`references`
|
|
108
|
+
`references` and `required-skills` are always resent when present, because they belong to the current request.
|
|
90
109
|
- After successful compaction, the foundation checkpoint extension detects the compacted context and
|
|
91
110
|
uses `renderCompactionCheckpoint` to append one hidden, ephemeral context message to each model
|
|
92
111
|
request while the active run continues, when a todo list or applicable plan instructions exist.
|
|
@@ -121,7 +140,7 @@ file SecretStore with a host Keychain. See [parity](../parity.md).
|
|
|
121
140
|
instructions. SDK sessions use `sessionLaunch`; terminal sessions use `compile` + `render`.
|
|
122
141
|
- **INV-PRM-2** The system prompt holds only stable text: identity, role, the variant's sections, the
|
|
123
142
|
harness's reporting guidance, tool guidance and the tool manifest. Changing state never enters it.
|
|
124
|
-
- **INV-PRM-3** `session-state`, `references`, `wiki`, and every section the application marks
|
|
143
|
+
- **INV-PRM-3** `session-state`, `references`, `required-skills`, `wiki`, and every section the application marks
|
|
125
144
|
`dynamic` are delivered as context updates in native history, never in the system prompt.
|
|
126
145
|
- **INV-PRM-4** An update carries only changed dynamic sections, so an unchanged turn appends
|
|
127
146
|
nothing. A compaction on the active branch forces the full snapshot again.
|
|
@@ -17,7 +17,7 @@ person without letting remembered per-tool grants widen that boundary.
|
|
|
17
17
|
| `harnessTools` | `tools/harnessTools.ts` | The definitions as `ToolSpec`s for one session. |
|
|
18
18
|
| `askUserTool` (`ask_user`) | `tools/askUser.ts` | Stops an attended plan run and asks up to five related questions with choices and concise 1–2 word labels; absent from newly resolved normal runs. |
|
|
19
19
|
| `todoTool` (`todo_write`) | `tools/todo.ts` | Writes the run's whole todo list as a `todo.updated` event (PROP-0006, renamed by PROP-0045). Read-only for the gate, so plan mode allows it. Terminal sessions register the same tool; it reaches the run through the `todo` telemetry call. The prompt's Progress group asks the model to call it before the first other tool call when work has several steps, keep it current, and write one final update before replying; unfinished work is not automatically marked complete. The pre-rename `checklist.updated` event is still read, never written. |
|
|
20
|
-
| `subagentTools` | `tools/spawnSubagent.ts` | `spawn_subagent` (requires a short `title`, PROP-0030) and `wait_subagents`; normal-mode descriptions guide selective, bounded delegation, while plan-mode spawns require a named eligible read-only research profile. |
|
|
20
|
+
| `subagentTools` | `tools/spawnSubagent.ts` | `spawn_subagent` (requires a short `title`, PROP-0030) and `wait_subagents`; normal-mode descriptions guide selective, bounded delegation, while plan-mode spawns require a named eligible read-only research profile. The description states the run's budget, and `Subagents.spawn` refuses a child past its per-profile or total limit (ADR-0052). |
|
|
21
21
|
| `annotationsPart` (`page_notes_list`, `page_notes_read`) | `annotations/part.ts` | Read-only world tools over the page notes a person shared with the session or agent (PROP-0017, PROP-0048). |
|
|
22
22
|
| `webTools` | `web/tools.ts` | `web_search` (Brave, key from the secret store), `web_fetch` (checked public reads). An application provides them as a provider. |
|
|
23
23
|
| gate | `sessions/sessions.ts::gate`, `sessions/runner.ts` | `ToolGate { ask, check }`, loaded as the last extension (`xagent-approvals`). |
|
|
@@ -130,7 +130,9 @@ Standing approvals (`approvals.json`): an always-approve switch per key (`chat`,
|
|
|
130
130
|
`harness`), tools allowed across the state directory per key, and targets remembered per
|
|
131
131
|
session. `humanOnly` calls are never answered by remembered tools, and by the switch only when
|
|
132
132
|
`allowGlobalApproval`. `ApprovalFacts.unattended` disables standing answers; `readOnly` disables
|
|
133
|
-
the switch and global tools.
|
|
133
|
+
the switch and global tools. A person answering through a phone paired by `/remote-control` answers
|
|
134
|
+
the same terminal request through the same controller call, with its complete action text shown
|
|
135
|
+
beside the choices (INV-RC-3, INV-RC-4).
|
|
134
136
|
|
|
135
137
|
## Invariants
|
|
136
138
|
|
|
@@ -88,6 +88,21 @@ operations, not an assertion that a host has built a review UI. Protected termin
|
|
|
88
88
|
require the terminal control key. The model tool can create only. Validation and trust details are
|
|
89
89
|
on page 06.
|
|
90
90
|
|
|
91
|
+
## Context attribution (PROP-0049)
|
|
92
|
+
|
|
93
|
+
| Method | Input | Reply |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `context.attribution` | `{ runId }` | `ContextAttribution`: the model and window, the measured `used` and its `share`, `source` (`measured` or `estimate`), the request index, the compaction reserve and `free`, one row per contributor (`tokens`, share, owner group, `always`, `estimated`, budget, cut, tool breakdown, note), `history` of the window at each request, `compactions`, `onDemand` variant components, group totals and the reconciliation `note` |
|
|
96
|
+
| `terminal.context` | `{ section? }` | `{ attribution, text }` for this terminal's latest run: the same attribution, and one row's text when `section` names it |
|
|
97
|
+
|
|
98
|
+
The rows are read from what the run recorded — `runs/<id>/context.json` for the sections and their
|
|
99
|
+
rendered sizes, `context.snapshot` for the prompt, the update and every tool schema, `usage.updated`
|
|
100
|
+
for the provider's own figure. Nothing is written. A row is estimated at four characters per token;
|
|
101
|
+
the conversation row is the measured total less the rows above it, so the rows add up to the figure
|
|
102
|
+
the provider reported rather than to an estimate of their own. When the pack carries no rendered
|
|
103
|
+
sizes, or the snapshot was truncated, the reply says the split is approximate. `terminal.context`
|
|
104
|
+
resolves the run from the terminal session, never from the request.
|
|
105
|
+
|
|
91
106
|
## Usage (PROP-0042)
|
|
92
107
|
|
|
93
108
|
| Method | Input | Reply |
|
|
@@ -209,6 +224,15 @@ from preparation, never from the request.
|
|
|
209
224
|
| `terminal.usage.allLimits` | `{ refresh? }` | All-account provider limits; refresh reads providers due under the configured cache interval, without changing the pinned account |
|
|
210
225
|
| `terminal.usage.headers` | `{ provider, status, headers }` | `{}`; only limit-bearing headers, throttled, stored as an account limit reading |
|
|
211
226
|
|
|
227
|
+
Remote control (page 15, ADR-0056) adds these protected controls:
|
|
228
|
+
|
|
229
|
+
| Method | Input | Reply |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `terminal.remoteControl.start` | `{ interface?: 'auto' \| 'tailscale' \| 'lan' }` | `RemoteStatus`; listening on one Tailscale or private address |
|
|
232
|
+
| `terminal.remoteControl.stop` \| `rotate` | `{}` | `RemoteStatus`; stopped, or a new link with every device unpaired |
|
|
233
|
+
| `terminal.remoteControl.confirm` | `{ requestId, allow }` | `RemoteStatus`; the person's answer to a pair request |
|
|
234
|
+
| `terminal.remoteControl.status` \| `status.wait` | `{}` / `{ revision }` | `RemoteStatus`; the wait returns at once for a changed revision, otherwise within 20 seconds |
|
|
235
|
+
|
|
212
236
|
Protected `plugins.install { source }`, `plugins.update { pluginId? }` and
|
|
213
237
|
`plugins.remove { pluginId }` delegate to the package service. Removing a Hub-owned package routes
|
|
214
238
|
through Hub's receipt-aware uninstall; a floating update refuses Hub-owned packages, while an
|
|
@@ -46,12 +46,20 @@ Screen: transcript above the composer, centred dialogs, full-screen resource bro
|
|
|
46
46
|
request and latest todo list into post-compaction model context until the run settles.
|
|
47
47
|
- Normal mode keeps `ask_user` out of the active tool set. An attended plan request enables the
|
|
48
48
|
shared tool alongside its read-only set and restores the normal set on success, failure or
|
|
49
|
-
interruption.
|
|
49
|
+
interruption. The question blocks until answered, then planning continues in the same run. There
|
|
50
|
+
is no terminal-only question alias (PROP-0040).
|
|
51
|
+
- The approved plan outcome contract on page 02 applies equally to the terminal and server:
|
|
52
|
+
only `plan_ready` offers a plan card and Implement; `blocked` reports unavailable required input
|
|
53
|
+
without Implement; `invalid` gets at most one shared bounded repair and, if still invalid, a
|
|
54
|
+
warning and the retained original text. New final replies do not ask free-text questions or use
|
|
55
|
+
`<plan_clarification>`: clarification uses `ask_user`, which blocks the active run. Repair at
|
|
56
|
+
Pi's settlement boundary is in the same run and may call `ask_user`. Existing `<proposed_plan>`
|
|
57
|
+
cards and old transcripts, including clarification blocks, remain intact; see ADR-0058.
|
|
50
58
|
- A plan request keeps `bash` in its active set; each call runs in the sandbox's read-only scope
|
|
51
59
|
(PROP-0043). A user `!` command submitted during a plan request waits as a separate turn; it
|
|
52
60
|
cannot run inside the plan's read-only request.
|
|
53
|
-
- The terminal message-delivery contract is Enter to send or
|
|
54
|
-
Cmd/Ctrl/Alt+Enter to
|
|
61
|
+
- The terminal message-delivery contract is Enter to send or queue a follow-up, and
|
|
62
|
+
Cmd/Ctrl/Alt+Enter to steer an active reply. Pending steering, follow-up and deferred plan/Bash
|
|
55
63
|
inputs are projected above the working line from Pi's queues and the deferred barrier. Pi's
|
|
56
64
|
projection may briefly include an item already taken for delivery. A queued plan or Bash command
|
|
57
65
|
runs separately after the active reply, and later inputs cannot bypass it through Pi's native
|
|
@@ -65,12 +73,22 @@ Screen: transcript above the composer, centred dialogs, full-screen resource bro
|
|
|
65
73
|
matching roots/scopes is explicit and matching roots alone does not share host Keychain secrets.
|
|
66
74
|
- `Launch.standard` supplies the shared role and folder text. History, Wiki and skill model tools
|
|
67
75
|
use the session socket; enabled output-trim/budget-guard hooks load through `firstParty.list`.
|
|
76
|
+
- `$name` is a skill invocation at any word start in the composer: the palette opens at the caret
|
|
77
|
+
and inserts the chosen skill there, keeping the rest of the draft, and every `$name` naming a
|
|
78
|
+
loaded skill is required for that request. The terminal builds the request-scoped text with
|
|
79
|
+
`requiredSkillsText` from Pi's loaded skills and appends it as a hidden `xingular-required-skills`
|
|
80
|
+
custom message. A required skill changes no tool or permission, and the person's message reaches
|
|
81
|
+
the model unchanged.
|
|
68
82
|
Pi CLI built-ins (llama.cpp, codemode, tool-search and MCP) are not loaded as a terminal-only
|
|
69
83
|
resource path. Harness MCP remains available.
|
|
70
84
|
- An empty conversation shows a centered welcome using `env.productName` (default `Xingular Agent`):
|
|
71
85
|
the first letter of a multiword name is the orange block mark and its remaining words are letter-spaced
|
|
72
86
|
in blue. The welcome adds a greeting, tagline and `/help` hint without replacing the composer or
|
|
73
87
|
status bar. Explicit host product names use the same generic formatting.
|
|
88
|
+
- `/remote-control` (`/rc`) lets a phone or iPad continue this session: a page with the pairing
|
|
89
|
+
link and its QR code, paired devices, and the person's answer to a pair request; `rc` / `rc ●` in
|
|
90
|
+
the status bar while it is on. `xagent --remote-control` (`--rc`) turns it on at start. The
|
|
91
|
+
binding rules are page 15.
|
|
74
92
|
- `xagent --worktree [name]` (`-w`) starts in a new managed worktree of the current repository,
|
|
75
93
|
or in the named one, and prunes older managed worktrees (PROP-0016). A session that names a
|
|
76
94
|
worktree holds it for its whole life. `/worktree` opens the worktree page; selecting
|
|
@@ -83,7 +101,12 @@ Screen: transcript above the composer, centred dialogs, full-screen resource bro
|
|
|
83
101
|
mirrors the main run's redacted prompt and public evidence, starts a separate tool-free run, and
|
|
84
102
|
leaves the main conversation untouched. The dock streams, scrolls and copies the complete answer.
|
|
85
103
|
Aliases remain callable and searchable, but help and palette show only the canonical
|
|
86
|
-
entry. `/
|
|
104
|
+
entry. `/context` opens the context attribution page: what the latest request was made of, one row
|
|
105
|
+
per contributor with its tokens and share of the window, grouped by owner (foundation, variant,
|
|
106
|
+
application, conversation) with the compaction reserve and free space below it. `/context growth`
|
|
107
|
+
opens its second tab, the window at each measured request with its compaction markers. The page
|
|
108
|
+
reads `terminal.context`, never computes a figure of its own, and says `?` where nothing measured a
|
|
109
|
+
window. `/usage:` and `/usage:analysis` open the usage page's Analysis tab, and `/usage:limit`
|
|
87
110
|
opens its Limits tab (PROP-0042); `/usage:accounts` opens the All accounts limits tab. These are plugin-owned namespaced routes. All-account refresh reads due providers using their own account credentials without switching the session account. The usage page is a
|
|
88
111
|
full-screen page with a top tab strip: each window is a label and reset line above a quota-left bar coloured green at
|
|
89
112
|
50% or more left, amber from 20–49%, and red below 20%. `commands.list`
|
|
@@ -108,9 +131,14 @@ the proposal service checks it again. Acceptance leaves the package switched off
|
|
|
108
131
|
through the controller and the run store.
|
|
109
132
|
- **INV-TUI-4** Changes made through the session socket (`plugins.attach|detach|trust|untrust|configure`,
|
|
110
133
|
`plugins.proposals.*`, `accounts.*`, Hub installs, `terminal.browser.*`, `terminal.worktrees.*`,
|
|
111
|
-
`terminal.annotations.*`, `terminal.sideQuestions.*`) require the control key the trusted terminal process claimed once
|
|
134
|
+
`terminal.annotations.*`, `terminal.sideQuestions.*`, `terminal.remoteControl.*`) require the control key the trusted terminal process claimed once
|
|
112
135
|
(`control.claim`). The session's shell can read the session token, so the token alone is never
|
|
113
|
-
enough.
|
|
136
|
+
enough. A phone paired through `/remote-control` is the one other party that answers decisions;
|
|
137
|
+
only the control-key holder can start remote control or allow a device (INV-RC-3, INV-RC-7).
|
|
138
|
+
- The terminal watches decisions through the control-key-protected `terminal.pending.wait` revision
|
|
139
|
+
snapshot. It returns immediately for a changed revision and otherwise waits at most 20 seconds;
|
|
140
|
+
additions, removals and disposal wake waiters. `terminal.pending` remains available. The terminal
|
|
141
|
+
reconnects and resynchronizes after errors, and cancels its outstanding request on shutdown.
|
|
114
142
|
- **INV-TUI-5** Every resource reload revalidates the original trusted selection; changed or
|
|
115
143
|
revoked code needs review and a fresh terminal.
|
|
116
144
|
- **INV-TUI-6** `sessions.run` stays the embedding and unattended path. The terminal keeps a
|
|
@@ -194,7 +222,7 @@ The browser uses the full terminal with persistent section tabs (PROP-0014); wid
|
|
|
194
222
|
inventory beside details, actions and content, while compact screens keep inline details under
|
|
195
223
|
the same tabs. The separate `/plugins` and `/hub` pages also use full-screen layouts with persistent
|
|
196
224
|
section tabs, responsive inventory/details panes and their existing protected operations. Other
|
|
197
|
-
command pages remain centered except `/settings`, `/accounts`, `/plugins`, `/hub` and `/usage:`, which use
|
|
225
|
+
command pages remain centered except `/settings`, `/accounts`, `/plugins`, `/hub`, `/context` and `/usage:`, which use
|
|
198
226
|
full-screen page layouts.
|
|
199
227
|
Accounts keeps Accounts/Pools tabs, search and account-service operations.
|
|
200
228
|
Wide terminals show selected-account details beside the list; Enter or a primary click opens grouped
|
|
@@ -323,11 +351,25 @@ extension registers the shared tool and sends it as `checkpoint` telemetry, whic
|
|
|
323
351
|
records as `checkpoint` on the
|
|
324
352
|
active run, so an App reading the same session sees the handoff. Terminal replies are not
|
|
325
353
|
auto-continued: at the boundary the person decides, which is what the terminal's own turn model
|
|
326
|
-
already does. The run record says `interrupted` with terminal reason `slice_complete
|
|
327
|
-
|
|
328
|
-
|
|
354
|
+
already does. The run record says `interrupted` with terminal reason `slice_complete`, so a host can
|
|
355
|
+
continue it; a checkpoint card in the transcript is not built yet (PAR-004). Start the terminal with
|
|
356
|
+
`XINGULAR_WORK_SLICE=off` to launch a reply with no
|
|
329
357
|
boundary at all; it is not persisted, so the next launch without it restores the guard.
|
|
330
358
|
|
|
359
|
+
## Liveness and a reply that stops working (INV-RUN-14, ADR-0054)
|
|
360
|
+
|
|
361
|
+
While a reply is live the extension sends `heartbeat` telemetry every 30 seconds, carrying the work
|
|
362
|
+
slice counters it already keeps (steps, elapsed time, last tool, compactions, slice and the call
|
|
363
|
+
budget); the controller records it as `heartbeat` on the active run, so the run sweep settles a
|
|
364
|
+
terminal reply that stopped reporting by the same five-minute rule as a structured one.
|
|
365
|
+
|
|
366
|
+
A heartbeat proves the turn is alive, not that it is working. A reply that produces no event other
|
|
367
|
+
than a heartbeat for fifteen minutes — the state a model request that never returns leaves it in —
|
|
368
|
+
is ended: the controller queues the same `interrupt` command the person's Esc queues, the native turn
|
|
369
|
+
aborts, and the run records `interrupted` with reason `stalled`. A reply whose oldest running tool
|
|
370
|
+
has run for thirty minutes ends the same way, and a reply waiting on an approval or question is never
|
|
371
|
+
cut short.
|
|
372
|
+
|
|
331
373
|
## Subagent activity monitor
|
|
332
374
|
|
|
333
375
|
During an active parent run, the terminal polls a private read-only monitor once per second. It
|
|
@@ -27,8 +27,8 @@ index.
|
|
|
27
27
|
trash/plugins/ removed packages (06)
|
|
28
28
|
hub/ installed documents and provenance (09)
|
|
29
29
|
runs/<id>/ run.json (including optional legacy-compatible workspaceRoot), events.jsonl, owner.json, subagents.json, context.json, caller.json, capabilities.json, mcp-context.json (02)
|
|
30
|
-
# `checkpoint` and `heartbeat` are run events in the same events.jsonl;
|
|
31
|
-
# heartbeat per 30 seconds (about 60 KB an hour) and no side file (PROP-0041).
|
|
30
|
+
# `checkpoint` and `heartbeat` are run events in the same events.jsonl; every live reply emits one
|
|
31
|
+
# heartbeat per 30 seconds (about 60 KB an hour) and no side file (PROP-0041, ADR-0054).
|
|
32
32
|
config/ plugin-attachments.json and settings the harness reads (06)
|
|
33
33
|
agents/<name>.md a person's global agent profiles; agents/profiles.ts owns it (02, PROP-0029)
|
|
34
34
|
index.sqlite derived index (FTS5, recall, usage ledger) (08, PROP-0042)
|
|
@@ -51,6 +51,9 @@ index.
|
|
|
51
51
|
harness.log standalone terminal diagnostics
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
`/remote-control` (page 15) stores nothing here: its pairing and device keys live in memory and end
|
|
55
|
+
with the session (INV-RC-2).
|
|
56
|
+
|
|
54
57
|
The interactive profile is a credential-free, versioned selection file at
|
|
55
58
|
`~/.xingular-harness/profile.json`, owned by `src/core/interactiveProfile.ts`. It selects the
|
|
56
59
|
interactive defaults: data root, state directory, default variant scope, and a credential-backend
|
|
@@ -18,14 +18,28 @@ synthetic page. Their input runs only after live-check confirmation, and their r
|
|
|
18
18
|
released on completion, timeout or cancellation. Computer interaction needs a host-provided
|
|
19
19
|
isolated surface; without one it is blocked.
|
|
20
20
|
|
|
21
|
+
## Approved-window screenshot contract
|
|
22
|
+
|
|
23
|
+
`computer/screenshot.ts` defines an opt-in host provider adapter for `computer_screenshot`.
|
|
24
|
+
The backend must capture exactly one currently approved window, excluding the desktop, other windows
|
|
25
|
+
and overlays even when they overlap, and refuse if it cannot guarantee that scope. It must not
|
|
26
|
+
fall back to a display capture. Coordinates supplied to a host's related interaction tools, if any,
|
|
27
|
+
are relative to the returned image pixels. The tool goes through the ordinary provider and gate,
|
|
28
|
+
returns an image to the model, and is not plan-mode safe by default. Image content is untrusted.
|
|
29
|
+
The contract itself does not prove that a host implements these requirements. In particular, the
|
|
30
|
+
existing macOS host capture has not yet adopted this contract and captures a display; it must not
|
|
31
|
+
be represented as approved-window-only capture. The standalone terminal has no capture backend.
|
|
32
|
+
|
|
33
|
+
|
|
21
34
|
## Shared browser contract and native WebKit limits (PROP-0040)
|
|
22
35
|
|
|
23
36
|
`browser/contracts.ts` owns the backend-neutral names and input schemas for `browser_*` tools;
|
|
24
37
|
providers adapt those definitions rather than inventing host-specific selectors. The isolated Harness
|
|
25
38
|
backend supports accessibility snapshots and snapshot-scoped refs. A native WebKit host backend
|
|
26
|
-
|
|
27
|
-
ref. It does not support accessibility snapshots, snapshot refs or ref-based
|
|
28
|
-
|
|
39
|
+
supports navigation, tab management, visible-text reads, page screenshots and page-level scrolling
|
|
40
|
+
without a ref. It does not support accessibility snapshots, snapshot refs or ref-based
|
|
41
|
+
click/type/select/scroll/screenshot, browser waits or keyboard presses. Native page screenshot
|
|
42
|
+
implementation and authorization checks are present, but live host capture acceptance remains open.
|
|
29
43
|
|
|
30
44
|
For an unsupported operation the native provider returns an explicit `browser_unsupported` refusal
|
|
31
45
|
with “Nothing was done”; it never treats a snapshot ref as a selector and never silently switches to
|
|
@@ -97,14 +97,18 @@ terminal command or page, a host screen.
|
|
|
97
97
|
It is in the shared plan-mode allowlist and grants no permission. Hosts use this Harness tool rather
|
|
98
98
|
than defining a duplicate.
|
|
99
99
|
- `browser/contracts.ts` owns shared backend-neutral browser schemas. A native WebKit host backend
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
automatic backend switch (page 13, PAR-015).
|
|
100
|
+
lacks accessibility snapshots and snapshot-ref interactions, waits and keyboard input; it supports
|
|
101
|
+
page screenshots without refs in source, with live acceptance pending. Unsupported calls return an
|
|
102
|
+
explicit refusal before effects and do not trigger an automatic backend switch (page 13, PAR-015).
|
|
103
103
|
- Shared interactive defaults, profile adoption, credentials and a cross-surface session catalog
|
|
104
104
|
remain in progress under PROP-0040. Existing pins and explicit profile choices win; adoption must
|
|
105
105
|
not merge stores, copy credentials or silently downgrade the secret backend.
|
|
106
106
|
- `PluginDescriptor.surfaces` / `Plugin.surfaces` optionally describe a host preference, terminal
|
|
107
107
|
fallback/unavailability and reason. This labels add-ons, not plugin categories or permissions.
|
|
108
|
+
- Remote control (page 15) is a third Harness-owned surface of a terminal session: a phone page the
|
|
109
|
+
harness serves. It is not a host and owns no capability. A paired device acts through the same
|
|
110
|
+
`Terminals` and controller paths as the terminal, and reaches no command, shell, link, plugin or
|
|
111
|
+
account control.
|
|
108
112
|
- The register of capabilities and their surfaces is [`docs/parity.md`](../parity.md). It is
|
|
109
113
|
descriptive; the invariants below are binding.
|
|
110
114
|
- A host keeps its own register of host-only surfaces. The Harness names no host surface.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# 15 Remote control
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Remote control lets a person continue a live terminal session from a phone or iPad. The session
|
|
6
|
+
keeps running in `xagent` on the computer. A paired device sends messages, watches the reply as it
|
|
7
|
+
streams, stops it, and answers approvals and questions. It works without a cloud relay: the
|
|
8
|
+
harness serves a small web page on the computer's Tailscale address, or its private Wi-Fi or LAN
|
|
9
|
+
address, and the device opens it from a QR code. Remote control ends when the terminal session
|
|
10
|
+
ends ([ADR-0056](../decisions/ADR-0056-remote-control.md)).
|
|
11
|
+
|
|
12
|
+
A paired device is a second person at the terminal. It reaches the same controller and `Terminals`
|
|
13
|
+
methods the terminal uses and nothing else, so remote control adds a surface, not a capability
|
|
14
|
+
path.
|
|
15
|
+
|
|
16
|
+
## Components
|
|
17
|
+
|
|
18
|
+
| Type / module | Role |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `remote/addresses.ts` | Bind candidates from `networkInterfaces()`: Tailscale (100.64.0.0/10, fd7a:115c:a1e0::/48) first, then RFC 1918 IPv4. `pickRemoteAddress(auto\|tailscale\|lan)`. |
|
|
21
|
+
| `remote/seal.ts` | Sealed frames: NaCl `secretbox` (XSalsa20-Poly1305, `tweetnacl`) with a random 24-byte nonce; a counter inside each frame, checked by `ReplayGuard` (a 256-counter window). |
|
|
22
|
+
| `remote/pairing.ts` | `Pairing`: the pairing link (id and key), one waiting pair request, paired devices (`RemoteDevice`), limits, rotation. |
|
|
23
|
+
| `remote/server.ts` | `listenRemote`: `node:http` on one address; the page assets, `POST /rc/hello`, `POST /rc/call`, `GET /rc/events` (server-sent events). Host and Origin checks, body caps, no-store and CSP headers. |
|
|
24
|
+
| `remote/service.ts` | `RemoteControl` for one terminal session over a `RemoteSessionPort`: start, stop, rotate, confirm, status and `waitStatus`; projects run events and decisions for the phone. |
|
|
25
|
+
| `remote/webClient.ts` | The phone page: HTML, CSS and script, served as is. |
|
|
26
|
+
| `terminal/tui/pages/remoteControlPage.ts`, `terminal/tui/qr.ts` | The `/remote-control` page and its half-block QR code (`uqr`). |
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
Phone browser ── http://<tailscale or LAN address>:<port> ──> RemoteControl (xagent parent)
|
|
30
|
+
sealed hello / calls / events │ RemoteSessionPort
|
|
31
|
+
├─ Terminals.send
|
|
32
|
+
├─ TerminalController.control (interrupt, approve, answer, pending.wait)
|
|
33
|
+
└─ RunRegistry.listen (this session's own runs)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Contracts
|
|
37
|
+
|
|
38
|
+
- **Terminal surface.** `/remote-control` (alias `/rc`) starts remote control for the current
|
|
39
|
+
session and opens its page: the link, the QR code (space hides it), the address kind, paired
|
|
40
|
+
devices, and a waiting pair request. Keys: `c` copy the link, `n` a new link (every device is
|
|
41
|
+
unpaired), `t` switch between Tailscale and Wi-Fi when both exist, `x` stop, `a`/`d` allow or
|
|
42
|
+
deny a request. Esc closes the page; remote control keeps running. `/rc off` stops it;
|
|
43
|
+
`/rc tailscale` and `/rc lan` choose the address kind. `xagent --remote-control` (`--rc`) runs
|
|
44
|
+
`/remote-control` as the terminal opens. The status bar shows `rc` while it is on and `rc ●`
|
|
45
|
+
while a device is connected. When the page is closed, the terminal asks about a pair request
|
|
46
|
+
in a confirmation dialog.
|
|
47
|
+
- **Methods.** `terminal.remoteControl.start { interface? }`, `.stop`, `.rotate`,
|
|
48
|
+
`.confirm { requestId, allow }`, `.status`, `.status.wait { revision }` (returns at once for a
|
|
49
|
+
changed revision, otherwise within 20 seconds). Each returns a `RemoteStatus`
|
|
50
|
+
`{ running, revision, url, kind, kinds, devices, request, warning }`. All of them go through the
|
|
51
|
+
session socket's control key (INV-TUI-4).
|
|
52
|
+
- **Pairing.** The link is `http://<address>:<port>/#<pairingId>.<key>`. A browser never sends the
|
|
53
|
+
fragment. The phone says hello with a frame sealed by the link key. The request waits up to 50
|
|
54
|
+
seconds for the person at the terminal to allow it; the reply, sealed by the link key, carries
|
|
55
|
+
the device's id and its own key. A link pairs one device and is then replaced, so the QR code on
|
|
56
|
+
screen pairs the next one. At most 3 devices; at most 10 hello attempts a minute; 20 failed
|
|
57
|
+
attempts replace the link and show a warning.
|
|
58
|
+
- **Calls.** `POST /rc/call { d: deviceId, f: frame }`. The frame holds `{ id, method, params }`;
|
|
59
|
+
the reply frame holds `{ id, result }` or `{ id, error }`. Methods: `snapshot` (title, the
|
|
60
|
+
session's own runs as phone events, pending decisions, state), `send { text }`, `interrupt`,
|
|
61
|
+
`approve { requestId, decision, alwaysAllow? }`, `answer { requestId, answer }`,
|
|
62
|
+
`answers { batchId, answers }`, `unpair`. Anything else is refused.
|
|
63
|
+
- **Events.** `GET /rc/events?d=&f=` opens a stream of sealed frames:
|
|
64
|
+
`{ type: 'event', event }`, `{ type: 'pending', requests }`, `{ type: 'ended' }`,
|
|
65
|
+
`{ type: 'unpaired' }`. A phone event is one of `delta`, `message`, `tool`, `run`, `todo`, with
|
|
66
|
+
only the fields the page shows: no tool input or output, no context snapshot, no native evidence.
|
|
67
|
+
An approval's `action` text is sent whole.
|
|
68
|
+
- **Streaming.** The terminal extension batches assistant text deltas (150 ms) into a `delta`
|
|
69
|
+
telemetry event; the controller records it as `message.delta` under the id the completed
|
|
70
|
+
`message` then carries. It is not host telemetry.
|
|
71
|
+
|
|
72
|
+
## Invariants
|
|
73
|
+
|
|
74
|
+
- **INV-RC-1** Remote control listens on exactly one Tailscale or private LAN address, never on a
|
|
75
|
+
public address or on all interfaces. With no such address it refuses with the reason. (Tests
|
|
76
|
+
inject a loopback address through `RemoteControlOptions.bindHost`.)
|
|
77
|
+
- **INV-RC-2** Every remote-control key lives in memory only. Nothing about pairing is written to
|
|
78
|
+
the data root or the state directory; a phone keeps its own device key in its browser storage.
|
|
79
|
+
- **INV-RC-3** A paired device acts only through the terminal's existing paths: `Terminals.send`,
|
|
80
|
+
and `TerminalController.control` for `terminal.interrupt`, `terminal.approve`,
|
|
81
|
+
`terminal.answer`, `terminal.answers` and `terminal.pending.wait`. The validation those paths
|
|
82
|
+
apply (a busy session, a terminal draft, the rememberable check on "always allow", the policy
|
|
83
|
+
recheck of a direct approval) applies unchanged.
|
|
84
|
+
- **INV-RC-4** A decision is shown on the phone with its complete action text, never shortened,
|
|
85
|
+
beside its choices. The first answer from either surface wins; the other surface's card goes
|
|
86
|
+
away on the next pending revision.
|
|
87
|
+
- **INV-RC-5** Remote control ends with its terminal session (`Terminals.dispose`): the server
|
|
88
|
+
closes, connected phones are told, and every device key is forgotten. `stop` does the same
|
|
89
|
+
without ending the session.
|
|
90
|
+
- **INV-RC-6** A paired device runs no slash command, no `!` shell command and no link, and reaches
|
|
91
|
+
no plugin, account, Hub, worktree or browser control. A message starting with `/` or `!` is
|
|
92
|
+
refused.
|
|
93
|
+
- **INV-RC-7** Starting remote control and allowing a device are decisions of the person at the
|
|
94
|
+
terminal: both are control-key calls, and no phone can pair without that confirmation.
|
|
95
|
+
- **INV-RC-8** Every body between the phone and the harness, except the static page, is sealed.
|
|
96
|
+
A frame that does not open, or whose counter was seen, is refused the same way.
|
|
97
|
+
|
|
98
|
+
## Threat notes
|
|
99
|
+
|
|
100
|
+
Sealing defeats a passive listener on the network and replays. It does not defeat an attacker who
|
|
101
|
+
can change traffic on the LAN (for example by ARP spoofing): the page itself is served over plain
|
|
102
|
+
HTTP, so such an attacker could serve a changed page to the phone. The terminal confirmation is the
|
|
103
|
+
remaining check. The `/rc` page tells the person to use Tailscale, whose traffic is end-to-end
|
|
104
|
+
encrypted, on a network they do not trust.
|
|
105
|
+
|
|
106
|
+
## Data
|
|
107
|
+
|
|
108
|
+
None stored. The phone's browser keeps `{ deviceId, deviceKey, label }` under
|
|
109
|
+
`remote-control:<host:port>` in its own storage; it is useless once the session ends.
|
|
110
|
+
|
|
111
|
+
## Source map
|
|
112
|
+
|
|
113
|
+
`src/remote/`, `src/terminal/terminals.ts` (`remote`, `remotePort`, `remoteCall`, dispose),
|
|
114
|
+
`src/terminal/controller.ts` (`delta`), `src/terminal/piExtension.ts` (`message_update`),
|
|
115
|
+
`src/terminal/tui/pages/remoteControlPage.ts`, `src/terminal/tui/qr.ts`,
|
|
116
|
+
`src/terminal/opentui/app.ts` (`watchRemoteControl`), `src/terminal/opentui/statusBar.ts`,
|
|
117
|
+
`src/terminal/standalone.ts` (`--remote-control`). Decision:
|
|
118
|
+
[ADR-0056](../decisions/ADR-0056-remote-control.md).
|
|
@@ -43,6 +43,7 @@ not one class (a set of functions in one module, say), it says so.
|
|
|
43
43
|
| [12 Storage](12-storage.md) | The data root and state directory, and who writes what | `INV-STO` |
|
|
44
44
|
| [13 World tools](13-world-tools.md) | Generic web and isolated browser backends, host boundary and diagnostics | — |
|
|
45
45
|
| [14 Surfaces and parity](14-surfaces-and-parity.md) | Every capability is a Harness capability with a terminal surface; host-backed capabilities; host-only surfaces | `INV-PAR` |
|
|
46
|
+
| [15 Remote control](15-remote-control.md) | Continuing a terminal session from a phone or iPad: pairing, sealed frames, the bind policy | `INV-RC` |
|
|
46
47
|
| [Proposals](proposals/README.md) | Architecture change proposals and their status | — |
|
|
47
48
|
|
|
48
49
|
Related records, not binding by themselves:
|
|
@@ -128,3 +129,5 @@ resolve relevant drift by updating the implementation and specification together
|
|
|
128
129
|
| D-20 | Terminal pending queues, page 11 — resolved | The terminal now projects native and deferred pending messages above the working line. |
|
|
129
130
|
| D-19 | Provider limits layout | Page 09 describes a centered Limits dialog; page 11 and `src/terminal/tui/pages/usagePage.ts` describe and implement a full-screen page. |
|
|
130
131
|
| D-18 | `core/index.ts` migration 5 | The `run_stats` table has no writer in the Harness. PROP-0042's `usage_requests` ledger is the analytics source; whether to drop the older table is open. |
|
|
132
|
+
| D-21 | `terminal-tui.md`, per-mode model and effort | The terminal page says the App reads and writes the per-mode choices through `accounts.choices` and `accounts.choose`. The App's source calls neither method: its account, model and effort are saved on the App Thread (App ADR-0023), and plan mode shares that Thread's tuple. Whether the App should adopt the Harness's per-mode store is an owner decision. |
|
|
133
|
+
| D-22 | Plan outcomes, pages 02 and 11; ADR-0055 — resolved | Page 02 previously described a missing-block repair without distinguishing paths: the runner repaired but the terminal only warned, and neither classified clarification. ADR-0057 adds shared outcome classification and a bounded repair in both paths. |
|