tackle-harness 0.0.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/LICENSE +21 -0
- package/README.en.md +259 -0
- package/README.md +261 -0
- package/bin/tackle.js +150 -0
- package/package.json +29 -0
- package/plugins/contracts/plugin-interface.js +244 -0
- package/plugins/core/hook-skill-gate/index.js +437 -0
- package/plugins/core/hook-skill-gate/plugin.json +12 -0
- package/plugins/core/provider-memory-store/index.js +403 -0
- package/plugins/core/provider-memory-store/plugin.json +9 -0
- package/plugins/core/provider-role-registry/index.js +477 -0
- package/plugins/core/provider-role-registry/plugin.json +9 -0
- package/plugins/core/provider-state-store/index.js +244 -0
- package/plugins/core/provider-state-store/plugin.json +9 -0
- package/plugins/core/skill-agent-dispatcher/plugin.json +13 -0
- package/plugins/core/skill-agent-dispatcher/skill.md +912 -0
- package/plugins/core/skill-batch-task-creator/plugin.json +13 -0
- package/plugins/core/skill-batch-task-creator/skill.md +616 -0
- package/plugins/core/skill-checklist/plugin.json +10 -0
- package/plugins/core/skill-checklist/skill.md +115 -0
- package/plugins/core/skill-completion-report/plugin.json +10 -0
- package/plugins/core/skill-completion-report/skill.md +331 -0
- package/plugins/core/skill-experience-logger/plugin.json +10 -0
- package/plugins/core/skill-experience-logger/skill.md +235 -0
- package/plugins/core/skill-human-checkpoint/plugin.json +10 -0
- package/plugins/core/skill-human-checkpoint/skill.md +194 -0
- package/plugins/core/skill-progress-tracker/plugin.json +10 -0
- package/plugins/core/skill-progress-tracker/skill.md +204 -0
- package/plugins/core/skill-role-manager/plugin.json +10 -0
- package/plugins/core/skill-role-manager/skill.md +252 -0
- package/plugins/core/skill-split-work-package/plugin.json +13 -0
- package/plugins/core/skill-split-work-package/skill.md +446 -0
- package/plugins/core/skill-task-creator/plugin.json +13 -0
- package/plugins/core/skill-task-creator/skill.md +744 -0
- package/plugins/core/skill-team-cleanup/plugin.json +10 -0
- package/plugins/core/skill-team-cleanup/skill.md +266 -0
- package/plugins/core/skill-workflow-orchestrator/plugin.json +13 -0
- package/plugins/core/skill-workflow-orchestrator/skill.md +274 -0
- package/plugins/core/validator-doc-sync/index.js +248 -0
- package/plugins/core/validator-doc-sync/plugin.json +9 -0
- package/plugins/core/validator-work-package/index.js +300 -0
- package/plugins/core/validator-work-package/plugin.json +9 -0
- package/plugins/plugin-registry.json +118 -0
- package/plugins/runtime/config-manager.js +306 -0
- package/plugins/runtime/event-bus.js +187 -0
- package/plugins/runtime/harness-build.js +1019 -0
- package/plugins/runtime/logger.js +174 -0
- package/plugins/runtime/plugin-loader.js +339 -0
- package/plugins/runtime/state-store.js +277 -0
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HarnessBuild - Plugin-to-native format builder for AI Agent Harness
|
|
3
|
+
*
|
|
4
|
+
* Reads plugins from plugin-registry.json, converts them to Claude Code
|
|
5
|
+
* native skill/hook format, and outputs to .claude/skills/ and .claude/hooks/.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node plugins/runtime/harness-build.js # build all plugins
|
|
9
|
+
* node plugins/runtime/harness-build.js --validate # validate only
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Builds skill plugins -> .claude/skills/{name}/skill.md
|
|
13
|
+
* - Builds hook plugins -> .claude/hooks/{name}/index.js
|
|
14
|
+
* - Validates plugin.json required fields
|
|
15
|
+
* - Handles empty registries gracefully
|
|
16
|
+
* - Build report with plugin counts and output paths
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
var fs = require('fs');
|
|
22
|
+
var path = require('path');
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Constants
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
var PLUGIN_REQUIRED_FIELDS = ['name', 'version', 'type', 'description'];
|
|
29
|
+
var VALID_PLUGIN_TYPES = ['skill', 'hook', 'validator', 'provider'];
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// HarnessBuild class
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {object} [options]
|
|
37
|
+
* @param {string} [options.rootDir] - project root directory
|
|
38
|
+
* @param {string} [options.registryPath] - override path to plugin-registry.json
|
|
39
|
+
* @param {string} [options.pluginsDir] - override path to plugins/core/
|
|
40
|
+
* @param {string} [options.outputSkillsDir] - override output .claude/skills/
|
|
41
|
+
* @param {string} [options.outputHooksDir] - override output .claude/hooks/
|
|
42
|
+
*/
|
|
43
|
+
function HarnessBuild(options) {
|
|
44
|
+
options = options || {};
|
|
45
|
+
|
|
46
|
+
this._rootDir = options.rootDir || process.cwd();
|
|
47
|
+
this._registryPath = options.registryPath || path.join(this._rootDir, 'plugins', 'plugin-registry.json');
|
|
48
|
+
this._pluginsDir = options.pluginsDir || path.join(this._rootDir, 'plugins', 'core');
|
|
49
|
+
this._outputSkillsDir = options.outputSkillsDir || path.join(this._rootDir, '.claude', 'skills');
|
|
50
|
+
this._outputHooksDir = options.outputHooksDir || path.join(this._rootDir, '.claude', 'hooks');
|
|
51
|
+
|
|
52
|
+
/** @type {object[]} validation errors collected during --validate */
|
|
53
|
+
this._validationErrors = [];
|
|
54
|
+
/** @type {object[]} validation warnings */
|
|
55
|
+
this._validationWarnings = [];
|
|
56
|
+
/** @type {object[]} build results */
|
|
57
|
+
this._buildResults = [];
|
|
58
|
+
/** @type {object|null} cached harness config */
|
|
59
|
+
this._harnessConfig = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Public API
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run validation on all plugins listed in the registry.
|
|
68
|
+
* Checks plugin.json format, required fields, and companion files.
|
|
69
|
+
*
|
|
70
|
+
* @returns {{ valid: boolean, errors: object[], warnings: object[], summary: string }}
|
|
71
|
+
*/
|
|
72
|
+
HarnessBuild.prototype.validate = function validate() {
|
|
73
|
+
this._validationErrors = [];
|
|
74
|
+
this._validationWarnings = [];
|
|
75
|
+
|
|
76
|
+
var registry = this._readRegistry();
|
|
77
|
+
var pluginEntries = this._getPluginEntries(registry);
|
|
78
|
+
|
|
79
|
+
if (pluginEntries.length === 0) {
|
|
80
|
+
return {
|
|
81
|
+
valid: true,
|
|
82
|
+
errors: [],
|
|
83
|
+
warnings: [],
|
|
84
|
+
summary: 'Registry is empty, nothing to validate.',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (var i = 0; i < pluginEntries.length; i++) {
|
|
89
|
+
this._validatePlugin(pluginEntries[i]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var valid = this._validationErrors.length === 0;
|
|
93
|
+
var summary = this._formatValidationSummary(pluginEntries.length);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
valid: valid,
|
|
97
|
+
errors: this._validationErrors,
|
|
98
|
+
warnings: this._validationWarnings,
|
|
99
|
+
summary: summary,
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build all plugins from the registry into Claude Code native format.
|
|
105
|
+
*
|
|
106
|
+
* @returns {{ success: boolean, built: object[], errors: object[], summary: string }}
|
|
107
|
+
*/
|
|
108
|
+
HarnessBuild.prototype.build = function build() {
|
|
109
|
+
this._buildResults = [];
|
|
110
|
+
|
|
111
|
+
var registry = this._readRegistry();
|
|
112
|
+
var pluginEntries = this._getPluginEntries(registry);
|
|
113
|
+
|
|
114
|
+
if (pluginEntries.length === 0) {
|
|
115
|
+
return {
|
|
116
|
+
success: true,
|
|
117
|
+
built: [],
|
|
118
|
+
errors: [],
|
|
119
|
+
summary: 'Registry is empty, build produced no output.',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
var buildErrors = [];
|
|
124
|
+
|
|
125
|
+
for (var i = 0; i < pluginEntries.length; i++) {
|
|
126
|
+
try {
|
|
127
|
+
var result = this._buildPlugin(pluginEntries[i]);
|
|
128
|
+
this._buildResults.push(result);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
buildErrors.push({
|
|
131
|
+
plugin: pluginEntries[i].name || 'unknown',
|
|
132
|
+
error: err.message,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
var success = buildErrors.length === 0;
|
|
138
|
+
var summary = this._formatBuildSummary(this._buildResults, buildErrors);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
success: success,
|
|
142
|
+
built: this._buildResults,
|
|
143
|
+
errors: buildErrors,
|
|
144
|
+
summary: summary,
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Registry reading
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Read and parse the plugin registry.
|
|
154
|
+
* @returns {object}
|
|
155
|
+
*/
|
|
156
|
+
HarnessBuild.prototype._readRegistry = function _readRegistry() {
|
|
157
|
+
try {
|
|
158
|
+
var content = fs.readFileSync(this._registryPath, 'utf-8');
|
|
159
|
+
return JSON.parse(content);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
this._log('warn', 'Could not read registry: ' + err.message + '. Using empty registry.');
|
|
162
|
+
return { version: '1.0.0', plugins: [] };
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Extract plugin entries from the registry.
|
|
168
|
+
* Supports array format: [{ name, source, enabled, config }]
|
|
169
|
+
*
|
|
170
|
+
* @param {object} registry
|
|
171
|
+
* @returns {object[]}
|
|
172
|
+
*/
|
|
173
|
+
HarnessBuild.prototype._getPluginEntries = function _getPluginEntries(registry) {
|
|
174
|
+
var plugins = registry.plugins;
|
|
175
|
+
if (!plugins || !Array.isArray(plugins)) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
// Filter out disabled plugins
|
|
179
|
+
var entries = [];
|
|
180
|
+
for (var i = 0; i < plugins.length; i++) {
|
|
181
|
+
var entry = plugins[i];
|
|
182
|
+
if (entry && entry.enabled !== false) {
|
|
183
|
+
entries.push(entry);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return entries;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Validation
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Validate a single plugin entry.
|
|
195
|
+
* @param {object} entry - registry entry with at least a name field
|
|
196
|
+
*/
|
|
197
|
+
HarnessBuild.prototype._validatePlugin = function _validatePlugin(entry) {
|
|
198
|
+
var pluginName = entry.name || entry.source || 'unknown';
|
|
199
|
+
var pluginDir = this._resolvePluginDir(entry);
|
|
200
|
+
|
|
201
|
+
// 1. Check plugin directory exists
|
|
202
|
+
if (!fs.existsSync(pluginDir)) {
|
|
203
|
+
this._validationErrors.push({
|
|
204
|
+
plugin: pluginName,
|
|
205
|
+
field: 'directory',
|
|
206
|
+
message: 'Plugin directory not found: ' + pluginDir,
|
|
207
|
+
});
|
|
208
|
+
return; // can't validate further without directory
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 2. Check plugin.json exists
|
|
212
|
+
var metaPath = path.join(pluginDir, 'plugin.json');
|
|
213
|
+
if (!fs.existsSync(metaPath)) {
|
|
214
|
+
this._validationErrors.push({
|
|
215
|
+
plugin: pluginName,
|
|
216
|
+
field: 'plugin.json',
|
|
217
|
+
message: 'plugin.json not found in ' + pluginDir,
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 3. Parse and validate plugin.json
|
|
223
|
+
var meta;
|
|
224
|
+
try {
|
|
225
|
+
meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
226
|
+
} catch (err) {
|
|
227
|
+
this._validationErrors.push({
|
|
228
|
+
plugin: pluginName,
|
|
229
|
+
field: 'plugin.json',
|
|
230
|
+
message: 'Invalid JSON in plugin.json: ' + err.message,
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 4. Check required fields
|
|
236
|
+
for (var i = 0; i < PLUGIN_REQUIRED_FIELDS.length; i++) {
|
|
237
|
+
var field = PLUGIN_REQUIRED_FIELDS[i];
|
|
238
|
+
if (!meta[field]) {
|
|
239
|
+
this._validationErrors.push({
|
|
240
|
+
plugin: pluginName,
|
|
241
|
+
field: field,
|
|
242
|
+
message: 'Missing required field: ' + field,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 5. Check plugin type is valid
|
|
248
|
+
if (meta.type && VALID_PLUGIN_TYPES.indexOf(meta.type) === -1) {
|
|
249
|
+
this._validationErrors.push({
|
|
250
|
+
plugin: pluginName,
|
|
251
|
+
field: 'type',
|
|
252
|
+
message: 'Invalid plugin type: "' + meta.type + '". Must be one of: ' + VALID_PLUGIN_TYPES.join(', '),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 6. Type-specific file checks
|
|
257
|
+
if (meta.type === 'skill') {
|
|
258
|
+
var skillMdPath = path.join(pluginDir, 'skill.md');
|
|
259
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
260
|
+
this._validationErrors.push({
|
|
261
|
+
plugin: pluginName,
|
|
262
|
+
field: 'skill.md',
|
|
263
|
+
message: 'Skill plugin is missing skill.md file',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (meta.type === 'hook') {
|
|
269
|
+
var indexPath = path.join(pluginDir, 'index.js');
|
|
270
|
+
if (!fs.existsSync(indexPath)) {
|
|
271
|
+
this._validationWarnings.push({
|
|
272
|
+
plugin: pluginName,
|
|
273
|
+
field: 'index.js',
|
|
274
|
+
message: 'Hook plugin is missing index.js file (build will generate stub)',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 7. Version format check (basic semver)
|
|
280
|
+
if (meta.version && !/^\d+\.\d+\.\d+/.test(meta.version)) {
|
|
281
|
+
this._validationWarnings.push({
|
|
282
|
+
plugin: pluginName,
|
|
283
|
+
field: 'version',
|
|
284
|
+
message: 'Version "' + meta.version + '" does not follow semver format (x.y.z)',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Format validation summary for output.
|
|
291
|
+
* @param {number} totalPlugins
|
|
292
|
+
* @returns {string}
|
|
293
|
+
*/
|
|
294
|
+
HarnessBuild.prototype._formatValidationSummary = function _formatValidationSummary(totalPlugins) {
|
|
295
|
+
var lines = [];
|
|
296
|
+
lines.push('');
|
|
297
|
+
lines.push('=== Validation Report ===');
|
|
298
|
+
lines.push('Plugins checked: ' + totalPlugins);
|
|
299
|
+
lines.push('Errors: ' + this._validationErrors.length);
|
|
300
|
+
lines.push('Warnings: ' + this._validationWarnings.length);
|
|
301
|
+
|
|
302
|
+
if (this._validationErrors.length > 0) {
|
|
303
|
+
lines.push('');
|
|
304
|
+
lines.push('--- Errors ---');
|
|
305
|
+
for (var i = 0; i < this._validationErrors.length; i++) {
|
|
306
|
+
var e = this._validationErrors[i];
|
|
307
|
+
lines.push(' [' + e.plugin + '] ' + e.message);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (this._validationWarnings.length > 0) {
|
|
312
|
+
lines.push('');
|
|
313
|
+
lines.push('--- Warnings ---');
|
|
314
|
+
for (var j = 0; j < this._validationWarnings.length; j++) {
|
|
315
|
+
var w = this._validationWarnings[j];
|
|
316
|
+
lines.push(' [' + w.plugin + '] ' + w.message);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
lines.push('');
|
|
321
|
+
lines.push(this._validationErrors.length === 0 ? 'Validation PASSED' : 'Validation FAILED');
|
|
322
|
+
lines.push('');
|
|
323
|
+
|
|
324
|
+
return lines.join('\n');
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Building
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Resolve the plugin directory from a registry entry.
|
|
333
|
+
* Uses entry.source if provided, otherwise derives from entry.name.
|
|
334
|
+
*
|
|
335
|
+
* @param {object} entry
|
|
336
|
+
* @returns {string}
|
|
337
|
+
*/
|
|
338
|
+
HarnessBuild.prototype._resolvePluginDir = function _resolvePluginDir(entry) {
|
|
339
|
+
var source = entry.source || entry.name;
|
|
340
|
+
if (!source) {
|
|
341
|
+
return path.join(this._pluginsDir, 'unknown');
|
|
342
|
+
}
|
|
343
|
+
// If source is an absolute path, use it directly
|
|
344
|
+
if (path.isAbsolute(source)) {
|
|
345
|
+
return source;
|
|
346
|
+
}
|
|
347
|
+
return path.join(this._pluginsDir, source);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Build a single plugin.
|
|
352
|
+
*
|
|
353
|
+
* @param {object} entry - registry entry
|
|
354
|
+
* @returns {{ name: string, type: string, outputPath: string, files: string[] }}
|
|
355
|
+
*/
|
|
356
|
+
HarnessBuild.prototype._buildPlugin = function _buildPlugin(entry) {
|
|
357
|
+
var pluginDir = this._resolvePluginDir(entry);
|
|
358
|
+
var metaPath = path.join(pluginDir, 'plugin.json');
|
|
359
|
+
|
|
360
|
+
// Read plugin metadata
|
|
361
|
+
if (!fs.existsSync(metaPath)) {
|
|
362
|
+
throw new Error('plugin.json not found in ' + pluginDir);
|
|
363
|
+
}
|
|
364
|
+
var meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
365
|
+
|
|
366
|
+
var pluginName = meta.name || entry.name;
|
|
367
|
+
var pluginType = meta.type;
|
|
368
|
+
|
|
369
|
+
this._log('info', 'Building plugin: ' + pluginName + ' (type: ' + pluginType + ')');
|
|
370
|
+
|
|
371
|
+
// Dispatch by type
|
|
372
|
+
switch (pluginType) {
|
|
373
|
+
case 'skill':
|
|
374
|
+
return this._buildSkillPlugin(pluginName, pluginDir, meta);
|
|
375
|
+
case 'hook':
|
|
376
|
+
return this._buildHookPlugin(pluginName, pluginDir, meta);
|
|
377
|
+
case 'validator':
|
|
378
|
+
return this._buildValidatorPlugin(pluginName, pluginDir, meta);
|
|
379
|
+
case 'provider':
|
|
380
|
+
return this._buildProviderPlugin(pluginName, pluginDir, meta);
|
|
381
|
+
default:
|
|
382
|
+
throw new Error('Unknown plugin type: ' + pluginType);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Build a skill plugin.
|
|
388
|
+
* Copies skill.md from plugin directory to .claude/skills/{name}/skill.md.
|
|
389
|
+
*
|
|
390
|
+
* @param {string} name
|
|
391
|
+
* @param {string} pluginDir
|
|
392
|
+
* @param {object} meta
|
|
393
|
+
* @returns {{ name: string, type: string, outputPath: string, files: string[] }}
|
|
394
|
+
*/
|
|
395
|
+
HarnessBuild.prototype._buildSkillPlugin = function _buildSkillPlugin(name, pluginDir, meta) {
|
|
396
|
+
var skillMdSrc = path.join(pluginDir, 'skill.md');
|
|
397
|
+
var outputDir = path.join(this._outputSkillsDir, name);
|
|
398
|
+
var skillMdDest = path.join(outputDir, 'skill.md');
|
|
399
|
+
var files = [];
|
|
400
|
+
|
|
401
|
+
// Ensure output directory exists
|
|
402
|
+
this._ensureDir(outputDir);
|
|
403
|
+
|
|
404
|
+
// Check if source skill.md exists
|
|
405
|
+
if (fs.existsSync(skillMdSrc)) {
|
|
406
|
+
// Read the source skill content
|
|
407
|
+
var content = fs.readFileSync(skillMdSrc, 'utf-8');
|
|
408
|
+
|
|
409
|
+
// If the skill.md has a front-matter header, keep it.
|
|
410
|
+
// If not, generate one from plugin.json metadata.
|
|
411
|
+
if (!this._hasFrontMatter(content)) {
|
|
412
|
+
content = this._generateSkillFrontMatter(meta) + '\n' + content;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Inject context window configuration
|
|
416
|
+
content = this._injectContextConfig(content, name);
|
|
417
|
+
|
|
418
|
+
fs.writeFileSync(skillMdDest, content, 'utf-8');
|
|
419
|
+
files.push(skillMdDest);
|
|
420
|
+
this._log('info', ' -> ' + skillMdDest);
|
|
421
|
+
} else {
|
|
422
|
+
// Generate a minimal skill.md from metadata
|
|
423
|
+
var generated = this._generateSkillContent(meta);
|
|
424
|
+
fs.writeFileSync(skillMdDest, generated, 'utf-8');
|
|
425
|
+
files.push(skillMdDest);
|
|
426
|
+
this._log('info', ' -> ' + skillMdDest + ' (generated from metadata)');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
name: name,
|
|
431
|
+
type: 'skill',
|
|
432
|
+
outputPath: outputDir,
|
|
433
|
+
files: files,
|
|
434
|
+
};
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Build a hook plugin.
|
|
439
|
+
* Copies index.js from plugin directory to .claude/hooks/{name}/index.js.
|
|
440
|
+
*
|
|
441
|
+
* @param {string} name
|
|
442
|
+
* @param {string} pluginDir
|
|
443
|
+
* @param {object} meta
|
|
444
|
+
* @returns {{ name: string, type: string, outputPath: string, files: string[] }}
|
|
445
|
+
*/
|
|
446
|
+
HarnessBuild.prototype._buildHookPlugin = function _buildHookPlugin(name, pluginDir, meta) {
|
|
447
|
+
var indexJsSrc = path.join(pluginDir, 'index.js');
|
|
448
|
+
var outputDir = path.join(this._outputHooksDir, name);
|
|
449
|
+
var indexJsDest = path.join(outputDir, 'index.js');
|
|
450
|
+
var files = [];
|
|
451
|
+
|
|
452
|
+
this._ensureDir(outputDir);
|
|
453
|
+
|
|
454
|
+
if (fs.existsSync(indexJsSrc)) {
|
|
455
|
+
var content = fs.readFileSync(indexJsSrc, 'utf-8');
|
|
456
|
+
fs.writeFileSync(indexJsDest, content, 'utf-8');
|
|
457
|
+
files.push(indexJsDest);
|
|
458
|
+
this._log('info', ' -> ' + indexJsDest);
|
|
459
|
+
} else {
|
|
460
|
+
// Generate a stub hook
|
|
461
|
+
var stub = this._generateHookStub(meta);
|
|
462
|
+
fs.writeFileSync(indexJsDest, stub, 'utf-8');
|
|
463
|
+
files.push(indexJsDest);
|
|
464
|
+
this._log('info', ' -> ' + indexJsDest + ' (stub generated)');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
name: name,
|
|
469
|
+
type: 'hook',
|
|
470
|
+
outputPath: outputDir,
|
|
471
|
+
files: files,
|
|
472
|
+
};
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Build a validator plugin.
|
|
477
|
+
* Validator plugins don't have a native Claude Code output format,
|
|
478
|
+
* so we record them but don't write files to skills/hooks.
|
|
479
|
+
*
|
|
480
|
+
* @param {string} name
|
|
481
|
+
* @param {string} pluginDir
|
|
482
|
+
* @param {object} meta
|
|
483
|
+
* @returns {{ name: string, type: string, outputPath: string, files: string[] }}
|
|
484
|
+
*/
|
|
485
|
+
HarnessBuild.prototype._buildValidatorPlugin = function _buildValidatorPlugin(name, pluginDir, meta) {
|
|
486
|
+
this._log('info', ' -> Validator plugin (no native output, registered internally)');
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
name: name,
|
|
490
|
+
type: 'validator',
|
|
491
|
+
outputPath: '(internal)',
|
|
492
|
+
files: [],
|
|
493
|
+
};
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Build a provider plugin.
|
|
498
|
+
* Provider plugins are runtime-only, no native Claude Code output.
|
|
499
|
+
*
|
|
500
|
+
* @param {string} name
|
|
501
|
+
* @param {string} pluginDir
|
|
502
|
+
* @param {object} meta
|
|
503
|
+
* @returns {{ name: string, type: string, outputPath: string, files: string[] }}
|
|
504
|
+
*/
|
|
505
|
+
HarnessBuild.prototype._buildProviderPlugin = function _buildProviderPlugin(name, pluginDir, meta) {
|
|
506
|
+
this._log('info', ' -> Provider plugin (no native output, registered internally)');
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
name: name,
|
|
510
|
+
type: 'provider',
|
|
511
|
+
outputPath: '(internal)',
|
|
512
|
+
files: [],
|
|
513
|
+
};
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Format build summary for output.
|
|
518
|
+
*
|
|
519
|
+
* @param {object[]} results
|
|
520
|
+
* @param {object[]} errors
|
|
521
|
+
* @returns {string}
|
|
522
|
+
*/
|
|
523
|
+
HarnessBuild.prototype._formatBuildSummary = function _formatBuildSummary(results, errors) {
|
|
524
|
+
var lines = [];
|
|
525
|
+
lines.push('');
|
|
526
|
+
lines.push('=== Build Report ===');
|
|
527
|
+
|
|
528
|
+
var skillCount = 0;
|
|
529
|
+
var hookCount = 0;
|
|
530
|
+
var validatorCount = 0;
|
|
531
|
+
var providerCount = 0;
|
|
532
|
+
var totalFiles = 0;
|
|
533
|
+
|
|
534
|
+
for (var i = 0; i < results.length; i++) {
|
|
535
|
+
var r = results[i];
|
|
536
|
+
totalFiles += r.files.length;
|
|
537
|
+
switch (r.type) {
|
|
538
|
+
case 'skill': skillCount++; break;
|
|
539
|
+
case 'hook': hookCount++; break;
|
|
540
|
+
case 'validator': validatorCount++; break;
|
|
541
|
+
case 'provider': providerCount++; break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
lines.push('Plugins built: ' + results.length);
|
|
546
|
+
lines.push(' Skills: ' + skillCount);
|
|
547
|
+
lines.push(' Hooks: ' + hookCount);
|
|
548
|
+
lines.push(' Validators: ' + validatorCount);
|
|
549
|
+
lines.push(' Providers: ' + providerCount);
|
|
550
|
+
lines.push('Files written: ' + totalFiles);
|
|
551
|
+
|
|
552
|
+
if (results.length > 0) {
|
|
553
|
+
lines.push('');
|
|
554
|
+
lines.push('--- Output Details ---');
|
|
555
|
+
for (var j = 0; j < results.length; j++) {
|
|
556
|
+
var item = results[j];
|
|
557
|
+
lines.push(' [' + item.type + '] ' + item.name + ' -> ' + item.outputPath);
|
|
558
|
+
for (var k = 0; k < item.files.length; k++) {
|
|
559
|
+
lines.push(' ' + item.files[k]);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (errors.length > 0) {
|
|
565
|
+
lines.push('');
|
|
566
|
+
lines.push('--- Build Errors ---');
|
|
567
|
+
for (var m = 0; m < errors.length; m++) {
|
|
568
|
+
lines.push(' [' + errors[m].plugin + '] ' + errors[m].error);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
lines.push('');
|
|
573
|
+
lines.push(errors.length === 0 ? 'Build SUCCEEDED' : 'Build COMPLETED WITH ERRORS');
|
|
574
|
+
lines.push('');
|
|
575
|
+
|
|
576
|
+
return lines.join('\n');
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
// Content generation helpers
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Check if a skill.md already has YAML front matter.
|
|
585
|
+
* @param {string} content
|
|
586
|
+
* @returns {boolean}
|
|
587
|
+
*/
|
|
588
|
+
HarnessBuild.prototype._hasFrontMatter = function _hasFrontMatter(content) {
|
|
589
|
+
return content.trimLeft().indexOf('---') === 0;
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Generate YAML front matter for a skill from plugin metadata.
|
|
594
|
+
* @param {object} meta
|
|
595
|
+
* @returns {string}
|
|
596
|
+
*/
|
|
597
|
+
HarnessBuild.prototype._generateSkillFrontMatter = function _generateSkillFrontMatter(meta) {
|
|
598
|
+
var lines = ['---'];
|
|
599
|
+
lines.push('name: ' + (meta.name || ''));
|
|
600
|
+
lines.push('description: ' + (meta.description || ''));
|
|
601
|
+
|
|
602
|
+
if (meta.triggers && meta.triggers.length > 0) {
|
|
603
|
+
lines.push('triggers:');
|
|
604
|
+
for (var i = 0; i < meta.triggers.length; i++) {
|
|
605
|
+
lines.push(' - ' + meta.triggers[i]);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (meta.config) {
|
|
610
|
+
if (meta.config.plan_mode_required) {
|
|
611
|
+
lines.push('plan_mode_required: true');
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
lines.push('---');
|
|
616
|
+
return lines.join('\n');
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Generate a full skill.md content from plugin metadata when no skill.md exists.
|
|
621
|
+
* @param {object} meta
|
|
622
|
+
* @returns {string}
|
|
623
|
+
*/
|
|
624
|
+
HarnessBuild.prototype._generateSkillContent = function _generateSkillContent(meta) {
|
|
625
|
+
var frontMatter = this._generateSkillFrontMatter(meta);
|
|
626
|
+
|
|
627
|
+
var body = '\n# ' + (meta.name || 'Unnamed Skill') + '\n';
|
|
628
|
+
body += '\n' + (meta.description || '') + '\n';
|
|
629
|
+
|
|
630
|
+
if (meta.triggers && meta.triggers.length > 0) {
|
|
631
|
+
body += '\n## Triggers\n';
|
|
632
|
+
for (var i = 0; i < meta.triggers.length; i++) {
|
|
633
|
+
body += '- ' + meta.triggers[i] + '\n';
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
body += '\n> Auto-generated by harness-build from plugin.json metadata.\n';
|
|
638
|
+
|
|
639
|
+
return frontMatter + body;
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Generate a stub hook index.js from plugin metadata.
|
|
644
|
+
* @param {object} meta
|
|
645
|
+
* @returns {string}
|
|
646
|
+
*/
|
|
647
|
+
HarnessBuild.prototype._generateHookStub = function _generateHookStub(meta) {
|
|
648
|
+
var lines = [
|
|
649
|
+
'/**',
|
|
650
|
+
' * Hook plugin: ' + (meta.name || 'unnamed'),
|
|
651
|
+
' *',
|
|
652
|
+
' * Auto-generated stub by harness-build.',
|
|
653
|
+
' * Replace with actual hook implementation.',
|
|
654
|
+
' */',
|
|
655
|
+
'',
|
|
656
|
+
'\'use strict\';',
|
|
657
|
+
'',
|
|
658
|
+
'module.exports = {',
|
|
659
|
+
' name: \'' + (meta.name || 'unnamed-hook') + '\',',
|
|
660
|
+
' version: \'' + (meta.version || '0.0.0') + '\',',
|
|
661
|
+
'',
|
|
662
|
+
' /**',
|
|
663
|
+
' * Handle hook invocation.',
|
|
664
|
+
' * @param {object} context',
|
|
665
|
+
' * @returns {Promise<{ allowed: boolean, reason?: string }>}',
|
|
666
|
+
' */',
|
|
667
|
+
' async handle(context) {',
|
|
668
|
+
' return { allowed: true };',
|
|
669
|
+
' },',
|
|
670
|
+
'};',
|
|
671
|
+
'',
|
|
672
|
+
];
|
|
673
|
+
return lines.join('\n');
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
// ---------------------------------------------------------------------------
|
|
677
|
+
// Utilities
|
|
678
|
+
// ---------------------------------------------------------------------------
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Ensure a directory exists, creating it recursively if needed.
|
|
682
|
+
* @param {string} dirPath
|
|
683
|
+
*/
|
|
684
|
+
HarnessBuild.prototype._ensureDir = function _ensureDir(dirPath) {
|
|
685
|
+
if (!fs.existsSync(dirPath)) {
|
|
686
|
+
this._mkdirRecursive(dirPath);
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Recursively create directories (like mkdir -p).
|
|
692
|
+
* @param {string} dirPath
|
|
693
|
+
*/
|
|
694
|
+
HarnessBuild.prototype._mkdirRecursive = function _mkdirRecursive(dirPath) {
|
|
695
|
+
var parent = path.dirname(dirPath);
|
|
696
|
+
if (!fs.existsSync(parent)) {
|
|
697
|
+
this._mkdirRecursive(parent);
|
|
698
|
+
}
|
|
699
|
+
fs.mkdirSync(dirPath);
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Internal logging.
|
|
704
|
+
* @param {string} level
|
|
705
|
+
* @param {string} message
|
|
706
|
+
*/
|
|
707
|
+
HarnessBuild.prototype._log = function _log(level, message) {
|
|
708
|
+
var prefix = '[harness-build] [' + level + ']';
|
|
709
|
+
if (level === 'error') {
|
|
710
|
+
console.error(prefix, message);
|
|
711
|
+
} else if (level === 'warn') {
|
|
712
|
+
console.warn(prefix, message);
|
|
713
|
+
} else {
|
|
714
|
+
console.log(prefix, message);
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Read and cache the harness config YAML file.
|
|
720
|
+
* @returns {object}
|
|
721
|
+
*/
|
|
722
|
+
HarnessBuild.prototype._readHarnessConfig = function _readHarnessConfig() {
|
|
723
|
+
if (this._harnessConfig !== null) {
|
|
724
|
+
return this._harnessConfig;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
var configPath = path.join(this._rootDir, '.claude', 'config', 'harness-config.yaml');
|
|
728
|
+
try {
|
|
729
|
+
var content = fs.readFileSync(configPath, 'utf-8');
|
|
730
|
+
// Extract context_window section using simple regex
|
|
731
|
+
var result = {};
|
|
732
|
+
var inSection = false;
|
|
733
|
+
var sectionIndent = -1;
|
|
734
|
+
var lines = content.split('\n');
|
|
735
|
+
|
|
736
|
+
for (var i = 0; i < lines.length; i++) {
|
|
737
|
+
var line = lines[i];
|
|
738
|
+
|
|
739
|
+
// Detect context_window section start
|
|
740
|
+
if (/^context_window\s*:/.test(line.trim())) {
|
|
741
|
+
inSection = true;
|
|
742
|
+
sectionIndent = line.search(/\S/);
|
|
743
|
+
result = { _source: 'context_window' };
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (inSection) {
|
|
748
|
+
var trimmed = line.trim();
|
|
749
|
+
|
|
750
|
+
// Stop at next top-level section (--- separator or same-indent key)
|
|
751
|
+
if (trimmed === '---' || (line.search(/\S/) >= 0 && line.search(/\S/) <= sectionIndent && !/^[\s#]/.test(trimmed) && trimmed.indexOf('context_window') === -1 && /^[a-z]/.test(trimmed))) {
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Skip empty lines and comments
|
|
756
|
+
if (!trimmed || trimmed.indexOf('#') === 0) {
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Parse simple key-value pairs (flat or one level nested)
|
|
761
|
+
var colonIdx = trimmed.indexOf(':');
|
|
762
|
+
if (colonIdx === -1) continue;
|
|
763
|
+
|
|
764
|
+
var key = trimmed.substring(0, colonIdx).trim();
|
|
765
|
+
var valuePart = trimmed.substring(colonIdx + 1).trim();
|
|
766
|
+
|
|
767
|
+
// Remove inline comments
|
|
768
|
+
var commentIdx = valuePart.indexOf(' #');
|
|
769
|
+
if (commentIdx >= 0) {
|
|
770
|
+
valuePart = valuePart.substring(0, commentIdx).trim();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Skip nested keys like "thresholds:" (we handle them inline)
|
|
774
|
+
if (valuePart === '' || valuePart === null) {
|
|
775
|
+
// This is a nested section header, read its children
|
|
776
|
+
var nestedIndent = lines[i] ? lines[i].search(/\S/) : -1;
|
|
777
|
+
var nested = {};
|
|
778
|
+
var j = i + 1;
|
|
779
|
+
for (; j < lines.length; j++) {
|
|
780
|
+
var nLine = lines[j];
|
|
781
|
+
if (!nLine.trim() || nLine.trim().indexOf('#') === 0) continue;
|
|
782
|
+
var nIndent = nLine.search(/\S/);
|
|
783
|
+
if (nIndent <= nestedIndent) break;
|
|
784
|
+
var nTrimmed = nLine.trim();
|
|
785
|
+
// Handle list items "- key: value" or "- value"
|
|
786
|
+
if (nTrimmed.indexOf('- ') === 0) {
|
|
787
|
+
nTrimmed = nTrimmed.substring(2);
|
|
788
|
+
}
|
|
789
|
+
var nColonIdx = nTrimmed.indexOf(':');
|
|
790
|
+
if (nColonIdx >= 0) {
|
|
791
|
+
var nKey = nTrimmed.substring(0, nColonIdx).trim();
|
|
792
|
+
var nVal = nTrimmed.substring(nColonIdx + 1).trim();
|
|
793
|
+
var nCommentIdx = nVal.indexOf(' #');
|
|
794
|
+
if (nCommentIdx >= 0) nVal = nVal.substring(0, nCommentIdx).trim();
|
|
795
|
+
nested[nKey] = _parseValue(nVal);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
result[key] = nested;
|
|
799
|
+
// Advance outer loop past the nested section
|
|
800
|
+
i = j - 1;
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Remove surrounding quotes
|
|
805
|
+
if ((valuePart.charAt(0) === '"' && valuePart.charAt(valuePart.length - 1) === '"') ||
|
|
806
|
+
(valuePart.charAt(0) === "'" && valuePart.charAt(valuePart.length - 1) === "'")) {
|
|
807
|
+
valuePart = valuePart.substring(1, valuePart.length - 1);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
result[key] = _parseValue(valuePart);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
this._harnessConfig = (inSection && result._source) ? result : {};
|
|
815
|
+
delete this._harnessConfig._source;
|
|
816
|
+
} catch (err) {
|
|
817
|
+
this._harnessConfig = {};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
return this._harnessConfig;
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Parse a YAML scalar value.
|
|
825
|
+
*/
|
|
826
|
+
function _parseValue(val) {
|
|
827
|
+
if (val === 'true') return true;
|
|
828
|
+
if (val === 'false') return false;
|
|
829
|
+
if (val === 'null' || val === '~') return null;
|
|
830
|
+
var num = Number(val);
|
|
831
|
+
if (!isNaN(num) && val !== '') return num;
|
|
832
|
+
return val;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Inject context window configuration into a skill.md file.
|
|
837
|
+
* Inserts a <!-- CONTEXT-CONFIG --> block after front matter.
|
|
838
|
+
*
|
|
839
|
+
* @param {string} content - the skill.md content
|
|
840
|
+
* @param {string} pluginName - the plugin name for per-plugin overrides
|
|
841
|
+
* @returns {string} content with injected config block
|
|
842
|
+
*/
|
|
843
|
+
HarnessBuild.prototype._injectContextConfig = function _injectContextConfig(content, pluginName) {
|
|
844
|
+
var config = this._readHarnessConfig();
|
|
845
|
+
if (!config || Object.keys(config).length === 0) {
|
|
846
|
+
return content; // No context_window config, skip injection
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Apply per-plugin override if exists
|
|
850
|
+
if (config.overrides && config.overrides[pluginName]) {
|
|
851
|
+
var override = config.overrides[pluginName];
|
|
852
|
+
for (var k in override) {
|
|
853
|
+
if (override.hasOwnProperty(k)) {
|
|
854
|
+
config[k] = override[k];
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
delete config.overrides;
|
|
859
|
+
|
|
860
|
+
// Build config block
|
|
861
|
+
var lines = ['\n<!-- CONTEXT-CONFIG'];
|
|
862
|
+
for (var key in config) {
|
|
863
|
+
if (!config.hasOwnProperty(key)) continue;
|
|
864
|
+
var val = config[key];
|
|
865
|
+
if (typeof val === 'object' && val !== null) {
|
|
866
|
+
// Flatten nested objects (e.g., thresholds)
|
|
867
|
+
var parts = [];
|
|
868
|
+
for (var sk in val) {
|
|
869
|
+
if (val.hasOwnProperty(sk)) {
|
|
870
|
+
parts.push(sk + '=' + val[sk]);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
lines.push(key + ': ' + parts.join(', '));
|
|
874
|
+
} else {
|
|
875
|
+
lines.push(key + ': ' + val);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
lines.push('CONTEXT-CONFIG -->\n');
|
|
879
|
+
|
|
880
|
+
var configBlock = lines.join('\n');
|
|
881
|
+
|
|
882
|
+
// Find insertion point: after front matter closing ---
|
|
883
|
+
var fmCloseIdx = content.indexOf('---', 1); // skip first ---
|
|
884
|
+
if (fmCloseIdx === -1) {
|
|
885
|
+
// No front matter, prepend
|
|
886
|
+
return configBlock + content;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
var afterFm = content.indexOf('\n', fmCloseIdx + 3);
|
|
890
|
+
if (afterFm === -1) {
|
|
891
|
+
afterFm = content.length;
|
|
892
|
+
} else {
|
|
893
|
+
afterFm += 1; // skip the newline itself
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return content.substring(0, afterFm) + configBlock + content.substring(afterFm);
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
// ---------------------------------------------------------------------------
|
|
900
|
+
// CLI entry point
|
|
901
|
+
// ---------------------------------------------------------------------------
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Run the CLI.
|
|
905
|
+
* @param {string[]} argv - process.argv
|
|
906
|
+
*/
|
|
907
|
+
HarnessBuild.run = function run(argv) {
|
|
908
|
+
var args = argv.slice(2);
|
|
909
|
+
var mode = 'build'; // default mode
|
|
910
|
+
|
|
911
|
+
for (var i = 0; i < args.length; i++) {
|
|
912
|
+
if (args[i] === '--validate') {
|
|
913
|
+
mode = 'validate';
|
|
914
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
915
|
+
console.log('Usage: node plugins/runtime/harness-build.js [--validate]');
|
|
916
|
+
console.log('');
|
|
917
|
+
console.log('Options:');
|
|
918
|
+
console.log(' --validate Validate plugin.json files without building');
|
|
919
|
+
console.log(' --help, -h Show this help message');
|
|
920
|
+
process.exit(0);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
var builder = new HarnessBuild();
|
|
925
|
+
|
|
926
|
+
if (mode === 'validate') {
|
|
927
|
+
var result = builder.validate();
|
|
928
|
+
console.log(result.summary);
|
|
929
|
+
process.exit(result.valid ? 0 : 1);
|
|
930
|
+
} else {
|
|
931
|
+
var buildResult = builder.build();
|
|
932
|
+
console.log(buildResult.summary);
|
|
933
|
+
process.exit(buildResult.success ? 0 : 1);
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
// ---------------------------------------------------------------------------
|
|
938
|
+
// Settings merge
|
|
939
|
+
// ---------------------------------------------------------------------------
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Merge tackle-harness hooks into the target project's .claude/settings.json.
|
|
943
|
+
* Reads existing settings, adds tackle-harness-specific hooks, and writes back.
|
|
944
|
+
* Idempotent: skips hooks that are already registered.
|
|
945
|
+
*
|
|
946
|
+
* @param {string} targetRoot - target project root directory
|
|
947
|
+
* @param {string} packageRoot - this package's root directory (node_modules/tackle-harness/)
|
|
948
|
+
*/
|
|
949
|
+
HarnessBuild.prototype.updateSettings = function updateSettings(targetRoot, packageRoot) {
|
|
950
|
+
var fs = require('fs');
|
|
951
|
+
var path = require('path');
|
|
952
|
+
var settingsPath = path.join(targetRoot, '.claude', 'settings.json');
|
|
953
|
+
var settings = {};
|
|
954
|
+
|
|
955
|
+
// Read existing settings if present
|
|
956
|
+
if (fs.existsSync(settingsPath)) {
|
|
957
|
+
try {
|
|
958
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
959
|
+
} catch (e) {
|
|
960
|
+
settings = {};
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Ensure hooks structure exists
|
|
965
|
+
if (!settings.hooks) settings.hooks = {};
|
|
966
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
967
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
968
|
+
|
|
969
|
+
// Resolve hook script path relative to target project
|
|
970
|
+
var hookScriptPath = path.join(packageRoot, 'plugins', 'core', 'hook-skill-gate', 'index.js');
|
|
971
|
+
// Use forward slashes for cross-platform compatibility
|
|
972
|
+
var hookScriptRelative = path.relative(targetRoot, hookScriptPath).replace(/\\/g, '/');
|
|
973
|
+
var hookCmd = 'node "' + hookScriptRelative + '"';
|
|
974
|
+
|
|
975
|
+
// Add PreToolUse hook for Edit|Write (if not already present)
|
|
976
|
+
var preMatcher = 'Edit|Write';
|
|
977
|
+
var preExists = settings.hooks.PreToolUse.some(function (h) {
|
|
978
|
+
return h.matcher === preMatcher;
|
|
979
|
+
});
|
|
980
|
+
if (!preExists) {
|
|
981
|
+
settings.hooks.PreToolUse.push({
|
|
982
|
+
matcher: preMatcher,
|
|
983
|
+
hooks: [{
|
|
984
|
+
type: 'command',
|
|
985
|
+
command: hookCmd + ' --pre-tool'
|
|
986
|
+
}]
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Add PostToolUse hook for Skill (if not already present)
|
|
991
|
+
var postMatcher = 'Skill';
|
|
992
|
+
var postExists = settings.hooks.PostToolUse.some(function (h) {
|
|
993
|
+
return h.matcher === postMatcher;
|
|
994
|
+
});
|
|
995
|
+
if (!postExists) {
|
|
996
|
+
settings.hooks.PostToolUse.push({
|
|
997
|
+
matcher: postMatcher,
|
|
998
|
+
hooks: [{
|
|
999
|
+
type: 'command',
|
|
1000
|
+
command: hookCmd + ' --post-skill'
|
|
1001
|
+
}]
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Write back
|
|
1006
|
+
this._ensureDir(path.dirname(settingsPath));
|
|
1007
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
// ---------------------------------------------------------------------------
|
|
1011
|
+
// Module exports
|
|
1012
|
+
// ---------------------------------------------------------------------------
|
|
1013
|
+
|
|
1014
|
+
module.exports = HarnessBuild;
|
|
1015
|
+
|
|
1016
|
+
// Run directly if executed as main
|
|
1017
|
+
if (require.main === module) {
|
|
1018
|
+
HarnessBuild.run(process.argv);
|
|
1019
|
+
}
|