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,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider: Role Registry
|
|
3
|
+
*
|
|
4
|
+
* Wraps role-registry.yaml and individual role YAML files to provide
|
|
5
|
+
* role matching, querying, and listing capabilities.
|
|
6
|
+
* Implements the ProviderPlugin interface from plugin-interface.js.
|
|
7
|
+
*
|
|
8
|
+
* Capabilities:
|
|
9
|
+
* - match(query) match roles by keyword, alias, or tag
|
|
10
|
+
* - getAll() return all loaded roles
|
|
11
|
+
* - getById(roleId) return a specific role by its id
|
|
12
|
+
* - getByAlias(alias) lookup role by Chinese alias
|
|
13
|
+
* - getAliases() return the full alias mapping
|
|
14
|
+
* - getCategories() return role category definitions
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
var path = require('path');
|
|
20
|
+
var fs = require('fs');
|
|
21
|
+
var { ProviderPlugin } = require('../../contracts/plugin-interface');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Minimal YAML parser for flat and simple nested structures.
|
|
25
|
+
* Handles: key: value, nested via indentation, list items via "- ", comments via #.
|
|
26
|
+
* Returns a plain JS object.
|
|
27
|
+
*/
|
|
28
|
+
function parseSimpleYaml(content) {
|
|
29
|
+
var lines = content.split('\n');
|
|
30
|
+
var root = {};
|
|
31
|
+
var stack = [{ obj: root, indent: -1 }];
|
|
32
|
+
|
|
33
|
+
for (var i = 0; i < lines.length; i++) {
|
|
34
|
+
var rawLine = lines[i];
|
|
35
|
+
|
|
36
|
+
// Strip comments (but not inside quoted strings - simple approach)
|
|
37
|
+
var commentIdx = rawLine.indexOf('#');
|
|
38
|
+
if (commentIdx === 0) continue; // full-line comment
|
|
39
|
+
var line = commentIdx > 0 ? rawLine.substring(0, commentIdx) : rawLine;
|
|
40
|
+
line = line.replace(/\s+$/, ''); // trim trailing whitespace
|
|
41
|
+
|
|
42
|
+
if (!line) continue;
|
|
43
|
+
|
|
44
|
+
// Calculate indentation
|
|
45
|
+
var indent = 0;
|
|
46
|
+
while (indent < line.length && (line[indent] === ' ' || line[indent] === '\t')) {
|
|
47
|
+
indent++;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Pop stack to find the correct parent
|
|
51
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
52
|
+
stack.pop();
|
|
53
|
+
}
|
|
54
|
+
var parent = stack[stack.length - 1].obj;
|
|
55
|
+
|
|
56
|
+
// List item
|
|
57
|
+
if (line.indexOf('- ', indent) === indent) {
|
|
58
|
+
var value = line.substring(indent + 2).trim();
|
|
59
|
+
// value could be "key: val" format in a list
|
|
60
|
+
var kvMatch = value.match(/^(\S+):\s*(.*)$/);
|
|
61
|
+
if (kvMatch && parent && typeof parent === 'object' && !Array.isArray(parent)) {
|
|
62
|
+
// If parent is not an array, this is a list of objects that should
|
|
63
|
+
// be converted - but we handle list items as array entries
|
|
64
|
+
if (!Array.isArray(parent)) {
|
|
65
|
+
// We need the grandparent to have the array
|
|
66
|
+
// Skip complex list-of-objects for role files
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Determine parent array
|
|
70
|
+
// The key name should be on the previous stack entry
|
|
71
|
+
var listValue = parseYamlValue(value);
|
|
72
|
+
// Ensure parent is the array
|
|
73
|
+
if (Array.isArray(parent)) {
|
|
74
|
+
parent.push(listValue);
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Key-value pair
|
|
80
|
+
var colonIdx = line.indexOf(':', indent);
|
|
81
|
+
if (colonIdx === -1) continue;
|
|
82
|
+
|
|
83
|
+
var key = line.substring(indent, colonIdx).trim();
|
|
84
|
+
var val = line.substring(colonIdx + 1).trim();
|
|
85
|
+
|
|
86
|
+
if (!val) {
|
|
87
|
+
// Nested structure - check if next non-empty line is a list
|
|
88
|
+
var nextNonEmpty = findNextNonEmpty(lines, i + 1);
|
|
89
|
+
if (nextNonEmpty && nextNonEmpty.line.indexOf('- ', nextNonEmpty.indent) === nextNonEmpty.indent) {
|
|
90
|
+
// It's a list
|
|
91
|
+
var arr = [];
|
|
92
|
+
parent[key] = arr;
|
|
93
|
+
stack.push({ obj: arr, indent: indent });
|
|
94
|
+
} else {
|
|
95
|
+
// It's an object
|
|
96
|
+
var newObj = {};
|
|
97
|
+
parent[key] = newObj;
|
|
98
|
+
stack.push({ obj: newObj, indent: indent });
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
parent[key] = parseYamlValue(val);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return root;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse a YAML scalar value.
|
|
110
|
+
*/
|
|
111
|
+
function parseYamlValue(val) {
|
|
112
|
+
if (!val) return val;
|
|
113
|
+
|
|
114
|
+
// Quoted string
|
|
115
|
+
if ((val[0] === '"' && val[val.length - 1] === '"') ||
|
|
116
|
+
(val[0] === "'" && val[val.length - 1] === "'")) {
|
|
117
|
+
return val.substring(1, val.length - 1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Boolean
|
|
121
|
+
if (val === 'true') return true;
|
|
122
|
+
if (val === 'false') return false;
|
|
123
|
+
|
|
124
|
+
// Number
|
|
125
|
+
if (/^-?\d+$/.test(val)) return parseInt(val, 10);
|
|
126
|
+
if (/^-?\d+\.\d+$/.test(val)) return parseFloat(val);
|
|
127
|
+
|
|
128
|
+
// Plain string
|
|
129
|
+
return val;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Find the next non-empty, non-comment line starting from the given index.
|
|
134
|
+
*/
|
|
135
|
+
function findNextNonEmpty(lines, startIdx) {
|
|
136
|
+
for (var i = startIdx; i < lines.length; i++) {
|
|
137
|
+
var l = lines[i];
|
|
138
|
+
var trimmed = l.trim();
|
|
139
|
+
if (!trimmed || trimmed[0] === '#') continue;
|
|
140
|
+
var indent = 0;
|
|
141
|
+
while (indent < l.length && (l[indent] === ' ' || l[indent] === '\t')) indent++;
|
|
142
|
+
return { line: l, indent: indent, trimmed: trimmed, lineNum: i };
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Parse a role definition YAML file.
|
|
149
|
+
* Extracts structured role data from a role file like coordinator.yaml.
|
|
150
|
+
*/
|
|
151
|
+
function parseRoleYaml(content) {
|
|
152
|
+
var data = parseSimpleYaml(content);
|
|
153
|
+
|
|
154
|
+
// Handle multi-line description (pipe | syntax) - already stripped by our parser
|
|
155
|
+
// Just return the parsed structure
|
|
156
|
+
return {
|
|
157
|
+
id: data.id || '',
|
|
158
|
+
name: data.name || '',
|
|
159
|
+
version: data.version || '1.0.0',
|
|
160
|
+
tier: data.tier || '',
|
|
161
|
+
description: data.description || '',
|
|
162
|
+
expertise: Array.isArray(data.expertise) ? data.expertise : [],
|
|
163
|
+
keywords: Array.isArray(data.keywords) ? data.keywords : [],
|
|
164
|
+
task_types: Array.isArray(data.task_types) ? data.task_types : [],
|
|
165
|
+
module_tags: Array.isArray(data.module_tags) ? data.module_tags : [],
|
|
166
|
+
tools: Array.isArray(data.tools) ? data.tools : [],
|
|
167
|
+
subagent_type: data.subagent_type || 'general-purpose',
|
|
168
|
+
abstract: data.abstract || false,
|
|
169
|
+
memory_file: data.memory_file || '',
|
|
170
|
+
experience_tags: Array.isArray(data.experience_tags) ? data.experience_tags : [],
|
|
171
|
+
prompt_template: data.prompt_template || '',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* RoleRegistryProvider - provides role matching and querying
|
|
178
|
+
*/
|
|
179
|
+
class RoleRegistryProvider extends ProviderPlugin {
|
|
180
|
+
constructor() {
|
|
181
|
+
super();
|
|
182
|
+
this.name = 'provider-role-registry';
|
|
183
|
+
this.version = '1.0.0';
|
|
184
|
+
this.description = 'Role Registry Provider';
|
|
185
|
+
this.provides = 'provider:role-registry';
|
|
186
|
+
this.dependencies = {};
|
|
187
|
+
|
|
188
|
+
/** @type {object|null} parsed registry data */
|
|
189
|
+
this._registry = null;
|
|
190
|
+
/** @type {Map<string, object>} loaded role definitions by id */
|
|
191
|
+
this._roles = new Map();
|
|
192
|
+
/** @type {object} alias mapping */
|
|
193
|
+
this._aliases = {};
|
|
194
|
+
/** @type {string} project root */
|
|
195
|
+
this._projectRoot = '';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Activate - load and parse the registry and role files.
|
|
200
|
+
* @param {PluginContext} context
|
|
201
|
+
*/
|
|
202
|
+
async onActivate(context) {
|
|
203
|
+
this._projectRoot = this._resolveProjectRoot();
|
|
204
|
+
var registryPath = path.join(this._projectRoot, '.claude', 'agents', 'role-registry.yaml');
|
|
205
|
+
this._loadRegistry(registryPath);
|
|
206
|
+
this._loadRoleFiles();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Factory - return the provider API.
|
|
211
|
+
* @param {PluginContext} context
|
|
212
|
+
* @returns {Promise<object>}
|
|
213
|
+
*/
|
|
214
|
+
async factory(context) {
|
|
215
|
+
var self = this;
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
/**
|
|
219
|
+
* Match roles by keyword, alias, tag, or expertise.
|
|
220
|
+
* @param {string} query - search term
|
|
221
|
+
* @returns {{ id: string, name: string, score: number }[]}
|
|
222
|
+
*/
|
|
223
|
+
match: function (query) {
|
|
224
|
+
return self._match(query);
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get all loaded roles.
|
|
229
|
+
* @returns {object[]}
|
|
230
|
+
*/
|
|
231
|
+
getAll: function () {
|
|
232
|
+
return Array.from(self._roles.values());
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get a role by its ID.
|
|
237
|
+
* @param {string} roleId
|
|
238
|
+
* @returns {object|undefined}
|
|
239
|
+
*/
|
|
240
|
+
getById: function (roleId) {
|
|
241
|
+
return self._roles.get(roleId);
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get a role by its Chinese alias.
|
|
246
|
+
* @param {string} alias
|
|
247
|
+
* @returns {object|undefined}
|
|
248
|
+
*/
|
|
249
|
+
getByAlias: function (alias) {
|
|
250
|
+
var roleId = self._aliases[alias];
|
|
251
|
+
if (!roleId) return undefined;
|
|
252
|
+
return self._roles.get(roleId);
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get the full alias mapping.
|
|
257
|
+
* @returns {object}
|
|
258
|
+
*/
|
|
259
|
+
getAliases: function () {
|
|
260
|
+
return Object.assign({}, self._aliases);
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get category definitions from the registry.
|
|
265
|
+
* @returns {object}
|
|
266
|
+
*/
|
|
267
|
+
getCategories: function () {
|
|
268
|
+
if (!self._registry || !self._registry.categories) return {};
|
|
269
|
+
return self._registry.categories;
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Get the default fallback role ID.
|
|
274
|
+
* @returns {string}
|
|
275
|
+
*/
|
|
276
|
+
getDefaultRole: function () {
|
|
277
|
+
return (self._registry && self._registry.default_role) || 'implementer';
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get tag-to-role mappings.
|
|
282
|
+
* @returns {object}
|
|
283
|
+
*/
|
|
284
|
+
getTagMappings: function () {
|
|
285
|
+
if (!self._registry || !self._registry.tag_to_role) return {};
|
|
286
|
+
return self._registry.tag_to_role;
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Match roles by query string.
|
|
293
|
+
* Scores each role based on keyword, expertise, alias, and tag matches.
|
|
294
|
+
* @param {string} query
|
|
295
|
+
* @returns {{ id: string, name: string, score: number }[]}
|
|
296
|
+
*/
|
|
297
|
+
_match(query) {
|
|
298
|
+
if (!query) return [];
|
|
299
|
+
var self = this;
|
|
300
|
+
var q = query.toLowerCase();
|
|
301
|
+
var scores = new Map();
|
|
302
|
+
var weights = (this._registry && this._registry.matching_weights) || {
|
|
303
|
+
keyword: 0.5,
|
|
304
|
+
task_type: 0.3,
|
|
305
|
+
module_tag: 0.2,
|
|
306
|
+
inheritance_factor: 0.5,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// Check aliases (exact match)
|
|
310
|
+
var aliasRoleIds = [];
|
|
311
|
+
for (var alias in this._aliases) {
|
|
312
|
+
if (alias.toLowerCase().indexOf(q) !== -1) {
|
|
313
|
+
aliasRoleIds.push(this._aliases[alias]);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check tag_to_role
|
|
318
|
+
var tagMappings = (this._registry && this._registry.tag_to_role) || {};
|
|
319
|
+
for (var tag in tagMappings) {
|
|
320
|
+
var tagClean = tag.replace(/[\[\]]/g, '').toLowerCase();
|
|
321
|
+
if (tagClean.indexOf(q) !== -1 || q.indexOf(tagClean) !== -1) {
|
|
322
|
+
var roleIds = tagMappings[tag];
|
|
323
|
+
if (Array.isArray(roleIds)) {
|
|
324
|
+
for (var t = 0; t < roleIds.length; t++) {
|
|
325
|
+
var rid = roleIds[t];
|
|
326
|
+
var prev = scores.get(rid) || 0;
|
|
327
|
+
scores.set(rid, prev + 0.4);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Score each role by keywords, expertise, and task_types
|
|
334
|
+
this._roles.forEach(function (role, id) {
|
|
335
|
+
var score = 0;
|
|
336
|
+
|
|
337
|
+
// Keyword match
|
|
338
|
+
if (role.keywords) {
|
|
339
|
+
for (var k = 0; k < role.keywords.length; k++) {
|
|
340
|
+
if (role.keywords[k].toLowerCase().indexOf(q) !== -1) {
|
|
341
|
+
score += weights.keyword;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Expertise match
|
|
348
|
+
if (role.expertise) {
|
|
349
|
+
for (var e = 0; e < role.expertise.length; e++) {
|
|
350
|
+
if (role.expertise[e].toLowerCase().indexOf(q) !== -1) {
|
|
351
|
+
score += 0.3;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Task type match
|
|
358
|
+
if (role.task_types) {
|
|
359
|
+
for (var tt = 0; tt < role.task_types.length; tt++) {
|
|
360
|
+
if (role.task_types[tt].toLowerCase().indexOf(q) !== -1) {
|
|
361
|
+
score += weights.task_type;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Module tag match
|
|
368
|
+
if (role.module_tags) {
|
|
369
|
+
for (var mt = 0; mt < role.module_tags.length; mt++) {
|
|
370
|
+
if (role.module_tags[mt].toLowerCase().indexOf(q) !== -1) {
|
|
371
|
+
score += weights.module_tag;
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Name match
|
|
378
|
+
if (role.name && role.name.toLowerCase().indexOf(q) !== -1) {
|
|
379
|
+
score += 0.4;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ID match
|
|
383
|
+
if (id.toLowerCase().indexOf(q) !== -1) {
|
|
384
|
+
score += 0.3;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Boost for alias matches
|
|
388
|
+
if (aliasRoleIds.indexOf(id) !== -1) {
|
|
389
|
+
score += 0.5;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (score > 0) {
|
|
393
|
+
scores.set(id, (scores.get(id) || 0) + score);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Convert scores to sorted results
|
|
398
|
+
var results = [];
|
|
399
|
+
scores.forEach(function (score, id) {
|
|
400
|
+
var role = self._roles.get(id);
|
|
401
|
+
results.push({
|
|
402
|
+
id: id,
|
|
403
|
+
name: role ? role.name : id,
|
|
404
|
+
score: Math.round(score * 100) / 100,
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
results.sort(function (a, b) { return b.score - a.score; });
|
|
409
|
+
return results;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Load and parse the role registry YAML.
|
|
414
|
+
* @param {string} filePath
|
|
415
|
+
*/
|
|
416
|
+
_loadRegistry(filePath) {
|
|
417
|
+
try {
|
|
418
|
+
var content = fs.readFileSync(filePath, 'utf-8');
|
|
419
|
+
this._registry = parseSimpleYaml(content);
|
|
420
|
+
this._aliases = this._registry.aliases || {};
|
|
421
|
+
} catch (err) {
|
|
422
|
+
this._registry = { categories: {}, aliases: {}, default_role: 'implementer' };
|
|
423
|
+
this._aliases = {};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Load all role files referenced in the registry.
|
|
429
|
+
*/
|
|
430
|
+
_loadRoleFiles() {
|
|
431
|
+
var roleFiles = (this._registry && this._registry.role_files) || [];
|
|
432
|
+
var agentsDir = path.join(this._projectRoot, '.claude', 'agents');
|
|
433
|
+
|
|
434
|
+
for (var i = 0; i < roleFiles.length; i++) {
|
|
435
|
+
var entry = roleFiles[i];
|
|
436
|
+
// entry can be a string "path: ..." or an object { path: "..." }
|
|
437
|
+
var relPath = '';
|
|
438
|
+
if (typeof entry === 'string') {
|
|
439
|
+
// parse "path: value"
|
|
440
|
+
var match = entry.match(/path:\s*(.+)/);
|
|
441
|
+
relPath = match ? match[1].trim() : entry;
|
|
442
|
+
} else if (entry && entry.path) {
|
|
443
|
+
relPath = entry.path;
|
|
444
|
+
}
|
|
445
|
+
if (!relPath) continue;
|
|
446
|
+
|
|
447
|
+
var fullPath = path.join(agentsDir, relPath);
|
|
448
|
+
try {
|
|
449
|
+
var content = fs.readFileSync(fullPath, 'utf-8');
|
|
450
|
+
var roleData = parseRoleYaml(content);
|
|
451
|
+
if (roleData.id) {
|
|
452
|
+
this._roles.set(roleData.id, roleData);
|
|
453
|
+
}
|
|
454
|
+
} catch (err) {
|
|
455
|
+
// Skip unreadable role files silently
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Resolve the project root directory.
|
|
462
|
+
* @returns {string}
|
|
463
|
+
*/
|
|
464
|
+
_resolveProjectRoot() {
|
|
465
|
+
var dir = process.cwd();
|
|
466
|
+
for (var i = 0; i < 10; i++) {
|
|
467
|
+
if (fs.existsSync(path.join(dir, 'task.md'))) return dir;
|
|
468
|
+
if (fs.existsSync(path.join(dir, '.claude'))) return dir;
|
|
469
|
+
var parent = path.dirname(dir);
|
|
470
|
+
if (parent === dir) break;
|
|
471
|
+
dir = parent;
|
|
472
|
+
}
|
|
473
|
+
return process.cwd();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
module.exports = RoleRegistryProvider;
|