dsh-trellis 0.1.15 → 0.1.16

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/README.md CHANGED
@@ -27,10 +27,10 @@ dsh plugin --profile tui add dsh-trellis
27
27
 
28
28
  DSH `0.2.0-rc.2` 的 macOS / Windows 桌面端可在菜单栏“Manage dsh command”中安装或管理内置 `dsh` 命令,无需单独安装 Node 或 pnpm。先用 `dsh --version` 确认 PATH 中实际调用的版本;旧的全局 CLI 可能与桌面端不同。桌面端默认使用 `desktop` profile,其他 profile 仍需分别安装。
29
29
 
30
- 新版本刚发布时,插件页预览的版本可能与安装器最终选择的版本不同。需要本版插件 `0.1.15` 时,在插件页输入 `dsh-trellis@0.1.15`,或显式安装到实际使用的 profile:
30
+ 新版本刚发布时,插件页预览的版本可能与安装器最终选择的版本不同。需要本版插件 `0.1.16` 时,在插件页输入 `dsh-trellis@0.1.16`,或显式安装到实际使用的 profile:
31
31
 
32
32
  ```powershell
33
- dsh plugin --profile desktop add dsh-trellis@0.1.15
33
+ dsh plugin --profile desktop add dsh-trellis@0.1.16
34
34
  # Web / headless 用户将 desktop 替换为对应 profile 名称。
35
35
  ```
36
36
 
@@ -63,6 +63,11 @@ dsh plugin --profile headless add file:C:/path/to/dsh-trellis
63
63
 
64
64
  运行时需要 profile 提供 `sessionProjections` 服务;该依赖由插件的 `inject` 声明。自定义或精简 profile 若未组装此服务,需先加载 `@deepseek-ai/dsh-session-projection`,否则插件会等待依赖,不会退回直接扫描历史。
65
65
 
66
+ ### 0.1.16:OIDC 发布自动化
67
+
68
+ - 增加指定标签的手动 OIDC 发布工作流,复用同一提交的 Windows/Linux 完整测试矩阵;不保存长期 npm 发布令牌。
69
+ - 核对 main 历史、包名和版本后才构建发布,不改插件运行时代码、依赖或兼容范围。
70
+
66
71
  ### 0.1.15:DSH 0.2.1-rc.4 兼容回归
67
72
 
68
73
  - 为 npm 发布的 RC.4 增加精确工具链映射、独立预期和 Windows/Linux CI 目标;保留生产代码、依赖基线及现有版本范围。未知 RC.5 仍拒绝执行。
@@ -206,6 +211,22 @@ RC.2 集成用例运行真实 AgentLoop、Loader、ToolRuntime 和 Session,以
206
211
 
207
212
  Host half 是直接由 `main` 加载的 ESM JavaScript;Web half 通过 tsdown 生成 DSH lazy-CJS factory 到 `lib/client.js`。`pnpm pack` 会在 prepack 阶段自动重建客户端 bundle。
208
213
 
