happyskills 1.0.2 → 1.2.0

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 (47) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +1 -1
  3. package/src/commands/access.js +41 -1
  4. package/src/commands/agents.js +30 -1
  5. package/src/commands/bump.js +33 -1
  6. package/src/commands/changelog.js +37 -1
  7. package/src/commands/check.js +30 -1
  8. package/src/commands/config.js +34 -1
  9. package/src/commands/convert.js +38 -1
  10. package/src/commands/delete.js +33 -1
  11. package/src/commands/diff.js +33 -1
  12. package/src/commands/disable.js +31 -1
  13. package/src/commands/enable.js +31 -1
  14. package/src/commands/feedback.js +41 -1
  15. package/src/commands/fork.js +35 -1
  16. package/src/commands/groups.js +40 -1
  17. package/src/commands/init.js +35 -1
  18. package/src/commands/install.js +35 -1
  19. package/src/commands/list.js +29 -1
  20. package/src/commands/login.js +34 -1
  21. package/src/commands/logout.js +20 -1
  22. package/src/commands/people.js +37 -1
  23. package/src/commands/postlex.js +38 -0
  24. package/src/commands/publish.js +40 -1
  25. package/src/commands/pull.js +47 -1
  26. package/src/commands/reconcile.js +36 -1
  27. package/src/commands/release.js +51 -1
  28. package/src/commands/schema.js +5 -0
  29. package/src/commands/search.js +46 -1
  30. package/src/commands/self-update.js +27 -1
  31. package/src/commands/setup.js +32 -1
  32. package/src/commands/snapshot.js +38 -1
  33. package/src/commands/status.js +26 -1
  34. package/src/commands/uninstall.js +30 -1
  35. package/src/commands/update.js +40 -1
  36. package/src/commands/validate.js +37 -1
  37. package/src/commands/versions.js +34 -1
  38. package/src/commands/visibility.js +33 -1
  39. package/src/commands/whoami.js +30 -1
  40. package/src/constants/error_codes.js +12 -1
  41. package/src/constants/next_step_actions.js +11 -1
  42. package/src/constants/next_step_by_error_code.js +27 -6
  43. package/src/constants/next_step_by_error_code.test.js +30 -0
  44. package/src/index.js +1 -1
  45. package/src/integration/cli.test.js +17 -4
  46. package/src/integration/schema.test.js +31 -0
  47. package/src/ui/output.js +7 -0
@@ -93,4 +93,33 @@ const run = (args) => catch_errors('Uninstall failed', async () => {
93
93
  }
94
94
  }).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
95
95
 
96
- module.exports = { run }
96
+ const schema = {
97
+ name: 'uninstall',
98
+ audience: 'consumer',
99
+ purpose: 'Remove one or more skills and prune their orphaned dependencies.',
100
+ mutation: true,
101
+ interactive_in_text_mode: false,
102
+ input: {
103
+ positional: [ { name: 'skills', required: true, type: 'string[]', pattern: '<owner>/<name>' } ],
104
+ flags: [
105
+ { name: 'global', alias: 'g', type: 'boolean', default: false, description: 'Remove from global scope' },
106
+ { name: 'agents', type: 'string', default: undefined, description: 'Target specific agents (comma-separated)' },
107
+ { name: 'yes', alias: 'y', type: 'boolean', default: false, description: 'Skip confirmation prompts' },
108
+ { name: 'json', type: 'boolean', default: false, description: 'Output as JSON' },
109
+ ],
110
+ },
111
+ output: {
112
+ data_shape: {
113
+ results: 'array<{ skill: string, status: string, removed: string[], orphans_pruned: string[] } | { skill: string, status: "failed", error: { code: string, message: string } }>',
114
+ },
115
+ },
116
+ errors: [
117
+ { code: 'USAGE_ERROR', next_step: { kind: 'routing', action: 'discover_schema' } },
118
+ ],
119
+ examples: [
120
+ 'happyskills uninstall acme/deploy-aws',
121
+ 'happyskills uninstall acme/deploy-aws acme/monitor acme/logging',
122
+ ],
123
+ }
124
+
125
+ module.exports = { run, schema }
@@ -333,4 +333,43 @@ const run = (args) => catch_errors('Update failed', async () => {
333
333
  }
334
334
  }).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
