palmier 0.4.6 → 0.4.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.
package/README.md CHANGED
@@ -193,8 +193,8 @@ The serve daemon exposes localhost-only HTTP endpoints for agents during task ex
193
193
 
194
194
  | Endpoint | Method | Description |
195
195
  |---|---|---|
196
- | `/notify` | GET | Send a push notification (requires server mode) |
197
- | `/request-input` | GET | Request user input; blocks until a response is provided |
196
+ | `/notify` | POST | Send a push notification (requires server mode) |
197
+ | `/request-input` | POST | Request user input; blocks until a response is provided |
198
198
 
199
199
  See [agent-instructions.md](src/agents/agent-instructions.md) for usage examples.
200
200
 
@@ -22,11 +22,18 @@ If the task fails because a tool was denied or you lack the required permissions
22
22
 
23
23
  ## HTTP Endpoints
24
24
 
25
- The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution.
26
-
27
- **Requesting user input** — If you need any information you do not have (credentials, configuration values, preferences, clarifications, etc.) or the task explicitly asks you to get input from the user, do NOT fail the task. Instead, GET `/request-input?taskId={{TASK_ID}}&descriptions=question+1&descriptions=question+2`. The request blocks until the user responds. The response is `{"values":["answer1","answer2"]}` on success, or `{"aborted":true}` if the user chooses to abort.
28
-
29
- **Sending push notifications** — GET `/notify?title=...&body=...` to send a push notification to the user's devices.
25
+ The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution. Use curl to call them.
26
+
27
+ **Requesting user input** — When you need information from the user (credentials, questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout. Instead, POST to `/request-input` with:
28
+ ```json
29
+ {"taskId":"{{TASK_ID}}","descriptions":["question 1","question 2"]}
30
+ ```
31
+ The request blocks until the user responds. Response: `{"values":["answer1","answer2"]}` on success, or `{"aborted":true}` if the user declines.
32
+
33
+ **Sending push notifications** — To notify the user, POST to `/notify` with:
34
+ ```json
35
+ {"title":"...","body":"..."}
36
+ ```
30
37
 
31
38
  ---
32
39
 
@@ -9,7 +9,7 @@ export class ClaudeAgent {
9
9
  };
10
10
  }
11
11
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
12
- const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
12
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
13
13
  const args = ["--permission-mode", "acceptEdits", "-p", "--allowedTools", "WebFetch"];
14
14
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
15
15
  for (const p of allPerms) {
@@ -9,7 +9,7 @@ export class CodexAgent {
9
9
  };
10
10
  }
11
11
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
12
- const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
12
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
13
13
  // Using danger-full-access until workspace-write is fixed: https://github.com/openai/codex/issues/12572
14
14
  const args = ["exec", "--full-auto", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
15
15
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
@@ -17,10 +17,10 @@ export class CodexAgent {
17
17
  args.push("--config");
18
18
  args.push(`apps.${p.name}.default_tools_approval_mode="approve"`);
19
19
  }
20
- args.push("-"); // read prompt from stdin
21
20
  if (followupPrompt) {
22
21
  args.push("resume", "--last");
23
22
  } // continue mode for followups
23
+ args.push("-"); // read prompt from stdin
24
24
  return { command: "codex", args, stdin: prompt };
25
25
  }
26
26
  async init() {
@@ -9,13 +9,10 @@ export class CopilotAgent {
9
9
  };
10
10
  }
11
11
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
12
- const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
13
- const args = ["-p", prompt, "--allowed-tools", "web_fetch"];
12
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
13
+ const args = ["-p", prompt];
14
14
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
15
- if (allPerms.length > 0) {
16
- args.push(`--allow-tool='${allPerms.map((p) => p.name).join(",")}'`);
17
- ;
18
- }
15
+ args.push(`--allow-tool=${["web_fetch", ...allPerms.map((p) => p.name)].join(",")}`);
19
16
  if (followupPrompt) {
20
17
  args.push("--continue");
21
18
  }
@@ -9,12 +9,10 @@ export class GeminiAgent {
9
9
  };
10
10
  }
