heartbeat-opencode-plugin 0.1.0 → 0.1.2

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/index.ts CHANGED
@@ -4,6 +4,8 @@ import { SchedulerPlugin } from "./scheduler";
4
4
  import { TaskManagerPlugin, getTask, listTasks } from "./task-manager";
5
5
 
6
6
  type PluginOutput = Awaited<ReturnType<Plugin>>;
7
+ const OUTPUT_TAG_NOTE =
8
+ "Runtime contract: while running Heartbeat programs, every emitted status/output line must be prefixed with [program][taskId] for grepability.";
7
9
 
8
10
  function mergeTools(outputs: PluginOutput[]): Record<string, unknown> {
9
11
  const toolMap: Record<string, unknown> = {};
@@ -30,7 +32,7 @@ export const HeartbeatPlugin: Plugin = async (ctx) => {
30
32
 
31
33
  heartbeat_boot_context: tool({
32
34
  description:
33
- "Boot helper for a CPU cycle. Returns task state + prior memory for [program][taskId].",
35
+ `Boot helper for a CPU cycle. Returns task state + prior memory for [program][taskId]. ${OUTPUT_TAG_NOTE}`,
34
36
  args: {
35
37
  program: tool.schema.string().describe("Program name"),
36
38
  taskId: tool.schema.string().describe("Task identifier"),
package/memory/index.ts CHANGED
@@ -35,6 +35,8 @@ export function writeLog(input: {
35
35
 
36
36
  const LOG_ENTRY_PREFIX =
37
37
  /^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\]\s/;
38
+ const OUTPUT_TAG_NOTE =
39
+ "Runtime contract: while running Heartbeat programs, every emitted status/output line must be prefixed with [program][taskId] for grepability.";
38
40
 
39
41
  function readAllEntries(limit?: number): string[] {
40
42
  const path = getLogFilePath();
@@ -180,7 +182,8 @@ export const MemoryPlugin: Plugin = async (ctx) => {
180
182
  return {
181
183
  tool: {
182
184
  memory_write: tool({
183
- description: "Write a tagged log line as [program][taskId] message.",
185
+ description:
186
+ `Write a tagged log line as [program][taskId] message. ${OUTPUT_TAG_NOTE}`,
184
187
  args: {
185
188
  program: tool.schema.string().describe("Program name"),
186
189
  taskId: tool.schema.string().describe("Task identifier"),
@@ -193,7 +196,7 @@ export const MemoryPlugin: Plugin = async (ctx) => {
193
196
 
194
197
  write_log: tool({
195
198
  description:
196
- "Alias for memory_write. Writes tagged memory lines into the shared heartbeat log.",
199
+ `Alias for memory_write. Writes tagged memory lines into the shared heartbeat log. ${OUTPUT_TAG_NOTE}`,
197
200
  args: {
198
201
  program: tool.schema.string().describe("Program name"),
199
202
  taskId: tool.schema.string().describe("Task identifier"),
@@ -206,7 +209,7 @@ export const MemoryPlugin: Plugin = async (ctx) => {
206
209
 
207
210
  search_memory: tool({
208
211
  description:
209
- "Search memory by [program] and optional [taskId]. Use this on boot to restore previous cycle context.",
212
+ `Search memory by [program] and optional [taskId]. Use this on boot to restore previous cycle context. ${OUTPUT_TAG_NOTE}`,
210
213
  args: {
211
214
  program: tool.schema.string().describe("Program name"),
212
215
  taskId: tool.schema
@@ -227,7 +230,7 @@ export const MemoryPlugin: Plugin = async (ctx) => {
227
230
  }),
228
231
 
229
232
  read_program: tool({
230
- description: "Read all log lines containing [program].",
233
+ description: `Read all log lines containing [program]. ${OUTPUT_TAG_NOTE}`,
231
234
  args: {
232
235
  program: tool.schema.string().describe("Program name"),
233
236
  limit: tool.schema.number().optional().describe("Optional max lines"),
@@ -238,7 +241,8 @@ export const MemoryPlugin: Plugin = async (ctx) => {
238
241
  }),
239
242
 
240
243
  read_program_task: tool({
241
- description: "Read all log lines containing [program][taskId].",
244
+ description:
245
+ `Read all log lines containing [program][taskId]. ${OUTPUT_TAG_NOTE}`,
242
246
  args: {
243
247
  program: tool.schema.string().describe("Program name"),
244
248
  taskId: tool.schema.string().describe("Task identifier"),
@@ -251,7 +255,7 @@ export const MemoryPlugin: Plugin = async (ctx) => {
251
255
 
252
256
  memory_grep: tool({
253
257
  description:
254
- "Regex grep across the shared heartbeat log file. Useful for free-form memory queries.",
258
+ `Regex grep across the shared heartbeat log file. Useful for free-form memory queries. ${OUTPUT_TAG_NOTE}`,
255
259
  args: {
256
260
  pattern: tool.schema.string().describe("JavaScript regex pattern"),
257
261
  limit: tool.schema.number().optional().describe("Optional max lines"),
@@ -266,7 +270,7 @@ export const MemoryPlugin: Plugin = async (ctx) => {
266
270
  }),
267
271
 
268
272
  grep_logs: tool({
269
- description: "Alias for memory_grep.",
273
+ description: `Alias for memory_grep. ${OUTPUT_TAG_NOTE}`,
270
274
  args: {
271
275
  pattern: tool.schema.string().describe("JavaScript regex pattern"),
272
276
  limit: tool.schema.number().optional().describe("Optional max lines"),
@@ -281,7 +285,7 @@ export const MemoryPlugin: Plugin = async (ctx) => {
281
285
  }),
282
286
 
283
287
  tail_logs: tool({
284
- description: "Return recent lines from the shared heartbeat log.",
288
+ description: `Return recent lines from the shared heartbeat log. ${OUTPUT_TAG_NOTE}`,
285
289
  args: {
286
290
  lines: tool.schema
287
291
  .number()
@@ -294,7 +298,7 @@ export const MemoryPlugin: Plugin = async (ctx) => {
294
298
  }),
295
299
 
296
300
  log_info: tool({
297
- description: "Get shared log file path and stats.",
301
+ description: `Get shared log file path and stats. ${OUTPUT_TAG_NOTE}`,
298
302
  args: {},
299
303
  async execute() {
300
304
  return JSON.stringify(getLogInfo(), null, 2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heartbeat-opencode-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Heartbeat Runtime",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -70,6 +70,24 @@ export interface RunResult {
70
70
  const IS_MAC = process.platform === "darwin";
71
71
  const LAUNCH_AGENTS_DIR = path.join(homedir(), "Library", "LaunchAgents");
72
72
  const SCHEDULER_ENTRY = fileURLToPath(import.meta.url);
73
+ const OPENCODE_COMMAND = "opencode";
74
+ const OPENCODE_FALLBACK_PATHS = [
75
+ "/opt/homebrew/bin/opencode",
76
+ "/usr/local/bin/opencode",
77
+ ];
78
+ const DEFAULT_EXEC_PATH_ENTRIES = [
79
+ "/opt/homebrew/bin",
80
+ "/opt/homebrew/sbin",
81
+ "/usr/local/bin",
82
+ "/usr/local/sbin",
83
+ "/usr/bin",
84
+ "/bin",
85
+ "/usr/sbin",
86
+ "/sbin",
87
+ "/Library/Apple/usr/bin",
88
+ ];
89
+ const OUTPUT_TAG_NOTE =
90
+ "Runtime contract: while running Heartbeat programs, every emitted status/output line must be prefixed with [program][taskId] for grepability.";
73
91
 
74
92
  function ensureDir(dir: string): void {
75
93
  if (!fs.existsSync(dir)) {
@@ -103,6 +121,75 @@ function appendJobLog(job: Pick<Job, "id">, message: string): void {
103
121
  appendLog(`[Scheduler][job:${job.id}] ${message}`);
104
122
  }
105
123
 
124
+ function isExecutable(filePath: string): boolean {
125
+ try {
126
+ fs.accessSync(filePath, fs.constants.X_OK);
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+
133
+ function findExecutableInPath(command: string, envPath?: string): string | null {
134
+ if (!envPath) return null;
135
+
136
+ const pathEntries = envPath.split(path.delimiter).filter(Boolean);
137
+ for (const pathEntry of pathEntries) {
138
+ const candidate = path.join(pathEntry, command);
139
+ if (isExecutable(candidate)) {
140
+ return candidate;
141
+ }
142
+ }
143
+
144
+ return null;
145
+ }
146
+
147
+ function resolveOpencodeCommand(command: string): string {
148
+ if (path.basename(command) !== OPENCODE_COMMAND) {
149
+ return command;
150
+ }
151
+
152
+ if (isExecutable(command)) {
153
+ return command;
154
+ }
155
+
156
+ const envOverride = process.env.OPENCODE_BIN?.trim();
157
+ if (envOverride && isExecutable(envOverride)) {
158
+ return envOverride;
159
+ }
160
+
161
+ const fromPath = findExecutableInPath(OPENCODE_COMMAND, process.env.PATH);
162
+ if (fromPath) {
163
+ return fromPath;
164
+ }
165
+
166
+ for (const fallbackPath of OPENCODE_FALLBACK_PATHS) {
167
+ if (isExecutable(fallbackPath)) {
168
+ return fallbackPath;
169
+ }
170
+ }
171
+
172
+ return command;
173
+ }
174
+
175
+ function buildExecutablePath(): string {
176
+ const currentEntries = (process.env.PATH ?? "")
177
+ .split(path.delimiter)
178
+ .map((entry) => entry.trim())
179
+ .filter(Boolean);
180
+
181
+ const merged = [...currentEntries, ...DEFAULT_EXEC_PATH_ENTRIES];
182
+ const unique = Array.from(new Set(merged));
183
+ return unique.join(path.delimiter);
184
+ }
185
+
186
+ function buildJobEnv(): NodeJS.ProcessEnv {
187
+ return {
188
+ ...process.env,
189
+ PATH: buildExecutablePath(),
190
+ };
191
+ }
192
+
106
193
  function readJob(jobId: string): Job | null {
107
194
  const file = jobFilePath(jobId);
108
195
  if (!fs.existsSync(file)) return null;
@@ -228,7 +315,7 @@ export function createOpencodeJob(input: CreateOpencodeJobInput): Job {
228
315
  kind: "opencode",
229
316
  schedule: input.schedule,
230
317
  prompt: input.prompt,
231
- command: "opencode",
318
+ command: resolveOpencodeCommand(OPENCODE_COMMAND),
232
319
  args: ["run", heartbeatPrompt],
233
320
  workdir: input.workdir ?? getDefaultWorkdir(),
234
321
  enabled: input.enabled ?? true,
@@ -261,7 +348,7 @@ export function updateJob(
261
348
  if (!nextPrompt) {
262
349
  throw new Error(`Opencode job ${existing.id} is missing prompt content.`);
263
350
  }
264
- nextCommand = "opencode";
351
+ nextCommand = resolveOpencodeCommand(OPENCODE_COMMAND);
265
352
  nextArgs = [
266
353
  "run",
267
354
  buildHeartbeatPrompt({
@@ -364,14 +451,23 @@ export function runJob(jobId: string): Promise<RunResult> {
364
451
  writeJob(job);
365
452
 
366
453
  appendJobLog(job, `Starting job "${job.name}"`);
367
- appendJobLog(job, `Command: ${job.command} ${job.args.join(" ")}`);
454
+ const jobEnv = buildJobEnv();
455
+ const resolvedCommand = job.kind === "opencode"
456
+ ? resolveOpencodeCommand(job.command)
457
+ : job.command;
458
+ if (resolvedCommand !== job.command) {
459
+ job.command = resolvedCommand;
460
+ appendJobLog(job, `Resolved opencode executable: ${resolvedCommand}`);
461
+ }
462
+ appendJobLog(job, `Command: ${resolvedCommand} ${job.args.join(" ")}`);
368
463
  appendJobLog(job, `Workdir: ${job.workdir}`);
464
+ appendJobLog(job, `PATH: ${jobEnv.PATH ?? ""}`);
369
465
 
370
466
  let child: ReturnType<typeof spawn>;
371
467
  try {
372
- child = spawn(job.command, job.args, {
468
+ child = spawn(resolvedCommand, job.args, {
373
469
  cwd: job.workdir,
374
- env: process.env,
470
+ env: jobEnv,
375
471
  shell: false,
376
472
  stdio: ["ignore", "pipe", "pipe"],
377
473
  });
@@ -625,6 +721,7 @@ function buildCalendarXml(calendars: CalendarEntry[]): string {
625
721
  function createLaunchdPlist(job: Job): string {
626
722
  const label = getConfigLaunchdLabel(job.id);
627
723
  const parsed = parseCronToLaunchd(job.schedule);
724
+ const executablePath = buildExecutablePath();
628
725
 
629
726
  const scheduleBlock = parsed.startInterval
630
727
  ? ` <key>StartInterval</key>\n <integer>${parsed.startInterval}</integer>`
@@ -646,6 +743,11 @@ function createLaunchdPlist(job: Job): string {
646
743
  <string>run</string>
647
744
  <string>${escapeXml(job.id)}</string>
648
745
  </array>
746
+ <key>EnvironmentVariables</key>
747
+ <dict>
748
+ <key>PATH</key>
749
+ <string>${escapeXml(executablePath)}</string>
750
+ </dict>
649
751
  ${scheduleBlock}
650
752
  <key>StandardOutPath</key>
651
753
  <string>${escapeXml(sharedLogPath())}</string>
@@ -752,7 +854,7 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
752
854
  tool: {
753
855
  scheduler_create_opencode_job: tool({
754
856
  description:
755
- "Create a human-managed cron job that runs `opencode run` with the provided prompt.",
857
+ `Create a human-managed cron job that runs \`opencode run\` with the provided prompt. ${OUTPUT_TAG_NOTE}`,
756
858
  args: {
757
859
  name: tool.schema.string().describe("Job name"),
758
860
  schedule: tool.schema.string().describe("Cron schedule (e.g. */5 * * * *)"),
@@ -803,7 +905,7 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
803
905
 
804
906
  scheduler_create_command_job: tool({
805
907
  description:
806
- "Create a human-managed cron job for a generic command. Output streams to the shared heartbeat log.",
908
+ `Create a human-managed cron job for a generic command. Output streams to the shared heartbeat log. ${OUTPUT_TAG_NOTE}`,
807
909
  args: {
808
910
  name: tool.schema.string().describe("Job name"),
809
911
  schedule: tool.schema.string().describe("Cron schedule"),
@@ -830,7 +932,7 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
830
932
  }),
831
933
 
832
934
  scheduler_list_jobs: tool({
833
- description: "List all heartbeat scheduler jobs.",
935
+ description: `List all heartbeat scheduler jobs. ${OUTPUT_TAG_NOTE}`,
834
936
  args: {},
835
937
  async execute() {
836
938
  const jobs = listJobs();
@@ -842,7 +944,7 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
842
944
  }),
843
945
 
844
946
  scheduler_get_job: tool({
845
- description: "Get a scheduler job by id.",
947
+ description: `Get a scheduler job by id. ${OUTPUT_TAG_NOTE}`,
846
948
  args: {
847
949
  jobId: tool.schema.string().describe("Job ID"),
848
950
  },
@@ -856,7 +958,8 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
856
958
  }),
857
959
 
858
960
  scheduler_update_job: tool({
859
- description: "Update schedule, prompt, or metadata of an existing job.",
961
+ description:
962
+ `Update schedule, prompt, or metadata of an existing job. ${OUTPUT_TAG_NOTE}`,
860
963
  args: {
861
964
  jobId: tool.schema.string().describe("Job ID"),
862
965
  name: tool.schema.string().optional().describe("Updated name"),
@@ -883,7 +986,8 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
883
986
  }),
884
987
 
885
988
  scheduler_run_job: tool({
886
- description: "Run a job immediately. Output is appended to one shared log file.",
989
+ description:
990
+ `Run a job immediately. Output is appended to one shared log file. ${OUTPUT_TAG_NOTE}`,
887
991
  args: {
888
992
  jobId: tool.schema.string().describe("Job ID"),
889
993
  },
@@ -898,7 +1002,7 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
898
1002
  }),
899
1003
 
900
1004
  scheduler_install_job: tool({
901
- description: "Install a job in macOS launchd.",
1005
+ description: `Install a job in macOS launchd. ${OUTPUT_TAG_NOTE}`,
902
1006
  args: {
903
1007
  jobId: tool.schema.string().describe("Job ID"),
904
1008
  },
@@ -916,7 +1020,7 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
916
1020
  }),
917
1021
 
918
1022
  scheduler_uninstall_job: tool({
919
- description: "Uninstall a job from macOS launchd.",
1023
+ description: `Uninstall a job from macOS launchd. ${OUTPUT_TAG_NOTE}`,
920
1024
  args: {
921
1025
  jobId: tool.schema.string().describe("Job ID"),
922
1026
  },
@@ -934,7 +1038,8 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
934
1038
  }),
935
1039
 
936
1040
  scheduler_delete_job: tool({
937
- description: "Delete a scheduler job and uninstall its launchd entry if present.",
1041
+ description:
1042
+ `Delete a scheduler job and uninstall its launchd entry if present. ${OUTPUT_TAG_NOTE}`,
938
1043
  args: {
939
1044
  jobId: tool.schema.string().describe("Job ID"),
940
1045
  },
@@ -945,7 +1050,7 @@ export const SchedulerPlugin: Plugin = async (ctx) => {
945
1050
  }),
946
1051
 
947
1052
  scheduler_info: tool({
948
- description: "Show scheduler storage paths and shared log path.",
1053
+ description: `Show scheduler storage paths and shared log path. ${OUTPUT_TAG_NOTE}`,
949
1054
  args: {},
950
1055
  async execute() {
951
1056
  return JSON.stringify(
@@ -40,6 +40,8 @@ const VALID_PRIORITIES = new Set<TaskPriority>([
40
40
  "high",
41
41
  "critical",
42
42
  ]);
43
+ const OUTPUT_TAG_NOTE =
44
+ "Runtime contract: while running Heartbeat programs, every emitted status/output line must be prefixed with [program][taskId] for grepability.";
43
45
 
44
46
  function nowIso(): string {
45
47
  return new Date().toISOString();
@@ -128,7 +130,8 @@ export function listTasks(filters?: TaskFilters): Task[] {
128
130
  const tasks = storeInstance().read();
129
131
  return tasks.filter((task) => {
130
132
  if (filters?.program && task.program !== filters.program) return false;
131
- if (filters?.parentTaskId && task.parentTaskId !== filters.parentTaskId) return false;
133
+ if (filters?.parentTaskId && task.parentTaskId !== filters.parentTaskId)
134
+ return false;
132
135
  if (filters?.status && task.status !== filters.status) return false;
133
136
  if (filters?.priority && task.priority !== filters.priority) return false;
134
137
  return true;
@@ -137,7 +140,9 @@ export function listTasks(filters?: TaskFilters): Task[] {
137
140
 
138
141
  export function getTask(program: string, taskId: string): Task | undefined {
139
142
  const tasks = storeInstance().read();
140
- return tasks.find((task) => taskKey(task.program, task.taskId) === taskKey(program, taskId));
143
+ return tasks.find(
144
+ (task) => taskKey(task.program, task.taskId) === taskKey(program, taskId),
145
+ );
141
146
  }
142
147
 
143
148
  export function createTask(input: {
@@ -152,7 +157,11 @@ export function createTask(input: {
152
157
  const instance = storeInstance();
153
158
  const tasks = instance.read();
154
159
 
155
- const duplicate = tasks.find((task) => taskKey(task.program, task.taskId) === taskKey(input.program, input.taskId));
160
+ const duplicate = tasks.find(
161
+ (task) =>
162
+ taskKey(task.program, task.taskId) ===
163
+ taskKey(input.program, input.taskId),
164
+ );
156
165
  if (duplicate) {
157
166
  throw new Error(`Task already exists: [${input.program}][${input.taskId}]`);
158
167
  }
@@ -172,7 +181,9 @@ export function createTask(input: {
172
181
 
173
182
  tasks.push(task);
174
183
  instance.write(tasks);
175
- appendLog(`[${task.program}][${task.taskId}] Task created with status=${task.status}`);
184
+ appendLog(
185
+ `[${task.program}][${task.taskId}] Task created with status=${task.status}`,
186
+ );
176
187
  return task;
177
188
  }
178
189
 
@@ -187,7 +198,11 @@ export function updateTask(input: {
187
198
  }): Task | null {
188
199
  const instance = storeInstance();
189
200
  const tasks = instance.read();
190
- const index = tasks.findIndex((task) => taskKey(task.program, task.taskId) === taskKey(input.program, input.taskId));
201
+ const index = tasks.findIndex(
202
+ (task) =>
203
+ taskKey(task.program, task.taskId) ===
204
+ taskKey(input.program, input.taskId),
205
+ );
191
206
 
192
207
  if (index === -1) return null;
193
208
 
@@ -195,9 +210,13 @@ export function updateTask(input: {
195
210
  const updated: Task = {
196
211
  ...existing,
197
212
  ...("parentTaskId" in input ? { parentTaskId: input.parentTaskId } : {}),
198
- ...("description" in input ? { description: input.description ?? existing.description } : {}),
213
+ ...("description" in input
214
+ ? { description: input.description ?? existing.description }
215
+ : {}),
199
216
  ...("status" in input ? { status: input.status ?? existing.status } : {}),
200
- ...("priority" in input ? { priority: input.priority ?? existing.priority } : {}),
217
+ ...("priority" in input
218
+ ? { priority: input.priority ?? existing.priority }
219
+ : {}),
201
220
  ...("references" in input ? { references: input.references } : {}),
202
221
  updatedAt: nowIso(),
203
222
  };
@@ -211,7 +230,9 @@ export function updateTask(input: {
211
230
 
212
231
  tasks[index] = updated;
213
232
  instance.write(tasks);
214
- appendLog(`[${updated.program}][${updated.taskId}] Task updated status=${updated.status}`);
233
+ appendLog(
234
+ `[${updated.program}][${updated.taskId}] Task updated status=${updated.status}`,
235
+ );
215
236
  return updated;
216
237
  }
217
238
 
@@ -226,7 +247,9 @@ export function markTaskDone(program: string, taskId: string): Task | null {
226
247
  export function deleteTask(program: string, taskId: string): boolean {
227
248
  const instance = storeInstance();
228
249
  const tasks = instance.read();
229
- const filtered = tasks.filter((task) => taskKey(task.program, task.taskId) !== taskKey(program, taskId));
250
+ const filtered = tasks.filter(
251
+ (task) => taskKey(task.program, task.taskId) !== taskKey(program, taskId),
252
+ );
230
253
 
231
254
  if (filtered.length === tasks.length) {
232
255
  return false;
@@ -247,20 +270,27 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
247
270
  return {
248
271
  tool: {
249
272
  task_create: tool({
250
- description:
251
- "Create a task tracked by [program][taskId]. Use this before running a cron cycle.",
273
+ description: `Create a task tracked by [program][taskId]. `,
252
274
  args: {
253
- program: tool.schema.string().describe("Program name used for task and memory tags"),
254
- taskId: tool.schema.string().describe("Task identifier in kebab-case"),
275
+ program: tool.schema
276
+ .string()
277
+ .describe("Program name used for task and memory tags"),
278
+ taskId: tool.schema
279
+ .string()
280
+ .describe("Task identifier in snake_case"),
255
281
  parentTaskId: tool.schema
256
282
  .string()
257
283
  .optional()
258
284
  .describe("Optional parent task ID to create task hierarchies"),
259
- description: tool.schema.string().describe("Human description of the task"),
285
+ description: tool.schema
286
+ .string()
287
+ .describe("Human description of the task"),
260
288
  status: tool.schema
261
289
  .string()
262
290
  .optional()
263
- .describe("Optional status: pending, running, blocked, raised, done"),
291
+ .describe(
292
+ "Optional status: pending, running, blocked, raised, done",
293
+ ),
264
294
  priority: tool.schema
265
295
  .string()
266
296
  .optional()
@@ -299,12 +329,24 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
299
329
  }),
300
330
 
301
331
  task_list: tool({
302
- description: "List tasks, optionally filtered by program/status/priority.",
332
+ description: `List tasks, optionally filtered by program/status/priority. ${OUTPUT_TAG_NOTE}`,
303
333
  args: {
304
- program: tool.schema.string().optional().describe("Optional program filter"),
305
- parentTaskId: tool.schema.string().optional().describe("Optional parent task filter"),
306
- status: tool.schema.string().optional().describe("Optional status filter"),
307
- priority: tool.schema.string().optional().describe("Optional priority filter"),
334
+ program: tool.schema
335
+ .string()
336
+ .optional()
337
+ .describe("Optional program filter"),
338
+ parentTaskId: tool.schema
339
+ .string()
340
+ .optional()
341
+ .describe("Optional parent task filter"),
342
+ status: tool.schema
343
+ .string()
344
+ .optional()
345
+ .describe("Optional status filter"),
346
+ priority: tool.schema
347
+ .string()
348
+ .optional()
349
+ .describe("Optional priority filter"),
308
350
  },
309
351
  async execute(args) {
310
352
  const status = normalizeStatus(args.status);
@@ -331,7 +373,7 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
331
373
  }),
332
374
 
333
375
  task_get: tool({
334
- description: "Get a single task by [program][taskId].",
376
+ description: `Get a single task by [program][taskId]. ${OUTPUT_TAG_NOTE}`,
335
377
  args: {
336
378
  program: tool.schema.string().describe("Program name"),
337
379
  taskId: tool.schema.string().describe("Task ID"),
@@ -346,7 +388,7 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
346
388
  }),
347
389
 
348
390
  task_update: tool({
349
- description: "Update task metadata or status.",
391
+ description: `Update task metadata or status. ${OUTPUT_TAG_NOTE}`,
350
392
  args: {
351
393
  program: tool.schema.string().describe("Program name"),
352
394
  taskId: tool.schema.string().describe("Task ID"),
@@ -354,10 +396,19 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
354
396
  .string()
355
397
  .optional()
356
398
  .describe("Updated parent task ID"),
357
- description: tool.schema.string().optional().describe("Updated description"),
399
+ description: tool.schema
400
+ .string()
401
+ .optional()
402
+ .describe("Updated description"),
358
403
  status: tool.schema.string().optional().describe("Updated status"),
359
- priority: tool.schema.string().optional().describe("Updated priority"),
360
- references: tool.schema.string().optional().describe("Updated references"),
404
+ priority: tool.schema
405
+ .string()
406
+ .optional()
407
+ .describe("Updated priority"),
408
+ references: tool.schema
409
+ .string()
410
+ .optional()
411
+ .describe("Updated references"),
361
412
  },
362
413
  async execute(args) {
363
414
  const status = normalizeStatus(args.status);
@@ -410,7 +461,7 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
410
461
  }),
411
462
 
412
463
  task_mark_done: tool({
413
- description: "Mark a task as done after it is resolved.",
464
+ description: `Mark a task as done after it is resolved. ${OUTPUT_TAG_NOTE}`,
414
465
  args: {
415
466
  program: tool.schema.string().describe("Program name"),
416
467
  taskId: tool.schema.string().describe("Task ID"),
@@ -425,7 +476,7 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
425
476
  }),
426
477
 
427
478
  task_delete: tool({
428
- description: "Delete a task from the task manager.",
479
+ description: `Delete a task from the task manager. ${OUTPUT_TAG_NOTE}`,
429
480
  args: {
430
481
  program: tool.schema.string().describe("Program name"),
431
482
  taskId: tool.schema.string().describe("Task ID"),
@@ -440,7 +491,7 @@ export const TaskManagerPlugin: Plugin = async (ctx) => {
440
491
  }),
441
492
 
442
493
  task_store_info: tool({
443
- description: "Get task store file path and current task count.",
494
+ description: `Get task store file path and current task count. ${OUTPUT_TAG_NOTE}`,
444
495
  args: {},
445
496
  async execute() {
446
497
  const tasks = listTasks();