happyskills 0.53.0 → 1.0.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 (43) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +1 -1
  3. package/src/api/auth.js +18 -2
  4. package/src/api/client.js +29 -3
  5. package/src/api/feedback.js +14 -5
  6. package/src/api/repos.js +28 -10
  7. package/src/api/translate.js +90 -0
  8. package/src/commands/delete.js +15 -1
  9. package/src/commands/feedback.js +2 -2
  10. package/src/commands/init.js +5 -1
  11. package/src/commands/install.js +58 -32
  12. package/src/commands/postlex.js +53 -35
  13. package/src/commands/postlex.test.js +48 -18
  14. package/src/commands/pull.js +5 -1
  15. package/src/commands/reconcile.js +52 -4
  16. package/src/commands/release.js +45 -15
  17. package/src/commands/schema.js +179 -0
  18. package/src/commands/search.js +34 -22
  19. package/src/commands/search.test.js +59 -33
  20. package/src/commands/uninstall.js +20 -11
  21. package/src/commands/validate.js +33 -11
  22. package/src/constants/error_codes.js +197 -0
  23. package/src/constants/exit_codes.js +54 -0
  24. package/src/constants/next_step_actions.js +133 -0
  25. package/src/constants/next_step_by_error_code.js +249 -0
  26. package/src/constants.js +2 -1
  27. package/src/index.js +51 -7
  28. package/src/integration/api_envelope.test.js +499 -0
  29. package/src/integration/bump.test.js +13 -4
  30. package/src/integration/cli.test.js +169 -147
  31. package/src/integration/drift.test.js +16 -4
  32. package/src/integration/install_fresh.test.js +37 -29
  33. package/src/integration/reconcile.test.js +77 -56
  34. package/src/integration/release.test.js +48 -31
  35. package/src/integration/schema.test.js +167 -0
  36. package/src/schema/envelope.schema.json +73 -0
  37. package/src/schema/envelope_test_helpers.js +94 -0
  38. package/src/schema/envelope_validator.js +239 -0
  39. package/src/schema/envelope_validator.test.js +333 -0
  40. package/src/ui/envelope.js +171 -0
  41. package/src/ui/output.js +66 -2
  42. package/src/utils/errors.js +116 -47
  43. package/src/utils/intent.js +22 -1
@@ -1,12 +1,36 @@
1
1
  // Unit tests for the next_step envelope emission rules on
2
- // cli/src/commands/search.js. Spec 260521-01 v2 § 5.1.
2
+ // cli/src/commands/search.js. Spec 260521-01 v2 § 5.1 + spec
3
+ // 260525-cli-default-json § 4.5 / § 7.
3
4
  //
4
- // The orchestration around the actual HTTP call is covered by manual E2E
5
- // testing (spec § 7); here we lock down the pure decision logic.
5
+ // Locked envelope contract: build_search_next_step returns either {} (no
6
+ // follow-up) or a populated three-key object { kind, action, instructions }
7
+ // plus optional context / principal_authorization_required / route_to_skill.
8
+ // Action names come from the closed enum (NEXT_STEP_ACTIONS); the legacy
9
+ // short names (`clarify`, `present_to_user` alone) no longer apply where the
10
+ // new enum supersedes them.
6
11
 
7
12
  const { describe, it } = require('node:test')
8
13
  const assert = require('node:assert/strict')
9
14
  const { build_search_next_step } = require('./search')
15
+ const {
16
+ NEXT_STEP_KINDS,
17
+ NEXT_STEP_ACTIONS,
18
+ kind_for_action,
19
+ } = require('../constants/next_step_actions')
20
+
21
+ const is_empty = (o) => o && typeof o === 'object' && !Array.isArray(o) && Object.keys(o).length === 0
22
+
23
+ const assert_envelope_next_step = (ns, expected_action, label) => {
24
+ assert.ok(ns && typeof ns === 'object', `${label}: next_step must be an object`)
25
+ if (expected_action === null) {
26
+ assert.ok(is_empty(ns), `${label}: expected empty {} next_step, got ${JSON.stringify(ns)}`)
27
+ return
28
+ }
29
+ assert.strictEqual(ns.action, expected_action, `${label}: action mismatch`)
30
+ assert.ok(NEXT_STEP_KINDS.includes(ns.kind), `${label}: kind "${ns.kind}" not in closed enum`)
31
+ assert.strictEqual(ns.kind, kind_for_action(expected_action), `${label}: kind must match action's kind`)
32
+ assert.ok(typeof ns.instructions === 'string' && ns.instructions.length > 0, `${label}: instructions must be non-empty`)
33
+ }
10
34
 
