@rubytech/taskmaster 1.0.10 → 1.0.12

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 (38) hide show
  1. package/dist/agents/pi-embedded-helpers/errors.js +6 -7
  2. package/dist/agents/pi-embedded-runner/compact.js +5 -0
  3. package/dist/agents/pi-embedded-runner/run/attempt.js +5 -0
  4. package/dist/agents/pi-embedded-runner/run/payloads.js +4 -4
  5. package/dist/agents/pi-embedded-runner/run.js +4 -4
  6. package/dist/agents/pi-tools.js +1 -0
  7. package/dist/agents/skills/workspace.js +40 -3
  8. package/dist/agents/system-prompt.js +12 -6
  9. package/dist/agents/taskmaster-tools.js +10 -0
  10. package/dist/agents/tool-display.json +5 -0
  11. package/dist/agents/tool-policy.js +1 -0
  12. package/dist/agents/tools/memory-reindex-tool.js +67 -0
  13. package/dist/agents/tools/skill-read-tool.js +75 -0
  14. package/dist/agents/workspace.js +2 -2
  15. package/dist/auto-reply/reply/agent-runner-execution.js +17 -40
  16. package/dist/auto-reply/reply/agent-runner.js +14 -24
  17. package/dist/auto-reply/reply/session.js +1 -1
  18. package/dist/build-info.json +3 -3
  19. package/dist/cli/gateway-cli/dev.js +1 -1
  20. package/dist/cli/provision-seed.js +1 -0
  21. package/dist/control-ui/assets/{index-Dcwiyz6o.js → index-D8ayJUWC.js} +205 -195
  22. package/dist/control-ui/assets/index-D8ayJUWC.js.map +1 -0
  23. package/dist/control-ui/assets/{index-DE-53MKi.css → index-dMMqL7A5.css} +1 -1
  24. package/dist/control-ui/index.html +2 -2
  25. package/dist/gateway/protocol/schema/sessions-transcript.js +1 -0
  26. package/dist/gateway/server-methods/memory.js +62 -0
  27. package/dist/gateway/server-methods/sessions-transcript.js +10 -9
  28. package/dist/gateway/server-methods.js +5 -1
  29. package/dist/memory/manager.js +17 -1
  30. package/dist/memory/memory-schema.js +23 -1
  31. package/dist/web/auto-reply/monitor.js +1 -1
  32. package/package.json +1 -1
  33. package/taskmaster-docs/USER-GUIDE.md +43 -12
  34. package/templates/customer/agents/admin/AGENTS.md +6 -5
  35. package/templates/customer/agents/admin/TOOLS.md +36 -0
  36. package/templates/taskmaster/agents/admin/AGENTS.md +7 -5
  37. package/templates/tradesupport/agents/admin/AGENTS.md +6 -5
  38. package/dist/control-ui/assets/index-Dcwiyz6o.js.map +0 -1
@@ -248,10 +248,10 @@ export function formatAssistantErrorText(msg, opts) {
248
248
  return ("Context overflow: prompt too large for the model. " +
249
249
  "Try again with less input or a larger-context model.");
250
250
  }
251
- // Catch role ordering errors - including JSON-wrapped and "400" prefix variants
252
- if (/incorrect role information|roles must alternate|400.*role|"message".*role.*information/i.test(raw)) {
253
- return ("Message ordering conflict - please try again. " +
254
- "If this persists, use /new to start a fresh session.");
251
+ // Conversation history corruption role ordering, orphaned tool results, etc.
252
+ // The session is stuck; every subsequent message will fail until reset.
253
+ if (/incorrect role information|roles must alternate|400.*role|"message".*role.*information|tool_use_id|tool_result.*tool_use|tool_use.*tool_result/i.test(raw)) {
254
+ return "This conversation got into a bad state. Use /new to start fresh.";
255
255
  }
256
256
  const invalidRequest = raw.match(/"type":"invalid_request_error".*?"message":"([^"]+)"/);
