svharness 0.8.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.
- package/README.md +531 -0
- package/bin/cli.js +3 -0
- package/dist/adapters/_frontmatter.js +24 -0
- package/dist/adapters/claude-code.js +12 -0
- package/dist/adapters/codechat.js +12 -0
- package/dist/adapters/cursor.js +19 -0
- package/dist/adapters/generic.js +19 -0
- package/dist/adapters/index.js +26 -0
- package/dist/adapters/qoder.js +12 -0
- package/dist/commands/apply.js +272 -0
- package/dist/commands/init.js +420 -0
- package/dist/core/agent-injector.js +192 -0
- package/dist/core/next-steps.js +91 -0
- package/dist/core/render-meta.js +81 -0
- package/dist/core/repomix-pack.js +54 -0
- package/dist/core/scaffold.js +93 -0
- package/dist/core/state.js +80 -0
- package/dist/index.js +239 -0
- package/dist/types.js +5 -0
- package/dist/utils/baseline-copy.js +591 -0
- package/dist/utils/baseline-defaults.js +106 -0
- package/dist/utils/logger.js +56 -0
- package/dist/utils/validate-args.js +132 -0
- package/dist/utils/version.js +23 -0
- package/dist/wiki/abort.js +30 -0
- package/dist/wiki/config.js +79 -0
- package/dist/wiki/defaults.js +16 -0
- package/dist/wiki/envLoader.js +78 -0
- package/dist/wiki/index.js +29 -0
- package/dist/wiki/openaiCompat.js +219 -0
- package/dist/wiki/repowikiCanonicalSections.js +67 -0
- package/dist/wiki/repowikiCheckpoint.js +106 -0
- package/dist/wiki/repowikiConfig.js +9 -0
- package/dist/wiki/repowikiGit.js +73 -0
- package/dist/wiki/repowikiIndexer.js +824 -0
- package/dist/wiki/repowikiMarkdownPost.js +123 -0
- package/dist/wiki/repowikiMetadataContent.js +64 -0
- package/dist/wiki/repowikiMetadataJson.js +15 -0
- package/dist/wiki/repowikiScanner.js +156 -0
- package/dist/wiki/repowikiStructureNav.js +286 -0
- package/dist/wiki/repowikiStructureNormalize.js +218 -0
- package/dist/wiki/wikiStructureXml.js +316 -0
- package/dist/wiki/wikiTasksWriter.js +127 -0
- package/package.json +57 -0
- package/templates/_shared/apply-skills/harness-apply-skills-main.md +91 -0
- package/templates/_shared/build-rules/harness-build-rule-agent-agnostic.md +35 -0
- package/templates/_shared/build-rules/harness-build-rule-chinese-only.md +49 -0
- package/templates/_shared/build-rules/harness-build-rule-memory-write.md +31 -0
- package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +25 -0
- package/templates/_shared/build-rules/harness-build-rule-skills-tasks-output.md +35 -0
- package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +32 -0
- package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +63 -0
- package/templates/_shared/build-skills/harness-build-skill-knowledge-builder.md +120 -0
- package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +87 -0
- package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +85 -0
- package/templates/_shared/build-skills/harness-build-skill-wiki-writer.md +77 -0
- package/templates/_shared/meta/AGENTS.md.ejs +53 -0
- package/templates/_shared/meta/CHANGELOG.md.ejs +15 -0
- package/templates/_shared/meta/README.md.ejs +51 -0
- package/templates/_shared/meta/VERSION.ejs +1 -0
- package/templates/_shared/meta/harness.yaml.ejs +52 -0
- package/templates/_shared/skeleton/agent-env/memory/categories/.gitkeep +1 -0
- package/templates/_shared/skeleton/agent-env/memory/inbox/.gitkeep +1 -0
- package/templates/_shared/skeleton/agent-env/skills/.gitkeep +1 -0
- package/templates/_shared/skeleton/agent-env/tools/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/baseline/code/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/baseline/repomix/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/baseline/wiki/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/raw/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/requirements/.gitkeep +1 -0
- package/templates/_shared/skeleton/commands/install/.gitkeep +1 -0
- package/templates/_shared/skeleton/commands/update/.gitkeep +1 -0
- package/templates/_shared/skeleton/specs/behavior/schema.json +39 -0
- package/templates/_shared/skeleton/specs/interfaces/schema.json +38 -0
- package/templates/_shared/skeleton/specs/signals/schema.json +37 -0
- package/templates/_shared/skeleton/specs/ui/schema.json +44 -0
- package/templates/_shared/skeleton/tasks/templates/.gitkeep +0 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-compose-mandatory.mdc +49 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-coroutines-scope.mdc +52 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-hilt-injection.mdc +47 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-mvi-layering.mdc +58 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/SKILL.md +260 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/gradle-module-patterns.md +66 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/implementation-checklist.md +45 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/udf-data-flow.md +80 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/SKILL.md +79 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/references/interact.md +83 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/references/journeys.md +97 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/SKILL.md +162 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/canonical-sources.md +116 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/diagnostics.md +182 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/report-template.md +135 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/scoring.md +277 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/search-playbook.md +303 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/scripts/compose-reports.init.gradle +58 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-state/SKILL.md +196 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/SKILL.md +192 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/composable-api-guide.md +123 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/performance-recipes.md +97 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/state-patterns.md +93 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-kotlin-coroutines/SKILL.md +167 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/SKILL.md +45 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/CONFIGURATION.md +44 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md +83 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/REDUNDANT-RULES.md +222 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/REFLECTION-GUIDE.md +139 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md +176 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/android/training/testing/other-components/ui-automator.md +312 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/SKILL.md +87 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/analysis-of-the-project-and-layout.md +42 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md +168 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md +183 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/identify-optimal-xml-candidate.md +31 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/xml-layout-migration.md +86 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-aidl-thread.md +29 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-lifecycle-awareness.md +32 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-mvc-layering.md +32 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-view-binding.md +33 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-xml-styling.md +27 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-cmake-explicit-sources.md +31 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-header-guards.md +34 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-include-layering.md +39 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-no-cyclic-deps.md +29 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-raii.md +30 -0
- package/templates/python/skeleton/agent-env/rules/seed-context-managers.md +60 -0
- package/templates/python/skeleton/agent-env/rules/seed-docstrings.md +48 -0
- package/templates/python/skeleton/agent-env/rules/seed-import-order.md +49 -0
- package/templates/python/skeleton/agent-env/rules/seed-pep8-naming.md +45 -0
- package/templates/python/skeleton/agent-env/rules/seed-type-annotations.md +43 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-controlled-component.md +43 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-effect-cleanup.md +43 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-hook-rules.md +42 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-key-stability.md +39 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-no-props-drilling.md +43 -0
|
@@ -0,0 +1,420 @@
|
|
|
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.runInit = runInit;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const prompts_1 = __importDefault(require("prompts"));
|
|
10
|
+
const validate_args_1 = require("../utils/validate-args");
|
|
11
|
+
const version_1 = require("../utils/version");
|
|
12
|
+
const logger_1 = require("../utils/logger");
|
|
13
|
+
const scaffold_1 = require("../core/scaffold");
|
|
14
|
+
const render_meta_1 = require("../core/render-meta");
|
|
15
|
+
const state_1 = require("../core/state");
|
|
16
|
+
const agent_injector_1 = require("../core/agent-injector");
|
|
17
|
+
const adapters_1 = require("../adapters");
|
|
18
|
+
const next_steps_1 = require("../core/next-steps");
|
|
19
|
+
const repomix_pack_1 = require("../core/repomix-pack");
|
|
20
|
+
const baseline_copy_1 = require("../utils/baseline-copy");
|
|
21
|
+
const wiki_1 = require("../wiki");
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the absolute path to the bundled templates/ directory.
|
|
24
|
+
* At runtime __dirname is dist/commands, so templates are at ../../templates.
|
|
25
|
+
*/
|
|
26
|
+
function resolveTemplatesRoot() {
|
|
27
|
+
return node_path_1.default.resolve(__dirname, '..', '..', 'templates');
|
|
28
|
+
}
|
|
29
|
+
async function runInit(opts) {
|
|
30
|
+
(0, logger_1.setVerbose)(!!opts.verbose);
|
|
31
|
+
// 0. Mutual-exclusion between wiki modes.
|
|
32
|
+
if (opts.generateWiki && opts.wikiTasksOnly) {
|
|
33
|
+
throw new Error('--generate-wiki 与 --wiki-tasks-only 不可同时启用');
|
|
34
|
+
}
|
|
35
|
+
// 1. Validate args (throws on failure)
|
|
36
|
+
const validated = (0, validate_args_1.validateArgs)({
|
|
37
|
+
name: opts.name,
|
|
38
|
+
arch: opts.arch,
|
|
39
|
+
agent: opts.agent,
|
|
40
|
+
baseline: opts.baseline,
|
|
41
|
+
baselineBranch: opts.baselineBranch,
|
|
42
|
+
baselineMaxFileKB: opts.baselineMaxFileKB,
|
|
43
|
+
});
|
|
44
|
+
// Warn if --baseline-branch was provided together with a local path.
|
|
45
|
+
if (opts.baselineBranch && validated.baselineMode === 'local') {
|
|
46
|
+
logger_1.logger.warn('--baseline-branch 在本地基线模式下被忽略');
|
|
47
|
+
}
|
|
48
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
49
|
+
const targetRoot = node_path_1.default.resolve(cwd, `${validated.name}-harness`);
|
|
50
|
+
const cliVersion = (0, version_1.getCliVersion)();
|
|
51
|
+
const createdAt = new Date().toISOString();
|
|
52
|
+
// Baseline code lands at <target>/assets/baseline/code/ for both git & local
|
|
53
|
+
// modes. Wiki generation consumes this directory by default so it always
|
|
54
|
+
// reflects the baseline the user asked for (not the caller's cwd).
|
|
55
|
+
const hasBaseline = !!validated.baseline;
|
|
56
|
+
const baselineCodeDir = node_path_1.default.join(targetRoot, 'assets', 'baseline', 'code');
|
|
57
|
+
const wikiSourceRoot = node_path_1.default.resolve(opts.wikiSource ?? (hasBaseline ? baselineCodeDir : cwd));
|
|
58
|
+
// Derive the final wiki mode:
|
|
59
|
+
// - no baseline -> 'off' (wiki flags are warned and ignored)
|
|
60
|
+
// - --generate-wiki -> 'full'
|
|
61
|
+
// - otherwise -> 'tasks' (new default when baseline is present)
|
|
62
|
+
let wikiMode;
|
|
63
|
+
if (!hasBaseline) {
|
|
64
|
+
if (opts.generateWiki || opts.wikiTasksOnly) {
|
|
65
|
+
logger_1.logger.warn('未提供 --baseline,忽略 wiki 相关开关(--generate-wiki / --wiki-tasks-only)');
|
|
66
|
+
}
|
|
67
|
+
wikiMode = 'off';
|
|
68
|
+
}
|
|
69
|
+
else if (opts.generateWiki) {
|
|
70
|
+
wikiMode = 'full';
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
wikiMode = 'tasks';
|
|
74
|
+
}
|
|
75
|
+
// 2. Display configuration and confirmation (unless --yes)
|
|
76
|
+
const configItems = [
|
|
77
|
+
{ label: '项目', value: validated.name },
|
|
78
|
+
{ label: '架构', value: validated.arch },
|
|
79
|
+
{ label: 'Agent', value: validated.agent },
|
|
80
|
+
{ label: '目标路径', value: targetRoot },
|
|
81
|
+
];
|
|
82
|
+
if (validated.baseline) {
|
|
83
|
+
const modeTag = validated.baselineMode === 'git' ? 'git' : 'local';
|
|
84
|
+
configItems.push({ label: `基线 (${modeTag})`, value: validated.baseline });
|
|
85
|
+
if (validated.baselineMode === 'git' && validated.baselineBranch) {
|
|
86
|
+
configItems.push({ label: '基线分支', value: validated.baselineBranch });
|
|
87
|
+
}
|
|
88
|
+
configItems.push({
|
|
89
|
+
label: '基线大小上限',
|
|
90
|
+
value: `${validated.baselineMaxFileKB} KB/文件`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
configItems.push({ label: '版本', value: `svharness@${cliVersion}` });
|
|
94
|
+
// Add wiki configuration items based on the resolved wiki mode.
|
|
95
|
+
const wikiEnabled = wikiMode !== 'off';
|
|
96
|
+
const wikiModeLabel = wikiMode === 'full' ? '完整生成' : wikiMode === 'tasks' ? '仅任务清单(默认)' : '禁用';
|
|
97
|
+
configItems.push({ label: 'Wiki 生成', value: wikiModeLabel });
|
|
98
|
+
if (wikiEnabled) {
|
|
99
|
+
const wikiSource = wikiSourceRoot;
|
|
100
|
+
configItems.push({ label: 'Wiki 源码', value: wikiSource });
|
|
101
|
+
// Resolve wiki config for display
|
|
102
|
+
try {
|
|
103
|
+
const wikiCfg = await (0, wiki_1.resolveWikiRunConfig)({
|
|
104
|
+
cwd,
|
|
105
|
+
apiKey: opts.wikiApiKey,
|
|
106
|
+
baseUrl: opts.wikiBaseUrl,
|
|
107
|
+
model: opts.wikiModel,
|
|
108
|
+
});
|
|
109
|
+
configItems.push({ label: 'Wiki 模型', value: wikiCfg.model });
|
|
110
|
+
const displayBaseUrl = wikiCfg.baseUrl.replace(/\/v1$/, '');
|
|
111
|
+
configItems.push({ label: 'Wiki API', value: displayBaseUrl });
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
// Ignore config resolution errors for display
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (hasBaseline) {
|
|
118
|
+
configItems.push({
|
|
119
|
+
label: 'Repomix 基线包',
|
|
120
|
+
value: `默认生成(${(0, repomix_pack_1.repomixPackRelFile)()})`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
logger_1.logger.configBox('配置确认', configItems);
|
|
124
|
+
if (!opts.yes) {
|
|
125
|
+
const { ok } = await (0, prompts_1.default)({
|
|
126
|
+
type: 'confirm',
|
|
127
|
+
name: 'ok',
|
|
128
|
+
message: '确认以上配置并继续?',
|
|
129
|
+
initial: true,
|
|
130
|
+
});
|
|
131
|
+
if (!ok) {
|
|
132
|
+
logger_1.logger.warn('用户取消');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// 3. Target availability
|
|
137
|
+
await (0, scaffold_1.ensureTargetAvailable)(targetRoot, !!opts.force);
|
|
138
|
+
// 4. Resolve template paths (shared + arch two-layer)
|
|
139
|
+
const templatesRoot = resolveTemplatesRoot();
|
|
140
|
+
const sharedRoot = node_path_1.default.join(templatesRoot, '_shared');
|
|
141
|
+
const archRoot = node_path_1.default.join(templatesRoot, validated.arch);
|
|
142
|
+
if (!(await fs_extra_1.default.pathExists(archRoot))) {
|
|
143
|
+
throw new Error(`架构模板不存在: ${archRoot}`);
|
|
144
|
+
}
|
|
145
|
+
if (!(await fs_extra_1.default.pathExists(sharedRoot))) {
|
|
146
|
+
throw new Error(`共享模板不存在: ${sharedRoot}`);
|
|
147
|
+
}
|
|
148
|
+
const skeletonSrcs = [
|
|
149
|
+
node_path_1.default.join(sharedRoot, 'skeleton'),
|
|
150
|
+
node_path_1.default.join(archRoot, 'skeleton'),
|
|
151
|
+
];
|
|
152
|
+
const metaSrcs = [
|
|
153
|
+
node_path_1.default.join(sharedRoot, 'meta'),
|
|
154
|
+
node_path_1.default.join(archRoot, 'meta'),
|
|
155
|
+
];
|
|
156
|
+
const buildSkillsSrcs = [
|
|
157
|
+
node_path_1.default.join(sharedRoot, 'build-skills'),
|
|
158
|
+
node_path_1.default.join(archRoot, 'build-skills'),
|
|
159
|
+
];
|
|
160
|
+
const buildRulesSrcs = [
|
|
161
|
+
node_path_1.default.join(sharedRoot, 'build-rules'),
|
|
162
|
+
node_path_1.default.join(archRoot, 'build-rules'),
|
|
163
|
+
];
|
|
164
|
+
const ctx = {
|
|
165
|
+
name: validated.name,
|
|
166
|
+
arch: validated.arch,
|
|
167
|
+
agent: validated.agent,
|
|
168
|
+
sourcePath: validated.baseline ?? '',
|
|
169
|
+
createdAt,
|
|
170
|
+
version: cliVersion,
|
|
171
|
+
};
|
|
172
|
+
// 5. Execute pipeline with rollback-on-failure
|
|
173
|
+
// Dynamic step numbering:
|
|
174
|
+
// 1: skeleton, 2: meta, 3: skills, 4: rules
|
|
175
|
+
// optional: baseline copy, repomix pack, wiki generation
|
|
176
|
+
// last: state file
|
|
177
|
+
let stepCursor = 4;
|
|
178
|
+
const stepBaseline = hasBaseline ? ++stepCursor : 0;
|
|
179
|
+
const stepRepomix = hasBaseline ? ++stepCursor : 0;
|
|
180
|
+
const stepWiki = wikiEnabled ? ++stepCursor : 0;
|
|
181
|
+
const stepState = ++stepCursor;
|
|
182
|
+
const totalSteps = stepCursor;
|
|
183
|
+
let baselineMeta;
|
|
184
|
+
try {
|
|
185
|
+
logger_1.logger.section(`步骤 1/${totalSteps} - 生成目录骨架`);
|
|
186
|
+
const copied = await (0, scaffold_1.scaffoldLayered)(skeletonSrcs, targetRoot);
|
|
187
|
+
logger_1.logger.success(`已拷贝骨架文件 ${copied.length} 个`);
|
|
188
|
+
logger_1.logger.section(`步骤 2/${totalSteps} - 渲染元文件`);
|
|
189
|
+
const meta = await (0, render_meta_1.renderMetaLayered)(metaSrcs, targetRoot, ctx);
|
|
190
|
+
logger_1.logger.success(`已渲染元文件 ${meta.length} 个: ${meta.join(', ')}`);
|
|
191
|
+
logger_1.logger.section(`步骤 3/${totalSteps} - 注入构建辅助 skill`);
|
|
192
|
+
const adapter = (0, adapters_1.getAdapter)(validated.agent);
|
|
193
|
+
// Skills are injected beside the harness folder (cwd), not inside it,
|
|
194
|
+
// so the Agent picks them up from the project root.
|
|
195
|
+
const harnessDirName = node_path_1.default.basename(targetRoot);
|
|
196
|
+
const injected = await (0, agent_injector_1.injectSkillsLayered)(buildSkillsSrcs, targetRoot, adapter, cwd, harnessDirName);
|
|
197
|
+
logger_1.logger.success(`已向 ${node_path_1.default.join(cwd, adapter.skillsDir)}/ 注入 skill ${injected.length} 个`);
|
|
198
|
+
logger_1.logger.section(`步骤 4/${totalSteps} - 注入 harness 构建规则`);
|
|
199
|
+
const rules = await (0, agent_injector_1.injectRulesLayered)(buildRulesSrcs, adapter, cwd);
|
|
200
|
+
if (adapter.rulesDir) {
|
|
201
|
+
logger_1.logger.success(`已向 ${node_path_1.default.join(cwd, adapter.rulesDir)}/ 注入 rule ${rules.length} 个`);
|
|
202
|
+
}
|
|
203
|
+
if (hasBaseline && validated.baseline && validated.baselineMode) {
|
|
204
|
+
logger_1.logger.section(`步骤 ${stepBaseline}/${totalSteps} - 拷贝基线源码`);
|
|
205
|
+
const destDir = node_path_1.default.join(targetRoot, 'assets', 'baseline', 'code');
|
|
206
|
+
baselineMeta = await (0, baseline_copy_1.copyBaseline)({
|
|
207
|
+
mode: validated.baselineMode,
|
|
208
|
+
source: validated.baseline,
|
|
209
|
+
branch: validated.baselineBranch,
|
|
210
|
+
destDir,
|
|
211
|
+
maxSizeKB: validated.baselineMaxFileKB,
|
|
212
|
+
onLog: (m) => logger_1.logger.info(m),
|
|
213
|
+
});
|
|
214
|
+
const { files, sizeKB, skipped } = baselineMeta.stats;
|
|
215
|
+
logger_1.logger.success(`基线已拷贝:${files} 个文件 · ${sizeKB} KB · 跳过 ${skipped} 个` +
|
|
216
|
+
(baselineMeta.commit ? ` · commit ${baselineMeta.commit.substring(0, 8)}` : ''));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
logger_1.logger.error(err.message);
|
|
221
|
+
logger_1.logger.warn('正在回滚已生成的不完整产物...');
|
|
222
|
+
await (0, scaffold_1.rollbackTarget)(targetRoot);
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
// 6a. Repomix XML pack of baseline code (default when --baseline; non-fatal on failure).
|
|
226
|
+
if (hasBaseline) {
|
|
227
|
+
logger_1.logger.section(`步骤 ${stepRepomix}/${totalSteps} - 生成 baseline Repomix 包`);
|
|
228
|
+
try {
|
|
229
|
+
const packRootFs = wikiSourceRoot;
|
|
230
|
+
if (hasBaseline) {
|
|
231
|
+
const exists = await fs_extra_1.default.pathExists(packRootFs);
|
|
232
|
+
const entries = exists ? await fs_extra_1.default.readdir(packRootFs) : [];
|
|
233
|
+
if (!exists || entries.length === 0) {
|
|
234
|
+
throw new Error(`Repomix 打包目录为空或不存在:${packRootFs}。` +
|
|
235
|
+
'请检查基线拷贝结果(黑名单/大小上限可能过滤掉了所有文件)。');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const outAbs = await (0, repomix_pack_1.runRepomixPackBaseline)({
|
|
239
|
+
packRootFs,
|
|
240
|
+
harnessRootFs: targetRoot,
|
|
241
|
+
onLog: (m) => logger_1.logger.info(` ${m}`),
|
|
242
|
+
});
|
|
243
|
+
logger_1.logger.success(`baseline Repomix 已生成:${node_path_1.default.relative(targetRoot, outAbs)}`);
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
logger_1.logger.warn(`Repomix 生成失败(不回滚 harness 骨架):${err.message}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// 6b. Optional: generate baseline wiki via repowiki core.
|
|
250
|
+
// Wiki failure is non-fatal: we only warn, do NOT rollback the scaffold above.
|
|
251
|
+
// Wiki generation runs BEFORE state file so that the state accurately reflects
|
|
252
|
+
// the wiki result.
|
|
253
|
+
let resolvedWikiPhase = wikiMode === 'off' ? 'none' : wikiMode === 'tasks' ? 'pending' : 'done';
|
|
254
|
+
let resolvedWikiSource;
|
|
255
|
+
if (wikiMode === 'full') {
|
|
256
|
+
logger_1.logger.section(`步骤 ${stepWiki}/${totalSteps} - 生成 baseline wiki`);
|
|
257
|
+
let wikiFullOk = false;
|
|
258
|
+
try {
|
|
259
|
+
const repoRootFs = wikiSourceRoot;
|
|
260
|
+
const wikiOutputRootFs = targetRoot;
|
|
261
|
+
const wikiRelPath = node_path_1.default.join('assets', 'baseline', 'wiki');
|
|
262
|
+
// Guard: baseline wiki must be based on real code. If the user provided
|
|
263
|
+
// --baseline but the copy step produced an empty directory (e.g. every
|
|
264
|
+
// file was filtered out), refuse to silently index cwd or an empty dir.
|
|
265
|
+
if (hasBaseline) {
|
|
266
|
+
const exists = await fs_extra_1.default.pathExists(repoRootFs);
|
|
267
|
+
const entries = exists ? await fs_extra_1.default.readdir(repoRootFs) : [];
|
|
268
|
+
if (!exists || entries.length === 0) {
|
|
269
|
+
throw new Error(`Wiki 源码目录为空或不存在:${repoRootFs}。` +
|
|
270
|
+
'请检查基线拷贝结果(黑名单/大小上限可能过滤掉了所有文件)。');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const cfg = await (0, wiki_1.resolveWikiRunConfig)({
|
|
274
|
+
cwd,
|
|
275
|
+
apiKey: opts.wikiApiKey,
|
|
276
|
+
baseUrl: opts.wikiBaseUrl,
|
|
277
|
+
model: opts.wikiModel,
|
|
278
|
+
});
|
|
279
|
+
const langFolder = opts.wikiLang === 'en' ? 'en' : 'zh';
|
|
280
|
+
const langRun = (0, wiki_1.langRunForRepowikiTarget)(langFolder);
|
|
281
|
+
logger_1.logger.info(` 源码根:${repoRootFs}`);
|
|
282
|
+
logger_1.logger.info(` 输出到:${node_path_1.default.join(wikiOutputRootFs, wikiRelPath)}`);
|
|
283
|
+
logger_1.logger.info(` 语言:${langFolder} · 模型:${cfg.model}`);
|
|
284
|
+
await (0, wiki_1.runRepowikiIndex)({
|
|
285
|
+
repoRootFs,
|
|
286
|
+
wikiOutputRootFs,
|
|
287
|
+
wikiRelPath,
|
|
288
|
+
baseUrl: cfg.baseUrl,
|
|
289
|
+
apiKey: cfg.apiKey,
|
|
290
|
+
model: cfg.model,
|
|
291
|
+
languages: [langRun],
|
|
292
|
+
comprehensiveSections: true,
|
|
293
|
+
onLog: (m) => logger_1.logger.info(m),
|
|
294
|
+
onProgress: (p) => {
|
|
295
|
+
if (p.total > 0) {
|
|
296
|
+
logger_1.logger.info(` [${p.done}/${p.total}] ${p.detail}`);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
logger_1.logger.info(` ${p.detail}`);
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
logger_1.logger.success('baseline wiki 已生成');
|
|
304
|
+
wikiFullOk = true;
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
if ((0, wiki_1.isAbortError)(err)) {
|
|
308
|
+
logger_1.logger.warn('wiki 生成被中断(已写出部分页可从 checkpoint 恢复)');
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
logger_1.logger.warn(`wiki 生成失败(不回滚 harness 骨架):${err.message}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// If the full run failed, downgrade P1_wiki from DONE to PENDING
|
|
315
|
+
// so the agent can pick it up via harness-build-skill-wiki-writer.
|
|
316
|
+
if (!wikiFullOk) {
|
|
317
|
+
resolvedWikiPhase = 'pending';
|
|
318
|
+
resolvedWikiSource = 'cli-full-degraded';
|
|
319
|
+
logger_1.logger.warn('P1_wiki 将标记为 PENDING,等待 agent 接力补齐');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else if (wikiMode === 'tasks') {
|
|
323
|
+
logger_1.logger.section(`步骤 ${stepWiki}/${totalSteps} - 生成 baseline wiki 任务清单`);
|
|
324
|
+
try {
|
|
325
|
+
const repoRootFs = wikiSourceRoot;
|
|
326
|
+
const wikiOutputRootFs = targetRoot;
|
|
327
|
+
const wikiRelPath = node_path_1.default.join('assets', 'baseline', 'wiki');
|
|
328
|
+
if (hasBaseline) {
|
|
329
|
+
const exists = await fs_extra_1.default.pathExists(repoRootFs);
|
|
330
|
+
const entries = exists ? await fs_extra_1.default.readdir(repoRootFs) : [];
|
|
331
|
+
if (!exists || entries.length === 0) {
|
|
332
|
+
throw new Error(`Wiki 源码目录为空或不存在:${repoRootFs}。` +
|
|
333
|
+
'请检查基线拷贝结果(黑名单/大小上限可能过滤掉了所有文件)。');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const cfg = await (0, wiki_1.resolveWikiRunConfig)({
|
|
337
|
+
cwd,
|
|
338
|
+
apiKey: opts.wikiApiKey,
|
|
339
|
+
baseUrl: opts.wikiBaseUrl,
|
|
340
|
+
model: opts.wikiModel,
|
|
341
|
+
});
|
|
342
|
+
const langFolder = opts.wikiLang === 'en' ? 'en' : 'zh';
|
|
343
|
+
const langRun = (0, wiki_1.langRunForRepowikiTarget)(langFolder);
|
|
344
|
+
logger_1.logger.info(` 源码根:${repoRootFs}`);
|
|
345
|
+
logger_1.logger.info(` 输出到:${node_path_1.default.join(wikiOutputRootFs, wikiRelPath)}`);
|
|
346
|
+
logger_1.logger.info(` 语言:${langFolder} · 模型:${cfg.model}`);
|
|
347
|
+
logger_1.logger.info(' 模式:仅任务清单(tasks-only,默认)');
|
|
348
|
+
const outline = await (0, wiki_1.runRepowikiOutlineOnly)({
|
|
349
|
+
repoRootFs,
|
|
350
|
+
wikiOutputRootFs,
|
|
351
|
+
wikiRelPath,
|
|
352
|
+
baseUrl: cfg.baseUrl,
|
|
353
|
+
apiKey: cfg.apiKey,
|
|
354
|
+
model: cfg.model,
|
|
355
|
+
languages: [langRun],
|
|
356
|
+
comprehensiveSections: true,
|
|
357
|
+
onLog: (m) => logger_1.logger.info(m),
|
|
358
|
+
onProgress: (p) => {
|
|
359
|
+
if (p.total > 0) {
|
|
360
|
+
logger_1.logger.info(` [${p.done}/${p.total}] ${p.detail}`);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
logger_1.logger.info(` ${p.detail}`);
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
const tasksPath = await (0, wiki_1.writeWikiTasks)({
|
|
368
|
+
outline,
|
|
369
|
+
targetRoot,
|
|
370
|
+
wikiRelPath,
|
|
371
|
+
projectName: validated.name,
|
|
372
|
+
cliVersion,
|
|
373
|
+
model: cfg.model,
|
|
374
|
+
generatedAtIso: new Date().toISOString(),
|
|
375
|
+
sourceRootRel: node_path_1.default
|
|
376
|
+
.relative(targetRoot, wikiSourceRoot)
|
|
377
|
+
.replace(/\\/g, '/') || '.',
|
|
378
|
+
});
|
|
379
|
+
logger_1.logger.success(`baseline wiki 任务清单已生成:${node_path_1.default.relative(targetRoot, tasksPath)} (共 ${outline.pages.length} 页任务)`);
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
if ((0, wiki_1.isAbortError)(err)) {
|
|
383
|
+
logger_1.logger.warn('wiki 任务清单生成被中断');
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
logger_1.logger.warn(`wiki 任务清单生成失败(不回滚 harness 骨架):${err.message}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// 7. Write build state file (after wiki, so wiki result is accurately reflected).
|
|
391
|
+
try {
|
|
392
|
+
logger_1.logger.section(`步骤 ${stepState}/${totalSteps} - 初始化构建状态文件`);
|
|
393
|
+
const state = (0, state_1.buildInitialState)({
|
|
394
|
+
name: validated.name,
|
|
395
|
+
arch: validated.arch,
|
|
396
|
+
agent: validated.agent,
|
|
397
|
+
baseline: baselineMeta ?? validated.baseline,
|
|
398
|
+
cliVersion,
|
|
399
|
+
createdAt,
|
|
400
|
+
wikiPhase: resolvedWikiPhase,
|
|
401
|
+
wikiSource: resolvedWikiSource,
|
|
402
|
+
});
|
|
403
|
+
const statePath = await (0, state_1.writeStateFile)(targetRoot, state);
|
|
404
|
+
logger_1.logger.success(`状态文件: ${node_path_1.default.relative(targetRoot, statePath)}`);
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
logger_1.logger.error(`状态文件写入失败:${err.message}`);
|
|
408
|
+
throw err;
|
|
409
|
+
}
|
|
410
|
+
// 8. Print next steps
|
|
411
|
+
const adapterForNext = (0, adapters_1.getAdapter)(validated.agent);
|
|
412
|
+
(0, next_steps_1.printNextSteps)({
|
|
413
|
+
projectName: validated.name,
|
|
414
|
+
targetRoot,
|
|
415
|
+
adapter: adapterForNext,
|
|
416
|
+
agent: validated.agent,
|
|
417
|
+
hasSource: !!validated.baseline,
|
|
418
|
+
wikiMode,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
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.injectSkills = injectSkills;
|
|
7
|
+
exports.injectSkillsLayered = injectSkillsLayered;
|
|
8
|
+
exports.injectRulesLayered = injectRulesLayered;
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const logger_1 = require("../utils/logger");
|
|
12
|
+
const BUILD_SKILLS = [
|
|
13
|
+
'harness-build-skill-orchestrator.md',
|
|
14
|
+
'harness-build-skill-spec-builder.md',
|
|
15
|
+
'harness-build-skill-knowledge-builder.md',
|
|
16
|
+
'harness-build-skill-wiki-writer.md',
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Copy build-helper skills into the target project's agent skills directory,
|
|
20
|
+
* transforming content via the adapter when needed.
|
|
21
|
+
*
|
|
22
|
+
* Each skill is placed in its own subdirectory:
|
|
23
|
+
* <skillsRoot>/<adapter.skillsDir>/<skillName>/<skillName><adapter.skillExt>
|
|
24
|
+
*
|
|
25
|
+
* @param skillsSrcDir Source directory containing <skill>.md files
|
|
26
|
+
* @param targetRoot Harness output root (used for relative-path logging)
|
|
27
|
+
* @param adapter Agent-specific configuration
|
|
28
|
+
* @param skillsRoot Base directory for the agent skills folder.
|
|
29
|
+
* Defaults to targetRoot (legacy). Pass cwd to inject
|
|
30
|
+
* beside the harness folder instead of inside it.
|
|
31
|
+
*/
|
|
32
|
+
async function injectSkills(skillsSrcDir, targetRoot, adapter, skillsRoot) {
|
|
33
|
+
if (!(await fs_extra_1.default.pathExists(skillsSrcDir))) {
|
|
34
|
+
throw new Error(`build-skills source directory missing: ${skillsSrcDir}`);
|
|
35
|
+
}
|
|
36
|
+
const base = skillsRoot ?? targetRoot;
|
|
37
|
+
const dstDir = node_path_1.default.join(base, adapter.skillsDir);
|
|
38
|
+
await fs_extra_1.default.ensureDir(dstDir);
|
|
39
|
+
const injected = [];
|
|
40
|
+
for (const srcName of BUILD_SKILLS) {
|
|
41
|
+
const srcPath = node_path_1.default.join(skillsSrcDir, srcName);
|
|
42
|
+
if (!(await fs_extra_1.default.pathExists(srcPath))) {
|
|
43
|
+
logger_1.logger.warn(`build-skill missing, skipped: ${srcName}`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const raw = await fs_extra_1.default.readFile(srcPath, 'utf8');
|
|
47
|
+
const content = adapter.transform
|
|
48
|
+
? adapter.transform(raw, srcName)
|
|
49
|
+
: raw;
|
|
50
|
+
const baseName = srcName.replace(/\.md$/, '');
|
|
51
|
+
// Each skill lives in its own subdirectory named after the skill,
|
|
52
|
+
// with the content file always named SKILL<ext> (e.g. SKILL.md).
|
|
53
|
+
const skillDir = node_path_1.default.join(dstDir, baseName);
|
|
54
|
+
await fs_extra_1.default.ensureDir(skillDir);
|
|
55
|
+
const dstPath = node_path_1.default.join(skillDir, 'SKILL' + adapter.skillExt);
|
|
56
|
+
await fs_extra_1.default.outputFile(dstPath, content, 'utf8');
|
|
57
|
+
injected.push(node_path_1.default.relative(base, dstPath));
|
|
58
|
+
logger_1.logger.debug(`injected skill: ${dstPath}`);
|
|
59
|
+
}
|
|
60
|
+
return injected;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Layered skill injection: search each build-skill in multiple source
|
|
64
|
+
* directories (ordered low→high priority); the last directory containing
|
|
65
|
+
* the file wins. Any arch can therefore override a shared skill by placing
|
|
66
|
+
* a same-named `.md` under its own `build-skills/`.
|
|
67
|
+
*
|
|
68
|
+
* Each skill is placed in its own subdirectory:
|
|
69
|
+
* <skillsRoot>/<adapter.skillsDir>/<skillName>/<skillName><adapter.skillExt>
|
|
70
|
+
*
|
|
71
|
+
* Template placeholder `__HARNESS_ROOT__` is replaced with the harness
|
|
72
|
+
* directory path relative to cwd (e.g. `./demo8-harness/`).
|
|
73
|
+
*
|
|
74
|
+
* @param skillsSrcDirs Source directories (low→high priority)
|
|
75
|
+
* @param targetRoot Harness output root (used for relative-path logging)
|
|
76
|
+
* @param adapter Agent-specific configuration
|
|
77
|
+
* @param skillsRoot Base directory for the agent skills folder.
|
|
78
|
+
* Defaults to targetRoot (legacy). Pass cwd to inject
|
|
79
|
+
* beside the harness folder instead of inside it.
|
|
80
|
+
* @param harnessDirName Directory name of the harness (e.g. `demo8-harness`).
|
|
81
|
+
* Used to replace `__HARNESS_ROOT__` in skill content.
|
|
82
|
+
*/
|
|
83
|
+
async function injectSkillsLayered(skillsSrcDirs, targetRoot, adapter, skillsRoot, harnessDirName) {
|
|
84
|
+
const existing = [];
|
|
85
|
+
for (const d of skillsSrcDirs) {
|
|
86
|
+
if (await fs_extra_1.default.pathExists(d))
|
|
87
|
+
existing.push(d);
|
|
88
|
+
}
|
|
89
|
+
if (existing.length === 0) {
|
|
90
|
+
throw new Error(`no build-skills source directory found among: ${skillsSrcDirs.join(', ')}`);
|
|
91
|
+
}
|
|
92
|
+
const base = skillsRoot ?? targetRoot;
|
|
93
|
+
const dstDir = node_path_1.default.join(base, adapter.skillsDir);
|
|
94
|
+
await fs_extra_1.default.ensureDir(dstDir);
|
|
95
|
+
const harnessPrefix = harnessDirName ? `./${harnessDirName}/` : './';
|
|
96
|
+
const injected = [];
|
|
97
|
+
for (const srcName of BUILD_SKILLS) {
|
|
98
|
+
let picked = null;
|
|
99
|
+
for (let i = existing.length - 1; i >= 0; i--) {
|
|
100
|
+
const cand = node_path_1.default.join(existing[i], srcName);
|
|
101
|
+
if (await fs_extra_1.default.pathExists(cand)) {
|
|
102
|
+
picked = cand;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!picked) {
|
|
107
|
+
logger_1.logger.warn(`build-skill missing in all layers, skipped: ${srcName}`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const raw = await fs_extra_1.default.readFile(picked, 'utf8');
|
|
111
|
+
// Replace harness root placeholder with the actual harness directory
|
|
112
|
+
const resolved = raw.replace(/__HARNESS_ROOT__/g, harnessPrefix);
|
|
113
|
+
const content = adapter.transform
|
|
114
|
+
? adapter.transform(resolved, srcName)
|
|
115
|
+
: resolved;
|
|
116
|
+
const baseName = srcName.replace(/\.md$/, '');
|
|
117
|
+
// Each skill lives in its own subdirectory named after the skill,
|
|
118
|
+
// with the content file always named SKILL<ext> (e.g. SKILL.md).
|
|
119
|
+
const skillDir = node_path_1.default.join(dstDir, baseName);
|
|
120
|
+
await fs_extra_1.default.ensureDir(skillDir);
|
|
121
|
+
const dstPath = node_path_1.default.join(skillDir, 'SKILL' + adapter.skillExt);
|
|
122
|
+
await fs_extra_1.default.outputFile(dstPath, content, 'utf8');
|
|
123
|
+
injected.push(node_path_1.default.relative(base, dstPath));
|
|
124
|
+
logger_1.logger.debug(`injected skill: ${dstPath} (from ${picked})`);
|
|
125
|
+
}
|
|
126
|
+
return injected;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Layered rule injection: walk the union of `.md` files across the given
|
|
130
|
+
* source directories (ordered low→high priority), let higher layers override
|
|
131
|
+
* same-named files from lower layers, and write them flat (no per-file
|
|
132
|
+
* subdirectory) into `<rulesRoot>/<adapter.rulesDir>/` using `adapter.ruleExt`.
|
|
133
|
+
*
|
|
134
|
+
* Unlike skills, rules are written flat because each rule is a single file
|
|
135
|
+
* consumed directly by the agent's rule loader.
|
|
136
|
+
*
|
|
137
|
+
* @param rulesSrcDirs Source directories (low→high priority), e.g.
|
|
138
|
+
* [_shared/build-rules, <arch>/build-rules]
|
|
139
|
+
* @param adapter Agent-specific configuration
|
|
140
|
+
* @param rulesRoot Base directory for the agent rules folder, typically cwd.
|
|
141
|
+
*/
|
|
142
|
+
async function injectRulesLayered(rulesSrcDirs, adapter, rulesRoot) {
|
|
143
|
+
if (!adapter.rulesDir) {
|
|
144
|
+
logger_1.logger.info(`agent ${adapter.name} 未声明 rulesDir,跳过 rules 注入`);
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
const existing = [];
|
|
148
|
+
for (const d of rulesSrcDirs) {
|
|
149
|
+
if (await fs_extra_1.default.pathExists(d))
|
|
150
|
+
existing.push(d);
|
|
151
|
+
}
|
|
152
|
+
if (existing.length === 0) {
|
|
153
|
+
logger_1.logger.warn(`no build-rules source directory found among: ${rulesSrcDirs.join(', ')}`);
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
// Collect the union of .md filenames across all layers.
|
|
157
|
+
const fileSet = new Set();
|
|
158
|
+
for (const d of existing) {
|
|
159
|
+
const entries = await fs_extra_1.default.readdir(d);
|
|
160
|
+
for (const e of entries) {
|
|
161
|
+
if (e.toLowerCase().endsWith('.md'))
|
|
162
|
+
fileSet.add(e);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const dstDir = node_path_1.default.join(rulesRoot, adapter.rulesDir);
|
|
166
|
+
await fs_extra_1.default.ensureDir(dstDir);
|
|
167
|
+
const ruleExt = adapter.ruleExt ?? '.md';
|
|
168
|
+
const injected = [];
|
|
169
|
+
for (const srcName of fileSet) {
|
|
170
|
+
// Resolve highest-priority source for this filename.
|
|
171
|
+
let picked = null;
|
|
172
|
+
for (let i = existing.length - 1; i >= 0; i--) {
|
|
173
|
+
const cand = node_path_1.default.join(existing[i], srcName);
|
|
174
|
+
if (await fs_extra_1.default.pathExists(cand)) {
|
|
175
|
+
picked = cand;
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!picked)
|
|
180
|
+
continue;
|
|
181
|
+
const raw = await fs_extra_1.default.readFile(picked, 'utf8');
|
|
182
|
+
const content = adapter.ruleTransform
|
|
183
|
+
? adapter.ruleTransform(raw, srcName)
|
|
184
|
+
: raw;
|
|
185
|
+
const baseName = srcName.replace(/\.md$/i, '');
|
|
186
|
+
const dstPath = node_path_1.default.join(dstDir, baseName + ruleExt);
|
|
187
|
+
await fs_extra_1.default.outputFile(dstPath, content, 'utf8');
|
|
188
|
+
injected.push(node_path_1.default.relative(rulesRoot, dstPath));
|
|
189
|
+
logger_1.logger.debug(`injected rule: ${dstPath} (from ${picked})`);
|
|
190
|
+
}
|
|
191
|
+
return injected;
|
|
192
|
+
}
|