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,174 @@
1
+ /**
2
+ * Logger - Plugin-level logging service for AI Agent Harness
3
+ *
4
+ * Features:
5
+ * - debug / info / warn / error log levels
6
+ * - Per-plugin log segregation
7
+ * - History query interface for debugging
8
+ * - No external dependencies
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const LOG_LEVELS = {
14
+ debug: 0,
15
+ info: 1,
16
+ warn: 2,
17
+ error: 3,
18
+ };
19
+
20
+ class Logger {
21
+ /**
22
+ * @param {object} [options]
23
+ * @param {string} [options.level='info'] - minimum log level
24
+ * @param {number} [options.maxHistory=500] - max history entries to retain
25
+ */
26
+ constructor(options) {
27
+ options = options || {};
28
+ this._minLevel = LOG_LEVELS[options.level] !== undefined
29
+ ? LOG_LEVELS[options.level]
30
+ : LOG_LEVELS.info;
31
+ this._maxHistory = options.maxHistory || 500;
32
+ this._history = [];
33
+ }
34
+
35
+ // --- public API ---
36
+
37
+ /**
38
+ * Log a debug message.
39
+ * @param {string} plugin - plugin name
40
+ * @param {string} message
41
+ * @param {object} [data]
42
+ */
43
+ debug(plugin, message, data) {
44
+ this._log('debug', plugin, message, data);
45
+ }
46
+
47
+ /**
48
+ * Log an info message.
49
+ * @param {string} plugin
50
+ * @param {string} message
51
+ * @param {object} [data]
52
+ */
53
+ info(plugin, message, data) {
54
+ this._log('info', plugin, message, data);
55
+ }
56
+
57
+ /**
58
+ * Log a warning message.
59
+ * @param {string} plugin
60
+ * @param {string} message
61
+ * @param {object} [data]
62
+ */
63
+ warn(plugin, message, data) {
64
+ this._log('warn', plugin, message, data);
65
+ }
66
+
67
+ /**
68
+ * Log an error message.
69
+ * @param {string} plugin
70
+ * @param {string} message
71
+ * @param {object} [data]
72
+ */
73
+ error(plugin, message, data) {
74
+ this._log('error', plugin, message, data);
75
+ }
76
+
77
+ /**
78
+ * Query log history.
79
+ * @param {object} [filter]
80
+ * @param {string} [filter.plugin] - filter by plugin name
81
+ * @param {string} [filter.level] - filter by level (debug/info/warn/error)
82
+ * @param {number} [filter.since] - timestamp lower bound (ms)
83
+ * @param {number} [filter.until] - timestamp upper bound (ms)
84
+ * @param {number} [filter.limit] - max entries to return
85
+ * @returns {object[]}
86
+ */
87
+ query(filter) {
88
+ filter = filter || {};
89
+ let results = this._history;
90
+
91
+ if (filter.plugin) {
92
+ results = results.filter(function (e) { return e.plugin === filter.plugin; });
93
+ }
94
+ if (filter.level) {
95
+ var levelNum = LOG_LEVELS[filter.level];
96
+ if (levelNum !== undefined) {
97
+ results = results.filter(function (e) { return LOG_LEVELS[e.level] >= levelNum; });
98
+ }
99
+ }
100
+ if (filter.since) {
101
+ results = results.filter(function (e) { return e.timestamp >= filter.since; });
102
+ }
103
+ if (filter.until) {
104
+ results = results.filter(function (e) { return e.timestamp <= filter.until; });
105
+ }
106
+ if (filter.limit) {
107
+ results = results.slice(-filter.limit);
108
+ }
109
+
110
+ return results;
111
+ }
112
+
113
+ /**
114
+ * Clear all history entries.
115
+ */
116
+ clear() {
117
+ this._history = [];
118
+ }
119
+
120
+ /**
121
+ * Create a child logger bound to a specific plugin name.
122
+ * The child logger exposes debug/info/warn/error without needing the plugin arg.
123
+ * @param {string} pluginName
124
+ * @returns {object} child logger with debug/info/warn/error methods
125
+ */
126
+ createChild(pluginName) {
127
+ var self = this;
128
+ return {
129
+ debug: function (message, data) { self.debug(pluginName, message, data); },
130
+ info: function (message, data) { self.info(pluginName, message, data); },
131
+ warn: function (message, data) { self.warn(pluginName, message, data); },
132
+ error: function (message, data) { self.error(pluginName, message, data); },
133
+ };
134
+ }
135
+
136
+ // --- internal ---
137
+
138
+ _log(level, plugin, message, data) {
139
+ var levelNum = LOG_LEVELS[level];
140
+ if (levelNum === undefined) {
141
+ return;
142
+ }
143
+ if (levelNum < this._minLevel) {
144
+ return;
145
+ }
146
+
147
+ var entry = {
148
+ level: level,
149
+ plugin: plugin || 'system',
150
+ message: message,
151
+ timestamp: Date.now(),
152
+ };
153
+ if (data !== undefined) {
154
+ entry.data = data;
155
+ }
156
+
157
+ this._history.push(entry);
158
+ if (this._history.length > this._maxHistory) {
159
+ this._history.shift();
160
+ }
161
+
162
+ // Also write to console for immediate visibility
163
+ var prefix = '[' + entry.timestamp + '] [' + level.toUpperCase() + '] [' + entry.plugin + ']';
164
+ if (level === 'error') {
165
+ console.error(prefix, message, data !== undefined ? data : '');
166
+ } else if (level === 'warn') {
167
+ console.warn(prefix, message, data !== undefined ? data : '');
168
+ } else {
169
+ console.log(prefix, message, data !== undefined ? data : '');
170
+ }
171
+ }
172
+ }
173
+
174
+ module.exports = Logger;
@@ -0,0 +1,339 @@
1
+ /**
2
+ * PluginLoader - Plugin discovery, dependency resolution, and lifecycle management
3
+ *
4
+ * Features:
5
+ * - Load plugins from plugin-registry.json
6
+ * - Topological sort for dependency order
7
+ * - Circular dependency detection
8
+ * - Lifecycle management: load -> activate -> deactivate
9
+ * - Graceful handling of empty registries
10
+ * - Error isolation per plugin
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ var fs = require('fs');
16
+ var path = require('path');
17
+ var { PluginState } = require('../contracts/plugin-interface');
18
+
19
+ class PluginLoader {
20
+ /**
21
+ * @param {object} options
22
+ * @param {string} options.registryPath - path to plugin-registry.json
23
+ * @param {object} options.eventBus - EventBus instance
24
+ * @param {object} options.stateStore - StateStore instance
25
+ * @param {object} options.configManager - ConfigManager instance
26
+ * @param {object} options.logger - Logger instance
27
+ */
28
+ constructor(options) {
29
+ options = options || {};
30
+ this._registryPath = options.registryPath || path.join(process.cwd(), 'plugins', 'plugin-registry.json');
31
+ this._eventBus = options.eventBus || null;
32
+ this._stateStore = options.stateStore || null;
33
+ this._configManager = options.configManager || null;
34
+ this._logger = options.logger || null;
35
+
36
+ /** @type {Map<string, object>} loaded plugin instances */
37
+ this.loadedPlugins = new Map();
38
+ /** @type {Map<string, object>} plugin configs from registry */
39
+ this._pluginConfigs = new Map();
40
+ /** @type {object} registry data */
41
+ this._registry = null;
42
+ }
43
+
44
+ // --- public API ---
45
+
46
+ /**
47
+ * Load and activate all plugins from the registry.
48
+ * Respects dependency order via topological sort.
49
+ * Handles empty registries gracefully.
50
+ *
51
+ * @returns {Promise<string[]>} names of successfully loaded plugins
52
+ */
53
+ async loadAll() {
54
+ this._log('info', 'Starting plugin loading...');
55
+
56
+ // 1. Read registry
57
+ this._registry = this._readRegistry();
58
+ var pluginNames = this._getPluginNames();
59
+
60
+ if (pluginNames.length === 0) {
61
+ this._log('info', 'Registry is empty, no plugins to load');
62
+ return [];
63
+ }
64
+
65
+ // 2. Build dependency graph from registry entries
66
+ var depGraph = this._buildDependencyGraph(pluginNames);
67
+
68
+ // 3. Topological sort (with cycle detection)
69
+ var loadOrder = this._topologicalSort(depGraph);
70
+
71
+ this._log('info', 'Load order resolved: ' + loadOrder.join(', '));
72
+
73
+ // 4. Load and activate in order
74
+ var loaded = [];
75
+ for (var i = 0; i < loadOrder.length; i++) {
76
+ var name = loadOrder[i];
77
+ var config = this._pluginConfigs.get(name);
78
+
79
+ if (config && config.enabled === false) {
80
+ this._log('info', 'Plugin "' + name + '" is disabled, skipping');
81
+ continue;
82
+ }
83
+
84
+ try {
85
+ await this._loadPlugin(name, config);
86
+ loaded.push(name);
87
+ if (this._eventBus) {
88
+ this._eventBus.emit('plugin:loaded', { pluginName: name });
89
+ }
90
+ this._log('info', 'Plugin "' + name + '" loaded successfully');
91
+ } catch (err) {
92
+ this._log('error', 'Failed to load plugin "' + name + '": ' + err.message);
93
+ // Non-critical plugins don't block others
94
+ }
95
+ }
96
+
97
+ this._log('info', 'Plugin loading complete. ' + loaded.length + ' plugins loaded');
98
+ return loaded;
99
+ }
100
+
101
+ /**
102
+ * Activate a single loaded plugin.
103
+ * @param {string} name - plugin name
104
+ * @param {object} context - PluginContext to inject
105
+ */
106
+ async activate(name, context) {
107
+ var plugin = this.loadedPlugins.get(name);
108
+ if (!plugin) {
109
+ throw new Error('Plugin "' + name + '" is not loaded');
110
+ }
111
+
112
+ if (typeof plugin.onActivate === 'function') {
113
+ await plugin.onActivate(context);
114
+ }
115
+ plugin.state = PluginState.ACTIVATED;
116
+
117
+ if (this._eventBus) {
118
+ this._eventBus.emit('plugin:activated', { pluginName: name });
119
+ }
120
+ this._log('info', 'Plugin "' + name + '" activated');
121
+ }
122
+
123
+ /**
124
+ * Deactivate a single plugin.
125
+ * @param {string} name
126
+ */
127
+ async deactivate(name) {
128
+ var plugin = this.loadedPlugins.get(name);
129
+ if (!plugin) {
130
+ throw new Error('Plugin "' + name + '" is not loaded');
131
+ }
132
+
133
+ if (typeof plugin.onDeactivate === 'function') {
134
+ await plugin.onDeactivate();
135
+ }
136
+ plugin.state = PluginState.DEACTIVATED;
137
+
138
+ if (this._eventBus) {
139
+ this._eventBus.emit('plugin:deactivated', { pluginName: name });
140
+ }
141
+ this._log('info', 'Plugin "' + name + '" deactivated');
142
+ }
143
+
144
+ /**
145
+ * Deactivate all loaded plugins in reverse order.
146
+ */
147
+ async deactivateAll() {
148
+ var names = Array.from(this.loadedPlugins.keys()).reverse();
149
+ for (var i = 0; i < names.length; i++) {
150
+ try {
151
+ await this.deactivate(names[i]);
152
+ } catch (err) {
153
+ this._log('error', 'Error deactivating "' + names[i] + '": ' + err.message);
154
+ }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Get a loaded plugin by name.
160
+ * @param {string} name
161
+ * @returns {object|undefined}
162
+ */
163
+ getPlugin(name) {
164
+ return this.loadedPlugins.get(name);
165
+ }
166
+
167
+ /**
168
+ * Check if a plugin is loaded.
169
+ * @param {string} name
170
+ * @returns {boolean}
171
+ */
172
+ isLoaded(name) {
173
+ return this.loadedPlugins.has(name);
174
+ }
175
+
176
+ /**
177
+ * Get names of all loaded plugins.
178
+ * @returns {string[]}
179
+ */
180
+ getLoadedNames() {
181
+ return Array.from(this.loadedPlugins.keys());
182
+ }
183
+
184
+ // --- internal ---
185
+
186
+ /**
187
+ * Read and parse the registry file.
188
+ * @returns {object}
189
+ */
190
+ _readRegistry() {
191
+ try {
192
+ var content = fs.readFileSync(this._registryPath, 'utf-8');
193
+ return JSON.parse(content);
194
+ } catch (err) {
195
+ // If file doesn't exist or is invalid, return empty registry
196
+ this._log('warn', 'Could not read registry: ' + err.message + '. Using empty registry.');
197
+ return { version: '1.0.0', plugins: [] };
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Extract plugin names and configs from registry.
203
+ * Supports both array format and object format.
204
+ * @returns {string[]}
205
+ */
206
+ _getPluginNames() {
207
+ var plugins = this._registry.plugins;
208
+ if (!plugins) return [];
209
+
210
+ // Array format: [{ name, source, enabled, config }]
211
+ if (Array.isArray(plugins)) {
212
+ var names = [];
213
+ for (var i = 0; i < plugins.length; i++) {
214
+ var entry = plugins[i];
215
+ var name = entry.name || entry;
216
+ this._pluginConfigs.set(name, entry);
217
+ names.push(name);
218
+ }
219
+ return names;
220
+ }
221
+
222
+ // Object format: { name: { source, enabled, config } }
223
+ if (typeof plugins === 'object') {
224
+ var keys = Object.keys(plugins);
225
+ for (var j = 0; j < keys.length; j++) {
226
+ this._pluginConfigs.set(keys[j], plugins[keys[j]]);
227
+ }
228
+ return keys;
229
+ }
230
+
231
+ return [];
232
+ }
233
+
234
+ /**
235
+ * Build a dependency graph from plugin configs.
236
+ * @param {string[]} pluginNames
237
+ * @returns {Map<string, string[]>} name -> dependency names
238
+ */
239
+ _buildDependencyGraph(pluginNames) {
240
+ var graph = new Map();
241
+ for (var i = 0; i < pluginNames.length; i++) {
242
+ var name = pluginNames[i];
243
+ var config = this._pluginConfigs.get(name);
244
+ var deps = [];
245
+
246
+ if (config && config.dependencies) {
247
+ deps = config.dependencies.plugins || config.dependencies || [];
248
+ if (!Array.isArray(deps)) deps = [];
249
+ }
250
+
251
+ graph.set(name, deps);
252
+ }
253
+ return graph;
254
+ }
255
+
256
+ /**
257
+ * Topological sort with circular dependency detection.
258
+ * @param {Map<string, string[]>} graph - name -> dependencies
259
+ * @returns {string[]} sorted plugin names
260
+ * @throws {Error} on circular dependency or missing dependency
261
+ */
262
+ _topologicalSort(graph) {
263
+ var WHITE = 0, GRAY = 1, BLACK = 2;
264
+ var color = new Map();
265
+ var result = [];
266
+
267
+ // Initialize all as white
268
+ var keys = Array.from(graph.keys());
269
+ for (var i = 0; i < keys.length; i++) {
270
+ color.set(keys[i], WHITE);
271
+ }
272
+
273
+ function visit(node) {
274
+ var c = color.get(node);
275
+ if (c === GRAY) {
276
+ throw new Error('Circular dependency detected involving plugin: ' + node);
277
+ }
278
+ if (c === BLACK) return;
279
+
280
+ color.set(node, GRAY);
281
+
282
+ var deps = graph.get(node) || [];
283
+ for (var j = 0; j < deps.length; j++) {
284
+ var dep = deps[j];
285
+ if (!graph.has(dep)) {
286
+ throw new Error('Plugin "' + node + '" depends on unknown plugin: ' + dep);
287
+ }
288
+ visit(dep);
289
+ }
290
+
291
+ color.set(node, BLACK);
292
+ result.push(node);
293
+ }
294
+
295
+ for (var k = 0; k < keys.length; k++) {
296
+ if (color.get(keys[k]) === WHITE) {
297
+ visit(keys[k]);
298
+ }
299
+ }
300
+
301
+ return result;
302
+ }
303
+
304
+ /**
305
+ * Load a single plugin.
306
+ * For now, records the plugin in the loaded map.
307
+ * Actual JS module loading will be added when plugins have executable code.
308
+ *
309
+ * @param {string} name
310
+ * @param {object} config
311
+ */
312
+ async _loadPlugin(name, config) {
313
+ // Store the plugin with metadata
314
+ // In future iterations, this will require() the actual plugin module
315
+ var pluginEntry = {
316
+ name: name,
317
+ config: config || {},
318
+ state: PluginState.LOADED,
319
+ // Placeholder - real plugins will have onActivate/onDeactivate
320
+ onActivate: function () {},
321
+ onDeactivate: function () {},
322
+ };
323
+
324
+ this.loadedPlugins.set(name, pluginEntry);
325
+ }
326
+
327
+ /**
328
+ * Internal logging helper.
329
+ */
330
+ _log(level, message) {
331
+ if (this._logger && typeof this._logger[level] === 'function') {
332
+ this._logger[level]('plugin-loader', message);
333
+ } else {
334
+ console.log('[plugin-loader] [' + level + '] ' + message);
335
+ }
336
+ }
337
+ }
338
+
339
+ module.exports = PluginLoader;