@nanmicoder/dsh-agent-teams 0.1.22-rc.1 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -12
- package/README_ZH.md +35 -12
- package/compatibility.json +6 -10
- package/lib/artifacts.js +342 -0
- package/lib/capabilities.js +6 -4
- package/lib/client/ActivityPanel.js +5 -2
- package/lib/client/agent-teams-card-definition.js +100 -6
- package/lib/client.js +90 -7
- package/lib/client.js.map +1 -1
- package/lib/command.js +3 -2
- package/lib/git-artifact-stamp.json +2 -2
- package/lib/harness-compat.js +70 -21
- package/lib/index.js +26 -3
- package/lib/lineage.js +198 -0
- package/lib/mailbox.js +59 -34
- package/lib/members.js +301 -53
- package/lib/profiles.js +3 -2
- package/lib/quality-gates.js +850 -65
- package/lib/scheduler.js +22 -3
- package/lib/snapshot.js +22 -1
- package/lib/state.js +279 -13
- package/lib/tool-policies.js +187 -0
- package/lib/tools.js +370 -89
- package/lib/types/artifacts.d.ts +66 -0
- package/lib/types/client/activity-monitor.d.ts +6 -0
- package/lib/types/client/agent-teams-card-definition.d.ts +56 -0
- package/lib/types/harness-compat.d.ts +56 -14
- package/lib/types/index.d.ts +26 -1
- package/lib/types/lineage.d.ts +92 -0
- package/lib/types/members.d.ts +112 -1
- package/lib/types/profiles.d.ts +6 -0
- package/lib/types/quality-gates.d.ts +216 -10
- package/lib/types/scheduler.d.ts +2 -1
- package/lib/types/snapshot.d.ts +20 -0
- package/lib/types/state.d.ts +22 -5
- package/lib/types/tool-policies.d.ts +119 -0
- package/lib/types/tools.d.ts +12 -1
- package/lib/types/types.d.ts +95 -0
- package/lib/types/workspace-audit.d.ts +27 -0
- package/lib/workspace-audit.js +52 -0
- package/package.json +59 -52
- package/pnpm-workspace.yaml +273 -273
- package/release-notes/v0.1.22.md +18 -0
- package/release-notes/v0.1.23.md +34 -0
- package/scripts/doctor.mjs +4 -4
- package/scripts/legacy-session-source-repair.mjs +344 -0
package/README.md
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
<img src="./assets/readme/hero.svg" width="100%" alt="dsh-agent-teams turns one DeepSeek Harness session into a coordinated multi-agent team">
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
|
+
<div align="center">
|
|
10
|
+
|
|
11
|
+
[](https://atomgit.com/NanmiCoder/dsh-agent-teams)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
9
15
|
<p align="center">
|
|
10
16
|
<a href="https://dshfind.com/en/plugins/NanmiCoder/dsh-agent-teams?ref=badge"><img src="https://img.shields.io/badge/recommended%20by-dshfind-FFD700?style=flat-square" alt="Recommended by dshfind"></a>
|
|
11
17
|
<a href="https://dshfind.com/en/plugins/NanmiCoder/dsh-agent-teams?ref=badge"><img src="https://dshfind.com/api/badge/NanmiCoder/dsh-agent-teams?lang=en" alt="dshfind score"></a>
|
|
@@ -30,7 +36,7 @@ Ask in natural language. The plugin provides the team protocol, 14 coordination
|
|
|
30
36
|
|
|
31
37
|
## Releases
|
|
32
38
|
|
|
33
|
-
[v0.1.
|
|
39
|
+
[v0.1.23](./release-notes/v0.1.23.md) fixes team identity, member model routing, quality and repair gates, and cold fork recovery, and adds owner-configured member tool policies. This plugin release uses npm `latest`; the recommended host remains Harness `0.2.0-rc.2` (its `next` channel). See the exact [support matrix](./compatibility.json).
|
|
34
40
|
|
|
35
41
|
## Why AgentTeams?
|
|
36
42
|
|
|
@@ -49,32 +55,33 @@ The conversation card and activity panel use Harness's official locale service.
|
|
|
49
55
|
|
|
50
56
|
## Install and choose versions
|
|
51
57
|
|
|
52
|
-
**Recommended pair: DeepSeek Harness `0.
|
|
58
|
+
**Recommended pair: DeepSeek Harness `0.2.0-rc.2` + AgentTeams `0.1.23`. The host remains an RC.**
|
|
53
59
|
|
|
54
60
|
| Use case | DeepSeek Harness | AgentTeams plugin |
|
|
55
61
|
| --- | --- | --- |
|
|
56
|
-
| **Recommended** | **`0.
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
62
|
+
| **Recommended** | **`0.2.0-rc.2`** | **`0.1.23`** |
|
|
63
|
+
| Previous recommended RC | `0.1.7-rc.2` | `0.1.23` |
|
|
64
|
+
| Legacy RC | `0.1.5-rc.1`, `0.1.5-rc.2`, `0.1.5-rc.3` | `0.1.23` |
|
|
65
|
+
| Retaining an older RC | `0.1.2-rc.1` | `0.1.23` |
|
|
66
|
+
| Developer Alpha testing | `0.1.2-alpha.5` | `0.1.23` |
|
|
67
|
+
| Retaining an older Alpha | `0.1.2-alpha.2` | `0.1.23` |
|
|
61
68
|
|
|
62
69
|
### Desktop app
|
|
63
70
|
|
|
64
71
|
In the app sidebar, open **Plugins → Add plugin** and enter the npm package spec:
|
|
65
72
|
|
|
66
73
|
```text
|
|
67
|
-
@nanmicoder/dsh-agent-teams@0.1.
|
|
74
|
+
@nanmicoder/dsh-agent-teams@0.1.23
|
|
68
75
|
```
|
|
69
76
|
|
|
70
77
|
Install it, then choose **Enable now**. If the host asks for a restart, restart the desktop app. Use the package spec above for the published build; an npm or GitHub webpage URL is not the same install source. For Git installation, use a commit containing the verified build outputs described below.
|
|
71
78
|
|
|
72
|
-
The desktop app supplies its own Harness core and package manager. Upgrading the global CLI does not upgrade that core, and
|
|
79
|
+
The desktop app supplies its own Harness core and package manager. Upgrading the global CLI does not upgrade that core, and an ordinary standalone CLI cannot manage its desktop profile. The CLI carrier installed by the desktop app can manage that profile in `0.2.0-rc.2`. Check the embedded core against the compatibility list. Harness `0.2.0-rc.2` is now the exact published target; its RC status is unchanged. The earlier source-only `0.2.0` prediction is no longer a support declaration. See the [release migration evidence](./docs/harness-0.2.0-rc.2/README.md).
|
|
73
80
|
|
|
74
81
|
### CLI / Web: 1. Install DeepSeek Harness
|
|
75
82
|
|
|
76
83
|
```sh
|
|
77
|
-
npm install --global @deepseek-ai/dsh@0.
|
|
84
|
+
npm install --global @deepseek-ai/dsh@0.2.0-rc.2
|
|
78
85
|
dsh --version
|
|
79
86
|
```
|
|
80
87
|
|
|
@@ -82,10 +89,10 @@ Skip this if you already run this version. Alpha is opt-in: select an exact Alph
|
|
|
82
89
|
|
|
83
90
|
### CLI / Web: 2. Install the AgentTeams plugin
|
|
84
91
|
|
|
85
|
-
Install or upgrade from npm (pinned to this `
|
|
92
|
+
Install or upgrade from npm (pinned to this `latest` release):
|
|
86
93
|
|
|
87
94
|
```sh
|
|
88
|
-
dsh plugin --profile web add --save-exact @nanmicoder/dsh-agent-teams@0.1.
|
|
95
|
+
dsh plugin --profile web add --save-exact @nanmicoder/dsh-agent-teams@0.1.23
|
|
89
96
|
```
|
|
90
97
|
|
|
91
98
|
For a CLI-managed installation, replace `web` with your active CLI profile. **Stop and restart that profile's Harness process, then refresh the browser.** Installing the plugin does not upgrade Harness; the host and plugin have independent `latest` channels.
|
|
@@ -159,10 +166,26 @@ Defaults work without extra setup. A trusted profile can override member behavio
|
|
|
159
166
|
memberModel: deepseek-v4
|
|
160
167
|
memberMaxDepth: 0
|
|
161
168
|
maxMembers: 8
|
|
169
|
+
toolPolicies:
|
|
170
|
+
researcher:
|
|
171
|
+
allow: [read, glob, grep]
|
|
172
|
+
implementer:
|
|
173
|
+
deny: [web_search]
|
|
162
174
|
```
|
|
163
175
|
|
|
164
176
|
`memberMaxDepth` defaults to `0`: team members cannot create nested subagents. Set `1` to explicitly permit one descendant level; the limit also covers runtime/code-tool calls. Default members report through team messages only, avoiding duplicate native parent reports. Idle roster members make no model requests. Task assignments start distinct turns; coordination joins the nearest model step. Acceptance and consumption are tracked separately. Removal/archive drains the selected branch and its pending input before reporting success.
|
|
165
177
|
|
|
178
|
+
`toolPolicies` is owner configuration: reusable per-member tool policies keyed by id. A member *selects* one by id — `tool_policy` on `agent_teams_add_member`, a member row of an inline `plan`, or a profile member row. The Captain model can select a declared policy but can never submit raw allow/deny lists or invent one at runtime.
|
|
179
|
+
|
|
180
|
+
- `deny` hides the named global tools from that member's schemas and rejects their invocation.
|
|
181
|
+
- `allow` keeps only the named global tools. The four coordination tools (`agent_teams_claim_task`, `agent_teams_update_task`, `agent_teams_send_message`, `agent_teams_status`) are always added to an allow list.
|
|
182
|
+
- The captain-only tool list is always denied and cannot be reopened by a policy; a policy may not deny a required coordination tool.
|
|
183
|
+
- Unknown ids, empty policies, and empty lists fail at mount or before a member is persisted.
|
|
184
|
+
- `agent_teams_status` shows the selected policy id per member; the complete resolved filter is kept in `team.json` for audit, and the child's durable descriptor reapplies it after a restart.
|
|
185
|
+
- Teams and members created without a policy keep their previous tool set and record shape. An empty `tool_policy` on `agent_teams_edit_plan` clears a staged member's policy.
|
|
186
|
+
|
|
187
|
+
This is model tool visibility and invocation policy for global tools, not an OS sandbox or an authorization boundary.
|
|
188
|
+
|
|
166
189
|
`memberProvider` is the sub-agent runtime backend (`spawn` / `fork`), not an LLM provider. Cross-LLM-provider routing uses the optional `provider` + `model` fields of `agent_teams_add_member`; `memberModel` is only a model default for all members. A member on the captain's current provider/model inherits the captain's reasoning effort, while a changed provider or model automatically uses the target model's default. To request a particular effort, pass the optional `reasoning_effort` field — one of the target model's supported effort ids, or `"default"` to force the model's own default.
|
|
167
190
|
|
|
168
191
|
`slashCommand: false` disables the deterministic `/agent-teams` activation surfaces (slash command and gesture boundary), leaving the natural-language trigger as the only entry point.
|
package/README_ZH.md
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
<img src="./assets/readme/hero.svg" width="100%" alt="dsh-agent-teams 把一个 DeepSeek Harness 会话变成可协作的多智能体团队">
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
|
+
<div align="center">
|
|
10
|
+
|
|
11
|
+
[](https://atomgit.com/NanmiCoder/dsh-agent-teams)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
9
15
|
<p align="center">
|
|
10
16
|
<a href="https://dshfind.com/zh/plugins/NanmiCoder/dsh-agent-teams?ref=badge"><img src="https://img.shields.io/badge/%E7%94%B1%20dshfind-%E6%8E%A8%E8%8D%90-FFD700?style=flat-square" alt="由 dshfind 推荐"></a>
|
|
11
17
|
<a href="https://dshfind.com/zh/plugins/NanmiCoder/dsh-agent-teams?ref=badge"><img src="https://dshfind.com/api/badge/NanmiCoder/dsh-agent-teams?lang=zh" alt="dshfind 评分"></a>
|
|
@@ -30,7 +36,7 @@
|
|
|
30
36
|
|
|
31
37
|
## 版本更新
|
|
32
38
|
|
|
33
|
-
[v0.1.
|
|
39
|
+
[v0.1.23](./release-notes/v0.1.23.md) 修复团队标识、成员模型路由、质量与修复门禁及 fork 冷恢复,并新增由工作区所有者配置的成员工具策略。本插件正式版使用 npm `latest` 渠道;推荐宿主仍为 Harness `0.2.0-rc.2`(宿主的 `next` 渠道)。精确支持范围见 [compatibility.json](./compatibility.json)。
|
|
34
40
|
|
|
35
41
|
## 为什么需要 AgentTeams?
|
|
36
42
|
|
|
@@ -48,32 +54,33 @@
|
|
|
48
54
|
|
|
49
55
|
## 安装与版本选择
|
|
50
56
|
|
|
51
|
-
**推荐组合:DeepSeek Harness `0.
|
|
57
|
+
**推荐组合:DeepSeek Harness `0.2.0-rc.2` + AgentTeams `0.1.23`。宿主仍为 RC 版本。**
|
|
52
58
|
|
|
53
59
|
| 使用场景 | DeepSeek Harness | AgentTeams 插件 |
|
|
54
60
|
| --- | --- | --- |
|
|
55
|
-
| **推荐版本** | **`0.
|
|
56
|
-
|
|
|
57
|
-
| 保留旧 RC | `0.1.
|
|
58
|
-
|
|
|
59
|
-
|
|
|
61
|
+
| **推荐版本** | **`0.2.0-rc.2`** | **`0.1.23`** |
|
|
62
|
+
| 上一推荐 RC | `0.1.7-rc.2` | `0.1.23` |
|
|
63
|
+
| 保留旧 RC | `0.1.5-rc.1`, `0.1.5-rc.2`, `0.1.5-rc.3` | `0.1.23` |
|
|
64
|
+
| 保留旧 RC | `0.1.2-rc.1` | `0.1.23` |
|
|
65
|
+
| 开发者测试 Alpha | `0.1.2-alpha.5` | `0.1.23` |
|
|
66
|
+
| 保留旧 Alpha | `0.1.2-alpha.2` | `0.1.23` |
|
|
60
67
|
|
|
61
68
|
### 桌面端
|
|
62
69
|
|
|
63
70
|
在应用侧栏打开「插件 → 添加插件」,输入 npm 包名和版本:
|
|
64
71
|
|
|
65
72
|
```text
|
|
66
|
-
@nanmicoder/dsh-agent-teams@0.1.
|
|
73
|
+
@nanmicoder/dsh-agent-teams@0.1.23
|
|
67
74
|
```
|
|
68
75
|
|
|
69
76
|
安装完成后点击「立即启用」;宿主提示需要重启时,重启桌面应用。发布版请使用上面的包名,npm 或 GitHub 网页链接与 npm 包名不是同一种安装来源。从 Git 安装时,请选用包含下方所述已验证构建产物的提交。
|
|
70
77
|
|
|
71
|
-
|
|
78
|
+
桌面端自带 Harness 内核与包管理器,升级全局 CLI 不会升级桌面端;普通独立 CLI 不能管理桌面 profile,桌面端自带的 CLI carrier 在此版本可以管理。当前精确目标是已发布的 `0.2.0-rc.2`,仍属于 RC;此前源码预测的 `0.2.0` 已退出支持声明。详见[迁移验收记录](./docs/harness-0.2.0-rc.2/README.md)。
|
|
72
79
|
|
|
73
80
|
### CLI / Web:1. 安装 DeepSeek Harness
|
|
74
81
|
|
|
75
82
|
```sh
|
|
76
|
-
npm install --global @deepseek-ai/dsh@0.
|
|
83
|
+
npm install --global @deepseek-ai/dsh@0.2.0-rc.2
|
|
77
84
|
dsh --version
|
|
78
85
|
```
|
|
79
86
|
|
|
@@ -81,10 +88,10 @@ dsh --version
|
|
|
81
88
|
|
|
82
89
|
### CLI / Web:2. 安装 AgentTeams 插件
|
|
83
90
|
|
|
84
|
-
从 npm 安装或升级插件(命令锁定本次 `
|
|
91
|
+
从 npm 安装或升级插件(命令锁定本次 `latest` 正式版):
|
|
85
92
|
|
|
86
93
|
```sh
|
|
87
|
-
dsh plugin --profile web add --save-exact @nanmicoder/dsh-agent-teams@0.1.
|
|
94
|
+
dsh plugin --profile web add --save-exact @nanmicoder/dsh-agent-teams@0.1.23
|
|
88
95
|
```
|
|
89
96
|
|
|
90
97
|
CLI 管理的安装可将 `web` 换成实际使用的 CLI profile。**安装后,停止并重新启动该 profile 的 Harness 进程,再刷新浏览器。** 安装插件不会自动升级宿主;Harness 的 `latest` 与插件的 `latest` 是两个独立渠道。
|
|
@@ -146,10 +153,26 @@ CLI 管理的安装可将 `web` 换成实际使用的 CLI profile。**安装后
|
|
|
146
153
|
memberModel: deepseek-v4
|
|
147
154
|
memberMaxDepth: 0
|
|
148
155
|
maxMembers: 8
|
|
156
|
+
toolPolicies:
|
|
157
|
+
researcher:
|
|
158
|
+
allow: [read, glob, grep]
|
|
159
|
+
implementer:
|
|
160
|
+
deny: [web_search]
|
|
149
161
|
```
|
|
150
162
|
|
|
151
163
|
`memberMaxDepth` 默认 `0`,团队成员不能再创建子代理;显式设为 `1` 可允许一层后代,运行时和代码工具调用同样受限。默认成员统一通过团队消息汇报,避免再走宿主消息重复通知队长。无任务成员不调用模型;任务分配开启独立轮次,纠正消息进入最近的模型步骤。消息投递与读取分别记录;移除和归档必须等成员分支及待处理输入清理完成后才报告成功。
|
|
152
164
|
|
|
165
|
+
`toolPolicies` 是部署方配置:按 id 复用的成员工具策略。成员只能“按 id 选择”——`agent_teams_add_member` 的 `tool_policy`、内联 `plan` 的成员行,或 profile 成员行的 `tool_policy`。队长模型只能选择已声明的策略,不能提交原始 allow/deny 列表,也不能在运行时新建策略。
|
|
166
|
+
|
|
167
|
+
- `deny` 让指定全局工具从该成员的 schema 中消失,并拒绝其调用。
|
|
168
|
+
- `allow` 只保留列出的全局工具;四个协调工具(`agent_teams_claim_task`、`agent_teams_update_task`、`agent_teams_send_message`、`agent_teams_status`)始终会被加入 allow 列表。
|
|
169
|
+
- 队长专属工具始终被拒绝,策略无法重新打开;策略也不允许拒绝必须保留的协调工具。
|
|
170
|
+
- 未知 id、空策略、空列表会在挂载时或写入成员前直接失败。
|
|
171
|
+
- `agent_teams_status` 会显示每个成员选择的策略 id;完整生效过滤器保存在 `team.json` 供审计,重启后由子会话的持久 descriptor 重新应用。
|
|
172
|
+
- 未配置策略的团队与成员保持原有工具集和记录结构;`agent_teams_edit_plan` 传空 `tool_policy` 可清除暂存成员的策略。
|
|
173
|
+
|
|
174
|
+
这是全局工具的模型可见性与调用限制,不是 OS 沙箱,也不是授权边界。
|
|
175
|
+
|
|
153
176
|
普通调研、仓库审计可用 `agent_teams_create({name, description, approval:"required", plan:{members:[{name:"researcher"}], tasks:[{id:"audit", subject:"审计现有实现", assignee:"researcher"}]}})` 一次创建完整草案。任务的 `dependencies` 引用同一批中的 `id`,支持前向引用并校验环;结果返回实际任务 ID。`kind=review` 专用于对既有 implementation/repair 任务的质量复审,普通仓库审计使用默认的 `kind=work`。
|
|
154
177
|
|
|
155
178
|
这里的 `memberProvider` 指子 Agent 的运行后端(`spawn` / `fork`),不是 LLM provider。跨 LLM provider 由 `agent_teams_add_member` 的可选 `provider` + `model` 参数表达;`memberModel` 只是所有成员的模型默认覆盖。成员沿用队长当前 provider/model 时会继承队长的思考强度;provider 或 model 任一改变时会自动使用目标模型的默认档。需要指定特定强度时,可传入可选的 `reasoning_effort` 参数(目标模型支持的档位 id,或 `"default"` 表示强制使用模型自身默认档)。
|
package/compatibility.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"recommendedHost": "0.
|
|
3
|
+
"recommendedHost": "0.2.0-rc.2",
|
|
4
4
|
"previewTag": "next",
|
|
5
5
|
"supportedHosts": [
|
|
6
6
|
{
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.2.0-rc.2",
|
|
8
8
|
"track": "recommended"
|
|
9
9
|
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.7-rc.2",
|
|
12
|
+
"track": "legacy"
|
|
13
|
+
},
|
|
10
14
|
{
|
|
11
15
|
"version": "0.1.5-rc.3",
|
|
12
16
|
"track": "legacy"
|
|
@@ -31,13 +35,5 @@
|
|
|
31
35
|
"version": "0.1.2-alpha.2",
|
|
32
36
|
"track": "legacy"
|
|
33
37
|
}
|
|
34
|
-
],
|
|
35
|
-
"sourceCandidates": [
|
|
36
|
-
{
|
|
37
|
-
"version": "0.2.0",
|
|
38
|
-
"repository": "https://github.com/deepseek-ai/deepseek-harness",
|
|
39
|
-
"commit": "21638c56315ae6a2b552d6091945d3144c9af32e",
|
|
40
|
-
"evidence": "docs/harness-0.2.0-pre-adaptation/README.md"
|
|
41
|
-
}
|
|
42
38
|
]
|
|
43
39
|
}
|
package/lib/artifacts.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace byte identity: the only module in this plugin that reads artifact
|
|
3
|
+
* bytes off disk.
|
|
4
|
+
*
|
|
5
|
+
* Two plugin surfaces need it:
|
|
6
|
+
* · a quality verdict must record which revision it judged (issue #161), so
|
|
7
|
+
* delivery can refuse to advertise bytes no review ever saw;
|
|
8
|
+
* · an auto-generated work order must record the workspace it was derived
|
|
9
|
+
* from (issue #174), so it can be marked stale after a later round edits a
|
|
10
|
+
* cited file.
|
|
11
|
+
*
|
|
12
|
+
* Safety rules this module must never lose:
|
|
13
|
+
* · directory traversal never follows symlinks, and every explicitly named
|
|
14
|
+
* path is resolved with `realpath` and rejected when it leaves the
|
|
15
|
+
* workspace — a symlink must not become a read of arbitrary host files, and
|
|
16
|
+
* a symlink cycle must not become an infinite walk;
|
|
17
|
+
* · a scope that cannot be enumerated completely (file cap, unreadable
|
|
18
|
+
* directory, non-workspace-relative path) produces an explicit
|
|
19
|
+
* {@link ArtifactScopeGap} instead of a short list that reads as complete;
|
|
20
|
+
* · "could not be read" is recorded separately from "does not exist";
|
|
21
|
+
* · plugins and secrets never enter a broad scope by accident.
|
|
22
|
+
*
|
|
23
|
+
* The gate rules themselves stay pure: they receive {@link ArtifactDigest}
|
|
24
|
+
* values from here and never touch the filesystem.
|
|
25
|
+
* @module dsh-agent-teams/artifacts
|
|
26
|
+
*/
|
|
27
|
+
import { createHash } from 'node:crypto';
|
|
28
|
+
import { closeSync, lstatSync, openSync, readSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
29
|
+
import { isAbsolute, join, relative, sep } from 'node:path';
|
|
30
|
+
import { isDefaultExcludedPath, normalizeWorkspacePath } from "./quality-gates.js";
|
|
31
|
+
/**
|
|
32
|
+
* Directory segments never walked when a scope pattern names a directory:
|
|
33
|
+
* version control, the harness state root, plugin-private team state, and
|
|
34
|
+
* installed packages. They are either huge, plugin-private, or not project
|
|
35
|
+
* artifacts.
|
|
36
|
+
*/
|
|
37
|
+
const SKIPPED_SEGMENTS = new Set(['.git', '.dsh', '.agent-teams', 'node_modules']);
|
|
38
|
+
/**
|
|
39
|
+
* Upper bound on files expanded from one scope. Verdicts are about a revision
|
|
40
|
+
* and an unbounded walk of a large tree would make every gate close slow, so
|
|
41
|
+
* the walk stops here — and says so with a gap, because a silently truncated
|
|
42
|
+
* list would let a change outside the list pass as "unchanged".
|
|
43
|
+
*/
|
|
44
|
+
export const MAX_EXPANDED_ARTIFACTS = 200;
|
|
45
|
+
function errorCode(error) {
|
|
46
|
+
const code = error?.code;
|
|
47
|
+
return typeof code === 'string' ? code : 'EIO';
|
|
48
|
+
}
|
|
49
|
+
function workspaceRootOf(workspace) {
|
|
50
|
+
try {
|
|
51
|
+
return realpathSync(workspace);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Is `absolute` (already resolved) inside the workspace root? */
|
|
58
|
+
function insideRoot(root, absolute) {
|
|
59
|
+
const rel = relative(root, absolute);
|
|
60
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
61
|
+
}
|
|
62
|
+
/** `true` inside, `false` escapes the workspace, `undefined` unresolvable. */
|
|
63
|
+
function resolvesInside(root, absolute) {
|
|
64
|
+
try {
|
|
65
|
+
return insideRoot(root, realpathSync(absolute));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Chunk size for the streaming digest. The activity panel polls the audit
|
|
73
|
+
* about once a second, so hashing must never read a whole artifact into
|
|
74
|
+
* memory: a multi-gigabyte file would otherwise stall the host process.
|
|
75
|
+
*/
|
|
76
|
+
const HASH_CHUNK_BYTES = 64 * 1024;
|
|
77
|
+
/**
|
|
78
|
+
* sha256 one file in constant memory (one reusable chunk buffer), never
|
|
79
|
+
* loading the whole artifact. Throws the underlying filesystem error so the
|
|
80
|
+
* caller can distinguish "cannot read" from "absent".
|
|
81
|
+
*/
|
|
82
|
+
function hashFileConstantMemory(absolute) {
|
|
83
|
+
const handle = openSync(absolute, 'r');
|
|
84
|
+
try {
|
|
85
|
+
const hash = createHash('sha256');
|
|
86
|
+
const buffer = Buffer.allocUnsafe(HASH_CHUNK_BYTES);
|
|
87
|
+
let size = 0;
|
|
88
|
+
for (;;) {
|
|
89
|
+
const read = readSync(handle, buffer, 0, buffer.byteLength, null);
|
|
90
|
+
if (read <= 0)
|
|
91
|
+
break;
|
|
92
|
+
size += read;
|
|
93
|
+
// `update` consumes the bytes synchronously, so the buffer is reused.
|
|
94
|
+
hash.update(read === buffer.byteLength ? buffer : buffer.subarray(0, read));
|
|
95
|
+
}
|
|
96
|
+
return { sha256: hash.digest('hex'), size };
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
closeSync(handle);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function inspectPath(workspaceRoot, workspace, normalized) {
|
|
103
|
+
const absolute = join(workspace, normalized);
|
|
104
|
+
let stats;
|
|
105
|
+
try {
|
|
106
|
+
stats = lstatSync(absolute);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const code = errorCode(error);
|
|
110
|
+
return code === 'ENOENT' || code === 'ENOTDIR'
|
|
111
|
+
? { kind: 'absent' }
|
|
112
|
+
: { kind: 'unreadable', reason: `${normalized} cannot be inspected (${code})` };
|
|
113
|
+
}
|
|
114
|
+
if (workspaceRoot === undefined) {
|
|
115
|
+
return { kind: 'unreadable', reason: `${normalized} cannot be verified against the workspace root` };
|
|
116
|
+
}
|
|
117
|
+
// Containment is checked on the fully resolved path, not only when the last
|
|
118
|
+
// segment is a symlink: an intermediate symlinked directory would otherwise
|
|
119
|
+
// let any inner path read (or walk) host files outside the workspace.
|
|
120
|
+
const inside = resolvesInside(workspaceRoot, absolute);
|
|
121
|
+
if (inside === undefined)
|
|
122
|
+
return { kind: 'unreadable', reason: `${normalized} cannot be resolved (broken or cyclic link)` };
|
|
123
|
+
if (!inside)
|
|
124
|
+
return { kind: 'unreadable', reason: `${normalized} resolves outside the workspace; refusing to read it` };
|
|
125
|
+
if (stats.isSymbolicLink()) {
|
|
126
|
+
try {
|
|
127
|
+
stats = statSync(absolute);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return { kind: 'unreadable', reason: `${normalized} symlink target cannot be inspected (${errorCode(error)})` };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (stats.isDirectory())
|
|
134
|
+
return { kind: 'directory' };
|
|
135
|
+
if (!stats.isFile())
|
|
136
|
+
return { kind: 'unreadable', reason: `${normalized} is not a regular file` };
|
|
137
|
+
try {
|
|
138
|
+
const { sha256, size } = hashFileConstantMemory(absolute);
|
|
139
|
+
return {
|
|
140
|
+
kind: 'file',
|
|
141
|
+
digest: {
|
|
142
|
+
path: normalized,
|
|
143
|
+
present: true,
|
|
144
|
+
sha256,
|
|
145
|
+
size,
|
|
146
|
+
mtimeMs: stats.mtimeMs,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return { kind: 'unreadable', reason: `${normalized} cannot be read (${errorCode(error)})` };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function isSkipped(relativePath) {
|
|
155
|
+
return relativePath.split('/').some((segment) => SKIPPED_SEGMENTS.has(segment)) || isDefaultExcludedPath(relativePath);
|
|
156
|
+
}
|
|
157
|
+
function walkDirectory(input) {
|
|
158
|
+
// The prefix itself must be inside the workspace before it is enumerated:
|
|
159
|
+
// `escape/` may be a symlink to an outside directory, and readdir would
|
|
160
|
+
// happily list host names through it. Nested prefixes come from
|
|
161
|
+
// non-symlink directory entries of an already-verified parent.
|
|
162
|
+
if (input.prefix !== '') {
|
|
163
|
+
if (input.workspaceRoot === undefined) {
|
|
164
|
+
input.gap(`${input.pattern}: the workspace root cannot be resolved, so it is not enumerated`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
let inside;
|
|
168
|
+
try {
|
|
169
|
+
inside = insideRoot(input.workspaceRoot, realpathSync(join(input.workspace, input.prefix)));
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (errorCode(error) === 'ENOENT' || errorCode(error) === 'ENOTDIR') {
|
|
173
|
+
input.gap(`${input.pattern}: ${input.prefix} does not exist, so the scope could not be enumerated`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
inside = undefined;
|
|
177
|
+
}
|
|
178
|
+
if (inside === undefined) {
|
|
179
|
+
input.gap(`${input.pattern}: ${input.prefix} cannot be resolved (broken or cyclic link); refusing to enumerate it`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (!inside) {
|
|
183
|
+
input.gap(`${input.pattern}: ${input.prefix} resolves outside the workspace; refusing to enumerate it`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const queue = input.prefix === '' ? [''] : [input.prefix];
|
|
188
|
+
while (queue.length > 0) {
|
|
189
|
+
const current = queue.shift();
|
|
190
|
+
let entries;
|
|
191
|
+
try {
|
|
192
|
+
entries = readdirSync(join(input.workspace, current), { withFileTypes: true });
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
input.gap(`${input.pattern}: cannot list ${current === '' ? '.' : current} (${errorCode(error)})`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
199
|
+
const relativePath = `${current === '' ? '' : `${current}/`}${entry.name}`;
|
|
200
|
+
// Traversal never follows symlinks: a symlinked directory can point
|
|
201
|
+
// outside the workspace or form a cycle, and neither may become a read
|
|
202
|
+
// or an unbounded walk.
|
|
203
|
+
if (entry.isSymbolicLink())
|
|
204
|
+
continue;
|
|
205
|
+
if (isSkipped(relativePath))
|
|
206
|
+
continue;
|
|
207
|
+
if (entry.isDirectory()) {
|
|
208
|
+
queue.push(relativePath);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (!entry.isFile())
|
|
212
|
+
continue;
|
|
213
|
+
if (input.full()) {
|
|
214
|
+
input.gap(`${input.pattern}: stopped at the ${MAX_EXPANDED_ARTIFACTS}-file cap, so its contents are only partially recorded`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
input.push(relativePath);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Read a declared scope: expand directory patterns to their current files,
|
|
223
|
+
* keep explicitly named paths even when they do not exist (a later creation is
|
|
224
|
+
* drift too), and report every part that could not be fully resolved.
|
|
225
|
+
*/
|
|
226
|
+
export function readArtifactScope(workspace, patterns) {
|
|
227
|
+
const workspaceRoot = workspaceRootOf(workspace);
|
|
228
|
+
const gaps = [];
|
|
229
|
+
const concrete = [];
|
|
230
|
+
const seen = new Set();
|
|
231
|
+
const full = () => concrete.length >= MAX_EXPANDED_ARTIFACTS;
|
|
232
|
+
const push = (path) => {
|
|
233
|
+
if (seen.has(path))
|
|
234
|
+
return;
|
|
235
|
+
seen.add(path);
|
|
236
|
+
concrete.push(path);
|
|
237
|
+
};
|
|
238
|
+
const gap = (pattern, reason) => {
|
|
239
|
+
if (gaps.some((item) => item.pattern === pattern && item.reason === reason))
|
|
240
|
+
return;
|
|
241
|
+
gaps.push({ pattern, reason });
|
|
242
|
+
};
|
|
243
|
+
for (const raw of patterns) {
|
|
244
|
+
if (typeof raw !== 'string')
|
|
245
|
+
continue;
|
|
246
|
+
const trimmed = raw.trim();
|
|
247
|
+
const posix = trimmed.replaceAll('\\', '/');
|
|
248
|
+
const directory = posix.endsWith('/') || posix === '.';
|
|
249
|
+
const normalized = normalizeWorkspacePath(raw);
|
|
250
|
+
if (normalized === undefined) {
|
|
251
|
+
// Absolute paths, `..`, `~`, drive letters: illegal, and never a reason
|
|
252
|
+
// to fall back to the workspace root.
|
|
253
|
+
gap(trimmed, 'not a workspace-relative path (absolute paths and ".." can never match scope)');
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (!directory) {
|
|
257
|
+
if (normalized === '') {
|
|
258
|
+
gap(trimmed, 'empty path is not a file; use "." for the whole workspace');
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (full()) {
|
|
262
|
+
gap(trimmed, `stopped at the ${MAX_EXPANDED_ARTIFACTS}-path cap, so the scope is only partially recorded`);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
push(normalized);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
// Only an explicitly root-anchored pattern means the workspace root.
|
|
269
|
+
const rootAnchored = posix === '.' || posix === './' || posix.startsWith('./');
|
|
270
|
+
if (normalized === '' && !rootAnchored) {
|
|
271
|
+
gap(trimmed, 'unresolved directory pattern; use "./" explicitly for the workspace root');
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (full()) {
|
|
275
|
+
gap(trimmed, `stopped at the ${MAX_EXPANDED_ARTIFACTS}-path cap, so its contents are only partially recorded`);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
walkDirectory({ workspace, workspaceRoot, pattern: trimmed, prefix: normalized, push, gap: (reason) => gap(trimmed, reason), full });
|
|
279
|
+
}
|
|
280
|
+
const artifacts = [];
|
|
281
|
+
// Index-based: expanding an explicitly named directory appends its files to
|
|
282
|
+
// `concrete`, and those must be digested in the same pass.
|
|
283
|
+
for (let index = 0; index < concrete.length; index += 1) {
|
|
284
|
+
const normalized = concrete[index];
|
|
285
|
+
const outcome = inspectPath(workspaceRoot, workspace, normalized);
|
|
286
|
+
if (outcome.kind === 'file') {
|
|
287
|
+
artifacts.push(outcome.digest);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (outcome.kind === 'directory') {
|
|
291
|
+
// An explicitly named directory is expanded instead of being reported as
|
|
292
|
+
// unreadable: `src/lib` and `src/lib/` must behave the same.
|
|
293
|
+
walkDirectory({ workspace, workspaceRoot, pattern: normalized, prefix: normalized, push, gap: (reason) => gap(normalized, reason), full });
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (outcome.kind === 'absent') {
|
|
297
|
+
artifacts.push({ path: normalized, present: false });
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
artifacts.push({ path: normalized, present: false, unreadable: true, reason: outcome.reason });
|
|
301
|
+
}
|
|
302
|
+
return { artifacts: artifacts.sort((a, b) => a.path.localeCompare(b.path)), gaps };
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Read the current bytes of workspace-relative paths (directories expand).
|
|
306
|
+
* Illegal and unreadable paths are reported, never silently dropped.
|
|
307
|
+
*/
|
|
308
|
+
export function digestWorkspacePaths(workspace, paths) {
|
|
309
|
+
return readArtifactScope(workspace, paths).artifacts;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Does this workspace-relative path currently exist as a file or directory?
|
|
313
|
+
* Uses the same containment rules as the digest reader: a symlink that leaves
|
|
314
|
+
* the workspace is not "an existing workspace path".
|
|
315
|
+
*/
|
|
316
|
+
export function workspacePathExists(workspace, path) {
|
|
317
|
+
const normalized = normalizeWorkspacePath(path);
|
|
318
|
+
if (normalized === undefined)
|
|
319
|
+
return false;
|
|
320
|
+
const outcome = inspectPath(workspaceRootOf(workspace), workspace, normalized);
|
|
321
|
+
return outcome.kind === 'file' || outcome.kind === 'directory';
|
|
322
|
+
}
|
|
323
|
+
/** Expand scope patterns to the concrete files they currently cover. */
|
|
324
|
+
export function expandScopePaths(workspace, patterns) {
|
|
325
|
+
return readArtifactScope(workspace, patterns)
|
|
326
|
+
.artifacts
|
|
327
|
+
.filter((artifact) => artifact.present)
|
|
328
|
+
.map((artifact) => artifact.path)
|
|
329
|
+
.sort();
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Read one coherent snapshot of the given scope patterns. Directory patterns
|
|
333
|
+
* expand to the files they currently contain; a pattern naming a path that
|
|
334
|
+
* does not exist is recorded as `present: false` so a later creation or
|
|
335
|
+
* deletion is visible as drift, and any incomplete coverage is carried in
|
|
336
|
+
* `gaps`.
|
|
337
|
+
*/
|
|
338
|
+
export function snapshotWorkspacePaths(workspace, patterns) {
|
|
339
|
+
const at = Date.now();
|
|
340
|
+
const { artifacts, gaps } = readArtifactScope(workspace, patterns);
|
|
341
|
+
return { at, artifacts, ...gaps.length === 0 ? {} : { gaps } };
|
|
342
|
+
}
|
package/lib/capabilities.js
CHANGED
|
@@ -3,6 +3,7 @@ import { onAgentReady } from "./harness-compat.js";
|
|
|
3
3
|
import { readdirSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { readTeamSync, readRetiredMemberIdsSync } from "./state.js";
|
|
6
|
+
import { leadIdentityOf, teamLeadMatch } from "./lineage.js";
|
|
6
7
|
import { MEMBER_TOOL_NAMES, TEAM_TOOL_NAMES } from "./tool-names.js";
|
|
7
8
|
export const TEAM_ACTIVATION_PROMPT = 'AgentTeams (Agent Teams) provides multi-agent team collaboration. Apply these rules when the user requests it (including /agent-teams) or when continuing an existing team. Mentioning, quoting, discussing, or declining AgentTeams alone is not a request to start work.';
|
|
8
9
|
export const TEAM_MEMBER_PROMPT = 'You are an AgentTeams member. Follow your assigned member persona and task contract. Use agent_teams_claim_task, agent_teams_update_task, agent_teams_send_message and agent_teams_status for your own work. Include the current attempt_id in updates; report completion or failure to the captain. Do not create, approve, edit or resume a team. If your durable membership is unavailable, report that to the parent instead of creating a replacement.';
|
|
@@ -10,7 +11,8 @@ function stateRoot(agent, config) {
|
|
|
10
11
|
return join(agent.session.header.cwd ?? process.cwd(), config.stateDir);
|
|
11
12
|
}
|
|
12
13
|
/** Synchronous startup/HMR hydration must finish before the first assembly. */
|
|
13
|
-
function currentTeam(agent, config) {
|
|
14
|
+
function currentTeam(ctx, agent, config) {
|
|
15
|
+
const lead = leadIdentityOf(ctx, agent.session);
|
|
14
16
|
const root = stateRoot(agent, config);
|
|
15
17
|
let entries;
|
|
16
18
|
try {
|
|
@@ -26,7 +28,7 @@ function currentTeam(agent, config) {
|
|
|
26
28
|
if (!entry.isDirectory() || entry.name === 'archive')
|
|
27
29
|
continue;
|
|
28
30
|
const team = readTeamSync(root, entry.name);
|
|
29
|
-
if (team === undefined || (team
|
|
31
|
+
if (team === undefined || (!teamLeadMatch(team, lead)
|
|
30
32
|
&& !team.members.some(member => member.id === agent.id)))
|
|
31
33
|
continue;
|
|
32
34
|
if (found !== undefined)
|
|
@@ -55,8 +57,8 @@ export function installTeamCapabilities(ctx, config) {
|
|
|
55
57
|
let member = config.isPendingMember(agent);
|
|
56
58
|
try {
|
|
57
59
|
member ||= readRetiredMemberIdsSync(stateRoot(agent, config)).has(agent.id);
|
|
58
|
-
const team = currentTeam(agent, config);
|
|
59
|
-
member ||= team !== undefined && team
|
|
60
|
+
const team = currentTeam(ctx, agent, config);
|
|
61
|
+
member ||= team !== undefined && !teamLeadMatch(team, leadIdentityOf(ctx, agent.session));
|
|
60
62
|
}
|
|
61
63
|
catch (error) {
|
|
62
64
|
// Unrelated damaged state must not disable ordinary conversation.
|