palmier 0.8.1 → 0.8.3

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 (125) hide show
  1. package/CLAUDE.md +13 -0
  2. package/README.md +11 -11
  3. package/dist/agents/agent.d.ts +0 -4
  4. package/dist/agents/claude.js +1 -1
  5. package/dist/agents/codex.js +2 -2
  6. package/dist/agents/cursor.js +1 -1
  7. package/dist/agents/deepagents.js +1 -1
  8. package/dist/agents/gemini.js +3 -2
  9. package/dist/agents/goose.js +1 -1
  10. package/dist/agents/hermes.js +1 -1
  11. package/dist/agents/kiro.js +1 -1
  12. package/dist/agents/opencode.js +1 -1
  13. package/dist/agents/qoder.js +1 -1
  14. package/dist/agents/shared-prompt.d.ts +0 -3
  15. package/dist/agents/shared-prompt.js +0 -3
  16. package/dist/app-registry.d.ts +10 -0
  17. package/dist/app-registry.js +44 -0
  18. package/dist/commands/info.d.ts +0 -3
  19. package/dist/commands/info.js +0 -5
  20. package/dist/commands/init.d.ts +0 -3
  21. package/dist/commands/init.js +2 -11
  22. package/dist/commands/pair.d.ts +1 -4
  23. package/dist/commands/pair.js +1 -12
  24. package/dist/commands/restart.d.ts +0 -3
  25. package/dist/commands/restart.js +0 -3
  26. package/dist/commands/run.d.ts +1 -14
  27. package/dist/commands/run.js +18 -61
  28. package/dist/commands/serve.d.ts +0 -3
  29. package/dist/commands/serve.js +33 -27
  30. package/dist/config.d.ts +0 -8
  31. package/dist/config.js +0 -8
  32. package/dist/device-capabilities.d.ts +1 -1
  33. package/dist/event-queues.d.ts +6 -21
  34. package/dist/event-queues.js +6 -21
  35. package/dist/events.d.ts +0 -6
  36. package/dist/events.js +1 -9
  37. package/dist/index.js +0 -1
  38. package/dist/mcp-handler.js +1 -2
  39. package/dist/mcp-tools.d.ts +0 -3
  40. package/dist/mcp-tools.js +12 -16
  41. package/dist/nats-client.d.ts +0 -3
  42. package/dist/nats-client.js +1 -4
  43. package/dist/pending-requests.d.ts +4 -18
  44. package/dist/pending-requests.js +4 -18
  45. package/dist/platform/index.d.ts +1 -4
  46. package/dist/platform/index.js +1 -4
  47. package/dist/platform/linux.d.ts +3 -9
  48. package/dist/platform/linux.js +9 -20
  49. package/dist/platform/platform.d.ts +1 -4
  50. package/dist/platform/windows.d.ts +2 -5
  51. package/dist/platform/windows.js +19 -39
  52. package/dist/pwa/assets/{index-CQxcuDhM.css → index-B0F9mtid.css} +1 -1
  53. package/dist/pwa/assets/index-SYs3mcdJ.js +120 -0
  54. package/dist/pwa/assets/{web-D7Kq3Nvk.js → web-C6lkQj9J.js} +1 -1
  55. package/dist/pwa/assets/{web-DOyOiwsW.js → web-Z1623me-.js} +1 -1
  56. package/dist/pwa/index.html +2 -2
  57. package/dist/pwa/service-worker.js +1 -1
  58. package/dist/rpc-handler.d.ts +0 -6
  59. package/dist/rpc-handler.js +18 -47
  60. package/dist/spawn-command.d.ts +10 -25
  61. package/dist/spawn-command.js +7 -15
  62. package/dist/task.d.ts +6 -64
  63. package/dist/task.js +7 -70
  64. package/dist/transports/http-transport.d.ts +0 -4
  65. package/dist/transports/http-transport.js +6 -28
  66. package/dist/transports/nats-transport.d.ts +0 -4
  67. package/dist/transports/nats-transport.js +3 -9
  68. package/dist/types.d.ts +3 -7
  69. package/dist/update-checker.d.ts +1 -4
  70. package/dist/update-checker.js +2 -5
  71. package/package.json +1 -1
  72. package/palmier-server/pwa/src/App.css +165 -20
  73. package/palmier-server/pwa/src/components/HostMenu.tsx +159 -49
  74. package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
  75. package/palmier-server/pwa/src/components/SessionsView.tsx +57 -31
  76. package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
  77. package/palmier-server/pwa/src/components/TaskForm.tsx +152 -2
  78. package/palmier-server/pwa/src/constants.ts +1 -1
  79. package/palmier-server/pwa/src/native/Device.ts +20 -2
  80. package/palmier-server/pwa/src/pages/Dashboard.tsx +11 -6
  81. package/palmier-server/server/src/index.ts +7 -7
  82. package/palmier-server/server/src/routes/device.ts +4 -4
  83. package/palmier-server/spec.md +38 -7
  84. package/src/agents/agent.ts +0 -4
  85. package/src/agents/claude.ts +1 -1
  86. package/src/agents/codex.ts +2 -2
  87. package/src/agents/cursor.ts +1 -1
  88. package/src/agents/deepagents.ts +1 -1
  89. package/src/agents/gemini.ts +3 -2
  90. package/src/agents/goose.ts +1 -1
  91. package/src/agents/hermes.ts +1 -1
  92. package/src/agents/kiro.ts +1 -1
  93. package/src/agents/opencode.ts +1 -1
  94. package/src/agents/qoder.ts +1 -1
  95. package/src/agents/shared-prompt.ts +0 -3
  96. package/src/app-registry.ts +52 -0
  97. package/src/commands/info.ts +0 -5
  98. package/src/commands/init.ts +2 -11
  99. package/src/commands/pair.ts +1 -12
  100. package/src/commands/restart.ts +0 -3
  101. package/src/commands/run.ts +18 -65
  102. package/src/commands/serve.ts +31 -27
  103. package/src/config.ts +0 -8
  104. package/src/device-capabilities.ts +3 -2
  105. package/src/event-queues.ts +6 -21
  106. package/src/events.ts +1 -9
  107. package/src/index.ts +0 -1
  108. package/src/mcp-handler.ts +1 -2
  109. package/src/mcp-tools.ts +12 -18
  110. package/src/nats-client.ts +1 -4
  111. package/src/pending-requests.ts +4 -18
  112. package/src/platform/index.ts +1 -4
  113. package/src/platform/linux.ts +9 -20
  114. package/src/platform/platform.ts +1 -4
  115. package/src/platform/windows.ts +19 -40
  116. package/src/rpc-handler.ts +19 -47
  117. package/src/spawn-command.ts +11 -27
  118. package/src/task.ts +7 -70
  119. package/src/transports/http-transport.ts +6 -39
  120. package/src/transports/nats-transport.ts +3 -9
  121. package/src/types.ts +3 -10
  122. package/src/update-checker.ts +2 -5
  123. package/test/task-parsing.test.ts +2 -3
  124. package/test/windows-xml.test.ts +11 -12
  125. package/dist/pwa/assets/index-DQfOEB03.js +0 -120
