dsh-taskboard 0.5.4 → 0.5.5

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
@@ -133,6 +133,7 @@ agent:
133
133
  - 新建/编辑弹窗:项目、模型(含思考强度)、紧急度、执行方式、cron 实时校验与下次运行预览、执行隔离开关、验收清单编辑
134
134
  - 详情面板:状态流转(done 仅限人工;清单未全勾时完成需二次确认并显示未勾数)、agent/用户评论流、执行记录(倒序,最新在最上;会话 ID 点击跳转打开该执行会话;已删除/已归档分开提示)、停止执行、Worktree 隔离块(分支 / 提交 / 改动统计 / 合并与清理)、执行报告块、验收清单块
135
135
  - 待验收列卡片快捷操作:「✓ 完成」一键验收、「✗ 退回」退回待办并可附退回原因(agent 开工前会读)
136
+ - **外部会话自动同步(0.5.5)**:看板设置开启「🔄 自动纳入会话」后,工作区直接新建的会话自动在看板生成任务卡片(按会话工作目录映射项目,取首条消息作标题/描述)——运行中进「进行中」并绑定会话(可一键跳转)、成功结算自动流转「待验收」、异常退回「待办」;自动过滤看板内部执行会话,多轮续跑延续同一张卡片;出厂默认关闭
136
137
  - **一键跳转执行会话(0.5.4)**:任务卡片新增「🤖 会话ID ↗」按钮、详情页顶部新增「🤖 跳转会话 ↗」按钮,持有者 Chip 同样可点——进行中优先、其次最近一次执行对应的会话一键直达(看板自动收起);已归档 / 已删除 / 会话服务不可用分别精准提示
137
138
  - **记住上次模型(0.5.4)**:新建任务自动带出上次选用的模型与思考强度(模板预填与编辑不受影响)
138
139
  - **验收清单 DoD(0.4.0)**:建卡时定验收条件(≤30 项);agent 用 `taskboard_checklist` 增补/勾选(附证据 note);用户在详情页直接勾选;待验收时未完成项红色高亮 + 卡片「☑ n/m」角标(未全勾显红);清单编辑在表单中整组管理(勾选状态与证据保留)
@@ -211,13 +212,17 @@ pnpm 的构建授权——按报错把 key 加进 profile 的 `pnpm-workspace.ya
211
212
  git clone https://github.com/cloader/dsh-taskboard.git
212
213
  cd dsh-taskboard
213
214
  npm install && npm run build # host ESM + client CJS 双构建
214
- npm test # vitest 全量(150 项)
215
+ npm test # vitest 全量(208 项)
215
216
  node tests/manual-git-e2e.mjs # 真 git 端到端手测(worktree 全链路 + 续跑 + diff 查看器)
216
217
  node scripts/screenshot.mjs # 重新生成 img/ 截图(需本机 Edge)
217
218
  ```
218
219
 
219
220
  ## 升级日志
220
221
 
