palmier 0.4.4 → 0.4.5

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
@@ -130,8 +130,9 @@ palmier restart
130
130
  - **Task execution** uses the system scheduler on both platforms — `systemctl --user start` on Linux, `schtasks /run` on Windows. On Windows, tasks run via a VBS wrapper (`wscript.exe`) to avoid visible console windows. The daemon polls every 30 seconds to detect crashed tasks (processes that exited without updating status) and marks them as failed, broadcasting the failure to connected clients.
131
131
  - **Command-triggered tasks** — optionally specify a shell command (e.g., `tail -f /var/log/app.log`). Palmier runs the command continuously and invokes the agent for each line of stdout, passing it alongside your prompt. Useful for log monitoring, event-driven automation, and reactive workflows.
132
132
  - **Task confirmation** — tasks can optionally require your approval before running. You'll get a push notification (server mode) or a prompt in the PWA to confirm or abort.
133
- - **Conversational run history** — each run produces a timestamped `RESULT-{ts}.md` file with a conversational structure: a sequence of assistant messages (agent output), user messages (input responses, permission grants, confirmations), and status entries (started, finished, failed, aborted). Timing and outcome are derived from status messages no redundant frontmatter. The PWA displays these as a chat-like thread. Reports are attached per-message.
134
- - **Real-time updates** — task status changes and result updates are pushed to connected PWA clients via NATS pub/sub (server mode) and/or SSE (LAN mode). The run detail view live-updates as the agent produces output.
133
+ - **Conversational run history** — each run gets its own directory (`tasks/<id>/<timestamp>/`) with a `TASKRUN.md` file containing a conversational thread: assistant messages (agent output), user messages (input responses, permission grants, confirmations), and status entries (started, finished, failed, aborted, stopped). The agent runs inside the run directory, so each run's session files and artifacts are isolated. The PWA displays runs as a chat-like thread with follow-up support.
134
+ - **Follow-up messages** — after a task run completes, users can send follow-up messages from the run detail view. The agent is invoked inline by the serve daemon (no new process spawning), and the response is appended to the same conversation thread.
135
+ - **Real-time updates** — task status changes and result updates are pushed to connected PWA clients via NATS pub/sub (server mode) and/or SSE (LAN mode). The run detail view live-updates as the agent produces output. Events are scoped to specific runs.
135
136
  - **Agent CLI commands** — `palmier notify` and `palmier request-input` allow agents to send push notifications and request user input during task execution without requiring MCP support.
136
137
 
137
138
  ## NATS Subjects
@@ -139,7 +140,7 @@ palmier restart
139
140
  | Subject | Direction | Description |
140
141
  |---|---|---|
141
142
  | `host.<hostId>.rpc.<method>` | Client → Host | RPC request/reply (e.g., `task.list`, `task.create`) |
142
- | `host-event.<hostId>.<taskId>` | Host → Client | Real-time task events (`running-state`, `confirm-request`, `permission-request`, `input-request`) |
143
+ | `host-event.<hostId>.<taskId>` | Host → Client | Real-time task events (`running-state`, `result-updated`, `confirm-request`, `permission-request`, `input-request`) |
143
144
  | `host.<hostId>.push.send` | Host → Server | Request server to deliver a push notification |
144
145
  | `pair.<code>` | Client → Host | OTP pairing request/reply |
145
146
 