@@ -19,7 +19,7 @@ export class Cursor implements AgentTool {
19
19
  if (yolo) {
20
20
  args.push("--force");
21
21
  }
22
- if (followupPrompt) {args.push("--continue");} // continue mode for followups
22
+ if (followupPrompt) {args.push("--continue");}
23
23
  args.push("-p", prompt);
24
24
 
25
25
  return { command: "cursor", args};
@@ -19,7 +19,7 @@ export class DeepAgents implements AgentTool {
19
19
  if (yolo) {
20
20
  args.push("--auto-approve");
21
21
  }
22
- if (followupPrompt) {args.push("--resume");} // continue mode for followups
22
+ if (followupPrompt) {args.push("--resume");}
23
23
  args.push("--non-interactive", prompt);
24
24
 
25
25
  return { command: "deepagents", args};
@@ -25,8 +25,9 @@ export class GeminiAgent implements AgentTool {
25
25
  args.push("--allowed-tools", tools.join(","));
26
26
  }
27
27
 
28
- if (followupPrompt) {args.push("--resume");} // continue mode for followups
29
- args.push("--prompt", "-"); // read prompt from stdin to avoid command line length limits
28
+ if (followupPrompt) {args.push("--resume");}
29
+ // Read prompt from stdin to avoid command-line length limits.
30
+ args.push("--prompt", "-");
30
31
 
31
32
  return { command: "gemini", args, stdin: prompt };
32
33
  }
@@ -16,7 +16,7 @@ export class GooseAgent implements AgentTool {
16
16
  const prompt = followupPrompt ?? getAgentInstructions(task, yolo || !this.supportsPermissions);
17
17
  const args = ["run"];
18
18
 
19
- if (followupPrompt) {args.push("--resume");} // continue mode for followups
19
+ if (followupPrompt) {args.push("--resume");}
20
20
  args.push("--text", prompt);
21
21
 
22
22
  return { command: "goose", args, ...(yolo ? { env: { GOOSE_MODE: "auto" } } : {}) };
@@ -19,7 +19,7 @@ export class Hermes implements AgentTool {
19
19
  if (yolo) {
20
20
  args.push("--trust-all-tools");
21
21
  }
22
- if (followupPrompt) {args.push("--continue");} // continue mode for followups
22
+ if (followupPrompt) {args.push("--continue");}
23
23
  args.push("-q", prompt);
24
24
 
25
25
  return { command: "hermes", args};
@@ -19,7 +19,7 @@ export class Kiro implements AgentTool {
19
19
  if (yolo) {
20
20
  args.push("--trust-all-tools");
21
21
  }
22
- if (followupPrompt) {args.push("--resume");} // continue mode for followups
22
+ if (followupPrompt) {args.push("--resume");}
23
23
  args.push("--no-interactive", prompt);
24
24
 
25
25
  return { command: "kiro-cli", args};
@@ -19,7 +19,7 @@ export class OpenCodeAgent implements AgentTool {
19
19
  if (yolo) {
20
20
  args.push("--dangerously-skip-permissions");
21
21
  }
22
- if (followupPrompt) {args.push("--continue");} // continue mode for followups
22
+ if (followupPrompt) {args.push("--continue");}
23
23
  args.push(prompt);
24
24
 
25
25
  return { command: "opencode", args};
@@ -19,7 +19,7 @@ export class Qoder implements AgentTool {
19
19
  if (yolo) {
20
20
  args.push("--yolo");
21
21
  }
22
- if (followupPrompt) {args.push("-c");} // continue mode for followups
22
+ if (followupPrompt) {args.push("-c");}
23
23
  args.push("-p", prompt);
24
24
 
25
25
  return { command: "qodercli", args};
@@ -12,9 +12,6 @@ const AGENT_INSTRUCTIONS_TEMPLATE = fs.readFileSync(
12
12
  "utf-8",
13
13
  );
14
14
 
15
- /**
16
- * Build the full agent prompt: instructions + endpoint docs + task description.
17
- */
18
15
  export function getAgentInstructions(task: ParsedTask, skipPermissions?: boolean): string {
19
16
  const port = loadConfig().httpPort ?? 7256;
20
17
  const taskDescription = task.frontmatter.user_prompt;
@@ -0,0 +1,52 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { CONFIG_DIR } from "./config.js";
4
+
5
+ const REGISTRY_FILE = path.join(CONFIG_DIR, "app-registry.json");
6
+
7
+ /** Persistent cache of packageName → appName pairs seen via device notifications. */
8
+ export interface AppInfo {
9
+ packageName: string;
10
+ appName: string;
11
+ }
12
+
13
+ let cache: Record<string, string> | null = null;
14
+
15
+ function load(): Record<string, string> {
16
+ if (cache) return cache;
17
+ try {
18
+ if (fs.existsSync(REGISTRY_FILE)) {
19
+ cache = JSON.parse(fs.readFileSync(REGISTRY_FILE, "utf-8")) as Record<string, string>;
20
+ return cache;
21
+ }
22
+ } catch {
23
+ // Corrupt file — start fresh rather than fail notifications.
24
+ }
25
+ cache = {};
26
+ return cache;
27
+ }
28
+
29
+ function persist(map: Record<string, string>): void {
30
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
31
+ fs.writeFileSync(REGISTRY_FILE, JSON.stringify(map, null, 2), "utf-8");
32
+ }
33
+
34
+ /** Writes only on change so we track the latest label if an app renames itself. */
35
+ export function recordApp(packageName: string, appName: string): void {
36
+ if (!packageName || !appName) return;
37
+ const map = load();
38
+ if (map[packageName] === appName) return;
39
+ map[packageName] = appName;
40
+ persist(map);
41
+ }
42
+
43
+ export function listApps(): AppInfo[] {
44
+ const map = load();
45
+ return Object.entries(map)
46
+ .map(([packageName, appName]) => ({ packageName, appName }))
47
+ .sort((a, b) => a.appName.localeCompare(b.appName));
48
+ }
49
+
50
+ export function getAppName(packageName: string): string | undefined {
51
+ return load()[packageName];
52
+ }
@@ -1,9 +1,6 @@
1
1
  import { loadConfig } from "../config.js";
2
2
  import { loadClients } from "../client-store.js";
3
3
 
4
- /**
5
- * Print host connection info for setting up clients.
6
- */
7
4
  export async function infoCommand(): Promise<void> {
8
5
  const config = loadConfig();
9
6
  const clients = loadClients();
@@ -11,14 +8,12 @@ export async function infoCommand(): Promise<void> {
11
8
  console.log(`Host ID: ${config.hostId}`);
12
9
  console.log(`Project root: ${config.projectRoot}`);
13
10
 
14
- // Detected agents
15
11
  if (config.agents && config.agents.length > 0) {
16
12
  console.log(`Agents: ${config.agents.map((a) => a.label).join(", ")}`);
17
13
  } else {
18
14
  console.log(`Agents: (none detected — run \`palmier agents\`)`);
19
15
  }
20
16
 
21
- // Clients
22
17
  console.log(`Clients: ${clients.length} active`);
23
18
 
24
19
  if (clients.length === 0) {
@@ -15,9 +15,6 @@ const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
15
15
  const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
16
16
  const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
17
17
 
18
- /**
19
- * Interactive wizard to provision this host.
20
- */
21
18
  export async function initCommand(): Promise<void> {
22
19
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
23
20
  const ask: AskFn = (q) => new Promise<string>((resolve) => rl.question(q, resolve));
@@ -27,7 +24,6 @@ export async function initCommand(): Promise<void> {
27
24
  console.log(`By continuing, you agree to the ${cyan("Terms of Service")} (https://www.palmier.me/terms)`);
28
25
  console.log(`and ${cyan("Privacy Policy")} (https://www.palmier.me/privacy).\n`);
29
26
 
30
- // Detect agents first — abort if none found
31
27
  console.log("Detecting installed agents...");
32
28
  const agents = await detectAgents();
33
29
 
@@ -41,7 +37,6 @@ export async function initCommand(): Promise<void> {
41
37
 
42
38
  console.log(` Found: ${green(agents.map((a) => a.label).join(", "))}\n`);
43
39
 
44
- // LAN mode
45
40
  const lanAnswer = await ask("Enable LAN access (direct HTTP from local network)? (y/N): ");
46
41
  const lanEnabled = lanAnswer.trim().toLowerCase() === "y";
47
42
 
@@ -51,7 +46,6 @@ export async function initCommand(): Promise<void> {
51
46
  const parsed = parseInt(portAnswer.trim(), 10);
52
47
  if (parsed > 0 && parsed < 65536) httpPort = parsed;
53
48
 
54
- // Display summary and ask for confirmation before making any changes
55
49
  console.log(`\n${bold("Setup summary:")}\n`);
56
50
  console.log(` ${dim("Task storage:")} ${bold(process.cwd())}`);
57
51
  console.log(` All tasks and execution data will be stored here.\n`);
@@ -64,7 +58,6 @@ export async function initCommand(): Promise<void> {
64
58
  }
65
59
  console.log(` ${dim("Agents:")} ${agents.map((a) => a.label).join(", ")}\n`);
66
60
 
67
- // Check for existing tasks to recover
68
61
  const existingTasks = listTasks(process.cwd());
69
62
  if (existingTasks.length > 0) {
70
63
  console.log(` ${dim("Recover tasks:")} ${existingTasks.length} existing task(s) found:`);
@@ -81,7 +74,6 @@ export async function initCommand(): Promise<void> {
81
74
  return;
82
75
  }
83
76
 
84
- // Register with server
85
77
  let existingHostId: string | undefined;
86
78
  try { existingHostId = loadConfig().hostId; } catch { /* first init */ }
87
79
 
@@ -105,7 +97,6 @@ export async function initCommand(): Promise<void> {
105
97
  }
106
98
  }
107
99
 
108
- // Build and save config
109
100
  const config: HostConfig = {
110
101
  hostId: registerResponse.hostId,
111
102
  projectRoot: process.cwd(),
@@ -123,8 +114,8 @@ export async function initCommand(): Promise<void> {
123
114
 
124
115
  getPlatform().installDaemon(config);
125
116
 
126
- // Task recovery happens in the daemon (palmier serve) on startup,
127
- // since the daemon runs elevated and can create S4U scheduled tasks.
117
+ // Task recovery runs in the daemon (palmier serve) because that process
118
+ // is elevated and can create S4U scheduled tasks.
128
119
 
129
120
  console.log("\nStarting pairing...");
130
121
  rl.close();
@@ -24,9 +24,6 @@ function buildPairResponse(config: HostConfig, label?: string) {
24
24
  };
25
25
  }
26
26
 
27
- /**
28
- * POST to the running serve daemon and long-poll until paired or expired.
29
- */
30
27
  function httpPairRegister(port: number, code: string): Promise<boolean> {
31
28
  const body = JSON.stringify({ code, expiryMs: PAIRING_EXPIRY_MS });
32
29
 
@@ -60,10 +57,7 @@ function httpPairRegister(port: number, code: string): Promise<boolean> {
60
57
  });
61
58
  }
62
59
 
63
- /**
64
- * Generate a pairing code and wait for a PWA client to pair.
65
- * Listens on NATS (server mode) and HTTP (via serve daemon) in parallel.
66
- */
60
+ /** Listens on NATS (server mode) and HTTP (via serve daemon) in parallel. */
67
61
  export async function pairCommand(): Promise<void> {
68
62
  const config = loadConfig();
69
63
  const code = generatePairingCode();
@@ -78,7 +72,6 @@ export async function pairCommand(): Promise<void> {
78
72
 
79
73
  const cleanups: Array<() => void | Promise<void>> = [];
80
74
 
81
- // Display pairing info
82
75
  console.log("");
83
76
  console.log("Enter this code in your Palmier app:");
84
77
  console.log("");
@@ -86,7 +79,6 @@ export async function pairCommand(): Promise<void> {
86
79
  console.log("");
87
80
  console.log("Code expires in 1 minute.");
88
81
 
89
- // NATS pairing (server mode)
90
82
  const nc = await connectNats(config);
91
83
  const sc = StringCodec();
92
84
  const subject = `pair.${code}`;
@@ -116,13 +108,11 @@ export async function pairCommand(): Promise<void> {
116
108
  }
117
109
  })();
118
110
 
119
- // HTTP pairing — register with serve daemon's /pair-register endpoint
120
111
  (async () => {
121
112
  const result = await httpPairRegister(httpPort, code);
122
113
  if (result) onPaired();
123
114
  })();
124
115
 
125
- // Wait for pairing or timeout
126
116
  const start = Date.now();
127
117
  await new Promise<void>((resolve) => {
128
118
  const interval = setInterval(() => {
@@ -133,7 +123,6 @@ export async function pairCommand(): Promise<void> {
133
123
  }, 500);
134
124
  });
135
125
 
136
- // Cleanup
137
126
  for (const cleanup of cleanups) {
138
127
  await cleanup();
139
128
  }
@@ -1,8 +1,5 @@
1
1
  import { getPlatform } from "../platform/index.js";
2
2
 
3
- /**
4
- * Restart the palmier serve daemon.
5
- */
6
3
  export async function restartCommand(): Promise<void> {
7
4
  const platform = getPlatform();
8
5
  await platform.restartDaemon();
@@ -13,10 +13,6 @@ import { publishHostEvent } from "../events.js";
13
13
  import type { HostConfig, ParsedTask, TaskRunningState, RequiredPermission } from "../types.js";
14
14
  import type { NatsConnection } from "nats";
15
15
 
16
- /**
17
- * Shared context for agent invocation retry loops.
18
- * Passed around to avoid threading many individual parameters.
19
- */
20
16
  interface InvocationContext {
21
17
  agent: AgentTool;
22
18
  task: ParsedTask;
@@ -35,11 +31,9 @@ interface InvocationResult {
35
31
  }
36
32
 
37
33
  /**
38
- * Invoke the agent CLI with a continuation loop for permissions and user input.
39
- *
40
- * Both standard and command-triggered execution use this.
41
- * The `invokeTask` is the ParsedTask whose prompt is passed to the agent
42
- * (for command-triggered mode this is the per-line augmented task).
34
+ * Invoke the agent CLI in a continuation loop to handle permission requests.
35
+ * `invokeTask` is the ParsedTask whose prompt is passed to the agent (in
36
+ * command-triggered mode this is the per-line augmented task).
43
37
  */
44
38
  async function invokeAgentWithRetries(
45
39
  ctx: InvocationContext,
@@ -47,7 +41,6 @@ async function invokeAgentWithRetries(
47
41
  ): Promise<InvocationResult> {
48
42
  // eslint-disable-next-line no-constant-condition
49
43
  while (true) {
50
- // Stream agent output to TASKRUN.md in real-time, throttled to 500ms
51
44
  const writer = beginStreamingMessage(ctx.taskDir, ctx.runId, Date.now());
52
45
  let lineBuf = "";
53
46
  let notifyPending = false;
@@ -89,12 +82,10 @@ async function invokeAgentWithRetries(
89
82
  const reportFiles = parseReportFiles(result.output);
90
83
  const requiredPermissions = parsePermissions(result.output);
91
84
 
92
- // Flush remaining buffered content
93
85
  if (lineBuf && !lineBuf.startsWith("[PALMIER")) {
94
86
  writer.write(lineBuf);
95
87
  }
96
88
 
97
- // Include permission requests in the assistant message
98
89
  if (requiredPermissions.length > 0) {
99
90
  const permLines = requiredPermissions.map((p) => `- **${p.name}** ${p.description}`).join("\n");
100
91
  writer.write(`\n\n**Permissions requested:**\n${permLines}\n`);
@@ -112,7 +103,6 @@ async function invokeAgentWithRetries(
112
103
  });
113
104
  }
114
105
 
115
- // Permission handling — agent requested permissions
116
106
  if (requiredPermissions.length > 0) {
117
107
  const response = await requestPermission(ctx.config, ctx.task, ctx.taskDir, requiredPermissions);
118
108
 
@@ -146,27 +136,20 @@ async function invokeAgentWithRetries(
146
136
  ctx.transientPermissions = [...ctx.transientPermissions, ...newPerms];
147
137
  }
148
138
 
149
- // If the agent actually failed, retry with the new permissions
139
+ // Retry with the new permissions if the agent failed.
150
140
  if (outcome === "failed") {
151
141
  continue;
152
142
  }
153
143
  }
154
144
 
155
- // Normal completion (success or terminal failure)
156
145
  return { outcome };
157
146
  }
158
147
  }
159
148
 
160
- /**
161
- * Strip [PALMIER_*] marker lines from agent output.
162
- */
163
149
  export function stripPalmierMarkers(output: string): string {
164
150
  return output.split("\n").filter((l) => !l.startsWith("[PALMIER")).join("\n").trim();
165
151
  }
166
152
 
167
- /**
168
- * Append a conversation message to the RESULT file and notify connected clients.
169
- */
170
153
  async function appendAndNotify(
171
154
  ctx: InvocationContext,
172
155
  msg: Parameters<typeof appendRunMessage>[2],
@@ -175,9 +158,7 @@ async function appendAndNotify(
175
158
  await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
176
159
  }
177
160
 
178
- /**
179
- * Find the latest run dir that has no status messages yet (just created by the RPC handler).
180
- */
161
+ /** The latest run dir with no status messages yet — freshly created by the RPC handler. */
181
162
  function findLatestPendingRunId(taskDir: string): string | null {
182
163
  const dirs = fs.readdirSync(taskDir)
183
164
  .filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
@@ -190,8 +171,8 @@ function findLatestPendingRunId(taskDir: string): string | null {
190
171
  }
191
172
 
192
173
  /**
193
- * If the RPC handler already wrote "aborted" to status.json (e.g. via task.abort),
194
- * respect that instead of overwriting with the process's own outcome.
174
+ * If the RPC handler already wrote "aborted" (via task.abort), respect that
175
+ * instead of overwriting with the process's own outcome.
195
176
  */
196
177
  function resolveOutcome(taskDir: string, outcome: TaskRunningState): TaskRunningState {
197
178
  const current = readTaskStatus(taskDir);
@@ -199,9 +180,6 @@ function resolveOutcome(taskDir: string, outcome: TaskRunningState): TaskRunning
199
180
  return outcome;
200
181
  }
201
182
 
202
- /**
203
- * Execute a task by ID.
204
- */
205
183
  export async function runCommand(taskId: string): Promise<void> {
206
184
  const config = loadConfig();
207
185
  const taskDir = getTaskDir(config.projectRoot, taskId);
@@ -211,7 +189,6 @@ export async function runCommand(taskId: string): Promise<void> {
211
189
  let nc: NatsConnection | undefined;
212
190
  const taskName = task.frontmatter.name;
213
191
 
214
- // Use existing run dir if just created by RPC, otherwise create a new one
215
192
  const existingRunId = findLatestPendingRunId(taskDir);
216
193
  const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now(), task.frontmatter.agent);
217
194
  if (!existingRunId) {
@@ -231,7 +208,6 @@ export async function runCommand(taskId: string): Promise<void> {
231
208
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "started" });
232
209
  await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
233
210
 
234
- // If requires_confirmation, notify clients and wait
235
211
  if (task.frontmatter.requires_confirmation) {
236
212
  const confirmed = await requestConfirmation(config, task, taskDir);
237
213
  const confirmPrompt = `**Task Confirmation**\n\nRun task "${taskName || task.frontmatter.user_prompt}"?`;
@@ -252,7 +228,6 @@ export async function runCommand(taskId: string): Promise<void> {
252
228
  await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
253
229
  }
254
230
 
255
- // Shared invocation context
256
231
  const guiEnv = getPlatform().getGuiEnv();
257
232
  const agent = getAgent(task.frontmatter.agent);
258
233
  const ctx: InvocationContext = {
@@ -261,7 +236,6 @@ export async function runCommand(taskId: string): Promise<void> {
261
236
  };
262
237
 
263
238
  if (task.frontmatter.command) {
264
- // Command-triggered mode
265
239
  const result = await runCommandTriggeredMode(ctx);
266
240
  const outcome = resolveOutcome(taskDir, result.outcome);
267
241
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
@@ -269,14 +243,12 @@ export async function runCommand(taskId: string): Promise<void> {
269
243
  console.log(`Task ${taskId} completed (command-triggered).`);
270
244
  } else if (task.frontmatter.schedule_type === "on_new_notification"
271
245
  || task.frontmatter.schedule_type === "on_new_sms") {
272
- // Event-triggered mode (driven by NATS pub/sub of device notifications/SMS)
273
246
  const result = await runEventTriggeredMode(ctx);
274
247
  const outcome = resolveOutcome(taskDir, result.outcome);
275
248
  appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
276
249
  await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
277
250
  console.log(`Task ${taskId} completed (event-triggered).`);
278
251
  } else {
279
- // Standard execution — add user prompt as first message
280
252
  await appendAndNotify(ctx, {
281
253
  role: "user",
282
254
  time: Date.now(),
@@ -312,11 +284,9 @@ const MAX_LOG_ENTRIES = 1000;
312
284
  const MAX_LINE_LENGTH = 200_000;
313
285
 
314
286
  /**
315
- * Command-triggered execution mode.
316
- *
317
- * Spawns a long-running shell command and, for each line of stdout,
318
- * invokes the agent CLI with the user's prompt augmented by that line.
319
- * Processes lines sequentially with a bounded queue.
287
+ * Spawn a long-running shell command and invoke the agent CLI once per stdout
288
+ * line, with the user's prompt augmented by that line. Sequential with a
289
+ * bounded queue.
320
290
  */
321
291
  async function runCommandTriggeredMode(
322
292
  ctx: InvocationContext,
@@ -346,7 +316,6 @@ async function runCommandTriggeredMode(
346
316
  const entry = `[${new Date().toISOString()}] (${outcome}) input: ${line}\n${agentOutput}\n---\n`;
347
317
  fs.appendFileSync(logPath, entry, "utf-8");
348
318
 
349
- // Trim log if too large (keep last MAX_LOG_ENTRIES entries)
350
319
  try {
351
320
  const content = fs.readFileSync(logPath, "utf-8");
352
321
  const entries = content.split("\n---\n").filter(Boolean);
@@ -380,7 +349,7 @@ async function runCommandTriggeredMode(
380
349
  }
381
350
  appendLog(line, "", result.outcome);
382
351
 
383
- // Append monitoring status so the UI shows the task is waiting for more input
352
+ // Signal "waiting for more input" in the UI.
384
353
  appendRunMessage(ctx.taskDir, ctx.runId, { role: "status", time: Date.now(), content: "", type: "monitoring" });
385
354
  await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
386
355
  }
@@ -422,7 +391,6 @@ async function runCommandTriggeredMode(
422
391
  process.stderr.write(d);
423
392
  });
424
393
 
425
- // Wait for command to exit
426
394
  const exitCode = await new Promise<number | null>((resolve) => {
427
395
  child.on("close", (code: number | null) => {
428
396
  commandExited = true;
@@ -438,7 +406,6 @@ async function runCommandTriggeredMode(
438
406
  });
439
407
  });
440
408
 
441
- // Wait for any remaining queued lines to finish processing
442
409
  if (lineQueue.length > 0 || processing) {
443
410
  await new Promise<void>((resolve) => {
444
411
  resolveWhenDone = resolve;
@@ -464,13 +431,10 @@ async function runCommandTriggeredMode(
464
431
  }
465
432
 
466
433
  /**
467
- * Event-triggered execution mode.
468
- *
469
- * Drains the daemon-owned per-task event queue via the local /task-event/pop
470
- * HTTP endpoint, invoking the agent once per event with the payload spliced
471
- * into the user prompt. The run process itself holds no NATS subscription;
472
- * the daemon handles that and atomically clears the active flag when we see
473
- * an empty pop, so it can fire up a fresh run on the next incoming event.
434
+ * Drain the daemon-owned per-task event queue via /task-event/pop, invoking
435
+ * the agent once per event. The run process holds no NATS subscription — the
436
+ * daemon owns that and atomically clears the active flag on empty pop so it
437
+ * can fire a fresh run on the next incoming event.
474
438
  */
475
439
  async function runEventTriggeredMode(
476
440
  ctx: InvocationContext,
@@ -590,10 +554,6 @@ async function requestConfirmation(
590
554
  return confirmed;
591
555
  }
592
556
 
593
- /**
594
- * Extract report file names from agent output.
595
- * Looks for lines matching: [PALMIER_REPORT] <filename>
596
- */
597
557
  const ALLOWED_REPORT_EXT = [".md", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"];
598
558
 
599
559
  export function parseReportFiles(output: string): string[] {
@@ -602,7 +562,7 @@ export function parseReportFiles(output: string): string[] {
602
562
  let match;
603
563
  while ((match = regex.exec(output)) !== null) {
604
564
  const name = match[1].trim();
605
- // Skip placeholder examples echoed from the prompt (e.g. "<filename>")
565
+ // Skip placeholder examples echoed from the prompt (e.g. "<filename>").
606
566
  if (!name || name.startsWith("<")) continue;
607
567
  const ext = name.lastIndexOf(".") >= 0 ? name.slice(name.lastIndexOf(".")).toLowerCase() : "";
608
568
  if (!ALLOWED_REPORT_EXT.includes(ext)) continue;
@@ -611,17 +571,13 @@ export function parseReportFiles(output: string): string[] {
611
571
  return files;
612
572
  }
613
573
 
614
- /**
615
- * Extract required permissions from agent output.
616
- * Looks for lines matching: [PALMIER_PERMISSION] <tool> | <description>
617
- */
618
574
  export function parsePermissions(output: string): RequiredPermission[] {
619
575
  const regex = new RegExp(`^\\${TASK_PERMISSION_PREFIX}\\s+(.+)$`, "gm");
620
576
  const perms: RequiredPermission[] = [];
621
577
  let match;
622
578
  while ((match = regex.exec(output)) !== null) {
623
579
  const raw = match[1].trim();
624
- // Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>")
580
+ // Skip placeholder examples echoed from the prompt (e.g. "<tool_name> | <description>").
625
581
  if (raw.startsWith("<")) continue;
626
582
  const sep = raw.indexOf("|");
627
583
  if (sep !== -1) {
@@ -633,10 +589,7 @@ export function parsePermissions(output: string): RequiredPermission[] {
633
589
  return perms;
634
590
  }
635
591
 
636
- /**
637
- * Parse the agent's output for success/failure markers.
638
- * Falls back to "finished" if no marker is found.
639
- */
592
+ /** Falls back to "finished" if no success/failure marker is found. */
640
593
  export function parseTaskOutcome(output: string): TaskRunningState {
641
594
  const lastChunk = output.slice(-500);
642
595
  const regex = new RegExp(`^\\${TASK_FAILURE_MARKER}$|^\\${TASK_SUCCESS_MARKER}$`, "gm");