214
+ ## 自动发布
215
+
216
+ `.github/workflows/publish.yml` 使用 npm [Trusted Publishing / OIDC](https://docs.npmjs.com/trusted-publishers/),不保存长期 `NPM_TOKEN`。需先在 npm 的 `dsh-trellis` 包设置中绑定 GitHub 用户 `SajoLuo`、仓库 `dsh-trellis`、工作流文件名 `publish.yml`,并允许 `npm publish`;不需要单独的 dist-tag 管理权限。首次配置需要维护者交互确认。
217
+
218
+ 发布流程仅允许在本仓库的 `main` 手动触发,指定已有的稳定版标签。标签必须位于 main 历史中,且包名、标签版本、main 的包版本一致。工作流解析出不可变提交 SHA,让现有 Windows/Linux 完整测试矩阵和发布构建都使用同一个 SHA;测试失败或跳过整个测试任务时不发布。只有发布任务拥有 OIDC 权限,普通 push / PR 不会触发发布。
219
+
220
+ 启用后,由发布操作者执行,例如:
221
+
222
+ ```powershell
223
+ gh workflow run publish.yml --repo SajoLuo/dsh-trellis --ref main -f tag=v0.1.16
224
+ ```
225
+
226
+ GitHub-hosted runner 通过 OIDC 获取临时发布凭据并生成 provenance。这里的授权属于指定发布流程;本机直接运行 `npm publish` 仍按 npm 的交互认证规则执行。首次 OIDC 真发布前,不把配置或本地检查视为认证已经验证成功。
227
+
228
+ 新 Trusted Publisher 创建后,需在 48 小时内成功发布一次才能完成验证并免于过期。过期后须重新创建连接;不要把待验证状态当作已完成端到端验证。参见 [npm 可信发布连接的验证期限](https://github.blog/changelog/2026-10-02-unvalidated-npm-trusted-publishing-configurations-now-expire/)。
229
+
209
230
  ## License
210
231
 
211
232
  MIT
package/lib/commands.js CHANGED
@@ -1,226 +1,226 @@
1
- /**
2
- * Human-facing /trellis commands: status and finish.
3
- *
4
- * Command names follow the dsh grammar (lowercase letters, digits, `_`, `-`;
5
- * no `:`), and deliberately avoid the skill names (`trellis-start`,
6
- * `trellis-continue`, `trellis-finish-work`): a command sharing a skill name
7
- * would shadow the skill's slash-pipeline injection. The Trellis session
8
- * rituals stay on the skill surface; these commands cover the deterministic,
9
- * zero-token side — pointer inspection and wrap-up.
10
- *
11
- * @module dsh-trellis/commands
12
- */
13
- import { execFile } from "node:child_process";
14
- import { promisify } from "node:util";
15
- import { join } from "node:path";
16
- import { findProjectRoot } from "./workflow.js";
17
- import { nodeFs } from "./node-fs.js";
18
- import { dshContextKey } from "./session-env.js";
19
- import { resolveWorkingDirectory } from "./working-directory.js";
20
-
21
- const execFileAsync = promisify(execFile);
22
-
23
- /** Platform-aware Python launch candidates. An explicit config disables fallback. */
24
- export function pythonCandidates(config, platform = process.platform) {
25
- if (config.pythonCmd) return [{ command: config.pythonCmd, prefixArgs: [] }];
26
- if (platform === "win32") {
27
- return [
28
- { command: "py", prefixArgs: ["-3"] },
29
- { command: "python", prefixArgs: [] },
30
- ];
31
- }
32
- return [
33
- { command: "python3", prefixArgs: [] },
34
- { command: "python", prefixArgs: [] },
35
- ];
36
- }
37
-
38
- /** Run a project Trellis script with bounded time and output. */
39
- export async function runTrellisScript(
40
- config,
41
- root,
42
- sessionId,
43
- scriptArgs,
44
- dependencies = {},
45
- ) {
46
- const execute = dependencies.execFile ?? execFileAsync;
47
- const platform = dependencies.platform ?? process.platform;
48
- const signal = dependencies.signal;
49
- const contextKey = dshContextKey(sessionId);
50
- const env = {
51
- ...process.env,
52
- ...(contextKey
53
- ? {
54
- TRELLIS_CONTEXT_ID: contextKey,
55
- DSH_TRELLIS_CONTEXT_ID: contextKey,
56
- DSH_SHELL: "1",
57
- DSH_SESSION_ID: sessionId,
58
- }
59
- : {}),
60
- };
61
- const script = join(root, ".trellis", "scripts", "task.py");
62
- const candidates = pythonCandidates(config, platform);
63
- for (const [index, candidate] of candidates.entries()) {
64
- const args = [...candidate.prefixArgs, script, ...scriptArgs];
65
- try {
66
- const { stdout, stderr } = await execute(candidate.command, args, {
67
- cwd: root,
68
- env,
69
- windowsHide: true,
70
- timeout: 15_000,
71
- maxBuffer: 256 * 1024,
72
- ...(signal ? { signal } : {}),
73
- });
74
- const text = `${stdout}${stderr ? `\n${stderr}` : ""}`.trim();
75
- return { kind: "success", text };
76
- } catch (error) {
77
- if (signal?.aborted || error?.name === "AbortError") {
78
- return { kind: "error", text: "Trellis command cancelled." };
79
- }
80
- const missing = error?.code === "ENOENT";
81
- if (missing && index < candidates.length - 1) continue;
82
- const attempted = candidates
83
- .slice(0, index + 1)
84
- .map(({ command, prefixArgs }) => [command, ...prefixArgs].join(" "))
85
- .join(", ");
86
- const message = missing
87
- ? `Python command not found (tried: ${attempted}). Set the plugin's pythonCmd config to a Python 3 executable.`
88
- : `Trellis script failed via ${[candidate.command, ...candidate.prefixArgs].join(" ")}: ${error?.message ?? String(error)}`;
89
- return { kind: "error", text: message };
90
- }
91
- }
92
- return {
93
- kind: "error",
94
- text: "No Python command candidates are configured.",
95
- };
96
- }
97
-
98
- /**
99
- * Register the /trellis command family.
100
- * @param {object} ctx - cordis context exposing `ctx.commands`.
101
- * @param {object} resolved - normalized plugin config.
102
- * @returns {() => void} disposer.
103
- */
104
- export function registerCommands(ctx, resolved, dependencies = {}) {
105
- const fs = dependencies.fs ?? nodeFs;
106
- const execute = dependencies.execFile ?? execFileAsync;
107
- const platform = dependencies.platform ?? process.platform;
108
- const disposers = [];
109
-
110
- const withoutInput = (invocation, fn) => {
111
- const rawInput = invocation.rawInput ?? "";
112
- const attachments = invocation.attachments ?? [];
113
- if (rawInput.trim() !== "" || attachments.length > 0) {
114
- return {
115
- kind: "error",
116
- text: "This Trellis command does not accept arguments or image attachments.",
117
- };
118
- }
119
- return fn();
120
- };
121
-
122
- const withProject = async (invocation, fn) => {
123
- let cwd;
124
- try {
125
- cwd = await resolveWorkingDirectory(ctx, invocation.agent, invocation.signal);
126
- } catch (error) {
127
- return {
128
- kind: "error",
129
- text: invocation.signal?.aborted
130
- ? "Trellis command cancelled."
131
- : `Cannot resolve the current DSH working directory: ${error?.message ?? String(error)}`,
132
- };
133
- }
134
- const root = await findProjectRoot(cwd, resolved.projectRootMarkers, fs);
135
- if (root === null) {
136
- return {
137
- kind: "error",
138
- text: "No project root found from the configured markers.",
139
- };
140
- }
141
- const workflow = join(root, ".trellis", "workflow.md");
142
- const taskScript = join(root, ".trellis", "scripts", "task.py");
143
- if (!(await fs.exists(workflow)) || !(await fs.exists(taskScript))) {
144
- return {
145
- kind: "error",
146
- text: "No Trellis project found (expected .trellis/workflow.md and .trellis/scripts/task.py).",
147
- };
148
- }
149
- const sessionId = invocation.agent?.session?.header?.id ?? "";
150
- return fn(root, sessionId, invocation.signal);
151
- };
152
-
153
- disposers.push(
154
- ctx.commands.register({
155
- name: "trellis-status",
156
- description: "Show the Trellis active task and git status.",
157
- // DSH rc.8 records command lifecycle events. These commands have no
158
- // payload of their own, so omit empty/invalid input from the durable log.
159
- recordInput: false,
160
- handler: (invocation) =>
161
- withoutInput(invocation, () =>
162
- withProject(invocation, async (root, sessionId, signal) => {
163
- const task = await runTrellisScript(
164
- resolved,
165
- root,
166
- sessionId,
167
- ["current", "--source"],
168
- { execFile: execute, platform, signal },
169
- );
170
- if (signal?.aborted) return task;
171
- let git = "";
172
- try {
173
- const { stdout } = await execute(
174
- "git",
175
- ["status", "--short", "--branch"],
176
- {
177
- cwd: root,
178
- windowsHide: true,
179
- timeout: 10_000,
180
- maxBuffer: 128 * 1024,
181
- ...(signal ? { signal } : {}),
182
- },
183
- );
184
- git = `\n\nGit status:\n${stdout.trim()}`;
185
- } catch {
186
- git = "";
187
- }
188
- const hint =
189
- task.kind === "success"
190
- ? `\n\nSession rituals: type /trellis-start (start), /trellis-continue (resume), /trellis-finish-work (skill wrap-up).`
191
- : "";
192
- return { kind: task.kind, text: `${task.text}${git}${hint}` };
193
- }),
194
- ),
195
- }),
196
- );
197
-
198
- disposers.push(
199
- ctx.commands.register({
200
- name: "trellis-finish",
201
- description:
202
- "Inspect the active task and print the safe wrap-up checklist.",
203
- recordInput: false,
204
- handler: (invocation) =>
205
- withoutInput(invocation, () =>
206
- withProject(invocation, async (root, sessionId, signal) => {
207
- const result = await runTrellisScript(
208
- resolved,
209
- root,
210
- sessionId,
211
- ["current", "--source"],
212
- { execFile: execute, platform, signal },
213
- );
214
- if (signal?.aborted) return result;
215
- const checklist =
216
- 'Wrap-up checklist (this command did not clear or archive anything):\n 1. Commit this task\'s changes (workflow.md Phase 3.4; do not push).\n 2. Type /trellis-finish-work so the skill can archive the task before the pointer is removed and then record the session journal.\n\nIf you must do it manually, archive first with the configured Python 3 runtime: .trellis/scripts/task.py archive <task-name>; then run .trellis/scripts/add_session.py --title "..." --commit <hash> --summary "...".';
217
- return { kind: result.kind, text: `${result.text}\n\n${checklist}` };
218
- }),
219
- ),
220
- }),
221
- );
222
-
223
- return () => {
224
- for (const dispose of disposers) dispose();
225
- };
226
- }
1
+ /**
2
+ * Human-facing /trellis commands: status and finish.
3
+ *
4
+ * Command names follow the dsh grammar (lowercase letters, digits, `_`, `-`;
5
+ * no `:`), and deliberately avoid the skill names (`trellis-start`,
6
+ * `trellis-continue`, `trellis-finish-work`): a command sharing a skill name
7
+ * would shadow the skill's slash-pipeline injection. The Trellis session
8
+ * rituals stay on the skill surface; these commands cover the deterministic,
9
+ * zero-token side — pointer inspection and wrap-up.
10
+ *
11
+ * @module dsh-trellis/commands
12
+ */
13
+ import { execFile } from "node:child_process";
14
+ import { promisify } from "node:util";
15
+ import { join } from "node:path";
16
+ import { findProjectRoot } from "./workflow.js";
17
+ import { nodeFs } from "./node-fs.js";
18
+ import { dshContextKey } from "./session-env.js";
19
+ import { resolveWorkingDirectory } from "./working-directory.js";
20
+
21
+ const execFileAsync = promisify(execFile);
22
+
23
+ /** Platform-aware Python launch candidates. An explicit config disables fallback. */
24
+ export function pythonCandidates(config, platform = process.platform) {
25
+ if (config.pythonCmd) return [{ command: config.pythonCmd, prefixArgs: [] }];
26
+ if (platform === "win32") {
27
+ return [
28
+ { command: "py", prefixArgs: ["-3"] },
29
+ { command: "python", prefixArgs: [] },
30
+ ];
31
+ }
32
+ return [
33
+ { command: "python3", prefixArgs: [] },
34
+ { command: "python", prefixArgs: [] },
35
+ ];
36
+ }
37
+
38
+ /** Run a project Trellis script with bounded time and output. */
39
+ export async function runTrellisScript(
40
+ config,
41
+ root,
42
+ sessionId,
43
+ scriptArgs,
44
+ dependencies = {},
45
+ ) {
46
+ const execute = dependencies.execFile ?? execFileAsync;
47
+ const platform = dependencies.platform ?? process.platform;
48
+ const signal = dependencies.signal;
49
+ const contextKey = dshContextKey(sessionId);
50
+ const env = {
51
+ ...process.env,
52
+ ...(contextKey
53
+ ? {
54
+ TRELLIS_CONTEXT_ID: contextKey,
55
+ DSH_TRELLIS_CONTEXT_ID: contextKey,
56
+ DSH_SHELL: "1",
57
+ DSH_SESSION_ID: sessionId,
58
+ }
59
+ : {}),
60
+ };
61
+ const script = join(root, ".trellis", "scripts", "task.py");
62
+ const candidates = pythonCandidates(config, platform);
63
+ for (const [index, candidate] of candidates.entries()) {
64
+ const args = [...candidate.prefixArgs, script, ...scriptArgs];
65
+ try {
66
+ const { stdout, stderr } = await execute(candidate.command, args, {
67
+ cwd: root,
68
+ env,
69
+ windowsHide: true,
70
+ timeout: 15_000,
71
+ maxBuffer: 256 * 1024,
72
+ ...(signal ? { signal } : {}),
73
+ });
74
+ const text = `${stdout}${stderr ? `\n${stderr}` : ""}`.trim();
75
+ return { kind: "success", text };
76
+ } catch (error) {
77
+ if (signal?.aborted || error?.name === "AbortError") {
78
+ return { kind: "error", text: "Trellis command cancelled." };
79
+ }
80
+ const missing = error?.code === "ENOENT";
81
+ if (missing && index < candidates.length - 1) continue;
82
+ const attempted = candidates
83
+ .slice(0, index + 1)
84
+ .map(({ command, prefixArgs }) => [command, ...prefixArgs].join(" "))
85
+ .join(", ");
86
+ const message = missing
87
+ ? `Python command not found (tried: ${attempted}). Set the plugin's pythonCmd config to a Python 3 executable.`
88
+ : `Trellis script failed via ${[candidate.command, ...candidate.prefixArgs].join(" ")}: ${error?.message ?? String(error)}`;
89
+ return { kind: "error", text: message };
90
+ }
91
+ }
92
+ return {
93
+ kind: "error",
94
+ text: "No Python command candidates are configured.",
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Register the /trellis command family.
100
+ * @param {object} ctx - cordis context exposing `ctx.commands`.
101
+ * @param {object} resolved - normalized plugin config.
102
+ * @returns {() => void} disposer.
103
+ */
104
+ export function registerCommands(ctx, resolved, dependencies = {}) {
105
+ const fs = dependencies.fs ?? nodeFs;
106
+ const execute = dependencies.execFile ?? execFileAsync;
107
+ const platform = dependencies.platform ?? process.platform;
108
+ const disposers = [];
109
+
110
+ const withoutInput = (invocation, fn) => {
111
+ const rawInput = invocation.rawInput ?? "";
112
+ const attachments = invocation.attachments ?? [];
113
+ if (rawInput.trim() !== "" || attachments.length > 0) {
114
+ return {
115
+ kind: "error",
116
+ text: "This Trellis command does not accept arguments or image attachments.",
117
+ };
118
+ }
119
+ return fn();
120
+ };
121
+
122
+ const withProject = async (invocation, fn) => {
123
+ let cwd;
124
+ try {
125
+ cwd = await resolveWorkingDirectory(ctx, invocation.agent, invocation.signal);
126
+ } catch (error) {
127
+ return {
128
+ kind: "error",
129
+ text: invocation.signal?.aborted
130
+ ? "Trellis command cancelled."
131
+ : `Cannot resolve the current DSH working directory: ${error?.message ?? String(error)}`,
132
+ };
133
+ }
134
+ const root = await findProjectRoot(cwd, resolved.projectRootMarkers, fs);
135
+ if (root === null) {
136
+ return {
137
+ kind: "error",
138
+ text: "No project root found from the configured markers.",
139
+ };
140
+ }
141
+ const workflow = join(root, ".trellis", "workflow.md");
142
+ const taskScript = join(root, ".trellis", "scripts", "task.py");
143
+ if (!(await fs.exists(workflow)) || !(await fs.exists(taskScript))) {
144
+ return {
145
+ kind: "error",
146
+ text: "No Trellis project found (expected .trellis/workflow.md and .trellis/scripts/task.py).",
147
+ };
148
+ }
149
+ const sessionId = invocation.agent?.session?.header?.id ?? "";
150
+ return fn(root, sessionId, invocation.signal);
151
+ };
152
+
153
+ disposers.push(
154
+ ctx.commands.register({
155
+ name: "trellis-status",
156
+ description: "Show the Trellis active task and git status.",
157
+ // DSH rc.8 records command lifecycle events. These commands have no
158
+ // payload of their own, so omit empty/invalid input from the durable log.
159
+ recordInput: false,
160
+ handler: (invocation) =>
161
+ withoutInput(invocation, () =>
162
+ withProject(invocation, async (root, sessionId, signal) => {
163
+ const task = await runTrellisScript(
164
+ resolved,
165
+ root,
166
+ sessionId,
167
+ ["current", "--source"],
168
+ { execFile: execute, platform, signal },
169
+ );
170
+ if (signal?.aborted) return task;
171
+ let git = "";
172
+ try {
173
+ const { stdout } = await execute(
174
+ "git",
175
+ ["status", "--short", "--branch"],
176
+ {
177
+ cwd: root,
178
+ windowsHide: true,
179
+ timeout: 10_000,
180
+ maxBuffer: 128 * 1024,
181
+ ...(signal ? { signal } : {}),
182
+ },
183
+ );
184
+ git = `\n\nGit status:\n${stdout.trim()}`;
185
+ } catch {
186
+ git = "";
187
+ }
188
+ const hint =
189
+ task.kind === "success"
190
+ ? `\n\nSession rituals: type /trellis-start (start), /trellis-continue (resume), /trellis-finish-work (skill wrap-up).`
191
+ : "";
192
+ return { kind: task.kind, text: `${task.text}${git}${hint}` };
193
+ }),
194
+ ),
195
+ }),
196
+ );
197
+
198
+ disposers.push(
199
+ ctx.commands.register({
200
+ name: "trellis-finish",
201
+ description:
202
+ "Inspect the active task and print the safe wrap-up checklist.",
203
+ recordInput: false,
204
+ handler: (invocation) =>
205
+ withoutInput(invocation, () =>
206
+ withProject(invocation, async (root, sessionId, signal) => {
207
+ const result = await runTrellisScript(
208
+ resolved,
209
+ root,
210
+ sessionId,
211
+ ["current", "--source"],
212
+ { execFile: execute, platform, signal },
213
+ );
214
+ if (signal?.aborted) return result;
215
+ const checklist =
216
+ 'Wrap-up checklist (this command did not clear or archive anything):\n 1. Commit this task\'s changes (workflow.md Phase 3.4; do not push).\n 2. Type /trellis-finish-work so the skill can archive the task before the pointer is removed and then record the session journal.\n\nIf you must do it manually, archive first with the configured Python 3 runtime: .trellis/scripts/task.py archive <task-name>; then run .trellis/scripts/add_session.py --title "..." --commit <hash> --summary "...".';
217
+ return { kind: result.kind, text: `${result.text}\n\n${checklist}` };
218
+ }),
219
+ ),
220
+ }),
221
+ );
222
+
223
+ return () => {
224
+ for (const dispose of disposers) dispose();
225
+ };
226
+ }
package/lib/config.js CHANGED
@@ -1,56 +1,56 @@
1
- /**
2
- * dsh-trellis plugin configuration (Schemastery).
3
- *
4
- * @module dsh-trellis/config
5
- */
1
+ /**
2
+ * dsh-trellis plugin configuration (Schemastery).
3
+ *
4
+ * @module dsh-trellis/config
5
+ */
6
6
  import z from "@deepseek-ai/schemastery";
7
7
  import * as settingsModule from "@deepseek-ai/dsh-settings";
8
8
 
9
9
  const fields = {
10
10
  /** Runtime master switch; the settings namespace remains available for re-enabling. */
11
- enabled: z
12
- .boolean()
13
- .default(true)
14
- .description(
15
- "Enable Trellis breadcrumbs, commands, session facts, and the wait tool for this profile.",
16
- ),
17
- /** UTF-8 byte budget for the injected breadcrumb text. 0 disables injection. */
18
- maxBytes: z
19
- .number()
20
- .step(1)
21
- .min(0)
22
- .default(4096)
23
- .description(
24
- "Maximum UTF-8 bytes in a workflow-state breadcrumb; 0 disables breadcrumbs.",
25
- ),
26
- /** Directory markers that identify a project root (walked upward from the session cwd). */
27
- projectRootMarkers: z
28
- .array(z.string())
29
- .default([".git"])
30
- .description(
31
- "Directory entries used while walking upward to find the project root.",
32
- ),
33
- /** Standalone word (case-insensitive) in the user prompt that skips breadcrumb injection for that turn. Empty disables. */
34
- skipKeyword: z
35
- .string()
36
- .default("no-trellis")
37
- .description(
38
- "Standalone prompt keyword that skips one breadcrumb injection; empty disables the escape hatch.",
39
- ),
40
- /** Python executable used by /trellis commands. Empty selects a platform-aware fallback chain. */
41
- pythonCmd: z
42
- .string()
43
- .default("")
44
- .description(
45
- "Python 3 executable for Trellis scripts; empty selects a platform-aware launcher chain.",
46
- ),
47
- /** Register the deterministic /trellis-status and read-only /trellis-finish commands. */
48
- commandsEnabled: z
49
- .boolean()
50
- .default(true)
51
- .description(
52
- "Register /trellis-status and the read-only /trellis-finish helper.",
53
- ),
11
+ enabled: z
12
+ .boolean()
13
+ .default(true)
14
+ .description(
15
+ "Enable Trellis breadcrumbs, commands, session facts, and the wait tool for this profile.",
16
+ ),
17
+ /** UTF-8 byte budget for the injected breadcrumb text. 0 disables injection. */
18
+ maxBytes: z
19
+ .number()
20
+ .step(1)
21
+ .min(0)
22
+ .default(4096)
23
+ .description(
24
+ "Maximum UTF-8 bytes in a workflow-state breadcrumb; 0 disables breadcrumbs.",
25
+ ),
26
+ /** Directory markers that identify a project root (walked upward from the session cwd). */
27
+ projectRootMarkers: z
28
+ .array(z.string())
29
+ .default([".git"])
30
+ .description(
31
+ "Directory entries used while walking upward to find the project root.",
32
+ ),
33
+ /** Standalone word (case-insensitive) in the user prompt that skips breadcrumb injection for that turn. Empty disables. */
34
+ skipKeyword: z
35
+ .string()
36
+ .default("no-trellis")
37
+ .description(
38
+ "Standalone prompt keyword that skips one breadcrumb injection; empty disables the escape hatch.",
39
+ ),
40
+ /** Python executable used by /trellis commands. Empty selects a platform-aware fallback chain. */
41
+ pythonCmd: z
42
+ .string()
43
+ .default("")
44
+ .description(
45
+ "Python 3 executable for Trellis scripts; empty selects a platform-aware launcher chain.",
46
+ ),
47
+ /** Register the deterministic /trellis-status and read-only /trellis-finish commands. */
48
+ commandsEnabled: z
49
+ .boolean()
50
+ .default(true)
51
+ .description(
52
+ "Register /trellis-status and the read-only /trellis-finish helper.",
53
+ ),
54
54
  };
55
55
 
56
56
  // Legacy Settings validates plain values; profile-backed Settings exposes only
@@ -67,31 +67,31 @@ export function hasLiveConfig(config) {
67
67
  Symbol.for("cosmokit.volatile.write") in value,
68
68
  );
69
69
  }
70
-
71
- /**
72
- * Normalized runtime configuration.
73
- * @param {Partial<Record<string, unknown>>} config
74
- * @returns {{
75
- * enabled: boolean,
76
- * maxBytes: number,
77
- * projectRootMarkers: string[],
78
- * skipKeyword: string,
79
- * pythonCmd: string,
80
- * commandsEnabled: boolean,
81
- * }}
82
- */
70
+
71
+ /**
72
+ * Normalized runtime configuration.
73
+ * @param {Partial<Record<string, unknown>>} config
74
+ * @returns {{
75
+ * enabled: boolean,
76
+ * maxBytes: number,
77
+ * projectRootMarkers: string[],
78
+ * skipKeyword: string,
79
+ * pythonCmd: string,
80
+ * commandsEnabled: boolean,
81
+ * }}
82
+ */
83
83
  export function resolveConfig(config) {
84
84
  config = Object.fromEntries(Object.entries(config).map(([key, value]) => [key,
85
85
  value !== null && typeof value === "object" &&
86
86
  Symbol.for("cosmokit.volatile.write") in value ? value.get() : value,
87
87
  ]));
88
- return {
89
- enabled: config.enabled !== false,
90
- maxBytes: config.maxBytes ?? 4096,
91
- projectRootMarkers: config.projectRootMarkers ?? [".git"],
92
- skipKeyword: config.skipKeyword ?? "no-trellis",
93
- pythonCmd:
94
- typeof config.pythonCmd === "string" ? config.pythonCmd.trim() : "",
95
- commandsEnabled: config.commandsEnabled !== false,
96
- };
97
- }
88
+ return {
89
+ enabled: config.enabled !== false,
90
+ maxBytes: config.maxBytes ?? 4096,
91
+ projectRootMarkers: config.projectRootMarkers ?? [".git"],
92
+ skipKeyword: config.skipKeyword ?? "no-trellis",
93
+ pythonCmd:
94
+ typeof config.pythonCmd === "string" ? config.pythonCmd.trim() : "",
95
+ commandsEnabled: config.commandsEnabled !== false,
96
+ };
97
+ }
package/lib/index.js CHANGED
@@ -1,24 +1,24 @@
1
- /**
2
- * dsh-trellis: Trellis workflow integration for DeepSeek Harness.
3
- *
4
- * - Per-turn workflow-state breadcrumb: at every eligible `agent/pre-step`,
5
- * resolves the project's Trellis state (.trellis/workflow.md blocks +
6
- * active task) and injects the matching [workflow-state:*] body into the
7
- * session, exactly where the workspace-instruction baseline lands.
8
- * - `/trellis-status` and read-only `/trellis-finish` utility commands.
9
- * Session rituals remain skills (`/trellis-start`, `/trellis-continue`,
10
- * `/trellis-finish-work`) so command registration cannot shadow them.
11
- * - `trellis_wait`, an event-driven synchronization tool for a continuable
12
- * role agent after the main session has exhausted independent work.
13
- * - A managed DSH_TRELLIS_CONTEXT_ID derived per shell execution from the
14
- * native DSH session, so an outer host's inherited TRELLIS_CONTEXT_ID cannot
15
- * silently claim the inner session. Utility commands set the same identity.
16
- *
17
- * In directories without a Trellis project it injects no breadcrumb; utility
18
- * commands validate the project only when a user explicitly invokes them.
19
- *
20
- * @module dsh-trellis
21
- */
1
+ /**
2
+ * dsh-trellis: Trellis workflow integration for DeepSeek Harness.
3
+ *
4
+ * - Per-turn workflow-state breadcrumb: at every eligible `agent/pre-step`,
5
+ * resolves the project's Trellis state (.trellis/workflow.md blocks +
6
+ * active task) and injects the matching [workflow-state:*] body into the
7
+ * session, exactly where the workspace-instruction baseline lands.
8
+ * - `/trellis-status` and read-only `/trellis-finish` utility commands.
9
+ * Session rituals remain skills (`/trellis-start`, `/trellis-continue`,
10
+ * `/trellis-finish-work`) so command registration cannot shadow them.
11
+ * - `trellis_wait`, an event-driven synchronization tool for a continuable
12
+ * role agent after the main session has exhausted independent work.
13
+ * - A managed DSH_TRELLIS_CONTEXT_ID derived per shell execution from the
14
+ * native DSH session, so an outer host's inherited TRELLIS_CONTEXT_ID cannot
15
+ * silently claim the inner session. Utility commands set the same identity.
16
+ *
17
+ * In directories without a Trellis project it injects no breadcrumb; utility
18
+ * commands validate the project only when a user explicitly invokes them.
19
+ *
20
+ * @module dsh-trellis
21
+ */
22
22
  import { isDeepStrictEqual } from "node:util";
23
23
  import * as settingsModule from "@deepseek-ai/dsh-settings";
24
24
  import { Config, hasLiveConfig, resolveConfig } from "./config.js";
@@ -34,55 +34,55 @@ import { registerWaitTool } from "./wait-tool.js";
34
34
 
35
35
  export const name = "dsh-trellis";
36
36
  export const SETTINGS_NAMESPACE = "dsh-trellis";
37
-
38
- /** @type {string[]} */
37
+
38
+ /** @type {string[]} */
39
39
  export const inject = ["commands", "shellEnv", "subagents", "tools", "sessionProjections"];
40
-
41
- export { Config };
42
-
43
- function stopAll(disposers) {
44
- for (const dispose of disposers.toReversed()) dispose();
45
- }
46
-
47
- function sameBreadcrumbPayload(left, right) {
48
- return (
49
- isDeepStrictEqual(left.content, right.content) &&
50
- isDeepStrictEqual(left.source, right.source)
51
- );
52
- }
53
-
54
- /** @param {import("@deepseek-ai/cordis").Context} ctx */
40
+
41
+ export { Config };
42
+
43
+ function stopAll(disposers) {
44
+ for (const dispose of disposers.toReversed()) dispose();
45
+ }
46
+
47
+ function sameBreadcrumbPayload(left, right) {
48
+ return (
49
+ isDeepStrictEqual(left.content, right.content) &&
50
+ isDeepStrictEqual(left.source, right.source)
51
+ );
52
+ }
53
+
54
+ /** @param {import("@deepseek-ai/cordis").Context} ctx */
55
55
  export function apply(ctx, config = {}) {
56
56
  let stopped = false;
57
57
  let disposeRuntime = () => {};
58
58
  let currentConfig = () => config;
59
59
  let mountedConfig;
60
-
61
- const mountRuntime = (nextConfig) => {
62
- if (stopped) return;
63
- disposeRuntime();
60
+
61
+ const mountRuntime = (nextConfig) => {
62
+ if (stopped) return;
63
+ disposeRuntime();
64
64
  const resolved = resolveConfig(nextConfig);
65
65
  mountedConfig = resolved;
66
- if (!resolved.enabled) {
67
- disposeRuntime = () => {};
68
- return;
69
- }
70
-
71
- const disposers = [];
72
- try {
73
- if (resolved.commandsEnabled) {
74
- disposers.push(registerCommands(ctx, resolved));
75
- }
76
- disposers.push(registerWaitTool(ctx));
77
- disposers.push(registerSessionEnv(ctx));
78
- disposers.push(registerBreadcrumb(ctx, resolved));
79
- } catch (error) {
80
- stopAll(disposers);
81
- throw error;
82
- }
83
- disposeRuntime = () => stopAll(disposers);
84
- };
85
-
66
+ if (!resolved.enabled) {
67
+ disposeRuntime = () => {};
68
+ return;
69
+ }
70
+
71
+ const disposers = [];
72
+ try {
73
+ if (resolved.commandsEnabled) {
74
+ disposers.push(registerCommands(ctx, resolved));
75
+ }
76
+ disposers.push(registerWaitTool(ctx));
77
+ disposers.push(registerSessionEnv(ctx));
78
+ disposers.push(registerBreadcrumb(ctx, resolved));
79
+ } catch (error) {
80
+ stopAll(disposers);
81
+ throw error;
82
+ }
83
+ disposeRuntime = () => stopAll(disposers);
84
+ };
85
+
86
86
  // Keep the composition entry working when no settings provider is mounted.
87
87
  // Once the service appears, its resolved section becomes the live source and
88
88
  // a saved Web card edit remounts only this plugin's runtime.
@@ -91,7 +91,7 @@ export function apply(ctx, config = {}) {
91
91
  return () => {
92
92
  stopped = true;
93
93
  disposeRuntime();
94
- };
94
+ };
95
95
  }, "dsh-trellis.runtime");
96
96
 
97
97
  if (hasLiveConfig(config)) {
@@ -101,7 +101,7 @@ export function apply(ctx, config = {}) {
101
101
  if (!isDeepStrictEqual(resolveConfig(config), mountedConfig)) mountRuntime(config);
102
102
  });
103
103
  }
