@rubytech/taskmaster 1.5.2 → 1.6.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/dist/agents/skills/workspace.js +23 -2
- package/dist/agents/taskmaster-tools.js +8 -0
- package/dist/agents/tool-policy.js +4 -0
- package/dist/agents/tools/skill-draft-save-tool.js +110 -0
- package/dist/auto-reply/tokens.js +23 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/provision-seed.js +1 -0
- package/dist/config/defaults.js +32 -0
- package/dist/config/io.js +4 -4
- package/dist/config/legacy.migrations.part-3.js +51 -0
- package/dist/config/validation.js +2 -2
- package/dist/control-ui/assets/{index-Ck-xl0xu.js → index-C12OTik-.js} +733 -613
- package/dist/control-ui/assets/index-C12OTik-.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/cron/isolated-agent/delivery-record.js +54 -0
- package/dist/cron/isolated-agent/history.js +221 -0
- package/dist/cron/isolated-agent/run.js +56 -2
- package/dist/gateway/protocol/index.js +2 -1
- package/dist/gateway/protocol/schema/agents-models-skills.js +9 -0
- package/dist/gateway/protocol/schema/protocol-schemas.js +2 -1
- package/dist/gateway/server-methods/skills.js +29 -5
- package/dist/gateway/server.impl.js +17 -0
- package/dist/memory/audit.js +19 -2
- package/dist/memory/manager.js +1 -0
- package/package.json +1 -1
- package/skills/skill-builder/SKILL.md +13 -8
- package/taskmaster-docs/USER-GUIDE.md +39 -8
- package/dist/control-ui/assets/index-Ck-xl0xu.js.map +0 -1
|
@@ -243,10 +243,26 @@ export async function syncSkillsToWorkspace(params) {
|
|
|
243
243
|
}
|
|
244
244
|
});
|
|
245
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Bundled skills that must be overwritten in existing workspaces.
|
|
248
|
+
*
|
|
249
|
+
* Normally, bundled skill sync preserves existing workspace skills so user
|
|
250
|
+
* modifications are not lost. When a bundled skill changes in a way that
|
|
251
|
+
* affects functionality (e.g. referencing a new tool), add its directory
|
|
252
|
+
* name here so it is force-copied on the next gateway restart.
|
|
253
|
+
*
|
|
254
|
+
* Remove entries after they have shipped in at least one release.
|
|
255
|
+
*/
|
|
256
|
+
const FORCE_RESYNC_SKILLS = new Set([
|
|
257
|
+
// v1.6: skill-builder updated to use skill_draft_save tool instead of write
|
|
258
|
+
"skill-builder",
|
|
259
|
+
]);
|
|
246
260
|
/**
|
|
247
261
|
* Copy bundled skills into a workspace's `skills/` directory.
|
|
248
262
|
* Only copies skills whose target directory does not already exist,
|
|
249
|
-
* preserving any user modifications to existing workspace skills
|
|
263
|
+
* preserving any user modifications to existing workspace skills —
|
|
264
|
+
* unless the skill is in {@link FORCE_RESYNC_SKILLS}, in which case
|
|
265
|
+
* the workspace copy is replaced with the bundled version.
|
|
250
266
|
*/
|
|
251
267
|
export async function syncBundledSkillsToWorkspace(workspaceDir, opts) {
|
|
252
268
|
const bundledDir = opts?.bundledSkillsDir ?? resolveBundledSkillsDir();
|
|
@@ -266,9 +282,14 @@ export async function syncBundledSkillsToWorkspace(workspaceDir, opts) {
|
|
|
266
282
|
const synced = [];
|
|
267
283
|
for (const name of entries) {
|
|
268
284
|
const dest = path.join(targetSkillsDir, name);
|
|
269
|
-
|
|
285
|
+
const exists = fs.existsSync(dest);
|
|
286
|
+
if (exists && !FORCE_RESYNC_SKILLS.has(name))
|
|
270
287
|
continue;
|
|
271
288
|
try {
|
|
289
|
+
if (exists) {
|
|
290
|
+
await fsp.rm(dest, { recursive: true, force: true });
|
|
291
|
+
skillsLogger.info(`force-resyncing bundled skill "${name}" (updated in this version)`);
|
|
292
|
+
}
|
|
272
293
|
await fsp.cp(path.join(bundledDir, name), dest, { recursive: true });
|
|
273
294
|
synced.push(name);
|
|
274
295
|
}
|
|
@@ -29,6 +29,7 @@ import { createFileListTool } from "./tools/file-list-tool.js";
|
|
|
29
29
|
import { createContactLookupTool } from "./tools/contact-lookup-tool.js";
|
|
30
30
|
import { createContactUpdateTool } from "./tools/contact-update-tool.js";
|
|
31
31
|
import { createRelayMessageTool } from "./tools/relay-message-tool.js";
|
|
32
|
+
import { createSkillDraftSaveTool } from "./tools/skill-draft-save-tool.js";
|
|
32
33
|
import { createSkillReadTool } from "./tools/skill-read-tool.js";
|
|
33
34
|
import { createApiKeysTool } from "./tools/apikeys-tool.js";
|
|
34
35
|
import { createImageGenerateTool } from "./tools/image-generate-tool.js";
|
|
@@ -162,6 +163,13 @@ export function createTaskmasterTools(options) {
|
|
|
162
163
|
if (skillDirs.length > 0) {
|
|
163
164
|
tools.push(createSkillReadTool({ allowedSkillDirs: skillDirs }));
|
|
164
165
|
}
|
|
166
|
+
// Add skill draft save tool
|
|
167
|
+
const skillDraftSaveTool = createSkillDraftSaveTool({
|
|
168
|
+
config: options?.config,
|
|
169
|
+
agentSessionKey: options?.agentSessionKey,
|
|
170
|
+
});
|
|
171
|
+
if (skillDraftSaveTool)
|
|
172
|
+
tools.push(skillDraftSaveTool);
|
|
165
173
|
// Add memory tools (conditionally based on config)
|
|
166
174
|
const memoryToolOptions = {
|
|
167
175
|
config: options?.config,
|
|
@@ -33,6 +33,8 @@ export const TOOL_GROUPS = {
|
|
|
33
33
|
"group:messaging": ["message"],
|
|
34
34
|
// Nodes + device tools
|
|
35
35
|
"group:nodes": ["nodes"],
|
|
36
|
+
// Skill management
|
|
37
|
+
"group:skills": ["skill_read", "skill_draft_save"],
|
|
36
38
|
// Contact record management
|
|
37
39
|
"group:contacts": ["contact_create", "contact_delete", "contact_lookup", "contact_update"],
|
|
38
40
|
// Admin management tools
|
|
@@ -68,6 +70,7 @@ export const TOOL_GROUPS = {
|
|
|
68
70
|
"api_keys",
|
|
69
71
|
"file_delete",
|
|
70
72
|
"file_list",
|
|
73
|
+
"skill_draft_save",
|
|
71
74
|
],
|
|
72
75
|
};
|
|
73
76
|
// Tools that are never granted by profiles — must be explicitly added to the
|
|
@@ -110,6 +113,7 @@ const TOOL_PROFILES = {
|
|
|
110
113
|
"tts",
|
|
111
114
|
"license_generate",
|
|
112
115
|
"group:contacts",
|
|
116
|
+
"group:skills",
|
|
113
117
|
"relay_message",
|
|
114
118
|
"memory_save_media",
|
|
115
119
|
"image_generate",
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import { resolveAgentWorkspaceRoot, resolveSessionAgentId } from "../agent-scope.js";
|
|
5
|
+
import { jsonResult, readStringParam } from "./common.js";
|
|
6
|
+
const ReferenceSchema = Type.Object({
|
|
7
|
+
name: Type.String({ description: "Filename for the reference (e.g. escalation.md)" }),
|
|
8
|
+
content: Type.String({ description: "Markdown content of the reference file" }),
|
|
9
|
+
});
|
|
10
|
+
const SkillDraftSaveSchema = Type.Object({
|
|
11
|
+
name: Type.String({
|
|
12
|
+
description: "Skill name — lowercase, hyphen-separated (e.g. inventory-management). " +
|
|
13
|
+
"Used as the directory name under .skill-drafts/.",
|
|
14
|
+
}),
|
|
15
|
+
skill_content: Type.String({
|
|
16
|
+
description: "Full content of SKILL.md including YAML frontmatter (name, description) " +
|
|
17
|
+
"and all behaviour rules.",
|
|
18
|
+
}),
|
|
19
|
+
references: Type.Optional(Type.Array(ReferenceSchema, {
|
|
20
|
+
description: "Optional reference files to save under references/. " +
|
|
21
|
+
"Each entry needs a name (filename) and content (markdown).",
|
|
22
|
+
})),
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Create a tool that saves skill drafts to the workspace `.skill-drafts/` directory.
|
|
26
|
+
*
|
|
27
|
+
* This is the directory the Control Panel scans when showing available drafts
|
|
28
|
+
* (via the `skills.drafts` gateway method). Agents that lack `group:fs` can
|
|
29
|
+
* still create skill drafts through this purpose-built tool.
|
|
30
|
+
*/
|
|
31
|
+
export function createSkillDraftSaveTool(options) {
|
|
32
|
+
const cfg = options?.config;
|
|
33
|
+
if (!cfg)
|
|
34
|
+
return null;
|
|
35
|
+
const agentId = resolveSessionAgentId({
|
|
36
|
+
sessionKey: options?.agentSessionKey,
|
|
37
|
+
config: cfg,
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
label: "Skill Draft Save",
|
|
41
|
+
name: "skill_draft_save",
|
|
42
|
+
description: "Save a skill draft so the business owner can review and install it from the Control Panel. " +
|
|
43
|
+
"Writes SKILL.md and optional reference files to the .skill-drafts/ directory. " +
|
|
44
|
+
"The draft will appear under Skills → Add Skill in the Control Panel.",
|
|
45
|
+
parameters: SkillDraftSaveSchema,
|
|
46
|
+
execute: async (_toolCallId, params) => {
|
|
47
|
+
const p = params;
|
|
48
|
+
const name = readStringParam(p, "name", { required: true });
|
|
49
|
+
const skillContent = readStringParam(p, "skill_content", { required: true });
|
|
50
|
+
// Validate skill name: lowercase, hyphens, no path traversal
|
|
51
|
+
if (!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(name)) {
|
|
52
|
+
return jsonResult({
|
|
53
|
+
success: false,
|
|
54
|
+
error: "Invalid skill name. Use lowercase letters, numbers, and hyphens " +
|
|
55
|
+
"(e.g. inventory-management). Must start and end with a letter or number.",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const workspaceRoot = resolveAgentWorkspaceRoot(cfg, agentId);
|
|
59
|
+
const draftDir = path.join(workspaceRoot, ".skill-drafts", name);
|
|
60
|
+
try {
|
|
61
|
+
// Create the draft directory
|
|
62
|
+
fs.mkdirSync(draftDir, { recursive: true });
|
|
63
|
+
// Write SKILL.md
|
|
64
|
+
const skillPath = path.join(draftDir, "SKILL.md");
|
|
65
|
+
fs.writeFileSync(skillPath, skillContent, "utf-8");
|
|
66
|
+
// Write reference files if provided
|
|
67
|
+
const refs = p.references;
|
|
68
|
+
const refsSaved = [];
|
|
69
|
+
if (Array.isArray(refs) && refs.length > 0) {
|
|
70
|
+
const refsDir = path.join(draftDir, "references");
|
|
71
|
+
fs.mkdirSync(refsDir, { recursive: true });
|
|
72
|
+
for (const ref of refs) {
|
|
73
|
+
if (typeof ref !== "object" ||
|
|
74
|
+
ref === null ||
|
|
75
|
+
typeof ref.name !== "string" ||
|
|
76
|
+
typeof ref.content !== "string") {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const refName = ref.name.trim();
|
|
80
|
+
const refContent = ref.content;
|
|
81
|
+
// Sanitise reference filename — no path traversal
|
|
82
|
+
if (!refName || refName.includes("/") || refName.includes("\\") || refName === "..") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const refPath = path.join(refsDir, refName);
|
|
86
|
+
fs.writeFileSync(refPath, refContent, "utf-8");
|
|
87
|
+
refsSaved.push(refName);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return jsonResult({
|
|
91
|
+
success: true,
|
|
92
|
+
name,
|
|
93
|
+
path: draftDir,
|
|
94
|
+
skillFile: "SKILL.md",
|
|
95
|
+
references: refsSaved,
|
|
96
|
+
message: `Skill draft "${name}" saved. ` +
|
|
97
|
+
"The business owner can install it from the Control Panel → Skills → Add Skill.",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
102
|
+
return jsonResult({
|
|
103
|
+
success: false,
|
|
104
|
+
name,
|
|
105
|
+
error: `Failed to save skill draft: ${message}`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -3,10 +3,33 @@ export const SILENT_REPLY_TOKEN = "NO_REPLY";
|
|
|
3
3
|
function escapeRegExp(value) {
|
|
4
4
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Maximum text length for prefix/suffix silent-reply matching.
|
|
8
|
+
* Short texts (under this limit) may be model artifacts like
|
|
9
|
+
* "NO_REPLY -- I have nothing to add" and are safely suppressed.
|
|
10
|
+
* Longer texts contain substantive content that must not be silently
|
|
11
|
+
* dropped just because the token appears at an edge.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_SILENT_ARTIFACT_LENGTH = 200;
|
|
6
14
|
export function isSilentReplyText(text, token = SILENT_REPLY_TOKEN) {
|
|
7
15
|
if (!text)
|
|
8
16
|
return false;
|
|
17
|
+
const trimmed = text.trim();
|
|
18
|
+
if (!trimmed)
|
|
19
|
+
return false;
|
|
9
20
|
const escaped = escapeRegExp(token);
|
|
21
|
+
// Exact match: the entire text is just the token (with optional
|
|
22
|
+
// surrounding whitespace and trailing punctuation). Always silent.
|
|
23
|
+
if (new RegExp(`^\\s*${escaped}\\W*$`).test(trimmed))
|
|
24
|
+
return true;
|
|
25
|
+
// Long text contains real content — never suppress it, even if the
|
|
26
|
+
// token appears at the edges. This prevents silently dropping
|
|
27
|
+
// substantive agent output (e.g. a multi-paragraph report ending
|
|
28
|
+
// with NO_REPLY).
|
|
29
|
+
if (trimmed.length > MAX_SILENT_ARTIFACT_LENGTH)
|
|
30
|
+
return false;
|
|
31
|
+
// Short text: allow prefix/suffix matching to catch model artifacts
|
|
32
|
+
// like "NO_REPLY -- I have nothing to add" or "interject.NO_REPLY".
|
|
10
33
|
const prefix = new RegExp(`^\\s*${escaped}(?=$|\\W)`);
|
|
11
34
|
if (prefix.test(text))
|
|
12
35
|
return true;
|
package/dist/build-info.json
CHANGED
package/dist/config/defaults.js
CHANGED
|
@@ -325,6 +325,38 @@ export function applyContextPruningDefaults(cfg) {
|
|
|
325
325
|
},
|
|
326
326
|
};
|
|
327
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Default model fallback chain — used when the primary model fails and no
|
|
330
|
+
* explicit fallbacks are configured. Candidates that lack an API key are
|
|
331
|
+
* silently skipped at runtime, so listing them here is always safe.
|
|
332
|
+
*/
|
|
333
|
+
const DEFAULT_MODEL_FALLBACKS = [
|
|
334
|
+
"google/gemini-3-pro-preview",
|
|
335
|
+
"openai/gpt-5.2",
|
|
336
|
+
];
|
|
337
|
+
export function applyModelFallbackDefaults(cfg) {
|
|
338
|
+
const model = cfg.agents?.defaults?.model;
|
|
339
|
+
// Already has explicit fallbacks — respect the user's choice.
|
|
340
|
+
// An empty array is treated as "no fallbacks configured" (migration artifact),
|
|
341
|
+
// not as "user deliberately disabled fallbacks".
|
|
342
|
+
if (model && Array.isArray(model.fallbacks) && model.fallbacks.length > 0)
|
|
343
|
+
return cfg;
|
|
344
|
+
const defaults = cfg.agents?.defaults ?? {};
|
|
345
|
+
const existing = (model && typeof model === "object" ? model : {});
|
|
346
|
+
return {
|
|
347
|
+
...cfg,
|
|
348
|
+
agents: {
|
|
349
|
+
...cfg.agents,
|
|
350
|
+
defaults: {
|
|
351
|
+
...defaults,
|
|
352
|
+
model: {
|
|
353
|
+
...existing,
|
|
354
|
+
fallbacks: [...DEFAULT_MODEL_FALLBACKS],
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
328
360
|
export function applyCompactionDefaults(cfg) {
|
|
329
361
|
const defaults = cfg.agents?.defaults;
|
|
330
362
|
if (!defaults)
|
package/dist/config/io.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import JSON5 from "json5";
|
|
6
6
|
import { loadShellEnvFallback, resolveShellEnvFallbackTimeoutMs, shouldDeferShellEnvFallback, shouldEnableShellEnvFallback, } from "../infra/shell-env.js";
|
|
7
7
|
import { DuplicateAgentDirError, findDuplicateAgentDirs } from "./agent-dirs.js";
|
|
8
|
-
import { applyApiKeys, applyCompactionDefaults, applyContextPruningDefaults, applyAgentDefaults, applyLoggingDefaults, applyMessageDefaults, applyModelDefaults, applySessionDefaults, applyTalkApiKey, } from "./defaults.js";
|
|
8
|
+
import { applyApiKeys, applyCompactionDefaults, applyContextPruningDefaults, applyAgentDefaults, applyLoggingDefaults, applyMessageDefaults, applyModelDefaults, applyModelFallbackDefaults, applySessionDefaults, applyTalkApiKey, } from "./defaults.js";
|
|
9
9
|
import { VERSION } from "../version.js";
|
|
10
10
|
import { MissingEnvVarError, resolveConfigEnvVars } from "./env-substitution.js";
|
|
11
11
|
import { collectConfigEnvVars } from "./env-vars.js";
|
|
@@ -203,7 +203,7 @@ export function createConfigIO(overrides = {}) {
|
|
|
203
203
|
deps.logger.warn(`Config warnings:\\n${details}`);
|
|
204
204
|
}
|
|
205
205
|
warnIfConfigFromFuture(validated.config, deps.logger);
|
|
206
|
-
const cfg = applyApiKeys(applyModelDefaults(applyCompactionDefaults(applyContextPruningDefaults(applyAgentDefaults(applySessionDefaults(applyLoggingDefaults(applyMessageDefaults(validated.config))))))));
|
|
206
|
+
const cfg = applyApiKeys(applyModelFallbackDefaults(applyModelDefaults(applyCompactionDefaults(applyContextPruningDefaults(applyAgentDefaults(applySessionDefaults(applyLoggingDefaults(applyMessageDefaults(validated.config)))))))));
|
|
207
207
|
normalizeConfigPaths(cfg);
|
|
208
208
|
const duplicates = findDuplicateAgentDirs(cfg, {
|
|
209
209
|
env: deps.env,
|
|
@@ -242,7 +242,7 @@ export function createConfigIO(overrides = {}) {
|
|
|
242
242
|
const exists = deps.fs.existsSync(configPath);
|
|
243
243
|
if (!exists) {
|
|
244
244
|
const hash = hashConfigRaw(null);
|
|
245
|
-
const config = applyApiKeys(applyTalkApiKey(applyModelDefaults(applyCompactionDefaults(applyContextPruningDefaults(applyAgentDefaults(applySessionDefaults(applyMessageDefaults({}))))))));
|
|
245
|
+
const config = applyApiKeys(applyTalkApiKey(applyModelFallbackDefaults(applyModelDefaults(applyCompactionDefaults(applyContextPruningDefaults(applyAgentDefaults(applySessionDefaults(applyMessageDefaults({})))))))));
|
|
246
246
|
const legacyIssues = [];
|
|
247
247
|
return {
|
|
248
248
|
path: configPath,
|
|
@@ -350,7 +350,7 @@ export function createConfigIO(overrides = {}) {
|
|
|
350
350
|
raw,
|
|
351
351
|
parsed: parsedRes.parsed,
|
|
352
352
|
valid: true,
|
|
353
|
-
config: normalizeConfigPaths(applyApiKeys(applyTalkApiKey(applyModelDefaults(applyAgentDefaults(applySessionDefaults(applyLoggingDefaults(applyMessageDefaults(validated.config)))))))),
|
|
353
|
+
config: normalizeConfigPaths(applyApiKeys(applyTalkApiKey(applyModelFallbackDefaults(applyModelDefaults(applyAgentDefaults(applySessionDefaults(applyLoggingDefaults(applyMessageDefaults(validated.config))))))))),
|
|
354
354
|
hash,
|
|
355
355
|
issues: [],
|
|
356
356
|
warnings: validated.warnings,
|
|
@@ -216,6 +216,57 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3 = [
|
|
|
216
216
|
}
|
|
217
217
|
},
|
|
218
218
|
},
|
|
219
|
+
{
|
|
220
|
+
id: "admin-agent-tools-add-skill_draft_save",
|
|
221
|
+
describe: "Add skill_draft_save to admin agent tools.allow",
|
|
222
|
+
apply: (raw, changes) => {
|
|
223
|
+
const agents = getRecord(raw.agents);
|
|
224
|
+
const list = getAgentsList(agents);
|
|
225
|
+
for (const entry of list) {
|
|
226
|
+
if (!isRecord(entry))
|
|
227
|
+
continue;
|
|
228
|
+
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
|
229
|
+
if (!id)
|
|
230
|
+
continue;
|
|
231
|
+
const isAdmin = id === "admin" || id.endsWith("-admin");
|
|
232
|
+
if (!isAdmin)
|
|
233
|
+
continue;
|
|
234
|
+
const tools = getRecord(entry.tools);
|
|
235
|
+
if (!tools)
|
|
236
|
+
continue;
|
|
237
|
+
if (!Array.isArray(tools.allow))
|
|
238
|
+
continue;
|
|
239
|
+
const allow = tools.allow;
|
|
240
|
+
if (!allow.includes("skill_draft_save") && !allow.includes("group:skills")) {
|
|
241
|
+
allow.push("skill_draft_save");
|
|
242
|
+
changes.push(`Added skill_draft_save to agent "${id}" tools.allow.`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "agents.defaults.model.fallbacks-defaults",
|
|
249
|
+
describe: "Set default model fallback chain (Google Gemini Pro, OpenAI GPT-5.2)",
|
|
250
|
+
apply: (raw, changes) => {
|
|
251
|
+
const agents = getRecord(raw.agents);
|
|
252
|
+
const defaults = getRecord(agents?.defaults);
|
|
253
|
+
if (!defaults)
|
|
254
|
+
return;
|
|
255
|
+
const model = getRecord(defaults.model);
|
|
256
|
+
if (model) {
|
|
257
|
+
// Already has non-empty fallbacks — respect the user's choice.
|
|
258
|
+
if (Array.isArray(model.fallbacks) && model.fallbacks.length > 0)
|
|
259
|
+
return;
|
|
260
|
+
model.fallbacks = ["google/gemini-3-pro-preview", "openai/gpt-5.2"];
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
defaults.model = {
|
|
264
|
+
fallbacks: ["google/gemini-3-pro-preview", "openai/gpt-5.2"],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
changes.push("Set default model fallbacks: google/gemini-3-pro-preview, openai/gpt-5.2.");
|
|
268
|
+
},
|
|
269
|
+
},
|
|
219
270
|
{
|
|
220
271
|
id: "agents-tools-remove-sessions_send",
|
|
221
272
|
describe: "Remove sessions_send from agent tools.allow (tool removed for security)",
|
|
@@ -5,7 +5,7 @@ import { normalizePluginsConfig, resolveEnableState, resolveMemorySlotDecision,
|
|
|
5
5
|
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
|
6
6
|
import { validateJsonSchemaValue } from "../plugins/schema-validator.js";
|
|
7
7
|
import { findDuplicateAgentDirs, formatDuplicateAgentDirError } from "./agent-dirs.js";
|
|
8
|
-
import { applyAgentDefaults, applyModelDefaults, applySessionDefaults } from "./defaults.js";
|
|
8
|
+
import { applyAgentDefaults, applyModelDefaults, applyModelFallbackDefaults, applySessionDefaults, } from "./defaults.js";
|
|
9
9
|
import { findLegacyConfigIssues } from "./legacy.js";
|
|
10
10
|
import { TaskmasterSchema } from "./zod-schema.js";
|
|
11
11
|
const AVATAR_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
|
|
@@ -102,7 +102,7 @@ export function validateConfigObject(raw) {
|
|
|
102
102
|
}
|
|
103
103
|
return {
|
|
104
104
|
ok: true,
|
|
105
|
-
config: applyModelDefaults(applyAgentDefaults(applySessionDefaults(validated.data))),
|
|
105
|
+
config: applyModelFallbackDefaults(applyModelDefaults(applyAgentDefaults(applySessionDefaults(validated.data)))),
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
function isRecord(value) {
|