palmier 0.6.7 → 0.6.8

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 (68) hide show
  1. package/README.md +14 -0
  2. package/dist/agents/agent-instructions.md +7 -37
  3. package/dist/agents/aider.js +1 -1
  4. package/dist/agents/claude.js +1 -1
  5. package/dist/agents/cline.js +1 -1
  6. package/dist/agents/codex.js +1 -1
  7. package/dist/agents/copilot.js +1 -1
  8. package/dist/agents/cursor.js +1 -1
  9. package/dist/agents/deepagents.js +1 -1
  10. package/dist/agents/droid.js +1 -1
  11. package/dist/agents/gemini.js +1 -1
  12. package/dist/agents/goose.js +1 -1
  13. package/dist/agents/hermes.js +1 -1
  14. package/dist/agents/kimi.js +1 -1
  15. package/dist/agents/kiro.js +1 -1
  16. package/dist/agents/openclaw.js +1 -1
  17. package/dist/agents/opencode.js +1 -1
  18. package/dist/agents/qoder.js +1 -1
  19. package/dist/agents/qwen.js +1 -1
  20. package/dist/agents/shared-prompt.d.ts +3 -2
  21. package/dist/agents/shared-prompt.js +6 -4
  22. package/dist/commands/run.js +2 -5
  23. package/dist/mcp-handler.js +1 -1
  24. package/dist/mcp-tools.d.ts +6 -1
  25. package/dist/mcp-tools.js +72 -6
  26. package/dist/pwa/assets/{index-DAI3J-jU.css → index-C6Lz09EY.css} +1 -1
  27. package/dist/pwa/assets/{index-RrJvjqz9.js → index-C8vJwUNi.js} +42 -42
  28. package/dist/pwa/assets/{web-DQteXlI7.js → web-6UChJFov.js} +1 -1
  29. package/dist/pwa/assets/{web-EzNEHXEh.js → web-NxTETXZK.js} +1 -1
  30. package/dist/pwa/index.html +2 -2
  31. package/dist/pwa/service-worker.js +1 -1
  32. package/dist/rpc-handler.js +0 -1
  33. package/dist/spawn-command.js +3 -1
  34. package/dist/transports/http-transport.js +4 -5
  35. package/package.json +1 -1
  36. package/palmier-server/README.md +1 -1
  37. package/palmier-server/pwa/src/App.css +9 -0
  38. package/palmier-server/pwa/src/components/TaskCard.tsx +36 -8
  39. package/palmier-server/pwa/src/components/TaskForm.tsx +63 -53
  40. package/palmier-server/pwa/src/constants.ts +1 -1
  41. package/palmier-server/spec.md +1 -1
  42. package/src/agents/agent-instructions.md +7 -37
  43. package/src/agents/aider.ts +1 -1
  44. package/src/agents/claude.ts +1 -1
  45. package/src/agents/cline.ts +1 -1
  46. package/src/agents/codex.ts +1 -1
  47. package/src/agents/copilot.ts +1 -1
  48. package/src/agents/cursor.ts +1 -1
  49. package/src/agents/deepagents.ts +1 -1
  50. package/src/agents/droid.ts +1 -1
  51. package/src/agents/gemini.ts +1 -1
  52. package/src/agents/goose.ts +1 -1
  53. package/src/agents/hermes.ts +1 -1
  54. package/src/agents/kimi.ts +1 -1
  55. package/src/agents/kiro.ts +1 -1
  56. package/src/agents/openclaw.ts +1 -1
  57. package/src/agents/opencode.ts +1 -1
  58. package/src/agents/qoder.ts +1 -1
  59. package/src/agents/qwen.ts +1 -1
  60. package/src/agents/shared-prompt.ts +7 -4
  61. package/src/commands/run.ts +2 -5
  62. package/src/mcp-handler.ts +1 -1
  63. package/src/mcp-tools.ts +78 -7
  64. package/src/rpc-handler.ts +0 -1
  65. package/src/spawn-command.ts +3 -1
  66. package/src/transports/http-transport.ts +4 -5
  67. package/test/agent-instructions.test.ts +68 -5
  68. package/test/fixtures/agent-instructions-snapshot.md +58 -0
