@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.
Files changed (125) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +3680 -1947
  4. package/dist/sdk/cli.js +2 -0
  5. package/dist/sdk/commands/registry.d.ts +15 -0
  6. package/dist/sdk/commands/registry.js +2 -0
  7. package/dist/sdk/computer/screenshot.d.ts +32 -0
  8. package/dist/sdk/computer/screenshot.js +35 -0
  9. package/dist/sdk/context/attribution.d.ts +18 -0
  10. package/dist/sdk/context/attribution.js +295 -0
  11. package/dist/sdk/context/types.d.ts +94 -0
  12. package/dist/sdk/context/types.js +10 -0
  13. package/dist/sdk/index.d.ts +2 -1
  14. package/dist/sdk/index.js +2 -1
  15. package/dist/sdk/plugins/builtin.js +1 -0
  16. package/dist/sdk/prompt/compile.d.ts +42 -2
  17. package/dist/sdk/prompt/compile.js +87 -15
  18. package/dist/sdk/remote/addresses.d.ts +19 -0
  19. package/dist/sdk/remote/addresses.js +52 -0
  20. package/dist/sdk/remote/pairing.d.ts +59 -0
  21. package/dist/sdk/remote/pairing.js +160 -0
  22. package/dist/sdk/remote/seal.d.ts +28 -0
  23. package/dist/sdk/remote/seal.js +84 -0
  24. package/dist/sdk/remote/server.d.ts +28 -0
  25. package/dist/sdk/remote/server.js +158 -0
  26. package/dist/sdk/remote/service.d.ts +125 -0
  27. package/dist/sdk/remote/service.js +330 -0
  28. package/dist/sdk/remote/webClient.d.ts +4 -0
  29. package/dist/sdk/remote/webClient.js +444 -0
  30. package/dist/sdk/server/methods.js +3 -0
  31. package/dist/sdk/sessions/contracts.d.ts +4 -0
  32. package/dist/sdk/sessions/contracts.js +2 -0
  33. package/dist/sdk/sessions/planMode.d.ts +26 -1
  34. package/dist/sdk/sessions/planMode.js +48 -1
  35. package/dist/sdk/sessions/runner.d.ts +12 -0
  36. package/dist/sdk/sessions/runner.js +34 -2
  37. package/dist/sdk/sessions/sessions.js +1 -0
  38. package/dist/sdk/sessions/subagentService.d.ts +11 -1
  39. package/dist/sdk/sessions/subagentService.js +120 -38
  40. package/dist/sdk/sessions/subagents.d.ts +31 -0
  41. package/dist/sdk/sessions/subagents.js +45 -0
  42. package/dist/sdk/sessions/workSlice.d.ts +5 -9
  43. package/dist/sdk/sessions/workSlice.js +8 -149
  44. package/dist/sdk/terminal/client.d.ts +3 -1
  45. package/dist/sdk/terminal/client.js +15 -6
  46. package/dist/sdk/terminal/control.d.ts +1 -1
  47. package/dist/sdk/terminal/control.js +2 -2
  48. package/dist/sdk/terminal/controller.d.ts +22 -0
  49. package/dist/sdk/terminal/controller.js +104 -13
  50. package/dist/sdk/terminal/piExtension.js +106 -4
  51. package/dist/sdk/terminal/stallWatch.d.ts +13 -0
  52. package/dist/sdk/terminal/stallWatch.js +16 -0
  53. package/dist/sdk/terminal/standalone.d.ts +2 -0
  54. package/dist/sdk/terminal/standalone.js +9 -2
  55. package/dist/sdk/terminal/terminals.d.ts +14 -0
  56. package/dist/sdk/terminal/terminals.js +98 -2
  57. package/dist/sdk/terminal/tui/commandPages.d.ts +4 -1
  58. package/dist/sdk/terminal/tui/commandPages.js +12 -0
  59. package/dist/sdk/terminal/tui/commands.d.ts +3 -2
  60. package/dist/sdk/terminal/tui/commands.js +19 -5
  61. package/dist/sdk/terminal/tui/dialogs.d.ts +3 -2
  62. package/dist/sdk/terminal/tui/dialogs.js +4 -3
  63. package/dist/sdk/terminal/tui/keybindings.d.ts +1 -1
  64. package/dist/sdk/terminal/tui/keybindings.js +1 -1
  65. package/dist/sdk/terminal/tui/markdown.d.ts +16 -0
  66. package/dist/sdk/terminal/tui/markdown.js +55 -0
  67. package/dist/sdk/terminal/tui/modeChoices.d.ts +3 -2
  68. package/dist/sdk/terminal/tui/modeChoices.js +4 -4
  69. package/dist/sdk/terminal/tui/mouseReport.d.ts +30 -0
  70. package/dist/sdk/terminal/tui/mouseReport.js +80 -0
  71. package/dist/sdk/terminal/tui/pages/annotatePage.js +3 -2
  72. package/dist/sdk/terminal/tui/pages/browserPage.d.ts +1 -1
  73. package/dist/sdk/terminal/tui/pages/contextPage.d.ts +62 -0
  74. package/dist/sdk/terminal/tui/pages/contextPage.js +484 -0
  75. package/dist/sdk/terminal/tui/pages/remoteControlPage.d.ts +32 -0
  76. package/dist/sdk/terminal/tui/pages/remoteControlPage.js +170 -0
  77. package/dist/sdk/terminal/tui/pages/trajectoryPage.js +5 -4
  78. package/dist/sdk/terminal/tui/qr.d.ts +4 -0
  79. package/dist/sdk/terminal/tui/qr.js +35 -0
  80. package/dist/sdk/terminal/tui/runtime.js +8 -2
  81. package/dist/sdk/terminal/tui/search.js +3 -2
  82. package/dist/sdk/terminal/tui/sideQuestion.d.ts +1 -0
  83. package/dist/sdk/terminal/tui/sideQuestion.js +8 -3
  84. package/dist/sdk/terminal/tui/skillInvocation.d.ts +21 -2
  85. package/dist/sdk/terminal/tui/skillInvocation.js +31 -11
  86. package/dist/sdk/terminal/tui/status.d.ts +6 -0
  87. package/dist/sdk/terminal/tui/status.js +15 -1
  88. package/dist/sdk/terminal/tui/theme.d.ts +9 -0
  89. package/dist/sdk/terminal/tui/theme.js +9 -0
  90. package/dist/sdk/tools/defaultSkills.d.ts +19 -0
  91. package/dist/sdk/tools/defaultSkills.js +27 -1
  92. package/dist/sdk/tools/dispatch.js +8 -9
  93. package/dist/sdk/tools/spawnSubagent.d.ts +1 -1
  94. package/dist/sdk/tools/spawnSubagent.js +4 -2
  95. package/dist/terminal-opentui.js +1607 -492
  96. package/dist/terminal-pi.js +256 -144
  97. package/docs/README.md +1 -0
  98. package/docs/architecture/02-sessions-and-runs.md +55 -22
  99. package/docs/architecture/04-prompt.md +23 -4
  100. package/docs/architecture/07-tools-and-gate.md +4 -2
  101. package/docs/architecture/10-server-protocol.md +24 -0
  102. package/docs/architecture/11-terminal.md +52 -10
  103. package/docs/architecture/12-storage.md +5 -2
  104. package/docs/architecture/13-world-tools.md +17 -3
  105. package/docs/architecture/14-surfaces-and-parity.md +7 -3
  106. package/docs/architecture/15-remote-control.md +118 -0
  107. package/docs/architecture/README.md +3 -0
  108. package/docs/architecture/proposals/PROP-0049-context-attribution.md +409 -0
  109. package/docs/architecture/proposals/README.md +2 -1
  110. package/docs/decisions/ADR-0039-progress-aware-stall-guards.md +4 -0
  111. package/docs/decisions/ADR-0048-required-skills-from-a-message.md +42 -0
  112. package/docs/decisions/ADR-0050-approved-window-screenshots.md +19 -0
  113. package/docs/decisions/ADR-0051-terminal-decision-wait.md +10 -0
  114. package/docs/decisions/ADR-0052-per-profile-subagent-budgets.md +25 -0
  115. package/docs/decisions/ADR-0053-no-repeated-work-guard.md +45 -0
  116. package/docs/decisions/ADR-0054-terminal-reply-heartbeat-and-stall.md +52 -0
  117. package/docs/decisions/ADR-0055-plan-output-repair.md +51 -0
  118. package/docs/decisions/ADR-0056-remote-control.md +53 -0
  119. package/docs/decisions/ADR-0057-structured-plan-outcomes.md +39 -0
  120. package/docs/decisions/ADR-0058-ask-user-only-plan-clarification.md +36 -0
  121. package/docs/parity.md +14 -10
  122. package/docs/releases/1.3.1-unstable.3.md +54 -0
  123. package/docs/releases/1.3.1-unstable.4.md +56 -0
  124. package/docs/terminal-tui.md +43 -18
  125. 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
