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