palmier 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/agents/agent-instructions.md +7 -11
  2. package/dist/agents/agent.d.ts +8 -3
  3. package/dist/agents/agent.js +7 -1
  4. package/dist/agents/claude.d.ts +2 -1
  5. package/dist/agents/claude.js +10 -5
  6. package/dist/agents/codex.d.ts +2 -1
  7. package/dist/agents/codex.js +10 -6
  8. package/dist/agents/copilot.d.ts +2 -1
  9. package/dist/agents/copilot.js +10 -3
  10. package/dist/agents/gemini.d.ts +2 -1
  11. package/dist/agents/gemini.js +11 -7
  12. package/dist/agents/kimi.d.ts +9 -0
  13. package/dist/agents/kimi.js +35 -0
  14. package/dist/agents/openclaw.d.ts +2 -1
  15. package/dist/agents/openclaw.js +3 -1
  16. package/dist/agents/qwen.d.ts +9 -0
  17. package/dist/agents/qwen.js +32 -0
  18. package/dist/agents/shared-prompt.d.ts +1 -1
  19. package/dist/agents/shared-prompt.js +6 -2
  20. package/dist/commands/run.js +12 -3
  21. package/dist/rpc-handler.js +7 -3
  22. package/dist/task.d.ts +13 -3
  23. package/dist/task.js +39 -7
  24. package/dist/transports/http-transport.js +23 -6
  25. package/dist/types.d.ts +1 -0
  26. package/package.json +1 -1
  27. package/src/agents/agent-instructions.md +7 -11
  28. package/src/agents/agent.ts +16 -4
  29. package/src/agents/claude.ts +11 -6
  30. package/src/agents/codex.ts +11 -7
  31. package/src/agents/copilot.ts +10 -4
  32. package/src/agents/gemini.ts +12 -8
  33. package/src/agents/kimi.ts +37 -0
  34. package/src/agents/openclaw.ts +4 -2
  35. package/src/agents/qwen.ts +34 -0
  36. package/src/agents/shared-prompt.ts +6 -2
  37. package/src/commands/run.ts +14 -3
  38. package/src/rpc-handler.ts +9 -3
  39. package/src/task.ts +43 -8
  40. package/src/transports/http-transport.ts +25 -6
  41. package/src/types.ts +1 -0
  42. package/test/agent-instructions.test.ts +31 -0
@@ -3,7 +3,8 @@ import * as os from "os";
3
3
  import { StringCodec } from "nats";
4
4
  import { validateSession, addSession } from "../session-store.js";
5
5
  import { registerPending } from "../pending-requests.js";
6
- import { getTaskDir, parseTaskFile, appendRunMessage } from "../task.js";
6
+ import * as fs from "node:fs";
7
+ import { getTaskDir, parseTaskFile, spliceUserMessage } from "../task.js";
7
8
  const PWA_ORIGIN = "https://app.palmier.me";
8
9
  const assetCache = new Map();
9
10
  /** Paths currently being fetched (dedup concurrent requests). */
@@ -78,6 +79,18 @@ export function detectLanIp() {
78
79
  }
79
80
  return "127.0.0.1";
80
81
  }