@@ -12,10 +12,10 @@ export interface CommandLine {
12
12
  export interface AgentTool {
13
13
  /** Return the command and args used to generate a plan from a prompt. */
14
14
  getPlanGenerationCommandLine(prompt: string): CommandLine;
15
- /** Return the command and args used to run a task. If retryPrompt is provided, use it instead of the task's prompt,
15
+ /** Return the command and args used to run a task. If followupPrompt is provided, use it instead of the task's prompt,
16
16
  * and treat it as a continuation of the original run (reuse the same session, etc). extraPermissions are transient
17
17
  * permissions granted for this run only (not persisted in frontmatter). */
18
- getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
18
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
19
19
  /** Detect whether the agent CLI is available and perform any agent-specific
20
20
  * initialization. Returns true if the agent was detected and initialized successfully. */
21
21
  init(): Promise<boolean>;
@@ -2,7 +2,7 @@ import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import type { AgentTool, CommandLine } from "./agent.js";
3
3
  export declare class ClaudeAgent implements AgentTool {
4
4
  getPlanGenerationCommandLine(prompt: string): CommandLine;
5
- getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
5
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
6
6
  init(): Promise<boolean>;
7
7
  }
8
8
  //# sourceMappingURL=claude.d.ts.map
@@ -8,16 +8,16 @@ export class ClaudeAgent {
8
8
  args: ["-p", prompt],
9
9
  };
10
10
  }
11
- getTaskRunCommandLine(task, retryPrompt, extraPermissions) {
12
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
11
+ getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
12
+ const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
13
13
  const args = ["--permission-mode", "acceptEdits", "-p"];
14
14
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
15
15
  for (const p of allPerms) {
16
16
  args.push("--allowedTools", p.name);
17
17
  }
18
- if (retryPrompt) {
18
+ if (followupPrompt) {
19
19
  args.push("-c");
20
- } // continue mode for retries
20
+ } // continue mode for followups
21
21
  return { command: "claude", args, stdin: prompt };
22
22
  }
23
23
  async init() {
@@ -2,7 +2,7 @@ import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import type { AgentTool, CommandLine } from "./agent.js";
3
3
  export declare class CodexAgent implements AgentTool {
4
4
  getPlanGenerationCommandLine(prompt: string): CommandLine;
5
- getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
5
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
6
6
  init(): Promise<boolean>;
7
7
  }
8
8
  //# sourceMappingURL=codex.d.ts.map
@@ -8,8 +8,8 @@ export class CodexAgent {
8
8
  args: ["exec", "--skip-git-repo-check", prompt],
9
9
  };
10
10
  }
11
- getTaskRunCommandLine(task, retryPrompt, extraPermissions) {
12
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
11
+ getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
12
+ const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (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 ?? [])];
@@ -18,9 +18,9 @@ export class CodexAgent {
18
18
  args.push(`apps.${p.name}.default_tools_approval_mode="approve"`);
19
19
  }
20
20
  args.push("-"); // read prompt from stdin
21
- if (retryPrompt) {
21
+ if (followupPrompt) {
22
22
  args.push("resume", "--last");
23
- } // continue mode for retries
23
+ } // continue mode for followups
24
24
  return { command: "codex", args, stdin: prompt };
25
25
  }
26
26
  async init() {
@@ -2,7 +2,7 @@ import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import type { AgentTool, CommandLine } from "./agent.js";
3
3
  export declare class CopilotAgent implements AgentTool {
4
4
  getPlanGenerationCommandLine(prompt: string): CommandLine;
5
- getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
5
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
6
6
  init(): Promise<boolean>;
7
7
  }
8
8
  //# sourceMappingURL=copilot.d.ts.map
@@ -8,15 +8,15 @@ export class CopilotAgent {
8
8
  args: ["-p", prompt],
9
9
  };
10
10
  }
11
- getTaskRunCommandLine(task, retryPrompt, extraPermissions) {
12
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
11
+ getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
12
+ const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
13
13
  const args = ["-p", prompt];
14
14
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
15
15
  if (allPerms.length > 0) {
16
16
  args.push(`--allow-tool='${allPerms.map((p) => p.name).join(",")}'`);
17
17
  ;
18
18
  }
19
- if (retryPrompt) {
19
+ if (followupPrompt) {
20
20
  args.push("--continue");
21
21
  }
22
22
  return { command: "copilot", args };
@@ -2,7 +2,7 @@ import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import type { AgentTool, CommandLine } from "./agent.js";
3
3
  export declare class GeminiAgent implements AgentTool {
4
4
  getPlanGenerationCommandLine(prompt: string): CommandLine;
5
- getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
5
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
6
6
  init(): Promise<boolean>;
7
7
  }
8
8
  //# sourceMappingURL=gemini.d.ts.map
@@ -8,8 +8,8 @@ export class GeminiAgent {
8
8
  args: ["--approval-mode", "auto_edit", "--prompt", prompt],
9
9
  };
10
10
  }
11
- getTaskRunCommandLine(task, retryPrompt, extraPermissions) {
12
- const prompt = retryPrompt ?? (task.body || task.frontmatter.user_prompt);
11
+ getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
12
+ const prompt = followupPrompt ?? (task.body || task.frontmatter.user_prompt);
13
13
  const fullPrompt = AGENT_INSTRUCTIONS + "\n\n" + prompt;
14
14
  const args = ["--prompt", "-"];
15
15
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
@@ -19,9 +19,9 @@ export class GeminiAgent {
19
19
  args.push(p.name);
20
20
  }
21
21
  }
22
- if (retryPrompt) {
22
+ if (followupPrompt) {
23
23
  args.push("--resume");
24
- } // continue mode for retries
24
+ } // continue mode for followups
25
25
  return { command: "gemini", args, stdin: fullPrompt };
26
26
  }
