@ai-setting/roy-task-plugin 0.7.2 → 0.7.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.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: roy
3
3
  description: Roy — task-as-first-class-citizen agent. Uses the roy-agent task lifecycle (task_tree → task_create → task_update → task_operation_create → task_complete) so every piece of work is tracked in the local task store.
4
- tools: Read, Write, Edit, Bash, Grep, Glob, TodoWrite, WebFetch, WebSearch
4
+ tools: Read, Write, Edit, Bash, Grep, Glob, TodoWrite, WebFetch, WebSearch, mcp__roy-task__task_create, mcp__roy-task__task_update, mcp__roy-task__task_operation_create, mcp__roy-task__task_complete, mcp__roy-task__task_get, mcp__roy-task__task_list, mcp__roy-task__task_tree, mcp__roy-task__task_search
5
5
  model: inherit
6
6
  ---
7
7
 
@@ -89,23 +89,28 @@ if it is not in the task store, it is not tracked.
89
89
 
90
90
  ## Tool surface on Claude Code
91
91
 
92
- The lifecycle above is expressed through the `roy-agent tasks` CLI (run it with
93
- the `Bash` tool), because Claude Code does not yet expose the `task_*` tools
94
- natively. The mapping is 1:1 — same store, same ids, same semantics:
92
+ The `task_*` tools are served by the **`roy-task` MCP server** this runtime
93
+ loads. Call them directly:
95
94
 
96
- | lifecycle step | Claude Code command |
95
+ | lifecycle step | tool |
97
96
  |---|---|
98
- | `task_tree(depth=1)` | `roy-agent tasks tree` |
99
- | `task_create` | `roy-agent tasks create "<title>" -d "<description>" -g "<goals>" -p high --project-path "<path>"` |
100
- | `task_update` | `roy-agent tasks update <id> --status active --progress 50 --current-status "…"` |
101
- | `task_operation_create` | `roy-agent tasks operations <id>` to read; record milestones via the CLI's update/ops surface |
102
- | `task_complete` | `roy-agent tasks complete <id> --deliverable "<path>"` |
103
- | `task_get` | `roy-agent tasks get <id>` |
104
- | `task_list` | `roy-agent tasks list` |
105
- | `task_search` | `roy-agent tasks search <keywords>` |
106
-
107
- Prefer `--json` (`-j`) when you need to parse ids. The task id printed by
108
- `create` is your handle for the rest of the turn.
97
+ | `task_tree(depth=1)` | `mcp__roy-task__task_tree` |
98
+ | `task_create` | `mcp__roy-task__task_create` |
99
+ | `task_update` | `mcp__roy-task__task_update` |
100
+ | `task_operation_create` | `mcp__roy-task__task_operation_create` |
101
+ | `task_complete` | `mcp__roy-task__task_complete` |
102
+ | `task_get` | `mcp__roy-task__task_get` |
103
+ | `task_list` | `mcp__roy-task__task_list` |
104
+ | `task_search` | `mcp__roy-task__task_search` |
105
+
106
+ Same store, same ids, same semantics as the opencode tools — the daemon's
107
+ progress card renders the task these tools create.
108
+
109
+ ⚠️ **Do not** shell out to the `roy-agent tasks` CLI as a substitute.
110
+ Measured: it has no way to write a milestone (its `operations <id>` subcommand
111
+ is a *reader*), and it targets a different database than this server. Use the
112
+ MCP tools above; if they are genuinely unavailable, say so in your answer
113
+ instead of silently skipping the lifecycle.
109
114
 
110
115
  ## Style
111
116
 
