trekoon 0.4.1 → 0.4.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 (58) hide show
  1. package/.agents/skills/trekoon/SKILL.md +97 -765
  2. package/.agents/skills/trekoon/reference/execution-with-team.md +91 -141
  3. package/.agents/skills/trekoon/reference/execution.md +188 -159
  4. package/.agents/skills/trekoon/reference/harness-primitives.md +77 -0
  5. package/.agents/skills/trekoon/reference/planning.md +213 -213
  6. package/.agents/skills/trekoon/reference/status-machine.md +21 -0
  7. package/.agents/skills/trekoon/reference/sync.md +82 -0
  8. package/README.md +29 -8
  9. package/docs/ai-agents.md +65 -6
  10. package/docs/commands.md +149 -5
  11. package/docs/machine-contracts.md +123 -0
  12. package/docs/quickstart.md +55 -3
  13. package/package.json +1 -1
  14. package/src/board/assets/app.js +47 -13
  15. package/src/board/assets/components/Component.js +20 -8
  16. package/src/board/assets/components/Workspace.js +9 -3
  17. package/src/board/assets/components/helpers.js +4 -0
  18. package/src/board/assets/runtime/delegation.js +8 -0
  19. package/src/board/assets/runtime/focus-trap.js +48 -0
  20. package/src/board/assets/state/actions.js +45 -4
  21. package/src/board/assets/state/api.js +304 -17
  22. package/src/board/assets/state/store.js +82 -11
  23. package/src/board/assets/state/url.js +10 -0
  24. package/src/board/assets/state/utils.js +2 -1
  25. package/src/board/event-bus.ts +81 -0
  26. package/src/board/routes.ts +430 -40
  27. package/src/board/server.ts +86 -10
  28. package/src/board/snapshot.ts +6 -0
  29. package/src/board/wal-watcher.ts +313 -0
  30. package/src/commands/board.ts +52 -17
  31. package/src/commands/epic.ts +7 -9
  32. package/src/commands/error-utils.ts +54 -1
  33. package/src/commands/help.ts +75 -10
  34. package/src/commands/migrate.ts +153 -24
  35. package/src/commands/quickstart.ts +7 -0
  36. package/src/commands/skills.ts +17 -5
  37. package/src/commands/subtask.ts +71 -10
  38. package/src/commands/suggest.ts +6 -13
  39. package/src/commands/task.ts +137 -88
  40. package/src/domain/batch-validation.ts +329 -0
  41. package/src/domain/cascade-planner.ts +412 -0
  42. package/src/domain/dependency-rules.ts +15 -0
  43. package/src/domain/mutation-service.ts +842 -187
  44. package/src/domain/search.ts +113 -0
  45. package/src/domain/tracker-domain.ts +167 -693
  46. package/src/domain/types.ts +56 -2
  47. package/src/export/render-markdown.ts +1 -2
  48. package/src/index.ts +37 -0
  49. package/src/runtime/cli-shell.ts +44 -0
  50. package/src/runtime/daemon.ts +700 -0
  51. package/src/storage/backup.ts +166 -0
  52. package/src/storage/database.ts +268 -4
  53. package/src/storage/migrations.ts +441 -22
  54. package/src/storage/path.ts +8 -0
  55. package/src/storage/schema.ts +5 -1
  56. package/src/sync/event-writes.ts +38 -11
  57. package/src/sync/git-context.ts +226 -8
  58. package/src/sync/service.ts +679 -156
@@ -39,6 +39,7 @@ const SEARCH_OPTIONS = ["fields", "preview"] as const;
39
39
  const REPLACE_OPTIONS = ["search", "replace", "fields", "preview", "apply"] as const;
40
40
  const CREATE_MANY_OPTIONS = ["task", "t", "subtask"] as const;
41
41
  const UPDATE_OPTIONS = ["all", "ids", "append", "description", "d", "status", "s", "title", "owner"] as const;
42
+ const CLAIM_OPTIONS = ["owner"] as const;
42
43
  const STATUS_CASCADE_UPDATE_STATUSES = ["done", "todo"] as const;
43
44
 
