@nanmicoder/dsh-agent-teams 0.1.18 → 0.1.20

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
@@ -22,7 +22,7 @@
22
22
 
23
23
  `dsh-agent-teams` turns the current DeepSeek Harness session into a captain that can assemble durable sub-agents, split a goal into dependency-aware tasks, and coordinate work through direct messages.
24
24
 
25
- Ask in natural language. The plugin provides the team protocol, 13 coordination tools, persistent state, an automatic shared-task scheduler, and a live Web UI—without requiring a separate workflow engine.
25
+ Ask in natural language. The plugin provides the team protocol, 14 coordination tools, persistent state, an automatic shared-task scheduler, and a live Web UI—without requiring a separate workflow engine.
26
26
 
27
27
  <p align="center">
28
28
  <img src="./assets/ui.png" width="100%" alt="DeepSeek Harness conversation with the AgentTeams live activity panel, members, tasks, dependencies, and reports">
@@ -30,7 +30,7 @@ Ask in natural language. The plugin provides the team protocol, 13 coordination
30
30
 
31
31
  ## Releases
32
32
 
33
- [v0.1.17](./release-notes/v0.1.17.md) adds automatic light, dark, and system theme support on the npm `latest` channel. Activity panels, conversation cards, and dialog controls follow Harness semantic colors. Recommended host: DeepSeek Harness `0.1.5-rc.1`; the three older supported host targets are retained.
33
+ [v0.1.20](./release-notes/v0.1.20.md) syncs the version references in this document with the npm `latest` channel. It changes no code and no behavior; the packaged artifact is identical to `0.1.19`. The substantive changes since the last documented release are in [v0.1.19](./release-notes/v0.1.19.md): member startup survives a host that disables or renames its delegation tools, automatic repair scope is derived from `requiredFix`, and captains gain `agent_teams_amend_task`. Recommended host: DeepSeek Harness `0.1.5-rc.1`; the three older supported host targets are retained.
34
34
 
35
35
  ## Why AgentTeams?
36
36
 
@@ -49,14 +49,14 @@ The conversation card and activity panel use Harness's official locale service.
49
49
 
50
50
  ## Install and choose versions
51
51
 
52
- **Recommended pair: DeepSeek Harness `0.1.5-rc.1` + AgentTeams `0.1.17`. Harness remains a prerelease.**
52
+ **Recommended pair: DeepSeek Harness `0.1.5-rc.1` + AgentTeams `0.1.20`. Harness remains a prerelease.**
53
53
 
54
54
  | Use case | DeepSeek Harness | AgentTeams plugin |
55
55
  | --- | --- | --- |
56
- | **Recommended installation** | **`0.1.5-rc.1`** | **`0.1.17`** |
57
- | Retaining an older RC | `0.1.2-rc.1` | `0.1.17` |
58
- | Developer Alpha testing | `0.1.2-alpha.5` | `0.1.17` |
59
- | Retaining an older Alpha | `0.1.2-alpha.2` | `0.1.17` |
56
+ | **Recommended installation** | **`0.1.5-rc.1`** | **`0.1.20`** |
57
+ | Retaining an older RC | `0.1.2-rc.1` | `0.1.20` |
58
+ | Developer Alpha testing | `0.1.2-alpha.5` | `0.1.20` |
59
+ | Retaining an older Alpha | `0.1.2-alpha.2` | `0.1.20` |
60
60
 
61
61
  ### 1. Install DeepSeek Harness
62
62
 
@@ -72,12 +72,12 @@ Skip this if you already run this version. Alpha is opt-in: select an exact Alph
72
72
  Install into the `web` profile. Replace the profile name if needed:
73
73
 
74
74
  ```sh
75
- dsh plugin --profile web add --save-exact @nanmicoder/dsh-agent-teams@0.1.17
75
+ dsh plugin --profile web add --save-exact @nanmicoder/dsh-agent-teams@0.1.20
76
76
  ```
77
77
 
78
78
  **After installation, stop and restart Harness for that profile, then refresh the browser.**
79
79
 
80
- The default npm `latest` tag points to `0.1.17`, so `dsh plugin --profile web add @nanmicoder/dsh-agent-teams` installs this version on a fresh profile. Use the exact-version command above to pin it. The recommended Harness version is `0.1.5-rc.1`; installing the plugin does not upgrade the host. See the [source installation guide](./docs/maintenance-workflow.md) and [release verification](./docs/releases/v0.1.17/README.md).
80
+ The default npm `latest` tag points to `0.1.20`, so `dsh plugin --profile web add @nanmicoder/dsh-agent-teams` installs this version on a fresh profile. Use the exact-version command above to pin it. The recommended Harness version is `0.1.5-rc.1`; installing the plugin does not upgrade the host. See the [source installation guide](./docs/maintenance-workflow.md) and [release verification](./docs/releases/v0.1.19/README.md).
81
81
 
82
82
  > Desktop users must check the app's embedded Harness core; upgrading the global CLI does not upgrade it. For older `0.1.0-*` / `0.1.1-*` or unlisted hosts, keep a working pair and follow the [older-version and diagnostic guide](./docs/maintenance-workflow.md).
83
83
 
@@ -100,7 +100,7 @@ Team state is stored under `<workspace>/.agent-teams/`; the Web panel reads that
100
100
 
101
101
  Member creation is zero-interaction by default: a member on the captain's current LLM route snapshots that provider, model, and reasoning effort, while a member on a requested alternative route snapshots the target model's default effort; later continuations restore the resolved snapshot. Only an explicit heterogeneous-team request (for example, “backend on provider A/model X, frontend on provider B/model Y”) supplies a member-specific `provider` + `model`; there is no per-member model or reasoning prompt.
102
102
 
103
- Captain sessions keep the concise core protocol and the original 13 native team tools from their first request. All business tools are directly available; no loading tool or extra activation call is needed. Configured profiles retain their bounded directory in the fixed system prompt. Creating, approving, continuing or ending a team does not rewrite the system prompt or tool schemas. Core rules remain available after history compaction or discarded code-mode tool results. Members receive four team tools, fixed member instructions, and their ordinary coding/research tools. Web approval wakes the captain with a control message; later member reports wake it again. See the [fixed protocol and benchmark contract](./docs/progressive-loading.md).
103
+ Captain sessions keep the concise core protocol and the original 14 native team tools from their first request. All business tools are directly available; no loading tool or extra activation call is needed. Configured profiles retain their bounded directory in the fixed system prompt. Creating, approving, continuing or ending a team does not rewrite the system prompt or tool schemas. Core rules remain available after history compaction or discarded code-mode tool results. Members receive four team tools, fixed member instructions, and their ordinary coding/research tools. Web approval wakes the captain with a control message; later member reports wake it again. See the [fixed protocol and benchmark contract](./docs/progressive-loading.md).
104
104
 
105
105
  ## Slash command
106
106
 
package/README_ZH.md CHANGED
@@ -22,7 +22,7 @@
22
22
 
23
23
  `dsh-agent-teams` 让当前 DeepSeek Harness 会话成为队长:创建可续聊的子 Agent、把目标拆成有依赖的任务,并通过直达消息协调成员工作。
24
24
 
25
- 你只需用自然语言提出目标。插件会提供精简的固定团队协议、13 个业务工具、持久化状态、自动共享任务调度和实时 Web UI,不需要额外的 Workflow 引擎。
25
+ 你只需用自然语言提出目标。插件会提供精简的固定团队协议、14 个业务工具、持久化状态、自动共享任务调度和实时 Web UI,不需要额外的 Workflow 引擎。
26
26
 
27
27
  <p align="center">
28
28
  <img src="./assets/ui.png" width="100%" alt="DeepSeek Harness 对话与 AgentTeams 实时活动面板,展示成员、任务依赖和回报">
@@ -30,7 +30,7 @@
30
30
 
31
31
  ## 版本更新
32
32
 
33
- [v0.1.17](./release-notes/v0.1.17.md) 新增浅色、深色和跟随系统的主题适配,作为 npm `latest` 默认安装版本。活动面板、对话卡片与弹窗控件自动继承宿主颜色。推荐 DeepSeek Harness `0.1.5-rc.1`,保留三个旧宿主目标。
33
+ [v0.1.20](./release-notes/v0.1.20.md) 仅把本文档中的版本引用同步到 npm `latest`,不含任何代码或行为变更,打包产物与 `0.1.19` 完全一致。上一份有实质变更的版本是 [v0.1.19](./release-notes/v0.1.19.md):宿主禁用或改名委派工具时成员仍能正常启动;自动修复范围改为从 `requiredFix` 推导;新增队长专用的 `agent_teams_amend_task`。推荐 DeepSeek Harness `0.1.5-rc.1`,保留三个旧宿主目标。
34
34
 
35
35
  ## 为什么需要 AgentTeams?
36
36
 
@@ -48,14 +48,14 @@
48
48
 
49
49
  ## 安装与版本选择
50
50
 