11
35
  const make_response = (overrides = {}) => ({
12
36
  mode: 'semantic',
@@ -17,38 +41,38 @@ const make_response = (overrides = {}) => ({
17
41
  ...overrides,
18
42
  })
19
43
 
20
- describe('build_search_next_step', () => {
21
- it('returns null when --with-rerank is not set', () => {
44
+ describe('build_search_next_step — envelope shape', () => {
45
+ it('returns empty {} when --with-rerank is not set', () => {
22
46
  const r = build_search_next_step(make_response({ rerank_digests: [{ candidate_id: 1 }] }), 'q', {
23
47
  with_rerank: false,
24
48
  clarification_turns_used: 0,
25
49
  })
26
- assert.equal(r, null)
50
+ assert_envelope_next_step(r, null, 'with_rerank=false')
27
51
  })
28
52
 
29
- it('returns null when mode is fuzzy_slug (no rerank for non-semantic modes)', () => {
53
+ it('returns empty {} when mode is fuzzy_slug (no rerank for non-semantic modes)', () => {
30
54
  const r = build_search_next_step(make_response({ mode: 'fuzzy_slug' }), 'q', {
31
55
  with_rerank: true, clarification_turns_used: 0,
32
56
  })
33
- assert.equal(r, null)
57
+ assert_envelope_next_step(r, null, 'fuzzy_slug')
34
58
  })
35
59
 
36
- it('returns null when mode is fuzzy_scoped', () => {
60
+ it('returns empty {} when mode is fuzzy_scoped', () => {
37
61
  const r = build_search_next_step(make_response({ mode: 'fuzzy_scoped' }), 'q', {
38
62
  with_rerank: true, clarification_turns_used: 0,
39
63
  })
40
- assert.equal(r, null)
64
+ assert_envelope_next_step(r, null, 'fuzzy_scoped')
41
65
  })
42
66
 
43
- it('emits rank_digests_inline when semantic mode + digests present', () => {
67
+ it('emits rank_digests_inline (kind=continuation) when semantic + digests present', () => {
44
68
  const r = build_search_next_step(make_response({
45
69
  rerank_digests: [{ candidate_id: 1, digest: '...' }, { candidate_id: 2, digest: '...' }],
46
70
  }), 'deploy aws', {
47
71
  with_rerank: true, clarification_turns_used: 0,
48
72
  })
49
- assert.equal(r.action, 'rank_digests_inline')
50
- assert.equal(r.context.original_query, 'deploy aws')
51
- assert.equal(r.context.clarification_turns_used, 0)
73
+ assert_envelope_next_step(r, NEXT_STEP_ACTIONS.RANK_DIGESTS_INLINE, 'rank inline')
74
+ assert.strictEqual(r.context.original_query, 'deploy aws')
75
+ assert.strictEqual(r.context.clarification_turns_used, 0)
52
76
  assert.match(r.instructions, /happyskills postlex/)
53
77
  // Instructions should carry the budget forward to the next call.
54
78
  assert.match(r.instructions, /--clarification-turns-used 0/)
@@ -58,65 +82,67 @@ describe('build_search_next_step', () => {
58
82
  const r = build_search_next_step(make_response({
59
83
  rerank_digests: [{ candidate_id: 1 }],
60
84
  }), 'q', { with_rerank: true, clarification_turns_used: 1 })
85
+ assert_envelope_next_step(r, NEXT_STEP_ACTIONS.RANK_DIGESTS_INLINE, 'rank inline with budget')
61
86
  assert.match(r.instructions, /--clarification-turns-used 1/)
62
- assert.equal(r.context.clarification_turns_used, 1)
87
+ assert.strictEqual(r.context.clarification_turns_used, 1)
63
88
  })
64
89
 
65
- it('emits clarify when semantic + no digests + match_notice fires + budget remains', () => {
90
+ it('emits clarify_query (kind=clarification) when semantic + no digests + match_notice + budget remains', () => {
66
91
  const r = build_search_next_step(make_response({
67
92
  match_notice: 'No strong or good matches.',
68
93
  }), 'vague query', { with_rerank: true, clarification_turns_used: 0 })
69
- assert.equal(r.action, 'clarify')
70
- assert.equal(r.max_turns_remaining, 2)
71
- // Always last option is "Just search anyway".
72
- const opts = r.suggested_questions[0].options
94
+ assert_envelope_next_step(r, NEXT_STEP_ACTIONS.CLARIFY_QUERY, 'clarify')
95
+ // suggested_questions and max_turns_remaining now live INSIDE context.
96
+ assert.strictEqual(r.context.max_turns_remaining, 2)
97
+ assert.ok(Array.isArray(r.context.suggested_questions))
98
+ const opts = r.context.suggested_questions[0].options
73
99
  assert.match(opts[opts.length - 1].label, /search anyway/i)
100
+ assert.strictEqual(r.principal_authorization_required, true)
74
101
  // Instructions carry the incremented budget forward.
75
102
  assert.match(r.instructions, /--clarification-turns-used 1/)
76
103
  })
77
104
 
78
- it('emits present_to_user (budget spent) when match_notice fires AND turns_used >= 2', () => {
105
+ it('emits present_to_user (kind=continuation) when match_notice fires AND turns_used >= 2', () => {
79
106
  const r = build_search_next_step(make_response({
80
107
  match_notice: 'No strong or good matches.',
81
108
  }), 'q', { with_rerank: true, clarification_turns_used: 2 })
82
- assert.equal(r.action, 'present_to_user')
109
+ assert_envelope_next_step(r, NEXT_STEP_ACTIONS.PRESENT_TO_USER, 'budget spent')
83
110
  assert.match(r.instructions, /budget.*spent/i)
84
111
  })
85
112
 
86
- it('returns null when semantic + no digests + no match_notice (no protocol applies)', () => {
113
+ it('returns empty {} when semantic + no digests + no match_notice (no protocol applies)', () => {
87
114
  const r = build_search_next_step(make_response(), 'q', {
88
115
  with_rerank: true, clarification_turns_used: 0,
89
116
  })
90
- assert.equal(r, null)
117
+ assert_envelope_next_step(r, null, 'no protocol applies')
91
118
  })
92
119
 
93
120
  it('treats empty rerank_digests array as "no digests"', () => {
94
121
  const r = build_search_next_step(make_response({ rerank_digests: [] }), 'q', {
95
122
  with_rerank: true, clarification_turns_used: 0,
96
123
  })
97
- // No digests → would clarify if notice fired; here no notice, so null.
98
- assert.equal(r, null)
124
+ assert_envelope_next_step(r, null, 'empty digests')
99
125
  })
100
126
 
101
127
  it('clamps clarification_turns_used to [0, 2]', () => {
102
128
  const r_neg = build_search_next_step(make_response({ match_notice: 'weak' }), 'q', {
103
129
  with_rerank: true, clarification_turns_used: -5,
104
130
  })
105
- assert.equal(r_neg.max_turns_remaining, 2)
131
+ assert.strictEqual(r_neg.context.max_turns_remaining, 2)
106
132
  const r_big = build_search_next_step(make_response({ match_notice: 'weak' }), 'q', {
107
133
  with_rerank: true, clarification_turns_used: 99,
108
134
  })
109
- assert.equal(r_big.action, 'present_to_user')
135
+ assert_envelope_next_step(r_big, NEXT_STEP_ACTIONS.PRESENT_TO_USER, 'budget overflow')
110
136
  })
111
137
 
112
- it('the clarify suggested_questions always includes 4 options ending in "Just search anyway"', () => {
138
+ it('the clarify_query context.suggested_questions always includes 4 options ending in "Just search anyway"', () => {
113
139
  const r = build_search_next_step(make_response({ match_notice: 'weak' }), 'q', {
114
140
  with_rerank: true, clarification_turns_used: 0,
115
141
  })
116
- assert.equal(r.suggested_questions.length, 1)
117
- const opts = r.suggested_questions[0].options
118
- assert.equal(opts.length, 4)
142
+ assert.strictEqual(r.context.suggested_questions.length, 1)
143
+ const opts = r.context.suggested_questions[0].options
144
+ assert.strictEqual(opts.length, 4)
119
145
  assert.match(opts[3].label, /search anyway/i)
120
- assert.equal(opts[3].refined_query_hint, null)
146
+ assert.strictEqual(opts[3].refined_query_hint, null)
121
147
  })
122
148
  })
@@ -69,17 +69,26 @@ const run = (args) => catch_errors('Uninstall failed', async () => {
69
69
  }
70
70
 
71
71
  if (args.flags.json) {
72
- const items = results.map(({ skill, result }) => ({
73
- skill,
74
- removed: result.removed || [],
75
- orphans_pruned: result.orphans_pruned || []
76
- }))
77
- const data = results.length === 1 && failures.length === 0 ? items[0] : items
78
- if (failures.length > 0) {
79
- print_json({ data, errors: failures })
80
- } else {
81
- print_json({ data })
82
- }
72
+ // Spec 260525-cli-default-json § 4.3 rules 2 + 4:
73
+ // - No shape-flipping between single and batch — always emit
74
+ // `data.results: [...]` so skills iterate uniformly.
75
+ // - Per-row failures live inside `data.results[i].error`, not at
76
+ // the envelope root (top-level `error` is reserved for
77
+ // whole-operation failures).
78
+ const items = [
79
+ ...results.map(({ skill, result }) => ({
80
+ skill,
81
+ status: 'uninstalled',
82
+ removed: result.removed || [],
83
+ orphans_pruned: result.orphans_pruned || []
84
+ })),
85
+ ...failures.map(({ skill, error }) => ({
86
+ skill,
87
+ status: 'failed',
88
+ error: { code: 'INTERNAL_ERROR', message: error }
89
+ }))
90
+ ]
91
+ print_json({ data: { results: items } })
83
92
  return
84
93
  }
85
94
  }).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
@@ -96,17 +96,39 @@ const format_json = (skill_name, all_results) => {
96
96
  const failed = all_results.filter(r => r.severity === 'error')
97
97
  const warned = all_results.filter(r => r.severity === 'warning')
98
98
 
99
- print_json({
100
- data: {
101
- skill: skill_name,
102
- valid: failed.length === 0,
103
- errors,
104
- warnings,
105
- checks_passed: passed.length,
106
- checks_failed: failed.length,
107
- checks_warned: warned.length
108
- }
109
- })
99
+ const { emit_envelope } = require('../ui/envelope')
100
+ const { ERROR_CODES } = require('../constants/error_codes')
101
+ const data = {
102
+ skill: skill_name,
103
+ valid: failed.length === 0,
104
+ errors,
105
+ warnings,
106
+ checks_passed: passed.length,
107
+ checks_failed: failed.length,
108
+ checks_warned: warned.length,
109
+ }
110
+ if (failed.length > 0) {
111
+ emit_envelope({
112
+ data,
113
+ error: {
114
+ code: ERROR_CODES.VALIDATION_FAILED,
115
+ message: `Validation failed: ${failed.length} error${failed.length === 1 ? '' : 's'}.`,
116
+ validation_errors: errors,
117
+ },
118
+ next_step: {
119
+ kind: 'recovery',
120
+ action: 'fix_validation_errors',
121
+ instructions: 'Fix the listed validation errors and re-run.',
122
+ context: {
123
+ validation_errors: errors,
124
+ commands: [`npx happyskills validate ${skill_name} --json`],
125
+ },
126
+ },
127
+ meta_overrides: { exit_code: 1 },
128
+ })
129
+ return
130
+ }
131
+ emit_envelope({ data })
110
132
  }
111
133
 
112
134
  const run = (args) => catch_errors('Validate failed', async () => {
@@ -0,0 +1,197 @@
1
+ 'use strict'
2
+ // Closed enum of error codes emitted by the CLI envelope. Mirrors spec
3
+ // 260525-cli-default-json § 5. Skeleton-only — Session 2 wires it into
4
+ // emit_envelope / exit_with_error. Until then, only the test suite consumes
5
+ // it (via envelope_validator).
6
+ //
7
+ // Renaming a code is a breaking envelope-schema change. Adding a code is
8
+ // non-breaking. Removing a code is breaking.
9
+ //
10
+ // In Session 2 a publish-time copy step keeps this file byte-identical to
11
+ // api/app/constants/error_codes.js. For Session 1b they evolve in lockstep
12
+ // by inspection.
13
+
14
+ // Auth & permission ────────────────────────────────────────────────────────
15
+ const AUTH_REQUIRED = 'AUTH_REQUIRED'
16
+ const INVALID_CREDENTIALS = 'INVALID_CREDENTIALS'
17
+ const EXPIRED_TOKEN = 'EXPIRED_TOKEN'
18
+ const INTERACTIVE_REQUIRED = 'INTERACTIVE_REQUIRED'
19
+ const FORBIDDEN = 'FORBIDDEN'
20
+ const INSUFFICIENT_ACCESS = 'INSUFFICIENT_ACCESS'
21
+ const LAST_OWNER = 'LAST_OWNER'
22
+ const ORG_RESTRICTED = 'ORG_RESTRICTED'
23
+
24
+ // Network & transient ──────────────────────────────────────────────────────
25
+ const NETWORK_ERROR = 'NETWORK_ERROR'
26
+ const RATE_LIMITED = 'RATE_LIMITED'
27
+ const DB_UNAVAILABLE = 'DB_UNAVAILABLE'
28
+ const GITHUB_UNAVAILABLE = 'GITHUB_UNAVAILABLE'
29
+ const EMBEDDING_UNAVAILABLE = 'EMBEDDING_UNAVAILABLE'
30
+ const REGISTRY_UNAVAILABLE = 'REGISTRY_UNAVAILABLE'
31
+
32
+ // Validation — input ───────────────────────────────────────────────────────
33
+ const USAGE_ERROR = 'USAGE_ERROR'
34
+ const INVALID_BODY = 'INVALID_BODY'
35
+ const INVALID_SLUG = 'INVALID_SLUG'
36
+ const INVALID_VERSION = 'INVALID_VERSION'
37
+ const INVALID_BUMP = 'INVALID_BUMP'
38
+ const INVALID_NAME = 'INVALID_NAME'
39
+ const INVALID_PARAM = 'INVALID_PARAM'
40
+ const INVALID_VALUE = 'INVALID_VALUE'
41
+ const INVALID_VISIBILITY = 'INVALID_VISIBILITY'
42
+ const INVALID_PERMISSION = 'INVALID_PERMISSION'
43
+ const INVALID_ROLE = 'INVALID_ROLE'
44
+ const INVALID_SCOPE = 'INVALID_SCOPE'
45
+ const INVALID_STATE = 'INVALID_STATE'
46
+ const INVALID_STATUS = 'INVALID_STATUS'
47
+ const INVALID_OPERATION = 'INVALID_OPERATION'
48
+ const INVALID_CATEGORY = 'INVALID_CATEGORY'
49
+ const INVALID_FILTER = 'INVALID_FILTER'
50
+ const MISSING_FIELDS = 'MISSING_FIELDS'
51
+
52
+ // Validation — semantic ────────────────────────────────────────────────────
53
+ const VALIDATION_FAILED = 'VALIDATION_FAILED'
54
+ const DEPENDENCY_VALIDATION_FAILED = 'DEPENDENCY_VALIDATION_FAILED'
55
+ const MISSING_CHANGELOG_ENTRY = 'MISSING_CHANGELOG_ENTRY'
56
+ const CHANGELOG_SOURCE_UNREADABLE = 'CHANGELOG_SOURCE_UNREADABLE'
57
+
58
+ // State & lifecycle ────────────────────────────────────────────────────────
59
+ const NOT_FOUND = 'NOT_FOUND'
60
+ const ALREADY_EXISTS = 'ALREADY_EXISTS'
61
+ const VERSION_EXISTS = 'VERSION_EXISTS'
62
+ const VERSION_NOT_FOUND = 'VERSION_NOT_FOUND'
63
+ const CONFLICT = 'CONFLICT'
64
+ const DIVERGED = 'DIVERGED'
65
+ const DRIFT_DETECTED = 'DRIFT_DETECTED'
66
+ const LOCAL_EDITS_PRESENT = 'LOCAL_EDITS_PRESENT'
67
+ const MISSING_VERSION = 'MISSING_VERSION'
68
+ const BUMP_DISAGREEMENT = 'BUMP_DISAGREEMENT'
69
+ const SLUG_TAKEN = 'SLUG_TAKEN'
70
+ const RESOLUTION_FAILED = 'RESOLUTION_FAILED'
71
+ const REGISTRY_INTEGRITY_FAILURE = 'REGISTRY_INTEGRITY_FAILURE'
72
+ const FORK_PARENT_NOT_FOUND = 'FORK_PARENT_NOT_FOUND'
73
+ const WORKSPACE_UNRESOLVED = 'WORKSPACE_UNRESOLVED'
74
+ const WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND'
75
+
76
+ // Confirmation ─────────────────────────────────────────────────────────────
77
+ const CONFIRMATION_REQUIRED = 'CONFIRMATION_REQUIRED'
78
+ const DEPENDENCY_CASCADE_REQUIRED = 'DEPENDENCY_CASCADE_REQUIRED'
79
+
80
+ // Size limits ──────────────────────────────────────────────────────────────
81
+ const PAYLOAD_TOO_LARGE = 'PAYLOAD_TOO_LARGE'
82
+ const FILE_TOO_LARGE = 'FILE_TOO_LARGE'
83
+ const BODY_TOO_LONG = 'BODY_TOO_LONG'
84
+ const SUBJECT_TOO_LONG = 'SUBJECT_TOO_LONG'
85
+ const ATTACHMENT_TOO_LARGE = 'ATTACHMENT_TOO_LARGE'
86
+ const TOTAL_ATTACHMENTS_TOO_LARGE = 'TOTAL_ATTACHMENTS_TOO_LARGE'
87
+ const ATTACHMENT_LIMIT_REACHED = 'ATTACHMENT_LIMIT_REACHED'
88
+ const CLIENT_CONTEXT_TOO_LARGE = 'CLIENT_CONTEXT_TOO_LARGE'
89
+
90
+ // GitHub / OAuth ───────────────────────────────────────────────────────────
91
+ const GITHUB_NOT_LINKED = 'GITHUB_NOT_LINKED'
92
+ const GITHUB_ALREADY_LINKED = 'GITHUB_ALREADY_LINKED'
93
+ const CANNOT_UNLINK = 'CANNOT_UNLINK'
94
+ const NO_GITHUB_MAPPING = 'NO_GITHUB_MAPPING'
95
+ const CLAIM_EXPIRED = 'CLAIM_EXPIRED'
96
+ const NO_PENDING_CLAIM = 'NO_PENDING_CLAIM'
97
+ const ALREADY_CLAIMED = 'ALREADY_CLAIMED'
98
+
99
+ // Device flow ──────────────────────────────────────────────────────────────
100
+ // Emitted by /auth/device/token on HTTP 202 polls (spec § 15.3.2). Not a
101
+ // terminal failure — paired with a recovery next_step (action: retry).
102
+ const AUTHORIZATION_PENDING = 'AUTHORIZATION_PENDING'
103
+
104
+ // CLI-only ─────────────────────────────────────────────────────────────────
105
+ const COMMAND_NOT_FOUND = 'COMMAND_NOT_FOUND'
106
+ const MIN_CLI_VERSION = 'MIN_CLI_VERSION'
107
+ const SNAPSHOT_FAILED = 'SNAPSHOT_FAILED'
108
+ const WRITE_FAILED = 'WRITE_FAILED'
109
+ const PUBLISH_FAILED = 'PUBLISH_FAILED'
110
+ const RANKING_SCHEMA_MISMATCH = 'RANKING_SCHEMA_MISMATCH'
111
+
112
+ // Internal ─────────────────────────────────────────────────────────────────
113
+ const INTERNAL_ERROR = 'INTERNAL_ERROR'
114
+ const PERSIST_FAILED = 'PERSIST_FAILED'
115
+ const VERIFICATION_FAILED = 'VERIFICATION_FAILED'
116
+
117
+ // Domain-specific codes used by API routes (additive — spec § 5 allows
118
+ // non-breaking additions). Grouped by route family.
119
+ // Auth / GitHub flows
120
+ const NO_COGNITO_USER = 'NO_COGNITO_USER'
121
+ const COGNITO_SYNC_FAILED = 'COGNITO_SYNC_FAILED'
122
+ const COGNITO_UNLINK_FAILED = 'COGNITO_UNLINK_FAILED'
123
+ const ALREADY_HAS_PASSWORD = 'ALREADY_HAS_PASSWORD'
124
+ const EMAIL_ALIAS_CONFLICT = 'EMAIL_ALIAS_CONFLICT'
125
+ const ALREADY_APPROVED = 'ALREADY_APPROVED'
126
+ // Membership / access
127
+ const ALREADY_MEMBER = 'ALREADY_MEMBER'
128
+ const ALREADY_COLLABORATOR = 'ALREADY_COLLABORATOR'
129
+ const DEPENDENCY_ACCESS_DENIED = 'DEPENDENCY_ACCESS_DENIED'
130
+ const GROUP_NAME_TAKEN = 'GROUP_NAME_TAKEN'
131
+ const GROUP_ALREADY_GRANTED = 'GROUP_ALREADY_GRANTED'
132
+ // Body / params
133
+ const BODY_REQUIRED = 'BODY_REQUIRED'
134
+ const FORBIDDEN_FIELD = 'FORBIDDEN_FIELD'
135
+ const INVALID_BYTES = 'INVALID_BYTES'
136
+ const INVALID_CONTENT_TYPE = 'INVALID_CONTENT_TYPE'
137
+ const INVALID_EVENT = 'INVALID_EVENT'
138
+ const INVALID_PARENTS = 'INVALID_PARENTS'
139
+ const TOO_MANY = 'TOO_MANY'
140
+ // Uploads
141
+ const INVALID_UPLOAD_ID = 'INVALID_UPLOAD_ID'
142
+ const UPLOAD_NOT_FOUND = 'UPLOAD_NOT_FOUND'
143
+ // Listing / discovery
144
+ const NOT_COMMUNITY_LISTED = 'NOT_COMMUNITY_LISTED'
145
+
146
+ // Forward-compat escape hatch (§ 5.2) ──────────────────────────────────────
147
+ const UNKNOWN_CODE = 'UNKNOWN_CODE'
148
+
149
+ const ERROR_CODES = Object.freeze({
150
+ AUTH_REQUIRED, INVALID_CREDENTIALS, EXPIRED_TOKEN, INTERACTIVE_REQUIRED,
151
+ FORBIDDEN, INSUFFICIENT_ACCESS, LAST_OWNER, ORG_RESTRICTED,
152
+ NETWORK_ERROR, RATE_LIMITED, DB_UNAVAILABLE, GITHUB_UNAVAILABLE,
153
+ EMBEDDING_UNAVAILABLE, REGISTRY_UNAVAILABLE,
154
+ USAGE_ERROR, INVALID_BODY, INVALID_SLUG, INVALID_VERSION, INVALID_BUMP,
155
+ INVALID_NAME, INVALID_PARAM, INVALID_VALUE, INVALID_VISIBILITY,
156
+ INVALID_PERMISSION, INVALID_ROLE, INVALID_SCOPE, INVALID_STATE,
157
+ INVALID_STATUS, INVALID_OPERATION, INVALID_CATEGORY, INVALID_FILTER,
158
+ MISSING_FIELDS,
159
+ VALIDATION_FAILED, DEPENDENCY_VALIDATION_FAILED, MISSING_CHANGELOG_ENTRY,
160
+ CHANGELOG_SOURCE_UNREADABLE,
161
+ NOT_FOUND, ALREADY_EXISTS, VERSION_EXISTS, VERSION_NOT_FOUND, CONFLICT,
162
+ DIVERGED, DRIFT_DETECTED, LOCAL_EDITS_PRESENT, MISSING_VERSION,
163
+ BUMP_DISAGREEMENT, SLUG_TAKEN, RESOLUTION_FAILED,
164
+ REGISTRY_INTEGRITY_FAILURE, FORK_PARENT_NOT_FOUND, WORKSPACE_UNRESOLVED,
165
+ WORKSPACE_NOT_FOUND,
166
+ CONFIRMATION_REQUIRED, DEPENDENCY_CASCADE_REQUIRED,
167
+ PAYLOAD_TOO_LARGE, FILE_TOO_LARGE, BODY_TOO_LONG, SUBJECT_TOO_LONG,
168
+ ATTACHMENT_TOO_LARGE, TOTAL_ATTACHMENTS_TOO_LARGE,
169
+ ATTACHMENT_LIMIT_REACHED, CLIENT_CONTEXT_TOO_LARGE,
170
+ GITHUB_NOT_LINKED, GITHUB_ALREADY_LINKED, CANNOT_UNLINK, NO_GITHUB_MAPPING,
171
+ CLAIM_EXPIRED, NO_PENDING_CLAIM, ALREADY_CLAIMED,
172
+ AUTHORIZATION_PENDING,
173
+ COMMAND_NOT_FOUND, MIN_CLI_VERSION, SNAPSHOT_FAILED, WRITE_FAILED,
174
+ PUBLISH_FAILED, RANKING_SCHEMA_MISMATCH,
175
+ INTERNAL_ERROR, PERSIST_FAILED, VERIFICATION_FAILED,
176
+ NO_COGNITO_USER, COGNITO_SYNC_FAILED, COGNITO_UNLINK_FAILED,
177
+ ALREADY_HAS_PASSWORD, EMAIL_ALIAS_CONFLICT, ALREADY_APPROVED,
178
+ ALREADY_MEMBER, ALREADY_COLLABORATOR, DEPENDENCY_ACCESS_DENIED,
179
+ GROUP_NAME_TAKEN, GROUP_ALREADY_GRANTED,
180
+ BODY_REQUIRED, FORBIDDEN_FIELD, INVALID_BYTES, INVALID_CONTENT_TYPE,
181
+ INVALID_EVENT, INVALID_PARENTS, TOO_MANY,
182
+ INVALID_UPLOAD_ID, UPLOAD_NOT_FOUND, NOT_COMMUNITY_LISTED,
183
+ UNKNOWN_CODE,
184
+ })
185
+
186
+ const ERROR_CODE_LIST = Object.freeze(Object.values(ERROR_CODES).slice().sort())
187
+ const ERROR_CODE_SET = new Set(ERROR_CODE_LIST)
188
+
189
+ const is_error_code = (s) => ERROR_CODE_SET.has(s)
190
+
191
+ module.exports = {
192
+ ERROR_CODES,
193
+ ERROR_CODE_LIST,
194
+ ERROR_CODE_SET,
195
+ is_error_code,
196
+ ...ERROR_CODES,
197
+ }
@@ -0,0 +1,54 @@
1
+ 'use strict'
2
+ // EXIT_CODE_BY_ERROR table — spec 260525-cli-default-json § 5.3.
3
+ // Maps each closed error.code to its process exit code. Codes not in the
4
+ // table default to exit 1 (general error). Adding a new error_code without
5
+ // adding it here is intentional: most domain failures map to 1.
6
+
7
+ const EXIT_CODE_BY_ERROR = Object.freeze({
8
+ // Exit 2 — usage / input validation
9
+ USAGE_ERROR: 2,
10
+ COMMAND_NOT_FOUND: 2,
11
+ INVALID_BODY: 2,
12
+ INVALID_SLUG: 2,
13
+ INVALID_VERSION: 2,
14
+ INVALID_BUMP: 2,
15
+ INVALID_NAME: 2,
16
+ INVALID_PARAM: 2,
17
+ INVALID_VALUE: 2,
18
+ INVALID_VISIBILITY: 2,
19
+ INVALID_PERMISSION: 2,
20
+ INVALID_ROLE: 2,
21
+ INVALID_SCOPE: 2,
22
+ INVALID_STATE: 2,
23
+ INVALID_STATUS: 2,
24
+ INVALID_OPERATION: 2,
25
+ INVALID_CATEGORY: 2,
26
+ INVALID_FILTER: 2,
27
+ MISSING_FIELDS: 2,
28
+ MISSING_VERSION: 2,
29
+ WORKSPACE_UNRESOLVED: 2,
30
+ VERSION_NOT_FOUND: 2,
31
+
32
+ // Exit 3 — auth
33
+ AUTH_REQUIRED: 3,
34
+ INVALID_CREDENTIALS: 3,
35
+ EXPIRED_TOKEN: 3,
36
+ INTERACTIVE_REQUIRED: 3,
37
+
38
+ // Exit 4 — network / transient
39
+ NETWORK_ERROR: 4,
40
+ RATE_LIMITED: 4,
41
+ DB_UNAVAILABLE: 4,
42
+ GITHUB_UNAVAILABLE: 4,
43
+ EMBEDDING_UNAVAILABLE: 4,
44
+ REGISTRY_UNAVAILABLE: 4,
45
+
46
+ // All other codes default to exit 1.
47
+ })
48
+
49
+ const exit_code_for_error = (code) => {
50
+ if (!code) return 0
51
+ return EXIT_CODE_BY_ERROR[code] || 1
52
+ }
53
+
54
+ module.exports = { EXIT_CODE_BY_ERROR, exit_code_for_error }
@@ -0,0 +1,133 @@
1
+ 'use strict'
2
+ // Closed enum of next_step actions and kinds emitted by the CLI envelope.
3
+ // Mirrors spec 260525-cli-default-json § 6 + § 7. Skeleton-only — Session 2
4
+ // wires it into the envelope builders. Until then, only the test suite
5
+ // consumes it (via envelope_validator).
6
+ //
7
+ // Renaming or removing an action is BREAKING. Adding one is non-breaking.
8
+
9
+ // Kinds (§ 6 — six closed values) ──────────────────────────────────────────
10
+ const RECOVERY = 'recovery'
11
+ const CLARIFICATION = 'clarification'
12
+ const DECISION = 'decision'
13
+ const CONFIRMATION = 'confirmation'
14
+ const CONTINUATION = 'continuation'
15
+ const ROUTING = 'routing'
16
+
17
+ const NEXT_STEP_KINDS = Object.freeze([
18
+ RECOVERY, CLARIFICATION, DECISION, CONFIRMATION, CONTINUATION, ROUTING,
19
+ ])
20
+ const NEXT_STEP_KIND_SET = new Set(NEXT_STEP_KINDS)
21
+
22
+ // Actions, grouped by kind (§ 7.1) ─────────────────────────────────────────
23
+
24
+ // kind: recovery
25
+ const LOGIN = 'login'
26
+ const RETRY = 'retry'
27
+ const RECONCILE_FIRST = 'reconcile_first'
28
+ const PULL_REBASE_FIRST = 'pull_rebase_first'
29
+ const FIX_VALIDATION_ERRORS = 'fix_validation_errors'
30
+ const PROVIDE_CHANGELOG = 'provide_changelog'
31
+ const SELF_UPDATE = 'self_update'
32
+ const SHOW_FORMAT = 'show_format'
33
+ const RETRY_RANK = 'retry_rank'
34
+
35
+ // kind: clarification
36
+ const CLARIFY_QUERY = 'clarify_query'
37
+
38
+ // kind: decision
39
+ const RESOLVE_REGRESSION = 'resolve_regression'
40
+ const RESOLVE_MISSING_SKILL_JSON = 'resolve_missing_skill_json'
41
+ const RESOLVE_MISSING_DIR = 'resolve_missing_dir'
42
+ const RESOLVE_CONFLICTS = 'resolve_conflicts'
43
+ const RESOLVE_PATCH_REJECTIONS = 'resolve_patch_rejections'
44
+ const SPECIFY_WORKSPACE = 'specify_workspace'
45
+ const SPECIFY_BUMP_TYPE = 'specify_bump_type'
46
+ const RESOLVE_BUMP_DISAGREEMENT = 'resolve_bump_disagreement'
47
+ const PICK_VERSION = 'pick_version'
48
+
49
+ // kind: confirmation
50
+ const CONFIRM_DISCARD_OR_SNAPSHOT_FIRST = 'confirm_discard_or_snapshot_first'
51
+ const CONFIRM_CASCADE = 'confirm_cascade'
52
+ const CONFIRM_DESTRUCTIVE = 'confirm_destructive'
53
+ const PASS_YES_FLAG = 'pass_yes_flag'
54
+
55
+ // kind: continuation
56
+ const RANK_DIGESTS_INLINE = 'rank_digests_inline'
57
+ const PRESENT_TO_USER = 'present_to_user'
58
+ const ATTACH_SCREENSHOT = 'attach_screenshot' // § 15.3.4
59
+
60
+ // kind: routing
61
+ const INSTALL_FIRST = 'install_first'
62
+
63
+ // kind lookup ──────────────────────────────────────────────────────────────
64
+ const ACTION_KIND = Object.freeze({
65
+ [LOGIN]: RECOVERY,
66
+ [RETRY]: RECOVERY,
67
+ [RECONCILE_FIRST]: RECOVERY,
68
+ [PULL_REBASE_FIRST]: RECOVERY,
69
+ [FIX_VALIDATION_ERRORS]: RECOVERY,
70
+ [PROVIDE_CHANGELOG]: RECOVERY,
71
+ [SELF_UPDATE]: RECOVERY,
72
+ [SHOW_FORMAT]: RECOVERY,
73
+ [RETRY_RANK]: RECOVERY,
74
+
75
+ [CLARIFY_QUERY]: CLARIFICATION,
76
+
77
+ [RESOLVE_REGRESSION]: DECISION,
78
+ [RESOLVE_MISSING_SKILL_JSON]: DECISION,
79
+ [RESOLVE_MISSING_DIR]: DECISION,
80
+ [RESOLVE_CONFLICTS]: DECISION,
81
+ [RESOLVE_PATCH_REJECTIONS]: DECISION,
82
+ [SPECIFY_WORKSPACE]: DECISION,
83
+ [SPECIFY_BUMP_TYPE]: DECISION,
84
+ [RESOLVE_BUMP_DISAGREEMENT]: DECISION,
85
+ [PICK_VERSION]: DECISION,
86
+
87
+ [CONFIRM_DISCARD_OR_SNAPSHOT_FIRST]: CONFIRMATION,
88
+ [CONFIRM_CASCADE]: CONFIRMATION,
89
+ [CONFIRM_DESTRUCTIVE]: CONFIRMATION,
90
+ [PASS_YES_FLAG]: CONFIRMATION,
91
+
92
+ [RANK_DIGESTS_INLINE]: CONTINUATION,
93
+ [PRESENT_TO_USER]: CONTINUATION,
94
+ [ATTACH_SCREENSHOT]: CONTINUATION,
95
+
96
+ [INSTALL_FIRST]: ROUTING,
97
+ })
98
+
99
+ const NEXT_STEP_ACTIONS = Object.freeze({
100
+ LOGIN, RETRY, RECONCILE_FIRST, PULL_REBASE_FIRST, FIX_VALIDATION_ERRORS,
101
+ PROVIDE_CHANGELOG, SELF_UPDATE, SHOW_FORMAT, RETRY_RANK,
102
+ CLARIFY_QUERY,
103
+ RESOLVE_REGRESSION, RESOLVE_MISSING_SKILL_JSON, RESOLVE_MISSING_DIR,
104
+ RESOLVE_CONFLICTS, RESOLVE_PATCH_REJECTIONS, SPECIFY_WORKSPACE,
105
+ SPECIFY_BUMP_TYPE, RESOLVE_BUMP_DISAGREEMENT, PICK_VERSION,
106
+ CONFIRM_DISCARD_OR_SNAPSHOT_FIRST, CONFIRM_CASCADE, CONFIRM_DESTRUCTIVE,
107
+ PASS_YES_FLAG,
108
+ RANK_DIGESTS_INLINE, PRESENT_TO_USER, ATTACH_SCREENSHOT,
109
+ INSTALL_FIRST,
110
+ })
111
+
112
+ const NEXT_STEP_ACTION_LIST = Object.freeze(Object.values(NEXT_STEP_ACTIONS).slice().sort())
113
+ const NEXT_STEP_ACTION_SET = new Set(NEXT_STEP_ACTION_LIST)
114
+
115
+ const is_next_step_action = (s) => NEXT_STEP_ACTION_SET.has(s)
116
+ const is_next_step_kind = (s) => NEXT_STEP_KIND_SET.has(s)
117
+ const kind_for_action = (a) => ACTION_KIND[a] || null
118
+
119
+ module.exports = {
120
+ NEXT_STEP_KINDS,
121
+ NEXT_STEP_KIND_SET,
122
+ NEXT_STEP_ACTIONS,
123
+ NEXT_STEP_ACTION_LIST,
124
+ NEXT_STEP_ACTION_SET,
125
+ ACTION_KIND,
126
+ is_next_step_action,
127
+ is_next_step_kind,
128
+ kind_for_action,
129
+ // kind constants
130
+ RECOVERY, CLARIFICATION, DECISION, CONFIRMATION, CONTINUATION, ROUTING,
131
+ // action constants
132
+ ...NEXT_STEP_ACTIONS,
133
+ }