@markusylisiurunen/tau 0.2.63 → 0.2.64

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 (48) hide show
  1. package/dist/core/async/cli.js +68 -104
  2. package/dist/core/async/cli.js.map +1 -1
  3. package/dist/core/async/http_protocol.js +119 -8
  4. package/dist/core/async/http_protocol.js.map +1 -1
  5. package/dist/core/async/http_server.js +120 -240
  6. package/dist/core/async/http_server.js.map +1 -1
  7. package/dist/core/async/index.js +1 -1
  8. package/dist/core/async/index.js.map +1 -1
  9. package/dist/core/async/server_config.js +161 -356
  10. package/dist/core/async/server_config.js.map +1 -1
  11. package/dist/core/async/telegram.js +191 -133
  12. package/dist/core/async/telegram.js.map +1 -1
  13. package/dist/core/config/content_loader.js +57 -205
  14. package/dist/core/config/content_loader.js.map +1 -1
  15. package/dist/core/config/markdown_frontmatter.js +34 -0
  16. package/dist/core/config/markdown_frontmatter.js.map +1 -0
  17. package/dist/core/config/schema.js +266 -332
  18. package/dist/core/config/schema.js.map +1 -1
  19. package/dist/core/config/skill_parser.js +8 -32
  20. package/dist/core/config/skill_parser.js.map +1 -1
  21. package/dist/core/config/skills_loader.js +32 -18
  22. package/dist/core/config/skills_loader.js.map +1 -1
  23. package/dist/core/events/index.js +1 -0
  24. package/dist/core/events/index.js.map +1 -1
  25. package/dist/core/events/parser.js +115 -0
  26. package/dist/core/events/parser.js.map +1 -0
  27. package/dist/core/index.js +1 -1
  28. package/dist/core/index.js.map +1 -1
  29. package/dist/core/modes/rpc_protocol.js +249 -189
  30. package/dist/core/modes/rpc_protocol.js.map +1 -1
  31. package/dist/core/tools/send_input_to_agent.js +7 -12
  32. package/dist/core/tools/send_input_to_agent.js.map +1 -1
  33. package/dist/core/tools/spawn_agent.js +13 -14
  34. package/dist/core/tools/spawn_agent.js.map +1 -1
  35. package/dist/core/tools/terminate_agent.js +6 -11
  36. package/dist/core/tools/terminate_agent.js.map +1 -1
  37. package/dist/core/tools/wait_for_agent.js +6 -11
  38. package/dist/core/tools/wait_for_agent.js.map +1 -1
  39. package/dist/core/utils/mistral_transcription.js +13 -21
  40. package/dist/core/utils/mistral_transcription.js.map +1 -1
  41. package/dist/core/utils/parallel_api.js +15 -19
  42. package/dist/core/utils/parallel_api.js.map +1 -1
  43. package/dist/core/utils/zod.js +7 -0
  44. package/dist/core/utils/zod.js.map +1 -1
  45. package/dist/core/version.js +1 -1
  46. package/dist/tui/chat_controller/session_maintenance_service.js +98 -171
  47. package/dist/tui/chat_controller/session_maintenance_service.js.map +1 -1
  48. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
- import { parse as parseYaml } from "yaml";
3
+ import { z } from "zod";
4
+ import { parseMarkdownFrontMatter } from "../config/markdown_frontmatter.js";
4
5
  import { formatPersonaReference, parsePersonaReference } from "../persona_reference.js";
5
6
  import { REASONING_LEVELS } from "../types.js";
6
7
  import { parseCronSchedule } from "./cron.js";
@@ -10,147 +11,117 @@ export class AsyncDaemonConfigError extends Error {
10
11
  this.name = "AsyncDaemonConfigError";
11
12
  }
12
13
  }
