palmier 0.9.8 → 0.9.10

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.
@@ -1,19 +1,21 @@
1
1
  You are an AI agent executing a task on behalf of the user. Follow these instructions carefully.
2
2
 
3
+ All `[PALMIER_*]` markers below are control signals parsed by the host. They MUST be written to **stdout** (not stderr). Markers on stderr are ignored.
4
+
3
5
  ## Reporting Output
4
6
 
5
- If you generate report or output files, print each file path on its own line using this exact format:
7
+ If you generate report or output files, print each file path on its own line to stdout using this exact format:
6
8
  [PALMIER_REPORT] <filename>
7
9
 
8
10
  ## Completion
9
11
 
10
- When you are done, output exactly one of these markers as the very last line (no other text on the same line):
12
+ When you are done, output exactly one of these markers as the very last line on stdout (no other text on the same line):
11
13
  [PALMIER_TASK_SUCCESS]
12
14
  [PALMIER_TASK_FAILURE]
13
15
 
14
16
  ## Permissions
15
17
 
16
- Whenever a tool you are trying to use is denied or you lack the required permissions, print each required permission on its own line using this exact format:
18
+ Whenever a tool you are trying to use is denied or you lack the required permissions, print each required permission on its own line to stdout using this exact format:
17
19
  [PALMIER_PERMISSION] <tool_name> | <description>
18
20
 
19
21
  ## HTTP Endpoints
