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
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "tackle-harness",
3
+ "version": "0.0.2",
4
+ "description": "Plugin-based AI Agent Harness framework for Claude Code",
5
+ "main": "plugins/runtime/harness-build.js",
6
+ "bin": {
7
+ "tackle-harness": "bin/tackle.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "plugins/"
12
+ ],
13
+ "scripts": {
14
+ "build": "node bin/tackle.js build",
15
+ "validate": "node bin/tackle.js validate"
16
+ },
17
+ "keywords": [
18
+ "claude",
19
+ "claude-code",
20
+ "agent",
21
+ "harness",
22
+ "plugin",
23
+ "workflow"
24
+ ],
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=14.0.0"
28
+ }
29
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Plugin Interface - Plugin contract definitions for AI Agent Harness
3
+ *
4
+ * Defines the base interface and type markers for all plugin types:
5
+ * - ProviderPlugin: capability providers (role-registry, memory-store, etc.)
6
+ * - HookPlugin: lifecycle hooks (pre/post tool use)
7
+ * - ValidatorPlugin: output validators
8
+ * - SkillPlugin: executable skills
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ /**
14
+ * Plugin lifecycle states
15
+ */
16
+ const PluginState = {
17
+ DISCOVERED: 'discovered',
18
+ LOADED: 'loaded',
19
+ RESOLVED: 'resolved',
20
+ ACTIVATED: 'activated',
21
+ RUNNING: 'running',
22
+ DEACTIVATED: 'deactivated',
23
+ UNLOADED: 'unloaded',
24
+ };
25
+
26
+ /**
27
+ * Plugin type constants
28
+ */
29
+ const PluginType = {
30
+ SKILL: 'skill',
31
+ HOOK: 'hook',
32
+ VALIDATOR: 'validator',
33
+ PROVIDER: 'provider',
34
+ };
35
+
36
+ /**
37
+ * Base Plugin class. All plugin implementations should extend this.
38
+ *
39
+ * Subclasses must set:
40
+ * - type {string} one of PluginType values
41
+ * - name {string} unique kebab-case identifier
42
+ * - version {string} semver version string
43
+ *
44
+ * Optional overrides:
45
+ * - description {string}
46
+ * - dependencies {{ plugins?: string[], providers?: string[] }}
47
+ * - onActivate(context) called during activation
48
+ * - onDeactivate() called during deactivation
49
+ */
50
+ class Plugin {
51
+ constructor() {
52
+ /** @type {string} */
53
+ this.type = '';
54
+ /** @type {string} */
55
+ this.name = '';
56
+ /** @type {string} */
57
+ this.version = '0.0.0';
58
+ /** @type {string} */
59
+ this.description = '';
60
+ /** @type {{ plugins?: string[], providers?: string[] }} */
61
+ this.dependencies = {};
62
+ /** @type {string} current lifecycle state */
63
+ this.state = PluginState.DISCOVERED;
64
+ }
65
+
66
+ /**
67
+ * Called when the plugin is activated.
68
+ * @param {PluginContext} context - injected runtime context
69
+ */
70
+ async onActivate(context) {
71
+ // default no-op
72
+ }
73
+
74
+ /**
75
+ * Called when the plugin is deactivated.
76
+ */
77
+ async onDeactivate() {
78
+ // default no-op
79
+ }
80
+ }
81
+
82
+ /**
83
+ * SkillPlugin - executable skill plugin
84
+ *
85
+ * Additional properties:
86
+ * - triggers {string[]} keywords that activate this skill
87
+ * - metadata.stage workflow stage
88
+ * - metadata.requiresPlanMode
89
+ * - metadata.gatedByHuman
90
+ * - metadata.gatedByCode
91
+ */
92
+ class SkillPlugin extends Plugin {
93
+ constructor() {
94
+ super();
95
+ this.type = PluginType.SKILL;
96
+ /** @type {string[]} */
97
+ this.triggers = [];
98
+ /** @type {object} */
99
+ this.metadata = {};
100
+ }
101
+
102
+ /**
103
+ * Execute the skill.
104
+ * @param {PluginContext} context
105
+ * @param {object} args - skill-specific arguments
106
+ * @returns {Promise<object>} skill result
107
+ */
108
+ async execute(context, args) {
109
+ throw new Error('SkillPlugin.execute() must be implemented by subclass');
110
+ }
111
+ }
112
+
113
+ /**
114
+ * HookPlugin - lifecycle hook plugin
115
+ *
116
+ * Additional properties:
117
+ * - trigger.event {'PreToolUse' | 'PostToolUse'}
118
+ * - trigger.tools {string[]} (optional) tool filter
119
+ * - trigger.skills {string[]} (optional) skill filter
120
+ * - priority {number} execution priority (lower = earlier)
121
+ */
122
+ class HookPlugin extends Plugin {
123
+ constructor() {
124
+ super();
125
+ this.type = PluginType.HOOK;
126
+ /** @type {{ event: string, tools?: string[], skills?: string[] }} */
127
+ this.trigger = { event: '', tools: [], skills: [] };
128
+ /** @type {number} */
129
+ this.priority = 100;
130
+ }
131
+
132
+ /**
133
+ * Handle a hook invocation.
134
+ * @param {object} context - hook context
135
+ * @returns {Promise<{ allowed: boolean, reason?: string, stateChanges?: object[] }>}
136
+ */
137
+ async handle(context) {
138
+ throw new Error('HookPlugin.handle() must be implemented by subclass');
139
+ }
140
+ }
141
+
142
+ /**
143
+ * ValidatorPlugin - output validation plugin
144
+ *
145
+ * Additional properties:
146
+ * - targets {string[]} skill names this validator checks
147
+ * - blocking {boolean} whether failure stops the workflow
148
+ */
149
+ class ValidatorPlugin extends Plugin {
150
+ constructor() {
151
+ super();
152
+ this.type = PluginType.VALIDATOR;
153
+ /** @type {string[]} */
154
+ this.targets = [];
155
+ /** @type {boolean} */
156
+ this.blocking = true;
157
+ }
158
+
159
+ /**
160
+ * Run validation.
161
+ * @param {object} context - validation context
162
+ * @returns {Promise<{ passed: boolean, errors: object[], warnings: object[] }>}
163
+ */
164
+ async validate(context) {
165
+ throw new Error('ValidatorPlugin.validate() must be implemented by subclass');
166
+ }
167
+ }
168
+
169
+ /**
170
+ * ProviderPlugin - capability provider plugin
171
+ *
172
+ * Additional properties:
173
+ * - provides {string} capability identifier
174
+ */
175
+ class ProviderPlugin extends Plugin {
176
+ constructor() {
177
+ super();
178
+ this.type = PluginType.PROVIDER;
179
+ /** @type {string} */
180
+ this.provides = '';
181
+ }
182
+
183
+ /**
184
+ * Create the provider instance.
185
+ * @param {PluginContext} context
186
+ * @returns {Promise<object>} provider instance
187
+ */
188
+ async factory(context) {
189
+ throw new Error('ProviderPlugin.factory() must be implemented by subclass');
190
+ }
191
+ }
192
+
193
+ /**
194
+ * PluginContext - injected into every plugin on activation.
195
+ */
196
+ class PluginContext {
197
+ /**
198
+ * @param {string} pluginName
199
+ * @param {object} runtime - { eventBus, stateStore, logger, configManager }
200
+ */
201
+ constructor(pluginName, runtime) {
202
+ this.pluginName = pluginName;
203
+ this.eventBus = runtime.eventBus;
204
+ this.stateStore = runtime.stateStore;
205
+ this.logger = runtime.logger;
206
+ this.config = runtime.configManager;
207
+ this._runtime = runtime;
208
+ this._providerCache = new Map();
209
+ }
210
+
211
+ /**
212
+ * Lazily query a provider by name.
213
+ * @param {string} name - provider identifier
214
+ * @returns {Promise<object>}
215
+ */
216
+ async getProvider(name) {
217
+ if (this._providerCache.has(name)) {
218
+ return this._providerCache.get(name);
219
+ }
220
+ const provider = await this._runtime.getProvider(name);
221
+ this._providerCache.set(name, provider);
222
+ return provider;
223
+ }
224
+
225
+ /**
226
+ * Get another loaded plugin by name.
227
+ * @param {string} name
228
+ * @returns {Plugin|undefined}
229
+ */
230
+ getPlugin(name) {
231
+ return this._runtime.loadedPlugins.get(name);
232
+ }
233
+ }
234
+
235
+ module.exports = {
236
+ PluginState,
237
+ PluginType,
238
+ Plugin,
239
+ SkillPlugin,
240
+ HookPlugin,
241
+ ValidatorPlugin,
242
+ ProviderPlugin,
243
+ PluginContext,
244
+ };
@@ -0,0 +1,437 @@
1
+ /**
2
+ * Hook: Skill Gate (Unified)
3
+ *
4
+ * Merges three hook behaviors into one plugin:
5
+ * 1. PreToolUse(Edit|Write) - block file edits when state is in a blocked state (e.g. "waiting")
6
+ * 2. PostToolUse(Skill) - set state to "waiting" after a gated skill completes
7
+ * 3. Dynamic gated skill query - read gated skills from plugin-registry.json metadata
8
+ *
9
+ * Usage (CLI):
10
+ * node plugins/core/hook-skill-gate/index.js --pre-tool (invoked by PreToolUse hook)
11
+ * node plugins/core/hook-skill-gate/index.js --post-skill (invoked by PostToolUse hook)
12
+ *
13
+ * Usage (Programmatic):
14
+ * const SkillGateHook = require('./index.js');
15
+ * const hook = new SkillGateHook();
16
+ * await hook.onActivate(context);
17
+ * const result = await hook.handle({ event: 'PreToolUse', tool: 'Edit', ... });
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ var fs = require('fs');
23
+ var path = require('path');
24
+ var { HookPlugin } = require('../../contracts/plugin-interface');
25
+ var { StateStore, FileSystemAdapter } = require('../../runtime/state-store');
26
+
27
+ /**
28
+ * Default configuration values.
29
+ */
30
+ var DEFAULT_CONFIG = {
31
+ gatedSkills: [],
32
+ blockedStates: ['waiting'],
33
+ stateKey: 'harness.state',
34
+ };
35
+
36
+ /**
37
+ * Resolve the project root directory.
38
+ * Walks up from a starting directory to find task.md or .claude/.
39
+ *
40
+ * @param {string} [startDir] - directory to start from (default: process.cwd())
41
+ * @returns {string}
42
+ */
43
+ function resolveProjectRoot(startDir) {
44
+ var dir = startDir || process.cwd();
45
+ for (var i = 0; i < 10; i++) {
46
+ if (fs.existsSync(path.join(dir, 'task.md'))) return dir;
47
+ if (fs.existsSync(path.join(dir, '.claude'))) return dir;
48
+ var parent = path.dirname(dir);
49
+ if (parent === dir) break;
50
+ dir = parent;
51
+ }
52
+ return process.cwd();
53
+ }
54
+
55
+ /**
56
+ * Read plugin-registry.json and return skill names that are marked as gated.
57
+ *
58
+ * A skill is considered "gated" if:
59
+ * - Its plugin.json has metadata.gatedByCode === true, OR
60
+ * - It appears in the hook-skill-gate config.gatedSkills list
61
+ *
62
+ * @param {string} projectRoot
63
+ * @param {object} hookConfig - the hook-skill-gate config section
64
+ * @returns {string[]} array of gated skill names
65
+ */
66
+ function discoverGatedSkills(projectRoot, hookConfig) {
67
+ var registryPath = path.join(projectRoot, 'plugins', 'plugin-registry.json');
68
+ var gatedFromMetadata = [];
69
+ var gatedFromConfig = (hookConfig && hookConfig.gatedSkills) || [];
70
+
71
+ // 1. Read registry and scan each plugin for metadata.gatedByCode
72
+ try {
73
+ var content = fs.readFileSync(registryPath, 'utf-8');
74
+ var registry = JSON.parse(content);
75
+ var plugins = registry.plugins || [];
76
+
77
+ for (var i = 0; i < plugins.length; i++) {
78
+ var entry = plugins[i];
79
+ if (!entry.source) continue;
80
+
81
+ // Attempt to read the plugin's plugin.json for metadata
82
+ var pluginJsonPath = path.join(
83
+ projectRoot,
84
+ 'plugins',
85
+ entry.source,
86
+ 'plugin.json'
87
+ );
88
+ try {
89
+ var pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf-8'));
90
+ if (
91
+ pluginJson.metadata &&
92
+ (pluginJson.metadata.gatedByCode === true ||
93
+ pluginJson.metadata.gatedByHuman === true)
94
+ ) {
95
+ gatedFromMetadata.push(pluginJson.name || entry.name);
96
+ }
97
+ } catch (e) {
98
+ // plugin.json may not exist or be unreadable - skip
99
+ }
100
+ }
101
+ } catch (e) {
102
+ // Registry may not exist - continue with config-only list
103
+ }
104
+
105
+ // 2. Merge and deduplicate
106
+ var all = gatedFromMetadata.concat(gatedFromConfig);
107
+ var seen = {};
108
+ var result = [];
109
+ for (var j = 0; j < all.length; j++) {
110
+ if (!seen[all[j]]) {
111
+ seen[all[j]] = true;
112
+ result.push(all[j]);
113
+ }
114
+ }
115
+
116
+ return result;
117
+ }
118
+
119
+ /**
120
+ * SkillGateHook - Unified skill gate hook plugin.
121
+ *
122
+ * Extends HookPlugin from the plugin interface contract.
123
+ */
124
+ class SkillGateHook extends HookPlugin {
125
+ constructor() {
126
+ super();
127
+ this.name = 'hook-skill-gate';
128
+ this.version = '1.0.0';
129
+ this.description = '统一技能门控 Hook';
130
+ this.dependencies = { providers: ['provider:state-store'] };
131
+ this.trigger = {
132
+ event: 'PreToolUse',
133
+ tools: ['Edit', 'Write'],
134
+ skills: [],
135
+ };
136
+ this.priority = 10;
137
+
138
+ /** @type {StateStore|null} */
139
+ this._store = null;
140
+ /** @type {object} */
141
+ this._config = Object.assign({}, DEFAULT_CONFIG);
142
+ /** @type {string} */
143
+ this._projectRoot = '';
144
+ /** @type {string[]|null} cached gated skills */
145
+ this._gatedSkillsCache = null;
146
+ }
147
+
148
+ /**
149
+ * Called during plugin activation.
150
+ * Initializes state store and reads config.
151
+ *
152
+ * @param {PluginContext} context
153
+ */
154
+ async onActivate(context) {
155
+ this._projectRoot = resolveProjectRoot();
156
+ var stateFilePath = path.join(this._projectRoot, '.claude-state');
157
+ this._store = new StateStore({ filePath: stateFilePath });
158
+
159
+ // Merge context config if available
160
+ if (context && context.config) {
161
+ var pluginConfig = context.config.getPluginConfig
162
+ ? context.config.getPluginConfig(this.name)
163
+ : null;
164
+ if (pluginConfig) {
165
+ for (var key in pluginConfig) {
166
+ if (pluginConfig.hasOwnProperty(key)) {
167
+ this._config[key] = pluginConfig[key];
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ // Initial discovery of gated skills
174
+ this._gatedSkillsCache = discoverGatedSkills(this._projectRoot, this._config);
175
+ }
176
+
177
+ /**
178
+ * Main hook handler.
179
+ * Dispatches to the appropriate sub-handler based on the event type.
180
+ *
181
+ * @param {object} context - hook context
182
+ * @param {string} context.event - 'PreToolUse' or 'PostToolUse'
183
+ * @param {string} [context.tool] - tool name (for PreToolUse)
184
+ * @param {string} [context.skill] - skill name (for PostToolUse)
185
+ * @returns {Promise<{ allowed: boolean, reason?: string, stateChanges?: object[] }>}
186
+ */
187
+ async handle(context) {
188
+ if (!context || !context.event) {
189
+ return { allowed: true };
190
+ }
191
+
192
+ if (context.event === 'PreToolUse') {
193
+ return this._handlePreToolUse(context);
194
+ }
195
+
196
+ if (context.event === 'PostToolUse') {
197
+ return this._handlePostToolUse(context);
198
+ }
199
+
200
+ return { allowed: true };
201
+ }
202
+
203
+ /**
204
+ * PreToolUse handler - block Edit/Write when state is in a blocked state.
205
+ *
206
+ * @param {object} context
207
+ * @param {string} context.tool - the tool being used ('Edit', 'Write', etc.)
208
+ * @returns {Promise<{ allowed: boolean, reason?: string }>}
209
+ */
210
+ async _handlePreToolUse(context) {
211
+ var tool = context.tool || '';
212
+ var restrictedTools = ['Edit', 'Write'];
213
+
214
+ // Only gate Edit and Write operations
215
+ var isRestricted = false;
216
+ for (var i = 0; i < restrictedTools.length; i++) {
217
+ if (tool === restrictedTools[i]) {
218
+ isRestricted = true;
219
+ break;
220
+ }
221
+ }
222
+
223
+ if (!isRestricted) {
224
+ return { allowed: true };
225
+ }
226
+
227
+ // Read current state from store
228
+ var currentState = await this._getState();
229
+ var blockedStates = this._config.blockedStates || ['waiting'];
230
+
231
+ for (var j = 0; j < blockedStates.length; j++) {
232
+ if (currentState === blockedStates[j]) {
233
+ return {
234
+ allowed: false,
235
+ reason:
236
+ 'Blocked: current state is "' +
237
+ currentState +
238
+ '". ' +
239
+ 'Edit/Write operations are not allowed while waiting for human confirmation.',
240
+ };
241
+ }
242
+ }
243
+
244
+ return { allowed: true };
245
+ }
246
+
247
+ /**
248
+ * PostToolUse handler - after a gated skill executes, set state to "waiting".
249
+ *
250
+ * @param {object} context
251
+ * @param {string} context.tool - tool name (should be 'Skill')
252
+ * @param {string} [context.skill] - the skill that was executed
253
+ * @param {string} [context.skillName] - alternative field for skill name
254
+ * @returns {Promise<{ allowed: boolean, handled: boolean, newState?: string, stateChanges?: object[] }>}
255
+ */
256
+ async _handlePostToolUse(context) {
257
+ var tool = context.tool || '';
258
+ if (tool !== 'Skill') {
259
+ return { allowed: true, handled: false };
260
+ }
261
+
262
+ // Get the skill name from context
263
+ var skillName = context.skill || context.skillName || '';
264
+ if (!skillName) {
265
+ return { allowed: true, handled: false };
266
+ }
267
+
268
+ // Refresh gated skills list (dynamic query)
269
+ var gatedSkills = this._getGatedSkills();
270
+
271
+ // Check if the skill that just ran is gated
272
+ var isGated = false;
273
+ for (var i = 0; i < gatedSkills.length; i++) {
274
+ if (gatedSkills[i] === skillName) {
275
+ isGated = true;
276
+ break;
277
+ }
278
+ }
279
+
280
+ if (!isGated) {
281
+ return { allowed: true, handled: false };
282
+ }
283
+
284
+ // Set state to "waiting" to pause for human confirmation
285
+ var waitingState = 'waiting';
286
+ await this._setState(waitingState);
287
+
288
+ return {
289
+ allowed: true,
290
+ handled: true,
291
+ newState: waitingState,
292
+ stateChanges: [
293
+ {
294
+ key: this._config.stateKey,
295
+ oldValue: await this._getState(),
296
+ newValue: waitingState,
297
+ },
298
+ ],
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Get the current list of gated skills.
304
+ * Refreshes from registry on each call for dynamic discovery.
305
+ *
306
+ * @returns {string[]}
307
+ */
308
+ _getGatedSkills() {
309
+ this._gatedSkillsCache = discoverGatedSkills(
310
+ this._projectRoot,
311
+ this._config
312
+ );
313
+ return this._gatedSkillsCache;
314
+ }
315
+
316
+ /**
317
+ * Read the current state from the store.
318
+ * @returns {Promise<string|undefined>}
319
+ */
320
+ async _getState() {
321
+ if (!this._store) return undefined;
322
+ return await this._store.get(this._config.stateKey);
323
+ }
324
+
325
+ /**
326
+ * Write a new state to the store.
327
+ * @param {string} value
328
+ * @returns {Promise<void>}
329
+ */
330
+ async _setState(value) {
331
+ if (!this._store) return;
332
+ await this._store.set(this._config.stateKey, value);
333
+ }
334
+ }
335
+
336
+ // --- CLI Entry Point ---
337
+ //
338
+ // When invoked via `node index.js --pre-tool` or `node index.js --post-skill`,
339
+ // reads stdin for the hook context JSON and outputs a JSON result.
340
+
341
+ /**
342
+ * Read all data from stdin and parse as JSON.
343
+ * @param {function} callback - callback(error, data)
344
+ */
345
+ function readStdin(callback) {
346
+ var chunks = [];
347
+ process.stdin.setEncoding('utf-8');
348
+ process.stdin.on('data', function (chunk) {
349
+ chunks.push(chunk);
350
+ });
351
+ process.stdin.on('end', function () {
352
+ var raw = chunks.join('');
353
+ if (!raw.trim()) {
354
+ return callback(null, {});
355
+ }
356
+ try {
357
+ var parsed = JSON.parse(raw);
358
+ callback(null, parsed);
359
+ } catch (e) {
360
+ callback(e);
361
+ }
362
+ });
363
+ process.stdin.on('error', function (err) {
364
+ callback(err);
365
+ });
366
+ }
367
+
368
+ /**
369
+ * Main CLI function.
370
+ */
371
+ function main() {
372
+ var args = process.argv.slice(2);
373
+
374
+ if (args.length === 0) {
375
+ console.error(
376
+ 'Usage: node index.js --pre-tool | --post-skill\n' +
377
+ ' --pre-tool Run PreToolUse check (reads context from stdin)\n' +
378
+ ' --post-skill Run PostToolUse handler (reads context from stdin)'
379
+ );
380
+ process.exit(1);
381
+ }
382
+
383
+ var mode = args[0];
384
+ if (mode !== '--pre-tool' && mode !== '--post-skill') {
385
+ console.error('Unknown mode: ' + mode);
386
+ console.error('Valid modes: --pre-tool, --post-skill');
387
+ process.exit(1);
388
+ }
389
+
390
+ readStdin(function (err, context) {
391
+ if (err) {
392
+ console.error('Failed to read stdin: ' + err.message);
393
+ process.exit(1);
394
+ }
395
+
396
+ var hook = new SkillGateHook();
397
+
398
+ // Activate the hook (initialize state store, etc.)
399
+ hook
400
+ .onActivate(null)
401
+ .then(function () {
402
+ // Set the event type based on CLI mode
403
+ if (mode === '--pre-tool') {
404
+ context.event = 'PreToolUse';
405
+ } else if (mode === '--post-skill') {
406
+ context.event = 'PostToolUse';
407
+ // Normalize: if tool is not set, default to 'Skill' for post-skill mode
408
+ if (!context.tool) {
409
+ context.tool = 'Skill';
410
+ }
411
+ }
412
+
413
+ return hook.handle(context);
414
+ })
415
+ .then(function (result) {
416
+ // Output the result as JSON
417
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
418
+
419
+ // Exit with code 0 if allowed, 2 if blocked (Claude Code hook convention)
420
+ if (result.allowed === false) {
421
+ process.exit(2);
422
+ }
423
+ process.exit(0);
424
+ })
425
+ .catch(function (error) {
426
+ console.error('Hook execution failed: ' + error.message);
427
+ process.exit(1);
428
+ });
429
+ });
430
+ }
431
+
432
+ // Run CLI if executed directly (not required as a module)
433
+ if (require.main === module) {
434
+ main();
435
+ }
436
+
437
+ module.exports = SkillGateHook;