13
- function isRecord(value) {
14
- return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ const CRON_JOB_FRONTMATTER_KEYS = new Set(["id", "projectId", "schedule", "enabled"]);
15
+ const nonEmptyStringSchema = z.string().trim().min(1, "must be a non-empty string.");
16
+ const positiveIntegerSchema = z
17
+ .number()
18
+ .int("must be a positive integer.")
19
+ .positive("must be a positive integer.");
20
+ const asyncIdListSchema = z.array(z.number().int(), {
21
+ message: "must be an array of integers.",
22
+ });
23
+ const asyncStringListSchema = z.array(nonEmptyStringSchema, {
24
+ message: "must be an array of non-empty strings.",
25
+ });
26
+ const asyncDaemonCronSchema = z
27
+ .object({
28
+ systemMessage: nonEmptyStringSchema.optional(),
29
+ jobsDir: nonEmptyStringSchema.optional(),
30
+ })
31
+ .strict();
32
+ const asyncDaemonTopLevelSchema = z
33
+ .object({
34
+ host: nonEmptyStringSchema.optional(),
35
+ port: z
36
+ .number()
37
+ .int("must be a positive integer <= 65535.")
38
+ .positive("must be a positive integer <= 65535.")
39
+ .max(65535, "must be a positive integer <= 65535.")
40
+ .optional(),
41
+ authToken: nonEmptyStringSchema.optional(),
42
+ maxSessions: positiveIntegerSchema.optional(),
43
+ workspaceRoot: nonEmptyStringSchema.optional(),
44
+ systemMessage: nonEmptyStringSchema.optional(),
45
+ telegram: z.unknown().optional(),
46
+ cron: z.unknown().optional(),
47
+ projects: z.unknown().optional(),
48
+ })
49
+ .strict();
50
+ const telegramBotSchema = z
51
+ .object({
52
+ botToken: nonEmptyStringSchema,
53
+ allowedProjectIds: asyncStringListSchema.min(1, "must not be empty.").optional(),
54
+ allowedUserIds: asyncIdListSchema.optional(),
55
+ allowedChatIds: asyncIdListSchema.optional(),
56
+ defaultProjectId: nonEmptyStringSchema.optional(),
57
+ systemMessage: nonEmptyStringSchema.optional(),
58
+ pollIntervalMs: positiveIntegerSchema.optional(),
59
+ requestTimeoutSeconds: positiveIntegerSchema.optional(),
60
+ })
61
+ .strict();
62
+ function createProjectSchema(configDir) {
63
+ return z
64
+ .object({
65
+ repo: nonEmptyStringSchema.refine((value) => isGithubRepoRef(value), {
66
+ message: "must be in owner/repo format (GitHub).",
67
+ }),
68
+ ref: nonEmptyStringSchema.optional(),
69
+ workspaceRoot: nonEmptyStringSchema
70
+ .transform((value) => resolve(configDir, value))
71
+ .optional(),
72
+ workingDirectory: nonEmptyStringSchema
73
+ .refine((value) => !isAbsolute(value), {
74
+ message: "must be a relative path.",
75
+ })
76
+ .optional(),
77
+ description: nonEmptyStringSchema.optional(),
78
+ bootstrapCommands: z
79
+ .array(z.string().refine((value) => value.trim().length > 0), {
80
+ message: "must be a non-empty string array.",
81
+ })
82
+ .min(1, "must be a non-empty string array.")
83
+ .optional(),
84
+ backgroundBootstrapCommands: z
85
+ .array(z.string().refine((value) => value.trim().length > 0), {
86
+ message: "must be a non-empty string array.",
87
+ })
88
+ .min(1, "must be a non-empty string array.")
89
+ .optional(),
90
+ persona: z.string().optional(),
91
+ riskLevel: z.enum(["read-only", "read-write"]).optional(),
92
+ sandbox: z.boolean().optional(),
93
+ noAgentContextFiles: z.boolean().optional(),
94
+ })
95
+ .strict();
15
96
  }
16
- function isPositiveInteger(value) {
17
- return (typeof value === "number" && Number.isInteger(value) && Number.isFinite(value) && value > 0);
97
+ function formatUnknownKeysError(sourceLabel, fieldPath, keys) {
98
+ const unknownKeys = [...keys].sort();
99
+ const keyLabel = unknownKeys.length === 1 ? "key" : "keys";
100
+ return `${sourceLabel}: unknown ${keyLabel} in ${fieldPath}: ${unknownKeys.join(", ")}.`;
18
101
  }
19
- function parseMarkdownWithFrontMatter(content) {
20
- const lines = content.split("\n");
21
- if (lines[0]?.trim() !== "---") {
22
- return { frontMatter: {}, body: content.trim() };
23
- }
24
- let endIndex = -1;
25
- for (let i = 1; i < lines.length; i += 1) {
26
- if (lines[i]?.trim() === "---") {
27
- endIndex = i;
28
- break;
29
- }
30
- }
31
- if (endIndex === -1) {
32
- return { frontMatter: {}, body: content.trim() };
33
- }
34
- const frontMatterLines = lines.slice(1, endIndex);
35
- const bodyLines = lines.slice(endIndex + 1);
36
- return {
37
- frontMatter: parseYamlFrontMatter(frontMatterLines.join("\n")),
38
- body: bodyLines.join("\n").trim(),
39
- };
40
- }
41
- function parseYamlFrontMatter(yamlText) {
42
- try {
43
- const parsed = parseYaml(yamlText);
44
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
45
- return {};
46
- }
47
- return parsed;
48
- }
49
- catch {
50
- return {};
51
- }
52
- }
53
- function parseAsyncIdList(raw, fieldPath, sourceLabel) {
54
- if (!Array.isArray(raw)) {
55
- return { errors: [`${sourceLabel}: ${fieldPath} must be an array of integers.`] };
56
- }
57
- const values = [];
58
- for (const entry of raw) {
59
- if (typeof entry !== "number" || !Number.isFinite(entry) || !Number.isInteger(entry)) {
60
- return { errors: [`${sourceLabel}: ${fieldPath} must be an array of integers.`] };
61
- }
62
- values.push(entry);
63
- }
64
- return { values, errors: [] };
65
- }
66
- function parseAsyncStringList(raw, fieldPath, sourceLabel) {
67
- if (!Array.isArray(raw)) {
68
- return { errors: [`${sourceLabel}: ${fieldPath} must be an array of non-empty strings.`] };
69
- }
70
- const values = [];
71
- for (const entry of raw) {
72
- if (typeof entry !== "string" || !entry.trim()) {
73
- return { errors: [`${sourceLabel}: ${fieldPath} must be an array of non-empty strings.`] };
102
+ function formatSectionZodErrors(error, sourceLabel, fieldPath) {
103
+ const errors = [];
104
+ for (const issue of error.issues) {
105
+ if (issue.code === "unrecognized_keys") {
106
+ errors.push(formatUnknownKeysError(sourceLabel, fieldPath, issue.keys));
107
+ continue;
74
108
  }
75
- values.push(entry.trim());
109
+ const issuePath = issue.path.length > 0 ? `.${issue.path.join(".")}` : "";
110
+ errors.push(`${sourceLabel}: ${fieldPath}${issuePath} ${issue.message}`);
76
111
  }
77
- return { values, errors: [] };
112
+ return errors;
78
113
  }
79
114
  function parseTelegramBotConfig(raw, fieldPath, sourceLabel, knownProjectIds) {
80
- if (!isRecord(raw)) {
81
- return { errors: [`${sourceLabel}: ${fieldPath} must be an object.`] };
115
+ const parsed = telegramBotSchema.safeParse(raw);
116
+ if (!parsed.success) {
117
+ return { errors: formatSectionZodErrors(parsed.error, sourceLabel, fieldPath) };
82
118
  }
83
- const data = raw;
84
- const config = {};
119
+ const config = parsed.data;
85
120
  const errors = [];
86
- if (typeof data.botToken === "string" && data.botToken.trim()) {
87
- config.botToken = data.botToken.trim();
88
- }
89
- else {
90
- errors.push(`${sourceLabel}: ${fieldPath}.botToken must be a non-empty string.`);
91
- }
92
- if (data.allowedProjectIds !== undefined) {
93
- const parsed = parseAsyncStringList(data.allowedProjectIds, `${fieldPath}.allowedProjectIds`, sourceLabel);
94
- if (parsed.values) {
95
- if (parsed.values.length === 0) {
96
- errors.push(`${sourceLabel}: ${fieldPath}.allowedProjectIds must not be empty.`);
97
- }
98
- else {
99
- const missingProjectIds = parsed.values.filter((projectId) => !knownProjectIds.has(projectId));
100
- if (missingProjectIds.length > 0) {
101
- errors.push(`${sourceLabel}: ${fieldPath}.allowedProjectIds contains unknown project ids: ${missingProjectIds.join(", ")}`);
102
- }
103
- else {
104
- config.allowedProjectIds = parsed.values;
105
- }
106
- }
107
- }
108
- errors.push(...parsed.errors);
109
- }
110
- if (data.allowedUserIds !== undefined) {
111
- const parsed = parseAsyncIdList(data.allowedUserIds, `${fieldPath}.allowedUserIds`, sourceLabel);
112
- if (parsed.values) {
113
- config.allowedUserIds = parsed.values;
114
- }
115
- errors.push(...parsed.errors);
116
- }
117
- if (data.allowedChatIds !== undefined) {
118
- const parsed = parseAsyncIdList(data.allowedChatIds, `${fieldPath}.allowedChatIds`, sourceLabel);
119
- if (parsed.values) {
120
- config.allowedChatIds = parsed.values;
121
- }
122
- errors.push(...parsed.errors);
123
- }
124
- if (data.defaultProjectId !== undefined) {
125
- if (typeof data.defaultProjectId === "string" && data.defaultProjectId.trim()) {
126
- config.defaultProjectId = data.defaultProjectId.trim();
127
- }
128
- else {
129
- errors.push(`${sourceLabel}: ${fieldPath}.defaultProjectId must be a non-empty string.`);
130
- }
131
- }
132
- if (data.systemMessage !== undefined) {
133
- if (typeof data.systemMessage === "string" && data.systemMessage.trim()) {
134
- config.systemMessage = data.systemMessage.trim();
135
- }
136
- else {
137
- errors.push(`${sourceLabel}: ${fieldPath}.systemMessage must be a non-empty string.`);
138
- }
139
- }
140
- if (data.pollIntervalMs !== undefined) {
141
- if (isPositiveInteger(data.pollIntervalMs)) {
142
- config.pollIntervalMs = data.pollIntervalMs;
143
- }
144
- else {
145
- errors.push(`${sourceLabel}: ${fieldPath}.pollIntervalMs must be a positive integer.`);
146
- }
147
- }
148
- if (data.requestTimeoutSeconds !== undefined) {
149
- if (isPositiveInteger(data.requestTimeoutSeconds)) {
150
- config.requestTimeoutSeconds = data.requestTimeoutSeconds;
151
- }
152
- else {
153
- errors.push(`${sourceLabel}: ${fieldPath}.requestTimeoutSeconds must be a positive integer.`);
121
+ if (config.allowedProjectIds) {
122
+ const missingProjectIds = config.allowedProjectIds.filter((projectId) => !knownProjectIds.has(projectId));
123
+ if (missingProjectIds.length > 0) {
124
+ errors.push(`${sourceLabel}: ${fieldPath}.allowedProjectIds contains unknown project ids: ${missingProjectIds.join(", ")}`);
154
125
  }
155
126
  }
156
127
  if (config.defaultProjectId && !knownProjectIds.has(config.defaultProjectId)) {
@@ -161,19 +132,20 @@ function parseTelegramBotConfig(raw, fieldPath, sourceLabel, knownProjectIds) {
161
132
  !config.allowedProjectIds.includes(config.defaultProjectId)) {
162
133
  errors.push(`${sourceLabel}: ${fieldPath}.defaultProjectId must be included in ${fieldPath}.allowedProjectIds`);
163
134
  }
164
- if (Object.keys(config).length === 0) {
135
+ if (errors.length > 0) {
165
136
  return { errors };
166
137
  }
167
- return { config, errors };
138
+ return { config, errors: [] };
168
139
  }
169
140
  function parseTelegramConfig(raw, sourceLabel, knownProjectIds) {
170
141
  if (raw === undefined) {
171
142
  return { errors: [] };
172
143
  }
173
- if (!isRecord(raw)) {
144
+ const parsedObject = z.record(z.string(), z.unknown()).safeParse(raw);
145
+ if (!parsedObject.success) {
174
146
  return { errors: [`${sourceLabel}: telegram must be an object.`] };
175
147
  }
176
- const entries = Object.entries(raw);
148
+ const entries = Object.entries(parsedObject.data);
177
149
  if (entries.length === 0) {
178
150
  return { errors: [`${sourceLabel}: telegram must define at least one bot id.`] };
179
151
  }
@@ -199,179 +171,43 @@ function parseCronConfig(raw, sourceLabel) {
199
171
  if (raw === undefined) {
200
172
  return { errors: [] };
201
173
  }
202
- if (!isRecord(raw)) {
203
- return { errors: [`${sourceLabel}: cron must be an object.`] };
204
- }
205
- const data = raw;
206
- const config = {};
207
- const errors = [];
208
- if (data.systemMessage !== undefined) {
209
- if (typeof data.systemMessage === "string" && data.systemMessage.trim()) {
210
- config.systemMessage = data.systemMessage.trim();
211
- }
212
- else {
213
- errors.push(`${sourceLabel}: cron.systemMessage must be a non-empty string.`);
214
- }
215
- }
216
- if (data.jobsDir !== undefined) {
217
- if (typeof data.jobsDir === "string" && data.jobsDir.trim()) {
218
- config.jobsDir = data.jobsDir.trim();
219
- }
220
- else {
221
- errors.push(`${sourceLabel}: cron.jobsDir must be a non-empty string.`);
222
- }
174
+ const parsed = asyncDaemonCronSchema.safeParse(raw);
175
+ if (!parsed.success) {
176
+ return { errors: formatSectionZodErrors(parsed.error, sourceLabel, "cron") };
223
177
  }
178
+ const config = parsed.data;
224
179
  if (Object.keys(config).length === 0) {
225
- return { errors };
226
- }
227
- return { config, errors };
228
- }
229
- function parseRiskLevel(raw) {
230
- if (raw === "read-only" || raw === "read-write") {
231
- return raw;
180
+ return { errors: [] };
232
181
  }
233
- return undefined;
182
+ return { config, errors: [] };
234
183
  }
235
184
  function isGithubRepoRef(value) {
236
185
  return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(value);
237
186
  }
238
187
  function parseProject(raw, sourceLabel, projectId, configDir) {
239
- if (!isRecord(raw)) {
240
- return { errors: [`${sourceLabel}: projects.${projectId} must be an object.`] };
188
+ const schema = createProjectSchema(configDir);
189
+ const parsed = schema.safeParse(raw);
190
+ if (!parsed.success) {
191
+ return { errors: formatSectionZodErrors(parsed.error, sourceLabel, `projects.${projectId}`) };
241
192
  }
242
- const data = raw;
193
+ const config = parsed.data;
243
194
  const errors = [];
244
- const repoRaw = data.repo;
245
- if (typeof repoRaw !== "string" || !repoRaw.trim()) {
246
- errors.push(`${sourceLabel}: projects.${projectId}.repo must be a non-empty string.`);
247
- }
248
- else if (!isGithubRepoRef(repoRaw.trim())) {
249
- errors.push(`${sourceLabel}: projects.${projectId}.repo must be in owner/repo format (GitHub).`);
250
- }
251
- const config = {
252
- repo: typeof repoRaw === "string" ? repoRaw.trim() : "",
253
- };
254
- if (data.ref !== undefined) {
255
- if (typeof data.ref === "string" && data.ref.trim()) {
256
- config.ref = data.ref.trim();
257
- }
258
- else {
259
- errors.push(`${sourceLabel}: projects.${projectId}.ref must be a non-empty string.`);
260
- }
261
- }
262
- if (data.workspaceRoot !== undefined) {
263
- if (typeof data.workspaceRoot === "string" && data.workspaceRoot.trim()) {
264
- config.workspaceRoot = resolve(configDir, data.workspaceRoot.trim());
265
- }
266
- else {
267
- errors.push(`${sourceLabel}: projects.${projectId}.workspaceRoot must be a non-empty string.`);
268
- }
269
- }
270
- if (data.workingDirectory !== undefined) {
271
- if (typeof data.workingDirectory === "string" && data.workingDirectory.trim()) {
272
- const workingDirectory = data.workingDirectory.trim();
273
- if (isAbsolute(workingDirectory)) {
274
- errors.push(`${sourceLabel}: projects.${projectId}.workingDirectory must be a relative path.`);
275
- }
276
- else {
277
- config.workingDirectory = workingDirectory;
278
- }
279
- }
280
- else {
281
- errors.push(`${sourceLabel}: projects.${projectId}.workingDirectory must be a non-empty string.`);
282
- }
283
- }
284
- if (data.description !== undefined) {
285
- if (typeof data.description === "string" && data.description.trim()) {
286
- config.description = data.description.trim();
287
- }
288
- else {
289
- errors.push(`${sourceLabel}: projects.${projectId}.description must be a non-empty string.`);
290
- }
291
- }
292
- if (data.bootstrapCommands !== undefined) {
293
- if (!Array.isArray(data.bootstrapCommands) || data.bootstrapCommands.length === 0) {
294
- errors.push(`${sourceLabel}: projects.${projectId}.bootstrapCommands must be a non-empty string array.`);
295
- }
296
- else {
297
- const commands = [];
298
- for (const command of data.bootstrapCommands) {
299
- if (typeof command !== "string" || !command.trim()) {
300
- errors.push(`${sourceLabel}: projects.${projectId}.bootstrapCommands must be a non-empty string array.`);
301
- break;
302
- }
303
- commands.push(command);
304
- }
305
- if (commands.length > 0) {
306
- config.bootstrapCommands = commands;
307
- }
308
- }
309
- }
310
- if (data.backgroundBootstrapCommands !== undefined) {
311
- if (!Array.isArray(data.backgroundBootstrapCommands) ||
312
- data.backgroundBootstrapCommands.length === 0) {
313
- errors.push(`${sourceLabel}: projects.${projectId}.backgroundBootstrapCommands must be a non-empty string array.`);
314
- }
315
- else {
316
- const commands = [];
317
- for (const command of data.backgroundBootstrapCommands) {
318
- if (typeof command !== "string" || !command.trim()) {
319
- errors.push(`${sourceLabel}: projects.${projectId}.backgroundBootstrapCommands must be a non-empty string array.`);
320
- break;
321
- }
322
- commands.push(command);
323
- }
324
- if (commands.length > 0) {
325
- config.backgroundBootstrapCommands = commands;
326
- }
327
- }
328
- }
329
- if (data.persona !== undefined) {
330
- if (typeof data.persona === "string") {
331
- const parsedPersona = parsePersonaReference(data.persona);
332
- if (parsedPersona.error === "empty-persona") {
333
- errors.push(`${sourceLabel}: projects.${projectId}.persona must be a non-empty string.`);
334
- }
335
- else if (parsedPersona.error === "missing-reasoning") {
336
- errors.push(`${sourceLabel}: projects.${projectId}.persona is missing a reasoning level after ':'.`);
337
- }
338
- else if (parsedPersona.error === "invalid-reasoning") {
339
- errors.push(`${sourceLabel}: projects.${projectId}.persona has invalid reasoning level '${parsedPersona.rawReasoning}'. allowed levels: ${REASONING_LEVELS.join(", ")}.`);
340
- }
341
- else if (parsedPersona.personaId) {
342
- config.persona = formatPersonaReference({
343
- personaId: parsedPersona.personaId,
344
- reasoning: parsedPersona.reasoning,
345
- });
346
- }
347
- }
348
- else {
195
+ if (config.persona !== undefined) {
196
+ const parsedPersona = parsePersonaReference(config.persona);
197
+ if (parsedPersona.error === "empty-persona") {
349
198
  errors.push(`${sourceLabel}: projects.${projectId}.persona must be a non-empty string.`);
350
199
  }
351
- }
352
- if (data.riskLevel !== undefined) {
353
- const riskLevel = parseRiskLevel(data.riskLevel);
354
- if (riskLevel) {
355
- config.riskLevel = riskLevel;
356
- }
357
- else {
358
- errors.push(`${sourceLabel}: projects.${projectId}.riskLevel must be read-only or read-write.`);
200
+ else if (parsedPersona.error === "missing-reasoning") {
201
+ errors.push(`${sourceLabel}: projects.${projectId}.persona is missing a reasoning level after ':'.`);
359
202
  }
360
- }
361
- if (data.sandbox !== undefined) {
362
- if (typeof data.sandbox === "boolean") {
363
- config.sandbox = data.sandbox;
203
+ else if (parsedPersona.error === "invalid-reasoning") {
204
+ errors.push(`${sourceLabel}: projects.${projectId}.persona has invalid reasoning level '${parsedPersona.rawReasoning}'. allowed levels: ${REASONING_LEVELS.join(", ")}.`);
364
205
  }
365
- else {
366
- errors.push(`${sourceLabel}: projects.${projectId}.sandbox must be a boolean.`);
367
- }
368
- }
369
- if (data.noAgentContextFiles !== undefined) {
370
- if (typeof data.noAgentContextFiles === "boolean") {
371
- config.noAgentContextFiles = data.noAgentContextFiles;
372
- }
373
- else {
374
- errors.push(`${sourceLabel}: projects.${projectId}.noAgentContextFiles must be a boolean.`);
206
+ else if (parsedPersona.personaId) {
207
+ config.persona = formatPersonaReference({
208
+ personaId: parsedPersona.personaId,
209
+ reasoning: parsedPersona.reasoning,
210
+ });
375
211
  }
376
212
  }
377
213
  if (errors.length > 0) {
@@ -380,12 +216,13 @@ function parseProject(raw, sourceLabel, projectId, configDir) {
380
216
  return { config, errors: [] };
381
217
  }
382
218
  function parseProjects(raw, sourceLabel, configDir) {
383
- if (!isRecord(raw)) {
219
+ const parsedObject = z.record(z.string(), z.unknown()).safeParse(raw);
220
+ if (!parsedObject.success) {
384
221
  return { projects: {}, errors: [`${sourceLabel}: projects must be an object.`] };
385
222
  }
386
223
  const errors = [];
387
224
  const projects = {};
388
- for (const [projectId, value] of Object.entries(raw)) {
225
+ for (const [projectId, value] of Object.entries(parsedObject.data)) {
389
226
  if (!projectId.trim()) {
390
227
  errors.push(`${sourceLabel}: projects keys must be non-empty.`);
391
228
  continue;
@@ -410,9 +247,19 @@ function parseCronJobMarkdownFile(filePath, projects, sourceLabel) {
410
247
  ],
411
248
  };
412
249
  }
413
- const { frontMatter, body } = parseMarkdownWithFrontMatter(content);
250
+ const markdownResult = parseMarkdownFrontMatter(content);
251
+ if (!markdownResult.ok) {
252
+ return { errors: [`${sourceLabel}: ${filePath}: ${markdownResult.message}.`] };
253
+ }
254
+ const { frontMatter, body } = markdownResult;
414
255
  const fileId = basename(filePath, ".md").trim();
415
256
  const errors = [];
257
+ const unknownKeys = Object.keys(frontMatter)
258
+ .filter((key) => !CRON_JOB_FRONTMATTER_KEYS.has(key))
259
+ .sort();
260
+ if (unknownKeys.length > 0) {
261
+ errors.push(formatUnknownKeysError(sourceLabel, `${filePath} frontmatter`, unknownKeys));
262
+ }
416
263
  const enabledRaw = frontMatter.enabled;
417
264
  if (enabledRaw !== undefined && typeof enabledRaw !== "boolean") {
418
265
  errors.push(`${sourceLabel}: ${filePath}: frontmatter enabled must be a boolean when set.`);
@@ -536,65 +383,23 @@ export function loadAsyncDaemonConfig(configFilePath) {
536
383
  catch (error) {
537
384
  throw new AsyncDaemonConfigError(`${sourceLabel}: failed to read/parse json: ${error instanceof Error ? error.message : String(error)}`);
538
385
  }
539
- if (!isRecord(parsed)) {
386
+ const parsedConfigObject = z.record(z.string(), z.unknown()).safeParse(parsed);
387
+ if (!parsedConfigObject.success) {
540
388
  throw new AsyncDaemonConfigError(`${sourceLabel}: config must be an object.`);
541
389
  }
542
- const data = parsed;
390
+ const data = parsedConfigObject.data;
543
391
  const errors = [];
544
- let host = "127.0.0.1";
545
- if (data.host !== undefined) {
546
- if (typeof data.host === "string" && data.host.trim()) {
547
- host = data.host.trim();
548
- }
549
- else {
550
- errors.push(`${sourceLabel}: host must be a non-empty string.`);
551
- }
552
- }
553
- let port = 7788;
554
- if (data.port !== undefined) {
555
- if (isPositiveInteger(data.port) && data.port <= 65535) {
556
- port = data.port;
557
- }
558
- else {
559
- errors.push(`${sourceLabel}: port must be a positive integer <= 65535.`);
560
- }
561
- }
562
- let authToken;
563
- if (data.authToken !== undefined) {
564
- if (typeof data.authToken === "string" && data.authToken.trim()) {
565
- authToken = data.authToken.trim();
566
- }
567
- else {
568
- errors.push(`${sourceLabel}: authToken must be a non-empty string.`);
569
- }
570
- }
571
- let maxSessions;
572
- if (data.maxSessions !== undefined) {
573
- if (isPositiveInteger(data.maxSessions)) {
574
- maxSessions = data.maxSessions;
575
- }
576
- else {
577
- errors.push(`${sourceLabel}: maxSessions must be a positive integer.`);
578
- }
579
- }
580
- let workspaceRoot = resolve(configDir, ".tau", "async-workspaces");
581
- if (data.workspaceRoot !== undefined) {
582
- if (typeof data.workspaceRoot === "string" && data.workspaceRoot.trim()) {
583
- workspaceRoot = resolve(configDir, data.workspaceRoot.trim());
584
- }
585
- else {
586
- errors.push(`${sourceLabel}: workspaceRoot must be a non-empty string.`);
587
- }
588
- }
589
- let systemMessage;
590
- if (data.systemMessage !== undefined) {
591
- if (typeof data.systemMessage === "string" && data.systemMessage.trim()) {
592
- systemMessage = data.systemMessage.trim();
593
- }
594
- else {
595
- errors.push(`${sourceLabel}: systemMessage must be a non-empty string.`);
596
- }
597
- }
392
+ const topLevelResult = asyncDaemonTopLevelSchema.safeParse(data);
393
+ if (!topLevelResult.success) {
394
+ errors.push(...formatSectionZodErrors(topLevelResult.error, sourceLabel, "config"));
395
+ }
396
+ const topLevel = topLevelResult.success ? topLevelResult.data : {};
397
+ const host = topLevel.host ?? "127.0.0.1";
398
+ const port = topLevel.port ?? 7788;
399
+ const authToken = topLevel.authToken;
400
+ const maxSessions = topLevel.maxSessions;
401
+ const workspaceRoot = resolve(configDir, topLevel.workspaceRoot ?? ".tau/async-workspaces");
402
+ const systemMessage = topLevel.systemMessage;
598
403
  const projectsResult = parseProjects(data.projects, sourceLabel, configDir);
599
404
  const telegramResult = parseTelegramConfig(data.telegram, sourceLabel, new Set(Object.keys(projectsResult.projects)));
600
405
  const cronResult = parseCronConfig(data.cron, sourceLabel);