@@ -30,8 +30,9 @@ async function sendPushNotification(nc, hostId, title, body) {
30
30
  async function invokeAgentWithRetries(ctx, invokeTask) {
31
31
  // eslint-disable-next-line no-constant-condition
32
32
  while (true) {
33
- const writer = beginStreamingMessage(ctx.taskDir, ctx.runId, Date.now());
34
- let lineBuf = "";
33
+ let writer = beginStreamingMessage(ctx.taskDir, ctx.runId, Date.now(), "stdout");
34
+ let activeStream = "stdout";
35
+ const lineBufs = { stdout: "", stderr: "" };
35
36
  let notifyPending = false;
36
37
  let notifyTimer;
37
38
  function throttledNotify() {
@@ -43,6 +44,21 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
43
44
  publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
44
45
  }, 500);
45
46
  }
47
+ function emit(stream, chunk) {
48
+ lineBufs[stream] += chunk;
49
+ const lines = lineBufs[stream].split("\n");
50
+ lineBufs[stream] = lines.pop() ?? "";
51
+ const filtered = lines.filter((l) => !l.startsWith("[PALMIER"));
52
+ if (filtered.length === 0)
53
+ return;
54
+ if (stream !== activeStream) {
55
+ writer.end();
56
+ writer = beginStreamingMessage(ctx.taskDir, ctx.runId, Date.now(), stream);
57
+ activeStream = stream;
58
+ }
59
+ writer.write(filtered.join("\n") + "\n");
60
+ throttledNotify();
61
+ }
46
62
  const { command, args, stdin, env: agentEnv } = ctx.agent.getTaskRunCommandLine(invokeTask, undefined, ctx.task.frontmatter.yolo_mode ? "yolo" : ctx.transientPermissions);
47
63
  const result = await spawnCommand(command, args, {
48
64
  cwd: getRunDir(ctx.taskDir, ctx.runId),
@@ -50,29 +66,39 @@ async function invokeAgentWithRetries(ctx, invokeTask) {
50
66
  echoStdout: true,
51
67
  resolveOnFailure: true,
52
68
  stdin,
53
- onData: (chunk) => {
54
- lineBuf += chunk;
55
- const lines = lineBuf.split("\n");
56
- lineBuf = lines.pop() ?? "";
57
- const filtered = lines.filter((l) => !l.startsWith("[PALMIER"));
58
- if (filtered.length > 0) {
59
- writer.write(filtered.join("\n") + "\n");
60
- throttledNotify();
61
- }
62
- },
69
+ onStdout: (chunk) => emit("stdout", chunk),
70
+ onStderr: (chunk) => emit("stderr", chunk),
63
71
  });
64
72
  if (notifyTimer)
65
73
  clearTimeout(notifyTimer);
66
74
  const outcome = result.exitCode !== 0 ? "failed" : parseTaskOutcome(result.output);
67
75
  const reportFiles = parseReportFiles(result.output);
68
76
  const requiredPermissions = parsePermissions(result.output);
69
- if (lineBuf && !lineBuf.startsWith("[PALMIER")) {
70
- writer.write(lineBuf);
77
+ for (const stream of ["stdout", "stderr"]) {
78
+ const trailing = lineBufs[stream];
79
+ if (trailing && !trailing.startsWith("[PALMIER")) {
80
+ if (stream !== activeStream) {
81
+ writer.end();
82
+ writer = beginStreamingMessage(ctx.taskDir, ctx.runId, Date.now(), stream);
83
+ activeStream = stream;
84
+ }
85
+ writer.write(trailing);
86
+ }
71
87
  }
72
88
  if (requiredPermissions.length > 0) {
89
+ if (activeStream !== "stdout") {
90
+ writer.end();
91
+ writer = beginStreamingMessage(ctx.taskDir, ctx.runId, Date.now(), "stdout");
92
+ activeStream = "stdout";
93
+ }
73
94
  const permLines = requiredPermissions.map((p) => `- **${p.name}** ${p.description}`).join("\n");
74
95
  writer.write(`\n\n**Permissions requested:**\n${permLines}\n`);
75
96
  }
97
+ if (reportFiles.length > 0 && activeStream !== "stdout") {
98
+ writer.end();
99
+ writer = beginStreamingMessage(ctx.taskDir, ctx.runId, Date.now(), "stdout");
100
+ activeStream = "stdout";
101
+ }
76
102
  writer.end(reportFiles.length > 0 ? reportFiles : undefined);
77
103
  await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
78
104
  if (reportFiles.length > 0) {
@@ -2,7 +2,7 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { execFileSync } from "child_process";
4
4
  import { CONFIG_DIR, loadConfig } from "../config.js";
5
- import { getTaskDir, readTaskStatus } from "../task.js";
5
+ import { getTaskDir, readTaskStatus, parseTaskFile } from "../task.js";
6
6
  const TASK_PREFIX = "\\Palmier\\PalmierTask-";
7
7
  const DAEMON_TASK_NAME = "PalmierDaemon";
8
8
  const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
@@ -72,6 +72,52 @@ export function buildTaskXml(tr, triggers, foreground) {
72
72
  function schtasksTaskName(taskId) {
73
73
  return `${TASK_PREFIX}${taskId}`;
74
74
  }
75
+ function querySchtasksStatus(tn) {
76
+ try {
77
+ const out = execFileSync("schtasks", ["/query", "/tn", tn, "/v", "/fo", "LIST"], {
78
+ encoding: "utf-8", windowsHide: true, stdio: ["ignore", "pipe", "pipe"],
79
+ });
80
+ const status = out.match(/^\s*Status:\s*(.+?)\s*$/im)?.[1] ?? "";
81
+ const lastResult = out.match(/^\s*Last Result:\s*(.+?)\s*$/im)?.[1] ?? "";
82
+ return { status, lastResult };
83
+ }
84
+ catch {
85
+ return undefined;
86
+ }
87
+ }
88
+ /** Map common Last Result HRESULTs to a human-readable cause. */
89
+ function explainLastResult(lastResult, foreground) {
90
+ const code = lastResult.trim();
91
+ // Decimal forms emitted by schtasks: 267011 = 0x41303 SCHED_S_TASK_HAS_NOT_RUN.
92
+ if (code === "267011" || /0x0*41303/i.test(code)) {
93
+ return foreground
94
+ ? "Foreground mode requires an active Windows session, but no user is logged in. Sign in to Windows and try again, or disable foreground mode for this task."
95
+ : "Task Scheduler reported the task did not run. Check that the daemon has permission to launch it.";
96
+ }
97
+ if (code === "0" || code === "0x0")
98
+ return undefined;
99
+ return `Task Scheduler reported Last Result=${code}.`;
100
+ }
101
+ /**
102
+ * 2s after `schtasks /run`, confirm the action actually launched. Some failure
103
+ * modes — most notably foreground tasks with no interactive session — make
104
+ * /run return success while the action is silently skipped (Status stays
105
+ * "Ready", Last Result stays at 0x41303). If the run process has already
106
+ * written status.json by then, it clearly launched; skip the Scheduler query.
107
+ */
108
+ async function verifyTaskLaunched(tn, taskDir, startTime, foreground) {
109
+ await new Promise((r) => setTimeout(r, 2000));
110
+ const status = readTaskStatus(taskDir);
111
+ if (status && status.time_stamp >= startTime)
112
+ return;
113
+ const last = querySchtasksStatus(tn);
114
+ if (last && /running/i.test(last.status))
115
+ return;
116
+ const explained = explainLastResult(last?.lastResult ?? "", foreground);
117
+ if (explained)
118
+ throw new Error(explained);
119
+ throw new Error(`Task Scheduler did not launch the task within 2s (status=${last?.status || "unknown"}, last_result=${last?.lastResult || "unknown"}).`);
120
+ }
75
121
  export class WindowsPlatform {
76
122
  installDaemon(config) {
77
123
  const script = process.argv[1] || "palmier";
@@ -218,6 +264,13 @@ export class WindowsPlatform {
218
264
  }
219
265
  async startTask(taskId) {
220
266
  const tn = schtasksTaskName(taskId);
267
+ const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
268
+ let foreground = false;
269
+ try {
270
+ foreground = !!parseTaskFile(taskDir).frontmatter.foreground_mode;
271
+ }
272
+ catch { /* fall through; verifyTaskLaunched still detects most failures */ }
273
+ const startTime = Date.now();
221
274
  try {
222
275
  execFileSync("schtasks", ["/run", "/tn", tn], { encoding: "utf-8", windowsHide: true });
223
276
  }
@@ -225,6 +278,7 @@ export class WindowsPlatform {
225
278
  const e = err;
226
279
  throw new Error(`Failed to start task via schtasks: ${e.stderr || e.message}`);
227
280
  }
281
+ await verifyTaskLaunched(tn, taskDir, startTime, foreground);
228
282
  }
229
283
  async stopTask(taskId) {
230
284
  // schtasks /end leaves agent children orphaned, so kill the process tree