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.
- package/README.md +290 -61
- package/dist/adapters/claude-code.js +1 -0
- package/dist/adapters/codechat.js +1 -0
- package/dist/adapters/cursor.js +1 -0
- package/dist/adapters/generic.js +1 -0
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/opencode.js +17 -0
- package/dist/adapters/qoder.js +1 -0
- package/dist/commands/apply.js +456 -71
- package/dist/commands/convert.js +371 -0
- package/dist/commands/init.js +156 -11
- package/dist/commands/references-apply-skills.js +47 -0
- package/dist/commands/wizard.js +442 -0
- package/dist/config/constants.js +7 -0
- package/dist/config/index.js +18 -0
- package/dist/config/load-config.js +54 -0
- package/dist/config/merge-options.js +115 -0
- package/dist/config/normalize.js +165 -0
- package/dist/config/save-config.js +40 -0
- package/dist/config/types.js +2 -0
- package/dist/core/agent-injector.js +58 -9
- package/dist/core/apply-project-entry.js +66 -0
- package/dist/core/build-project-entry.js +98 -0
- package/dist/core/doc-intake-paths.js +155 -0
- package/dist/core/extra-assets-intake.js +254 -0
- package/dist/core/markitdown-client.js +156 -0
- package/dist/core/next-steps.js +33 -22
- package/dist/core/project-ignore.js +53 -0
- package/dist/core/reference-apply-skills.js +35 -0
- package/dist/core/render-meta.js +2 -1
- package/dist/core/repomix-pack.js +3 -3
- package/dist/core/state.js +44 -24
- package/dist/index.js +211 -140
- package/dist/utils/harness-name.js +41 -0
- package/dist/utils/validate-args.js +147 -6
- package/dist/wiki/wikiTasksWriter.js +5 -6
- package/package.json +2 -1
- package/templates/_shared/apply-skills/harness-apply-skills-main.md +19 -78
- package/templates/_shared/build-rules/harness-build-rule-agent-agnostic.md +5 -5
- package/templates/_shared/build-rules/harness-build-rule-chinese-only.md +5 -5
- package/templates/_shared/build-rules/harness-build-rule-convert-check.md +46 -0
- package/templates/_shared/build-rules/harness-build-rule-memory-write.md +1 -1
- package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +36 -10
- package/templates/_shared/build-rules/harness-build-rule-skills-tasks-output.md +3 -2
- package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +3 -3
- package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +36 -16
- package/templates/_shared/build-skills/harness-build-skill-agent-env-merge.md +75 -0
- package/templates/_shared/build-skills/harness-build-skill-knowledge-builder.md +49 -85
- package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +35 -18
- package/templates/_shared/build-skills/harness-build-skill-references-intake.md +91 -0
- package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +19 -9
- package/templates/_shared/build-skills/harness-build-skill-wiki-writer.md +24 -24
- package/templates/_shared/build-skills/harness-build-skills-main.md +83 -0
- package/templates/_shared/meta/AGENTS_APPLY.md.ejs +139 -0
- package/templates/_shared/meta/{AGENTS.md.ejs → AGENTS_BUILD.md.ejs} +7 -5
- package/templates/_shared/meta/CHANGELOG.md.ejs +3 -3
- package/templates/_shared/meta/README.md.ejs +11 -9
- package/templates/_shared/meta/harness.yaml.ejs +28 -7
- package/templates/_shared/skeleton/baseline/code/.gitkeep +1 -0
- package/templates/_shared/skeleton/baseline/wiki/.gitkeep +1 -0
- package/templates/_shared/skeleton/references/apply-skills-registry.example.yaml +11 -0
- package/templates/_shared/skeleton/references/md/.gitkeep +1 -0
- package/templates/_shared/skeleton/references/raw/.gitkeep +1 -0
- package/templates/_shared/skeleton/references/yaml/.gitkeep +1 -0
- package/templates/_shared/skeleton/requirements/md/.gitkeep +1 -0
- package/templates/_shared/skeleton/requirements/raw/.gitkeep +1 -0
- package/templates/_shared/skeleton/requirements/yaml/.gitkeep +1 -0
- package/templates/android-xml/skeleton/agent-env/skills/harness-android-cli/SKILL.md +88 -0
- package/templates/android-xml/skeleton/agent-env/skills/harness-android-service-patterns/SKILL.md +205 -0
- package/templates/android-xml/skeleton/agent-env/skills/harness-android-xml-architecture/SKILL.md +138 -0
- package/templates/android-xml/skeleton/agent-env/skills/harness-lifecycle-management/SKILL.md +158 -0
- package/templates/android-xml/skeleton/agent-env/skills/harness-xml-ui/SKILL.md +112 -0
- package/templates/cpp/skeleton/agent-env/skills/harness-cmake-build/SKILL.md +163 -0
- package/templates/cpp/skeleton/agent-env/skills/harness-cpp-architecture/SKILL.md +157 -0
- package/templates/cpp/skeleton/agent-env/skills/harness-cpp-concurrency/SKILL.md +180 -0
- package/templates/cpp/skeleton/agent-env/skills/harness-memory-safety/SKILL.md +163 -0
- package/templates/cpp/skeleton/agent-env/skills/harness-modern-cpp/SKILL.md +149 -0
- package/templates/python/skeleton/agent-env/skills/harness-async-patterns/SKILL.md +162 -0
- package/templates/python/skeleton/agent-env/skills/harness-python-architecture/SKILL.md +160 -0
- package/templates/python/skeleton/agent-env/skills/harness-python-package-structure/SKILL.md +210 -0
- package/templates/python/skeleton/agent-env/skills/harness-python-performance/SKILL.md +207 -0
- package/templates/python/skeleton/agent-env/skills/harness-python-testing/SKILL.md +198 -0
- package/templates/svharness.config.example.yaml +40 -0
- package/templates/web-react/skeleton/agent-env/skills/harness-react-architecture/SKILL.md +177 -0
- package/templates/web-react/skeleton/agent-env/skills/harness-react-performance/SKILL.md +177 -0
- package/templates/web-react/skeleton/agent-env/skills/harness-react-testing/SKILL.md +193 -0
- package/templates/web-react/skeleton/agent-env/skills/harness-react-ui-patterns/SKILL.md +257 -0
- package/templates/web-react/skeleton/agent-env/skills/harness-state-management/SKILL.md +189 -0
- package/templates/_shared/skeleton/assets/baseline/code/.gitkeep +0 -1
- package/templates/_shared/skeleton/assets/baseline/wiki/.gitkeep +0 -1
- package/templates/_shared/skeleton/assets/raw/.gitkeep +0 -1
- package/templates/_shared/skeleton/assets/requirements/.gitkeep +0 -1
- /package/templates/_shared/skeleton/{assets/baseline/repomix → agent-env/_incoming/skills}/.gitkeep +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
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.intakeExtraAssets = intakeExtraAssets;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
10
|
+
/**
|
|
11
|
+
* Stage user-provided extra runtime assets into
|
|
12
|
+
* `<harness>/agent-env/_incoming/skills/` and emit a manifest for S65 merge.
|
|
13
|
+
*/
|
|
14
|
+
async function intakeExtraAssets(opts) {
|
|
15
|
+
const incomingRoot = node_path_1.default.join(opts.harnessRoot, 'agent-env', '_incoming');
|
|
16
|
+
const incomingSkillsDir = node_path_1.default.join(incomingRoot, 'skills');
|
|
17
|
+
await fs_extra_1.default.ensureDir(incomingSkillsDir);
|
|
18
|
+
const sourcePaths = await collectAssetSourcePaths(opts.inputs, opts.cwd);
|
|
19
|
+
const usedIncomingNames = new Set();
|
|
20
|
+
const entries = [];
|
|
21
|
+
for (const sourcePath of sourcePaths) {
|
|
22
|
+
const stat = await fs_extra_1.default.stat(sourcePath);
|
|
23
|
+
const baseName = node_path_1.default.basename(sourcePath);
|
|
24
|
+
const incomingName = await allocateName(incomingSkillsDir, baseName, usedIncomingNames);
|
|
25
|
+
usedIncomingNames.add(incomingName);
|
|
26
|
+
const incomingAbsPath = node_path_1.default.join(incomingSkillsDir, incomingName);
|
|
27
|
+
await fs_extra_1.default.copy(sourcePath, incomingAbsPath, { overwrite: true, errorOnExist: false });
|
|
28
|
+
const detectedType = await detectAssetType(sourcePath, stat);
|
|
29
|
+
entries.push({
|
|
30
|
+
source_path: sourcePath,
|
|
31
|
+
source_kind: stat.isDirectory() ? 'directory' : 'file',
|
|
32
|
+
incoming_path: toPosix(node_path_1.default.relative(opts.harnessRoot, incomingAbsPath)),
|
|
33
|
+
detected_type: detectedType,
|
|
34
|
+
suggested_name: buildSuggestedName(sourcePath, detectedType),
|
|
35
|
+
conflicts: [],
|
|
36
|
+
action: 'PENDING_CONFIRMATION',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const manifest = {
|
|
40
|
+
schema_version: 1,
|
|
41
|
+
generated_at: new Date().toISOString(),
|
|
42
|
+
source_inputs: opts.inputs,
|
|
43
|
+
entries,
|
|
44
|
+
};
|
|
45
|
+
const manifestAbsPath = node_path_1.default.join(incomingRoot, 'manifest.yaml');
|
|
46
|
+
await fs_extra_1.default.outputFile(manifestAbsPath, js_yaml_1.default.dump(manifest, { lineWidth: 100, noRefs: true, sortKeys: false }), 'utf8');
|
|
47
|
+
return {
|
|
48
|
+
manifestAbsPath,
|
|
49
|
+
manifestRelPath: toPosix(node_path_1.default.relative(opts.harnessRoot, manifestAbsPath)),
|
|
50
|
+
incomingTotal: entries.length,
|
|
51
|
+
skillsCount: entries.filter((entry) => entry.detected_type === 'skill').length,
|
|
52
|
+
rulesCount: entries.filter((entry) => entry.detected_type === 'rule').length,
|
|
53
|
+
unknownCount: entries.filter((entry) => entry.detected_type === 'unknown').length,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function collectAssetSourcePaths(inputs, cwd) {
|
|
57
|
+
const out = new Set();
|
|
58
|
+
for (const input of inputs) {
|
|
59
|
+
const value = input.trim();
|
|
60
|
+
if (!value)
|
|
61
|
+
continue;
|
|
62
|
+
if (isGlob(value)) {
|
|
63
|
+
const matched = await globExpand(value, cwd);
|
|
64
|
+
for (const item of matched) {
|
|
65
|
+
for (const resolved of await resolveAssetPath(item)) {
|
|
66
|
+
out.add(resolved);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const absPath = node_path_1.default.isAbsolute(value) ? value : node_path_1.default.resolve(cwd, value);
|
|
72
|
+
if (!(await fs_extra_1.default.pathExists(absPath)))
|
|
73
|
+
continue;
|
|
74
|
+
for (const resolved of await resolveAssetPath(absPath)) {
|
|
75
|
+
out.add(resolved);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return [...out].sort();
|
|
79
|
+
}
|
|
80
|
+
async function resolveAssetPath(inputPath) {
|
|
81
|
+
const stat = await fs_extra_1.default.stat(inputPath);
|
|
82
|
+
if (stat.isFile()) {
|
|
83
|
+
if (node_path_1.default.basename(inputPath).toLowerCase() === 'skill.md') {
|
|
84
|
+
return [node_path_1.default.dirname(inputPath)];
|
|
85
|
+
}
|
|
86
|
+
if (node_path_1.default.basename(inputPath).toLowerCase() === '.gitkeep') {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
return [inputPath];
|
|
90
|
+
}
|
|
91
|
+
if (!(await hasSkillFile(inputPath))) {
|
|
92
|
+
return collectAssetsFromDir(inputPath);
|
|
93
|
+
}
|
|
94
|
+
return [inputPath];
|
|
95
|
+
}
|
|
96
|
+
async function collectAssetsFromDir(dirPath) {
|
|
97
|
+
if (await hasSkillFile(dirPath))
|
|
98
|
+
return [dirPath];
|
|
99
|
+
const out = [];
|
|
100
|
+
const entries = await fs_extra_1.default.readdir(dirPath, { withFileTypes: true });
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === 'dist')
|
|
103
|
+
continue;
|
|
104
|
+
const abs = node_path_1.default.join(dirPath, entry.name);
|
|
105
|
+
if (entry.isDirectory()) {
|
|
106
|
+
out.push(...(await collectAssetsFromDir(abs)));
|
|
107
|
+
}
|
|
108
|
+
else if (entry.isFile() && entry.name.toLowerCase() !== '.gitkeep') {
|
|
109
|
+
if (entry.name.toLowerCase() === 'skill.md') {
|
|
110
|
+
out.push(node_path_1.default.dirname(abs));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
out.push(abs);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
function isGlob(value) {
|
|
120
|
+
return /[*?[\]{}]/.test(value);
|
|
121
|
+
}
|
|
122
|
+
async function globExpand(pattern, cwd) {
|
|
123
|
+
const expanded = expandBraces(pattern);
|
|
124
|
+
const out = new Set();
|
|
125
|
+
for (const one of expanded) {
|
|
126
|
+
for (const pathItem of await globOne(one, cwd))
|
|
127
|
+
out.add(pathItem);
|
|
128
|
+
}
|
|
129
|
+
return [...out];
|
|
130
|
+
}
|
|
131
|
+
function expandBraces(pattern) {
|
|
132
|
+
const m = /\{([^{}]+)\}/.exec(pattern);
|
|
133
|
+
if (!m)
|
|
134
|
+
return [pattern];
|
|
135
|
+
const [whole, inner] = m;
|
|
136
|
+
const options = inner.split(',');
|
|
137
|
+
const head = pattern.slice(0, m.index);
|
|
138
|
+
const tail = pattern.slice(m.index + whole.length);
|
|
139
|
+
return options.flatMap((option) => expandBraces(head + option + tail));
|
|
140
|
+
}
|
|
141
|
+
async function globOne(pattern, cwd) {
|
|
142
|
+
const normalized = pattern.replace(/\\/g, '/');
|
|
143
|
+
const firstMeta = normalized.search(/[*?[]/);
|
|
144
|
+
if (firstMeta === -1) {
|
|
145
|
+
const abs = node_path_1.default.isAbsolute(normalized)
|
|
146
|
+
? node_path_1.default.normalize(normalized)
|
|
147
|
+
: node_path_1.default.resolve(cwd, normalized);
|
|
148
|
+
return (await fs_extra_1.default.pathExists(abs)) ? [abs] : [];
|
|
149
|
+
}
|
|
150
|
+
const lastSlashBeforeMeta = normalized.lastIndexOf('/', firstMeta);
|
|
151
|
+
const basePart = lastSlashBeforeMeta === -1 ? '.' : normalized.slice(0, lastSlashBeforeMeta) || '/';
|
|
152
|
+
const tail = lastSlashBeforeMeta === -1 ? normalized : normalized.slice(lastSlashBeforeMeta + 1);
|
|
153
|
+
const baseAbs = node_path_1.default.isAbsolute(basePart) ? basePart : node_path_1.default.resolve(cwd, basePart);
|
|
154
|
+
if (!(await fs_extra_1.default.pathExists(baseAbs)))
|
|
155
|
+
return [];
|
|
156
|
+
const matcher = globToRegex(tail);
|
|
157
|
+
const all = await walkEntries(baseAbs);
|
|
158
|
+
return all.filter((entryPath) => matcher.test(toPosix(node_path_1.default.relative(baseAbs, entryPath))));
|
|
159
|
+
}
|
|
160
|
+
function globToRegex(glob) {
|
|
161
|
+
let source = '^';
|
|
162
|
+
for (let i = 0; i < glob.length; i++) {
|
|
163
|
+
const c = glob[i];
|
|
164
|
+
if (c === '*') {
|
|
165
|
+
if (glob[i + 1] === '*') {
|
|
166
|
+
source += '.*';
|
|
167
|
+
i++;
|
|
168
|
+
if (glob[i + 1] === '/')
|
|
169
|
+
i++;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
source += '[^/]*';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (c === '?') {
|
|
176
|
+
source += '[^/]';
|
|
177
|
+
}
|
|
178
|
+
else if ('.+^$()|\\'.includes(c)) {
|
|
179
|
+
source += '\\' + c;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
source += c;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
source += '$';
|
|
186
|
+
return new RegExp(source);
|
|
187
|
+
}
|
|
188
|
+
async function walkEntries(dirPath) {
|
|
189
|
+
const out = [];
|
|
190
|
+
const entries = await fs_extra_1.default.readdir(dirPath, { withFileTypes: true });
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === 'dist')
|
|
193
|
+
continue;
|
|
194
|
+
const abs = node_path_1.default.join(dirPath, entry.name);
|
|
195
|
+
out.push(abs);
|
|
196
|
+
if (entry.isDirectory()) {
|
|
197
|
+
out.push(...(await walkEntries(abs)));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
async function detectAssetType(sourcePath, stat) {
|
|
203
|
+
if (stat.isDirectory()) {
|
|
204
|
+
return (await hasSkillFile(sourcePath)) ? 'skill' : 'unknown';
|
|
205
|
+
}
|
|
206
|
+
const fileName = node_path_1.default.basename(sourcePath).toLowerCase();
|
|
207
|
+
const ext = node_path_1.default.extname(fileName);
|
|
208
|
+
if (ext === '.mdc')
|
|
209
|
+
return 'rule';
|
|
210
|
+
if (fileName === 'skill.md')
|
|
211
|
+
return 'skill';
|
|
212
|
+
if (fileName.startsWith('harness-apply-rules-'))
|
|
213
|
+
return 'rule';
|
|
214
|
+
if (fileName.startsWith('harness-apply-skills-'))
|
|
215
|
+
return 'skill';
|
|
216
|
+
if (fileName.includes('-rules-') && ext === '.md')
|
|
217
|
+
return 'rule';
|
|
218
|
+
return 'unknown';
|
|
219
|
+
}
|
|
220
|
+
async function hasSkillFile(dirPath) {
|
|
221
|
+
return fs_extra_1.default.pathExists(node_path_1.default.join(dirPath, 'SKILL.md'));
|
|
222
|
+
}
|
|
223
|
+
function buildSuggestedName(sourcePath, detectedType) {
|
|
224
|
+
if (detectedType === 'unknown')
|
|
225
|
+
return undefined;
|
|
226
|
+
const raw = node_path_1.default.basename(sourcePath, node_path_1.default.extname(sourcePath));
|
|
227
|
+
const normalized = raw
|
|
228
|
+
.toLowerCase()
|
|
229
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
230
|
+
.replace(/^-+|-+$/g, '')
|
|
231
|
+
.replace(/--+/g, '-');
|
|
232
|
+
if (!normalized)
|
|
233
|
+
return undefined;
|
|
234
|
+
if (detectedType === 'skill')
|
|
235
|
+
return `harness-apply-skills-${normalized}`;
|
|
236
|
+
return `harness-apply-rules-${normalized}`;
|
|
237
|
+
}
|
|
238
|
+
async function allocateName(targetDir, baseName, used) {
|
|
239
|
+
if (!used.has(baseName) && !(await fs_extra_1.default.pathExists(node_path_1.default.join(targetDir, baseName)))) {
|
|
240
|
+
return baseName;
|
|
241
|
+
}
|
|
242
|
+
const ext = node_path_1.default.extname(baseName);
|
|
243
|
+
const stem = ext ? baseName.slice(0, -ext.length) : baseName;
|
|
244
|
+
for (let i = 1; i < 1000; i++) {
|
|
245
|
+
const next = ext ? `${stem}-${i}${ext}` : `${stem}-${i}`;
|
|
246
|
+
if (!used.has(next) && !(await fs_extra_1.default.pathExists(node_path_1.default.join(targetDir, next)))) {
|
|
247
|
+
return next;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`无法为 incoming 资源分配唯一文件名: ${baseName}`);
|
|
251
|
+
}
|
|
252
|
+
function toPosix(input) {
|
|
253
|
+
return input.replace(/\\/g, '/');
|
|
254
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
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.MarkItDownClientError = void 0;
|
|
7
|
+
exports.ping = ping;
|
|
8
|
+
exports.postConvert = postConvert;
|
|
9
|
+
exports.postConvertWithRetry = postConvertWithRetry;
|
|
10
|
+
/**
|
|
11
|
+
* Thin HTTP client for a cloud-deployed `markitdown_serve` instance
|
|
12
|
+
* (FastAPI + Microsoft MarkItDown).
|
|
13
|
+
*
|
|
14
|
+
* The server contract is defined once in `markitdown_serve/README.md`:
|
|
15
|
+
*
|
|
16
|
+
* POST /convert
|
|
17
|
+
* Content-Type: multipart/form-data
|
|
18
|
+
* field: file (binary)
|
|
19
|
+
* 200 { markdown, source_name, mime }
|
|
20
|
+
* 413 { error: 'file_too_large', limit_mb }
|
|
21
|
+
* 415 { error: 'unsupported_format', ext }
|
|
22
|
+
* 500 { error: 'internal', trace_id }
|
|
23
|
+
*
|
|
24
|
+
* GET /healthz -> 200 { status: 'ok', markitdown_version }
|
|
25
|
+
*
|
|
26
|
+
* We rely exclusively on Node 18+ built-ins (`fetch`, `FormData`, `Blob`), so
|
|
27
|
+
* there is no extra npm dependency to ship.
|
|
28
|
+
*/
|
|
29
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
30
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
31
|
+
class MarkItDownClientError extends Error {
|
|
32
|
+
status;
|
|
33
|
+
retryable;
|
|
34
|
+
body;
|
|
35
|
+
constructor(message, status, retryable, body) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = 'MarkItDownClientError';
|
|
38
|
+
this.status = status;
|
|
39
|
+
this.retryable = retryable;
|
|
40
|
+
this.body = body;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.MarkItDownClientError = MarkItDownClientError;
|
|
44
|
+
/**
|
|
45
|
+
* Probe the remote service. Resolves to the parsed health payload on success,
|
|
46
|
+
* throws a `MarkItDownClientError` otherwise. Used by the `convert` command
|
|
47
|
+
* to fail fast with a clear message before attempting any upload.
|
|
48
|
+
*/
|
|
49
|
+
async function ping(endpoint, timeoutMs = 10_000) {
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch(`${endpoint}/healthz`, {
|
|
54
|
+
method: 'GET',
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
throw new MarkItDownClientError(`/healthz responded HTTP ${res.status}`, res.status, false);
|
|
59
|
+
}
|
|
60
|
+
return (await res.json());
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (err instanceof MarkItDownClientError)
|
|
64
|
+
throw err;
|
|
65
|
+
throw new MarkItDownClientError(`healthz unreachable: ${err.message}`, 0, true);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Upload a single file to `POST /convert` and return the parsed Markdown
|
|
73
|
+
* payload. 5xx / network errors surface as retryable errors; 4xx surfaces as
|
|
74
|
+
* non-retryable (no point re-uploading an unsupported format).
|
|
75
|
+
*/
|
|
76
|
+
async function postConvert(endpoint, filePath, timeoutMs) {
|
|
77
|
+
const buf = await promises_1.default.readFile(filePath);
|
|
78
|
+
const fileName = node_path_1.default.basename(filePath);
|
|
79
|
+
const form = new FormData();
|
|
80
|
+
// Node's global Blob accepts Uint8Array; DOM's BlobPart type is not in
|
|
81
|
+
// ES2022 lib, so we go through `any` to avoid pulling in DOM typings.
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
+
form.append('file', new Blob([buf]), fileName);
|
|
84
|
+
const controller = new AbortController();
|
|
85
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(`${endpoint}/convert`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
body: form,
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
});
|
|
92
|
+
const rawText = await res.text();
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = rawText ? JSON.parse(rawText) : undefined;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
parsed = rawText;
|
|
99
|
+
}
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
const retryable = res.status >= 500;
|
|
102
|
+
throw new MarkItDownClientError(describeHttpError(res.status, parsed), res.status, retryable, parsed);
|
|
103
|
+
}
|
|
104
|
+
const body = parsed;
|
|
105
|
+
if (!body || typeof body.markdown !== 'string') {
|
|
106
|
+
throw new MarkItDownClientError(`unexpected response shape, missing "markdown" field`, res.status, false, parsed);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
markdown: body.markdown,
|
|
110
|
+
source_name: body.source_name || fileName,
|
|
111
|
+
mime: body.mime,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
if (err instanceof MarkItDownClientError)
|
|
116
|
+
throw err;
|
|
117
|
+
if (err.name === 'AbortError') {
|
|
118
|
+
throw new MarkItDownClientError(`request timed out after ${Math.round(timeoutMs / 1000)}s`, 0, true);
|
|
119
|
+
}
|
|
120
|
+
throw new MarkItDownClientError(`network error: ${err.message}`, 0, true);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* `postConvert` with one automatic retry on retryable errors (5xx / network).
|
|
128
|
+
* 4xx errors bypass the retry to keep diagnostics fast.
|
|
129
|
+
*/
|
|
130
|
+
async function postConvertWithRetry(endpoint, filePath, timeoutMs) {
|
|
131
|
+
try {
|
|
132
|
+
return await postConvert(endpoint, filePath, timeoutMs);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (err instanceof MarkItDownClientError && err.retryable) {
|
|
136
|
+
return await postConvert(endpoint, filePath, timeoutMs);
|
|
137
|
+
}
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function describeHttpError(status, body) {
|
|
142
|
+
if (body && typeof body === 'object') {
|
|
143
|
+
const b = body;
|
|
144
|
+
if (status === 415 && b.error === 'unsupported_format') {
|
|
145
|
+
return `server rejected unsupported format (ext=${String(b.ext ?? '?')})`;
|
|
146
|
+
}
|
|
147
|
+
if (status === 413 && b.error === 'file_too_large') {
|
|
148
|
+
return `server size limit exceeded (limit_mb=${String(b.limit_mb ?? '?')})`;
|
|
149
|
+
}
|
|
150
|
+
if (b.error) {
|
|
151
|
+
const trace = b.trace_id ? ` trace_id=${String(b.trace_id)}` : '';
|
|
152
|
+
return `server error ${status}: ${String(b.error)}${trace}`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return `server returned HTTP ${status}`;
|
|
156
|
+
}
|
package/dist/core/next-steps.js
CHANGED
|
@@ -20,11 +20,16 @@ function printNextSteps(input) {
|
|
|
20
20
|
lines.push('');
|
|
21
21
|
lines.push(`1. ${picocolors_1.default.cyan('cd ' + input.targetRoot)}`);
|
|
22
22
|
lines.push('');
|
|
23
|
-
lines.push('2.
|
|
23
|
+
lines.push('2. 打开项目根下的 ' +
|
|
24
|
+
picocolors_1.default.cyan(input.adapter.projectEntryFile) +
|
|
25
|
+
',并确认构建辅助 skill 已加载: ' +
|
|
24
26
|
picocolors_1.default.cyan(input.adapter.skillsDir + '/'));
|
|
27
|
+
lines.push(' - harness-build-skills-main (harness-build 总清单 / 二级调度,优先入口)');
|
|
25
28
|
lines.push(' - harness-build-skill-orchestrator (构建总调度)');
|
|
26
29
|
lines.push(' - harness-build-skill-spec-builder (raw → requirements → specs)');
|
|
27
|
-
lines.push(' - harness-build-skill-
|
|
30
|
+
lines.push(' - harness-build-skill-references-intake (S60 references convert + 结构化索引)');
|
|
31
|
+
lines.push(' - harness-build-skill-agent-env-merge (S61 确认 + S65 合并写入)');
|
|
32
|
+
lines.push(' - harness-build-skill-knowledge-builder (S10 样本 / S70 skills & tasks)');
|
|
28
33
|
lines.push(' - harness-build-skill-wiki-writer (按 TASKS.md 逐页撰写 baseline wiki 正文)');
|
|
29
34
|
lines.push('');
|
|
30
35
|
if (input.adapter.rulesDir) {
|
|
@@ -33,17 +38,24 @@ function printNextSteps(input) {
|
|
|
33
38
|
lines.push('');
|
|
34
39
|
}
|
|
35
40
|
if (input.hasSource) {
|
|
36
|
-
lines.push('3.
|
|
37
|
-
picocolors_1.default.cyan('
|
|
38
|
-
'
|
|
41
|
+
lines.push('3. 已识别到基线源码(反向提取模式)。先准备 P2 相关文件:' +
|
|
42
|
+
picocolors_1.default.cyan('requirements/raw/') +
|
|
43
|
+
'、' +
|
|
44
|
+
picocolors_1.default.cyan('references/raw/') +
|
|
45
|
+
',并确认是否有额外运行期 skills/rules,然后打开 AI 会话并说:');
|
|
39
46
|
}
|
|
40
47
|
else {
|
|
41
|
-
lines.push('3.
|
|
42
|
-
picocolors_1.default.cyan('
|
|
43
|
-
'
|
|
48
|
+
lines.push('3. 全新项目模式。先准备 P2 相关文件:' +
|
|
49
|
+
picocolors_1.default.cyan('requirements/raw/') +
|
|
50
|
+
'、' +
|
|
51
|
+
picocolors_1.default.cyan('references/raw/') +
|
|
52
|
+
',并确认是否有额外运行期 skills/rules,然后打开 AI 会话并说:');
|
|
44
53
|
}
|
|
45
54
|
lines.push('');
|
|
46
|
-
lines.push(' ' +
|
|
55
|
+
lines.push(' ' +
|
|
56
|
+
picocolors_1.default.bold(picocolors_1.default.yellow('>> 按 harness-build-skills-main 开始构建')) +
|
|
57
|
+
' 或 ' +
|
|
58
|
+
picocolors_1.default.bold(picocolors_1.default.yellow('>> 开始构建 harness')));
|
|
47
59
|
lines.push('');
|
|
48
60
|
lines.push('4. 随时用下面的命令查看进度: ' +
|
|
49
61
|
picocolors_1.default.cyan('cat .harness-build-state.yaml'));
|
|
@@ -51,34 +63,33 @@ function printNextSteps(input) {
|
|
|
51
63
|
if (input.hasSource) {
|
|
52
64
|
lines.push(picocolors_1.default.bold('Baseline Repomix(XML,默认)'));
|
|
53
65
|
lines.push(' 输出文件: ' + picocolors_1.default.cyan((0, repomix_pack_1.repomixPackRelFile)()));
|
|
54
|
-
lines.push(' (若 init 中 Repomix 步骤失败,请查看上方日志中的具体报错)');
|
|
55
66
|
lines.push('');
|
|
56
67
|
}
|
|
57
68
|
if (wikiMode === 'tasks') {
|
|
58
69
|
lines.push(picocolors_1.default.bold('Baseline wiki 任务清单已生成(默认 tasks-only 模式)'));
|
|
59
|
-
lines.push(' 清单位置: ' + picocolors_1.default.cyan('
|
|
60
|
-
lines.push(' Outline XML: ' + picocolors_1.default.cyan('
|
|
61
|
-
lines.push(' 源码快照: ' + picocolors_1.default.cyan('
|
|
70
|
+
lines.push(' 清单位置: ' + picocolors_1.default.cyan('baseline/wiki/TASKS.md'));
|
|
71
|
+
lines.push(' Outline XML: ' + picocolors_1.default.cyan('baseline/wiki/structure.xml'));
|
|
72
|
+
lines.push(' 源码快照: ' + picocolors_1.default.cyan('baseline/code/'));
|
|
62
73
|
lines.push(' 打开 AI 会话并说: ' +
|
|
63
74
|
picocolors_1.default.bold(picocolors_1.default.yellow('>> 按 TASKS.md 逐页生成 baseline wiki')));
|
|
64
|
-
lines.push(' (由 harness-build-skill-wiki-writer skill 接管,源码引用统一指向
|
|
65
|
-
lines.push(' .harness-build-state.yaml 中
|
|
75
|
+
lines.push(' (由 harness-build-skill-wiki-writer skill 接管,源码引用统一指向 baseline/code/)');
|
|
76
|
+
lines.push(' .harness-build-state.yaml 中 S10_wiki 初始为 PENDING,');
|
|
66
77
|
lines.push(' 全部 [x] 后由 harness-build-skill-orchestrator 标记 DONE。');
|
|
67
78
|
lines.push('');
|
|
68
79
|
}
|
|
69
80
|
else if (wikiMode === 'full') {
|
|
70
81
|
lines.push(picocolors_1.default.bold('Baseline wiki 已完整生成(--generate-wiki 模式)'));
|
|
71
|
-
lines.push(' wiki 目录: ' + picocolors_1.default.cyan('
|
|
72
|
-
lines.push(' .harness-build-state.yaml 中
|
|
82
|
+
lines.push(' wiki 目录: ' + picocolors_1.default.cyan('baseline/wiki/'));
|
|
83
|
+
lines.push(' .harness-build-state.yaml 中 S10_wiki 初始为 DONE。');
|
|
73
84
|
lines.push('');
|
|
74
85
|
}
|
|
75
86
|
// 更显眼的提示框
|
|
76
87
|
const tipBox = [
|
|
77
|
-
'
|
|
78
|
-
'│ 💡 提示
|
|
79
|
-
'│ svharness 已完成骨架初始化及辅助 skill 的注入到 Agent 的 skill目录
|
|
80
|
-
'│ 自此起所有构建步骤均由 Agent 驱动
|
|
81
|
-
'
|
|
88
|
+
'┌──────────────────────────────────────────────────────────────────────────',
|
|
89
|
+
'│ 💡 提示 ',
|
|
90
|
+
'│ svharness 已完成骨架初始化及辅助 skill 的注入到 Agent 的 skill目录 ',
|
|
91
|
+
'│ 自此起所有构建步骤均由 Agent 驱动 ',
|
|
92
|
+
'└──────────────────────────────────────────────────────────────────────────',
|
|
82
93
|
];
|
|
83
94
|
for (const line of tipBox) {
|
|
84
95
|
lines.push(picocolors_1.default.bold(picocolors_1.default.yellow(line)));
|
|
@@ -0,0 +1,53 @@
|
|
|
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.appendProjectIgnoreEntries = appendProjectIgnoreEntries;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const DEFAULT_IGNORE_FILENAMES = ['.gitignore', '.cursorignore'];
|
|
10
|
+
/**
|
|
11
|
+
* Idempotently append ignore entries under `projectRoot`.
|
|
12
|
+
* Always writes `.gitignore` (creates if missing). Updates `.cursorignore` only when it already exists.
|
|
13
|
+
*/
|
|
14
|
+
async function appendProjectIgnoreEntries(input) {
|
|
15
|
+
const marker = input.marker ?? '# svharness';
|
|
16
|
+
const filenames = input.filenames ?? DEFAULT_IGNORE_FILENAMES;
|
|
17
|
+
const normalizedEntries = input.entries.map((e) => e.startsWith('/') ? e : `/${e.replace(/^\/+/, '')}`);
|
|
18
|
+
const result = {};
|
|
19
|
+
for (const filename of filenames) {
|
|
20
|
+
const filePath = node_path_1.default.join(input.projectRoot, filename);
|
|
21
|
+
const exists = await fs_extra_1.default.pathExists(filePath);
|
|
22
|
+
if (!exists && filename !== '.gitignore') {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const added = await appendToIgnoreFile(filePath, normalizedEntries, marker, !exists);
|
|
26
|
+
if (added.length > 0) {
|
|
27
|
+
result[filename] = added;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
async function appendToIgnoreFile(filePath, entries, marker, createNew) {
|
|
33
|
+
const raw = createNew || !(await fs_extra_1.default.pathExists(filePath))
|
|
34
|
+
? ''
|
|
35
|
+
: await fs_extra_1.default.readFile(filePath, 'utf8');
|
|
36
|
+
const existing = new Set(raw
|
|
37
|
+
.split(/\r?\n/)
|
|
38
|
+
.map((line) => line.trim())
|
|
39
|
+
.filter((line) => line.length > 0 && !line.startsWith('#')));
|
|
40
|
+
const added = [];
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (!existing.has(entry)) {
|
|
43
|
+
existing.add(entry);
|
|
44
|
+
added.push(entry);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (added.length === 0)
|
|
48
|
+
return [];
|
|
49
|
+
const appendix = `\n${marker}\n` + added.map((line) => `${line}\n`).join('');
|
|
50
|
+
const next = raw.endsWith('\n') || raw.length === 0 ? raw + appendix : raw + '\n' + appendix;
|
|
51
|
+
await fs_extra_1.default.outputFile(filePath, next, 'utf8');
|
|
52
|
+
return added;
|
|
53
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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.loadApplySkillRegistry = loadApplySkillRegistry;
|
|
7
|
+
exports.registryEntriesForBinding = registryEntriesForBinding;
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
11
|
+
async function loadApplySkillRegistry(harnessRoot) {
|
|
12
|
+
const registryPath = node_path_1.default.join(harnessRoot, 'references', 'apply-skills-registry.yaml');
|
|
13
|
+
if (!(await fs_extra_1.default.pathExists(registryPath)))
|
|
14
|
+
return undefined;
|
|
15
|
+
const raw = js_yaml_1.default.load(await fs_extra_1.default.readFile(registryPath, 'utf8'));
|
|
16
|
+
if (!raw?.skills?.length)
|
|
17
|
+
return undefined;
|
|
18
|
+
return raw;
|
|
19
|
+
}
|
|
20
|
+
function registryEntriesForBinding(registry) {
|
|
21
|
+
return registry.skills
|
|
22
|
+
.filter((s) => s.enabled)
|
|
23
|
+
.map((s) => {
|
|
24
|
+
const row = {
|
|
25
|
+
name: s.skill_name,
|
|
26
|
+
reference_md: s.reference_md,
|
|
27
|
+
description: s.description,
|
|
28
|
+
};
|
|
29
|
+
if (s.entry)
|
|
30
|
+
row.entry = s.entry;
|
|
31
|
+
if (s.source_id)
|
|
32
|
+
row.source_id = s.source_id;
|
|
33
|
+
return row;
|
|
34
|
+
});
|
|
35
|
+
}
|
package/dist/core/render-meta.js
CHANGED
|
@@ -11,7 +11,8 @@ const ejs_1 = __importDefault(require("ejs"));
|
|
|
11
11
|
const logger_1 = require("../utils/logger");
|
|
12
12
|
const META_FILES = [
|
|
13
13
|
{ tplName: 'harness.yaml.ejs', outName: 'harness.yaml' },
|
|
14
|
-
{ tplName: '
|
|
14
|
+
{ tplName: 'AGENTS_BUILD.md.ejs', outName: 'AGENTS_BUILD.md' },
|
|
15
|
+
{ tplName: 'AGENTS_APPLY.md.ejs', outName: 'AGENTS_APPLY.md' },
|
|
15
16
|
{ tplName: 'README.md.ejs', outName: 'README.md' },
|
|
16
17
|
{ tplName: 'VERSION.ejs', outName: 'VERSION' },
|
|
17
18
|
{ tplName: 'CHANGELOG.md.ejs', outName: 'CHANGELOG.md' },
|
|
@@ -9,15 +9,15 @@ exports.runRepomixPackBaseline = runRepomixPackBaseline;
|
|
|
9
9
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
11
|
const repomix_1 = require("repomix");
|
|
12
|
-
/** Relative to harness root: `
|
|
13
|
-
exports.REPOMIX_BASELINE_REL_DIR = node_path_1.default.join('
|
|
12
|
+
/** Relative to harness root: `baseline/repomix/repomix-pack.xml`. */
|
|
13
|
+
exports.REPOMIX_BASELINE_REL_DIR = node_path_1.default.join('baseline', 'repomix');
|
|
14
14
|
exports.REPOMIX_PACK_FILENAME = 'repomix-pack.xml';
|
|
15
15
|
function repomixPackRelFile() {
|
|
16
16
|
return node_path_1.default.join(exports.REPOMIX_BASELINE_REL_DIR, exports.REPOMIX_PACK_FILENAME).replace(/\\/g, '/');
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Pack `packRootFs` with Repomix (library `runCli`) and write XML to
|
|
20
|
-
* `<harnessRootFs>/
|
|
20
|
+
* `<harnessRootFs>/baseline/repomix/repomix-pack.xml`.
|
|
21
21
|
*/
|
|
22
22
|
async function runRepomixPackBaseline(opts) {
|
|
23
23
|
const { packRootFs, harnessRootFs, onLog } = opts;
|