335
335
 
336
- module.exports = { run }
336
+ const schema = {
337
+ name: 'update',
338
+ audience: 'consumer',
339
+ purpose: 'Bring installed skills up to date by batch-checking the registry and re-installing only outdated skills.',
340
+ mutation: true,
341
+ interactive_in_text_mode: true,
342
+ input: {
343
+ positional: [{ name: 'skill', required: false, type: 'string', pattern: '<owner>/<name>' }],
344
+ flags: [
345
+ { name: 'all', type: 'boolean', default: false },
346
+ { name: 'force', type: 'boolean', default: false },
347
+ { name: 'global', type: 'boolean', default: false },
348
+ { name: 'yes', type: 'boolean', default: false },
349
+ { name: 'agents', type: 'string', default: null },
350
+ { name: 'json', type: 'boolean', default: false }
351
+ ]
352
+ },
353
+ output: {
354
+ data_shape: {
355
+ results: 'array<{ skill: string, installed: string, latest: string, status: string }>',
356
+ outdated_count: 'number',
357
+ up_to_date_count: 'number',
358
+ drift_count: 'number',
359
+ updated: 'array<{ skill: string, from: string, to: string }>',
360
+ skipped: 'array<{ skill: string, reason: string, suggestion: string }>',
361
+ already_up_to_date: 'array<{ skill: string, version: string }>',
362
+ symlink_repairs: 'array',
363
+ errors: 'array<{ skill: string, message: string }>'
364
+ }
365
+ },
366
+ errors: [
367
+ { code: 'USAGE_ERROR', next_step: { kind: 'routing', action: 'discover_schema' } }
368
+ ],
369
+ examples: [
370
+ 'happyskills update acme/deploy-aws',
371
+ 'happyskills up --all -y --json'
372
+ ]
373
+ }
374
+
375
+ module.exports = { run, schema }
@@ -232,4 +232,40 @@ const run = (args) => catch_errors('Validate failed', async () => {
232
232
  process.exit(has_errors ? EXIT_CODES.ERROR : EXIT_CODES.SUCCESS)
233
233
  }).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
234
234
 
235
- module.exports = { run }
235
+ const schema = {
236
+ name: 'validate',
237
+ audience: 'author',
238
+ purpose: 'Validate a local skill against all agentskills.io spec rules (structure, cross-file, file sizes, changelog, dependencies).',
239
+ mutation: true,
240
+ interactive_in_text_mode: false,
241
+ input: {
242
+ positional: [
243
+ { name: 'skill-name', required: true, type: 'string', pattern: '<skill-name>' },
244
+ ],
245
+ flags: [
246
+ { name: 'global', short: 'g', type: 'boolean', default: false },
247
+ { name: 'json', type: 'boolean', default: false },
248
+ ],
249
+ },
250
+ output: {
251
+ data_shape: {
252
+ skill: 'string',
253
+ valid: 'boolean',
254
+ errors: 'array',
255
+ warnings: 'array',
256
+ checks_passed: 'number',
257
+ checks_failed: 'number',
258
+ checks_warned: 'number',
259
+ },
260
+ },
261
+ errors: [
262
+ { code: 'USAGE_ERROR', next_step: { kind: 'routing', action: 'discover_schema' } },
263
+ { code: 'VALIDATION_FAILED', next_step: { kind: 'recovery', action: 'fix_validation_errors' } },
264
+ ],
265
+ examples: [
266
+ 'happyskills validate my-skill',
267
+ 'happyskills v my-skill --json',
268
+ ],
269
+ }
270
+
271
+ module.exports = { run, schema }
@@ -89,4 +89,37 @@ const run = (args) => catch_errors('Versions failed', async () => {
89
89
  print_table(['VERSION', 'PUBLISHED', 'COMMIT', 'MESSAGE'], rows)
90
90
  }).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