222
+ ### 0.5.5
223
+
224
+ - **外部工作区会话自动同步看板: [@jw5555555555](https://github.com/jw5555555555)([#13](https://github.com/cloader/dsh-taskboard/pull/13))**:看板「设置」新增「自动同步工作区会话」开关(出厂默认关闭)——开启后,工作区直接新建的会话自动在看板生成任务卡片:按会话工作目录(cwd)映射到对应项目,取首条用户消息与会话标题作为任务的描述与标题;运行中自动进入「进行中」并绑定会话 ID(卡片可一键跳转),执行成功自动流转「待验收」并生成系统评论,异常退回「待办」;自动过滤看板自身创建的内部执行会话防止重复建卡;多轮续跑延续同一张卡片
225
+
221
226
  ### 0.5.4
222
227
 
223
228
 
@@ -226,164 +231,6 @@ node scripts/screenshot.mjs # 重新生成 img/ 截图(需本机 Edge)
226
231
  - **列内排序新增「按标题」: [@Amoss-1](https://github.com/Amoss-1)([#4](https://github.com/cloader/dsh-taskboard/pull/4))**:数字感知比较,数字前缀按真实数值排序(`01 < 02 < 10 < 90`,字符串比较会把 `10` 排到 `02` 前);排序选择随视图状态持久化
227
232
  - 界面细节:下拉 / 输入框适配明暗主题(DSH 主题变量 + color-scheme);模板管理弹窗布局优化
228
233
 
229
- ### 0.5.3
230
-
231
- - **执行 Prompt 改为追加式**:自定义 Prompt 不再整体替换开场指令——实际发给执行会话的是「标题+任务描述+Prompt」,写在描述里的背景不再被漏掉;新建/编辑表单文案与 `taskboard_create` 工具描述同步更新
232
- - **新增内置「新增功能」模板**:排在「Bug 修复」之前——标题前缀「新增:」,按「明确需求要点 → 实现 → 补测试 → 跑套件」四步走,自带三项验收清单
233
- - **「Bug 修复」模板首行改为「修复以上问题并按序交接」**:描述现在先于 Prompt 送达,「以上」才指得住
234
-
235
- ### 0.5.2
236
-
237
- - **支持 DSH Desktop 非兼容模式下侧栏没有「任务看板」入口、看板也打不开的问题([#6](https://github.com/cloader/dsh-taskboard/issues/6))**:
238
- - 支持 DSH Desktop V2.0.3 非兼容模式下侧栏没有「任务看板」入口、看板也打不开的问题
239
- - 侧栏入口换上新图标:更直观的三列看板样式
240
- - 新增非兼容模式的回归测试(共 199 项)
241
-
242
- ### 0.5.1
243
-
244
- 这一版主要来自一次全面的代码体检:看板更防手滑、更稳,行为上有几处小变化。
245
-
246
- - **详情页不再「串卡」**:以前在任务 A 点开「确认清除」后再点开任务 B,B 会直接停在待确认状态,手快就误删了。现在每次打开详情页都是干净状态
247
- - **按钮防连点**:「创建任务」「立即执行」「复制」这些按钮在提交过程中会变灰。以前手快双击会建出两张一样的卡,甚至同时启动两个执行会话白烧 token
248
- - **DSH 重启后马上能正常查板**:以前刚重启的几秒里 AI 查到的是一块空板,可能照流程重复建卡;现在不会了
249
- - **归档的任务真正封存**:以前归档后还能从部分入口继续编辑、评论;现在彻底只读
250
- - **新建任务只能落在 待规划 / 待办**:想开工请正常认领,不会再出现一张没有归属人的「进行中」
251
- - **取消已结束的任务会给明确提示**:以前对已经跑完的任务点「停止」显示取消成功,其实什么都没发生;现在会如实告诉你没取消成
252
- - **执行报告可以补交**:会话结束了才发现忘交报告?现在还能补上(限自己名下最近一次成功执行的任务)
253
- - **复制长标题任务不再报错**:标题太长会自动截断再加(副本)后缀
254
- - **编辑任务不再被偷偷改设置**:以前打开编辑器再保存,会把「跟随部署默认预设」悄悄固化成具体值;现在保持原样
255
- - 内部质量:定时调度出错只记日志不再惊动整个进程、一批测试与构建卫生改进
256
-
257
- ### 0.5.0
258
-
259
- - **新增「看板设置」**:看板顶栏多了个「🛠 设置」按钮,可以选择新建任务默认怎么执行——在独立分支里隔离着干,或者直接在项目文件夹里干。保存后,新建的任务都按这个来;已经建好的任务不受影响
260
- - **新建任务默认改为「直接在项目文件夹执行」**:以前新任务默认会在独立分支里执行,现在默认不开分支、直接在项目文件夹干活。习惯分支隔离的团队,到「🛠 设置」里改回来就行
261
- - **导出按钮合并成一个**:原来的「⬇ JSON」「⬇ CSV」两个按钮合并成一个「⬇ 导出」,点开再选要哪种格式
262
- - 新建任务时不再记住上一次的隔离选择,统一跟着「看板设置」走
263
-
264
- ### 0.4.5
265
-
266
- - **移除 0.4.4 引入的「每 2 秒自动检查样式」机制**:上一版给样式表打的归属标记已经从根上解决了误删问题——其他插件更新时不会再删看板的样式,周期性检查只是多余的保险。现在去掉这个常驻的后台定时器,页面少一份轮询;防掉样式的效果不变(归属标记仍在,本插件自己热更新后也会重新挂回样式)
267
- - 对使用者无任何可见变化,纯粹是内部做减法
268
-
269
- ### 0.4.4
270
-
271
- - **修复看板偶尔突然掉样式的问题([ #3](https://github.com/cloader/dsh-taskboard/issues/3))**。排查结论:浏览器里其他插件在后台热更新时,会按"样式归属"做清理,而看板自己的样式表没有归属标记,被误当成别的插件的样式连带删掉了。本次修复后:
272
- - 看板的样式表明确打上了自己的归属标记,其他插件更新时不会再误删它
273
- - 看板每 2 秒自动检查一次样式是否完好,即便被误删也会立刻自动恢复——以后遇到掉样式,最多闪一下就自动修好,不用再刷新页面
274
- - 新增回归测试:样式归属标记生效、误删后 2 秒内自动恢复、样式不会重复注入
275
-
276
- ### 0.4.3
277
-
278
- - **修复折叠侧边栏时入口不收成纯图标(用户反馈)**:收起侧边栏后,shell 把侧栏收成 36×36 图标轨道(layout frame 出现 `data-sidebar-collapsed` 属性、侧栏根节点切换 CSS-Module `*_collapsed` 哈希类),而看板入口仍保持全宽——图标、「任务看板」文字、右侧计数角标挤在窄轨里。修复:折叠信号下入口镜像原生轨道几何(36×36、居中、与原生 newSession 折叠态同款间距),隐藏文字与角标,图标 14→16px 保证可读;双信号选择器(`[data-sidebar-collapsed]` 与 `[class*="_collapsed"]`)沿用 0.4.2 双选择器原则,新旧 shell 通吃;展开态完全不受影响,`aria-label` 保留无障碍提示
279
- - 回归测试:断言两组折叠选择器与 36px 轨道几何都注入样式表、shell 真实产生的 `hHd-Xa_collapsed` 类名命中选择器模式(150 项)
280
- - README 按 dsh.market 五维评分体系重构:新增目录 / 环境要求 / 快速开始(含完整 agent 工作流示例)/ Agent 工具参考表 / 安全 / 配置与数据 / 常见问题;仓库元数据补 homepage 与 topics
281
-
282
- ### 0.4.2
283
-
284
- - **修复 DSH Desktop 上看板不出现(用户反馈:入口能点、后端正常,但中间看板无变化)**:当前 DSH Desktop 的 Web shell(`dsh-client-ui-layout`)已完全移除 `data-pane` 属性,中间列改用 CSS Module 哈希类名(`pI_x6G_centerCol`)——而看板挂载选择器写死 `[data-pane="conversation"]`,`querySelector` 永远落空,看板容器从未创建(侧栏入口正常,是因为它一直有 `[class*="sidebarCol"]` 兜底)。修复:挂载选择器与隐藏规则双兼容 `'[data-pane="conversation"], [class*="centerCol"]'`(新旧 shell 通吃,与侧栏入口同款兜底策略);`.dsh-atb-view` 以 `height:100%` 撑满列容器,不依赖列内部结构
285
- - 回归测试:构造无 `data-pane`、哈希类名 `centerCol` 的 Desktop shell DOM——看板容器正确创建于列内、隐藏规则双选择器齐备
286
-
287
- ### 0.4.1
288
-
289
- - **修复侧栏入口点击竞态(用户反馈)**:部分 shell 的 DOM 里,任务看板侧栏入口被插在 class 含 `newSession` 的容器内部——点击入口时,看板的全局捕获监听器(「点侧栏会话/新会话让位」语义)先 `closeBoard`,入口自身的 `toggleBoard` 再翻回来,表现为**看板一开就闪关、或入口按钮关不掉**。修复:捕获监听器先按 `closest('[data-dsh-atb-entry]')` 整体豁免入口子树(比给选择器加 `:not()` 更稳——`:not()` 只排除元素自身匹配,挡不住入口嵌在带类祖先容器内的形态);真会话行/新会话按钮的让位语义不变。顺带加固 `newSessionButton()` 锚点扫描排除自身入口,杜绝自愈重插时锚点自引用
290
- - 回归测试:构造「入口位于 newSession 容器内」的竞态形态 DOM——点击入口开/关 toggle 正常;点真新会话按钮仍让位
291
-
292
- ### 0.4.0
293
-
294
- **验收效率包——把「人验收」环节在看板内闭环:**
295
-
296
- - **验收清单 DoD**:任务模型新增 `checklist`(≤30 项 × 200 字,勾选人/时间/证据 note 全程留痕)。建卡表单整组编辑(编辑保留勾选状态与证据);agent 侧新工具 `taskboard_checklist`(add 增补 / check 勾选附证据 / uncheck 重开,代码闸:勾选永远不等于完成);执行开场框架行注入清单并下达「完成一项勾一项」纪律;待验收卡片与详情未完成项红色高亮,「✓ 完成」在有未勾项时二次确认并显示数量
297
- - **结构化执行报告**:执行记录新增 `report { summary, changedFiles, checks, artifacts, risk }`;agent 收尾用新工具 `taskboard_execution_report` 提交——按「调用会话 + 运行中执行」定位挂载(agent 无需知道执行 id,重复提交覆盖);待验收详情页分栏渲染(摘要 / 改动文件 / 自验 / 产物 / 风险);开场交接协议更新为「报告 → 评论 → 移待验收」三步
298
- - **JSON 导入**:`validateLedgerImport` 纯函数逐卡校验分类(新增 / 覆盖 / 无效+原因,文件内重复 id 拒绝;schemaVersion 不符明确报错;导入时残留 `running` 执行标记为失败——其结算守望已随源主机消亡);`POST /import/preview` 干跑 + `POST /import` 提交(merge 按 id upsert;replace 整册替换前自动写 `ledger.backup-<ts>.json`);GUI「⬆ 导入」弹窗全流程可视化,整册替换双重确认
299
- - **任务模板**:host 侧 `templates.json` side file(首启播种内置 Bug 修复 / 发布检查 / 例行巡检);「+ 新建任务 ▼」下拉选模板预填全部配置(含清单与 preset);任务详情「⌗ 存为模板」;管理弹窗改名 / 删除 / 直接使用
300
- - **Diff 查看器**:git face 新增 `showCommit` / `showPathDiff`(fail-soft,128KB / 2000 行封顶标注截断);详情页隔离块的提交行与未提交修改文件行点击即懒加载展开 diff;worktree 已删时自动回落主仓
301
- - 测试 116 → 145;`taskboard_get` 详情渲染清单与报告标记;复制任务携带清单
302
-
303
- ### 0.3.3
304
-
305
- - **修复执行会话拿不到工具(非标准模式)**:0.3.0 起部署引入 preset 体系后,工具不再是全局注册,而是各 preset 组合在会话创建时挂载(GUI 建会话走 apiproxy 的 composeAgent → presets.mount)。我们的执行服务此前直接 agents.create,不挂任何组合 → 会话无工具。现对齐该链路:执行会话按 preset 组合创建,`agentPreset` 记入会话头
306
- - **任务级 preset 字段**:创建任务时可选「执行模式(preset)」,默认预选部署默认 preset(本部署即标准模式)——即按你的决定「默认标准模式」;可切任意 preset(如梁神模式)或选「跟随部署默认」(不落字段,随部署默认变);编辑随时可改(每轮执行现组合,无锁定);复制任务携带;`taskboard_create` 增 `presetId` 参数;详情页显示 preset chip
307
- - **坏 preset 防护**:preset 解析失败(不存在/损坏)→ 执行直接失败、原因写入执行记录、任务退回待办——不产出半组合会话(与 apiproxy 回滚语义一致)
308
- - preset 目录用来自 `agentPreset.list` 连接 RPC(无 roster 或离线时下拉隐藏,行为回落到裸组合);测试 111 → 116 项
309
-
310
- ### 0.3.2
311
-
312
- - **修复 worktree 执行会话脱离项目组、工具不可用**:0.3.0–0.3.1 把执行会话 cwd 指向 worktree 子目录,而 DSH 的会话模型要求 cwd === 工作区根目录(全等)——子目录导致会话无法挂入项目组(显示「未分组」),且文件沙箱边界与相对路径解析全部失效(agent 无法创建文件)。现改回:会话 cwd = 项目根(分组、工具、沙箱完整可用),worktree 绝对路径与边界纪律(命令 workdir 指向 worktree、文件读写用绝对路径、禁改主工作区其它文件、提交到任务分支)在开场框架行中明确下达。git 隔离与证据采集不受影响
313
-
314
- ### 0.3.1
315
-
316
- - **↻ 续跑**:详情页新增「续跑」——保留现有 worktree 与分支原状(上次的提交和未提交修改都在),在其上继续执行;基线取 worktree 当前 HEAD,证据只统计本轮新增提交。默认「立即执行」仍为全新基线(重置分支到主区 HEAD)
317
- - **同仓库 git 操作互斥**:同一仓库的建/删 worktree、合并、删分支在进程内串行执行,消除并发隔离执行之间的 index.lock 竞争
318
- - **失败/取消也留证据**:turn 出错或用户停止时,已产生的提交与未提交修改照样采集进执行记录(原先只在成功结算时采集)——中断的半成品可凭记录判断是否续跑
319
- - **执行记录证据封顶**:提交最多保留 50 条、未提交修改最多 100 行(均记录总数),超大改动不再撑爆每次全量重写的台账
320
- - **合并空跑检测**:分支没有领先主工作区的新提交时直接提示「无需合并」,不再产生假的「已合并」系统评论
321
- - **物理清除连带清理**:purge 任务前先检查 worktree——有未提交修改拒绝并列出文件;通过后连带删除 worktree 与任务分支
322
- - **降级语义细化**:降级原因区分「git 不可用(未安装/不在 PATH)」与「当前项目不是 git 仓库」;降级运行的开场提示明确警告会话正在主项目目录工作(先 git status、改动集中、结束说明改动文件)
323
- - **全新检出提示**:全新 worktree 的开场提示告知不含 node_modules/构建产物,构建测试前可能需重装依赖
324
- - **诊断面板增强**:新增 gitignore 建议列表(git 项目未忽略 `.dsh-worktrees/` 一目了然,仍不自动修改);「悬挂执行中」更名为「执行中」
325
- - README 注明 Worktree 隔离是协作约定而非沙箱;测试 97 → 111 项
326
-
327
- ### 0.3.0
328
-
329
- - **Git Worktree 隔离执行(旗舰)**:并发执行互不污染代码,验收即审分支
330
- - 任务级隔离开关:新建表单「执行隔离」(默认 Worktree、记住上次选择;非 git 项目自动禁用并提示);`taskboard_create` 增 `isolation` 参数;复制任务携带;首次执行前可改、开始后锁定
331
- - 执行链路:每次全新 worktree(`git worktree add --force` 到固定路径 `.dsh-worktrees/<任务ID>`,固定分支 `task/<标题>+<任务ID>`,标题清洗+截断、空标题回退纯 ID、改名不改分支);执行会话 cwd 指向 worktree;开场框架行引导 agent 把改动提交到该分支
332
- - 结算采集:提交列表(hash+主题)、未提交修改警告、改动统计与文件数——「执行记录 ↔ commits 关联」并入本轮
333
- - 验收操作(详情页):⇥ 合并(`--no-ff`,主区脏/冲突原样报告且自动 `merge --abort` 复原)、🗑 删除 worktree(有未提交修改拒绝,可选连分支删除)、保留分支退回继续修改
334
- - 安全规则:显式关闭 = 全程零 git 调用;非 git 项目 / git 不可用 / 超时全部自动降级原目录(isolationNote 注明原因,fail-soft:台账与执行永不因 git 失败而失败);合并的干净检查豁免插件自有的 `.dsh-worktrees/` 目录;所有 git 操作带超时(查询 2s / 结构操作 15s)
335
- - host 新模块 `src/host/git.ts`:窄接口 GitFace(detect/prepare/collect/merge/remove/deleteBranch),exec 层可注入(测试零真 git);workspaces 路由下发 git 检测结果(60s 缓存);启动时对未忽略 `.dsh-worktrees/` 的项目提示 gitignore 建议(不自动改)
336
- - **⚙ 健康诊断**:台账基本项(修订号/任务数/悬挂执行中)+ 遗留 worktree 列表(台账无主但目录存在)一键清理(未注册目录 fs 兜底,有未提交修改仍拒绝)
337
- - `taskboard_get` 详情输出隔离与分支信息;测试 67 → 97 项
338
-
339
- ### 0.2.2
340
-
341
- - **待验收快捷操作**:待验收列卡片上直接「✓ 完成 / ✗ 退回」,退回可附原因(可留空),退回与评语一次原子提交(移卡失败不会产生孤儿评论)
342
- - 非法状态流转的 HTTP 状态码由 500 修正为 400
343
-
344
- ### 0.2.1
345
-
346
- - 调整任务发起时的提示词
347
-
348
- ### 0.2.0
349
-
350
- - **看板效率**:顶栏新增搜索(标题/ID,大小写不敏感)、列内排序(默认/最近更新/紧急度/创建时间);筛选与排序按 localStorage 持久化(搜索不持久)
351
- - **侧边栏状态条**:「任务看板」入口同行右侧显示 `待办|进行中|待验收` 计数(如 `0|1|2`),三个数字分别着蓝/橙/紫状态色,悬停提示含义与实时数字,数字变化时上下滚动动画(增长上滚、减少下滚,尊重 prefers-reduced-motion)
352
- - **执行结算闭环**:执行会话结束但未按协议交接(没评论/没移待验收)时,系统自动补评论并移入待验收——任务不再静默挂在进行中;执行失败也留系统评论
353
- - **认领租约**:认领超 30 分钟未动的任务,卡片与详情显示「⏱ 认领超时」;详情页新增「🔓 释放认领」一键退回待办
354
- - **执行记录保留上限**:每任务保留最近 20 条执行记录(详情页显示「+N 已清理」),定时任务的台账不再无限膨胀
355
- - **全局并发上限**:同时执行的会话数默认上限 3(`DSH_TASKBOARD_MAX_CONCURRENT` 可调);调度器满载时保留到期窗口不烧掉,下个 tick 重试
356
- - **执行 Prompt 模板变量**:`{{lastExecution}}`(上次执行结果)、`{{lastComments}}`(最近 3 条评论)——巡检类定时任务能看到上次跑出了什么
357
- - **复制任务**:详情页「⧉ 复制」一键克隆全部配置为新卡
358
- - **导出**:顶栏「⬇ JSON」全量台账备份、「⬇ CSV」任务清单(带 BOM,Excel 直接打开)
359
- - **状态色点**:五列列头与「其它任务」三列的分类文字前显示状态色圆点(待规划灰/待办蓝/进行中橙/待验收紫/已完成绿/已删除红),与详情页状态 pill 同色系
360
- - **执行会话跳转**:详情页执行记录(倒序,最新在最上)点击会话短 ID 直接打开该执行会话,看板自动收起;已删除与已归档分开提示并带短 ID;运行时服务惰性解析、会话列表镜像滞后时自动刷新重查一次;短 ID 剥离 `taskboard-` 中缀,各记录可区分
361
-
362
- ### 0.1.3
363
-
364
- - **执行竞态与持锁修复**:
365
- - 「立即执行」原子闸:in-progress 检查移入串行写队列,重复点击 / 调度重叠最多开一个执行会话
366
- - 认领改为显式 `claimedBy/claimedAt` 字段(原先从 updatedBy 推断,用户编辑会误清持锁);执行期间任务由执行会话持有,其他会话不可移动;旧台账加载时自动迁移
367
- - 执行失败自动回退 todo(原先永久卡在 in_progress);host 重启后残留的 running 执行标记为 failed 并归还任务;成功结算后释放持锁
368
- - **执行可取消**:详情页「■ 停止执行」——停止执行会话、执行记录标记 cancelled、任务回到待办
369
- - **模型校验**:建卡/改卡的固定模型做结构校验 + provider 路由存在性校验(host llm 运行时不可达时只做前者)
370
- - **台账安全**:`store.snapshot()/get()` 返回冻结副本,外部修改直接抛错,杜绝绕过版本号的原地改数据
371
- - **调度器**:tick 前先加载台账(修复 host 重启后首个 tick 读到空表导致定时任务不触发);启动补偿定时器随 dispose 清理
372
- - **其它**:执行记录会话 ID 点击复制;详情页显示持锁会话;删除死代码(`store.flush` / routes `taskPath`)
373
-
374
- ### 0.1.2
375
-
376
- - **看板拖拽**:所有列的卡片可互相拖动(按状态机校验合法流转,非法拖放弹窗提示);正在执行的任务拖动会被拦截,提示「该任务正在由【任务名】会话执行,不能拖动」
377
- - **执行会话体验**:执行会话标题固定为任务名(经 `sessionTitle.rename` 写入,不会被自动重命名覆盖);首条消息以正常用户消息呈现(不再是插件上下文行)
378
- - **交互优化**:
379
- - 新建/编辑弹框新增「⚡ 立即执行」按钮(保存后直接发起执行)
380
- - 详情页「立即执行」移至「编辑」按钮旁
381
- - 「+ 新建任务」移至看板标题统计旁
382
- - 状态流转按钮文案统一为「移至→{状态}」
383
- - **弹窗**:原生 `alert()` 全部替换为主题化模态弹窗(Esc/遮罩可关)
384
-
385
- ### 0.1.1
386
-
387
- - 初始 npm 发布:看板协作、8 个 agent 工具、手动/cron 执行、SSE 实时视图
234
+ > 📜 更早版本的完整更新日志见 [changelog.md](changelog.md)。
388
235
 
389
236
  License: Apache-2.0
package/lib/client.js CHANGED
@@ -161,6 +161,10 @@ window.__ModuleLoader__.load({
161
161
  function defaultIsolationOf(settings) {
162
162
  return settings?.defaultIsolation ?? "none";
163
163
  }
164
+ /** The effective external session sync switch (board setting → factory default false). */
165
+ function defaultSyncExternalSessionsOf(settings) {
166
+ return settings?.syncExternalSessions ?? false;
167
+ }
164
168
  /**
165
169
  * Parse a five-field cron expression. Supported field syntax: star, star/step
166
170
  * (`* / n` without spaces), a single number, an `a-b` range, and comma lists
@@ -4016,11 +4020,17 @@ window.__ModuleLoader__.load({
4016
4020
  * @param controller - the board controller.
4017
4021
  */
4018
4022
  function SettingsModal({ controller }) {
4019
- const current = controller.getSnapshot().ledger.settings?.defaultIsolation ?? "none";
4020
- const [draft, setDraft] = (0, react.useState)(current);
4021
- const dirty = draft !== current;
4023
+ const state = controller.getSnapshot();
4024
+ const currentIso = state.ledger.settings?.defaultIsolation ?? "none";
4025
+ const currentSync = defaultSyncExternalSessionsOf(state.ledger.settings);
4026
+ const [draftIso, setDraftIso] = (0, react.useState)(currentIso);
4027
+ const [draftSync, setDraftSync] = (0, react.useState)(currentSync);
4028
+ const dirty = draftIso !== currentIso || draftSync !== currentSync;
4022
4029
  const save = () => {
4023
- controller.updateSettings({ defaultIsolation: draft }).then((ok) => {
4030
+ controller.updateSettings({
4031
+ defaultIsolation: draftIso,
4032
+ syncExternalSessions: draftSync
4033
+ }).then((ok) => {
4024
4034
  if (ok) controller.closeSettings();
4025
4035
  });
4026
4036
  };
@@ -4044,7 +4054,7 @@ window.__ModuleLoader__.load({
4044
4054
  }),
4045
4055
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4046
4056
  className: "dsh-atb-modal-headtext",
4047
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "看板设置" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "新建任务时应用的默认值(不影响已有任务)" })]
4057
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "看板设置" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "新建任务与会话同步的全局默认值" })]
4048
4058
  }),
4049
4059
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4050
4060
  type: "button",
@@ -4055,9 +4065,9 @@ window.__ModuleLoader__.load({
4055
4065
  })
4056
4066
  ]
4057
4067
  }),
