palmier 0.2.5 → 0.2.6
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/.github/workflows/ci.yml +16 -0
- package/LICENSE +190 -0
- package/README.md +288 -219
- package/dist/agents/agent.d.ts +6 -3
- package/dist/agents/agent.js +2 -0
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claude.js +12 -9
- package/dist/agents/codex.d.ts +1 -1
- package/dist/agents/codex.js +12 -10
- package/dist/agents/gemini.d.ts +1 -1
- package/dist/agents/gemini.js +13 -9
- package/dist/agents/openclaw.d.ts +2 -2
- package/dist/agents/openclaw.js +8 -7
- package/dist/agents/shared-prompt.d.ts +5 -4
- package/dist/agents/shared-prompt.js +10 -8
- package/dist/commands/agents.js +11 -0
- package/dist/commands/init.js +109 -49
- package/dist/commands/mcpserver.js +11 -21
- package/dist/commands/plan-generation.md +24 -32
- package/dist/commands/restart.d.ts +5 -0
- package/dist/commands/restart.js +9 -0
- package/dist/commands/run.js +293 -101
- package/dist/commands/serve.js +83 -4
- package/dist/commands/task-cleanup.d.ts +14 -0
- package/dist/commands/task-cleanup.js +84 -0
- package/dist/events.d.ts +10 -0
- package/dist/events.js +29 -0
- package/dist/index.js +7 -0
- package/dist/platform/linux.d.ts +2 -0
- package/dist/platform/linux.js +22 -1
- package/dist/platform/platform.d.ts +4 -0
- package/dist/platform/windows.d.ts +3 -0
- package/dist/platform/windows.js +99 -82
- package/dist/rpc-handler.d.ts +2 -1
- package/dist/rpc-handler.js +43 -28
- package/dist/spawn-command.d.ts +29 -6
- package/dist/spawn-command.js +38 -15
- package/dist/transports/nats-transport.d.ts +4 -2
- package/dist/transports/nats-transport.js +3 -4
- package/dist/types.d.ts +4 -2
- package/package.json +5 -3
- package/src/agents/agent.ts +8 -3
- package/src/agents/claude.ts +44 -43
- package/src/agents/codex.ts +11 -12
- package/src/agents/gemini.ts +12 -10
- package/src/agents/openclaw.ts +8 -7
- package/src/agents/shared-prompt.ts +10 -8
- package/src/commands/agents.ts +11 -0
- package/src/commands/init.ts +120 -56
- package/src/commands/mcpserver.ts +11 -22
- package/src/commands/plan-generation.md +24 -32
- package/src/commands/restart.ts +9 -0
- package/src/commands/run.ts +365 -119
- package/src/commands/serve.ts +101 -5
- package/src/cross-spawn.d.ts +5 -0
- package/src/events.ts +38 -0
- package/src/index.ts +8 -0
- package/src/platform/linux.ts +25 -1
- package/src/platform/platform.ts +6 -0
- package/src/platform/windows.ts +100 -89
- package/src/rpc-handler.ts +46 -29
- package/src/spawn-command.ts +120 -83
- package/src/transports/nats-transport.ts +4 -4
- package/src/types.ts +4 -2
package/dist/commands/run.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import
|
|
3
|
+
import * as readline from "readline";
|
|
4
|
+
import { spawnCommand, spawnStreamingCommand } from "../spawn-command.js";
|
|
4
5
|
import { loadConfig } from "../config.js";
|
|
5
6
|
import { connectNats } from "../nats-client.js";
|
|
6
7
|
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory } from "../task.js";
|
|
7
8
|
import { getAgent } from "../agents/agent.js";
|
|
8
9
|
import { getPlatform } from "../platform/index.js";
|
|
9
|
-
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
|
|
10
|
-
import {
|
|
10
|
+
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX, TASK_INPUT_PREFIX } from "../agents/shared-prompt.js";
|
|
11
|
+
import { publishHostEvent } from "../events.js";
|
|
11
12
|
/**
|
|
12
13
|
* Write a time-stamped RESULT file with frontmatter.
|
|
13
14
|
* Always generated, even for abort/fail.
|
|
@@ -20,6 +21,74 @@ function writeResult(taskDir, taskName, taskSnapshotName, runningState, startTim
|
|
|
20
21
|
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
21
22
|
return resultFileName;
|
|
22
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Invoke the agent CLI with a retry loop for permissions and user input.
|
|
26
|
+
*
|
|
27
|
+
* Both standard and command-triggered execution use this.
|
|
28
|
+
* The `invokeTask` is the ParsedTask whose prompt is passed to the agent
|
|
29
|
+
* (for command-triggered mode this is the per-line augmented task).
|
|
30
|
+
*/
|
|
31
|
+
async function invokeAgentWithRetry(ctx, invokeTask) {
|
|
32
|
+
let retryPrompt;
|
|
33
|
+
// eslint-disable-next-line no-constant-condition
|
|
34
|
+
while (true) {
|
|
35
|
+
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, retryPrompt, ctx.transientPermissions);
|
|
36
|
+
const result = await spawnCommand(command, args, {
|
|
37
|
+
cwd: ctx.taskDir,
|
|
38
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id },
|
|
39
|
+
echoStdout: true,
|
|
40
|
+
resolveOnFailure: true,
|
|
41
|
+
stdin,
|
|
42
|
+
});
|
|
43
|
+
const outcome = result.exitCode !== 0 ? "failed" : parseTaskOutcome(result.output);
|
|
44
|
+
const reportFiles = parseReportFiles(result.output);
|
|
45
|
+
const requiredPermissions = parsePermissions(result.output);
|
|
46
|
+
// Permission retry
|
|
47
|
+
if (outcome === "failed" && requiredPermissions.length > 0) {
|
|
48
|
+
const response = await requestPermission(ctx.nc, ctx.config, ctx.task, ctx.taskDir, requiredPermissions, ctx.useHttp);
|
|
49
|
+
await publishPermissionResolved(ctx.nc, ctx.config, ctx.taskId, response, ctx.useHttp);
|
|
50
|
+
if (response === "aborted") {
|
|
51
|
+
return { output: result.output, outcome: "failed", reportFiles, requiredPermissions };
|
|
52
|
+
}
|
|
53
|
+
const newPerms = requiredPermissions.filter((rp) => !ctx.task.frontmatter.permissions?.some((ep) => ep.name === rp.name)
|
|
54
|
+
&& !ctx.transientPermissions.some((ep) => ep.name === rp.name));
|
|
55
|
+
if (response === "granted_all") {
|
|
56
|
+
ctx.task.frontmatter.permissions = [...(ctx.task.frontmatter.permissions ?? []), ...newPerms];
|
|
57
|
+
invokeTask.frontmatter.permissions = ctx.task.frontmatter.permissions;
|
|
58
|
+
writeTaskFile(ctx.taskDir, ctx.task);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
ctx.transientPermissions = [...ctx.transientPermissions, ...newPerms];
|
|
62
|
+
}
|
|
63
|
+
retryPrompt = "Permissions granted, please continue.";
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Input retry
|
|
67
|
+
const inputRequests = parseInputRequests(result.output);
|
|
68
|
+
if (outcome === "failed" && inputRequests.length > 0) {
|
|
69
|
+
const response = await requestUserInput(ctx.nc, ctx.config, ctx.task, ctx.taskDir, inputRequests, ctx.useHttp);
|
|
70
|
+
await publishInputResolved(ctx.nc, ctx.config, ctx.taskId, response === "aborted" ? "aborted" : "provided", ctx.useHttp);
|
|
71
|
+
if (response === "aborted") {
|
|
72
|
+
return { output: result.output, outcome: "failed", reportFiles, requiredPermissions };
|
|
73
|
+
}
|
|
74
|
+
const inputLines = inputRequests.map((desc, i) => `- ${desc} → ${response[i]}`).join("\n");
|
|
75
|
+
retryPrompt = `The user provided the following inputs:\n${inputLines}\nPlease continue with these values.`;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Normal completion (success or non-retryable failure)
|
|
79
|
+
return { output: result.output, outcome, reportFiles, requiredPermissions };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* If the RPC handler already wrote "aborted" to status.json (e.g. via task.abort),
|
|
84
|
+
* respect that instead of overwriting with the process's own outcome.
|
|
85
|
+
*/
|
|
86
|
+
function resolveOutcome(taskDir, outcome) {
|
|
87
|
+
const current = readTaskStatus(taskDir);
|
|
88
|
+
if (current?.running_state === "aborted")
|
|
89
|
+
return "aborted";
|
|
90
|
+
return outcome;
|
|
91
|
+
}
|
|
23
92
|
/**
|
|
24
93
|
* Execute a task by ID.
|
|
25
94
|
*/
|
|
@@ -42,24 +111,12 @@ export async function runCommand(taskId) {
|
|
|
42
111
|
await nc.drain();
|
|
43
112
|
}
|
|
44
113
|
};
|
|
45
|
-
// Handle signals
|
|
46
|
-
const onSignal = async () => {
|
|
47
|
-
console.log("Received signal, cleaning up...");
|
|
48
|
-
const endTime = Date.now();
|
|
49
|
-
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName, "abort", startTime, endTime, "", [], []);
|
|
50
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
51
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "abort", useHttp, taskName);
|
|
52
|
-
await cleanup();
|
|
53
|
-
process.exit(1);
|
|
54
|
-
};
|
|
55
|
-
process.on("SIGINT", onSignal);
|
|
56
|
-
process.on("SIGTERM", onSignal);
|
|
57
114
|
try {
|
|
58
115
|
if (useNats) {
|
|
59
116
|
nc = await connectNats(config);
|
|
60
117
|
}
|
|
61
118
|
// Mark as started immediately
|
|
62
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "
|
|
119
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "started", useHttp, taskName);
|
|
63
120
|
// If requires_confirmation, notify clients and wait
|
|
64
121
|
if (task.frontmatter.requires_confirmation) {
|
|
65
122
|
const confirmed = await requestConfirmation(nc, config, task, taskDir, useHttp);
|
|
@@ -68,110 +125,194 @@ export async function runCommand(taskId) {
|
|
|
68
125
|
if (!confirmed) {
|
|
69
126
|
console.log("Task aborted by user.");
|
|
70
127
|
const endTime = Date.now();
|
|
71
|
-
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName, "
|
|
128
|
+
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName, "aborted", startTime, endTime, "", [], []);
|
|
72
129
|
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
73
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "
|
|
130
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", useHttp, taskName);
|
|
74
131
|
await cleanup();
|
|
75
132
|
return;
|
|
76
133
|
}
|
|
77
134
|
console.log("Task confirmed by user.");
|
|
78
135
|
}
|
|
79
|
-
//
|
|
136
|
+
// Shared invocation context
|
|
80
137
|
const guiEnv = getPlatform().getGuiEnv();
|
|
81
138
|
const agent = getAgent(task.frontmatter.agent);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const response = await requestPermission(nc, config, task, taskDir, lastRequiredPermissions, useHttp);
|
|
110
|
-
await publishPermissionResolved(nc, config, taskId, response, useHttp);
|
|
111
|
-
if (response === "aborted") {
|
|
112
|
-
console.log("Permission request aborted by user.");
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
const newPerms = lastRequiredPermissions.filter((rp) => !task.frontmatter.permissions?.some((ep) => ep.name === rp.name)
|
|
116
|
-
&& !transientPermissions.some((ep) => ep.name === rp.name));
|
|
117
|
-
if (response === "granted_all") {
|
|
118
|
-
// Persist permissions to task frontmatter for all future runs
|
|
119
|
-
task.frontmatter.permissions = [...(task.frontmatter.permissions ?? []), ...newPerms];
|
|
120
|
-
writeTaskFile(taskDir, task);
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
// "granted" — allow for this run only
|
|
124
|
-
transientPermissions = [...transientPermissions, ...newPerms];
|
|
125
|
-
}
|
|
126
|
-
console.log(`Permissions granted, retrying task ${taskId}...`);
|
|
127
|
-
retryPrompt = "Permissions granted, please continue.";
|
|
128
|
-
continue;
|
|
139
|
+
const ctx = {
|
|
140
|
+
agent, task, taskDir, guiEnv, nc, config, taskId, useHttp,
|
|
141
|
+
transientPermissions: [],
|
|
142
|
+
};
|
|
143
|
+
if (task.frontmatter.command) {
|
|
144
|
+
// Command-triggered mode
|
|
145
|
+
const result = await runCommandTriggeredMode(ctx);
|
|
146
|
+
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
147
|
+
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName, outcome, startTime, result.endTime, result.output, [], []);
|
|
148
|
+
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
149
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, useHttp, taskName);
|
|
150
|
+
console.log(`Task ${taskId} completed (command-triggered).`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Standard execution
|
|
154
|
+
const result = await invokeAgentWithRetry(ctx, task);
|
|
155
|
+
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
156
|
+
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName, outcome, startTime, Date.now(), result.output, result.reportFiles, result.requiredPermissions);
|
|
157
|
+
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
158
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, useHttp, taskName);
|
|
159
|
+
if (result.reportFiles.length > 0) {
|
|
160
|
+
await publishHostEvent(nc, config, taskId, {
|
|
161
|
+
event_type: "report-generated",
|
|
162
|
+
name: taskName,
|
|
163
|
+
report_files: result.reportFiles,
|
|
164
|
+
running_state: outcome,
|
|
165
|
+
}, useHttp);
|
|
129
166
|
}
|
|
130
|
-
|
|
131
|
-
break;
|
|
167
|
+
console.log(`Task ${taskId} completed.`);
|
|
132
168
|
}
|
|
133
|
-
// Write result and history once after the loop
|
|
134
|
-
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName, lastOutcome, startTime, lastEndTime, lastOutput, lastReportFiles, lastRequiredPermissions);
|
|
135
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
136
|
-
await publishTaskEvent(nc, config, taskDir, taskId, lastOutcome, useHttp, taskName);
|
|
137
|
-
console.log(`Task ${taskId} completed.`);
|
|
138
169
|
}
|
|
139
170
|
catch (err) {
|
|
140
171
|
console.error(`Task ${taskId} failed:`, err);
|
|
141
172
|
const endTime = Date.now();
|
|
173
|
+
const outcome = resolveOutcome(taskDir, "failed");
|
|
142
174
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
143
|
-
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName,
|
|
175
|
+
const resultFileName = writeResult(taskDir, taskName, taskSnapshotName, outcome, startTime, endTime, errorMsg, [], []);
|
|
144
176
|
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
145
|
-
await publishTaskEvent(nc, config, taskDir, taskId,
|
|
177
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, useHttp, taskName);
|
|
146
178
|
process.exitCode = 1;
|
|
147
179
|
}
|
|
148
180
|
finally {
|
|
149
181
|
await cleanup();
|
|
150
182
|
}
|
|
151
183
|
}
|
|
152
|
-
const
|
|
184
|
+
const MAX_QUEUE_SIZE = 100;
|
|
185
|
+
const MAX_LOG_ENTRIES = 1000;
|
|
186
|
+
/** Max input line length (chars). Long emails can take up to 200k chars. */
|
|
187
|
+
const MAX_LINE_LENGTH = 200_000;
|
|
153
188
|
/**
|
|
154
|
-
*
|
|
189
|
+
* Command-triggered execution mode.
|
|
190
|
+
*
|
|
191
|
+
* Spawns a long-running shell command and, for each line of stdout,
|
|
192
|
+
* invokes the agent CLI with the user's prompt augmented by that line.
|
|
193
|
+
* Processes lines sequentially with a bounded queue.
|
|
155
194
|
*/
|
|
156
|
-
async function
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
195
|
+
async function runCommandTriggeredMode(ctx) {
|
|
196
|
+
const commandStr = ctx.task.frontmatter.command;
|
|
197
|
+
console.log(`[command-triggered] Spawning: ${commandStr}`);
|
|
198
|
+
const child = spawnStreamingCommand(commandStr, {
|
|
199
|
+
cwd: ctx.taskDir,
|
|
200
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id },
|
|
201
|
+
});
|
|
202
|
+
// Stats
|
|
203
|
+
let linesProcessed = 0;
|
|
204
|
+
let invocationsSucceeded = 0;
|
|
205
|
+
let invocationsFailed = 0;
|
|
206
|
+
// Bounded queue for incoming lines
|
|
207
|
+
const lineQueue = [];
|
|
208
|
+
let processing = false;
|
|
209
|
+
let commandExited = false;
|
|
210
|
+
let resolveWhenDone;
|
|
211
|
+
// Rolling log of per-line agent outputs
|
|
212
|
+
const logPath = path.join(ctx.taskDir, "command-output.log");
|
|
213
|
+
function appendLog(line, agentOutput, outcome) {
|
|
214
|
+
const entry = `[${new Date().toISOString()}] (${outcome}) input: ${line}\n${agentOutput}\n---\n`;
|
|
215
|
+
fs.appendFileSync(logPath, entry, "utf-8");
|
|
216
|
+
// Trim log if too large (keep last MAX_LOG_ENTRIES entries)
|
|
217
|
+
try {
|
|
218
|
+
const content = fs.readFileSync(logPath, "utf-8");
|
|
219
|
+
const entries = content.split("\n---\n").filter(Boolean);
|
|
220
|
+
if (entries.length > MAX_LOG_ENTRIES) {
|
|
221
|
+
const trimmed = entries.slice(-MAX_LOG_ENTRIES).join("\n---\n") + "\n---\n";
|
|
222
|
+
fs.writeFileSync(logPath, trimmed, "utf-8");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch { /* ignore trim errors */ }
|
|
226
|
+
}
|
|
227
|
+
async function processLine(line) {
|
|
228
|
+
linesProcessed++;
|
|
229
|
+
if (line.length > MAX_LINE_LENGTH) {
|
|
230
|
+
console.warn(`[command-triggered] Skipping line #${linesProcessed}: ${line.length} chars exceeds limit`);
|
|
231
|
+
invocationsFailed++;
|
|
232
|
+
appendLog(line.slice(0, 200) + "...(truncated)", "", "skipped");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
console.log(`[command-triggered] Processing line #${linesProcessed}: ${line}`);
|
|
236
|
+
const perLinePrompt = `${ctx.task.frontmatter.user_prompt}\n\nProcess this input:\n${line}`;
|
|
237
|
+
const perLineTask = {
|
|
238
|
+
frontmatter: { ...ctx.task.frontmatter, user_prompt: perLinePrompt },
|
|
239
|
+
body: "",
|
|
240
|
+
};
|
|
241
|
+
const result = await invokeAgentWithRetry(ctx, perLineTask);
|
|
242
|
+
if (result.outcome === "finished") {
|
|
243
|
+
invocationsSucceeded++;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
invocationsFailed++;
|
|
247
|
+
}
|
|
248
|
+
appendLog(line, result.output, result.outcome);
|
|
161
249
|
}
|
|
162
|
-
|
|
250
|
+
async function drainQueue() {
|
|
251
|
+
if (processing)
|
|
252
|
+
return;
|
|
253
|
+
processing = true;
|
|
163
254
|
try {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
255
|
+
while (lineQueue.length > 0) {
|
|
256
|
+
const line = lineQueue.shift();
|
|
257
|
+
await processLine(line);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
processing = false;
|
|
262
|
+
if (commandExited && lineQueue.length === 0 && resolveWhenDone) {
|
|
263
|
+
resolveWhenDone();
|
|
264
|
+
}
|
|
170
265
|
}
|
|
171
|
-
|
|
172
|
-
|
|
266
|
+
}
|
|
267
|
+
// Read stdout line by line
|
|
268
|
+
const rl = readline.createInterface({ input: child.stdout });
|
|
269
|
+
rl.on("line", (line) => {
|
|
270
|
+
if (!line.trim())
|
|
271
|
+
return; // skip empty lines
|
|
272
|
+
if (lineQueue.length >= MAX_QUEUE_SIZE) {
|
|
273
|
+
console.warn(`[command-triggered] Queue full, dropping oldest line.`);
|
|
274
|
+
lineQueue.shift();
|
|
173
275
|
}
|
|
276
|
+
lineQueue.push(line);
|
|
277
|
+
drainQueue().catch((err) => {
|
|
278
|
+
console.error(`[command-triggered] Error processing line:`, err);
|
|
279
|
+
invocationsFailed++;
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
// Log stderr
|
|
283
|
+
child.stderr?.on("data", (d) => process.stderr.write(d));
|
|
284
|
+
// Wait for command to exit
|
|
285
|
+
const exitCode = await new Promise((resolve) => {
|
|
286
|
+
child.on("close", (code) => {
|
|
287
|
+
commandExited = true;
|
|
288
|
+
rl.close();
|
|
289
|
+
resolve(code);
|
|
290
|
+
});
|
|
291
|
+
child.on("error", (err) => {
|
|
292
|
+
console.error(`[command-triggered] Command error:`, err);
|
|
293
|
+
commandExited = true;
|
|
294
|
+
rl.close();
|
|
295
|
+
resolve(1);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
// Wait for any remaining queued lines to finish processing
|
|
299
|
+
if (lineQueue.length > 0 || processing) {
|
|
300
|
+
await new Promise((resolve) => {
|
|
301
|
+
resolveWhenDone = resolve;
|
|
302
|
+
drainQueue();
|
|
303
|
+
});
|
|
174
304
|
}
|
|
305
|
+
const endTime = Date.now();
|
|
306
|
+
const summary = [
|
|
307
|
+
`Command: ${commandStr}`,
|
|
308
|
+
`Exit code: ${exitCode}`,
|
|
309
|
+
`Lines processed: ${linesProcessed}`,
|
|
310
|
+
`Agent invocations succeeded: ${invocationsSucceeded}`,
|
|
311
|
+
`Agent invocations failed: ${invocationsFailed}`,
|
|
312
|
+
].join("\n");
|
|
313
|
+
// Command-triggered tasks run until the command exits — any exit is a normal finish.
|
|
314
|
+
const outcome = "finished";
|
|
315
|
+
return { outcome, endTime, output: summary };
|
|
175
316
|
}
|
|
176
317
|
async function publishTaskEvent(nc, config, taskDir, taskId, eventType, useHttp, taskName) {
|
|
177
318
|
writeTaskStatus(taskDir, {
|
|
@@ -207,12 +348,12 @@ async function requestPermission(nc, config, task, taskDir, requiredPermissions,
|
|
|
207
348
|
return new Promise((resolve) => {
|
|
208
349
|
const watcher = fs.watch(statusPath, () => {
|
|
209
350
|
const status = readTaskStatus(taskDir);
|
|
210
|
-
if (!status || status.user_input
|
|
351
|
+
if (!status || !status.user_input?.length)
|
|
211
352
|
return;
|
|
212
353
|
watcher.close();
|
|
213
|
-
const response = status.user_input;
|
|
354
|
+
const response = status.user_input[0];
|
|
214
355
|
writeTaskStatus(taskDir, {
|
|
215
|
-
running_state: response === "aborted" ? "
|
|
356
|
+
running_state: response === "aborted" ? "aborted" : "started",
|
|
216
357
|
time_stamp: Date.now(),
|
|
217
358
|
});
|
|
218
359
|
resolve(response);
|
|
@@ -226,6 +367,42 @@ async function publishPermissionResolved(nc, config, taskId, status, useHttp) {
|
|
|
226
367
|
status,
|
|
227
368
|
}, useHttp);
|
|
228
369
|
}
|
|
370
|
+
async function requestUserInput(nc, config, task, taskDir, inputDescriptions, useHttp) {
|
|
371
|
+
const taskId = task.frontmatter.id;
|
|
372
|
+
const statusPath = path.join(taskDir, "status.json");
|
|
373
|
+
const currentStatus = readTaskStatus(taskDir);
|
|
374
|
+
writeTaskStatus(taskDir, { ...currentStatus, pending_input: inputDescriptions });
|
|
375
|
+
await publishHostEvent(nc, config, taskId, {
|
|
376
|
+
event_type: "input-request",
|
|
377
|
+
host_id: config.hostId,
|
|
378
|
+
input_descriptions: inputDescriptions,
|
|
379
|
+
name: task.frontmatter.name,
|
|
380
|
+
}, useHttp);
|
|
381
|
+
return new Promise((resolve) => {
|
|
382
|
+
const watcher = fs.watch(statusPath, () => {
|
|
383
|
+
const status = readTaskStatus(taskDir);
|
|
384
|
+
if (!status || !status.user_input?.length)
|
|
385
|
+
return;
|
|
386
|
+
watcher.close();
|
|
387
|
+
const response = status.user_input;
|
|
388
|
+
if (response.length === 1 && response[0] === "aborted") {
|
|
389
|
+
writeTaskStatus(taskDir, { running_state: "aborted", time_stamp: Date.now() });
|
|
390
|
+
resolve("aborted");
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
writeTaskStatus(taskDir, { running_state: "started", time_stamp: Date.now() });
|
|
394
|
+
resolve(response);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
async function publishInputResolved(nc, config, taskId, status, useHttp) {
|
|
400
|
+
await publishHostEvent(nc, config, taskId, {
|
|
401
|
+
event_type: "input-resolved",
|
|
402
|
+
host_id: config.hostId,
|
|
403
|
+
status,
|
|
404
|
+
}, useHttp);
|
|
405
|
+
}
|
|
229
406
|
async function requestConfirmation(nc, config, task, taskDir, useHttp) {
|
|
230
407
|
const taskId = task.frontmatter.id;
|
|
231
408
|
const statusPath = path.join(taskDir, "status.json");
|
|
@@ -241,13 +418,13 @@ async function requestConfirmation(nc, config, task, taskDir, useHttp) {
|
|
|
241
418
|
return new Promise((resolve) => {
|
|
242
419
|
const watcher = fs.watch(statusPath, () => {
|
|
243
420
|
const status = readTaskStatus(taskDir);
|
|
244
|
-
if (!status || status.user_input
|
|
421
|
+
if (!status || !status.user_input?.length)
|
|
245
422
|
return; // still pending
|
|
246
423
|
watcher.close();
|
|
247
|
-
const confirmed = status.user_input === "confirmed";
|
|
424
|
+
const confirmed = status.user_input[0] === "confirmed";
|
|
248
425
|
// Clear pending_confirmation/user_input and update running_state
|
|
249
426
|
writeTaskStatus(taskDir, {
|
|
250
|
-
running_state: confirmed ? "
|
|
427
|
+
running_state: confirmed ? "started" : "aborted",
|
|
251
428
|
time_stamp: Date.now(),
|
|
252
429
|
});
|
|
253
430
|
resolve(confirmed);
|
|
@@ -289,16 +466,31 @@ function parsePermissions(output) {
|
|
|
289
466
|
}
|
|
290
467
|
return perms;
|
|
291
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Extract user input requests from agent output.
|
|
471
|
+
* Looks for lines matching: [PALMIER_INPUT] <description>
|
|
472
|
+
*/
|
|
473
|
+
function parseInputRequests(output) {
|
|
474
|
+
const regex = new RegExp(`^\\${TASK_INPUT_PREFIX}\\s+(.+)$`, "gm");
|
|
475
|
+
const inputs = [];
|
|
476
|
+
let match;
|
|
477
|
+
while ((match = regex.exec(output)) !== null) {
|
|
478
|
+
const desc = match[1].trim();
|
|
479
|
+
if (desc)
|
|
480
|
+
inputs.push(desc);
|
|
481
|
+
}
|
|
482
|
+
return inputs;
|
|
483
|
+
}
|
|
292
484
|
/**
|
|
293
485
|
* Parse the agent's output for success/failure markers.
|
|
294
|
-
* Falls back to "
|
|
486
|
+
* Falls back to "finished" if no marker is found.
|
|
295
487
|
*/
|
|
296
488
|
function parseTaskOutcome(output) {
|
|
297
489
|
const lastChunk = output.slice(-500);
|
|
298
490
|
if (lastChunk.includes(TASK_FAILURE_MARKER))
|
|
299
|
-
return "
|
|
491
|
+
return "failed";
|
|
300
492
|
if (lastChunk.includes(TASK_SUCCESS_MARKER))
|
|
301
|
-
return "
|
|
302
|
-
return "
|
|
493
|
+
return "finished";
|
|
494
|
+
return "finished";
|
|
303
495
|
}
|
|
304
496
|
//# sourceMappingURL=run.js.map
|
package/dist/commands/serve.js
CHANGED
|
@@ -1,26 +1,105 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
1
3
|
import { loadConfig } from "../config.js";
|
|
4
|
+
import { connectNats } from "../nats-client.js";
|
|
2
5
|
import { createRpcHandler } from "../rpc-handler.js";
|
|
3
6
|
import { startNatsTransport } from "../transports/nats-transport.js";
|
|
4
7
|
import { startHttpTransport } from "../transports/http-transport.js";
|
|
8
|
+
import { getTaskDir, readTaskStatus, writeTaskStatus, appendHistory, parseTaskFile } from "../task.js";
|
|
9
|
+
import { publishHostEvent } from "../events.js";
|
|
10
|
+
import { getPlatform } from "../platform/index.js";
|
|
11
|
+
const POLL_INTERVAL_MS = 30_000;
|
|
12
|
+
/**
|
|
13
|
+
* Mark a stuck task as failed: update status.json, write RESULT, append history,
|
|
14
|
+
* and broadcast the failure event.
|
|
15
|
+
*/
|
|
16
|
+
async function markTaskFailed(config, nc, useHttp, taskId, reason) {
|
|
17
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
18
|
+
const status = readTaskStatus(taskDir);
|
|
19
|
+
if (!status || status.running_state !== "started")
|
|
20
|
+
return;
|
|
21
|
+
console.log(`[monitor] Task ${taskId} ${reason}, marking as failed.`);
|
|
22
|
+
const endTime = Date.now();
|
|
23
|
+
writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
|
|
24
|
+
let taskName = taskId;
|
|
25
|
+
try {
|
|
26
|
+
const task = parseTaskFile(taskDir);
|
|
27
|
+
taskName = task.frontmatter.name || taskId;
|
|
28
|
+
}
|
|
29
|
+
catch { /* use taskId as fallback */ }
|
|
30
|
+
const resultFileName = `RESULT-${endTime}.md`;
|
|
31
|
+
const content = `---\ntask_name: ${taskName}\nrunning_state: failed\nstart_time: ${status.time_stamp}\nend_time: ${endTime}\ntask_file: \n---\n${reason}`;
|
|
32
|
+
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
33
|
+
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
34
|
+
const payload = { event_type: "running-state", running_state: "failed", name: taskName };
|
|
35
|
+
await publishHostEvent(nc, config, taskId, payload, useHttp);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Scan all tasks for any stuck in "start" state whose process is no longer alive.
|
|
39
|
+
* Uses the system scheduler (Task Scheduler / systemd) as the authoritative source.
|
|
40
|
+
*/
|
|
41
|
+
async function checkStaleTasks(config, nc, useHttp) {
|
|
42
|
+
const tasksJsonl = path.join(config.projectRoot, "tasks.jsonl");
|
|
43
|
+
if (!fs.existsSync(tasksJsonl))
|
|
44
|
+
return;
|
|
45
|
+
const platform = getPlatform();
|
|
46
|
+
const lines = fs.readFileSync(tasksJsonl, "utf-8").split("\n").filter(Boolean);
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
let taskId;
|
|
49
|
+
try {
|
|
50
|
+
taskId = JSON.parse(line).task_id;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
56
|
+
const status = readTaskStatus(taskDir);
|
|
57
|
+
if (!status || status.running_state !== "started")
|
|
58
|
+
continue;
|
|
59
|
+
// Ask the system scheduler if the task is still running
|
|
60
|
+
if (platform.isTaskRunning(taskId))
|
|
61
|
+
continue;
|
|
62
|
+
await markTaskFailed(config, nc, useHttp, taskId, "Task process exited unexpectedly");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
5
65
|
/**
|
|
6
66
|
* Start the persistent RPC handler using the configured transport(s).
|
|
7
67
|
*/
|
|
8
68
|
export async function serveCommand() {
|
|
9
69
|
const config = loadConfig();
|
|
10
|
-
const handleRpc = createRpcHandler(config);
|
|
11
70
|
const mode = config.mode ?? "nats";
|
|
12
71
|
console.log(`Starting in ${mode} mode...`);
|
|
72
|
+
// Connect NATS once, share between RPC handler, transport, and monitor
|
|
73
|
+
const useNats = mode === "nats" || mode === "auto";
|
|
74
|
+
const useHttp = mode === "lan" || mode === "auto";
|
|
75
|
+
const nc = useNats ? await connectNats(config) : undefined;
|
|
76
|
+
// Reconcile any tasks stuck from before daemon started
|
|
77
|
+
await checkStaleTasks(config, nc, useHttp);
|
|
78
|
+
// Poll for crashed tasks every 30 seconds
|
|
79
|
+
setInterval(() => {
|
|
80
|
+
checkStaleTasks(config, nc, useHttp).catch((err) => {
|
|
81
|
+
console.error("[monitor] Error checking stale tasks:", err);
|
|
82
|
+
});
|
|
83
|
+
}, POLL_INTERVAL_MS);
|
|
84
|
+
const handleRpc = createRpcHandler(config, nc);
|
|
13
85
|
if (mode === "auto") {
|
|
14
86
|
await Promise.all([
|
|
15
|
-
startNatsTransport(config, handleRpc),
|
|
16
|
-
startHttpTransport(config, handleRpc)
|
|
87
|
+
startNatsTransport(config, handleRpc, nc),
|
|
88
|
+
startHttpTransport(config, handleRpc).catch((err) => {
|
|
89
|
+
if (err.code === "EADDRINUSE") {
|
|
90
|
+
console.warn(`[http] Port already in use, skipping HTTP transport.`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
}),
|
|
17
96
|
]);
|
|
18
97
|
}
|
|
19
98
|
else if (mode === "lan") {
|
|
20
99
|
await startHttpTransport(config, handleRpc);
|
|
21
100
|
}
|
|
22
101
|
else {
|
|
23
|
-
await startNatsTransport(config, handleRpc);
|
|
102
|
+
await startNatsTransport(config, handleRpc, nc);
|
|
24
103
|
}
|
|
25
104
|
}
|
|
26
105
|
//# sourceMappingURL=serve.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-exit cleanup for a task process.
|
|
3
|
+
*
|
|
4
|
+
* Called by the platform hook (ExecStopPost on Linux, wrapper script on Windows)
|
|
5
|
+
* after the main `palmier run <taskId>` process exits.
|
|
6
|
+
*
|
|
7
|
+
* - If status.json shows "finish" or "fail", the process handled its own cleanup — no-op.
|
|
8
|
+
* - If status.json shows "abort", the RPC handler already wrote status and broadcast —
|
|
9
|
+
* just write the RESULT file and append history.
|
|
10
|
+
* - If status.json shows "start", the process died unexpectedly — write "fail" status,
|
|
11
|
+
* RESULT file, append history, and broadcast event.
|
|
12
|
+
*/
|
|
13
|
+
export declare function taskCleanupCommand(taskId: string): Promise<void>;
|
|
14
|
+
//# sourceMappingURL=task-cleanup.d.ts.map
|