27
27
  async init() {
@@ -2,7 +2,7 @@ import type { ParsedTask, RequiredPermission } from "../types.js";
2
2
  import type { AgentTool, CommandLine } from "./agent.js";
3
3
  export declare class OpenClawAgent implements AgentTool {
4
4
  getPlanGenerationCommandLine(prompt: string): CommandLine;
5
- getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
5
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
6
6
  init(): Promise<boolean>;
7
7
  }
8
8
  //# sourceMappingURL=openclaw.d.ts.map
@@ -7,8 +7,8 @@ export class OpenClawAgent {
7
7
  args: ["agent", "--local", "--agent", "main", "--message", prompt],
8
8
  };
9
9
  }
10
- getTaskRunCommandLine(task, retryPrompt, extraPermissions) {
11
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
10
+ getTaskRunCommandLine(task, followupPrompt, extraPermissions) {
11
+ const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (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 };
@@ -11,17 +11,17 @@ import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERM
11
11
  import { publishHostEvent } from "../events.js";
12
12
  import { waitForUserInput } from "../user-input.js";
13
13
  /**
14
- * Invoke the agent CLI with a retry loop for permissions and user input.
14
+ * Invoke the agent CLI with a continuation loop for permissions and user input.
15
15
  *
16
16
  * Both standard and command-triggered execution use this.
17
17
  * The `invokeTask` is the ParsedTask whose prompt is passed to the agent
18
18
  * (for command-triggered mode this is the per-line augmented task).
19
19
  */
20
- async function invokeAgentWithRetry(ctx, invokeTask) {
21
- let retryPrompt;
20
+ async function invokeAgentWithContinuation(ctx, invokeTask) {
21
+ let followupPrompt;
22
22
  // eslint-disable-next-line no-constant-condition
23
23
  while (true) {
24
- const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, retryPrompt, ctx.transientPermissions);
24
+ const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, followupPrompt, ctx.transientPermissions);
25
25
  const result = await spawnCommand(command, args, {
26
26
  cwd: getRunDir(ctx.taskDir, ctx.runId),
27
27
  env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
@@ -39,8 +39,8 @@ async function invokeAgentWithRetry(ctx, invokeTask) {
39
39
  content: stripPalmierMarkers(result.output),
40
40
  attachments: reportFiles.length > 0 ? reportFiles : undefined,
41
41
  });
42
- // Permission retry
43
- if (outcome === "failed" && requiredPermissions.length > 0) {
42
+ // Permission handling — agent requested permissions
43
+ if (requiredPermissions.length > 0) {
44
44
  const response = await requestPermission(ctx.nc, ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
45
45
  await publishPermissionResolved(ctx.nc, ctx.config, ctx.taskId, response);
46
46
  if (response === "aborted") {
@@ -69,10 +69,13 @@ async function invokeAgentWithRetry(ctx, invokeTask) {
69
69
  else {
70
70
  ctx.transientPermissions = [...ctx.transientPermissions, ...newPerms];
71
71
  }
72
- retryPrompt = "Permissions granted, please continue.";
73
- continue;
72
+ // If the agent actually failed, retry with the new permissions
73
+ if (outcome === "failed") {
74
+ followupPrompt = "Permissions granted, please continue.";
75
+ continue;
76
+ }
74
77
  }
75
- // Normal completion (success or non-retryable failure)
78
+ // Normal completion (success or terminal failure)
76
79
  return { outcome };
77
80
  }
78
81
  }
@@ -177,7 +180,7 @@ export async function runCommand(taskId) {
177
180
  time: Date.now(),
178
181
  content: task.body || task.frontmatter.user_prompt,
179
182
  });
180
- const result = await invokeAgentWithRetry(ctx, task);
183
+ const result = await invokeAgentWithContinuation(ctx, task);
181
184
  const outcome = resolveOutcome(taskDir, result.outcome);
182
185
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
183
186
  await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
@@ -255,7 +258,7 @@ async function runCommandTriggeredMode(ctx) {
255
258
  frontmatter: { ...ctx.task.frontmatter, user_prompt: perLinePrompt },
256
259
  body: "",
257
260
  };
258
- const result = await invokeAgentWithRetry(ctx, perLineTask);
261
+ const result = await invokeAgentWithContinuation(ctx, perLineTask);
259
262
  if (result.outcome === "finished") {
260
263
  invocationsSucceeded++;
261
264
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -20,10 +20,10 @@ export interface AgentTool {
20
20
  /** Return the command and args used to generate a plan from a prompt. */
21
21
  getPlanGenerationCommandLine(prompt: string): CommandLine;
22
22
 
23
- /** Return the command and args used to run a task. If retryPrompt is provided, use it instead of the task's prompt,
23
+ /** Return the command and args used to run a task. If followupPrompt is provided, use it instead of the task's prompt,
24
24
  * and treat it as a continuation of the original run (reuse the same session, etc). extraPermissions are transient
25
25
  * permissions granted for this run only (not persisted in frontmatter). */
26
- getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
26
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
27
27
 
28
28
  /** Detect whether the agent CLI is available and perform any agent-specific
29
29
  * initialization. Returns true if the agent was detected and initialized successfully. */
@@ -12,8 +12,8 @@ export class ClaudeAgent implements AgentTool {
12
12
  };
13
13
  }
14
14
 
15
- getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
15
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
+ const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
17
17
  const args = ["--permission-mode", "acceptEdits", "-p"];
18
18
 
19
19
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
@@ -21,7 +21,7 @@ export class ClaudeAgent implements AgentTool {
21
21
  args.push("--allowedTools", p.name);
22
22
  }
23
23
 
24
- if (retryPrompt) {args.push("-c");} // continue mode for retries
24
+ if (followupPrompt) {args.push("-c");} // continue mode for followups
25
25
  return { command: "claude", args, stdin: prompt };
26
26
  }
27
27
 
@@ -12,8 +12,8 @@ export class CodexAgent implements AgentTool {
12
12
  };
13
13
  }
14
14
 
15
- getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
15
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
+ const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (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
 
@@ -24,7 +24,7 @@ export class CodexAgent implements AgentTool {
24
24
  }
25
25
  args.push("-"); // read prompt from stdin
26
26
 
27
- if (retryPrompt) {args.push("resume", "--last");} // continue mode for retries
27
+ if (followupPrompt) {args.push("resume", "--last");} // continue mode for followups
28
28
  return { command: "codex", args, stdin: prompt };
29
29
  }
30
30
 
@@ -12,8 +12,8 @@ export class CopilotAgent implements AgentTool {
12
12
  };
13
13
  }
14
14
 
15
- getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
15
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
+ const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (task.body || task.frontmatter.user_prompt));
17
17
  const args = ["-p", prompt];
18
18
 
19
19
  const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
@@ -21,7 +21,7 @@ export class CopilotAgent implements AgentTool {
21
21
  args.push(`--allow-tool='${allPerms.map((p) => p.name).join(",")}'`);;
22
22
  }
23
23
 
24
- if (retryPrompt) { args.push("--continue"); }
24
+ if (followupPrompt) { args.push("--continue"); }
25
25
  return { command: "copilot", args};
26
26
  }
27
27
 
@@ -12,8 +12,8 @@ export class GeminiAgent implements AgentTool {
12
12
  };
13
13
  }