91
91
 
92
- module.exports = { run, extract_version, sort_refs }
92
+ const schema = {
93
+ name: 'versions',
94
+ audience: 'consumer',
95
+ purpose: 'List all published versions of a skill from the registry, newest first.',
96
+ mutation: false,
97
+ interactive_in_text_mode: false,
98
+ input: {
99
+ positional: [
100
+ { name: 'skill', required: true, type: 'string', pattern: '<owner>/<name>' },
101
+ ],
102
+ flags: [
103
+ { name: 'limit', type: 'number', default: null },
104
+ { name: 'json', type: 'boolean', default: false },
105
+ ],
106
+ },
107
+ output: {
108
+ data_shape: {
109
+ skill: 'string',
110
+ count: 'number',
111
+ versions: 'array<{ version: string, ref: string, commit: string, message: string, published_at: string|null }>',
112
+ },
113
+ },
114
+ errors: [
115
+ { code: 'USAGE_ERROR', next_step: { kind: 'routing', action: 'discover_schema' } },
116
+ { code: 'NOT_FOUND' },
117
+ { code: 'NETWORK_ERROR', next_step: { kind: 'recovery', action: 'retry' } },
118
+ ],
119
+ examples: [
120
+ 'happyskills versions acme/deploy-aws',
121
+ 'happyskills versions acme/deploy-aws --limit 20 --json',
122
+ ],
123
+ }
124
+
125
+ module.exports = { run, extract_version, sort_refs, schema }
@@ -76,4 +76,36 @@ const run = (args) => catch_errors('Visibility failed', async () => {
76
76
  }
77
77
  }).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
78
78
 
79
- module.exports = { run }
79
+ const schema = {
80
+ name: 'visibility',
81
+ audience: 'author',
82
+ purpose: 'Check or set a skill\'s visibility (public, private, workspace) in the registry.',
83
+ mutation: true,
84
+ interactive_in_text_mode: false,
85
+ input: {
86
+ positional: [
87
+ { name: 'skill', required: true, type: 'string', pattern: '<owner>/<name>' },
88
+ { name: 'visibility', required: false, type: 'string', pattern: 'public|private|workspace' },
89
+ ],
90
+ flags: [
91
+ { name: 'json', type: 'boolean', default: false },
92
+ ],
93
+ },
94
+ output: {
95
+ data_shape: {
96
+ skill: 'string',
97
+ visibility: 'string',
98
+ },
99
+ },
100
+ errors: [
101
+ { code: 'USAGE_ERROR', next_step: { kind: 'routing', action: 'discover_schema' } },
102
+ { code: 'AUTH_REQUIRED', next_step: { kind: 'recovery', action: 'login' } },
103
+ { code: 'NOT_FOUND' },
104
+ ],
105
+ examples: [
106
+ 'happyskills visibility acme/deploy-aws',
107
+ 'happyskills visibility acme/deploy-aws public',
108
+ ],
109
+ }
110
+
111
+ module.exports = { run, schema }
@@ -67,4 +67,33 @@ const run = (args) => catch_errors('Whoami failed', async () => {
67
67
  }
68
68
  }).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
69
69
 
70
- module.exports = { run }
70
+ const schema = {
71
+ name: 'whoami',
72
+ audience: 'account',
73
+ purpose: 'Show the currently authenticated user and their workspaces.',
74
+ mutation: false,
75
+ interactive_in_text_mode: false,
76
+ input: {
77
+ positional: [],
78
+ flags: [
79
+ { name: 'json', type: 'boolean', default: false }
80
+ ]
81
+ },
82
+ output: {
83
+ data_shape: {
84
+ username: 'string',
85
+ email: 'string',
86
+ workspaces: 'array'
87
+ }
88
+ },
89
+ errors: [
90
+ { code: 'AUTH_REQUIRED', next_step: { kind: 'recovery', action: 'login' } },
91
+ { code: 'NETWORK_ERROR', next_step: { kind: 'recovery', action: 'retry' } }
92
+ ],
93
+ examples: [
94
+ 'happyskills whoami',
95
+ 'happyskills whoami --json'
96
+ ]
97
+ }
98
+
99
+ module.exports = { run, schema }
@@ -14,6 +14,7 @@
14
14
  // Auth & permission ────────────────────────────────────────────────────────