@@ -15,7 +15,7 @@ export class KimiAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = [];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class Kiro implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = [];
20
20
 
21
21
  if (yolo) {
@@ -14,7 +14,7 @@ export class OpenClawAgent implements AgentTool {
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
16
16
  const yolo = extraPermissions === "yolo";
17
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
18
18
  // OpenClaw does not support stdin as prompt.
19
19
  const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
20
20
 
@@ -15,7 +15,7 @@ export class OpenCodeAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["run"];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class Qoder implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = [];
20
20
 
21
21
  if (yolo) {
@@ -15,7 +15,7 @@ export class QwenAgent implements AgentTool {
15
15
 
16
16
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
17
  const yolo = extraPermissions === "yolo";
18
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
18
+ const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
19
19
  const args = ["--approval-mode", yolo ? "yolo" : "auto-edit"];
20
20
 
21
21
  if (followupPrompt) { args.push("-c"); }
@@ -2,6 +2,8 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  import { loadConfig } from "../config.js";
5
+ import { generateEndpointDocs } from "../mcp-tools.js";
6
+ import type { ParsedTask } from "../types.js";
5
7
 
6
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
9
 
@@ -11,13 +13,14 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
11
13
  );
12
14
 
13
15
  /**
14
- * Agent instructions with the serve daemon's HTTP port and task ID baked in.
16
+ * Build the full agent prompt: instructions + endpoint docs + task description.
15
17
  */
16
- export function getAgentInstructions(taskId: string, skipPermissions?: boolean): string {
18
+ export function getAgentInstructions(task: ParsedTask, skipPermissions?: boolean): string {
17
19
  const port = loadConfig().httpPort ?? 9966;
20
+ const taskDescription = task.body || task.frontmatter.user_prompt;
18
21
  let instructions = AGENT_INSTRUCTIONS_TEMPLATE
19
- .replace(/\{\{PORT\}\}/g, String(port))
20
- .replace(/\{\{TASK_ID\}\}/g, taskId);
22
+ .replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(port, task.frontmatter.id))
23
+ .replace(/\{\{TASK_DESCRIPTION\}\}/g, taskDescription);
21
24
  if (skipPermissions) {
22
25
  instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
23
26
  }
@@ -65,9 +65,6 @@ async function invokeAgentWithRetries(
65
65
  const { command, args, stdin, env: agentEnv } = ctx.agent.getTaskRunCommandLine(
66
66
  invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions,
67
67
  );
68
- const truncate = (s: string, max = 100) => s.length > max ? s.slice(0, max) + "…" : s;
69
- const displayArgs = args.map((a) => truncate(a));
70
- console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
71
68
  const result = await spawnCommand(command, args, {
72
69
  cwd: getRunDir(ctx.taskDir, ctx.runId),
73
70
  env: { ...ctx.guiEnv, ...agentEnv, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 9966) },
@@ -516,10 +513,10 @@ async function requestConfirmation(
516
513
  taskDir: string,
517
514
  ): Promise<boolean> {
518
515
  const port = config.httpPort ?? 9966;
519
- const res = await fetch(`http://localhost:${port}/request-confirmation`, {
516
+ const res = await fetch(`http://localhost:${port}/request-confirmation?taskId=${encodeURIComponent(task.frontmatter.id)}`, {
520
517
  method: "POST",
521
518
  headers: { "Content-Type": "application/json" },
522
- body: JSON.stringify({ taskId: task.frontmatter.id, description: `Run task "${task.frontmatter.name || task.frontmatter.id}"?` }),
519
+ body: JSON.stringify({ description: `Run task "${task.frontmatter.name || task.frontmatter.id}"?` }),
523
520
  });
524
521
  const body = await res.json() as { confirmed?: boolean; error?: string };
525
522
  if (typeof body.confirmed !== "boolean") {
@@ -90,7 +90,7 @@ export async function handleMcpRequest(body: string, sessionId: string | undefin
90
90
  body: rpcResult(id, {
91
91
  tools: agentTools.map((t) => ({
92
92
  name: t.name,
93
- description: t.description,
93
+ description: t.description.join(" "),
94
94
  inputSchema: t.inputSchema,
95
95
  })),
96
96
  }),
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): 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 agentTools) {
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
+ }
@@ -126,7 +126,6 @@ async function generatePlan(
126
126
  const fullPrompt = PLAN_GENERATION_PROMPT + userPrompt;
127
127
  const planAgent = getAgent(agentName);
128
128
  const { command, args, stdin, env: agentEnv } = planAgent.getPlanGenerationCommandLine(fullPrompt);
129
- console.log(`[generatePlan] Running: ${command} ${args.join(" ")}`);
130
129
 
131
130
  const { output } = await spawnCommand(command, args, {
132
131
  cwd: projectRoot,
@@ -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,
@@ -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);
@@ -3,6 +3,7 @@ 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, agentTools } 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");
@@ -11,8 +12,8 @@ const template = fs.readFileSync(templatePath, "utf-8");
11
12
  /** Minimal replica of getAgentInstructions that doesn't need host.json */
12
13
  function buildInstructions(taskId: string, skipPermissions?: boolean): string {
13
14
  let instructions = template
14
- .replace(/\{\{PORT\}\}/g, "9966")
15
- .replace(/\{\{TASK_ID\}\}/g, taskId);
15
+ .replace(/\{\{ENDPOINT_DOCS\}\}/g, generateEndpointDocs(9966, taskId))
16
+ .replace(/\{\{TASK_DESCRIPTION\}\}/g, "Test task prompt");
16
17
  if (skipPermissions) {
17
18
  instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
18
19
  }
@@ -39,10 +40,72 @@ describe("getAgentInstructions", () => {
39
40
  assert.match(result, /## HTTP Endpoints/);
40
41
  });
41
42
 
42
- it("replaces template variables", () => {
43
+ it("replaces all template variables", () => {
44
+ const result = buildInstructions("my-task-123");
45
+ assert.doesNotMatch(result, /\{\{ENDPOINT_DOCS\}\}/);
46
+ assert.doesNotMatch(result, /\{\{TASK_DESCRIPTION\}\}/);
47
+ });
48
+
49
+ it("includes task ID in endpoint examples", () => {
43
50
  const result = buildInstructions("my-task-123");
44
51
  assert.match(result, /my-task-123/);
45
- assert.doesNotMatch(result, /\{\{TASK_ID\}\}/);
46
- assert.doesNotMatch(result, /\{\{PORT\}\}/);
52
+ });
53
+
54
+ it("includes port in endpoint URL", () => {
55
+ const result = buildInstructions("test");
56
+ assert.match(result, /localhost:9966/);
57
+ });
58
+
59
+ it("includes task description", () => {
60
+ const result = buildInstructions("test");
61
+ assert.match(result, /Test task prompt/);
62
+ });
63
+ });
64
+
65
+ describe("full agent instruction snapshot", () => {
66
+ it("matches the expected full text exactly", () => {
67
+ const result = buildInstructions("test-task-id").replace(/\r\n/g, "\n").trimEnd();
68
+ const snapshotPath = path.join(__dirname, "fixtures", "agent-instructions-snapshot.md");
69
+ const expected = fs.readFileSync(snapshotPath, "utf-8").replace(/\r\n/g, "\n").trimEnd();
70
+ assert.equal(result, expected);
71
+ });
72
+ });
73
+
74
+ describe("generateEndpointDocs", () => {
75
+ const docs = generateEndpointDocs(9966, "test-id");
76
+
77
+ it("generates docs for all MCP tools", () => {
78
+ for (const tool of agentTools) {
79
+ assert.match(docs, new RegExp(`POST http://localhost:9966/${tool.name}\\?taskId=`), `Missing endpoint for ${tool.name}`);
80
+ }
81
+ });
82
+
83
+ it("includes taskId parameter for every endpoint", () => {
84
+ const endpointBlocks = docs.split("**`POST");
85
+ // First element is the header, skip it
86
+ for (let i = 1; i < endpointBlocks.length; i++) {
87
+ assert.match(endpointBlocks[i], /taskId/, `Missing taskId in endpoint block ${i}`);
88
+ }
89
+ });
90
+
91
+ it("includes port in the header", () => {
92
+ assert.match(docs, /localhost:9966/);
93
+ });
94
+
95
+ it("includes task ID in query parameters", () => {
96
+ assert.match(docs, /taskId=test-id/);
97
+ });
98
+
99
+ it("includes response descriptions", () => {
100
+ for (const tool of agentTools) {
101
+ if (tool.responseDescription) {
102
+ assert.match(docs, /Response:/, `Missing response description for ${tool.name}`);
103
+ }
104
+ }
105
+ });
106
+
107
+ it("marks required and optional parameters correctly", () => {
108
+ assert.match(docs, /\(required, string\)/);
109
+ assert.match(docs, /\(optional, string\)/);
47
110
  });
48
111
  });
@@ -0,0 +1,58 @@
1
+ You are an AI agent executing a task on behalf of the user. Follow these instructions carefully.
2
+
3
+ ## Reporting Output
4
+
5
+ If you generate report or output files, print each file path on its own line using this exact format:
6
+ [PALMIER_REPORT] <filename>
7
+
8
+ ## Completion
9
+
10
+ When you are done, output exactly one of these markers as the very last line (no other text on the same line):
11
+ [PALMIER_TASK_SUCCESS]
12
+ [PALMIER_TASK_FAILURE]
13
+
14
+ ## Permissions
15
+
16
+ Whenever a tool you are trying to use is denied or you lack the required permissions, print each required permission on its own line using this exact format:
17
+ [PALMIER_PERMISSION] <tool_name> | <description>
18
+
19
+ ## HTTP Endpoints
20
+
21
+ The following HTTP endpoints are available during task execution. Use curl to call them.
22
+
23
+ **`POST http://localhost:9966/notify?taskId=test-task-id`** — Send a push notification to the user's device.
24
+ ```json
25
+ {"title":"...","body":"..."}
26
+ ```
27
+ - `title` (required, string): Notification title
28
+ - `body` (required, string): Notification body
29
+ - Response: `{"ok": true}` on success.
30
+
31
+ **`POST http://localhost:9966/request-input?taskId=test-task-id`** — Request input from the user.
32
+ ```json
33
+ {"description":"...","questions":["..."]}
34
+ ```
35
+ - `description` (optional, string): Context or heading for the input request
36
+ - `questions` (required, string array): Questions to present to the user
37
+ - The request blocks until the user responds.
38
+ - Response: `{"values": ["answer1", "answer2"]}` on success, or `{"aborted": true}` if the user declines.
39
+ - 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.
40
+
41
+ **`POST http://localhost:9966/request-confirmation?taskId=test-task-id`** — Request confirmation from the user.
42
+ ```json
43
+ {"description":"..."}
44
+ ```
45
+ - `description` (required, string): What the user is confirming
46
+ - The request blocks until the user confirms or aborts.
47
+ - Response: `{"confirmed": true}` or `{"confirmed": false}`.
48
+
49
+ **`POST http://localhost:9966/device-geolocation?taskId=test-task-id`** — Get the GPS location of the user's mobile device.
50
+ - When you need the user's real-time location, use this endpoint.
51
+ - Blocks until the device responds (up to 30 seconds).
52
+ - Response: `{"latitude": ..., "longitude": ..., "accuracy": ..., "timestamp": ...}` on success, or `{"error": "..."}` on failure.
53
+
54
+ The task to execute follows below:
55
+
56
+ ---
57
+
58
+ Test task prompt