4058
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4068
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4059
4069
  className: "dsh-atb-modal-body",
4060
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4070
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4061
4071
  className: "dsh-atb-diag-sec",
4062
4072
  children: [
4063
4073
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: "默认执行隔离" }),
@@ -4066,9 +4076,9 @@ window.__ModuleLoader__.load({
4066
4076
  children: ISOLATION_OPTIONS.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4067
4077
  type: "button",
4068
4078
  className: "dsh-atb-mode-opt",
4069
- "data-on": draft === o.value,
4079
+ "data-on": draftIso === o.value,
4070
4080
  title: o.hint,
4071
- onClick: () => setDraft(o.value),
4081
+ onClick: () => setDraftIso(o.value),
4072
4082
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4073
4083
  className: "dsh-atb-mode-name",
4074
4084
  children: o.name
@@ -4082,12 +4092,51 @@ window.__ModuleLoader__.load({
4082
4092
  className: "dsh-atb-isolation-note",
4083
4093
  children: [
4084
4094
  "当前保存的默认:",
4085
- current === "worktree" ? "🌿 Worktree 隔离" : "📁 原目录执行",
4095
+ currentIso === "worktree" ? "🌿 Worktree 隔离" : "📁 原目录执行",
4086
4096
  "。 仅影响之后新建的任务;已有任务保持创建时的选择,非 git 项目运行时仍自动降级原目录。"
4087
4097
  ]
4088
4098
  })
4089
4099
  ]
4090
- })
4100
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4101
+ className: "dsh-atb-diag-sec",
4102
+ children: [
4103
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: "自动同步工作区会话" }),
4104
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4105
+ className: "dsh-atb-mode-picker",
4106
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4107
+ type: "button",
4108
+ className: "dsh-atb-mode-opt",
4109
+ "data-on": !draftSync,
4110
+ title: "仅管理在任务看板中创建和触发的任务",
4111
+ onClick: () => setDraftSync(false),
4112
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4113
+ className: "dsh-atb-mode-name",
4114
+ children: "🚫 关闭同步"
4115
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4116
+ className: "dsh-atb-mode-hint",
4117
+ children: "仅管理看板任务(出厂默认)"
4118
+ })]
4119
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4120
+ type: "button",
4121
+ className: "dsh-atb-mode-opt",
4122
+ "data-on": draftSync,
4123
+ title: "各工作区直接新建的会话也会在看板展示,运行中进入「进行中」,完成后自动进入「待验收」",
4124
+ onClick: () => setDraftSync(true),
4125
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4126
+ className: "dsh-atb-mode-name",
4127
+ children: "🔄 自动纳入会话"
4128
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4129
+ className: "dsh-atb-mode-hint",
4130
+ children: "跟踪运行并在完成后进入待验收"
4131
+ })]
4132
+ })]
4133
+ }),
4134
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4135
+ className: "dsh-atb-isolation-note",
4136
+ children: currentSync ? "已开启:工作区直接新建并执行的会话将自动在看板生成任务卡片,并在完成后流转至「待验收」列。" : "已关闭:仅在看板内部创建与触发执行的任务会出现在看板上。"
4137
+ })
4138
+ ]
4139
+ })]
4091
4140
  }),