11
11
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
12
- const prompt = followupPrompt ?? (task.body || task.frontmatter.user_prompt);
13
- const fullPrompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + prompt;
14
- const args = ["--prompt", "--allowed-tools", "web_fetch", "-"];
12
+ const fullPrompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
13
+ const args = ["--allowed-tools", "web_fetch"];
15
14
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
16
15
  if (allPerms.length > 0) {
17
- args.push("--allowed-tools");
18
16
  for (const p of allPerms) {
19
17
  args.push(p.name);
20
18
  }
@@ -22,6 +20,7 @@ export class GeminiAgent {
22
20
  if (followupPrompt) {
23
21
  args.push("--resume");
24
22
  } // continue mode for followups
23
+ args.push("--prompt", "-"); // read prompt from stdin
25
24
  return { command: "gemini", args, stdin: fullPrompt };
26
25
  }
27
26
  async init() {
@@ -8,7 +8,7 @@ export class OpenClawAgent {
8
8
  };
9
9
  }
10
10
  getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
11
- const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
11
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
12
12
  // OpenClaw does not support stdin as prompt.
13
13
  const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
14
14
  return { command: "openclaw", args };
@@ -1,24 +1,21 @@
1
- You are a task planning assistant. Given a task description, produce a Markdown execution plan for an agent. **Do not execute any part of the plan yourself.**
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
2
 
3
- Output a raw YAML frontmatter block (delimited by `---`) followed by the plan body. Do NOT wrap frontmatter in code fences. The first line of output must be `---`.
3
+ ## Output Format
4
+
5
+ Start with a YAML frontmatter block (no code fences), then the plan body:
4
6
 
5
7
  ---
6
- task_name: <short name, 3-6 words>
8
+ task_name: <concise label, 3-6 words>
7
9
  ---
8
10
 
9
- **Frontmatter:** `task_name` — concise label (e.g., "Clean up temp files", "Backup database daily").
10
-
11
- **Plan body:**
12
-
13
- ### 1. Goal
14
- What the task accomplishes and the expected end state.
11
+ <plan body>
15
12
 
16
- ### 2. Plan
17
- Numbered sequence of concrete, actionable steps. Include conditional branches where behavior may vary. Each step must be unambiguous.
13
+ ## Plan Body Guidelines
18
14
 
19
- ### 3. Output Format (if applicable)
20
- If the task produces formatted output (report, email, etc.), specify structure, sections, tone, and templates.
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
+ - Relative times in the task description (e.g., "yesterday", "last week") refer to execution time, not plan generation time.
21
19
 
22
- Relative times in the task description (e.g., "yesterday") are relative to execution time, not plan generation time.
20
+ ## Task Description
23
21
 
24
- **Task description:**
@@ -17,10 +17,9 @@ import { publishHostEvent } from "../events.js";
17
17
  * (for command-triggered mode this is the per-line augmented task).
18
18
  */
19
19
  async function invokeAgentWithContinuation(ctx, invokeTask) {
20
- let followupPrompt;
21
20
  // eslint-disable-next-line no-constant-condition
22
21
  while (true) {
23
- const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, followupPrompt, ctx.transientPermissions);
22
+ const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, undefined, ctx.transientPermissions);
24
23
  const result = await spawnCommand(command, args, {
25
24
  cwd: getRunDir(ctx.taskDir, ctx.runId),
26
25
  env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7400) },
@@ -69,7 +68,6 @@ async function invokeAgentWithContinuation(ctx, invokeTask) {
69
68
  }
70
69
  // If the agent actually failed, retry with the new permissions
71
70
  if (outcome === "failed") {
72
- followupPrompt = "Permissions granted, please continue.";
73
71
  continue;
74
72
  }
75
73
  }
@@ -126,7 +124,7 @@ export async function runCommand(taskId) {
126
124
  const taskName = task.frontmatter.name;
127
125
  // Use existing run dir if just created by RPC, otherwise create a new one
128
126
  const existingRunId = findLatestPendingRunId(taskDir);
129
- const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now());
127
+ const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now(), task.frontmatter.agent);
130
128
  if (!existingRunId) {
131
129
  appendHistory(config.projectRoot, { task_id: taskId, run_id: runId });
132
130
  }
@@ -334,12 +332,15 @@ async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName
334
332
  }
