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 +2 -2
- package/dist/agents/agent-instructions.md +12 -5
- package/dist/agents/claude.js +1 -1
- package/dist/agents/codex.js +2 -2
- package/dist/agents/copilot.js +3 -6
- package/dist/agents/gemini.js +3 -4
- package/dist/agents/openclaw.js +1 -1
- package/dist/commands/plan-generation.md +12 -15
- package/dist/commands/run.js +14 -13
- package/dist/rpc-handler.js +3 -2
- package/dist/task.d.ts +1 -1
- package/dist/task.js +3 -2
- package/dist/transports/http-transport.js +22 -30
- package/package.json +1 -1
- package/src/agents/agent-instructions.md +12 -5
- package/src/agents/claude.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/copilot.ts +3 -6
- package/src/agents/gemini.ts +4 -4
- package/src/agents/openclaw.ts +1 -1
- package/src/commands/plan-generation.md +12 -15
- package/src/commands/run.ts +14 -13
- package/src/rpc-handler.ts +3 -2
- package/src/task.ts +3 -1
- package/src/transports/http-transport.ts +26 -27
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` |
|
|
197
|
-
| `/request-input` |
|
|
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** —
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
package/dist/agents/claude.js
CHANGED
|
@@ -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" + (
|
|
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) {
|
package/dist/agents/codex.js
CHANGED
|
@@ -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" + (
|
|
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() {
|
package/dist/agents/copilot.js
CHANGED
|
@@ -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" + (
|
|
13
|
-
const args = ["-p", prompt
|
|
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
|
-
|
|
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
|
}
|
package/dist/agents/gemini.js
CHANGED
|
@@ -9,12 +9,10 @@ export class GeminiAgent {
|
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
11
|
getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
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() {
|
package/dist/agents/openclaw.js
CHANGED
|
@@ -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" + (
|
|
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.
|
|
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
|
|
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: <
|
|
8
|
+
task_name: <concise label, 3-6 words>
|
|
7
9
|
---
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
**Plan body:**
|
|
12
|
-
|
|
13
|
-
### 1. Goal
|
|
14
|
-
What the task accomplishes and the expected end state.
|
|
11
|
+
<plan body>
|
|
15
12
|
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
If the task produces formatted output (report, email, etc.), specify structure, sections,
|
|
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
|
-
|
|
20
|
+
## Task Description
|
|
23
21
|
|
|
24
|
-
**Task description:**
|
package/dist/commands/run.js
CHANGED
|
@@ -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,
|
|
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
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
353
|
-
|
|
354
|
-
|
|
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",
|
package/dist/rpc-handler.js
CHANGED
|
@@ -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
|
|
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
|
-
// ──
|
|
197
|
-
if (req.method === "
|
|
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
|
|
208
|
-
const notifBody =
|
|
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
|
|
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
|
-
// ──
|
|
231
|
-
if (req.method === "
|
|
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
|
|
238
|
-
const runId =
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
// ──
|
|
275
|
-
if (req.method === "
|
|
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
|
|
280
|
+
const body = await readBody(req);
|
|
281
|
+
const { taskId } = JSON.parse(body);
|
|
282
282
|
if (!taskId) {
|
|
283
|
-
sendJson(res, 400, { error: "taskId
|
|
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
|
-
// ──
|
|
305
|
-
if (req.method === "
|
|
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
|
|
312
|
-
const taskName =
|
|
313
|
-
|
|
314
|
-
|
|
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
|
@@ -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** —
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
package/src/agents/claude.ts
CHANGED
|
@@ -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" + (
|
|
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 ?? [])];
|
package/src/agents/codex.ts
CHANGED
|
@@ -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" + (
|
|
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
|
|
package/src/agents/copilot.ts
CHANGED
|
@@ -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" + (
|
|
17
|
-
const args = ["-p", prompt
|
|
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
|
-
|
|
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
|
}
|
package/src/agents/gemini.ts
CHANGED
|
@@ -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
|
|
17
|
-
const
|
|
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
|
|
package/src/agents/openclaw.ts
CHANGED
|
@@ -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" + (
|
|
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.
|
|
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
|
|
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: <
|
|
8
|
+
task_name: <concise label, 3-6 words>
|
|
7
9
|
---
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
**Plan body:**
|
|
12
|
-
|
|
13
|
-
### 1. Goal
|
|
14
|
-
What the task accomplishes and the expected end state.
|
|
11
|
+
<plan body>
|
|
15
12
|
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
If the task produces formatted output (report, email, etc.), specify structure, sections,
|
|
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
|
-
|
|
20
|
+
## Task Description
|
|
23
21
|
|
|
24
|
-
**Task description:**
|
package/src/commands/run.ts
CHANGED
|
@@ -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,
|
|
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
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
437
|
-
|
|
438
|
-
|
|
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",
|
package/src/rpc-handler.ts
CHANGED
|
@@ -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
|
|
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
|
-
// ──
|
|
220
|
+
// ── POST /notify — send push notification via NATS ─────────────────
|
|
221
221
|
|
|
222
|
-
if (req.method === "
|
|
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
|
|
228
|
-
const notifBody =
|
|
229
|
-
if (!title || !notifBody) { sendJson(res, 400, { error: "title and body
|
|
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
|
-
// ──
|
|
248
|
+
// ── POST /request-input — held connection until user responds ────────
|
|
249
249
|
|
|
250
|
-
if (req.method === "
|
|
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
|
|
254
|
-
const runId =
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
// ──
|
|
294
|
+
// ── POST /request-confirmation — held connection ────────────────────
|
|
294
295
|
|
|
295
|
-
if (req.method === "
|
|
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
|
|
299
|
-
|
|
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
|
-
// ──
|
|
324
|
+
// ── POST /request-permission — held connection ──────────────────────
|
|
323
325
|
|
|
324
|
-
if (req.method === "
|
|
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
|
|
328
|
-
const taskName =
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (
|
|
332
|
-
|
|
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
|
|