4092
4141
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4093
4142
  className: "dsh-atb-modal-foot",
@@ -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 TaskModel,\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; reasoningEffort?: 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?: () => TaskModel | 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 ? {\n agentOptions: {\n provider: model.provider,\n model: model.model,\n ...(model.reasoningEffort !== undefined ? { reasoningEffort: model.reasoningEffort } : {}),\n },\n } : {}),\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":";;;;AAmHA,SAAS,eAAe,MAAgD;CACtE,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM,OAAO,KAAA;CACtD,MAAM,SAAU,KAA8B;CAC9C,IAAI,OAAO,WAAW,YAAY,WAAW,MAAM,OAAO,KAAA;CAE1D,IADc,OAA8B,SAC/B,SAAS,OAAO,KAAA;CAC7B,MAAM,QAAS,OAA6C;CAC5D,MAAM,UAAU,OAAO,OAAO,YAAY,WAAW,MAAM,UAAU;CACrE,QAAQ,MAAM,sCAAsC,KAAK,UAAU,KAAK,CAAC,EAAE,MAAM,GAAG,GAAI,KAAK,EAAE;CAC/F,OAAO,EAAE,QAAQ;AACnB;;;;AAiCA,IAAa,mBAAb,MAA8B;CAQC;;CAN7B,uBAAwB,IAAI,IAAsB;;CAGlD;;CAGA,YAAY,MAAsC;EAArB,KAAA,OAAA;EAC3B,KAAK,oBAAoB,KAAK,OAAO,gBAAgB,WAAW,UAAU;GACxE,IAAI,MAAM,SAAS,YAAY;GAM/B,MAAM,UAAU,eAAe,MAAM,IAAI;GACzC,IAAI,YAAY,KAAA,GACd,KAAK,YAAY,WAAW,QAAQ,OAAO,CAAC,CAAC,OAAM,UAAS;IAC1D,QAAQ,MAAM,6CAA6C,KAAK;GAClE,CAAC;EAEL,CAAC;CACH;;CAGA,UAAgB;EACd,KAAK,kBAAkB;CACzB;;;;;CAMA,MAAc,gBAAgB,UAA8E;EAC1G,IAAI,aAAa,KAAA,KAAa,KAAK,KAAK,QAAQ,KAAA,GAAW,OAAO,KAAA;EAClE,IAAI;GACF,OAAO,MAAM,KAAK,KAAK,IAAI,QAAQ,SAAS,cAAc,SAAS,UAAU;EAC/E,QAAQ;GACN;EACF;CACF;;CAGA,WAAmB,WAA4B,OAA0C;EACvF,IAAI,UAAU,KAAA,GAAW;EACzB,IAAI,MAAM,eAAe,KAAA,GAAW,UAAU,aAAa,MAAM;EACjE,UAAU,UAAU,MAAM;EAC1B,UAAU,eAAe,MAAM;EAC/B,UAAU,aAAa,MAAM;EAC7B,UAAU,kBAAkB,MAAM;EAClC,UAAU,eAAe,MAAM;EAC/B,IAAI,MAAM,aAAa,KAAA,GAAW,UAAU,WAAW,MAAM;CAC/D;;;;;;;;CASA,YAAoB,WAAmB,SAAgC;EAGrE,MAAM,QAAQ,CAAC,GAAG,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,MAAK,MAAK,EAAE,cAAc,SAAS;EACzE,OAAO,KAAK,gBAAgB,OAAO,QAAQ,CAAC,CAAC,MAAK,UAChD,KAAK,KAAK,MAAM,OAAO,uBAAuB,WAAW;GACvD,KAAK,MAAM,QAAQ,OAAO,OACxB,KAAK,MAAM,aAAa,KAAK,YAC3B,IAAI,UAAU,cAAc,aAAa,UAAU,YAAY,WAAW;IACxE,UAAU,UAAU;IACpB,UAAU,QAAQ,QAAQ,MAAM,GAAG,GAAG;IACtC,UAAU,UAAU,KAAK,KAAK,IAAI;IAClC,KAAK,WAAW,WAAW,KAAK;IAIhC,IAAI,KAAK,WAAW,iBAAiB,KAAK,cAAc,WAAW;KACjE,KAAK,SAAS;KACd,KAAK,YAAY,KAAK,KAAK,IAAI;KAC/B,OAAO,KAAK;KACZ,OAAO,KAAK;KACZ,KAAK,SAAS,KAAK;MACjB,IAAI,aAAa;MACjB,MAAM,cAAc,aAAa,QAAQ,MAAM,GAAG,GAAG,EAAE,UAAU;MACjE,SAAS;MACT,WAAW,KAAK,KAAK,IAAI;KAC3B,CAAC;IACH;IACA,OAAO,CAAC,IAAI;GACd;EAIN,CAAC,CACH,CAAC,CAAC,WAAW,CAAqC,CAAC;CACrD;;;;;;;CAQA,MAAc,eAAe,aAAqB,OAAgD;EAChG,MAAM,KAAK,KAAK,MAAM,OAAO,uBAAuB,WAAW;GAC7D,KAAK,MAAM,QAAQ,OAAO,OAAO;IAC/B,MAAM,YAAY,KAAK,WAAW,MAAK,MAAK,EAAE,OAAO,WAAW;IAChE,IAAI,cAAc,KAAA,GAAW;KAC3B,IAAI,UAAU,YAAY,WAAW,OAAO,KAAA;KAC5C,OAAO,OAAO,WAAW,KAAK;KAC9B,OAAO,CAAC,IAAI;IACd;GACF;EAEF,CAAC;CACH;;;;;;;;;;;;;CAcA,MAAM,IAAI,QAAgB,SAAqC,SAAiD;EAC9G,MAAM,MAAM,KAAK,KAAK,iBAAA;EACtB,IAAI,KAAK,KAAK,QAAQ,KACpB,OAAO;GAAE,IAAI;GAAO,OAAO,wCAAwC,KAAK,KAAK,KAAK,GAAG,IAAI;EAAW;EAEtG,MAAM,OAAO,KAAK,KAAK,MAAM,IAAI,MAAM;EACvC,IAAI,SAAS,KAAA,KAAa,KAAK,cAAc,KAAA,GAC3C,OAAO;GAAE,IAAI;GAAO,OAAO,WAAW;EAAS;EAEjD,MAAM,YAAY,KAAK,KAAK,WAAW,IAAI,KAAK,WAAW;EAC3D,IAAI,cAAc,KAAA,GAChB,OAAO;GAAE,IAAI;GAAO,OAAO,qBAAqB,KAAK;EAAc;EAGrE,MAAM,cAAc,eAAe;EACnC,MAAM,YAAY,KAAK,KAAK,gBAAgB,KAAK,qBAAqB,OAAO,WAAW;EAKxF,MAAM,YAA2B,mBAAmB,IAAI;EACxD,MAAM,SAAS,KAAK,UAAU,mBAAmB,KAAK,OAAO,KAAK,EAAE;EACpE,MAAM,eAAe,eAAe,UAAU,MAAM,KAAK,EAAE;EAI3D,IAAI;EACJ,MAAM,KAAK,KAAK,MAAM,OAAO,uBAAuB,WAAW;GAC7D,MAAM,SAAS,OAAO,MAAM,MAAK,MAAK,EAAE,OAAO,MAAM;GACrD,IAAI,WAAW,KAAA,KAAa,OAAO,cAAc,KAAA,GAAW;IAC1D,OAAO,WAAW;IAClB;GACF;GACA,IAAI,OAAO,WAAW,eAAe;IACnC,OAAO;IACP;GACF;GAIA,MAAM,UAAU,OAAO,MAAM,QAAQ,GAAG,MAAM,IAAI,EAAE,WAAW,QAAO,MAAK,EAAE,YAAY,SAAS,CAAC,CAAC,QAAQ,CAAC;GAC7G,IAAI,WAAW,KAAK;IAClB,OAAO,wCAAwC,QAAQ,GAAG,IAAI;IAC9D;GACF;GACA,OAAO,WAAW,KAAK;IACrB,IAAI;IACJ;IACA,WAAW,KAAK,KAAK,IAAI;IACzB,SAAS;IACT,GAAI,cAAc,SAAS,EAAE,WAAW,OAAgB,IAAI;KAAE,WAAW;KAAqB;IAAO;GACvG,CAAC;GACD,OAAO,SAAS;GAChB,OAAO,YAAY,KAAK,KAAK,IAAI;GACjC,OAAO,YAAY,EAAE,MAAM,SAAS;GACpC,OAAO,YAAY;GACnB,OAAO,YAAY,KAAK,KAAK,IAAI;GACjC,OAAO,CAAC,MAAM;EAChB,CAAC;EACD,IAAI,SAAS,KAAA,GAAW,OAAO;GAAE,IAAI;GAAO,OAAO;EAAK;EAKxD,IAAI;EACJ,IAAI;EACJ,IAAI,cAAc,YAAY;GAC5B,MAAM,UAAU,MAAM,KAAK,iBAAiB,MAAM,UAAU,MAAM,cAAc,QAAQ,SAAS,kBAAkB,IAAI;GACvH,IAAI,QAAQ,aAAa,KAAA,GAAW;IAClC,WAAW,QAAQ;IAGnB,MAAM,KAAK,eAAe,aAAa;KACrC,cAAc,QAAQ,SAAS;KAC/B,YAAY,QAAQ,SAAS;IAC/B,CAAC;GACH,OAAO;IACL,gBAAgB,QAAQ;IAExB,MAAM,KAAK,eAAe,aAAa;KAAE,WAAW;KAAQ;KAAe,QAAQ,KAAA;KAAW,cAAc,KAAA;KAAW,YAAY,KAAA;IAAU,CAAC;GAChJ;EACF;EAeA,IAAI;EACJ,IAAI;GACF,cAAc,KAAK,KAAK,iBAAiB,KAAA,IAAY,KAAA,IAAY,MAAM,KAAK,KAAK,aAAa,KAAK,QAAQ;EAC7G,SAAS,OAAO;GACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GACrE,MAAM,KAAK,eAAe,aAAa;IAAE,SAAS;IAAU,OAAO,eAAe,QAAQ,MAAM,GAAG,GAAG;IAAK,SAAS,KAAK,KAAK,IAAI;GAAE,CAAC;GACrI,MAAM,KAAK,eAAe,MAAM;GAEhC,IAAI,aAAa,KAAA,KAAa,KAAK,KAAK,QAAQ,KAAA,GAC9C,IAAI;IAAE,MAAM,KAAK,KAAK,IAAI,eAAe,UAAU,MAAM,SAAS,YAAY;GAAE,QAAQ,CAA+C;GAEzI,OAAO;IAAE,IAAI;IAAO,OAAO,8BAA8B;GAAU;EACrE;EACA,IAAI;EACJ,IAAI;GACF,MAAM,QAAQ,KAAK,SAAS,KAAK,KAAK,eAAe;GACrD,SAAS,MAAM,KAAK,KAAK,OAAO,OAAO;IACrC;IACA,MAAM;KACJ,KAAK,UAAU;KACf,GAAI,gBAAgB,KAAA,IAAY,EAAE,aAAa,YAAY,YAAY,IAAI,CAAC;IAC9E;IACA,GAAI,UAAU,KAAA,IAAY,EACxB,cAAc;KACZ,UAAU,MAAM;KAChB,OAAO,MAAM;KACb,GAAI,MAAM,oBAAoB,KAAA,IAAY,EAAE,iBAAiB,MAAM,gBAAgB,IAAI,CAAC;IAC1F,EACF,IAAI,CAAC;IACL,GAAI,gBAAgB,KAAA,IAAY,EAAE,OAAO,YAAY,MAAM,IAAI,CAAC;GAClE,CAAC;EACH,SAAS,OAAO;GACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GACrE,MAAM,KAAK,eAAe,aAAa;IAAE,SAAS;IAAU,OAAO,QAAQ,MAAM,GAAG,GAAG;IAAG,SAAS,KAAK,KAAK,IAAI;GAAE,CAAC;GACpH,MAAM,KAAK,eAAe,MAAM;GAEhC,IAAI,aAAa,KAAA,KAAa,KAAK,KAAK,QAAQ,KAAA,GAC9C,IAAI;IAAE,MAAM,KAAK,KAAK,IAAI,eAAe,UAAU,MAAM,SAAS,YAAY;GAAE,QAAQ,CAA+C;GAEzI,OAAO;IAAE,IAAI;IAAO,OAAO;GAAQ;EACrC;EAWA,IAAI,CAAC,MAFsB,KAAK,KAAK,MAAM,MAAK,WAC9C,OAAO,MAAM,MAAK,MAAK,EAAE,WAAW,MAAK,MAAK,EAAE,OAAO,eAAe,EAAE,YAAY,SAAS,CAAC,CAAC,GAC9E;GACjB,MAAM,OAAO,QAAQ,CAAC,CAAC,YAAY,CAAoB,CAAC;GAExD,IAAI,aAAa,KAAA,KAAa,KAAK,KAAK,QAAQ,KAAA,GAC9C,IAAI;IAAE,MAAM,KAAK,KAAK,IAAI,eAAe,UAAU,MAAM,SAAS,YAAY;GAAE,QAAQ,CAAoB;GAE9G,OAAO;IAAE,IAAI;IAAO,OAAO;GAA2B;EACxD;EAGA,MAAM,KAAK,KAAK,WAAW,OAAO,KAAK,aAAa,SAAS,CAAC,CAAC,YAAY,CAAiB,CAAC;EAK7F,IAAI;GACF,KAAK,KAAK,gBAAgB,WAAW,KAAK,KAAK;EACjD,QAAQ,CAAiB;EAGzB,MAAM,KAAK,eAAe,aAAa,EAAE,UAAU,CAAC;EAUpD,OAAO,MAAM,OAAO;GAClB,IAAI,KAAK,KAAK,gBAAgB,KAAK,UAAU,iBAAiB,OAAO,WAAW,GAAG;GACnF,MAAM;GACN,SAAS,CAAC;IAAE,MAAM;IAAiB,MAAM,KAAK,cAAc,MAAM,UAAU,aAAa;GAAE,CAAC;GAC5F,QAAQ;IAAE,MAAM;IAAmB,QAAQ;GAAgB;EAC7D,CAAC;EACD,OAAO,MAAM,SAAS;GACpB,IAAI,KAAK,KAAK,gBAAgB,KAAK,UAAU,iBAAiB,OAAO,WAAW,GAAG;GACnF,MAAM;GACN,SAAS,CAAC;IAAE,MAAM;IAAiB,MAAM,KAAK,SAAS,IAAI;GAAE,CAAC;GAC9D,QAAQ,EAAE,MAAM,OAAgB;EAClC,CAAC;EAMD,MAAM,eAAqB;GACzB,KAAK,KAAK,OAAO,WAAW;GAC5B,KAAU,gBAAgB,aAAa,WAAW,QAAQ;EAC5D;EACA,KAAK,KAAK,IAAI,aAAa;GAAE;GAAW,GAAI,aAAa,KAAA,IAAY,EAAE,SAAS,IAAI,CAAC;GAAI;GAAQ,eAAe,OAAO,QAAQ;EAAE,CAAC;EAOlI,OAAY,MAAM,SAAS,CAAC,CAAC,KAAK,cAAc;GAC9C,KAAK,YAAY,WAAW,gCAAgC,CAAC,CAC1D,WAAW;IAAE,KAAK,KAAK,OAAO,WAAW;GAAE,CAAC,CAAC,CAC7C,YAAY;IAAE,KAAK,KAAK,OAAO,WAAW;GAAE,CAAC;EAClD,CAAC;EAED,OAAO;GAAE,IAAI;GAAM;GAAa;EAAU;CAC5C;;;;;;CAOA,MAAc,gBACZ,aACA,WACA,UACe;EACf,MAAM,QAAQ,MAAM,KAAK,gBAAgB,QAAQ;EACjD,MAAM,KAAK,KAAK,MAAM,OAAO,uBAAuB,WAAW;GAC7D,KAAK,MAAM,KAAK,OAAO,OAAO;IAC5B,MAAM,YAAY,EAAE,WAAW,MAAK,MAAK,EAAE,OAAO,WAAW;IAC7D,IAAI,cAAc,KAAA,KAAa,UAAU,YAAY,WAAW;KAC9D,MAAM,MAAM,KAAK,KAAK,IAAI;KAC1B,UAAU,UAAU;KACpB,UAAU,UAAU;KACpB,KAAK,WAAW,WAAW,KAAK;KAChC,IAAI,EAAE,WAAW,iBAAiB,EAAE,cAAc,WAAW;MAC3D,OAAO,EAAE;MACT,OAAO,EAAE;KACX;KACA,IAAI,EAAE,WAAW,eAAe;MAC9B,MAAM,YAAY,EAAE,SAAS,MAAK,MAAK,EAAE,aAAa,SAAS;MAC/D,EAAE,SAAS,KAAK;OACd,IAAI,aAAa;OACjB,MAAM,cAAc,YAChB,yCACA,uDAAuD;OAC3D,SAAS;OACT,WAAW;MACb,CAAC;MACD,EAAE,SAAS;MACX,EAAE,YAAY;MACd,EAAE,YAAY,EAAE,MAAM,SAAS;KACjC;KACA,OAAO,CAAC,CAAC;IACX;GACF;EAEF,CAAC;CACH;;;;;;;;CASA,MAAc,iBACZ,MACA,eACA,cACA,QACA,OACyD;EACzD,MAAM,MAAM,KAAK,KAAK;EACtB,IAAI,QAAQ,KAAA,GAAW,OAAO,EAAE,MAAM,oBAAoB;EAC1D,IAAI,SAAS;EACb,IAAI;GACF,SAAS,MAAM,IAAI,OAAO,aAAa;EACzC,QAAQ,CAAkB;EAC1B,IAAI,CAAC,QAAQ;GAGX,IAAI,YAAY;GAChB,IAAI;IACF,YAAY,MAAM,IAAI,gBAAgB;GACxC,QAAQ,CAAuC;GAC/C,OAAO,EAAE,MAAM,YAAY,0BAA0B,+BAA+B;EACtF;EACA,IAAI;EACJ,IAAI;GACF,OAAO,MAAM,IAAI,gBAAgB,eAAe,cAAc,QAAQ,QAAQ,UAAU,OAAO;EACjG,QAAQ,CAAkB;EAC1B,IAAI,SAAS,KAAA,GAAW,OAAO,EAAE,MAAM,sCAAsC;EAE7E,IAAI,KAAK,WAAW,KAAA,GAClB,MAAM,KAAK,KAAK,MAAM,OAAO,iBAAiB,WAAW;GACvD,MAAM,SAAS,OAAO,MAAM,MAAK,MAAK,EAAE,OAAO,KAAK,EAAE;GACtD,IAAI,WAAW,KAAA,KAAa,OAAO,WAAW,KAAA,GAAW;IACvD,OAAO,SAAS;IAChB,OAAO,CAAC,MAAM;GAChB;EAEF,CAAC;EAEH,OAAO,EACL,UAAU;GACR,QAAQ,KAAK;GACb,cAAc,KAAK;GACnB,YAAY,KAAK;GACjB,GAAI,KAAK,WAAW,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;EACjD,EACF;CACF;;CAGA,WAAmB;EACjB,OAAO,KAAK,KAAK;CACnB;;;;;;;CAQA,MAAM,OAAO,QAA8C;EACzD,MAAM,OAAO,KAAK,KAAK,MAAM,IAAI,MAAM;EACvC,IAAI,SAAS,KAAA,GAAW,OAAO;GAAE,IAAI;GAAO,OAAO,WAAW;EAAS;EACvE,MAAM,UAAU,CAAC,GAAG,KAAK,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAK,MAAK,EAAE,YAAY,SAAS;EAChF,IAAI,YAAY,KAAA,GAAW,OAAO;GAAE,IAAI;GAAO,OAAO;EAAuB;EAE7E,MAAM,QAAQ,KAAK,KAAK,IAAI,QAAQ,EAAE;EACtC,KAAK,KAAK,OAAO,QAAQ,EAAE;EAI3B,IAAI;GACF,MAAM,OAAO,QAAQ;EACvB,QAAQ,CAAqB;EAI7B,MAAM,QAAQ,MAAM,KAAK,gBAAgB,OAAO,QAAQ;EACxD,IAAI,UAAU;EACd,MAAM,KAAK,KAAK,MAAM,OAAO,uBAAuB,WAAW;GAC7D,MAAM,SAAS,OAAO,MAAM,MAAK,MAAK,EAAE,OAAO,MAAM;GACrD,IAAI,WAAW,KAAA,GAAW,OAAO,KAAA;GACjC,MAAM,YAAY,OAAO,WAAW,MAAK,MAAK,EAAE,OAAO,QAAQ,EAAE;GACjE,IAAI,cAAc,KAAA,KAAa,UAAU,YAAY,WAAW,OAAO,KAAA;GACvE,UAAU;GACV,UAAU,UAAU;GACpB,UAAU,UAAU,KAAK,KAAK,IAAI;GAClC,KAAK,WAAW,WAAW,KAAK;GAChC,IAAI,OAAO,WAAW,eAAe;IACnC,OAAO,SAAS;IAChB,OAAO,YAAY,KAAK,KAAK,IAAI;IACjC,OAAO,OAAO;IACd,OAAO,OAAO;GAChB;GACA,OAAO,CAAC,MAAM;EAChB,CAAC;EAID,IAAI,CAAC,SAAS,OAAO;GAAE,IAAI;GAAO,OAAO;EAA4B;EACrE,OAAO;GAAE,IAAI;GAAM,aAAa,QAAQ;EAAG;CAC7C;;;;;;CAOA,MAAM,YAA2B;EAC/B,MAAM,KAAK,KAAK,MAAM,OAAO,uBAAuB,WAAW;GAC7D,MAAM,MAAM,KAAK,KAAK,IAAI;GAC1B,MAAM,UAAwB,CAAC;GAC/B,KAAK,MAAM,QAAQ,OAAO,OAAO;IAC/B,IAAI,QAAQ;IACZ,KAAK,MAAM,aAAa,KAAK,YAC3B,IAAI,UAAU,YAAY,WAAW;KACnC,UAAU,UAAU;KACpB,UAAU,QAAQ;KAClB,UAAU,UAAU;KACpB,QAAQ;IACV;IAEF,IAAI,CAAC,OAAO;IACZ,IAAI,KAAK,WAAW,eAAe;KACjC,KAAK,SAAS;KACd,KAAK,YAAY;KACjB,OAAO,KAAK;KACZ,OAAO,KAAK;IACd;IACA,QAAQ,KAAK,IAAI;GACnB;GACA,OAAO,QAAQ,SAAS,IAAI,UAAU,KAAA;EACxC,CAAC;CACH;;;;;;;;;;;;;CAcA,cAAsB,MAAkB,UAA6B,aAA8B;EACjG,IAAI,OAAO,SAAS,KAAK,MAAM,OAAO,KAAK,GAAG;EAQ9C,IAAI,KAAK,cAAc,KAAA,KAAa,KAAK,UAAU,SAAS,GAAG;GAC7D,MAAM,QAAQ,KAAK,UAChB,KAAK,MAAM,UAAU,GAAG,KAAK,UAAU,MAAM,IAAI,GAAG,QAAQ,EAAE,IAAI,KAAK,OAAO,KAAK,SAAS,KAAA,IAAY,QAAQ,KAAK,KAAK,KAAK,IAAI,CAAC,CACpI,KAAK,IAAI;GACZ,MAAM,OAAO,KAAK,UAAU,QAAO,MAAK,EAAE,OAAO,CAAC,CAAC;GACnD,QAAQ,kBAAkB,KAAK,GAAG,KAAK,UAAU,OAAO,iBAAiB,MAAM;EACjF;EACA,IAAI,aAAa,KAAA,GACf,IAAI,SAAS,WAAW,MACtB,QAAQ,+CAA+C,SAAS,OAAO,iBAAiB,SAAS,aAAa;OAE9G,QAAQ,wCAAwC,SAAS,OAAO,mBAAmB,SAAS,aAAa;OAEtG,IAAI,gBAAgB,KAAA,GACzB,QAAQ,gCAAgC,YAAY;EAEtD,OAAO;CACT;;;;;;;;;CAUA,SAAiB,MAA0B;EACzC,MAAM,WAAW,CAAC,GAAG,KAAK,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAK,MAAK,EAAE,YAAY,SAAS;EACjF,MAAM,eAAe,aAAa,KAAA,IAC9B,QACA,GAAG,SAAS,QAAQ,KAAK,SAAS,UAAU,SAAS,UAAU,KAAA,IAAY,MAAM,SAAS,MAAM,MAAM,GAAG,GAAG,MAAM,GAAG,KAAK,SAAS,cAAc,KAAA,IAAY,IAAI,KAAK,SAAS,SAAS,CAAC,CAAC,YAAY,IAAI;EAC9M,MAAM,mBAAmB,KAAK,SAAS,MAAM,EAAE,CAAC,CAC7C,KAAI,MAAK,IAAI,EAAE,aAAa,KAAA,IAAY,UAAU,OAAO,IAAI,EAAE,MAAM,CAAC,CACtE,KAAK,IAAI,KAAK;EACjB,OAAO,gBAAgB,IAAI,CAAC,CACzB,QAAQ,0BAA0B,YAAY,CAAC,CAC/C,QAAQ,yBAAyB,gBAAgB;CACtD;;CAGA,MAAc,eAAe,QAA+B;EAC1D,MAAM,KAAK,KAAK,MAAM,OAAO,uBAAuB,WAAW;GAC7D,MAAM,SAAS,OAAO,MAAM,MAAK,MAAK,EAAE,OAAO,MAAM;GACrD,IAAI,WAAW,KAAA,KAAa,OAAO,WAAW,eAAe;IAC3D,OAAO,SAAS;IAChB,OAAO,YAAY,KAAK,KAAK,IAAI;IACjC,OAAO,OAAO;IACd,OAAO,OAAO;IACd,OAAO,CAAC,MAAM;GAChB;EAEF,CAAC;CACH;AACF"}
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 TaskModel,\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; reasoningEffort?: 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 }, sessionMeta?: { header?: { cwd?: string } }) => 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?: () => TaskModel | 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 ? {\n agentOptions: {\n provider: model.provider,\n model: model.model,\n ...(model.reasoningEffort !== undefined ? { reasoningEffort: model.reasoningEffort } : {}),\n },\n } : {}),\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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"}