agent-harness-init 0.1.0
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 +197 -0
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +99 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +26 -0
- package/dist/discovery.d.ts +2 -0
- package/dist/discovery.js +70 -0
- package/dist/filesystem.d.ts +4 -0
- package/dist/filesystem.js +93 -0
- package/dist/format.d.ts +3 -0
- package/dist/format.js +14 -0
- package/dist/hash.d.ts +1 -0
- package/dist/hash.js +4 -0
- package/dist/manifest.d.ts +4 -0
- package/dist/manifest.js +16 -0
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +21 -0
- package/dist/planner.d.ts +2 -0
- package/dist/planner.js +36 -0
- package/dist/render.d.ts +2 -0
- package/dist/render.js +134 -0
- package/dist/skill-install.d.ts +2 -0
- package/dist/skill-install.js +32 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.js +1 -0
- package/dist/verify.d.ts +7 -0
- package/dist/verify.js +16 -0
- package/licenses/harness-ospx-MIT.txt +21 -0
- package/package.json +57 -0
- package/skills/agent-harness-init/SKILL.md +36 -0
- package/skills/agent-harness-init/agents/openai.yaml +6 -0
- package/skills/agent-harness-init/references/install-protocol.md +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Agent Harness Init Contributors
|
|
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,197 @@
|
|
|
1
|
+
# Agent Harness Init
|
|
2
|
+
|
|
3
|
+
让用户在任意软件项目中对 AI Agent 说一句“帮我安装并适配 Harness”,
|
|
4
|
+
Agent 就能先分析项目、生成安全计划,再部署项目级规则和验证门控。
|
|
5
|
+
|
|
6
|
+
Agent Harness Init 是一个独立、Provider-neutral 的 Harness 安装器。它把
|
|
7
|
+
Agent 的判断力用于识别项目,把文件生成、冲突检测、哈希所有权和写入交给
|
|
8
|
+
确定性的 CLI,避免不同 Agent 手工复制模板造成漂移。
|
|
9
|
+
|
|
10
|
+
> 当前版本:`0.1.0`。GitHub 仓库与 npm 包均公开发布。
|
|
11
|
+
|
|
12
|
+
## 它解决什么问题
|
|
13
|
+
|
|
14
|
+
传统模板要求用户自己复制 `AGENTS.md`、Skills、验证文档和配置,并且后续
|
|
15
|
+
无法安全升级。本项目提供完整生命周期:
|
|
16
|
+
|
|
17
|
+
- 自动识别 JavaScript/TypeScript、Python、全栈和 Monorepo 项目;
|
|
18
|
+
- 无法识别时使用 `custom`,不虚构构建或测试命令;
|
|
19
|
+
- 生成项目级 `AGENT_HARNESS.md`、Harness 文档和 Codex/Claude Skills;
|
|
20
|
+
- 使用 `.harness/manifest.json` 记录所有生成文件的 SHA-256;
|
|
21
|
+
- 更新和卸载只处理仍与 manifest 一致的文件;
|
|
22
|
+
- 已存在或被用户修改的文件绝不静默覆盖;
|
|
23
|
+
- OpenSpec 是可选工作流,不是运行时硬依赖;
|
|
24
|
+
- 不读取无关源码、不上传代码、不包含遥测。
|
|
25
|
+
|
|
26
|
+
## 工作方式
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
自然语言请求
|
|
30
|
+
│
|
|
31
|
+
▼
|
|
32
|
+
Bootstrap Skill
|
|
33
|
+
│
|
|
34
|
+
▼
|
|
35
|
+
只读项目发现 ──> dry-run 安装计划 ──> 冲突检查
|
|
36
|
+
│
|
|
37
|
+
无冲突且用户已授权
|
|
38
|
+
│
|
|
39
|
+
▼
|
|
40
|
+
原子写入 + 所有权 manifest
|
|
41
|
+
│
|
|
42
|
+
▼
|
|
43
|
+
doctor
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 快速安装
|
|
47
|
+
|
|
48
|
+
要求 Node.js 20 或更高版本。
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 在目标项目中预览适配计划(不写入)
|
|
52
|
+
npx agent-harness-init plan --root .
|
|
53
|
+
|
|
54
|
+
# 应用无冲突的安装计划
|
|
55
|
+
npx agent-harness-init init --root . --yes
|
|
56
|
+
|
|
57
|
+
# 检查生成文件是否完整、未漂移
|
|
58
|
+
npx agent-harness-init doctor --root .
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
安装一次 Bootstrap Skill:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Codex
|
|
65
|
+
npx agent-harness-init install-skill --provider codex
|
|
66
|
+
|
|
67
|
+
# Claude Code
|
|
68
|
+
npx agent-harness-init install-skill --provider claude
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
安装 Skill 是用户级写入,与在具体项目中安装 Harness 是两个独立授权范围。
|
|
72
|
+
|
|
73
|
+
## 一句话让 Agent 部署
|
|
74
|
+
|
|
75
|
+
打开目标项目后告诉 Agent:
|
|
76
|
+
|
|
77
|
+
> 帮我安装并适配 Agent Harness Init。先做只读计划,不覆盖已有规则,安装后运行 doctor 并报告结果。
|
|
78
|
+
|
|
79
|
+
Bootstrap Skill 会引导 Agent 执行:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx agent-harness-init plan --root .
|
|
83
|
+
npx agent-harness-init init --root . --yes
|
|
84
|
+
npx agent-harness-init doctor --root .
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
如果项目已经有 `AGENTS.md`,CLI 不会接管或覆盖它。Agent 会先阅读原规则,
|
|
88
|
+
然后只添加一个指向 `AGENT_HARNESS.md` 的最小引用;原项目更严格的规则继续优先。
|
|
89
|
+
|
|
90
|
+
## CLI
|
|
91
|
+
|
|
92
|
+
| 命令 | 作用 | 是否写入 |
|
|
93
|
+
|---|---|:---:|
|
|
94
|
+
| `plan` | 发现项目并显示安装或升级计划 | 否 |
|
|
95
|
+
| `init --yes` | 应用无冲突的安装计划 | 是 |
|
|
96
|
+
| `update --yes` | 更新仍未被用户修改的托管文件 | 是 |
|
|
97
|
+
| `doctor` | 检查托管文件缺失和漂移 | 否 |
|
|
98
|
+
| `verify` | 按配置依次执行项目门控 | 运行项目命令 |
|
|
99
|
+
| `uninstall --yes` | 删除未被修改的托管文件 | 是 |
|
|
100
|
+
| `install-skill` | 安装用户级 Bootstrap Skill | 是,项目外 |
|
|
101
|
+
|
|
102
|
+
所有项目命令都支持 `--root <path>`。`init` 和 `update` 在不带 `--yes` 时只
|
|
103
|
+
显示计划。
|
|
104
|
+
|
|
105
|
+
## 目标项目中生成的内容
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
target-project/
|
|
109
|
+
├── AGENT_HARNESS.md
|
|
110
|
+
├── AGENTS.md # 仅在原项目不存在时生成
|
|
111
|
+
├── INSTALL_AGENT.md
|
|
112
|
+
├── harness.config.yaml
|
|
113
|
+
├── harness/
|
|
114
|
+
│ ├── architecture.md
|
|
115
|
+
│ └── verification.md
|
|
116
|
+
├── .agents/skills/agent-harness/ # Codex 项目 Skill
|
|
117
|
+
├── .claude/skills/agent-harness/ # Claude 项目 Skill
|
|
118
|
+
└── .harness/
|
|
119
|
+
├── manifest.json
|
|
120
|
+
└── backups/ # 仅升级托管文件时使用
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`harness.config.yaml` 是适配层,保存检测到的项目类型、源码和测试目录、Agent
|
|
124
|
+
Provider、工作流以及验证命令。框架源码不硬编码用户项目的包名和目录。
|
|
125
|
+
|
|
126
|
+
## 安全模型
|
|
127
|
+
|
|
128
|
+
- 计划阶段完全只读。
|
|
129
|
+
- 目标路径必须解析在用户指定的项目根目录内。
|
|
130
|
+
- 首次安装遇到同名文件会报告冲突,不使用覆盖开关绕过。
|
|
131
|
+
- manifest 记录每个托管文件的 SHA-256。
|
|
132
|
+
- 升级前当前哈希必须与旧 manifest 一致。
|
|
133
|
+
- 文件通过同目录临时文件和 rename 原子替换。
|
|
134
|
+
- 安装事务失败时回滚本次新建和已备份的托管文件。
|
|
135
|
+
- 卸载保留项目拥有的 `harness.config.yaml`、缺失项和已修改文件,只删除确认未变的托管内容。
|
|
136
|
+
- 项目内安装不授权全局 Skill、Git commit/push、远程建仓或 npm 发布。
|
|
137
|
+
|
|
138
|
+
## 支持范围
|
|
139
|
+
|
|
140
|
+
0.1 版本内置以下 Profile:
|
|
141
|
+
|
|
142
|
+
- `javascript`
|
|
143
|
+
- `python`
|
|
144
|
+
- `fullstack`
|
|
145
|
+
- `monorepo`
|
|
146
|
+
- `custom`
|
|
147
|
+
|
|
148
|
+
“支持任意项目”意味着未知项目可以通过 `harness.config.yaml` 配置接入,
|
|
149
|
+
而不是框架声称能够自动理解所有业务架构。更多技术栈应通过配置和适配器扩展。
|
|
150
|
+
|
|
151
|
+
## 开发
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
git clone https://github.com/xixixixi55/agent-harness-init.git
|
|
155
|
+
cd agent-harness-init
|
|
156
|
+
npm install
|
|
157
|
+
npm run typecheck
|
|
158
|
+
npm test
|
|
159
|
+
npm run build
|
|
160
|
+
npm run verify
|
|
161
|
+
npm run release:check
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## npm 发布维护
|
|
165
|
+
|
|
166
|
+
仓库的 `publishConfig` 固定使用官方 npm registry 并公开发布。维护者发布前应:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npm version <patch|minor|major>
|
|
170
|
+
npm run release:check
|
|
171
|
+
npm publish --access public
|
|
172
|
+
git push --follow-tags
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`.github/workflows/ci.yml` 会在提交和 Pull Request 上验证 Node.js 20/22。
|
|
176
|
+
`.github/workflows/npm-publish.yml` 可在 GitHub Release 发布时使用仓库的
|
|
177
|
+
`NPM_TOKEN` secret 自动发布;首次配置 token 前不会执行外部发布。
|
|
178
|
+
|
|
179
|
+
正式行为位于 `openspec/`。核心依赖方向为:
|
|
180
|
+
|
|
181
|
+
```text
|
|
182
|
+
types -> discovery -> planning -> rendering -> filesystem -> commands -> cli
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## 独立性、致谢与许可证
|
|
186
|
+
|
|
187
|
+
Agent Harness Init 是独立开发和维护的项目,不隶属于、不代表、也未获得
|
|
188
|
+
Harness-OSPX 的官方背书。
|
|
189
|
+
|
|
190
|
+
本项目早期设计参考了
|
|
191
|
+
[Harness-OSPX](https://github.com/pxp995/harness-ospx) 将 Harness Engineering、
|
|
192
|
+
项目级 Agent 指令、验证门控与 OpenSpec 工作流结合的思路。Harness-OSPX 由
|
|
193
|
+
pxp995 及其贡献者维护,并以 MIT License 发布。对应版权和许可文本保存在
|
|
194
|
+
[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) 与
|
|
195
|
+
[`licenses/harness-ospx-MIT.txt`](licenses/harness-ospx-MIT.txt)。
|
|
196
|
+
|
|
197
|
+
本项目自身以 [MIT License](LICENSE) 发布。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
## Harness-OSPX
|
|
4
|
+
|
|
5
|
+
Agent Harness Init is an independent project. Its early design was informed by
|
|
6
|
+
[Harness-OSPX](https://github.com/pxp995/harness-ospx), including the idea of
|
|
7
|
+
combining Harness Engineering, project-local agent instructions, verification
|
|
8
|
+
gates, and OpenSpec-oriented change management.
|
|
9
|
+
|
|
10
|
+
Harness-OSPX is maintained by pxp995 and its contributors and is distributed
|
|
11
|
+
under the MIT License. The notice shipped with the locally reviewed
|
|
12
|
+
`@pxp995/harness-ospx@1.0.2` distribution is preserved in
|
|
13
|
+
`licenses/harness-ospx-MIT.txt`.
|
|
14
|
+
|
|
15
|
+
This project is not affiliated with, endorsed by, or maintained by the
|
|
16
|
+
Harness-OSPX project.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { loadConfig } from './config.js';
|
|
7
|
+
import { applyInstallPlan, diagnoseInstallation, uninstallManagedFiles } from './filesystem.js';
|
|
8
|
+
import { formatDoctor, formatPlan } from './format.js';
|
|
9
|
+
import { createInstallPlan } from './planner.js';
|
|
10
|
+
import { installBootstrapSkill } from './skill-install.js';
|
|
11
|
+
import { runConfiguredVerification } from './verify.js';
|
|
12
|
+
function packageVersion() {
|
|
13
|
+
const packagePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
14
|
+
return JSON.parse(readFileSync(packagePath, 'utf8')).version;
|
|
15
|
+
}
|
|
16
|
+
function parseArgs(argv) {
|
|
17
|
+
const command = argv[0] ?? 'help';
|
|
18
|
+
let root = process.cwd();
|
|
19
|
+
let provider;
|
|
20
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
21
|
+
const arg = argv[index];
|
|
22
|
+
if (arg === '--root')
|
|
23
|
+
root = path.resolve(argv[++index] ?? '');
|
|
24
|
+
if (arg === '--provider') {
|
|
25
|
+
const value = argv[++index];
|
|
26
|
+
if (value !== 'codex' && value !== 'claude')
|
|
27
|
+
throw new Error('--provider must be codex or claude');
|
|
28
|
+
provider = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { command, root, provider, yes: argv.includes('--yes'), force: argv.includes('--force') };
|
|
32
|
+
}
|
|
33
|
+
function help() {
|
|
34
|
+
return `Agent Harness Init
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
agent-harness plan [--root PATH]
|
|
38
|
+
agent-harness init [--root PATH] --yes
|
|
39
|
+
agent-harness update [--root PATH] --yes
|
|
40
|
+
agent-harness doctor [--root PATH]
|
|
41
|
+
agent-harness verify [--root PATH]
|
|
42
|
+
agent-harness uninstall [--root PATH] --yes
|
|
43
|
+
agent-harness install-skill --provider codex|claude [--force]
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
export function runCli(argv = process.argv.slice(2)) {
|
|
47
|
+
try {
|
|
48
|
+
const args = parseArgs(argv);
|
|
49
|
+
const version = packageVersion();
|
|
50
|
+
if (args.command === 'help' || args.command === '--help' || args.command === '-h') {
|
|
51
|
+
console.log(help());
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
if (args.command === 'plan') {
|
|
55
|
+
console.log(formatPlan(createInstallPlan(args.root, version)));
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
if (args.command === 'init' || args.command === 'update') {
|
|
59
|
+
const plan = createInstallPlan(args.root, version);
|
|
60
|
+
console.log(formatPlan(plan));
|
|
61
|
+
if (!args.yes) {
|
|
62
|
+
console.error('Dry-run only. Re-run with --yes to apply this exact conflict-free plan.');
|
|
63
|
+
return plan.conflicts.length > 0 ? 2 : 0;
|
|
64
|
+
}
|
|
65
|
+
applyInstallPlan(plan);
|
|
66
|
+
console.log(`Installed Agent Harness Init ${version}.`);
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
if (args.command === 'doctor') {
|
|
70
|
+
const findings = diagnoseInstallation(args.root);
|
|
71
|
+
console.log(formatDoctor(findings));
|
|
72
|
+
return findings.every((finding) => finding.status === 'ok') ? 0 : 2;
|
|
73
|
+
}
|
|
74
|
+
if (args.command === 'verify') {
|
|
75
|
+
const results = runConfiguredVerification(args.root, loadConfig(args.root));
|
|
76
|
+
return results.every((result) => result.status === 0) ? 0 : 1;
|
|
77
|
+
}
|
|
78
|
+
if (args.command === 'uninstall') {
|
|
79
|
+
if (!args.yes)
|
|
80
|
+
throw new Error('Uninstall requires --yes');
|
|
81
|
+
const findings = uninstallManagedFiles(args.root);
|
|
82
|
+
console.log(formatDoctor(findings));
|
|
83
|
+
return findings.every((finding) => finding.status === 'ok') ? 0 : 2;
|
|
84
|
+
}
|
|
85
|
+
if (args.command === 'install-skill') {
|
|
86
|
+
if (!args.provider)
|
|
87
|
+
throw new Error('install-skill requires --provider codex|claude');
|
|
88
|
+
console.log(`Installed Bootstrap Skill at ${installBootstrapSkill(args.provider, args.force)}`);
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
console.error(help());
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error(`agent-harness: ${error instanceof Error ? error.message : String(error)}`);
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
process.exitCode = runCli();
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AgentProvider, HarnessConfig, ProjectFacts } from './types.js';
|
|
2
|
+
export declare function configFromFacts(facts: ProjectFacts, agents?: AgentProvider[]): HarnessConfig;
|
|
3
|
+
export declare function serializeConfig(config: HarnessConfig): string;
|
|
4
|
+
export declare function loadConfig(root: string): HarnessConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse, stringify } from 'yaml';
|
|
4
|
+
export function configFromFacts(facts, agents = ['codex', 'claude']) {
|
|
5
|
+
return {
|
|
6
|
+
schemaVersion: 1,
|
|
7
|
+
project: { name: facts.name, profile: facts.profile },
|
|
8
|
+
agents,
|
|
9
|
+
workflow: { provider: facts.markers.includes('openspec/') ? 'openspec' : 'native' },
|
|
10
|
+
architecture: { sourceRoots: facts.sourceRoots, testRoots: facts.testRoots },
|
|
11
|
+
commands: facts.commands,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function serializeConfig(config) {
|
|
15
|
+
return stringify(config, { lineWidth: 100 });
|
|
16
|
+
}
|
|
17
|
+
export function loadConfig(root) {
|
|
18
|
+
const configPath = path.join(root, 'harness.config.yaml');
|
|
19
|
+
if (!existsSync(configPath))
|
|
20
|
+
throw new Error('harness.config.yaml is missing; run init first');
|
|
21
|
+
const value = parse(readFileSync(configPath, 'utf8'));
|
|
22
|
+
if (value?.schemaVersion !== 1 || !value.project?.name || !value.commands) {
|
|
23
|
+
throw new Error('harness.config.yaml is invalid or unsupported');
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
function exists(root, relative) {
|
|
5
|
+
return existsSync(path.join(root, relative));
|
|
6
|
+
}
|
|
7
|
+
function readPackageJson(root) {
|
|
8
|
+
const packagePath = path.join(root, 'package.json');
|
|
9
|
+
if (!existsSync(packagePath))
|
|
10
|
+
return undefined;
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function npmCommand(script, scripts) {
|
|
19
|
+
return scripts?.[script] ? `npm run ${script}` : undefined;
|
|
20
|
+
}
|
|
21
|
+
function detectCommands(packageJson, python) {
|
|
22
|
+
const scripts = packageJson?.scripts;
|
|
23
|
+
return {
|
|
24
|
+
build: npmCommand('build', scripts),
|
|
25
|
+
typecheck: npmCommand('typecheck', scripts),
|
|
26
|
+
lint: npmCommand('lint', scripts) ?? npmCommand('lint:arch', scripts),
|
|
27
|
+
test: npmCommand('test', scripts) ?? (python ? 'python -m pytest -q --tb=short' : undefined),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function detectProfile(javaScript, python, monorepo) {
|
|
31
|
+
if (monorepo)
|
|
32
|
+
return 'monorepo';
|
|
33
|
+
if (javaScript && python)
|
|
34
|
+
return 'fullstack';
|
|
35
|
+
if (javaScript)
|
|
36
|
+
return 'javascript';
|
|
37
|
+
if (python)
|
|
38
|
+
return 'python';
|
|
39
|
+
return 'custom';
|
|
40
|
+
}
|
|
41
|
+
function detectGitDirty(root) {
|
|
42
|
+
const result = spawnSync('git', ['status', '--porcelain'], { cwd: root, encoding: 'utf8', windowsHide: true });
|
|
43
|
+
return result.status === 0 ? result.stdout.trim().length > 0 : null;
|
|
44
|
+
}
|
|
45
|
+
export function discoverProject(rootInput) {
|
|
46
|
+
const root = path.resolve(rootInput);
|
|
47
|
+
const packageJson = readPackageJson(root);
|
|
48
|
+
const javaScript = Boolean(packageJson);
|
|
49
|
+
const python = exists(root, 'pyproject.toml') || exists(root, 'requirements.txt') || exists(root, 'setup.py');
|
|
50
|
+
const monorepo = Boolean(packageJson?.workspaces) || exists(root, 'pnpm-workspace.yaml') || exists(root, 'lerna.json');
|
|
51
|
+
const markers = [
|
|
52
|
+
javaScript && 'package.json',
|
|
53
|
+
exists(root, 'pnpm-workspace.yaml') && 'pnpm-workspace.yaml',
|
|
54
|
+
exists(root, 'pyproject.toml') && 'pyproject.toml',
|
|
55
|
+
exists(root, 'requirements.txt') && 'requirements.txt',
|
|
56
|
+
exists(root, 'openspec') && 'openspec/',
|
|
57
|
+
].filter((value) => Boolean(value));
|
|
58
|
+
return {
|
|
59
|
+
root,
|
|
60
|
+
name: packageJson?.name ?? path.basename(root),
|
|
61
|
+
profile: detectProfile(javaScript, python, monorepo),
|
|
62
|
+
markers,
|
|
63
|
+
sourceRoots: ['src', 'app', 'packages', 'lib'].filter((candidate) => exists(root, candidate)),
|
|
64
|
+
testRoots: ['tests', 'test', '__tests__', 'e2e'].filter((candidate) => exists(root, candidate)),
|
|
65
|
+
commands: detectCommands(packageJson, python),
|
|
66
|
+
packageManager: packageJson?.packageManager?.split('@')[0],
|
|
67
|
+
gitDirty: detectGitDirty(root),
|
|
68
|
+
existingAgentFiles: ['AGENTS.md', 'CLAUDE.md', '.agents', '.claude'].filter((candidate) => exists(root, candidate)),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DoctorFinding, HarnessManifest, InstallPlan } from './types.js';
|
|
2
|
+
export declare function applyInstallPlan(plan: InstallPlan, now?: Date): HarnessManifest;
|
|
3
|
+
export declare function diagnoseInstallation(root: string): DoctorFinding[];
|
|
4
|
+
export declare function uninstallManagedFiles(root: string): DoctorFinding[];
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { sha256 } from './hash.js';
|
|
4
|
+
import { loadManifest, manifestRelativePath, serializeManifest } from './manifest.js';
|
|
5
|
+
import { resolveInside } from './paths.js';
|
|
6
|
+
function writeAtomic(target, content) {
|
|
7
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
8
|
+
const temporary = `${target}.agent-harness-${process.pid}.tmp`;
|
|
9
|
+
writeFileSync(temporary, content, 'utf8');
|
|
10
|
+
renameSync(temporary, target);
|
|
11
|
+
}
|
|
12
|
+
export function applyInstallPlan(plan, now = new Date()) {
|
|
13
|
+
if (plan.conflicts.length > 0) {
|
|
14
|
+
throw new Error(`Installation has conflicts: ${plan.conflicts.join(', ')}`);
|
|
15
|
+
}
|
|
16
|
+
const previousManifest = loadManifest(plan.root);
|
|
17
|
+
const transactionRoot = resolveInside(plan.root, `.harness/backups/${now.toISOString().replaceAll(':', '-')}`);
|
|
18
|
+
const created = [];
|
|
19
|
+
const updated = [];
|
|
20
|
+
try {
|
|
21
|
+
for (const artifact of plan.artifacts) {
|
|
22
|
+
if (artifact.action === 'unchanged')
|
|
23
|
+
continue;
|
|
24
|
+
const target = resolveInside(plan.root, artifact.path);
|
|
25
|
+
if (artifact.action === 'update') {
|
|
26
|
+
const backup = path.join(transactionRoot, ...artifact.path.split('/'));
|
|
27
|
+
mkdirSync(path.dirname(backup), { recursive: true });
|
|
28
|
+
copyFileSync(target, backup);
|
|
29
|
+
updated.push({ target, backup });
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
created.push(target);
|
|
33
|
+
}
|
|
34
|
+
writeAtomic(target, artifact.content);
|
|
35
|
+
}
|
|
36
|
+
const manifest = {
|
|
37
|
+
schemaVersion: 1,
|
|
38
|
+
framework: { name: 'agent-harness-init', version: plan.frameworkVersion },
|
|
39
|
+
installedAt: now.toISOString(),
|
|
40
|
+
files: Object.fromEntries(plan.artifacts
|
|
41
|
+
.filter((artifact) => artifact.path !== 'harness.config.yaml')
|
|
42
|
+
.map((artifact) => [artifact.path, { hash: artifact.nextHash }])),
|
|
43
|
+
};
|
|
44
|
+
writeAtomic(resolveInside(plan.root, manifestRelativePath), serializeManifest(manifest));
|
|
45
|
+
return manifest;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
for (const target of created.reverse()) {
|
|
49
|
+
if (existsSync(target))
|
|
50
|
+
rmSync(target, { force: true });
|
|
51
|
+
}
|
|
52
|
+
for (const entry of updated.reverse())
|
|
53
|
+
copyFileSync(entry.backup, entry.target);
|
|
54
|
+
if (previousManifest) {
|
|
55
|
+
writeAtomic(resolveInside(plan.root, manifestRelativePath), serializeManifest(previousManifest));
|
|
56
|
+
}
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function diagnoseInstallation(root) {
|
|
61
|
+
const manifest = loadManifest(root);
|
|
62
|
+
if (!manifest)
|
|
63
|
+
throw new Error('Harness is not installed: .harness/manifest.json is missing');
|
|
64
|
+
return Object.entries(manifest.files).map(([relativePath, entry]) => {
|
|
65
|
+
const target = resolveInside(root, relativePath);
|
|
66
|
+
if (!existsSync(target))
|
|
67
|
+
return { path: relativePath, status: 'missing' };
|
|
68
|
+
return { path: relativePath, status: sha256(readFileSync(target)) === entry.hash ? 'ok' : 'modified' };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export function uninstallManagedFiles(root) {
|
|
72
|
+
const manifest = loadManifest(root);
|
|
73
|
+
if (!manifest)
|
|
74
|
+
throw new Error('Harness is not installed');
|
|
75
|
+
const findings = diagnoseInstallation(root);
|
|
76
|
+
const preserved = {};
|
|
77
|
+
for (const finding of findings) {
|
|
78
|
+
if (finding.status === 'ok') {
|
|
79
|
+
unlinkSync(resolveInside(root, finding.path));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
preserved[finding.path] = manifest.files[finding.path];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const manifestPath = resolveInside(root, manifestRelativePath);
|
|
86
|
+
if (Object.keys(preserved).length === 0) {
|
|
87
|
+
unlinkSync(manifestPath);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
writeAtomic(manifestPath, serializeManifest({ ...manifest, files: preserved }));
|
|
91
|
+
}
|
|
92
|
+
return findings;
|
|
93
|
+
}
|
package/dist/format.d.ts
ADDED
package/dist/format.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function formatPlan(plan) {
|
|
2
|
+
const lines = [
|
|
3
|
+
`Agent Harness plan | root=${plan.root}`,
|
|
4
|
+
`profile=${plan.facts.profile} | gitDirty=${String(plan.facts.gitDirty)} | markers=${plan.facts.markers.join(',') || 'none'}`,
|
|
5
|
+
];
|
|
6
|
+
for (const artifact of plan.artifacts) {
|
|
7
|
+
lines.push(`${artifact.action.toUpperCase().padEnd(9)} ${artifact.path} — ${artifact.reason}`);
|
|
8
|
+
}
|
|
9
|
+
lines.push(`conflicts=${plan.conflicts.length}`);
|
|
10
|
+
return lines.join('\n');
|
|
11
|
+
}
|
|
12
|
+
export function formatDoctor(findings) {
|
|
13
|
+
return findings.map((finding) => `${finding.status.toUpperCase().padEnd(8)} ${finding.path}`).join('\n');
|
|
14
|
+
}
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sha256(content: string | Buffer): string;
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HarnessManifest } from './types.js';
|
|
2
|
+
export declare const manifestRelativePath = ".harness/manifest.json";
|
|
3
|
+
export declare function loadManifest(root: string): HarnessManifest | undefined;
|
|
4
|
+
export declare function serializeManifest(manifest: HarnessManifest): string;
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const manifestRelativePath = '.harness/manifest.json';
|
|
4
|
+
export function loadManifest(root) {
|
|
5
|
+
const manifestPath = path.join(root, manifestRelativePath);
|
|
6
|
+
if (!existsSync(manifestPath))
|
|
7
|
+
return undefined;
|
|
8
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
9
|
+
if (manifest?.schemaVersion !== 1 || manifest.framework?.name !== 'agent-harness-init') {
|
|
10
|
+
throw new Error('Unsupported or invalid .harness/manifest.json');
|
|
11
|
+
}
|
|
12
|
+
return manifest;
|
|
13
|
+
}
|
|
14
|
+
export function serializeManifest(manifest) {
|
|
15
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
16
|
+
}
|
package/dist/paths.d.ts
ADDED
package/dist/paths.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export function normalizeRelativePath(value) {
|
|
3
|
+
const normalized = value.replaceAll('\\', '/').replace(/^\.\//, '');
|
|
4
|
+
if (!normalized || normalized === '.' || path.isAbsolute(value)) {
|
|
5
|
+
throw new Error(`Path must be a non-empty relative path: ${value}`);
|
|
6
|
+
}
|
|
7
|
+
const segments = normalized.split('/');
|
|
8
|
+
if (segments.some((segment) => segment === '..' || segment === '')) {
|
|
9
|
+
throw new Error(`Path escapes or is malformed: ${value}`);
|
|
10
|
+
}
|
|
11
|
+
return segments.join('/');
|
|
12
|
+
}
|
|
13
|
+
export function resolveInside(root, relativePath) {
|
|
14
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
15
|
+
const resolvedRoot = path.resolve(root);
|
|
16
|
+
const resolved = path.resolve(resolvedRoot, ...normalized.split('/'));
|
|
17
|
+
if (!resolved.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
18
|
+
throw new Error(`Resolved path escapes target root: ${relativePath}`);
|
|
19
|
+
}
|
|
20
|
+
return resolved;
|
|
21
|
+
}
|
package/dist/planner.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { configFromFacts, loadConfig } from './config.js';
|
|
4
|
+
import { discoverProject } from './discovery.js';
|
|
5
|
+
import { sha256 } from './hash.js';
|
|
6
|
+
import { loadManifest } from './manifest.js';
|
|
7
|
+
import { renderArtifacts } from './render.js';
|
|
8
|
+
function classify(root, pathValue, content, managedHash) {
|
|
9
|
+
const target = path.join(root, ...pathValue.split('/'));
|
|
10
|
+
const nextHash = sha256(content);
|
|
11
|
+
if (!existsSync(target)) {
|
|
12
|
+
return { path: pathValue, content, action: 'create', reason: 'destination is absent', nextHash };
|
|
13
|
+
}
|
|
14
|
+
const currentHash = sha256(readFileSync(target));
|
|
15
|
+
if (currentHash === nextHash) {
|
|
16
|
+
return { path: pathValue, content, action: 'unchanged', reason: 'content already matches', nextHash, previousHash: currentHash };
|
|
17
|
+
}
|
|
18
|
+
if (managedHash && currentHash === managedHash) {
|
|
19
|
+
return { path: pathValue, content, action: 'update', reason: 'unchanged managed file can be updated', nextHash, previousHash: currentHash };
|
|
20
|
+
}
|
|
21
|
+
return { path: pathValue, content, action: 'conflict', reason: 'destination is unowned or was modified', nextHash, previousHash: currentHash };
|
|
22
|
+
}
|
|
23
|
+
export function createInstallPlan(root, frameworkVersion, agents = ['codex', 'claude']) {
|
|
24
|
+
const facts = discoverProject(root);
|
|
25
|
+
const manifest = loadManifest(facts.root);
|
|
26
|
+
const config = manifest ? loadConfig(facts.root) : configFromFacts(facts, agents);
|
|
27
|
+
const rendered = renderArtifacts(facts, config).filter((artifact) => !manifest || artifact.path !== 'harness.config.yaml');
|
|
28
|
+
const artifacts = rendered.map((artifact) => classify(facts.root, artifact.path, artifact.content, manifest?.files[artifact.path]?.hash));
|
|
29
|
+
return {
|
|
30
|
+
root: facts.root,
|
|
31
|
+
frameworkVersion,
|
|
32
|
+
facts,
|
|
33
|
+
artifacts,
|
|
34
|
+
conflicts: artifacts.filter((artifact) => artifact.action === 'conflict').map((artifact) => artifact.path),
|
|
35
|
+
};
|
|
36
|
+
}
|
package/dist/render.d.ts
ADDED
package/dist/render.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { serializeConfig } from './config.js';
|
|
2
|
+
function list(values, fallback) {
|
|
3
|
+
return values.length > 0 ? values.map((value) => `- \`${value}/\``).join('\n') : `- ${fallback}`;
|
|
4
|
+
}
|
|
5
|
+
function commandList(config) {
|
|
6
|
+
const entries = Object.entries(config.commands).filter((entry) => Boolean(entry[1]));
|
|
7
|
+
if (entries.length === 0)
|
|
8
|
+
return '- No commands detected. Configure `harness.config.yaml` before verification.';
|
|
9
|
+
return entries.map(([name, command]) => `- ${name}: \`${command}\``).join('\n');
|
|
10
|
+
}
|
|
11
|
+
function agentHarness(config) {
|
|
12
|
+
return `# Agent Harness — ${config.project.name}
|
|
13
|
+
|
|
14
|
+
This file is managed by Agent Harness Init. Project-specific rules in an existing
|
|
15
|
+
\`AGENTS.md\` or \`CLAUDE.md\` take precedence when they are more restrictive.
|
|
16
|
+
|
|
17
|
+
## Operating contract
|
|
18
|
+
|
|
19
|
+
- Inspect relevant source and tests before changing behavior.
|
|
20
|
+
- Keep changes inside the authorized project and preserve unrelated work.
|
|
21
|
+
- Use commands declared in \`harness.config.yaml\`; do not invent replacements.
|
|
22
|
+
- Choose verification evidence according to risk. Security, persistence, public
|
|
23
|
+
contracts, and critical transformations require automated regression evidence.
|
|
24
|
+
- Never commit, push, publish, install global tooling, or contact external
|
|
25
|
+
services unless the user authorizes that action.
|
|
26
|
+
|
|
27
|
+
## Project profile
|
|
28
|
+
|
|
29
|
+
- Profile: \`${config.project.profile}\`
|
|
30
|
+
- Workflow: \`${config.workflow.provider}\`
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
${commandList(config)}
|
|
35
|
+
|
|
36
|
+
Detailed architecture and verification guidance lives in \`harness/\`.
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
function rootAgents(config) {
|
|
40
|
+
return `# AGENTS.md — ${config.project.name}
|
|
41
|
+
|
|
42
|
+
Read \`AGENT_HARNESS.md\` before modifying this project. Project owners may add
|
|
43
|
+
more restrictive rules below this notice.
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
function architecture(config) {
|
|
47
|
+
return `# Project architecture
|
|
48
|
+
|
|
49
|
+
This file records detected structure, not inferred business behavior. Edit
|
|
50
|
+
\`harness.config.yaml\` when the project structure changes, then run
|
|
51
|
+
\`agent-harness update\`.
|
|
52
|
+
|
|
53
|
+
## Source roots
|
|
54
|
+
|
|
55
|
+
${list(config.architecture.sourceRoots, 'No source root detected; configure explicitly.')}
|
|
56
|
+
|
|
57
|
+
## Test roots
|
|
58
|
+
|
|
59
|
+
${list(config.architecture.testRoots, 'No test root detected; configure explicitly.')}
|
|
60
|
+
|
|
61
|
+
## Dependency policy
|
|
62
|
+
|
|
63
|
+
Architecture boundaries are project-owned. Define allowed dependency directions
|
|
64
|
+
here or in a dedicated project rule before enabling an architecture lint gate.
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
function verification(config) {
|
|
68
|
+
return `# Verification strategy
|
|
69
|
+
|
|
70
|
+
Run the smallest evidence that distinguishes the changed risk, then broaden only
|
|
71
|
+
when the change crosses contracts or release boundaries.
|
|
72
|
+
|
|
73
|
+
The CLI runs only commands declared in \`harness.config.yaml\`:
|
|
74
|
+
|
|
75
|
+
${commandList(config)}
|
|
76
|
+
|
|
77
|
+
Passing output is summarized. On failure, stop after the first failing command,
|
|
78
|
+
inspect that failure, and do not hide unrelated baseline failures.
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
function projectSkill() {
|
|
82
|
+
return `---
|
|
83
|
+
name: agent-harness
|
|
84
|
+
description: Apply this project's Harness rules when implementing, fixing, reviewing, verifying, or planning changes in this repository.
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
Read \`AGENT_HARNESS.md\` and \`harness.config.yaml\`. Load only directly relevant
|
|
88
|
+
project source, tests, and Harness guidance. Preserve user changes and run
|
|
89
|
+
verification proportionate to actual risk. Use \`agent-harness doctor\` to
|
|
90
|
+
diagnose managed-file drift and \`agent-harness verify\` only when requested or
|
|
91
|
+
when implementation completion requires the configured project gates.
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
function installAgent() {
|
|
95
|
+
return `# Installing Agent Harness with an AI agent
|
|
96
|
+
|
|
97
|
+
Ask your agent:
|
|
98
|
+
|
|
99
|
+
> Install and adapt Agent Harness Init to this project. Run a read-only plan
|
|
100
|
+
> first, preserve existing rules, apply only a conflict-free plan, then run
|
|
101
|
+
> doctor and report the result.
|
|
102
|
+
|
|
103
|
+
Deterministic protocol:
|
|
104
|
+
|
|
105
|
+
1. \`agent-harness plan --root .\`
|
|
106
|
+
2. Review detected commands and every conflict.
|
|
107
|
+
3. If conflict-free, run \`agent-harness init --root . --yes\`.
|
|
108
|
+
4. If an unowned \`AGENTS.md\` exists, inspect it and add only a reference to
|
|
109
|
+
\`AGENT_HARNESS.md\` without weakening existing rules.
|
|
110
|
+
5. Run \`agent-harness doctor --root .\`.
|
|
111
|
+
|
|
112
|
+
Do not authorize global Skill installation, repository creation, pushing, or
|
|
113
|
+
publishing merely from a project-local installation request.
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
export function renderArtifacts(facts, config) {
|
|
117
|
+
const artifacts = [
|
|
118
|
+
{ path: 'harness.config.yaml', content: serializeConfig(config) },
|
|
119
|
+
{ path: 'AGENT_HARNESS.md', content: agentHarness(config) },
|
|
120
|
+
{ path: 'harness/architecture.md', content: architecture(config) },
|
|
121
|
+
{ path: 'harness/verification.md', content: verification(config) },
|
|
122
|
+
{ path: 'INSTALL_AGENT.md', content: installAgent() },
|
|
123
|
+
];
|
|
124
|
+
if (!facts.existingAgentFiles.includes('AGENTS.md')) {
|
|
125
|
+
artifacts.push({ path: 'AGENTS.md', content: rootAgents(config) });
|
|
126
|
+
}
|
|
127
|
+
if (config.agents.includes('codex')) {
|
|
128
|
+
artifacts.push({ path: '.agents/skills/agent-harness/SKILL.md', content: projectSkill() });
|
|
129
|
+
}
|
|
130
|
+
if (config.agents.includes('claude')) {
|
|
131
|
+
artifacts.push({ path: '.claude/skills/agent-harness/SKILL.md', content: projectSkill() });
|
|
132
|
+
}
|
|
133
|
+
return artifacts.sort((left, right) => left.path.localeCompare(right.path));
|
|
134
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
function packageRoot() {
|
|
6
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
}
|
|
8
|
+
function providerSkillRoot(provider) {
|
|
9
|
+
if (provider === 'codex') {
|
|
10
|
+
return path.join(process.env.CODEX_HOME || path.join(homedir(), '.codex'), 'skills');
|
|
11
|
+
}
|
|
12
|
+
return path.join(homedir(), '.claude', 'skills');
|
|
13
|
+
}
|
|
14
|
+
export function installBootstrapSkill(provider, force = false) {
|
|
15
|
+
const sourceRoot = path.join(packageRoot(), 'skills', 'agent-harness-init');
|
|
16
|
+
const destinationRoot = path.join(providerSkillRoot(provider), 'agent-harness-init');
|
|
17
|
+
if (!existsSync(path.join(sourceRoot, 'SKILL.md'))) {
|
|
18
|
+
throw new Error(`Packaged Bootstrap Skill is missing at ${sourceRoot}`);
|
|
19
|
+
}
|
|
20
|
+
if (existsSync(destinationRoot) && !force) {
|
|
21
|
+
throw new Error(`Skill destination already exists: ${destinationRoot}; use --force only after review`);
|
|
22
|
+
}
|
|
23
|
+
for (const relativePath of ['SKILL.md', 'agents/openai.yaml', 'references/install-protocol.md']) {
|
|
24
|
+
const source = path.join(sourceRoot, ...relativePath.split('/'));
|
|
25
|
+
if (!existsSync(source))
|
|
26
|
+
continue;
|
|
27
|
+
const destination = path.join(destinationRoot, ...relativePath.split('/'));
|
|
28
|
+
mkdirSync(path.dirname(destination), { recursive: true });
|
|
29
|
+
writeFileSync(destination, readFileSync(source));
|
|
30
|
+
}
|
|
31
|
+
return destinationRoot;
|
|
32
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type ProjectProfile = 'javascript' | 'python' | 'fullstack' | 'monorepo' | 'custom';
|
|
2
|
+
export type AgentProvider = 'codex' | 'claude';
|
|
3
|
+
export interface ProjectCommands {
|
|
4
|
+
build?: string;
|
|
5
|
+
typecheck?: string;
|
|
6
|
+
lint?: string;
|
|
7
|
+
test?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ProjectFacts {
|
|
10
|
+
root: string;
|
|
11
|
+
name: string;
|
|
12
|
+
profile: ProjectProfile;
|
|
13
|
+
markers: string[];
|
|
14
|
+
sourceRoots: string[];
|
|
15
|
+
testRoots: string[];
|
|
16
|
+
commands: ProjectCommands;
|
|
17
|
+
packageManager?: string;
|
|
18
|
+
gitDirty: boolean | null;
|
|
19
|
+
existingAgentFiles: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface HarnessConfig {
|
|
22
|
+
schemaVersion: 1;
|
|
23
|
+
project: {
|
|
24
|
+
name: string;
|
|
25
|
+
profile: ProjectProfile;
|
|
26
|
+
};
|
|
27
|
+
agents: AgentProvider[];
|
|
28
|
+
workflow: {
|
|
29
|
+
provider: 'native' | 'openspec';
|
|
30
|
+
};
|
|
31
|
+
architecture: {
|
|
32
|
+
sourceRoots: string[];
|
|
33
|
+
testRoots: string[];
|
|
34
|
+
};
|
|
35
|
+
commands: ProjectCommands;
|
|
36
|
+
}
|
|
37
|
+
export interface RenderedArtifact {
|
|
38
|
+
path: string;
|
|
39
|
+
content: string;
|
|
40
|
+
}
|
|
41
|
+
export type PlanAction = 'create' | 'update' | 'unchanged' | 'conflict';
|
|
42
|
+
export interface PlannedArtifact extends RenderedArtifact {
|
|
43
|
+
action: PlanAction;
|
|
44
|
+
reason: string;
|
|
45
|
+
previousHash?: string;
|
|
46
|
+
nextHash: string;
|
|
47
|
+
}
|
|
48
|
+
export interface InstallPlan {
|
|
49
|
+
root: string;
|
|
50
|
+
frameworkVersion: string;
|
|
51
|
+
facts: ProjectFacts;
|
|
52
|
+
artifacts: PlannedArtifact[];
|
|
53
|
+
conflicts: string[];
|
|
54
|
+
}
|
|
55
|
+
export interface ManifestEntry {
|
|
56
|
+
hash: string;
|
|
57
|
+
}
|
|
58
|
+
export interface HarnessManifest {
|
|
59
|
+
schemaVersion: 1;
|
|
60
|
+
framework: {
|
|
61
|
+
name: 'agent-harness-init';
|
|
62
|
+
version: string;
|
|
63
|
+
};
|
|
64
|
+
installedAt: string;
|
|
65
|
+
files: Record<string, ManifestEntry>;
|
|
66
|
+
}
|
|
67
|
+
export interface DoctorFinding {
|
|
68
|
+
path: string;
|
|
69
|
+
status: 'ok' | 'missing' | 'modified';
|
|
70
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/verify.d.ts
ADDED
package/dist/verify.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
export function runConfiguredVerification(root, config) {
|
|
3
|
+
const ordered = ['lint', 'typecheck', 'test', 'build'];
|
|
4
|
+
const results = [];
|
|
5
|
+
for (const name of ordered) {
|
|
6
|
+
const command = config.commands[name];
|
|
7
|
+
if (!command)
|
|
8
|
+
continue;
|
|
9
|
+
const result = spawnSync(command, { cwd: root, shell: true, stdio: 'inherit', windowsHide: true });
|
|
10
|
+
const status = result.status ?? 1;
|
|
11
|
+
results.push({ name, command, status });
|
|
12
|
+
if (status !== 0)
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
return results;
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Harness-OSPX Contributors
|
|
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/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-harness-init",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent-native, provider-neutral Harness installer for adapting governance and verification gates to existing projects.",
|
|
5
|
+
"author": "xixixixi55",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"agent-harness": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"skills/",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"THIRD_PARTY_NOTICES.md",
|
|
16
|
+
"licenses/"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"verify": "npm run typecheck && npm test && npm run build",
|
|
23
|
+
"release:check": "npm run verify && npm pack --dry-run --ignore-scripts",
|
|
24
|
+
"prepack": "npm run verify"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"ai-agent",
|
|
28
|
+
"harness-engineering",
|
|
29
|
+
"governance",
|
|
30
|
+
"project-scaffold",
|
|
31
|
+
"openspec"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/xixixixi55/agent-harness-init.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/xixixixi55/agent-harness-init#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/xixixixi55/agent-harness-init/issues"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public",
|
|
44
|
+
"registry": "https://registry.npmjs.org/"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"yaml": "^2.8.1"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.18.0",
|
|
54
|
+
"typescript": "^5.9.2",
|
|
55
|
+
"vitest": "^3.2.4"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-harness-init
|
|
3
|
+
description: Install, deploy, initialize, adapt, update, diagnose, or remove Agent Harness Init in an existing or new software project. Use when a user asks to add Harness gates or make a project Agent-governed. Do not use for ordinary feature implementation after the Harness is installed.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agent Harness Init
|
|
7
|
+
|
|
8
|
+
Use the deterministic `agent-harness` CLI for discovery, planning, rendering,
|
|
9
|
+
hashing, and writes. Use Agent judgment only to validate detected project facts
|
|
10
|
+
and merge a minimal reference into an existing project-owned instruction file.
|
|
11
|
+
|
|
12
|
+
## Install or adapt
|
|
13
|
+
|
|
14
|
+
1. Resolve the exact target-project root and read its existing `AGENTS.md` or
|
|
15
|
+
equivalent instructions. A request to install in the current project
|
|
16
|
+
authorizes project-local Harness files, not global tools or external actions.
|
|
17
|
+
2. Run `agent-harness plan --root <root>` before any mutation. If the executable
|
|
18
|
+
is unavailable, report the missing installation; do not silently install a
|
|
19
|
+
global package. A user-approved ephemeral fallback is
|
|
20
|
+
`npx agent-harness-init plan --root <root>`.
|
|
21
|
+
3. Validate the detected profile and commands against bounded project metadata
|
|
22
|
+
such as `package.json`, `pyproject.toml`, workspace files, and CI entrypoints.
|
|
23
|
+
Do not broadly read source contents merely to initialize the Harness.
|
|
24
|
+
4. If the plan reports any conflict, stop and explain the exact paths. Never use
|
|
25
|
+
overwrite flags to bypass an unowned or modified file.
|
|
26
|
+
5. Apply with `agent-harness init --root <root> --yes`.
|
|
27
|
+
6. When a project-owned `AGENTS.md` already exists, preserve it and add only a
|
|
28
|
+
concise reference to `AGENT_HARNESS.md` if no equivalent reference exists.
|
|
29
|
+
Do not weaken or replace existing rules.
|
|
30
|
+
7. Run `agent-harness doctor --root <root>` and report the detected profile,
|
|
31
|
+
generated files, preserved files, and diagnostic result. Do not run the
|
|
32
|
+
project's full verification suite unless requested or needed for a separate
|
|
33
|
+
implementation task.
|
|
34
|
+
|
|
35
|
+
For updates, uninstall, provider paths, conflict behavior, and authorization
|
|
36
|
+
boundaries, read [references/install-protocol.md](references/install-protocol.md).
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Installation protocol
|
|
2
|
+
|
|
3
|
+
## Update
|
|
4
|
+
|
|
5
|
+
Run a plan first, then `agent-harness update --root <root> --yes` only when no
|
|
6
|
+
conflicts exist. Updates may replace files only when their current SHA-256 still
|
|
7
|
+
matches `.harness/manifest.json`. Preserve modified managed files and report them.
|
|
8
|
+
|
|
9
|
+
## Diagnose
|
|
10
|
+
|
|
11
|
+
`agent-harness doctor --root <root>` returns non-zero when a managed file is
|
|
12
|
+
missing or modified. Treat this as project drift, not permission to restore it.
|
|
13
|
+
|
|
14
|
+
## Verify
|
|
15
|
+
|
|
16
|
+
`agent-harness verify --root <root>` executes only commands declared in
|
|
17
|
+
`harness.config.yaml`, stops at the first failure, and may be expensive. Use it
|
|
18
|
+
when the user asks for gates or when implementation completion requires them.
|
|
19
|
+
|
|
20
|
+
## Uninstall
|
|
21
|
+
|
|
22
|
+
Run doctor first. `agent-harness uninstall --root <root> --yes` removes only
|
|
23
|
+
unchanged managed files. It preserves modified or missing entries for manual
|
|
24
|
+
resolution. Do not delete an existing project-owned `AGENTS.md` reference until
|
|
25
|
+
the managed sidecar has been removed and the user wants full cleanup.
|
|
26
|
+
|
|
27
|
+
## Global Bootstrap Skill
|
|
28
|
+
|
|
29
|
+
Installing a global Skill is a separate mutation outside the target project:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
agent-harness install-skill --provider codex
|
|
33
|
+
agent-harness install-skill --provider claude
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Require explicit authorization for that scope. Existing destinations are not
|
|
37
|
+
overwritten unless the user reviews and explicitly requests `--force`.
|
|
38
|
+
|
|
39
|
+
## External boundaries
|
|
40
|
+
|
|
41
|
+
A local Harness installation never authorizes Git commits, pushes, remote
|
|
42
|
+
repository creation, package publication, telemetry, uploads, or network access.
|
|
43
|
+
Obtain separate authorization when those actions are needed.
|