44
45
  function parseIdsOption(rawIds: string | undefined): string[] {
@@ -892,10 +893,9 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
892
893
 
893
894
  const targets = updateAll ? [...domain.listSubtasks()] : ids.map((id) => domain.getSubtaskOrThrow(id));
894
895
  const subtasks = targets.map((target) =>
895
- mutations.updateSubtask(target.id, {
896
- status,
897
- description: append === undefined ? undefined : appendLine(target.description, append),
898
- }),
896
+ append !== undefined
897
+ ? mutations.appendToSubtaskDescription({ subtaskId: target.id, append, status })
898
+ : mutations.updateSubtask(target.id, { status }),
899
899
  );
900
900
 
901
901
  return okResult({
@@ -921,11 +921,10 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
921
921
  });
922
922
  }
923
923
 
924
- const nextDescription =
925
- append === undefined
926
- ? description
927
- : appendLine(domain.getSubtaskOrThrow(subtaskId).description, append);
928
- const subtask = mutations.updateSubtask(subtaskId, { title, description: nextDescription, status, owner });
924
+ const subtask =
925
+ append !== undefined
926
+ ? mutations.appendToSubtaskDescription({ subtaskId, append, status, owner })
927
+ : mutations.updateSubtask(subtaskId, { title, description, status, owner });
929
928
 
930
929
  return okResult({
931
930
  command: "subtask.update",
@@ -933,6 +932,68 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
933
932
  data: { subtask },
934
933
  });
935
934
  }
