svharness 0.8.0 → 0.11.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.
Files changed (91) hide show
  1. package/README.md +267 -31
  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 +48 -3
  10. package/dist/commands/convert.js +369 -0
  11. package/dist/commands/init.js +125 -8
  12. package/dist/commands/references-apply-skills.js +47 -0
  13. package/dist/commands/wizard.js +441 -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 +92 -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/reference-apply-skills.js +35 -0
  29. package/dist/core/render-meta.js +2 -1
  30. package/dist/core/repomix-pack.js +3 -3
  31. package/dist/core/state.js +44 -24
  32. package/dist/index.js +207 -132
  33. package/dist/utils/validate-args.js +100 -0
  34. package/dist/wiki/wikiTasksWriter.js +5 -6
  35. package/package.json +2 -1
  36. package/templates/_shared/apply-skills/harness-apply-skills-main.md +45 -14
  37. package/templates/_shared/build-rules/harness-build-rule-agent-agnostic.md +5 -5
  38. package/templates/_shared/build-rules/harness-build-rule-chinese-only.md +5 -5
  39. package/templates/_shared/build-rules/harness-build-rule-convert-check.md +46 -0
  40. package/templates/_shared/build-rules/harness-build-rule-memory-write.md +1 -1
  41. package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +36 -10
  42. package/templates/_shared/build-rules/harness-build-rule-skills-tasks-output.md +3 -2
  43. package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +3 -3
  44. package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +36 -16
  45. package/templates/_shared/build-skills/harness-build-skill-agent-env-merge.md +75 -0
  46. package/templates/_shared/build-skills/harness-build-skill-knowledge-builder.md +49 -85
  47. package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +35 -18
  48. package/templates/_shared/build-skills/harness-build-skill-references-intake.md +91 -0
  49. package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +19 -9
  50. package/templates/_shared/build-skills/harness-build-skill-wiki-writer.md +24 -24
  51. package/templates/_shared/build-skills/harness-build-skills-main.md +83 -0
  52. package/templates/_shared/meta/AGENTS_APPLY.md.ejs +139 -0
  53. package/templates/_shared/meta/{AGENTS.md.ejs → AGENTS_BUILD.md.ejs} +7 -5
  54. package/templates/_shared/meta/CHANGELOG.md.ejs +3 -3
  55. package/templates/_shared/meta/README.md.ejs +11 -9
  56. package/templates/_shared/meta/harness.yaml.ejs +28 -7
  57. package/templates/_shared/skeleton/baseline/code/.gitkeep +1 -0
  58. package/templates/_shared/skeleton/baseline/wiki/.gitkeep +1 -0
  59. package/templates/_shared/skeleton/references/apply-skills-registry.example.yaml +11 -0
  60. package/templates/_shared/skeleton/references/md/.gitkeep +1 -0
  61. package/templates/_shared/skeleton/references/raw/.gitkeep +1 -0
  62. package/templates/_shared/skeleton/references/yaml/.gitkeep +1 -0
  63. package/templates/_shared/skeleton/requirements/md/.gitkeep +1 -0
  64. package/templates/_shared/skeleton/requirements/raw/.gitkeep +1 -0
  65. package/templates/_shared/skeleton/requirements/yaml/.gitkeep +1 -0
  66. package/templates/android-xml/skeleton/agent-env/skills/harness-android-cli/SKILL.md +88 -0
  67. package/templates/android-xml/skeleton/agent-env/skills/harness-android-service-patterns/SKILL.md +205 -0
  68. package/templates/android-xml/skeleton/agent-env/skills/harness-android-xml-architecture/SKILL.md +138 -0
  69. package/templates/android-xml/skeleton/agent-env/skills/harness-lifecycle-management/SKILL.md +158 -0
  70. package/templates/android-xml/skeleton/agent-env/skills/harness-xml-ui/SKILL.md +112 -0
  71. package/templates/cpp/skeleton/agent-env/skills/harness-cmake-build/SKILL.md +163 -0
  72. package/templates/cpp/skeleton/agent-env/skills/harness-cpp-architecture/SKILL.md +157 -0
  73. package/templates/cpp/skeleton/agent-env/skills/harness-cpp-concurrency/SKILL.md +180 -0
  74. package/templates/cpp/skeleton/agent-env/skills/harness-memory-safety/SKILL.md +163 -0
  75. package/templates/cpp/skeleton/agent-env/skills/harness-modern-cpp/SKILL.md +149 -0
  76. package/templates/python/skeleton/agent-env/skills/harness-async-patterns/SKILL.md +162 -0
  77. package/templates/python/skeleton/agent-env/skills/harness-python-architecture/SKILL.md +160 -0
  78. package/templates/python/skeleton/agent-env/skills/harness-python-package-structure/SKILL.md +210 -0
  79. package/templates/python/skeleton/agent-env/skills/harness-python-performance/SKILL.md +207 -0
  80. package/templates/python/skeleton/agent-env/skills/harness-python-testing/SKILL.md +198 -0
  81. package/templates/svharness.config.example.yaml +39 -0
  82. package/templates/web-react/skeleton/agent-env/skills/harness-react-architecture/SKILL.md +177 -0
  83. package/templates/web-react/skeleton/agent-env/skills/harness-react-performance/SKILL.md +177 -0
  84. package/templates/web-react/skeleton/agent-env/skills/harness-react-testing/SKILL.md +193 -0
  85. package/templates/web-react/skeleton/agent-env/skills/harness-react-ui-patterns/SKILL.md +257 -0
  86. package/templates/web-react/skeleton/agent-env/skills/harness-state-management/SKILL.md +189 -0
  87. package/templates/_shared/skeleton/assets/baseline/code/.gitkeep +0 -1
  88. package/templates/_shared/skeleton/assets/baseline/wiki/.gitkeep +0 -1
  89. package/templates/_shared/skeleton/assets/raw/.gitkeep +0 -1
  90. package/templates/_shared/skeleton/assets/requirements/.gitkeep +0 -1
  91. /package/templates/_shared/skeleton/{assets/baseline/repomix → agent-env/_incoming/skills}/.gitkeep +0 -0