14
14
 
15
- getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = retryPrompt ?? (task.body || task.frontmatter.user_prompt);
15
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
+ const prompt = followupPrompt ?? (task.body || task.frontmatter.user_prompt);
17
17
  const fullPrompt = AGENT_INSTRUCTIONS + "\n\n" + prompt;
18
18
  const args = ["--prompt", "-"];
19
19
 
@@ -25,7 +25,7 @@ export class GeminiAgent implements AgentTool {
25
25
  }
26
26
  }
27
27
 
28
- if (retryPrompt) {args.push("--resume");} // continue mode for retries
28
+ if (followupPrompt) {args.push("--resume");} // continue mode for followups
29
29
  return { command: "gemini", args, stdin: fullPrompt };
30
30
  }
31
31
 
@@ -11,8 +11,8 @@ export class OpenClawAgent implements AgentTool {
11
11
  };
12
12
  }
13
13
 
14
- getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
15
- const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
14
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
15
+ const prompt = AGENT_INSTRUCTIONS + "\n\n" + (followupPrompt ?? (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
 
@@ -36,20 +36,20 @@ interface InvocationResult {
36
36
  }
37
37
 
38
38
  /**
39
- * Invoke the agent CLI with a retry loop for permissions and user input.
39
+ * Invoke the agent CLI with a continuation loop for permissions and user input.
40
40
  *
41
41
  * Both standard and command-triggered execution use this.
42
42
  * The `invokeTask` is the ParsedTask whose prompt is passed to the agent
43
43
  * (for command-triggered mode this is the per-line augmented task).
44
44
  */
45
- async function invokeAgentWithRetry(
45
+ async function invokeAgentWithContinuation(
46
46
  ctx: InvocationContext,
47
47
  invokeTask: ParsedTask,
48
48
  ): Promise<InvocationResult> {
49
- let retryPrompt: string | undefined;
49
+ let followupPrompt: string | undefined;
50
50
  // eslint-disable-next-line no-constant-condition
51
51
  while (true) {
52
- const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, retryPrompt, ctx.transientPermissions);
52
+ const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, followupPrompt, ctx.transientPermissions);
53
53
  const result = await spawnCommand(command, args, {
54
54
  cwd: getRunDir(ctx.taskDir, ctx.runId),
55
55
  env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
@@ -70,8 +70,8 @@ async function invokeAgentWithRetry(
70
70
  attachments: reportFiles.length > 0 ? reportFiles : undefined,
71
71
  });
72
72
 
73
- // Permission retry
74
- if (outcome === "failed" && requiredPermissions.length > 0) {
73
+ // Permission handling — agent requested permissions
74
+ if (requiredPermissions.length > 0) {
75
75
  const response = await requestPermission(ctx.nc, ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
76
76
  await publishPermissionResolved(ctx.nc, ctx.config, ctx.taskId, response);
77
77
 
@@ -106,11 +106,14 @@ async function invokeAgentWithRetry(
106
106
  ctx.transientPermissions = [...ctx.transientPermissions, ...newPerms];
107
107
  }
108
108
 
109
- retryPrompt = "Permissions granted, please continue.";
110
- continue;
109
+ // If the agent actually failed, retry with the new permissions
110
+ if (outcome === "failed") {
111
+ followupPrompt = "Permissions granted, please continue.";
112
+ continue;
113
+ }
111
114
  }
112
115
 
113
- // Normal completion (success or non-retryable failure)
116
+ // Normal completion (success or terminal failure)
114
117
  return { outcome };
115
118
  }
116
119
  }
@@ -229,7 +232,7 @@ export async function runCommand(taskId: string): Promise<void> {
229
232
  content: task.body || task.frontmatter.user_prompt,
230
233
  });
231
234
 
232
- const result = await invokeAgentWithRetry(ctx, task);
235
+ const result = await invokeAgentWithContinuation(ctx, task);
233
236
  const outcome = resolveOutcome(taskDir, result.outcome);
234
237
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
235
238
  await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
@@ -316,7 +319,7 @@ async function runCommandTriggeredMode(
316
319
  body: "",
317
320
  };
318
321
 
319
- const result = await invokeAgentWithRetry(ctx, perLineTask);
322
+ const result = await invokeAgentWithContinuation(ctx, perLineTask);
320
323
  if (result.outcome === "finished") {
321
324
  invocationsSucceeded++;
322
325
  } else {
@@ -1,6 +1,6 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { parseTaskOutcome, parseReportFiles, parsePermissions, parseInputRequests } from "../src/commands/run.js";
3
+ import { parseTaskOutcome, parseReportFiles, parsePermissions } from "../src/commands/run.js";
4
4
 
5
5
  describe("parseTaskOutcome", () => {
6
6
  it("returns 'finished' for success marker", () => {
@@ -59,16 +59,3 @@ describe("parsePermissions", () => {
59
59
  });
60
60
  });
61
61
 
62
- describe("parseInputRequests", () => {
63
- it("extracts input descriptions", () => {
64
- const output = "[PALMIER_INPUT] What is the API key?\n[PALMIER_INPUT] Database connection string?";
65
- assert.deepEqual(parseInputRequests(output), [
66
- "What is the API key?",
67
- "Database connection string?",
68
- ]);
69
- });
70
-
71
- it("returns empty array when no inputs", () => {
72
- assert.deepEqual(parseInputRequests("no inputs"), []);
73
- });
74
- });