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,12 @@
1
+ {
2
+ "name": "hook-skill-gate",
3
+ "version": "1.0.0",
4
+ "type": "hook",
5
+ "description": "统一技能门控 Hook - 合并 check-state + post-skill + skill-gate 三合一,动态查询 gated skills",
6
+ "dependencies": ["provider:state-store"],
7
+ "provides": ["hook:skill-gate"],
8
+ "config": {
9
+ "gatedSkills": [],
10
+ "blockedStates": ["waiting"]
11
+ }
12
+ }
@@ -0,0 +1,403 @@
1
+ /**
2
+ * Provider: Memory Store
3
+ *
4
+ * Wraps the agents/memories/ directory to provide role memory data access.
5
+ * Implements the ProviderPlugin interface from plugin-interface.js.
6
+ *
7
+ * Capabilities:
8
+ * - getByRole(roleName) return memory data for a specific role
9
+ * - setByRole(roleName, data) write/update memory data for a role
10
+ * - list() list all available memory entries
11
+ * - getByPath(filePath) read a specific memory file by relative path
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ var path = require('path');
17
+ var fs = require('fs');
18
+ var { ProviderPlugin } = require('../../contracts/plugin-interface');
19
+
20
+ /**
21
+ * MemoryStoreProvider - provides memory data access
22
+ */
23
+ class MemoryStoreProvider extends ProviderPlugin {
24
+ constructor() {
25
+ super();
26
+ this.name = 'provider-memory-store';
27
+ this.version = '1.0.0';
28
+ this.description = 'Memory Store Provider';
29
+ this.provides = 'provider:memory-store';
30
+ this.dependencies = {};
31
+
32
+ /** @type {string} absolute path to memories directory */
33
+ this._memoriesDir = '';
34
+ /** @type {Map<string, object>} cache of loaded memories */
35
+ this._cache = new Map();
36
+ }
37
+
38
+ /**
39
+ * Activate - set up the memories directory path.
40
+ * @param {PluginContext} context
41
+ */
42
+ async onActivate(context) {
43
+ var projectRoot = this._resolveProjectRoot();
44
+ this._memoriesDir = path.join(projectRoot, '.claude', 'agents', 'memories');
45
+
46
+ // Ensure the directory exists
47
+ if (!fs.existsSync(this._memoriesDir)) {
48
+ fs.mkdirSync(this._memoriesDir, { recursive: true });
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Factory - return the provider API.
54
+ * @param {PluginContext} context
55
+ * @returns {Promise<object>}
56
+ */
57
+ async factory(context) {
58
+ var self = this;
59
+
60
+ return {
61
+ /**
62
+ * Get memory data for a specific role.
63
+ * Looks for {roleName}.md in the memories directory.
64
+ *
65
+ * @param {string} roleName - role identifier (e.g. 'coordinator')
66
+ * @returns {Promise<{ roleName: string, content: string, filePath: string, exists: boolean }>}
67
+ */
68
+ getByRole: function (roleName) {
69
+ return self._getByRole(roleName);
70
+ },
71
+
72
+ /**
73
+ * Write memory data for a specific role.
74
+ * Creates or overwrites the memory file for the role.
75
+ *
76
+ * @param {string} roleName
77
+ * @param {string|object} data - memory content (string) or structured data to serialize
78
+ * @returns {Promise<{ roleName: string, filePath: string, written: boolean }>}
79
+ */
80
+ setByRole: function (roleName, data) {
81
+ return self._setByRole(roleName, data);
82
+ },
83
+
84
+ /**
85
+ * List all available memory entries.
86
+ * @returns {Promise<{ name: string, roleName: string, filePath: string, size: number, modified: string }[]>}
87
+ */
88
+ list: function () {
89
+ return self._list();
90
+ },
91
+
92
+ /**
93
+ * Read a memory file by its relative path within the memories directory.
94
+ * @param {string} relPath - relative path (e.g. 'coordinator.md')
95
+ * @returns {Promise<{ content: string, filePath: string, exists: boolean }>}
96
+ */
97
+ getByPath: function (relPath) {
98
+ return self._getByPath(relPath);
99
+ },
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Get memory data for a role.
105
+ * @param {string} roleName
106
+ * @returns {Promise<object>}
107
+ */
108
+ async _getByRole(roleName) {
109
+ // Sanitize roleName to prevent path traversal
110
+ var safeName = roleName.replace(/[^a-zA-Z0-9_\-]/g, '');
111
+ var fileName = safeName + '.md';
112
+ var filePath = path.join(this._memoriesDir, fileName);
113
+
114
+ // Check cache first
115
+ if (this._cache.has(safeName)) {
116
+ var cached = this._cache.get(safeName);
117
+ return {
118
+ roleName: safeName,
119
+ content: cached.content,
120
+ filePath: filePath,
121
+ exists: true,
122
+ metadata: cached.metadata,
123
+ };
124
+ }
125
+
126
+ try {
127
+ var content = fs.readFileSync(filePath, 'utf-8');
128
+ var metadata = this._parseMemoryMetadata(content);
129
+
130
+ // Cache the result
131
+ this._cache.set(safeName, { content: content, metadata: metadata });
132
+
133
+ return {
134
+ roleName: safeName,
135
+ content: content,
136
+ filePath: filePath,
137
+ exists: true,
138
+ metadata: metadata,
139
+ };
140
+ } catch (err) {
141
+ return {
142
+ roleName: safeName,
143
+ content: '',
144
+ filePath: filePath,
145
+ exists: false,
146
+ metadata: {},
147
+ };
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Write memory data for a role.
153
+ * @param {string} roleName
154
+ * @param {string|object} data
155
+ * @returns {Promise<object>}
156
+ */
157
+ async _setByRole(roleName, data) {
158
+ var safeName = roleName.replace(/[^a-zA-Z0-9_\-]/g, '');
159
+ var fileName = safeName + '.md';
160
+ var filePath = path.join(this._memoriesDir, fileName);
161
+
162
+ var content;
163
+ if (typeof data === 'string') {
164
+ content = data;
165
+ } else if (typeof data === 'object' && data !== null) {
166
+ // Serialize structured data to markdown-like format
167
+ content = this._serializeMemory(safeName, data);
168
+ } else {
169
+ content = String(data);
170
+ }
171
+
172
+ // Ensure directory exists
173
+ var dir = path.dirname(filePath);
174
+ if (!fs.existsSync(dir)) {
175
+ fs.mkdirSync(dir, { recursive: true });
176
+ }
177
+
178
+ fs.writeFileSync(filePath, content, 'utf-8');
179
+
180
+ // Update cache
181
+ var metadata = this._parseMemoryMetadata(content);
182
+ this._cache.set(safeName, { content: content, metadata: metadata });
183
+
184
+ return {
185
+ roleName: safeName,
186
+ filePath: filePath,
187
+ written: true,
188
+ };
189
+ }
190
+
191
+ /**
192
+ * List all memory files.
193
+ * @returns {Promise<object[]>}
194
+ */
195
+ async _list() {
196
+ var results = [];
197
+
198
+ try {
199
+ var files = fs.readdirSync(this._memoriesDir);
200
+ for (var i = 0; i < files.length; i++) {
201
+ var file = files[i];
202
+ if (!file.match(/\.(md|yaml|yml|json)$/)) continue;
203
+
204
+ var fullPath = path.join(this._memoriesDir, file);
205
+ try {
206
+ var stat = fs.statSync(fullPath);
207
+ var roleName = file.replace(/\.[^/.]+$/, '');
208
+
209
+ results.push({
210
+ name: file,
211
+ roleName: roleName,
212
+ filePath: fullPath,
213
+ size: stat.size,
214
+ modified: stat.mtime.toISOString(),
215
+ });
216
+ } catch (statErr) {
217
+ // Skip files that can't be stat'd
218
+ }
219
+ }
220
+ } catch (err) {
221
+ // Directory doesn't exist or can't be read
222
+ }
223
+
224
+ return results;
225
+ }
226
+
227
+ /**
228
+ * Read a memory file by relative path.
229
+ * @param {string} relPath
230
+ * @returns {Promise<object>}
231
+ */
232
+ async _getByPath(relPath) {
233
+ // Sanitize to prevent path traversal
234
+ var safePath = relPath.replace(/\.\./g, '').replace(/[\\]/g, '/');
235
+ var filePath = path.join(this._memoriesDir, safePath);
236
+
237
+ // Ensure the resolved path is still within the memories directory
238
+ if (!filePath.startsWith(this._memoriesDir)) {
239
+ return { content: '', filePath: safePath, exists: false };
240
+ }
241
+
242
+ try {
243
+ var content = fs.readFileSync(filePath, 'utf-8');
244
+ return { content: content, filePath: filePath, exists: true };
245
+ } catch (err) {
246
+ return { content: '', filePath: safePath, exists: false };
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Parse metadata from a memory markdown file.
252
+ * Extracts role name, creation date, usage, and experience count.
253
+ * @param {string} content
254
+ * @returns {object}
255
+ */
256
+ _parseMemoryMetadata(content) {
257
+ var metadata = {};
258
+ var lines = content.split('\n');
259
+
260
+ for (var i = 0; i < lines.length; i++) {
261
+ var line = lines[i];
262
+
263
+ // Role name from heading
264
+ var headingMatch = line.match(/^#\s+(.+)角色记忆库/);
265
+ if (headingMatch) {
266
+ metadata.roleTitle = headingMatch[1].trim();
267
+ continue;
268
+ }
269
+
270
+ // Role from metadata line
271
+ var roleMatch = line.match(/^>\s*角色:\s*(.+)/);
272
+ if (roleMatch) {
273
+ metadata.role = roleMatch[1].trim();
274
+ continue;
275
+ }
276
+
277
+ // Creation date
278
+ var dateMatch = line.match(/^>\s*创建日期:\s*(.+)/);
279
+ if (dateMatch) {
280
+ metadata.createdDate = dateMatch[1].trim();
281
+ continue;
282
+ }
283
+
284
+ // Usage
285
+ var usageMatch = line.match(/^>\s*用途:\s*(.+)/);
286
+ if (usageMatch) {
287
+ metadata.usage = usageMatch[1].trim();
288
+ continue;
289
+ }
290
+
291
+ // Experience count
292
+ var expMatch = line.match(/^-?\s*总经验数:\s*(\d+)/);
293
+ if (expMatch) {
294
+ metadata.experienceCount = parseInt(expMatch[1], 10);
295
+ continue;
296
+ }
297
+
298
+ // Last update
299
+ var updateMatch = line.match(/^-?\s*最后更新:\s*(.+)/);
300
+ if (updateMatch) {
301
+ metadata.lastUpdate = updateMatch[1].trim();
302
+ continue;
303
+ }
304
+
305
+ // Total tasks
306
+ var taskMatch = line.match(/^-?\s*累计执行任务:\s*(\d+)/);
307
+ if (taskMatch) {
308
+ metadata.totalTasks = parseInt(taskMatch[1], 10);
309
+ continue;
310
+ }
311
+ }
312
+
313
+ return metadata;
314
+ }
315
+
316
+ /**
317
+ * Serialize structured data to a memory markdown file.
318
+ * @param {string} roleName
319
+ * @param {object} data
320
+ * @returns {string}
321
+ */
322
+ _serializeMemory(roleName, data) {
323
+ var lines = [];
324
+ var title = data.title || roleName;
325
+ var role = data.role || roleName;
326
+ var createdDate = data.createdDate || new Date().toISOString().split('T')[0];
327
+ var usage = data.usage || '';
328
+ var experienceCount = data.experienceCount || 0;
329
+ var lastUpdate = data.lastUpdate || createdDate;
330
+ var totalTasks = data.totalTasks || 0;
331
+ var experiences = data.experiences || [];
332
+ var notes = data.notes || '';
333
+
334
+ lines.push('# ' + title + '角色记忆库');
335
+ lines.push('');
336
+ lines.push('> 角色: ' + role);
337
+ lines.push('> 创建日期: ' + createdDate);
338
+ lines.push('> 用途: ' + usage);
339
+ lines.push('');
340
+ lines.push('## 统计');
341
+ lines.push('');
342
+ lines.push('- 总经验数: ' + experienceCount);
343
+ lines.push('- 最后更新: ' + lastUpdate);
344
+ lines.push('- 累计执行任务: ' + totalTasks + ' 次');
345
+ lines.push('');
346
+ lines.push('---');
347
+ lines.push('');
348
+ lines.push('## 经验列表');
349
+ lines.push('');
350
+
351
+ if (experiences.length > 0) {
352
+ for (var i = 0; i < experiences.length; i++) {
353
+ var exp = experiences[i];
354
+ lines.push('### [' + (exp.tag || '') + '] ' + (exp.title || ''));
355
+ lines.push('');
356
+ if (exp.description) {
357
+ lines.push('**问题描述**: ' + exp.description);
358
+ lines.push('');
359
+ }
360
+ if (exp.solution) {
361
+ lines.push('**解决方案**: ' + exp.solution);
362
+ lines.push('');
363
+ }
364
+ if (exp.source) {
365
+ lines.push('**来源工作包**: ' + exp.source);
366
+ }
367
+ if (exp.date) {
368
+ lines.push('**记录日期**: ' + exp.date);
369
+ }
370
+ lines.push('');
371
+ }
372
+ }
373
+
374
+ lines.push('---');
375
+ lines.push('');
376
+ lines.push('## 注意事项');
377
+ lines.push('');
378
+
379
+ if (notes) {
380
+ lines.push(notes);
381
+ }
382
+
383
+ return lines.join('\n');
384
+ }
385
+
386
+ /**
387
+ * Resolve the project root directory.
388
+ * @returns {string}
389
+ */
390
+ _resolveProjectRoot() {
391
+ var dir = process.cwd();
392
+ for (var i = 0; i < 10; i++) {
393
+ if (fs.existsSync(path.join(dir, 'task.md'))) return dir;
394
+ if (fs.existsSync(path.join(dir, '.claude'))) return dir;
395
+ var parent = path.dirname(dir);
396
+ if (parent === dir) break;
397
+ dir = parent;
398
+ }
399
+ return process.cwd();
400
+ }
401
+ }
402
+
403
+ module.exports = MemoryStoreProvider;
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "provider-memory-store",
3
+ "version": "1.0.0",
4
+ "type": "provider",
5
+ "description": "记忆存储 Provider - 包装 agents/memories/ 目录读写,提供角色记忆数据访问接口",
6
+ "dependencies": [],
7
+ "provides": ["provider:memory-store"],
8
+ "config": {}
9
+ }