svharness 0.8.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.
Files changed (93) hide show
  1. package/README.md +290 -61
  2. package/dist/adapters/claude-code.js +1 -0
  3. package/dist/adapters/codechat.js +1 -0
  4. package/dist/adapters/cursor.js +1 -0
  5. package/dist/adapters/generic.js +1 -0
  6. package/dist/adapters/index.js +2 -0
  7. package/dist/adapters/opencode.js +17 -0
  8. package/dist/adapters/qoder.js +1 -0
  9. package/dist/commands/apply.js +456 -71
  10. package/dist/commands/convert.js +371 -0
  11. package/dist/commands/init.js +156 -11
  12. package/dist/commands/references-apply-skills.js +47 -0
  13. package/dist/commands/wizard.js +442 -0
  14. package/dist/config/constants.js +7 -0
  15. package/dist/config/index.js +18 -0
  16. package/dist/config/load-config.js +54 -0
  17. package/dist/config/merge-options.js +115 -0
  18. package/dist/config/normalize.js +165 -0
  19. package/dist/config/save-config.js +40 -0
  20. package/dist/config/types.js +2 -0
  21. package/dist/core/agent-injector.js +58 -9
  22. package/dist/core/apply-project-entry.js +66 -0
  23. package/dist/core/build-project-entry.js +98 -0
  24. package/dist/core/doc-intake-paths.js +155 -0
  25. package/dist/core/extra-assets-intake.js +254 -0
  26. package/dist/core/markitdown-client.js +156 -0
  27. package/dist/core/next-steps.js +33 -22
  28. package/dist/core/project-ignore.js +53 -0
  29. package/dist/core/reference-apply-skills.js +35 -0
  30. package/dist/core/render-meta.js +2 -1
  31. package/dist/core/repomix-pack.js +3 -3
  32. package/dist/core/state.js +44 -24
  33. package/dist/index.js +211 -140
  34. package/dist/utils/harness-name.js +41 -0
  35. package/dist/utils/validate-args.js +147 -6
  36. package/dist/wiki/wikiTasksWriter.js +5 -6
  37. package/package.json +2 -1
  38. package/templates/_shared/apply-skills/harness-apply-skills-main.md +19 -78
  39. package/templates/_shared/build-rules/harness-build-rule-agent-agnostic.md +5 -5
  40. package/templates/_shared/build-rules/harness-build-rule-chinese-only.md +5 -5
  41. package/templates/_shared/build-rules/harness-build-rule-convert-check.md +46 -0
  42. package/templates/_shared/build-rules/harness-build-rule-memory-write.md +1 -1
  43. package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +36 -10
  44. package/templates/_shared/build-rules/harness-build-rule-skills-tasks-output.md +3 -2
  45. package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +3 -3
  46. package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +36 -16
  47. package/templates/_shared/build-skills/harness-build-skill-agent-env-merge.md +75 -0
  48. package/templates/_shared/build-skills/harness-build-skill-knowledge-builder.md +49 -85
  49. package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +35 -18
  50. package/templates/_shared/build-skills/harness-build-skill-references-intake.md +91 -0
  51. package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +19 -9
  52. package/templates/_shared/build-skills/harness-build-skill-wiki-writer.md +24 -24
  53. package/templates/_shared/build-skills/harness-build-skills-main.md +83 -0
  54. package/templates/_shared/meta/AGENTS_APPLY.md.ejs +139 -0
  55. package/templates/_shared/meta/{AGENTS.md.ejs → AGENTS_BUILD.md.ejs} +7 -5
  56. package/templates/_shared/meta/CHANGELOG.md.ejs +3 -3
  57. package/templates/_shared/meta/README.md.ejs +11 -9
  58. package/templates/_shared/meta/harness.yaml.ejs +28 -7
  59. package/templates/_shared/skeleton/baseline/code/.gitkeep +1 -0
  60. package/templates/_shared/skeleton/baseline/wiki/.gitkeep +1 -0
  61. package/templates/_shared/skeleton/references/apply-skills-registry.example.yaml +11 -0
  62. package/templates/_shared/skeleton/references/md/.gitkeep +1 -0
  63. package/templates/_shared/skeleton/references/raw/.gitkeep +1 -0
  64. package/templates/_shared/skeleton/references/yaml/.gitkeep +1 -0
  65. package/templates/_shared/skeleton/requirements/md/.gitkeep +1 -0
  66. package/templates/_shared/skeleton/requirements/raw/.gitkeep +1 -0
  67. package/templates/_shared/skeleton/requirements/yaml/.gitkeep +1 -0
  68. package/templates/android-xml/skeleton/agent-env/skills/harness-android-cli/SKILL.md +88 -0
  69. package/templates/android-xml/skeleton/agent-env/skills/harness-android-service-patterns/SKILL.md +205 -0
  70. package/templates/android-xml/skeleton/agent-env/skills/harness-android-xml-architecture/SKILL.md +138 -0
  71. package/templates/android-xml/skeleton/agent-env/skills/harness-lifecycle-management/SKILL.md +158 -0
  72. package/templates/android-xml/skeleton/agent-env/skills/harness-xml-ui/SKILL.md +112 -0
  73. package/templates/cpp/skeleton/agent-env/skills/harness-cmake-build/SKILL.md +163 -0
  74. package/templates/cpp/skeleton/agent-env/skills/harness-cpp-architecture/SKILL.md +157 -0
  75. package/templates/cpp/skeleton/agent-env/skills/harness-cpp-concurrency/SKILL.md +180 -0
  76. package/templates/cpp/skeleton/agent-env/skills/harness-memory-safety/SKILL.md +163 -0
  77. package/templates/cpp/skeleton/agent-env/skills/harness-modern-cpp/SKILL.md +149 -0
  78. package/templates/python/skeleton/agent-env/skills/harness-async-patterns/SKILL.md +162 -0
  79. package/templates/python/skeleton/agent-env/skills/harness-python-architecture/SKILL.md +160 -0
  80. package/templates/python/skeleton/agent-env/skills/harness-python-package-structure/SKILL.md +210 -0
  81. package/templates/python/skeleton/agent-env/skills/harness-python-performance/SKILL.md +207 -0
  82. package/templates/python/skeleton/agent-env/skills/harness-python-testing/SKILL.md +198 -0
  83. package/templates/svharness.config.example.yaml +40 -0
  84. package/templates/web-react/skeleton/agent-env/skills/harness-react-architecture/SKILL.md +177 -0
  85. package/templates/web-react/skeleton/agent-env/skills/harness-react-performance/SKILL.md +177 -0
  86. package/templates/web-react/skeleton/agent-env/skills/harness-react-testing/SKILL.md +193 -0
  87. package/templates/web-react/skeleton/agent-env/skills/harness-react-ui-patterns/SKILL.md +257 -0
  88. package/templates/web-react/skeleton/agent-env/skills/harness-state-management/SKILL.md +189 -0
  89. package/templates/_shared/skeleton/assets/baseline/code/.gitkeep +0 -1
  90. package/templates/_shared/skeleton/assets/baseline/wiki/.gitkeep +0 -1
  91. package/templates/_shared/skeleton/assets/raw/.gitkeep +0 -1
  92. package/templates/_shared/skeleton/assets/requirements/.gitkeep +0 -1
  93. /package/templates/_shared/skeleton/{assets/baseline/repomix → agent-env/_incoming/skills}/.gitkeep +0 -0
