@open-agent-toolkit/cli 0.1.32 → 0.1.34

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 (43) hide show
  1. package/assets/docs/cli-utilities/config-and-local-state.md +21 -1
  2. package/assets/docs/cli-utilities/configuration.md +17 -6
  3. package/assets/docs/cli-utilities/index.md +5 -0
  4. package/assets/docs/cli-utilities/tool-packs.md +1 -1
  5. package/assets/docs/cli-utilities/workflow-gates.md +163 -0
  6. package/assets/docs/contributing/skills.md +25 -0
  7. package/assets/docs/reference/cli-reference.md +17 -14
  8. package/assets/docs/reference/file-locations.md +1 -0
  9. package/assets/docs/reference/oat-directory-structure.md +3 -0
  10. package/assets/public-package-versions.json +4 -4
  11. package/assets/scripts/resolve-tracking.sh +52 -0
  12. package/assets/skills/oat-project-implement/SKILL.md +23 -1
  13. package/assets/skills/oat-project-plan/SKILL.md +23 -1
  14. package/dist/commands/doctor/index.d.ts +4 -1
  15. package/dist/commands/doctor/index.d.ts.map +1 -1
  16. package/dist/commands/doctor/index.js +12 -3
  17. package/dist/commands/gate/index.d.ts +35 -0
  18. package/dist/commands/gate/index.d.ts.map +1 -0
  19. package/dist/commands/gate/index.js +500 -0
  20. package/dist/commands/index.d.ts.map +1 -1
  21. package/dist/commands/index.js +2 -0
  22. package/dist/commands/internal/validate-oat-skills.d.ts +3 -0
  23. package/dist/commands/internal/validate-oat-skills.d.ts.map +1 -1
  24. package/dist/commands/internal/validate-oat-skills.js +44 -2
  25. package/dist/commands/pjm/doctor.d.ts +5 -1
  26. package/dist/commands/pjm/doctor.d.ts.map +1 -1
  27. package/dist/commands/pjm/doctor.js +13 -1
  28. package/dist/commands/pjm/index.d.ts.map +1 -1
  29. package/dist/commands/pjm/index.js +5 -2
  30. package/dist/commands/sync/apply.d.ts.map +1 -1
  31. package/dist/commands/sync/apply.js +6 -2
  32. package/dist/config/oat-config.d.ts +22 -0
  33. package/dist/config/oat-config.d.ts.map +1 -1
  34. package/dist/config/oat-config.js +136 -0
  35. package/dist/config/resolve.d.ts +3 -1
  36. package/dist/config/resolve.d.ts.map +1 -1
  37. package/dist/config/resolve.js +103 -1
  38. package/dist/manifest/manager.d.ts.map +1 -1
  39. package/dist/manifest/manager.js +4 -1
  40. package/dist/validation/skills.d.ts +2 -0
  41. package/dist/validation/skills.d.ts.map +1 -1
  42. package/dist/validation/skills.js +39 -0
  43. package/package.json +2 -2
@@ -3,9 +3,10 @@ import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { buildCommandContext, } from '../../app/command-context.js';
5
5
  import { compareVersions } from '../init/tools/shared/version.js';
6
- import { runPjmDoctorChecks } from '../pjm/doctor.js';
6
+ import { createPjmDisabledCheck, runPjmDoctorChecks, } from '../pjm/doctor.js';
7
7
  import { getSkillVersion } from '../shared/frontmatter.js';
8
8
  import { readGlobalOptions, resolveConcreteScopes, } from '../shared/shared.utils.js';
9
+ import { readOatConfig } from '../../config/oat-config.js';
9
10
  import { resolveAssetsRoot } from '../../fs/assets.js';
10
11
  import { resolveProjectRoot, resolveScopeRoot } from '../../fs/paths.js';
11
12
  import TOML from '@iarna/toml';
@@ -126,6 +127,7 @@ function createDependencies() {
126
127
  checkProviders: checkProvidersDefault,
127
128
  readFile: async (path) => readFile(path, 'utf8'),
128
129
  resolveAssetsRoot,
130
+ readOatConfig,
129
131
  runPjmDoctorChecks,
130
132
  // Default binding remains self-contained, but still honors the caller-
131
133
  // provided pathExists dependency from runChecksForScope when available.
@@ -342,8 +344,15 @@ async function runChecksForScope(scope, scopeRoot, dependencies) {
342
344
  }
343
345
  }