335
333
  async function requestPermission(config, task, taskDir, requiredPermissions) {
336
334
  const port = config.httpPort ?? 7400;
337
- const params = new URLSearchParams({
338
- taskId: task.frontmatter.id,
339
- taskName: task.frontmatter.name,
340
- permissions: JSON.stringify(requiredPermissions),
335
+ const res = await fetch(`http://localhost:${port}/request-permission`, {
336
+ method: "POST",
337
+ headers: { "Content-Type": "application/json" },
338
+ body: JSON.stringify({
339
+ taskId: task.frontmatter.id,
340
+ taskName: task.frontmatter.name,
341
+ permissions: requiredPermissions,
342
+ }),
341
343
  });
342
- const res = await fetch(`http://localhost:${port}/request-permission?${params}`);
343
344
  const { response } = await res.json();
344
345
  writeTaskStatus(taskDir, {
345
346
  running_state: response === "aborted" ? "aborted" : "started",
@@ -349,11 +350,11 @@ async function requestPermission(config, task, taskDir, requiredPermissions) {
349
350
  }
350
351
  async function requestConfirmation(config, task, taskDir) {
351
352
  const port = config.httpPort ?? 7400;
352
- const params = new URLSearchParams({
353
- taskId: task.frontmatter.id,
354
- taskName: task.frontmatter.name,
353
+ const res = await fetch(`http://localhost:${port}/request-confirmation`, {
354
+ method: "POST",
355
+ headers: { "Content-Type": "application/json" },
356
+ body: JSON.stringify({ taskId: task.frontmatter.id, taskName: task.frontmatter.name }),
355
357
  });
356
- const res = await fetch(`http://localhost:${port}/request-confirmation?${params}`);
357
358
  const { confirmed } = await res.json();
358
359
  writeTaskStatus(taskDir, {
359
360
  running_state: confirmed ? "started" : "aborted",
@@ -48,6 +48,7 @@ function parseResultFrontmatter(raw) {
48
48
  return {
49
49
  messages,
50
50
  task_name: meta.task_name,
51
+ agent: meta.agent,
51
52
  running_state: runningState,
52
53
  start_time: startedMsg?.time || undefined,
53
54
  end_time: terminalMsg?.time || undefined,
@@ -260,7 +261,7 @@ export function createRpcHandler(config, nc) {
260
261
  writeTaskFile(taskDir, task);
261
262
  // Do NOT append to tasks.jsonl — this is a one-off run
262
263
  // Create initial result file so it appears in runs list immediately
263
- const runId = createRunDir(taskDir, name, Date.now());
264
+ const runId = createRunDir(taskDir, name, Date.now(), params.agent);
264
265
  appendHistory(config.projectRoot, { task_id: id, run_id: runId });
265
266
  // Spawn `palmier run <id>` directly as a detached process
266
267
  const script = process.argv[1] || "palmier";
@@ -278,7 +279,7 @@ export function createRpcHandler(config, nc) {
278
279
  // Create initial result file so it appears in runs list immediately
279
280
  const runTaskDir = getTaskDir(config.projectRoot, params.id);
280
281
  const runTask = parseTaskFile(runTaskDir);
281
- const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now());
282
+ const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now(), runTask.frontmatter.agent);
282
283
  appendHistory(config.projectRoot, { task_id: params.id, run_id: taskRunId });
283
284
  await getPlatform().startTask(params.id);
284
285
  return { ok: true, task_id: params.id, run_id: taskRunId };
package/dist/task.d.ts CHANGED
@@ -42,7 +42,7 @@ export declare function readTaskStatus(taskDir: string): TaskStatus | undefined;
42
42
  * Create a run directory with an initial TASKRUN.md file.
43
43
  * Returns the run ID (timestamp string used as directory name).
44
44
  */
45
- export declare function createRunDir(taskDir: string, taskName: string, startTime: number): string;
45
+ export declare function createRunDir(taskDir: string, taskName: string, startTime: number, agent?: string): string;
46
46
  /**
47
47
  * Get the path to a run directory.
48
48
  */
package/dist/task.js CHANGED
@@ -133,11 +133,12 @@ export function readTaskStatus(taskDir) {
133
133
  * Create a run directory with an initial TASKRUN.md file.
134
134
  * Returns the run ID (timestamp string used as directory name).
135
135
  */
136
- export function createRunDir(taskDir, taskName, startTime) {
136
+ export function createRunDir(taskDir, taskName, startTime, agent) {
137
137
  const runId = String(startTime);
138
138
  const runDir = path.join(taskDir, runId);
139
139
  fs.mkdirSync(runDir, { recursive: true });
140
- const content = `---\ntask_name: ${taskName}\n---\n\n`;
140
+ const agentLine = agent ? `\nagent: ${agent}` : "";
141
+ const content = `---\ntask_name: ${taskName}${agentLine}\n---\n\n`;
141
142
  fs.writeFileSync(path.join(runDir, "TASKRUN.md"), content, "utf-8");
142
143
  return runId;
143
144
  }
@@ -193,8 +193,8 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
193
193
  }
194
194
  return;
195
195
  }
196
- // ── GET /notify — send push notification via NATS ──────────────────
197
- if (req.method === "GET" && pathname === "/notify") {
196
+ // ── POST /notify — send push notification via NATS ─────────────────
197
+ if (req.method === "POST" && pathname === "/notify") {
198
198
  if (!isLocalhost(req)) {
199
199
  sendJson(res, 403, { error: "localhost only" });
200
200
  return;
@@ -204,10 +204,10 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
204
204
  return;
205
205
  }
206
206
  try {
207
- const title = url.searchParams.get("title");
208
- const notifBody = url.searchParams.get("body");
207
+ const body = await readBody(req);
208
+ const { title, body: notifBody } = JSON.parse(body);
209
209
  if (!title || !notifBody) {
210
- sendJson(res, 400, { error: "title and body query params are required" });
210
+ sendJson(res, 400, { error: "title and body are required" });
211
211
  return;
212
212
  }
213
213
  const sc = StringCodec();
@@ -227,18 +227,17 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
227
227
  }
228
228
  return;
229
229
  }
230
- // ── GET /request-input — held connection until user responds ────────
231
- if (req.method === "GET" && pathname === "/request-input") {
230
+ // ── POST /request-input — held connection until user responds ────────
231
+ if (req.method === "POST" && pathname === "/request-input") {
232
232
  if (!isLocalhost(req)) {
233
233
  sendJson(res, 403, { error: "localhost only" });
234
234
  return;
235
235
  }
236
236
  try {
237
- const taskId = url.searchParams.get("taskId");
238
- const runId = url.searchParams.get("runId");
239
- const descriptions = url.searchParams.getAll("descriptions");
240
- if (!taskId || !descriptions.length) {
241
- sendJson(res, 400, { error: "taskId and descriptions query params are required" });
237
+ const body = await readBody(req);
238
+ const { taskId, runId, descriptions } = JSON.parse(body);
239
+ if (!taskId || !descriptions?.length) {
240
+ sendJson(res, 400, { error: "taskId and descriptions are required" });
242
241
  return;
243
242
  }
244
243
  const taskDir = getTaskDir(config.projectRoot, taskId);
@@ -271,16 +270,17 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
271
270
  }
272
271
  return;
273
272
  }
274
- // ── GET /request-confirmation — held connection ─────────────────────
275
- if (req.method === "GET" && pathname === "/request-confirmation") {
273
+ // ── POST /request-confirmation — held connection ────────────────────
274
+ if (req.method === "POST" && pathname === "/request-confirmation") {
276
275
  if (!isLocalhost(req)) {
277
276
  sendJson(res, 403, { error: "localhost only" });
278
277
  return;
279
278
  }
280
279
  try {
281
- const taskId = url.searchParams.get("taskId");
280
+ const body = await readBody(req);
281
+ const { taskId } = JSON.parse(body);
282
282
  if (!taskId) {
283
- sendJson(res, 400, { error: "taskId query param is required" });
283
+ sendJson(res, 400, { error: "taskId is required" });
284
284
  return;
285
285
  }
286
286
  await publishEvent(taskId, {
@@ -301,25 +301,17 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
301
301
  }
302
302
  return;
303
303
  }
304
- // ── GET /request-permission — held connection ───────────────────────
305
- if (req.method === "GET" && pathname === "/request-permission") {
304
+ // ── POST /request-permission — held connection ──────────────────────
305
+ if (req.method === "POST" && pathname === "/request-permission") {
306
306
  if (!isLocalhost(req)) {
307
307
  sendJson(res, 403, { error: "localhost only" });
308
308
  return;
309
309
  }
310
310
  try {
311
- const taskId = url.searchParams.get("taskId");
312
- const taskName = url.searchParams.get("taskName");
313
- const permissionsRaw = url.searchParams.get("permissions");
314
- let permissions = [];
315
- if (permissionsRaw) {
316
- try {
317
- permissions = JSON.parse(permissionsRaw);
318
- }
319
- catch { /* ignore */ }
320
- }
321
- if (!taskId || !permissions.length) {
322
- sendJson(res, 400, { error: "taskId and permissions query params are required" });
311
+ const body = await readBody(req);
312
+ const { taskId, taskName, permissions } = JSON.parse(body);
313
+ if (!taskId || !permissions?.length) {
314
+ sendJson(res, 400, { error: "taskId and permissions are required" });
323
315
  return;
324
316
  }
325
317
  await publishEvent(taskId, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -22,11 +22,18 @@ If the task fails because a tool was denied or you lack the required permissions
22
22
 
23
23
  ## HTTP Endpoints
24
24
 
25
- The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution.
26
-
27
- **Requesting user input** — If you need any information you do not have (credentials, configuration values, preferences, clarifications, etc.) or the task explicitly asks you to get input from the user, do NOT fail the task. Instead, GET `/request-input?taskId={{TASK_ID}}&descriptions=question+1&descriptions=question+2`. The request blocks until the user responds. The response is `{"values":["answer1","answer2"]}` on success, or `{"aborted":true}` if the user chooses to abort.
28
-
29
- **Sending push notifications** — GET `/notify?title=...&body=...` to send a push notification to the user's devices.
25
+ The following HTTP endpoints are available at http://localhost:{{PORT}} during task execution. Use curl to call them.
26
+
27
+ **Requesting user input** — When you need information from the user (credentials, questions, preferences, clarifications, etc.), do not guess, fail, or prompt via stdout. Instead, POST to `/request-input` with:
28
+ ```json
29
+ {"taskId":"{{TASK_ID}}","descriptions":["question 1","question 2"]}
30
+ ```
31
+ The request blocks until the user responds. Response: `{"values":["answer1","answer2"]}` on success, or `{"aborted":true}` if the user declines.
32
+
33
+ **Sending push notifications** — To notify the user, POST to `/notify` with:
34
+ ```json
35
+ {"title":"...","body":"..."}
36
+ ```
30
37
 
31
38
  ---
32
39
 
@@ -13,7 +13,7 @@ export class ClaudeAgent implements AgentTool {
13
13
  }
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
16
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
17
  const args = ["--permission-mode", "acceptEdits", "-p", "--allowedTools", "WebFetch"];
18
18
 
19
19
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
@@ -13,7 +13,7 @@ export class CodexAgent implements AgentTool {
13
13
  }
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
16
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
17
  // Using danger-full-access until workspace-write is fixed: https://github.com/openai/codex/issues/12572
18
18
  const args = ["exec", "--full-auto", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
19
19
 
@@ -22,9 +22,9 @@ export class CodexAgent implements AgentTool {
22
22
  args.push("--config");
23
23
  args.push(`apps.${p.name}.default_tools_approval_mode="approve"`);
24
24
  }
25
+ if (followupPrompt) {args.push("resume", "--last");} // continue mode for followups
25
26
  args.push("-"); // read prompt from stdin
26
27
 
27
- if (followupPrompt) {args.push("resume", "--last");} // continue mode for followups
28
28
  return { command: "codex", args, stdin: prompt };
29
29
  }
30
30
 
@@ -13,14 +13,11 @@ export class CopilotAgent implements AgentTool {
13
13
  }
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
17
- const args = ["-p", prompt, "--allowed-tools", "web_fetch"];
16
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
+ const args = ["-p", prompt];
18
18
 
19
19
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
20
- if (allPerms.length > 0) {
21
- args.push(`--allow-tool='${allPerms.map((p) => p.name).join(",")}'`);;
22
- }
23
-
20
+ args.push(`--allow-tool=${["web_fetch", ...allPerms.map((p) => p.name)].join(",")}`);
24
21
  if (followupPrompt) { args.push("--continue"); }
25
22
  return { command: "copilot", args};
26
23
  }
@@ -13,19 +13,19 @@ export class GeminiAgent implements AgentTool {
13
13
  }
14
14
 
15
15
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = followupPrompt ?? (task.body || task.frontmatter.user_prompt);
17
- const fullPrompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + prompt;
18
- const args = ["--prompt", "--allowed-tools", "web_fetch", "-"];
16
+ const fullPrompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
+ const args = ["--allowed-tools", "web_fetch"];
19
18
 
20
19
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
21
20
  if (allPerms.length > 0) {
22
- args.push("--allowed-tools");
23
21
  for (const p of allPerms) {
24
22
  args.push(p.name);
25
23
  }
26
24
  }
27
25
 
28
26
  if (followupPrompt) {args.push("--resume");} // continue mode for followups
27
+ args.push("--prompt", "-"); // read prompt from stdin
28
+
29
29
  return { command: "gemini", args, stdin: fullPrompt };
30
30
  }
31
31
 
@@ -12,7 +12,7 @@ export class OpenClawAgent implements AgentTool {
12
12
  }
13
13
 
14
14
  getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
15
- const prompt = getAgentInstructions(task.frontmatter.id) + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
15
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
16
16
  // OpenClaw does not support stdin as prompt.
17
17
  const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
18
18
 
@@ -1,24 +1,21 @@
1
- You are a task planning assistant. Given a task description, produce a Markdown execution plan for an agent. **Do not execute any part of the plan yourself.**
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
2
 
3
- Output a raw YAML frontmatter block (delimited by `---`) followed by the plan body. Do NOT wrap frontmatter in code fences. The first line of output must be `---`.
3
+ ## Output Format
4
+
5
+ Start with a YAML frontmatter block (no code fences), then the plan body:
4
6
 
5
7
  ---
6
- task_name: <short name, 3-6 words>
8
+ task_name: <concise label, 3-6 words>
7
9
  ---
8
10
 
9
- **Frontmatter:** `task_name` — concise label (e.g., "Clean up temp files", "Backup database daily").
10
-
11
- **Plan body:**
12
-
13
- ### 1. Goal
14
- What the task accomplishes and the expected end state.
11
+ <plan body>
15
12
 
16
- ### 2. Plan
17
- Numbered sequence of concrete, actionable steps. Include conditional branches where behavior may vary. Each step must be unambiguous.
13
+ ## Plan Body Guidelines
18
14
 
19
- ### 3. Output Format (if applicable)
20
- If the task produces formatted output (report, email, etc.), specify structure, sections, tone, and templates.
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
+ - Relative times in the task description (e.g., "yesterday", "last week") refer to execution time, not plan generation time.
21
19
 
22
- Relative times in the task description (e.g., "yesterday") are relative to execution time, not plan generation time.
20
+ ## Task Description
23
21
 
24
- **Task description:**
@@ -45,10 +45,9 @@ async function invokeAgentWithContinuation(
45
45
  ctx: InvocationContext,
46
46
  invokeTask: ParsedTask,
47
47
  ): Promise<InvocationResult> {
48
- let followupPrompt: string | undefined;
49
48
  // eslint-disable-next-line no-constant-condition
50
49
  while (true) {
51
- const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, followupPrompt, ctx.transientPermissions);
50
+ const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, undefined, ctx.transientPermissions);
52
51
  const result = await spawnCommand(command, args, {
53
52
  cwd: getRunDir(ctx.taskDir, ctx.runId),
54
53
  env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId), PALMIER_HTTP_PORT: String(ctx.config.httpPort ?? 7400) },
@@ -106,7 +105,6 @@ async function invokeAgentWithContinuation(
106
105
 
107
106
  // If the agent actually failed, retry with the new permissions
108
107
  if (outcome === "failed") {
109
- followupPrompt = "Permissions granted, please continue.";
110
108
  continue;
111
109
  }
112
110
  }
@@ -172,7 +170,7 @@ export async function runCommand(taskId: string): Promise<void> {
172
170
 
173
171
  // Use existing run dir if just created by RPC, otherwise create a new one
174
172
  const existingRunId = findLatestPendingRunId(taskDir);
175
- const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now());
173
+ const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now(), task.frontmatter.agent);
176
174
  if (!existingRunId) {
177
175
  appendHistory(config.projectRoot, { task_id: taskId, run_id: runId });
178
176
  }
@@ -412,12 +410,15 @@ async function requestPermission(
412
410
  requiredPermissions: RequiredPermission[],
413
411
  ): Promise<"granted" | "granted_all" | "aborted"> {
414
412
  const port = config.httpPort ?? 7400;
415
- const params = new URLSearchParams({
416
- taskId: task.frontmatter.id,
417
- taskName: task.frontmatter.name,
418
- permissions: JSON.stringify(requiredPermissions),
413
+ const res = await fetch(`http://localhost:${port}/request-permission`, {
414
+ method: "POST",
415
+ headers: { "Content-Type": "application/json" },
416
+ body: JSON.stringify({
417
+ taskId: task.frontmatter.id,
418
+ taskName: task.frontmatter.name,
419
+ permissions: requiredPermissions,
420
+ }),
419
421
  });
420
- const res = await fetch(`http://localhost:${port}/request-permission?${params}`);
421
422
  const { response } = await res.json() as { response: "granted" | "granted_all" | "aborted" };
422
423
  writeTaskStatus(taskDir, {
423
424
  running_state: response === "aborted" ? "aborted" : "started",
@@ -433,11 +434,11 @@ async function requestConfirmation(
433
434
  taskDir: string,
434
435
  ): Promise<boolean> {
435
436
  const port = config.httpPort ?? 7400;
436
- const params = new URLSearchParams({
437
- taskId: task.frontmatter.id,
438
- taskName: task.frontmatter.name,
437
+ const res = await fetch(`http://localhost:${port}/request-confirmation`, {
438
+ method: "POST",
439
+ headers: { "Content-Type": "application/json" },
440
+ body: JSON.stringify({ taskId: task.frontmatter.id, taskName: task.frontmatter.name }),
439
441
  });
440
- const res = await fetch(`http://localhost:${port}/request-confirmation?${params}`);
441
442
  const { confirmed } = await res.json() as { confirmed: boolean };
442
443
  writeTaskStatus(taskDir, {
443
444
  running_state: confirmed ? "started" : "aborted",
@@ -58,6 +58,7 @@ function parseResultFrontmatter(raw: string): Record<string, unknown> {
58
58
  return {
59
59
  messages,
60
60
  task_name: meta.task_name,
61
+ agent: meta.agent,
61
62
  running_state: runningState,
62
63
  start_time: startedMsg?.time || undefined,
63
64
  end_time: terminalMsg?.time || undefined,
@@ -318,7 +319,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
318
319
  // Do NOT append to tasks.jsonl — this is a one-off run
319
320
 
320
321
  // Create initial result file so it appears in runs list immediately
321
- const runId = createRunDir(taskDir, name, Date.now());
322
+ const runId = createRunDir(taskDir, name, Date.now(), params.agent);
322
323
  appendHistory(config.projectRoot, { task_id: id, run_id: runId });
323
324
 
324
325
  // Spawn `palmier run <id>` directly as a detached process
@@ -339,7 +340,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
339
340
  // Create initial result file so it appears in runs list immediately
340
341
  const runTaskDir = getTaskDir(config.projectRoot, params.id);
341
342
  const runTask = parseTaskFile(runTaskDir);
342
- const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now());
343
+ const taskRunId = createRunDir(runTaskDir, runTask.frontmatter.name, Date.now(), runTask.frontmatter.agent);
343
344
  appendHistory(config.projectRoot, { task_id: params.id, run_id: taskRunId });
344
345
 
345
346
  await getPlatform().startTask(params.id);
package/src/task.ts CHANGED
@@ -155,11 +155,13 @@ export function createRunDir(
155
155
  taskDir: string,
156
156
  taskName: string,
157
157
  startTime: number,
158
+ agent?: string,
158
159
  ): string {
159
160
  const runId = String(startTime);
160
161
  const runDir = path.join(taskDir, runId);
161
162
  fs.mkdirSync(runDir, { recursive: true });
162
- const content = `---\ntask_name: ${taskName}\n---\n\n`;
163
+ const agentLine = agent ? `\nagent: ${agent}` : "";
164
+ const content = `---\ntask_name: ${taskName}${agentLine}\n---\n\n`;
163
165
  fs.writeFileSync(path.join(runDir, "TASKRUN.md"), content, "utf-8");
164
166
  return runId;
165
167
  }
@@ -217,16 +217,16 @@ export async function startHttpTransport(
217
217
  return;
218
218
  }
219
219
 
220
- // ── GET /notify — send push notification via NATS ──────────────────
220
+ // ── POST /notify — send push notification via NATS ─────────────────
221
221
 
222
- if (req.method === "GET" && pathname === "/notify") {
222
+ if (req.method === "POST" && pathname === "/notify") {
223
223
  if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
224
224
  if (!nc) { sendJson(res, 503, { error: "NATS not connected — push notifications require server mode" }); return; }
225
225
 
226
226
  try {
227
- const title = url.searchParams.get("title");
228
- const notifBody = url.searchParams.get("body");
229
- if (!title || !notifBody) { sendJson(res, 400, { error: "title and body query params are required" }); return; }
227
+ const body = await readBody(req);
228
+ const { title, body: notifBody } = JSON.parse(body) as { title: string; body: string };
229
+ if (!title || !notifBody) { sendJson(res, 400, { error: "title and body are required" }); return; }
230
230
 
231
231
  const sc = StringCodec();
232
232
  const payload = { hostId: config.hostId, title, body: notifBody };
@@ -245,16 +245,17 @@ export async function startHttpTransport(
245
245
  return;
246
246
  }
247
247
 
248
- // ── GET /request-input — held connection until user responds ────────
248
+ // ── POST /request-input — held connection until user responds ────────
249
249
 
250
- if (req.method === "GET" && pathname === "/request-input") {
250
+ if (req.method === "POST" && pathname === "/request-input") {
251
251
  if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
252
252
  try {
253
- const taskId = url.searchParams.get("taskId");
254
- const runId = url.searchParams.get("runId");
255
- const descriptions = url.searchParams.getAll("descriptions");
256
- if (!taskId || !descriptions.length) {
257
- sendJson(res, 400, { error: "taskId and descriptions query params are required" });
253
+ const body = await readBody(req);
254
+ const { taskId, runId, descriptions } = JSON.parse(body) as {
255
+ taskId: string; runId?: string; descriptions: string[];
256
+ };
257
+ if (!taskId || !descriptions?.length) {
258
+ sendJson(res, 400, { error: "taskId and descriptions are required" });
258
259
  return;
259
260
  }
260
261
 
@@ -290,13 +291,14 @@ export async function startHttpTransport(
290
291
  return;
291
292
  }
292
293
 
293
- // ── GET /request-confirmation — held connection ─────────────────────
294
+ // ── POST /request-confirmation — held connection ────────────────────
294
295
 
295
- if (req.method === "GET" && pathname === "/request-confirmation") {
296
+ if (req.method === "POST" && pathname === "/request-confirmation") {
296
297
  if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
297
298
  try {
298
- const taskId = url.searchParams.get("taskId");
299
- if (!taskId) { sendJson(res, 400, { error: "taskId query param is required" }); return; }
299
+ const body = await readBody(req);
300
+ const { taskId } = JSON.parse(body) as { taskId: string };
301
+ if (!taskId) { sendJson(res, 400, { error: "taskId is required" }); return; }
300
302
 
301
303
  await publishEvent(taskId, {
302
304
  event_type: "confirm-request",
@@ -319,20 +321,17 @@ export async function startHttpTransport(
319
321
  return;
320
322
  }
321
323
 
322
- // ── GET /request-permission — held connection ───────────────────────
324
+ // ── POST /request-permission — held connection ──────────────────────
323
325
 
324
- if (req.method === "GET" && pathname === "/request-permission") {
326
+ if (req.method === "POST" && pathname === "/request-permission") {
325
327
  if (!isLocalhost(req)) { sendJson(res, 403, { error: "localhost only" }); return; }
326
328
  try {
327
- const taskId = url.searchParams.get("taskId");
328
- const taskName = url.searchParams.get("taskName");
329
- const permissionsRaw = url.searchParams.get("permissions");
330
- let permissions: RequiredPermission[] = [];
331
- if (permissionsRaw) {
332
- try { permissions = JSON.parse(permissionsRaw) as RequiredPermission[]; } catch { /* ignore */ }
333
- }
334
- if (!taskId || !permissions.length) {
335
- sendJson(res, 400, { error: "taskId and permissions query params are required" });
329
+ const body = await readBody(req);
330
+ const { taskId, taskName, permissions } = JSON.parse(body) as {
331
+ taskId: string; taskName?: string; permissions: RequiredPermission[];
332
+ };
333
+ if (!taskId || !permissions?.length) {
334
+ sendJson(res, 400, { error: "taskId and permissions are required" });
336
335
  return;
337
336
  }
338
337