palmier 0.4.2 → 0.4.4
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 +18 -30
- package/dist/agents/agent-instructions.md +40 -0
- package/dist/agents/claude.js +2 -8
- package/dist/agents/codex.js +0 -6
- package/dist/agents/copilot.js +0 -20
- package/dist/agents/gemini.js +0 -6
- package/dist/agents/shared-prompt.d.ts +1 -2
- package/dist/agents/shared-prompt.js +5 -18
- package/dist/commands/notify.d.ts +9 -0
- package/dist/commands/notify.js +43 -0
- package/dist/commands/request-input.d.ts +10 -0
- package/dist/commands/request-input.js +49 -0
- package/dist/commands/run.d.ts +4 -5
- package/dist/commands/run.js +90 -105
- package/dist/commands/serve.js +31 -28
- package/dist/index.js +15 -5
- package/dist/platform/linux.js +16 -6
- package/dist/platform/windows.js +54 -14
- package/dist/rpc-handler.js +217 -54
- package/dist/spawn-command.d.ts +1 -1
- package/dist/spawn-command.js +13 -1
- package/dist/task.d.ts +18 -7
- package/dist/task.js +70 -27
- package/dist/types.d.ts +10 -1
- package/package.json +2 -3
- package/src/agents/agent-instructions.md +40 -0
- package/src/agents/claude.ts +2 -7
- package/src/agents/codex.ts +0 -5
- package/src/agents/copilot.ts +0 -19
- package/src/agents/gemini.ts +0 -5
- package/src/agents/shared-prompt.ts +10 -18
- package/src/commands/notify.ts +44 -0
- package/src/commands/request-input.ts +51 -0
- package/src/commands/run.ts +98 -129
- package/src/commands/serve.ts +34 -36
- package/src/index.ts +16 -5
- package/src/platform/linux.ts +17 -7
- package/src/platform/windows.ts +53 -15
- package/src/rpc-handler.ts +244 -57
- package/src/spawn-command.ts +13 -2
- package/src/task.ts +79 -29
- package/src/types.ts +11 -1
- package/dist/commands/mcpserver.d.ts +0 -2
- package/dist/commands/mcpserver.js +0 -93
- package/src/commands/mcpserver.ts +0 -113
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
You are an AI agent executing a task on behalf of the user via the Palmier platform. Follow these instructions carefully.
|
|
2
|
+
|
|
3
|
+
## Reporting Output
|
|
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
|
|
8
|
+
|
|
9
|
+
## Completion
|
|
10
|
+
|
|
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.
|
|
15
|
+
|
|
16
|
+
## Permissions
|
|
17
|
+
|
|
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
|
|
22
|
+
|
|
23
|
+
## CLI Commands
|
|
24
|
+
|
|
25
|
+
You have access to the following palmier CLI commands:
|
|
26
|
+
|
|
27
|
+
**Requesting user input** — If you need any information you do not have (credentials, configuration values, preferences, clarifications, etc.) or the task explicitly asks you to get input from the user, do NOT fail the task. Instead, request it:
|
|
28
|
+
```
|
|
29
|
+
palmier request-input --description "What is the database connection string?" --description "What is the API key?"
|
|
30
|
+
```
|
|
31
|
+
The command blocks until the user responds and prints each value on its own line. If the user aborts, the command exits with a non-zero status.
|
|
32
|
+
|
|
33
|
+
**Sending push notifications** — If you need to send a push notification to the user:
|
|
34
|
+
```
|
|
35
|
+
palmier notify --title "Task Complete" --body "The deployment finished successfully."
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
The task to execute follows below.
|
package/src/agents/claude.ts
CHANGED
|
@@ -13,8 +13,8 @@ export class ClaudeAgent implements AgentTool {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
getTaskRunCommandLine(task: ParsedTask, retryPrompt?: string, extraPermissions?: RequiredPermission[]): CommandLine {
|
|
16
|
-
const prompt = retryPrompt ?? (task.body || task.frontmatter.user_prompt);
|
|
17
|
-
const args = ["--permission-mode", "acceptEdits", "
|
|
16
|
+
const prompt = AGENT_INSTRUCTIONS + "\n\n" + (retryPrompt ?? (task.body || task.frontmatter.user_prompt));
|
|
17
|
+
const args = ["--permission-mode", "acceptEdits", "-p"];
|
|
18
18
|
|
|
19
19
|
const allPerms = [...(task.frontmatter.permissions ?? []), ...(extraPermissions ?? [])];
|
|
20
20
|
for (const p of allPerms) {
|
|
@@ -31,11 +31,6 @@ export class ClaudeAgent implements AgentTool {
|
|
|
31
31
|
} catch {
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
34
|
-
try {
|
|
35
|
-
execSync("claude mcp add --transport stdio palmier --scope user -- palmier mcpserver", { stdio: "ignore", shell: SHELL });
|
|
36
|
-
} catch {
|
|
37
|
-
// MCP registration is best-effort; agent still works without it
|
|
38
|
-
}
|
|
39
34
|
return true;
|
|
40
35
|
}
|
|
41
36
|
}
|
package/src/agents/codex.ts
CHANGED
|
@@ -34,11 +34,6 @@ export class CodexAgent implements AgentTool {
|
|
|
34
34
|
} catch {
|
|
35
35
|
return false;
|
|
36
36
|
}
|
|
37
|
-
try {
|
|
38
|
-
execSync("codex mcp add palmier palmier mcpserver", { stdio: "ignore", shell: SHELL });
|
|
39
|
-
} catch {
|
|
40
|
-
// MCP registration is best-effort; agent still works without it
|
|
41
|
-
}
|
|
42
37
|
return true;
|
|
43
38
|
}
|
|
44
39
|
}
|
package/src/agents/copilot.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import { homedir } from "os";
|
|
4
1
|
import type { ParsedTask, RequiredPermission } from "../types.js";
|
|
5
2
|
import { execSync } from "child_process";
|
|
6
3
|
import type { AgentTool, CommandLine } from "./agent.js";
|
|
@@ -34,22 +31,6 @@ export class CopilotAgent implements AgentTool {
|
|
|
34
31
|
} catch {
|
|
35
32
|
return false;
|
|
36
33
|
}
|
|
37
|
-
// Register Palmier MCP server in ~/.copilot/mcp-config.json
|
|
38
|
-
try {
|
|
39
|
-
const configDir = path.join(homedir(), ".copilot");
|
|
40
|
-
const configFile = path.join(configDir, "mcp-config.json");
|
|
41
|
-
let config: Record<string, unknown> = {};
|
|
42
|
-
if (fs.existsSync(configFile)) {
|
|
43
|
-
config = JSON.parse(fs.readFileSync(configFile, "utf-8")) as Record<string, unknown>;
|
|
44
|
-
}
|
|
45
|
-
const servers = (config.mcpServers ?? {}) as Record<string, unknown>;
|
|
46
|
-
servers.palmier = { command: "palmier", args: ["mcpserver"] };
|
|
47
|
-
config.mcpServers = servers;
|
|
48
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
49
|
-
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), "utf-8");
|
|
50
|
-
} catch {
|
|
51
|
-
// MCP registration is best-effort
|
|
52
|
-
}
|
|
53
34
|
return true;
|
|
54
35
|
}
|
|
55
36
|
}
|
package/src/agents/gemini.ts
CHANGED
|
@@ -35,11 +35,6 @@ export class GeminiAgent implements AgentTool {
|
|
|
35
35
|
} catch {
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
|
-
try {
|
|
39
|
-
execSync("gemini mcp add --scope user palmier palmier mcpserver", { stdio: "ignore", shell: SHELL });
|
|
40
|
-
} catch {
|
|
41
|
-
// MCP registration is best-effort; agent still works without it
|
|
42
|
-
}
|
|
43
38
|
return true;
|
|
44
39
|
}
|
|
45
40
|
}
|
|
@@ -1,28 +1,20 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* Instructions prepended or injected as system prompt for every task invocation.
|
|
3
9
|
* Instructs the agent to output structured markers so palmier can determine
|
|
4
10
|
* the task outcome, report files, and permission/input requests.
|
|
5
11
|
*/
|
|
6
|
-
export const AGENT_INSTRUCTIONS =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
When you are done, output exactly one of these markers as the very last line:
|
|
11
|
-
- Success: [PALMIER_TASK_SUCCESS]
|
|
12
|
-
- Failure: [PALMIER_TASK_FAILURE]
|
|
13
|
-
Do not wrap them in code blocks or add text on the same line.
|
|
14
|
-
|
|
15
|
-
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]: e.g.
|
|
16
|
-
[PALMIER_PERMISSION] Read | Read file contents from the repository
|
|
17
|
-
[PALMIER_PERMISSION] Bash(npm test) | Run the test suite via npm
|
|
18
|
-
[PALMIER_PERMISSION] Write | Write generated output files
|
|
19
|
-
|
|
20
|
-
If the task requires information from the user that you do not have (such as credentials, connection strings, API keys, or configuration values), print each required input on its own line prefixed with [PALMIER_INPUT]: e.g.
|
|
21
|
-
[PALMIER_INPUT] What is the database connection string?
|
|
22
|
-
[PALMIER_INPUT] What is the API key for the external service?`;
|
|
12
|
+
export const AGENT_INSTRUCTIONS = fs.readFileSync(
|
|
13
|
+
path.join(__dirname, "agent-instructions.md"),
|
|
14
|
+
"utf-8",
|
|
15
|
+
);
|
|
23
16
|
|
|
24
17
|
export const TASK_SUCCESS_MARKER = "[PALMIER_TASK_SUCCESS]";
|
|
25
18
|
export const TASK_FAILURE_MARKER = "[PALMIER_TASK_FAILURE]";
|
|
26
19
|
export const TASK_REPORT_PREFIX = "[PALMIER_REPORT]";
|
|
27
20
|
export const TASK_PERMISSION_PREFIX = "[PALMIER_PERMISSION]";
|
|
28
|
-
export const TASK_INPUT_PREFIX = "[PALMIER_INPUT]";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { StringCodec } from "nats";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
import { connectNats } from "../nats-client.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Send a push notification to the user via NATS.
|
|
7
|
+
* Usage: palmier notify --title "Title" --body "Body text"
|
|
8
|
+
*/
|
|
9
|
+
export async function notifyCommand(opts: { title: string; body: string }): Promise<void> {
|
|
10
|
+
const config = loadConfig();
|
|
11
|
+
const nc = await connectNats(config);
|
|
12
|
+
|
|
13
|
+
if (!nc) {
|
|
14
|
+
console.error("Error: NATS connection required for push notifications.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const sc = StringCodec();
|
|
19
|
+
const payload = {
|
|
20
|
+
hostId: config.hostId,
|
|
21
|
+
title: opts.title,
|
|
22
|
+
body: opts.body,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const subject = `host.${config.hostId}.push.send`;
|
|
27
|
+
const reply = await nc.request(subject, sc.encode(JSON.stringify(payload)), {
|
|
28
|
+
timeout: 15_000,
|
|
29
|
+
});
|
|
30
|
+
const result = JSON.parse(sc.decode(reply.data)) as { ok?: boolean; error?: string };
|
|
31
|
+
|
|
32
|
+
if (result.ok) {
|
|
33
|
+
console.log("Push notification sent successfully.");
|
|
34
|
+
} else {
|
|
35
|
+
console.error(`Failed to send push notification: ${result.error}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.error(`Error sending push notification: ${err}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
} finally {
|
|
42
|
+
await nc.drain();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { loadConfig } from "../config.js";
|
|
2
|
+
import { connectNats } from "../nats-client.js";
|
|
3
|
+
import { getTaskDir, parseTaskFile, appendRunMessage } from "../task.js";
|
|
4
|
+
import { requestUserInput, publishInputResolved } from "../user-input.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Request input from the user and print responses to stdout.
|
|
8
|
+
* Usage: palmier request-input --description "Question 1" --description "Question 2"
|
|
9
|
+
*
|
|
10
|
+
* Requires PALMIER_TASK_ID and PALMIER_RUN_DIR environment variables.
|
|
11
|
+
*/
|
|
12
|
+
export async function requestInputCommand(opts: { description: string[] }): Promise<void> {
|
|
13
|
+
const taskId = process.env.PALMIER_TASK_ID;
|
|
14
|
+
if (!taskId) {
|
|
15
|
+
console.error("Error: PALMIER_TASK_ID environment variable is not set.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const config = loadConfig();
|
|
20
|
+
const nc = await connectNats(config);
|
|
21
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
22
|
+
const task = parseTaskFile(taskDir);
|
|
23
|
+
const runId = process.env.PALMIER_RUN_DIR?.split(/[/\\]/).pop();
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const response = await requestUserInput(nc, config, taskId, task.frontmatter.name, taskDir, opts.description);
|
|
27
|
+
await publishInputResolved(nc, config, taskId, response === "aborted" ? "aborted" : "provided");
|
|
28
|
+
|
|
29
|
+
if (response === "aborted") {
|
|
30
|
+
if (runId) {
|
|
31
|
+
appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: "Input request aborted.", type: "input" });
|
|
32
|
+
}
|
|
33
|
+
console.error("User aborted the input request.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (runId) {
|
|
38
|
+
const lines = opts.description.map((desc, i) => `**${desc}** ${response[i]}`);
|
|
39
|
+
appendRunMessage(taskDir, runId, { role: "user", time: Date.now(), content: lines.join("\n"), type: "input" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < opts.description.length; i++) {
|
|
43
|
+
console.log(response[i]);
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(`Error requesting user input: ${err}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
} finally {
|
|
49
|
+
if (nc) await nc.drain();
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/commands/run.ts
CHANGED
|
@@ -4,42 +4,16 @@ 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
|
-
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX
|
|
10
|
+
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
|
|
11
11
|
import type { AgentTool } from "../agents/agent.js";
|
|
12
12
|
import { publishHostEvent } from "../events.js";
|
|
13
|
-
import { waitForUserInput
|
|
13
|
+
import { waitForUserInput } from "../user-input.js";
|
|
14
14
|
import type { HostConfig, ParsedTask, TaskRunningState, RequiredPermission } from "../types.js";
|
|
15
15
|
import type { NatsConnection } from "nats";
|
|
16
16
|
|
|
17
|
-
/**
|
|
18
|
-
* Write a time-stamped RESULT file with frontmatter.
|
|
19
|
-
* Always generated, even for abort/fail.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Update an existing result file with the final outcome.
|
|
24
|
-
*/
|
|
25
|
-
function finalizeResultFile(
|
|
26
|
-
taskDir: string,
|
|
27
|
-
resultFileName: string,
|
|
28
|
-
taskName: string,
|
|
29
|
-
taskSnapshotName: string,
|
|
30
|
-
runningState: string,
|
|
31
|
-
startTime: number,
|
|
32
|
-
endTime: number,
|
|
33
|
-
output: string,
|
|
34
|
-
reportFiles: string[],
|
|
35
|
-
requiredPermissions: RequiredPermission[],
|
|
36
|
-
): void {
|
|
37
|
-
const reportLine = reportFiles.length > 0 ? `\nreport_files: ${reportFiles.join(", ")}` : "";
|
|
38
|
-
const permLines = requiredPermissions.map((p) => `\nrequired_permission: ${p.name} | ${p.description}`).join("");
|
|
39
|
-
const content = `---\ntask_name: ${taskName}\nrunning_state: ${runningState}\nstart_time: ${startTime}\nend_time: ${endTime}\ntask_file: ${taskSnapshotName}${reportLine}${permLines}\n---\n${output}`;
|
|
40
|
-
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
17
|
/**
|
|
44
18
|
* Shared context for agent invocation retry loops.
|
|
45
19
|
* Passed around to avoid threading many individual parameters.
|
|
@@ -48,6 +22,7 @@ interface InvocationContext {
|
|
|
48
22
|
agent: AgentTool;
|
|
49
23
|
task: ParsedTask;
|
|
50
24
|
taskDir: string;
|
|
25
|
+
runId: string;
|
|
51
26
|
guiEnv: Record<string, string>;
|
|
52
27
|
nc: NatsConnection | undefined;
|
|
53
28
|
config: HostConfig;
|
|
@@ -57,10 +32,7 @@ interface InvocationContext {
|
|
|
57
32
|
}
|
|
58
33
|
|
|
59
34
|
interface InvocationResult {
|
|
60
|
-
output: string;
|
|
61
35
|
outcome: TaskRunningState;
|
|
62
|
-
reportFiles: string[];
|
|
63
|
-
requiredPermissions: RequiredPermission[];
|
|
64
36
|
}
|
|
65
37
|
|
|
66
38
|
/**
|
|
@@ -79,8 +51,8 @@ async function invokeAgentWithRetry(
|
|
|
79
51
|
while (true) {
|
|
80
52
|
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, retryPrompt, ctx.transientPermissions);
|
|
81
53
|
const result = await spawnCommand(command, args, {
|
|
82
|
-
cwd: ctx.taskDir,
|
|
83
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id },
|
|
54
|
+
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
55
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
|
|
84
56
|
echoStdout: true,
|
|
85
57
|
resolveOnFailure: true,
|
|
86
58
|
stdin,
|
|
@@ -90,13 +62,27 @@ async function invokeAgentWithRetry(
|
|
|
90
62
|
const reportFiles = parseReportFiles(result.output);
|
|
91
63
|
const requiredPermissions = parsePermissions(result.output);
|
|
92
64
|
|
|
65
|
+
// Append assistant message for this invocation
|
|
66
|
+
await appendAndNotify(ctx, {
|
|
67
|
+
role: "assistant",
|
|
68
|
+
time: Date.now(),
|
|
69
|
+
content: stripPalmierMarkers(result.output),
|
|
70
|
+
attachments: reportFiles.length > 0 ? reportFiles : undefined,
|
|
71
|
+
});
|
|
72
|
+
|
|
93
73
|
// Permission retry
|
|
94
74
|
if (outcome === "failed" && requiredPermissions.length > 0) {
|
|
95
75
|
const response = await requestPermission(ctx.nc, ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
|
|
96
76
|
await publishPermissionResolved(ctx.nc, ctx.config, ctx.taskId, response);
|
|
97
77
|
|
|
98
78
|
if (response === "aborted") {
|
|
99
|
-
|
|
79
|
+
await appendAndNotify(ctx, {
|
|
80
|
+
role: "user",
|
|
81
|
+
time: Date.now(),
|
|
82
|
+
content: "Permissions denied. Task aborted.",
|
|
83
|
+
type: "permission",
|
|
84
|
+
});
|
|
85
|
+
return { outcome: "failed" };
|
|
100
86
|
}
|
|
101
87
|
|
|
102
88
|
const newPerms = requiredPermissions.filter(
|
|
@@ -104,6 +90,14 @@ async function invokeAgentWithRetry(
|
|
|
104
90
|
&& !ctx.transientPermissions.some((ep) => ep.name === rp.name),
|
|
105
91
|
);
|
|
106
92
|
|
|
93
|
+
// Append user message for permission grant
|
|
94
|
+
await appendAndNotify(ctx, {
|
|
95
|
+
role: "user",
|
|
96
|
+
time: Date.now(),
|
|
97
|
+
content: `Permissions granted: ${newPerms.map((p) => p.name).join(", ")}`,
|
|
98
|
+
type: "permission",
|
|
99
|
+
});
|
|
100
|
+
|
|
107
101
|
if (response === "granted_all") {
|
|
108
102
|
ctx.task.frontmatter.permissions = [...(ctx.task.frontmatter.permissions ?? []), ...newPerms];
|
|
109
103
|
invokeTask.frontmatter.permissions = ctx.task.frontmatter.permissions;
|
|
@@ -116,36 +110,41 @@ async function invokeAgentWithRetry(
|
|
|
116
110
|
continue;
|
|
117
111
|
}
|
|
118
112
|
|
|
119
|
-
// Input retry
|
|
120
|
-
const inputRequests = parseInputRequests(result.output);
|
|
121
|
-
if (outcome === "failed" && inputRequests.length > 0) {
|
|
122
|
-
const response = await requestUserInput(ctx.nc, ctx.config, ctx.taskId, ctx.task.frontmatter.name, ctx.taskDir, inputRequests);
|
|
123
|
-
await publishInputResolved(ctx.nc, ctx.config, ctx.taskId, response === "aborted" ? "aborted" : "provided");
|
|
124
|
-
|
|
125
|
-
if (response === "aborted") {
|
|
126
|
-
return { output: result.output, outcome: "failed", reportFiles, requiredPermissions };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const inputLines = inputRequests.map((desc, i) => `- ${desc} → ${response[i]}`).join("\n");
|
|
130
|
-
retryPrompt = `The user provided the following inputs:\n${inputLines}\nPlease continue with these values.`;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
113
|
// Normal completion (success or non-retryable failure)
|
|
135
|
-
return {
|
|
114
|
+
return { outcome };
|
|
136
115
|
}
|
|
137
116
|
}
|
|
138
117
|
|
|
139
118
|
/**
|
|
140
|
-
*
|
|
119
|
+
* Strip [PALMIER_*] marker lines from agent output.
|
|
141
120
|
*/
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
121
|
+
export function stripPalmierMarkers(output: string): string {
|
|
122
|
+
return output.split("\n").filter((l) => !l.startsWith("[PALMIER")).join("\n").trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Append a conversation message to the RESULT file and notify connected clients.
|
|
127
|
+
*/
|
|
128
|
+
async function appendAndNotify(
|
|
129
|
+
ctx: InvocationContext,
|
|
130
|
+
msg: Parameters<typeof appendRunMessage>[2],
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
appendRunMessage(ctx.taskDir, ctx.runId, msg);
|
|
133
|
+
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Find the latest run dir that has no status messages yet (just created by the RPC handler).
|
|
138
|
+
*/
|
|
139
|
+
function findLatestPendingRunId(taskDir: string): string | null {
|
|
140
|
+
const dirs = fs.readdirSync(taskDir)
|
|
141
|
+
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
|
|
142
|
+
.sort();
|
|
143
|
+
if (dirs.length === 0) return null;
|
|
144
|
+
const latest = dirs[dirs.length - 1];
|
|
145
|
+
const messages = readRunMessages(taskDir, latest);
|
|
146
|
+
const hasStatus = messages.some((m) => m.role === "status");
|
|
147
|
+
return hasStatus ? null : latest;
|
|
149
148
|
}
|
|
150
149
|
|
|
151
150
|
/**
|
|
@@ -170,15 +169,11 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
170
169
|
let nc: NatsConnection | undefined;
|
|
171
170
|
const taskName = task.frontmatter.name;
|
|
172
171
|
|
|
173
|
-
//
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// Snapshot the task file at run time
|
|
179
|
-
const taskSnapshotName = `TASK-${startTime}.md`;
|
|
180
|
-
if (!fs.existsSync(path.join(taskDir, taskSnapshotName))) {
|
|
181
|
-
fs.copyFileSync(path.join(taskDir, "TASK.md"), path.join(taskDir, taskSnapshotName));
|
|
172
|
+
// Use existing run dir if just created by RPC, otherwise create a new one
|
|
173
|
+
const existingRunId = findLatestPendingRunId(taskDir);
|
|
174
|
+
const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now());
|
|
175
|
+
if (!existingRunId) {
|
|
176
|
+
appendHistory(config.projectRoot, { task_id: taskId, run_id: runId });
|
|
182
177
|
}
|
|
183
178
|
|
|
184
179
|
const cleanup = async () => {
|
|
@@ -187,15 +182,12 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
187
182
|
}
|
|
188
183
|
};
|
|
189
184
|
|
|
190
|
-
if (!existingResult) {
|
|
191
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
192
|
-
}
|
|
193
|
-
|
|
194
185
|
try {
|
|
195
186
|
nc = await connectNats(config);
|
|
196
187
|
|
|
197
|
-
|
|
198
|
-
|
|
188
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, runId);
|
|
189
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "started" });
|
|
190
|
+
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
199
191
|
|
|
200
192
|
// If requires_confirmation, notify clients and wait
|
|
201
193
|
if (task.frontmatter.requires_confirmation) {
|
|
@@ -204,20 +196,21 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
204
196
|
await publishConfirmResolved(nc, config, taskId, resolvedStatus);
|
|
205
197
|
if (!confirmed) {
|
|
206
198
|
console.log("Task aborted by user.");
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, resultFileName);
|
|
199
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "aborted" });
|
|
200
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, runId);
|
|
210
201
|
await cleanup();
|
|
211
202
|
return;
|
|
212
203
|
}
|
|
213
204
|
console.log("Task confirmed by user.");
|
|
205
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "confirmation" });
|
|
206
|
+
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
214
207
|
}
|
|
215
208
|
|
|
216
209
|
// Shared invocation context
|
|
217
210
|
const guiEnv = getPlatform().getGuiEnv();
|
|
218
211
|
const agent = getAgent(task.frontmatter.agent);
|
|
219
212
|
const ctx: InvocationContext = {
|
|
220
|
-
agent, task, taskDir, guiEnv, nc, config, taskId,
|
|
213
|
+
agent, task, taskDir, runId, guiEnv, nc, config, taskId,
|
|
221
214
|
transientPermissions: [],
|
|
222
215
|
};
|
|
223
216
|
|
|
@@ -225,36 +218,34 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
225
218
|
// Command-triggered mode
|
|
226
219
|
const result = await runCommandTriggeredMode(ctx);
|
|
227
220
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
228
|
-
|
|
229
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName,
|
|
221
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
222
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
230
223
|
console.log(`Task ${taskId} completed (command-triggered).`);
|
|
231
224
|
} else {
|
|
232
|
-
// Standard execution
|
|
225
|
+
// Standard execution — add user prompt as first message
|
|
226
|
+
await appendAndNotify(ctx, {
|
|
227
|
+
role: "user",
|
|
228
|
+
time: Date.now(),
|
|
229
|
+
content: task.body || task.frontmatter.user_prompt,
|
|
230
|
+
});
|
|
231
|
+
|
|
233
232
|
const result = await invokeAgentWithRetry(ctx, task);
|
|
234
233
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
238
|
-
|
|
239
|
-
if (result.reportFiles.length > 0) {
|
|
240
|
-
await publishHostEvent(nc, config.hostId, taskId, {
|
|
241
|
-
event_type: "report-generated",
|
|
242
|
-
name: taskName,
|
|
243
|
-
report_files: result.reportFiles,
|
|
244
|
-
running_state: outcome,
|
|
245
|
-
result_file: resultFileName,
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
234
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
235
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
249
236
|
console.log(`Task ${taskId} completed.`);
|
|
250
237
|
}
|
|
251
238
|
} catch (err) {
|
|
252
239
|
console.error(`Task ${taskId} failed:`, err);
|
|
253
|
-
const endTime = Date.now();
|
|
254
240
|
const outcome = resolveOutcome(taskDir, "failed");
|
|
255
241
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
256
|
-
|
|
257
|
-
|
|
242
|
+
appendRunMessage(taskDir, runId, {
|
|
243
|
+
role: "assistant",
|
|
244
|
+
time: Date.now(),
|
|
245
|
+
content: errorMsg,
|
|
246
|
+
});
|
|
247
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
248
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
258
249
|
process.exitCode = 1;
|
|
259
250
|
} finally {
|
|
260
251
|
await cleanup();
|
|
@@ -275,13 +266,13 @@ const MAX_LINE_LENGTH = 200_000;
|
|
|
275
266
|
*/
|
|
276
267
|
async function runCommandTriggeredMode(
|
|
277
268
|
ctx: InvocationContext,
|
|
278
|
-
): Promise<{ outcome: TaskRunningState; endTime: number
|
|
269
|
+
): Promise<{ outcome: TaskRunningState; endTime: number }> {
|
|
279
270
|
const commandStr = ctx.task.frontmatter.command!;
|
|
280
271
|
console.log(`[command-triggered] Spawning: ${commandStr}`);
|
|
281
272
|
|
|
282
273
|
const child = spawnStreamingCommand(commandStr, {
|
|
283
|
-
cwd: ctx.taskDir,
|
|
284
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id },
|
|
274
|
+
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
275
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
|
|
285
276
|
});
|
|
286
277
|
|
|
287
278
|
let linesProcessed = 0;
|
|
@@ -293,7 +284,7 @@ async function runCommandTriggeredMode(
|
|
|
293
284
|
let commandExited = false;
|
|
294
285
|
let resolveWhenDone: (() => void) | undefined;
|
|
295
286
|
|
|
296
|
-
const logPath = path.join(ctx.taskDir, "command-output.log");
|
|
287
|
+
const logPath = path.join(getRunDir(ctx.taskDir, ctx.runId), "command-output.log");
|
|
297
288
|
function appendLog(line: string, agentOutput: string, outcome: string) {
|
|
298
289
|
const entry = `[${new Date().toISOString()}] (${outcome}) input: ${line}\n${agentOutput}\n---\n`;
|
|
299
290
|
fs.appendFileSync(logPath, entry, "utf-8");
|
|
@@ -331,7 +322,7 @@ async function runCommandTriggeredMode(
|
|
|
331
322
|
} else {
|
|
332
323
|
invocationsFailed++;
|
|
333
324
|
}
|
|
334
|
-
appendLog(line,
|
|
325
|
+
appendLog(line, "", result.outcome);
|
|
335
326
|
}
|
|
336
327
|
|
|
337
328
|
async function drainQueue(): Promise<void> {
|
|
@@ -390,15 +381,7 @@ async function runCommandTriggeredMode(
|
|
|
390
381
|
}
|
|
391
382
|
|
|
392
383
|
const endTime = Date.now();
|
|
393
|
-
|
|
394
|
-
`Command: ${commandStr}`,
|
|
395
|
-
`Exit code: ${exitCode}`,
|
|
396
|
-
`Lines processed: ${linesProcessed}`,
|
|
397
|
-
`Agent invocations succeeded: ${invocationsSucceeded}`,
|
|
398
|
-
`Agent invocations failed: ${invocationsFailed}`,
|
|
399
|
-
].join("\n");
|
|
400
|
-
|
|
401
|
-
return { outcome: "finished", endTime, output: summary };
|
|
384
|
+
return { outcome: "finished", endTime };
|
|
402
385
|
}
|
|
403
386
|
|
|
404
387
|
async function publishTaskEvent(
|
|
@@ -408,16 +391,17 @@ async function publishTaskEvent(
|
|
|
408
391
|
taskId: string,
|
|
409
392
|
eventType: TaskRunningState,
|
|
410
393
|
taskName?: string,
|
|
411
|
-
|
|
394
|
+
runId?: string,
|
|
412
395
|
): Promise<void> {
|
|
413
396
|
writeTaskStatus(taskDir, {
|
|
414
397
|
running_state: eventType,
|
|
415
398
|
time_stamp: Date.now(),
|
|
399
|
+
...(eventType === "started" ? { pid: process.pid } : {}),
|
|
416
400
|
});
|
|
417
401
|
|
|
418
402
|
const payload: Record<string, unknown> = { event_type: "running-state", running_state: eventType };
|
|
419
403
|
if (taskName) payload.name = taskName;
|
|
420
|
-
if (
|
|
404
|
+
if (runId) payload.run_id = runId;
|
|
421
405
|
await publishHostEvent(nc, config.hostId, taskId, payload);
|
|
422
406
|
}
|
|
423
407
|
|
|
@@ -534,21 +518,6 @@ export function parsePermissions(output: string): RequiredPermission[] {
|
|
|
534
518
|
return perms;
|
|
535
519
|
}
|
|
536
520
|
|
|
537
|
-
/**
|
|
538
|
-
* Extract user input requests from agent output.
|
|
539
|
-
* Looks for lines matching: [PALMIER_INPUT] <description>
|
|
540
|
-
*/
|
|
541
|
-
export function parseInputRequests(output: string): string[] {
|
|
542
|
-
const regex = new RegExp(`^\\${TASK_INPUT_PREFIX}\\s+(.+)$`, "gm");
|
|
543
|
-
const inputs: string[] = [];
|
|
544
|
-
let match;
|
|
545
|
-
while ((match = regex.exec(output)) !== null) {
|
|
546
|
-
const desc = match[1].trim();
|
|
547
|
-
if (desc) inputs.push(desc);
|
|
548
|
-
}
|
|
549
|
-
return inputs;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
521
|
/**
|
|
553
522
|
* Parse the agent's output for success/failure markers.
|
|
554
523
|
* Falls back to "finished" if no marker is found.
|