82
+ /** Find the latest (highest-numbered) run directory for a task. */
83
+ function findLatestRunId(taskDir) {
84
+ try {
85
+ const dirs = fs.readdirSync(taskDir)
86
+ .filter((f) => /^\d+$/.test(f) && fs.statSync(`${taskDir}/${f}`).isDirectory())
87
+ .sort();
88
+ return dirs.length > 0 ? dirs[dirs.length - 1] : null;
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
81
94
  /**
82
95
  * Start the HTTP transport: server with RPC, SSE, PWA proxy, pairing, and
83
96
  * localhost-only agent endpoints (notify, request-input, confirmation, permission).
@@ -242,6 +255,8 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
242
255
  }
243
256
  const taskDir = getTaskDir(config.projectRoot, taskId);
244
257
  const task = parseTaskFile(taskDir);
258
+ // Resolve runId: use provided value, otherwise find the latest run directory
259
+ const effectiveRunId = runId ?? findLatestRunId(taskDir);
245
260
  const pendingPromise = registerPending(taskId, "input", descriptions);
246
261
  await publishEvent(taskId, {
247
262
  event_type: "input-request",
@@ -250,18 +265,20 @@ export async function startHttpTransport(config, handleRpc, port, nc, pairingCod
250
265
  name: task.frontmatter.name,
251
266
  });
252
267
  const response = await pendingPromise;
268
+ const questionsBlock = "\n\n" + descriptions.map((d) => `**${d}**`).join("\n");
253
269
  if (response.length === 1 && response[0] === "aborted") {
254
270
  await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "aborted" });
255
- if (runId) {
256
- appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Input request aborted.", type: "input" });
271
+ if (effectiveRunId) {
272
+ spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: "Aborted", type: "input" }, questionsBlock);
273
+ await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
257
274
  }
258
275
  sendJson(res, 200, { aborted: true });
259
276
  }
260
277
  else {
261
278
  await publishEvent(taskId, { event_type: "input-resolved", host_id: config.hostId, status: "provided" });
262
- if (runId) {
263
- const lines = descriptions.map((desc, i) => `**${desc}** ${response[i]}`);
264
- appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: lines.join("\n"), type: "input" });
279
+ if (effectiveRunId) {
280
+ spliceUserMessage(taskDir, effectiveRunId, { role: "user", time: Date.now(), content: response.join("\n"), type: "input" }, questionsBlock);
281
+ await publishEvent(taskId, { event_type: "result-updated", run_id: effectiveRunId });
265
282
  }
266
283
  sendJson(res, 200, { values: response });
267
284
  }
package/dist/types.d.ts CHANGED
@@ -19,6 +19,7 @@ export interface TaskFrontmatter {
19
19
  triggers: Trigger[];
20
20
  triggers_enabled: boolean;
21
21
  requires_confirmation: boolean;
22
+ yolo_mode?: boolean;
22
23
  permissions?: RequiredPermission[];
23
24
  command?: string;
24
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palmier",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Palmier host CLI - provisions, executes tasks, and serves NATS RPC",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hongxu Cai",
@@ -2,23 +2,19 @@ You are an AI agent executing a task on behalf of the user via the Palmier platf
2
2
 
3
3
  ## Reporting Output
4
4
 
5
- If you generate report or output files, print each file path on its own line prefixed with [PALMIER_REPORT]:
6
- [PALMIER_REPORT] report.md
7
- [PALMIER_REPORT] summary.md
5
+ If you generate report or output files, print each file path on its own line using this exact format (no code block):
6
+ `[PALMIER_REPORT] <filename>`
8
7
 
9
8
  ## Completion
10
9
 
11
- When you are done, output exactly one of these markers as the very last line:
12
- - Success: [PALMIER_TASK_SUCCESS]
13
- - Failure: [PALMIER_TASK_FAILURE]
14
- Do not wrap them in code blocks or add text on the same line.
10
+ When you are done, output exactly one of these markers as the very last line (no code block, no other text on the same line):
11
+ - Success: `[PALMIER_TASK_SUCCESS]`
12
+ - Failure: `[PALMIER_TASK_FAILURE]`
15
13
 
16
14
  ## Permissions
17
15
 
18
- If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line prefixed with [PALMIER_PERMISSION]:
19
- [PALMIER_PERMISSION] Read | Read file contents from the repository
20
- [PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
21
- [PALMIER_PERMISSION] Write | Write generated output files
16
+ If the task fails because a tool was denied or you lack the required permissions, print each required permission on its own line using this exact format (no code block):
17
+ `[PALMIER_PERMISSION] <tool_name> | <description>`
22
18
 
23
19
  ## HTTP Endpoints
24
20
 
@@ -4,6 +4,8 @@ import { GeminiAgent } from "./gemini.js";
4
4
  import { CodexAgent } from "./codex.js";
5
5
  import { OpenClawAgent } from "./openclaw.js";
6
6
  import { CopilotAgent } from "./copilot.js";
7
+ import { QwenAgent } from "./qwen.js";
8
+ import { KimiAgent } from "./kimi.js";
7
9
 
8
10
  export interface CommandLine {
9
11
  command: string;
@@ -21,9 +23,14 @@ export interface AgentTool {
21
23
  getPlanGenerationCommandLine(prompt: string): CommandLine;
22
24
 
23
25
  /** Return the command and args used to run a task. If followupPrompt is provided, use it instead of the task's prompt,
24
- * and treat it as a continuation of the original run (reuse the same session, etc). extraPermissions are transient
25
- * permissions granted for this run only (not persisted in frontmatter). */
26
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
26
+ * and treat it as a continuation of the original run (reuse the same session, etc).
27
+ * extraPermissions: pass an array of RequiredPermission for transient permissions granted for this run only,
28
+ * or pass `"yolo"` to enable yolo mode (auto-approve all tools, skip permission instructions). */
29
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine;
30
+
31
+ /** Whether this agent supports permission overrides (e.g. --allowedTools).
32
+ * If false, the permissions section is omitted from agent instructions. */
33
+ supportsPermissions: boolean;
27
34
 
28
35
  /** Detect whether the agent CLI is available and perform any agent-specific
29
36
  * initialization. Returns true if the agent was detected and initialized successfully. */
@@ -36,6 +43,8 @@ const agentRegistry: Record<string, AgentTool> = {
36
43
  codex: new CodexAgent(),
37
44
  openclaw: new OpenClawAgent(),
38
45
  copilot: new CopilotAgent(),
46
+ qwen: new QwenAgent(),
47
+ kimi: new KimiAgent(),
39
48
  };
40
49
 
41
50
  const agentLabels: Record<string, string> = {
@@ -44,11 +53,14 @@ const agentLabels: Record<string, string> = {
44
53
  codex: "Codex CLI",
45
54
  openclaw: "OpenClaw",
46
55
  copilot: "Copilot CLI",
56
+ qwen: "Qwen Code",
57
+ kimi: "Kimi Code",
47
58
  };
48
59
 
49
60
  export interface DetectedAgent {
50
61
  key: string;
51
62
  label: string;
63
+ supportsPermissions: boolean;
52
64
  }
53
65
 
54
66
  export async function detectAgents(): Promise<DetectedAgent[]> {
@@ -56,7 +68,7 @@ export async function detectAgents(): Promise<DetectedAgent[]> {
56
68
  for (const [key, agent] of Object.entries(agentRegistry)) {
57
69
  const label = agentLabels[key] ?? key;
58
70
  const ok = await agent.init();
59
- if (ok) detected.push({ key, label });
71
+ if (ok) detected.push({ key, label, supportsPermissions: agent.supportsPermissions });
60
72
  }
61
73
  return detected;
62
74
  }
@@ -5,6 +5,7 @@ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class ClaudeAgent implements AgentTool {
8
+ supportsPermissions = true;
8
9
  getPlanGenerationCommandLine(prompt: string): CommandLine {
9
10
  return {
10
11
  command: "claude",
@@ -12,13 +13,17 @@ export class ClaudeAgent implements AgentTool {
12
13
  };
13
14
  }
14
15
 
15
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
- const args = ["--permission-mode", "acceptEdits", "-p", "--allowedTools", "WebFetch"];
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
19
+ const args = ["--permission-mode", yolo ? "bypassPermissions" : "acceptEdits", "-p"];
18
20
 
19
- const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
20
- for (const p of allPerms) {
21
- args.push("--allowedTools", p.name);
21
+ if (!yolo) {
22
+ args.push("--allowedTools", "WebFetch");
23
+ const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
24
+ for (const p of allPerms) {
25
+ args.push("--allowedTools", p.name);
26
+ }
22
27
  }
23
28
 
24
29
  if (followupPrompt) {args.push("-c");} // continue mode for followups
@@ -5,6 +5,7 @@ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class CodexAgent implements AgentTool {
8
+ supportsPermissions = true;
8
9
  getPlanGenerationCommandLine(prompt: string): CommandLine {
9
10
  return {
10
11
  command: "codex",
@@ -12,15 +13,18 @@ export class CodexAgent implements AgentTool {
12
13
  };
13
14
  }
14
15
 
15
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
19
  // Using danger-full-access until workspace-write is fixed: https://github.com/openai/codex/issues/12572
18
- const args = ["exec", "--full-auto", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
20
+ const args = ["exec", "--skip-git-repo-check", "--sandbox", "danger-full-access"];
19
21
 
20
- const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
21
- for (const p of allPerms) {
22
- args.push("--config");
23
- args.push(`apps.${p.name}.default_tools_approval_mode="approve"`);
22
+ if (!yolo) {
23
+ const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
24
+ for (const p of allPerms) {
25
+ args.push("--config");
26
+ args.push(`apps.${p.name}.default_tools_approval_mode="approve"`);
27
+ }
24
28
  }
25
29
  if (followupPrompt) {args.push("resume", "--last");} // continue mode for followups
26
30
  args.push("-"); // read prompt from stdin
@@ -5,6 +5,7 @@ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class CopilotAgent implements AgentTool {
8
+ supportsPermissions = false;
8
9
  getPlanGenerationCommandLine(prompt: string): CommandLine {
9
10
  return {
10
11
  command: "copilot",
@@ -12,12 +13,17 @@ export class CopilotAgent implements AgentTool {
12
13
  };
13
14
  }
14
15
 
15
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
19
  const args = ["-p", prompt];
18
20
 
19
- const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
20
- args.push(`--allow-tool=${["web_fetch", ...allPerms.map((p) => p.name)].join(",")}`);
21
+ if (yolo) {
22
+ args.push("--yolo");
23
+ } else {
24
+ const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
25
+ args.push(`--allow-tool=${["web_fetch", ...allPerms.map((p) => p.name)].join(",")}`);
26
+ }
21
27
  if (followupPrompt) { args.push("--continue"); }
22
28
  return { command: "copilot", args};
23
29
  }
@@ -5,6 +5,7 @@ import { getAgentInstructions } from "./shared-prompt.js";
5
5
  import { SHELL } from "../platform/index.js";
6
6
 
7
7
  export class GeminiAgent implements AgentTool {
8
+ supportsPermissions = true;
8
9
  getPlanGenerationCommandLine(prompt: string): CommandLine {
9
10
  return {
10
11
  command: "gemini",
@@ -12,21 +13,24 @@ export class GeminiAgent implements AgentTool {
12
13
  };
13
14
  }
14
15
 
15
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
16
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
17
- const args = ["--approval-mode", "auto_edit", "--allowed-tools", "web_fetch"];
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
19
+ const args = ["--approval-mode", yolo ? "yolo" : "auto_edit"];
18
20
 
19
- const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
20
- if (allPerms.length > 0) {
21
+ if (!yolo) {
22
+ const tools = ["run_shell_command", "web_fetch"];
23
+ const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
21
24
  for (const p of allPerms) {
22
- args.push(p.name);
25
+ tools.push(p.name);
23
26
  }
27
+ args.push("--allowed-tools", tools.join(","));
24
28
  }
25
29
 
26
30
  if (followupPrompt) {args.push("--resume");} // continue mode for followups
27
- args.push("--prompt", prompt);
31
+ args.push("--prompt", "-"); // read prompt from stdin to avoid command line length limits
28
32
 
29
- return { command: "gemini", args };
33
+ return { command: "gemini", args, stdin: prompt };
30
34
  }
31
35
 
32
36
  async init(): Promise<boolean> {
@@ -0,0 +1,37 @@
1
+ import type { ParsedTask, RequiredPermission } from "../types.js";
2
+ import { execSync } from "child_process";
3
+ import type { AgentTool, CommandLine } from "./agent.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
+ import { SHELL } from "../platform/index.js";
6
+
7
+ export class KimiAgent implements AgentTool {
8
+ supportsPermissions = false;
9
+ getPlanGenerationCommandLine(prompt: string): CommandLine {
10
+ return {
11
+ command: "kimi",
12
+ args: ["-p", prompt],
13
+ };
14
+ }
15
+
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
19
+ const args = [];
20
+
21
+ if (yolo) {
22
+ args.push("--yolo");
23
+ }
24
+ if (followupPrompt) { args.push("--continue"); }
25
+ args.push("-p", prompt);
26
+ return { command: "kimi", args };
27
+ }
28
+
29
+ async init(): Promise<boolean> {
30
+ try {
31
+ execSync("kimi --version", { stdio: "ignore", shell: SHELL });
32
+ } catch {
33
+ return false;
34
+ }
35
+ return true;
36
+ }
37
+ }
@@ -4,6 +4,7 @@ import type { AgentTool, CommandLine } from "./agent.js";
4
4
  import { getAgentInstructions } from "./shared-prompt.js";
5
5
 
6
6
  export class OpenClawAgent implements AgentTool {
7
+ supportsPermissions = false;
7
8
  getPlanGenerationCommandLine(prompt: string): CommandLine {
8
9
  return {
9
10
  command: "openclaw",
@@ -11,8 +12,9 @@ export class OpenClawAgent implements AgentTool {
11
12
  };
12
13
  }
13
14
 
14
- getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
15
- const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id) + "\n\n" + (task.body || task.frontmatter.user_prompt));
15
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
16
+ const yolo = extraPermissions === "yolo";
17
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
16
18
  // OpenClaw does not support stdin as prompt.
17
19
  const args = ["agent", "--local", "--session-id", task.frontmatter.id, "--message", prompt];
18
20
 
@@ -0,0 +1,34 @@
1
+ import type { ParsedTask, RequiredPermission } from "../types.js";
2
+ import { execSync } from "child_process";
3
+ import type { AgentTool, CommandLine } from "./agent.js";
4
+ import { getAgentInstructions } from "./shared-prompt.js";
5
+ import { SHELL } from "../platform/index.js";
6
+
7
+ export class QwenAgent implements AgentTool {
8
+ supportsPermissions = false;
9
+ getPlanGenerationCommandLine(prompt: string): CommandLine {
10
+ return {
11
+ command: "qwen",
12
+ args: ["-p", prompt],
13
+ };
14
+ }
15
+
16
+ getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[] | "yolo"): CommandLine {
17
+ const yolo = extraPermissions === "yolo";
18
+ const prompt = followupPrompt ?? (getAgentInstructions(task.frontmatter.id, yolo || !this.supportsPermissions) + "\n\n" + (task.body || task.frontmatter.user_prompt));
19
+ const args = ["--approval-mode", yolo ? "yolo" : "auto-edit"];
20
+
21
+ if (followupPrompt) { args.push("-c"); }
22
+ args.push("-p", prompt);
23
+ return { command: "qwen", args };
24
+ }
25
+
26
+ async init(): Promise<boolean> {
27
+ try {
28
+ execSync("qwen --version", { stdio: "ignore", shell: SHELL });
29
+ } catch {
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+ }
@@ -13,11 +13,15 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
13
13
  /**
14
14
  * Agent instructions with the serve daemon's HTTP port and task ID baked in.
15
15
  */
16
- export function getAgentInstructions(taskId: string): string {
16
+ export function getAgentInstructions(taskId: string, skipPermissions?: boolean): string {
17
17
  const port = loadConfig().httpPort ?? 7400;
18
- return AGENT_INSTRUCTIONS_TEMPLATE
18
+ let instructions = AGENT_INSTRUCTIONS_TEMPLATE
19
19
  .replace(/\{\{PORT\}\}/g, String(port))
20
20
  .replace(/\{\{TASK_ID\}\}/g, taskId);
21
+ if (skipPermissions) {
22
+ instructions = instructions.replace(/## Permissions\r?\n[\s\S]*?(?=## |\r?\n---)/m, "");
23
+ }
24
+ return instructions;
21
25
  }
22
26
 
23
27
  export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
@@ -62,7 +62,12 @@ async function invokeAgentWithRetries(
62
62
  }, 500);
63
63
  }
64
64
 
65
- const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, undefined, ctx.transientPermissions);
65
+ const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(
66
+ invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions,
67
+ );
68
+ const truncate = (s: string, max = 100) => s.length > max ? s.slice(0, max) + "…" : s;
69
+ const displayArgs = args.map((a) => truncate(a));
70
+ console.log(`[invoke] ${command} ${displayArgs.join(" ")}${stdin ? ` (stdin: ${truncate(stdin, 100)})` : ""}`);
66
71
  const result = await spawnCommand(command, args, {
67
72
  cwd: getRunDir(ctx.taskDir, ctx.runId),
68
73
  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) },
@@ -531,8 +536,14 @@ export function parsePermissions(output: string): RequiredPermission[] {
531
536
  */
532
537
  export function parseTaskOutcome(output: string): TaskRunningState {
533
538
  const lastChunk = output.slice(-500);
534
- if (lastChunk.includes(TASK_FAILURE_MARKER)) return "failed";
535
- if (lastChunk.includes(TASK_SUCCESS_MARKER)) return "finished";
539
+ const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$`, "gm");
540
+ let last: string | null = null;
541
+ let match;
542
+ while ((match = regex.exec(lastChunk)) !== null) {
543
+ last = match[0];
544
+ }
545
+ if (last === TASK_FAILURE_MARKER) return "failed";
546
+ if (last === TASK_SUCCESS_MARKER) return "finished";
536
547
  return "finished";
537
548
  }
538
549
 
@@ -188,6 +188,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
188
188
  triggers?: Array<{ type: "cron" | "once"; value: string }>;
189
189
  triggers_enabled?: boolean;
190
190
  requires_confirmation?: boolean;
191
+ yolo_mode?: boolean;
191
192
  command?: string;
192
193
  };
193
194
 
@@ -218,6 +219,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
218
219
  triggers: params.triggers ?? [],
219
220
  triggers_enabled: params.triggers_enabled ?? true,
220
221
  requires_confirmation: params.requires_confirmation ?? true,
222
+ ...(params.yolo_mode ? { yolo_mode: true } : {}),
221
223
  ...(params.command ? { command: params.command } : {}),
222
224
  },
223
225
  body,
@@ -238,6 +240,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
238
240
  triggers?: Array<{ type: "cron" | "once"; value: string }>;
239
241
  triggers_enabled?: boolean;
240
242
  requires_confirmation?: boolean;
243
+ yolo_mode?: boolean;
241
244
  command?: string;
242
245
  };
243
246
 
@@ -256,6 +259,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
256
259
  if (params.triggers_enabled !== undefined) existing.frontmatter.triggers_enabled = params.triggers_enabled;
257
260
  if (params.requires_confirmation !== undefined)
258
261
  existing.frontmatter.requires_confirmation = params.requires_confirmation;
262
+ if (params.yolo_mode !== undefined) existing.frontmatter.yolo_mode = params.yolo_mode || undefined;
259
263
  if (params.command !== undefined) {
260
264
  if (params.command) {
261
265
  existing.frontmatter.command = params.command;
@@ -302,6 +306,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
302
306
  user_prompt: string;
303
307
  agent: string;
304
308
  requires_confirmation?: boolean;
309
+ yolo_mode?: boolean;
305
310
  command?: string;
306
311
  };
307
312
 
@@ -317,6 +322,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
317
322
  triggers: [],
318
323
  triggers_enabled: false,
319
324
  requires_confirmation: params.requires_confirmation ?? false,
325
+ ...(params.yolo_mode ? { yolo_mode: true } : {}),
320
326
  ...(params.command ? { command: params.command } : {}),
321
327
  },
322
328
  body: "",
@@ -390,7 +396,7 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
390
396
  // Fire-and-forget: invoke agent inline as a child of the serve process
391
397
  const followupAgent = getAgent(followupTask.frontmatter.agent);
392
398
  const { command: cmd, args: cmdArgs, stdin } = followupAgent.getTaskRunCommandLine(
393
- followupTask, params.message, followupTask.frontmatter.permissions,
399
+ followupTask, params.message, followupTask.frontmatter.yolo_mode ? "yolo" : followupTask.frontmatter.permissions,
394
400
  );
395
401
 
396
402
  // Spawn directly via crossSpawn so we can track and kill the child
@@ -564,8 +570,8 @@ export function createRpcHandler(config: HostConfig, nc?: NatsConnection) {
564
570
  const reports: Array<{ file: string; content?: string; error?: string }> = [];
565
571
  const runDir = path.join(config.projectRoot, "tasks", params.id, params.run_id);
566
572
  for (const file of params.report_files) {
567
- if (!file.endsWith(".md")) {
568
- reports.push({ file, error: "must end with .md" });
573
+ if (!file.endsWith(".md") && !file.endsWith(".txt")) {
574
+ reports.push({ file, error: "must end with .md or .txt" });
569
575
  continue;
570
576
  }
571
577
  const basename = path.basename(file);
package/src/task.ts CHANGED
@@ -202,31 +202,66 @@ export function beginStreamingMessage(
202
202
  const filePath = path.join(taskDir, runId, "TASKRUN.md");
203
203
  const delimiter = `<!-- palmier:message role="assistant" time="${time}" -->`;
204
204
  fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
205
- return new StreamingMessageWriter(filePath, delimiter);
205
+ return new StreamingMessageWriter(filePath);
206
206
  }
207
207
 
208
208
  export class StreamingMessageWriter {
209
- private delimiter: string;
210
- constructor(private filePath: string, delimiter: string) {
211
- this.delimiter = delimiter;
212
- }
209
+ constructor(private filePath: string) {}
213
210
 
214
211
  /** Append a chunk of content to the current message. */
215
212
  write(chunk: string): void {
216
213
  fs.appendFileSync(this.filePath, chunk, "utf-8");
217
214
  }
218
215
 
219
- /** Finalize the message. If attachments are provided, rewrites the delimiter to include them. */
216
+ /** Finalize the message. If attachments are provided, rewrites the last assistant delimiter to include them. */
220
217
  end(attachments?: string[]): void {
221
218
  fs.appendFileSync(this.filePath, "\n\n", "utf-8");
222
219
  if (attachments?.length) {
223
220
  const raw = fs.readFileSync(this.filePath, "utf-8");
224
- const updated = raw.replace(this.delimiter, `${this.delimiter.slice(0, -4)} attachments="${attachments.join(",")}" -->`);
225
- fs.writeFileSync(this.filePath, updated, "utf-8");
221
+ // Find the last assistant delimiter (may differ from the original if spliceUserMessage created a new one)
222
+ const pattern = /<!-- palmier:message role="assistant" time="\d+" -->/g;
223
+ let lastMatch: RegExpExecArray | null = null;
224
+ let m;
225
+ while ((m = pattern.exec(raw)) !== null) lastMatch = m;
226
+ if (lastMatch) {
227
+ const before = raw.slice(0, lastMatch.index);
228
+ const after = raw.slice(lastMatch.index + lastMatch[0].length);
229
+ const updated = before + `${lastMatch[0].slice(0, -4)} attachments="${attachments.join(",")}" -->` + after;
230
+ fs.writeFileSync(this.filePath, updated, "utf-8");
231
+ }
226
232
  }
227
233
  }
228
234
  }
229
235
 
236
+ /**
237
+ * Splice a user message into a running assistant stream.
238
+ * Ends the current assistant block, writes the user message,
239
+ * then opens a new assistant block — all as direct file appends.
240
+ * The existing StreamingMessageWriter keeps working because its
241
+ * write() is just appendFileSync, so subsequent chunks land in
242
+ * the new assistant block.
243
+ */
244
+ export function spliceUserMessage(
245
+ taskDir: string,
246
+ runId: string,
247
+ userMsg: ConversationMessage,
248
+ /** Optional text to append to the current assistant block before ending it. */
249
+ assistantAppend?: string,
250
+ ): void {
251
+ const filePath = path.join(taskDir, runId, "TASKRUN.md");
252
+ // 1. Optionally append to the current assistant block (e.g. the input questions)
253
+ if (assistantAppend) {
254
+ fs.appendFileSync(filePath, assistantAppend, "utf-8");
255
+ }
256
+ // 2. End the current assistant block
257
+ fs.appendFileSync(filePath, "\n\n", "utf-8");
258
+ // 3. Write the user message
259
+ appendRunMessage(taskDir, runId, userMsg);
260
+ // 4. Open a new assistant block for subsequent agent output
261
+ const delimiter = `<!-- palmier:message role="assistant" time="${Date.now()}" -->`;
262
+ fs.appendFileSync(filePath, `${delimiter}\n\n`, "utf-8");
263
+ }
264
+
230
265
  /**
231
266
  * Read conversation messages from a run's TASKRUN.md file.
232
267
  */