- The reply ends with one `<proposed_plan>` block.
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`; a reply ended by a stall guard is `stalled`.
278
- The targeted stall guard identifies calls by name and every argument; three identical calls cause
279
- one refusal, and a repeated trigger ends the reply. A Bash check that fails twice is a trigger. The
280
- guard counts only successful native `edit`/`write` results (`isError === false`) with a path toward
281
- the four-edit per-path churn limit; the next edit is refused, and a repeated trigger ends the
282
- reply. Failed or blocked edits do not count. A successful edit/write clears failed-check counts,
283
- identical Bash-check repeat counts and the pending nudge, but leaves edit counts and other
284
- repeated-call counts intact. A passing check clears edit counts. The general 25-call file/check
285
- no-progress trigger is removed. `todo_write` is bookkeeping: it is exempt from repeated-call
286
- detection and does not reset loop state, but counts toward the tool-call budget. Third-compaction
287
- thrash detection remains. Plan-research children without `checkpoint_write` receive the configured
288
- bound without a new capability and do not auto-continue. A new run continues it only up to the
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). A heartbeat older than
297
- five minutes while the owner process lives marks the run `interrupted` with reason `stalled`; a
298
- run that never emitted one keeps the 30-minute silence rule. The watchdog never restarts work.
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` is always resent when present, because it belongs to the current request.
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. There is no terminal-only question alias (PROP-0040).
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 steer an active reply, and
54
- Cmd/Ctrl/Alt+Enter to queue a follow-up. Pending steering, follow-up and deferred plan/Bash
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. `/usage:` and `/usage:analysis` open the usage page's Analysis tab, and `/usage:limit`
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` (or `stalled`
327
- when the guard ended it), so a host can continue it; a checkpoint card in the transcript is not built
328
- yet (PAR-004). Start the terminal with `XINGULAR_WORK_SLICE=off` to launch a reply with no
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; a bounded reply emits one
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
- currently supports navigation, tab management, visible-text reads and page-level scrolling without a
27
- ref. It does not support accessibility snapshots, snapshot refs or ref-based click/type/select/scroll,
28
- browser screenshots, browser waits or keyboard presses.
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
- still lacks accessibility snapshots and snapshot-ref interactions, browser screenshots, waits and
101
- keyboard input; unsupported calls return an explicit refusal before effects and do not trigger an
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. |