gentle-pi 3.3.0 → 3.4.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/README.md +63 -59
- package/assets/orchestrator-delegation.md +1 -1
- package/docs/assets/brand/gentle-shell-banner.gif +0 -0
- package/docs/assets/diagrams/odd-workflow.svg +74 -0
- package/docs/assets/features/agents-view.png +0 -0
- package/docs/assets/features/changes-view.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/assets/features/profiles-routing.png +0 -0
- package/docs/gentle-shell.md +26 -2
- package/docs/readme-reference.md +32 -6
- package/extensions/ask-user-question.ts +210 -0
- package/extensions/gentle-agents.ts +8 -1
- package/extensions/gentle-ai.ts +59 -18
- package/extensions/gentle-shell.ts +99 -10
- package/extensions/quiet-tools.ts +28 -5
- package/extensions/startup-banner.ts +25 -10
- package/lib/animation-policy.ts +52 -0
- package/lib/background-cache-warming.ts +38 -0
- package/lib/command-palette-catalog.ts +1 -0
- package/lib/inprocess-reviewer.ts +38 -1
- package/lib/native-review-cli.ts +36 -10
- package/lib/questionnaire/questionnaire-view.ts +603 -0
- package/lib/questionnaire/schema.ts +82 -0
- package/lib/questionnaire/validate.ts +141 -0
- package/lib/review-candidate-view-owner.ts +20 -5
- package/lib/review-candidate-view.ts +9 -2
- package/lib/review-host-relay.ts +10 -0
- package/lib/review-integration-v2.ts +4 -1
- package/lib/shell-bar.ts +13 -0
- package/lib/shell-sidebar-layout.ts +10 -4
- package/lib/shell-usage-view.ts +5 -2
- package/lib/shell-usage.ts +120 -6
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +35 -9
- package/runtime/review-integration-v2.mjs +4 -1
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/verify-package-files.mjs +2 -3
- package/tests/animation-policy.test.ts +42 -0
- package/tests/ask-user-question.test.ts +435 -0
- package/tests/background-cache-warming.test.ts +60 -0
- package/tests/background-subagents.test.ts +68 -0
- package/tests/command-palette.test.ts +9 -0
- package/tests/gentle-agents.test.ts +71 -2
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +56 -4
- package/tests/gentle-shell.test.ts +355 -2
- package/tests/inprocess-reviewer.test.ts +92 -0
- package/tests/native-review-capability-contract.test.ts +16 -1
- package/tests/native-review-parity.test.ts +19 -0
- package/tests/package-manifest.test.ts +6 -17
- package/tests/questionnaire-schema.test.ts +274 -0
- package/tests/questionnaire-view.test.ts +446 -0
- package/tests/rdd-status-line.test.ts +21 -4
- package/tests/review-candidate-owner-retry.test.ts +63 -0
- package/tests/review-candidate-view.test.ts +15 -0
- package/tests/review-controller-native-routing.test.ts +86 -0
- package/tests/review-host-relay.test.ts +21 -0
- package/tests/review-integration-v2.test.ts +30 -0
- package/tests/review-ledger-contract.test.ts +1 -2
- package/tests/review-relay-transport-agent.test.ts +107 -2
- package/tests/review-risk-assessment.test.ts +104 -0
- package/tests/shell-bar.test.ts +8 -0
- package/tests/shell-sidebar-layout.test.ts +60 -5
- package/tests/shell-usage.test.ts +129 -0
- package/tests/skill-collision-prefixes.test.ts +1 -1
- package/tests/startup-banner.test.ts +93 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/skills/release/SKILL.md +0 -137
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Value } from "typebox/value";
|
|
4
|
+
import {
|
|
5
|
+
CUSTOM_ROW_LABEL,
|
|
6
|
+
MAX_HEADER_LENGTH,
|
|
7
|
+
MAX_LABEL_LENGTH,
|
|
8
|
+
MAX_OPTIONS,
|
|
9
|
+
MAX_QUESTIONS,
|
|
10
|
+
MIN_OPTIONS,
|
|
11
|
+
QuestionParamsSchema,
|
|
12
|
+
type OptionData,
|
|
13
|
+
type QuestionParams,
|
|
14
|
+
} from "../lib/questionnaire/schema.ts";
|
|
15
|
+
import { validateQuestionnaire, type QuestionnaireError } from "../lib/questionnaire/validate.ts";
|
|
16
|
+
|
|
17
|
+
const H = "Header";
|
|
18
|
+
const option = (label: string, preview?: string): OptionData =>
|
|
19
|
+
preview === undefined ? { label, description: `${label} description` } : { label, description: `${label} description`, preview };
|
|
20
|
+
|
|
21
|
+
const question = (
|
|
22
|
+
text: string,
|
|
23
|
+
options: OptionData[],
|
|
24
|
+
overrides: { header?: string; multiSelect?: boolean } = {},
|
|
25
|
+
) => ({
|
|
26
|
+
question: text,
|
|
27
|
+
header: overrides.header ?? H,
|
|
28
|
+
options,
|
|
29
|
+
...(overrides.multiSelect === undefined ? {} : { multiSelect: overrides.multiSelect }),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const params = (questions: ReturnType<typeof question>[]): QuestionParams => ({ questions });
|
|
33
|
+
const valid = (): ReturnType<typeof question>[] => [question("First?", [option("Alpha"), option("Beta")])];
|
|
34
|
+
|
|
35
|
+
interface Case {
|
|
36
|
+
name: string;
|
|
37
|
+
input: ReturnType<typeof params>;
|
|
38
|
+
expected: QuestionnaireError | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeCases(): Case[] {
|
|
42
|
+
return [
|
|
43
|
+
{ name: "accepts a minimal valid questionnaire", input: params(valid()), expected: undefined },
|
|
44
|
+
{
|
|
45
|
+
name: "accepts exactly one question with two options",
|
|
46
|
+
input: params([question("Solo?", [option("A"), option("B")])]),
|
|
47
|
+
expected: undefined,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "accepts exactly four questions and four options",
|
|
51
|
+
input: params([
|
|
52
|
+
question("Q1", [option("A1"), option("B1"), option("C1"), option("D1")]),
|
|
53
|
+
question("Q2", [option("A2"), option("B2")]),
|
|
54
|
+
question("Q3", [option("A3"), option("B3")]),
|
|
55
|
+
question("Q4", [option("A4"), option("B4")]),
|
|
56
|
+
]),
|
|
57
|
+
expected: undefined,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "accepts a header at the 16 character limit",
|
|
61
|
+
input: params([question("Long header?", [option("A"), option("B")], { header: "x".repeat(MAX_HEADER_LENGTH) })]),
|
|
62
|
+
expected: undefined,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "accepts a label at the 60 character limit",
|
|
66
|
+
input: params([question("Long label?", [option("y".repeat(MAX_LABEL_LENGTH)), option("B")])]),
|
|
67
|
+
expected: undefined,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "rejects an empty question list",
|
|
71
|
+
input: params([]),
|
|
72
|
+
expected: { code: "no_questions", message: "At least one question is required." },
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "rejects a question with empty options",
|
|
76
|
+
input: params([question("Empty?", [])]),
|
|
77
|
+
expected: {
|
|
78
|
+
code: "empty_options",
|
|
79
|
+
message: "Question 1 must declare at least one option.",
|
|
80
|
+
questionIndex: 0,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "rejects a single option below the minimum",
|
|
85
|
+
input: params([question("One?", [option("Only")])]),
|
|
86
|
+
expected: {
|
|
87
|
+
code: "option_count",
|
|
88
|
+
message: "Question 1 must have between 2 and 4 options, received 1.",
|
|
89
|
+
questionIndex: 0,
|
|
90
|
+
count: 1,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "rejects five options above the maximum",
|
|
95
|
+
input: params([question("Five?", [option("A"), option("B"), option("C"), option("D"), option("E")])]),
|
|
96
|
+
expected: {
|
|
97
|
+
code: "option_count",
|
|
98
|
+
message: "Question 1 must have between 2 and 4 options, received 5.",
|
|
99
|
+
questionIndex: 0,
|
|
100
|
+
count: 5,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "rejects more than four questions",
|
|
105
|
+
input: params([
|
|
106
|
+
question("Q1", [option("A1"), option("B1")]),
|
|
107
|
+
question("Q2", [option("A2"), option("B2")]),
|
|
108
|
+
question("Q3", [option("A3"), option("B3")]),
|
|
109
|
+
question("Q4", [option("A4"), option("B4")]),
|
|
110
|
+
question("Q5", [option("A5"), option("B5")]),
|
|
111
|
+
]),
|
|
112
|
+
expected: {
|
|
113
|
+
code: "too_many_questions",
|
|
114
|
+
message: "A questionnaire accepts at most 4 questions, received 5.",
|
|
115
|
+
count: 5,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "rejects duplicate question text",
|
|
120
|
+
input: params([
|
|
121
|
+
question("Same?", [option("A1"), option("B1")]),
|
|
122
|
+
question("Same?", [option("A2"), option("B2")]),
|
|
123
|
+
]),
|
|
124
|
+
expected: {
|
|
125
|
+
code: "duplicate_question",
|
|
126
|
+
message: "Question 2 duplicates an earlier question text.",
|
|
127
|
+
questionIndex: 1,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "rejects a duplicate option label within one question",
|
|
132
|
+
input: params([question("Dupe?", [option("Alpha"), option("Alpha")])]),
|
|
133
|
+
expected: {
|
|
134
|
+
code: "duplicate_option_label",
|
|
135
|
+
message: 'Question 1 option 2 duplicates the label "Alpha".',
|
|
136
|
+
questionIndex: 0,
|
|
137
|
+
optionIndex: 1,
|
|
138
|
+
label: "Alpha",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "rejects a label over the 60 character limit",
|
|
143
|
+
input: params([question("Too long?", [option("z".repeat(MAX_LABEL_LENGTH + 1)), option("B")])]),
|
|
144
|
+
expected: {
|
|
145
|
+
code: "label_too_long",
|
|
146
|
+
message: "Question 1 option 1 label exceeds 60 characters.",
|
|
147
|
+
questionIndex: 0,
|
|
148
|
+
optionIndex: 0,
|
|
149
|
+
length: MAX_LABEL_LENGTH + 1,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "rejects a header over the 16 character limit",
|
|
154
|
+
input: params([question("Header too long?", [option("A"), option("B")], { header: "x".repeat(MAX_HEADER_LENGTH + 1) })]),
|
|
155
|
+
expected: {
|
|
156
|
+
code: "header_too_long",
|
|
157
|
+
message: "Question 1 header exceeds 16 characters.",
|
|
158
|
+
questionIndex: 0,
|
|
159
|
+
length: MAX_HEADER_LENGTH + 1,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "rejects the reserved Other label",
|
|
164
|
+
input: params([question("Reserved?", [option("Other"), option("B")])]),
|
|
165
|
+
expected: {
|
|
166
|
+
code: "reserved_label",
|
|
167
|
+
message: 'Question 1 option 1 uses the reserved label "Other".',
|
|
168
|
+
questionIndex: 0,
|
|
169
|
+
optionIndex: 0,
|
|
170
|
+
label: "Other",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "rejects the reserved custom-row label",
|
|
175
|
+
input: params([question("Reserved?", [option(CUSTOM_ROW_LABEL), option("B")])]),
|
|
176
|
+
expected: {
|
|
177
|
+
code: "reserved_label",
|
|
178
|
+
message: `Question 1 option 1 uses the reserved label "${CUSTOM_ROW_LABEL}".`,
|
|
179
|
+
questionIndex: 0,
|
|
180
|
+
optionIndex: 0,
|
|
181
|
+
label: CUSTOM_ROW_LABEL,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "does not treat a reserved label with different casing as reserved",
|
|
186
|
+
input: params([question("Case?", [option("other"), option("type something.")])]),
|
|
187
|
+
expected: undefined,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "reports a duplicated question before its duplicated option label",
|
|
191
|
+
input: params([
|
|
192
|
+
question("Same?", [option("A"), option("B")]),
|
|
193
|
+
question("Same?", [option("A"), option("B")]),
|
|
194
|
+
]),
|
|
195
|
+
expected: {
|
|
196
|
+
code: "duplicate_question",
|
|
197
|
+
message: "Question 2 duplicates an earlier question text.",
|
|
198
|
+
questionIndex: 1,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
test("validateQuestionnaire reports the first violation for each guard", () => {
|
|
205
|
+
for (const testCase of makeCases()) {
|
|
206
|
+
const actual = validateQuestionnaire(testCase.input);
|
|
207
|
+
assert.deepEqual(actual, testCase.expected, testCase.name);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("schema constants expose the documented limits", () => {
|
|
212
|
+
assert.equal(MAX_QUESTIONS, 4);
|
|
213
|
+
assert.equal(MIN_OPTIONS, 2);
|
|
214
|
+
assert.equal(MAX_OPTIONS, 4);
|
|
215
|
+
assert.equal(MAX_HEADER_LENGTH, 16);
|
|
216
|
+
assert.equal(MAX_LABEL_LENGTH, 60);
|
|
217
|
+
assert.equal(CUSTOM_ROW_LABEL, "Type something.");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("QuestionParamsSchema pins the questionnaire parameter shape", () => {
|
|
221
|
+
const root = QuestionParamsSchema as unknown as {
|
|
222
|
+
additionalProperties?: boolean;
|
|
223
|
+
properties?: {
|
|
224
|
+
questions?: {
|
|
225
|
+
minItems?: number;
|
|
226
|
+
maxItems?: number;
|
|
227
|
+
items?: {
|
|
228
|
+
additionalProperties?: boolean;
|
|
229
|
+
properties?: Record<string, unknown>;
|
|
230
|
+
required?: string[];
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
const questions = root.properties?.questions;
|
|
236
|
+
const questionSchema = questions?.items;
|
|
237
|
+
const questionProperties = questionSchema?.properties ?? {};
|
|
238
|
+
|
|
239
|
+
assert.equal(root.additionalProperties, false);
|
|
240
|
+
assert.equal(questions?.minItems, 1);
|
|
241
|
+
assert.equal(questions?.maxItems, MAX_QUESTIONS);
|
|
242
|
+
assert.equal(questionSchema?.additionalProperties, false);
|
|
243
|
+
assert.deepEqual([...(questionSchema?.required ?? [])].sort(), ["header", "options", "question"]);
|
|
244
|
+
assert.deepEqual(Object.keys(questionProperties).sort(), ["header", "multiSelect", "options", "question"]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("schema rejects malformed questionnaire parameters", () => {
|
|
248
|
+
const validParams = params(valid());
|
|
249
|
+
assert.equal(Value.Check(QuestionParamsSchema, validParams), true);
|
|
250
|
+
|
|
251
|
+
const invalidCases: Array<[string, unknown]> = [
|
|
252
|
+
["missing questions", {}],
|
|
253
|
+
["empty questions", { questions: [] }],
|
|
254
|
+
["five questions", params([
|
|
255
|
+
question("Q1", [option("A1"), option("B1")]),
|
|
256
|
+
question("Q2", [option("A2"), option("B2")]),
|
|
257
|
+
question("Q3", [option("A3"), option("B3")]),
|
|
258
|
+
question("Q4", [option("A4"), option("B4")]),
|
|
259
|
+
question("Q5", [option("A5"), option("B5")]),
|
|
260
|
+
])],
|
|
261
|
+
["one option", params([question("One?", [option("Only")])])],
|
|
262
|
+
["label over the limit", params([question("Long?", [option("z".repeat(MAX_LABEL_LENGTH + 1)), option("B")])])],
|
|
263
|
+
["header over the limit", params([question("Long?", [option("A"), option("B")], { header: "x".repeat(MAX_HEADER_LENGTH + 1) })])],
|
|
264
|
+
["unknown root property", { questions: valid(), extra: true }],
|
|
265
|
+
];
|
|
266
|
+
for (const [name, subject] of invalidCases) {
|
|
267
|
+
assert.equal(Value.Check(QuestionParamsSchema, subject), false, name);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("schema accepts multiSelect and preview as optional fields", () => {
|
|
272
|
+
const withOptionals = params([question("Options?", [option("A", "Preview A"), option("B")], { multiSelect: true })]);
|
|
273
|
+
assert.equal(Value.Check(QuestionParamsSchema, withOptionals), true);
|
|
274
|
+
});
|