happyskills 1.0.2 → 1.1.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.0] - 2026-06-02
11
+
12
+ ### Added
13
+
14
+ - Add `discover_schema` routing action (`next_step.kind: routing`) so a confused agent is steered to the machine-readable CLI surface. Every `--help` output (global and per-command) now ends with a footer pointing to `happyskills schema --json`, and the text-mode unknown-command message points there too — agents reach for the conventional `help`, not the unconventional `schema`, so every help-seeking path now leads back to it. Added to both the CLI and API closed-enum mirrors (additive, non-breaking).
15
+
16
+ ### Changed
17
+
18
+ - Route the `COMMAND_NOT_FOUND` and `USAGE_ERROR` `--json` envelopes to `discover_schema` (previously `show_format` / none), with `context.commands: ["npx happyskills schema --json"]` — a mistyped-command or bad-flags agent now receives the full CLI contract through the structured `next_step` channel it is already parsing.
19
+
10
20
  ## [1.0.2] - 2026-06-01
11
21
 
12
22
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Package manager for AI agent skills",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Nicolas Dao <nic@cloudlesslabs.com> (https://cloudlesslabs.com)",
@@ -59,6 +59,7 @@ const ATTACH_SCREENSHOT = 'attach_screenshot' // § 15.3.4
59
59
 
60
60
  // kind: routing
61
61
  const INSTALL_FIRST = 'install_first'
62
+ const DISCOVER_SCHEMA = 'discover_schema'
62
63
 
63
64
  // kind lookup ──────────────────────────────────────────────────────────────
64
65
  const ACTION_KIND = Object.freeze({
@@ -94,6 +95,7 @@ const ACTION_KIND = Object.freeze({
94
95
  [ATTACH_SCREENSHOT]: CONTINUATION,
95
96
 
96
97
  [INSTALL_FIRST]: ROUTING,
98
+ [DISCOVER_SCHEMA]: ROUTING,
97
99
  })
98
100
 
99
101
  const NEXT_STEP_ACTIONS = Object.freeze({
@@ -106,7 +108,7 @@ const NEXT_STEP_ACTIONS = Object.freeze({
106
108
  CONFIRM_DISCARD_OR_SNAPSHOT_FIRST, CONFIRM_CASCADE, CONFIRM_DESTRUCTIVE,
107
109
  PASS_YES_FLAG,
108
110
  RANK_DIGESTS_INLINE, PRESENT_TO_USER, ATTACH_SCREENSHOT,
109
- INSTALL_FIRST,
111
+ INSTALL_FIRST, DISCOVER_SCHEMA,
110
112
  })
111
113
 
112
114
  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,6 +45,14 @@ 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,
@@ -86,13 +94,21 @@ const NEXT_STEP_BY_ERROR_CODE = Object.freeze({
86
94
  'The embedding service is temporarily unavailable. Retry shortly.',
87
95
  { retry_after_seconds: 10, max_attempts: 3 }
88
96
  ),
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.',
97
+ COMMAND_NOT_FOUND: (_msg, ctx = {}) => routing(
98
+ DISCOVER_SCHEMA,
99
+ '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
100
  {
93
101
  got: ctx.got,
94
102
  ...(ctx.suggestion ? { suggestion: ctx.suggestion } : {}),
95
- commands: ['npx happyskills --help'],
103
+ commands: ['npx happyskills schema --json'],
104
+ }
105
+ ),
106
+ USAGE_ERROR: (_msg, ctx = {}) => routing(
107
+ DISCOVER_SCHEMA,
108
+ '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.',
109
+ {
110
+ ...(ctx.got ? { got: ctx.got } : {}),
111
+ commands: ['npx happyskills schema --json'],
96
112
  }
97
113
  ),
98
114
  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', () => {
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