dsh-taskboard 0.5.1 → 0.5.3
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 +19 -4
- package/lib/client.js +26 -16
- package/lib/host/execution.js +3 -2
- package/lib/host/execution.js.map +1 -1
- package/lib/host/templates.js +21 -1
- package/lib/host/templates.js.map +1 -1
- package/lib/host/tools.js +1 -1
- package/lib/host/tools.js.map +1 -1
- package/lib/shared/protocol.js +4 -3
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +1 -1
- package/src/client/board/TaskFormModal.tsx +2 -2
- package/src/client/board-mount.tsx +9 -6
- package/src/client/sidebar-entry.ts +11 -4
- package/src/client/styles.ts +5 -3
- package/src/host/execution.ts +3 -2
- package/src/host/templates.ts +17 -1
- package/src/host/tools.ts +1 -1
- package/src/shared/protocol.ts +5 -4
- package/src/shared/version.ts +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
[](https://www.npmjs.com/package/dsh-taskboard)
|
|
2
2
|
[](https://github.com/cloader/dsh-taskboard/blob/main/LICENSE)
|
|
3
3
|
|
|
4
|
+
[English](./README_en.md) | 简体中文
|
|
5
|
+
|
|
4
6
|
# dsh-taskboard
|
|
5
7
|
|
|
6
8
|
DeepSeek Harness 的**任务看板插件**:人建卡、agent 认领执行、人验收。任务挂项目(workspace)、可指定模型与 preset、支持手动与定时执行,全流程双向协作。
|
|
@@ -134,7 +136,7 @@ agent:
|
|
|
134
136
|
- **验收清单 DoD(0.4.0)**:建卡时定验收条件(≤30 项);agent 用 `taskboard_checklist` 增补/勾选(附证据 note);用户在详情页直接勾选;待验收时未完成项红色高亮 + 卡片「☑ n/m」角标(未全勾显红);清单编辑在表单中整组管理(勾选状态与证据保留)
|
|
135
137
|
- **结构化执行报告(0.4.0)**:agent 收尾用 `taskboard_execution_report` 提交(摘要 / 改动文件 / 自验 / 产物 / 剩余风险),自动挂到当前执行记录;待验收详情页分栏渲染;开场框架行明示提交时序(报告 → 评论 → 移待验收)
|
|
136
138
|
- **JSON 导入(0.4.0)**:顶栏「⬆ 导入」选择备份文件 → 干跑预览(新增 / 覆盖 / 无效分类明细)→ 合并(按 id upsert)或整册替换(自动先备份当前台账 + 二次确认);⬇ JSON 导出的文件即同格式可直接恢复
|
|
137
|
-
- **任务模板(0.4.0)**:「+ 新建任务 ▼」下拉(空白 / 内置 Bug 修复·发布检查·例行巡检 / 管理模板)一键预填表单(标题/描述/Prompt/紧急度/定时/隔离/preset/清单);任务详情「⌗ 存为模板」沉淀常用配置;模板存 DSH 主目录 side file,改名/删除在管理弹窗
|
|
139
|
+
- **任务模板(0.4.0)**:「+ 新建任务 ▼」下拉(空白 / 内置 新增功能·Bug 修复·发布检查·例行巡检 / 管理模板)一键预填表单(标题/描述/Prompt/紧急度/定时/隔离/preset/清单);任务详情「⌗ 存为模板」沉淀常用配置;模板存 DSH 主目录 side file,改名/删除在管理弹窗
|
|
138
140
|
- **Diff 查看器(0.4.0)**:隔离块提交行、未提交修改文件行点击即在看板内展开 diff(提交 `git show` / 文件 `git diff`,128KB·2000 行封顶标注截断);worktree 已删时回落主仓(仅限提交与带基线的范围 diff)
|
|
139
141
|
|
|
140
142
|
**Agent 工具(taskboard_\*)**
|
|
@@ -142,7 +144,7 @@ agent:
|
|
|
142
144
|
- 代码级协议闸:agent 永远移不到 done(清单全勾也不行);任务被持有时不可抢占;model/execution 对 agent 只读
|
|
143
145
|
|
|
144
146
|
**执行**
|
|
145
|
-
- 手动执行或 cron 定时:每次执行在任务项目内新建全新会话(干净上下文、可指定模型、可指定 preset
|
|
147
|
+
- 手动执行或 cron 定时:每次执行在任务项目内新建全新会话(干净上下文、可指定模型、可指定 preset);开场两条消息同一回合送达——插件上下文行携带任务框架与交接协议(含失败回退路径),卡片内容(标题+描述+提示词)以正常用户消息呈现
|
|
146
148
|
- **任务级 preset(0.3.3)**:新建/编辑表单「执行模式(preset)」下拉——执行会话按该 preset 组合(工具集与人设由此而来,对齐 GUI 新会话的组合方式);默认预选部署默认 preset,也可选「跟随部署默认」;preset 损坏时执行直接失败并把原因写进执行记录(不产出半组合会话);随时可改,下轮执行生效
|
|
147
149
|
- **Git Worktree 隔离执行(0.3.0)**:任务级开关(0.5.0 起新建任务的默认值由「看板设置」统一决定,出厂默认原目录执行),每次执行在 `<项目>/.dsh-worktrees/<任务ID>` 独立 worktree 上进行,分支 `task/<标题>+<任务ID>`(首次创建后定死,改名不改);执行会话归属项目根目录(分组、工具与文件沙箱完整可用——DSH 要求会话 cwd 即工作区根,0.3.2 修正),worktree 路径与边界纪律在开场指令中明确下达;结算自动采集提交列表 / 未提交修改警告 / 改动统计;非 git 项目或 git 不可用时自动降级原目录执行(执行记录注明降级原因,台账与执行主流程永不因 git 失败而失败);验收时详情页一键 `--no-ff` 合并到主工作区(主区脏或冲突原样报告,不自动解决)、删除 worktree(有未提交修改时拒绝)、可选删分支;支持「↻ 续跑」在现有 worktree/分支上继续执行(保留上次改动与提交)
|
|
148
150
|
- **看板设置(0.5.0)**:顶栏「🛠 设置」——选择新建任务默认怎么执行(🌿 Worktree 隔离 / 📁 原目录执行,出厂默认后者)。保存后新建的任务都按它来;之后改设置,已建好的任务不受影响
|
|
@@ -157,7 +159,7 @@ agent:
|
|
|
157
159
|
- **Worktree 隔离是协作约定而非沙箱**:执行会话拥有完整工具权限,隔离依赖分支约定,不适用于运行不可信代码的场景。
|
|
158
160
|
- **数据本地**:台账、模板全部存本机 DSH 主目录;不外发任何数据,无需 Token / API Key。
|
|
159
161
|
|
|
160
|
-
## 配置与数据
|
|
162
|
+
## 配置与数据
|
|
161
163
|
|
|
162
164
|
开箱即用,全部配置项如下(均为可选):
|
|
163
165
|
|
|
@@ -181,7 +183,7 @@ agent:
|
|
|
181
183
|
## 常见问题
|
|
182
184
|
|
|
183
185
|
**侧边栏没有「任务看板」入口?**
|
|
184
|
-
刷新页面;仍没有则确认插件已装进当前 profile 并重启 `dsh web
|
|
186
|
+
刷新页面;仍没有则确认插件已装进当前 profile 并重启 `dsh web`(宿主半区在进程启动时加载)。三代 shell 均支持:`data-pane`(dev)、哈希类名(官方布局,0.4.2 起)、DSH Desktop 非兼容 extended frame(0.5.2 起)。
|
|
185
187
|
|
|
186
188
|
**任务数据存在哪?怎么备份?**
|
|
187
189
|
见[配置与数据](#配置与数据)。GUI「⬇ JSON」随时全量导出;「⬆ 导入」可恢复。
|
|
@@ -214,6 +216,19 @@ node scripts/screenshot.mjs # 重新生成 img/ 截图(需本机 Edge)
|
|
|
214
216
|
|
|
215
217
|
## 升级日志
|
|
216
218
|
|
|
219
|
+
### 0.5.3
|
|
220
|
+
|
|
221
|
+
- **执行 Prompt 改为追加式**:自定义 Prompt 不再整体替换开场指令——实际发给执行会话的是「标题+任务描述+Prompt」,写在描述里的背景不再被漏掉;新建/编辑表单文案与 `taskboard_create` 工具描述同步更新
|
|
222
|
+
- **新增内置「新增功能」模板**:排在「Bug 修复」之前——标题前缀「新增:」,按「明确需求要点 → 实现 → 补测试 → 跑套件」四步走,自带三项验收清单
|
|
223
|
+
- **「Bug 修复」模板首行改为「修复以上问题并按序交接」**:描述现在先于 Prompt 送达,「以上」才指得住
|
|
224
|
+
|
|
225
|
+
### 0.5.2
|
|
226
|
+
|
|
227
|
+
- **支持 DSH Desktop 非兼容模式下侧栏没有「任务看板」入口、看板也打不开的问题([#6](https://github.com/cloader/dsh-taskboard/issues/6))**:
|
|
228
|
+
- 支持 DSH Desktop V2.0.3 非兼容模式下侧栏没有「任务看板」入口、看板也打不开的问题
|
|
229
|
+
- 侧栏入口换上新图标:更直观的三列看板样式
|
|
230
|
+
- 新增非兼容模式的回归测试(共 199 项)
|
|
231
|
+
|
|
217
232
|
### 0.5.1
|
|
218
233
|
|
|
219
234
|
这一版主要来自一次全面的代码体检:看板更防手滑、更稳,行为上有几处小变化。
|
package/lib/client.js
CHANGED
|
@@ -1027,10 +1027,12 @@ window.__ModuleLoader__.load({
|
|
|
1027
1027
|
.dsh-atb-search { width: 130px; }
|
|
1028
1028
|
.dsh-atb-badge[data-kind="stale"] { background: rgba(217,130,43,.15); color: #d9822b; }
|
|
1029
1029
|
|
|
1030
|
-
/*
|
|
1031
|
-
* shell's CSS-Module hashed centerCol (
|
|
1030
|
+
/* Triple-generation column matching — dev shell's data-pane pane, the
|
|
1031
|
+
* official layout shell's CSS-Module hashed centerCol (0.4.2), or DSH
|
|
1032
|
+
* Desktop's non-compat extended frame surface (0.5.2, see board-mount.tsx). */
|
|
1032
1033
|
html[data-dsh-atb-active] [data-pane="conversation"] > *:not([data-dsh-atb-view]),
|
|
1033
|
-
html[data-dsh-atb-active] [class*="centerCol"] > *:not([data-dsh-atb-view])
|
|
1034
|
+
html[data-dsh-atb-active] [class*="centerCol"] > *:not([data-dsh-atb-view]),
|
|
1035
|
+
html[data-dsh-atb-active] .dshDesktopConversationSurface > *:not([data-dsh-atb-view]) { display: none !important; }
|
|
1034
1036
|
.dsh-atb-view { display: none; }
|
|
1035
1037
|
html[data-dsh-atb-active] .dsh-atb-view { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
|
1036
1038
|
|
|
@@ -1698,14 +1700,19 @@ window.__ModuleLoader__.load({
|
|
|
1698
1700
|
//#region src/client/sidebar-entry.ts
|
|
1699
1701
|
/** Stable data attribute identifying this entry row. */
|
|
1700
1702
|
const ENTRY_SELECTOR = "[data-dsh-atb-entry]";
|
|
1701
|
-
/** Inline icon (16px nav-icon look). */
|
|
1702
|
-
const ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"2\" y=\"2
|
|
1703
|
+
/** Inline icon: a three-lane kanban board (16px nav-icon look). */
|
|
1704
|
+
const ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"2\" y=\"2\" width=\"12\" height=\"12\" rx=\"2\"/><path d=\"M6 2v12M10 2v12\"/></svg>";
|
|
1703
1705
|
/**
|
|
1704
1706
|
* Find the sidebar shell root element, or undefined while not yet mounted.
|
|
1705
|
-
* (
|
|
1707
|
+
* Triple-generation matching (0.5.2): the dev shell's data-pane pane, the
|
|
1708
|
+
* official layout's CSS-Module sidebarCol, and DSH Desktop's non-compat
|
|
1709
|
+
* (extended) frame — which disables the official ui-layout row and owns
|
|
1710
|
+
* the columns itself (aside.dshDesktopSidebarSurface >
|
|
1711
|
+
* div.dshDesktopUpstreamSidebar wrapping the unchanged official sidebar).
|
|
1712
|
+
* logoRow-owner resolution is identical on all three generations.
|
|
1706
1713
|
*/
|
|
1707
1714
|
function sidebarRoot() {
|
|
1708
|
-
const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
|
|
1715
|
+
const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"], .dshDesktopUpstreamSidebar, .dshDesktopSidebarSurface");
|
|
1709
1716
|
if (column === null) return void 0;
|
|
1710
1717
|
return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
|
|
1711
1718
|
}
|
|
@@ -1932,7 +1939,7 @@ window.__ModuleLoader__.load({
|
|
|
1932
1939
|
* @module dsh-taskboard/shared/version
|
|
1933
1940
|
*/
|
|
1934
1941
|
/** The package version (must equal package.json "version"). */
|
|
1935
|
-
const PLUGIN_VERSION = "0.5.
|
|
1942
|
+
const PLUGIN_VERSION = "0.5.3";
|
|
1936
1943
|
|
|
1937
1944
|
//#endregion
|
|
1938
1945
|
//#region src/client/board/labels.ts
|
|
@@ -3608,12 +3615,12 @@ window.__ModuleLoader__.load({
|
|
|
3608
3615
|
})
|
|
3609
3616
|
}),
|
|
3610
3617
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
3611
|
-
label: editing ? "执行 Prompt" : "执行 Prompt
|
|
3618
|
+
label: editing ? "执行 Prompt(实际 Prompt = 标题+任务描述+Prompt)" : "执行 Prompt(可选;实际 Prompt = 标题+任务描述+Prompt)",
|
|
3612
3619
|
full: true,
|
|
3613
3620
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
3614
3621
|
value: prompt,
|
|
3615
3622
|
onChange: (e) => setPrompt(e.target.value),
|
|
3616
|
-
placeholder: "
|
|
3623
|
+
placeholder: "追加在「标题+任务描述」之后发给执行会话的补充指令。支持模板变量:{{lastExecution}}(上次执行结果)、{{lastComments}}(最近 3 条评论)"
|
|
3617
3624
|
})
|
|
3618
3625
|
}),
|
|
3619
3626
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
@@ -4834,15 +4841,18 @@ window.__ModuleLoader__.load({
|
|
|
4834
4841
|
* conversation content while the board is active. Toggling rides a data
|
|
4835
4842
|
* attribute on <html> — no React involvement in the shell.
|
|
4836
4843
|
*
|
|
4837
|
-
* Column matching is
|
|
4838
|
-
* `data-pane="conversation"`; the
|
|
4839
|
-
* dropped data-pane
|
|
4840
|
-
* (`pI_x6G_centerCol`) —
|
|
4841
|
-
*
|
|
4844
|
+
* Column matching is TRIPLE-generation: the dev shell marks the column
|
|
4845
|
+
* with `data-pane="conversation"`; the official layout shell
|
|
4846
|
+
* (dsh-client-ui-layout) dropped data-pane and uses CSS-Module hashed
|
|
4847
|
+
* class names (`pI_x6G_centerCol`) — and DSH Desktop's non-compat
|
|
4848
|
+
* (extended) mode disables the official layout row entirely, owning the
|
|
4849
|
+
* columns itself (`main.dshDesktopConversationSurface`, 0.5.2) — the
|
|
4850
|
+
* fallbacks keep all three mounting, exactly like sidebar-entry's column
|
|
4851
|
+
* selector.
|
|
4842
4852
|
*
|
|
4843
4853
|
* @module dsh-taskboard/client/board-mount
|
|
4844
4854
|
*/
|
|
4845
|
-
const CONVERSATION_COLUMN_SELECTOR = "[data-pane=\"conversation\"], [class*=\"centerCol\"]";
|
|
4855
|
+
const CONVERSATION_COLUMN_SELECTOR = "[data-pane=\"conversation\"], [class*=\"centerCol\"], .dshDesktopConversationSurface";
|
|
4846
4856
|
const ACTIVE_ATTR = "data-dsh-atb-active";
|
|
4847
4857
|
/** Sibling panels' activation attributes, evicted when this board opens. */
|
|
4848
4858
|
const OTHER_ACTIVE_ATTRS = ["data-dsh-taskboard-active", "data-dsh-ssh-active"];
|
package/lib/host/execution.js
CHANGED
|
@@ -492,8 +492,9 @@ var ExecutionService = class {
|
|
|
492
492
|
return text;
|
|
493
493
|
}
|
|
494
494
|
/**
|
|
495
|
-
* The card body as a normal user bubble: the effective prompt (
|
|
496
|
-
*
|
|
495
|
+
* The card body as a normal user bubble: the effective prompt (title+
|
|
496
|
+
* description, with the explicit prompt appended when set) with template
|
|
497
|
+
* variables resolved from
|
|
497
498
|
* the task's own history at submit time (valuable for recurring patrols):
|
|
498
499
|
* `{{lastExecution}}` → the previous execution's trigger/outcome/error;
|
|
499
500
|
* `{{lastComments}}` → the last three comments (who + body).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execution.js","names":[],"sources":["../../src/host/execution.ts"],"sourcesContent":["/**\n * Host execution service: runs a task through dsh's REAL session machinery —\n * a fresh agent+session inside the task's project workspace (creation carries\n * the pinned model when the task has one), the session is attached to the\n * workspace so it appears in the GUI's project session list, the effective\n * prompt is submitted as an ordinary user message, and the turn settlement\n * (turn/end reason) is folded back into the task's execution record.\n *\n * Every execution is a NEW session: clean context, no reuse of previous runs.\n *\n * @module dsh-taskboard/host/execution\n */\nimport {\n effectiveIsolation,\n effectivePrompt,\n newCommentId,\n newExecutionId,\n normalizeBody,\n type ExecutionRecord,\n type IsolationMode,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport { sanitizeBranchName, worktreePathOf, type GitFace, type SettlementFacts } from './git.ts'\nimport { MessageId } from './sdk.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Default cap on concurrently running executions (env-overridable). */\nexport const DEFAULT_MAX_CONCURRENT = 3\n\n/** Narrow agents face (the registry's create, structurally). */\nexport interface AgentsFace {\n create(options: {\n sessionId: string\n meta?: { cwd?: string; agentPreset?: string }\n agentOptions?: { provider?: string; model?: string }\n /** Preset composition callback: mounts tools/persona into the agent's scoped context. */\n setup?: (agentCtx: unknown) => Promise<void> | void\n }): Promise<{\n agent: {\n id: string\n followup(message: unknown): void\n inject(message: unknown): void\n whenIdle(): Promise<void>\n }\n dispose(): Promise<void>\n }>\n}\n\n/**\n * The preset composition an execution session is built from — the shape\n * apiproxy's ensureSession produces: resolve → record on the session header,\n * mount → inside agents.create's setup callback.\n */\nexport interface AgentComposition {\n /** The resolved preset id recorded on the session header. */\n agentPreset: string\n /** Mounts the preset's plugins (tools, persona) into the agent's scope. */\n setup: (agentCtx: unknown) => Promise<void> | void\n}\n\n/** Narrow workspaces face for execution. */\nexport interface ExecutionWorkspaceFace {\n get(id: string): { id: string; path: string } | undefined\n attach(workspaceId: string, sessionId: string): Promise<void>\n}\n\n/** Narrow event-bus face for settlement listening. */\nexport interface EventsFace {\n onSessionEvent(listener: (sessionId: string, event: { type: string; data?: unknown }) => void): () => void\n}\n\n/** Everything the execution service needs. */\nexport interface ExecutionDeps {\n store: TaskStore\n agents: AgentsFace\n workspaces: ExecutionWorkspaceFace\n events: EventsFace\n now: () => number\n /** The deployment default model (fills sessions of unpinned tasks). */\n defaultModel?: () => { provider: string; model: string } | undefined\n /** Mint session ids (injectable for tests). */\n mintSessionId?: () => string\n /** Mint message ids (injectable for tests). */\n mintMessageId?: () => string\n /** Best-effort session rename (pins the session list title to the task title). */\n renameSession?: (sessionId: string, title: string) => void\n /** Max concurrently running executions across all tasks (default 3). */\n maxConcurrent?: number\n /**\n * Git face for worktree isolation (0.3.0). Absent → every worktree-mode\n * task degrades to the original directory with an isolationNote.\n */\n git?: GitFace\n /**\n * Resolve the preset composition for an execution session (0.3.3): hands\n * the session its tool set. Absent → sessions run on the bare host\n * composition (pre-preset behavior). A rejection fails the run through\n * the existing failure path — a broken preset never yields a half-composed\n * session (same rollback semantics as apiproxy).\n */\n composeAgent?: (presetId?: string) => Promise<AgentComposition | undefined>\n}\n\n/** Outcome of a run request (immediate; the run settles asynchronously). */\nexport type RunRequestResult =\n | { ok: true; executionId: string; sessionId: string }\n | { ok: false; error: string }\n\n/** Outcome of a cancel request. */\nexport type CancelRequestResult =\n | { ok: true; executionId: string }\n | { ok: false; error: string }\n\n/** Whether a turn/end payload closed with an error reason. */\nfunction isErrorTurnEnd(data: unknown): { message: string } | undefined {\n if (typeof data !== 'object' || data === null) return undefined\n const reason = (data as { reason?: unknown }).reason\n if (typeof reason !== 'object' || reason === null) return undefined\n const kind = (reason as { kind?: unknown }).kind\n if (kind !== 'error') return undefined\n const error = (reason as { error?: { message?: unknown } }).error\n const message = typeof error?.message === 'string' ? error.message : 'turn failed'\n console.error('[dsh-taskboard] turn error detail:', JSON.stringify(error)?.slice(0, 2000) ?? '')\n return { message }\n}\n\n/** Prepared worktree facts threaded through a live run (settlement evidence). */\nexport interface PreparedWorktree {\n branch: string\n worktreePath: string\n baseCommit: string\n /** True when an existing live worktree was kept as-is (续跑). */\n reused?: boolean\n}\n\n/** Per-run options. */\nexport interface RunOptions {\n /**\n * 续跑: keep a live worktree/branch exactly as-is (the previous agent's\n * commits and uncommitted changes survive) instead of resetting to the\n * main HEAD. Falls back to a fresh preparation when none is alive.\n */\n reuseWorktree?: boolean\n}\n\n/** One live execution tracked for settlement and cancellation. */\ninterface RunEntry {\n sessionId: string\n /** Worktree prepared for this run (evidence collection at ANY settlement). */\n prepared?: PreparedWorktree\n settle: () => void\n dispose: () => Promise<void>\n}\n\n/**\n * The execution service.\n */\nexport class ExecutionService {\n /** Live executions by execution id (settles and cancels remove entries). */\n private readonly runs = new Map<string, RunEntry>()\n\n /** Detaches the turn/end listener (plugin teardown — review P1). */\n private readonly unsubscribeEvents: () => void\n\n /** @param deps - store + agents + workspaces + events + clock. */\n constructor(private readonly deps: ExecutionDeps) {\n this.unsubscribeEvents = deps.events.onSessionEvent((sessionId, event) => {\n if (event.type !== 'turn/end') return\n // S7 (open question): ANY turn/end with an error reason fails the whole\n // execution and hands the task back. Whether the DSH session loop can\n // produce recoverable per-turn errors (and keep the session alive) needs\n // host-side confirmation; if it can, this should count consecutive\n // errors or wait for an explicit termination signal instead.\n const failure = isErrorTurnEnd(event.data)\n if (failure !== undefined) {\n this.noteFailure(sessionId, failure.message).catch(error => {\n console.error('[dsh-taskboard] failure settlement error:', error)\n })\n }\n })\n }\n\n /** Detach the settlement listener; safe to call once at plugin teardown. */\n dispose(): void {\n this.unsubscribeEvents()\n }\n\n /**\n * Best-effort evidence collection for a prepared run (fail-soft: undefined\n * on any git problem — settlement NEVER blocks on git).\n */\n private async collectEvidence(prepared: PreparedWorktree | undefined): Promise<SettlementFacts | undefined> {\n if (prepared === undefined || this.deps.git === undefined) return undefined\n try {\n return await this.deps.git.collect(prepared.worktreePath, prepared.baseCommit)\n } catch {\n return undefined\n }\n }\n\n /** Copy collected facts onto an execution record (in place). */\n private applyFacts(execution: ExecutionRecord, facts: SettlementFacts | undefined): void {\n if (facts === undefined) return\n if (facts.headCommit !== undefined) execution.headCommit = facts.headCommit\n execution.commits = facts.commits\n execution.commitsTotal = facts.commitsTotal\n execution.dirtyFiles = facts.dirtyFiles\n execution.dirtyFilesTotal = facts.dirtyFilesTotal\n execution.changedFiles = facts.changedFiles\n if (facts.diffStat !== undefined) execution.diffStat = facts.diffStat\n }\n\n /**\n * Record a turn failure against the running execution of that session and\n * give the task back. Resolves once the failure settlement has COMMITTED —\n * R2: the whenIdle rejection path awaits this (and only this) before\n * releasing its run entry, so a success settlement can never race it into\n * the ledger and record a failed run as succeeded.\n */\n private noteFailure(sessionId: string, message: string): Promise<void> {\n // The failed session may already have committed work — collect the\n // evidence (best effort) BEFORE marking the execution failed (0.3.1).\n const entry = [...this.runs.values()].find(e => e.sessionId === sessionId)\n return this.collectEvidence(entry?.prepared).then(facts =>\n this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n for (const execution of task.executions) {\n if (execution.sessionId === sessionId && execution.outcome === 'running') {\n execution.outcome = 'failed'\n execution.error = message.slice(0, 500)\n execution.endedAt = this.deps.now()\n this.applyFacts(execution, facts)\n // The failed session will not finish the work: hand the task back\n // instead of leaving it stuck in in_progress forever — and leave a\n // system comment so the GUI shows why.\n if (task.status === 'in_progress' && task.claimedBy === sessionId) {\n task.status = 'todo'\n task.updatedAt = this.deps.now()\n delete task.claimedBy\n delete task.claimedAt\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody(`[系统] 执行失败:${message.slice(0, 300)};任务已退回待办。`),\n version: 1,\n createdAt: this.deps.now(),\n })\n }\n return [task]\n }\n }\n }\n return undefined\n }),\n ).then(() => { /* failure settlement committed */ })\n }\n\n /**\n * Patch one task's execution record in the ledger. R3 depth: a record that\n * already settled (cancelled/failed/succeeded) is never resurrected — the\n * startup path patches sessionId long after the gate opened, and a cancel\n * may have committed in between.\n */\n private async patchExecution(executionId: string, patch: Partial<ExecutionRecord>): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n const execution = task.executions.find(e => e.id === executionId)\n if (execution !== undefined) {\n if (execution.outcome !== 'running') return undefined\n Object.assign(execution, patch)\n return [task]\n }\n }\n return undefined\n })\n }\n\n /**\n * Run one task now (manual button or scheduler tick).\n *\n * The in-progress gate and the execution-open write happen inside ONE\n * serial-queue mutation, so two overlapping run() calls (double click,\n * overlapping scheduler ticks) can never both pass — exactly one session\n * is opened per task.\n * @param taskId - the task to run.\n * @param trigger - what started it.\n * @param options - per-run options (`reuseWorktree` = 续跑).\n * @returns the immediate result; settlement lands in the ledger.\n */\n async run(taskId: string, trigger: ExecutionRecord['trigger'], options?: RunOptions): Promise<RunRequestResult> {\n const max = this.deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT\n if (this.runs.size >= max) {\n return { ok: false, error: `execution concurrency limit reached (${this.runs.size}/${max} running)` }\n }\n const task = this.deps.store.get(taskId)\n if (task === undefined || task.trashedAt !== undefined) {\n return { ok: false, error: `no task ${taskId}` }\n }\n const workspace = this.deps.workspaces.get(task.workspaceId)\n if (workspace === undefined) {\n return { ok: false, error: `unknown workspace ${task.workspaceId}` }\n }\n\n const executionId = newExecutionId()\n const sessionId = this.deps.mintSessionId?.() ?? `session-taskboard-${crypto.randomUUID()}`\n\n // 0. Resolve code isolation (plan §3.2): explicit 'none' → zero git calls;\n // 'worktree' (also the omitted default) → prepare below, degrading to\n // the original directory fail-soft on any git problem.\n const isolation: IsolationMode = effectiveIsolation(task)\n const branch = task.branch ?? sanitizeBranchName(task.title, task.id)\n const worktreePath = worktreePathOf(workspace.path, task.id)\n\n // 1. Open the execution record, flip the card to in_progress, and record\n // the executing session as the claim holder — atomically.\n let gate: string | undefined\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target === undefined || target.trashedAt !== undefined) {\n gate = `no task ${taskId}`\n return undefined\n }\n if (target.status === 'in_progress') {\n gate = 'task is already in progress'\n return undefined\n }\n // S4: authoritative capacity check INSIDE the gate — counts ledger-wide\n // running executions, immune to the startup window (`runs` registers\n // only after agent creation, seconds later).\n const running = ledger.tasks.reduce((n, t) => n + t.executions.filter(e => e.outcome === 'running').length, 0)\n if (running >= max) {\n gate = `execution concurrency limit reached (${running}/${max} running)`\n return undefined\n }\n target.executions.push({\n id: executionId,\n trigger,\n startedAt: this.deps.now(),\n outcome: 'running',\n ...(isolation === 'none' ? { isolation: 'none' as const } : { isolation: 'worktree' as const, branch }),\n })\n target.status = 'in_progress'\n target.updatedAt = this.deps.now()\n target.updatedBy = { kind: 'system' }\n target.claimedBy = sessionId\n target.claimedAt = this.deps.now()\n return [target]\n })\n if (gate !== undefined) return { ok: false, error: gate }\n\n // 1b. Worktree preparation (fail-soft): any failure degrades this run to\n // the original directory with an isolationNote — the ledger and the\n // execution pipeline itself never fail over git.\n let isolationNote: string | undefined\n let prepared: PreparedWorktree | undefined\n if (isolation === 'worktree') {\n const outcome = await this.prepareIsolation(task, workspace.path, worktreePath, branch, options?.reuseWorktree === true)\n if (outcome.prepared !== undefined) {\n prepared = outcome.prepared\n // Persist the isolation facts of the run (branch is already on the\n // record from the gate mutation).\n await this.patchExecution(executionId, {\n worktreePath: outcome.prepared.worktreePath,\n baseCommit: outcome.prepared.baseCommit,\n })\n } else {\n isolationNote = outcome.note\n // Degraded run: clear the optimistic worktree markers.\n await this.patchExecution(executionId, { isolation: 'none', isolationNote, branch: undefined, worktreePath: undefined, baseCommit: undefined })\n }\n }\n\n // 2. Create the fresh agent+session inside the task's project, carrying\n // the pinned model — or the deployment default when unpinned (the\n // persona template renders {{model}}, so the session always needs one).\n // The session cwd is ALWAYS the project root: DSH's session model\n // requires cwd === the workspace path EXACTLY (attachSession validates\n // it, the sidebar groups by it, and the file sandbox takes it as the\n // workspace-write boundary) — a subdirectory cwd (the worktree) breaks\n // all three. The worktree is instead handed to the agent explicitly in\n // the framing line below.\n // Preset composition (0.3.3): resolve BEFORE creation so the header\n // snapshots `agentPreset` and the setup callback mounts the preset's\n // tools/persona into the agent's scope. undefined composeAgent (or an\n // absent preset roster) keeps the bare host composition.\n let composition: AgentComposition | undefined\n try {\n composition = this.deps.composeAgent === undefined ? undefined : await this.deps.composeAgent(task.presetId)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await this.patchExecution(executionId, { outcome: 'failed', error: `preset 组合失败:${message.slice(0, 400)}`, endedAt: this.deps.now() })\n await this.revertProgress(taskId)\n // S1: a run that never started must not leave its worktree behind.\n if (prepared !== undefined && this.deps.git !== undefined) {\n try { await this.deps.git.removeWorktree(workspace.path, prepared.worktreePath) } catch { /* best effort (dirty worktrees are kept) */ }\n }\n return { ok: false, error: `preset composition failed: ${message}` }\n }\n let handle: Awaited<ReturnType<AgentsFace['create']>>\n try {\n const model = task.model ?? this.deps.defaultModel?.()\n handle = await this.deps.agents.create({\n sessionId,\n meta: {\n cwd: workspace.path,\n ...(composition !== undefined ? { agentPreset: composition.agentPreset } : {}),\n },\n ...(model !== undefined ? { agentOptions: { provider: model.provider, model: model.model } } : {}),\n ...(composition !== undefined ? { setup: composition.setup } : {}),\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await this.patchExecution(executionId, { outcome: 'failed', error: message.slice(0, 500), endedAt: this.deps.now() })\n await this.revertProgress(taskId)\n // S1: a run that never started must not leave its worktree behind.\n if (prepared !== undefined && this.deps.git !== undefined) {\n try { await this.deps.git.removeWorktree(workspace.path, prepared.worktreePath) } catch { /* best effort (dirty worktrees are kept) */ }\n }\n return { ok: false, error: message }\n }\n\n // R3: the startup path above awaited seconds of git + agent work. A\n // cancel() that landed inside that window already settled the execution\n // (cancelled + task back to todo) — with nothing registered in `runs`,\n // it could not dispose the agent this path was about to create. Re-verify\n // INSIDE the queue (after any enqueued cancel committed) BEFORE injecting:\n // a cancelled card must not gain a zombie session that burns tokens and\n // edits files while the task sits in todo, re-runnable by anyone.\n const stillRunning = await this.deps.store.read(ledger =>\n ledger.tasks.some(t => t.executions.some(e => e.id === executionId && e.outcome === 'running')))\n if (!stillRunning) {\n await handle.dispose().catch(() => { /* best effort */ })\n // S1: do not leave the startup artifacts behind a cancelled run either.\n if (prepared !== undefined && this.deps.git !== undefined) {\n try { await this.deps.git.removeWorktree(workspace.path, prepared.worktreePath) } catch { /* best effort */ }\n }\n return { ok: false, error: 'cancelled during startup' }\n }\n\n // 3. Attach the session to the workspace (GUI project session list).\n await this.deps.workspaces.attach(task.workspaceId, sessionId).catch(() => { /* cosmetic */ })\n\n // 3b. Best-effort rename: pin the session title to the task title so the\n // session list shows the task name (a user-sourced title also stops\n // automatic first-prompt retitling).\n try {\n this.deps.renameSession?.(sessionId, task.title)\n } catch { /* cosmetic */ }\n\n // 4. Record the session id (execution is really started now).\n await this.patchExecution(executionId, { sessionId })\n\n // 5. Submit the opening pair and settle on quiescence (turn/end errors\n // were already folded by the listener). Two messages, ONE turn:\n // - inject() queues the plugin framing line (next-step, no wake); it\n // renders as a plugin context row in the conversation.\n // - followup() queues the card body as a normal user message\n // (next-turn, wakes the driver). At claim time the loop drains ALL\n // next-step messages plus the one next-turn message into a single\n // turn — framing first, then the user bubble.\n handle.agent.inject({\n id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),\n role: 'user' as const,\n content: [{ type: 'text' as const, text: this.pluginFraming(task, prepared, isolationNote) }],\n source: { kind: 'plugin' as const, plugin: 'dsh-taskboard' },\n })\n handle.agent.followup({\n id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),\n role: 'user' as const,\n content: [{ type: 'text' as const, text: this.userBody(task) }],\n source: { kind: 'user' as const },\n })\n\n // 6. Settlement watcher: mark succeeded, release the executing session's\n // hold, collect the worktree evidence (commits / dirty / diff), and —\n // when the session did NOT follow the handoff protocol — auto-move the\n // card to in_review with a system comment.\n const settle = (): void => {\n this.runs.delete(executionId)\n void this.settleExecution(executionId, sessionId, prepared)\n }\n this.runs.set(executionId, { sessionId, ...(prepared !== undefined ? { prepared } : {}), settle, dispose: () => handle.dispose() })\n // R2: the rejection path owns its state transition EXCLUSIVELY — the old\n // code also called settle() here, racing two evidence collections whose\n // mutations both checked outcome === 'running': whoever committed first\n // won, so a run that never reached quiescence could be recorded as\n // succeeded (and auto-moved to in_review). Now only the failure\n // settlement writes, and the run entry is released after it commits.\n void handle.agent.whenIdle().then(settle, () => {\n this.noteFailure(sessionId, 'agent did not reach quiescence')\n .then(() => { this.runs.delete(executionId) })\n .catch(() => { this.runs.delete(executionId) })\n })\n\n return { ok: true, executionId, sessionId }\n }\n\n /**\n * Settle one execution: collect worktree facts first (fail-soft — git\n * problems never block settlement), then commit outcome + release + the\n * protocol-auto-review move in ONE ledger mutation.\n */\n private async settleExecution(\n executionId: string,\n sessionId: string,\n prepared: PreparedWorktree | undefined,\n ): Promise<void> {\n const facts = await this.collectEvidence(prepared)\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const t of ledger.tasks) {\n const execution = t.executions.find(e => e.id === executionId)\n if (execution !== undefined && execution.outcome === 'running') {\n const now = this.deps.now()\n execution.outcome = 'succeeded'\n execution.endedAt = now\n this.applyFacts(execution, facts)\n if (t.status === 'in_progress' && t.claimedBy === sessionId) {\n delete t.claimedBy\n delete t.claimedAt\n }\n if (t.status === 'in_progress') {\n const commented = t.comments.some(c => c.threadId === sessionId)\n t.comments.push({\n id: newCommentId(),\n body: normalizeBody(commented\n ? '[系统] 执行会话已结束并留有评论,但未移至待验收;系统自动移入待验收。'\n : '[系统] 执行会话已结束,但未按协议交接(无评论、未移至待验收);系统自动移入待验收,请审查后退回或验收。'),\n version: 1,\n createdAt: now,\n })\n t.status = 'in_review'\n t.updatedAt = now\n t.updatedBy = { kind: 'system' }\n }\n return [t]\n }\n }\n return undefined\n })\n }\n\n /**\n * Prepare the dedicated worktree for a run (fail-soft): detect git, then\n * create/reset the fixed task branch — fresh baseline, or keep a live\n * worktree as-is for 续跑. On success the branch name is pinned onto the\n * task once (renames never change it); on any failure the run degrades\n * with a human-readable note.\n */\n private async prepareIsolation(\n task: TaskRecord,\n workspacePath: string,\n worktreePath: string,\n branch: string,\n reuse: boolean,\n ): Promise<{ prepared?: PreparedWorktree; note?: string }> {\n const git = this.deps.git\n if (git === undefined) return { note: 'git 集成不可用,已在原目录执行' }\n let inside = false\n try {\n inside = await git.detect(workspacePath)\n } catch { /* fail-soft */ }\n if (!inside) {\n // Distinguish 未装 git from 非 git 仓库 (0.3.1): probe the binary so the\n // degradation note names the real cause.\n let hasBinary = true\n try {\n hasBinary = await git.binaryAvailable()\n } catch { /* fail-soft → treat as repo-side */ }\n return { note: hasBinary ? '当前项目不是 git 仓库,已在原目录执行' : 'git 不可用(未安装或不在 PATH),已在原目录执行' }\n }\n let info\n try {\n info = await git.prepareWorktree(workspacePath, worktreePath, branch, reuse ? 'reuse' : 'fresh')\n } catch { /* fail-soft */ }\n if (info === undefined) return { note: 'worktree 准备失败(git 报错或目录被占用),已在原目录执行' }\n // Pin the branch name at first SUCCESSFUL creation (§9: 改名不改分支).\n if (task.branch === undefined) {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const target = ledger.tasks.find(t => t.id === task.id)\n if (target !== undefined && target.branch === undefined) {\n target.branch = branch\n return [target]\n }\n return undefined\n })\n }\n return {\n prepared: {\n branch: info.branch,\n worktreePath: info.path,\n baseCommit: info.baseCommit,\n ...(info.reused === true ? { reused: true } : {}),\n },\n }\n }\n\n /** How many executions are currently running (for the concurrency cap). */\n inFlight(): number {\n return this.runs.size\n }\n\n /**\n * Cancel the running execution of a task (user action): stop the agent\n * session, mark the execution cancelled, and hand the task back to todo.\n * @param taskId - the task whose execution should be stopped.\n * @returns the immediate result.\n */\n async cancel(taskId: string): Promise<CancelRequestResult> {\n const task = this.deps.store.get(taskId)\n if (task === undefined) return { ok: false, error: `no task ${taskId}` }\n const running = [...task.executions].reverse().find(e => e.outcome === 'running')\n if (running === undefined) return { ok: false, error: 'no running execution' }\n\n const entry = this.runs.get(running.id)\n this.runs.delete(running.id)\n // Stop the agent first (best effort): dispose stops the loop, unregisters\n // the agent, and removes its session. A late whenIdle settlement no-ops —\n // the record is no longer 'running'.\n try {\n await entry?.dispose()\n } catch { /* already gone */ }\n\n // The cancelled session may already have committed work — keep the\n // evidence (best effort) so the user can inspect or 续跑 (0.3.1).\n const facts = await this.collectEvidence(entry?.prepared)\n let settled = false\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target === undefined) return undefined\n const execution = target.executions.find(e => e.id === running.id)\n if (execution === undefined || execution.outcome !== 'running') return undefined\n settled = true\n execution.outcome = 'cancelled'\n execution.endedAt = this.deps.now()\n this.applyFacts(execution, facts)\n if (target.status === 'in_progress') {\n target.status = 'todo'\n target.updatedAt = this.deps.now()\n delete target.claimedBy\n delete target.claimedAt\n }\n return [target]\n })\n // The execution may have settled (succeeded/failed) between the stale\n // read above and this mutation — a no-op cancel must NOT report success\n // (the GUI used to show 取消成功 for an already-succeeded run, review P1).\n if (!settled) return { ok: false, error: 'execution already settled' }\n return { ok: true, executionId: running.id }\n }\n\n /**\n * Startup reconciliation after a host restart: executions left `running`\n * by the previous process can never settle here (their settlement watchers\n * died with it), so mark them failed and hand their tasks back to todo.\n */\n async reconcile(): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const now = this.deps.now()\n const touched: TaskRecord[] = []\n for (const task of ledger.tasks) {\n let dirty = false\n for (const execution of task.executions) {\n if (execution.outcome === 'running') {\n execution.outcome = 'failed'\n execution.error = 'interrupted by host restart'\n execution.endedAt = now\n dirty = true\n }\n }\n if (!dirty) continue\n if (task.status === 'in_progress') {\n task.status = 'todo'\n task.updatedAt = now\n delete task.claimedBy\n delete task.claimedAt\n }\n touched.push(task)\n }\n return touched.length > 0 ? touched : undefined\n })\n }\n\n /**\n * The plugin framing line (rendered as a plugin context row): task head,\n * already-claimed state, and the handoff protocol — everything the session\n * must know about the board. The task id appears exactly once (here); the\n * protocol steps below refer to it as 本任务. Isolated runs add one line\n * steering the session onto its dedicated branch (commits are the evidence\n * the user reviews at merge time); 续跑 and degraded runs each add their\n * own steering line (0.3.1).\n * @param task - the task.\n * @param prepared - worktree facts when this run is isolated.\n * @param degradeNote - why a worktree task degraded to the main directory.\n */\n private pluginFraming(task: TaskRecord, prepared?: PreparedWorktree, degradeNote?: string): string {\n let text = `【任务看板】${task.title}(ID: ${task.id})\\n`\n + `本会话由任务看板执行服务启动,任务已置为进行中——无需认领;「已完成」仅限用户在界面操作(代码已限制,移了会被拒)。\\n`\n + `完成后按序交接:\\n`\n + `1. taskboard_get 读取本任务,取得最新 version\\n`\n + `2. taskboard_execution_report 提交结构化执行报告(做了什么/改了哪些文件/如何验证/剩余风险;提交与评论不冲突,都会展示给验收人)\\n`\n + `3. taskboard_comment_add 留评论:做了什么改动 / 如何验证 / 剩余风险\\n`\n + `4. taskboard_move 将本任务移至待验收 in_review(带 ifVersion)\\n`\n + `若无法完成:留评论说明原因,将任务移回待办 todo。`\n if (task.checklist !== undefined && task.checklist.length > 0) {\n const items = task.checklist\n .map((item, index) => `${item.checked ? '☑' : '☐'} ${index + 1}. ${item.text}${item.note !== undefined ? `(证据: ${item.note})` : ''}`)\n .join('\\n')\n const done = task.checklist.filter(i => i.checked).length\n text += `\\n本任务有验收清单(DoD,${done}/${task.checklist.length} 已完成)——按清单干活:\\n${items}\\n完成一项就用 taskboard_checklist(action=check,附 note 证据)勾选;未完成项会在验收时高亮,全部完成再移待验收。需要补充验收项也可用 action=add 追加。`\n }\n if (prepared !== undefined) {\n if (prepared.reused === true) {\n text += `\\n本任务启用了 Git Worktree 隔离,且本次为续跑:任务工作目录是独立分支 ${prepared.branch} 的 worktree——\\n${prepared.worktreePath}\\n上一次执行的改动与提交都保留在原处——请先查看已有改动(git status / git log)再继续,避免重复劳动,并把新完成的工作提交到该分支。`\n } else {\n text += `\\n本任务启用了 Git Worktree 隔离:任务工作目录是独立分支 ${prepared.branch} 的全新 worktree——\\n${prepared.worktreePath}\\n(全新检出,不含 node_modules/构建产物,构建或测试前可能需要先安装依赖)。\\n⚠ 边界纪律:你的会话根目录是整个项目,但本任务的全部改动必须只发生在上述 worktree 目录内——命令用 workdir 指向它、文件读写用它的绝对路径;不要改动主工作区的任何其它文件;把完成的工作提交(git commit)到该分支,验收将基于该分支的提交记录合并。`\n }\n } else if (degradeNote !== undefined) {\n text += `\\n⚠ 本次执行未能建立隔离,正在主项目目录中工作(原因:${degradeNote})。该目录可能有他人未提交的改动:动手前先 git status 检查现状,改动尽量集中,结束时在评论中说明动了哪些文件;避免把未经验证的改动直接提交到主分支。`\n }\n return text\n }\n\n /**\n * The card body as a normal user bubble: the effective prompt (explicit\n * prompt, else title+description) with template variables resolved from\n * the task's own history at submit time (valuable for recurring patrols):\n * `{{lastExecution}}` → the previous execution's trigger/outcome/error;\n * `{{lastComments}}` → the last three comments (who + body).\n */\n private userBody(task: TaskRecord): string {\n const lastExec = [...task.executions].reverse().find(e => e.outcome !== 'running')\n const lastExecText = lastExec === undefined\n ? '(无)'\n : `${lastExec.trigger} · ${lastExec.outcome}${lastExec.error !== undefined ? ` · ${lastExec.error.slice(0, 200)}` : ''} · ${lastExec.startedAt !== undefined ? new Date(lastExec.startedAt).toISOString() : '?'}`\n const lastCommentsText = task.comments.slice(-3)\n .map(c => `[${c.threadId !== undefined ? 'agent' : 'user'}] ${c.body}`)\n .join('\\n') || '(无)'\n return effectivePrompt(task)\n .replace(/\\{\\{lastExecution\\}\\}/g, lastExecText)\n .replace(/\\{\\{lastComments\\}\\}/g, lastCommentsText)\n }\n\n /** Move a task back out of in_progress (and release its hold) after a failed start. */\n private async revertProgress(taskId: string): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target !== undefined && target.status === 'in_progress') {\n target.status = 'todo'\n target.updatedAt = this.deps.now()\n delete target.claimedBy\n delete target.claimedAt\n return [target]\n }\n return undefined\n })\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"execution.js","names":[],"sources":["../../src/host/execution.ts"],"sourcesContent":["/**\n * Host execution service: runs a task through dsh's REAL session machinery —\n * a fresh agent+session inside the task's project workspace (creation carries\n * the pinned model when the task has one), the session is attached to the\n * workspace so it appears in the GUI's project session list, the effective\n * prompt is submitted as an ordinary user message, and the turn settlement\n * (turn/end reason) is folded back into the task's execution record.\n *\n * Every execution is a NEW session: clean context, no reuse of previous runs.\n *\n * @module dsh-taskboard/host/execution\n */\nimport {\n effectiveIsolation,\n effectivePrompt,\n newCommentId,\n newExecutionId,\n normalizeBody,\n type ExecutionRecord,\n type IsolationMode,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport { sanitizeBranchName, worktreePathOf, type GitFace, type SettlementFacts } from './git.ts'\nimport { MessageId } from './sdk.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Default cap on concurrently running executions (env-overridable). */\nexport const DEFAULT_MAX_CONCURRENT = 3\n\n/** Narrow agents face (the registry's create, structurally). */\nexport interface AgentsFace {\n create(options: {\n sessionId: string\n meta?: { cwd?: string; agentPreset?: string }\n agentOptions?: { provider?: string; model?: string }\n /** Preset composition callback: mounts tools/persona into the agent's scoped context. */\n setup?: (agentCtx: unknown) => Promise<void> | void\n }): Promise<{\n agent: {\n id: string\n followup(message: unknown): void\n inject(message: unknown): void\n whenIdle(): Promise<void>\n }\n dispose(): Promise<void>\n }>\n}\n\n/**\n * The preset composition an execution session is built from — the shape\n * apiproxy's ensureSession produces: resolve → record on the session header,\n * mount → inside agents.create's setup callback.\n */\nexport interface AgentComposition {\n /** The resolved preset id recorded on the session header. */\n agentPreset: string\n /** Mounts the preset's plugins (tools, persona) into the agent's scope. */\n setup: (agentCtx: unknown) => Promise<void> | void\n}\n\n/** Narrow workspaces face for execution. */\nexport interface ExecutionWorkspaceFace {\n get(id: string): { id: string; path: string } | undefined\n attach(workspaceId: string, sessionId: string): Promise<void>\n}\n\n/** Narrow event-bus face for settlement listening. */\nexport interface EventsFace {\n onSessionEvent(listener: (sessionId: string, event: { type: string; data?: unknown }) => void): () => void\n}\n\n/** Everything the execution service needs. */\nexport interface ExecutionDeps {\n store: TaskStore\n agents: AgentsFace\n workspaces: ExecutionWorkspaceFace\n events: EventsFace\n now: () => number\n /** The deployment default model (fills sessions of unpinned tasks). */\n defaultModel?: () => { provider: string; model: string } | undefined\n /** Mint session ids (injectable for tests). */\n mintSessionId?: () => string\n /** Mint message ids (injectable for tests). */\n mintMessageId?: () => string\n /** Best-effort session rename (pins the session list title to the task title). */\n renameSession?: (sessionId: string, title: string) => void\n /** Max concurrently running executions across all tasks (default 3). */\n maxConcurrent?: number\n /**\n * Git face for worktree isolation (0.3.0). Absent → every worktree-mode\n * task degrades to the original directory with an isolationNote.\n */\n git?: GitFace\n /**\n * Resolve the preset composition for an execution session (0.3.3): hands\n * the session its tool set. Absent → sessions run on the bare host\n * composition (pre-preset behavior). A rejection fails the run through\n * the existing failure path — a broken preset never yields a half-composed\n * session (same rollback semantics as apiproxy).\n */\n composeAgent?: (presetId?: string) => Promise<AgentComposition | undefined>\n}\n\n/** Outcome of a run request (immediate; the run settles asynchronously). */\nexport type RunRequestResult =\n | { ok: true; executionId: string; sessionId: string }\n | { ok: false; error: string }\n\n/** Outcome of a cancel request. */\nexport type CancelRequestResult =\n | { ok: true; executionId: string }\n | { ok: false; error: string }\n\n/** Whether a turn/end payload closed with an error reason. */\nfunction isErrorTurnEnd(data: unknown): { message: string } | undefined {\n if (typeof data !== 'object' || data === null) return undefined\n const reason = (data as { reason?: unknown }).reason\n if (typeof reason !== 'object' || reason === null) return undefined\n const kind = (reason as { kind?: unknown }).kind\n if (kind !== 'error') return undefined\n const error = (reason as { error?: { message?: unknown } }).error\n const message = typeof error?.message === 'string' ? error.message : 'turn failed'\n console.error('[dsh-taskboard] turn error detail:', JSON.stringify(error)?.slice(0, 2000) ?? '')\n return { message }\n}\n\n/** Prepared worktree facts threaded through a live run (settlement evidence). */\nexport interface PreparedWorktree {\n branch: string\n worktreePath: string\n baseCommit: string\n /** True when an existing live worktree was kept as-is (续跑). */\n reused?: boolean\n}\n\n/** Per-run options. */\nexport interface RunOptions {\n /**\n * 续跑: keep a live worktree/branch exactly as-is (the previous agent's\n * commits and uncommitted changes survive) instead of resetting to the\n * main HEAD. Falls back to a fresh preparation when none is alive.\n */\n reuseWorktree?: boolean\n}\n\n/** One live execution tracked for settlement and cancellation. */\ninterface RunEntry {\n sessionId: string\n /** Worktree prepared for this run (evidence collection at ANY settlement). */\n prepared?: PreparedWorktree\n settle: () => void\n dispose: () => Promise<void>\n}\n\n/**\n * The execution service.\n */\nexport class ExecutionService {\n /** Live executions by execution id (settles and cancels remove entries). */\n private readonly runs = new Map<string, RunEntry>()\n\n /** Detaches the turn/end listener (plugin teardown — review P1). */\n private readonly unsubscribeEvents: () => void\n\n /** @param deps - store + agents + workspaces + events + clock. */\n constructor(private readonly deps: ExecutionDeps) {\n this.unsubscribeEvents = deps.events.onSessionEvent((sessionId, event) => {\n if (event.type !== 'turn/end') return\n // S7 (open question): ANY turn/end with an error reason fails the whole\n // execution and hands the task back. Whether the DSH session loop can\n // produce recoverable per-turn errors (and keep the session alive) needs\n // host-side confirmation; if it can, this should count consecutive\n // errors or wait for an explicit termination signal instead.\n const failure = isErrorTurnEnd(event.data)\n if (failure !== undefined) {\n this.noteFailure(sessionId, failure.message).catch(error => {\n console.error('[dsh-taskboard] failure settlement error:', error)\n })\n }\n })\n }\n\n /** Detach the settlement listener; safe to call once at plugin teardown. */\n dispose(): void {\n this.unsubscribeEvents()\n }\n\n /**\n * Best-effort evidence collection for a prepared run (fail-soft: undefined\n * on any git problem — settlement NEVER blocks on git).\n */\n private async collectEvidence(prepared: PreparedWorktree | undefined): Promise<SettlementFacts | undefined> {\n if (prepared === undefined || this.deps.git === undefined) return undefined\n try {\n return await this.deps.git.collect(prepared.worktreePath, prepared.baseCommit)\n } catch {\n return undefined\n }\n }\n\n /** Copy collected facts onto an execution record (in place). */\n private applyFacts(execution: ExecutionRecord, facts: SettlementFacts | undefined): void {\n if (facts === undefined) return\n if (facts.headCommit !== undefined) execution.headCommit = facts.headCommit\n execution.commits = facts.commits\n execution.commitsTotal = facts.commitsTotal\n execution.dirtyFiles = facts.dirtyFiles\n execution.dirtyFilesTotal = facts.dirtyFilesTotal\n execution.changedFiles = facts.changedFiles\n if (facts.diffStat !== undefined) execution.diffStat = facts.diffStat\n }\n\n /**\n * Record a turn failure against the running execution of that session and\n * give the task back. Resolves once the failure settlement has COMMITTED —\n * R2: the whenIdle rejection path awaits this (and only this) before\n * releasing its run entry, so a success settlement can never race it into\n * the ledger and record a failed run as succeeded.\n */\n private noteFailure(sessionId: string, message: string): Promise<void> {\n // The failed session may already have committed work — collect the\n // evidence (best effort) BEFORE marking the execution failed (0.3.1).\n const entry = [...this.runs.values()].find(e => e.sessionId === sessionId)\n return this.collectEvidence(entry?.prepared).then(facts =>\n this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n for (const execution of task.executions) {\n if (execution.sessionId === sessionId && execution.outcome === 'running') {\n execution.outcome = 'failed'\n execution.error = message.slice(0, 500)\n execution.endedAt = this.deps.now()\n this.applyFacts(execution, facts)\n // The failed session will not finish the work: hand the task back\n // instead of leaving it stuck in in_progress forever — and leave a\n // system comment so the GUI shows why.\n if (task.status === 'in_progress' && task.claimedBy === sessionId) {\n task.status = 'todo'\n task.updatedAt = this.deps.now()\n delete task.claimedBy\n delete task.claimedAt\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody(`[系统] 执行失败:${message.slice(0, 300)};任务已退回待办。`),\n version: 1,\n createdAt: this.deps.now(),\n })\n }\n return [task]\n }\n }\n }\n return undefined\n }),\n ).then(() => { /* failure settlement committed */ })\n }\n\n /**\n * Patch one task's execution record in the ledger. R3 depth: a record that\n * already settled (cancelled/failed/succeeded) is never resurrected — the\n * startup path patches sessionId long after the gate opened, and a cancel\n * may have committed in between.\n */\n private async patchExecution(executionId: string, patch: Partial<ExecutionRecord>): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n const execution = task.executions.find(e => e.id === executionId)\n if (execution !== undefined) {\n if (execution.outcome !== 'running') return undefined\n Object.assign(execution, patch)\n return [task]\n }\n }\n return undefined\n })\n }\n\n /**\n * Run one task now (manual button or scheduler tick).\n *\n * The in-progress gate and the execution-open write happen inside ONE\n * serial-queue mutation, so two overlapping run() calls (double click,\n * overlapping scheduler ticks) can never both pass — exactly one session\n * is opened per task.\n * @param taskId - the task to run.\n * @param trigger - what started it.\n * @param options - per-run options (`reuseWorktree` = 续跑).\n * @returns the immediate result; settlement lands in the ledger.\n */\n async run(taskId: string, trigger: ExecutionRecord['trigger'], options?: RunOptions): Promise<RunRequestResult> {\n const max = this.deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT\n if (this.runs.size >= max) {\n return { ok: false, error: `execution concurrency limit reached (${this.runs.size}/${max} running)` }\n }\n const task = this.deps.store.get(taskId)\n if (task === undefined || task.trashedAt !== undefined) {\n return { ok: false, error: `no task ${taskId}` }\n }\n const workspace = this.deps.workspaces.get(task.workspaceId)\n if (workspace === undefined) {\n return { ok: false, error: `unknown workspace ${task.workspaceId}` }\n }\n\n const executionId = newExecutionId()\n const sessionId = this.deps.mintSessionId?.() ?? `session-taskboard-${crypto.randomUUID()}`\n\n // 0. Resolve code isolation (plan §3.2): explicit 'none' → zero git calls;\n // 'worktree' (also the omitted default) → prepare below, degrading to\n // the original directory fail-soft on any git problem.\n const isolation: IsolationMode = effectiveIsolation(task)\n const branch = task.branch ?? sanitizeBranchName(task.title, task.id)\n const worktreePath = worktreePathOf(workspace.path, task.id)\n\n // 1. Open the execution record, flip the card to in_progress, and record\n // the executing session as the claim holder — atomically.\n let gate: string | undefined\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target === undefined || target.trashedAt !== undefined) {\n gate = `no task ${taskId}`\n return undefined\n }\n if (target.status === 'in_progress') {\n gate = 'task is already in progress'\n return undefined\n }\n // S4: authoritative capacity check INSIDE the gate — counts ledger-wide\n // running executions, immune to the startup window (`runs` registers\n // only after agent creation, seconds later).\n const running = ledger.tasks.reduce((n, t) => n + t.executions.filter(e => e.outcome === 'running').length, 0)\n if (running >= max) {\n gate = `execution concurrency limit reached (${running}/${max} running)`\n return undefined\n }\n target.executions.push({\n id: executionId,\n trigger,\n startedAt: this.deps.now(),\n outcome: 'running',\n ...(isolation === 'none' ? { isolation: 'none' as const } : { isolation: 'worktree' as const, branch }),\n })\n target.status = 'in_progress'\n target.updatedAt = this.deps.now()\n target.updatedBy = { kind: 'system' }\n target.claimedBy = sessionId\n target.claimedAt = this.deps.now()\n return [target]\n })\n if (gate !== undefined) return { ok: false, error: gate }\n\n // 1b. Worktree preparation (fail-soft): any failure degrades this run to\n // the original directory with an isolationNote — the ledger and the\n // execution pipeline itself never fail over git.\n let isolationNote: string | undefined\n let prepared: PreparedWorktree | undefined\n if (isolation === 'worktree') {\n const outcome = await this.prepareIsolation(task, workspace.path, worktreePath, branch, options?.reuseWorktree === true)\n if (outcome.prepared !== undefined) {\n prepared = outcome.prepared\n // Persist the isolation facts of the run (branch is already on the\n // record from the gate mutation).\n await this.patchExecution(executionId, {\n worktreePath: outcome.prepared.worktreePath,\n baseCommit: outcome.prepared.baseCommit,\n })\n } else {\n isolationNote = outcome.note\n // Degraded run: clear the optimistic worktree markers.\n await this.patchExecution(executionId, { isolation: 'none', isolationNote, branch: undefined, worktreePath: undefined, baseCommit: undefined })\n }\n }\n\n // 2. Create the fresh agent+session inside the task's project, carrying\n // the pinned model — or the deployment default when unpinned (the\n // persona template renders {{model}}, so the session always needs one).\n // The session cwd is ALWAYS the project root: DSH's session model\n // requires cwd === the workspace path EXACTLY (attachSession validates\n // it, the sidebar groups by it, and the file sandbox takes it as the\n // workspace-write boundary) — a subdirectory cwd (the worktree) breaks\n // all three. The worktree is instead handed to the agent explicitly in\n // the framing line below.\n // Preset composition (0.3.3): resolve BEFORE creation so the header\n // snapshots `agentPreset` and the setup callback mounts the preset's\n // tools/persona into the agent's scope. undefined composeAgent (or an\n // absent preset roster) keeps the bare host composition.\n let composition: AgentComposition | undefined\n try {\n composition = this.deps.composeAgent === undefined ? undefined : await this.deps.composeAgent(task.presetId)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await this.patchExecution(executionId, { outcome: 'failed', error: `preset 组合失败:${message.slice(0, 400)}`, endedAt: this.deps.now() })\n await this.revertProgress(taskId)\n // S1: a run that never started must not leave its worktree behind.\n if (prepared !== undefined && this.deps.git !== undefined) {\n try { await this.deps.git.removeWorktree(workspace.path, prepared.worktreePath) } catch { /* best effort (dirty worktrees are kept) */ }\n }\n return { ok: false, error: `preset composition failed: ${message}` }\n }\n let handle: Awaited<ReturnType<AgentsFace['create']>>\n try {\n const model = task.model ?? this.deps.defaultModel?.()\n handle = await this.deps.agents.create({\n sessionId,\n meta: {\n cwd: workspace.path,\n ...(composition !== undefined ? { agentPreset: composition.agentPreset } : {}),\n },\n ...(model !== undefined ? { agentOptions: { provider: model.provider, model: model.model } } : {}),\n ...(composition !== undefined ? { setup: composition.setup } : {}),\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await this.patchExecution(executionId, { outcome: 'failed', error: message.slice(0, 500), endedAt: this.deps.now() })\n await this.revertProgress(taskId)\n // S1: a run that never started must not leave its worktree behind.\n if (prepared !== undefined && this.deps.git !== undefined) {\n try { await this.deps.git.removeWorktree(workspace.path, prepared.worktreePath) } catch { /* best effort (dirty worktrees are kept) */ }\n }\n return { ok: false, error: message }\n }\n\n // R3: the startup path above awaited seconds of git + agent work. A\n // cancel() that landed inside that window already settled the execution\n // (cancelled + task back to todo) — with nothing registered in `runs`,\n // it could not dispose the agent this path was about to create. Re-verify\n // INSIDE the queue (after any enqueued cancel committed) BEFORE injecting:\n // a cancelled card must not gain a zombie session that burns tokens and\n // edits files while the task sits in todo, re-runnable by anyone.\n const stillRunning = await this.deps.store.read(ledger =>\n ledger.tasks.some(t => t.executions.some(e => e.id === executionId && e.outcome === 'running')))\n if (!stillRunning) {\n await handle.dispose().catch(() => { /* best effort */ })\n // S1: do not leave the startup artifacts behind a cancelled run either.\n if (prepared !== undefined && this.deps.git !== undefined) {\n try { await this.deps.git.removeWorktree(workspace.path, prepared.worktreePath) } catch { /* best effort */ }\n }\n return { ok: false, error: 'cancelled during startup' }\n }\n\n // 3. Attach the session to the workspace (GUI project session list).\n await this.deps.workspaces.attach(task.workspaceId, sessionId).catch(() => { /* cosmetic */ })\n\n // 3b. Best-effort rename: pin the session title to the task title so the\n // session list shows the task name (a user-sourced title also stops\n // automatic first-prompt retitling).\n try {\n this.deps.renameSession?.(sessionId, task.title)\n } catch { /* cosmetic */ }\n\n // 4. Record the session id (execution is really started now).\n await this.patchExecution(executionId, { sessionId })\n\n // 5. Submit the opening pair and settle on quiescence (turn/end errors\n // were already folded by the listener). Two messages, ONE turn:\n // - inject() queues the plugin framing line (next-step, no wake); it\n // renders as a plugin context row in the conversation.\n // - followup() queues the card body as a normal user message\n // (next-turn, wakes the driver). At claim time the loop drains ALL\n // next-step messages plus the one next-turn message into a single\n // turn — framing first, then the user bubble.\n handle.agent.inject({\n id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),\n role: 'user' as const,\n content: [{ type: 'text' as const, text: this.pluginFraming(task, prepared, isolationNote) }],\n source: { kind: 'plugin' as const, plugin: 'dsh-taskboard' },\n })\n handle.agent.followup({\n id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),\n role: 'user' as const,\n content: [{ type: 'text' as const, text: this.userBody(task) }],\n source: { kind: 'user' as const },\n })\n\n // 6. Settlement watcher: mark succeeded, release the executing session's\n // hold, collect the worktree evidence (commits / dirty / diff), and —\n // when the session did NOT follow the handoff protocol — auto-move the\n // card to in_review with a system comment.\n const settle = (): void => {\n this.runs.delete(executionId)\n void this.settleExecution(executionId, sessionId, prepared)\n }\n this.runs.set(executionId, { sessionId, ...(prepared !== undefined ? { prepared } : {}), settle, dispose: () => handle.dispose() })\n // R2: the rejection path owns its state transition EXCLUSIVELY — the old\n // code also called settle() here, racing two evidence collections whose\n // mutations both checked outcome === 'running': whoever committed first\n // won, so a run that never reached quiescence could be recorded as\n // succeeded (and auto-moved to in_review). Now only the failure\n // settlement writes, and the run entry is released after it commits.\n void handle.agent.whenIdle().then(settle, () => {\n this.noteFailure(sessionId, 'agent did not reach quiescence')\n .then(() => { this.runs.delete(executionId) })\n .catch(() => { this.runs.delete(executionId) })\n })\n\n return { ok: true, executionId, sessionId }\n }\n\n /**\n * Settle one execution: collect worktree facts first (fail-soft — git\n * problems never block settlement), then commit outcome + release + the\n * protocol-auto-review move in ONE ledger mutation.\n */\n private async settleExecution(\n executionId: string,\n sessionId: string,\n prepared: PreparedWorktree | undefined,\n ): Promise<void> {\n const facts = await this.collectEvidence(prepared)\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const t of ledger.tasks) {\n const execution = t.executions.find(e => e.id === executionId)\n if (execution !== undefined && execution.outcome === 'running') {\n const now = this.deps.now()\n execution.outcome = 'succeeded'\n execution.endedAt = now\n this.applyFacts(execution, facts)\n if (t.status === 'in_progress' && t.claimedBy === sessionId) {\n delete t.claimedBy\n delete t.claimedAt\n }\n if (t.status === 'in_progress') {\n const commented = t.comments.some(c => c.threadId === sessionId)\n t.comments.push({\n id: newCommentId(),\n body: normalizeBody(commented\n ? '[系统] 执行会话已结束并留有评论,但未移至待验收;系统自动移入待验收。'\n : '[系统] 执行会话已结束,但未按协议交接(无评论、未移至待验收);系统自动移入待验收,请审查后退回或验收。'),\n version: 1,\n createdAt: now,\n })\n t.status = 'in_review'\n t.updatedAt = now\n t.updatedBy = { kind: 'system' }\n }\n return [t]\n }\n }\n return undefined\n })\n }\n\n /**\n * Prepare the dedicated worktree for a run (fail-soft): detect git, then\n * create/reset the fixed task branch — fresh baseline, or keep a live\n * worktree as-is for 续跑. On success the branch name is pinned onto the\n * task once (renames never change it); on any failure the run degrades\n * with a human-readable note.\n */\n private async prepareIsolation(\n task: TaskRecord,\n workspacePath: string,\n worktreePath: string,\n branch: string,\n reuse: boolean,\n ): Promise<{ prepared?: PreparedWorktree; note?: string }> {\n const git = this.deps.git\n if (git === undefined) return { note: 'git 集成不可用,已在原目录执行' }\n let inside = false\n try {\n inside = await git.detect(workspacePath)\n } catch { /* fail-soft */ }\n if (!inside) {\n // Distinguish 未装 git from 非 git 仓库 (0.3.1): probe the binary so the\n // degradation note names the real cause.\n let hasBinary = true\n try {\n hasBinary = await git.binaryAvailable()\n } catch { /* fail-soft → treat as repo-side */ }\n return { note: hasBinary ? '当前项目不是 git 仓库,已在原目录执行' : 'git 不可用(未安装或不在 PATH),已在原目录执行' }\n }\n let info\n try {\n info = await git.prepareWorktree(workspacePath, worktreePath, branch, reuse ? 'reuse' : 'fresh')\n } catch { /* fail-soft */ }\n if (info === undefined) return { note: 'worktree 准备失败(git 报错或目录被占用),已在原目录执行' }\n // Pin the branch name at first SUCCESSFUL creation (§9: 改名不改分支).\n if (task.branch === undefined) {\n await this.deps.store.mutate('task-updated', (ledger) => {\n const target = ledger.tasks.find(t => t.id === task.id)\n if (target !== undefined && target.branch === undefined) {\n target.branch = branch\n return [target]\n }\n return undefined\n })\n }\n return {\n prepared: {\n branch: info.branch,\n worktreePath: info.path,\n baseCommit: info.baseCommit,\n ...(info.reused === true ? { reused: true } : {}),\n },\n }\n }\n\n /** How many executions are currently running (for the concurrency cap). */\n inFlight(): number {\n return this.runs.size\n }\n\n /**\n * Cancel the running execution of a task (user action): stop the agent\n * session, mark the execution cancelled, and hand the task back to todo.\n * @param taskId - the task whose execution should be stopped.\n * @returns the immediate result.\n */\n async cancel(taskId: string): Promise<CancelRequestResult> {\n const task = this.deps.store.get(taskId)\n if (task === undefined) return { ok: false, error: `no task ${taskId}` }\n const running = [...task.executions].reverse().find(e => e.outcome === 'running')\n if (running === undefined) return { ok: false, error: 'no running execution' }\n\n const entry = this.runs.get(running.id)\n this.runs.delete(running.id)\n // Stop the agent first (best effort): dispose stops the loop, unregisters\n // the agent, and removes its session. A late whenIdle settlement no-ops —\n // the record is no longer 'running'.\n try {\n await entry?.dispose()\n } catch { /* already gone */ }\n\n // The cancelled session may already have committed work — keep the\n // evidence (best effort) so the user can inspect or 续跑 (0.3.1).\n const facts = await this.collectEvidence(entry?.prepared)\n let settled = false\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target === undefined) return undefined\n const execution = target.executions.find(e => e.id === running.id)\n if (execution === undefined || execution.outcome !== 'running') return undefined\n settled = true\n execution.outcome = 'cancelled'\n execution.endedAt = this.deps.now()\n this.applyFacts(execution, facts)\n if (target.status === 'in_progress') {\n target.status = 'todo'\n target.updatedAt = this.deps.now()\n delete target.claimedBy\n delete target.claimedAt\n }\n return [target]\n })\n // The execution may have settled (succeeded/failed) between the stale\n // read above and this mutation — a no-op cancel must NOT report success\n // (the GUI used to show 取消成功 for an already-succeeded run, review P1).\n if (!settled) return { ok: false, error: 'execution already settled' }\n return { ok: true, executionId: running.id }\n }\n\n /**\n * Startup reconciliation after a host restart: executions left `running`\n * by the previous process can never settle here (their settlement watchers\n * died with it), so mark them failed and hand their tasks back to todo.\n */\n async reconcile(): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const now = this.deps.now()\n const touched: TaskRecord[] = []\n for (const task of ledger.tasks) {\n let dirty = false\n for (const execution of task.executions) {\n if (execution.outcome === 'running') {\n execution.outcome = 'failed'\n execution.error = 'interrupted by host restart'\n execution.endedAt = now\n dirty = true\n }\n }\n if (!dirty) continue\n if (task.status === 'in_progress') {\n task.status = 'todo'\n task.updatedAt = now\n delete task.claimedBy\n delete task.claimedAt\n }\n touched.push(task)\n }\n return touched.length > 0 ? touched : undefined\n })\n }\n\n /**\n * The plugin framing line (rendered as a plugin context row): task head,\n * already-claimed state, and the handoff protocol — everything the session\n * must know about the board. The task id appears exactly once (here); the\n * protocol steps below refer to it as 本任务. Isolated runs add one line\n * steering the session onto its dedicated branch (commits are the evidence\n * the user reviews at merge time); 续跑 and degraded runs each add their\n * own steering line (0.3.1).\n * @param task - the task.\n * @param prepared - worktree facts when this run is isolated.\n * @param degradeNote - why a worktree task degraded to the main directory.\n */\n private pluginFraming(task: TaskRecord, prepared?: PreparedWorktree, degradeNote?: string): string {\n let text = `【任务看板】${task.title}(ID: ${task.id})\\n`\n + `本会话由任务看板执行服务启动,任务已置为进行中——无需认领;「已完成」仅限用户在界面操作(代码已限制,移了会被拒)。\\n`\n + `完成后按序交接:\\n`\n + `1. taskboard_get 读取本任务,取得最新 version\\n`\n + `2. taskboard_execution_report 提交结构化执行报告(做了什么/改了哪些文件/如何验证/剩余风险;提交与评论不冲突,都会展示给验收人)\\n`\n + `3. taskboard_comment_add 留评论:做了什么改动 / 如何验证 / 剩余风险\\n`\n + `4. taskboard_move 将本任务移至待验收 in_review(带 ifVersion)\\n`\n + `若无法完成:留评论说明原因,将任务移回待办 todo。`\n if (task.checklist !== undefined && task.checklist.length > 0) {\n const items = task.checklist\n .map((item, index) => `${item.checked ? '☑' : '☐'} ${index + 1}. ${item.text}${item.note !== undefined ? `(证据: ${item.note})` : ''}`)\n .join('\\n')\n const done = task.checklist.filter(i => i.checked).length\n text += `\\n本任务有验收清单(DoD,${done}/${task.checklist.length} 已完成)——按清单干活:\\n${items}\\n完成一项就用 taskboard_checklist(action=check,附 note 证据)勾选;未完成项会在验收时高亮,全部完成再移待验收。需要补充验收项也可用 action=add 追加。`\n }\n if (prepared !== undefined) {\n if (prepared.reused === true) {\n text += `\\n本任务启用了 Git Worktree 隔离,且本次为续跑:任务工作目录是独立分支 ${prepared.branch} 的 worktree——\\n${prepared.worktreePath}\\n上一次执行的改动与提交都保留在原处——请先查看已有改动(git status / git log)再继续,避免重复劳动,并把新完成的工作提交到该分支。`\n } else {\n text += `\\n本任务启用了 Git Worktree 隔离:任务工作目录是独立分支 ${prepared.branch} 的全新 worktree——\\n${prepared.worktreePath}\\n(全新检出,不含 node_modules/构建产物,构建或测试前可能需要先安装依赖)。\\n⚠ 边界纪律:你的会话根目录是整个项目,但本任务的全部改动必须只发生在上述 worktree 目录内——命令用 workdir 指向它、文件读写用它的绝对路径;不要改动主工作区的任何其它文件;把完成的工作提交(git commit)到该分支,验收将基于该分支的提交记录合并。`\n }\n } else if (degradeNote !== undefined) {\n text += `\\n⚠ 本次执行未能建立隔离,正在主项目目录中工作(原因:${degradeNote})。该目录可能有他人未提交的改动:动手前先 git status 检查现状,改动尽量集中,结束时在评论中说明动了哪些文件;避免把未经验证的改动直接提交到主分支。`\n }\n return text\n }\n\n /**\n * The card body as a normal user bubble: the effective prompt (title+\n * description, with the explicit prompt appended when set) with template\n * variables resolved from\n * the task's own history at submit time (valuable for recurring patrols):\n * `{{lastExecution}}` → the previous execution's trigger/outcome/error;\n * `{{lastComments}}` → the last three comments (who + body).\n */\n private userBody(task: TaskRecord): string {\n const lastExec = [...task.executions].reverse().find(e => e.outcome !== 'running')\n const lastExecText = lastExec === undefined\n ? '(无)'\n : `${lastExec.trigger} · ${lastExec.outcome}${lastExec.error !== undefined ? ` · ${lastExec.error.slice(0, 200)}` : ''} · ${lastExec.startedAt !== undefined ? new Date(lastExec.startedAt).toISOString() : '?'}`\n const lastCommentsText = task.comments.slice(-3)\n .map(c => `[${c.threadId !== undefined ? 'agent' : 'user'}] ${c.body}`)\n .join('\\n') || '(无)'\n return effectivePrompt(task)\n .replace(/\\{\\{lastExecution\\}\\}/g, lastExecText)\n .replace(/\\{\\{lastComments\\}\\}/g, lastCommentsText)\n }\n\n /** Move a task back out of in_progress (and release its hold) after a failed start. */\n private async revertProgress(taskId: string): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target !== undefined && target.status === 'in_progress') {\n target.status = 'todo'\n target.updatedAt = this.deps.now()\n delete target.claimedBy\n delete target.claimedAt\n return [target]\n }\n return undefined\n })\n }\n}\n"],"mappings":"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"}
|
package/lib/host/templates.js
CHANGED
|
@@ -12,13 +12,33 @@ import { readFile } from "node:fs/promises";
|
|
|
12
12
|
*/
|
|
13
13
|
/** The built-in templates seeded when the side file does not exist yet. */
|
|
14
14
|
const BUILTIN_TEMPLATES = [
|
|
15
|
+
{
|
|
16
|
+
id: "tpl-feature",
|
|
17
|
+
name: "新增功能",
|
|
18
|
+
task: {
|
|
19
|
+
title: "新增:",
|
|
20
|
+
prompt: [
|
|
21
|
+
"实现以上新功能并按序交接:",
|
|
22
|
+
"1. 明确需求边界与验收标准,列出实现要点",
|
|
23
|
+
"2. 实现功能(含类型定义与错误处理)",
|
|
24
|
+
"3. 补充测试(单测/回归)",
|
|
25
|
+
"4. 运行相关测试套件确认通过"
|
|
26
|
+
].join("\n"),
|
|
27
|
+
urgency: "normal",
|
|
28
|
+
checklist: [
|
|
29
|
+
"实现要点已明确(需求边界与验收标准)",
|
|
30
|
+
"功能已实现并补充测试",
|
|
31
|
+
"相关测试套件通过"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
15
35
|
{
|
|
16
36
|
id: "tpl-bugfix",
|
|
17
37
|
name: "Bug 修复",
|
|
18
38
|
task: {
|
|
19
39
|
title: "修复:",
|
|
20
40
|
prompt: [
|
|
21
|
-
"
|
|
41
|
+
"修复以上问题并按序交接:",
|
|
22
42
|
"1. 复现问题(写最小复现步骤或测试)",
|
|
23
43
|
"2. 定位根因,说明为什么会发生",
|
|
24
44
|
"3. 修复并补回归测试",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.js","names":[],"sources":["../../src/host/templates.ts"],"sourcesContent":["/**\n * Host-side task-template store (0.4.0): one JSON side file next to the\n * ledger, seeded with the built-in templates on first load, mutated through\n * the same atomic persist discipline as the ledger.\n *\n * Pure data, no Cordis deps — the routes layer owns it and tests drive it\n * directly against a temp dir.\n *\n * @module dsh-taskboard/host/templates\n */\nimport { readFile } from 'node:fs/promises'\nimport type { TaskTemplate } from '../shared/api.ts'\n\n/** The built-in templates seeded when the side file does not exist yet. */\nexport const BUILTIN_TEMPLATES: ReadonlyArray<{ id: string; name: string; task: TaskTemplate['task'] }> = [\n {\n id: 'tpl-bugfix',\n name: 'Bug 修复',\n task: {\n title: '修复:',\n prompt: [\n '
|
|
1
|
+
{"version":3,"file":"templates.js","names":[],"sources":["../../src/host/templates.ts"],"sourcesContent":["/**\n * Host-side task-template store (0.4.0): one JSON side file next to the\n * ledger, seeded with the built-in templates on first load, mutated through\n * the same atomic persist discipline as the ledger.\n *\n * Pure data, no Cordis deps — the routes layer owns it and tests drive it\n * directly against a temp dir.\n *\n * @module dsh-taskboard/host/templates\n */\nimport { readFile } from 'node:fs/promises'\nimport type { TaskTemplate } from '../shared/api.ts'\n\n/** The built-in templates seeded when the side file does not exist yet. */\nexport const BUILTIN_TEMPLATES: ReadonlyArray<{ id: string; name: string; task: TaskTemplate['task'] }> = [\n {\n id: 'tpl-feature',\n name: '新增功能',\n task: {\n title: '新增:',\n prompt: [\n '实现以上新功能并按序交接:',\n '1. 明确需求边界与验收标准,列出实现要点',\n '2. 实现功能(含类型定义与错误处理)',\n '3. 补充测试(单测/回归)',\n '4. 运行相关测试套件确认通过',\n ].join('\\n'),\n urgency: 'normal',\n checklist: ['实现要点已明确(需求边界与验收标准)', '功能已实现并补充测试', '相关测试套件通过'],\n },\n },\n {\n id: 'tpl-bugfix',\n name: 'Bug 修复',\n task: {\n title: '修复:',\n prompt: [\n '修复以上问题并按序交接:',\n '1. 复现问题(写最小复现步骤或测试)',\n '2. 定位根因,说明为什么会发生',\n '3. 修复并补回归测试',\n '4. 运行相关测试套件确认无回归',\n ].join('\\n'),\n urgency: 'urgent',\n checklist: ['已复现并定位根因', '修复已提交到任务分支', '回归测试通过'],\n },\n },\n {\n id: 'tpl-release',\n name: '发布检查',\n task: {\n title: '发布:',\n prompt: '执行发布流程:版本号更新、构建、测试、变更记录,完成后按序交接(不要实际推送/发布,等用户确认)。',\n urgency: 'normal',\n checklist: ['版本号已更新(package.json 与版本常量同步)', '构建通过', '全部测试通过', '变更记录已写'],\n },\n },\n {\n id: 'tpl-patrol',\n name: '例行巡检',\n task: {\n title: '巡检:',\n prompt: [\n '例行巡检:检查依赖更新、失败测试、明显代码问题与未处理的告警。',\n '发现的问题逐条列出(严重度/位置/建议),小问题直接修复,大问题只报告不动手。',\n '输出巡检摘要(用 {{lastComments}} 可回看上次巡检结论)。',\n ].join('\\n'),\n urgency: 'relaxed',\n execution: { mode: 'scheduled', cron: '0 9 * * 1' },\n },\n },\n]\n\n/** Mint a template id. */\nfunction newTemplateId(): string {\n return `tpl-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`\n}\n\n/**\n * The template store. NOT thread-synchronized like the ledger (template\n * writes are rare, human-paced GUI operations; last-write-wins is fine).\n */\nexport class TemplateStore {\n private templates: TaskTemplate[] | undefined\n private loaded = false\n\n /** @param file - absolute side-file path (next to the ledger). */\n constructor(private readonly file: string) {}\n\n /** Load once; a missing file seeds the built-ins; a corrupt file resets. */\n private async ensure(): Promise<void> {\n if (this.loaded) return\n let parsed: TaskTemplate[] | undefined\n try {\n const raw = await readFile(this.file, 'utf8')\n const value = JSON.parse(raw) as { templates?: unknown }\n if (Array.isArray(value.templates)) {\n parsed = value.templates.filter((t): t is TaskTemplate =>\n typeof t === 'object' && t !== null && typeof (t as TaskTemplate).id === 'string'\n && typeof (t as TaskTemplate).name === 'string' && typeof (t as TaskTemplate).task === 'object')\n }\n } catch { /* missing or corrupt → seed */ }\n if (parsed === undefined) {\n const now = Date.now()\n parsed = BUILTIN_TEMPLATES.map((t, i) => ({ ...t, task: { ...t.task }, builtin: true, createdAt: now, updatedAt: now + i }))\n try { await this.persist(parsed) } catch { /* best effort — the seed returns in-memory */ }\n }\n this.templates = parsed\n this.loaded = true\n }\n\n /** Atomic persist (temp + fsync + rename — S10, same discipline as the ledger). */\n private async persist(templates: TaskTemplate[]): Promise<void> {\n const { mkdir, open, rename } = await import('node:fs/promises')\n const { dirname, join } = await import('node:path')\n await mkdir(dirname(this.file), { recursive: true })\n const temp = join(dirname(this.file), `.${Math.random().toString(36).slice(2)}.tmp`)\n const fh = await open(temp, 'w')\n try {\n await fh.writeFile(JSON.stringify({ templates }, null, 2), 'utf8')\n await fh.sync()\n } finally {\n await fh.close()\n }\n await rename(temp, this.file)\n }\n\n /** All templates (oldest first). */\n async list(): Promise<TaskTemplate[]> {\n await this.ensure()\n return (this.templates ?? []).slice()\n }\n\n /**\n * Create or replace a template by id (a body without id creates).\n * @returns the stored template.\n */\n async upsert(input: { id?: string; name: string; task: TaskTemplate['task'] }): Promise<TaskTemplate> {\n await this.ensure()\n const templates = this.templates ?? []\n const name = input.name.trim()\n if (name.length === 0 || name.length > 60) throw new Error('模板名必须 1..60 字符')\n const now = Date.now()\n const existing = input.id !== undefined ? templates.find(t => t.id === input.id) : undefined\n // T12: built-ins are factory content — editable only by delete + recreate\n // (deleting stays allowed), never silently overwritten in place.\n if (existing?.builtin === true) throw new Error('内置模板不可覆盖;可删除后另建,或以新名称存为新模板')\n const stored: TaskTemplate = existing !== undefined\n ? { ...existing, name, task: input.task, updatedAt: now }\n : { id: input.id ?? newTemplateId(), name, task: input.task, createdAt: now, updatedAt: now }\n const index = existing !== undefined ? templates.indexOf(existing) : -1\n if (index >= 0) templates[index] = stored\n else templates.push(stored)\n await this.persist(templates)\n return stored\n }\n\n /** Delete a template by id; returns whether it existed. */\n async remove(id: string): Promise<boolean> {\n await this.ensure()\n const templates = this.templates ?? []\n const index = templates.findIndex(t => t.id === id)\n if (index < 0) return false\n templates.splice(index, 1)\n await this.persist(templates)\n return true\n }\n}\n"],"mappings":"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"}
|
package/lib/host/tools.js
CHANGED
|
@@ -290,7 +290,7 @@ function registerTaskboardTools(ctx, deps) {
|
|
|
290
290
|
},
|
|
291
291
|
prompt: {
|
|
292
292
|
type: "string",
|
|
293
|
-
description: "
|
|
293
|
+
description: "Extra execution instructions; the session receives title+description+this prompt."
|
|
294
294
|
},
|
|
295
295
|
status: {
|
|
296
296
|
type: "string",
|