palmier 0.6.7 → 0.6.9

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 (94) hide show
  1. package/README.md +14 -0
  2. package/dist/agents/agent-instructions.md +7 -37
  3. package/dist/agents/agent.d.ts +2 -2
  4. package/dist/agents/aider.d.ts +1 -1
  5. package/dist/agents/aider.js +3 -6
  6. package/dist/agents/claude.d.ts +1 -1
  7. package/dist/agents/claude.js +3 -6
  8. package/dist/agents/cline.d.ts +1 -1
  9. package/dist/agents/cline.js +3 -6
  10. package/dist/agents/codex.d.ts +1 -1
  11. package/dist/agents/codex.js +3 -6
  12. package/dist/agents/copilot.d.ts +1 -1
  13. package/dist/agents/copilot.js +3 -6
  14. package/dist/agents/cursor.d.ts +1 -1
  15. package/dist/agents/cursor.js +3 -6
  16. package/dist/agents/deepagents.d.ts +1 -1
  17. package/dist/agents/deepagents.js +3 -6
  18. package/dist/agents/droid.d.ts +1 -1
  19. package/dist/agents/droid.js +3 -6
  20. package/dist/agents/gemini.d.ts +1 -1
  21. package/dist/agents/gemini.js +3 -6
  22. package/dist/agents/goose.d.ts +1 -1
  23. package/dist/agents/goose.js +3 -6
  24. package/dist/agents/hermes.d.ts +1 -1
  25. package/dist/agents/hermes.js +3 -6
  26. package/dist/agents/kimi.d.ts +1 -1
  27. package/dist/agents/kimi.js +3 -6
  28. package/dist/agents/kiro.d.ts +1 -1
  29. package/dist/agents/kiro.js +3 -6
  30. package/dist/agents/openclaw.d.ts +1 -1
  31. package/dist/agents/openclaw.js +3 -6
  32. package/dist/agents/opencode.d.ts +1 -1
  33. package/dist/agents/opencode.js +3 -6
  34. package/dist/agents/qoder.d.ts +1 -1
  35. package/dist/agents/qoder.js +3 -6
  36. package/dist/agents/qwen.d.ts +1 -1
  37. package/dist/agents/qwen.js +3 -6
  38. package/dist/agents/shared-prompt.d.ts +3 -2
  39. package/dist/agents/shared-prompt.js +6 -4
  40. package/dist/commands/run.js +3 -7
  41. package/dist/mcp-handler.js +1 -1
  42. package/dist/mcp-tools.d.ts +6 -1
  43. package/dist/mcp-tools.js +72 -6
  44. package/dist/pwa/assets/{index-DAI3J-jU.css → index-C6Lz09EY.css} +1 -1
  45. package/dist/pwa/assets/{index-RrJvjqz9.js → index-CZejk2al.js} +42 -42
  46. package/dist/pwa/assets/{web-EzNEHXEh.js → web-C48txJFl.js} +1 -1
  47. package/dist/pwa/assets/{web-DQteXlI7.js → web-zj8Blync.js} +1 -1
  48. package/dist/pwa/index.html +2 -2
  49. package/dist/pwa/service-worker.js +1 -1
  50. package/dist/rpc-handler.js +27 -68
  51. package/dist/spawn-command.js +3 -1
  52. package/dist/task.js +2 -3
  53. package/dist/transports/http-transport.js +4 -5
  54. package/dist/types.d.ts +0 -1
  55. package/package.json +2 -2
  56. package/palmier-server/README.md +1 -1
  57. package/palmier-server/pwa/src/App.css +9 -0
  58. package/palmier-server/pwa/src/components/TaskCard.tsx +36 -8
  59. package/palmier-server/pwa/src/components/TaskForm.tsx +63 -53
  60. package/palmier-server/pwa/src/constants.ts +1 -1
  61. package/palmier-server/spec.md +1 -1
  62. package/src/agents/agent-instructions.md +7 -37
  63. package/src/agents/agent.ts +2 -2
  64. package/src/agents/aider.ts +3 -6
  65. package/src/agents/claude.ts +3 -6
  66. package/src/agents/cline.ts +3 -6
  67. package/src/agents/codex.ts +3 -6
  68. package/src/agents/copilot.ts +3 -6
  69. package/src/agents/cursor.ts +3 -6
  70. package/src/agents/deepagents.ts +3 -6
  71. package/src/agents/droid.ts +3 -6
  72. package/src/agents/gemini.ts +3 -6
  73. package/src/agents/goose.ts +3 -6
  74. package/src/agents/hermes.ts +3 -6
  75. package/src/agents/kimi.ts +3 -6
  76. package/src/agents/kiro.ts +3 -6
  77. package/src/agents/openclaw.ts +3 -6
  78. package/src/agents/opencode.ts +3 -6
  79. package/src/agents/qoder.ts +3 -6
  80. package/src/agents/qwen.ts +3 -6
  81. package/src/agents/shared-prompt.ts +7 -4
  82. package/src/commands/run.ts +3 -7
  83. package/src/mcp-handler.ts +1 -1
  84. package/src/mcp-tools.ts +78 -7
  85. package/src/rpc-handler.ts +29 -72
  86. package/src/spawn-command.ts +3 -1
  87. package/src/task.ts +2 -3
  88. package/src/transports/http-transport.ts +4 -5
  89. package/src/types.ts +0 -1
  90. package/test/agent-instructions.test.ts +137 -9
  91. package/test/agent-output-parsing.test.ts +1 -0
  92. package/test/task-parsing.test.ts +3 -3
  93. package/dist/commands/plan-generation.md +0 -22
  94. package/src/commands/plan-generation.md +0 -22
