dsh-trellis 0.1.14 → 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 +35 -8
- package/lib/commands.js +226 -226
- package/lib/config.js +71 -71
- package/lib/index.js +80 -80
- package/lib/wait-tool.js +180 -180
- package/package.json +1 -1
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.
|
|
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.
|
|
33
|
+
dsh plugin --profile desktop add dsh-trellis@0.1.16
|
|
34
34
|
# Web / headless 用户将 desktop 替换为对应 profile 名称。
|
|
35
35
|
```
|
|
36
36
|
|
|
@@ -59,10 +59,20 @@ dsh plugin --profile headless add file:C:/path/to/dsh-trellis
|
|
|
59
59
|
|
|
60
60
|
`file:` 插件会作为 profile 内的 pnpm 快照安装;拉取源码更新后,尤其是版本新增文件时,需要先 remove 再 add 刷新该 profile。外部 CLI 刷新快照后重启对应进程,不要假定源码目录变化会自动热更新。
|
|
61
61
|
|
|
62
|
-
配套要求:项目的 Trellis 平台需包含 dsh(`trellis init --dsh`,见 Trellis-DeepSeekHarness 适配分支),且 `.trellis/scripts` 需包含读取原生 `DSH_SESSION_ID` 的适配(已含在同一分支)。现有 `engines.dsh` 和 Host peer 范围已接纳 `0.2.1-rc.
|
|
62
|
+
配套要求:项目的 Trellis 平台需包含 dsh(`trellis init --dsh`,见 Trellis-DeepSeekHarness 适配分支),且 `.trellis/scripts` 需包含读取原生 `DSH_SESSION_ID` 的适配(已含在同一分支)。现有 `engines.dsh` 和 Host peer 范围已接纳 `0.2.1-rc.4`,无需为每个预发布标签追加范围。`0.2.1-alpha.4` 起宿主优先检查显式 `engines.dsh`,仅未声明时才回退 DSH peer;其 `includePrerelease: true` 规则与 npm 默认匹配不同。通过准入不等于实际兼容,不应使用 `allow-version` 绕过缺失的 API。开发基线固定为 `0.2.0-rc.1`,隔离回归目标见开发章节的十四个明确宿主,不代表测试了范围内所有版本。Web 配置只在具备对应 client surface 的 profile 中加载。参见[官方版本声明迁移说明](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.1-rc.2/docs/upgrade-guide/v0.2.1-alpha.3/plugin-engines-dsh/guide.md)。
|
|
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
|
+
|
|
71
|
+
### 0.1.15:DSH 0.2.1-rc.4 兼容回归
|
|
72
|
+
|
|
73
|
+
- 为 npm 发布的 RC.4 增加精确工具链映射、独立预期和 Windows/Linux CI 目标;保留生产代码、依赖基线及现有版本范围。未知 RC.5 仍拒绝执行。
|
|
74
|
+
- 十四个宿主的本地跨平台结果及隔离 Web 验证范围见 [RC.4 验证记录](docs/validation-dsh-0.2.1-rc.4.md);不将本地通过写作远端 CI 或新版已发布。
|
|
75
|
+
|
|
66
76
|
### 0.1.14:DSH 0.2.1-rc.3 兼容回归
|
|
67
77
|
|
|
68
78
|
- 为 RC.3 增加精确工具链映射、独立预期和 Windows/Linux CI 目标;版本准入测试自动覆盖新增目标。保留开发基线、peer / engine 范围及生产代码,未知宿主仍拒绝执行。
|
|
@@ -177,16 +187,17 @@ pnpm run test:compat 0.2.0-rc.2 # 保留 0.2.0 RC.2 回归
|
|
|
177
187
|
pnpm run test:compat 0.2.1-alpha.2 # 新工作目录与统一 activation 契约
|
|
178
188
|
pnpm run test:compat 0.2.1-rc.2 # 保留 RC.2 隔离回归
|
|
179
189
|
pnpm run test:compat 0.2.1-rc.3 # RC.3 隔离回归
|
|
180
|
-
pnpm run test:compat #
|
|
190
|
+
pnpm run test:compat 0.2.1-rc.4 # RC.4 隔离回归
|
|
191
|
+
pnpm run test:compat # 默认跑全部十四个显式目标
|
|
181
192
|
```
|
|
182
193
|
|
|
183
|
-
GitHub Actions 配置在 Windows / Linux 的 Node 24 环境执行锁定的 `0.2.0-rc.1`
|
|
194
|
+
GitHub Actions 配置在 Windows / Linux 的 Node 24 环境执行锁定的 `0.2.0-rc.1` 安装、客户端构建、测试、其余十三个宿主的隔离回归及打包检查。测试覆盖真实 Session/projection 的恢复、分叉、压缩和 checkpoint,临时项目的精确绑定与任务错误恢复,以及新版真实 Loader 的 volatile 更新、子代理目录/查询/驻留状态;新版专属测试在旧宿主明确跳过。另有编译客户端 React 渲染、slot 生命周期、原子保存与拒绝写入测试。配置了 CI 目标不等于远端 CI 已运行,结果以对应提交为准。
|
|
184
195
|
|
|
185
196
|
RC.2 集成用例运行真实 AgentLoop、Loader、ToolRuntime 和 Session,以本地脚本化模型响应验证三种 `toolUpdate` 能力:不支持、`addition-only`、`in-history`。覆盖插件启停的工具增删消息、Settings 重挂不重复注入、混合历史的恢复/分叉投影,以及 surface 替换后重建工具基线并重新注入面包屑。等待工具另覆盖目录损坏/不存在及卸载期间查询失败的错误保留与监听器清理。该验证不调用外部模型,也不替代 Web 浏览器或真实子代理派发验收;详情见 [2026-09-28 验证记录](https://github.com/SajoLuo/dsh-trellis/blob/main/docs/validation-dsh-0.1.7-rc.2.md)。
|
|
186
197
|
|
|
187
198
|
0.2 集成用例另以真实 AgentLoop / ToolRuntime / Session 和实际 `trellis_wait` 验证失败工具历史的补齐顺序、错误分类、监听器清理及继续会话的消息配对。只注入 scheduler 故障和本地模型响应,不增加运行时补丁或自动重试;旧宿主按 `ToolCallRecovery` 能力明确跳过。还覆盖“目录查询期间收到成功结算、随后查询失败”的竞争,查询错误不能被伪装成成功。
|
|
188
199
|
|
|
189
|
-
`test:compat` 不改当前 checkout
|
|
200
|
+
`test:compat` 不改当前 checkout 的依赖,临时副本保留在系统临时目录并打印路径供检查。十四个目标采用以下明确配对,不按“非最新版本”猜测;未知宿主必须先核对官方包并补齐测试。
|
|
190
201
|
|
|
191
202
|
| DSH 目标 | Cordis | Schemastery | Include | Loader |
|
|
192
203
|
|---|---|---|---|---|
|
|
@@ -194,12 +205,28 @@ RC.2 集成用例运行真实 AgentLoop、Loader、ToolRuntime 和 Session,以
|
|
|
194
205
|
| 0.1.7-rc.1 / rc.2、0.2.0-rc.1 / rc.2 | 4.0.4 | 3.18.4 | 1.0.9 | 1.0.5 |
|
|
195
206
|
| 0.2.1-alpha.2 | 4.0.5-alpha.1 | 3.18.5-alpha.1 | 1.0.10-alpha.1 | 1.0.6-alpha.1 |
|
|
196
207
|
| 0.2.1-alpha.3 / alpha.4 / alpha.5 | 4.0.5-alpha.2 | 3.18.5-alpha.2 | 1.0.10-alpha.2 | 1.0.6-alpha.2 |
|
|
197
|
-
| 0.2.1-rc.1 / rc.2 / rc.3 | 4.0.5 | 3.18.5 | 1.0.10 | 1.0.6 |
|
|
208
|
+
| 0.2.1-rc.1 / rc.2 / rc.3 / rc.4 | 4.0.5 | 3.18.5 | 1.0.10 | 1.0.6 |
|
|
198
209
|
|
|
199
|
-
发布前还应使用隔离 `DSH_HOME` 启动真实 Web profile,验证配置保存/读回、整包及组件开关、重新挂载和卸载;不复用个人凭据或会话。代码 HMR、外部 CLI 快照刷新和浏览器跨宿主 release 刷新是不同验收项,不能用一次开关测试代替。当前结果与未覆盖项见 [2026-10-
|
|
210
|
+
发布前还应使用隔离 `DSH_HOME` 启动真实 Web profile,验证配置保存/读回、整包及组件开关、重新挂载和卸载;不复用个人凭据或会话。代码 HMR、外部 CLI 快照刷新和浏览器跨宿主 release 刷新是不同验收项,不能用一次开关测试代替。当前结果与未覆盖项见 [2026-10-27 RC.4 验证记录](docs/validation-dsh-0.2.1-rc.4.md),[RC.3 记录](docs/validation-dsh-0.2.1-rc.3.md)与 [RC.2 记录](docs/validation-dsh-0.2.1-rc.2.md)保留为历史证据。
|
|
200
211
|
|
|
201
212
|
Host half 是直接由 `main` 加载的 ESM JavaScript;Web half 通过 tsdown 生成 DSH lazy-CJS factory 到 `lib/client.js`。`pnpm pack` 会在 prepack 阶段自动重建客户端 bundle。
|
|
202
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
|
+
|
|
203
230
|
## License
|
|
204
231
|
|
|
205
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.
|
|
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",
|