gestalt-mobile 0.13.0 → 0.15.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 +42 -11
- package/dist/client/assets/index-2L2KZzwK.css +1 -0
- package/dist/client/assets/index-Dnm_kS5p.js +24 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-ext-400-normal-Bh0R7Dhr.woff +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-ext-400-normal-C_uLvvQ5.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-ByoDsISC.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/client/assets/syne-greek-600-normal-BJs4kvyH.woff +0 -0
- package/dist/client/assets/syne-greek-600-normal-BpFII5Ja.woff2 +0 -0
- package/dist/client/assets/syne-latin-600-normal-BiwQbQXw.woff2 +0 -0
- package/dist/client/assets/syne-latin-600-normal-dRu9QuIh.woff +0 -0
- package/dist/client/assets/syne-latin-ext-600-normal-DEzBMW8B.woff +0 -0
- package/dist/client/assets/syne-latin-ext-600-normal-ZnizrDKU.woff2 +0 -0
- package/dist/client/icons/gestalt-mobile-180.png +0 -0
- package/dist/client/icons/gestalt-mobile-192.png +0 -0
- package/dist/client/icons/gestalt-mobile-512.png +0 -0
- package/dist/client/icons/gestalt-mobile-maskable-512.png +0 -0
- package/dist/client/icons/gestalt-mobile-maskable.svg +10 -0
- package/dist/client/icons/gestalt-mobile.svg +10 -0
- package/dist/client/index.html +9 -3
- package/dist/client/manifest.webmanifest +32 -0
- package/dist/client/service-worker.js +9 -0
- package/dist/server/server/cli.js +3 -1
- package/dist/server/server/composition.js +25 -21
- package/dist/server/server/features/auth/application/ports.js +7 -1
- package/dist/server/server/features/auth/devices/revoke/endpoint.js +3 -1
- package/dist/server/server/features/auth/enrollment-tickets/endpoint.js +7 -2
- package/dist/server/server/features/auth/enrollment-tickets/status/endpoint.js +3 -1
- package/dist/server/server/features/auth/http/cookies.js +12 -2
- package/dist/server/server/features/auth/login/verify/endpoint.js +1 -1
- package/dist/server/server/features/auth/register/verify/endpoint.js +89 -19
- package/dist/server/server/features/auth/register-routes.js +4 -1
- package/dist/server/server/features/plans/application/measurement-snapshot.js +1 -1
- package/dist/server/server/features/plans/get-measurement/endpoint.js +1 -3
- package/dist/server/server/features/sessions/interaction/kind.js +7 -1
- package/dist/server/server/features/sessions/list-recent-threads/endpoint.js +3 -1
- package/dist/server/server/features/sessions/model/relay-session.js +3 -1
- package/dist/server/server/features/sessions/start-session/use-case.js +1 -1
- package/dist/server/server/features/skills/delete-profile/endpoint.js +12 -3
- package/dist/server/server/features/skills/list-available/endpoint.js +24 -6
- package/dist/server/server/features/skills/list-profiles/endpoint.js +29 -5
- package/dist/server/server/features/skills/model/skill-profile.js +23 -3
- package/dist/server/server/platform/auth/simple-webauthn-adapter.js +55 -9
- package/dist/server/server/platform/catalog/profile-command.js +8 -1
- package/dist/server/server/platform/codex/normalizer.js +22 -4
- package/dist/server/server/platform/codex/session-runtime.js +52 -11
- package/dist/server/server/platform/codex/thread-plan-name.js +5 -2
- package/dist/server/server/platform/http/problem-reply.js +4 -1
- package/dist/server/server/platform/plans/filesystem-plan-status-source.js +11 -6
- package/dist/server/server/platform/plans/filesystem-workspace-plan-catalog.js +4 -1
- package/dist/server/server/platform/plans/plan-measurement-command.js +4 -1
- package/dist/server/server/platform/skills/codex-skill-catalog.js +29 -7
- package/dist/server/server/platform/skills/filesystem-skill-profile-store.js +5 -1
- package/dist/server/shared/contracts/quiz.js +30 -9
- package/package.json +3 -1
- package/dist/client/assets/index-CMs5ZzxJ.css +0 -1
- package/dist/client/assets/index-D2sT9ZHY.js +0 -19
|
@@ -8,21 +8,45 @@ export function registerListSkillProfiles(app, deps) {
|
|
|
8
8
|
app.get('/api/skill-profiles', async (_request, reply) => {
|
|
9
9
|
try {
|
|
10
10
|
const names = await deps.listGlobalProfileNames();
|
|
11
|
-
const profiles = await Promise.all(names
|
|
11
|
+
const profiles = await Promise.all(names
|
|
12
|
+
.sort((a, b) => a.localeCompare(b))
|
|
13
|
+
.map(async (name) => {
|
|
12
14
|
try {
|
|
13
15
|
const profile = await deps.readGlobalProfile(name);
|
|
14
16
|
return profile
|
|
15
|
-
? {
|
|
16
|
-
|
|
17
|
+
? {
|
|
18
|
+
name: profile.name,
|
|
19
|
+
version: profile.version,
|
|
20
|
+
path: deps.profilePath(name),
|
|
21
|
+
skills: profile.skills,
|
|
22
|
+
}
|
|
23
|
+
: {
|
|
24
|
+
name,
|
|
25
|
+
path: deps.profilePath(name),
|
|
26
|
+
error: {
|
|
27
|
+
code: 'INVALID_SKILL_PROFILE',
|
|
28
|
+
message: 'Profile disappeared while reading.',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
17
31
|
}
|
|
18
32
|
catch (error) {
|
|
19
|
-
return {
|
|
33
|
+
return {
|
|
34
|
+
name,
|
|
35
|
+
path: deps.profilePath(name),
|
|
36
|
+
error: {
|
|
37
|
+
code: 'INVALID_SKILL_PROFILE',
|
|
38
|
+
message: error instanceof Error ? error.message : 'Invalid profile.',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
20
41
|
}
|
|
21
42
|
}));
|
|
22
43
|
return { profiles };
|
|
23
44
|
}
|
|
24
45
|
catch {
|
|
25
|
-
return reply
|
|
46
|
+
return reply
|
|
47
|
+
.code(500)
|
|
48
|
+
.type('application/problem+json')
|
|
49
|
+
.send(problem('SKILL_PROFILE_PERSISTENCE_FAILED', 500, 'Skill profiles could not be read.', true));
|
|
26
50
|
}
|
|
27
51
|
});
|
|
28
52
|
}
|
|
@@ -37,7 +37,20 @@ export const availableSkillSchema = z
|
|
|
37
37
|
defaultPrompt: z.string().optional(),
|
|
38
38
|
})
|
|
39
39
|
.optional(),
|
|
40
|
-
dependencies: z
|
|
40
|
+
dependencies: z
|
|
41
|
+
.object({
|
|
42
|
+
tools: z
|
|
43
|
+
.array(z.object({
|
|
44
|
+
type: z.string(),
|
|
45
|
+
value: z.string(),
|
|
46
|
+
description: z.string().optional(),
|
|
47
|
+
transport: z.string().optional(),
|
|
48
|
+
command: z.string().optional(),
|
|
49
|
+
url: z.string().optional(),
|
|
50
|
+
}))
|
|
51
|
+
.optional(),
|
|
52
|
+
})
|
|
53
|
+
.optional(),
|
|
41
54
|
scope: z.string().optional(),
|
|
42
55
|
})
|
|
43
56
|
.strict();
|
|
@@ -53,7 +66,10 @@ const profileDocumentSchema = z
|
|
|
53
66
|
* is deliberately narrow so a profile name can never become a path traversal.
|
|
54
67
|
*/
|
|
55
68
|
export function normalizeSkillProfileName(value) {
|
|
56
|
-
const normalized = value
|
|
69
|
+
const normalized = value
|
|
70
|
+
.trim()
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
.replace(/[\s_]+/g, '-');
|
|
57
73
|
if (!profileNamePattern.test(normalized)) {
|
|
58
74
|
throw new SkillProfileError('INVALID_SKILL_PROFILE', 'Invalid skill profile name.');
|
|
59
75
|
}
|
|
@@ -136,7 +152,11 @@ export function compileSkillOverride(input) {
|
|
|
136
152
|
}
|
|
137
153
|
/** Construct the single profile contract shared by global and project YAML files. */
|
|
138
154
|
export function createSkillProfile(input) {
|
|
139
|
-
return {
|
|
155
|
+
return {
|
|
156
|
+
version: 1,
|
|
157
|
+
name: normalizeSkillProfileName(input.name),
|
|
158
|
+
skills: createSkillSelection(input.skills),
|
|
159
|
+
};
|
|
140
160
|
}
|
|
141
161
|
/** Parse the version-1 YAML document without performing filesystem access. */
|
|
142
162
|
export function parseSkillProfileYaml(source) {
|
|
@@ -7,6 +7,32 @@ import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthe
|
|
|
7
7
|
import { PasskeyVerificationError } from '../../features/auth/application/ports.js';
|
|
8
8
|
import { webAuthnCredentialId } from '../../features/auth/domain/identifiers.js';
|
|
9
9
|
const transports = new Set(['ble', 'hybrid', 'internal', 'nfc', 'usb']);
|
|
10
|
+
function verificationFailure(error) {
|
|
11
|
+
const message = error instanceof Error ? error.message.toLowerCase() : '';
|
|
12
|
+
if (message.includes('challenge'))
|
|
13
|
+
return 'CHALLENGE_MISMATCH';
|
|
14
|
+
if (message.includes('origin'))
|
|
15
|
+
return 'ORIGIN_MISMATCH';
|
|
16
|
+
if (message.includes('rp id') || message.includes('rpid'))
|
|
17
|
+
return 'RP_ID_MISMATCH';
|
|
18
|
+
if (message.includes('user presence'))
|
|
19
|
+
return 'USER_PRESENCE_REQUIRED';
|
|
20
|
+
if (message.includes('user verification'))
|
|
21
|
+
return 'USER_VERIFICATION_REQUIRED';
|
|
22
|
+
if (message.includes('credential id') || message.includes('base64'))
|
|
23
|
+
return 'CREDENTIAL_ENCODING_INVALID';
|
|
24
|
+
if (message.includes('alg') || message.includes('cose'))
|
|
25
|
+
return 'ALGORITHM_UNSUPPORTED';
|
|
26
|
+
if (message.includes('public key') ||
|
|
27
|
+
message.includes('aaguid') ||
|
|
28
|
+
message.includes('authenticator data'))
|
|
29
|
+
return 'CREDENTIAL_MATERIAL_MISSING';
|
|
30
|
+
if (message.includes('attestation'))
|
|
31
|
+
return 'ATTESTATION_UNSUPPORTED';
|
|
32
|
+
if (message.includes('signature'))
|
|
33
|
+
return 'SIGNATURE_INVALID';
|
|
34
|
+
return 'VERIFIER_REJECTED';
|
|
35
|
+
}
|
|
10
36
|
/** Translates SimpleWebAuthn DTOs at the platform boundary. */
|
|
11
37
|
export class SimpleWebAuthnAdapter {
|
|
12
38
|
async registrationOptions(input) {
|
|
@@ -19,7 +45,11 @@ export class SimpleWebAuthnAdapter {
|
|
|
19
45
|
challenge: new Uint8Array(input.challenge),
|
|
20
46
|
attestationType: 'none',
|
|
21
47
|
excludeCredentials: input.excludeCredentialIds.map((id) => ({ id })),
|
|
22
|
-
authenticatorSelection: {
|
|
48
|
+
authenticatorSelection: {
|
|
49
|
+
residentKey: 'required',
|
|
50
|
+
requireResidentKey: true,
|
|
51
|
+
userVerification: 'required',
|
|
52
|
+
},
|
|
23
53
|
supportedAlgorithmIDs: [-7, -257],
|
|
24
54
|
});
|
|
25
55
|
}
|
|
@@ -43,11 +73,11 @@ export class SimpleWebAuthnAdapter {
|
|
|
43
73
|
supportedAlgorithmIDs: [-7, -257],
|
|
44
74
|
});
|
|
45
75
|
}
|
|
46
|
-
catch {
|
|
47
|
-
throw new PasskeyVerificationError(
|
|
76
|
+
catch (error) {
|
|
77
|
+
throw new PasskeyVerificationError(verificationFailure(error));
|
|
48
78
|
}
|
|
49
79
|
if (!result.verified || !result.registrationInfo?.userVerified)
|
|
50
|
-
throw new PasskeyVerificationError('
|
|
80
|
+
throw new PasskeyVerificationError('VERIFIER_REJECTED');
|
|
51
81
|
const credential = result.registrationInfo.credential;
|
|
52
82
|
return {
|
|
53
83
|
credentialId: webAuthnCredentialId(credential.id),
|
|
@@ -63,13 +93,29 @@ export class SimpleWebAuthnAdapter {
|
|
|
63
93
|
const value = input;
|
|
64
94
|
let result;
|
|
65
95
|
try {
|
|
66
|
-
result = await verifyAuthenticationResponse({
|
|
96
|
+
result = await verifyAuthenticationResponse({
|
|
97
|
+
response: value.response,
|
|
98
|
+
expectedChallenge: Buffer.from(value.challenge).toString('base64url'),
|
|
99
|
+
expectedOrigin: value.expectedOrigin,
|
|
100
|
+
expectedRPID: value.rpId,
|
|
101
|
+
requireUserVerification: true,
|
|
102
|
+
credential: {
|
|
103
|
+
id: value.credential.id,
|
|
104
|
+
publicKey: new Uint8Array(value.credential.publicKey),
|
|
105
|
+
counter: value.credential.counter,
|
|
106
|
+
transports: [...value.credential.transports],
|
|
107
|
+
},
|
|
108
|
+
});
|
|
67
109
|
}
|
|
68
|
-
catch {
|
|
69
|
-
throw new PasskeyVerificationError(
|
|
110
|
+
catch (error) {
|
|
111
|
+
throw new PasskeyVerificationError(verificationFailure(error));
|
|
70
112
|
}
|
|
71
113
|
if (!result.verified || !result.authenticationInfo?.userVerified)
|
|
72
|
-
throw new PasskeyVerificationError('
|
|
73
|
-
return {
|
|
114
|
+
throw new PasskeyVerificationError('VERIFIER_REJECTED');
|
|
115
|
+
return {
|
|
116
|
+
credentialId: webAuthnCredentialId(value.response.id),
|
|
117
|
+
counter: result.authenticationInfo.newCounter,
|
|
118
|
+
userVerified: result.authenticationInfo.userVerified,
|
|
119
|
+
};
|
|
74
120
|
}
|
|
75
121
|
}
|
|
@@ -18,5 +18,12 @@ export function profileAppServerCommand(_profile, availability = localAvailabili
|
|
|
18
18
|
: { command: 'codex', args: ['app-server', '--stdio'] };
|
|
19
19
|
return skillsConfig === undefined
|
|
20
20
|
? base
|
|
21
|
-
: {
|
|
21
|
+
: {
|
|
22
|
+
...base,
|
|
23
|
+
args: [
|
|
24
|
+
...base.args,
|
|
25
|
+
'--config',
|
|
26
|
+
`skills.config = [${skillsConfig.map((entry) => `{ path = ${JSON.stringify(entry.path)}, enabled = ${entry.enabled} }`).join(', ')}]`,
|
|
27
|
+
],
|
|
28
|
+
};
|
|
22
29
|
}
|
|
@@ -36,18 +36,34 @@ export function decodeNotification(input) {
|
|
|
36
36
|
if (input.method === 'item/agentMessage/delta') {
|
|
37
37
|
const params = record(input.params);
|
|
38
38
|
return typeof params?.delta === 'string' && params.delta.length <= 64_000
|
|
39
|
-
? { method: input.method, params: { delta: params.delta } }
|
|
39
|
+
? { method: input.method, params: { delta: params.delta } }
|
|
40
|
+
: null;
|
|
40
41
|
}
|
|
41
42
|
if (input.method === 'turn/completed') {
|
|
42
43
|
const params = record(input.params);
|
|
43
44
|
const turn = record(params?.turn);
|
|
44
45
|
if (!turn || typeof turn.id !== 'string' || turn.id.length > 256)
|
|
45
46
|
return null;
|
|
46
|
-
return {
|
|
47
|
+
return {
|
|
48
|
+
method: input.method,
|
|
49
|
+
params: {
|
|
50
|
+
...(typeof params?.threadId === 'string' && params.threadId.length <= 256
|
|
51
|
+
? { threadId: params.threadId }
|
|
52
|
+
: {}),
|
|
53
|
+
turn: {
|
|
54
|
+
id: turn.id,
|
|
55
|
+
...(typeof turn.status === 'string' && turn.status.length <= 64
|
|
56
|
+
? { status: turn.status }
|
|
57
|
+
: {}),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
47
61
|
}
|
|
48
62
|
if (input.method === 'item/started' || input.method === 'item/completed') {
|
|
49
63
|
const params = record(input.params);
|
|
50
|
-
return params && 'item' in params
|
|
64
|
+
return params && 'item' in params
|
|
65
|
+
? { method: input.method, params: { item: params.item } }
|
|
66
|
+
: null;
|
|
51
67
|
}
|
|
52
68
|
return null;
|
|
53
69
|
}
|
|
@@ -103,5 +119,7 @@ function reasoningSummary(parts) {
|
|
|
103
119
|
});
|
|
104
120
|
}
|
|
105
121
|
function record(value) {
|
|
106
|
-
return value && typeof value === 'object' && !Array.isArray(value)
|
|
122
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
123
|
+
? value
|
|
124
|
+
: null;
|
|
107
125
|
}
|
|
@@ -47,8 +47,12 @@ class SessionResource {
|
|
|
47
47
|
this.onDisposed();
|
|
48
48
|
return true;
|
|
49
49
|
}
|
|
50
|
-
get active() {
|
|
51
|
-
|
|
50
|
+
get active() {
|
|
51
|
+
return !this.disposed;
|
|
52
|
+
}
|
|
53
|
+
attach(unregister) {
|
|
54
|
+
this.unregister = unregister;
|
|
55
|
+
}
|
|
52
56
|
}
|
|
53
57
|
export class CodexSessionRuntime {
|
|
54
58
|
launch;
|
|
@@ -149,7 +153,10 @@ export class CodexSessionRuntime {
|
|
|
149
153
|
throw new Error('CODEX_SESSION_NOT_RUNNING');
|
|
150
154
|
const [rateLimits, thread] = await Promise.all([
|
|
151
155
|
resource.process.rpc.request('account/rateLimits/read', {}),
|
|
152
|
-
resource.process.rpc.request('thread/read', {
|
|
156
|
+
resource.process.rpc.request('thread/read', {
|
|
157
|
+
threadId: session.threadId,
|
|
158
|
+
includeTurns: true,
|
|
159
|
+
}),
|
|
153
160
|
]);
|
|
154
161
|
return createPlanMeasurementSnapshot({
|
|
155
162
|
capturedAt: new Date().toISOString(),
|
|
@@ -208,7 +215,9 @@ export class CodexSessionRuntime {
|
|
|
208
215
|
catch (error) {
|
|
209
216
|
if (!canRebindMissingRollout(session, error))
|
|
210
217
|
throw error;
|
|
211
|
-
const replacementThreadId = await this.startThread(resource.process, session, {
|
|
218
|
+
const replacementThreadId = await this.startThread(resource.process, session, {
|
|
219
|
+
model: session.model,
|
|
220
|
+
});
|
|
212
221
|
result = rebindMissingRollout(session, error, replacementThreadId, now);
|
|
213
222
|
}
|
|
214
223
|
resource.threadId = result.session.threadId;
|
|
@@ -234,7 +243,11 @@ export class CodexSessionRuntime {
|
|
|
234
243
|
const resource = this.sessions.get(sessionId);
|
|
235
244
|
const threadId = resource?.threadId;
|
|
236
245
|
const name = resource?.pendingThreadName;
|
|
237
|
-
if (!resource ||
|
|
246
|
+
if (!resource ||
|
|
247
|
+
!threadId ||
|
|
248
|
+
!name ||
|
|
249
|
+
resource.writtenThreadName === name ||
|
|
250
|
+
resource.capabilities.get('thread/name/set') === false)
|
|
238
251
|
return;
|
|
239
252
|
try {
|
|
240
253
|
await resource.process.rpc.request('thread/name/set', { threadId, name });
|
|
@@ -286,7 +299,7 @@ export class CodexSessionRuntime {
|
|
|
286
299
|
profile: session.profile,
|
|
287
300
|
cwd: session.workspacePath,
|
|
288
301
|
skillsConfig: await this.resolveSkills?.(session),
|
|
289
|
-
...(
|
|
302
|
+
...(lease || this.planMeasurementBaseUrl
|
|
290
303
|
? {
|
|
291
304
|
environment: {
|
|
292
305
|
...(lease
|
|
@@ -347,7 +360,12 @@ function decodeRateLimits(value) {
|
|
|
347
360
|
return undefined;
|
|
348
361
|
const decoded = limits.flatMap((limit) => {
|
|
349
362
|
const record = asRecord(limit);
|
|
350
|
-
return typeof record?.windowDurationMins === 'number' &&
|
|
363
|
+
return typeof record?.windowDurationMins === 'number' &&
|
|
364
|
+
Number.isFinite(record.windowDurationMins) &&
|
|
365
|
+
typeof record.usedPercent === 'number' &&
|
|
366
|
+
Number.isFinite(record.usedPercent)
|
|
367
|
+
? [{ windowDurationMins: record.windowDurationMins, usedPercent: record.usedPercent }]
|
|
368
|
+
: [];
|
|
351
369
|
});
|
|
352
370
|
return decoded.length === limits.length ? { rateLimits: decoded } : undefined;
|
|
353
371
|
}
|
|
@@ -396,10 +414,18 @@ function decodeThreadRead(value) {
|
|
|
396
414
|
const record = asRecord(turn);
|
|
397
415
|
if (!record)
|
|
398
416
|
throw new Error('CODEX_THREAD_READ_MALFORMED');
|
|
399
|
-
return {
|
|
417
|
+
return {
|
|
418
|
+
id: boundedString(record.id),
|
|
419
|
+
status: boundedString(record.status),
|
|
420
|
+
startedAt: typeof record.startedAt === 'number' ? record.startedAt : undefined,
|
|
421
|
+
completedAt: typeof record.completedAt === 'number' ? record.completedAt : undefined,
|
|
422
|
+
items: Array.isArray(record.items) ? record.items.flatMap(decodeHistoryItem) : undefined,
|
|
423
|
+
};
|
|
400
424
|
});
|
|
401
425
|
}
|
|
402
|
-
function boundedString(value, max = 64_000) {
|
|
426
|
+
function boundedString(value, max = 64_000) {
|
|
427
|
+
return typeof value === 'string' && value.length <= max ? value : undefined;
|
|
428
|
+
}
|
|
403
429
|
function decodeHistoryItem(value) {
|
|
404
430
|
const item = asRecord(value);
|
|
405
431
|
const id = boundedString(item?.id, 256);
|
|
@@ -407,10 +433,25 @@ function decodeHistoryItem(value) {
|
|
|
407
433
|
if (!item || !id || !type)
|
|
408
434
|
return [];
|
|
409
435
|
if (type === 'agentMessage')
|
|
410
|
-
return [
|
|
436
|
+
return [
|
|
437
|
+
{
|
|
438
|
+
id,
|
|
439
|
+
type,
|
|
440
|
+
...(boundedString(item.text) ? { text: boundedString(item.text) } : {}),
|
|
441
|
+
...(boundedString(item.phase, 64) ? { phase: boundedString(item.phase, 64) } : {}),
|
|
442
|
+
},
|
|
443
|
+
];
|
|
411
444
|
if (type === 'plan' && boundedString(item.text))
|
|
412
445
|
return [{ id, type, text: boundedString(item.text) }];
|
|
413
446
|
if (type === 'commandExecution' && boundedString(item.command) && boundedString(item.status, 64))
|
|
414
|
-
return [
|
|
447
|
+
return [
|
|
448
|
+
{
|
|
449
|
+
id,
|
|
450
|
+
type,
|
|
451
|
+
command: boundedString(item.command),
|
|
452
|
+
status: boundedString(item.status, 64),
|
|
453
|
+
...(typeof item.exitCode === 'number' ? { exitCode: item.exitCode } : {}),
|
|
454
|
+
},
|
|
455
|
+
];
|
|
415
456
|
return [];
|
|
416
457
|
}
|
|
@@ -12,12 +12,15 @@ export function threadPlanName(plan, limit = 120) {
|
|
|
12
12
|
? `L1 ${l1.indexOf(active) + 1}/${l1.length}`
|
|
13
13
|
: awaiting
|
|
14
14
|
? `Review ${l1.indexOf(awaiting) + 1}/${l1.length}`
|
|
15
|
-
: l1.length > 0 &&
|
|
15
|
+
: l1.length > 0 &&
|
|
16
|
+
l1.every((step) => step.state === 'DONE' && step.reviewStatus === 'REVIEWED')
|
|
16
17
|
? 'Complete'
|
|
17
18
|
: 'Authoring';
|
|
18
19
|
return `${truncateTitle(plan.title, Math.max(1, limit - suffix.length - 3))} — ${suffix}`;
|
|
19
20
|
}
|
|
20
21
|
function truncateTitle(value, limit) {
|
|
21
22
|
const characters = Array.from(value);
|
|
22
|
-
return characters.length <= limit
|
|
23
|
+
return characters.length <= limit
|
|
24
|
+
? value
|
|
25
|
+
: `${characters.slice(0, Math.max(0, limit - 1)).join('')}…`;
|
|
23
26
|
}
|
|
@@ -6,5 +6,8 @@
|
|
|
6
6
|
import { problem } from './problem.js';
|
|
7
7
|
/** Send the established RFC 7807-compatible response without deciding its semantics. */
|
|
8
8
|
export function sendProblem(reply, code, status, detail) {
|
|
9
|
-
return reply
|
|
9
|
+
return reply
|
|
10
|
+
.code(status)
|
|
11
|
+
.type('application/problem+json')
|
|
12
|
+
.send(problem(code, status, detail));
|
|
10
13
|
}
|
|
@@ -29,10 +29,7 @@ export class FilesystemPlanStatusSource {
|
|
|
29
29
|
await mkdir(this.dismissalDirectory, { recursive: true, mode: 0o700 });
|
|
30
30
|
await mkdir(statusDirectory, { recursive: true, mode: 0o700 });
|
|
31
31
|
if (process.platform !== 'win32') {
|
|
32
|
-
await Promise.all([
|
|
33
|
-
chmod(this.dismissalDirectory, 0o700),
|
|
34
|
-
chmod(statusDirectory, 0o700),
|
|
35
|
-
]);
|
|
32
|
+
await Promise.all([chmod(this.dismissalDirectory, 0o700), chmod(statusDirectory, 0o700)]);
|
|
36
33
|
}
|
|
37
34
|
const lease = new ActiveLease(statusDirectory, session.id, session.workspacePath, listener, this.planReadFilesystem, (identity) => this.isDismissed(session.id, identity), (statusPath) => this.activeStatusPaths.set(session.id, statusPath), () => {
|
|
38
35
|
this.leases.delete(session.id);
|
|
@@ -97,7 +94,9 @@ export class FilesystemPlanStatusSource {
|
|
|
97
94
|
const next = new Set(dismissed).add(identity);
|
|
98
95
|
const candidate = join(this.dismissalDirectory, `.${randomUUID()}.dismissals.tmp`);
|
|
99
96
|
try {
|
|
100
|
-
await this.dismissalFilesystem.writeFile(candidate, JSON.stringify([...next]), {
|
|
97
|
+
await this.dismissalFilesystem.writeFile(candidate, JSON.stringify([...next]), {
|
|
98
|
+
mode: 0o600,
|
|
99
|
+
});
|
|
101
100
|
await this.dismissalFilesystem.rename(candidate, path);
|
|
102
101
|
}
|
|
103
102
|
catch (error) {
|
|
@@ -272,7 +271,13 @@ class ActiveLease {
|
|
|
272
271
|
const previousStatusPath = this.activeStatusPath;
|
|
273
272
|
this.activeStatusPath = statusPath;
|
|
274
273
|
this.onActiveStatusPath(statusPath);
|
|
275
|
-
const update = {
|
|
274
|
+
const update = {
|
|
275
|
+
kind: 'updated',
|
|
276
|
+
plan: result.plan,
|
|
277
|
+
identity,
|
|
278
|
+
planPath,
|
|
279
|
+
reason: signal.reason,
|
|
280
|
+
};
|
|
276
281
|
const serializedUpdate = JSON.stringify(update);
|
|
277
282
|
if (serializedUpdate !== this.lastEmittedUpdate) {
|
|
278
283
|
this.lastEmittedUpdate = serializedUpdate;
|
|
@@ -78,7 +78,10 @@ export class FilesystemWorkspacePlanCatalog {
|
|
|
78
78
|
return { kind: 'unavailable' };
|
|
79
79
|
const source = await this.filesystem.readFile(canonical, 'utf8');
|
|
80
80
|
const after = await this.filesystem.stat(canonical);
|
|
81
|
-
if (!after.isFile() ||
|
|
81
|
+
if (!after.isFile() ||
|
|
82
|
+
after.size !== before.size ||
|
|
83
|
+
after.ino !== before.ino ||
|
|
84
|
+
after.dev !== before.dev)
|
|
82
85
|
return { kind: 'unavailable' };
|
|
83
86
|
const parsed = parseSupervisedPlan({ source, planPath: canonical, workspacePath });
|
|
84
87
|
return parsed.kind === 'available' ? parsed : { kind: 'unavailable' };
|
|
@@ -9,5 +9,8 @@ const execute = promisify(execFile);
|
|
|
9
9
|
export const PLAN_MEASUREMENT_COMMAND_TIMEOUT_MS = 15_000;
|
|
10
10
|
/** Invokes the explicitly configured Org Plan helper without a shell. */
|
|
11
11
|
export async function checkpointPlanMeasurement(helperPath, planPath, stepId, snapshot) {
|
|
12
|
-
await execute(helperPath, ['measure', 'checkpoint', planPath, stepId, JSON.stringify(snapshot)], {
|
|
12
|
+
await execute(helperPath, ['measure', 'checkpoint', planPath, stepId, JSON.stringify(snapshot)], {
|
|
13
|
+
shell: false,
|
|
14
|
+
timeout: PLAN_MEASUREMENT_COMMAND_TIMEOUT_MS,
|
|
15
|
+
});
|
|
13
16
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
|
-
import { availableSkillSchema } from '../../features/skills/model/skill-profile.js';
|
|
8
|
+
import { availableSkillSchema, } from '../../features/skills/model/skill-profile.js';
|
|
9
9
|
import { SkillProfileError } from '../../features/skills/model/errors.js';
|
|
10
10
|
import { launchCodexAppServer } from '../codex/codex-process-launcher.js';
|
|
11
11
|
const optionalString = z.preprocess((value) => (value === null ? undefined : value), z.string().optional());
|
|
@@ -19,12 +19,25 @@ const toolSchema = z.object({
|
|
|
19
19
|
url: optionalString,
|
|
20
20
|
});
|
|
21
21
|
const wireSkillSchema = z.object({
|
|
22
|
-
name: z.string(),
|
|
23
|
-
|
|
22
|
+
name: z.string(),
|
|
23
|
+
description: z.string(),
|
|
24
|
+
shortDescription: optionalString,
|
|
25
|
+
interface: optionalObject(z.object({
|
|
26
|
+
displayName: optionalString,
|
|
27
|
+
shortDescription: optionalString,
|
|
28
|
+
iconSmall: optionalString,
|
|
29
|
+
iconLarge: optionalString,
|
|
30
|
+
brandColor: optionalString,
|
|
31
|
+
defaultPrompt: optionalString,
|
|
32
|
+
})),
|
|
24
33
|
dependencies: optionalObject(z.object({ tools: optionalObject(z.array(toolSchema)) })),
|
|
25
|
-
path: z.string(),
|
|
34
|
+
path: z.string(),
|
|
35
|
+
scope: optionalString,
|
|
36
|
+
enabled: z.boolean(),
|
|
37
|
+
});
|
|
38
|
+
const resultSchema = z.object({
|
|
39
|
+
data: z.array(z.object({ cwd: z.string(), skills: z.array(wireSkillSchema), errors: z.array(z.unknown()) })),
|
|
26
40
|
});
|
|
27
|
-
const resultSchema = z.object({ data: z.array(z.object({ cwd: z.string(), skills: z.array(wireSkillSchema), errors: z.array(z.unknown()) })) });
|
|
28
41
|
/** Short-lived Codex app-server adapter: initialize, discover, and always terminate. */
|
|
29
42
|
export class CodexSkillCatalog {
|
|
30
43
|
profile;
|
|
@@ -58,7 +71,14 @@ export class CodexSkillCatalog {
|
|
|
58
71
|
});
|
|
59
72
|
return {
|
|
60
73
|
skills,
|
|
61
|
-
errors: entry.errors.map((error) => ({
|
|
74
|
+
errors: entry.errors.map((error) => ({
|
|
75
|
+
message: typeof error === 'object' &&
|
|
76
|
+
error !== null &&
|
|
77
|
+
'message' in error &&
|
|
78
|
+
typeof error.message === 'string'
|
|
79
|
+
? error.message
|
|
80
|
+
: 'Codex skill discovery error.',
|
|
81
|
+
})),
|
|
62
82
|
};
|
|
63
83
|
}
|
|
64
84
|
catch (error) {
|
|
@@ -75,7 +95,9 @@ export class CodexSkillCatalog {
|
|
|
75
95
|
try {
|
|
76
96
|
return await Promise.race([
|
|
77
97
|
promise,
|
|
78
|
-
new Promise((_, reject) => {
|
|
98
|
+
new Promise((_, reject) => {
|
|
99
|
+
timer = setTimeout(() => reject(new Error('timeout')), this.timeoutMs);
|
|
100
|
+
}),
|
|
79
101
|
]);
|
|
80
102
|
}
|
|
81
103
|
finally {
|
|
@@ -55,7 +55,11 @@ export class FilesystemSkillProfileStore {
|
|
|
55
55
|
const destination = join(root, `${name}.yml`);
|
|
56
56
|
const temporary = join(root, `.${name}.${randomUUID()}.tmp`);
|
|
57
57
|
try {
|
|
58
|
-
await writeFile(temporary, serializeSkillProfileYaml(profile), {
|
|
58
|
+
await writeFile(temporary, serializeSkillProfileYaml(profile), {
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
mode: 0o600,
|
|
61
|
+
flag: 'wx',
|
|
62
|
+
});
|
|
59
63
|
await rename(temporary, destination);
|
|
60
64
|
}
|
|
61
65
|
finally {
|
|
@@ -26,7 +26,12 @@ export const gestaltQuizDynamicTool = {
|
|
|
26
26
|
additionalProperties: false,
|
|
27
27
|
required: ['id', 'header', 'question', 'choices', 'allowCustom'],
|
|
28
28
|
properties: {
|
|
29
|
-
id: {
|
|
29
|
+
id: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
minLength: 1,
|
|
32
|
+
maxLength: 64,
|
|
33
|
+
pattern: '^[A-Za-z][A-Za-z0-9_-]*$',
|
|
34
|
+
},
|
|
30
35
|
header: { type: 'string', minLength: 1, maxLength: 120 },
|
|
31
36
|
question: { type: 'string', minLength: 1, maxLength: 600 },
|
|
32
37
|
choices: {
|
|
@@ -61,7 +66,9 @@ export function parseQuiz(value, minimumChoices = 2) {
|
|
|
61
66
|
if (questions.some((question) => question === null))
|
|
62
67
|
return null;
|
|
63
68
|
const parsed = questions;
|
|
64
|
-
return new Set(parsed.map((question) => question.id)).size === parsed.length
|
|
69
|
+
return new Set(parsed.map((question) => question.id)).size === parsed.length
|
|
70
|
+
? { questions: parsed }
|
|
71
|
+
: null;
|
|
65
72
|
}
|
|
66
73
|
/** Maps Codex's experimental native request shape into the common quiz value. */
|
|
67
74
|
export function mapNativeUserInputToQuiz(value) {
|
|
@@ -87,7 +94,9 @@ export function toQuizToolResponse(answers) {
|
|
|
87
94
|
{
|
|
88
95
|
type: 'input_text',
|
|
89
96
|
text: JSON.stringify({
|
|
90
|
-
answers: Object.fromEntries(answers
|
|
97
|
+
answers: Object.fromEntries(answers
|
|
98
|
+
.filter((answer) => answer.answer.trim())
|
|
99
|
+
.map((answer) => [answer.id, answer.answer])),
|
|
91
100
|
}),
|
|
92
101
|
},
|
|
93
102
|
],
|
|
@@ -98,7 +107,10 @@ export function isQuizToolResponseForQuiz(quiz, value) {
|
|
|
98
107
|
if (!isRecord(value) || value.success !== true || !Array.isArray(value.contentItems))
|
|
99
108
|
return false;
|
|
100
109
|
const item = value.contentItems[0];
|
|
101
|
-
if (value.contentItems.length !== 1 ||
|
|
110
|
+
if (value.contentItems.length !== 1 ||
|
|
111
|
+
!isRecord(item) ||
|
|
112
|
+
item.type !== 'input_text' ||
|
|
113
|
+
typeof item.text !== 'string')
|
|
102
114
|
return false;
|
|
103
115
|
try {
|
|
104
116
|
const response = parseQuizResponse(JSON.parse(item.text));
|
|
@@ -109,9 +121,14 @@ export function isQuizToolResponseForQuiz(quiz, value) {
|
|
|
109
121
|
}
|
|
110
122
|
}
|
|
111
123
|
function parseQuestion(value, minimumChoices) {
|
|
112
|
-
if (!isRecord(value) ||
|
|
124
|
+
if (!isRecord(value) ||
|
|
125
|
+
!Array.isArray(value.choices) ||
|
|
126
|
+
value.choices.length < minimumChoices ||
|
|
127
|
+
value.choices.length > 5)
|
|
113
128
|
return null;
|
|
114
|
-
if (!isQuestionId(value.id) ||
|
|
129
|
+
if (!isQuestionId(value.id) ||
|
|
130
|
+
!isBoundedText(value.header, 120) ||
|
|
131
|
+
!isBoundedText(value.question, 600))
|
|
115
132
|
return null;
|
|
116
133
|
if (typeof value.allowCustom !== 'boolean')
|
|
117
134
|
return null;
|
|
@@ -128,7 +145,9 @@ function parseQuestion(value, minimumChoices) {
|
|
|
128
145
|
};
|
|
129
146
|
}
|
|
130
147
|
function parseChoice(value) {
|
|
131
|
-
if (!isRecord(value) ||
|
|
148
|
+
if (!isRecord(value) ||
|
|
149
|
+
!isBoundedText(value.label, 160) ||
|
|
150
|
+
!isBoundedText(value.description, 600))
|
|
132
151
|
return null;
|
|
133
152
|
return { label: value.label, description: value.description };
|
|
134
153
|
}
|
|
@@ -136,13 +155,15 @@ function parseQuizResponse(value) {
|
|
|
136
155
|
if (!isRecord(value) || !isRecord(value.answers))
|
|
137
156
|
return null;
|
|
138
157
|
const entries = Object.entries(value.answers);
|
|
139
|
-
if (entries.length === 0 ||
|
|
158
|
+
if (entries.length === 0 ||
|
|
159
|
+
entries.some(([id, answer]) => !isQuestionId(id) || !isBoundedText(answer, 2_000)))
|
|
140
160
|
return null;
|
|
141
161
|
return { answers: Object.fromEntries(entries) };
|
|
142
162
|
}
|
|
143
163
|
function isCompleteQuizResponse(quiz, response) {
|
|
144
164
|
const ids = Object.keys(response.answers);
|
|
145
|
-
if (ids.length !== quiz.questions.length ||
|
|
165
|
+
if (ids.length !== quiz.questions.length ||
|
|
166
|
+
ids.some((id) => !quiz.questions.some((question) => question.id === id)))
|
|
146
167
|
return false;
|
|
147
168
|
return quiz.questions.every((question) => {
|
|
148
169
|
const answer = response.answers[question.id];
|