svharness 0.11.0 → 0.13.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.
@@ -7,9 +7,12 @@ exports.detectBaselineMode = detectBaselineMode;
7
7
  exports.validateArgs = validateArgs;
8
8
  exports.listSupportedArches = listSupportedArches;
9
9
  exports.listSupportedAgents = listSupportedAgents;
10
+ exports.isHarnessRoot = isHarnessRoot;
11
+ exports.resolveConvertPaths = resolveConvertPaths;
10
12
  exports.validateConvertArgs = validateConvertArgs;
11
13
  const fs_extra_1 = __importDefault(require("fs-extra"));
12
14
  const node_path_1 = __importDefault(require("node:path"));
15
+ const harness_name_1 = require("./harness-name");
13
16
  const SUPPORTED_ARCHES = [
14
17
  'android-compose',
15
18
  'android-xml',
@@ -25,7 +28,6 @@ const SUPPORTED_AGENTS = [
25
28
  'opencode',
26
29
  'generic',
27
30
  ];
28
- const NAME_RE = /^[a-z][a-z0-9-]{1,39}$/;
29
31
  const DEFAULT_BASELINE_MAX_FILE_KB = 1024;
30
32
  /**
31
33
  * Decide whether the `--baseline` value looks like a git remote or a local path.
@@ -57,12 +59,19 @@ function detectBaselineMode(input) {
57
59
  }
58
60
  function validateArgs(input) {
59
61
  const errors = [];
60
- // name
62
+ // name (normalized to leading `.`, e.g. my-app → .my-app)
63
+ let normalizedName;
61
64
  if (!input.name) {
62
- errors.push('--name is required');
65
+ errors.push('--harness-name is required');
63
66
  }
64
- else if (!NAME_RE.test(input.name)) {
65
- errors.push(`--name "${input.name}" is invalid. Allowed: lowercase letters, digits, hyphen; start with letter; length 2-40.`);
67
+ else {
68
+ const nameCheck = (0, harness_name_1.validateHarnessNameInput)(input.name);
69
+ if (!nameCheck.ok) {
70
+ errors.push(nameCheck.message);
71
+ }
72
+ else {
73
+ normalizedName = nameCheck.normalized;
74
+ }
66
75
  }
67
76
  // arch
68
77
  if (!input.arch) {
@@ -117,7 +126,7 @@ function validateArgs(input) {
117
126
  throw new Error('Argument validation failed:\n - ' + errors.join('\n - '));
118
127
  }
119
128
  return {
120
- name: input.name,
129
+ name: normalizedName ?? (0, harness_name_1.normalizeHarnessName)(input.name),
121
130
  arch: input.arch,
122
131
  agent: input.agent,
123
132
  baseline: baselineValue,
@@ -138,54 +147,79 @@ const DEFAULT_TIMEOUT_SEC = 120;
138
147
  const DEFAULT_ENDPOINT = 'http://markitdown.desaysz.site';
139
148
  const ENV_ENDPOINT = 'SVHARNESS_MARKITDOWN_ENDPOINT';
140
149
  const DEFAULT_CONVERT_TYPE = 'requirements';
150
+ /** True when `dir` exists and contains `harness.yaml`. */
151
+ function isHarnessRoot(dir) {
152
+ return fs_extra_1.default.existsSync(node_path_1.default.join(dir, 'harness.yaml'));
153
+ }
154
+ function harnessMdDir(harnessRoot, type) {
155
+ return node_path_1.default.join(harnessRoot, type, 'md');
156
+ }
157
+ /**
158
+ * Resolve markdown output directory vs harness layout.
159
+ *
160
+ * - Standalone: `--output` is the final md directory (no `harness.yaml` under it).
161
+ * - Harness: `--output` points at harness root (has `harness.yaml`), or `--harness` is set
162
+ * → writes to `<harness>/<type>/md/` unless both `--harness` and `--output` are given
163
+ * (then `--output` is the final directory).
164
+ */
165
+ function resolveConvertPaths(input) {
166
+ const outputProvided = input.output !== undefined;
167
+ const outputAbs = node_path_1.default.resolve(input.output ?? '.');
168
+ const harnessExplicit = !!input.harness;
169
+ if (harnessExplicit) {
170
+ const harnessRoot = node_path_1.default.resolve(input.harness);
171
+ if (outputProvided) {
172
+ return {
173
+ outputDir: outputAbs,
174
+ harnessRoot,
175
+ harnessMode: true,
176
+ };
177
+ }
178
+ return {
179
+ outputDir: harnessMdDir(harnessRoot, input.type),
180
+ harnessRoot,
181
+ harnessMode: true,
182
+ };
183
+ }
184
+ if (isHarnessRoot(outputAbs)) {
185
+ return {
186
+ outputDir: harnessMdDir(outputAbs, input.type),
187
+ harnessRoot: outputAbs,
188
+ harnessMode: true,
189
+ };
190
+ }
191
+ return {
192
+ outputDir: outputAbs,
193
+ harnessMode: false,
194
+ };
195
+ }
141
196
  /**
142
197
  * Validate and normalise options for `svharness convert`.
143
198
  *
144
199
  * All parameters have sensible defaults:
145
200
  * - --input defaults to `.` (current directory)
146
201
  * - --endpoint defaults to http://markitdown.desaysz.site
147
- * - --harness defaults to the --output path (or current directory)
148
- * - --output defaults to `.` (current directory)
202
+ * - --output defaults to `.` (current directory); see `resolveConvertPaths` for layout
149
203
  *
150
204
  * When --harness is explicitly given, it must exist and contain harness.yaml.
151
- * When inferred from --output, we skip the harness.yaml requirement so users
152
- * can run `svharness convert` without ceremony.
153
205
  */
154
206
  function validateConvertArgs(input) {
155
207
  const errors = [];
156
- // resolve output base (defaults to cwd)
157
- const outputRaw = input.output ?? '.';
158
- const outputAbs = node_path_1.default.resolve(outputRaw);
159
208
  // input: default to ['.']
160
209
  const patterns = (input.input ?? ['.']).map((s) => s.trim()).filter(Boolean);
161
- // harness: explicit > output path
162
- let harnessAbs;
163
210
  const harnessExplicit = !!input.harness;
164
211
  if (harnessExplicit) {
165
- harnessAbs = node_path_1.default.resolve(input.harness);
212
+ const harnessAbs = node_path_1.default.resolve(input.harness);
166
213
  if (!fs_extra_1.default.existsSync(harnessAbs)) {
167
214
  errors.push(`--harness path does not exist: ${harnessAbs}`);
168
215
  }
169
216
  else if (!fs_extra_1.default.statSync(harnessAbs).isDirectory()) {
170
217
  errors.push(`--harness must be a directory: ${harnessAbs}`);
171
218
  }
172
- else if (!fs_extra_1.default.existsSync(node_path_1.default.join(harnessAbs, 'harness.yaml'))) {
219
+ else if (!input.allowMissingHarnessYaml && !isHarnessRoot(harnessAbs)) {
173
220
  errors.push(`--harness "${harnessAbs}" does not look like a harness root (missing harness.yaml)`);
174
221
  }
175
222
  }
176
- else {
177
- // fallback to output path
178
- harnessAbs = outputAbs;
179
- // skip harness.yaml validation when inferred — just warn if missing
180
- if (fs_extra_1.default.existsSync(harnessAbs) && fs_extra_1.default.statSync(harnessAbs).isDirectory()) {
181
- if (!fs_extra_1.default.existsSync(node_path_1.default.join(harnessAbs, 'harness.yaml'))) {
182
- // warn-only (harmless: outDir just gets created)
183
- }
184
- }
185
- else {
186
- // if output path doesn't exist yet, that's fine — we'll create it
187
- }
188
- }
189
223
  // endpoint: flag > env > default
190
224
  let endpoint = (input.endpoint && input.endpoint.trim()) ||
191
225
  (process.env[ENV_ENDPOINT] && process.env[ENV_ENDPOINT].trim()) ||
@@ -211,9 +245,16 @@ function validateConvertArgs(input) {
211
245
  if (errors.length > 0) {
212
246
  throw new Error('Argument validation failed:\n - ' + errors.join('\n - '));
213
247
  }
248
+ const paths = resolveConvertPaths({
249
+ harness: input.harness,
250
+ output: input.output,
251
+ type: convertType,
252
+ });
214
253
  return {
215
254
  input: patterns,
216
- harness: harnessAbs,
255
+ outputDir: paths.outputDir,
256
+ harnessRoot: paths.harnessRoot,
257
+ harnessMode: paths.harnessMode,
217
258
  endpoint,
218
259
  concurrency,
219
260
  maxFileMB,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svharness",
3
- "version": "0.11.0",
3
+ "version": "0.13.2",
4
4
  "description": "CLI scaffolder for SDD-Driven Agent-Agnostic Coding Framework (harness)",
5
5
  "bin": {
6
6
  "svharness": "./bin/cli.js",
@@ -1,122 +1,32 @@
1
1
  ---
2
2
  name: harness-apply-skills-main
3
3
  description: >
4
- harness 知识层(rules / skills / specs / baseline / memory)应用到目标项目,
5
- 完成具体功能开发。**从本 skill 正文内嵌的 `## 绑定元数据` 小节解析** `harness_root`,
6
- 然后加载 `<harness_root>/agent-env/rules/` 作为强约束、扫描
7
- `<harness_root>/agent-env/skills/*/SKILL.md` 作为子能力,并参考
8
- `<harness_root>/baseline/`、`<harness_root>/specs/` 产出代码。
9
- 当用户说"应用 harness-apply-skills-main 完成 xxx 功能开发"、
10
- "使用 harness 开发 xxx"、"基于 harness 做 xxx"、"apply harness"、
11
- "按 harness 规范实现 xxx" 时触发。
4
+ harness 应用入口(薄索引模式)。提示用户当前项目已注入 runtime skills/rules,
5
+ 优先直接调用已注入的 `harness-apply-skills-*` skill;如无匹配,再按普通开发流程执行。
12
6
  ---
13
7
 
14
- # harness-apply-skills-main(harness 应用二级调度)
8
+ # harness-apply-skills-main(harness 应用入口)
15
9
 
16
- 你是一个 **harness 知识层调度器**。当用户请求基于当前项目绑定的 harness
17
- 完成功能开发时,你不直接写业务代码,而是先把 harness 的约束、能力、基线、规格
18
- 装载到上下文,再按语义把任务路由到 harness 内部的子 skill。
10
+ 你是一个 **harness 应用入口提示器**。当前项目已经注入运行期 rules 与 skills:
19
11
 
20
- > skill 由 `svharness apply` 注入。harness 本体可能位于目标项目之外
21
- > 的任意路径(方案 B:不同根),因此**所有 harness 文件引用必须通过本 skill
22
- > 正文末尾的 `## 绑定元数据` YAML 块中的 `harness_root` 解析**,禁止假设相对
23
- > 位置,也不要去读取外部 `binding.yaml`——它只是冗余副本,不是权威来源。
12
+ - harness 根目录:`__HARNESS_ROOT_REL__`
13
+ - 运行期技能目录:`__HARNESS_ROOT_REL__/agent-env/skills/`
14
+ - 运行期规则目录:`__HARNESS_ROOT_REL__/agent-env/rules/`
24
15
 
25
- ## 输入
16
+ ## 使用原则
26
17
 
27
- - **本 skill 文件内嵌的 `## 绑定元数据` YAML 块** —— 权威来源,运行时从这里读取:
28
- - `harness_root` —— harness 目录(形如 `<name>-harness/`)的**绝对路径**
29
- - `harness_root_rel` —— 相对目标项目根目录的路径(用于在 md 中渲染跨项目链接)
30
- - `harness_name` —— harness 名称
31
- - `harness_version` —— harness 构建状态版本(来自 `<harness_root>/VERSION`)
32
- - `target_project` —— 绑定的目标项目根路径
33
- - `agent` —— 当前 IDE / agent 标识
34
- - `applied_at` / `applied_by` —— 绑定时间与工具版本
35
- - `apply_skill_registry` —— 由 S60 登记的**内容引用**清单(`name` / `reference_md` /
36
- `description`,可选 `entry` / `source_id`);`svharness apply` 将其内联到本 skill,
37
- 运行期**仅通过本清单 + `references/md/` 原文**路由与执行,不向目标项目注入
38
- `harness-apply-skills-*` 子目录
39
- - (旁置,不读)`./references/binding.yaml` —— 冗余副本,仅供 `svharness`
40
- 自身工具链(status / detach 等)解析;skill 运行时**不读它**。
18
+ 1. 优先枚举当前项目已注入的 `harness-apply-skills-*` skill,并调用最匹配者完成任务。
19
+ 2. 每次任务必须遵守已注入的 rules(always_on 优先)。
20
+ 3. 若没有匹配 skill,按普通实现流程完成,但仍需参考 `__HARNESS_ROOT_REL__/specs/`、`__HARNESS_ROOT_REL__/baseline/`。
21
+ 4. 不修改 `__HARNESS_ROOT_REL__` 下的任何文件;harness 作为只读知识源。
41
22
 
42
- ## 算法
23
+ ## 典型触发
43
24
 
44
- 1. **加载绑定**:从本 skill 正文末尾的 `## 绑定元数据` 小节(fenced ```yaml 代码块)
45
- 解析 `harness_root` 等字段。若解析失败或 `harness_root` 指向的目录不存在,
46
- 停止并提示用户重新运行 `svharness apply --force`。
25
+ - 应用 harness-apply-skills-main 完成 xxx 功能开发
26
+ - 使用 harness 开发 xxx
27
+ - 基于 harness 做 xxx
47
28
 
48
- 2. **加载强约束**:枚举并读取 `<harness_root>/agent-env/rules/*.mdc` 与
49
- `<harness_root>/agent-env/rules/*.md`(`always_on` 必须全部遵守;
50
- `model_decision` 按本次任务语义挑选)。规则冲突时以 `always_on` 为准。
29
+ ## 注意
51
30
 
52
- 3. **扫描子能力清单**(按优先级合并路由表):
53
- - **优先**使用绑定元数据中的 `apply_skill_registry`(见下文「References 能力」);
54
- - 再枚举 `<harness_root>/agent-env/skills/*/SKILL.md`(模板 skill、baseline 合并
55
- skill;**不含** `harness-apply-skills-*` 前缀——references 已由 registry 承载),
56
- 提取 `name` + `description`;
57
- - 合并去重后得到本次会话可用的 **子 skill 路由表**。不要整目录朗读内容,按需要再深读。
58
-
59
- 4. **语义路由**:
60
- - 若任务匹配 `apply_skill_registry` 中某项,按「References 能力 → 执行」处理,
61
- **直接读** `<harness_root>/<reference_md>`,勿再查找同名子 skill 目录;
62
- - 否则在路由表中选择最匹配的 1 ~ N 个 agent-env 子 skill;
63
- - 若无匹配,回到普通实现路径,但仍必须遵守步骤 2 的规则。
64
-
65
- 5. **查阅基线与规格**(按需):
66
- - `<harness_root>/baseline/wiki/` —— 项目知识摘要,优先用于建立上下文
67
- - `<harness_root>/baseline/code/` —— 权威参考实现;禁止引用该目录外的路径作为"参考实现"
68
- - `<harness_root>/specs/signals|ui|behavior|interfaces/` —— 若任务与信号/UI/行为/接口相关,必须先对齐 schema
69
- - `<harness_root>/tasks/` —— 若存在匹配的任务模板,优先复用
70
-
71
- 6. **执行与交付**:严格在 `target_project` 下写代码,引用 harness 文件时用
72
- 相对路径(`harness_root_rel` + 子路径)。不得把 harness 内部文件复制到
73
- 目标项目;harness 是只读的知识源。
74
-
75
- 7. **记忆沉淀**:若本次产生了可复用经验,按 agent-env 的 memory 规范追加到
76
- `<harness_root>/agent-env/memory/inbox/`(如果目录存在);不确定时停住询问。
77
-
78
- ## 守则(不得违反)
79
-
80
- - **不得绕过本 skill 内嵌的 `## 绑定元数据` 小节**去猜测 harness 路径;路径错误必须让用户重绑定。
81
- - **不得修改 harness 目录下的文件**(rules / skills / specs / baseline / VERSION)。
82
- harness 是知识源,演进由 `svharness` 和 harness 内部的 maintainer skill 负责。
83
- - **不得把 harness 的 always_on rules 当成"建议"**;它们是硬约束。
84
- - **不得引用 `baseline/code/` 之外的源码**作为"参考实现"。
85
- - **不得把本 skill 当作业务 skill**:它只做上下文装载 + 路由,业务实现由子 skill 或
86
- agent 本体完成。
87
- - 所有面向用户的说明使用**中文**;字段名、类名、路径保留英文。
88
- - 多个子 skill 有冲突结论时,显式告知用户并请其裁决,不得自行二选一。
89
-
90
- ## 典型触发例
91
-
92
- - "应用 harness-apply-skills-main 完成无线充电开关功能开发"
93
- - "基于 harness 新增设置项 Fragment"
94
- - "按 harness 规范实现新信号 CMDID 0xA3"
95
- - "apply harness 给我做一个埋点上报"
96
-
97
- 以上触发均需先走算法 1~5,再进入业务实现。
98
-
99
- ## References 能力(`apply_skill_registry`)
100
-
101
- `apply_skill_registry` 是 S60 为**内容引用**登记的清单,由 `svharness apply` 内联到
102
- 本 skill 绑定元数据。运行期以清单 + `<harness_root>/references/md/` 为准,**不依赖**
103
- 目标项目或 harness 内是否存在 `harness-apply-skills-*` 子 skill 目录。
104
-
105
- ### 路由
106
-
107
- 1. 将用户任务与 `apply_skill_registry[].description`、`name` 做语义匹配。
108
- 2. 选出 0 ~ 1 条(必要时可多条)登记项;无匹配时回退到 agent-env 子 skill 扫描。
109
-
110
- ### 执行(匹配到登记项后)
111
-
112
- 1. 从绑定元数据取 `harness_root`、`target_project`。
113
- 2. 阅读 `<harness_root>/<reference_md>` 中与本次任务相关的章节;有 `source_id` 时可
114
- 对照 `<harness_root>/references/yaml/index.yaml`。**禁止**跳过原文臆造流程。
115
- 3. 遵守 `<harness_root>/agent-env/rules/` 中 `always_on` 规则。
116
- 4. 若登记项含 `entry`(如 `/agent analyst`),按 references 原文执行该入口。
117
- 5. 在 `target_project` 下交付;与 `<harness_root>/specs/` 冲突时先请用户裁决。
118
- 6. 不得修改 harness 目录内任何文件;不得把本路径当作构建期 skill(`harness-build-*`)。
119
-
120
- ## 绑定元数据(权威来源,由 svharness apply 生成,请勿手工编辑)
121
-
122
- {{BINDING_YAML_BLOCK}}
31
+ - 本 skill 不负责二级调度与 binding 解析。
32
+ - 如发现路径不一致,请重新执行 `svharness apply --force`。
@@ -31,9 +31,10 @@ apply:
31
31
  clone: false
32
32
  yes: true
33
33
 
34
+ # convert:output 无 harness.yaml 时写入该目录;有 harness.yaml 时写入 <output>/<type>/md/
34
35
  convert:
35
36
  input:
36
37
  - ./docs/*.pdf
37
- output: .
38
+ output: ./my-app-harness
38
39
  type: requirements
39
40
  yes: true