@@ -176,7 +176,13 @@ async function main() {
176
176
 
177
177
  let backend;
178
178
  try {
179
- const server = createServer(process.env.ROY_TASK_DB_PATH || defaultTaskDbPath());
179
+ // Task #4692: createServer is async (it loads core's hook modules before
180
+ // the first request can fire them). Without the await, `server` is a
181
+ // Promise and `server.backend` is undefined — the open-task check would
182
+ // silently degrade to "approve" and the Stop discipline would die.
183
+ const server = await createServer(
184
+ process.env.ROY_TASK_DB_PATH || defaultTaskDbPath(),
185
+ );
180
186
  // The server owns the store; the backend is what the hook needs.
181
187
  backend = server.backend;
182
188
  } catch (err) {
@@ -1,38 +1,16 @@
1
1
  /**
2
2
  * Bridge: roy's task store + roy's MCP server, wired together.
3
3
  *
4
- * ## What this file used to be
4
+ * A thin adapter (Task #4619): store location + binding to roy's
5
+ * semantics + transport. Every semantic decision lives in core, once.
6
+ * (Before #4619 this was 482 lines of raw SQL re-implementing roy.)
5
7
  *
6
- * Before Task #4619 this was a **second implementation** of roy's task
7
- * semantics: 482 lines that created its own `task` / `task_operation`
8
- * schema and implemented every tool as raw SQL — `taskTree` (BFS +
9
- * `pruneDepth`), `taskCreate`, `taskGet`, `taskList` (COUNT for `total`),
10
- * `taskUpdate`, `taskOperationCreate`, `taskComplete`, `taskSearch` — with
11
- * **zero** imports from `@ai-setting/roy-agent-core`.
8
+ * Task #4692 adds a fourth, opt-in responsibility: firing core's
9
+ * `LarkCliTaskNotifyHook` around each tool call (see
10
+ * `task-notify-hooks.mjs`) so `task_create` opens a Feishu card,
11
+ * `task_update` patches it, `task_complete` closes it.
12
12
  *
13
- * That was the same defect review #4371 found on the opencode side (an
14
- * 869-line `facade.ts`), except it hid inside a `.mjs` file, so the reviews
15
- * that counted TypeScript lines never saw it.
16
- *
17
- * ## What it is now
18
- *
19
- * A thin adapter with exactly three responsibilities:
20
- *
21
- * 1. **where** the store lives (roy-agent's own `tasks.db`, not the
22
- * plugin's private one — otherwise Claude would create tasks the
23
- * progress card can never see);
24
- * 2. **binding** that store to roy's semantics (`TaskStoreBackend` +
25
- * `createSQLiteTaskPort`, both from core);
26
- * 3. **transport** — handing core's MCP server the bytes.
27
- *
28
- * Every semantic decision (tree shape, depth, `total`, deliverable
29
- * merging, milestone filtering, search filters) lives in core, once.
30
- *
31
- * ## Runtime
32
- *
33
- * Plain node (`#!/usr/bin/env node` in the server entry) — hence core's
34
- * `node:sqlite` support (no native `better-sqlite3` needed) and core's
35
- * `createCompatibleDatabase` compat shim.
13
+ * Plain node runtime — core's `node:sqlite` (no native better-sqlite3).
36
14
  */
37
15
 
38
16
  import { createTaskMcpServer } from "@ai-setting/roy-agent-core/env/task/mcp";
@@ -43,6 +21,7 @@ import {
43
21
  import { SQLiteTaskStore } from "@ai-setting/roy-agent-core/env/task/storage";
44
22
  import { homedir } from "node:os";
45
23
  import { join } from "node:path";
24
+ import { createNotifyDispatcher } from "./task-notify-hooks.mjs";
46
25
 
47
26
  /** Raised when a tool call cannot be served. */
48
27
  export class BridgeError extends Error {
@@ -62,11 +41,15 @@ export function defaultTaskDbPath() {
62
41
  /**
63
42
  * Build the MCP server over roy's own task store.
64
43
  *
65
- * This is the whole "binding" the plugin does: core's `SQLiteTaskStore` →
66
- * core's `createSQLiteTaskPort` → core's `TaskStoreBackend` (the semantics)
67
- * → core's `createTaskMcpServer` (the transport-agnostic JSON-RPC surface).
44
+ * Task #4692: when `ROY_TASK_NOTIFY_ENABLED=1`, the same core hook the
45
+ * act pipeline uses is registered on `globalHookManager`, and every tool
46
+ * call runs inside the injected EnvContext — so `task_create` now opens
47
+ * a card in Feishu, `task_update` patches it, `task_complete` closes it.
48
+ *
49
+ * @param {string} dbPath task store path
50
+ * @param {{ cardNotifier?: object }} deps test seams (fake notifier)
68
51
  */
69
- export function createServer(dbPath = defaultTaskDbPath()) {
52
+ export async function createServer(dbPath = defaultTaskDbPath(), deps = {}) {
70
53
  const store = new SQLiteTaskStore(dbPath);
71
54
  const backend = new TaskStoreBackend(createSQLiteTaskPort(store));
72
55
  const server = createTaskMcpServer(backend, {
@@ -74,10 +57,20 @@ export function createServer(dbPath = defaultTaskDbPath()) {
74
57
  version: "1.0.0",
75
58
  sessionId: process.env.ROY_TASK_SESSION_ID || "claude-mcp",
76
59
  });
60
+
61
+ // Task #4692: fire the task:after.* hooks around every tool call. The
62
+ // wiring lives in task-notify-hooks.mjs; core owns the hook itself.
63
+ const notify = await createNotifyDispatcher({ backend, deps });
64
+ const callToolWithHooks = async (name, args = {}) => {
65
+ const result = await server.callTool(name, args);
66
+ await notify.fire(name, args, result);
67
+ return result;
68
+ };
69
+
77
70
  // The hooks (stop-check) need the *backend*, not the MCP surface: they
78
71
  // ask "is there an open task?" — a task-semantics question, answered by
79
72
  // core's backend, not by the JSON-RPC layer.
80
- return { ...server, backend, store };
73
+ return { ...server, backend, store, callTool: callToolWithHooks };
81
74
  }
82
75
 
83
76
  /**
@@ -93,6 +86,8 @@ export async function callTool(server, name, args = {}) {
93
86
  throw new BridgeError("tools/call requires a tool name");
94
87
  }
95
88
  try {
89
+ // Task #4692: `createServer` wraps callTool with the hook firing;
90
+ // a bare core server (no wrapper) still works.
96
91
  return await server.callTool(name, args);
97
92
  } catch (err) {
98
93
  if (err instanceof BridgeError) throw err;
@@ -83,7 +83,11 @@ setStdoutStrictMode(true);
83
83
  * the same server answered perfectly over a hand-rolled pipe.
84
84
  */
85
85
  function send(message) {
86
- process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", ...message })}\n`);
86
+ return new Promise((resolve) => {
87
+ const ok = process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", ...message })}\n`);
88
+ if (ok) resolve();
89
+ else process.stdout.once("drain", resolve);
90
+ });
87
91
  }
88
92
 
89
93
  function sendResult(id, result) {
@@ -246,11 +250,14 @@ const SERVER_INFO = { name: SERVER_NAME, version: SERVER_VERSION };
246
250
  const SERVER_CAPABILITIES = { tools: { listChanged: false } };
247
251
 
248
252
  /** Read newline-delimited JSON-RPC from stdin and answer on stdout. */
249
- export function main() {
253
+ export async function main() {
254
+ const pending = new Set();
250
255
  const dbPath = process.env.ROY_TASK_DB_PATH || defaultTaskDbPath();
251
256
  let server;
252
257
  try {
253
- server = createServer(dbPath);
258
+ // Task #4692: createServer is async (it loads core's hook modules
259
+ // before the first request can fire them).
260
+ server = await createServer(dbPath);
254
261
  } catch (err) {
255
262
  process.stderr.write(
256
263
  `roy-task mcp server: cannot open task store at ${dbPath}: ${err && err.message}\n`,
@@ -277,9 +284,9 @@ export function main() {
277
284
  return;
278
285
  }
279
286
 
280
- Promise.resolve(handleRequest(request, server))
281
- .then((response) => {
282
- if (response) send(response);
287
+ const done = Promise.resolve(handleRequest(request, server))
288
+ .then(async (response) => {
289
+ if (response) await send(response);
283
290
  })
284
291
  .catch((err) => {
285
292
  sendError(
@@ -288,9 +295,26 @@ export function main() {
288
295
  err && err.message ? err.message : String(err),
289
296
  );
290
297
  });
298
+ pending.add(done);
299
+ done.finally(() => pending.delete(done));
291
300
  });
292
301
 
293
- rl.on("close", () => {
302
+ // Task #4666: stdin EOF must not race the last response out.
303
+ //
304
+ // `process.exit(0)` from a `close` handler runs while big tool
305
+ // responses (a 45 KB `task_list` over the real DB) are still in
306
+ // node's stdout buffer — the client then sees a truncated JSON
307
+ // line (measured: `Unterminated string ... at position 50539`).
308
+ // Wait for every in-flight request to finish writing *and* for
309
+ // stdout to drain before exiting.
310
+ rl.on("close", async () => {
311
+ while (pending.size > 0) {
312
+ await Promise.all([...pending]);
313
+ }
314
+ await new Promise((resolve) => {
315
+ if (process.stdout.writableLength === 0) resolve();
316
+ else process.stdout.once("drain", resolve);
317
+ });
294
318
  process.exit(0);
295
319
  });
296
320
  }
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Task #4692 — the MCP server's task:after.* hook wiring.
3
+ *
4
+ * ## What this file is
5
+ *
6
+ * The "install core's Feishu card hooks into the MCP server
7
+ * process" half of the bridge. Kept in its own module so
8
+ * `bridge.mjs` stays a thin adapter (the < 140-line guard in
9
+ * `claude-bridge-thin.test.ts` pins that).
10
+ *
11
+ * Everything semantic lives in core: the hook
12
+ * (`LarkCliTaskNotifyHook`), the card renderer
13
+ * (`LarkCardPatchNotifier`), the card state store, the hook
14
+ * manager and the ALS EnvContext. This module only wires them —
15
+ * the user's architecture: "飞书插件机制就在roy-agent上,mcp server
16
+ * 就复用就好呀".
17
+ *
18
+ * ## The three pieces
19
+ *
20
+ * 1. **opt-in** — `notifyEnabled()`: off unless
21
+ * `ROY_TASK_NOTIFY_ENABLED=1` (existing deployments see zero
22
+ * change);
23
+ * 2. **destination** — `buildNotifyEnvContext()`: the hook reads
24
+ * the card's chat from the ALS EnvContext
25
+ * (`replyChannel.chatId/profile`), which is empty inside the
26
+ * MCP server process, so it is injected from
27
+ * `ROY_TASK_NOTIFY_CHAT_ID` / `ROY_TASK_NOTIFY_PROFILE` (env,
28
+ * because the server inherits the daemon's env — no argv
29
+ * changes, no TOML escaping);
30
+ * 3. **firing** — `hookPointsForTool()` + `buildHookData()`:
31
+ * map a completed tool call to the hook point(s) it implies,
32
+ * with context data shaped exactly like `TaskComponent`'s
33
+ * (the hook reads `task` / `changes` / `operation`).
34
+ */
35
+
36
+ /**
37
+ * Should the MCP server install the Feishu card hooks?
38
+ */
39
+ export function notifyEnabled() {
40
+ const v = process.env.ROY_TASK_NOTIFY_ENABLED;
41
+ return v === "1" || v === "true";
42
+ }
43
+
44
+ /**
45
+ * The EnvContext the hooks read for the card's destination.
46
+ *
47
+ * When no chat id is configured the context carries no
48
+ * replyChannel and the hook falls back to the task's own
49
+ * per-task group (same rule as the act pipeline).
50
+ */
51
+ export function buildNotifyEnvContext(contextMod) {
52
+ const chatId = process.env.ROY_TASK_NOTIFY_CHAT_ID;
53
+ const profile = process.env.ROY_TASK_NOTIFY_PROFILE;
54
+ return contextMod.createEnvContext({
55
+ envEvent: { source: "lark", type: "mcp-server" },
56
+ sourceId: "roy-task-mcp",
57
+ userId: "mcp-agent",
58
+ ...(chatId ? { replyChannel: { type: "lark", chatId, profile } } : {}),
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Map a tool call to the hook point(s) it semantically completed.
64
+ */
65
+ export function hookPointsForTool(name) {
66
+ switch (name) {
67
+ case "task_create":
68
+ return ["task:after.create"];
69
+ case "task_update":
70
+ return ["task:after.update"];
71
+ case "task_complete":
72
+ return ["task:after.complete"];
73
+ case "task_operation_create":
74
+ return ["task:operation:after.create"];
75
+ default:
76
+ return [];
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Install the core `LarkCliTaskNotifyHook` on the global hook manager.
82
+ *
83
+ * @returns the installed hook, or null when notify is disabled.
84
+ */
85
+ export async function installTaskNotifyHooks({ hookMod, pluginsMod, cardNotifier }) {
86
+ if (!notifyEnabled()) return null;
87
+ const hook = pluginsMod.createLarkCliTaskNotifyHook({
88
+ ...(cardNotifier ? { cardNotifier } : {}),
89
+ });
90
+ for (const hp of [
91
+ "task:after.create",
92
+ "task:after.update",
93
+ "task:after.complete",
94
+ "task:operation:after.create",
95
+ ]) {
96
+ // Idempotent: a second createServer() in the same process must not
97
+ // stack another copy of the hook (DS CV MINOR-5). The hook manager
98
+ // keys hooks by name, so removing the previous one first keeps the
99
+ // registration count at exactly 1.
100
+ hookMod.globalHookManager.unregister(hp, hook.name);
101
+ hookMod.globalHookManager.register(hp, hook);
102
+ }
103
+ return hook;
104
+ }
105
+
106
+ /**
107
+ * How long the card hooks may take before the tool call stops waiting.
108
+ *
109
+ * The same deadline the daemon's own card port uses (15s): a stuck
110
+ * lark-cli (network hang, profile permission prompt) must never make an
111
+ * MCP tool call hang forever — the agent would see "the tool never
112
+ * returns" (DS CV MAJOR-4).
113
+ */
114
+ const HOOK_DEADLINE_MS = 15_000;
115
+
116
+ /** Resolve `promise`, or give up after `ms` (the caller swallows the rejection). */
117
+ function withDeadline(promise, ms) {
118
+ return Promise.race([
119
+ promise,
120
+ new Promise((_resolve, reject) => {
121
+ const timer = setTimeout(
122
+ () => reject(new Error(`hook deadline exceeded (${ms}ms)`)),
123
+ ms,
124
+ );
125
+ // Never keep the process alive for the timer.
126
+ if (typeof timer.unref === "function") timer.unref();
127
+ }),
128
+ ]);
129
+ }
130
+
131
+ /**
132
+ * Build the notifier that fires the task:after.* hooks around tool calls.
133
+ *
134
+ * Returns a no-op dispatcher when the notify env is unset, so the caller
135
+ * can always use one code path.
136
+ *
137
+ * @param {{ backend: object, deps: { cardNotifier?: object } }} input
138
+ */
139
+ export async function createNotifyDispatcher({ backend, deps = {} }) {
140
+ if (!notifyEnabled()) {
141
+ return { fire: async () => {} };
142
+ }
143
+ const [hookMod, pluginsMod, contextMod] = await Promise.all([
144
+ import("@ai-setting/roy-agent-core/env/hook"),
145
+ import("@ai-setting/roy-agent-core/env/task/plugins"),
146
+ import("@ai-setting/roy-agent-core/env/context"),
147
+ ]);
148
+ const notifyHook = await installTaskNotifyHooks({
149
+ hookMod,
150
+ pluginsMod,
151
+ ...(deps.cardNotifier ? { cardNotifier: deps.cardNotifier } : {}),
152
+ });
153
+ const envCtx = buildNotifyEnvContext(contextMod);
154
+
155
+ /**
156
+ * Read the task the tool just touched, so the card shows the REAL
157
+ * fields (priority / type / project_path / goals) instead of values
158
+ * guessed from the call arguments (DS CV MAJOR-3).
159
+ */
160
+ const readTask = async (name, args, result) => {
161
+ const id =
162
+ name === "task_create"
163
+ ? taskIdFromResult(result)
164
+ : typeof args.id === "number"
165
+ ? args.id
166
+ : Number(args.task_id) || taskIdFromResult(result);
167
+ if (!id) return undefined;
168
+ try {
169
+ return (await backend.getTask(id)) ?? undefined;
170
+ } catch {
171
+ return undefined;
172
+ }
173
+ };
174
+
175
+ return {
176
+ async fire(name, args, result) {
177
+ const points = hookPointsForTool(name);
178
+ if (!notifyHook || !envCtx || points.length === 0) return;
179
+ try {
180
+ const task = await readTask(name, args, result);
181
+ await withDeadline(
182
+ contextMod.runWithEnvContextAsync(envCtx, async () => {
183
+ for (const hp of points) {
184
+ // globalHookManager.execute isolates hook errors
185
+ // (safeExecute), so a broken lark-cli can never fail
186
+ // the tool call.
187
+ await hookMod.globalHookManager.execute(
188
+ hp,
189
+ buildHookData(name, args, result, task),
190
+ {},
191
+ );
192
+ }
193
+ }),
194
+ HOOK_DEADLINE_MS,
195
+ );
196
+ } catch {
197
+ // Hook failure never breaks the tool call.
198
+ }
199
+ },
200
+ };
201
+ }
202
+
203
+ /** "Task created successfully: #42 - title" → 42. */
204
+ export function taskIdFromResult(result) {
205
+ const m = typeof result === "string" ? /#(\d+)/.exec(result) : null;
206
+ return m ? Number(m[1]) : 0;
207
+ }
208
+
209
+ function str(v) {
210
+ return v === undefined || v === null ? "" : String(v);
211
+ }
212
+
213
+ function sessionId() {
214
+ return process.env.ROY_TASK_SESSION_ID || "claude-mcp";
215
+ }
216
+
217
+ /**
218
+ * Build the hook context data for a completed tool call.
219
+ *
220
+ * The card hook reads `task` / `changes` / `operation` from
221
+ * `ctx.data`; the tool result is a human string, so the data is
222
+ * rebuilt from the call's own arguments (the same values the
223
+ * backend just persisted).
224
+ */
225
+ export function buildHookData(name, args, result, task) {
226
+ switch (name) {
227
+ case "task_create":
228
+ return {
229
+ // The REAL task, read back from the store (DS CV MAJOR-3: building
230
+ // it from the call arguments faked priority/type/project_path, and
231
+ // the card then showed those wrong values).
232
+ task: task ?? fallbackTask(taskIdFromResult(result), args),
233
+ sessionId: sessionId(),
234
+ tagService: undefined,
235
+ };
236
+ case "task_update":
237
+ case "task_complete": {
238
+ const changes =
239
+ name === "task_complete"
240
+ ? { status: "completed", progress: 100 }
241
+ : {
242
+ ...(args.status !== undefined ? { status: str(args.status) } : {}),
243
+ ...(args.progress !== undefined ? { progress: args.progress } : {}),
244
+ ...(args.current_status !== undefined
245
+ ? { current_status: str(args.current_status) }
246
+ : {}),
247
+ ...(args.title !== undefined ? { title: str(args.title) } : {}),
248
+ ...(args.description !== undefined
249
+ ? { description: str(args.description) }
250
+ : {}),
251
+ };
252
+ return {
253
+ task: task ?? fallbackTask(taskIdFromResult(result), args),
254
+ changes,
255
+ ...(name === "task_complete"
256
+ ? { terminalStatus: "completed", sessionId: sessionId() }
257
+ : {}),
258
+ tagService: undefined,
259
+ };
260
+ }
261
+ case "task_operation_create":
262
+ return {
263
+ operation: {
264
+ id: 0,
265
+ taskId: typeof args.task_id === "number" ? args.task_id : Number(args.task_id) || 0,
266
+ sessionId: sessionId(),
267
+ milestoneType: str(args.milestone_type ?? args.action_type ?? "progress"),
268
+ milestoneTitle: str(args.milestone_title ?? args.action_title ?? ""),
269
+ milestoneDescription: str(args.milestone_description ?? args.action_description ?? ""),
270
+ processDescription: str(args.process_description ?? ""),
271
+ timestamp: new Date().toISOString(),
272
+ },
273
+ };
274
+ default:
275
+ return {};
276
+ }
277
+ }
278
+
279
+ /**
280
+ * The last-resort task shape, used only when the store could not be read.
281
+ *
282
+ * Every field is left empty rather than faked: core's `mergeTaskMeta`
283
+ * skips empty strings, so an empty field keeps the card's original value
284
+ * instead of overwriting it with a lie (DS CV MAJOR-3).
285
+ */
286
+ function fallbackTask(id, args) {
287
+ return {
288
+ id,
289
+ title: str(args.title ?? ""),
290
+ description: "",
291
+ status: "",
292
+ priority: "",
293
+ type: "",
294
+ progress: 0,
295
+ current_status: "",
296
+ goals_and_expected_deliverables: "",
297
+ parent_task_id: args.parent_task_id,
298
+ createdAt: new Date().toISOString(),
299
+ updatedAt: new Date().toISOString(),
300
+ project_path: "",
301
+ context: str(args.context),
302
+ };
303
+ }