palmier 0.4.3 → 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 +7 -4
- package/dist/agents/agent.d.ts +2 -2
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claude.js +4 -4
- package/dist/agents/codex.d.ts +1 -1
- package/dist/agents/codex.js +4 -4
- package/dist/agents/copilot.d.ts +1 -1
- package/dist/agents/copilot.js +3 -3
- package/dist/agents/gemini.d.ts +1 -1
- package/dist/agents/gemini.js +4 -4
- package/dist/agents/openclaw.d.ts +1 -1
- package/dist/agents/openclaw.js +2 -2
- package/dist/commands/request-input.d.ts +1 -2
- package/dist/commands/request-input.js +7 -21
- package/dist/commands/run.d.ts +4 -0
- package/dist/commands/run.js +57 -64
- package/dist/commands/serve.js +31 -33
- package/dist/platform/linux.js +16 -6
- package/dist/platform/windows.js +37 -12
- package/dist/rpc-handler.js +177 -30
- package/dist/task.d.ts +13 -13
- package/dist/task.js +59 -51
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/agents/agent.ts +2 -2
- package/src/agents/claude.ts +3 -3
- package/src/agents/codex.ts +3 -3
- package/src/agents/copilot.ts +3 -3
- package/src/agents/gemini.ts +3 -3
- package/src/agents/openclaw.ts +2 -2
- package/src/commands/request-input.ts +7 -21
- package/src/commands/run.ts +57 -67
- package/src/commands/serve.ts +34 -41
- package/src/platform/linux.ts +17 -7
- package/src/platform/windows.ts +36 -13
- package/src/rpc-handler.ts +195 -34
- package/src/task.ts +60 -55
- package/src/types.ts +2 -2
- package/test/agent-output-parsing.test.ts +1 -14
package/README.md
CHANGED
|
@@ -127,11 +127,12 @@ palmier restart
|
|
|
127
127
|
- **Tasks** are stored locally as Markdown files in a `tasks/` directory. Each task has a name, prompt, execution plan, and optional schedules (cron schedules or one-time dates).
|
|
128
128
|
- **Plan generation** is automatic — when you create or update a task, the host invokes your chosen agent CLI to generate an execution plan and name.
|
|
129
129
|
- **Schedules** are backed by systemd timers (Linux) or Task Scheduler (Windows). You can enable/disable them without deleting the task, and any task can still be run manually at any time.
|
|
130
|
-
- **Task execution** uses the system scheduler on both platforms — `systemctl --user start` on Linux, `schtasks /run` on 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.
|
|
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
|
-
- **
|
|
134
|
-
- **
|
|
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
|
|
|
@@ -159,6 +160,8 @@ src/
|
|
|
159
160
|
events.ts # Event broadcasting (NATS pub/sub or HTTP SSE)
|
|
160
161
|
agents/
|
|
161
162
|
agent.ts # AgentTool interface, registry, and agent detection
|
|
163
|
+
shared-prompt.ts # Agent instructions loader
|
|
164
|
+
agent-instructions.md # System prompt injected into every agent invocation
|
|
162
165
|
claude.ts # Claude Code agent implementation
|
|
163
166
|
gemini.ts # Gemini CLI agent implementation
|
|
164
167
|
codex.ts # Codex CLI agent implementation
|
package/dist/agents/agent.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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>;
|
package/dist/agents/claude.d.ts
CHANGED
|
@@ -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,
|
|
5
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
|
|
6
6
|
init(): Promise<boolean>;
|
|
7
7
|
}
|
|
8
8
|
//# sourceMappingURL=claude.d.ts.map
|
package/dist/agents/claude.js
CHANGED
|
@@ -8,16 +8,16 @@ export class ClaudeAgent {
|
|
|
8
8
|
args: ["-p", prompt],
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
-
getTaskRunCommandLine(task,
|
|
12
|
-
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (
|
|
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 (
|
|
18
|
+
if (followupPrompt) {
|
|
19
19
|
args.push("-c");
|
|
20
|
-
} // continue mode for
|
|
20
|
+
} // continue mode for followups
|
|
21
21
|
return { command: "claude", args, stdin: prompt };
|
|
22
22
|
}
|
|
23
23
|
async init() {
|
package/dist/agents/codex.d.ts
CHANGED
|
@@ -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,
|
|
5
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
|
|
6
6
|
init(): Promise<boolean>;
|
|
7
7
|
}
|
|
8
8
|
//# sourceMappingURL=codex.d.ts.map
|
package/dist/agents/codex.js
CHANGED
|
@@ -8,8 +8,8 @@ export class CodexAgent {
|
|
|
8
8
|
args: ["exec", "--skip-git-repo-check", prompt],
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
-
getTaskRunCommandLine(task,
|
|
12
|
-
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (
|
|
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 (
|
|
21
|
+
if (followupPrompt) {
|
|
22
22
|
args.push("resume", "--last");
|
|
23
|
-
} // continue mode for
|
|
23
|
+
} // continue mode for followups
|
|
24
24
|
return { command: "codex", args, stdin: prompt };
|
|
25
25
|
}
|
|
26
26
|
async init() {
|
package/dist/agents/copilot.d.ts
CHANGED
|
@@ -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,
|
|
5
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
|
|
6
6
|
init(): Promise<boolean>;
|
|
7
7
|
}
|
|
8
8
|
//# sourceMappingURL=copilot.d.ts.map
|
package/dist/agents/copilot.js
CHANGED
|
@@ -8,15 +8,15 @@ export class CopilotAgent {
|
|
|
8
8
|
args: ["-p", prompt],
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
-
getTaskRunCommandLine(task,
|
|
12
|
-
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (
|
|
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 (
|
|
19
|
+
if (followupPrompt) {
|
|
20
20
|
args.push("--continue");
|
|
21
21
|
}
|
|
22
22
|
return { command: "copilot", args };
|
package/dist/agents/gemini.d.ts
CHANGED
|
@@ -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,
|
|
5
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
|
|
6
6
|
init(): Promise<boolean>;
|
|
7
7
|
}
|
|
8
8
|
//# sourceMappingURL=gemini.d.ts.map
|
package/dist/agents/gemini.js
CHANGED
|
@@ -8,8 +8,8 @@ export class GeminiAgent {
|
|
|
8
8
|
args: ["--approval-mode", "auto_edit", "--prompt", prompt],
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
|
-
getTaskRunCommandLine(task,
|
|
12
|
-
const 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 (
|
|
22
|
+
if (followupPrompt) {
|
|
23
23
|
args.push("--resume");
|
|
24
|
-
} // continue mode for
|
|
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,
|
|
5
|
+
getTaskRunCommandLine(task: ParsedTask, followupPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine;
|
|
6
6
|
init(): Promise<boolean>;
|
|
7
7
|
}
|
|
8
8
|
//# sourceMappingURL=openclaw.d.ts.map
|
package/dist/agents/openclaw.js
CHANGED
|
@@ -7,8 +7,8 @@ export class OpenClawAgent {
|
|
|
7
7
|
args: ["agent", "--local", "--agent", "main", "--message", prompt],
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
|
-
getTaskRunCommandLine(task,
|
|
11
|
-
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (
|
|
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 };
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
* Request input from the user and print responses to stdout.
|
|
3
3
|
* Usage: palmier request-input --description "Question 1" --description "Question 2"
|
|
4
4
|
*
|
|
5
|
-
* Requires PALMIER_TASK_ID
|
|
6
|
-
* Outputs each response on its own line: "description: value"
|
|
5
|
+
* Requires PALMIER_TASK_ID and PALMIER_RUN_DIR environment variables.
|
|
7
6
|
*/
|
|
8
7
|
export declare function requestInputCommand(opts: {
|
|
9
8
|
description: string[];
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { loadConfig } from "../config.js";
|
|
2
2
|
import { connectNats } from "../nats-client.js";
|
|
3
|
-
import { getTaskDir, parseTaskFile,
|
|
3
|
+
import { getTaskDir, parseTaskFile, appendRunMessage } from "../task.js";
|
|
4
4
|
import { requestUserInput, publishInputResolved } from "../user-input.js";
|
|
5
5
|
/**
|
|
6
6
|
* Request input from the user and print responses to stdout.
|
|
7
7
|
* Usage: palmier request-input --description "Question 1" --description "Question 2"
|
|
8
8
|
*
|
|
9
|
-
* Requires PALMIER_TASK_ID
|
|
10
|
-
* Outputs each response on its own line: "description: value"
|
|
9
|
+
* Requires PALMIER_TASK_ID and PALMIER_RUN_DIR environment variables.
|
|
11
10
|
*/
|
|
12
11
|
export async function requestInputCommand(opts) {
|
|
13
12
|
const taskId = process.env.PALMIER_TASK_ID;
|
|
@@ -19,33 +18,20 @@ export async function requestInputCommand(opts) {
|
|
|
19
18
|
const nc = await connectNats(config);
|
|
20
19
|
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
21
20
|
const task = parseTaskFile(taskDir);
|
|
21
|
+
const runId = process.env.PALMIER_RUN_DIR?.split(/[/\\]/).pop();
|
|
22
22
|
try {
|
|
23
23
|
const response = await requestUserInput(nc, config, taskId, task.frontmatter.name, taskDir, opts.description);
|
|
24
24
|
await publishInputResolved(nc, config, taskId, response === "aborted" ? "aborted" : "provided");
|
|
25
25
|
if (response === "aborted") {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (resultFile) {
|
|
29
|
-
appendResultMessage(taskDir, resultFile, {
|
|
30
|
-
role: "user",
|
|
31
|
-
time: Date.now(),
|
|
32
|
-
content: "Input request aborted.",
|
|
33
|
-
type: "input",
|
|
34
|
-
});
|
|
26
|
+
if (runId) {
|
|
27
|
+
appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Input request aborted.", type: "input" });
|
|
35
28
|
}
|
|
36
29
|
console.error("User aborted the input request.");
|
|
37
30
|
process.exit(1);
|
|
38
31
|
}
|
|
39
|
-
|
|
40
|
-
const resultFile = process.env.PALMIER_RESULT_FILE;
|
|
41
|
-
if (resultFile) {
|
|
32
|
+
if (runId) {
|
|
42
33
|
const lines = opts.description.map((desc, i) => `**${desc}** ${response[i]}`);
|
|
43
|
-
|
|
44
|
-
role: "user",
|
|
45
|
-
time: Date.now(),
|
|
46
|
-
content: lines.join("\n"),
|
|
47
|
-
type: "input",
|
|
48
|
-
});
|
|
34
|
+
appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: lines.join("\n"), type: "input" });
|
|
49
35
|
}
|
|
50
36
|
for (let i = 0; i < opts.description.length; i++) {
|
|
51
37
|
console.log(response[i]);
|
package/dist/commands/run.d.ts
CHANGED
package/dist/commands/run.js
CHANGED
|
@@ -4,27 +4,27 @@ import * as readline from "readline";
|
|
|
4
4
|
import { spawnCommand, spawnStreamingCommand } from "../spawn-command.js";
|
|
5
5
|
import { loadConfig } from "../config.js";
|
|
6
6
|
import { connectNats } from "../nats-client.js";
|
|
7
|
-
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory,
|
|
7
|
+
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createRunDir, appendRunMessage, readRunMessages, getRunDir } from "../task.js";
|
|
8
8
|
import { getAgent } from "../agents/agent.js";
|
|
9
9
|
import { getPlatform } from "../platform/index.js";
|
|
10
10
|
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
|
|
11
11
|
import { publishHostEvent } from "../events.js";
|
|
12
12
|
import { waitForUserInput } from "../user-input.js";
|
|
13
13
|
/**
|
|
14
|
-
* Invoke the agent CLI with a
|
|
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
|
|
21
|
-
let
|
|
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,
|
|
24
|
+
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, followupPrompt, ctx.transientPermissions);
|
|
25
25
|
const result = await spawnCommand(command, args, {
|
|
26
|
-
cwd: ctx.taskDir,
|
|
27
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id,
|
|
26
|
+
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
27
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
|
|
28
28
|
echoStdout: true,
|
|
29
29
|
resolveOnFailure: true,
|
|
30
30
|
stdin,
|
|
@@ -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
|
|
43
|
-
if (
|
|
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,37 +69,42 @@ async function invokeAgentWithRetry(ctx, invokeTask) {
|
|
|
69
69
|
else {
|
|
70
70
|
ctx.transientPermissions = [...ctx.transientPermissions, ...newPerms];
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
78
|
+
// Normal completion (success or terminal failure)
|
|
76
79
|
return { outcome };
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
/**
|
|
80
83
|
* Strip [PALMIER_*] marker lines from agent output.
|
|
81
84
|
*/
|
|
82
|
-
function stripPalmierMarkers(output) {
|
|
85
|
+
export function stripPalmierMarkers(output) {
|
|
83
86
|
return output.split("\n").filter((l) => !l.startsWith("[PALMIER")).join("\n").trim();
|
|
84
87
|
}
|
|
85
88
|
/**
|
|
86
89
|
* Append a conversation message to the RESULT file and notify connected clients.
|
|
87
90
|
*/
|
|
88
91
|
async function appendAndNotify(ctx, msg) {
|
|
89
|
-
|
|
90
|
-
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated" });
|
|
92
|
+
appendRunMessage(ctx.taskDir, ctx.runId, msg);
|
|
93
|
+
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
91
94
|
}
|
|
92
95
|
/**
|
|
93
|
-
* Find
|
|
96
|
+
* Find the latest run dir that has no status messages yet (just created by the RPC handler).
|
|
94
97
|
*/
|
|
95
|
-
function
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
function findLatestPendingRunId(taskDir) {
|
|
99
|
+
const dirs = fs.readdirSync(taskDir)
|
|
100
|
+
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
|
|
101
|
+
.sort();
|
|
102
|
+
if (dirs.length === 0)
|
|
103
|
+
return null;
|
|
104
|
+
const latest = dirs[dirs.length - 1];
|
|
105
|
+
const messages = readRunMessages(taskDir, latest);
|
|
106
|
+
const hasStatus = messages.some((m) => m.role === "status");
|
|
107
|
+
return hasStatus ? null : latest;
|
|
103
108
|
}
|
|
104
109
|
/**
|
|
105
110
|
* If the RPC handler already wrote "aborted" to status.json (e.g. via task.abort),
|
|
@@ -121,30 +126,22 @@ export async function runCommand(taskId) {
|
|
|
121
126
|
console.log(`Running task: ${taskId}`);
|
|
122
127
|
let nc;
|
|
123
128
|
const taskName = task.frontmatter.name;
|
|
124
|
-
//
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const taskSnapshotName = `TASK-${startTime}.md`;
|
|
130
|
-
if (!fs.existsSync(path.join(taskDir, taskSnapshotName))) {
|
|
131
|
-
fs.copyFileSync(path.join(taskDir, "TASK.md"), path.join(taskDir, taskSnapshotName));
|
|
129
|
+
// Use existing run dir if just created by RPC, otherwise create a new one
|
|
130
|
+
const existingRunId = findLatestPendingRunId(taskDir);
|
|
131
|
+
const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now());
|
|
132
|
+
if (!existingRunId) {
|
|
133
|
+
appendHistory(config.projectRoot, { task_id: taskId, run_id: runId });
|
|
132
134
|
}
|
|
133
135
|
const cleanup = async () => {
|
|
134
136
|
if (nc && !nc.isClosed()) {
|
|
135
137
|
await nc.drain();
|
|
136
138
|
}
|
|
137
139
|
};
|
|
138
|
-
if (!existingResult) {
|
|
139
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
140
|
-
}
|
|
141
140
|
try {
|
|
142
141
|
nc = await connectNats(config);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: "started" });
|
|
147
|
-
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated" });
|
|
142
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, runId);
|
|
143
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "started" });
|
|
144
|
+
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
148
145
|
// If requires_confirmation, notify clients and wait
|
|
149
146
|
if (task.frontmatter.requires_confirmation) {
|
|
150
147
|
const confirmed = await requestConfirmation(nc, config, task, taskDir);
|
|
@@ -152,30 +149,28 @@ export async function runCommand(taskId) {
|
|
|
152
149
|
await publishConfirmResolved(nc, config, taskId, resolvedStatus);
|
|
153
150
|
if (!confirmed) {
|
|
154
151
|
console.log("Task aborted by user.");
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, resultFileName);
|
|
152
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "aborted" });
|
|
153
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, runId);
|
|
158
154
|
await cleanup();
|
|
159
155
|
return;
|
|
160
156
|
}
|
|
161
157
|
console.log("Task confirmed by user.");
|
|
162
|
-
|
|
163
|
-
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated" });
|
|
158
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "confirmation" });
|
|
159
|
+
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
164
160
|
}
|
|
165
161
|
// Shared invocation context
|
|
166
162
|
const guiEnv = getPlatform().getGuiEnv();
|
|
167
163
|
const agent = getAgent(task.frontmatter.agent);
|
|
168
164
|
const ctx = {
|
|
169
|
-
agent, task, taskDir,
|
|
165
|
+
agent, task, taskDir, runId, guiEnv, nc, config, taskId,
|
|
170
166
|
transientPermissions: [],
|
|
171
167
|
};
|
|
172
168
|
if (task.frontmatter.command) {
|
|
173
169
|
// Command-triggered mode
|
|
174
170
|
const result = await runCommandTriggeredMode(ctx);
|
|
175
171
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
172
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
173
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
179
174
|
console.log(`Task ${taskId} completed (command-triggered).`);
|
|
180
175
|
}
|
|
181
176
|
else {
|
|
@@ -185,11 +180,10 @@ export async function runCommand(taskId) {
|
|
|
185
180
|
time: Date.now(),
|
|
186
181
|
content: task.body || task.frontmatter.user_prompt,
|
|
187
182
|
});
|
|
188
|
-
const result = await
|
|
183
|
+
const result = await invokeAgentWithContinuation(ctx, task);
|
|
189
184
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
185
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
186
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
193
187
|
console.log(`Task ${taskId} completed.`);
|
|
194
188
|
}
|
|
195
189
|
}
|
|
@@ -197,14 +191,13 @@ export async function runCommand(taskId) {
|
|
|
197
191
|
console.error(`Task ${taskId} failed:`, err);
|
|
198
192
|
const outcome = resolveOutcome(taskDir, "failed");
|
|
199
193
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
200
|
-
|
|
194
|
+
appendRunMessage(taskDir, runId, {
|
|
201
195
|
role: "assistant",
|
|
202
196
|
time: Date.now(),
|
|
203
197
|
content: errorMsg,
|
|
204
198
|
});
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
199
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
200
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
208
201
|
process.exitCode = 1;
|
|
209
202
|
}
|
|
210
203
|
finally {
|
|
@@ -226,8 +219,8 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
226
219
|
const commandStr = ctx.task.frontmatter.command;
|
|
227
220
|
console.log(`[command-triggered] Spawning: ${commandStr}`);
|
|
228
221
|
const child = spawnStreamingCommand(commandStr, {
|
|
229
|
-
cwd: ctx.taskDir,
|
|
230
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id },
|
|
222
|
+
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
223
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
|
|
231
224
|
});
|
|
232
225
|
let linesProcessed = 0;
|
|
233
226
|
let invocationsSucceeded = 0;
|
|
@@ -236,7 +229,7 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
236
229
|
let processing = false;
|
|
237
230
|
let commandExited = false;
|
|
238
231
|
let resolveWhenDone;
|
|
239
|
-
const logPath = path.join(ctx.taskDir, "command-output.log");
|
|
232
|
+
const logPath = path.join(getRunDir(ctx.taskDir, ctx.runId), "command-output.log");
|
|
240
233
|
function appendLog(line, agentOutput, outcome) {
|
|
241
234
|
const entry = `[${new Date().toISOString()}] (${outcome}) input: ${line}\n${agentOutput}\n---\n`;
|
|
242
235
|
fs.appendFileSync(logPath, entry, "utf-8");
|
|
@@ -265,7 +258,7 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
265
258
|
frontmatter: { ...ctx.task.frontmatter, user_prompt: perLinePrompt },
|
|
266
259
|
body: "",
|
|
267
260
|
};
|
|
268
|
-
const result = await
|
|
261
|
+
const result = await invokeAgentWithContinuation(ctx, perLineTask);
|
|
269
262
|
if (result.outcome === "finished") {
|
|
270
263
|
invocationsSucceeded++;
|
|
271
264
|
}
|
|
@@ -330,7 +323,7 @@ async function runCommandTriggeredMode(ctx) {
|
|
|
330
323
|
const endTime = Date.now();
|
|
331
324
|
return { outcome: "finished", endTime };
|
|
332
325
|
}
|
|
333
|
-
async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName,
|
|
326
|
+
async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName, runId) {
|
|
334
327
|
writeTaskStatus(taskDir, {
|
|
335
328
|
running_state: eventType,
|
|
336
329
|
time_stamp: Date.now(),
|
|
@@ -339,8 +332,8 @@ async function publishTaskEvent(nc, config, taskDir, taskId, eventType, taskName
|
|
|
339
332
|
const payload = { event_type: "running-state", running_state: eventType };
|
|
340
333
|
if (taskName)
|
|
341
334
|
payload.name = taskName;
|
|
342
|
-
if (
|
|
343
|
-
payload.
|
|
335
|
+
if (runId)
|
|
336
|
+
payload.run_id = runId;
|
|
344
337
|
await publishHostEvent(nc, config.hostId, taskId, payload);
|
|
345
338
|
}
|
|
346
339
|
/**
|
package/dist/commands/serve.js
CHANGED
|
@@ -4,7 +4,7 @@ import { loadConfig } from "../config.js";
|
|
|
4
4
|
import { connectNats } from "../nats-client.js";
|
|
5
5
|
import { createRpcHandler } from "../rpc-handler.js";
|
|
6
6
|
import { startNatsTransport } from "../transports/nats-transport.js";
|
|
7
|
-
import { getTaskDir, readTaskStatus, writeTaskStatus,
|
|
7
|
+
import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage } from "../task.js";
|
|
8
8
|
import { publishHostEvent } from "../events.js";
|
|
9
9
|
import { getPlatform } from "../platform/index.js";
|
|
10
10
|
import { detectAgents } from "../agents/agent.js";
|
|
@@ -13,38 +13,11 @@ import { CONFIG_DIR } from "../config.js";
|
|
|
13
13
|
const POLL_INTERVAL_MS = 30_000;
|
|
14
14
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
* and broadcast the failure event.
|
|
18
|
-
*/
|
|
19
|
-
async function markTaskFailed(config, nc, taskId, reason) {
|
|
20
|
-
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
21
|
-
const status = readTaskStatus(taskDir);
|
|
22
|
-
if (!status || status.running_state !== "started")
|
|
23
|
-
return;
|
|
24
|
-
console.log(`[monitor] Task ${taskId} ${reason}, marking as failed.`);
|
|
25
|
-
const endTime = Date.now();
|
|
26
|
-
writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
|
|
27
|
-
let taskName = taskId;
|
|
28
|
-
try {
|
|
29
|
-
const task = parseTaskFile(taskDir);
|
|
30
|
-
taskName = task.frontmatter.name || taskId;
|
|
31
|
-
}
|
|
32
|
-
catch { /* use taskId as fallback */ }
|
|
33
|
-
const resultFileName = `RESULT-${endTime}.md`;
|
|
34
|
-
const content = `---\ntask_name: ${taskName}\nrunning_state: failed\nstart_time: ${status.time_stamp}\nend_time: ${endTime}\ntask_file: \n---\n\n`;
|
|
35
|
-
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
36
|
-
appendResultMessage(taskDir, resultFileName, {
|
|
37
|
-
role: "assistant",
|
|
38
|
-
time: endTime,
|
|
39
|
-
content: reason,
|
|
40
|
-
});
|
|
41
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
42
|
-
const payload = { event_type: "running-state", running_state: "failed", name: taskName };
|
|
43
|
-
await publishHostEvent(nc, config.hostId, taskId, payload);
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Scan all tasks for any stuck in "start" state whose process is no longer alive.
|
|
16
|
+
* Scan all tasks for any stuck in "started" state whose process is no longer alive.
|
|
47
17
|
* Uses the system scheduler (Task Scheduler / systemd) as the authoritative source.
|
|
18
|
+
*
|
|
19
|
+
* Since run.ts creates the RESULT file and history entry at start, we just need to
|
|
20
|
+
* finalize the existing RESULT file, append a failed status entry, and broadcast.
|
|
48
21
|
*/
|
|
49
22
|
async function checkStaleTasks(config, nc) {
|
|
50
23
|
const tasksJsonl = path.join(config.projectRoot, "tasks.jsonl");
|
|
@@ -67,7 +40,32 @@ async function checkStaleTasks(config, nc) {
|
|
|
67
40
|
// Ask the system scheduler if the task is still running
|
|
68
41
|
if (platform.isTaskRunning(taskId))
|
|
69
42
|
continue;
|
|
70
|
-
|
|
43
|
+
console.log(`[monitor] Task ${taskId} process exited unexpectedly, marking as failed.`);
|
|
44
|
+
const endTime = Date.now();
|
|
45
|
+
writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
|
|
46
|
+
// Find the latest run directory (created by run.ts at start)
|
|
47
|
+
const runId = fs.readdirSync(taskDir)
|
|
48
|
+
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
|
|
49
|
+
.sort()
|
|
50
|
+
.pop();
|
|
51
|
+
if (runId) {
|
|
52
|
+
appendRunMessage(taskDir, runId, {
|
|
53
|
+
role: "status",
|
|
54
|
+
time: endTime,
|
|
55
|
+
content: "",
|
|
56
|
+
type: "failed",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
let taskName = taskId;
|
|
60
|
+
try {
|
|
61
|
+
taskName = parseTaskFile(taskDir).frontmatter.name || taskId;
|
|
62
|
+
}
|
|
63
|
+
catch { /* use taskId as fallback */ }
|
|
64
|
+
await publishHostEvent(nc, config.hostId, taskId, {
|
|
65
|
+
event_type: "running-state",
|
|
66
|
+
running_state: "failed",
|
|
67
|
+
name: taskName,
|
|
68
|
+
});
|
|
71
69
|
}
|
|
72
70
|
}
|
|
73
71
|
/**
|