@@ -10,49 +10,69 @@ const node_path_1 = __importDefault(require("node:path"));
10
10
  const js_yaml_1 = __importDefault(require("js-yaml"));
11
11
  const logger_1 = require("../utils/logger");
12
12
  /**
13
- * Build the initial state object. P-1 is DONE (skeleton just created);
14
- * everything else starts PENDING. `P1_wiki` is conditional on
13
+ * Build the initial state object. S00 is DONE (skeleton just created);
14
+ * everything else starts PENDING. `S10_wiki` is conditional on
15
15
  * `wikiPhase` (only present when project has baseline).
16
16
  *
17
- * Phase order: P-1_skeleton → [P1_wiki] → P2_rawP3_requirementsP4_specs
18
- * → P5_rulesP6_skills_tasksP7_memory → P-Final_seal
17
+ * Phase order: S00_bootstrap → [S10_wiki] → S20_collect_inputsS30_convert_docsS40_extract_requirements
18
+ * → S50_generate_specsS60_process_referencesS61_confirm_baseline_extraction
19
+ * → S65_customize_agent_env → S70_runtime_assets → S80_seed_memory → S90_finalize
19
20
  *
20
- * Design: P1_wiki comes before P2_raw because baseline wiki is the project
21
+ * Design: S10_wiki comes before S20_collect_inputs because baseline wiki is the project
21
22
  * experience foundation — it provides architectural knowledge that helps the
22
- * user write better requirement documents. When no baseline, P1_wiki is
23
- * absent and the flow starts from P2_raw.
23
+ * user write better requirement documents. When no baseline, S10_wiki is
24
+ * absent and the flow starts from S20_collect_inputs.
24
25
  */
