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,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;
|