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.
Files changed (64) hide show
  1. package/.github/workflows/ci.yml +16 -0
  2. package/LICENSE +190 -0
  3. package/README.md +288 -219
  4. package/dist/agents/agent.d.ts +6 -3
  5. package/dist/agents/agent.js +2 -0
  6. package/dist/agents/claude.d.ts +1 -1
  7. package/dist/agents/claude.js +12 -9
  8. package/dist/agents/codex.d.ts +1 -1
  9. package/dist/agents/codex.js +12 -10
  10. package/dist/agents/gemini.d.ts +1 -1
  11. package/dist/agents/gemini.js +13 -9
  12. package/dist/agents/openclaw.d.ts +2 -2
  13. package/dist/agents/openclaw.js +8 -7
  14. package/dist/agents/shared-prompt.d.ts +5 -4
  15. package/dist/agents/shared-prompt.js +10 -8
  16. package/dist/commands/agents.js +11 -0
  17. package/dist/commands/init.js +109 -49
  18. package/dist/commands/mcpserver.js +11 -21
  19. package/dist/commands/plan-generation.md +24 -32
  20. package/dist/commands/restart.d.ts +5 -0
  21. package/dist/commands/restart.js +9 -0
  22. package/dist/commands/run.js +293 -101
  23. package/dist/commands/serve.js +83 -4
  24. package/dist/commands/task-cleanup.d.ts +14 -0
  25. package/dist/commands/task-cleanup.js +84 -0
  26. package/dist/events.d.ts +10 -0
  27. package/dist/events.js +29 -0
  28. package/dist/index.js +7 -0
  29. package/dist/platform/linux.d.ts +2 -0
  30. package/dist/platform/linux.js +22 -1
  31. package/dist/platform/platform.d.ts +4 -0
  32. package/dist/platform/windows.d.ts +3 -0
  33. package/dist/platform/windows.js +99 -82
  34. package/dist/rpc-handler.d.ts +2 -1
  35. package/dist/rpc-handler.js +43 -28
  36. package/dist/spawn-command.d.ts +29 -6
  37. package/dist/spawn-command.js +38 -15
  38. package/dist/transports/nats-transport.d.ts +4 -2
  39. package/dist/transports/nats-transport.js +3 -4
  40. package/dist/types.d.ts +4 -2
  41. package/package.json +5 -3
  42. package/src/agents/agent.ts +8 -3
  43. package/src/agents/claude.ts +44 -43
  44. package/src/agents/codex.ts +11 -12
  45. package/src/agents/gemini.ts +12 -10
  46. package/src/agents/openclaw.ts +8 -7
  47. package/src/agents/shared-prompt.ts +10 -8
  48. package/src/commands/agents.ts +11 -0
  49. package/src/commands/init.ts +120 -56
  50. package/src/commands/mcpserver.ts +11 -22
  51. package/src/commands/plan-generation.md +24 -32
  52. package/src/commands/restart.ts +9 -0
  53. package/src/commands/run.ts +365 -119
  54. package/src/commands/serve.ts +101 -5
  55. package/src/cross-spawn.d.ts +5 -0
  56. package/src/events.ts +38 -0
  57. package/src/index.ts +8 -0
  58. package/src/platform/linux.ts +25 -1
  59. package/src/platform/platform.ts +6 -0
  60. package/src/platform/windows.ts +100 -89
  61. package/src/rpc-handler.ts +46 -29
  62. package/src/spawn-command.ts +120 -83
  63. package/src/transports/nats-transport.ts +4 -4
  64. package/src/types.ts +4 -2
@@ -1,13 +1,14 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import { spawnCommand } from "../spawn-command.js";
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 { StringCodec } from "nats";
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, "start", useHttp, taskName);
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, "abort", startTime, endTime, "", [], []);
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, "abort", useHttp, taskName);
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
- // Execution loop: retry on permission failure if user grants
136
+ // Shared invocation context
80
137
  const guiEnv = getPlatform().getGuiEnv();
81
138
  const agent = getAgent(task.frontmatter.agent);
82
- let lastOutput = "";
83
- let lastOutcome = "fail";
84
- let lastReportFiles = [];
85
- let lastRequiredPermissions = [];
86
- let lastEndTime = Date.now();
87
- let retryPrompt;
88
- let transientPermissions = [];
89
- // eslint-disable-next-line no-constant-condition
90
- while (true) {
91
- const { command, args } = agent.getTaskRunCommandLine(task, retryPrompt, transientPermissions);
92
- const result = await spawnCommand(command, args, {
93
- cwd: taskDir,
94
- env: {
95
- ...guiEnv,
96
- PALMIER_TASK_ID: task.frontmatter.id,
97
- },
98
- echoStdout: true,
99
- forwardSignals: true,
100
- resolveOnFailure: true,
101
- });
102
- lastOutput = result.output;
103
- lastEndTime = Date.now();
104
- lastOutcome = result.exitCode !== 0 ? "fail" : parseTaskOutcome(lastOutput);
105
- lastReportFiles = parseReportFiles(lastOutput);
106
- lastRequiredPermissions = parsePermissions(lastOutput);
107
- // If failed with permission requirements, ask user to grant
108
- if (lastOutcome === "fail" && lastRequiredPermissions.length > 0) {
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
- // Normal completion (success or non-permission failure)
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, "fail", startTime, endTime, errorMsg, [], []);
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, "fail", useHttp, taskName);
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 sc = StringCodec();
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
- * Write status.json and notify connected clients via NATS and/or HTTP SSE.
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 publishHostEvent(nc, config, taskId, payload, useHttp) {
157
- const subject = `host-event.${config.hostId}.${taskId}`;
158
- if (nc) {
159
- nc.publish(subject, sc.encode(JSON.stringify(payload)));
160
- console.log(`[nats] ${subject} →`, payload);
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
- if (useHttp && config.directPort) {
250
+ async function drainQueue() {
251
+ if (processing)
252
+ return;
253
+ processing = true;
163
254
  try {
164
- await fetch(`http://localhost:${config.directPort}/internal/event`, {
165
- method: "POST",
166
- headers: { "Content-Type": "application/json" },
167
- body: JSON.stringify({ task_id: taskId, ...payload }),
168
- });
169
- console.log(`[http] host-event: ${taskId} →`, payload);
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
- catch (err) {
172
- console.error(`[http] Failed to push event:`, err);
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 === undefined)
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" ? "abort" : "start",
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 === undefined)
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 ? "start" : "abort",
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 "finish" if no marker is found.
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 "fail";
491
+ return "failed";
300
492
  if (lastChunk.includes(TASK_SUCCESS_MARKER))
301
- return "finish";
302
- return "finish";
493
+ return "finished";
494
+ return "finished";
303
495
  }
304
496
  //# sourceMappingURL=run.js.map
@@ -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