935
+ case "claim": {
936
+ const claimUnknownOption = findUnknownOption(parsed, CLAIM_OPTIONS);
937
+ if (claimUnknownOption !== undefined) {
938
+ return unknownOption("subtask.claim", claimUnknownOption, CLAIM_OPTIONS);
939
+ }
940
+
941
+ const missingClaimOption = readMissingOptionValue(parsed.missingOptionValues, "owner");
942
+ if (missingClaimOption !== undefined) {
943
+ return failMissingOptionValue("subtask.claim", missingClaimOption);
944
+ }
945
+
946
+ const subtaskId: string = parsed.positional[1] ?? "";
947
+ if (subtaskId.length === 0) {
948
+ return failResult({
949
+ command: "subtask.claim",
950
+ human: "Provide a subtask id. Usage: trekoon subtask claim <id> --owner <owner>",
951
+ data: { code: "invalid_input" },
952
+ error: {
953
+ code: "invalid_input",
954
+ message: "Missing subtask id",
955
+ },
956
+ });
957
+ }
958
+
959
+ const owner: string | undefined = readOption(parsed.options, "owner");
960
+ if (owner === undefined || owner.trim().length === 0) {
961
+ return failResult({
962
+ command: "subtask.claim",
963
+ human: "--owner is required. Usage: trekoon subtask claim <id> --owner <owner>",
964
+ data: { code: "invalid_input", option: "owner" },
965
+ error: {
966
+ code: "invalid_input",
967
+ message: "Missing required option --owner",
968
+ },
969
+ });
970
+ }
971
+
972
+ const claimResult = mutations.claimSubtask({ subtaskId, owner });
973
+
974
+ if (claimResult.claimed) {
975
+ return okResult({
976
+ command: "subtask.claim",
977
+ human: `Claimed subtask ${subtaskId} for ${owner}`,
978
+ data: {
979
+ claimed: true,
980
+ currentOwner: claimResult.currentOwner,
981
+ currentStatus: claimResult.currentStatus,
982
+ subtask: claimResult.subtask,
983
+ },
984
+ });
985
+ }
986
+
987
+ return okResult({
988
+ command: "subtask.claim",
989
+ human: `Subtask ${subtaskId} not claimed: status=${claimResult.currentStatus}, owner=${claimResult.currentOwner ?? "none"}`,
990
+ data: {
991
+ claimed: false,
992
+ currentOwner: claimResult.currentOwner,
993
+ currentStatus: claimResult.currentStatus,
994
+ },
995
+ });
996
+ }
936
997
  case "delete": {
937
998
  const subtaskId: string = parsed.positional[1] ?? "";
938
999
  const result = mutations.deleteSubtask(subtaskId);
@@ -946,7 +1007,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
946
1007
  default:
947
1008
  return failResult({
948
1009
  command: "subtask",
949
- human: "Usage: trekoon subtask <create|create-many|list|search|replace|update|delete>",
1010
+ human: "Usage: trekoon subtask <create|create-many|list|search|replace|update|delete|claim>",
950
1011
  data: {
951
1012
  args: context.args,
952
1013
  },
@@ -44,14 +44,7 @@ function resolveActiveEpic(domain: TrackerDomain, epicId: string | undefined): E
44
44
  return domain.getEpic(epicId);
45
45
  }
46
46
 
47
- const epics = domain.listEpics();
48
- const inProgress = epics.find((epic) => epic.status === "in_progress");
49
- if (inProgress) {
50
- return inProgress;
51
- }
52
-
53
- const todo = epics.find((epic) => epic.status === "todo");
54
- return todo ?? epics[0] ?? null;
47
+ return domain.findActiveEpic();
55
48
  }
56
49
 
57
50
  function findInProgressTasks(readiness: TaskReadinessResult): { count: number; first: { id: string; title: string } | null } {
@@ -76,7 +69,7 @@ function buildSuggestions(
76
69
  recoveryRequired: boolean,
77
70
  syncSummary: SyncStatusSummary,
78
71
  readiness: TaskReadinessResult,
79
- epics: readonly EpicRecord[],
72
+ epicCount: number,
80
73
  activeEpic: EpicRecord | null,
81
74
  ): readonly Suggestion[] {
82
75
  const suggestions: Suggestion[] = [];
@@ -186,7 +179,7 @@ function buildSuggestions(
186
179
  }
187
180
 
188
181
  // Priority 8: No epics exist
189
- if (suggestions.length < MAX_SUGGESTIONS && epics.length === 0) {
182
+ if (suggestions.length < MAX_SUGGESTIONS && epicCount === 0) {
190
183
  suggestions.push({
191
184
  priority: suggestions.length + 1,
192
185
  action: "quickstart",
@@ -241,7 +234,7 @@ export async function runSuggest(context: CliContext): Promise<CliResult> {
241
234
 
242
235
  const syncSummary = resolveSyncStatus(database, context.cwd, DEFAULT_SOURCE_BRANCH);
243
236
  const domain = new TrackerDomain(database.db);
244
- const epics = domain.listEpics();
237
+ const epicCount = domain.countEpics();
245
238
  const activeEpic = resolveActiveEpic(domain, epicId);
246
239
 
247
240
  const readiness = buildTaskReadiness(domain, epicId ?? activeEpic?.id);
@@ -250,14 +243,14 @@ export async function runSuggest(context: CliContext): Promise<CliResult> {
250
243
  diagnostics.recoveryRequired,
251
244
  syncSummary,
252
245
  readiness,
253
- epics,
246
+ epicCount,
254
247
  activeEpic,
255
248
  );
256
249
 
257
250
  const result: SuggestResult = {
258
251
  suggestions,
259
252
  context: {
260
- totalEpics: epics.length,
253
+ totalEpics: epicCount,
261
254
  activeEpic: activeEpic?.id ?? null,
262
255
  readyTasks: readiness.summary.readyCount,
263
256
  blockedTasks: readiness.summary.blockedCount,
@@ -47,6 +47,7 @@ const SEARCH_OPTIONS = ["fields", "preview"] as const;
47
47
  const REPLACE_OPTIONS = ["search", "replace", "fields", "preview", "apply"] as const;
48
48
  const CREATE_MANY_OPTIONS = ["epic", "e", "task"] as const;
49
49
  const UPDATE_OPTIONS = ["all", "ids", "append", "description", "d", "status", "s", "title", "t", "owner"] as const;
50
+ const CLAIM_OPTIONS = ["owner"] as const;
50
51
  const STATUS_CASCADE_UPDATE_STATUSES = ["done", "todo"] as const;
51
52
 
52
53
  function parseIdsOption(rawIds: string | undefined): string[] {
@@ -1179,10 +1180,9 @@ export async function runTask(context: CliContext): Promise<CliResult> {
1179
1180
 
1180
1181
  const targets = updateAll ? [...domain.listTasks()] : ids.map((id) => domain.getTaskOrThrow(id));
1181
1182
  const tasks = targets.map((target) =>
1182
- mutations.updateTask(target.id, {
1183
- status,
1184
- description: append === undefined ? undefined : appendLine(target.description, append),
1185
- }),
1183
+ append !== undefined
1184
+ ? mutations.appendToTaskDescription({ taskId: target.id, append, status })
1185
+ : mutations.updateTask(target.id, { status }),
1186
1186
  );
1187
1187
 
1188
1188
  return okResult({
@@ -1208,11 +1208,10 @@ export async function runTask(context: CliContext): Promise<CliResult> {
1208
1208
  });
1209
1209
  }
1210
1210
 
1211
- const nextDescription =
1212
- append === undefined
1213
- ? description
1214
- : appendLine(domain.getTaskOrThrow(taskId).description, append);
1215
- const task = mutations.updateTask(taskId, { title, description: nextDescription, status, owner });
1211
+ const task =
1212
+ append !== undefined
1213
+ ? mutations.appendToTaskDescription({ taskId, append, status, owner })
1214
+ : mutations.updateTask(taskId, { title, description, status, owner });
1216
1215
 
1217
1216
  return okResult({
1218
1217
  command: "task.update",
@@ -1247,98 +1246,148 @@ export async function runTask(context: CliContext): Promise<CliResult> {
1247
1246
  });
1248
1247
  }
1249
1248
 
1250
- if (existingTask.status === "done") {
1251
- return failResult({
1252
- command: "task.done",
1253
- human: "Task is already done",
1254
- data: { code: "already_done", id: taskId },
1255
- error: {
1256
- code: "already_done",
1257
- message: "Task is already done",
1258
- },
1259
- });
1260
- }
1261
-
1262
- // Check for open subtasks (lenient: warn but allow completion)
1263
- const openSubtasks = domain.getOpenSubtasks(taskId);
1264
- const openSubtaskCount = openSubtasks.length;
1265
- const openSubtaskIds = openSubtasks.map((s) => s.id);
1266
-
1267
- // Snapshot blocked reverse deps before marking done (lightweight: no full readiness rebuild).
1268
- // Only direct task-level reverse deps are tracked here; subtask reverse deps are excluded
1269
- // because subtasks are children within a task, not independent workflow items.
1270
- const reverseDeps = domain.listReverseDependencies(taskId);
1271
- const directRevDepTaskIds = reverseDeps
1272
- .filter((rd) => rd.isDirect && rd.kind === "task")
1273
- .map((rd) => rd.id);
1274
- const preDepStatuses = domain.batchResolveDependencyStatuses(directRevDepTaskIds);
1275
- const preBlockedIds = new Set(
1276
- directRevDepTaskIds.filter((id) => {
1277
- const resolved = preDepStatuses.get(id);
1278
- return resolved !== undefined && resolved.blockers.length > 0;
1279
- }),
1280
- );
1281
-
1282
- // Auto-transition through in_progress when current status is todo or blocked.
1283
- // Note: this emits two sync events (→in_progress, →done) because each
1284
- // updateTask call appends its own event. This is intentional — the status
1285
- // machine requires the intermediate step, and event consumers should treat
1286
- // a rapid in_progress→done pair from `task done` as a single logical completion.
1287
- if (existingTask.status === "todo" || existingTask.status === "blocked") {
1288
- mutations.updateTask(taskId, { status: "in_progress" });
1289
- }
1290
-
1291
- const completed = mutations.updateTask(taskId, { status: "done" });
1292
- const readiness = buildTaskReadiness(domain, completed.epicId);
1293
- const nextCandidate = readiness.candidates[0] ?? null;
1294
-
1295
- // Diff: tasks that were blocked before but are now ready
1296
- const unblockedTasks = readiness.candidates
1297
- .filter((item) => preBlockedIds.has(item.task.id))
1298
- .map((item) => ({
1299
- id: item.task.id,
1300
- kind: "task" as const,
1301
- title: item.task.title,
1302
- status: item.task.status,
1303
- wasBlockedBy: [taskId],
1304
- }));
1305
-
1306
- const nextTree = nextCandidate !== null ? domain.buildTaskTreeDetailed(nextCandidate.task.id) : null;
1307
- const nextDeps = nextCandidate?.blockerSummary.blockedBy ?? [];
1308
-
1309
- const readinessStats = {
1310
- readyCount: readiness.summary.readyCount,
1311
- blockedCount: readiness.summary.blockedCount,
1312
- };
1249
+ // Note: the redundant `if (existingTask.status === "done")` pre-check
1250
+ // was removed in the cr-expert hardening pass — `markTaskDoneAtomically`
1251
+ // raises `already_done` itself inside the same transaction that would
1252
+ // perform the flip, eliminating a race window between this read and
1253
+ // the atomic write. The thrown DomainError propagates to the outer
1254
+ // catch and is rendered with the same `code: "already_done"` payload
1255
+ // by `unexpectedFailureResult`.
1256
+
1257
+ // Single-transaction atomic completion: the entire flow (status flip,
1258
+ // event emission, unblocked-diff snapshot) runs inside ONE
1259
+ // `BEGIN IMMEDIATE`/`COMMIT` pair. Bypasses the public transition
1260
+ // checker — this is the documented allowed exception (see
1261
+ // MutationService.markTaskDoneAtomically and docs/machine-contracts.md).
1262
+ // A crash mid-flight rolls the row back to its original status; the
1263
+ // task is never observable in a phantom `in_progress` state.
1264
+ const snapshot = mutations.markTaskDoneAtomically({
1265
+ taskId,
1266
+ computeSnapshot: ({ domain: txDomain, completed, preBlockedReverseDepIds }) => {
1267
+ const openSubtasks = txDomain.getOpenSubtasks(taskId);
1268
+ const readiness = buildTaskReadiness(txDomain, completed.epicId);
1269
+ const nextCandidate = readiness.candidates[0] ?? null;
1270
+
1271
+ const preBlockedIds = new Set<string>(preBlockedReverseDepIds);
1272
+ const unblockedTasks = readiness.candidates
1273
+ .filter((item) => preBlockedIds.has(item.task.id))
1274
+ .map((item) => ({
1275
+ id: item.task.id,
1276
+ kind: "task" as const,
1277
+ title: item.task.title,
1278
+ status: item.task.status,
1279
+ wasBlockedBy: [taskId],
1280
+ }));
1281
+
1282
+ const nextTree = nextCandidate !== null ? txDomain.buildTaskTreeDetailed(nextCandidate.task.id) : null;
1283
+ const nextDeps = nextCandidate?.blockerSummary.blockedBy ?? [];
1284
+
1285
+ return {
1286
+ completed,
1287
+ openSubtaskCount: openSubtasks.length,
1288
+ openSubtaskIds: openSubtasks.map((s) => s.id),
1289
+ unblocked: unblockedTasks,
1290
+ next: nextTree,
1291
+ nextCandidate,
1292
+ nextDeps,
1293
+ readiness: {
1294
+ readyCount: readiness.summary.readyCount,
1295
+ blockedCount: readiness.summary.blockedCount,
1296
+ },
1297
+ };
1298
+ },
1299
+ });
1313
1300
 
1314
- const subtaskWarning = openSubtaskCount > 0
1315
- ? `Warning: ${openSubtaskCount} subtask(s) still open.`
1301
+ const subtaskWarning = snapshot.openSubtaskCount > 0
1302
+ ? `Warning: ${snapshot.openSubtaskCount} subtask(s) still open.`
1316
1303
  : null;
1317
1304
 
1318
- let human = `Task ${completed.title} marked done.`;
1305
+ let human = `Task ${snapshot.completed.title} marked done.`;
1319
1306
  if (subtaskWarning !== null) {
1320
1307
  human += `\n${subtaskWarning}`;
1321
1308
  }
1322
- if (unblockedTasks.length > 0) {
1323
- human += `\nUnblocked: ${unblockedTasks.map((t) => t.title).join(", ")}`;
1309
+ if (snapshot.unblocked.length > 0) {
1310
+ human += `\nUnblocked: ${snapshot.unblocked.map((t) => t.title).join(", ")}`;
1324
1311
  }
1325
- if (nextTree !== null && nextCandidate !== null) {
1326
- human += `\nNext: ${formatTask(nextCandidate.task)}`;
1312
+ if (snapshot.next !== null && snapshot.nextCandidate !== null) {
1313
+ human += `\nNext: ${formatTask(snapshot.nextCandidate.task)}`;
1327
1314
  }
1328
- human += `\nReadiness: ready=${readinessStats.readyCount}, blocked=${readinessStats.blockedCount}.`;
1315
+ human += `\nReadiness: ready=${snapshot.readiness.readyCount}, blocked=${snapshot.readiness.blockedCount}.`;
1329
1316
 
1330
1317
  return okResult({
1331
1318
  command: "task.done",
1332
1319
  human,
1333
1320
  data: {
1334
- completed,
1335
- openSubtaskCount,
1336
- openSubtaskIds,
1321
+ completed: snapshot.completed,
1322
+ openSubtaskCount: snapshot.openSubtaskCount,
1323
+ openSubtaskIds: snapshot.openSubtaskIds,
1337
1324
  warning: subtaskWarning,
1338
- unblocked: unblockedTasks,
1339
- next: nextTree,
1340
- nextDeps,
1341
- readiness: readinessStats,
1325
+ unblocked: snapshot.unblocked,
1326
+ next: snapshot.next,
1327
+ nextDeps: snapshot.nextDeps,
1328
+ readiness: snapshot.readiness,
1329
+ },
1330
+ });
1331
+ }
1332
+ case "claim": {
1333
+ const claimUnknownOption = findUnknownOption(parsed, CLAIM_OPTIONS);
1334
+ if (claimUnknownOption !== undefined) {
1335
+ return unknownOption("task.claim", claimUnknownOption, CLAIM_OPTIONS);
1336
+ }
1337
+
1338
+ const missingClaimOption = readMissingOptionValue(parsed.missingOptionValues, "owner");
1339
+ if (missingClaimOption !== undefined) {
1340
+ return failMissingOptionValue("task.claim", missingClaimOption);
1341
+ }
1342
+
1343
+ const taskId: string = parsed.positional[1] ?? "";
1344
+ if (taskId.length === 0) {
1345
+ return failResult({
1346
+ command: "task.claim",
1347
+ human: "Provide a task id. Usage: trekoon task claim <id> --owner <owner>",
1348
+ data: { code: "invalid_input" },
1349
+ error: {
1350
+ code: "invalid_input",
1351
+ message: "Missing task id",
1352
+ },
1353
+ });
1354
+ }
1355
+
1356
+ const owner: string | undefined = readOption(parsed.options, "owner");
1357
+ if (owner === undefined || owner.trim().length === 0) {
1358
+ return failResult({
1359
+ command: "task.claim",
1360
+ human: "--owner is required. Usage: trekoon task claim <id> --owner <owner>",
1361
+ data: { code: "invalid_input", option: "owner" },
1362
+ error: {
1363
+ code: "invalid_input",
1364
+ message: "Missing required option --owner",
1365
+ },
1366
+ });
1367
+ }
1368
+
1369
+ const claimResult = mutations.claimTask({ taskId, owner });
1370
+
1371
+ if (claimResult.claimed) {
1372
+ return okResult({
1373
+ command: "task.claim",
1374
+ human: `Claimed task ${taskId} for ${owner}`,
1375
+ data: {
1376
+ claimed: true,
1377
+ currentOwner: claimResult.currentOwner,
1378
+ currentStatus: claimResult.currentStatus,
1379
+ task: claimResult.task,
1380
+ },
1381
+ });
1382
+ }
1383
+
1384
+ return okResult({
1385
+ command: "task.claim",
1386
+ human: `Task ${taskId} not claimed: status=${claimResult.currentStatus}, owner=${claimResult.currentOwner ?? "none"}`,
1387
+ data: {
1388
+ claimed: false,
1389
+ currentOwner: claimResult.currentOwner,
1390
+ currentStatus: claimResult.currentStatus,
1342
1391
  },
1343
1392
  });
1344
1393
  }
@@ -1355,7 +1404,7 @@ export async function runTask(context: CliContext): Promise<CliResult> {
1355
1404
  default:
1356
1405
  return failResult({
1357
1406
  command: "task",
1358
- human: "Usage: trekoon task <create|create-many|list|show|ready|next|done|search|replace|update|delete>",
1407
+ human: "Usage: trekoon task <create|create-many|list|show|ready|next|done|search|replace|update|delete|claim>",
1359
1408
  data: {
1360
1409
  args: context.args,
1361
1410
  },