@cyning/harness 0.3.1 → 0.3.2
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/CHANGELOG.md +16 -0
- package/README.md +9 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/methodology/ROADMAP_v1_zh.md +4 -4
- package/examples/demo_checkout/task_demo_p0_golden_v1.harness.json +12 -0
- package/harness/prompts/40-self-check.md +48 -0
- package/harness/prompts/README.md +3 -0
- package/lib/cli.js +77 -3
- package/lib/task.js +205 -0
- package/package.json +3 -2
- package/schema/task.harness.v1.schema.json +76 -0
- package/wizard/gate-check.sh +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.3.2] - 2026-06-15
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Starter 40-self-check 帽**:[`harness/prompts/40-self-check.md`](harness/prompts/40-self-check.md) · Starter 闭包 **10/22/30/40**
|
|
12
|
+
- **`task.harness.v1.json` schema**:[`schema/task.harness.v1.schema.json`](schema/task.harness.v1.schema.json)
|
|
13
|
+
- **CLI `task check`**:`harness task check --file PATH [--no-circular] [--registry DIR]` · depends_on 环检测
|
|
14
|
+
- **gate-check manifest 版本**:输出 `manifest.version` / preset · 与产品包版本不一致时提示 upgrade
|
|
15
|
+
- **金样 sidecar**:[`examples/demo_checkout/task_demo_p0_golden_v1.harness.json`](examples/demo_checkout/task_demo_p0_golden_v1.harness.json)
|
|
16
|
+
- **测试**:`test/task.check.test.js` · `npm test` 扩展
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `harness/prompts/README.md`:标注 Starter 四帽闭包
|
|
21
|
+
- 维护者本地验证:`npm run harness -- task check --file …`
|
|
22
|
+
|
|
7
23
|
## [0.3.1] - 2026-06-15
|
|
8
24
|
|
|
9
25
|
### Fixed
|
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
## Quick Start(npx · v0.3+)
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
|
-
#
|
|
34
|
+
# 新业务仓根(须在目标业务仓执行,勿在本产品仓根目录跑 npx)
|
|
35
35
|
npx @cyning/harness@latest init --preset harness-only --ide cursor,agents
|
|
36
36
|
|
|
37
37
|
# 日常升级
|
|
@@ -39,6 +39,14 @@ npx @cyning/harness upgrade
|
|
|
39
39
|
npx @cyning/harness check
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
**维护者 · 在本仓库根验证 CLI**(npm 不会为「当前包」链接 `bin`,`npx` 会报 `harness: command not found`):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm run harness -- check --target /tmp/foo
|
|
46
|
+
# 或
|
|
47
|
+
node bin/harness.js check --target /tmp/foo
|
|
48
|
+
```
|
|
49
|
+
|
|
42
50
|
**维护者 / 离线(clone 路径):**
|
|
43
51
|
|
|
44
52
|
```bash
|
package/docs/ONBOARDING.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
## 2. 安装(v0.3+ · npx 优先)
|
|
12
12
|
|
|
13
|
-
**推荐 ·
|
|
13
|
+
**推荐 · 目标业务仓根目录**(不要在 **cyning-harness 产品仓根** 跑 `npx @cyning/harness`,否则会 `harness: command not found`;维护者用 `npm run harness -- …` 或 `node bin/harness.js`):
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
npx @cyning/harness@latest init --preset harness-only --ide cursor,agents
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
| --- | --- | --- | --- |
|
|
40
40
|
| **v0.2.x** | **P0 闭环** | harness-only · sync plan/apply · gate-check · 金样 10→22→30 | methodology/ 归集 · P0 gap |
|
|
41
41
|
| **v0.3.0** | **A1 分发** | manifest · npx init/upgrade/check · S5 git-clean · **✅ 2026-06-15**(Release/npm 留维护者) | FailureReport 模板 · rejected→draft gate-check |
|
|
42
|
-
| **v0.3.x** | **A2 Starter** | 40-self-check 帽 · `task.harness.v1.json` · depends_on 禁环 CLI | ONBOARDING npx 优先 |
|
|
42
|
+
| **v0.3.x** | **A2 Starter** | 40-self-check 帽 · `task.harness.v1.json` · depends_on 禁环 CLI · **✅ 2026-06-15** | ONBOARDING npx 优先 |
|
|
43
43
|
| **v0.4.0** | **A3 + push** | ontology.yaml · D7 HG-RELEASE · demo_checkout 脱敏 · MIT public | ETCLOVG 映射页 · README 叙事 |
|
|
44
44
|
| **v1.0.0** | **A4 stable** | ICVO audit CLI · invoke_index · gate-check --graph(Inform) | CHANGELOG 冻结 · B2 量化进 README |
|
|
45
45
|
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
|
|
59
59
|
| 轨 | STRATEGY 名 | 本路线重点 | 当前 |
|
|
60
60
|
| --- | --- | --- | --- |
|
|
61
|
-
| **A** | 产品包 | §2 表 · v0.2→v1.0 | **
|
|
61
|
+
| **A** | 产品包 | §2 表 · v0.2→v1.0 | **A2 40 绿** · A3 / B next |
|
|
62
62
|
| **B** | kimi-code-meta 试点 | oss-fork-meta / kimi-code-meta preset · 上游 PR 证据 · **#8 B2+micro-bench(1+3 主链)** · **#9 Agent-shell(2 并行)** | B0 · 证据方案待审 |
|
|
63
63
|
| **C** | Runtime | harness ctx · Worker · **HGM 不替代 C** | 后置 |
|
|
64
64
|
| **D** | 对外叙事 | ICVO · §7.5 对比 · 理论文章(见 prompts/) | 待 P0 绿 |
|
|
@@ -100,8 +100,8 @@ Epic 与子 task 真值:`Projects/docs/harness/tasks/active/`
|
|
|
100
100
|
| [`TASK_epic_cyning_harness_roadmap_v0_2_to_v1_v1.md`](../../../../docs/harness/tasks/active/TASK_epic_cyning_harness_roadmap_v0_2_to_v1_v1.md) | 全文 |
|
|
101
101
|
| #1 P0 golden close | v0.2.x · **done** 2026-06-15 |
|
|
102
102
|
| #2 A1 npx | v0.3.0 · **done** 2026-06-15 |
|
|
103
|
-
| #3 A2 Starter | v0.3.x · **
|
|
104
|
-
| #4 B kimi dogfood | Track B |
|
|
103
|
+
| #3 A2 Starter | v0.3.x · **done 2026-06-15** |
|
|
104
|
+
| #4 B kimi dogfood | Track B · **next** |
|
|
105
105
|
| #5 D 续篇 | Track D · 篇 1 已 draft |
|
|
106
106
|
| #6 A3 public push | v0.4.0 |
|
|
107
107
|
| #7 A4 v1.0 | v1.0.0 · 引用 #8 |
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1",
|
|
3
|
+
"task_slug": "demo_p0_golden",
|
|
4
|
+
"test_strategy": "not_applicable",
|
|
5
|
+
"test_strategy_note": "金样仅验证 SDD 链与 gate-check · 无产品代码测试",
|
|
6
|
+
"depends_on": [],
|
|
7
|
+
"parallel_group": null,
|
|
8
|
+
"git_branch": "task/demo-p0-golden",
|
|
9
|
+
"worktree_root": "examples/demo_checkout/_sandbox",
|
|
10
|
+
"status": "draft",
|
|
11
|
+
"task_markdown": "examples/demo_checkout/task_demo_p0_golden_v1.md"
|
|
12
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# 帽子:自检(执行者)(Harness · Starter 子集)
|
|
2
|
+
|
|
3
|
+
> **完整版 POINTER**(Ink 工作区):`docs/harness/prompts/40-self-check.md`
|
|
4
|
+
> **本文件**:嵌入用户仓 `docs/harness/prompts/` 的 **精简真值** · A2 Starter 闭包 **10/22/30/40**。
|
|
5
|
+
|
|
6
|
+
## 身份
|
|
7
|
+
|
|
8
|
+
**执行者自检**:与 30 执行帽 **可同上下文连续**;把「声称完成」变成 **可核对证据**。
|
|
9
|
+
|
|
10
|
+
## 只做什么
|
|
11
|
+
|
|
12
|
+
- **逐条**对照 task **验收标准**,标记 pass / fail
|
|
13
|
+
- **必须**运行 task 所列 **验证命令**(与子仓 CI 对齐者优先),摘要 **退出码与关键输出**
|
|
14
|
+
- fail 时定位:命令、文件路径、错误摘要、是否可重试
|
|
15
|
+
- **必须**回填 task 正文 **`### 自检结论(执行者)`**(命令列表、退出码、验收表摘要、已知未测项)
|
|
16
|
+
|
|
17
|
+
## 禁止什么
|
|
18
|
+
|
|
19
|
+
- 不凭记忆声称「测过」;无命令输出则 **不勾选** 验收项
|
|
20
|
+
- 不把 **50 独立复检** 的深度走查塞满本帽(本帽以 **命令与验收表** 为主)
|
|
21
|
+
- **禁止**改 `docs/tasks/` · `reviews/` · `invokes/by-task/`(S2)
|
|
22
|
+
|
|
23
|
+
## 输入假设
|
|
24
|
+
|
|
25
|
+
- 当前分支 **diff**、task 全文、本地可运行环境(或说明 CI 替代)
|
|
26
|
+
- 可选 sidecar:`<task>.harness.json` · 用 `harness task check` 校验
|
|
27
|
+
|
|
28
|
+
## 输出形状
|
|
29
|
+
|
|
30
|
+
- **验收表**(勾选 + 证据摘录)
|
|
31
|
+
- **命令块**:所跑命令、工作目录、退出码
|
|
32
|
+
|
|
33
|
+
## 停止条件
|
|
34
|
+
|
|
35
|
+
- task 列出的验证命令已跑完,或已记录 **阻塞** 并停止勾选未验证项
|
|
36
|
+
|
|
37
|
+
## 交接物
|
|
38
|
+
|
|
39
|
+
- 给 50 复检 / 00 统筹:**diff + 日志摘要 + 自检验收表**
|
|
40
|
+
- task 内 **`### 自检结论(执行者)`** 相对路径引用
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 修订记录
|
|
45
|
+
|
|
46
|
+
| 日期 | 摘要 |
|
|
47
|
+
|------|------|
|
|
48
|
+
| 2026-06-15 | A2 Starter 初版 · 与 Ink 完整库 POINTER 对齐 |
|
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
| [`10-requirements.md`](./10-requirements.md) | ✅ | 需求 / 任务分析 · 精简 + POINTER |
|
|
10
10
|
| [`22-task-audit.md`](./22-task-audit.md) | ✅ | 任务审核 · 落盘 reviews · HG-AUDIT-R1 |
|
|
11
11
|
| [`30-execute-code.md`](./30-execute-code.md) | ✅ v0.1.1 | 执行编码 · 强制闸扫描 · AUDIT approved |
|
|
12
|
+
| [`40-self-check.md`](./40-self-check.md) | ✅ v0.3.2 | 自检 · 命令证据 · 回填 task |
|
|
12
13
|
| [`TEMPLATE_30_gate_stop.md`](./TEMPLATE_30_gate_stop.md) | ✅ v0.1.1 | 30 拒开工输出模板 |
|
|
13
14
|
|
|
15
|
+
**Starter 闭包**:10 / 22 / 30 / **40**(A2 · v0.3.x)
|
|
16
|
+
|
|
14
17
|
## 完整库(POINTER · 不复制全文)
|
|
15
18
|
|
|
16
19
|
Ink 工作区维护完整帽子链:`docs/harness/prompts/`(00/20/40/50、TEMPLATE-*-invoke、PROMPT_*_chain_*)。
|
package/lib/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
wizardScript,
|
|
7
7
|
} from './paths.js';
|
|
8
8
|
import { runBash } from './run.js';
|
|
9
|
+
import { checkTaskFile } from './task.js';
|
|
9
10
|
|
|
10
11
|
function usage() {
|
|
11
12
|
console.log(`@cyning/harness · cyning-harness CLI (v0.3+)
|
|
@@ -14,11 +15,13 @@ function usage() {
|
|
|
14
15
|
npx @cyning/harness init [--preset NAME] [--ide LIST] [--target PATH] [--yes]
|
|
15
16
|
npx @cyning/harness upgrade [--target PATH] [--ide LIST] [--yes] [--force] [--gate-check]
|
|
16
17
|
npx @cyning/harness check [--target PATH]
|
|
18
|
+
npx @cyning/harness task check --file PATH [--no-circular] [--registry DIR]...
|
|
17
19
|
|
|
18
20
|
说明:
|
|
19
|
-
init
|
|
20
|
-
upgrade
|
|
21
|
-
check
|
|
21
|
+
init 首次接入 · 内部调用 wizard/install.sh + harness-sync.sh apply
|
|
22
|
+
upgrade 升级已接入仓 · 等价 wizard/upgrade.sh --yes
|
|
23
|
+
check 读 manifest.json · 对比当前包版本是否有可升级版本
|
|
24
|
+
task check 校验 task.harness.v1.json sidecar · --no-circular 检测 depends_on 环
|
|
22
25
|
|
|
23
26
|
环境:
|
|
24
27
|
CYNING_HARNESS 覆盖产品包根(维护者本地 clone 开发)
|
|
@@ -59,6 +62,10 @@ export async function runCli(argv) {
|
|
|
59
62
|
await cmdCheck(rest, harnessRoot, pkgVersion);
|
|
60
63
|
return;
|
|
61
64
|
}
|
|
65
|
+
if (cmd === 'task') {
|
|
66
|
+
await cmdTask(rest);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
62
69
|
|
|
63
70
|
const err = new Error(`未知命令: ${cmd}\n`);
|
|
64
71
|
err.exitCode = 1;
|
|
@@ -178,3 +185,70 @@ async function cmdCheck(args, harnessRoot, pkgVersion) {
|
|
|
178
185
|
console.log('状态: 已是最新(或版本不可 semver 比较且相同)');
|
|
179
186
|
}
|
|
180
187
|
}
|
|
188
|
+
|
|
189
|
+
async function cmdTask(args) {
|
|
190
|
+
const [sub, ...rest] = args;
|
|
191
|
+
if (sub !== 'check') {
|
|
192
|
+
const err = new Error(`task 子命令未知: ${sub ?? '(空)'}\n用法: task check --file PATH [--no-circular] [--registry DIR]...`);
|
|
193
|
+
err.exitCode = 1;
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const noCircular = rest.includes('--no-circular');
|
|
198
|
+
let filePath;
|
|
199
|
+
const registryDirs = [];
|
|
200
|
+
const filtered = [];
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
203
|
+
const arg = rest[i];
|
|
204
|
+
if (arg === '--no-circular') continue;
|
|
205
|
+
if (arg === '--file') {
|
|
206
|
+
filePath = rest[i + 1];
|
|
207
|
+
i += 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (arg === '--registry') {
|
|
211
|
+
registryDirs.push(rest[i + 1]);
|
|
212
|
+
i += 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
filtered.push(arg);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (filtered.length > 0) {
|
|
219
|
+
const err = new Error(`task check 未知参数: ${filtered.join(' ')}`);
|
|
220
|
+
err.exitCode = 1;
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
if (!filePath) {
|
|
224
|
+
const err = new Error('task check 须指定 --file PATH');
|
|
225
|
+
err.exitCode = 1;
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const result = checkTaskFile(filePath, { noCircular, registryDirs });
|
|
230
|
+
console.log(`文件: ${result.file}`);
|
|
231
|
+
console.log(`task_slug: ${result.task_slug}`);
|
|
232
|
+
|
|
233
|
+
if (!result.validation.ok) {
|
|
234
|
+
console.log('schema: FAIL');
|
|
235
|
+
for (const msg of result.validation.errors) {
|
|
236
|
+
console.log(` - ${msg}`);
|
|
237
|
+
}
|
|
238
|
+
const err = new Error('task sidecar 校验失败');
|
|
239
|
+
err.exitCode = 1;
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log('schema: OK');
|
|
244
|
+
|
|
245
|
+
if (noCircular) {
|
|
246
|
+
if (!result.cycle.ok) {
|
|
247
|
+
console.log(`depends_on: CYCLE ${result.cycle.cycle.join(' → ')}`);
|
|
248
|
+
const err = new Error('depends_on 存在环');
|
|
249
|
+
err.exitCode = 1;
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
console.log('depends_on: acyclic');
|
|
253
|
+
}
|
|
254
|
+
}
|
package/lib/task.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 读取并解析 task sidecar JSON。
|
|
8
|
+
*/
|
|
9
|
+
export function loadTaskSidecar(filePath) {
|
|
10
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
11
|
+
let data;
|
|
12
|
+
try {
|
|
13
|
+
data = JSON.parse(raw);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
const e = new Error(`JSON 解析失败: ${filePath}: ${err.message}`);
|
|
16
|
+
e.exitCode = 1;
|
|
17
|
+
throw e;
|
|
18
|
+
}
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 轻量校验 task.harness.v1(无外部 JSON Schema 依赖)。
|
|
24
|
+
*/
|
|
25
|
+
export function validateTaskSidecar(data, fileLabel = 'task') {
|
|
26
|
+
const errors = [];
|
|
27
|
+
|
|
28
|
+
if (data == null || typeof data !== 'object' || Array.isArray(data)) {
|
|
29
|
+
return { ok: false, errors: [`${fileLabel}: 根节点须为 object`] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const allowed = new Set([
|
|
33
|
+
'schema_version',
|
|
34
|
+
'task_slug',
|
|
35
|
+
'test_strategy',
|
|
36
|
+
'test_strategy_note',
|
|
37
|
+
'depends_on',
|
|
38
|
+
'parallel_group',
|
|
39
|
+
'git_branch',
|
|
40
|
+
'worktree_root',
|
|
41
|
+
'epic_slug',
|
|
42
|
+
'status',
|
|
43
|
+
'task_markdown',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
for (const key of Object.keys(data)) {
|
|
47
|
+
if (!allowed.has(key)) {
|
|
48
|
+
errors.push(`${fileLabel}: 未知字段 "${key}"`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (data.schema_version !== '1') {
|
|
53
|
+
errors.push(`${fileLabel}: schema_version 须为 "1"`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof data.task_slug !== 'string' || !SLUG_RE.test(data.task_slug)) {
|
|
57
|
+
errors.push(`${fileLabel}: task_slug 无效(小写 slug)`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const strategies = ['required', 'recommended', 'not_applicable'];
|
|
61
|
+
if (!strategies.includes(data.test_strategy)) {
|
|
62
|
+
errors.push(`${fileLabel}: test_strategy 须为 ${strategies.join(' | ')}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (data.test_strategy === 'not_applicable') {
|
|
66
|
+
if (typeof data.test_strategy_note !== 'string' || !data.test_strategy_note.trim()) {
|
|
67
|
+
errors.push(`${fileLabel}: test_strategy=not_applicable 须填 test_strategy_note`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (data.depends_on !== undefined) {
|
|
72
|
+
if (!Array.isArray(data.depends_on)) {
|
|
73
|
+
errors.push(`${fileLabel}: depends_on 须为 array`);
|
|
74
|
+
} else {
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
for (const dep of data.depends_on) {
|
|
77
|
+
if (typeof dep !== 'string' || !SLUG_RE.test(dep)) {
|
|
78
|
+
errors.push(`${fileLabel}: depends_on 项无效 "${dep}"`);
|
|
79
|
+
}
|
|
80
|
+
if (dep === data.task_slug) {
|
|
81
|
+
errors.push(`${fileLabel}: depends_on 不可自引用 "${dep}"`);
|
|
82
|
+
}
|
|
83
|
+
if (seen.has(dep)) {
|
|
84
|
+
errors.push(`${fileLabel}: depends_on 重复 "${dep}"`);
|
|
85
|
+
}
|
|
86
|
+
seen.add(dep);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (data.status !== undefined) {
|
|
92
|
+
const statuses = ['draft', 'pending', 'in_progress', 'done'];
|
|
93
|
+
if (!statuses.includes(data.status)) {
|
|
94
|
+
errors.push(`${fileLabel}: status 无效`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { ok: errors.length === 0, errors };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 扫描目录下所有 *.harness.json(一层 + 递归可选)。
|
|
103
|
+
*/
|
|
104
|
+
export function collectTaskSidecars(dirs, { recursive = true } = {}) {
|
|
105
|
+
const map = new Map();
|
|
106
|
+
|
|
107
|
+
function scanDir(dir) {
|
|
108
|
+
if (!fs.existsSync(dir)) return;
|
|
109
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
110
|
+
for (const ent of entries) {
|
|
111
|
+
const full = path.join(dir, ent.name);
|
|
112
|
+
if (ent.isDirectory() && recursive) {
|
|
113
|
+
scanDir(full);
|
|
114
|
+
} else if (ent.isFile() && ent.name.endsWith('.harness.json')) {
|
|
115
|
+
const data = loadTaskSidecar(full);
|
|
116
|
+
const label = path.relative(process.cwd(), full) || full;
|
|
117
|
+
map.set(data.task_slug, {
|
|
118
|
+
filePath: full,
|
|
119
|
+
label,
|
|
120
|
+
data,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const dir of dirs) {
|
|
127
|
+
scanDir(path.resolve(dir));
|
|
128
|
+
}
|
|
129
|
+
return map;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* depends_on 有向图环检测(DFS)。
|
|
134
|
+
*/
|
|
135
|
+
export function detectDependsOnCycle(taskMap) {
|
|
136
|
+
const visiting = new Set();
|
|
137
|
+
const visited = new Set();
|
|
138
|
+
let cyclePath = null;
|
|
139
|
+
|
|
140
|
+
function dfs(slug, stack) {
|
|
141
|
+
if (visited.has(slug)) return false;
|
|
142
|
+
if (visiting.has(slug)) {
|
|
143
|
+
const idx = stack.indexOf(slug);
|
|
144
|
+
cyclePath = stack.slice(idx).concat(slug);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
visiting.add(slug);
|
|
148
|
+
stack.push(slug);
|
|
149
|
+
const entry = taskMap.get(slug);
|
|
150
|
+
const deps = entry?.data?.depends_on ?? [];
|
|
151
|
+
for (const dep of deps) {
|
|
152
|
+
if (dfs(dep, stack)) return true;
|
|
153
|
+
}
|
|
154
|
+
stack.pop();
|
|
155
|
+
visiting.delete(slug);
|
|
156
|
+
visited.add(slug);
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const slug of taskMap.keys()) {
|
|
161
|
+
if (dfs(slug, [])) {
|
|
162
|
+
return { ok: false, cycle: cyclePath };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { ok: true };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 校验单文件;可选合并 registry 目录做环检测。
|
|
170
|
+
*/
|
|
171
|
+
export function checkTaskFile(filePath, { noCircular = false, registryDirs = [] } = {}) {
|
|
172
|
+
const abs = path.resolve(filePath);
|
|
173
|
+
const data = loadTaskSidecar(abs);
|
|
174
|
+
const label = path.basename(abs);
|
|
175
|
+
const validation = validateTaskSidecar(data, label);
|
|
176
|
+
const result = {
|
|
177
|
+
file: abs,
|
|
178
|
+
task_slug: data.task_slug,
|
|
179
|
+
validation,
|
|
180
|
+
cycle: { ok: true },
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
if (!validation.ok) {
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (noCircular) {
|
|
188
|
+
const dirs = new Set([path.dirname(abs), ...registryDirs.map((d) => path.resolve(d))]);
|
|
189
|
+
const taskMap = collectTaskSidecars([...dirs]);
|
|
190
|
+
if (!taskMap.has(data.task_slug)) {
|
|
191
|
+
taskMap.set(data.task_slug, { filePath: abs, label, data });
|
|
192
|
+
}
|
|
193
|
+
for (const dep of data.depends_on ?? []) {
|
|
194
|
+
if (!taskMap.has(dep)) {
|
|
195
|
+
validation.errors.push(`${label}: depends_on 未找到 sidecar "${dep}"`);
|
|
196
|
+
validation.ok = false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (validation.ok) {
|
|
200
|
+
result.cycle = detectDependsOnCycle(taskMap);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return result;
|
|
205
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyning/harness",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "cyning-harness discipline package · init / upgrade / check CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"AGENTS.md"
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
|
-
"test": "node --test test/*.test.js"
|
|
32
|
+
"test": "node --test test/*.test.js",
|
|
33
|
+
"harness": "node bin/harness.js"
|
|
33
34
|
},
|
|
34
35
|
"engines": {
|
|
35
36
|
"node": ">=18"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://cyning.dev/schemas/cyning-harness/task.harness.v1.json",
|
|
4
|
+
"title": "cyning-harness Task Sidecar",
|
|
5
|
+
"description": "WorkItem 机器可读元数据 sidecar · 与 task_*.md 配对(本体 WorkItem · STRATEGY A2)",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schema_version", "task_slug", "test_strategy"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schema_version": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"const": "1",
|
|
13
|
+
"description": "Sidecar schema 主版本"
|
|
14
|
+
},
|
|
15
|
+
"task_slug": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"pattern": "^[a-z0-9][a-z0-9_-]*$",
|
|
18
|
+
"description": "与 task 元信息 task_slug 一致"
|
|
19
|
+
},
|
|
20
|
+
"test_strategy": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"enum": ["required", "recommended", "not_applicable"],
|
|
23
|
+
"description": "Harness V2 test_strategy 字段"
|
|
24
|
+
},
|
|
25
|
+
"test_strategy_note": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"minLength": 1,
|
|
28
|
+
"description": "test_strategy=not_applicable 时必填"
|
|
29
|
+
},
|
|
30
|
+
"depends_on": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"items": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
35
|
+
},
|
|
36
|
+
"uniqueItems": true,
|
|
37
|
+
"default": [],
|
|
38
|
+
"description": "Epic 编排依赖 · 须无环(harness task check --no-circular)"
|
|
39
|
+
},
|
|
40
|
+
"parallel_group": {
|
|
41
|
+
"type": ["string", "null"],
|
|
42
|
+
"description": "Epic parallel_group · 无则 null 或省略"
|
|
43
|
+
},
|
|
44
|
+
"git_branch": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "建议分支名 task/<slug>"
|
|
47
|
+
},
|
|
48
|
+
"worktree_root": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"description": "Open Folder / 验证 cwd"
|
|
51
|
+
},
|
|
52
|
+
"epic_slug": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "所属 Epic slug"
|
|
55
|
+
},
|
|
56
|
+
"status": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"enum": ["draft", "pending", "in_progress", "done"]
|
|
59
|
+
},
|
|
60
|
+
"task_markdown": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "配对 task Markdown 相对路径(可选 · 便于 CLI 定位)"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"allOf": [
|
|
66
|
+
{
|
|
67
|
+
"if": {
|
|
68
|
+
"properties": { "test_strategy": { "const": "not_applicable" } },
|
|
69
|
+
"required": ["test_strategy"]
|
|
70
|
+
},
|
|
71
|
+
"then": {
|
|
72
|
+
"required": ["test_strategy_note"]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
package/wizard/gate-check.sh
CHANGED
|
@@ -84,6 +84,22 @@ check_one_task() {
|
|
|
84
84
|
|
|
85
85
|
echo "=== Harness gate-check ==="
|
|
86
86
|
echo "目标: $TARGET"
|
|
87
|
+
|
|
88
|
+
MANIFEST_FILE="$TARGET/.cyning-harness/manifest.json"
|
|
89
|
+
if [[ -f "$MANIFEST_FILE" ]]; then
|
|
90
|
+
mf_version="$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$MANIFEST_FILE" | head -1 | sed -E 's/.*"([^"]+)"$/\1/')"
|
|
91
|
+
mf_preset="$(grep -o '"preset"[[:space:]]*:[[:space:]]*"[^"]*"' "$MANIFEST_FILE" | head -1 | sed -E 's/.*"([^"]+)"$/\1/')"
|
|
92
|
+
echo "manifest.version: ${mf_version:-?}"
|
|
93
|
+
echo "manifest.preset: ${mf_preset:-?}"
|
|
94
|
+
if [[ -n "${CYNING_HARNESS:-}" && -f "$CYNING_HARNESS/package.json" ]]; then
|
|
95
|
+
pkg_version="$(grep '"version"' "$CYNING_HARNESS/package.json" | head -1 | sed -E 's/.*:[[:space:]]*"([^"]+)".*/\1/')"
|
|
96
|
+
if [[ -n "$pkg_version" && -n "$mf_version" && "$mf_version" != "$pkg_version" ]]; then
|
|
97
|
+
echo "提示: manifest 版本 ($mf_version) 与产品包 ($pkg_version) 不一致 · 可运行 upgrade"
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
else
|
|
101
|
+
echo "manifest: (未接入 · 无 $MANIFEST_FILE)"
|
|
102
|
+
fi
|
|
87
103
|
echo ""
|
|
88
104
|
|
|
89
105
|
BLOCKED=0
|