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
@@ -0,0 +1,371 @@
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 `--output` (standalone) or `<harness>/<type>/md/` (harness mode).
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
+ allowMissingHarnessYaml: opts.allowMissingHarnessYaml,
72
+ endpoint: opts.endpoint,
73
+ concurrency: opts.concurrency,
74
+ maxFileMB: opts.maxFileMB,
75
+ timeoutSec: opts.timeoutSec,
76
+ type: opts.type,
77
+ });
78
+ const cwd = opts.cwd ?? process.cwd();
79
+ const outDir = v.outputDir;
80
+ await fs_extra_1.default.ensureDir(outDir);
81
+ const configRows = [
82
+ { label: 'mode ', value: v.harnessMode ? 'harness' : 'standalone' },
83
+ ];
84
+ if (v.harnessMode && v.harnessRoot) {
85
+ configRows.push({ label: 'harness ', value: v.harnessRoot });
86
+ configRows.push({ label: 'type ', value: v.type });
87
+ }
88
+ configRows.push({ label: 'output dir ', value: outDir }, { label: 'endpoint ', value: v.endpoint }, { label: 'concurrency', value: String(v.concurrency) }, { label: 'max file ', value: `${v.maxFileMB} MB` }, { label: 'timeout ', value: `${v.timeoutSec}s` }, { label: 'force ', value: opts.force ? 'yes' : 'no' });
89
+ logger_1.logger.configBox('svharness convert', configRows);
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
+ if (v.harnessMode && v.harnessRoot) {
161
+ const relocated = await (0, doc_intake_paths_1.relocateMisplacedConvertedMd)(v.harnessRoot, v.type);
162
+ if (relocated > 0) {
163
+ logger_1.logger.warn(`已将 ${relocated} 个误落在 raw/**/converted_md/ 下的 .md 移至 ${v.type}/md/`);
164
+ }
165
+ }
166
+ const failed = results.filter((r) => r.status === 'failed');
167
+ if (failed.length > 0) {
168
+ process.exitCode = 1;
169
+ }
170
+ }
171
+ /** Walk glob patterns / explicit paths / directories into an absolute file list. */
172
+ async function collectFiles(patterns, cwd) {
173
+ const out = new Set();
174
+ for (const p of patterns) {
175
+ if (isGlob(p)) {
176
+ const matched = await globExpand(p, cwd);
177
+ for (const m of matched)
178
+ out.add(m);
179
+ continue;
180
+ }
181
+ const abs = node_path_1.default.isAbsolute(p) ? p : node_path_1.default.resolve(cwd, p);
182
+ if (!(await fs_extra_1.default.pathExists(abs))) {
183
+ logger_1.logger.warn(`input not found, skipped: ${p}`);
184
+ continue;
185
+ }
186
+ const st = await fs_extra_1.default.stat(abs);
187
+ if (st.isDirectory()) {
188
+ for (const f of await walkDir(abs))
189
+ out.add(f);
190
+ }
191
+ else if (st.isFile()) {
192
+ out.add(abs);
193
+ }
194
+ }
195
+ return [...out].sort();
196
+ }
197
+ function isGlob(p) {
198
+ return /[*?[\]{}]/.test(p);
199
+ }
200
+ /**
201
+ * Minimal glob expander to avoid pulling in a dependency. Supports the
202
+ * common forms users actually type: `./docs/*.pdf`, `docs/**\/*.docx`,
203
+ * `a/{x,y}.md`. Falls back to treating the pattern as a literal path if no
204
+ * metachars remain after expansion.
205
+ *
206
+ * Implementation: split pattern into base directory (up to first metachar)
207
+ * and the glob tail, then walk the base directory and regex-match each file.
208
+ */
209
+ async function globExpand(pattern, cwd) {
210
+ // Expand simple brace groups `{a,b,c}` into multiple patterns first.
211
+ const expanded = expandBraces(pattern);
212
+ const results = new Set();
213
+ for (const pat of expanded) {
214
+ for (const f of await globOne(pat, cwd))
215
+ results.add(f);
216
+ }
217
+ return [...results];
218
+ }
219
+ function expandBraces(pattern) {
220
+ const m = /\{([^{}]+)\}/.exec(pattern);
221
+ if (!m)
222
+ return [pattern];
223
+ const [whole, inner] = m;
224
+ const options = inner.split(',');
225
+ const head = pattern.slice(0, m.index);
226
+ const tail = pattern.slice(m.index + whole.length);
227
+ return options.flatMap((opt) => expandBraces(head + opt + tail));
228
+ }
229
+ async function globOne(pattern, cwd) {
230
+ const norm = pattern.replace(/\\/g, '/');
231
+ const firstMeta = norm.search(/[*?[]/);
232
+ if (firstMeta === -1) {
233
+ const abs = node_path_1.default.isAbsolute(norm) ? norm : node_path_1.default.resolve(cwd, norm);
234
+ return (await fs_extra_1.default.pathExists(abs)) ? [abs] : [];
235
+ }
236
+ const lastSlashBeforeMeta = norm.lastIndexOf('/', firstMeta);
237
+ const basePart = lastSlashBeforeMeta === -1 ? '.' : norm.slice(0, lastSlashBeforeMeta) || '/';
238
+ const tail = lastSlashBeforeMeta === -1 ? norm : norm.slice(lastSlashBeforeMeta + 1);
239
+ const baseAbs = node_path_1.default.isAbsolute(basePart)
240
+ ? basePart
241
+ : node_path_1.default.resolve(cwd, basePart);
242
+ if (!(await fs_extra_1.default.pathExists(baseAbs)))
243
+ return [];
244
+ const re = globToRegex(tail);
245
+ const all = await walkDir(baseAbs);
246
+ return all.filter((f) => re.test(node_path_1.default.relative(baseAbs, f).replace(/\\/g, '/')));
247
+ }
248
+ function globToRegex(glob) {
249
+ let re = '^';
250
+ for (let i = 0; i < glob.length; i++) {
251
+ const c = glob[i];
252
+ if (c === '*') {
253
+ if (glob[i + 1] === '*') {
254
+ re += '.*';
255
+ i++;
256
+ if (glob[i + 1] === '/')
257
+ i++;
258
+ }
259
+ else {
260
+ re += '[^/]*';
261
+ }
262
+ }
263
+ else if (c === '?') {
264
+ re += '[^/]';
265
+ }
266
+ else if ('.+^$()|\\'.includes(c)) {
267
+ re += '\\' + c;
268
+ }
269
+ else {
270
+ re += c;
271
+ }
272
+ }
273
+ re += '$';
274
+ return new RegExp(re);
275
+ }
276
+ async function walkDir(dir) {
277
+ const out = [];
278
+ const entries = await fs_extra_1.default.readdir(dir, { withFileTypes: true });
279
+ for (const e of entries) {
280
+ const abs = node_path_1.default.join(dir, e.name);
281
+ if (e.isDirectory()) {
282
+ // Skip harness artifact dirs and noisy dot-dirs.
283
+ if ((0, doc_intake_paths_1.shouldSkipRawSubdir)(e.name)) {
284
+ continue;
285
+ }
286
+ out.push(...(await walkDir(abs)));
287
+ }
288
+ else if (e.isFile()) {
289
+ out.push(abs);
290
+ }
291
+ }
292
+ return out;
293
+ }
294
+ function planToResult(p) {
295
+ return p.status === 'skipped'
296
+ ? { source: p.source, status: 'skipped', reason: p.reason }
297
+ : { source: p.source, status: 'ok' };
298
+ }
299
+ async function buildPlan(files, limitBytes) {
300
+ const out = [];
301
+ for (const f of files) {
302
+ const ext = node_path_1.default.extname(f).toLowerCase();
303
+ if (!SUPPORTED_EXTS.has(ext)) {
304
+ out.push({ source: f, status: 'skipped', reason: `unsupported ext ${ext || '(none)'}` });
305
+ continue;
306
+ }
307
+ const st = await fs_extra_1.default.stat(f);
308
+ if (st.size > limitBytes) {
309
+ out.push({
310
+ source: f,
311
+ status: 'skipped',
312
+ reason: `exceeds max-file-mb (${Math.round(st.size / 1024 / 1024)}MB)`,
313
+ });
314
+ continue;
315
+ }
316
+ out.push({ source: f, status: 'ready' });
317
+ }
318
+ return out;
319
+ }
320
+ async function uploadOne(source, outDir, used, v, opts) {
321
+ const basename = node_path_1.default.basename(source, node_path_1.default.extname(source));
322
+ const outPath = await resolveOutputPath(outDir, basename, used, !!opts.force);
323
+ used.add(outPath);
324
+ logger_1.logger.debug(`upload ${source} -> ${outPath}`);
325
+ try {
326
+ const resp = await (0, markitdown_client_1.postConvertWithRetry)(v.endpoint, source, v.timeoutSec * 1000);
327
+ await fs_extra_1.default.writeFile(outPath, resp.markdown, 'utf8');
328
+ const bytes = Buffer.byteLength(resp.markdown, 'utf8');
329
+ logger_1.logger.success(`${node_path_1.default.basename(source)} -> ${node_path_1.default.basename(outPath)} (${bytes}B)`);
330
+ return { source, output: outPath, status: 'ok', bytes };
331
+ }
332
+ catch (err) {
333
+ const e = err;
334
+ const reason = e.message;
335
+ logger_1.logger.error(`failed: ${node_path_1.default.basename(source)} — ${reason}`);
336
+ return { source, status: 'failed', reason };
337
+ }
338
+ }
339
+ /**
340
+ * Decide the final .md path. With `--force` we always write to
341
+ * `<basename>.md`; otherwise we append `-1`, `-2`, ... until a free slot is
342
+ * found (also respecting files reserved by the current run via `used`).
343
+ */
344
+ async function resolveOutputPath(outDir, basename, used, force) {
345
+ const primary = node_path_1.default.join(outDir, `${basename}.md`);
346
+ if (force)
347
+ return primary;
348
+ if (!used.has(primary) && !(await fs_extra_1.default.pathExists(primary)))
349
+ return primary;
350
+ for (let i = 1; i < 1000; i++) {
351
+ const cand = node_path_1.default.join(outDir, `${basename}-${i}.md`);
352
+ if (!used.has(cand) && !(await fs_extra_1.default.pathExists(cand)))
353
+ return cand;
354
+ }
355
+ throw new Error(`unable to allocate unique output filename for ${basename} under ${outDir}`);
356
+ }
357
+ function printSummary(results) {
358
+ const ok = results.filter((r) => r.status === 'ok').length;
359
+ const failed = results.filter((r) => r.status === 'failed').length;
360
+ const skipped = results.filter((r) => r.status === 'skipped').length;
361
+ logger_1.logger.section('convert summary');
362
+ logger_1.logger.plain(` ok : ${ok}`);
363
+ logger_1.logger.plain(` skipped : ${skipped}`);
364
+ logger_1.logger.plain(` failed : ${failed}`);
365
+ if (failed > 0) {
366
+ logger_1.logger.plain('');
367
+ for (const r of results.filter((x) => x.status === 'failed')) {
368
+ logger_1.logger.plain(` - ${node_path_1.default.basename(r.source)}: ${r.reason}`);
369
+ }
370
+ }
371
+ }
@@ -16,8 +16,14 @@ 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");
20
+ const project_ignore_1 = require("../core/project-ignore");
21
+ const harness_name_1 = require("../utils/harness-name");
19
22
  const repomix_pack_1 = require("../core/repomix-pack");
23
+ const extra_assets_intake_1 = require("../core/extra-assets-intake");
20
24
  const baseline_copy_1 = require("../utils/baseline-copy");
25
+ const doc_intake_paths_1 = require("../core/doc-intake-paths");
26
+ const convert_1 = require("./convert");
21
27
  const wiki_1 = require("../wiki");
22
28
  /**
23
29
  * Resolve the absolute path to the bundled templates/ directory.
@@ -26,6 +32,45 @@ const wiki_1 = require("../wiki");
26
32
  function resolveTemplatesRoot() {
27
33
  return node_path_1.default.resolve(__dirname, '..', '..', 'templates');
28
34
  }
35
+ async function runBuildAutoConvert(targetRoot, rawDir, type, opts) {
36
+ logger_1.logger.info(`开始自动转换 ${type}:${rawDir} -> ${node_path_1.default.join(targetRoot, type, 'md')}`);
37
+ const prevExitCode = process.exitCode;
38
+ process.exitCode = undefined;
39
+ try {
40
+ await (0, convert_1.runConvert)({
41
+ input: [rawDir],
42
+ // Explicit md dir: harness.yaml may not exist yet (convert runs before render-meta).
43
+ output: node_path_1.default.join(targetRoot, type, 'md'),
44
+ harness: targetRoot,
45
+ type,
46
+ endpoint: opts.convertEndpoint,
47
+ concurrency: opts.convertConcurrency,
48
+ maxFileMB: opts.convertMaxFileMB,
49
+ timeoutSec: opts.convertTimeoutSec,
50
+ force: opts.convertForce,
51
+ yes: true,
52
+ verbose: opts.verbose,
53
+ cwd: opts.cwd,
54
+ allowMissingHarnessYaml: true,
55
+ });
56
+ const relocated = await (0, doc_intake_paths_1.relocateMisplacedConvertedMd)(targetRoot, type);
57
+ if (relocated > 0) {
58
+ logger_1.logger.warn(`已将 ${relocated} 个误落在 raw/**/converted_md/ 下的 .md 移至 ${type}/md/`);
59
+ }
60
+ if ((process.exitCode ?? 0) !== 0) {
61
+ logger_1.logger.warn(`自动转换 ${type} 存在失败项,已继续 build 流程`);
62
+ }
63
+ else {
64
+ logger_1.logger.success(`自动转换 ${type} 完成`);
65
+ }
66
+ }
67
+ catch (err) {
68
+ logger_1.logger.warn(`自动转换 ${type} 失败,已继续 build:${err.message}`);
69
+ }
70
+ finally {
71
+ process.exitCode = prevExitCode;
72
+ }
73
+ }
29
74
  async function runInit(opts) {
30
75
  (0, logger_1.setVerbose)(!!opts.verbose);
31
76
  // 0. Mutual-exclusion between wiki modes.
@@ -46,14 +91,15 @@ async function runInit(opts) {
46
91
  logger_1.logger.warn('--baseline-branch 在本地基线模式下被忽略');
47
92
  }
48
93
  const cwd = opts.cwd ?? process.cwd();
49
- const targetRoot = node_path_1.default.resolve(cwd, `${validated.name}-harness`);
94
+ const harnessDirName = (0, harness_name_1.harnessDirNameFromName)(validated.name);
95
+ const targetRoot = node_path_1.default.resolve(cwd, harnessDirName);
50
96
  const cliVersion = (0, version_1.getCliVersion)();
51
97
  const createdAt = new Date().toISOString();
52
- // Baseline code lands at <target>/assets/baseline/code/ for both git & local
98
+ // Baseline code lands at <target>/baseline/code/ for both git & local
53
99
  // modes. Wiki generation consumes this directory by default so it always
54
100
  // reflects the baseline the user asked for (not the caller's cwd).
55
101
  const hasBaseline = !!validated.baseline;
56
- const baselineCodeDir = node_path_1.default.join(targetRoot, 'assets', 'baseline', 'code');
102
+ const baselineCodeDir = node_path_1.default.join(targetRoot, 'baseline', 'code');
57
103
  const wikiSourceRoot = node_path_1.default.resolve(opts.wikiSource ?? (hasBaseline ? baselineCodeDir : cwd));
58
104
  // Derive the final wiki mode:
59
105
  // - no baseline -> 'off' (wiki flags are warned and ignored)
@@ -79,6 +125,24 @@ async function runInit(opts) {
79
125
  { label: 'Agent', value: validated.agent },
80
126
  { label: '目标路径', value: targetRoot },
81
127
  ];
128
+ if (opts.requirements) {
129
+ configItems.push({
130
+ label: 'Requirements 路径',
131
+ value: node_path_1.default.resolve(cwd, opts.requirements),
132
+ });
133
+ }
134
+ if (opts.references) {
135
+ configItems.push({
136
+ label: 'References 路径',
137
+ value: node_path_1.default.resolve(cwd, opts.references),
138
+ });
139
+ }
140
+ if ((opts.extraSkills?.length ?? 0) > 0) {
141
+ configItems.push({
142
+ label: 'Extra 资源',
143
+ value: opts.extraSkills.map((item) => node_path_1.default.resolve(cwd, item)).join(', '),
144
+ });
145
+ }
82
146
  if (validated.baseline) {
83
147
  const modeTag = validated.baselineMode === 'git' ? 'git' : 'local';
84
148
  configItems.push({ label: `基线 (${modeTag})`, value: validated.baseline });
@@ -106,9 +170,9 @@ async function runInit(opts) {
106
170
  baseUrl: opts.wikiBaseUrl,
107
171
  model: opts.wikiModel,
108
172
  });
109
- configItems.push({ label: 'Wiki 模型', value: wikiCfg.model });
173
+ // configItems.push({ label: 'Wiki 模型', value: wikiCfg.model });
110
174
  const displayBaseUrl = wikiCfg.baseUrl.replace(/\/v1$/, '');
111
- configItems.push({ label: 'Wiki API', value: displayBaseUrl });
175
+ // configItems.push({ label: 'Wiki API', value: displayBaseUrl });
112
176
  }
113
177
  catch (e) {
114
178
  // Ignore config resolution errors for display
@@ -166,6 +230,8 @@ async function runInit(opts) {
166
230
  arch: validated.arch,
167
231
  agent: validated.agent,
168
232
  sourcePath: validated.baseline ?? '',
233
+ requirementsPath: opts.requirements ? node_path_1.default.resolve(cwd, opts.requirements) : '',
234
+ referencesPath: opts.references ? node_path_1.default.resolve(cwd, opts.references) : '',
169
235
  createdAt,
170
236
  version: cliVersion,
171
237
  };
@@ -175,16 +241,35 @@ async function runInit(opts) {
175
241
  // optional: baseline copy, repomix pack, wiki generation
176
242
  // last: state file
177
243
  let stepCursor = 4;
244
+ const hasExtraAssets = (opts.extraSkills?.length ?? 0) > 0;
245
+ const stepExtraAssets = hasExtraAssets ? ++stepCursor : 0;
178
246
  const stepBaseline = hasBaseline ? ++stepCursor : 0;
179
247
  const stepRepomix = hasBaseline ? ++stepCursor : 0;
180
248
  const stepWiki = wikiEnabled ? ++stepCursor : 0;
181
249
  const stepState = ++stepCursor;
182
250
  const totalSteps = stepCursor;
183
251
  let baselineMeta;
252
+ let extraAssetsResult;
184
253
  try {
185
254
  logger_1.logger.section(`步骤 1/${totalSteps} - 生成目录骨架`);
186
255
  const copied = await (0, scaffold_1.scaffoldLayered)(skeletonSrcs, targetRoot);
187
256
  logger_1.logger.success(`已拷贝骨架文件 ${copied.length} 个`);
257
+ if (opts.requirements) {
258
+ const requirementRawDir = node_path_1.default.join(targetRoot, 'requirements', 'raw');
259
+ const copiedReq = await (0, doc_intake_paths_1.copySeedInputToRaw)(node_path_1.default.resolve(cwd, opts.requirements), requirementRawDir, 'requirements');
260
+ logger_1.logger.success(`已注入 requirements 输入到 raw:${copiedReq} 项`);
261
+ if (copiedReq > 0) {
262
+ await runBuildAutoConvert(targetRoot, requirementRawDir, 'requirements', opts);
263
+ }
264
+ }
265
+ if (opts.references) {
266
+ const referenceRawDir = node_path_1.default.join(targetRoot, 'references', 'raw');
267
+ const copiedRef = await (0, doc_intake_paths_1.copySeedInputToRaw)(node_path_1.default.resolve(cwd, opts.references), referenceRawDir, 'references');
268
+ logger_1.logger.success(`已注入 references 输入到 raw:${copiedRef} 项`);
269
+ if (copiedRef > 0) {
270
+ await runBuildAutoConvert(targetRoot, referenceRawDir, 'references', opts);
271
+ }
272
+ }
188
273
  logger_1.logger.section(`步骤 2/${totalSteps} - 渲染元文件`);
189
274
  const meta = await (0, render_meta_1.renderMetaLayered)(metaSrcs, targetRoot, ctx);
190
275
  logger_1.logger.success(`已渲染元文件 ${meta.length} 个: ${meta.join(', ')}`);
@@ -193,16 +278,34 @@ async function runInit(opts) {
193
278
  // Skills are injected beside the harness folder (cwd), not inside it,
194
279
  // so the Agent picks them up from the project root.
195
280
  const harnessDirName = node_path_1.default.basename(targetRoot);
196
- const injected = await (0, agent_injector_1.injectSkillsLayered)(buildSkillsSrcs, targetRoot, adapter, cwd, harnessDirName);
281
+ const buildSkillsBinding = {
282
+ harnessDirName,
283
+ harnessName: validated.name,
284
+ arch: validated.arch,
285
+ agent: validated.agent,
286
+ cliVersion,
287
+ generatedAt: createdAt,
288
+ };
289
+ const injected = await (0, agent_injector_1.injectSkillsLayered)(buildSkillsSrcs, targetRoot, adapter, cwd, buildSkillsBinding);
197
290
  logger_1.logger.success(`已向 ${node_path_1.default.join(cwd, adapter.skillsDir)}/ 注入 skill ${injected.length} 个`);
198
291
  logger_1.logger.section(`步骤 4/${totalSteps} - 注入 harness 构建规则`);
199
292
  const rules = await (0, agent_injector_1.injectRulesLayered)(buildRulesSrcs, adapter, cwd);
200
293
  if (adapter.rulesDir) {
201
294
  logger_1.logger.success(`已向 ${node_path_1.default.join(cwd, adapter.rulesDir)}/ 注入 rule ${rules.length} 个`);
202
295
  }
296
+ if (hasExtraAssets && opts.extraSkills) {
297
+ logger_1.logger.section(`步骤 ${stepExtraAssets}/${totalSteps} - 导入额外运行期资源到 _incoming`);
298
+ extraAssetsResult = await (0, extra_assets_intake_1.intakeExtraAssets)({
299
+ inputs: opts.extraSkills,
300
+ cwd,
301
+ harnessRoot: targetRoot,
302
+ });
303
+ logger_1.logger.success(`incoming 清单: ${extraAssetsResult.manifestRelPath}(总计 ${extraAssetsResult.incomingTotal},` +
304
+ `skills ${extraAssetsResult.skillsCount},rules ${extraAssetsResult.rulesCount},unknown ${extraAssetsResult.unknownCount})`);
305
+ }
203
306
  if (hasBaseline && validated.baseline && validated.baselineMode) {
204
307
  logger_1.logger.section(`步骤 ${stepBaseline}/${totalSteps} - 拷贝基线源码`);
205
- const destDir = node_path_1.default.join(targetRoot, 'assets', 'baseline', 'code');
308
+ const destDir = node_path_1.default.join(targetRoot, 'baseline', 'code');
206
309
  baselineMeta = await (0, baseline_copy_1.copyBaseline)({
207
310
  mode: validated.baselineMode,
208
311
  source: validated.baseline,
@@ -222,6 +325,40 @@ async function runInit(opts) {
222
325
  await (0, scaffold_1.rollbackTarget)(targetRoot);
223
326
  throw err;
224
327
  }
328
+ {
329
+ const adapterEntry = (0, adapters_1.getAdapter)(validated.agent);
330
+ const dirName = node_path_1.default.basename(targetRoot);
331
+ await (0, build_project_entry_1.writeBuildProjectEntry)({
332
+ projectRoot: cwd,
333
+ adapter: adapterEntry,
334
+ harnessDirName: dirName,
335
+ harnessName: validated.name,
336
+ arch: validated.arch,
337
+ agent: validated.agent,
338
+ force: !!opts.force,
339
+ });
340
+ const ignoreEntries = [
341
+ `/${dirName}/`,
342
+ '/AGENTS.md',
343
+ '/CLAUDE.md',
344
+ '/.codechat/',
345
+ ];
346
+ const ignoreAdded = await (0, project_ignore_1.appendProjectIgnoreEntries)({
347
+ projectRoot: cwd,
348
+ entries: ignoreEntries,
349
+ marker: '# svharness build',
350
+ });
351
+ const totalLines = Object.values(ignoreAdded).reduce((n, a) => n + a.length, 0);
352
+ if (totalLines > 0) {
353
+ const detail = Object.entries(ignoreAdded)
354
+ .map(([f, lines]) => `${f}: ${lines.join(', ')}`)
355
+ .join(';');
356
+ logger_1.logger.success(`已在项目 ignore 文件中追加:${detail}`);
357
+ }
358
+ else {
359
+ logger_1.logger.info('项目 ignore 文件已包含 build 注入路径,未追加新行');
360
+ }
361
+ }
225
362
  // 6a. Repomix XML pack of baseline code (default when --baseline; non-fatal on failure).
226
363
  if (hasBaseline) {
227
364
  logger_1.logger.section(`步骤 ${stepRepomix}/${totalSteps} - 生成 baseline Repomix 包`);
@@ -258,7 +395,7 @@ async function runInit(opts) {
258
395
  try {
259
396
  const repoRootFs = wikiSourceRoot;
260
397
  const wikiOutputRootFs = targetRoot;
261
- const wikiRelPath = node_path_1.default.join('assets', 'baseline', 'wiki');
398
+ const wikiRelPath = node_path_1.default.join('baseline', 'wiki');
262
399
  // Guard: baseline wiki must be based on real code. If the user provided
263
400
  // --baseline but the copy step produced an empty directory (e.g. every
264
401
  // file was filtered out), refuse to silently index cwd or an empty dir.
@@ -311,12 +448,12 @@ async function runInit(opts) {
311
448
  logger_1.logger.warn(`wiki 生成失败(不回滚 harness 骨架):${err.message}`);
312
449
  }
313
450
  }
314
- // If the full run failed, downgrade P1_wiki from DONE to PENDING
451
+ // If the full run failed, downgrade S10_wiki from DONE to PENDING
315
452
  // so the agent can pick it up via harness-build-skill-wiki-writer.
316
453
  if (!wikiFullOk) {
317
454
  resolvedWikiPhase = 'pending';
318
455
  resolvedWikiSource = 'cli-full-degraded';
319
- logger_1.logger.warn('P1_wiki 将标记为 PENDING,等待 agent 接力补齐');
456
+ logger_1.logger.warn('S10_wiki 将标记为 PENDING,等待 agent 接力补齐');
320
457
  }
321
458
  }
322
459
  else if (wikiMode === 'tasks') {
@@ -324,7 +461,7 @@ async function runInit(opts) {
324
461
  try {
325
462
  const repoRootFs = wikiSourceRoot;
326
463
  const wikiOutputRootFs = targetRoot;
327
- const wikiRelPath = node_path_1.default.join('assets', 'baseline', 'wiki');
464
+ const wikiRelPath = node_path_1.default.join('baseline', 'wiki');
328
465
  if (hasBaseline) {
329
466
  const exists = await fs_extra_1.default.pathExists(repoRootFs);
330
467
  const entries = exists ? await fs_extra_1.default.readdir(repoRootFs) : [];
@@ -400,6 +537,14 @@ async function runInit(opts) {
400
537
  wikiPhase: resolvedWikiPhase,
401
538
  wikiSource: resolvedWikiSource,
402
539
  });
540
+ if (extraAssetsResult) {
541
+ const s65 = state.phases.S65_customize_agent_env;
542
+ s65.incoming_manifest = extraAssetsResult.manifestRelPath;
543
+ s65.incoming_total = extraAssetsResult.incomingTotal;
544
+ s65.skills_count = extraAssetsResult.skillsCount;
545
+ s65.rules_count = extraAssetsResult.rulesCount;
546
+ s65.unknown_count = extraAssetsResult.unknownCount;
547
+ }
403
548
  const statePath = await (0, state_1.writeStateFile)(targetRoot, state);
404
549
  logger_1.logger.success(`状态文件: ${node_path_1.default.relative(targetRoot, statePath)}`);
405
550
  }