15
15
  const AUTH_REQUIRED = 'AUTH_REQUIRED'
16
16
  const INVALID_CREDENTIALS = 'INVALID_CREDENTIALS'
17
+ const AUTH_FAILED = 'AUTH_FAILED'
17
18
  const EXPIRED_TOKEN = 'EXPIRED_TOKEN'
18
19
  const INTERACTIVE_REQUIRED = 'INTERACTIVE_REQUIRED'
19
20
  const FORBIDDEN = 'FORBIDDEN'
@@ -109,6 +110,13 @@ const WRITE_FAILED = 'WRITE_FAILED'
109
110
  const PUBLISH_FAILED = 'PUBLISH_FAILED'
110
111
  const RANKING_SCHEMA_MISMATCH = 'RANKING_SCHEMA_MISMATCH'
111
112
 
113
+ // pull --rebase pipeline (client-side, spec 260523-02) ──────────────────────
114
+ const COMPARE_FAILED = 'COMPARE_FAILED'
115
+ const CLONE_BASE_FAILED = 'CLONE_BASE_FAILED'
116
+ const CLONE_REMOTE_FAILED = 'CLONE_REMOTE_FAILED'
117
+ const READ_LOCAL_FAILED = 'READ_LOCAL_FAILED'
118
+ const SWAP_FAILED = 'SWAP_FAILED'
119
+
112
120
  // Internal ─────────────────────────────────────────────────────────────────
113
121
  const INTERNAL_ERROR = 'INTERNAL_ERROR'
114
122
  const PERSIST_FAILED = 'PERSIST_FAILED'
@@ -147,7 +155,8 @@ const NOT_COMMUNITY_LISTED = 'NOT_COMMUNITY_LISTED'
147
155
  const UNKNOWN_CODE = 'UNKNOWN_CODE'
148
156
 
