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,306 @@
1
+ /**
2
+ * ConfigManager - Three-layer configuration management for AI Agent Harness
3
+ *
4
+ * Layer precedence (highest wins):
5
+ * 1. Environment variables (HARNESS_<KEY>)
6
+ * 2. harness-config.yaml (user/project-level)
7
+ * 3. plugin defaults (built into each plugin)
8
+ *
9
+ * Supports:
10
+ * - get(key) with dot-notation
11
+ * - getForPlugin(pluginName, key) with per-plugin override
12
+ * - setOverride(key, value) runtime override
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ var fs = require('fs');
18
+ var path = require('path');
19
+
20
+ /**
21
+ * Minimal YAML-like parser for simple key-value YAML files.
22
+ * Handles nested structure via indentation (spaces only).
23
+ * This is NOT a full YAML parser - it covers the subset used by harness-config.yaml.
24
+ */
25
+ function parseSimpleYaml(content) {
26
+ var result = {};
27
+ var lines = content.split('\n');
28
+ var stack = [{ obj: result, indent: -1 }];
29
+
30
+ for (var i = 0; i < lines.length; i++) {
31
+ var line = lines[i];
32
+
33
+ // Skip empty lines, comments, and document separators
34
+ if (!line.trim() || line.trim().indexOf('#') === 0 || line.trim() === '---') {
35
+ continue;
36
+ }
37
+
38
+ // Calculate indentation
39
+ var indent = line.search(/\S/);
40
+ if (indent < 0) continue;
41
+
42
+ // Pop stack to find parent
43
+ while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
44
+ stack.pop();
45
+ }
46
+
47
+ var parent = stack[stack.length - 1].obj;
48
+ var trimmed = line.trim();
49
+
50
+ // Check if it's a list item
51
+ if (trimmed.indexOf('- ') === 0) {
52
+ if (!Array.isArray(parent)) continue;
53
+ var itemValue = parseValue(trimmed.substring(2));
54
+ parent.push(itemValue);
55
+ continue;
56
+ }
57
+
58
+ // Key-value pair
59
+ var colonIdx = trimmed.indexOf(':');
60
+ if (colonIdx === -1) continue;
61
+
62
+ var key = trimmed.substring(0, colonIdx).trim();
63
+ var valuePart = trimmed.substring(colonIdx + 1).trim();
64
+
65
+ if (valuePart === '' || valuePart === null) {
66
+ // Nested object
67
+ var child = {};
68
+ parent[key] = child;
69
+ stack.push({ obj: child, indent: indent });
70
+ } else {
71
+ parent[key] = parseValue(valuePart);
72
+ }
73
+ }
74
+
75
+ return result;
76
+ }
77
+
78
+ /**
79
+ * Parse a YAML scalar value.
80
+ */
81
+ function parseValue(val) {
82
+ if (val === 'true') return true;
83
+ if (val === 'false') return false;
84
+ if (val === 'null' || val === '~') return null;
85
+
86
+ // Remove surrounding quotes
87
+ if ((val.charAt(0) === '"' && val.charAt(val.length - 1) === '"') ||
88
+ (val.charAt(0) === "'" && val.charAt(val.length - 1) === "'")) {
89
+ return val.substring(1, val.length - 1);
90
+ }
91
+
92
+ // Try number
93
+ var num = Number(val);
94
+ if (!isNaN(num) && val !== '') return num;
95
+
96
+ return val;
97
+ }
98
+
99
+ class ConfigManager {
100
+ /**
101
+ * @param {object} [options]
102
+ * @param {string} [options.configPath] - path to harness-config.yaml
103
+ * @param {object} [options.defaults] - plugin default configs { pluginName: { key: value } }
104
+ */
105
+ constructor(options) {
106
+ options = options || {};
107
+ /** @type {string} */
108
+ this._configPath = options.configPath || path.join(this._findProjectRoot(), '.claude', 'config', 'harness-config.yaml');
109
+ /** @type {object} plugin defaults */
110
+ this._defaults = options.defaults || {};
111
+ /** @type {object|null} parsed YAML config */
112
+ this._yamlConfig = null;
113
+ /** @type {object} runtime overrides */
114
+ this._overrides = {};
115
+ }
116
+
117
+ // --- public API ---
118
+
119
+ /**
120
+ * Get a config value with three-layer resolution:
121
+ * runtime override > harness-config.yaml > plugin defaults
122
+ *
123
+ * @param {string} key - dot-notation key
124
+ * @param {*} [defaultValue] - fallback if key not found in any layer
125
+ * @returns {*}
126
+ */
127
+ get(key, defaultValue) {
128
+ // Layer 1: runtime overrides
129
+ var overrideVal = this._getNested(this._overrides, key);
130
+ if (overrideVal !== undefined) {
131
+ return overrideVal;
132
+ }
133
+
134
+ // Layer 2: environment variables (HARNESS_ prefix, double underscore for nesting)
135
+ var envKey = 'HARNESS_' + key.toUpperCase().replace(/\./g, '__');
136
+ if (process.env[envKey] !== undefined) {
137
+ return parseValue(process.env[envKey]);
138
+ }
139
+
140
+ // Layer 3: harness-config.yaml
141
+ var yamlVal = this._getNested(this._getYamlConfig(), key);
142
+ if (yamlVal !== undefined) {
143
+ return yamlVal;
144
+ }
145
+
146
+ return defaultValue;
147
+ }
148
+
149
+ /**
150
+ * Get a config value for a specific plugin.
151
+ * Resolution order:
152
+ * runtime override > env var > harness-config overrides section > harness-config root > plugin defaults
153
+ *
154
+ * @param {string} pluginName
155
+ * @param {string} key
156
+ * @param {*} [defaultValue]
157
+ * @returns {*}
158
+ */
159
+ getForPlugin(pluginName, key, defaultValue) {
160
+ // Try plugin-specific override first
161
+ var pluginOverride = this.get('overrides.' + pluginName + '.' + key);
162
+ if (pluginOverride !== undefined) {
163
+ return pluginOverride;
164
+ }
165
+
166
+ // Try general get
167
+ var generalVal = this.get(key);
168
+ if (generalVal !== undefined) {
169
+ return generalVal;
170
+ }
171
+
172
+ // Try plugin defaults
173
+ var defaultVal = this._getNested(this._defaults, pluginName + '.' + key);
174
+ if (defaultVal !== undefined) {
175
+ return defaultVal;
176
+ }
177
+
178
+ return defaultValue;
179
+ }
180
+
181
+ /**
182
+ * Set a runtime override. Takes highest priority.
183
+ * @param {string} key
184
+ * @param {*} value
185
+ */
186
+ setOverride(key, value) {
187
+ this._setNested(this._overrides, key, value);
188
+ }
189
+
190
+ /**
191
+ * Clear a runtime override.
192
+ * @param {string} key
193
+ */
194
+ clearOverride(key) {
195
+ this._deleteNested(this._overrides, key);
196
+ }
197
+
198
+ /**
199
+ * Get the full parsed YAML config (for advanced usage).
200
+ * @returns {object}
201
+ */
202
+ getAll() {
203
+ return this._getYamlConfig();
204
+ }
205
+
206
+ /**
207
+ * Create a scoped config getter for a specific plugin.
208
+ * @param {string} pluginName
209
+ * @returns {{ get: Function }}
210
+ */
211
+ forPlugin(pluginName) {
212
+ var self = this;
213
+ return {
214
+ get: function (key, defaultValue) {
215
+ return self.getForPlugin(pluginName, key, defaultValue);
216
+ },
217
+ };
218
+ }
219
+
220
+ // --- internal ---
221
+
222
+ /**
223
+ * Load and cache the YAML config.
224
+ * @returns {object}
225
+ */
226
+ _getYamlConfig() {
227
+ if (this._yamlConfig !== null) {
228
+ return this._yamlConfig;
229
+ }
230
+
231
+ try {
232
+ var content = fs.readFileSync(this._configPath, 'utf-8');
233
+ this._yamlConfig = parseSimpleYaml(content);
234
+ } catch (err) {
235
+ // Config file not found or unreadable - use empty config
236
+ this._yamlConfig = {};
237
+ }
238
+
239
+ return this._yamlConfig;
240
+ }
241
+
242
+ /**
243
+ * Get nested value by dot-notation key.
244
+ */
245
+ _getNested(obj, key) {
246
+ var parts = key.split('.');
247
+ var current = obj;
248
+ for (var i = 0; i < parts.length; i++) {
249
+ if (current === null || current === undefined || typeof current !== 'object') {
250
+ return undefined;
251
+ }
252
+ current = current[parts[i]];
253
+ }
254
+ return current;
255
+ }
256
+
257
+ /**
258
+ * Set nested value by dot-notation key.
259
+ */
260
+ _setNested(obj, key, value) {
261
+ var parts = key.split('.');
262
+ var current = obj;
263
+ for (var i = 0; i < parts.length - 1; i++) {
264
+ if (typeof current[parts[i]] !== 'object' || current[parts[i]] === null) {
265
+ current[parts[i]] = {};
266
+ }
267
+ current = current[parts[i]];
268
+ }
269
+ current[parts[parts.length - 1]] = value;
270
+ }
271
+
272
+ /**
273
+ * Delete nested value by dot-notation key.
274
+ */
275
+ _deleteNested(obj, key) {
276
+ var parts = key.split('.');
277
+ var current = obj;
278
+ for (var i = 0; i < parts.length - 1; i++) {
279
+ if (typeof current[parts[i]] !== 'object' || current[parts[i]] === null) {
280
+ return;
281
+ }
282
+ current = current[parts[i]];
283
+ }
284
+ delete current[parts[parts.length - 1]];
285
+ }
286
+
287
+ /**
288
+ * Walk up from cwd to find the project root (directory containing task.md or CLAUDE.md).
289
+ * Falls back to cwd if not found.
290
+ * @returns {string}
291
+ */
292
+ _findProjectRoot() {
293
+ var dir = process.cwd();
294
+ for (var i = 0; i < 10; i++) {
295
+ if (fs.existsSync(path.join(dir, 'task.md')) || fs.existsSync(path.join(dir, 'CLAUDE.md'))) {
296
+ return dir;
297
+ }
298
+ var parent = path.dirname(dir);
299
+ if (parent === dir) break; // filesystem root
300
+ dir = parent;
301
+ }
302
+ return process.cwd();
303
+ }
304
+ }
305
+
306
+ module.exports = ConfigManager;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * EventBus - Event dispatch system for AI Agent Harness
3
+ *
4
+ * Features:
5
+ * - on(event, handler) register a listener
6
+ * - once(event, handler) one-time listener
7
+ * - off(event, handler) unregister a listener
8
+ * - emit(event, data) dispatch an event
9
+ * - History recording and query for debugging
10
+ * - Subscription objects with unsubscribe() support
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ class EventBus {
16
+ /**
17
+ * @param {object} [options]
18
+ * @param {number} [options.maxHistory=100] - max event history entries
19
+ */
20
+ constructor(options) {
21
+ options = options || {};
22
+ /** @type {Map<string, Set<Function>>} event -> handlers */
23
+ this._handlers = new Map();
24
+ /** @type {object[]} */
25
+ this._history = [];
26
+ /** @type {number} */
27
+ this._maxHistory = options.maxHistory || 100;
28
+ }
29
+
30
+ // --- public API ---
31
+
32
+ /**
33
+ * Register an event handler.
34
+ * @param {string} event - event name
35
+ * @param {Function} handler - callback(eventData)
36
+ * @returns {{ unsubscribe: Function }} subscription handle
37
+ */
38
+ on(event, handler) {
39
+ if (typeof handler !== 'function') {
40
+ throw new TypeError('EventBus.on(): handler must be a function');
41
+ }
42
+ if (!this._handlers.has(event)) {
43
+ this._handlers.set(event, new Set());
44
+ }
45
+ this._handlers.get(event).add(handler);
46
+
47
+ var self = this;
48
+ var handlers = this._handlers.get(event);
49
+ return {
50
+ unsubscribe: function () {
51
+ handlers.delete(handler);
52
+ if (handlers.size === 0) {
53
+ self._handlers.delete(event);
54
+ }
55
+ },
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Register a one-time event handler. Automatically removed after first invocation.
61
+ * @param {string} event
62
+ * @param {Function} handler - callback(eventData)
63
+ * @returns {{ unsubscribe: Function }}
64
+ */
65
+ once(event, handler) {
66
+ if (typeof handler !== 'function') {
67
+ throw new TypeError('EventBus.once(): handler must be a function');
68
+ }
69
+ var self = this;
70
+ var wrapped = function (data) {
71
+ handler(data);
72
+ innerSub.unsubscribe();
73
+ };
74
+ var innerSub = this.on(event, wrapped);
75
+ return innerSub;
76
+ }
77
+
78
+ /**
79
+ * Unregister a specific handler from an event.
80
+ * @param {string} event
81
+ * @param {Function} handler - the exact function reference originally passed to on()
82
+ */
83
+ off(event, handler) {
84
+ var handlers = this._handlers.get(event);
85
+ if (!handlers) return;
86
+ handlers.delete(handler);
87
+ if (handlers.size === 0) {
88
+ this._handlers.delete(event);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Emit an event, calling all registered handlers synchronously.
94
+ * Errors in individual handlers are caught and logged, not propagated.
95
+ * @param {string} event
96
+ * @param {*} data
97
+ */
98
+ emit(event, data) {
99
+ // Record in history
100
+ this._history.push({
101
+ event: event,
102
+ data: data,
103
+ timestamp: Date.now(),
104
+ });
105
+ if (this._history.length > this._maxHistory) {
106
+ this._history.shift();
107
+ }
108
+
109
+ // Dispatch to handlers
110
+ var handlers = this._handlers.get(event);
111
+ if (!handlers) return;
112
+
113
+ handlers.forEach(function (handler) {
114
+ try {
115
+ handler(data);
116
+ } catch (err) {
117
+ console.error(
118
+ '[EventBus] Error in handler for event "' + event + '":',
119
+ err.message
120
+ );
121
+ }
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Query event history for debugging.
127
+ * @param {object} [filter]
128
+ * @param {string} [filter.event] - substring match on event name
129
+ * @param {number} [filter.since] - timestamp lower bound (ms)
130
+ * @param {number} [filter.until] - timestamp upper bound (ms)
131
+ * @param {number} [filter.limit] - max entries to return
132
+ * @returns {object[]}
133
+ */
134
+ getHistory(filter) {
135
+ filter = filter || {};
136
+ var results = this._history;
137
+
138
+ if (filter.event) {
139
+ var substr = filter.event;
140
+ results = results.filter(function (e) {
141
+ return e.event.indexOf(substr) !== -1;
142
+ });
143
+ }
144
+ if (filter.since) {
145
+ results = results.filter(function (e) { return e.timestamp >= filter.since; });
146
+ }
147
+ if (filter.until) {
148
+ results = results.filter(function (e) { return e.timestamp <= filter.until; });
149
+ }
150
+ if (filter.limit) {
151
+ results = results.slice(-filter.limit);
152
+ }
153
+
154
+ return results;
155
+ }
156
+
157
+ /**
158
+ * Clear all event history.
159
+ */
160
+ clearHistory() {
161
+ this._history = [];
162
+ }
163
+
164
+ /**
165
+ * Remove all handlers for a specific event, or all events.
166
+ * @param {string} [event] - if omitted, clears all handlers
167
+ */
168
+ removeAllListeners(event) {
169
+ if (event) {
170
+ this._handlers.delete(event);
171
+ } else {
172
+ this._handlers.clear();
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Get the number of handlers registered for a given event.
178
+ * @param {string} event
179
+ * @returns {number}
180
+ */
181
+ listenerCount(event) {
182
+ var handlers = this._handlers.get(event);
183
+ return handlers ? handlers.size : 0;
184
+ }
185
+ }
186
+
187
+ module.exports = EventBus;