25
26
  function buildInitialState(input) {
26
27
  const phases = {
27
- 'P-1_skeleton': { status: 'DONE' },
28
+ S00_bootstrap: { status: 'DONE' },
28
29
  };
29
30
  const wp = input.wikiPhase ?? 'none';
30
- // P1_wiki: baseline wiki (project experience foundation) — only when baseline exists
31
+ // S10_wiki: baseline wiki (project experience foundation) — only when baseline exists
31
32
  if (wp === 'done') {
32
- phases.P1_wiki = { status: 'DONE', requires_agent: false, mode: 'full', source: 'cli-full' };
33
+ phases.S10_wiki = { status: 'DONE', requires_agent: false, mode: 'full', source: 'cli-full' };
33
34
  }
34
35
  else if (wp === 'pending') {
35
- phases.P1_wiki = {
36
+ phases.S10_wiki = {
36
37
  status: 'PENDING',
37
38
  requires_agent: true,
38
39
  mode: 'outline-only',
39
40
  source: input.wikiSource ?? 'cli-tasks',
40
- tasks_file: 'assets/baseline/wiki/TASKS.md',
41
+ tasks_file: 'baseline/wiki/TASKS.md',
41
42
  };
42
43
  }
43
- // P2_raw: user adds requirement documents (can reference wiki for context)
44
- phases.P2_raw = { status: 'PENDING', manual_required: true };
45
- phases.P3_requirements = { status: 'PENDING', requires_agent: true };
46
- phases.P4_specs = { status: 'PENDING', requires_agent: true };
47
- phases.P5_rules = { status: 'PENDING', manual_required: true };
48
- phases.P6_skills_tasks = { status: 'PENDING', requires_agent: true };
49
- phases.P7_memory = { status: 'PENDING', requires_agent: true };
50
- phases['P-Final_seal'] = { status: 'PENDING' };
51
- // current_phase starts at P1_wiki (if present) or P2_raw (if no baseline)
52
- const startPhase = wp !== 'none' ? 'P1_wiki' : 'P2_raw';
44
+ // S20_collect_inputs: collect related inputs (requirements/references/optional extra skills & rules)
45
+ phases.S20_collect_inputs = { status: 'PENDING', manual_required: true };
46
+ // S30_convert_docs: convert non-md raw docs to markdown for downstream entry-ization
47
+ phases.S30_convert_docs = { status: 'PENDING', requires_agent: true };
48
+ phases.S40_extract_requirements = { status: 'PENDING', requires_agent: true };
49
+ phases.S50_generate_specs = { status: 'PENDING', requires_agent: true };
50
+ // S60: references convert + structured indexing
51
+ phases.S60_process_references = { status: 'PENDING', requires_agent: true };
52
+ // S61: explicitly confirm whether baseline auto-extraction (skills/rules) is enabled
53
+ phases.S61_confirm_baseline_extraction = {
54
+ status: 'PENDING',
55
+ manual_required: true,
56
+ baseline_auto_extract: 'UNSET',
57
+ };
58
+ // S65: merge extra-skills / extra-rules with user-confirmed conflict handling
59
+ phases.S65_customize_agent_env = {
60
+ status: 'PENDING',
61
+ manual_required: true,
62
+ incoming_total: 0,
63
+ skills_count: 0,
64
+ rules_count: 0,
65
+ unknown_count: 0,
66
+ conflicts_resolved: 0,
67
+ };
68
+ phases.S70_runtime_assets = { status: 'PENDING', requires_agent: true };
69
+ phases.S80_seed_memory = { status: 'PENDING', requires_agent: true };
70
+ phases.S90_finalize = { status: 'PENDING' };
71
+ // current_phase starts at S10_wiki (if present) or S20_collect_inputs (if no baseline)
72
+ const startPhase = wp !== 'none' ? 'S10_wiki' : 'S20_collect_inputs';
53
73
  const startAction = wp !== 'none'
54
- ? '请先构建 baseline wiki(项目经验底座),完成后继续下一步。'
55
- : '请将原始需求文档放入 assets/raw/ 目录后,告诉 Agent 继续构建。';
74
+ ? '请先构建 baseline wiki(项目经验底座),完成后进入 S20 输入文件收集。'
75
+ : '请先完成 S20 输入文件收集:requirements/raw、references/raw,并确认是否有额外 skills/rules。';
56
76
  const state = {
57
77
  project: input.name,
58
78
  arch: input.arch,
package/dist/index.js CHANGED
@@ -4,70 +4,124 @@ const commander_1 = require("commander");
4
4
  const logger_1 = require("./utils/logger");
5
5
  const version_1 = require("./utils/version");
6
6
  const validate_args_1 = require("./utils/validate-args");
7
+ const harness_name_1 = require("./utils/harness-name");
7
8
  const init_1 = require("./commands/init");
8
9
  const apply_1 = require("./commands/apply");
10
+ const convert_1 = require("./commands/convert");
11
+ const wizard_1 = require("./commands/wizard");
12
+ const config_1 = require("./config");
9
13
  /**
10
14
  * Default values surfaced to users (also reflected in README "全部参数" tables).
11
- *
12
- * Keep this list in one place so README, banner help text and command options
13
- * stay in sync. Updating a default here is a *single* source of truth change.
14
15
  */
15
16
  const DEFAULT_ARCH = 'android-compose';
16
17
  const DEFAULT_AGENT = 'codechat';
17
- /**
18
- * Resolve `--harness-name` / `--name` (alias) into a single value and emit a
19
- * deprecation warning when only the legacy `--name` form is used.
20
- */
21
18
  function resolveHarnessName(opts) {
22
19
  if (opts.harnessName && opts.name && opts.harnessName !== opts.name) {
23
20
  throw new Error('同时收到 --harness-name 与 --name 但取值不同,请只保留一个(推荐 --harness-name)');
24
21
  }
25
- if (opts.harnessName)
26
- return opts.harnessName;
27
- if (opts.name) {
22
+ const raw = opts.harnessName ?? opts.name;
23
+ if (!raw) {
24
+ throw new Error('--harness-name <name> 是必填参数(可在配置文件的 build.harnessName 中提供)');
25
+ }
26
+ if (opts.name && !opts.harnessName) {
28
27
  logger_1.logger.warn('⚠️ --name 已重命名为 --harness-name,旧名将在下个 minor 版本移除。');
29
- return opts.name;
30
28
  }
31
- throw new Error('--harness-name <name> 是必填参数');
29
+ const check = (0, harness_name_1.validateHarnessNameInput)(raw);
30
+ if (!check.ok) {
31
+ throw new Error(check.message);
32
+ }
33
+ if (check.normalized !== raw.trim()) {
34
+ logger_1.logger.info(`harness 名称已规范为 ${check.normalized}(目录:${check.normalized}-harness)`);
35
+ }
36
+ return check.normalized;
32
37
  }
33
- async function runBuildAction(opts) {
38
+ function buildSectionFromOpts(opts, harnessName) {
39
+ return {
40
+ harnessName,
41
+ arch: opts.arch,
42
+ agent: opts.agent,
43
+ baseline: opts.baseline,
44
+ requirements: opts.requirements,
45
+ references: opts.references,
46
+ extraSkills: opts.extraSkills,
47
+ baselineBranch: opts.baselineBranch,
48
+ baselineMaxFileKb: opts.baselineMaxFileKb,
49
+ convertEndpoint: opts.convertEndpoint,
50
+ convertConcurrency: opts.convertConcurrency,
51
+ convertMaxFileMb: opts.convertMaxFileMb,
52
+ convertTimeoutSec: opts.convertTimeoutSec,
53
+ convertForce: opts.convertForce,
54
+ force: opts.force,
55
+ yes: opts.yes,
56
+ verbose: opts.verbose,
57
+ generateWiki: opts.generateWiki,
58
+ wikiTasksOnly: opts.wikiTasksOnly,
59
+ wikiLang: opts.wikiLang === 'en' ? 'en' : opts.wikiLang === 'zh' ? 'zh' : undefined,
60
+ wikiModel: opts.wikiModel,
61
+ wikiBaseUrl: opts.wikiBaseUrl,
62
+ wikiApiKey: opts.wikiApiKey,
63
+ wikiSource: opts.wikiSource,
64
+ };
65
+ }
66
+ async function runBuildAction(opts, cmd) {
34
67
  try {
35
- const harnessName = resolveHarnessName(opts);
68
+ const loaded = (0, config_1.loadConfig)({ configPath: opts.config });
69
+ const merged = (0, config_1.mergeBuildOptions)(opts, loaded?.config.build, loaded?.config.defaults, cmd);
70
+ const harnessName = resolveHarnessName(merged);
36
71
  await (0, init_1.runInit)({
37
72
  name: harnessName,
38
- arch: opts.arch,
39
- agent: opts.agent,
40
- baseline: opts.baseline,
41
- baselineBranch: opts.baselineBranch,
42
- baselineMaxFileKB: opts.baselineMaxFileKb,
43
- force: !!opts.force,
44
- yes: !!opts.yes,
45
- verbose: !!opts.verbose,
46
- generateWiki: !!opts.generateWiki,
47
- wikiTasksOnly: !!opts.wikiTasksOnly,
48
- wikiLang: opts.wikiLang === 'en' ? 'en' : opts.wikiLang === 'zh' ? 'zh' : undefined,
49
- wikiModel: opts.wikiModel,
50
- wikiBaseUrl: opts.wikiBaseUrl,
51
- wikiApiKey: opts.wikiApiKey,
52
- wikiSource: opts.wikiSource,
73
+ arch: (merged.arch ?? DEFAULT_ARCH),
74
+ agent: (merged.agent ?? DEFAULT_AGENT),
75
+ baseline: merged.baseline,
76
+ requirements: merged.requirements,
77
+ references: merged.references,
78
+ extraSkills: merged.extraSkills,
79
+ baselineBranch: merged.baselineBranch,
80
+ baselineMaxFileKB: merged.baselineMaxFileKb,
81
+ convertEndpoint: merged.convertEndpoint,
82
+ convertConcurrency: merged.convertConcurrency,
83
+ convertMaxFileMB: merged.convertMaxFileMb,
84
+ convertTimeoutSec: merged.convertTimeoutSec,
85
+ convertForce: merged.convertForce,
86
+ force: !!merged.force,
87
+ yes: !!merged.yes,
88
+ verbose: !!merged.verbose,
89
+ generateWiki: !!merged.generateWiki,
90
+ wikiTasksOnly: !!merged.wikiTasksOnly,
91
+ wikiLang: merged.wikiLang === 'en' ? 'en' : merged.wikiLang === 'zh' ? 'zh' : undefined,
92
+ wikiModel: merged.wikiModel,
93
+ wikiBaseUrl: merged.wikiBaseUrl,
94
+ wikiApiKey: merged.wikiApiKey,
95
+ wikiSource: merged.wikiSource,
53
96
  });
97
+ if (opts.saveConfig !== undefined && opts.saveConfig !== false) {
98
+ const outPath = typeof opts.saveConfig === 'string' && opts.saveConfig.trim()
99
+ ? opts.saveConfig
100
+ : undefined;
101
+ const saved = await (0, config_1.saveConfigSection)({
102
+ outPath,
103
+ build: buildSectionFromOpts(merged, harnessName),
104
+ });
105
+ logger_1.logger.success(`当前参数已写入配置:${saved}`);
106
+ }
54
107
  }
55
108
  catch (err) {
56
109
  logger_1.logger.error(err.message);
57
110
  process.exitCode = 1;
58
111
  }
59
112
  }
60
- /**
61
- * Attach all options shared between `build` and the legacy `init` alias.
62
- * Centralised so the two stay byte-for-byte identical.
63
- */
64
113
  function attachBuildOptions(cmd) {
65
114
  return cmd
66
- .option('--harness-name <name>', '【必填】harness 名称(小写字母、数字、中连线),生成 <name>-harness/ 目录')
115
+ .option('--config <path>', `从 YAML/JSON 读取参数(默认可选 ${config_1.DEFAULT_CONFIG_FILENAME})`)
116
+ .option('--save-config [path]', `执行成功后把参数写入配置文件(默认 ${config_1.DEFAULT_CONFIG_FILENAME})`)
117
+ .option('--harness-name <name>', '【必填】harness 名称(如 my-app → .my-app-harness/ 目录)')
67
118
  .option('--name <name>', '【已废弃】--harness-name 的旧别名,下个 minor 版本移除')
68
119
  .option('--arch <arch>', '架构模板:' + (0, validate_args_1.listSupportedArches)().join(' | '), DEFAULT_ARCH)
69
120
  .option('--agent <agent>', '目标 Agent:' + (0, validate_args_1.listSupportedAgents)().join(' | '), DEFAULT_AGENT)
70
121
  .option('--baseline <path|url>', '【可选】基线源码路径或 Git 仓库地址')
122
+ .option('--requirements <path>', '【可选】需求文档输入路径(文件或目录)')
123
+ .option('--references <path>', '【可选】参考资料输入路径(文件或目录)')
124
+ .option('--extra-skills <path...>', '【可选】额外运行期资源(skills/rules 混放,先入 _incoming)')
71
125
  .option('--baseline-branch <name>', '【Git 模式】指定分支名(默认:main)')
72
126
  .option('--baseline-max-file-kb <kb>', '【过滤】单文件大小上限 KB(默认:1024)', (v) => {
73
127
  const n = Number(v);
@@ -76,16 +130,21 @@ function attachBuildOptions(cmd) {
76
130
  }
77
131
  return n;
78
132
  })
133
+ .option('--convert-endpoint <url>', '【build 自动 convert】markitdown_serve 基址(默认 http://markitdown.desaysz.site)')
134
+ .option('--convert-concurrency <n>', '【build 自动 convert】并发上传数(默认沿用 convert)', (v) => Number(v))
135
+ .option('--convert-max-file-mb <n>', '【build 自动 convert】单文件大小上限 MB(默认沿用 convert)', (v) => Number(v))
136
+ .option('--convert-timeout-sec <n>', '【build 自动 convert】超时秒数(默认沿用 convert)', (v) => Number(v))
137
+ .option('--convert-force', '【build 自动 convert】覆盖已存在同名 .md')
79
138
  .option('--force', '强制覆盖已存在的目标目录')
80
139
  .option('-y, --yes', '跳过交互确认,使用默认值')
81
140
  .option('--verbose', '显示详细日志')
82
141
  .option('--generate-wiki', '【Wiki】自动生成完整 Wiki 文档(需 --baseline)')
83
- .option('--wiki-tasks-only', '【Wiki】仅生成 Wiki 大纲和任务列表,由 Agent 后续撰写(有 --baseline 时为隐式默认)')
142
+ .option('--wiki-tasks-only', '【Wiki】仅生成 Wiki 大纲和任务列表(有 --baseline 时为隐式默认)')
84
143
  .option('--wiki-lang <lang>', '【Wiki】输出语言:zh(默认)| en')
85
- .option('--wiki-model <model>', '【Wiki】AI 模型名称(覆写默认配置)')
144
+ .option('--wiki-model <model>', '【Wiki】AI 模型名称')
86
145
  .option('--wiki-base-url <url>', '【Wiki】OpenAI 兼容 API 地址')
87
146
  .option('--wiki-api-key <key>', '【Wiki】API 密钥')
88
- .option('--wiki-source <path>', '【Wiki】扫描的源码根路径(默认:--baseline 或当前目录)');
147
+ .option('--wiki-source <path>', '【Wiki】扫描的源码根路径');
89
148
  }
90
149
  function main() {
91
150
  const program = new commander_1.Command();
@@ -97,132 +156,145 @@ function main() {
97
156
  .helpOption('-h, --help', '显示帮助信息');
98
157
  const banner = [
99
158
  '',
100
- '╔══════════════════════════════════════════════════════════════════════════╗',
101
- '║ svharness v' + version.padEnd(33) + '',
102
- '╠══════════════════════════════════════════════════════════════════════════╣',
103
- '║ 规格驱动开发(SDD)AI 协作框架脚手架 ',
104
- '║ 为任意 Agent IDE 生成项目本地知识层骨架(harness) ',
105
- '╠══════════════════════════════════════════════════════════════════════════╣',
106
- '║ 支持架构:android-compose | android-xml | cpp | web-react | python ',
107
- '║ 支持 Agent:codechat | qoder | cursor | claude-code | generic',
108
- '║ 运行要求:Node.js ≥ 18 ',
109
- '╚══════════════════════════════════════════════════════════════════════════╝',
159
+ '╔══════════════════════════════════════════════════════════════════════════',
160
+ '║ svharness v' + version.padEnd(33) + '',
161
+ '╠══════════════════════════════════════════════════════════════════════════',
162
+ '║ 规格驱动开发(SDD)AI 协作框架脚手架 ',
163
+ '║ 为任意 Agent IDE 生成项目本地知识层骨架(harness) ',
164
+ '╠══════════════════════════════════════════════════════════════════════════',
165
+ '║ 支持架构:android-compose | android-xml | cpp | web-react | python ',
166
+ '║ 支持 Agent:codechat | qoder | cursor | claude-code | opencode | generic ',
167
+ '║ 运行要求:Node.js ≥ 18 ',
168
+ '╚══════════════════════════════════════════════════════════════════════════',
110
169
  '',
111
170
  ].join('\n');
112
171
  program.addHelpText('beforeAll', banner);
113
172
  program.addHelpText('afterAll', [
114
173
  '',
115
- '┌──────────────────────────────────────────────────────────────────────────┐',
116
- '│ 📖 快速上手 ',
117
- '├──────────────────────────────────────────────────────────────────────────┤',
118
- '│',
119
- '│ # 方式一:交互式创建(仅 --harness-name 必填,其余取默认值) │',
120
- '│ svharness build --harness-name my-app │',
121
- '│',
122
- '│ # 方式二:本地基线 + 自动生成 wiki │',
123
- '│ svharness build \\ │',
124
- '│ --harness-name my-app \\ │',
125
- '│ --baseline your_project_dir \\ │',
126
- '│ --generate-wiki \\ │',
127
- '│ --yes │',
128
- '│',
129
- '│ # 方式三:Git 基线(指定分支与单文件上限) │',
130
- '│ svharness build \\ │',
131
- '│ --harness-name my-app \\ ',
132
- '│ --baseline git@github.com:your/repo.git \\ │',
133
- '│ --baseline-branch main \\ │',
134
- '│ --baseline-max-file-kb 1024 \\ │',
135
- '│ --generate-wiki │',
136
- '│ │',
137
- '│ # 方式四:把已构建好的 harness 绑定到目标项目 │',
138
- '│ svharness apply \\ │',
139
- '│ --harness ./my-app-harness \\ │',
140
- '│ --target . \\ │',
141
- '│ --yes │',
142
- '│ │',
143
- '└──────────────────────────────────────────────────────────────────────────┘',
144
- '',
145
- '┌──────────────────────────────────────────────────────────────────────────┐',
146
- '│ 📋 build 参数说明(含默认值) │',
147
- '├──────────────────────────────────────────────────────────────────────────┤',
148
- '│ │',
149
- '│ 必填参数: │',
150
- '│ --harness-name <name> harness 名称(小写字母、数字、中连线) │',
151
- '│ │',
152
- '│ 含默认值参数: │',
153
- '│ --arch <架构> 默认:android-compose │',
154
- '│ --agent <Agent> 默认:codechat │',
155
- '│ --wiki-lang <语言> 默认:zh │',
156
- '│ --baseline-branch 默认:main(仅 git 模式) │',
157
- '│ --baseline-max-file-kb 默认:1024 │',
158
- '│ │',
159
- '│ 可选开关: │',
160
- '│ --baseline <路径|URL> 本地目录或 Git 仓库地址 │',
161
- '│ --generate-wiki 自动生成完整 Wiki │',
162
- '│ --wiki-tasks-only 仅生成 Wiki 大纲(有 --baseline 时为隐式默认) │',
163
- '│ --force 覆盖已存在的目标目录 │',
164
- '│ -y, --yes 跳过交互确认 │',
165
- '│ --verbose 显示详细日志 │',
166
- '│ │',
167
- '└──────────────────────────────────────────────────────────────────────────┘',
168
- '',
169
- '┌──────────────────────────────────────────────────────────────────────────┐',
170
- '│ 📋 apply 参数说明(含默认值) │',
171
- '├──────────────────────────────────────────────────────────────────────────┤',
172
- '│ │',
173
- '│ 必填参数: │',
174
- '│ --harness <path> 已构建好的 harness 目录路径 │',
175
- '│ │',
176
- '│ 含默认值参数: │',
177
- '│ --target <path> 默认:当前工作目录(cwd) │',
178
- '│ --agent <agent> 默认:从 <harness>/.harness-build-state.yaml 读取│',
179
- '│ │',
180
- '│ 可选开关: │',
181
- '│ --clone 完整拷贝 harness 到目标项目(默认 bind-only) │',
182
- '│ --force 覆盖已存在的 dispatcher skill 目录 │',
183
- '│ -y, --yes 跳过交互确认 │',
184
- '│ --verbose 显示详细日志 │',
185
- '│ │',
186
- '└──────────────────────────────────────────────────────────────────────────┘',
187
- '',
188
- ' 📚 完整文档:https://yesv-desaysv.feishu.cn/docx/YEIHdD4NGooMdrxQO4McqwfNnjf?pre_pathname=%2Fdrive%2Fhome%2F',
174
+ '┌──────────────────────────────────────────────────────────────────────────',
175
+ '│ 快速上手, 帮助:svharness <command> --help 或 svharness help <command> ',
176
+ '├──────────────────────────────────────────────────────────────────────────',
177
+ '│ # 方式零:交互式向导(推荐新手) ',
178
+ '│ svharness wizard ',
179
+ '│ ',
180
+ '│ # 方式二:仅 --harness-name 必填 ',
181
+ '│ svharness build --harness-name my-app ',
182
+ '│ ',
183
+ '│ # 方式三:本地基线 + wiki + requirements + references ',
184
+ '│ svharness build --harness-name my-app --baseline ./src \ ',
185
+ '│ --requirements ./requirements.xlsx --references ./references.pdf ',
186
+ '│ ',
187
+ '│ # 方式四:绑定 harness 到目标项目 ',
188
+ '│ svharness apply --harness ./my-app-harness --target . ',
189
+ '│ ',
190
+ '│ # 方式五:文档转 Markdown ',
191
+ '│ svharness convert --input ./docs/*.pdf --output ./docs/md --type requirements',
192
+ '│ ',
193
+ '└──────────────────────────────────────────────────────────────────────────',
194
+ ' 📚 使用说明文档:https://yesv-desaysv.feishu.cn/docx/OEBwdywTfoncPNxPZ0acClNincg',
189
195
  '',
190
196
  ].join('\n'));
191
- // ── build:主命令(v0.8+) ───────────────────────────────────────────────
192
197
  attachBuildOptions(program
193
198
  .command('build')
194
- .description('构建 harness:生成目录骨架、渲染元文件、向对应 Agent 注入构建辅助 skill。')).action(async (opts) => {
195
- await runBuildAction(opts);
199
+ .description('构建 harness:生成目录骨架、渲染元文件、注入 harness-build skill 与构建规则。')).action(async (opts, cmd) => {
200
+ await runBuildAction(opts, cmd);
196
201
  });
197
- // ── init:隐藏别名(兼容老脚本,下个 minor 版本移除) ─────────────────────
198
202
  attachBuildOptions(program
199
203
  .command('init', { hidden: true })
200
- .description('【已废弃别名】等同于 `build`,下个 minor 版本移除,请改用 `svharness build`。')).action(async (opts) => {
204
+ .description('【已废弃别名】等同于 build')).action(async (opts, cmd) => {
201
205
  logger_1.logger.warn('⚠️ `svharness init` 已重命名为 `svharness build`,旧名将在下个 minor 版本移除。');
202
- await runBuildAction(opts);
206
+ await runBuildAction(opts, cmd);
207
+ });
208
+ program
209
+ .command('wizard')
210
+ .description('交互式向导:分步填写路径与说明,可保存到 svharness.config.yaml 并/或立即执行 build/apply/convert。')
211
+ .action(async () => {
212
+ try {
213
+ await (0, wizard_1.runWizard)();
214
+ }
215
+ catch (err) {
216
+ logger_1.logger.error(err.message);
217
+ process.exitCode = 1;
218
+ }
219
+ });
220
+ program
221
+ .command('convert')
222
+ .description('把本地原始文档通过 markitdown_serve 转为 Markdown,产物落到 <harness>/<type>/md/。')
223
+ .option('--config <path>', `从配置文件读取 convert 节(默认 ${config_1.DEFAULT_CONFIG_FILENAME})`)
224
+ .option('--input <path...>', '【可选,默认 .】源文件、目录或 glob')
225
+ .option('--harness <path>', '【可选】目标 harness 目录')
226
+ .option('--output <path>', '【可选,默认 .】Markdown 输出目录;含 harness.yaml 时写入 <output>/<type>/md/')
227
+ .option('--endpoint <url>', 'markitdown_serve 基址')
228
+ .option('--concurrency <n>', '【默认 3】并发上传数', (v) => Number(v))
229
+ .option('--max-file-mb <n>', '【默认 50】单文件大小上限(MB)', (v) => Number(v))
230
+ .option('--timeout-sec <n>', '【默认 120】超时秒数', (v) => Number(v))
231
+ .option('--type <type>', 'requirements | references')
232
+ .option('--force', '覆盖已存在同名 .md')
233
+ .option('-y, --yes', '跳过交互确认')
234
+ .option('--verbose', '显示详细日志')
235
+ .action(async (opts, cmd) => {
236
+ try {
237
+ const loaded = (0, config_1.loadConfig)({ configPath: opts.config });
238
+ const merged = (0, config_1.mergeConvertOptions)({
239
+ input: opts.input,
240
+ harness: opts.harness,
241
+ output: opts.output,
242
+ endpoint: opts.endpoint,
243
+ concurrency: opts.concurrency,
244
+ maxFileMb: opts.maxFileMb,
245
+ timeoutSec: opts.timeoutSec,
246
+ type: opts.type,
247
+ force: opts.force,
248
+ yes: opts.yes,
249
+ verbose: opts.verbose,
250
+ }, loaded?.config.convert, loaded?.config.defaults, cmd);
251
+ await (0, convert_1.runConvert)({
252
+ input: merged.input ?? [],
253
+ harness: merged.harness,
254
+ output: merged.output,
255
+ endpoint: merged.endpoint,
256
+ concurrency: merged.concurrency,
257
+ maxFileMB: merged.maxFileMb,
258
+ timeoutSec: merged.timeoutSec,
259
+ type: merged.type,
260
+ force: !!merged.force,
261
+ yes: !!merged.yes,
262
+ verbose: !!merged.verbose,
263
+ });
264
+ }
265
+ catch (err) {
266
+ logger_1.logger.error(err.message);
267
+ process.exitCode = 1;
268
+ }
203
269
  });
204
270
  program
205
271
  .command('apply')
206
- .description('把已构建好的 harness(<name>-harness/)绑定到目标项目:向目标项目的 agent skills 目录注入单一 dispatcher skill(harness-apply-skills-main),binding 元数据直接内联到 SKILL.md 的「## 绑定元数据」小节作为运行时权威来源;references/binding.yaml 同时写出作为冗余副本供工具链解析。')
207
- .requiredOption('--harness <path>', '已构建好的 harness 目录路径(形如 ./my-app-harness)')
272
+ .description('把已构建好的 harness 绑定到目标项目(注入 harness-apply-skills-main,内联 apply_skill_registry)。')
273
+ .option('--config <path>', `从配置文件读取 apply 节(默认 ${config_1.DEFAULT_CONFIG_FILENAME})`)
274
+ .option('--harness <path>', '已构建好的 harness 目录路径(可在配置文件的 apply.harness 中提供)')
208
275
  .option('--target <path>', '目标项目根目录(默认:当前工作目录)')
209
276
  .option('--agent <agent>', '目标 Agent:' +
210
277
  (0, validate_args_1.listSupportedAgents)().join(' | ') +
211
- '(省略时从 harness/.harness-build-state.yaml 读取)')
278
+ '(省略时从 harness 状态文件读取)')
212
279
  .option('--force', '覆盖已存在的 dispatcher skill 目录')
213
- .option('--clone', '把整个 harness 目录拷贝到目标项目根下(默认仅绑定,不拷贝)')
280
+ .option('--clone', '拷贝整个 harness 到目标项目(默认 bind-only)')
214
281
  .option('-y, --yes', '跳过交互确认')
215
282
  .option('--verbose', '显示详细日志')
216
- .action(async (opts) => {
283
+ .action(async (opts, cmd) => {
217
284
  try {
285
+ const loaded = (0, config_1.loadConfig)({ configPath: opts.config });
286
+ const merged = (0, config_1.mergeApplyOptions)(opts, loaded?.config.apply, loaded?.config.defaults, cmd);
287
+ if (!merged.harness) {
288
+ throw new Error('--harness <path> 是必填参数(可在配置文件的 apply.harness 中提供)');
289
+ }
218
290
  await (0, apply_1.runApply)({
219
- harness: opts.harness,
220
- target: opts.target,
221
- agent: opts.agent,
222
- force: !!opts.force,
223
- clone: !!opts.clone,
224
- yes: !!opts.yes,
225
- verbose: !!opts.verbose,
291
+ harness: merged.harness,
292
+ target: merged.target,
293
+ agent: merged.agent,
294
+ force: !!merged.force,
295
+ clone: !!merged.clone,
296
+ yes: !!merged.yes,
297
+ verbose: !!merged.verbose,
226
298
  });
227
299
  }
228
300
  catch (err) {
@@ -230,7 +302,6 @@ function main() {
230
302
  process.exitCode = 1;
231
303
  }
232
304
  });
233
- // Parse argv. Use process.argv directly so --help and --version work as-is.
234
305
  program.parseAsync(process.argv).catch((err) => {
235
306
  logger_1.logger.error(err.message);
236
307
  process.exitCode = 1;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeHarnessName = normalizeHarnessName;
4
+ exports.harnessNameCore = harnessNameCore;
5
+ exports.validateHarnessNameInput = validateHarnessNameInput;
6
+ exports.harnessDirNameFromName = harnessDirNameFromName;
7
+ /**
8
+ * Normalize and validate harness names for `svharness build`.
9
+ *
10
+ * User-facing input `my-app` becomes `.my-app`; directory is `.my-app-harness`.
11
+ */
12
+ const HARNESS_NAME_CORE_RE = /^[a-z][a-z0-9-]{1,39}$/;
13
+ /** Ensure harness name starts with `.` (e.g. `my-app` → `.my-app`). */
14
+ function normalizeHarnessName(raw) {
15
+ const trimmed = raw.trim();
16
+ if (!trimmed)
17
+ return trimmed;
18
+ return trimmed.startsWith('.') ? trimmed : `.${trimmed}`;
19
+ }
20
+ /** Core segment without leading dot, for validation messages. */
21
+ function harnessNameCore(raw) {
22
+ const trimmed = raw.trim();
23
+ return trimmed.startsWith('.') ? trimmed.slice(1) : trimmed;
24
+ }
25
+ function validateHarnessNameInput(raw) {
26
+ if (!raw?.trim()) {
27
+ return { ok: false, message: '--harness-name 为必填参数' };
28
+ }
29
+ const core = harnessNameCore(raw);
30
+ if (!HARNESS_NAME_CORE_RE.test(core)) {
31
+ return {
32
+ ok: false,
33
+ message: `harness 名称 "${raw.trim()}" 无效:允许小写字母、数字、中划线;` +
34
+ '须以字母开头;核心段长度 2–40(不含可选的前导 `.`)。',
35
+ };
36
+ }
37
+ return { ok: true, normalized: normalizeHarnessName(raw) };
38
+ }
39
+ function harnessDirNameFromName(harnessName) {
40
+ return `${normalizeHarnessName(harnessName)}-harness`;
41
+ }