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.
- package/CHANGELOG.md +30 -0
- package/package.json +1 -1
- package/src/api/auth.js +18 -2
- package/src/api/client.js +29 -3
- package/src/api/feedback.js +14 -5
- package/src/api/repos.js +28 -10
- package/src/api/translate.js +90 -0
- package/src/commands/delete.js +15 -1
- package/src/commands/feedback.js +2 -2
- package/src/commands/init.js +5 -1
- package/src/commands/install.js +58 -32
- package/src/commands/postlex.js +53 -35
- package/src/commands/postlex.test.js +48 -18
- package/src/commands/pull.js +5 -1
- package/src/commands/reconcile.js +52 -4
- package/src/commands/release.js +45 -15
- package/src/commands/schema.js +179 -0
- package/src/commands/search.js +34 -22
- package/src/commands/search.test.js +59 -33
- package/src/commands/uninstall.js +20 -11
- package/src/commands/validate.js +33 -11
- package/src/constants/error_codes.js +197 -0
- package/src/constants/exit_codes.js +54 -0
- package/src/constants/next_step_actions.js +133 -0
- package/src/constants/next_step_by_error_code.js +249 -0
- package/src/constants.js +2 -1
- package/src/index.js +51 -7
- package/src/integration/api_envelope.test.js +499 -0
- package/src/integration/bump.test.js +13 -4
- package/src/integration/cli.test.js +169 -147
- package/src/integration/drift.test.js +16 -4
- package/src/integration/install_fresh.test.js +37 -29
- package/src/integration/reconcile.test.js +77 -56
- package/src/integration/release.test.js +48 -31
- package/src/integration/schema.test.js +167 -0
- package/src/schema/envelope.schema.json +73 -0
- package/src/schema/envelope_test_helpers.js +94 -0
- package/src/schema/envelope_validator.js +239 -0
- package/src/schema/envelope_validator.test.js +333 -0
- package/src/ui/envelope.js +171 -0
- package/src/ui/output.js +66 -2
- package/src/utils/errors.js +116 -47
- 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
|
-
//
|
|
5
|
-
//
|
|
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
|
|
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
|
-
|
|
50
|
+
assert_envelope_next_step(r, null, 'with_rerank=false')
|
|
27
51
|
})
|
|
28
52
|
|
|
29
|
-
it('returns
|
|
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
|
-
|
|
57
|
+
assert_envelope_next_step(r, null, 'fuzzy_slug')
|
|
34
58
|
})
|
|
35
59
|
|
|
36
|
-
it('returns
|
|
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
|
-
|
|
64
|
+
assert_envelope_next_step(r, null, 'fuzzy_scoped')
|
|
41
65
|
})
|
|
42
66
|
|
|
43
|
-
it('emits rank_digests_inline when semantic
|
|
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
|
-
|
|
50
|
-
assert.
|
|
51
|
-
assert.
|
|
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.
|
|
87
|
+
assert.strictEqual(r.context.clarification_turns_used, 1)
|
|
63
88
|
})
|
|
64
89
|
|
|
65
|
-
it('emits
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
135
|
+
assert_envelope_next_step(r_big, NEXT_STEP_ACTIONS.PRESENT_TO_USER, 'budget overflow')
|
|
110
136
|
})
|
|
111
137
|
|
|
112
|
-
it('the
|
|
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.
|
|
117
|
-
const opts = r.suggested_questions[0].options
|
|
118
|
-
assert.
|
|
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.
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 } })
|
package/src/commands/validate.js
CHANGED
|
@@ -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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|