51
- **推荐组合:DeepSeek Harness `0.1.5-rc.1` + AgentTeams `0.1.17`。Harness 仍为预发布版本。**
51
+ **推荐组合:DeepSeek Harness `0.1.5-rc.1` + AgentTeams `0.1.20`。Harness 仍为预发布版本。**
52
52
 
53
53
  | 使用场景 | DeepSeek Harness | AgentTeams 插件 |
54
54
  | --- | --- | --- |
55
- | **推荐安装** | **`0.1.5-rc.1`** | **`0.1.17`** |
56
- | 保留旧 RC | `0.1.2-rc.1` | `0.1.17` |
57
- | 开发者测试 Alpha | `0.1.2-alpha.5` | `0.1.17` |
58
- | 保留旧 Alpha | `0.1.2-alpha.2` | `0.1.17` |
55
+ | **推荐安装** | **`0.1.5-rc.1`** | **`0.1.20`** |
56
+ | 保留旧 RC | `0.1.2-rc.1` | `0.1.20` |
57
+ | 开发者测试 Alpha | `0.1.2-alpha.5` | `0.1.20` |
58
+ | 保留旧 Alpha | `0.1.2-alpha.2` | `0.1.20` |
59
59
 
60
60
  ### 1. 安装 DeepSeek Harness
61
61
 
@@ -71,12 +71,12 @@ dsh --version
71
71
  安装到 `web` profile;使用其他 profile 时替换名称:
72
72
 
73
73
  ```sh
74
- dsh plugin --profile web add --save-exact @nanmicoder/dsh-agent-teams@0.1.17
74
+ dsh plugin --profile web add --save-exact @nanmicoder/dsh-agent-teams@0.1.20
75
75
  ```
76
76
 
77
77
  **安装后,停止并重新启动该 profile 的 Harness 进程,再刷新浏览器。**
78
78
 
79
- npm 默认标签 `latest` 现指向 `0.1.17`,因此新 profile 使用 `dsh plugin --profile web add @nanmicoder/dsh-agent-teams` 即可安装本版;需要锁定版本时使用上面的精确版本命令。推荐宿主为 Harness `0.1.5-rc.1`,安装插件不会自动升级宿主。源码安装见[维护指南](./docs/maintenance-workflow.md),验证范围见[本版验收记录](./docs/releases/v0.1.17/README.md)。
79
+ npm 默认标签 `latest` 现指向 `0.1.20`,因此新 profile 使用 `dsh plugin --profile web add @nanmicoder/dsh-agent-teams` 即可安装本版;需要锁定版本时使用上面的精确版本命令。推荐宿主为 Harness `0.1.5-rc.1`,安装插件不会自动升级宿主。源码安装见[维护指南](./docs/maintenance-workflow.md),验证范围见[本版验收记录](./docs/releases/v0.1.19/README.md)。
80
80
 
81
81
  > Desktop 用户需核对应用内置的 Harness 核心;全局 CLI 升级不会升级桌面内核。旧 `0.1.0-*` / `0.1.1-*` 或其他未列出的宿主,请先保留已工作的组合,参考[旧版本与诊断指引](./docs/maintenance-workflow.md)。
82
82
 