257
257
  if (invalidRequest?.[1]) {
@@ -276,9 +276,8 @@ export function sanitizeUserFacingText(text) {
276
276
  const trimmed = stripped.trim();
277
277
  if (!trimmed)
278
278
  return stripped;
279
- if (/incorrect role information|roles must alternate/i.test(trimmed)) {
280
- return ("Message ordering conflict - please try again. " +
281
- "If this persists, use /new to start a fresh session.");
279
+ if (/incorrect role information|roles must alternate|tool_use_id|tool_result.*tool_use|tool_use.*tool_result/i.test(trimmed)) {
280
+ return "This conversation got into a bad state. Use /new to start fresh.";
282
281
  }
283
282
  if (isContextOverflowError(trimmed)) {
284
283
  return ("Context overflow: prompt too large for the model. " +
@@ -138,6 +138,10 @@ export async function compactEmbeddedPiSessionDirect(params) {
138
138
  sessionId: params.sessionId,
139
139
  warn: makeBootstrapWarn({ sessionLabel, warn: (message) => log.warn(message) }),
140
140
  });
141
+ // Extract skill base dirs for scoped skill_read tool
142
+ const skillBaseDirs = (params.skillsSnapshot?.resolvedSkills ?? [])
143
+ .map((s) => s.baseDir)
144
+ .filter(Boolean);
141
145
  const runAbortController = new AbortController();
142
146
  const toolsRaw = createTaskmasterCodingTools({
143
147
  exec: {
@@ -159,6 +163,7 @@ export async function compactEmbeddedPiSessionDirect(params) {
159
163
  modelProvider: model.provider,
160
164
  modelId,
161
165
  modelAuthMode: resolveModelAuthMode(model.provider, params.config),
166
+ skillBaseDirs,
162
167
  });
163
168
  const tools = sanitizeToolsForGoogle({ tools: toolsRaw, provider });
164
169
  logToolSchemasForGoogle({ tools, provider });
@@ -143,6 +143,10 @@ export async function runEmbeddedAttempt(params) {
143
143
  ? ["Reminder: commit your changes in this workspace after edits."]
144
144
  : undefined;
145
145
  const agentDir = params.agentDir ?? resolveTaskmasterAgentDir();
146
+ // Extract skill base dirs for scoped skill_read tool
147
+ const skillBaseDirs = (params.skillsSnapshot?.resolvedSkills ?? [])
148
+ .map((s) => s.baseDir)
149
+ .filter(Boolean);
146
150
  // Check if the model supports native image input
147
151
  const modelHasVision = params.model.input?.includes("image") ?? false;
148
152
  const toolsRaw = params.disableTools
@@ -175,6 +179,7 @@ export async function runEmbeddedAttempt(params) {
175
179
  hasRepliedRef: params.hasRepliedRef,
176
180
  modelHasVision,
177
181
  skipMessageDelivery: params.skipMessageDelivery,
182
+ skillBaseDirs,
178
183
  });
179
184
  const tools = sanitizeToolsForGoogle({ tools: toolsRaw, provider: params.provider });
180
185
  logToolSchemasForGoogle({ tools, provider: params.provider });
@@ -30,10 +30,10 @@ export function buildEmbeddedRunPayloads(params) {
30
30
  : null;
31
31
  const normalizedErrorText = errorText ? normalizeTextForComparison(errorText) : null;
32
32
  const genericErrorText = "The AI service returned an error. Please try again.";
33
- if (errorText) {
34
- // Suppress raw API errors for public-facing agents
35
- const displayError = params.suppressRawErrors ? genericErrorText : errorText;
36
- replyItems.push({ text: displayError, isError: true });
33
+ // Public-facing agents: silently swallow errors — no error text of any kind.
34
+ // Admin agent: show the formatted error from formatAssistantErrorText.
35
+ if (errorText && !params.suppressRawErrors) {
36
+ replyItems.push({ text: errorText, isError: true });
37
37
  }
38
38
  const inlineToolResults = params.inlineToolResultsAllowed && params.verboseLevel !== "off" && params.toolMetas.length > 0;
39
39
  if (inlineToolResults) {
@@ -331,13 +331,13 @@ export async function runEmbeddedPiAgent(params) {
331
331
  },
332
332
  };
333
333
  }
334
- // Handle role ordering errors with a user-friendly message
335
- if (/incorrect role information|roles must alternate/i.test(errorText)) {
334
+ // Handle session corruption errors (role ordering, orphaned tool results)
335
+ // Returning kind: "role_ordering" triggers auto-recovery (session reset) upstream.
336
+ if (/incorrect role information|roles must alternate|tool_use_id|tool_result.*tool_use|tool_use.*tool_result/i.test(errorText)) {
336
337
  return {
337
338
  payloads: [
338
339
  {
339
- text: "Message ordering conflict - please try again. " +
340
- "If this persists, use /new to start a fresh session.",
340
+ text: "This conversation got into a bad state — resetting now.",
341
341
  isError: true,
342
342
  },
343
343
  ],
@@ -218,6 +218,7 @@ export function createTaskmasterCodingTools(options) {
218
218
  modelHasVision: options?.modelHasVision,
219
219
  requesterAgentIdOverride: agentId,
220
220
  messageSkipDelivery: options?.skipMessageDelivery,
221
+ skillBaseDirs: options?.skillBaseDirs,
221
222
  }),
222
223
  ];
223
224
  const coreToolNames = new Set(tools
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { formatSkillsForPrompt, loadSkillsFromDir, } from "@mariozechner/pi-coding-agent";
3
+ import { loadSkillsFromDir } from "@mariozechner/pi-coding-agent";
4
4
  import { createSubsystemLogger } from "../../logging/subsystem.js";
5
5
  import { CONFIG_DIR, resolveUserPath } from "../../utils.js";
6
6
  import { resolveBundledSkillsDir } from "./bundled-dir.js";
@@ -10,6 +10,41 @@ import { resolvePluginSkillDirs } from "./plugin-skills.js";
10
10
  import { serializeByKey } from "./serialize.js";
11
11
  const fsp = fs.promises;
12
12
  const skillsLogger = createSubsystemLogger("skills");
13
+ function escapeXml(str) {
14
+ return str
15
+ .replace(/&/g, "&")
16
+ .replace(/</g, "&lt;")
17
+ .replace(/>/g, "&gt;")
18
+ .replace(/"/g, "&quot;")
19
+ .replace(/'/g, "&apos;");
20
+ }
21
+ /**
22
+ * Format skills for inclusion in a system prompt.
23
+ * Identical to the library's formatSkillsForPrompt except it references
24
+ * `skill_read` instead of `read` so agents denied group:fs can still
25
+ * load skill content.
26
+ */
27
+ function formatSkillsForPromptTaskmaster(skills) {
28
+ const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
29
+ if (visibleSkills.length === 0)
30
+ return "";
31
+ const lines = [
32
+ "\n\nThe following skills provide specialized instructions for specific tasks.",
33
+ "Use the skill_read tool to load a skill's file when the task matches its description.",
34
+ "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
35
+ "",
36
+ "<available_skills>",
37
+ ];
38
+ for (const skill of visibleSkills) {
39
+ lines.push(" <skill>");
40
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
41
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
42
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
43
+ lines.push(" </skill>");
44
+ }
45
+ lines.push("</available_skills>");
46
+ return lines.join("\n");
47
+ }
13
48
  const skillCommandDebugOnce = new Set();
14
49
  function debugSkillCommandOnce(messageKey, message, meta) {
15
50
  if (skillCommandDebugOnce.has(messageKey))
@@ -141,7 +176,9 @@ export function buildWorkspaceSkillSnapshot(workspaceDir, opts) {
141
176
  const promptEntries = eligible.filter((entry) => entry.invocation?.disableModelInvocation !== true);
142
177
  const resolvedSkills = promptEntries.map((entry) => entry.skill);
143
178
  const remoteNote = opts?.eligibility?.remote?.note?.trim();
144
- const prompt = [remoteNote, formatSkillsForPrompt(resolvedSkills)].filter(Boolean).join("\n");
179
+ const prompt = [remoteNote, formatSkillsForPromptTaskmaster(resolvedSkills)]
180
+ .filter(Boolean)
181
+ .join("\n");
145
182
  return {
146
183
  prompt,
147
184
  skills: eligible.map((entry) => ({
@@ -157,7 +194,7 @@ export function buildWorkspaceSkillsPrompt(workspaceDir, opts) {
157
194
  const eligible = filterSkillEntries(skillEntries, opts?.config, opts?.skillFilter, opts?.eligibility);
158
195
  const promptEntries = eligible.filter((entry) => entry.invocation?.disableModelInvocation !== true);
159
196
  const remoteNote = opts?.eligibility?.remote?.note?.trim();
160
- return [remoteNote, formatSkillsForPrompt(promptEntries.map((entry) => entry.skill))]
197
+ return [remoteNote, formatSkillsForPromptTaskmaster(promptEntries.map((entry) => entry.skill))]
161
198
  .filter(Boolean)
162
199
  .join("\n");
163
200
  }
@@ -132,9 +132,11 @@ export function buildAgentSystemPrompt(params) {
132
132
  sessions_spawn: "Spawn a sub-agent session",
133
133
  session_status: "Show a /status-equivalent status card (usage + time + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override",
134
134
  image: "Analyze an image with the configured image model",
135
+ skill_read: "Read files within skill directories (SKILL.md, references)",
135
136
  };
136
137
  const toolOrder = [
137
138
  "read",
139
+ "skill_read",
138
140
  "write",
139
141
  "edit",
140
142
  "apply_patch",
@@ -192,6 +194,10 @@ export function buildAgentSystemPrompt(params) {
192
194
  }
193
195
  const hasGateway = availableTools.has("gateway");
194
196
  const readToolName = resolveToolName("read");
197
+ // For skills section: prefer skill_read when agent has it but not read
198
+ const skillReadToolName = availableTools.has("skill_read") && !availableTools.has("read")
199
+ ? resolveToolName("skill_read")
200
+ : readToolName;
195
201
  const execToolName = resolveToolName("exec");
196
202
  const processToolName = resolveToolName("process");
197
203
  const extraSystemPrompt = params.extraSystemPrompt?.trim();
@@ -231,7 +237,7 @@ export function buildAgentSystemPrompt(params) {
231
237
  const skillsSection = buildSkillsSection({
232
238
  skillsPrompt,
233
239
  isMinimal,
234
- readToolName,
240
+ readToolName: skillReadToolName,
235
241
  });
236
242
  const memorySection = buildMemorySection({ isMinimal, availableTools });
237
243
  const docsSection = buildDocsSection({
@@ -272,11 +278,11 @@ export function buildAgentSystemPrompt(params) {
272
278
  "If a task is more complex or takes longer, spawn a sub-agent. It will do the work for you and ping you when it's done. You can always check up on it.",
273
279
  "",
274
280
  "## Tool Call Style",
275
- "Default: do not narrate routine, low-risk tool calls (just call the tool).",
276
- "Narrate only when it helps: multi-step work, complex/challenging problems, sensitive actions (e.g., deletions), or when the user explicitly asks.",
277
- "Keep narration brief and value-dense; avoid repeating obvious steps.",
278
- "Use plain human language for narration unless in a technical context.",
279
- "Never narrate internal housekeeping to the user: creating profiles, looking up records, checking memory, saving notes. Do those silently. The user sees the finished result, not the process.",
281
+ "Every text block you produce is delivered as a separate message. Unnecessary text blocks = unnecessary messages sent to the user.",
282
+ "Call tools silently by default. Do not announce what you are about to do, explain what you just did, or summarise completed steps.",
283
+ "If a task requires multiple tool calls, execute them without commentary. The user sees only your final response.",
284
+ "Narrate only when the user needs to know why there is a delay, or when an action has consequences they should be aware of (e.g. a deletion or sending on their behalf).",
285
+ "Never produce process narration like 'I'll check...', 'Let me look up...', 'Perfect — found it', 'Done I've sent...'. These become unwanted messages.",
280
286
  "",
281
287
  "## Taskmaster CLI Quick Reference",
282
288
  "Taskmaster is controlled via subcommands. Do not invent commands.",
@@ -8,6 +8,7 @@ import { createDocumentTool } from "./tools/document-tool.js";
8
8
  import { createGatewayTool } from "./tools/gateway-tool.js";
9
9
  import { createImageTool } from "./tools/image-tool.js";
10
10
  import { createMemoryGetTool, createMemorySaveMediaTool, createMemorySearchTool, createMemoryWriteTool, } from "./tools/memory-tool.js";
11
+ import { createMemoryReindexTool } from "./tools/memory-reindex-tool.js";
11
12
  import { createMessageTool } from "./tools/message-tool.js";
12
13
  import { createNodesTool } from "./tools/nodes-tool.js";
13
14
  import { createSessionStatusTool } from "./tools/session-status-tool.js";
@@ -23,6 +24,7 @@ import { createLicenseGenerateTool } from "./tools/license-tool.js";
23
24
  import { createCustomerLookupTool } from "./tools/customer-lookup-tool.js";
24
25
  import { createCustomerUpdateTool } from "./tools/customer-update-tool.js";
25
26
  import { createRelayMessageTool } from "./tools/relay-message-tool.js";
27
+ import { createSkillReadTool } from "./tools/skill-read-tool.js";
26
28
  export function createTaskmasterTools(options) {
27
29
  const imageTool = options?.agentDir?.trim()
28
30
  ? createImageTool({
@@ -127,6 +129,11 @@ export function createTaskmasterTools(options) {
127
129
  createCustomerUpdateTool(),
128
130
  createRelayMessageTool(),
129
131
  ];
132
+ // Add scoped skill_read tool when skill directories are provided
133
+ const skillDirs = (options?.skillBaseDirs ?? []).filter(Boolean);
134
+ if (skillDirs.length > 0) {
135
+ tools.push(createSkillReadTool({ allowedSkillDirs: skillDirs }));
136
+ }
130
137
  // Add memory tools (conditionally based on config)
131
138
  const memoryToolOptions = {
132
139
  config: options?.config,
@@ -136,6 +143,7 @@ export function createTaskmasterTools(options) {
136
143
  const memoryGetTool = createMemoryGetTool(memoryToolOptions);
137
144
  const memoryWriteTool = createMemoryWriteTool(memoryToolOptions);
138
145
  const memorySaveMediaTool = createMemorySaveMediaTool(memoryToolOptions);
146
+ const memoryReindexTool = createMemoryReindexTool(memoryToolOptions);
139
147
  if (memorySearchTool)
140
148
  tools.push(memorySearchTool);
141
149
  if (memoryGetTool)
@@ -144,6 +152,8 @@ export function createTaskmasterTools(options) {
144
152
  tools.push(memoryWriteTool);
145
153
  if (memorySaveMediaTool)
146
154
  tools.push(memorySaveMediaTool);
155
+ if (memoryReindexTool)
156
+ tools.push(memoryReindexTool);
147
157
  const pluginTools = resolvePluginTools({
148
158
  context: {
149
159
  config: options?.config,
@@ -40,6 +40,11 @@
40
40
  "title": "Read",
41
41
  "detailKeys": ["path"]
42
42
  },
43
+ "skill_read": {
44
+ "emoji": "📖",
45
+ "title": "Skill Read",
46
+ "detailKeys": ["path"]
47
+ },
43
48
  "write": {
44
49
  "emoji": "✍️",
45
50
  "title": "Write",
@@ -9,6 +9,7 @@ export const TOOL_GROUPS = {
9
9
  "memory_get",
10
10
  "memory_write",
11
11
  "memory_save_media",
12
+ "memory_reindex",
12
13
  "document_read",
13
14
  ],
14
15
  "group:web": ["web_search", "web_fetch"],
@@ -0,0 +1,67 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { getMemorySearchManager } from "../../memory/index.js";
3
+ import { resolveSessionAgentId } from "../agent-scope.js";
4
+ import { resolveMemorySearchConfig } from "../memory-search.js";
5
+ import { jsonResult, readStringParam } from "./common.js";
6
+ const MemoryReindexSchema = Type.Object({
7
+ action: Type.String({
8
+ description: "Action to perform: 'reindex' (force full reindex) or 'status' (return index stats)",
9
+ }),
10
+ });
11
+ export function createMemoryReindexTool(options) {
12
+ const cfg = options.config;
13
+ if (!cfg)
14
+ return null;
15
+ const agentId = resolveSessionAgentId({
16
+ sessionKey: options.agentSessionKey,
17
+ config: cfg,
18
+ });
19
+ if (!resolveMemorySearchConfig(cfg, agentId))
20
+ return null;
21
+ return {
22
+ label: "Memory Reindex",
23
+ name: "memory_reindex",
24
+ description: "Trigger a memory index rebuild or check index status. " +
25
+ "Use action='reindex' to force a full reindex of all memory files and sessions. " +
26
+ "Use action='status' to return current index stats (file counts, provider, sources).",
27
+ parameters: MemoryReindexSchema,
28
+ execute: async (_toolCallId, params) => {
29
+ const action = readStringParam(params, "action", { required: true });
30
+ const { manager, error } = await getMemorySearchManager({ cfg, agentId });
31
+ if (!manager) {
32
+ return jsonResult({ ok: false, error: error ?? "memory index unavailable" });
33
+ }
34
+ if (action === "status") {
35
+ const status = manager.status();
36
+ return jsonResult({
37
+ ok: true,
38
+ action: "status",
39
+ files: status.files,
40
+ chunks: status.chunks,
41
+ sources: status.sources,
42
+ sourceCounts: status.sourceCounts,
43
+ provider: status.provider,
44
+ model: status.model,
45
+ dirty: status.dirty,
46
+ });
47
+ }
48
+ // Default: reindex
49
+ try {
50
+ await manager.sync({ reason: "agent-tool", force: true });
51
+ const status = manager.status();
52
+ return jsonResult({
53
+ ok: true,
54
+ action: "reindex",
55
+ files: status.files,
56
+ chunks: status.chunks,
57
+ sources: status.sources,
58
+ sourceCounts: status.sourceCounts,
59
+ });
60
+ }
61
+ catch (err) {
62
+ const message = err instanceof Error ? err.message : String(err);
63
+ return jsonResult({ ok: false, action: "reindex", error: message });
64
+ }
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,75 @@
1
+ import fs from "node:fs/promises";
2
+ import { realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { Type } from "@sinclair/typebox";
5
+ import { readStringParam } from "./common.js";
6
+ const SkillReadSchema = Type.Object({
7
+ path: Type.String({ description: "Absolute or relative path to the file to read" }),
8
+ skill_dir: Type.Optional(Type.String({
9
+ description: "Skill directory for resolving relative paths. If omitted, path must be absolute.",
10
+ })),
11
+ });
12
+ /**
13
+ * Create a scoped file-read tool that only allows reading files within
14
+ * the provided skill directories. This gives agents access to skill
15
+ * content (SKILL.md, references/) without exposing the full filesystem.
16
+ */
17
+ export function createSkillReadTool(opts) {
18
+ // Pre-resolve allowed dirs at creation time (async realpath done at call time).
19
+ const allowedDirs = opts.allowedSkillDirs.filter(Boolean);
20
+ return {
21
+ label: "Skill Read",
22
+ name: "skill_read",
23
+ description: "Read a file within a skill directory (SKILL.md, references, etc.). " +
24
+ "Use this to load skill content on demand. " +
25
+ "Only files inside skill directories are accessible.",
26
+ parameters: SkillReadSchema,
27
+ execute: async (_toolCallId, args) => {
28
+ const params = args;
29
+ const rawPath = readStringParam(params, "path", { required: true });
30
+ const skillDir = readStringParam(params, "skill_dir");
31
+ // Resolve the target path
32
+ let targetPath;
33
+ if (path.isAbsolute(rawPath)) {
34
+ targetPath = path.resolve(rawPath);
35
+ }
36
+ else if (skillDir) {
37
+ targetPath = path.resolve(skillDir, rawPath);
38
+ }
39
+ else {
40
+ throw new Error("Relative path requires skill_dir. Provide an absolute path or include skill_dir.");
41
+ }
42
+ // Security: resolve symlinks on the target and check containment
43
+ let realTarget;
44
+ try {
45
+ realTarget = await realpath(targetPath);
46
+ }
47
+ catch {
48
+ throw new Error(`File not found: ${targetPath}`);
49
+ }
50
+ let allowed = false;
51
+ for (const dir of allowedDirs) {
52
+ let realDir;
53
+ try {
54
+ realDir = await realpath(dir);
55
+ }
56
+ catch {
57
+ continue;
58
+ }
59
+ const prefix = realDir.endsWith("/") ? realDir : `${realDir}/`;
60
+ if (realTarget === realDir || realTarget.startsWith(prefix)) {
61
+ allowed = true;
62
+ break;
63
+ }
64
+ }
65
+ if (!allowed) {
66
+ throw new Error(`Access denied: ${rawPath} is outside all skill directories.`);
67
+ }
68
+ const content = await fs.readFile(realTarget, "utf-8");
69
+ return {
70
+ content: [{ type: "text", text: content }],
71
+ details: { path: realTarget },
72
+ };
73
+ },
74
+ };
75
+ }
@@ -20,7 +20,7 @@ export const DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";
20
20
  export const DEFAULT_USER_FILENAME = "USER.md";
21
21
  export const DEFAULT_HEARTBEAT_FILENAME = "HEARTBEAT.md";
22
22
  export const DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
23
- const TEMPLATE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../docs/reference/templates");
23
+ const TEMPLATE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../templates/customer/agents/admin");
24
24
  function stripFrontMatter(content) {
25
25
  if (!content.startsWith("---"))
26
26
  return content;
@@ -39,7 +39,7 @@ async function loadTemplate(name) {
39
39
  return stripFrontMatter(content);
40
40
  }
41
41
  catch {
42
- throw new Error(`Missing workspace template: ${name} (${templatePath}). Ensure docs/reference/templates are packaged.`);
42
+ throw new Error(`Missing workspace template: ${name} (${templatePath}). Ensure templates/customer is packaged.`);
43
43
  }
44
44
  }
45
45
  async function writeFileIfMissing(filePath, content) {
@@ -1,5 +1,4 @@
1
1
  import crypto from "node:crypto";
2
- import fs from "node:fs";
3
2
  import { resolveAgentModelFallbacksOverride, resolveDefaultAgentId, } from "../../agents/agent-scope.js";
4
3
  import { runCliAgent } from "../../agents/cli-runner.js";
5
4
  import { getCliSessionId } from "../../agents/cli-session.js";
@@ -7,7 +6,7 @@ import { runWithModelFallback } from "../../agents/model-fallback.js";
7
6
  import { isCliProvider } from "../../agents/model-selection.js";
8
7
  import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js";
9
8
  import { isCompactionFailureError, isContextOverflowError, isLikelyContextOverflowError, sanitizeUserFacingText, } from "../../agents/pi-embedded-helpers.js";
10
- import { resolveAgentIdFromSessionKey, resolveGroupSessionKey, resolveSessionTranscriptPath, updateSessionStore, } from "../../config/sessions.js";
9
+ import { resolveAgentIdFromSessionKey, resolveGroupSessionKey, } from "../../config/sessions.js";
11
10
  import { logVerbose } from "../../globals.js";
12
11
  import { emitAgentEvent, registerAgentRunContext } from "../../infra/agent-events.js";
13
12
  import { defaultRuntime } from "../../runtime.js";
@@ -198,7 +197,9 @@ export async function runAgentTurnWithFallback(params) {
198
197
  ownerNumbers: params.followupRun.run.ownerNumbers,
199
198
  enforceFinalTag: resolveEnforceFinalTag(params.followupRun.run, provider),
200
199
  senderE164: params.sessionCtx.SenderE164,
201
- // Suppress raw API errors for non-default agents (public-facing)
200
+ // Suppress raw API errors for non-default agents (public-facing).
201
+ // Default (admin) agent gets formatted error text from formatAssistantErrorText
202
+ // which provides human-readable messages for known error types.
202
203
  suppressRawErrors: (() => {
203
204
  const cfg = params.followupRun.run.config;
204
205
  if (!cfg)
@@ -405,7 +406,7 @@ export async function runAgentTurnWithFallback(params) {
405
406
  return {
406
407
  kind: "final",
407
408
  payload: {
408
- text: "⚠️ Message ordering conflict. I've reset the conversation - please try again.",
409
+ text: "⚠️ Conversation got into a bad state. I've reset it - please try again.",
409
410
  },
410
411
  };
411
412
  }
@@ -417,7 +418,7 @@ export async function runAgentTurnWithFallback(params) {
417
418
  const isContextOverflow = isLikelyContextOverflowError(message);
418
419
  const isCompactionFailure = isCompactionFailureError(message);
419
420
  const isSessionCorruption = /function call turn comes immediately after/i.test(message);
420
- const isRoleOrderingError = /incorrect role information|roles must alternate/i.test(message);
421
+ const isRoleOrderingError = /incorrect role information|roles must alternate|tool_use_id|tool_result.*tool_use|tool_use.*tool_result/i.test(message);
421
422
  if (isCompactionFailure &&
422
423
  !didResetAfterCompactionFailure &&
423
424
  (await params.resetSessionAfterCompactionFailure(message))) {
@@ -435,52 +436,28 @@ export async function runAgentTurnWithFallback(params) {
435
436
  return {
436
437
  kind: "final",
437
438
  payload: {
438
- text: "⚠️ Message ordering conflict. I've reset the conversation - please try again.",
439
+ text: "⚠️ Conversation got into a bad state. I've reset it - please try again.",
439
440
  },
440
441
  };
441
442
  }
442
443
  }
443
444
  // Auto-recover from Gemini session corruption by resetting the session
444
- if (isSessionCorruption &&
445
- params.sessionKey &&
446
- params.activeSessionStore &&
447
- params.storePath) {
448
- const sessionKey = params.sessionKey;
449
- const corruptedSessionId = params.getActiveSessionEntry()?.sessionId;
450
- defaultRuntime.error(`Session history corrupted (Gemini function call ordering). Resetting session: ${params.sessionKey}`);
451
- try {
452
- // Delete transcript file if it exists
453
- if (corruptedSessionId) {
454
- const transcriptPath = resolveSessionTranscriptPath(corruptedSessionId);
455
- try {
456
- fs.unlinkSync(transcriptPath);
457
- }
458
- catch {
459
- // Ignore if file doesn't exist
460
- }
461
- }
462
- // Keep the in-memory snapshot consistent with the on-disk store reset.
463
- delete params.activeSessionStore[sessionKey];
464
- // Remove session entry from store using a fresh, locked snapshot.
465
- await updateSessionStore(params.storePath, (store) => {
466
- delete store[sessionKey];
467
- });
468
- }
469
- catch (cleanupErr) {
470
- defaultRuntime.error(`Failed to reset corrupted session ${params.sessionKey}: ${String(cleanupErr)}`);
445
+ if (isSessionCorruption) {
446
+ const didReset = await params.resetSessionAfterRoleOrderingConflict(message);
447
+ if (didReset) {
448
+ return {
449
+ kind: "final",
450
+ payload: {
451
+ text: "⚠️ Conversation got into a bad state. I've reset it - please try again.",
452
+ },
453
+ };
471
454
  }
472
- return {
473
- kind: "final",
474
- payload: {
475
- text: "⚠️ Session history was corrupted. I've reset the conversation - please try again!",
476
- },
477
- };
478
455
  }
479
456
  defaultRuntime.error(`Embedded agent failed before reply: ${message}`);
480
457
  const fallbackText = isContextOverflow
481
458
  ? "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model."
482
459
  : isRoleOrderingError
483
- ? "⚠️ Message ordering conflict - please try again. If this persists, use /new to start a fresh session."
460
+ ? "⚠️ Conversation got into a bad state. Use /new to start a fresh session."
484
461
  : "Sorry, I'm having trouble right now. Please try again in a few minutes.";
485
462
  return {
486
463
  kind: "final",