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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +259 -0
  3. package/README.md +261 -0
  4. package/bin/tackle.js +150 -0
  5. package/package.json +29 -0
  6. package/plugins/contracts/plugin-interface.js +244 -0
  7. package/plugins/core/hook-skill-gate/index.js +437 -0
  8. package/plugins/core/hook-skill-gate/plugin.json +12 -0
  9. package/plugins/core/provider-memory-store/index.js +403 -0
  10. package/plugins/core/provider-memory-store/plugin.json +9 -0
  11. package/plugins/core/provider-role-registry/index.js +477 -0
  12. package/plugins/core/provider-role-registry/plugin.json +9 -0
  13. package/plugins/core/provider-state-store/index.js +244 -0
  14. package/plugins/core/provider-state-store/plugin.json +9 -0
  15. package/plugins/core/skill-agent-dispatcher/plugin.json +13 -0
  16. package/plugins/core/skill-agent-dispatcher/skill.md +912 -0
  17. package/plugins/core/skill-batch-task-creator/plugin.json +13 -0
  18. package/plugins/core/skill-batch-task-creator/skill.md +616 -0
  19. package/plugins/core/skill-checklist/plugin.json +10 -0
  20. package/plugins/core/skill-checklist/skill.md +115 -0
  21. package/plugins/core/skill-completion-report/plugin.json +10 -0
  22. package/plugins/core/skill-completion-report/skill.md +331 -0
  23. package/plugins/core/skill-experience-logger/plugin.json +10 -0
  24. package/plugins/core/skill-experience-logger/skill.md +235 -0
  25. package/plugins/core/skill-human-checkpoint/plugin.json +10 -0
  26. package/plugins/core/skill-human-checkpoint/skill.md +194 -0
  27. package/plugins/core/skill-progress-tracker/plugin.json +10 -0
  28. package/plugins/core/skill-progress-tracker/skill.md +204 -0
  29. package/plugins/core/skill-role-manager/plugin.json +10 -0
  30. package/plugins/core/skill-role-manager/skill.md +252 -0
  31. package/plugins/core/skill-split-work-package/plugin.json +13 -0
  32. package/plugins/core/skill-split-work-package/skill.md +446 -0
  33. package/plugins/core/skill-task-creator/plugin.json +13 -0
  34. package/plugins/core/skill-task-creator/skill.md +744 -0
  35. package/plugins/core/skill-team-cleanup/plugin.json +10 -0
  36. package/plugins/core/skill-team-cleanup/skill.md +266 -0
  37. package/plugins/core/skill-workflow-orchestrator/plugin.json +13 -0
  38. package/plugins/core/skill-workflow-orchestrator/skill.md +274 -0
  39. package/plugins/core/validator-doc-sync/index.js +248 -0
  40. package/plugins/core/validator-doc-sync/plugin.json +9 -0
  41. package/plugins/core/validator-work-package/index.js +300 -0
  42. package/plugins/core/validator-work-package/plugin.json +9 -0
  43. package/plugins/plugin-registry.json +118 -0
  44. package/plugins/runtime/config-manager.js +306 -0
  45. package/plugins/runtime/event-bus.js +187 -0
  46. package/plugins/runtime/harness-build.js +1019 -0
  47. package/plugins/runtime/logger.js +174 -0
  48. package/plugins/runtime/plugin-loader.js +339 -0
  49. 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;
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "provider-role-registry",
3
+ "version": "1.0.0",
4
+ "type": "provider",
5
+ "description": "角色注册表 Provider - 包装 role-registry.yaml 和角色文件读取,提供角色匹配与查询接口",
6
+ "dependencies": [],
7
+ "provides": ["provider:role-registry"],
8
+ "config": {}
9
+ }