@@ -86,7 +86,7 @@ npm 默认标签 `latest` 现指向 `0.1.17`,因此新 profile 使用 `dsh plu
86
86
 
87
87
  > 使用 AgentTeams 审查 v0.5.3 之后的提交,分别从性能、安全和产品角度分工,最后输出一份汇总报告。
88
88
 
89
- 队长会话从第一轮起保留精简的固定核心协议和原有 13 个业务工具,直接调用,无需额外的加载工具或激活调用。已配置模板的精简目录固定保留。创建、批准、继续或结束团队都不会改写系统提示词和工具 schema,会话压缩或代码模式丢弃工具结果也不会丢失核心规则。成员保留四个团队工具、固定成员说明及普通编程/研究工具。Web 批准后会通知并唤醒队长,后续成员报告会再次唤醒它,无需忙轮询。详见[固定协议与 benchmark 标准](./docs/progressive-loading.md)。
89
+ 队长会话从第一轮起保留精简的固定核心协议和原有 14 个业务工具,直接调用,无需额外的加载工具或激活调用。已配置模板的精简目录固定保留。创建、批准、继续或结束团队都不会改写系统提示词和工具 schema,会话压缩或代码模式丢弃工具结果也不会丢失核心规则。成员保留四个团队工具、固定成员说明及普通编程/研究工具。Web 批准后会通知并唤醒队长,后续成员报告会再次唤醒它,无需忙轮询。详见[固定协议与 benchmark 标准](./docs/progressive-loading.md)。
90
90
 
91
91
  ## 工作方式
92
92
 
@@ -1,5 +1,6 @@
1
1
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
2
2
  import { SubagentError } from '@deepseek-ai/dsh-subagent';
3
+ import { CAPTAIN_TOOL_NAMES } from "./tool-names.js";
3
4
  /**
4
5
  * Exact protocol exported by dsh-subagent/internal in Alpha.5 and rc.1.
5
6
  * That subpath does not exist in Alpha.2, so importing it statically prevents
@@ -199,3 +200,86 @@ export function guardSubagentDelivery(ctx, isRetired) {
199
200
  };
200
201
  }, 'agent-teams: retired member guard');
201
202
  }
203
+ /**
204
+ * Names the host's `tools.restrict()` would admit for this agent's layer
205
+ * chain, or undefined when the host does not expose its registry view.
206
+ * restrict() rejects unknown names with a hard error, so the spawn path must
207
+ * check entries against this view before forwarding them. Source:
208
+ * dsh-tools view()/restrict() in 0.1.5-rc.1; older Harness generations lack
209
+ * the surface and keep the verbatim list.
210
+ */
211
+ export function restrictableToolNames(agent) {
212
+ const tools = agent?.ctx?.tools;
213
+ if (typeof tools?.view !== 'function')
214
+ return undefined;
215
+ try {
216
+ const names = tools.view.call(tools, agent.ctx)?.restrictableNames;
217
+ return names instanceof Set ? names : undefined;
218
+ }
219
+ catch {
220
+ return undefined;
221
+ }
222
+ }
223
+ /**
224
+ * Member `toolFilter` for one spawn. Captain-only names are registered by
225
+ * this plugin itself and always resolvable; the depth-related entries name
226
+ * HOST tools and are only known under some compositions, so they must be
227
+ * resolved against the running host's registry: the delegation tool name is
228
+ * host configuration (dsh-tool-subagent `toolName`, default `subagent`,
229
+ * renamed per composition, e.g. `subagent_fork` in web-style profiles), and
230
+ * forwarding a name the host dropped aborts every member spawn (#163, #164).
231
+ * Depth enforcement itself does not depend on these names —
232
+ * installMemberDelegationGuard bounds descendant creation by parent chain.
233
+ */
234
+ export function memberToolFilter(maxDepth, knownTools) {
235
+ const depthDeny = maxDepth === 0 ? ['subagent', 'send_message'] : [];
236
+ return {
237
+ deny: [
238
+ ...CAPTAIN_TOOL_NAMES,
239
+ ...(knownTools === undefined ? depthDeny : depthDeny.filter(name => knownTools.has(name))),
240
+ ],
241
+ };
242
+ }
243
+ /**
244
+ * Names a `tools.restrict()` rejection reported as unknown, or an empty list
245
+ * for every other failure. The host names the offenders verbatim
246
+ * (`tools.restrict() names unknown global tool "x"; known global tools: …`);
247
+ * only that report may relax a filter (#164, #166).
248
+ */
249
+ function unknownToolNames(error) {
250
+ const message = error instanceof Error ? error.message : String(error);
251
+ if (!message.includes('unknown global tool'))
252
+ return [];
253
+ return [...message.matchAll(/"([^"]+)"/g)].map(match => match[1]);
254
+ }
255
+ /**
256
+ * Start one member, dropping any filter name the host reports as unknown and
257
+ * retrying.
258
+ *
259
+ * Resolving the filter against the registry view ({@link memberToolFilter})
260
+ * only covers hosts that expose one. A rejected filter still leaves the member
261
+ * without a session for the lifetime of the team, so the start must survive the
262
+ * cases that resolution cannot see — no registry view, a name that is known but
263
+ * not restrictable, a composition that mounted differently than expected. The
264
+ * host names the offenders in its rejection, so they are removed and the start
265
+ * retried; the retry only runs while the deny list strictly shrinks, and any
266
+ * failure that is not an unknown-name report propagates untouched.
267
+ * @param start - performs one start attempt for the given `toolFilter`.
268
+ * @param filter - the filter to attempt first.
269
+ * @returns the started member.
270
+ */
271
+ export async function startMemberWithLenientFilter(start, filter) {
272
+ let deny = [...filter.deny];
273
+ for (;;) {
274
+ try {
275
+ return await start({ deny });
276
+ }
277
+ catch (error) {
278
+ const unknown = unknownToolNames(error);
279
+ const remaining = deny.filter(name => !unknown.includes(name));
280
+ if (unknown.length === 0 || remaining.length === deny.length)
281
+ throw error;
282
+ deny = remaining;
283
+ }
284
+ }
285
+ }
package/lib/index.js CHANGED
@@ -94,7 +94,7 @@ export function usageSectionText(toolNames, profilesText = '') {
94
94
  4. Respect Web approve/return/discard control messages. On return, ask what to change before editing; after the answer, use one atomic agent_teams_edit_plan batch (edit downstream references before removals), summarize and await review again. Never inspect or edit .agent-teams state files or plugin source code to revise plans. Discard does not authorize a replacement.
95
95
  5. The scheduler dispatches ready tasks after approval. Delegate; do not duplicate slow work or send messages merely to start a stage. Handle reports/user work, then yield when waiting is all that remains: reports wake you automatically. Use status after a delivery or user request, never busy-poll or wait for unassigned members.
96
96
  6. Tasks carry attempt_id capabilities. Use the current attempt_id; stale means ownership changed. Pause members only on explicit request; later guidance via send_message continues that same attempt. Retry, transfer or take over through reassign_task first; it revokes the old attempt and waits for quiescence. Prefer a member. Captain implementation/review takeover requires a user request. Every takeover is one ready task at a time, finished in this turn; never yield with captain-owned work open.
97
- 7. In a running team, correct never-started pending tasks with edit_plan update_task; preserve dependency and ownership contracts instead of cancelling and recreating the graph. A captain can cancel a never-started pending task directly. Active attempts still require reassign_task. Quality kinds (requirements, implementation, verification, review, repair, integration) require objective + acceptance; implementation/repair also require inScope + verify. Derive paths/commands from the workspace/profile, never assume src/ or pnpm test. Review/requirements complete only with verdict=pass; needs_revision/reject fail with findings. Never approve your own implementation or ask for a deliberate failure.
97
+ 7. In a running team, correct never-started pending tasks with edit_plan update_task; preserve dependency and ownership contracts instead of cancelling and recreating the graph. A captain can cancel a never-started pending task directly. Active attempts still require reassign_task. Quality kinds (requirements, implementation, verification, review, repair, integration) require objective + acceptance; implementation/repair also require inScope + verify. Derive paths/commands from the workspace/profile, never assume src/ or pnpm test. Review/requirements complete only with verdict=pass; needs_revision/reject fail with findings. Never approve your own implementation or ask for a deliberate failure. When a quality contract itself is wrong (a verify command that cannot pass, an inScope that forbids the file the objective requires), fix it with agent_teams_amend_task — captain-only, non-terminal tasks only, frozen after a passing review, and every amendment is recorded in the task's revisions ledger — instead of letting the worker dead-lock or game the gate.
98
98
  8. When full quality mode is requested: requirements → implementation → verification → review → integration. Plan the entire DAG while staged, including implementation before requirements finishes and integration depending on review round 1. Failed review automatically adds repair + next review and rewires pending downstream gates. Do not recreate this loop, omit integration or depend on a failed task. Review acceptance judges the latest implementation. Do not put smoke-test scripts into task instructions.
99
99
  9. Halted means the user stopped work (including the captain turn). Resume only on a later explicit user request with a reason, via agent_teams_resume or create_task({resume:true,resumeReason}); creating tasks alone never resumes. Escalated means the review loop hit its limit, not a halt. Deployment requires explicit user confirmation.
100
100
  10. Wait for all required tasks to be terminal and members idle/ready, present results, then delete/archive unless the user wants to continue. Never discard unfinished work without authorization.
package/lib/members.js CHANGED
@@ -16,16 +16,13 @@ import { installModelSelection } from '@deepseek-ai/dsh-agent';
16
16
  import { foldSubagentDescriptor } from '@deepseek-ai/dsh-subagent';
17
17
  import { createUserMessage, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
18
18
  import { join } from 'node:path';
19
- import { guardSubagentDelivery, installContinuableMemberSetup, queueMemberPrompt, steerMemberPrompt, sessionOwnEvents } from "./harness-compat.js";
19
+ import { guardSubagentDelivery, installContinuableMemberSetup, memberToolFilter, queueMemberPrompt, restrictableToolNames, startMemberWithLenientFilter, steerMemberPrompt, sessionOwnEvents } from "./harness-compat.js";
20
20
  import { markMailboxDelivered, appendMailbox, CAPTAIN_KEY, createMessage, readRetiredMemberIds, readTeamSync, readTeam, releaseMailboxDelivery, withTeamLock, writeTeam } from "./state.js";
21
21
  import { mailboxPrompt } from "./mailbox.js";
22
22
  import { appendTeamEvent, captainSessionOf } from "./events.js";
23
23
  import { TERMINAL_TASK_STATUSES } from "./types.js";
24
- import { CAPTAIN_TOOL_NAMES } from "./tool-names.js";
25
24
  /** Persona snapshot of a profile protocol; the full text lives on team.json. */
26
25
  export const PERSONA_PROTOCOL_MAX_CHARS = 400;
27
- /** Captain-only AgentTeams tools hidden from newly spawned members. */
28
- const MEMBER_DENIED_TOOLS = CAPTAIN_TOOL_NAMES;
29
26
  /**
30
27
  * Restore the SessionId brand on a value that round-tripped through the
31
28
  * durable team file. The brand is erased by JSON serialization; the value
@@ -514,14 +511,19 @@ export async function spawnMember(ctx, config, selections, llmSelection, captain
514
511
  throw new Error(`agent-teams: provider "${config.provider}" cannot restrict captain-only tools for members`);
515
512
  }
516
513
  const label = `${MEMBER_LABEL_PREFIX}${team.id}:${member.name}`;
517
- const start = await selections.withPending(captain.id, label, llmSelection, () => (ctx.subagents.startContinuable({
514
+ const start = await selections.withPending(captain.id, label, llmSelection, () => (startMemberWithLenientFilter(toolFilter => ctx.subagents.startContinuable({
518
515
  provider: config.provider,
519
516
  label,
520
517
  request: {
521
518
  prompt: [{ type: 'text', text: initialPrompt ?? memberWelcome(team, member.name) }],
522
519
  parent: captain,
523
520
  persona: memberPersona(team, member, stateDir, config.executionPrompt),
524
- toolFilter: { deny: [...MEMBER_DENIED_TOOLS, ...(config.maxDepth === 0 ? ['subagent', 'send_message'] : [])] },
521
+ // Depth deny entries name host tools; resolve them against the
522
+ // running host's registry so a renamed delegation tool cannot abort
523
+ // the spawn (see memberToolFilter). Depth is still enforced by the
524
+ // parent-chain guard installed at plugin mount. A filter the host
525
+ // still rejects is retried without the names it reported.
526
+ toolFilter,
525
527
  agentOptions: {
526
528
  provider: llmSelection.provider,
527
529
  model: llmSelection.model,
@@ -534,7 +536,7 @@ export async function spawnMember(ctx, config, selections, llmSelection, captain
534
536
  // the configured member-relative descendant budget instead.
535
537
  },
536
538
  signal,
537
- })));
539
+ }), memberToolFilter(config.maxDepth, restrictableToolNames(captain)))));
538
540
  member.id = start.childId;
539
541
  }
540
542
  /**
@@ -3,7 +3,7 @@
3
3
  * coverage, and resume. Tools and persistence call these; they do not I/O.
4
4
  * @module dsh-agent-teams/quality-gates
5
5
  */
6
- import { FINDING_SEVERITIES, REVIEW_VERDICTS, TASK_KINDS, } from "./types.js";
6
+ import { FINDING_SEVERITIES, REVIEW_VERDICTS, TASK_KINDS, TERMINAL_TASK_STATUSES, } from "./types.js";
7
7
  const QUALITY_KINDS = [
8
8
  'requirements',
9
9
  'implementation',
@@ -405,6 +405,140 @@ function unresolvedFindings(task) {
405
405
  function findingKey(ids) {
406
406
  return [...ids].sort().join(',');
407
407
  }
408
+ /**
409
+ * Path-like tokens worth considering as repair-scope candidates. Two shapes:
410
+ * slash paths (`src/parser.ts`, `docs/guide.md`) and bare filenames with a
411
+ * known code/doc extension (`README.md`, `wc.js`). An optional `:line`
412
+ * suffix is tolerated and stripped. The extension allowlist keeps version
413
+ * tokens (`v0.1.17`), hex hashes, and prose out of the derived scope.
414
+ */
415
+ const REPAIR_SCOPE_PATH_PATTERN = /(?:[A-Za-z0-9_.\-]+(?:\/[A-Za-z0-9_.\-]+)+|[\w.\-]+\.(?:tsx?|jsx?|mjs|cjs|json|md|txt|ya?ml|py|rs|go|java|html?|css|scss|sh|ps1|toml|xml|sql))(?::\d+)?/gu;
416
+ const REPAIR_SCOPE_LINE_SUFFIX = /:\d+$/;
417
+ /**
418
+ * Derive the repair round's inScope from the findings that caused it.
419
+ *
420
+ * `finding.file` records where the problem was OBSERVED, but the fix often
421
+ * targets a different file named in `requiredFix` (docs vs sample data,
422
+ * config vs code). Deriving the scope from both keeps the auto-generated
423
+ * repair contract satisfiable; deriving from `file` alone can produce a
424
+ * contract where the acceptance ("edit README.md") names a path the scope
425
+ * forbids, so no honest completion exists and the repair dead-locks.
426
+ *
427
+ * Absolute and otherwise illegal paths are dropped (they can never match
428
+ * workspace-relative scope patterns anyway); when nothing legal remains,
429
+ * the source task's own inScope is kept as the fallback. Over-inclusion is
430
+ * accepted: inScope is an audit upper bound, and the requiredFix text still
431
+ * tells the implementer what to touch.
432
+ *
433
+ * Keep all derived paths until the generator resolves inherited exclusions;
434
+ * filtering first would silently discard a required fix target.
435
+ */
436
+ export function repairScopeFromFindings(findings, fallback) {
437
+ const derived = [];
438
+ const push = (raw) => {
439
+ const normalized = normalizeWorkspacePath(raw.replace(REPAIR_SCOPE_LINE_SUFFIX, ''));
440
+ if (normalized === undefined || derived.includes(normalized))
441
+ return;
442
+ derived.push(normalized);
443
+ };
444
+ for (const finding of findings) {
445
+ if (nonemptyString(finding.file))
446
+ push(finding.file);
447
+ for (const match of finding.requiredFix.matchAll(REPAIR_SCOPE_PATH_PATTERN))
448
+ push(match[0]);
449
+ }
450
+ return derived.length > 0 ? derived : fallback === undefined ? undefined : [...new Set(fallback)];
451
+ }
452
+ const AMENDABLE_CONTRACT_FIELDS = ['objective', 'acceptance', 'verify', 'inScope', 'outOfScope'];
453
+ /**
454
+ * Controlled contract amendment (the pure rule; tooling keeps it
455
+ * captain-only). When a quality contract is wrong — a verify command that
456
+ * cannot pass, an inScope that forbids the file the objective names — the
457
+ * worker has no honest completion and either dead-locks or games the gate.
458
+ * Instead the captain may fix the contract mid-flight: every amendment is
459
+ * recorded on the task as a {@link TaskRevision} (previous values + reason),
460
+ * and once a review/requirements task has passed judgment on this task the
461
+ * contract is frozen. Amendments replace whole fields (lists are full
462
+ * replacements, not deltas); the implementer re-reads the amended contract
463
+ * before its next quality gate. Completion gates need no special casing:
464
+ * they read the task's current fields, so they naturally evaluate the
465
+ * amended contract.
466
+ */
467
+ export function amendTaskContract(team, task, input, by, reason) {
468
+ if (!nonemptyString(by))
469
+ return { ok: false, error: 'contract amendment requires a non-empty author identity' };
470
+ if (!nonemptyString(reason))
471
+ return { ok: false, error: 'contract amendment requires a non-empty reason' };
472
+ if (TERMINAL_TASK_STATUSES.includes(task.status)) {
473
+ return { ok: false, error: `task ${task.id} is ${task.status}; terminal contracts are immutable` };
474
+ }
475
+ if (taskKindOf(task) === 'work') {
476
+ return { ok: false, error: `task ${task.id} has kind=work and no contract to amend` };
477
+ }
478
+ if (!AMENDABLE_CONTRACT_FIELDS.some((field) => input[field] !== undefined)) {
479
+ return { ok: false, error: `amendment requires at least one of: ${AMENDABLE_CONTRACT_FIELDS.join(', ')}` };
480
+ }
481
+ const next = {};
482
+ const previous = {};
483
+ if (input.objective !== undefined) {
484
+ if (!nonemptyString(input.objective)) {
485
+ return { ok: false, error: 'amended objective must be a non-empty string' };
486
+ }
487
+ next['objective'] = input.objective;
488
+ previous['objective'] = task.objective;
489
+ }
490
+ for (const field of ['acceptance', 'verify']) {
491
+ const value = input[field];
492
+ if (value === undefined)
493
+ continue;
494
+ if (!nonemptyStringList(value)) {
495
+ return { ok: false, error: `amended ${field} must be a non-empty list of non-empty strings` };
496
+ }
497
+ next[field] = value;
498
+ previous[field] = task[field];
499
+ }
500
+ for (const field of ['inScope', 'outOfScope']) {
501
+ const value = input[field];
502
+ if (value === undefined)
503
+ continue;
504
+ if (!nonemptyStringList(value)) {
505
+ return { ok: false, error: `amended ${field} must be a non-empty list of non-empty strings` };
506
+ }
507
+ for (const entry of value) {
508
+ if (normalizeWorkspacePath(entry) === undefined) {
509
+ return {
510
+ ok: false,
511
+ error: `amended ${field} entry "${entry}" is not a workspace-relative path (absolute paths and ".." can never match scope patterns)`,
512
+ };
513
+ }
514
+ }
515
+ next[field] = value;
516
+ previous[field] = task[field];
517
+ }
518
+ const reviewPassed = team.tasks.some((item) => ((taskKindOf(item) === 'review' || taskKindOf(item) === 'requirements')
519
+ && item.reviewedTaskId === task.id
520
+ && item.verdict === 'pass'));
521
+ if (reviewPassed) {
522
+ return { ok: false, error: `task ${task.id} already passed review; its contract is frozen` };
523
+ }
524
+ const revision = {
525
+ at: Date.now(),
526
+ by,
527
+ reason,
528
+ fields: Object.keys(next),
529
+ previous,
530
+ };
531
+ return {
532
+ ok: true,
533
+ revision,
534
+ task: {
535
+ ...task,
536
+ ...next,
537
+ revisions: [...(task.revisions ?? []), revision],
538
+ updatedAt: Date.now(),
539
+ },
540
+ };
541
+ }
408
542
  const CAPTAIN_ASSIGNEE = 'captain';
409
543
  const OPEN_FOLLOW_UP_STATUSES = ['pending', 'claimed', 'in_progress'];
410
544
  function schedulableAssignee(preferred, team, forbidden) {
@@ -430,6 +564,13 @@ function hasOpenFollowUp(team, sourceTaskId, findingIds) {
430
564
  && findingKey(item.sourceFindingIds ?? []) === key
431
565
  && OPEN_FOLLOW_UP_STATUSES.includes(item.status)));
432
566
  }
567
+ /** Drop previous-round exclusions that intersect this generated repair's scope. */
568
+ function withoutScopeConflicts(outOfScope, inScope) {
569
+ if (outOfScope === undefined)
570
+ return undefined;
571
+ const conflicting = new Set(inScopeOverlap(outOfScope, inScope ?? []));
572
+ return outOfScope.filter((pattern) => !conflicting.has(pattern));
573
+ }
433
574
  export function planQualityFollowUp(team, closed) {
434
575
  const empty = { created: [], tasks: [] };
435
576
  const kind = taskKindOf(closed);
@@ -468,7 +609,9 @@ export function planQualityFollowUp(team, closed) {
468
609
  if (countRepairAttempts(team, sourceId, findingIds) >= policy.maxRepairAttempts) {
469
610
  return { ...empty, escalated: true, status: 'escalated' };
470
611
  }
471
- const files = findings.map((finding) => finding.file).filter((file) => nonemptyString(file));
612
+ // inScope is derived from the findings below: the observed file plus any
613
+ // workspace-relative paths referenced by the requiredFix instructions.
614
+ const repairScope = repairScopeFromFindings(findings, source?.inScope);
472
615
  const implementer = schedulableAssignee(source?.assignee, team);
473
616
  const repair = {
474
617
  id: `repair-round-${nextRound}`,
@@ -478,8 +621,8 @@ export function planQualityFollowUp(team, closed) {
478
621
  dependencies: [sourceId],
479
622
  round: nextRound,
480
623
  objective: source?.objective ?? closed.objective ?? `Fix findings from ${sourceId}`,
481
- inScope: files.length > 0 ? files : source?.inScope,
482
- outOfScope: source?.outOfScope,
624
+ inScope: repairScope,
625
+ outOfScope: withoutScopeConflicts(source?.outOfScope, repairScope),
483
626
  verify: source?.verify,
484
627
  acceptance: findings.map((finding) => finding.requiredFix),
485
628
  sourceTaskId: sourceId,
@@ -615,6 +758,15 @@ export function isCommandResult(value) {
615
758
  && (value['exitCode'] === undefined || (Number.isSafeInteger(value['exitCode'])))
616
759
  && (value['evidence'] === undefined || typeof value['evidence'] === 'string');
617
760
  }
761
+ export function isTaskRevision(value) {
762
+ if (!isRecord(value))
763
+ return false;
764
+ return Number.isSafeInteger(value['at'])
765
+ && nonemptyString(value['by'])
766
+ && nonemptyString(value['reason'])
767
+ && nonemptyStringList(value['fields'])
768
+ && isRecord(value['previous']);
769
+ }
618
770
  // Optional fields whose persisted values must be non-empty when present
619
771
  // (mirrors the checks in hasValidQualityTaskFields). Some models materialize
620
772
  // optional tool parameters as "" instead of omitting them (e.g. GPT-5.6
@@ -698,6 +850,10 @@ export function hasValidQualityTaskFields(value) {
698
850
  if (!Array.isArray(value['commandsRun']) || !value['commandsRun'].every(isCommandResult))
699
851
  return false;
700
852
  }
853
+ if (value['revisions'] !== undefined) {
854
+ if (!Array.isArray(value['revisions']) || !value['revisions'].every(isTaskRevision))
855
+ return false;
856
+ }
701
857
  return true;
702
858
  }
703
859
  function isRecord(value) {
package/lib/state.js CHANGED
@@ -18,7 +18,7 @@ import { mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promise
18
18
  import { join } from 'node:path';
19
19
  import { TERMINAL_TASK_STATUSES } from "./types.js";
20
20
  import { hasValidQualityTaskFields, isReviewPolicy, normalizeBlankOptionalTaskFields } from "./quality-gates.js";
21
- export { buildCoverageMatrix, canDeclareDelivery, classifyChangedPath, collectChangedPaths, defaultQualityDeliveryGraph, describeQualityLoop, evaluateQualityCompletion, hasValidQualityTaskFields, isQualityKind, normalizeBlankOptionalTaskFields, pathMatchesScope, planQualityFollowUp, qualityPlanningPrompt, resumeTeamState, sanitizeReviewAcceptance, sanitizeReviewObjective, taskKindOf, validateCreateTask, } from "./quality-gates.js";
21
+ export { amendTaskContract, buildCoverageMatrix, canDeclareDelivery, classifyChangedPath, collectChangedPaths, defaultQualityDeliveryGraph, describeQualityLoop, evaluateQualityCompletion, hasValidQualityTaskFields, isQualityKind, isTaskRevision, normalizeBlankOptionalTaskFields, pathMatchesScope, planQualityFollowUp, qualityPlanningPrompt, resumeTeamState, sanitizeReviewAcceptance, sanitizeReviewObjective, taskKindOf, validateCreateTask, } from "./quality-gates.js";
22
22
  /** Mailbox key of the captain. */
23
23
  export const CAPTAIN_KEY = 'captain';
24
24
  /** A crashed live-delivery attempt becomes retryable after this interval. */
@@ -612,6 +612,7 @@ function isTeamMember(value) {
612
612
  && isOptionalString(value['reasoningEffort'])
613
613
  && isOptionalString(value['activeProvider'])
614
614
  && isOptionalString(value['activeModel'])
615
+ && isOptionalString(value['spawnError'])
615
616
  && (value['executionPrompt'] === undefined || typeof value['executionPrompt'] === 'string')
616
617
  && (value['fallback'] === undefined || (isRecord(value['fallback']) && typeof value['fallback']['provider'] === 'string' && typeof value['fallback']['model'] === 'string'))
617
618
  && (value['fallbackActive'] === undefined || typeof value['fallbackActive'] === 'boolean')
package/lib/tool-names.js CHANGED
@@ -3,6 +3,7 @@ export const TEAM_TOOL_NAMES = [
3
3
  'agent_teams_create', 'agent_teams_approve', 'agent_teams_edit_plan',
4
4
  'agent_teams_add_member', 'agent_teams_remove_member', 'agent_teams_create_task',
5
5
  'agent_teams_reassign_task', 'agent_teams_claim_task', 'agent_teams_update_task',
6
+ 'agent_teams_amend_task',
6
7
  'agent_teams_send_message', 'agent_teams_status', 'agent_teams_resume', 'agent_teams_delete',
7
8
  ];
8
9
  export const MEMBER_TOOL_NAMES = [
package/lib/tools.js CHANGED
@@ -12,7 +12,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm';
12
12
  import { defineTool } from '@deepseek-ai/dsh-tools';
13
13
  import { join } from 'node:path';
14
14
  import { appendTeamEvent, captainSessionOf } from "./events.js";
15
- import { acknowledgeMailbox, markMailboxDelivered, discardMailboxMessages, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
15
+ import { amendTaskContract, acknowledgeMailbox, markMailboxDelivered, discardMailboxMessages, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
16
16
  import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime, installMemberDelegationGuard, memberActivity, resolveMemberLlmSelection, spawnMember, steerCaptainReport, validateMemberLlmSelections, } from "./members.js";
17
17
  import { TERMINAL_TASK_STATUSES } from "./types.js";
18
18
  import { collectCompletedDependencyOutputs, formatDependencyOutputs, installTeamScheduler } from "./scheduler.js";
@@ -286,6 +286,24 @@ export function registerAgentTeamsTools(ctx, config) {
286
286
  const memberSelections = installMemberSelectionRuntime(ctx, config.stateDir, (workspace, teamId, memberName) => (scheduler.kickMember(workspace, teamId, memberName)));
287
287
  async function dispatchMember(captain, teamId, memberName, text, signal, mode, attemptId) {
288
288
  const root = stateRootOf(workspaceOf(captain), config);
289
+ // Record why a member never started. The scheduler treats a failed dispatch as
290
+ // "not now" and retries, so without this the captain only ever sees an
291
+ // unexplained `unspawned` member and no diagnostic reaches any surface.
292
+ const recordSpawnError = async (reason) => {
293
+ try {
294
+ await withTeamLock(teamLockKey(root, teamId), async () => {
295
+ const fresh = await requireFreshCaptainTeam(root, teamId, captain.id);
296
+ const failed = fresh.members.find(item => item.name === memberName && item.status !== 'removed');
297
+ if (failed === undefined || failed.id !== '')
298
+ return;
299
+ failed.spawnError = reason;
300
+ await writeTeam(root, fresh);
301
+ });
302
+ }
303
+ catch (error) {
304
+ ctx.logger.warn(`agent-teams: could not record the member start failure for ${memberName}: ${String(error)}`);
305
+ }
306
+ };
289
307
  let orphan;
290
308
  try {
291
309
  return await withTeamLock(teamLockKey(root, teamId), async () => {
@@ -304,6 +322,7 @@ export function registerAgentTeamsTools(ctx, config) {
304
322
  }, signal);
305
323
  await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, team, member, config.stateDir, signal, text);
306
324
  orphan = { ...member };
325
+ delete member.spawnError;
307
326
  await writeTeam(root, team);
308
327
  orphan = undefined;
309
328
  return true;
@@ -314,7 +333,12 @@ export function registerAgentTeamsTools(ctx, config) {
314
333
  await recordRetiredMemberIds(root, [orphan.id]);
315
334
  await stopTeamMemberActivations(ctx, captain, [orphan]);
316
335
  }
336
+ // The stack carries the failing frame; the message alone rarely does.
337
+ let reason = String(error);
338
+ if (error instanceof Error && typeof error.stack === 'string' && error.stack !== '')
339
+ reason = error.stack;
317
340
  ctx.logger.warn(`agent-teams: member dispatch failed for ${memberName}: ${String(error)}`);
341
+ await recordSpawnError(reason);
318
342
  return false;
319
343
  }
320
344
  }
@@ -1641,6 +1665,86 @@ export function registerAgentTeamsTools(ctx, config) {
1641
1665
  return updated;
1642
1666
  },
1643
1667
  }));
1668
+ ctx.tools.register(defineTool({
1669
+ name: 'agent_teams_amend_task',
1670
+ description: 'Captain-only controlled contract amendment for one non-terminal quality task: replace a wrong objective/acceptance/verify/inScope/outOfScope when the original contract makes honest completion impossible (for example a verify command that cannot pass, or an inScope that forbids the file the objective names). The amendment is appended to the task\'s revisions ledger with previous values and the reason, and is rejected once a review/requirements task has passed judgment on this task. Members cannot amend contracts; the implementer re-reads the amended contract before its next quality gate. Lists are full replacements, not deltas.',
1671
+ parameters: {
1672
+ task_id: { type: 'string', required: true, description: 'Task whose contract is being amended.' },
1673
+ reason: { type: 'string', required: true, description: 'Why the current contract is wrong; recorded in the revisions ledger.' },
1674
+ objective: { type: 'string', description: 'Replacement objective.' },
1675
+ acceptance: { type: 'array', items: { type: 'string' }, description: 'Replacement acceptance criteria (full list, not a delta).' },
1676
+ verify: { type: 'array', items: { type: 'string' }, description: 'Replacement verification commands (full list, not a delta).' },
1677
+ inScope: { type: 'array', items: { type: 'string' }, description: 'Replacement workspace-relative inScope paths (full list).' },
1678
+ outOfScope: { type: 'array', items: { type: 'string' }, description: 'Replacement workspace-relative outOfScope paths (full list).' },
1679
+ },
1680
+ output: {
1681
+ schema: {
1682
+ type: 'object',
1683
+ additionalProperties: false,
1684
+ properties: {
1685
+ task_id: { type: 'string', required: true },
1686
+ status: { type: 'string', required: true },
1687
+ revised_fields: { type: 'string', required: true },
1688
+ revision_count: { type: 'number', required: true },
1689
+ contract: { type: 'string', required: true },
1690
+ },
1691
+ },
1692
+ render: (_args, value) => [{
1693
+ type: 'text',
1694
+ text: `Task ${value.task_id} contract amended (${value.revised_fields}); ${value.revision_count} revision(s) on record, status ${value.status}. New contract: ${value.contract}`,
1695
+ }],
1696
+ },
1697
+ async execute(args, exec) {
1698
+ const captain = requireCaptain(exec);
1699
+ const workspace = workspaceOf(captain);
1700
+ const stateRoot = stateRootOf(workspace, config);
1701
+ const team = await requireCaptainTeam(workspace, config, captain);
1702
+ const amended = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
1703
+ const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
1704
+ const task = requireTask(fresh, args.task_id);
1705
+ const input = {
1706
+ ...args.objective === undefined ? {} : { objective: args.objective },
1707
+ ...args.acceptance === undefined ? {} : { acceptance: args.acceptance },
1708
+ ...args.verify === undefined ? {} : { verify: args.verify },
1709
+ ...args.inScope === undefined ? {} : { inScope: args.inScope },
1710
+ ...args.outOfScope === undefined ? {} : { outOfScope: args.outOfScope },
1711
+ };
1712
+ const result = amendTaskContract(fresh, task, normalizeBlankOptionalTaskFields(input), CAPTAIN_KEY, args.reason);
1713
+ if (!result.ok || result.task === undefined) {
1714
+ throw new Error(result.error ?? 'amend_task rejected by quality gates');
1715
+ }
1716
+ Object.assign(task, result.task);
1717
+ task.updatedAt = Date.now();
1718
+ await writeTeam(stateRoot, fresh);
1719
+ return {
1720
+ taskId: task.id,
1721
+ status: task.status,
1722
+ fields: result.revision?.fields ?? [],
1723
+ revisionCount: task.revisions?.length ?? 0,
1724
+ contract: {
1725
+ ...task.objective === undefined ? {} : { objective: task.objective },
1726
+ ...task.acceptance === undefined ? {} : { acceptance: task.acceptance },
1727
+ ...task.verify === undefined ? {} : { verify: task.verify },
1728
+ ...task.inScope === undefined ? {} : { inScope: task.inScope },
1729
+ ...task.outOfScope === undefined ? {} : { outOfScope: task.outOfScope },
1730
+ },
1731
+ };
1732
+ });
1733
+ appendTeamEvent(ctx, captainSessionOf(ctx, team.captainSessionId, captain.session), 'agent-teams/task-amended', {
1734
+ teamId: team.id,
1735
+ taskId: amended.taskId,
1736
+ fields: amended.fields,
1737
+ reason: args.reason,
1738
+ });
1739
+ return {
1740
+ task_id: amended.taskId,
1741
+ status: amended.status,
1742
+ revised_fields: amended.fields.join(', '),
1743
+ revision_count: amended.revisionCount,
1744
+ contract: JSON.stringify(amended.contract),
1745
+ };
1746
+ },
1747
+ }));
1644
1748
  ctx.tools.register(defineTool({
1645
1749
  name: 'agent_teams_send_message',
1646
1750
  description: 'Send coordination or current-task guidance directly to the captain or a teammate. A running recipient receives it at the next model step; an idle recipient wakes. Messages are durably retained until read. Use task creation/reassignment for a new unit of work, not repeated status nudges.',
@@ -1775,6 +1879,7 @@ export function registerAgentTeamsTools(ctx, config) {
1775
1879
  reasoning_effort: member.reasoningEffort ?? '',
1776
1880
  status: member.status,
1777
1881
  activity: member.id !== '' ? (activity.get(member.id) ?? 'unknown') : 'unspawned',
1882
+ ...member.spawnError === undefined ? {} : { spawn_error: member.spawnError },
1778
1883
  }));
1779
1884
  const tasks = team.tasks.map((task) => ({
1780
1885
  id: task.id,
@@ -2214,7 +2319,8 @@ function renderStatus(value) {
2214
2319
  ...team.members.map((member) => {
2215
2320
  const route = member.provider && member.model ? ` · ${member.provider}/${member.model}` : '';
2216
2321
  const effort = member.reasoning_effort ? ` · reasoning ${member.reasoning_effort}` : '';
2217
- return ` - ${member.name} [${member.role}] ${member.status}/${member.activity}${route}${effort}`;
2322
+ const failure = member.spawn_error === undefined ? '' : `\n start failed: ${member.spawn_error.slice(0, 400)}`;
2323
+ return ` - ${member.name} [${member.role}] ${member.status}/${member.activity}${route}${effort}${failure}`;
2218
2324
  }),
2219
2325
  `Tasks (${team.tasks.length}):`,
2220
2326
  ...team.tasks.map((task) => {
@@ -52,6 +52,15 @@ export interface AgentTeamsTaskUpdatedData {
52
52
  readonly verdict?: string;
53
53
  readonly round?: number;
54
54
  }
55
+ /** Records one captain-only contract amendment on a task. */
56
+ export interface AgentTeamsTaskAmendedData {
57
+ readonly teamId: string;
58
+ readonly taskId: string;
59
+ /** Amended contract field names (`objective`, `inScope`, …). */
60
+ readonly fields: readonly string[];
61
+ /** Why the previous contract was wrong. */
62
+ readonly reason: string;
63
+ }
55
64
  /** Records a human halt from the captain chat. */
56
65
  export interface AgentTeamsTeamHaltedData {
57
66
  readonly teamId: string;
@@ -108,6 +117,11 @@ declare module '@deepseek-ai/dsh-session/types' {
108
117
  * @param data - team identity, task id, and the new status/assignee/output.
109
118
  */
110
119
  'agent-teams/task-updated': AgentTeamsTaskUpdatedData;
120
+ /**
121
+ * Records one captain-only contract amendment.
122
+ * @param data - team identity, task id, amended field names, and reason.
123
+ */
124
+ 'agent-teams/task-amended': AgentTeamsTaskAmendedData;
111
125
  /**
112
126
  * Records one mailbox message.
113
127
  * @param data - team identity, sender, recipient, and content.
@@ -136,4 +150,4 @@ declare module '@deepseek-ai/dsh-session/types' {
136
150
  }
137
151
  }
138
152
  /** The full set of `agent-teams/*` event names. */
