dsh-trellis 0.1.5 → 0.1.6

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  DeepSeek Harness (dsh) host 插件:把 [Trellis](https://github.com/mindfold-ai/Trellis) 工作流带进 dsh,并尽量沿用 DSH 原生能力补齐 Codex / Claude Code 级体验:
4
4
 
5
- 1. **每轮 workflow-state 面包屑注入** — 在每个 `agent/pre-step`,解析项目 `.trellis/workflow.md` 的 `[workflow-state:*]` 块 + 活跃任务状态(no_task / planning / in_progress),把对应面包屑注入会话(与 `dsh-agent-instructions` 的注入管道相同)。未变化不重复注入;被压缩后自动重注入;提示词里出现独立单词 `no-trellis` 可跳过当轮。
5
+ 1. **每轮 workflow-state 面包屑注入** — 在每个 `agent/pre-step`,解析项目 `.trellis/workflow.md` 的 `[workflow-state:*]` 块 + 活跃任务状态(no_task / task_error / planning / in_progress),把对应面包屑注入会话(与 `dsh-agent-instructions` 的注入管道相同)。未变化不重复注入;被压缩后自动重注入;提示词里出现独立单词 `no-trellis` 可跳过当轮。
6
6
  2. **隔离的原生会话上下文身份** — 从当前 agent 的 DSH 原生 session header 生成受管的 `DSH_TRELLIS_CONTEXT_ID`,让 `task.py start/create/current` 解析到会话级 active-task 指针,并在子代理身份不同于 shell 自身 session 时优先使用被转发的身份;插件命令启动的子进程也显式使用同一个 DSH 身份。
7
7
  3. **原生子代理同步** — 安装插件后,Trellis 角色可用 DSH continuable 后台子代理;主会话先继续独立工作,耗尽后可调用事件驱动的 `trellis_wait`。它监听 DSH 的 `subagent/end`,利用 lifecycle 自带的 run/provider/output-block 元数据返回 `completed / failed / aborted / unknown` 的 fail-closed 结论。`error`、`max-tokens`、`refusal` 和未来未知失败原因都不会被误报成通过。rc.8 在父会话 idle 时也会用原生 settlement notice 唤醒,因此已经结束当前轮时无需额外调用 wait。
8
8
  4. **`/trellis` 命令** — `/trellis-status`(活跃任务 + git 状态)、`/trellis-finish`(只读检查 + 安全收尾清单,不提前清 active-task 指针)。命令输出不进模型历史;真正的会话收尾走技能面 `/trellis-finish-work`,由技能先归档再写 journal。rc.8 下两条零输入命令使用 `recordInput: false`,并显式拒绝参数和图片附件,避免静默忽略输入。
@@ -75,9 +75,16 @@ Headless、rc.6 或需要声明部署默认值时,仍可在 profile 的 `cordi
75
75
  commandsEnabled: true
76
76
  ```
77
77
 
78
+ ### 0.1.6:Trellis 会话隔离与任务错误兼容
79
+
80
+ - 跟随 [Trellis #608](https://github.com/mindfold-ai/Trellis/pull/608):只读取当前 DSH 会话的精确指针。缺少身份或匹配指针时返回 `no_task`,不再借用唯一的其他会话;也不从派发提示词反向建立会话绑定。子代理继续通过明确的 `Active task:` 派发上下文读取任务。
81
+ - 对齐 [官方 `task_error` 工作流](https://github.com/mindfold-ai/marketplace/commit/62a77c9b57fb8bb081c110394bce08b6ebc6e09a):已绑定任务的 `task.json` 缺失、不可读、JSON 损坏或状态为空时,保留任务路径,提示先修复记录,不把它当成“没有任务”。不修改或删除任何任务与指针。
82
+ - 旧项目没有 `[workflow-state:task_error]` 块时,插件提供保守的修复提示;恢复有效记录后,下一轮自动恢复正常状态。因此无需先更新 Trellis CLI 才能使用本插件的修复。
83
+ - 插件修复不替代 Trellis Python 脚本升级:旧版 `task.py current/finish` 的回退策略、归档恢复和上下文清单检查仍由项目中的 `.trellis/scripts` 实现。
84
+
78
85
  ## 工作原理
79
86
 
80
- - **状态解析**(`lib/workflow.js`):向上找项目根 → 读 `.trellis/workflow.md` 解析状态块 → 先看当前会话指针 `.trellis/.runtime/sessions/dsh_<id>.json`。当前指针缺失时只允许 Trellis 官方的“唯一 session 文件”回退;存在 0 个或 2 个以上 session 文件就拒绝猜测,避免多个 DSH 窗口串任务。
87
+ - **状态解析**(`lib/workflow.js`):向上找项目根 → 读 `.trellis/workflow.md` 解析状态块 → 只读当前会话指针 `.trellis/.runtime/sessions/dsh_<id>.json`,不枚举其他会话。指针缺失时返回 `no_task`;指向的任务记录不可读时返回 `task_error` 并保留路径。修复记录或重新绑定任务后会在下一轮重新解析。
81
88
  - **注入去重**(`lib/breadcrumb-projection.js`):投影仅保存面包屑的事件序号和 source/content 指纹,与 DSH 已维护的 `session.surface.nodes` 一起确定最后可见面包屑。恢复、分叉和重载均从 projection 重建;不缓存“本进程最后注入”的临时判断,也不扫描完整事件历史。
82
89
  - **会话身份**:DSH 原生提供 `DSH_SESSION_ID = agent.session.header.id`,并先丢弃环境中已有的 `DSH_*` 再重建受管命名空间。Trellis beta 因此会在同时看到 `DSH_SHELL=1` 与 `DSH_SESSION_ID` 时优先解析当前 DSH 身份,即使没有插件也不会被外层 host 继承的 `TRELLIS_CONTEXT_ID` 串任务。插件通过 `shellEnv` 为每次执行额外生成 `DSH_TRELLIS_CONTEXT_ID = dsh_<session-id>`,用于转发可能不同于 shell 自身 session 的子代理身份;主会话与子代理仍各自保留 DSH 身份,子代理通过派发 prompt 首行的 `Active task:` 和角色 prelude 取得父任务上下文。
83
90
  - **Headless 会话**:每次 `dsh --profile headless` 调用都是新的 DSH session。需要跨轮保留 active-task 指针时,应保持同一会话或显式 resume 返回的 session id,不能把多个独立 headless 调用当成同一 session。
@@ -92,7 +99,7 @@ pnpm run build:client
92
99
  pnpm test # node --test test/*.test.js
93
100
  ```
94
101
 
95
- GitHub Actions 在 Windows / Linux 的 Node 24 环境执行锁定安装、客户端构建、测试和打包检查;其中包含真实 Session/projection 服务的增量驱动、恢复、分叉、压缩和 checkpoint 回归。
102
+ GitHub Actions 在 Windows / Linux 的 Node 24 环境执行锁定安装、客户端构建、测试和打包检查;其中包含真实 Session/projection 服务的增量驱动、恢复、分叉、压缩和 checkpoint 回归,以及真实临时项目中的精确身份绑定、跨会话隔离、任务记录损坏/修复、解绑与 Settings 重载回归。
96
103
 
97
104
  Host half 是直接由 `main` 加载的 ESM JavaScript;Web half 通过 tsdown 生成 DSH lazy-CJS factory 到 `lib/client.js`。`pnpm pack` 会在 prepack 阶段自动重建客户端 bundle。
98
105
 
package/lib/breadcrumb.js CHANGED
@@ -101,7 +101,7 @@ export function createBreadcrumbComposer(resolved, ctx, dependencies = {}) {
101
101
  const cwd = agent.session.header.cwd ?? process.cwd();
102
102
  const sessionId = agent.session.header.id ?? "";
103
103
  const contextKey =
104
- sessionId === "" ? "dsh" : `dsh_${sanitizeContextKey(sessionId)}`;
104
+ sessionId === "" ? "" : `dsh_${sanitizeContextKey(sessionId)}`;
105
105
  signal?.throwIfAborted();
106
106
  const state = await resolveTrellisState({
107
107
  cwd,
package/lib/node-fs.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @module dsh-trellis/node-fs
5
5
  */
6
- import { stat as nodeStat, readFile as nodeReadFile, readdir as nodeReaddir } from "node:fs/promises";
6
+ import { stat as nodeStat, readFile as nodeReadFile } from "node:fs/promises";
7
7
 
8
8
  /** @type {import("./workflow.js").FsShim} */
9
9
  export const nodeFs = {
@@ -30,11 +30,4 @@ export const nodeFs = {
30
30
  return null;
31
31
  }
32
32
  },
33
- async listDir(path) {
34
- try {
35
- return await nodeReaddir(path);
36
- } catch {
37
- return [];
38
- }
39
- },
40
33
  };
package/lib/workflow.js CHANGED
@@ -25,6 +25,12 @@ export function sanitizeContextKey(value) {
25
25
  export const FALLBACK_BREADCRUMB =
26
26
  "Refer to `.trellis/workflow.md` for the current workflow step (no matching [workflow-state] block found).";
27
27
 
28
+ /** Old workflows may not yet define task_error; never turn a bad record into no_task. */
29
+ export const TASK_ERROR_BREADCRUMB =
30
+ "The active task record could not be read. Do not create or activate another task. " +
31
+ "Inspect the active task directory and repair its task.json (a JSON object with a non-empty status). " +
32
+ "Preserve existing fields and artifacts; ask the user if the correct status is uncertain.";
33
+
28
34
  /**
29
35
  * Parse every `[workflow-state:STATUS] ... [/workflow-state:STATUS]` block.
30
36
  * @param {string} content - workflow.md content.
@@ -82,71 +88,42 @@ export async function readCached(path, fs, cache) {
82
88
  /**
83
89
  * Resolve the active task for a dsh session.
84
90
  *
85
- * Canonical Trellis semantics are session-pointer-driven. If this session has
86
- * no usable pointer, mirror `active_task.py`'s conservative class-2 fallback:
87
- * infer a task only when the runtime contains exactly one session JSON file.
88
- * Two or more files means multiple windows may be active, so the resolver
89
- * refuses to guess even when one task happens to be newer.
91
+ * Read only this session's pointer. A sole foreign session is not proof of
92
+ * ownership (Trellis #608). Child agents receive an explicit Active task:
93
+ * dispatch prompt; they must not borrow arbitrary session bindings here.
94
+ * Once bound, an unreadable task record is task_error, not an absent task.
90
95
  *
91
96
  * @param {string} root - project root.
92
97
  * @param {string} contextKey - sanitized `dsh_<session-id>` key for this agent.
93
- * @param {{listDir(path: string): Promise<string[]>, readFile(path: string): Promise<string | null>, stat(path: string): Promise<{mtimeMs: number} | null>}} fs
98
+ * @param {{readFile(path: string): Promise<string | null>}} fs
94
99
  * @returns {Promise<{path: string, status: string} | null>} task dir-relative path + status.
95
100
  */
96
101
  export async function resolveActiveTask(root, contextKey, fs) {
97
- // 1. Session-scoped pointer (task.py start / create).
98
- const sessionsDir = join(root, ".trellis", ".runtime", "sessions");
99
- let names = [];
102
+ if (!contextKey) return null;
103
+ const pointer = join(
104
+ root, ".trellis", ".runtime", "sessions", `${contextKey}.json`,
105
+ );
106
+ let data = null;
100
107
  try {
101
- names = await fs.listDir(sessionsDir);
102
- } catch {
103
- names = [];
104
- }
105
- for (const name of names) {
106
- if (name !== `${contextKey}.json`) continue;
107
- let data = null;
108
- try {
109
- data = JSON.parse((await fs.readFile(join(sessionsDir, name))) ?? "null");
110
- } catch {
111
- continue;
112
- }
113
- if (
114
- data === null ||
115
- typeof data !== "object" ||
116
- typeof data.current_task !== "string"
117
- )
118
- continue;
119
- const status = await readTaskStatus(root, data.current_task, fs);
120
- if (status !== null) return { path: data.current_task, status };
121
- }
122
-
123
- // 2. Official single-session fallback. Count files before parsing so one
124
- // malformed pointer cannot make a multi-window runtime look unambiguous.
125
- const sessionFiles = names.filter((name) => name.endsWith(".json"));
126
- if (sessionFiles.length !== 1) return null;
127
-
128
- let fallback = null;
129
- try {
130
- fallback = JSON.parse(
131
- (await fs.readFile(join(sessionsDir, sessionFiles[0]))) ?? "null",
132
- );
108
+ data = JSON.parse((await fs.readFile(pointer)) ?? "null");
133
109
  } catch {
134
110
  return null;
135
111
  }
136
112
  if (
137
- fallback === null ||
138
- typeof fallback !== "object" ||
139
- typeof fallback.current_task !== "string" ||
140
- fallback.current_task === ""
113
+ data === null ||
114
+ typeof data !== "object" ||
115
+ Array.isArray(data) ||
116
+ typeof data.current_task !== "string" ||
117
+ data.current_task.trim() === ""
141
118
  ) {
142
119
  return null;
143
120
  }
144
- const status = await readTaskStatus(root, fallback.current_task, fs);
145
- return status === null ? null : { path: fallback.current_task, status };
121
+ const status = await readTaskStatus(root, data.current_task, fs);
122
+ return { path: data.current_task, status: status ?? "task_error" };
146
123
  }
147
124
 
148
125
  /**
149
- * Read one task.json status, or null when missing/unparseable.
126
+ * Read a non-empty task.json status, or null for an unreadable/invalid record.
150
127
  * @param {string} root
151
128
  * @param {string} taskPath - repo-root relative task dir (e.g. `.trellis/tasks/04-17-foo`).
152
129
  * @param {{readFile(path: string): Promise<string | null>}} fs
@@ -160,7 +137,9 @@ export async function readTaskStatus(root, taskPath, fs) {
160
137
  if (
161
138
  data !== null &&
162
139
  typeof data === "object" &&
163
- typeof data.status === "string"
140
+ !Array.isArray(data) &&
141
+ typeof data.status === "string" &&
142
+ data.status.trim() !== ""
164
143
  ) {
165
144
  return data.status;
166
145
  }
@@ -173,11 +152,12 @@ export async function readTaskStatus(root, taskPath, fs) {
173
152
  /**
174
153
  * Map a task.json status to a workflow-state breadcrumb status.
175
154
  * @param {string | null} status
176
- * @returns {"planning" | "in_progress" | "no_task"}
155
+ * @returns {"planning" | "in_progress" | "task_error" | "no_task"}
177
156
  */
178
157
  export function mapStatus(status) {
179
158
  if (status === "planning") return "planning";
180
159
  if (status === "in_progress") return "in_progress";
160
+ if (status === "task_error") return "task_error";
181
161
  return "no_task";
182
162
  }
183
163
 
@@ -187,7 +167,7 @@ export function mapStatus(status) {
187
167
  * @param {string} options.cwd
188
168
  * @param {string[]} options.markers
189
169
  * @param {string} options.contextKey - sanitized session context key.
190
- * @param {object} options.fs - filesystem shim ({exists, listDir, readFile, stat}).
170
+ * @param {object} options.fs - filesystem shim ({exists, readFile, stat}).
191
171
  * @param {Map<string, {mtimeMs: number, content: string | null}>} options.cache
192
172
  * @returns {Promise<{root: string, status: string, taskPath: string | null, body: string, digest: string} | null>}
193
173
  */
@@ -207,7 +187,10 @@ export async function resolveTrellisState({
207
187
  if (blocks.size === 0) return null;
208
188
  const task = await resolveActiveTask(root, contextKey, fs);
209
189
  const status = mapStatus(task?.status ?? null);
210
- const body = blocks.get(status) ?? FALLBACK_BREADCRUMB;
190
+ const body =
191
+ status === "task_error"
192
+ ? blocks.get(status) || TASK_ERROR_BREADCRUMB
193
+ : blocks.get(status) ?? FALLBACK_BREADCRUMB;
211
194
  const digest = createHash("sha1")
212
195
  .update(`${status}\n${task?.path ?? ""}\n${body}`)
213
196
  .digest("hex");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-trellis",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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",