344
346
  const repoReferenceRoot = join(scopeRoot, '.oat', 'repo');
345
- if (await dependencies.pathExists(repoReferenceRoot)) {
346
- checks.push(...(await dependencies.runPjmDoctorChecks(repoReferenceRoot)));
347
+ const config = await dependencies.readOatConfig(scopeRoot);
348
+ const projectManagementEnabled = config.tools?.['project-management'] === true;
349
+ if (projectManagementEnabled) {
350
+ checks.push(...(await dependencies.runPjmDoctorChecks(repoReferenceRoot, {
351
+ projectManagementEnabled: true,
352
+ })));
353
+ }
354
+ else if (await dependencies.pathExists(repoReferenceRoot)) {
355
+ checks.push(createPjmDisabledCheck());
347
356
  }
348
357
  }
349
358
  return checks;
@@ -0,0 +1,35 @@
1
+ import { type CommandContext, type GlobalOptions } from '../../app/command-context.js';
2
+ import { type ExecTarget, type OatConfig, type OatLocalConfig, type UserConfig } from '../../config/oat-config.js';
3
+ import { type ResolvedConfig } from '../../config/resolve.js';
4
+ import { Command } from 'commander';
5
+ interface GateCommandDependencies {
6
+ buildCommandContext: (options: GlobalOptions) => CommandContext;
7
+ resolveProjectRoot: (cwd: string) => Promise<string>;
8
+ readOatConfig: (repoRoot: string) => Promise<OatConfig>;
9
+ writeOatConfig: (repoRoot: string, config: OatConfig) => Promise<void>;
10
+ readOatLocalConfig: (repoRoot: string) => Promise<OatLocalConfig>;
11
+ writeOatLocalConfig: (repoRoot: string, config: OatLocalConfig) => Promise<void>;
12
+ readUserConfig: (userConfigDir: string) => Promise<UserConfig>;
13
+ writeUserConfig: (userConfigDir: string, config: UserConfig) => Promise<void>;
14
+ resolveEffectiveConfig: (repoRoot: string, userConfigDir: string, env: NodeJS.ProcessEnv) => Promise<ResolvedConfig>;
15
+ runProcess: (command: string, args: string[], options: ProcessRunOptions) => Promise<ProcessRunResult>;
16
+ processEnv: NodeJS.ProcessEnv;
17
+ }
18
+ interface ProcessRunOptions {
19
+ cwd: string;
20
+ env: NodeJS.ProcessEnv;
21
+ purpose: 'host-detection' | 'availability' | 'execute';
22
+ stdio: 'ignore' | 'inherit';
23
+ }
24
+ interface ProcessRunResult {
25
+ exitCode: number;
26
+ }
27
+ type CrossProviderAvoid = 'same-runtime' | 'none';
28
+ export interface SelectedExecTarget {
29
+ id: string;
30
+ target: ExecTarget;
31
+ }
32
+ export declare function selectExecTarget(registry: Readonly<Record<string, ExecTarget>>, currentRuntime: string, avoid: CrossProviderAvoid): SelectedExecTarget | null;
33
+ export declare function createGateCommand(overrides?: Partial<GateCommandDependencies>): Command;
34
+ export {};
35
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAQL,KAAK,UAAU,EAIf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,UAAU,EAEhB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,UAAU,uBAAuB;IAC/B,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;IACxD,cAAc,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,kBAAkB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IAClE,mBAAmB,EAAE,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,cAAc,KACnB,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,cAAc,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/D,eAAe,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,sBAAsB,EAAE,CACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,CAAC,UAAU,KACnB,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,UAAU,EAAE,CACV,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,iBAAiB,KACvB,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B;AAmCD,UAAU,iBAAiB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,OAAO,EAAE,gBAAgB,GAAG,cAAc,GAAG,SAAS,CAAC;IACvD,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAC;CAC7B;AAED,UAAU,gBAAgB;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,kBAAkB,GAAG,cAAc,GAAG,MAAM,CAAC;AAKlD,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;CACpB;AAmWD,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,EAC9C,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,kBAAkB,GACxB,kBAAkB,GAAG,IAAI,CAE3B;AA2SD,wBAAgB,iBAAiB,CAC/B,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CAoIT"}
@@ -0,0 +1,500 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join } from 'node:path';
3
+ import { buildCommandContext, } from '../../app/command-context.js';
4
+ import { readGlobalOptions } from '../shared/shared.utils.js';
5
+ import { BUILTIN_EXEC_TARGETS, readOatConfig, readOatLocalConfig, readUserConfig, writeOatConfig, writeOatLocalConfig, writeUserConfig, } from '../../config/oat-config.js';
6
+ import { resolveEffectiveConfig, resolveExecTargets, resolveGate, } from '../../config/resolve.js';
7
+ import { resolveProjectRoot } from '../../fs/paths.js';
8
+ import { Command } from 'commander';
9
+ const DEFAULT_DEPENDENCIES = {
10
+ buildCommandContext,
11
+ resolveProjectRoot,
12
+ readOatConfig,
13
+ writeOatConfig,
14
+ readOatLocalConfig,
15
+ writeOatLocalConfig,
16
+ readUserConfig,
17
+ writeUserConfig,
18
+ resolveEffectiveConfig,
19
+ runProcess: runChildProcess,
20
+ processEnv: process.env,
21
+ };
22
+ const VALID_ON_FAILURE = ['block', 'prompt', 'warn'];
23
+ const VALID_WRITE_LAYERS = [
24
+ 'shared',
25
+ 'local',
26
+ 'user',
27
+ ];
28
+ const VALID_CROSS_PROVIDER_AVOIDS = [
29
+ 'same-runtime',
30
+ 'none',
31
+ ];
32
+ async function runChildProcess(command, args, options) {
33
+ return new Promise((resolve, reject) => {
34
+ const child = spawn(command, args, {
35
+ cwd: options.cwd,
36
+ env: options.env,
37
+ stdio: options.stdio,
38
+ });
39
+ child.on('error', reject);
40
+ child.on('close', (code) => {
41
+ resolve({ exitCode: code ?? 1 });
42
+ });
43
+ });
44
+ }
45
+ function isGateWriteLayer(value) {
46
+ return VALID_WRITE_LAYERS.includes(value);
47
+ }
48
+ function writeError(context, error) {
49
+ const message = error instanceof Error ? error.message : String(error);
50
+ if (context.json) {
51
+ context.logger.json({ status: 'error', message });
52
+ }
53
+ else {
54
+ context.logger.error(message);
55
+ }
56
+ process.exitCode = 1;
57
+ }
58
+ function writeJsonValue(context, payload) {
59
+ if (context.json) {
60
+ context.logger.json(payload);
61
+ return;
62
+ }
63
+ context.logger.info(JSON.stringify(payload, null, 2));
64
+ }
65
+ function writeSuccess(context, payload) {
66
+ if (context.json) {
67
+ context.logger.json({ status: 'ok', ...payload });
68
+ return;
69
+ }
70
+ context.logger.info(JSON.stringify({ status: 'ok', ...payload }, null, 2));
71
+ }
72
+ function trimRequired(value, label) {
73
+ const trimmed = value.trim();
74
+ if (!trimmed) {
75
+ throw new Error(`${label} must be a non-empty string.`);
76
+ }
77
+ return trimmed;
78
+ }
79
+ function parseLayer(value) {
80
+ const layer = value?.trim() || 'user';
81
+ if (!isGateWriteLayer(layer)) {
82
+ throw new Error('--layer must be one of shared | local | user; auto is not supported for gate writes.');
83
+ }
84
+ return layer;
85
+ }
86
+ function parseCrossProviderAvoid(value) {
87
+ const avoid = value?.trim() || 'same-runtime';
88
+ if (!VALID_CROSS_PROVIDER_AVOIDS.includes(avoid)) {
89
+ throw new Error('--avoid must be one of same-runtime | none.');
90
+ }
91
+ return avoid;
92
+ }
93
+ function parseOnFailure(value) {
94
+ if (!value || !VALID_ON_FAILURE.includes(value)) {
95
+ throw new Error('--on-failure must be one of block | prompt | warn.');
96
+ }
97
+ return value;
98
+ }
99
+ function parsePositiveInteger(value, flag, defaultValue) {
100
+ if (value === undefined) {
101
+ return defaultValue;
102
+ }
103
+ const parsed = Number(value);
104
+ if (!Number.isInteger(parsed) || parsed < 1) {
105
+ throw new Error(`${flag} must be an integer greater than or equal to 1.`);
106
+ }
107
+ return parsed;
108
+ }
109
+ function parseNumericFlag(value, flag, defaultValue) {
110
+ if (value === undefined) {
111
+ return defaultValue;
112
+ }
113
+ const parsed = Number(value);
114
+ if (!Number.isFinite(parsed)) {
115
+ throw new Error(`${flag} must be a finite number.`);
116
+ }
117
+ return parsed;
118
+ }
119
+ function parseArgvJson(value, flag) {
120
+ let parsed;
121
+ try {
122
+ parsed = JSON.parse(value);
123
+ }
124
+ catch (error) {
125
+ const detail = error instanceof Error ? `: ${error.message}` : '';
126
+ throw new Error(`${flag} must be valid JSON${detail}`, { cause: error });
127
+ }
128
+ if (!Array.isArray(parsed) ||
129
+ parsed.length === 0 ||
130
+ !parsed.every((item) => typeof item === 'string') ||
131
+ !parsed[0]?.trim()) {
132
+ throw new Error(`${flag} must be a non-empty JSON array of strings whose first item is not empty.`);
133
+ }
134
+ return [...parsed];
135
+ }
136
+ function parseOptionalArgvJson(value, flag) {
137
+ return value === undefined ? undefined : parseArgvJson(value, flag);
138
+ }
139
+ function parseGateConfig(options) {
140
+ if (options.disable) {
141
+ return null;
142
+ }
143
+ return {
144
+ command: trimRequired(options.command ?? '', '--command'),
145
+ onFailure: parseOnFailure(options.onFailure),
146
+ maxAttempts: parsePositiveInteger(options.maxAttempts, '--max-attempts', 2),
147
+ ...(options.description?.trim()
148
+ ? { description: options.description.trim() }
149
+ : {}),
150
+ };
151
+ }
152
+ function parseExecTargetConfig(options) {
153
+ if (options.disable) {
154
+ return null;
155
+ }
156
+ const baseCommandJson = options.baseCommandJson;
157
+ if (baseCommandJson === undefined) {
158
+ throw new Error('--base-command-json is required unless --disable is set.');
159
+ }
160
+ return {
161
+ runtime: trimRequired(options.runtime ?? '', '--runtime'),
162
+ baseCommand: parseArgvJson(baseCommandJson, '--base-command-json'),
163
+ priority: parseNumericFlag(options.priority, '--priority', 0),
164
+ ...(options.hostDetectionJson !== undefined
165
+ ? {
166
+ hostDetectionCommand: parseOptionalArgvJson(options.hostDetectionJson, '--host-detection-json'),
167
+ }
168
+ : {}),
169
+ ...(options.availabilityJson !== undefined
170
+ ? {
171
+ availabilityCommand: parseOptionalArgvJson(options.availabilityJson, '--availability-json'),
172
+ }
173
+ : {}),
174
+ };
175
+ }
176
+ function updateWorkflowGates(config, update) {
177
+ return {
178
+ ...config,
179
+ workflow: {
180
+ ...config.workflow,
181
+ gates: update(config.workflow?.gates ?? {}),
182
+ },
183
+ };
184
+ }
185
+ function setSkillGate(config, skillName, gate) {
186
+ return updateWorkflowGates(config, (gates) => ({
187
+ ...gates,
188
+ skills: {
189
+ ...gates.skills,
190
+ [skillName]: gate,
191
+ },
192
+ }));
193
+ }
194
+ function unsetSkillGate(config, skillName) {
195
+ return updateWorkflowGates(config, (gates) => {
196
+ const skills = { ...gates.skills };
197
+ delete skills[skillName];
198
+ return {
199
+ ...gates,
200
+ skills,
201
+ };
202
+ });
203
+ }
204
+ function setExecTarget(config, targetId, target) {
205
+ return updateWorkflowGates(config, (gates) => ({
206
+ ...gates,
207
+ execTargets: {
208
+ ...gates.execTargets,
209
+ [targetId]: target,
210
+ },
211
+ }));
212
+ }
213
+ function unsetExecTarget(config, targetId) {
214
+ return updateWorkflowGates(config, (gates) => {
215
+ const execTargets = { ...gates.execTargets };
216
+ delete execTargets[targetId];
217
+ return {
218
+ ...gates,
219
+ execTargets,
220
+ };
221
+ });
222
+ }
223
+ async function readEffectiveConfig(context, dependencies) {
224
+ const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
225
+ const userConfigDir = join(context.home, '.oat');
226
+ return dependencies.resolveEffectiveConfig(repoRoot, userConfigDir, dependencies.processEnv);
227
+ }
228
+ function sortedExecTargetEntries(registry) {
229
+ return Object.entries(registry)
230
+ .map(([id, target]) => ({ id, target }))
231
+ .sort((left, right) => {
232
+ const priority = right.target.priority - left.target.priority;
233
+ return priority === 0 ? left.id.localeCompare(right.id) : priority;
234
+ });
235
+ }
236
+ function cloneExecTarget(target) {
237
+ return {
238
+ runtime: target.runtime,
239
+ baseCommand: [...target.baseCommand],
240
+ priority: target.priority,
241
+ ...(target.hostDetectionCommand
242
+ ? { hostDetectionCommand: [...target.hostDetectionCommand] }
243
+ : {}),
244
+ ...(target.availabilityCommand
245
+ ? { availabilityCommand: [...target.availabilityCommand] }
246
+ : {}),
247
+ };
248
+ }
249
+ function argvHead(argv) {
250
+ return [argv[0] ?? '', argv.slice(1)];
251
+ }
252
+ function listExecTargetCandidates(registry, currentRuntime, avoid) {
253
+ const shouldAvoidSameRuntime = avoid === 'same-runtime' && currentRuntime !== 'unknown';
254
+ return sortedExecTargetEntries(registry)
255
+ .filter(({ target }) => !shouldAvoidSameRuntime || target.runtime !== currentRuntime)
256
+ .map(({ id, target }) => ({ id, target: cloneExecTarget(target) }));
257
+ }
258
+ export function selectExecTarget(registry, currentRuntime, avoid) {
259
+ return listExecTargetCandidates(registry, currentRuntime, avoid)[0] ?? null;
260
+ }
261
+ async function checkArgv(argv, purpose, context, dependencies) {
262
+ const [command, args] = argvHead(argv);
263
+ if (!command) {
264
+ return false;
265
+ }
266
+ try {
267
+ const result = await dependencies.runProcess(command, args, {
268
+ cwd: context.cwd,
269
+ env: dependencies.processEnv,
270
+ purpose,
271
+ stdio: 'ignore',
272
+ });
273
+ return result.exitCode === 0;
274
+ }
275
+ catch {
276
+ return false;
277
+ }
278
+ }
279
+ async function resolveCurrentRuntime(currentRuntime, context, dependencies) {
280
+ const override = currentRuntime?.trim();
281
+ if (override) {
282
+ return override;
283
+ }
284
+ for (const { target } of sortedExecTargetEntries(BUILTIN_EXEC_TARGETS)) {
285
+ if (target.hostDetectionCommand &&
286
+ (await checkArgv(target.hostDetectionCommand, 'host-detection', context, dependencies))) {
287
+ return target.runtime;
288
+ }
289
+ }
290
+ return 'unknown';
291
+ }
292
+ async function selectAvailableExecTarget(registry, currentRuntime, avoid, context, dependencies) {
293
+ for (const candidate of listExecTargetCandidates(registry, currentRuntime, avoid)) {
294
+ const availabilityCommand = candidate.target.availabilityCommand;
295
+ if (!availabilityCommand ||
296
+ (await checkArgv(availabilityCommand, 'availability', context, dependencies))) {
297
+ return candidate;
298
+ }
299
+ }
300
+ return null;
301
+ }
302
+ async function executeTarget(selected, prompt, context, dependencies) {
303
+ const [command, baseArgs] = argvHead(selected.target.baseCommand);
304
+ if (!command) {
305
+ throw new Error(`Exec target "${selected.id}" has an empty base command.`);
306
+ }
307
+ try {
308
+ const result = await dependencies.runProcess(command, [...baseArgs, ...prompt], {
309
+ cwd: context.cwd,
310
+ env: dependencies.processEnv,
311
+ purpose: 'execute',
312
+ stdio: 'inherit',
313
+ });
314
+ return result.exitCode;
315
+ }
316
+ catch (error) {
317
+ const message = error instanceof Error ? error.message : String(error);
318
+ throw new Error(`Failed to launch exec target "${selected.id}" (${command}): ${message}`, { cause: error });
319
+ }
320
+ }
321
+ function noEligibleTargetMessage(currentRuntime, avoid) {
322
+ return `No eligible gate exec target found for current runtime "${currentRuntime}" with --avoid ${avoid}. Install or configure an alternate runtime, rerun with --avoid none, or pin a target with --target <id>.`;
323
+ }
324
+ async function updateConfigLayer(context, layer, dependencies, mutate) {
325
+ const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
326
+ const userConfigDir = join(context.home, '.oat');
327
+ if (layer === 'shared') {
328
+ const config = await dependencies.readOatConfig(repoRoot);
329
+ await dependencies.writeOatConfig(repoRoot, mutate(config));
330
+ return;
331
+ }
332
+ if (layer === 'local') {
333
+ const config = await dependencies.readOatLocalConfig(repoRoot);
334
+ await dependencies.writeOatLocalConfig(repoRoot, mutate(config));
335
+ return;
336
+ }
337
+ const config = await dependencies.readUserConfig(userConfigDir);
338
+ await dependencies.writeUserConfig(userConfigDir, mutate(config));
339
+ }
340
+ async function runResolve(skillName, context, dependencies) {
341
+ try {
342
+ const effective = await readEffectiveConfig(context, dependencies);
343
+ writeJsonValue(context, resolveGate(effective, skillName));
344
+ process.exitCode = 0;
345
+ }
346
+ catch (error) {
347
+ writeError(context, error);
348
+ }
349
+ }
350
+ async function runGateSet(skillName, options, context, dependencies) {
351
+ try {
352
+ const layer = parseLayer(options.layer);
353
+ const normalizedSkill = trimRequired(skillName, '<skill>');
354
+ const gate = parseGateConfig(options);
355
+ await updateConfigLayer(context, layer, dependencies, (config) => setSkillGate(config, normalizedSkill, gate));
356
+ writeSuccess(context, { layer, skill: normalizedSkill, gate });
357
+ process.exitCode = 0;
358
+ }
359
+ catch (error) {
360
+ writeError(context, error);
361
+ }
362
+ }
363
+ async function runGateUnset(skillName, options, context, dependencies) {
364
+ try {
365
+ const layer = parseLayer(options.layer);
366
+ const normalizedSkill = trimRequired(skillName, '<skill>');
367
+ await updateConfigLayer(context, layer, dependencies, (config) => unsetSkillGate(config, normalizedSkill));
368
+ writeSuccess(context, { layer, skill: normalizedSkill });
369
+ process.exitCode = 0;
370
+ }
371
+ catch (error) {
372
+ writeError(context, error);
373
+ }
374
+ }
375
+ async function runTargetSet(targetId, options, context, dependencies) {
376
+ try {
377
+ const layer = parseLayer(options.layer);
378
+ const normalizedTargetId = trimRequired(targetId, '<id>');
379
+ const target = parseExecTargetConfig(options);
380
+ await updateConfigLayer(context, layer, dependencies, (config) => setExecTarget(config, normalizedTargetId, target));
381
+ writeSuccess(context, { layer, target: normalizedTargetId, value: target });
382
+ process.exitCode = 0;
383
+ }
384
+ catch (error) {
385
+ writeError(context, error);
386
+ }
387
+ }
388
+ async function runTargetUnset(targetId, options, context, dependencies) {
389
+ try {
390
+ const layer = parseLayer(options.layer);
391
+ const normalizedTargetId = trimRequired(targetId, '<id>');
392
+ await updateConfigLayer(context, layer, dependencies, (config) => unsetExecTarget(config, normalizedTargetId));
393
+ writeSuccess(context, { layer, target: normalizedTargetId });
394
+ process.exitCode = 0;
395
+ }
396
+ catch (error) {
397
+ writeError(context, error);
398
+ }
399
+ }
400
+ async function runCrossProviderExec(prompt, options, context, dependencies) {
401
+ try {
402
+ const effective = await readEffectiveConfig(context, dependencies);
403
+ const targets = resolveExecTargets(effective);
404
+ const explicitTarget = options.target?.trim();
405
+ if (explicitTarget) {
406
+ const target = targets[explicitTarget];
407
+ if (!target) {
408
+ throw new Error(`Unknown exec target "${explicitTarget}".`);
409
+ }
410
+ process.exitCode = await executeTarget({ id: explicitTarget, target: cloneExecTarget(target) }, prompt, context, dependencies);
411
+ return;
412
+ }
413
+ const avoid = parseCrossProviderAvoid(options.avoid);
414
+ const currentRuntime = await resolveCurrentRuntime(options.currentRuntime, context, dependencies);
415
+ const selected = await selectAvailableExecTarget(targets, currentRuntime, avoid, context, dependencies);
416
+ if (!selected) {
417
+ throw new Error(noEligibleTargetMessage(currentRuntime, avoid));
418
+ }
419
+ process.exitCode = await executeTarget(selected, prompt, context, dependencies);
420
+ }
421
+ catch (error) {
422
+ writeError(context, error);
423
+ }
424
+ }
425
+ export function createGateCommand(overrides = {}) {
426
+ const dependencies = {
427
+ ...DEFAULT_DEPENDENCIES,
428
+ ...overrides,
429
+ };
430
+ const cmd = new Command('gate').description('Resolve and manage workflow gate configuration');
431
+ cmd
432
+ .command('resolve')
433
+ .description('Print the resolved gate configuration for a skill')
434
+ .argument('<skill>', 'Gate-aware skill name')
435
+ .action(async (skillName, _options, command) => {
436
+ const context = dependencies.buildCommandContext(readGlobalOptions(command));
437
+ await runResolve(skillName, context, dependencies);
438
+ });
439
+ cmd
440
+ .command('set')
441
+ .description('Set a skill gate in a concrete config layer')
442
+ .argument('<skill>', 'Gate-aware skill name')
443
+ .option('--command <command>', 'Gate command to run')
444
+ .option('--description <description>', 'Gate purpose and follow-up notes')
445
+ .option('--on-failure <mode>', 'Failure behavior: block, prompt, or warn')
446
+ .option('--max-attempts <count>', 'Maximum block remediation attempts')
447
+ .option('--disable', 'Disable this skill gate in the selected layer')
448
+ .option('--layer <layer>', 'Config layer to write: shared, local, or user')
449
+ .action(async (skillName, options, command) => {
450
+ const context = dependencies.buildCommandContext(readGlobalOptions(command));
451
+ await runGateSet(skillName, options, context, dependencies);
452
+ });
453
+ cmd
454
+ .command('unset')
455
+ .description('Remove a skill gate entry from a concrete config layer')
456
+ .argument('<skill>', 'Gate-aware skill name')
457
+ .option('--layer <layer>', 'Config layer to write: shared, local, or user')
458
+ .action(async (skillName, options, command) => {
459
+ const context = dependencies.buildCommandContext(readGlobalOptions(command));
460
+ await runGateUnset(skillName, options, context, dependencies);
461
+ });
462
+ cmd
463
+ .command('cross-provider-exec')
464
+ .description('Run a prompt through an alternate configured runtime target')
465
+ .option('--target <id>', 'Run this exact exec target')
466
+ .option('--avoid <mode>', 'Avoidance mode: same-runtime or none')
467
+ .option('--current-runtime <runtime>', 'Override detected runtime for testing or manual routing')
468
+ .argument('<prompt...>', 'Prompt arguments appended to the target command')
469
+ .action(async (prompt, options, command) => {
470
+ const context = dependencies.buildCommandContext(readGlobalOptions(command));
471
+ await runCrossProviderExec(prompt, options, context, dependencies);
472
+ });
473
+ const target = new Command('target').description('Manage gate execution targets');
474
+ target
475
+ .command('set')
476
+ .description('Set an exec target in a concrete config layer')
477
+ .argument('<id>', 'Exec target id')
478
+ .option('--runtime <runtime>', 'Logical runtime family')
479
+ .option('--base-command-json <json>', 'JSON argv array for the base command')
480
+ .option('--host-detection-json <json>', 'JSON argv array for host detection')
481
+ .option('--availability-json <json>', 'JSON argv array for availability')
482
+ .option('--priority <number>', 'Target priority, higher wins')
483
+ .option('--disable', 'Disable this exec target in the selected layer')
484
+ .option('--layer <layer>', 'Config layer to write: shared, local, or user')
485
+ .action(async (targetId, options, command) => {
486
+ const context = dependencies.buildCommandContext(readGlobalOptions(command));
487
+ await runTargetSet(targetId, options, context, dependencies);
488
+ });
489
+ target
490
+ .command('unset')
491
+ .description('Remove an exec target entry from a concrete config layer')
492
+ .argument('<id>', 'Exec target id')
493
+ .option('--layer <layer>', 'Config layer to write: shared, local, or user')
494
+ .action(async (targetId, options, command) => {
495
+ const context = dependencies.buildCommandContext(readGlobalOptions(command));
496
+ await runTargetUnset(targetId, options, context, dependencies);
497
+ });
498
+ cmd.addCommand(target);
499
+ return cmd;
500
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwBzC,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAsBvD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyBzC,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuBvD"}
@@ -4,6 +4,7 @@ import { createConfigCommand } from './config/index.js';
4
4
  import { createDecisionCommand } from './decision/index.js';
5
5
  import { createDocsCommand } from './docs/index.js';
6
6
  import { createDoctorCommand } from './doctor/index.js';
7
+ import { createGateCommand } from './gate/index.js';
7
8
  import { createIndexCommand } from './index-cmd/index.js';
8
9
  import { createInitCommand } from './init/index.js';
9
10
  import { createInstructionsCommand } from './instructions/index.js';
@@ -26,6 +27,7 @@ export function registerCommands(program) {
26
27
  program.addCommand(createStatusCommand());
27
28
  program.addCommand(createSyncCommand());
28
29
  program.addCommand(createConfigCommand());
30
+ program.addCommand(createGateCommand());
29
31
  program.addCommand(createLocalCommand());
30
32
  program.addCommand(createProvidersCommand());
31
33
  program.addCommand(createRemoveCommand());
@@ -1,9 +1,12 @@
1
1
  import { type CommandContext, type GlobalOptions } from '../../app/command-context.js';
2
+ import { type ResolvedConfig } from '../../config/resolve.js';
2
3
  import { type ValidateOatSkillsOptions, type ValidateOatSkillsResult } from '../../validation/index.js';
3
4
  import { Command } from 'commander';
4
5
  interface ValidateOatSkillsDependencies {
5
6
  buildCommandContext: (options: GlobalOptions) => CommandContext;
6
7
  validateOatSkills: (repoRoot: string, options?: ValidateOatSkillsOptions) => Promise<ValidateOatSkillsResult>;
8
+ resolveEffectiveConfig: (repoRoot: string, userConfigDir: string, env?: NodeJS.ProcessEnv) => Promise<ResolvedConfig>;
9
+ env?: NodeJS.ProcessEnv;
7
10
  }
8
11
  export declare function createValidateOatSkillsCommand(overrides?: Partial<ValidateOatSkillsDependencies>): Command;
9
12
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"validate-oat-skills.d.ts","sourceRoot":"","sources":["../../../src/commands/internal/validate-oat-skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC7B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,UAAU,6BAA6B;IACrC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,iBAAiB,EAAE,CACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,wBAAwB,KAC/B,OAAO,CAAC,uBAAuB,CAAC,CAAC;CACvC;AAkED,wBAAgB,8BAA8B,CAC5C,SAAS,GAAE,OAAO,CAAC,6BAA6B,CAAM,GACrD,OAAO,CAkBT"}
1
+ {"version":3,"file":"validate-oat-skills.d.ts","sourceRoot":"","sources":["../../../src/commands/internal/validate-oat-skills.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAE7B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,UAAU,6BAA6B;IACrC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,iBAAiB,EAAE,CACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,wBAAwB,KAC/B,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACtC,sBAAsB,EAAE,CACtB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,KACpB,OAAO,CAAC,cAAc,CAAC,CAAC;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AA+HD,wBAAgB,8BAA8B,CAC5C,SAAS,GAAE,OAAO,CAAC,6BAA6B,CAAM,GACrD,OAAO,CAkBT"}