149
157
  const ERROR_CODES = Object.freeze({
150
- AUTH_REQUIRED, INVALID_CREDENTIALS, EXPIRED_TOKEN, INTERACTIVE_REQUIRED,
158
+ AUTH_REQUIRED, INVALID_CREDENTIALS, AUTH_FAILED, EXPIRED_TOKEN,
159
+ INTERACTIVE_REQUIRED,
151
160
  FORBIDDEN, INSUFFICIENT_ACCESS, LAST_OWNER, ORG_RESTRICTED,
152
161
  NETWORK_ERROR, RATE_LIMITED, DB_UNAVAILABLE, GITHUB_UNAVAILABLE,
153
162
  EMBEDDING_UNAVAILABLE, REGISTRY_UNAVAILABLE,
@@ -172,6 +181,8 @@ const ERROR_CODES = Object.freeze({
172
181
  AUTHORIZATION_PENDING,
173
182
  COMMAND_NOT_FOUND, MIN_CLI_VERSION, SNAPSHOT_FAILED, WRITE_FAILED,
174
183
  PUBLISH_FAILED, RANKING_SCHEMA_MISMATCH,
184
+ COMPARE_FAILED, CLONE_BASE_FAILED, CLONE_REMOTE_FAILED, READ_LOCAL_FAILED,
185
+ SWAP_FAILED,
175
186
  INTERNAL_ERROR, PERSIST_FAILED, VERIFICATION_FAILED,
176
187
  NO_COGNITO_USER, COGNITO_SYNC_FAILED, COGNITO_UNLINK_FAILED,
177
188
  ALREADY_HAS_PASSWORD, EMAIL_ALIAS_CONFLICT, ALREADY_APPROVED,
@@ -31,6 +31,8 @@ const PROVIDE_CHANGELOG = 'provide_changelog'
31
31
  const SELF_UPDATE = 'self_update'
32
32
  const SHOW_FORMAT = 'show_format'
33
33
  const RETRY_RANK = 'retry_rank'
34
+ const RETRY_OR_ABANDON = 'retry_or_abandon'
35
+ const REVIEW_PUBLISH_ERROR = 'review_publish_error'
34
36
 
35
37
  // kind: clarification
36
38
  const CLARIFY_QUERY = 'clarify_query'
@@ -45,6 +47,7 @@ const SPECIFY_WORKSPACE = 'specify_workspace'
45
47
  const SPECIFY_BUMP_TYPE = 'specify_bump_type'
46
48
  const RESOLVE_BUMP_DISAGREEMENT = 'resolve_bump_disagreement'
47
49
  const PICK_VERSION = 'pick_version'
50
+ const RESOLVE_UNKNOWN_DRIFT = 'resolve_unknown_drift'
48
51
 
49
52
  // kind: confirmation
50
53
  const CONFIRM_DISCARD_OR_SNAPSHOT_FIRST = 'confirm_discard_or_snapshot_first'
@@ -59,6 +62,7 @@ const ATTACH_SCREENSHOT = 'attach_screenshot' // § 15.3.4
59
62
 
60
63
  // kind: routing
61
64
  const INSTALL_FIRST = 'install_first'
65
+ const DISCOVER_SCHEMA = 'discover_schema'
62
66
 
63
67
  // kind lookup ──────────────────────────────────────────────────────────────
64
68
  const ACTION_KIND = Object.freeze({
@@ -71,6 +75,8 @@ const ACTION_KIND = Object.freeze({
71
75
  [SELF_UPDATE]: RECOVERY,
72
76
  [SHOW_FORMAT]: RECOVERY,
73
77
  [RETRY_RANK]: RECOVERY,
78
+ [RETRY_OR_ABANDON]: RECOVERY,
79
+ [REVIEW_PUBLISH_ERROR]: RECOVERY,
74
80
 
75
81
  [CLARIFY_QUERY]: CLARIFICATION,
76
82
 
@@ -83,6 +89,7 @@ const ACTION_KIND = Object.freeze({
83
89
  [SPECIFY_BUMP_TYPE]: DECISION,
84
90
  [RESOLVE_BUMP_DISAGREEMENT]: DECISION,
85
91
  [PICK_VERSION]: DECISION,
92
+ [RESOLVE_UNKNOWN_DRIFT]: DECISION,
86
93
 
87
94
  [CONFIRM_DISCARD_OR_SNAPSHOT_FIRST]: CONFIRMATION,
88
95
  [CONFIRM_CASCADE]: CONFIRMATION,
@@ -94,19 +101,22 @@ const ACTION_KIND = Object.freeze({
94
101
  [ATTACH_SCREENSHOT]: CONTINUATION,
95
102
 
96
103
  [INSTALL_FIRST]: ROUTING,
104
+ [DISCOVER_SCHEMA]: ROUTING,
97
105
  })
98
106
 
99
107
  const NEXT_STEP_ACTIONS = Object.freeze({
100
108
  LOGIN, RETRY, RECONCILE_FIRST, PULL_REBASE_FIRST, FIX_VALIDATION_ERRORS,
101
109
  PROVIDE_CHANGELOG, SELF_UPDATE, SHOW_FORMAT, RETRY_RANK,
110
+ RETRY_OR_ABANDON, REVIEW_PUBLISH_ERROR,
102
111
  CLARIFY_QUERY,
103
112
  RESOLVE_REGRESSION, RESOLVE_MISSING_SKILL_JSON, RESOLVE_MISSING_DIR,
104
113
  RESOLVE_CONFLICTS, RESOLVE_PATCH_REJECTIONS, SPECIFY_WORKSPACE,
105
114
  SPECIFY_BUMP_TYPE, RESOLVE_BUMP_DISAGREEMENT, PICK_VERSION,
115
+ RESOLVE_UNKNOWN_DRIFT,
106
116
  CONFIRM_DISCARD_OR_SNAPSHOT_FIRST, CONFIRM_CASCADE, CONFIRM_DESTRUCTIVE,
107
117
  PASS_YES_FLAG,
108
118
  RANK_DIGESTS_INLINE, PRESENT_TO_USER, ATTACH_SCREENSHOT,
109
- INSTALL_FIRST,
119
+ INSTALL_FIRST, DISCOVER_SCHEMA,
110
120
  })
111
121
 
112
122
  const NEXT_STEP_ACTION_LIST = Object.freeze(Object.values(NEXT_STEP_ACTIONS).slice().sort())
@@ -9,9 +9,9 @@
9
9
  // applies — agent falls back to "explain to principal").
10
10
 
11
11
  const {
12
- RECOVERY, DECISION, CONFIRMATION,
12
+ RECOVERY, DECISION, CONFIRMATION, ROUTING,
13
13
  LOGIN, RETRY, RECONCILE_FIRST, PULL_REBASE_FIRST, FIX_VALIDATION_ERRORS,
14
- PROVIDE_CHANGELOG, SELF_UPDATE, SHOW_FORMAT,
14
+ PROVIDE_CHANGELOG, SELF_UPDATE, SHOW_FORMAT, DISCOVER_SCHEMA,
15
15
  RESOLVE_REGRESSION, RESOLVE_MISSING_SKILL_JSON, RESOLVE_MISSING_DIR,
16
16
  RESOLVE_CONFLICTS, RESOLVE_PATCH_REJECTIONS, SPECIFY_WORKSPACE,
17
17
  SPECIFY_BUMP_TYPE, RESOLVE_BUMP_DISAGREEMENT, PICK_VERSION,
@@ -45,12 +45,25 @@ const confirmation = (action, instructions, context = {}) => ({
45
45
  principal_authorization_required: true,
46
46
  })
47
47
 
48
+ const routing = (action, instructions, context = {}, opts = {}) => ({
49
+ kind: ROUTING,
50
+ action,
51
+ instructions,
52
+ context,
53
+ ...(opts.route_to_skill ? { route_to_skill: opts.route_to_skill } : {}),
54
+ })
55
+
48
56
  const NEXT_STEP_BY_ERROR_CODE = Object.freeze({
49
57
  AUTH_REQUIRED: () => recovery(
50
58
  LOGIN,
51
59
  'Sign in to HappySkills, then re-run the command.',
52
60
  { commands: ['npx happyskills login --browser --json'] }
53
61
  ),
62
+ AUTH_FAILED: () => recovery(
63
+ LOGIN,
64
+ 'The sign-in attempt did not complete. Start the login flow again, then re-run the command.',
65
+ { commands: ['npx happyskills login --browser --json'] }
66
+ ),
54
67
  EXPIRED_TOKEN: () => recovery(
55
68
  LOGIN,
56
69
  'Your session has expired. Sign in again, then re-run the command.',
@@ -86,13 +99,21 @@ const NEXT_STEP_BY_ERROR_CODE = Object.freeze({
86
99
  'The embedding service is temporarily unavailable. Retry shortly.',
87
100
  { retry_after_seconds: 10, max_attempts: 3 }
88
101
  ),
89
- COMMAND_NOT_FOUND: (_msg, ctx = {}) => recovery(
90
- SHOW_FORMAT,
91
- 'The command does not exist. Suggest the principal run `happyskills --help` to see available commands, or pick from the suggestion if it matches their intent.',
102
+ COMMAND_NOT_FOUND: (_msg, ctx = {}) => routing(
103
+ DISCOVER_SCHEMA,
104
+ 'The command does not exist. Run `happyskills schema --json` to discover every available command with its exact input, output, and error contract. If a suggestion is present and it matches the intent, you may re-run with that command instead.',
92
105
  {
93
106
  got: ctx.got,
94
107
  ...(ctx.suggestion ? { suggestion: ctx.suggestion } : {}),
95
- commands: ['npx happyskills --help'],
108
+ commands: ['npx happyskills schema --json'],
109
+ }
110
+ ),
111
+ USAGE_ERROR: (_msg, ctx = {}) => routing(
112
+ DISCOVER_SCHEMA,
113
+ 'The command was invoked with invalid arguments or flags. Run `happyskills schema --json` to see the exact input contract for every command, then re-run with corrected arguments.',
114
+ {
115
+ ...(ctx.got ? { got: ctx.got } : {}),
116
+ commands: ['npx happyskills schema --json'],
96
117
  }
97
118
  ),
98
119
  INVALID_SLUG: () => recovery(
@@ -0,0 +1,30 @@
1
+ 'use strict'
2
+ // Unit coverage for the schema-discovery routing defaults. A generic agent
3
+ // that reaches for the conventional `help` fallback must be routed to the
4
+ // `schema --json` surface — the machine-readable contract is what gives it
5
+ // clarity and correctness on how to use the CLI, not just discoverability.
6
+
7
+ const { describe, it } = require('node:test')
8
+ const assert = require('node:assert/strict')
9
+ const { next_step_for_error } = require('./next_step_by_error_code')
10
+ const { DISCOVER_SCHEMA, ROUTING, kind_for_action } = require('./next_step_actions')
11
+
12
+ describe('next_step_by_error_code — schema discovery routing', () => {
13
+ it('discover_schema is a routing-kind action', () => {
14
+ assert.strictEqual(kind_for_action(DISCOVER_SCHEMA), ROUTING)
15
+ })
16
+
17
+ for (const code of ['COMMAND_NOT_FOUND', 'USAGE_ERROR']) {
18
+ it(`${code} routes the agent to \`schema --json\``, () => {
19
+ const ns = next_step_for_error(code, 'boom', { got: 'foo' })
20
+ assert.ok(ns, `${code} must produce a next_step`)
21
+ assert.strictEqual(ns.kind, ROUTING)
22
+ assert.strictEqual(ns.action, DISCOVER_SCHEMA)
23
+ assert.ok(Array.isArray(ns.context.commands))
24
+ assert.ok(
25
+ ns.context.commands.some(c => c.includes('schema --json')),
26
+ `${code} next_step must point at \`happyskills schema --json\``
27
+ )
28
+ })
29
+ }
30
+ })
package/src/index.js CHANGED
@@ -206,7 +206,7 @@ const run = (argv) => {
206
206
  if (suggestion) {
207
207
  console.error(dim(` Did you mean: happyskills ${suggestion}?`))
208
208
  } else {
209
- console.error(dim(` Run happyskills --help for available commands.`))
209
+ console.error(dim(` Run happyskills --help for available commands, or happyskills schema --json for the full machine-readable surface.`))
210
210
  }
211
211
  return process.exit(EXIT_CODES.USAGE)
212
212
  }
@@ -280,14 +280,27 @@ describe('CLI — --json: stdout is always valid JSON', () => {
280
280
  assert.ok(typeof env.error.message === 'string' && env.error.message.length > 0)
281
281
  })
282
282
 
283
- it('unknown command with --json emits the COMMAND_NOT_FOUND envelope', () => {
283
+ it('unknown command with --json routes the agent to schema discovery', () => {
284
284
  const { stdout, code } = run(['not-a-command', '--json'])
285
285
  assert.strictEqual(code, 2)
286
286
  const env = parse_json_output(stdout, 'unknown-command --json')
287
287
  assert_error_envelope(env, 'COMMAND_NOT_FOUND', 'unknown command')
288
- // Recovery hint: show_format with --help in commands[].
289
- assert.strictEqual(env.next_step.action, 'show_format')
290
- assert.ok(env.next_step.context.commands.some(c => c.includes('--help')))
288
+ // Routing hint: discover_schema, pointing at `schema --json`. A confused
289
+ // agent that hit a bad command is handed the full machine-readable surface.
290
+ assert.strictEqual(env.next_step.kind, 'routing')
291
+ assert.strictEqual(env.next_step.action, 'discover_schema')
292
+ assert.ok(env.next_step.context.commands.some(c => c.includes('schema --json')))
293
+ })
294
+
295
+ it('usage error with --json routes the agent to schema discovery', () => {
296
+ // `--json --text` is mutually exclusive → USAGE_ERROR with no command-
297
+ // specific next_step, so the default discover_schema routing applies.
298
+ const { stdout, code } = run(['--json', '--text'])
299
+ assert.strictEqual(code, 2)
300
+ const env = parse_json_output(stdout, 'usage-error --json')
301
+ assert_error_envelope(env, 'USAGE_ERROR', 'mutually exclusive')
302
+ assert.strictEqual(env.next_step.action, 'discover_schema')
303
+ assert.ok(env.next_step.context.commands.some(c => c.includes('schema --json')))
291
304
  })
292
305
 
293
306
  it('network error produces an envelope with code NETWORK_ERROR + retry next_step', () => {
@@ -94,6 +94,37 @@ describe('happyskills schema --json', () => {
94
94
  }
95
95
  })
96
96
 
97
+ it('no command returns the stub purpose (anti-stub guard — spec 260602-01)', () => {
98
+ const { stdout } = run(['schema', '--json'])
99
+ const env = parse_envelope(stdout, 'schema --json anti-stub')
100
+ const stubbed = env.data.commands.filter(c => /no curated purpose/i.test(c.purpose))
101
+ assert.deepStrictEqual(
102
+ stubbed.map(c => c.name),
103
+ [],
104
+ `these commands still return the stub purpose and need a curated schema export: ${stubbed.map(c => c.name).join(', ')}`
105
+ )
106
+ })
107
+
108
+ it('every command carries a non-empty examples array of strings', () => {
109
+ const { stdout } = run(['schema', '--json'])
110
+ const env = parse_envelope(stdout, 'schema --json examples')
111
+ for (const cmd of env.data.commands) {
112
+ assert.ok(Array.isArray(cmd.examples) && cmd.examples.length > 0, `command.examples must be a non-empty array for ${cmd.name}`)
113
+ for (const ex of cmd.examples) {
114
+ assert.ok(typeof ex === 'string' && ex.length > 0, `command.examples[] entries must be non-empty strings for ${cmd.name}`)
115
+ }
116
+ }
117
+ })
118
+
119
+ it('every command.input exposes positional and flags arrays', () => {
120
+ const { stdout } = run(['schema', '--json'])
121
+ const env = parse_envelope(stdout, 'schema --json input shape')
122
+ for (const cmd of env.data.commands) {
123
+ assert.ok(Array.isArray(cmd.input.positional), `command.input.positional must be an array for ${cmd.name}`)
124
+ assert.ok(Array.isArray(cmd.input.flags), `command.input.flags must be an array for ${cmd.name}`)
125
+ }
126
+ })
127
+
97
128
  it('every command.errors[].code is in the closed enum, and the kind/action pair is valid', () => {
98
129
  const { stdout } = run(['schema', '--json'])
99
130
  const env = parse_envelope(stdout, 'schema --json error refs')
package/src/ui/output.js CHANGED
@@ -44,8 +44,15 @@ const format_help = (text) => {
44
44
  }).join('\n')
45
45
  }
46
46
 
47
+ // Every --help (global and per-command) ends with a pointer to `schema
48
+ // --json`. An agent that reaches for the conventional `help` as a fallback
49
+ // then discovers the machine-readable surface, which is what it actually
50
+ // wants for clarity and correctness on how to use the CLI.
51
+ const SCHEMA_HINT = 'For the complete machine-readable CLI surface — every command\'s inputs, outputs, and error contracts in one call — run: happyskills schema --json'
52
+
47
53
  const print_help = (text) => {
48
54
  console.log(format_help(text))
55
+ if (!is_json_mode()) console.log(dim(`\n${SCHEMA_HINT}`))
49
56
  }
50
57
 
51
58
  // Visible length of a string, ignoring ANSI escape codes