package/src/mcp-tools.ts CHANGED
@@ -19,14 +19,18 @@ export interface ToolContext {
19
19
 
20
20
  export interface ToolDefinition {
21
21
  name: string;
22
- description: string;
22
+ /** First line is the summary (used as endpoint header). Remaining lines become bullet points in docs. */
23
+ description: string[];
23
24
  inputSchema: object;
24
25
  handler: (args: Record<string, unknown>, ctx: ToolContext) => Promise<unknown>;
25
26
  }
26
27
 
27
28
  const notifyTool: ToolDefinition = {
28
29
  name: "notify",
29
- description: "Send a push notification to the user's device.",
30
+ description: [
31
+ "Send a push notification to the user's device.",
32
+ 'Response: `{"ok": true}` on success.',
33
+ ],
30
34
  inputSchema: {
31
35
  type: "object",
32
36
  properties: {
@@ -55,7 +59,12 @@ const notifyTool: ToolDefinition = {
55
59
 
56
60
  const requestInputTool: ToolDefinition = {
57
61
  name: "request-input",
58
- description: "Request input from the user. The request blocks until the user responds.",
62
+ description: [
63
+ "Request input from the user.",
64
+ "The request blocks until the user responds.",
65
+ 'Response: `{"values": ["answer1", "answer2"]}` on success, or `{"aborted": true}` if the user declines.',
66
+ "When you need information from the user (credentials, answers to questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout, even in a non-interactive environment — use this endpoint instead.",
67
+ ],
59
68
  inputSchema: {
60
69
  type: "object",
61
70
  properties: {
@@ -87,18 +96,28 @@ const requestInputTool: ToolDefinition = {
87
96
  const response = await pendingPromise;
88
97
 
89
98
  if (response.length === 1 && response[0] === "aborted") {
90
- await ctx.publishEvent("_input", { event_type: "input-resolved", host_id: ctx.config.hostId, session_id: ctx.sessionId, status: "aborted" });
99
+ await ctx.publishEvent("_input", {
100
+ event_type: "input-resolved", host_id: ctx.config.hostId,
101
+ session_id: ctx.sessionId, status: "aborted",
102
+ });
91
103
  return { aborted: true };
92
104
  }
93
105
 
94
- await ctx.publishEvent("_input", { event_type: "input-resolved", host_id: ctx.config.hostId, session_id: ctx.sessionId, status: "provided" });
106
+ await ctx.publishEvent("_input", {
107
+ event_type: "input-resolved", host_id: ctx.config.hostId,
108
+ session_id: ctx.sessionId, status: "provided",
109
+ });
95
110
  return { values: response };
96
111
  },
97
112
  };
98
113
 
99
114
  const requestConfirmationTool: ToolDefinition = {
100
115
  name: "request-confirmation",
101
- description: "Request confirmation from the user. The request blocks until the user confirms or aborts.",
116
+ description: [
117
+ "Request confirmation from the user.",
118
+ "The request blocks until the user confirms or aborts.",
119
+ 'Response: `{"confirmed": true}` or `{"confirmed": false}`.',
120
+ ],
102
121
  inputSchema: {
103
122
  type: "object",
104
123
  properties: {
@@ -136,7 +155,12 @@ const requestConfirmationTool: ToolDefinition = {
136
155
 
137
156
  const deviceGeolocationTool: ToolDefinition = {
138
157
  name: "device-geolocation",
139
- description: "Get the GPS location of the user's mobile device. Blocks until the device responds (up to 30 seconds).",
158
+ description: [
159
+ "Get the GPS location of the user's mobile device.",
160
+ "When you need the user's real-time location, use this endpoint.",
161
+ "Blocks until the device responds (up to 30 seconds).",
162
+ 'Response: `{"latitude": ..., "longitude": ..., "accuracy": ..., "timestamp": ...}` on success, or `{"error": "..."}` on failure.',
163
+ ],
140
164
  inputSchema: {
141
165
  type: "object",
142
166
  properties: {},
@@ -180,3 +204,50 @@ const deviceGeolocationTool: ToolDefinition = {
180
204
 
181
205
  export const agentTools: ToolDefinition[] = [notifyTool, requestInputTool, requestConfirmationTool, deviceGeolocationTool];
182
206
  export const agentToolMap = new Map<string, ToolDefinition>(agentTools.map((t) => [t.name, t]));
207
+
208
+ /**
209
+ * Generate the HTTP Endpoints markdown section for agent-instructions.md from the tool registry.
210
+ */
211
+ export function generateEndpointDocs(port: number, taskId: string, tools: ToolDefinition[] = agentTools): string {
212
+ const baseUrl = `http://localhost:${port}`;
213
+ const lines: string[] = [
214
+ `The following HTTP endpoints are available during task execution. Use curl to call them.`,
215
+ "",
216
+ ];
217
+
218
+ for (const tool of tools) {
219
+ const schema = tool.inputSchema as { properties?: Record<string, { type?: string; description?: string; items?: { type?: string } }>; required?: string[] };
220
+ const props = schema.properties ?? {};
221
+ const required = new Set(schema.required ?? []);
222
+
223
+ // Build example JSON (body only, no taskId)
224
+ const example: Record<string, unknown> = {};
225
+ for (const [key, prop] of Object.entries(props)) {
226
+ if (prop.type === "array") example[key] = ["..."];
227
+ else example[key] = "...";
228
+ }
229
+
230
+ const queryUrl = `${baseUrl}/${tool.name}?taskId=${taskId}`;
231
+ const [header, ...details] = tool.description;
232
+
233
+ lines.push(`**\`POST ${queryUrl}\`** — ${header}`);
234
+ if (Object.keys(example).length > 0) {
235
+ lines.push("```json");
236
+ lines.push(JSON.stringify(example));
237
+ lines.push("```");
238
+ }
239
+ for (const [key, prop] of Object.entries(props)) {
240
+ const req = required.has(key) ? "required" : "optional";
241
+ let typeStr = prop.type ?? "unknown";
242
+ if (prop.type === "array" && prop.items?.type) typeStr = `${prop.items.type} array`;
243
+ lines.push(`- \`${key}\` (${req}, ${typeStr}): ${prop.description ?? ""}`);
244
+ }
245
+ for (const detail of details) {
246
+ lines.push(`- ${detail}`);
247
+ }
248
+
249
+ lines.push("");
250
+ }
251
+
252
+ return lines.join("\n").trimEnd();
253
+ }
@@ -1,9 +1,7 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
- import { fileURLToPath } from "url";
5
4
  import { spawn, type ChildProcess } from "child_process";
6
- import { parse as parseYaml } from "yaml";
7
5
  import { type NatsConnection } from "nats";
8
6
  import { listTasks, parseTaskFile, writeTaskFile, getTaskDir, readTaskStatus, writeTaskStatus, readHistory, deleteHistoryEntry, appendTaskList, removeFromTaskList, appendHistory, createRunDir, appendRunMessage, getRunDir } from "./task.js";
9
7
  import { resolvePending, getPending } from "./pending-requests.js";
@@ -18,13 +16,6 @@ import { currentVersion, performUpdate } from "./update-checker.js";
18
16
  import { parseReportFiles, parseTaskOutcome, stripPalmierMarkers } from "./commands/run.js";
19
17
  import type { HostConfig, ParsedTask, RpcMessage, ConversationMessage } from "./types.js";
20
18
 
21
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
-
23
- const PLAN_GENERATION_PROMPT = fs.readFileSync(
24
- path.join(__dirname, "commands", "plan-generation.md"),
25
- "utf-8",
26
- );
27
-
28
19
  /**
29
20
  * Parse RESULT frontmatter and conversation messages.
30
21
  */
@@ -115,40 +106,30 @@ function parseAttr(attrs: string, name: string): string | undefined {
115
106
  }
116
107
 
117
108
  /**
118
- * Run plan generation for a task prompt using the given agent.
119
- * Returns the generated plan body and task name.
109
+ * Generate a concise task name from a user prompt using the given agent.
110
+ * Falls back to the raw prompt on failure.
120
111
  */
121
- async function generatePlan(
112
+ async function generateName(
122
113
  projectRoot: string,
123
114
  userPrompt: string,
124
115
  agentName: string,
125
- ): Promise<{ name: string; body: string }> {
126
- const fullPrompt = PLAN_GENERATION_PROMPT + userPrompt;
127
- const planAgent = getAgent(agentName);
128
- const { command, args, stdin, env: agentEnv } = planAgent.getPlanGenerationCommandLine(fullPrompt);
129
- console.log(`[generatePlan] Running: ${command} ${args.join(" ")}`);
130
-
131
- const { output } = await spawnCommand(command, args, {
132
- cwd: projectRoot,
133
- timeout: 120_000,
134
- stdin,
135
- ...(agentEnv ? { env: agentEnv } : {}),
136
- });
137
-
138
- let name = "";
139
- const trimmed = output.trim();
140
- let body = trimmed;
141
- const fmMatch = trimmed.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
142
- if (fmMatch) {
143
- try {
144
- const fm = parseYaml(fmMatch[1]) as { task_name?: string };
145
- name = fm.task_name ?? "";
146
- } catch {
147
- // If frontmatter parsing fails, treat entire output as body
148
- }
149
- body = fmMatch[2].trimStart();
116
+ ): Promise<string> {
117
+ const prompt = `Generate a concise 3-6 word name for this task. Reply with ONLY the name, nothing else.\n\nTask: ${userPrompt}`;
118
+ const agent = getAgent(agentName);
119
+ const { command, args, stdin, env: agentEnv } = agent.getPromptCommandLine(prompt);
120
+
121
+ try {
122
+ const { output } = await spawnCommand(command, args, {
123
+ cwd: projectRoot,
124
+ timeout: 30_000,
125
+ stdin,
126
+ ...(agentEnv ? { env: agentEnv } : {}),
127
+ });
128
+ const name = output.trim().replace(/^["']|["']$/g, "").slice(0, 80);
129
+ return name || userPrompt;
130
+ } catch {
131
+ return userPrompt;
150
132
  }
151
- return { name, body };
152
133
  }
153
134
 
154
135
  /** Active follow-up child processes, keyed by "taskId:runId". */
@@ -164,7 +145,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
164
145
  const pending = getPending(task.frontmatter.id);
165
146
  return {
166
147
  ...task.frontmatter,
167
- body: task.body,
168
148
  status: status ? {
169
149
  ...status,
170
150
  ...(pending?.type === "permission" ? { pending_permission: pending.params } : {}),
@@ -216,25 +196,13 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
216
196
  command?: string;
217
197
  };
218
198
 
219
- // Only generate a plan for longer prompts that benefit from it
220
- let name = "";
221
- let body = "";
222
- if (params.user_prompt.length <= 50) {
223
- name = params.user_prompt;
224
- } else {
225
- try {
226
- const plan = await generatePlan(config.projectRoot, params.user_prompt, params.agent);
227
- name = plan.name;
228
- body = plan.body;
229
- } catch (err: unknown) {
230
- const error = err as { stdout?: string; stderr?: string };
231
- return { error: "plan generation failed", stdout: error.stdout, stderr: error.stderr };
232
- }
233
- }
199
+ const name = params.user_prompt.length <= 50
200
+ ? params.user_prompt
201
+ : await generateName(config.projectRoot, params.user_prompt, params.agent);
234
202
 
235
203
  const id = randomUUID();
236
204
  const taskDir = getTaskDir(config.projectRoot, id);
237
- const task = {
205
+ const task: ParsedTask = {
238
206
  frontmatter: {
239
207
  id,
240
208
  name,
@@ -247,7 +215,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
247
215
  ...(params.foreground_mode ? { foreground_mode: true } : {}),
248
216
  ...(params.command ? { command: params.command } : {}),
249
217
  },
250
- body,
251
218
  };
252
219
 
253
220
  writeTaskFile(taskDir, task);
@@ -273,10 +240,9 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
273
240
  const taskDir = getTaskDir(config.projectRoot, params.id);
274
241
  const existing = parseTaskFile(taskDir);
275
242
 
276
- // Detect whether plan needs regeneration
243
+ // Detect whether name needs regeneration
277
244
  const promptChanged = params.user_prompt !== undefined && params.user_prompt !== existing.frontmatter.user_prompt;
278
245
  const agentChanged = params.agent !== undefined && params.agent !== existing.frontmatter.agent;
279
- const needsRegeneration = promptChanged || agentChanged || !existing.body;
280
246
 
281
247
  // Merge updates
282
248
  if (params.user_prompt !== undefined) existing.frontmatter.user_prompt = params.user_prompt;
@@ -298,19 +264,11 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
298
264
  }
299
265
  }
300
266
 
301
- // Regenerate plan if needed (only for longer prompts)
302
- if (existing.frontmatter.user_prompt.length <= 50) {
303
- existing.frontmatter.name = existing.frontmatter.user_prompt;
304
- existing.body = "";
305
- } else if (needsRegeneration) {
306
- try {
307
- const plan = await generatePlan(config.projectRoot, existing.frontmatter.user_prompt, existing.frontmatter.agent);
308
- existing.frontmatter.name = plan.name;
309
- existing.body = plan.body;
310
- } catch (err: unknown) {
311
- const error = err as { stdout?: string; stderr?: string };
312
- return { error: "plan generation failed", stdout: error.stdout, stderr: error.stderr };
313
- }
267
+ // Regenerate name when prompt or agent changes
268
+ if (promptChanged || agentChanged) {
269
+ existing.frontmatter.name = existing.frontmatter.user_prompt.length <= 50
270
+ ? existing.frontmatter.user_prompt
271
+ : await generateName(config.projectRoot, existing.frontmatter.user_prompt, existing.frontmatter.agent);
314
272
  }
315
273
 
316
274
  writeTaskFile(taskDir, existing);
@@ -357,7 +315,6 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
357
315
  ...(params.foreground_mode ? { foreground_mode: true } : {}),
358
316
  ...(params.command ? { command: params.command } : {}),
359
317
  },
360
- body: "",
361
318
  };
362
319
 
363
320
  writeTaskFile(taskDir, task);
@@ -89,8 +89,10 @@ export function spawnCommand(
89
89
  const finalArgs = process.platform === "win32"
90
90
  ? args.map((a) => a.replace(/[\r\n]+/g, " "))
91
91
  : args;
92
+ const truncate = (s: string, max = 100) => s.length > max ? s.slice(0, max) + "..." : s;
93
+ const displayArgs = finalArgs.map((arg) => truncate(arg));
92
94
 
93
- // console.log(`[spawn] ${command} ${finalArgs.join(" ")}`);
95
+ console.log(`[spawn] ${command} ${displayArgs.join(" ")}`);
94
96
 
95
97
  const child = crossSpawn(command, finalArgs, {
96
98
  cwd: opts.cwd,
package/src/task.ts CHANGED
@@ -29,7 +29,6 @@ export function parseTaskContent(content: string): ParsedTask {
29
29
  }
30
30
 
31
31
  const frontmatter = parseYaml(match[1]) as TaskFrontmatter;
32
- const body = (match[2] || "").trim();
33
32
 
34
33
  if (!frontmatter.id) {
35
34
  throw new Error("TASK.md frontmatter must include at least: id");
@@ -39,7 +38,7 @@ export function parseTaskContent(content: string): ParsedTask {
39
38
  frontmatter.agent ??= "claude";
40
39
  frontmatter.triggers_enabled ??= true;
41
40
 
42
- return { frontmatter, body };
41
+ return { frontmatter };
43
42
  }
44
43
 
45
44
  /**
@@ -50,7 +49,7 @@ export function writeTaskFile(taskDir: string, task: ParsedTask): void {
50
49
  fs.mkdirSync(taskDir, { recursive: true });
51
50
 
52
51
  const yamlStr = stringifyYaml(task.frontmatter).trim();
53
- const content = `---\n${yamlStr}\n---\n${task.body}\n`;
52
+ const content = `---\n${yamlStr}\n---\n`;
54
53
 
55
54
  const filePath = path.join(taskDir, "TASK.md");
56
55
  fs.writeFileSync(filePath, content, "utf-8");
@@ -195,11 +195,9 @@ export async function startHttpTransport(
195
195
  if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
196
196
  const tool = agentToolMap.get(pathname.slice(1))!;
197
197
  try {
198
- const body = await readBody(req);
199
- const args = body.trim() ? JSON.parse(body) : {};
200
- const { taskId } = args as { taskId?: string };
198
+ const taskId = url.searchParams.get("taskId");
201
199
  if (!taskId) {
202
- sendJson(res, 400, { error: "taskId is required" });
200
+ sendJson(res, 400, { error: "taskId query parameter is required" });
203
201
  return;
204
202
  }
205
203
  const taskDir = getTaskDir(config.projectRoot, taskId);
@@ -207,7 +205,8 @@ export async function startHttpTransport(
207
205
  sendJson(res, 404, { error: `Task not found: ${taskId}` });
208
206
  return;
209
207
  }
210
- delete args.taskId;
208
+ const body = await readBody(req);
209
+ const args = body.trim() ? JSON.parse(body) : {};
211
210
  const ctx = makeToolContext(taskId);
212
211
  console.log(`[mcp] REST [${taskId.slice(0, 8)}] ${tool.name}`);
213
212
  const result = await tool.handler(args, ctx);
package/src/types.ts CHANGED
@@ -36,7 +36,6 @@ export interface Trigger {
36
36
 
37
37
  export interface ParsedTask {
38
38
  frontmatter: TaskFrontmatter;
39
- body: string;
40
39
  }
41
40
 
42
41
  /**
@@ -3,17 +3,57 @@ import assert from "node:assert/strict";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import { fileURLToPath } from "url";
6
+ import { generateEndpointDocs, type ToolDefinition } from "../src/mcp-tools.js";
6
7
 
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
9
  const templatePath = path.join(__dirname, "..", "src", "agents", "agent-instructions.md");
9
10
  const template = fs.readFileSync(templatePath, "utf-8");
10
11
 
12
+ /** Mock tools with a known, stable shape for testing */
13
+ const mockTools: ToolDefinition[] = [
14
+ {
15
+ name: "mock-action",
16
+ description: [
17
+ "Perform a mock action.",
18
+ 'Response: `{"ok": true}` on success.',
19
+ ],
20
+ inputSchema: {
21
+ type: "object",
22
+ properties: {
23
+ title: { type: "string", description: "Action title" },
24
+ detail: { type: "string", description: "Optional detail" },
25
+ },
26
+ required: ["title"],
27
+ },
28
+ handler: async () => ({ ok: true }),
29
+ },
30
+ {
31
+ name: "mock-query",
32
+ description: [
33
+ "Query mock data from the device.",
34
+ "Blocks until the device responds.",
35
+ 'Response: `{"data": ...}` on success.',
36
+ ],
37
+ inputSchema: {
38
+ type: "object",
39
+ properties: {
40
+ tags: {
41
+ type: "array",
42
+ items: { type: "string" },
43
+ description: "Filter tags",
44
+ },
45
+ },
46
+ },
47
+ handler: async () => ({ data: [] }),
48
+ },
49
+ ];
50
+
11
51
  /** Minimal replica of getAgentInstructions that doesn't need host.json */
12
- function buildInstructions(taskId: string, skipPermissions?: boolean): string {
52
+ function buildInstructions(taskId: string, opts?: { skipPermissions?: boolean }): string {
13
53
  let instructions = template
14
- .replace(/\{\{PORT\}\}/g, "9966")
15
- .replace(/\{\{TASK_ID\}\}/g, taskId);
16
- if (skipPermissions) {
54
+ .replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(9966, taskId, mockTools))
55
+ .replace(/\{\{TASK_DESCRIPTION\}\}/g, "Test task prompt");
56
+ if (opts?.skipPermissions) {
17
57
  instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
18
58
  }
19
59
  return instructions;
@@ -27,22 +67,110 @@ describe("getAgentInstructions", () => {
27
67
  });
28
68
 
29
69
  it("strips Permissions section when skipPermissions is true", () => {
30
- const result = buildInstructions("test-task-id", true);
70
+ const result = buildInstructions("test-task-id", { skipPermissions: true });
31
71
  assert.doesNotMatch(result, /## Permissions/);
32
72
  assert.doesNotMatch(result, /PALMIER_PERMISSION/);
33
73
  });
34
74
 
35
75
  it("preserves other sections when Permissions is stripped", () => {
36
- const result = buildInstructions("test-task-id", true);
76
+ const result = buildInstructions("test-task-id", { skipPermissions: true });
37
77
  assert.match(result, /## Reporting Output/);
38
78
  assert.match(result, /## Completion/);
39
79
  assert.match(result, /## HTTP Endpoints/);
40
80
  });
41
81
 
42
- it("replaces template variables", () => {
82
+ it("replaces all template variables", () => {
83
+ const result = buildInstructions("my-task-123");
84
+ assert.doesNotMatch(result, /\{\{ENDPOINT_DOCS\}\}/);
85
+ assert.doesNotMatch(result, /\{\{TASK_DESCRIPTION\}\}/);
86
+ });
87
+
88
+ it("includes task ID in endpoint examples", () => {
43
89
  const result = buildInstructions("my-task-123");
44
90
  assert.match(result, /my-task-123/);
45
- assert.doesNotMatch(result, /\{\{TASK_ID\}\}/);
46
- assert.doesNotMatch(result, /\{\{PORT\}\}/);
91
+ });
92
+
93
+ it("includes port in endpoint URL", () => {
94
+ const result = buildInstructions("test");
95
+ assert.match(result, /localhost:9966/);
96
+ });
97
+
98
+ it("includes task description", () => {
99
+ const result = buildInstructions("test");
100
+ assert.match(result, /Test task prompt/);
101
+ });
102
+
103
+ });
104
+
105
+
106
+ describe("generateEndpointDocs", () => {
107
+ const docs = generateEndpointDocs(9966, "test-id", mockTools);
108
+
109
+ it("matches expected full output", () => {
110
+ const expected = [
111
+ "The following HTTP endpoints are available during task execution. Use curl to call them.",
112
+ "",
113
+ "**`POST http://localhost:9966/mock-action?taskId=test-id`** — Perform a mock action.",
114
+ "```json",
115
+ '{"title":"...","detail":"..."}',
116
+ "```",
117
+ "- `title` (required, string): Action title",
118
+ "- `detail` (optional, string): Optional detail",
119
+ '- Response: `{"ok": true}` on success.',
120
+ "",
121
+ "**`POST http://localhost:9966/mock-query?taskId=test-id`** — Query mock data from the device.",
122
+ "```json",
123
+ '{"tags":["..."]}',
124
+ "```",
125
+ "- `tags` (optional, string array): Filter tags",
126
+ "- Blocks until the device responds.",
127
+ '- Response: `{"data": ...}` on success.',
128
+ ].join("\n");
129
+ assert.equal(docs, expected);
130
+ });
131
+
132
+ it("generates docs for all provided tools", () => {
133
+ for (const tool of mockTools) {
134
+ assert.match(docs, new RegExp(`POST http://localhost:9966/${tool.name}\\?taskId=`), `Missing endpoint for ${tool.name}`);
135
+ }
136
+ });
137
+
138
+ it("includes taskId parameter for every endpoint", () => {
139
+ const endpointBlocks = docs.split("**`POST");
140
+ // First element is the header, skip it
141
+ for (let i = 1; i < endpointBlocks.length; i++) {
142
+ assert.match(endpointBlocks[i], /taskId/, `Missing taskId in endpoint block ${i}`);
143
+ }
144
+ });
145
+
146
+ it("includes port in the header", () => {
147
+ assert.match(docs, /localhost:9966/);
148
+ });
149
+
150
+ it("includes task ID in query parameters", () => {
151
+ assert.match(docs, /taskId=test-id/);
152
+ });
153
+
154
+ it("includes response descriptions", () => {
155
+ for (const tool of mockTools) {
156
+ if (tool.description.length > 1) {
157
+ assert.match(docs, /Response:/, `Missing response description for ${tool.name}`);
158
+ }
159
+ }
160
+ });
161
+
162
+ it("marks required and optional parameters correctly", () => {
163
+ assert.match(docs, /\(required, string\)/);
164
+ // "detail" has no required entry, so it should be optional
165
+ assert.match(docs, /\(optional, string\)/);
166
+ });
167
+
168
+ it("handles array-type parameters", () => {
169
+ assert.match(docs, /\(optional, string array\)/);
170
+ assert.match(docs, /Filter tags/);
171
+ });
172
+
173
+ it("renders multi-line descriptions as bullet points", () => {
174
+ assert.match(docs, /- Blocks until the device responds\./);
47
175
  });
48
176
  });
@@ -71,3 +71,4 @@ describe("parsePermissions", () => {
71
71
  });
72
72
  });
73
73
 
74
+
@@ -19,7 +19,7 @@ This is the task body.`;
19
19
  assert.equal(result.frontmatter.id, "abc123");
20
20
  assert.equal(result.frontmatter.name, "Test Task");
21
21
  assert.equal(result.frontmatter.agent, "claude");
22
- assert.equal(result.body, "This is the task body.");
22
+ assert.equal(result.frontmatter.user_prompt, "Do something");
23
23
  });
24
24
 
25
25
  it("defaults agent to claude when not specified", () => {
@@ -68,7 +68,7 @@ requires_confirmation: false
68
68
  assert.throws(() => parseTaskContent("---\nname: test\n---\n"), /must include at least: id/);
69
69
  });
70
70
 
71
- it("handles empty body", () => {
71
+ it("handles empty body gracefully", () => {
72
72
  const content = `---
73
73
  id: abc123
74
74
  user_prompt: test
@@ -78,6 +78,6 @@ requires_confirmation: false
78
78
  ---`;
79
79
 
80
80
  const result = parseTaskContent(content);
81
- assert.equal(result.body, "");
81
+ assert.equal(result.frontmatter.id, "abc123");
82
82
  });
83
83
  });
@@ -1,22 +0,0 @@
1
- You are a task planning assistant. Given a task description, produce a Markdown execution plan for an AI agent to follow. Do not execute any part of the plan yourself.
2
-
3
- ## Output Format
4
-
5
- Start with a YAML frontmatter block (no code fences), then the plan body:
6
-
7
- ---
8
- task_name: <concise label, 3-6 words>
9
- ---
10
-
11
- <plan body>
12
-
13
- ## Plan Body Guidelines
14
-
15
- - Write a numbered sequence of concrete, actionable steps.
16
- - If the task produces formatted output (report, email, summary, etc.), specify the structure, sections, and tone.
17
- - When a step requires user input, simply state what information is needed from the user. Do **not** specify how to obtain it — the agent has its own tool for requesting user input.
18
- - Preserve relative time expressions (e.g., "today", "yesterday", "last week") exactly as written — do **not** resolve them to specific dates. The plan may be executed on a different day than it was generated.
19
- - If the task involves opening a web browser or application, include a final step to close it before finishing.
20
-
21
- ## Task Description
22
-
@@ -1,22 +0,0 @@
1
- You are a task planning assistant. Given a task description, produce a Markdown execution plan for an AI agent to follow. Do not execute any part of the plan yourself.
2
-
3
- ## Output Format
4
-
5
- Start with a YAML frontmatter block (no code fences), then the plan body:
6
-
7
- ---
8
- task_name: <concise label, 3-6 words>
9
- ---
10
-
11
- <plan body>
12
-
13
- ## Plan Body Guidelines
14
-
15
- - Write a numbered sequence of concrete, actionable steps.
16
- - If the task produces formatted output (report, email, summary, etc.), specify the structure, sections, and tone.
17
- - When a step requires user input, simply state what information is needed from the user. Do **not** specify how to obtain it — the agent has its own tool for requesting user input.
18
- - Preserve relative time expressions (e.g., "today", "yesterday", "last week") exactly as written — do **not** resolve them to specific dates. The plan may be executed on a different day than it was generated.
19
- - If the task involves opening a web browser or application, include a final step to close it before finishing.
20
-
21
- ## Task Description
22
-