@@ -0,0 +1,369 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runConvert = runConvert;
7
+ /**
8
+ * `svharness convert`
9
+ *
10
+ * Upload local source documents (pdf / docx / pptx / xlsx / html / txt / ...)
11
+ * to a remotely-deployed `markitdown_serve` instance and persist the returned
12
+ * Markdown under `<harness>/requirements/md/`.
13
+ *
14
+ * Design rules (aligned with the approved plan):
15
+ * - The CLI is a pure HTTP client. It never spawns / installs / manages the
16
+ * Python-side service.
17
+ * - Independent subcommand — does NOT run as part of `build`.
18
+ * - Single-file failures do not abort the whole batch; the summary prints
19
+ * success / skipped / failed counts at the end.
20
+ * - Existing output files are kept by default and receive a `-N` suffix on
21
+ * name collision; pass `--force` to overwrite instead.
22
+ */
23
+ const fs_extra_1 = __importDefault(require("fs-extra"));
24
+ const node_path_1 = __importDefault(require("node:path"));
25
+ const prompts_1 = __importDefault(require("prompts"));
26
+ const logger_1 = require("../utils/logger");
27
+ const validate_args_1 = require("../utils/validate-args");
28
+ const markitdown_client_1 = require("../core/markitdown-client");
29
+ const doc_intake_paths_1 = require("../core/doc-intake-paths");
30
+ /**
31
+ * Extensions recognised by Microsoft MarkItDown (v0.x). Anything outside this
32
+ * whitelist is skipped client-side to avoid a round-trip that is guaranteed to
33
+ * return 415. Keep in sync with the server-side whitelist in
34
+ * `markitdown_serve/app/main.py`.
35
+ */
36
+ const SUPPORTED_EXTS = new Set([
37
+ '.pdf',
38
+ '.docx',
39
+ '.doc',
40
+ '.pptx',
41
+ '.ppt',
42
+ '.xlsx',
43
+ '.xls',
44
+ '.html',
45
+ '.htm',
46
+ '.epub',
47
+ '.csv',
48
+ '.json',
49
+ '.xml',
50
+ '.txt',
51
+ '.md',
52
+ '.rtf',
53
+ '.msg',
54
+ '.eml',
55
+ '.zip',
56
+ '.jpg',
57
+ '.jpeg',
58
+ '.png',
59
+ '.bmp',
60
+ '.tiff',
61
+ '.mp3',
62
+ '.wav',
63
+ '.m4a',
64
+ ]);
65
+ async function runConvert(opts) {
66
+ (0, logger_1.setVerbose)(!!opts.verbose);
67
+ const v = (0, validate_args_1.validateConvertArgs)({
68
+ input: opts.input,
69
+ harness: opts.harness,
70
+ output: opts.output,
71
+ endpoint: opts.endpoint,
72
+ concurrency: opts.concurrency,
73
+ maxFileMB: opts.maxFileMB,
74
+ timeoutSec: opts.timeoutSec,
75
+ type: opts.type,
76
+ });
77
+ const cwd = opts.cwd ?? process.cwd();
78
+ const outDir = node_path_1.default.join(v.harness, v.type, 'md');
79
+ await fs_extra_1.default.ensureDir(outDir);
80
+ logger_1.logger.configBox('svharness convert', [
81
+ { label: 'harness ', value: v.harness },
82
+ { label: 'endpoint ', value: v.endpoint },
83
+ { label: 'type ', value: v.type },
84
+ { label: 'output dir ', value: outDir },
85
+ { label: 'concurrency', value: String(v.concurrency) },
86
+ { label: 'max file ', value: `${v.maxFileMB} MB` },
87
+ { label: 'timeout ', value: `${v.timeoutSec}s` },
88
+ { label: 'force ', value: opts.force ? 'yes' : 'no' },
89
+ ]);
90
+ // 1. Collect candidate files.
91
+ const candidates = await collectFiles(v.input, cwd);
92
+ if (candidates.length === 0) {
93
+ logger_1.logger.warn('no files matched the given --input patterns');
94
+ return;
95
+ }
96
+ logger_1.logger.info(`discovered ${candidates.length} candidate file(s)`);
97
+ // 2. Partition into convertible vs skipped (size / ext).
98
+ const limitBytes = v.maxFileMB * 1024 * 1024;
99
+ const plan = await buildPlan(candidates, limitBytes);
100
+ const skipped = plan.filter((p) => p.status === 'skipped');
101
+ const toUpload = plan.filter((p) => p.status !== 'skipped');
102
+ for (const s of skipped) {
103
+ logger_1.logger.debug(`skip ${s.source}: ${s.reason}`);
104
+ }
105
+ if (skipped.length > 0) {
106
+ logger_1.logger.warn(`skipping ${skipped.length} file(s) locally (unsupported ext or oversized)`);
107
+ }
108
+ if (toUpload.length === 0) {
109
+ logger_1.logger.warn('nothing to upload after local filtering');
110
+ printSummary(skipped.map(planToResult));
111
+ return;
112
+ }
113
+ // 3. Interactive confirmation (skipped with -y).
114
+ if (!opts.yes) {
115
+ const ans = await (0, prompts_1.default)({
116
+ type: 'confirm',
117
+ name: 'go',
118
+ message: `Upload ${toUpload.length} file(s) to ${v.endpoint}?`,
119
+ initial: true,
120
+ });
121
+ if (!ans.go) {
122
+ logger_1.logger.info('aborted by user');
123
+ return;
124
+ }
125
+ }
126
+ // 4. Probe health endpoint before any upload.
127
+ try {
128
+ const h = await (0, markitdown_client_1.ping)(v.endpoint);
129
+ logger_1.logger.debug(`healthz ok: status=${h.status} markitdown=${h.markitdown_version ?? 'n/a'}`);
130
+ }
131
+ catch (err) {
132
+ const e = err;
133
+ logger_1.logger.error(`markitdown_serve unreachable at ${v.endpoint}: ${e.message}`);
134
+ logger_1.logger.plain('');
135
+ logger_1.logger.plain(' - confirm the service is deployed and reachable from this machine');
136
+ logger_1.logger.plain(' - pass --endpoint <url> or set env SVHARNESS_MARKITDOWN_ENDPOINT');
137
+ logger_1.logger.plain(' - check the server code under svharnessbuild/markitdown_serve/');
138
+ process.exitCode = 1;
139
+ return;
140
+ }
141
+ // 5. Bounded-concurrency upload.
142
+ const results = skipped.map(planToResult);
143
+ const used = new Set();
144
+ const queue = toUpload.slice();
145
+ const workers = [];
146
+ const runOne = async () => {
147
+ while (queue.length > 0) {
148
+ const item = queue.shift();
149
+ if (!item)
150
+ break;
151
+ results.push(await uploadOne(item.source, outDir, used, v, opts));
152
+ }
153
+ };
154
+ for (let i = 0; i < Math.min(v.concurrency, toUpload.length); i++) {
155
+ workers.push(runOne());
156
+ }
157
+ await Promise.all(workers);
158
+ // 6. Summary.
159
+ printSummary(results);
160
+ const relocated = await (0, doc_intake_paths_1.relocateMisplacedConvertedMd)(v.harness, v.type);
161
+ if (relocated > 0) {
162
+ logger_1.logger.warn(`已将 ${relocated} 个误落在 raw/**/converted_md/ 下的 .md 移至 ${v.type}/md/`);
163
+ }
164
+ const failed = results.filter((r) => r.status === 'failed');
165
+ if (failed.length > 0) {
166
+ process.exitCode = 1;
167
+ }
168
+ }
169
+ /** Walk glob patterns / explicit paths / directories into an absolute file list. */
170
+ async function collectFiles(patterns, cwd) {
171
+ const out = new Set();
172
+ for (const p of patterns) {
173
+ if (isGlob(p)) {
174
+ const matched = await globExpand(p, cwd);
175
+ for (const m of matched)
176
+ out.add(m);
177
+ continue;
178
+ }
179
+ const abs = node_path_1.default.isAbsolute(p) ? p : node_path_1.default.resolve(cwd, p);
180
+ if (!(await fs_extra_1.default.pathExists(abs))) {
181
+ logger_1.logger.warn(`input not found, skipped: ${p}`);
182
+ continue;
183
+ }
184
+ const st = await fs_extra_1.default.stat(abs);
185
+ if (st.isDirectory()) {
186
+ for (const f of await walkDir(abs))
187
+ out.add(f);
188
+ }
189
+ else if (st.isFile()) {
190
+ out.add(abs);
191
+ }
192
+ }
193
+ return [...out].sort();
194
+ }
195
+ function isGlob(p) {
196
+ return /[*?[\]{}]/.test(p);
197
+ }
198
+ /**
199
+ * Minimal glob expander to avoid pulling in a dependency. Supports the
200
+ * common forms users actually type: `./docs/*.pdf`, `docs/**\/*.docx`,
201
+ * `a/{x,y}.md`. Falls back to treating the pattern as a literal path if no
202
+ * metachars remain after expansion.
203
+ *
204
+ * Implementation: split pattern into base directory (up to first metachar)
205
+ * and the glob tail, then walk the base directory and regex-match each file.
206
+ */
207
+ async function globExpand(pattern, cwd) {
208
+ // Expand simple brace groups `{a,b,c}` into multiple patterns first.
209
+ const expanded = expandBraces(pattern);
210
+ const results = new Set();
211
+ for (const pat of expanded) {
212
+ for (const f of await globOne(pat, cwd))
213
+ results.add(f);
214
+ }
215
+ return [...results];
216
+ }
217
+ function expandBraces(pattern) {
218
+ const m = /\{([^{}]+)\}/.exec(pattern);
219
+ if (!m)
220
+ return [pattern];
221
+ const [whole, inner] = m;
222
+ const options = inner.split(',');
223
+ const head = pattern.slice(0, m.index);
224
+ const tail = pattern.slice(m.index + whole.length);
225
+ return options.flatMap((opt) => expandBraces(head + opt + tail));
226
+ }
227
+ async function globOne(pattern, cwd) {
228
+ const norm = pattern.replace(/\\/g, '/');
229
+ const firstMeta = norm.search(/[*?[]/);
230
+ if (firstMeta === -1) {
231
+ const abs = node_path_1.default.isAbsolute(norm) ? norm : node_path_1.default.resolve(cwd, norm);
232
+ return (await fs_extra_1.default.pathExists(abs)) ? [abs] : [];
233
+ }
234
+ const lastSlashBeforeMeta = norm.lastIndexOf('/', firstMeta);
235
+ const basePart = lastSlashBeforeMeta === -1 ? '.' : norm.slice(0, lastSlashBeforeMeta) || '/';
236
+ const tail = lastSlashBeforeMeta === -1 ? norm : norm.slice(lastSlashBeforeMeta + 1);
237
+ const baseAbs = node_path_1.default.isAbsolute(basePart)
238
+ ? basePart
239
+ : node_path_1.default.resolve(cwd, basePart);
240
+ if (!(await fs_extra_1.default.pathExists(baseAbs)))
241
+ return [];
242
+ const re = globToRegex(tail);
243
+ const all = await walkDir(baseAbs);
244
+ return all.filter((f) => re.test(node_path_1.default.relative(baseAbs, f).replace(/\\/g, '/')));
245
+ }
246
+ function globToRegex(glob) {
247
+ let re = '^';
248
+ for (let i = 0; i < glob.length; i++) {
249
+ const c = glob[i];
250
+ if (c === '*') {
251
+ if (glob[i + 1] === '*') {
252
+ re += '.*';
253
+ i++;
254
+ if (glob[i + 1] === '/')
255
+ i++;
256
+ }
257
+ else {
258
+ re += '[^/]*';
259
+ }
260
+ }
261
+ else if (c === '?') {
262
+ re += '[^/]';
263
+ }
264
+ else if ('.+^$()|\\'.includes(c)) {
265
+ re += '\\' + c;
266
+ }
267
+ else {
268
+ re += c;
269
+ }
270
+ }
271
+ re += '$';
272
+ return new RegExp(re);
273
+ }
274
+ async function walkDir(dir) {
275
+ const out = [];
276
+ const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
277
+ for (const e of entries) {
278
+ const abs = node_path_1.default.join(dir, e.name);
279
+ if (e.isDirectory()) {
280
+ // Skip harness artifact dirs and noisy dot-dirs.
281
+ if ((0, doc_intake_paths_1.shouldSkipRawSubdir)(e.name)) {
282
+ continue;
283
+ }
284
+ out.push(...(await walkDir(abs)));
285
+ }
286
+ else if (e.isFile()) {
287
+ out.push(abs);
288
+ }
289
+ }
290
+ return out;
291
+ }
292
+ function planToResult(p) {
293
+ return p.status === 'skipped'
294
+ ? { source: p.source, status: 'skipped', reason: p.reason }
295
+ : { source: p.source, status: 'ok' };
296
+ }
297
+ async function buildPlan(files, limitBytes) {
298
+ const out = [];
299
+ for (const f of files) {
300
+ const ext = node_path_1.default.extname(f).toLowerCase();
301
+ if (!SUPPORTED_EXTS.has(ext)) {
302
+ out.push({ source: f, status: 'skipped', reason: `unsupported ext ${ext || '(none)'}` });
303
+ continue;
304
+ }
305
+ const st = await fs_extra_1.default.stat(f);
306
+ if (st.size > limitBytes) {
307
+ out.push({
308
+ source: f,
309
+ status: 'skipped',
310
+ reason: `exceeds max-file-mb (${Math.round(st.size / 1024 / 1024)}MB)`,
311
+ });
312
+ continue;
313
+ }
314
+ out.push({ source: f, status: 'ready' });
315
+ }
316
+ return out;
317
+ }
318
+ async function uploadOne(source, outDir, used, v, opts) {
319
+ const basename = node_path_1.default.basename(source, node_path_1.default.extname(source));
320
+ const outPath = await resolveOutputPath(outDir, basename, used, !!opts.force);
321
+ used.add(outPath);
322
+ logger_1.logger.debug(`upload ${source} -> ${outPath}`);
323
+ try {
324
+ const resp = await (0, markitdown_client_1.postConvertWithRetry)(v.endpoint, source, v.timeoutSec * 1000);
325
+ await fs_extra_1.default.writeFile(outPath, resp.markdown, 'utf8');
326
+ const bytes = Buffer.byteLength(resp.markdown, 'utf8');
327
+ logger_1.logger.success(`${node_path_1.default.basename(source)} -> ${node_path_1.default.basename(outPath)} (${bytes}B)`);
328
+ return { source, output: outPath, status: 'ok', bytes };
329
+ }
330
+ catch (err) {
331
+ const e = err;
332
+ const reason = e.message;
333
+ logger_1.logger.error(`failed: ${node_path_1.default.basename(source)} — ${reason}`);
334
+ return { source, status: 'failed', reason };
335
+ }
336
+ }
337
+ /**
338
+ * Decide the final .md path. With `--force` we always write to
339
+ * `<basename>.md`; otherwise we append `-1`, `-2`, ... until a free slot is
340
+ * found (also respecting files reserved by the current run via `used`).
341
+ */
342
+ async function resolveOutputPath(outDir, basename, used, force) {
343
+ const primary = node_path_1.default.join(outDir, `${basename}.md`);
344
+ if (force)
345
+ return primary;
346
+ if (!used.has(primary) && !(await fs_extra_1.default.pathExists(primary)))
347
+ return primary;
348
+ for (let i = 1; i < 1000; i++) {
349
+ const cand = node_path_1.default.join(outDir, `${basename}-${i}.md`);
350
+ if (!used.has(cand) && !(await fs_extra_1.default.pathExists(cand)))
351
+ return cand;
352
+ }
353
+ throw new Error(`unable to allocate unique output filename for ${basename} under ${outDir}`);
354
+ }
355
+ function printSummary(results) {
356
+ const ok = results.filter((r) => r.status === 'ok').length;
357
+ const failed = results.filter((r) => r.status === 'failed').length;
358
+ const skipped = results.filter((r) => r.status === 'skipped').length;
359
+ logger_1.logger.section('convert summary');
360
+ logger_1.logger.plain(` ok : ${ok}`);
361
+ logger_1.logger.plain(` skipped : ${skipped}`);
362
+ logger_1.logger.plain(` failed : ${failed}`);
363
+ if (failed > 0) {
364
+ logger_1.logger.plain('');
365
+ for (const r of results.filter((x) => x.status === 'failed')) {
366
+ logger_1.logger.plain(` - ${node_path_1.default.basename(r.source)}: ${r.reason}`);
367
+ }
368
+ }
369
+ }
@@ -16,8 +16,12 @@ const state_1 = require("../core/state");
16
16
  const agent_injector_1 = require("../core/agent-injector");
17
17
  const adapters_1 = require("../adapters");
18
18
  const next_steps_1 = require("../core/next-steps");
19
+ const build_project_entry_1 = require("../core/build-project-entry");
19
20
  const repomix_pack_1 = require("../core/repomix-pack");
21
+ const extra_assets_intake_1 = require("../core/extra-assets-intake");
20
22
  const baseline_copy_1 = require("../utils/baseline-copy");
23
+ const doc_intake_paths_1 = require("../core/doc-intake-paths");
24
+ const convert_1 = require("./convert");
21
25
  const wiki_1 = require("../wiki");
22
26
  /**
23
27
  * Resolve the absolute path to the bundled templates/ directory.
@@ -26,6 +30,42 @@ const wiki_1 = require("../wiki");
26
30
  function resolveTemplatesRoot() {
27
31
  return node_path_1.default.resolve(__dirname, '..', '..', 'templates');
28
32
  }
33
+ async function runBuildAutoConvert(targetRoot, rawDir, type, opts) {
34
+ logger_1.logger.info(`开始自动转换 ${type}:${rawDir} -> ${node_path_1.default.join(targetRoot, type, 'md')}`);
35
+ const prevExitCode = process.exitCode;
36
+ process.exitCode = undefined;
37
+ try {
38
+ await (0, convert_1.runConvert)({
39
+ input: [rawDir],
40
+ output: targetRoot,
41
+ type,
42
+ endpoint: opts.convertEndpoint,
43
+ concurrency: opts.convertConcurrency,
44
+ maxFileMB: opts.convertMaxFileMB,
45
+ timeoutSec: opts.convertTimeoutSec,
46
+ force: opts.convertForce,
47
+ yes: true,
48
+ verbose: opts.verbose,
49
+ cwd: opts.cwd,
50
+ });
51
+ const relocated = await (0, doc_intake_paths_1.relocateMisplacedConvertedMd)(targetRoot, type);
52
+ if (relocated > 0) {
53
+ logger_1.logger.warn(`已将 ${relocated} 个误落在 raw/**/converted_md/ 下的 .md 移至 ${type}/md/`);
54
+ }
55
+ if ((process.exitCode ?? 0) !== 0) {
56
+ logger_1.logger.warn(`自动转换 ${type} 存在失败项,已继续 build 流程`);
57
+ }
58
+ else {
59
+ logger_1.logger.success(`自动转换 ${type} 完成`);
60
+ }
61
+ }
62
+ catch (err) {
63
+ logger_1.logger.warn(`自动转换 ${type} 失败,已继续 build:${err.message}`);
64
+ }
65
+ finally {
66
+ process.exitCode = prevExitCode;
67
+ }
68
+ }
29
69
  async function runInit(opts) {
30
70
  (0, logger_1.setVerbose)(!!opts.verbose);
31
71
  // 0. Mutual-exclusion between wiki modes.
@@ -49,11 +89,11 @@ async function runInit(opts) {
49
89
  const targetRoot = node_path_1.default.resolve(cwd, `${validated.name}-harness`);
50
90
  const cliVersion = (0, version_1.getCliVersion)();
51
91
  const createdAt = new Date().toISOString();
52
- // Baseline code lands at <target>/assets/baseline/code/ for both git & local
92
+ // Baseline code lands at <target>/baseline/code/ for both git & local
53
93
  // modes. Wiki generation consumes this directory by default so it always
54
94
  // reflects the baseline the user asked for (not the caller's cwd).
55
95
  const hasBaseline = !!validated.baseline;
56
- const baselineCodeDir = node_path_1.default.join(targetRoot, 'assets', 'baseline', 'code');
96
+ const baselineCodeDir = node_path_1.default.join(targetRoot, 'baseline', 'code');
57
97
  const wikiSourceRoot = node_path_1.default.resolve(opts.wikiSource ?? (hasBaseline ? baselineCodeDir : cwd));
58
98
  // Derive the final wiki mode:
59
99
  // - no baseline -> 'off' (wiki flags are warned and ignored)
@@ -79,6 +119,24 @@ async function runInit(opts) {
79
119
  { label: 'Agent', value: validated.agent },
80
120
  { label: '目标路径', value: targetRoot },
81
121
  ];
122
+ if (opts.requirements) {
123
+ configItems.push({
124
+ label: 'Requirements 路径',
125
+ value: node_path_1.default.resolve(cwd, opts.requirements),
126
+ });
127
+ }
128
+ if (opts.references) {
129
+ configItems.push({
130
+ label: 'References 路径',
131
+ value: node_path_1.default.resolve(cwd, opts.references),
132
+ });
133
+ }
134
+ if ((opts.extraSkills?.length ?? 0) > 0) {
135
+ configItems.push({
136
+ label: 'Extra 资源',
137
+ value: opts.extraSkills.map((item) => node_path_1.default.resolve(cwd, item)).join(', '),
138
+ });
139
+ }
82
140
  if (validated.baseline) {
83
141
  const modeTag = validated.baselineMode === 'git' ? 'git' : 'local';
84
142
  configItems.push({ label: `基线 (${modeTag})`, value: validated.baseline });
@@ -166,6 +224,8 @@ async function runInit(opts) {
166
224
  arch: validated.arch,
167
225
  agent: validated.agent,
168
226
  sourcePath: validated.baseline ?? '',
227
+ requirementsPath: opts.requirements ? node_path_1.default.resolve(cwd, opts.requirements) : '',
228
+ referencesPath: opts.references ? node_path_1.default.resolve(cwd, opts.references) : '',
169
229
  createdAt,
170
230
  version: cliVersion,
171
231
  };
@@ -175,16 +235,35 @@ async function runInit(opts) {
175
235
  // optional: baseline copy, repomix pack, wiki generation
176
236
  // last: state file
177
237
  let stepCursor = 4;
238
+ const hasExtraAssets = (opts.extraSkills?.length ?? 0) > 0;
239
+ const stepExtraAssets = hasExtraAssets ? ++stepCursor : 0;
178
240
  const stepBaseline = hasBaseline ? ++stepCursor : 0;
179
241
  const stepRepomix = hasBaseline ? ++stepCursor : 0;
180
242
  const stepWiki = wikiEnabled ? ++stepCursor : 0;
181
243
  const stepState = ++stepCursor;
182
244
  const totalSteps = stepCursor;
183
245
  let baselineMeta;
246
+ let extraAssetsResult;
184
247
  try {
185
248
  logger_1.logger.section(`步骤 1/${totalSteps} - 生成目录骨架`);
186
249
  const copied = await (0, scaffold_1.scaffoldLayered)(skeletonSrcs, targetRoot);
187
250
  logger_1.logger.success(`已拷贝骨架文件 ${copied.length} 个`);
251
+ if (opts.requirements) {
252
+ const requirementRawDir = node_path_1.default.join(targetRoot, 'requirements', 'raw');
253
+ const copiedReq = await (0, doc_intake_paths_1.copySeedInputToRaw)(node_path_1.default.resolve(cwd, opts.requirements), requirementRawDir, 'requirements');
254
+ logger_1.logger.success(`已注入 requirements 输入到 raw:${copiedReq} 项`);
255
+ if (copiedReq > 0) {
256
+ await runBuildAutoConvert(targetRoot, requirementRawDir, 'requirements', opts);
257
+ }
258
+ }
259
+ if (opts.references) {
260
+ const referenceRawDir = node_path_1.default.join(targetRoot, 'references', 'raw');
261
+ const copiedRef = await (0, doc_intake_paths_1.copySeedInputToRaw)(node_path_1.default.resolve(cwd, opts.references), referenceRawDir, 'references');
262
+ logger_1.logger.success(`已注入 references 输入到 raw:${copiedRef} 项`);
263
+ if (copiedRef > 0) {
264
+ await runBuildAutoConvert(targetRoot, referenceRawDir, 'references', opts);
265
+ }
266
+ }
188
267
  logger_1.logger.section(`步骤 2/${totalSteps} - 渲染元文件`);
189
268
  const meta = await (0, render_meta_1.renderMetaLayered)(metaSrcs, targetRoot, ctx);
190
269
  logger_1.logger.success(`已渲染元文件 ${meta.length} 个: ${meta.join(', ')}`);
@@ -193,16 +272,34 @@ async function runInit(opts) {
193
272
  // Skills are injected beside the harness folder (cwd), not inside it,
194
273
  // so the Agent picks them up from the project root.
195
274
  const harnessDirName = node_path_1.default.basename(targetRoot);
196
- const injected = await (0, agent_injector_1.injectSkillsLayered)(buildSkillsSrcs, targetRoot, adapter, cwd, harnessDirName);
275
+ const buildSkillsBinding = {
276
+ harnessDirName,
277
+ harnessName: validated.name,
278
+ arch: validated.arch,
279
+ agent: validated.agent,
280
+ cliVersion,
281
+ generatedAt: createdAt,
282
+ };
283
+ const injected = await (0, agent_injector_1.injectSkillsLayered)(buildSkillsSrcs, targetRoot, adapter, cwd, buildSkillsBinding);
197
284
  logger_1.logger.success(`已向 ${node_path_1.default.join(cwd, adapter.skillsDir)}/ 注入 skill ${injected.length} 个`);
198
285
  logger_1.logger.section(`步骤 4/${totalSteps} - 注入 harness 构建规则`);
199
286
  const rules = await (0, agent_injector_1.injectRulesLayered)(buildRulesSrcs, adapter, cwd);
200
287
  if (adapter.rulesDir) {
201
288
  logger_1.logger.success(`已向 ${node_path_1.default.join(cwd, adapter.rulesDir)}/ 注入 rule ${rules.length} 个`);
202
289
  }
290
+ if (hasExtraAssets && opts.extraSkills) {
291
+ logger_1.logger.section(`步骤 ${stepExtraAssets}/${totalSteps} - 导入额外运行期资源到 _incoming`);
292
+ extraAssetsResult = await (0, extra_assets_intake_1.intakeExtraAssets)({
293
+ inputs: opts.extraSkills,
294
+ cwd,
295
+ harnessRoot: targetRoot,
296
+ });
297
+ logger_1.logger.success(`incoming 清单: ${extraAssetsResult.manifestRelPath}(总计 ${extraAssetsResult.incomingTotal},` +
298
+ `skills ${extraAssetsResult.skillsCount},rules ${extraAssetsResult.rulesCount},unknown ${extraAssetsResult.unknownCount})`);
299
+ }
203
300
  if (hasBaseline && validated.baseline && validated.baselineMode) {
204
301
  logger_1.logger.section(`步骤 ${stepBaseline}/${totalSteps} - 拷贝基线源码`);
205
- const destDir = node_path_1.default.join(targetRoot, 'assets', 'baseline', 'code');
302
+ const destDir = node_path_1.default.join(targetRoot, 'baseline', 'code');
206
303
  baselineMeta = await (0, baseline_copy_1.copyBaseline)({
207
304
  mode: validated.baselineMode,
208
305
  source: validated.baseline,
@@ -222,6 +319,18 @@ async function runInit(opts) {
222
319
  await (0, scaffold_1.rollbackTarget)(targetRoot);
223
320
  throw err;
224
321
  }
322
+ {
323
+ const adapterEntry = (0, adapters_1.getAdapter)(validated.agent);
324
+ await (0, build_project_entry_1.writeBuildProjectEntry)({
325
+ projectRoot: cwd,
326
+ adapter: adapterEntry,
327
+ harnessDirName: node_path_1.default.basename(targetRoot),
328
+ harnessName: validated.name,
329
+ arch: validated.arch,
330
+ agent: validated.agent,
331
+ force: !!opts.force,
332
+ });
333
+ }
225
334
  // 6a. Repomix XML pack of baseline code (default when --baseline; non-fatal on failure).
226
335
  if (hasBaseline) {
227
336
  logger_1.logger.section(`步骤 ${stepRepomix}/${totalSteps} - 生成 baseline Repomix 包`);
@@ -258,7 +367,7 @@ async function runInit(opts) {
258
367
  try {
259
368
  const repoRootFs = wikiSourceRoot;
260
369
  const wikiOutputRootFs = targetRoot;
261
- const wikiRelPath = node_path_1.default.join('assets', 'baseline', 'wiki');
370
+ const wikiRelPath = node_path_1.default.join('baseline', 'wiki');
262
371
  // Guard: baseline wiki must be based on real code. If the user provided
263
372
  // --baseline but the copy step produced an empty directory (e.g. every
264
373
  // file was filtered out), refuse to silently index cwd or an empty dir.
@@ -311,12 +420,12 @@ async function runInit(opts) {
311
420
  logger_1.logger.warn(`wiki 生成失败(不回滚 harness 骨架):${err.message}`);
312
421
  }
313
422
  }
314
- // If the full run failed, downgrade P1_wiki from DONE to PENDING
423
+ // If the full run failed, downgrade S10_wiki from DONE to PENDING
315
424
  // so the agent can pick it up via harness-build-skill-wiki-writer.
316
425
  if (!wikiFullOk) {
317
426
  resolvedWikiPhase = 'pending';
318
427
  resolvedWikiSource = 'cli-full-degraded';
319
- logger_1.logger.warn('P1_wiki 将标记为 PENDING,等待 agent 接力补齐');
428
+ logger_1.logger.warn('S10_wiki 将标记为 PENDING,等待 agent 接力补齐');
320
429
  }
321
430
  }
322
431
  else if (wikiMode === 'tasks') {
@@ -324,7 +433,7 @@ async function runInit(opts) {
324
433
  try {
325
434
  const repoRootFs = wikiSourceRoot;
326
435
  const wikiOutputRootFs = targetRoot;
327
- const wikiRelPath = node_path_1.default.join('assets', 'baseline', 'wiki');
436
+ const wikiRelPath = node_path_1.default.join('baseline', 'wiki');
328
437
  if (hasBaseline) {
329
438
  const exists = await fs_extra_1.default.pathExists(repoRootFs);
330
439
  const entries = exists ? await fs_extra_1.default.readdir(repoRootFs) : [];
@@ -400,6 +509,14 @@ async function runInit(opts) {
400
509
  wikiPhase: resolvedWikiPhase,
401
510
  wikiSource: resolvedWikiSource,
402
511
  });
512
+ if (extraAssetsResult) {
513
+ const s65 = state.phases.S65_customize_agent_env;
514
+ s65.incoming_manifest = extraAssetsResult.manifestRelPath;
515
+ s65.incoming_total = extraAssetsResult.incomingTotal;
516
+ s65.skills_count = extraAssetsResult.skillsCount;
517
+ s65.rules_count = extraAssetsResult.rulesCount;
518
+ s65.unknown_count = extraAssetsResult.unknownCount;
519
+ }
403
520
  const statePath = await (0, state_1.writeStateFile)(targetRoot, state);
404
521
  logger_1.logger.success(`状态文件: ${node_path_1.default.relative(targetRoot, statePath)}`);
405
522
  }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runReferencesApplySkills = runReferencesApplySkills;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const prompts_1 = __importDefault(require("prompts"));
9
+ const logger_1 = require("../utils/logger");
10
+ const reference_apply_skills_1 = require("../core/reference-apply-skills");
11
+ async function runReferencesApplySkills(opts) {
12
+ (0, logger_1.setVerbose)(!!opts.verbose);
13
+ const cwd = opts.cwd ?? process.cwd();
14
+ const harnessRoot = node_path_1.default.resolve(cwd, opts.harness);
15
+ logger_1.logger.configBox('references → apply skills', [
16
+ { label: 'harness', value: harnessRoot },
17
+ { label: 'force', value: opts.force ? 'yes' : 'no' },
18
+ ]);
19
+ if (!opts.yes) {
20
+ const { ok } = await (0, prompts_1.default)({
21
+ type: 'confirm',
22
+ name: 'ok',
23
+ message: '根据 references/yaml/index.yaml 生成 harness-apply-skills-* 并写入 registry?',
24
+ initial: true,
25
+ });
26
+ if (!ok) {
27
+ logger_1.logger.warn('用户取消');
28
+ return;
29
+ }
30
+ }
31
+ const result = await (0, reference_apply_skills_1.materializeReferenceApplySkills)(harnessRoot, {
32
+ force: opts.force,
33
+ });
34
+ logger_1.logger.success(`registry: ${node_path_1.default.relative(harnessRoot, result.registryPath)}`);
35
+ if (result.created.length > 0) {
36
+ logger_1.logger.success(`新建 skill: ${result.created.join(', ')}`);
37
+ }
38
+ if (result.updated.length > 0) {
39
+ logger_1.logger.success(`更新 skill: ${result.updated.join(', ')}`);
40
+ }
41
+ if (result.skipped.length > 0) {
42
+ logger_1.logger.warn(`跳过(已存在,未使用 --force): ${result.skipped.join(', ')}`);
43
+ }
44
+ logger_1.logger.plain('');
45
+ logger_1.logger.plain('下一步:svharness apply --harness <path> --target <project> --yes');
46
+ logger_1.logger.plain(' apply 会把 registry 中的 skill 注入目标 Agent,并注册到 harness-apply-skills-main');
47
+ }