104
-
104
+
105
105
  // Optional by design: profiles without a settings service retain the
106
106
  // loader-config behavior. The canonical bridge also distinguishes a settings
107
107
  // provider reload (fall back to config) from this plugin's own teardown
@@ -140,7 +140,7 @@ export function insertBreadcrumbDecision(decision, claimedMessages, desired) {
140
140
  messages: decision.messages.toSpliced(lastClaimedIndex + 1, 0, desired),
141
141
  };
142
142
  }
143
-
143
+
144
144
  function registerBreadcrumb(ctx, resolved) {
145
145
  const { compose } = createBreadcrumbComposer(resolved, ctx);
146
146
  const projection = registerBreadcrumbProjection(ctx);
@@ -153,21 +153,21 @@ function registerBreadcrumb(ctx, resolved) {
153
153
 
154
154
  const syncInbox = (agent, desired) => {
155
155
  pendingInboxes.add(agent.inbox);
156
- const pending = agent.inbox.nextStep.filter(isBreadcrumbMessage);
157
- const reusable = pending.find((message) =>
158
- sameBreadcrumbPayload(message, desired),
159
- );
160
- if (reusable !== undefined) {
161
- for (const message of pending)
162
- if (message !== reusable) agent.inbox.remove(message.id);
163
- return;
164
- }
165
- const replaced = pending[0];
166
- if (replaced === undefined) agent.inbox.prepend("next-step", desired);
167
- else agent.inbox.replace(replaced.id, desired);
168
- for (const message of pending.slice(1)) agent.inbox.remove(message.id);
169
- };
170
-
156
+ const pending = agent.inbox.nextStep.filter(isBreadcrumbMessage);
157
+ const reusable = pending.find((message) =>
158
+ sameBreadcrumbPayload(message, desired),
159
+ );
160
+ if (reusable !== undefined) {
161
+ for (const message of pending)
162
+ if (message !== reusable) agent.inbox.remove(message.id);
163
+ return;
164
+ }
165
+ const replaced = pending[0];
166
+ if (replaced === undefined) agent.inbox.prepend("next-step", desired);
167
+ else agent.inbox.replace(replaced.id, desired);
168
+ for (const message of pending.slice(1)) agent.inbox.remove(message.id);
169
+ };
170
+
171
171
  let disposeStep;
172
172
  try {
173
173
  disposeStep = ctx.on("agent/pre-step", async ({ agent, messages, step, signal }, next) => {
package/lib/wait-tool.js CHANGED
@@ -1,103 +1,103 @@
1
- /**
2
- * Event-driven waiting for DeepSeek Harness continuable subagents.
3
- *
4
- * Trellis dispatches role agents in DSH's default background mode so the main
5
- * agent can keep doing independent work. When that work is exhausted, this
6
- * tool provides a native synchronization point: it waits for `subagent/end`,
7
- * which proves execution settlement, not delivery of a parent notice (alpha.2
8
- * delivery is best effort and caller-owned activations may omit it entirely).
9
- * It deliberately replaces shell sleeps and polling loops.
10
- *
11
- * @module dsh-trellis/wait-tool
12
- */
13
- import { defineTool } from "@deepseek-ai/dsh-tools";
14
-
15
- function createSettlementWaiter(parentCtx, subagentId, signal) {
16
- let disposeListener = () => {};
17
- let settled = false;
18
- let resolvePromise;
1
+ /**
2
+ * Event-driven waiting for DeepSeek Harness continuable subagents.
3
+ *
4
+ * Trellis dispatches role agents in DSH's default background mode so the main
5
+ * agent can keep doing independent work. When that work is exhausted, this
6
+ * tool provides a native synchronization point: it waits for `subagent/end`,
7
+ * which proves execution settlement, not delivery of a parent notice (alpha.2
8
+ * delivery is best effort and caller-owned activations may omit it entirely).
9
+ * It deliberately replaces shell sleeps and polling loops.
10
+ *
11
+ * @module dsh-trellis/wait-tool
12
+ */
13
+ import { defineTool } from "@deepseek-ai/dsh-tools";
14
+
15
+ function createSettlementWaiter(parentCtx, subagentId, signal) {
16
+ let disposeListener = () => {};
17
+ let settled = false;
18
+ let resolvePromise;
19
19
  let rejectPromise;
20
20
  let terminal;
21
-
22
- const cleanup = () => {
23
- disposeListener?.();
24
- signal?.removeEventListener("abort", onAbort);
25
- };
26
- const onAbort = () => {
27
- if (settled) return;
28
- settled = true;
29
- cleanup();
30
- rejectPromise(signal.reason ?? new Error("Trellis subagent wait cancelled."));
31
- };
21
+
22
+ const cleanup = () => {
23
+ disposeListener?.();
24
+ signal?.removeEventListener("abort", onAbort);
25
+ };
26
+ const onAbort = () => {
27
+ if (settled) return;
28
+ settled = true;
29
+ cleanup();
30
+ rejectPromise(signal.reason ?? new Error("Trellis subagent wait cancelled."));
31
+ };
32
32
  const promise = new Promise((resolve, reject) => {
33
- resolvePromise = resolve;
34
- rejectPromise = reject;
33
+ resolvePromise = resolve;
34
+ rejectPromise = reject;
35
35
  });
36
36
  // Cancellation can precede completion of the catalog lookup below. Observe
37
37
  // the rejection immediately; the execute path still awaits the original.
38
38
  void promise.catch(() => {});
39
-
40
- disposeListener =
41
- parentCtx.on("subagent/end", (info) => {
42
- if (settled || info.id !== subagentId) return;
43
- settled = true;
44
- cleanup();
39
+
40
+ disposeListener =
41
+ parentCtx.on("subagent/end", (info) => {
42
+ if (settled || info.id !== subagentId) return;
43
+ settled = true;
44
+ cleanup();
45
45
  resolvePromise(info);
46
46
  terminal = info;
47
- }) ?? (() => {});
48
- signal?.addEventListener("abort", onAbort, { once: true });
49
- if (signal?.aborted) onAbort();
50
-
51
- return {
47
+ }) ?? (() => {});
48
+ signal?.addEventListener("abort", onAbort, { once: true });
49
+ if (signal?.aborted) onAbort();
50
+
51
+ return {
52
52
  promise,
53
53
  result: () => terminal,
54
- cancel() {
55
- if (settled) return;
56
- settled = true;
57
- cleanup();
58
- },
59
- };
60
- }
61
-
62
- /** Map a DSH stop reason to a gate-safe Trellis outcome. */
63
- export function settlementOutcome(stopReason) {
64
- if (stopReason === "completed") return "completed";
65
- if (stopReason === "aborted") return "aborted";
66
- if (stopReason === "unknown") return "unknown";
67
- return "failed";
68
- }
69
-
70
- function settledResult(subagentId, info) {
71
- const stopReason =
72
- typeof info?.stopReason === "string" ? info.stopReason : "unknown";
73
- return {
74
- subagentId,
75
- state: "settled",
76
- outcome: settlementOutcome(stopReason),
77
- stopReason,
78
- runId: typeof info?.runId === "string" ? info.runId : "",
79
- provider: typeof info?.provider === "string" ? info.provider : "",
80
- assistantOutputBlocks: Array.isArray(info?.lastAssistantMessage)
81
- ? info.lastAssistantMessage.length
82
- : 0,
83
- // Compatibility field: false means unverified, not proven non-delivery.
84
- settlementNoticeQueued: false,
85
- };
86
- }
87
-
88
- function inactiveResult(subagentId) {
89
- return {
90
- subagentId,
91
- state: "already-inactive",
92
- outcome: "unknown",
93
- stopReason: "unknown",
94
- runId: "",
95
- provider: "",
96
- assistantOutputBlocks: 0,
97
- settlementNoticeQueued: false,
98
- };
99
- }
100
-
54
+ cancel() {
55
+ if (settled) return;
56
+ settled = true;
57
+ cleanup();
58
+ },
59
+ };
60
+ }
61
+
62
+ /** Map a DSH stop reason to a gate-safe Trellis outcome. */
63
+ export function settlementOutcome(stopReason) {
64
+ if (stopReason === "completed") return "completed";
65
+ if (stopReason === "aborted") return "aborted";
66
+ if (stopReason === "unknown") return "unknown";
67
+ return "failed";
68
+ }
69
+
70
+ function settledResult(subagentId, info) {
71
+ const stopReason =
72
+ typeof info?.stopReason === "string" ? info.stopReason : "unknown";
73
+ return {
74
+ subagentId,
75
+ state: "settled",
76
+ outcome: settlementOutcome(stopReason),
77
+ stopReason,
78
+ runId: typeof info?.runId === "string" ? info.runId : "",
79
+ provider: typeof info?.provider === "string" ? info.provider : "",
80
+ assistantOutputBlocks: Array.isArray(info?.lastAssistantMessage)
81
+ ? info.lastAssistantMessage.length
82
+ : 0,
83
+ // Compatibility field: false means unverified, not proven non-delivery.
84
+ settlementNoticeQueued: false,
85
+ };
86
+ }
87
+
88
+ function inactiveResult(subagentId) {
89
+ return {
90
+ subagentId,
91
+ state: "already-inactive",
92
+ outcome: "unknown",
93
+ stopReason: "unknown",
94
+ runId: "",
95
+ provider: "",
96
+ assistantOutputBlocks: 0,
97
+ settlementNoticeQueued: false,
98
+ };
99
+ }
100
+
101
101
  /** Catalogs prove ownership, not residency or successful completion. */
102
102
  function childActivity(ctx, parent, child) {
103
103
  if (child.kind === "child") {
@@ -122,111 +122,111 @@ function childActivity(ctx, parent, child) {
122
122
  export function registerWaitTool(ctx) {
123
123
  const lifetime = new AbortController();
124
124
  const unregister = ctx.tools.register(
125
- defineTool({
126
- name: "trellis_wait",
127
- description:
128
- "Wait efficiently for one direct background Trellis subagent after independent work is exhausted. This is an event-driven synchronization barrier over DSH's native settlement lifecycle. Use the subagent id returned by `subagent`; never replace this with shell sleep, polling, or job_output. On DSH rc.8+, an idle parent can instead yield and be woken by the native settlement notice.",
129
- parameters: {
130
- subagent_id: {
131
- type: "string",
132
- required: true,
133
- description:
134
- "Direct continuable subagent id returned by the `subagent` tool.",
135
- },
136
- },
137
- output: {
138
- schema: {
139
- type: "object",
140
- additionalProperties: false,
141
- properties: {
142
- subagentId: { type: "string", required: true },
143
- state: { type: "string", required: true },
144
- outcome: { type: "string", required: true },
145
- stopReason: { type: "string", required: true },
146
- runId: { type: "string", required: true },
147
- provider: { type: "string", required: true },
148
- assistantOutputBlocks: { type: "number", required: true },
149
- settlementNoticeQueued: {
150
- type: "boolean", required: true,
151
- description: "Compatibility field: false means notice delivery is unverified, not proven absent.",
152
- },
153
- },
154
- },
155
- render(_args, value) {
156
- if (value.state === "already-inactive") {
157
- return [
158
- {
159
- type: "text",
160
- text: `subagent ${value.subagentId} is already inactive; no stop reason can be proven from the catalog snapshot, so inspect its transcript or an already-visible settlement notice before passing a gate`,
161
- },
162
- ];
163
- }
164
- if (value.outcome !== "completed") {
165
- return [
166
- {
167
- type: "text",
168
- text: `subagent ${value.subagentId} settled with ${value.outcome} (${value.stopReason}); do not treat this gate as passed; settlement notice delivery is unverified`,
169
- },
170
- ];
171
- }
172
- return [
173
- {
174
- type: "text",
175
- text: `subagent ${value.subagentId} completed; settlement notice delivery is unverified; inspect its report or transcript before passing a dependent gate`,
176
- },
177
- ];
178
- },
179
- },
125
+ defineTool({
126
+ name: "trellis_wait",
127
+ description:
128
+ "Wait efficiently for one direct background Trellis subagent after independent work is exhausted. This is an event-driven synchronization barrier over DSH's native settlement lifecycle. Use the subagent id returned by `subagent`; never replace this with shell sleep, polling, or job_output. On DSH rc.8+, an idle parent can instead yield and be woken by the native settlement notice.",
129
+ parameters: {
130
+ subagent_id: {
131
+ type: "string",
132
+ required: true,
133
+ description:
134
+ "Direct continuable subagent id returned by the `subagent` tool.",
135
+ },
136
+ },
137
+ output: {
138
+ schema: {
139
+ type: "object",
140
+ additionalProperties: false,
141
+ properties: {
142
+ subagentId: { type: "string", required: true },
143
+ state: { type: "string", required: true },
144
+ outcome: { type: "string", required: true },
145
+ stopReason: { type: "string", required: true },
146
+ runId: { type: "string", required: true },
147
+ provider: { type: "string", required: true },
148
+ assistantOutputBlocks: { type: "number", required: true },
149
+ settlementNoticeQueued: {
150
+ type: "boolean", required: true,
151
+ description: "Compatibility field: false means notice delivery is unverified, not proven absent.",
152
+ },
153
+ },
154
+ },
155
+ render(_args, value) {
156
+ if (value.state === "already-inactive") {
157
+ return [
158
+ {
159
+ type: "text",
160
+ text: `subagent ${value.subagentId} is already inactive; no stop reason can be proven from the catalog snapshot, so inspect its transcript or an already-visible settlement notice before passing a gate`,
161
+ },
162
+ ];
163
+ }
164
+ if (value.outcome !== "completed") {
165
+ return [
166
+ {
167
+ type: "text",
168
+ text: `subagent ${value.subagentId} settled with ${value.outcome} (${value.stopReason}); do not treat this gate as passed; settlement notice delivery is unverified`,
169
+ },
170
+ ];
171
+ }
172
+ return [
173
+ {
174
+ type: "text",
175
+ text: `subagent ${value.subagentId} completed; settlement notice delivery is unverified; inspect its report or transcript before passing a dependent gate`,
176
+ },
177
+ ];
178
+ },
179
+ },
180
180
  async execute(args, exec) {
181
181
  const signal = exec.signal
182
182
  ? AbortSignal.any([exec.signal, lifetime.signal])
183
183
  : lifetime.signal;
184
184
  signal.throwIfAborted();
185
185
  const parent = exec.agent;
186
- if (!parent)
187
- throw new Error(
188
- "trellis_wait requires a calling agent (exec.agent was undefined)",
189
- );
190
-
191
- // Install the listener before reading the durable catalog so settlement
192
- // cannot slip through the validation/read-to-wait window.
193
- const waiter = createSettlementWaiter(
194
- parent.ctx,
195
- args.subagent_id,
186
+ if (!parent)
187
+ throw new Error(
188
+ "trellis_wait requires a calling agent (exec.agent was undefined)",
189
+ );
190
+
191
+ // Install the listener before reading the durable catalog so settlement
192
+ // cannot slip through the validation/read-to-wait window.
193
+ const waiter = createSettlementWaiter(
194
+ parent.ctx,
195
+ args.subagent_id,
196
196
  signal,
197
- );
198
- try {
199
- const entries = await ctx.subagents.listChildren(
200
- parent.id,
197
+ );
198
+ try {
199
+ const entries = await ctx.subagents.listChildren(
200
+ parent.id,
201
201
  signal,
202
202
  );
203
203
  signal.throwIfAborted();
204
- const child = entries.find(
205
- (entry) =>
204
+ const child = entries.find(
205
+ (entry) =>
206
206
  (entry.kind === "child" || entry.kind === undefined) &&
207
- entry.mode === "continuable" &&
208
- entry.id === args.subagent_id,
209
- );
210
- if (child === undefined) {
211
- throw new Error(
212
- `trellis_wait can only wait for a direct continuable subagent of this session: ${args.subagent_id}`,
213
- );
214
- }
207
+ entry.mode === "continuable" &&
208
+ entry.id === args.subagent_id,
209
+ );
210
+ if (child === undefined) {
211
+ throw new Error(
212
+ `trellis_wait can only wait for a direct continuable subagent of this session: ${args.subagent_id}`,
213
+ );
214
+ }
215
215
  // Preserve a proven event captured during the asynchronous lookup.
216
216
  if (waiter.result()) return settledResult(args.subagent_id, waiter.result());
217
217
  if (childActivity(ctx, parent, child) === "inactive") {
218
- waiter.cancel();
219
- return inactiveResult(args.subagent_id);
220
- }
221
-
222
- const info = await waiter.promise;
223
- return settledResult(args.subagent_id, info);
224
- } catch (error) {
225
- waiter.cancel();
226
- throw error;
227
- }
228
- },
229
- }),
218
+ waiter.cancel();
219
+ return inactiveResult(args.subagent_id);
220
+ }
221
+
222
+ const info = await waiter.promise;
223
+ return settledResult(args.subagent_id, info);
224
+ } catch (error) {
225
+ waiter.cancel();
226
+ throw error;
227
+ }
228
+ },
229
+ }),
230
230
  );
231
231
  return () => {
232
232
  if (lifetime.signal.aborted) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-trellis",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Trellis workflow integration for DeepSeek Harness: per-turn workflow-state breadcrumbs, utility commands, and native session identity.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",