139
- export type AgentTeamsEventType = 'agent-teams/team-created' | 'agent-teams/member-added' | 'agent-teams/member-removed' | 'agent-teams/task-created' | 'agent-teams/task-updated' | 'agent-teams/message-sent' | 'agent-teams/team-halted' | 'agent-teams/team-resumed' | 'agent-teams/team-deleted' | 'agent-teams/plan-discarded';
153
+ export type AgentTeamsEventType = 'agent-teams/team-created' | 'agent-teams/member-added' | 'agent-teams/member-removed' | 'agent-teams/task-created' | 'agent-teams/task-updated' | 'agent-teams/task-amended' | 'agent-teams/message-sent' | 'agent-teams/team-halted' | 'agent-teams/team-resumed' | 'agent-teams/team-deleted' | 'agent-teams/plan-discarded';
@@ -22,4 +22,48 @@ export declare function queueMemberPrompt(runtime: Context['subagents'], parent:
22
22
  export declare function steerMemberPrompt(runtime: Context['subagents'], parent: Agent, childId: SessionId, content: ContentBlock[], signal: AbortSignal, live?: Agent): Promise<MessageId>;
23
23
  /** Guard all resumable delivery paths, preserving the native service receiver. */
24
24
  export declare function guardSubagentDelivery(ctx: Context, isRetired: (sender: Agent, targetId: SessionId) => Promise<boolean>): void;
25
+ /**
26
+ * Names the host's `tools.restrict()` would admit for this agent's layer
27
+ * chain, or undefined when the host does not expose its registry view.
28
+ * restrict() rejects unknown names with a hard error, so the spawn path must
29
+ * check entries against this view before forwarding them. Source:
30
+ * dsh-tools view()/restrict() in 0.1.5-rc.1; older Harness generations lack
31
+ * the surface and keep the verbatim list.
32
+ */
33
+ export declare function restrictableToolNames(agent: Agent): ReadonlySet<string> | undefined;
34
+ /**
35
+ * Member `toolFilter` for one spawn. Captain-only names are registered by
36
+ * this plugin itself and always resolvable; the depth-related entries name
37
+ * HOST tools and are only known under some compositions, so they must be
38
+ * resolved against the running host's registry: the delegation tool name is
39
+ * host configuration (dsh-tool-subagent `toolName`, default `subagent`,
40
+ * renamed per composition, e.g. `subagent_fork` in web-style profiles), and
41
+ * forwarding a name the host dropped aborts every member spawn (#163, #164).
42
+ * Depth enforcement itself does not depend on these names —
43
+ * installMemberDelegationGuard bounds descendant creation by parent chain.
44
+ */
45
+ export declare function memberToolFilter(maxDepth: number | undefined, knownTools: ReadonlySet<string> | undefined): {
46
+ deny: string[];
47
+ };
48
+ /**
49
+ * Start one member, dropping any filter name the host reports as unknown and
50
+ * retrying.
51
+ *
52
+ * Resolving the filter against the registry view ({@link memberToolFilter})
53
+ * only covers hosts that expose one. A rejected filter still leaves the member
54
+ * without a session for the lifetime of the team, so the start must survive the
55
+ * cases that resolution cannot see — no registry view, a name that is known but
56
+ * not restrictable, a composition that mounted differently than expected. The
57
+ * host names the offenders in its rejection, so they are removed and the start
58
+ * retried; the retry only runs while the deny list strictly shrinks, and any
59
+ * failure that is not an unknown-name report propagates untouched.
60
+ * @param start - performs one start attempt for the given `toolFilter`.
61
+ * @param filter - the filter to attempt first.
62
+ * @returns the started member.
63
+ */
64
+ export declare function startMemberWithLenientFilter<T>(start: (filter: {
65
+ deny: string[];
66
+ }) => Promise<T>, filter: {
67
+ deny: string[];
68
+ }): Promise<T>;
25
69
  export {};
@@ -3,7 +3,7 @@
3
3
  * coverage, and resume. Tools and persistence call these; they do not I/O.
4
4
  * @module dsh-agent-teams/quality-gates
5
5
  */
6
- import { type AcceptanceResult, type CommandResult, type FindingSeverity, type ReviewFinding, type ReviewPolicy, type ReviewVerdict, type TaskKind, type TaskStatus, type TeamState, type TeamTask } from './types.ts';
6
+ import { type AcceptanceResult, type CommandResult, type FindingSeverity, type ReviewFinding, type ReviewPolicy, type ReviewVerdict, type TaskKind, type TaskRevision, type TaskStatus, type TeamState, type TeamTask } from './types.ts';
7
7
  declare const QUALITY_KINDS: readonly TaskKind[];
8
8
  declare const WRITE_KINDS: readonly TaskKind[];
9
9
  declare const DEFAULT_REVIEW_POLICY: Required<Pick<ReviewPolicy, 'requirementsMinRounds' | 'requirementsMaxRounds' | 'codeMaxRounds' | 'maxRepairAttempts'>>;
@@ -121,6 +121,55 @@ export declare function collectChangedPaths(gitStatusText: string): string[];
121
121
  export declare function inScopeOverlap(left: readonly string[] | undefined, right: readonly string[] | undefined): string[];
122
122
  export declare function validateCreateTask(team: TeamState, input: CreateTaskInput): ValidateCreateTaskResult;
123
123
  export declare function evaluateQualityCompletion(task: TeamTask, update: QualityCompletionUpdate): QualityCompletionResult;
124
+ /**
125
+ * Derive the repair round's inScope from the findings that caused it.
126
+ *
127
+ * `finding.file` records where the problem was OBSERVED, but the fix often
128
+ * targets a different file named in `requiredFix` (docs vs sample data,
129
+ * config vs code). Deriving the scope from both keeps the auto-generated
130
+ * repair contract satisfiable; deriving from `file` alone can produce a
131
+ * contract where the acceptance ("edit README.md") names a path the scope
132
+ * forbids, so no honest completion exists and the repair dead-locks.
133
+ *
134
+ * Absolute and otherwise illegal paths are dropped (they can never match
135
+ * workspace-relative scope patterns anyway); when nothing legal remains,
136
+ * the source task's own inScope is kept as the fallback. Over-inclusion is
137
+ * accepted: inScope is an audit upper bound, and the requiredFix text still
138
+ * tells the implementer what to touch.
139
+ *
140
+ * Keep all derived paths until the generator resolves inherited exclusions;
141
+ * filtering first would silently discard a required fix target.
142
+ */
143
+ export declare function repairScopeFromFindings(findings: readonly ReviewFinding[], fallback: string[] | undefined): string[] | undefined;
144
+ /** Captain-only amendment payload: replacement values for contract fields. */
145
+ export interface ContractAmendmentInput {
146
+ objective?: string;
147
+ acceptance?: string[];
148
+ verify?: string[];
149
+ inScope?: string[];
150
+ outOfScope?: string[];
151
+ }
152
+ export interface AmendTaskContractResult {
153
+ ok: boolean;
154
+ error?: string;
155
+ task?: TeamTask;
156
+ revision?: TaskRevision;
157
+ }
158
+ /**
159
+ * Controlled contract amendment (the pure rule; tooling keeps it
160
+ * captain-only). When a quality contract is wrong — a verify command that
161
+ * cannot pass, an inScope that forbids the file the objective names — the
162
+ * worker has no honest completion and either dead-locks or games the gate.
163
+ * Instead the captain may fix the contract mid-flight: every amendment is
164
+ * recorded on the task as a {@link TaskRevision} (previous values + reason),
165
+ * and once a review/requirements task has passed judgment on this task the
166
+ * contract is frozen. Amendments replace whole fields (lists are full
167
+ * replacements, not deltas); the implementer re-reads the amended contract
168
+ * before its next quality gate. Completion gates need no special casing:
169
+ * they read the task's current fields, so they naturally evaluate the
170
+ * amended contract.
171
+ */
172
+ export declare function amendTaskContract(team: TeamState, task: TeamTask, input: ContractAmendmentInput, by: string, reason: string): AmendTaskContractResult;
124
173
  export declare function planQualityFollowUp(team: TeamState, closed: TeamTask): PlanQualityFollowUpResult;
125
174
  export declare function buildCoverageMatrix(goalItems: readonly string[], tasks: readonly TeamTask[]): CoverageRow[];
126
175
  export declare function canDeclareDelivery(team: TeamState): DeliveryResult;
@@ -128,6 +177,7 @@ export declare function resumeTeamState(team: TeamState, reason: string): Resume
128
177
  export declare function isReviewFinding(value: unknown): value is ReviewFinding;
129
178
  export declare function isAcceptanceResult(value: unknown): value is AcceptanceResult;
130
179
  export declare function isCommandResult(value: unknown): value is CommandResult;
180
+ export declare function isTaskRevision(value: unknown): value is TaskRevision;
131
181
  /**
132
182
  * Normalize blank optional task fields to omitted ("blank means absent").
133
183
  * Blank string scalars are deleted; string lists have blank entries filtered
@@ -13,7 +13,8 @@
13
13
  * @module dsh-agent-teams/state
14
14
  */
15
15
  import { type TaskStatus, type TeamMessage, type TeamState, type TeamTask } from './types.ts';
16
- export { buildCoverageMatrix, canDeclareDelivery, classifyChangedPath, collectChangedPaths, defaultQualityDeliveryGraph, describeQualityLoop, evaluateQualityCompletion, hasValidQualityTaskFields, isQualityKind, normalizeBlankOptionalTaskFields, pathMatchesScope, planQualityFollowUp, qualityPlanningPrompt, resumeTeamState, sanitizeReviewAcceptance, sanitizeReviewObjective, taskKindOf, validateCreateTask, } from './quality-gates.ts';
16
+ export { amendTaskContract, buildCoverageMatrix, canDeclareDelivery, classifyChangedPath, collectChangedPaths, defaultQualityDeliveryGraph, describeQualityLoop, evaluateQualityCompletion, hasValidQualityTaskFields, isQualityKind, isTaskRevision, normalizeBlankOptionalTaskFields, pathMatchesScope, planQualityFollowUp, qualityPlanningPrompt, resumeTeamState, sanitizeReviewAcceptance, sanitizeReviewObjective, taskKindOf, validateCreateTask, } from './quality-gates.ts';
17
+ export type { ContractAmendmentInput } from './quality-gates.ts';
17
18
  /** Mailbox key of the captain. */
18
19
  export declare const CAPTAIN_KEY = "captain";
19
20
  /**
@@ -1,4 +1,4 @@
1
1
  /** Stable business API names; exposure changes never rename these operations. */
2
- export declare const TEAM_TOOL_NAMES: readonly ["agent_teams_create", "agent_teams_approve", "agent_teams_edit_plan", "agent_teams_add_member", "agent_teams_remove_member", "agent_teams_create_task", "agent_teams_reassign_task", "agent_teams_claim_task", "agent_teams_update_task", "agent_teams_send_message", "agent_teams_status", "agent_teams_resume", "agent_teams_delete"];
2
+ export declare const TEAM_TOOL_NAMES: readonly ["agent_teams_create", "agent_teams_approve", "agent_teams_edit_plan", "agent_teams_add_member", "agent_teams_remove_member", "agent_teams_create_task", "agent_teams_reassign_task", "agent_teams_claim_task", "agent_teams_update_task", "agent_teams_amend_task", "agent_teams_send_message", "agent_teams_status", "agent_teams_resume", "agent_teams_delete"];
3
3
  export declare const MEMBER_TOOL_NAMES: readonly string[];
4
- export declare const CAPTAIN_TOOL_NAMES: ("agent_teams_create" | "agent_teams_approve" | "agent_teams_edit_plan" | "agent_teams_add_member" | "agent_teams_remove_member" | "agent_teams_create_task" | "agent_teams_reassign_task" | "agent_teams_claim_task" | "agent_teams_update_task" | "agent_teams_send_message" | "agent_teams_status" | "agent_teams_resume" | "agent_teams_delete")[];
4
+ export declare const CAPTAIN_TOOL_NAMES: ("agent_teams_create" | "agent_teams_approve" | "agent_teams_edit_plan" | "agent_teams_add_member" | "agent_teams_remove_member" | "agent_teams_create_task" | "agent_teams_reassign_task" | "agent_teams_claim_task" | "agent_teams_update_task" | "agent_teams_amend_task" | "agent_teams_send_message" | "agent_teams_status" | "agent_teams_resume" | "agent_teams_delete")[];
@@ -52,6 +52,19 @@ export interface ReviewPolicy {
52
52
  maxRepairAttempts?: number;
53
53
  requiredReviewers?: string[];
54
54
  }
55
+ /** One captain-only contract amendment recorded on a quality task. */
56
+ export interface TaskRevision {
57
+ /** Epoch ms when the amendment was applied. */
58
+ at: number;
59
+ /** Identity that applied it (`captain`). */
60
+ by: string;
61
+ /** Why the previous contract was wrong; kept for the audit trail. */
62
+ reason: string;
63
+ /** Amended contract field names (`objective`, `acceptance`, …). */
64
+ fields: string[];
65
+ /** Previous values of the amended fields; fields absent before are omitted. */
66
+ previous: Record<string, unknown>;
67
+ }
55
68
  /** One task of a team's task list. */
56
69
  export interface TeamTask {
57
70
  /** Stable task id from the profile template; absent for ad-hoc tasks. */
@@ -102,6 +115,8 @@ export interface TeamTask {
102
115
  sourceFindingIds?: string[];
103
116
  /** User-constraint / goal items this task claims to cover. */
104
117
  coverageOf?: string[];
118
+ /** Captain-only contract amendments, oldest first (see amendTaskContract). */
119
+ revisions?: TaskRevision[];
105
120
  createdAt: number;
106
121
  updatedAt: number;
107
122
  }
@@ -134,6 +149,12 @@ export interface TeamMember {
134
149
  status: MemberStatus;
135
150
  /** Execution admission is closed while a failed/pending handoff is drained. */
136
151
  stopping?: boolean;
152
+ /**
153
+ * Last member-start failure, recorded so the captain can see why a member
154
+ * never acquired a session instead of observing an unexplained `unspawned`
155
+ * member. Cleared by the next successful start.
156
+ */
157
+ spawnError?: string;
137
158
  }
138
159
  /** One mailbox message. */
139
160
  export interface TeamMessage {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanmicoder/dsh-agent-teams",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "AgentTeams for DeepSeek Harness: multi-agent team collaboration (captain, members, tasks with dependencies, messaging) driven by natural language, with a tree monitor in the web GUI",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -225,13 +225,15 @@
225
225
  "update:skills": "node scripts/update-skills.mjs",
226
226
  "test:skills-updater": "node --test scripts/update-skills.test.mjs",
227
227
  "verify:skill": "node scripts/sync-skill.mjs --check",
228
- "verify": "node scripts/verify.mjs && node scripts/fallback-tdd.mjs && node scripts/member-failure-tdd.mjs && node scripts/quality-gates-tdd.mjs && node scripts/lifecycle-verify.mjs && node scripts/stress-verify.mjs && pnpm verify:web-routes && pnpm verify:release && pnpm verify:skill && pnpm verify:harness-contract && pnpm verify:http-body && pnpm verify:compatibility && pnpm verify:capabilities",
228
+ "verify": "node scripts/verify.mjs && node scripts/fallback-tdd.mjs && node scripts/member-failure-tdd.mjs && pnpm verify:quality-gates && node scripts/lifecycle-verify.mjs && node scripts/stress-verify.mjs && pnpm verify:web-routes && pnpm verify:release && pnpm verify:skill && pnpm verify:harness-contract && pnpm verify:http-body && pnpm verify:compatibility && pnpm verify:capabilities && pnpm verify:member-spawn-recovery",
229
+ "verify:quality-gates": "node scripts/quality-gates-tdd.mjs && node --test scripts/quality-gates-repair-scope.test.mjs scripts/quality-gates-amend.test.mjs",
229
230
  "verify:web-routes": "node scripts/web-routes-verify.mjs",
230
231
  "verify:release": "node --test scripts/release-metadata.test.mjs",
231
232
  "verify:harness-contract": "node scripts/harness-compat-tdd.mjs && node scripts/stability-tdd.mjs && node scripts/lifecycle-verify.mjs --modern-harness && node scripts/member-failure-tdd.mjs --modern-harness && node scripts/lifecycle-verify.mjs --delivery-harness && node scripts/member-failure-tdd.mjs --delivery-harness",
232
233
  "verify:compatibility": "node scripts/compatibility.mjs && node --test scripts/compatibility.test.mjs && node scripts/doctor.mjs",
233
234
  "verify:http-body": "node --test scripts/http-body.test.mjs",
234
235
  "doctor": "node scripts/doctor.mjs",
235
- "verify:capabilities": "node --test scripts/capabilities.test.mjs"
236
+ "verify:capabilities": "node --test scripts/capabilities.test.mjs",
237
+ "verify:member-spawn-recovery": "node --test scripts/member-spawn-recovery.test.mjs"
236
238
  }
237
239
  }
@@ -0,0 +1,20 @@
1
+ # AgentTeams v0.1.19
2
+
3
+ - Fix member startup when the host disables or renames its delegation tools. Missing global tool names no longer abort all member starts; unrelated startup failures remain visible in team status. Thanks to PRs #167 and #172.
4
+ - Derive automatic repair scope from both the observed file and paths named in `requiredFix`. Resolve conflicting exclusions inherited from the previous round after deriving that scope, so a repair can truthfully report the files it must change. Integrates PR #155 and the generated-contract portion of PR #177; manually authored exclusions keep their existing precedence.
5
+ - Add captain-only `agent_teams_amend_task` for correcting non-terminal quality contracts, with durable revision history and freeze checks after a passing review. Members cannot call it.
6
+ - Include repair-scope and amendment regressions in the standard release gate; update the captain tool-surface fixture and tolerate the known closed-session teardown race (PR #180).
7
+
8
+ ## Compatibility
9
+
10
+ The supported Harness matrix is unchanged: recommended `0.1.5-rc.1`, plus `0.1.2-rc.1`, `0.1.2-alpha.5`, and `0.1.2-alpha.2`. This release does not claim support for untested newer Harness versions or every Desktop distribution.
11
+
12
+ Existing malformed terminal tasks are not rewritten automatically. Broader findings/revision linkage and stale generated acceptance criteria remain separate issues.
13
+
14
+ Update the plugin in your existing profile, then restart Harness:
15
+
16
+ ```sh
17
+ dsh plugin --profile <your-profile> add --save-exact @nanmicoder/dsh-agent-teams@0.1.19
18
+ ```
19
+
20
+ See [release verification](https://github.com/NanmiCoder/dsh-agent-teams/tree/main/docs/releases/v0.1.19) for real-provider scenarios, evidence boundaries, and publication results.
@@ -0,0 +1,20 @@
1
+ # AgentTeams v0.1.20
2
+
3
+ Documentation-only release. No source, behavior, or dependency changes.
4
+
5
+ - Sync the version references in `README.md` and `README_ZH.md` with the npm `latest` channel. Both still described `0.1.17` as the recommended installation, so readers copying the documented command installed `0.1.17` while `latest` already pointed at `0.1.19`.
6
+ - The packaged artifact is therefore identical to `0.1.19` apart from the two README files. `0.1.20` exists only because npm writes a package's README at publish time and offers no way to update it afterwards.
7
+
8
+ ## Compatibility
9
+
10
+ The supported Harness matrix is unchanged: recommended `0.1.5-rc.1`, plus `0.1.2-rc.1`, `0.1.2-alpha.5`, and `0.1.2-alpha.2`.
11
+
12
+ Because this release ships the same code as `0.1.19`, the verification recorded for that version still covers it. See [v0.1.19 release verification](https://github.com/NanmiCoder/dsh-agent-teams/tree/main/docs/releases/v0.1.19) for the real-provider scenarios, evidence boundaries, and publication results.
13
+
14
+ For the substantive changes since the last documented release, see [v0.1.19](./v0.1.19.md) and [v0.1.18](./v0.1.18.md).
15
+
16
+ Update the plugin in your existing profile, then restart Harness:
17
+
18
+ ```sh
19
+ dsh plugin --profile <your-profile> add --save-exact @nanmicoder/dsh-agent-teams@0.1.20
20
+ ```