@4fu/dsh-arbor-prompt 0.0.0-stage → 2026.1110.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 4fuu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,250 @@
1
- # Temporary Holding Version
1
+ # dsh-arbor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Run [arbor](https://github.com/4fuu/arbor) as the agent driver inside
4
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH).
5
+
6
+ DSH gives a user a session, a transcript, approvals, plugins, skills and a model
7
+ picker. arbor gives an agent disposable context windows over one shared journal,
8
+ note store and workspace, instead of one long conversation. dsh-arbor connects
9
+ them: one arbor host runs inside the DSH process, each DSH session that selects
10
+ **Arbor** is served by that host, and the window's reply is written back to the
11
+ DSH session as an ordinary assistant message. A DSH session that does not select
12
+ Arbor keeps running on DSH's own embedded AgentLoop, in the same process.
13
+
14
+ > **Status.** This is a research-grade integration, not a DSH release. It is
15
+ > pinned to one DSH version and relies on DSH internals. Read
16
+ > [Versions and compatibility](docs/versions.md) before installing it on
17
+ > anything you care about. The packages are published under `@4fu` at
18
+ > `2026.1110.1`; the bundle and the font plugin install independently from npm.
19
+
20
+ ## What an arbor session changes
21
+
22
+ Choosing **Arbor** in a DSH composer creates the session under the `arbor` agent
23
+ preset. That session then runs on arbor, and the following are true for it:
24
+
25
+ - **One window per turn, one chain per session.** A DSH session is one arbor
26
+ conversation: its first turn opens a window, a window that ends hands off to a
27
+ continuation, and the next turn drives the chain. The journal and note store
28
+ live under `$DSH_HOME/arbor/` and survive a restart.
29
+ - **arbor's workspace, not DSH's sandbox.** A window works in arbor's shared
30
+ folder (`$DSH_HOME/arbor/shared`). When the `ovnfs` binary is available a
31
+ window opens a copy-on-write checkout and merges back with `apply`; otherwise
32
+ arbor uses a plain directory. **There is no DSH sandbox for an arbor session:**
33
+ arbor's file tools and commands run on the host, and arbor's own secret scrub
34
+ is the only protection on what reaches the model.
35
+ - **arbor's projects.** The shared folder's project folders are the DSH project
36
+ list for arbor composers. A directory the user adds outside the shared folder
37
+ is recorded as an external pointer for that session and used in place.
38
+ - **arbor's prompt and tools.** DSH's official prompt sections are decided one by
39
+ one; the ones arbor replaces are absent, third-party sections go into a
40
+ `<dsh>` data block, and a trusted `<harness>` section says what remains true of
41
+ DSH. DSH tools are offered to the model as arbor external tools, and arbor's
42
+ own tools replace DSH tools with the same capability.
43
+ - **The Arbor page.** A permanent **Arbor** entry in DSH's left navigation opens
44
+ the whole-host overview and one tab per arbor session: windows, chains,
45
+ handoffs, projects, the board, notes, `/env`, schedules and arbor's search.
46
+ An arbor composer hides DSH's sandbox access-mode switch, because that switch
47
+ does not apply to it.
48
+
49
+ ## What stays untouched in ordinary DSH sessions
50
+
51
+ A session that does not select Arbor is an ordinary DSH session. It keeps:
52
+
53
+ - DSH's embedded AgentLoop, session format, turn boundaries and streaming path;
54
+ - DSH's sandbox policy and its `ctx.fs` / `ctx.shell` file and command tools;
55
+ - DSH's project management, working directory and prompt assembly;
56
+ - its composer, sandbox access-mode switch, header and sidebar behavior.
57
+
58
+ It gets **no Arbor tab** and no arbor windows. The only shared surface is the
59
+ permanent Arbor entry in the left navigation, which simply does not list that
60
+ session. The routing registry replaces DSH's base `agent` row so it can send
61
+ `agentPreset: 'arbor'` sessions to arbor; every other session goes to the
62
+ official loop. Approvals still use DSH's approval path for both kinds of
63
+ session, and arbor's windows are registered so DSH's sidebar leaves them out.
64
+
65
+ ## Screenshots
66
+
67
+ | Overview, light | Overview, dark |
68
+ | --- | --- |
69
+ | ![Arbor overview in light mode](docs/screenshots/overview-light.png) | ![Arbor overview in dark mode](docs/screenshots/overview-dark.png) |
70
+ | **Session tab, light** | **Session tab, dark** |
71
+ | ![An arbor session tab in light mode](docs/screenshots/session-light.png) | ![An arbor session tab in dark mode](docs/screenshots/session-dark.png) |
72
+
73
+ These are the web profile at 1440 px wide with the optional
74
+ [font plugin](https://github.com/4fuu/dsh-arbor-fonts) installed: ChillDIN
75
+ Gothic for labels and ChillDIN for numerals, while body text and code keep
76
+ DSH's fonts. The same overview without the plugin is
77
+ [here](docs/screenshots/overview-without-fonts.png).
78
+
79
+ ## Install
80
+
81
+ Requirements: Node.js 24.9 or newer and DeepSeek Harness `dsh-v0.2.2-rc.1`.
82
+ The bundle is published with all packages it depends on; nothing has to be
83
+ built or linked.
84
+
85
+ ```bash
86
+ npm install -g @deepseek-ai/dsh@0.2.2-rc.1
87
+ dsh plugin --profile web add @4fu/dsh-arbor-bundle@2026.1110.1
88
+ dsh web
89
+ ```
90
+
91
+ `dsh plugin` adds the bundle to the profile's `dsh.profile.bundles` and installs
92
+ its packages into the profile. The packages declare DSH's runtime packages as
93
+ peer dependencies, so the profile keeps DSH's own copies rather than installing
94
+ a second set. Pick **Arbor** in a composer and send a message.
95
+ The optional [font plugin](https://github.com/4fuu/dsh-arbor-fonts) installs the
96
+ same way, but it is a Host row rather than a bundle. Add the row to the
97
+ profile's `cordis.patch.yml` after installing it:
98
+
99
+ ```bash
100
+ dsh plugin --profile web add @4fu/dsh-arbor-fonts@2026.1110.0
101
+ ```
102
+
103
+ ```yaml
104
+ - insert:
105
+ - id: arbor-fonts
106
+ name: '@4fu/dsh-arbor-fonts'
107
+ ```
108
+
109
+ Replace the default `[]` in that file with the entry, then restart the profile.
110
+
111
+ ### From a checkout
112
+
113
+ The proofs and development use the TypeScript sources directly; that checkout is
114
+ what the scripts and the pin check run against.
115
+
116
+ ```bash
117
+ git clone https://github.com/4fuu/dsh-arbor
118
+ cd dsh-arbor
119
+ pnpm install --frozen-lockfile
120
+ pnpm check # tsc --noEmit over every package and proof
121
+ pnpm proofs # 29 proofs against the scripted model
122
+ ```
123
+
124
+ The first install may ask pnpm to approve dependency build scripts. Do not
125
+ approve them by hand: the repository's `pnpm-workspace.yaml` already denies the
126
+ six scripts it pulls in, each with the reason, and `pnpm install` finishes
127
+ cleanly with that policy.
128
+
129
+ ### Hand the install to a model
130
+
131
+ [docs/install-for-agents.md](docs/install-for-agents.md) is a step-by-step guide
132
+ written for an AI agent: install from npm or a checkout, run the proofs, drive a
133
+ live turn with a restart, boot the web profile, and inspect what ran. Give a
134
+ model this prompt, editing the bracketed parts or dropping them:
135
+
136
+ ```text copy Copy Install Prompt
137
+ Install dsh-arbor (https://github.com/4fuu/dsh-arbor) in this environment [in a
138
+ scratch DSH home under /tmp] and show me that it works: follow
139
+ docs/install-for-agents.md, install @4fu/dsh-arbor-bundle@2026.1110.1 into a DSH
140
+ web profile, pick Arbor for one session, and [also run the proof suite from a
141
+ checkout with pnpm install --frozen-lockfile, pnpm check and pnpm proofs]. Report
142
+ which checks passed, what the live session answered, and anything that did not
143
+ work. Fix only what is yours to fix and tell me what you changed.
144
+ ```
145
+
146
+ ## Use it in DSH
147
+
148
+ The npm install above adds the bundle to a DSH web profile. Pick **Arbor** in a
149
+ composer and send a message: the session is created under the preset, its window
150
+ and chain appear under the **Arbor** entry, and an ordinary session started
151
+ beside it keeps DSH's own composer and sandbox behavior. A checkout can also be
152
+ wired by hand; [section 7 of the install guide](docs/install-for-agents.md#7-wiring-the-bundle-into-a-real-dsh-installation)
153
+ shows the profile layout.
154
+
155
+ The one-shot `dsh <task>` runner cannot drive an arbor-preset session: DSH's
156
+ runner refuses to adopt a session that runs under a preset. Use the web client,
157
+ the SDK path the proofs use, or a remote `agent-preset/select` on a blank.
158
+
159
+ ## Pinned DSH version
160
+
161
+ DSH is pinned to **`dsh-v0.2.2-rc.1`** (npm
162
+ `@deepseek-ai/dsh@0.2.2-rc.1`, commit `66491b05e3` of
163
+ `deepseek-ai/deepseek-harness`). Every `@deepseek-ai/dsh*` dependency is an
164
+ exact version, every package under `packages/` declares
165
+ `engines.dsh: 0.2.2-rc.1`, and `pnpm check:pin` checks both plus the prompt
166
+ bridge's `DSH_PINNED_VERSION`. DSH's runtime packages are peer dependencies, and
167
+ `pnpm check:profile` installs the packed packages into a fresh DSH web profile to
168
+ prove the profile keeps one copy and can create both kinds of session.
169
+
170
+ A different DSH version is unsupported. The bundle depends on DSH's row names,
171
+ its one-`AgentFactory` rule, its prompt-section registry, its client slot
172
+ registration and its exact Fetch registry; a move in any of them can fail the
173
+ boot, leave an official prompt section undecided, or stop the Arbor page from
174
+ registering. `proofs/12-official-sections.ts` and `proofs/18-picker-pin.ts`
175
+ are the behavioral checks. The canonical pin and the upgrade procedure are in
176
+ [Versions and compatibility](docs/versions.md).
177
+
178
+ ## Optional: the font plugin
179
+
180
+ [dsh-arbor-fonts](https://github.com/4fuu/dsh-arbor-fonts) is a separate DSH
181
+ client plugin that changes only the typography of the Arbor page. It sets
182
+ ChillDIN Gothic for display labels and ChillDIN for numerals, leaves body text
183
+ and code on DSH's fonts, and does nothing on ordinary pages. It is optional and
184
+ installs into the same DSH profile:
185
+
186
+ ```bash
187
+ dsh plugin --profile web add @4fu/dsh-arbor-fonts@2026.1110.0
188
+ ```
189
+
190
+ The fonts are licensed under the SIL Open Font License 1.1; the plugin ships the
191
+ license text, per-file hashes and attribution. The plugin is a Host row rather
192
+ than a bundle: after installing it, add the `arbor-fonts` entry described under
193
+ [Install](#install) to `cordis.patch.yml` and restart the profile.
194
+
195
+ ## Limits worth knowing
196
+
197
+ - The judge is off: a model-based judge cannot run through the DSH backend yet.
198
+ - The 12 bundle packages are published together at one version; install and
199
+ upgrade the bundle as a unit, because its packages name each other exactly.
200
+ - `ovnfs` is optional. Without it arbor uses the plain-directory workspace, and a
201
+ window no longer gets a copy-on-write checkout.
202
+ - The web page's numbers and layout are read from arbor's journal and API; it is
203
+ not a generic DSH dashboard, and it deliberately shows only arbor sessions.
204
+ - The prompt bridge, the UI and the picker filter are pinned to DSH's internals,
205
+ not to a stable public API. See [Versions and compatibility](docs/versions.md).
206
+
207
+ ## Documentation
208
+
209
+ - [Installing and inspecting dsh-arbor](docs/install-for-agents.md) — the
210
+ agent-facing install, proof, live-turn, web-profile and wiring guide.
211
+ - [Versions and compatibility](docs/versions.md) — the DSH and arbor pins, and
212
+ what breaks on another DSH version.
213
+ - [B0 findings](docs/b0-findings.md), [B3 findings](docs/b3-findings.md) and
214
+ [B3b projects and sandbox](docs/b3b-projects-and-sandbox.md) — historical
215
+ records of the integration's decisions, with the DSH source locations each
216
+ one rests on. They describe earlier stages; the install guide is the current
217
+ status.
218
+ - [Layout and proofs](#layout) below for a quick map of the packages.
219
+
220
+ ## Layout
221
+
222
+ ```text
223
+ packages/
224
+ arbor-host/ one Arbor host per process: SQLite journal and notes,
225
+ settings, window lifecycle, workspace and environment
226
+ arbor-driver/ the routing AgentRegistry, AgentFactory, session agent,
227
+ inbox and stream mirrors, and the journal relay
228
+ arbor-llm/ the arbor AgentBackend over DSH's ctx.llm and ctx.tools
229
+ arbor-prompt/ <harness>, the <dsh> data block, and the section table
230
+ arbor-tools/ DSH tools as arbor external tools, same-name replacement
231
+ arbor-workspace/ arbor's workspace over the shared folder (ovnfs or dir)
232
+ arbor-env/ /env, with DSH's skills and MCP servers mounted read-only
233
+ arbor-bundle/ the installable patch that mounts the rows and UI
234
+ arbor-facts/ the read face behind the Arbor page and its Fetch routes
235
+ arbor-ui/ the Arbor page, session tabs, copy and composer gate
236
+ arbor-ui-workspace/
237
+ the per-preset hero project picker
238
+ arbor-preset/ the agent-preset row that puts Arbor in the roster
239
+ arbor-testkit/ the boot harness, memory journal and scripted model
240
+ patches/ overlay layers a proof applies above the shipped profile
241
+ proofs/ one runnable proof per integration point
242
+ docs/ install, versions, screenshots and historical findings
243
+ scripts/ the pin and profile checks, the release build and packing
244
+ ```
245
+
246
+ ## License
247
+
248
+ MIT. See [LICENSE](LICENSE). The optional font plugin vendors fonts under the
249
+ SIL Open Font License 1.1; its repository carries the license text and
250
+ attribution.
package/blocks.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The `<dsh>` block kind: one data field, no tag of its own, so arbor's
3
+ * renderer wraps the third-party content in `<data>` and marks it as data
4
+ * rather than instructions.
5
+ */
6
+ import type { BlockKindSpec } from '@4fu/arbor-interfaces';
7
+ /** The `<dsh>` block kind the section renders: one data field, no tag of its own. */
8
+ export declare const DSH_DATA_BLOCK: BlockKindSpec;
package/blocks.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The `<dsh>` block kind: one data field, no tag of its own, so arbor's
3
+ * renderer wraps the third-party content in `<data>` and marks it as data
4
+ * rather than instructions.
5
+ */
6
+ /** The `<dsh>` block kind the section renders: one data field, no tag of its own. */
7
+ export const DSH_DATA_BLOCK = {
8
+ kind: 'data',
9
+ fields: [{ name: 'content', type: 'string', required: true }],
10
+ };
package/harness.d.ts ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The `<harness>` section: what dsh-arbor itself writes about running inside
3
+ * DeepSeek Harness.
4
+ *
5
+ * Arbor's own sections (`<identity>`, `<agent>`, `<tools>`, `<modules>`,
6
+ * `<env>`, `<workspace>`) carry the framework's logic. This section carries
7
+ * DSH's: that the window runs inside DeepSeek Harness and the user only sees
8
+ * what `send` writes, the approval policy, how to write
9
+ * image and file links into `send` so DSH renders them, how `@path`
10
+ * references arrive, plan mode's guidance while it is active, and the usage
11
+ * text of the DSH tools that are actually exposed to arbor windows.
12
+ *
13
+ * It is not marked `outside`: it is framework prompt, written by this bridge,
14
+ * not third-party data. Only the third-party material in `<dsh>` is data.
15
+ *
16
+ * Every fixed line is named by its key in {@link dshTextTable} rather than
17
+ * written into the block, because arbor's renderer escapes a block's field
18
+ * values and the `send` link syntax needs its angle brackets to survive. The
19
+ * two kinds that carry text — a DSH tool's usage and the deployment's plan
20
+ * guidance — are escaped, which is right: they are material from outside this
21
+ * bridge.
22
+ *
23
+ * The section's `revision` covers every input the content depends on, so a
24
+ * change to any of them reaches a running window as a `prompt_delta` carrying
25
+ * only the changed lines, rather than a rebuilt prefix.
26
+ */
27
+ import type { Block, BlockKindSpec } from '@4fu/arbor-interfaces';
28
+ import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt';
29
+ import { type SectionDecision } from './sections.js';
30
+ /**
31
+ * One fixed line of the `<harness>` section, named by its text-table key.
32
+ *
33
+ * The key is a field so one kind serves every line; the line itself comes from
34
+ * the table, which the renderer writes verbatim rather than escaped.
35
+ */
36
+ export declare const HARNESS_LINE_BLOCK: BlockKindSpec;
37
+ /** One DSH tool's usage text, carried through verbatim. */
38
+ export declare const HARNESS_TOOL_BLOCK: BlockKindSpec;
39
+ /** One block of text configured from outside this bridge: plan mode's guidance. */
40
+ export declare const HARNESS_TEXT_BLOCK: BlockKindSpec;
41
+ /** Every block kind the `<harness>` section renders. */
42
+ export declare const HARNESS_BLOCKS: readonly BlockKindSpec[];
43
+ /** The dynamic inputs the `<harness>` section renders, and its revision covers. */
44
+ export interface HarnessState {
45
+ /** The approval policy in effect, or `undefined` when DSH reports none. */
46
+ readonly approval: 'ask' | 'never' | undefined;
47
+ /** Whether plan mode is active for the session whose turn is rendering. */
48
+ readonly planActive: boolean;
49
+ /** The DSH tool names actually exposed to arbor windows, sorted. */
50
+ readonly exposedTools: readonly string[];
51
+ }
52
+ /** One rendered `<harness>` section, with what it decided. */
53
+ export interface HarnessRender {
54
+ /** The section's blocks. */
55
+ readonly blocks: readonly Block[];
56
+ /** The official tool sections this render carried through, by name. */
57
+ readonly carried: readonly string[];
58
+ }
59
+ /**
60
+ * Render the `<harness>` section's blocks from DSH's own assembly.
61
+ *
62
+ * A DSH section the table drops contributes nothing. A section the table
63
+ * keeps is either rewritten in this bridge's words (the approval policy, the
64
+ * `send` link wording, the `@path` wording) or carried through
65
+ * verbatim (a DSH tool's usage text, and the deployment's plan-mode guidance).
66
+ */
67
+ export declare function renderHarness(assembly: PromptAssembly, state: HarnessState): HarnessRender;
68
+ /**
69
+ * Detect a DSH section declared `complete: true`.
70
+ *
71
+ * A complete section replaces every other section in DSH's own assembly, so
72
+ * the only observable is the collapse: `SystemPrompt`'s constructor always
73
+ * registers `harness:identity` and the two persona sections, so an assembly
74
+ * that holds a persona section and no `harness:identity` is one a complete
75
+ * section took over. For an arbor session that must not be honoured — it would
76
+ * replace arbor's whole prompt — so the bridge records a warning and carries
77
+ * on with the persona as ordinary text.
78
+ */
79
+ export declare function detectCompleteSection(assembly: PromptAssembly): string | undefined;
80
+ /** The decision each consumed official section was rendered under, for diagnostics. */
81
+ export declare function consumedDecisions(names: readonly string[]): readonly {
82
+ name: string;
83
+ decision: SectionDecision;
84
+ }[];
package/harness.js ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * The `<harness>` section: what dsh-arbor itself writes about running inside
3
+ * DeepSeek Harness.
4
+ *
5
+ * Arbor's own sections (`<identity>`, `<agent>`, `<tools>`, `<modules>`,
6
+ * `<env>`, `<workspace>`) carry the framework's logic. This section carries
7
+ * DSH's: that the window runs inside DeepSeek Harness and the user only sees
8
+ * what `send` writes, the approval policy, how to write
9
+ * image and file links into `send` so DSH renders them, how `@path`
10
+ * references arrive, plan mode's guidance while it is active, and the usage
11
+ * text of the DSH tools that are actually exposed to arbor windows.
12
+ *
13
+ * It is not marked `outside`: it is framework prompt, written by this bridge,
14
+ * not third-party data. Only the third-party material in `<dsh>` is data.
15
+ *
16
+ * Every fixed line is named by its key in {@link dshTextTable} rather than
17
+ * written into the block, because arbor's renderer escapes a block's field
18
+ * values and the `send` link syntax needs its angle brackets to survive. The
19
+ * two kinds that carry text — a DSH tool's usage and the deployment's plan
20
+ * guidance — are escaped, which is right: they are material from outside this
21
+ * bridge.
22
+ *
23
+ * The section's `revision` covers every input the content depends on, so a
24
+ * change to any of them reaches a running window as a `prompt_delta` carrying
25
+ * only the changed lines, rather than a rebuilt prefix.
26
+ */
27
+ import { decideSection } from "./sections.js";
28
+ import { dshTextTable } from "./text.js";
29
+ /**
30
+ * One fixed line of the `<harness>` section, named by its text-table key.
31
+ *
32
+ * The key is a field so one kind serves every line; the line itself comes from
33
+ * the table, which the renderer writes verbatim rather than escaped.
34
+ */
35
+ export const HARNESS_LINE_BLOCK = {
36
+ kind: 'harness_line',
37
+ fields: [{ name: 'key', type: 'string', required: true }],
38
+ lines: ['{key}'],
39
+ };
40
+ /** One DSH tool's usage text, carried through verbatim. */
41
+ export const HARNESS_TOOL_BLOCK = {
42
+ kind: 'harness_tool',
43
+ fields: [
44
+ { name: 'name', type: 'string', required: true },
45
+ { name: 'text', type: 'string', required: true },
46
+ ],
47
+ lines: ['dsh.harness.tool'],
48
+ };
49
+ /** One block of text configured from outside this bridge: plan mode's guidance. */
50
+ export const HARNESS_TEXT_BLOCK = {
51
+ kind: 'harness_text',
52
+ fields: [{ name: 'text', type: 'string', required: true }],
53
+ };
54
+ /** Every block kind the `<harness>` section renders. */
55
+ export const HARNESS_BLOCKS = [
56
+ HARNESS_LINE_BLOCK,
57
+ HARNESS_TOOL_BLOCK,
58
+ HARNESS_TEXT_BLOCK,
59
+ ];
60
+ /** One fixed line, named by its text-table key. */
61
+ function line(key) {
62
+ return { kind: 'harness_line', fields: { key } };
63
+ }
64
+ /** The section texts of one assembly, by name. */
65
+ function textsOf(assembly) {
66
+ const texts = new Map();
67
+ for (const section of assembly.sections)
68
+ texts.set(section.name, section.text);
69
+ for (const context of assembly.contexts)
70
+ texts.set(context.name, context.text);
71
+ return texts;
72
+ }
73
+ /**
74
+ * Whether one kept tool section governs at least one exposed tool.
75
+ *
76
+ * `tool:<x>` governs the tool named `<x>`. `tool:pty` is DSH's terminal
77
+ * family, whose tools are named `terminal_*`. A prefixed section
78
+ * (`browser-use:`, `computer-use:`, `terminal:`) governs the tools that share
79
+ * its prefix.
80
+ */
81
+ function governs(sectionName, exposed) {
82
+ if (sectionName === 'tool:pty')
83
+ return exposed.some(name => name.startsWith('terminal_') || name === 'pty');
84
+ if (sectionName.endsWith(':'))
85
+ return exposed.some(name => name.startsWith(sectionName));
86
+ return exposed.includes(sectionName.slice('tool:'.length));
87
+ }
88
+ /**
89
+ * Render the `<harness>` section's blocks from DSH's own assembly.
90
+ *
91
+ * A DSH section the table drops contributes nothing. A section the table
92
+ * keeps is either rewritten in this bridge's words (the approval policy, the
93
+ * `send` link wording, the `@path` wording) or carried through
94
+ * verbatim (a DSH tool's usage text, and the deployment's plan-mode guidance).
95
+ */
96
+ export function renderHarness(assembly, state) {
97
+ const texts = textsOf(assembly);
98
+ const blocks = [line('dsh.harness.lead')];
99
+ const carried = [];
100
+ // Approval policy: dsh-arbor's own wording for DSH's two policies. The
101
+ // policy itself is read from the state, so a mid-window switch moves the
102
+ // section's revision and reaches the window as a delta.
103
+ if (state.approval === 'never') {
104
+ blocks.push(line('dsh.harness.approval.never'));
105
+ }
106
+ else if (state.approval === 'ask') {
107
+ blocks.push(line('dsh.harness.approval.ask'));
108
+ }
109
+ // How to write links into `send`, so DSH renders them. DSH's own section
110
+ // speaks of the final response; only `send` reaches the user here. The
111
+ // target line comes first, because a send to a target nobody owns is an
112
+ // effect that never reaches anybody.
113
+ blocks.push(line('dsh.harness.send.target'));
114
+ blocks.push(line('dsh.harness.send.image'));
115
+ blocks.push(line('dsh.harness.send.file'));
116
+ if (state.exposedTools.includes('present'))
117
+ blocks.push(line('dsh.harness.send.present'));
118
+ blocks.push(line('dsh.harness.reference'));
119
+ // Plan mode: the deployment's own guidance, verbatim, behind a lead line of
120
+ // this bridge's, and only while plan mode is active.
121
+ const plan = state.planActive ? texts.get('plan:policy') ?? '' : '';
122
+ if (plan !== '') {
123
+ blocks.push(line('dsh.harness.plan.lead'));
124
+ blocks.push({ kind: 'harness_text', fields: { text: plan } });
125
+ }
126
+ // The DSH tools that are actually exposed: their usage text, verbatim, in
127
+ // DSH's own English. A tool that is not exposed writes nothing.
128
+ const exposed = [...state.exposedTools].sort();
129
+ for (const [name, text] of texts) {
130
+ if (text === '')
131
+ continue;
132
+ if (decideSection(name) !== 'harness')
133
+ continue;
134
+ if (!name.startsWith('tool:') && !name.endsWith(':'))
135
+ continue;
136
+ if (!governs(name, exposed))
137
+ continue;
138
+ carried.push(name);
139
+ blocks.push({
140
+ kind: 'harness_tool',
141
+ fields: { name: name.slice('tool:'.length), text },
142
+ });
143
+ }
144
+ return { blocks, carried };
145
+ }
146
+ /**
147
+ * Detect a DSH section declared `complete: true`.
148
+ *
149
+ * A complete section replaces every other section in DSH's own assembly, so
150
+ * the only observable is the collapse: `SystemPrompt`'s constructor always
151
+ * registers `harness:identity` and the two persona sections, so an assembly
152
+ * that holds a persona section and no `harness:identity` is one a complete
153
+ * section took over. For an arbor session that must not be honoured — it would
154
+ * replace arbor's whole prompt — so the bridge records a warning and carries
155
+ * on with the persona as ordinary text.
156
+ */
157
+ export function detectCompleteSection(assembly) {
158
+ const names = assembly.sections.map(section => section.name);
159
+ if (names.length !== 1)
160
+ return undefined;
161
+ const [only] = names;
162
+ if (only === undefined || !only.startsWith('deployment:persona-'))
163
+ return undefined;
164
+ return only;
165
+ }
166
+ /** The decision each consumed official section was rendered under, for diagnostics. */
167
+ export function consumedDecisions(names) {
168
+ return names.map(name => ({ name, decision: decideSection(name) }));
169
+ }