cc-agent-harness 0.0.1
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/LICENSE +21 -0
- package/README.md +236 -0
- package/README.zh-CN.md +236 -0
- package/dist/chunk-3P72TGGQ.js +82 -0
- package/dist/chunk-3P72TGGQ.js.map +1 -0
- package/dist/chunk-JKJ3FP6T.js +51 -0
- package/dist/chunk-JKJ3FP6T.js.map +1 -0
- package/dist/chunk-LOE6IDTT.js +65 -0
- package/dist/chunk-LOE6IDTT.js.map +1 -0
- package/dist/chunk-PQWK2OBN.js +18 -0
- package/dist/chunk-PQWK2OBN.js.map +1 -0
- package/dist/chunk-R6VGYQOH.js +771 -0
- package/dist/chunk-R6VGYQOH.js.map +1 -0
- package/dist/config-PUMLWJWT.js +46 -0
- package/dist/config-PUMLWJWT.js.map +1 -0
- package/dist/context-SRWWNVTI.js +33 -0
- package/dist/context-SRWWNVTI.js.map +1 -0
- package/dist/doctor-TYLZH27K.js +199 -0
- package/dist/doctor-TYLZH27K.js.map +1 -0
- package/dist/harness.js +62 -0
- package/dist/harness.js.map +1 -0
- package/dist/index.d.ts +846 -0
- package/dist/index.js +1181 -0
- package/dist/index.js.map +1 -0
- package/dist/list-ABNY2TO7.js +124 -0
- package/dist/list-ABNY2TO7.js.map +1 -0
- package/dist/loader-RDQZFHOB.js +19 -0
- package/dist/loader-RDQZFHOB.js.map +1 -0
- package/dist/run-I5GVJH3N.js +45 -0
- package/dist/run-I5GVJH3N.js.map +1 -0
- package/dist/scaffold-C6JA3STC.js +98 -0
- package/dist/scaffold-C6JA3STC.js.map +1 -0
- package/dist/schema-AHX7LXUQ.js +27 -0
- package/dist/schema-AHX7LXUQ.js.map +1 -0
- package/dist/setup-CWDCKM34.js +115 -0
- package/dist/setup-CWDCKM34.js.map +1 -0
- package/dist/update-CTPDQCLU.js +87 -0
- package/dist/update-CTPDQCLU.js.map +1 -0
- package/dist/verify-NI3VCE2H.js +136 -0
- package/dist/verify-NI3VCE2H.js.map +1 -0
- package/package.json +61 -0
- package/templates/agents-md/full.md.tmpl +98 -0
- package/templates/agents-md/minimal.md.tmpl +20 -0
- package/templates/agents-md/standard.md.tmpl +43 -0
- package/templates/configs/harness.config.yaml.tmpl +28 -0
- package/templates/skills/basic/SKILL.md.tmpl +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KwokJay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
<p align="center"><code>npm install -g cc-agent-harness</code></p>
|
|
2
|
+
<p align="center"><strong>cc-agent-harness</strong> is a vendor-neutral CLI and TypeScript toolkit for AI-assisted development workflows.</p>
|
|
3
|
+
<p align="center"><a href="./README.md">English</a> | <a href="./README.zh-CN.md">简体中文</a></p>
|
|
4
|
+
|
|
5
|
+
It helps teams standardize project setup, `AGENTS.md` generation, skill discovery, health checks, and verification pipelines without locking into a single model vendor or agent framework.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
### Install and run
|
|
12
|
+
|
|
13
|
+
Install globally with npm:
|
|
14
|
+
|
|
15
|
+
```shell
|
|
16
|
+
npm install -g cc-agent-harness
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then initialize a project with the `agent-harness` CLI:
|
|
20
|
+
|
|
21
|
+
```shell
|
|
22
|
+
agent-harness setup
|
|
23
|
+
agent-harness doctor
|
|
24
|
+
agent-harness verify
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Develop from source
|
|
28
|
+
|
|
29
|
+
This repository targets `Node >=22` and uses `pnpm` for local development.
|
|
30
|
+
|
|
31
|
+
```shell
|
|
32
|
+
pnpm install
|
|
33
|
+
pnpm build
|
|
34
|
+
pnpm test
|
|
35
|
+
pnpm lint
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## What It Does
|
|
39
|
+
|
|
40
|
+
- Generates a consistent `.harness/` project scaffold and `AGENTS.md` from templates.
|
|
41
|
+
- Loads layered YAML configuration and validates it with Zod.
|
|
42
|
+
- Routes agent/model tiers through vendor-neutral `low` / `medium` / `high` mappings.
|
|
43
|
+
- Discovers, validates, and scaffolds reusable project skills.
|
|
44
|
+
- Detects project type and resolves verification commands for TypeScript, Python, and Rust.
|
|
45
|
+
- Runs project health checks plus configurable verification pipelines like build, test, and lint.
|
|
46
|
+
- Assembles reusable agent context from hierarchical `AGENTS.md`, custom rules, and discovered skills.
|
|
47
|
+
- Exposes lifecycle hooks, audit logging, and feature observability through the main CLI.
|
|
48
|
+
- Exposes the same primitives as a TypeScript library for deeper integration.
|
|
49
|
+
|
|
50
|
+
## Common Commands
|
|
51
|
+
|
|
52
|
+
| Command | Description |
|
|
53
|
+
|---------|-------------|
|
|
54
|
+
| `agent-harness setup` | Initialize `.harness/` and generate `AGENTS.md` |
|
|
55
|
+
| `agent-harness update` | Sync templates and configuration |
|
|
56
|
+
| `agent-harness doctor` | Run health checks for the current project |
|
|
57
|
+
| `agent-harness doctor --json` | Output machine-readable health results |
|
|
58
|
+
| `agent-harness verify` | Execute the configured verification pipeline |
|
|
59
|
+
| `agent-harness verify --json` | Output machine-readable verification results |
|
|
60
|
+
| `agent-harness run <task>` | Run a named workflow or adapter command |
|
|
61
|
+
| `agent-harness context build` | Build reusable agent context from project docs and skills |
|
|
62
|
+
| `agent-harness list <resource>` | List `skills`, `agents`, `commands`, `templates`, or `features` |
|
|
63
|
+
| `agent-harness config show` | Print merged configuration |
|
|
64
|
+
| `agent-harness config validate` | Validate config files |
|
|
65
|
+
| `agent-harness schema generate` | Generate JSON Schema for the config |
|
|
66
|
+
| `agent-harness scaffold skill <name>` | Create a new skill scaffold |
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
Project config lives at `.harness/harness.config.yaml`. Optional user-level defaults can live at `~/.harness/config.yaml`.
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
project:
|
|
74
|
+
name: my-project
|
|
75
|
+
language: typescript
|
|
76
|
+
description: "My AI-assisted project"
|
|
77
|
+
|
|
78
|
+
agents:
|
|
79
|
+
delegation_first: true
|
|
80
|
+
model_routing:
|
|
81
|
+
low: low
|
|
82
|
+
medium: medium
|
|
83
|
+
high: high
|
|
84
|
+
providers:
|
|
85
|
+
low: "gpt-4o-mini"
|
|
86
|
+
medium: "claude-sonnet-4-20250514"
|
|
87
|
+
high: "o3"
|
|
88
|
+
definitions: []
|
|
89
|
+
|
|
90
|
+
skills:
|
|
91
|
+
directories:
|
|
92
|
+
- ".harness/skills"
|
|
93
|
+
auto_detect: true
|
|
94
|
+
|
|
95
|
+
workflows:
|
|
96
|
+
commands:
|
|
97
|
+
build: "npm run build"
|
|
98
|
+
test: "npm test"
|
|
99
|
+
lint: "npm run lint"
|
|
100
|
+
verification:
|
|
101
|
+
checks: ["build", "test", "lint"]
|
|
102
|
+
|
|
103
|
+
templates:
|
|
104
|
+
agents_md:
|
|
105
|
+
variant: standard
|
|
106
|
+
custom_rules: []
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Model Tiers
|
|
110
|
+
|
|
111
|
+
`cc-agent-harness` keeps model routing vendor-neutral. Agents and workflows use `low`, `medium`, and `high`, while `providers` maps those tiers to concrete model IDs.
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
agents:
|
|
115
|
+
providers:
|
|
116
|
+
low: "gpt-4o-mini"
|
|
117
|
+
medium: "claude-sonnet-4-20250514"
|
|
118
|
+
high: "o3"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Programmatic API
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import {
|
|
125
|
+
HarnessRuntime,
|
|
126
|
+
loadConfig,
|
|
127
|
+
AgentRegistry,
|
|
128
|
+
discoverSkills,
|
|
129
|
+
routeModel,
|
|
130
|
+
inferComplexity,
|
|
131
|
+
runHealthChecks,
|
|
132
|
+
render,
|
|
133
|
+
} from "cc-agent-harness";
|
|
134
|
+
|
|
135
|
+
const config = await loadConfig();
|
|
136
|
+
const runtime = await HarnessRuntime.create();
|
|
137
|
+
const registry = new AgentRegistry(config.agents.definitions);
|
|
138
|
+
const agent = registry.get("executor");
|
|
139
|
+
const tier = routeModel(
|
|
140
|
+
inferComplexity("refactor the auth module"),
|
|
141
|
+
config.agents.model_routing,
|
|
142
|
+
);
|
|
143
|
+
const skills = await discoverSkills(config.skills.directories);
|
|
144
|
+
const report = await runHealthChecks([]);
|
|
145
|
+
const context = await runtime.buildContext({ tagStyle: "xml" });
|
|
146
|
+
const output = render("Hello {{name}}", { name: "World" });
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Built-in Adapters
|
|
150
|
+
|
|
151
|
+
Built-in adapters detect the current project and provide default commands and checks.
|
|
152
|
+
|
|
153
|
+
| Adapter | Detection | Typical commands |
|
|
154
|
+
|---------|-----------|------------------|
|
|
155
|
+
| TypeScript | `tsconfig.json` or `package.json` | `build`, `test`, `lint` |
|
|
156
|
+
| Python | `pyproject.toml`, `setup.py`, or `requirements.txt` | `test`, `lint`, `fmt` |
|
|
157
|
+
| Rust | `Cargo.toml` | `fmt`, `test`, `clippy`, `build` |
|
|
158
|
+
|
|
159
|
+
## Skills
|
|
160
|
+
|
|
161
|
+
Skills are directories containing a `SKILL.md` file with YAML frontmatter.
|
|
162
|
+
|
|
163
|
+
```markdown
|
|
164
|
+
---
|
|
165
|
+
name: my-skill
|
|
166
|
+
description: What this skill does
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
# My Skill
|
|
170
|
+
|
|
171
|
+
Usage instructions here.
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Create one with:
|
|
175
|
+
|
|
176
|
+
```shell
|
|
177
|
+
agent-harness scaffold skill my-skill -d "Description of the skill"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Context Assembly
|
|
181
|
+
|
|
182
|
+
Build reusable prompt context from hierarchical `AGENTS.md` files, configured custom rules, and discovered skills:
|
|
183
|
+
|
|
184
|
+
```shell
|
|
185
|
+
agent-harness context build
|
|
186
|
+
agent-harness context build --format xml
|
|
187
|
+
agent-harness context build --output .harness/context.md
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
This is useful when integrating with IDE agents, external runners, or any workflow that needs a stable context artifact.
|
|
191
|
+
|
|
192
|
+
## Package And CLI
|
|
193
|
+
|
|
194
|
+
- npm package: `cc-agent-harness`
|
|
195
|
+
- CLI command: `agent-harness`
|
|
196
|
+
|
|
197
|
+
## Architecture
|
|
198
|
+
|
|
199
|
+
Generated project structure:
|
|
200
|
+
|
|
201
|
+
```text
|
|
202
|
+
.harness/
|
|
203
|
+
harness.config.yaml
|
|
204
|
+
skills/
|
|
205
|
+
AGENTS.md
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Core source layout:
|
|
209
|
+
|
|
210
|
+
```text
|
|
211
|
+
src/
|
|
212
|
+
config/ Schema, defaults, layered config loading
|
|
213
|
+
agent/ Agent registry and model tier routing
|
|
214
|
+
adapter/ Project type detection and language adapters
|
|
215
|
+
skill/ Skill discovery, validation, scaffolding
|
|
216
|
+
health/ Health checks and reporting
|
|
217
|
+
template/ Template rendering and file generation
|
|
218
|
+
hook/ Lifecycle hook discovery and dispatch
|
|
219
|
+
feature/ Feature registry
|
|
220
|
+
plugin/ Plugin interface and registry
|
|
221
|
+
context/ Context assembly pipeline
|
|
222
|
+
audit/ Append-only audit logging
|
|
223
|
+
cli/ Command implementations
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Docs
|
|
227
|
+
|
|
228
|
+
- [`docs/getting-started.md`](./docs/getting-started.md)
|
|
229
|
+
- [`docs/architecture.md`](./docs/architecture.md)
|
|
230
|
+
- [`docs/config-reference.md`](./docs/config-reference.md)
|
|
231
|
+
- [`docs/adapter-guide.md`](./docs/adapter-guide.md)
|
|
232
|
+
- [`docs/plugin-guide.md`](./docs/plugin-guide.md)
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
Licensed under the [MIT License](./LICENSE).
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
<p align="center"><code>npm install -g cc-agent-harness</code></p>
|
|
2
|
+
<p align="center"><strong>cc-agent-harness</strong> 是一个面向 AI 辅助开发工作流的、厂商中立的 CLI 与 TypeScript 工具包。</p>
|
|
3
|
+
<p align="center"><a href="./README.md">English</a> | <a href="./README.zh-CN.md">简体中文</a></p>
|
|
4
|
+
|
|
5
|
+
它帮助团队统一项目初始化、`AGENTS.md` 生成、技能发现、健康检查和验证流水线,同时避免被某一家模型供应商或某一种 agent 框架绑定。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 快速开始
|
|
10
|
+
|
|
11
|
+
### 安装并使用
|
|
12
|
+
|
|
13
|
+
使用 npm 全局安装:
|
|
14
|
+
|
|
15
|
+
```shell
|
|
16
|
+
npm install -g cc-agent-harness
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
安装后使用 `agent-harness` CLI 进入项目初始化:
|
|
20
|
+
|
|
21
|
+
```shell
|
|
22
|
+
agent-harness setup
|
|
23
|
+
agent-harness doctor
|
|
24
|
+
agent-harness verify
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 从源码开发
|
|
28
|
+
|
|
29
|
+
本仓库要求 `Node >=22`,本地开发使用 `pnpm`。
|
|
30
|
+
|
|
31
|
+
```shell
|
|
32
|
+
pnpm install
|
|
33
|
+
pnpm build
|
|
34
|
+
pnpm test
|
|
35
|
+
pnpm lint
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 它能做什么
|
|
39
|
+
|
|
40
|
+
- 生成统一的 `.harness/` 项目骨架,并按模板产出 `AGENTS.md`。
|
|
41
|
+
- 加载分层 YAML 配置,并通过 Zod 做结构校验。
|
|
42
|
+
- 使用厂商中立的 `low` / `medium` / `high` 档位路由 agent 与模型。
|
|
43
|
+
- 发现、校验并脚手架化可复用技能。
|
|
44
|
+
- 检测当前项目类型,并为 TypeScript、Python、Rust 提供默认验证命令。
|
|
45
|
+
- 执行项目健康检查,以及可配置的 build、test、lint 验证流水线。
|
|
46
|
+
- 基于分层 `AGENTS.md`、自定义规则和本地技能构建可复用上下文。
|
|
47
|
+
- 在主 CLI 中暴露 hooks、审计日志和 feature 可观测能力。
|
|
48
|
+
- 以 TypeScript 库形式暴露同一套能力,便于程序化集成。
|
|
49
|
+
|
|
50
|
+
## 常用命令
|
|
51
|
+
|
|
52
|
+
| 命令 | 说明 |
|
|
53
|
+
|------|------|
|
|
54
|
+
| `agent-harness setup` | 初始化 `.harness/` 并生成 `AGENTS.md` |
|
|
55
|
+
| `agent-harness update` | 同步模板和配置 |
|
|
56
|
+
| `agent-harness doctor` | 对当前项目执行健康检查 |
|
|
57
|
+
| `agent-harness doctor --json` | 输出机器可读的健康检查结果 |
|
|
58
|
+
| `agent-harness verify` | 运行配置好的验证流水线 |
|
|
59
|
+
| `agent-harness verify --json` | 输出机器可读的验证结果 |
|
|
60
|
+
| `agent-harness run <task>` | 执行命名工作流或适配器提供的命令 |
|
|
61
|
+
| `agent-harness context build` | 构建可复用的 agent 上下文 |
|
|
62
|
+
| `agent-harness list <resource>` | 列出 `skills`、`agents`、`commands`、`templates` 或 `features` |
|
|
63
|
+
| `agent-harness config show` | 输出合并后的配置 |
|
|
64
|
+
| `agent-harness config validate` | 校验配置文件是否合法 |
|
|
65
|
+
| `agent-harness schema generate` | 生成配置对应的 JSON Schema |
|
|
66
|
+
| `agent-harness scaffold skill <name>` | 创建一个新的 skill 脚手架 |
|
|
67
|
+
|
|
68
|
+
## 配置
|
|
69
|
+
|
|
70
|
+
项目级配置位于 `.harness/harness.config.yaml`,可选的用户级默认配置位于 `~/.harness/config.yaml`。
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
project:
|
|
74
|
+
name: my-project
|
|
75
|
+
language: typescript
|
|
76
|
+
description: "My AI-assisted project"
|
|
77
|
+
|
|
78
|
+
agents:
|
|
79
|
+
delegation_first: true
|
|
80
|
+
model_routing:
|
|
81
|
+
low: low
|
|
82
|
+
medium: medium
|
|
83
|
+
high: high
|
|
84
|
+
providers:
|
|
85
|
+
low: "gpt-4o-mini"
|
|
86
|
+
medium: "claude-sonnet-4-20250514"
|
|
87
|
+
high: "o3"
|
|
88
|
+
definitions: []
|
|
89
|
+
|
|
90
|
+
skills:
|
|
91
|
+
directories:
|
|
92
|
+
- ".harness/skills"
|
|
93
|
+
auto_detect: true
|
|
94
|
+
|
|
95
|
+
workflows:
|
|
96
|
+
commands:
|
|
97
|
+
build: "npm run build"
|
|
98
|
+
test: "npm test"
|
|
99
|
+
lint: "npm run lint"
|
|
100
|
+
verification:
|
|
101
|
+
checks: ["build", "test", "lint"]
|
|
102
|
+
|
|
103
|
+
templates:
|
|
104
|
+
agents_md:
|
|
105
|
+
variant: standard
|
|
106
|
+
custom_rules: []
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 模型档位
|
|
110
|
+
|
|
111
|
+
`cc-agent-harness` 的模型路由是厂商中立的。agent 和工作流只感知 `low`、`medium`、`high` 三个档位,`providers` 负责把这些档位映射到真实模型 ID。
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
agents:
|
|
115
|
+
providers:
|
|
116
|
+
low: "gpt-4o-mini"
|
|
117
|
+
medium: "claude-sonnet-4-20250514"
|
|
118
|
+
high: "o3"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 程序化 API
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import {
|
|
125
|
+
HarnessRuntime,
|
|
126
|
+
loadConfig,
|
|
127
|
+
AgentRegistry,
|
|
128
|
+
discoverSkills,
|
|
129
|
+
routeModel,
|
|
130
|
+
inferComplexity,
|
|
131
|
+
runHealthChecks,
|
|
132
|
+
render,
|
|
133
|
+
} from "cc-agent-harness";
|
|
134
|
+
|
|
135
|
+
const config = await loadConfig();
|
|
136
|
+
const runtime = await HarnessRuntime.create();
|
|
137
|
+
const registry = new AgentRegistry(config.agents.definitions);
|
|
138
|
+
const agent = registry.get("executor");
|
|
139
|
+
const tier = routeModel(
|
|
140
|
+
inferComplexity("refactor the auth module"),
|
|
141
|
+
config.agents.model_routing,
|
|
142
|
+
);
|
|
143
|
+
const skills = await discoverSkills(config.skills.directories);
|
|
144
|
+
const report = await runHealthChecks([]);
|
|
145
|
+
const context = await runtime.buildContext({ tagStyle: "xml" });
|
|
146
|
+
const output = render("Hello {{name}}", { name: "World" });
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 内置适配器
|
|
150
|
+
|
|
151
|
+
内置适配器会自动检测项目类型,并提供默认命令和健康检查。
|
|
152
|
+
|
|
153
|
+
| 适配器 | 检测方式 | 常见命令 |
|
|
154
|
+
|--------|----------|----------|
|
|
155
|
+
| TypeScript | `tsconfig.json` 或 `package.json` | `build`、`test`、`lint` |
|
|
156
|
+
| Python | `pyproject.toml`、`setup.py` 或 `requirements.txt` | `test`、`lint`、`fmt` |
|
|
157
|
+
| Rust | `Cargo.toml` | `fmt`、`test`、`clippy`、`build` |
|
|
158
|
+
|
|
159
|
+
## Skills
|
|
160
|
+
|
|
161
|
+
Skill 是一个包含 `SKILL.md` 文件的目录,文件头使用 YAML frontmatter:
|
|
162
|
+
|
|
163
|
+
```markdown
|
|
164
|
+
---
|
|
165
|
+
name: my-skill
|
|
166
|
+
description: What this skill does
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
# My Skill
|
|
170
|
+
|
|
171
|
+
Usage instructions here.
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
可通过下面的命令快速创建:
|
|
175
|
+
|
|
176
|
+
```shell
|
|
177
|
+
agent-harness scaffold skill my-skill -d "Description of the skill"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## 上下文构建
|
|
181
|
+
|
|
182
|
+
你可以基于分层 `AGENTS.md`、配置里的自定义规则和发现到的技能,构建可复用的上下文产物:
|
|
183
|
+
|
|
184
|
+
```shell
|
|
185
|
+
agent-harness context build
|
|
186
|
+
agent-harness context build --format xml
|
|
187
|
+
agent-harness context build --output .harness/context.md
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
这对 IDE agent、外部执行器,以及任何需要稳定上下文输入的工作流都很有用。
|
|
191
|
+
|
|
192
|
+
## 包名与命令名
|
|
193
|
+
|
|
194
|
+
- npm 包名:`cc-agent-harness`
|
|
195
|
+
- CLI 命令:`agent-harness`
|
|
196
|
+
|
|
197
|
+
## 架构概览
|
|
198
|
+
|
|
199
|
+
生成后的项目结构:
|
|
200
|
+
|
|
201
|
+
```text
|
|
202
|
+
.harness/
|
|
203
|
+
harness.config.yaml
|
|
204
|
+
skills/
|
|
205
|
+
AGENTS.md
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
核心源码结构:
|
|
209
|
+
|
|
210
|
+
```text
|
|
211
|
+
src/
|
|
212
|
+
config/ Schema、默认值、分层配置加载
|
|
213
|
+
agent/ Agent 注册表与模型档位路由
|
|
214
|
+
adapter/ 项目类型检测与语言适配器
|
|
215
|
+
skill/ Skill 发现、校验、脚手架
|
|
216
|
+
health/ 健康检查与报告
|
|
217
|
+
template/ 模板渲染与文件生成
|
|
218
|
+
hook/ 生命周期 Hook 发现与分发
|
|
219
|
+
feature/ Feature 注册表
|
|
220
|
+
plugin/ 插件接口与注册表
|
|
221
|
+
context/ 上下文组装流水线
|
|
222
|
+
audit/ 追加写入式审计日志
|
|
223
|
+
cli/ CLI 命令实现
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## 文档
|
|
227
|
+
|
|
228
|
+
- [`docs/getting-started.md`](./docs/getting-started.md)
|
|
229
|
+
- [`docs/architecture.md`](./docs/architecture.md)
|
|
230
|
+
- [`docs/config-reference.md`](./docs/config-reference.md)
|
|
231
|
+
- [`docs/adapter-guide.md`](./docs/adapter-guide.md)
|
|
232
|
+
- [`docs/plugin-guide.md`](./docs/plugin-guide.md)
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
基于 [MIT License](./LICENSE) 开源。
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
harnessConfigSchema
|
|
4
|
+
} from "./chunk-LOE6IDTT.js";
|
|
5
|
+
|
|
6
|
+
// src/config/loader.ts
|
|
7
|
+
import { readFile } from "fs/promises";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { resolve, join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { parse as parseYaml } from "yaml";
|
|
12
|
+
var PROJECT_CONFIG_PATH = ".harness/harness.config.yaml";
|
|
13
|
+
var USER_CONFIG_PATH = ".harness/config.yaml";
|
|
14
|
+
async function readYamlFile(path) {
|
|
15
|
+
if (!existsSync(path)) return null;
|
|
16
|
+
const content = await readFile(path, "utf-8");
|
|
17
|
+
return parseYaml(content) ?? null;
|
|
18
|
+
}
|
|
19
|
+
function deepMerge(base, override) {
|
|
20
|
+
const result = { ...base };
|
|
21
|
+
for (const key of Object.keys(override)) {
|
|
22
|
+
const bVal = base[key];
|
|
23
|
+
const oVal = override[key];
|
|
24
|
+
if (bVal && oVal && typeof bVal === "object" && typeof oVal === "object" && !Array.isArray(bVal) && !Array.isArray(oVal)) {
|
|
25
|
+
result[key] = deepMerge(
|
|
26
|
+
bVal,
|
|
27
|
+
oVal
|
|
28
|
+
);
|
|
29
|
+
} else {
|
|
30
|
+
result[key] = oVal;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
async function loadConfig(opts) {
|
|
36
|
+
const result = await loadConfigWithLayers(opts);
|
|
37
|
+
return result.config;
|
|
38
|
+
}
|
|
39
|
+
async function loadConfigWithLayers(opts) {
|
|
40
|
+
const cwd = opts?.cwd ?? process.cwd();
|
|
41
|
+
const layers = [];
|
|
42
|
+
const userPath = join(homedir(), USER_CONFIG_PATH);
|
|
43
|
+
const userData = await readYamlFile(userPath);
|
|
44
|
+
layers.push({ name: "user", path: userPath, data: userData });
|
|
45
|
+
const projectPath = resolve(cwd, PROJECT_CONFIG_PATH);
|
|
46
|
+
const projectData = await readYamlFile(projectPath);
|
|
47
|
+
layers.push({ name: "project", path: projectPath, data: projectData });
|
|
48
|
+
if (opts?.extraLayers) {
|
|
49
|
+
layers.push(...opts.extraLayers);
|
|
50
|
+
}
|
|
51
|
+
let merged = {};
|
|
52
|
+
for (const layer of layers) {
|
|
53
|
+
if (layer.data) {
|
|
54
|
+
merged = deepMerge(merged, layer.data);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const config = harnessConfigSchema.parse(merged);
|
|
58
|
+
return { config, layers };
|
|
59
|
+
}
|
|
60
|
+
function configExists(cwd) {
|
|
61
|
+
return anyConfigExists(cwd);
|
|
62
|
+
}
|
|
63
|
+
function projectConfigExists(cwd) {
|
|
64
|
+
const dir = cwd ?? process.cwd();
|
|
65
|
+
return existsSync(resolve(dir, PROJECT_CONFIG_PATH));
|
|
66
|
+
}
|
|
67
|
+
function userConfigExists() {
|
|
68
|
+
return existsSync(join(homedir(), USER_CONFIG_PATH));
|
|
69
|
+
}
|
|
70
|
+
function anyConfigExists(cwd) {
|
|
71
|
+
return projectConfigExists(cwd) || userConfigExists();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
loadConfig,
|
|
76
|
+
loadConfigWithLayers,
|
|
77
|
+
configExists,
|
|
78
|
+
projectConfigExists,
|
|
79
|
+
userConfigExists,
|
|
80
|
+
anyConfigExists
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=chunk-3P72TGGQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config/loader.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { resolve, join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { parse as parseYaml } from \"yaml\";\nimport { harnessConfigSchema, type HarnessConfig } from \"./schema.js\";\n\nexport type { HarnessConfig };\n\nconst PROJECT_CONFIG_PATH = \".harness/harness.config.yaml\";\nconst USER_CONFIG_PATH = \".harness/config.yaml\";\n\nexport interface ConfigLayer {\n name: string;\n path: string;\n data: Record<string, unknown> | null;\n}\n\nexport interface LoadConfigOptions {\n cwd?: string;\n extraLayers?: ConfigLayer[];\n}\n\nexport interface LoadConfigResult {\n config: HarnessConfig;\n layers: ConfigLayer[];\n}\n\nasync function readYamlFile(path: string): Promise<Record<string, unknown> | null> {\n if (!existsSync(path)) return null;\n const content = await readFile(path, \"utf-8\");\n return (parseYaml(content) as Record<string, unknown>) ?? null;\n}\n\nfunction deepMerge(\n base: Record<string, unknown>,\n override: Record<string, unknown>,\n): Record<string, unknown> {\n const result = { ...base };\n for (const key of Object.keys(override)) {\n const bVal = base[key];\n const oVal = override[key];\n if (\n bVal &&\n oVal &&\n typeof bVal === \"object\" &&\n typeof oVal === \"object\" &&\n !Array.isArray(bVal) &&\n !Array.isArray(oVal)\n ) {\n result[key] = deepMerge(\n bVal as Record<string, unknown>,\n oVal as Record<string, unknown>,\n );\n } else {\n result[key] = oVal;\n }\n }\n return result;\n}\n\n/**\n * Load harness configuration with N-layer merge and source tracking.\n *\n * Default layer order (lowest to highest priority):\n * 1. Built-in defaults (via Zod schema defaults)\n * 2. User-level (~/.harness/config.yaml)\n * 3. Project-level (.harness/harness.config.yaml)\n * 4. Extra layers (programmatic overrides)\n */\nexport async function loadConfig(opts?: LoadConfigOptions): Promise<HarnessConfig> {\n const result = await loadConfigWithLayers(opts);\n return result.config;\n}\n\nexport async function loadConfigWithLayers(opts?: LoadConfigOptions): Promise<LoadConfigResult> {\n const cwd = opts?.cwd ?? process.cwd();\n\n const layers: ConfigLayer[] = [];\n\n const userPath = join(homedir(), USER_CONFIG_PATH);\n const userData = await readYamlFile(userPath);\n layers.push({ name: \"user\", path: userPath, data: userData });\n\n const projectPath = resolve(cwd, PROJECT_CONFIG_PATH);\n const projectData = await readYamlFile(projectPath);\n layers.push({ name: \"project\", path: projectPath, data: projectData });\n\n if (opts?.extraLayers) {\n layers.push(...opts.extraLayers);\n }\n\n let merged: Record<string, unknown> = {};\n for (const layer of layers) {\n if (layer.data) {\n merged = deepMerge(merged, layer.data);\n }\n }\n\n const config = harnessConfigSchema.parse(merged);\n return { config, layers };\n}\n\nexport function configExists(cwd?: string): boolean {\n return anyConfigExists(cwd);\n}\n\nexport function projectConfigExists(cwd?: string): boolean {\n const dir = cwd ?? process.cwd();\n return existsSync(resolve(dir, PROJECT_CONFIG_PATH));\n}\n\nexport function userConfigExists(): boolean {\n return existsSync(join(homedir(), USER_CONFIG_PATH));\n}\n\nexport function anyConfigExists(cwd?: string): boolean {\n return projectConfigExists(cwd) || userConfigExists();\n}\n"],"mappings":";;;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,SAAS,YAAY;AAC9B,SAAS,eAAe;AACxB,SAAS,SAAS,iBAAiB;AAKnC,IAAM,sBAAsB;AAC5B,IAAM,mBAAmB;AAkBzB,eAAe,aAAa,MAAuD;AACjF,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO;AAC9B,QAAM,UAAU,MAAM,SAAS,MAAM,OAAO;AAC5C,SAAQ,UAAU,OAAO,KAAiC;AAC5D;AAEA,SAAS,UACP,MACA,UACyB;AACzB,QAAM,SAAS,EAAE,GAAG,KAAK;AACzB,aAAW,OAAO,OAAO,KAAK,QAAQ,GAAG;AACvC,UAAM,OAAO,KAAK,GAAG;AACrB,UAAM,OAAO,SAAS,GAAG;AACzB,QACE,QACA,QACA,OAAO,SAAS,YAChB,OAAO,SAAS,YAChB,CAAC,MAAM,QAAQ,IAAI,KACnB,CAAC,MAAM,QAAQ,IAAI,GACnB;AACA,aAAO,GAAG,IAAI;AAAA,QACZ;AAAA,QACA;AAAA,MACF;AAAA,IACF,OAAO;AACL,aAAO,GAAG,IAAI;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;AAWA,eAAsB,WAAW,MAAkD;AACjF,QAAM,SAAS,MAAM,qBAAqB,IAAI;AAC9C,SAAO,OAAO;AAChB;AAEA,eAAsB,qBAAqB,MAAqD;AAC9F,QAAM,MAAM,MAAM,OAAO,QAAQ,IAAI;AAErC,QAAM,SAAwB,CAAC;AAE/B,QAAM,WAAW,KAAK,QAAQ,GAAG,gBAAgB;AACjD,QAAM,WAAW,MAAM,aAAa,QAAQ;AAC5C,SAAO,KAAK,EAAE,MAAM,QAAQ,MAAM,UAAU,MAAM,SAAS,CAAC;AAE5D,QAAM,cAAc,QAAQ,KAAK,mBAAmB;AACpD,QAAM,cAAc,MAAM,aAAa,WAAW;AAClD,SAAO,KAAK,EAAE,MAAM,WAAW,MAAM,aAAa,MAAM,YAAY,CAAC;AAErE,MAAI,MAAM,aAAa;AACrB,WAAO,KAAK,GAAG,KAAK,WAAW;AAAA,EACjC;AAEA,MAAI,SAAkC,CAAC;AACvC,aAAW,SAAS,QAAQ;AAC1B,QAAI,MAAM,MAAM;AACd,eAAS,UAAU,QAAQ,MAAM,IAAI;AAAA,IACvC;AAAA,EACF;AAEA,QAAM,SAAS,oBAAoB,MAAM,MAAM;AAC/C,SAAO,EAAE,QAAQ,OAAO;AAC1B;AAEO,SAAS,aAAa,KAAuB;AAClD,SAAO,gBAAgB,GAAG;AAC5B;AAEO,SAAS,oBAAoB,KAAuB;AACzD,QAAM,MAAM,OAAO,QAAQ,IAAI;AAC/B,SAAO,WAAW,QAAQ,KAAK,mBAAmB,CAAC;AACrD;AAEO,SAAS,mBAA4B;AAC1C,SAAO,WAAW,KAAK,QAAQ,GAAG,gBAAgB,CAAC;AACrD;AAEO,SAAS,gBAAgB,KAAuB;AACrD,SAAO,oBAAoB,GAAG,KAAK,iBAAiB;AACtD;","names":[]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/template/engine.ts
|
|
4
|
+
function render(template, context) {
|
|
5
|
+
let result = template;
|
|
6
|
+
result = processEach(result, context);
|
|
7
|
+
result = processConditionals(result, context);
|
|
8
|
+
result = interpolateVariables(result, context);
|
|
9
|
+
return result;
|
|
10
|
+
}
|
|
11
|
+
function processEach(template, context) {
|
|
12
|
+
const eachRegex = /\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g;
|
|
13
|
+
return template.replace(eachRegex, (_match, key, body) => {
|
|
14
|
+
const value = context[key];
|
|
15
|
+
if (!Array.isArray(value)) return "";
|
|
16
|
+
return value.map((item) => body.replace(/\{\{\.\}\}/g, item)).join("");
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function processConditionals(template, context) {
|
|
20
|
+
const ifElseRegex = /\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{#else\}\}([\s\S]*?)\{\{\/if\}\}/g;
|
|
21
|
+
let result = template.replace(
|
|
22
|
+
ifElseRegex,
|
|
23
|
+
(_match, key, ifBody, elseBody) => {
|
|
24
|
+
return isTruthy(context[key]) ? ifBody : elseBody;
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
const ifRegex = /\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g;
|
|
28
|
+
result = result.replace(ifRegex, (_match, key, body) => {
|
|
29
|
+
return isTruthy(context[key]) ? body : "";
|
|
30
|
+
});
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
function interpolateVariables(template, context) {
|
|
34
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
35
|
+
const value = context[key];
|
|
36
|
+
if (value === void 0) return "";
|
|
37
|
+
if (typeof value === "boolean") return String(value);
|
|
38
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
39
|
+
return value;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function isTruthy(value) {
|
|
43
|
+
if (value === void 0 || value === false || value === "") return false;
|
|
44
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
render
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=chunk-JKJ3FP6T.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/template/engine.ts"],"sourcesContent":["export interface TemplateContext {\n [key: string]: string | boolean | string[] | undefined;\n}\n\n/**\n * Lightweight template engine:\n * - {{variable}} — interpolation\n * - {{#if condition}}...{{/if}} — conditional block (truthy check)\n * - {{#if condition}}...{{#else}}...{{/if}} — conditional with else\n * - {{#each items}}...{{/each}} — array iteration (current item as {{.}})\n */\nexport function render(template: string, context: TemplateContext): string {\n let result = template;\n\n result = processEach(result, context);\n result = processConditionals(result, context);\n result = interpolateVariables(result, context);\n\n return result;\n}\n\nfunction processEach(template: string, context: TemplateContext): string {\n const eachRegex = /\\{\\{#each\\s+(\\w+)\\}\\}([\\s\\S]*?)\\{\\{\\/each\\}\\}/g;\n return template.replace(eachRegex, (_match, key: string, body: string) => {\n const value = context[key];\n if (!Array.isArray(value)) return \"\";\n return value.map((item) => body.replace(/\\{\\{\\.\\}\\}/g, item)).join(\"\");\n });\n}\n\nfunction processConditionals(template: string, context: TemplateContext): string {\n const ifElseRegex =\n /\\{\\{#if\\s+(\\w+)\\}\\}([\\s\\S]*?)\\{\\{#else\\}\\}([\\s\\S]*?)\\{\\{\\/if\\}\\}/g;\n let result = template.replace(\n ifElseRegex,\n (_match, key: string, ifBody: string, elseBody: string) => {\n return isTruthy(context[key]) ? ifBody : elseBody;\n },\n );\n\n const ifRegex = /\\{\\{#if\\s+(\\w+)\\}\\}([\\s\\S]*?)\\{\\{\\/if\\}\\}/g;\n result = result.replace(ifRegex, (_match, key: string, body: string) => {\n return isTruthy(context[key]) ? body : \"\";\n });\n\n return result;\n}\n\nfunction interpolateVariables(template: string, context: TemplateContext): string {\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_match, key: string) => {\n const value = context[key];\n if (value === undefined) return \"\";\n if (typeof value === \"boolean\") return String(value);\n if (Array.isArray(value)) return value.join(\", \");\n return value;\n });\n}\n\nfunction isTruthy(value: string | boolean | string[] | undefined): boolean {\n if (value === undefined || value === false || value === \"\") return false;\n if (Array.isArray(value)) return value.length > 0;\n return true;\n}\n"],"mappings":";;;AAWO,SAAS,OAAO,UAAkB,SAAkC;AACzE,MAAI,SAAS;AAEb,WAAS,YAAY,QAAQ,OAAO;AACpC,WAAS,oBAAoB,QAAQ,OAAO;AAC5C,WAAS,qBAAqB,QAAQ,OAAO;AAE7C,SAAO;AACT;AAEA,SAAS,YAAY,UAAkB,SAAkC;AACvE,QAAM,YAAY;AAClB,SAAO,SAAS,QAAQ,WAAW,CAAC,QAAQ,KAAa,SAAiB;AACxE,UAAM,QAAQ,QAAQ,GAAG;AACzB,QAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO;AAClC,WAAO,MAAM,IAAI,CAAC,SAAS,KAAK,QAAQ,eAAe,IAAI,CAAC,EAAE,KAAK,EAAE;AAAA,EACvE,CAAC;AACH;AAEA,SAAS,oBAAoB,UAAkB,SAAkC;AAC/E,QAAM,cACJ;AACF,MAAI,SAAS,SAAS;AAAA,IACpB;AAAA,IACA,CAAC,QAAQ,KAAa,QAAgB,aAAqB;AACzD,aAAO,SAAS,QAAQ,GAAG,CAAC,IAAI,SAAS;AAAA,IAC3C;AAAA,EACF;AAEA,QAAM,UAAU;AAChB,WAAS,OAAO,QAAQ,SAAS,CAAC,QAAQ,KAAa,SAAiB;AACtE,WAAO,SAAS,QAAQ,GAAG,CAAC,IAAI,OAAO;AAAA,EACzC,CAAC;AAED,SAAO;AACT;AAEA,SAAS,qBAAqB,UAAkB,SAAkC;AAChF,SAAO,SAAS,QAAQ,kBAAkB,CAAC,QAAQ,QAAgB;AACjE,UAAM,QAAQ,QAAQ,GAAG;AACzB,QAAI,UAAU,OAAW,QAAO;AAChC,QAAI,OAAO,UAAU,UAAW,QAAO,OAAO,KAAK;AACnD,QAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,KAAK,IAAI;AAChD,WAAO;AAAA,EACT,CAAC;AACH;AAEA,SAAS,SAAS,OAAyD;AACzE,MAAI,UAAU,UAAa,UAAU,SAAS,UAAU,GAAI,QAAO;AACnE,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,SAAS;AAChD,SAAO;AACT;","names":[]}
|