@plandesk/api 0.8.0 → 0.9.0

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/dist/events.d.ts CHANGED
@@ -67,7 +67,12 @@ export type SubmissionsPulledEvent = {
67
67
  type: 'submissions_pulled';
68
68
  projectId: string;
69
69
  };
70
- export type PlankDeskEvent = TaskUpdatedEvent | TagUpdatedEvent | CanvasUpdatedEvent | DocumentCreatedEvent | CommentCreatedEvent | CommentUpdatedEvent | FolderCreatedEvent | FolderUpdatedEvent | NoteCreatedEvent | NoteUpdatedEvent | AgentRunStartedEvent | AgentRunProgressEvent | AgentRunCompletedEvent | SubmissionsPulledEvent;
70
+ export type GoalUpdatedEvent = {
71
+ type: 'goal_updated';
72
+ goalId: string;
73
+ projectId: string;
74
+ };
75
+ export type PlankDeskEvent = TaskUpdatedEvent | TagUpdatedEvent | CanvasUpdatedEvent | DocumentCreatedEvent | CommentCreatedEvent | CommentUpdatedEvent | FolderCreatedEvent | FolderUpdatedEvent | NoteCreatedEvent | NoteUpdatedEvent | AgentRunStartedEvent | AgentRunProgressEvent | AgentRunCompletedEvent | SubmissionsPulledEvent | GoalUpdatedEvent;
71
76
  export type EventListener = (event: PlankDeskEvent) => void;
72
77
  export type EventBus = {
73
78
  subscribe(listener: EventListener): () => void;
package/dist/index.d.ts CHANGED
@@ -5,7 +5,10 @@ export { mountStatic } from './static.js';
5
5
  export { createServices, type Services, type ServicesDeps } from './services/index.js';
6
6
  export { createEventBus, type EventBus, type PlankDeskEvent } from './events.js';
7
7
  export type { ProjectService } from './services/projects.js';
8
+ export type { GoalService, VerificationEvidence } from './services/goals.js';
9
+ export { GoalCompletionBlockedError, GoalVerificationRequiredError, InvalidGoalTransitionError, InvalidVerificationSurfaceError, } from './services/goals.js';
8
10
  export type { TaskService } from './services/tasks.js';
11
+ export { InvalidGoalReferenceError } from './services/tasks.js';
9
12
  export type { TagService } from './services/tags.js';
10
13
  export type { CanvasService } from './services/canvas.js';
11
14
  export type { DocumentService } from './services/documents.js';
@@ -22,6 +25,6 @@ export { InvalidCanvasError } from './services/canvas.js';
22
25
  export { InvalidAgentRunError } from './services/agent-runs.js';
23
26
  export { InvalidScaffoldError } from './services/projects.js';
24
27
  export { InvalidShareError } from './services/share.js';
25
- export { InvalidTriageError, SyncUnavailableError, SyncUnauthorizedError, type SyncRemote, type SyncService, } from './services/sync.js';
28
+ export { InvalidTriageError, InvalidTriageInputError, SyncUnavailableError, SyncUnauthorizedError, type SyncRemote, type SyncService, } from './services/sync.js';
26
29
  export declare const version: () => string;
27
30
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -4,6 +4,8 @@ export { healthRouter } from './routes/health.js';
4
4
  export { mountStatic } from './static.js';
5
5
  export { createServices } from './services/index.js';
6
6
  export { createEventBus } from './events.js';
7
+ export { GoalCompletionBlockedError, GoalVerificationRequiredError, InvalidGoalTransitionError, InvalidVerificationSurfaceError, } from './services/goals.js';
8
+ export { InvalidGoalReferenceError } from './services/tasks.js';
7
9
  export { InvalidDocumentError } from './services/documents.js';
8
10
  export { InvalidFolderError } from './services/folders.js';
9
11
  export { InvalidNoteError } from './services/notes.js';
@@ -13,7 +15,7 @@ export { InvalidCanvasError } from './services/canvas.js';
13
15
  export { InvalidAgentRunError } from './services/agent-runs.js';
14
16
  export { InvalidScaffoldError } from './services/projects.js';
15
17
  export { InvalidShareError } from './services/share.js';
16
- export { InvalidTriageError, SyncUnavailableError, SyncUnauthorizedError, } from './services/sync.js';
18
+ export { InvalidTriageError, InvalidTriageInputError, SyncUnavailableError, SyncUnauthorizedError, } from './services/sync.js';
17
19
  import { readFileSync } from 'node:fs';
18
20
  import { dirname, join } from 'node:path';
19
21
  import { fileURLToPath } from 'node:url';
@@ -1,4 +1,4 @@
1
1
  import { Hono } from 'hono';
2
- import type { AgentRunService } from '../services/agent-runs.js';
2
+ import { type AgentRunService } from '../services/agent-runs.js';
3
3
  export declare function createAgentRunsRouter(agentRunService: AgentRunService): Hono;
4
4
  //# sourceMappingURL=agent-runs.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { Hono } from 'hono';
2
+ import { InvalidAgentRunError } from '../services/agent-runs.js';
2
3
  import { parsePaginationParams } from '../serialize.js';
3
4
  export function createAgentRunsRouter(agentRunService) {
4
5
  const router = new Hono();
@@ -13,6 +14,25 @@ export function createAgentRunsRouter(agentRunService) {
13
14
  }
14
15
  return c.json(runs);
15
16
  });
17
+ router.post('/agent-runs/:id/progress', async (c) => {
18
+ const body = await c.req.json();
19
+ if (typeof body.message !== 'string' || body.message.trim() === '') {
20
+ return c.json({ error: 'invalid_argument' }, 400);
21
+ }
22
+ try {
23
+ const event = agentRunService.recordProgress(c.req.param('id'), body.message);
24
+ if (!event) {
25
+ return c.json({ error: 'not_found' }, 404);
26
+ }
27
+ return c.json(event, 201);
28
+ }
29
+ catch (error) {
30
+ if (error instanceof InvalidAgentRunError) {
31
+ return c.json({ error: 'invalid_argument' }, 400);
32
+ }
33
+ throw error;
34
+ }
35
+ });
16
36
  return router;
17
37
  }
18
38
  //# sourceMappingURL=agent-runs.js.map
@@ -0,0 +1,4 @@
1
+ import { Hono } from 'hono';
2
+ import { type GoalService } from '../services/goals.js';
3
+ export declare function createGoalsRouter(goalService: GoalService): Hono;
4
+ //# sourceMappingURL=goals.d.ts.map
@@ -0,0 +1,138 @@
1
+ import { Hono } from 'hono';
2
+ import { InvalidGoalStatusError, isGoalStatus } from '@plandesk/db';
3
+ import { GoalCompletionBlockedError, GoalVerificationRequiredError, InvalidGoalTransitionError, InvalidVerificationSurfaceError, } from '../services/goals.js';
4
+ function mapGoalInput(body) {
5
+ return {
6
+ ...(body.objective !== undefined ? { objective: body.objective } : {}),
7
+ ...(body.verification_surface !== undefined
8
+ ? { verificationSurface: body.verification_surface }
9
+ : {}),
10
+ ...(body.constraints !== undefined ? { constraints: body.constraints } : {}),
11
+ ...(body.boundaries !== undefined ? { boundaries: body.boundaries } : {}),
12
+ ...(body.iteration_policy !== undefined ? { iterationPolicy: body.iteration_policy } : {}),
13
+ ...(body.stop_condition !== undefined ? { stopCondition: body.stop_condition } : {}),
14
+ ...(body.budget !== undefined ? { budget: body.budget } : {}),
15
+ };
16
+ }
17
+ function handleGoalError(c, error) {
18
+ if (error instanceof InvalidGoalStatusError || error instanceof InvalidGoalTransitionError) {
19
+ return c.json({ error: 'invalid_argument' }, 400);
20
+ }
21
+ if (error instanceof InvalidVerificationSurfaceError) {
22
+ return c.json({ error: 'invalid_argument' }, 400);
23
+ }
24
+ if (error instanceof GoalVerificationRequiredError) {
25
+ return c.json({
26
+ error: 'verification_required',
27
+ required_kind: error.requiredKind,
28
+ }, 400);
29
+ }
30
+ if (error instanceof GoalCompletionBlockedError) {
31
+ return c.json({
32
+ error: 'blocked_by_incomplete_tasks',
33
+ incomplete_task_ids: error.incompleteTaskIds,
34
+ }, 400);
35
+ }
36
+ throw error;
37
+ }
38
+ export function createGoalsRouter(goalService) {
39
+ const router = new Hono();
40
+ router.post('/projects/:id/goals', async (c) => {
41
+ const body = await c.req.json();
42
+ if (typeof body.objective !== 'string' || body.objective.trim() === '') {
43
+ return c.json({ error: 'invalid_argument' }, 400);
44
+ }
45
+ if (body.status !== undefined && !isGoalStatus(body.status)) {
46
+ return c.json({ error: 'invalid_argument' }, 400);
47
+ }
48
+ try {
49
+ const goal = goalService.create(c.req.param('id'), {
50
+ objective: body.objective,
51
+ ...(body.status !== undefined ? { status: body.status } : {}),
52
+ ...mapGoalInput(body),
53
+ });
54
+ if (!goal) {
55
+ return c.json({ error: 'not_found' }, 404);
56
+ }
57
+ return c.json(goal, 201);
58
+ }
59
+ catch (error) {
60
+ return handleGoalError(c, error);
61
+ }
62
+ });
63
+ router.get('/projects/:id/goals', (c) => {
64
+ const goals = goalService.listByProject(c.req.param('id'));
65
+ if (!goals) {
66
+ return c.json({ error: 'not_found' }, 404);
67
+ }
68
+ return c.json(goals);
69
+ });
70
+ router.get('/goals/:id', (c) => {
71
+ const goal = goalService.get(c.req.param('id'));
72
+ if (!goal) {
73
+ return c.json({ error: 'not_found' }, 404);
74
+ }
75
+ return c.json(goal);
76
+ });
77
+ router.patch('/goals/:id', async (c) => {
78
+ const body = await c.req.json();
79
+ if (body.objective !== undefined && body.objective.trim() === '') {
80
+ return c.json({ error: 'invalid_argument' }, 400);
81
+ }
82
+ try {
83
+ const goal = goalService.update(c.req.param('id'), mapGoalInput(body));
84
+ if (!goal) {
85
+ return c.json({ error: 'not_found' }, 404);
86
+ }
87
+ return c.json(goal);
88
+ }
89
+ catch (error) {
90
+ return handleGoalError(c, error);
91
+ }
92
+ });
93
+ router.post('/goals/:id/pause', (c) => {
94
+ try {
95
+ const goal = goalService.pause(c.req.param('id'));
96
+ if (!goal) {
97
+ return c.json({ error: 'not_found' }, 404);
98
+ }
99
+ return c.json(goal);
100
+ }
101
+ catch (error) {
102
+ return handleGoalError(c, error);
103
+ }
104
+ });
105
+ router.post('/goals/:id/resume', (c) => {
106
+ try {
107
+ const goal = goalService.resume(c.req.param('id'));
108
+ if (!goal) {
109
+ return c.json({ error: 'not_found' }, 404);
110
+ }
111
+ return c.json(goal);
112
+ }
113
+ catch (error) {
114
+ return handleGoalError(c, error);
115
+ }
116
+ });
117
+ router.post('/goals/:id/complete', async (c) => {
118
+ let body = {};
119
+ try {
120
+ body = await c.req.json();
121
+ }
122
+ catch {
123
+ // empty body is valid when the goal has no verification_surface
124
+ }
125
+ try {
126
+ const goal = goalService.complete(c.req.param('id'), body.evidence);
127
+ if (!goal) {
128
+ return c.json({ error: 'not_found' }, 404);
129
+ }
130
+ return c.json(goal);
131
+ }
132
+ catch (error) {
133
+ return handleGoalError(c, error);
134
+ }
135
+ });
136
+ return router;
137
+ }
138
+ //# sourceMappingURL=goals.js.map
@@ -1,5 +1,5 @@
1
1
  import { Hono } from 'hono';
2
2
  import type { ProjectService } from '../services/projects.js';
3
- import type { TaskService } from '../services/tasks.js';
3
+ import { type TaskService } from '../services/tasks.js';
4
4
  export declare function createProjectsRouter(projectService: ProjectService, taskService: TaskService): Hono;
5
5
  //# sourceMappingURL=projects.d.ts.map
@@ -1,5 +1,6 @@
1
1
  import { Hono } from 'hono';
2
2
  import { InvalidTaskStatusError, isTaskStatus } from '@plandesk/db';
3
+ import { InvalidGoalReferenceError } from '../services/tasks.js';
3
4
  import { InvalidTagError } from '../services/tags.js';
4
5
  import { isStringArray } from './tasks.js';
5
6
  import { parsePaginationParams } from '../serialize.js';
@@ -81,6 +82,7 @@ export function createProjectsRouter(projectService, taskService) {
81
82
  ...(body.y !== undefined ? { y: body.y } : {}),
82
83
  ...(body.assignee !== undefined ? { assignee: body.assignee } : {}),
83
84
  ...(dueDate !== undefined ? { dueDate } : {}),
85
+ ...(body.goal_id !== undefined ? { goalId: body.goal_id } : {}),
84
86
  ...(body.tags !== undefined ? { tags: body.tags } : {}),
85
87
  });
86
88
  if (!task) {
@@ -89,7 +91,9 @@ export function createProjectsRouter(projectService, taskService) {
89
91
  return c.json(task, 201);
90
92
  }
91
93
  catch (error) {
92
- if (error instanceof InvalidTaskStatusError || error instanceof InvalidTagError) {
94
+ if (error instanceof InvalidTaskStatusError ||
95
+ error instanceof InvalidTagError ||
96
+ error instanceof InvalidGoalReferenceError) {
93
97
  return c.json({ error: 'invalid_argument' }, 400);
94
98
  }
95
99
  throw error;
@@ -118,6 +122,13 @@ export function createProjectsRouter(projectService, taskService) {
118
122
  throw error;
119
123
  }
120
124
  });
125
+ router.get('/projects/:id/next-task', (c) => {
126
+ const result = taskService.nextActionable(c.req.param('id'));
127
+ if (!result) {
128
+ return c.json({ error: 'not_found' }, 404);
129
+ }
130
+ return c.json(result);
131
+ });
121
132
  return router;
122
133
  }
123
134
  //# sourceMappingURL=projects.js.map
@@ -0,0 +1,5 @@
1
+ import { Hono } from 'hono';
2
+ import { type SyncService } from '../services/sync.js';
3
+ import type { ProjectService } from '../services/projects.js';
4
+ export declare function createSubmissionsRouter(syncService: SyncService, projectService: ProjectService): Hono;
5
+ //# sourceMappingURL=submissions.d.ts.map
@@ -0,0 +1,58 @@
1
+ import { Hono } from 'hono';
2
+ import { shareSubmissionStatuses } from '@plandesk/db';
3
+ import { InvalidTriageError, SyncUnauthorizedError, SyncUnavailableError, } from '../services/sync.js';
4
+ function isSubmissionStatus(value) {
5
+ return shareSubmissionStatuses.includes(value);
6
+ }
7
+ export function createSubmissionsRouter(syncService, projectService) {
8
+ const router = new Hono();
9
+ router.get('/projects/:id/submissions', (c) => {
10
+ const projectId = c.req.param('id');
11
+ if (!projectService.get(projectId)) {
12
+ return c.json({ error: 'not_found' }, 404);
13
+ }
14
+ const statusParam = c.req.query('status') ?? 'pending';
15
+ if (!isSubmissionStatus(statusParam)) {
16
+ return c.json({ error: 'invalid_argument' }, 400);
17
+ }
18
+ return c.json(syncService.listTriage(projectId, statusParam));
19
+ });
20
+ router.post('/submissions/:id/triage', async (c) => {
21
+ const submissionId = c.req.param('id');
22
+ const body = await c.req.json();
23
+ if (body.action !== 'accept' && body.action !== 'reject') {
24
+ return c.json({ error: 'invalid_argument' }, 400);
25
+ }
26
+ const submission = syncService.getSubmission(submissionId);
27
+ if (submission === undefined) {
28
+ return c.json({ error: 'not_found' }, 404);
29
+ }
30
+ const remote = syncService.getRemote(submission.project_id);
31
+ if (remote === undefined) {
32
+ return c.json({ error: 'not_published' }, 400);
33
+ }
34
+ try {
35
+ // Triage never creates a `todo` task — the scope->todo release is the human's own
36
+ // board action, enforced in syncService.triage(). A merge (link_task_id) links to
37
+ // an existing task instead of creating one; the two are mutually exclusive, so
38
+ // only one is forwarded.
39
+ const linkTaskId = body.action === 'accept' ? body.link_task_id : undefined;
40
+ const result = await syncService.triage(submissionId, body.action, remote, body.action === 'accept' && linkTaskId === undefined ? body.as_task : undefined, linkTaskId);
41
+ return c.json(result);
42
+ }
43
+ catch (error) {
44
+ if (error instanceof InvalidTriageError) {
45
+ return c.json({ error: 'not_found' }, 404);
46
+ }
47
+ if (error instanceof SyncUnauthorizedError) {
48
+ return c.json({ error: 'sync_unauthorized' }, 400);
49
+ }
50
+ if (error instanceof SyncUnavailableError) {
51
+ return c.json({ error: 'sync_unavailable' }, 502);
52
+ }
53
+ throw error;
54
+ }
55
+ });
56
+ return router;
57
+ }
58
+ //# sourceMappingURL=submissions.js.map
@@ -1,4 +1,4 @@
1
- import type { AgentRun, AgentRunEvent, Document, DocumentComment, Edge, Folder, Note, Project, Tag, Task, TaskStatus } from '@plandesk/db';
1
+ import type { AgentRun, AgentRunEvent, Document, DocumentComment, Edge, Folder, Goal, Note, Project, Tag, Task, TaskStatus } from '@plandesk/db';
2
2
  export type PaginationParams = {
3
3
  limit?: number;
4
4
  offset?: number;
@@ -29,10 +29,31 @@ export type SerializedTag = {
29
29
  created_at: string;
30
30
  };
31
31
  export declare function serializeTag(tag: Tag): SerializedTag;
32
+ export declare function serializeGoal(goal: Goal): {
33
+ id: string;
34
+ project_id: string;
35
+ objective: string;
36
+ status: "active" | "paused" | "complete" | "blocked";
37
+ verification_surface: string | null;
38
+ constraints: string | null;
39
+ boundaries: string | null;
40
+ iteration_policy: string | null;
41
+ stop_condition: string | null;
42
+ budget: string | null;
43
+ last_verification: {
44
+ at: string;
45
+ green: boolean;
46
+ kind: string | null;
47
+ detail?: string;
48
+ } | null;
49
+ created_at: string;
50
+ updated_at: string;
51
+ };
32
52
  export declare function serializeTask(task: Task, tags?: Tag[]): {
33
53
  tags?: SerializedTag[] | undefined;
34
54
  id: string;
35
55
  project_id: string;
56
+ goal_id: string;
36
57
  label: string;
37
58
  status: "scope" | "todo" | "in_progress" | "done" | "backlog";
38
59
  description: string | null;
package/dist/serialize.js CHANGED
@@ -45,10 +45,45 @@ export function serializeTag(tag) {
45
45
  created_at: tag.createdAt.toISOString(),
46
46
  };
47
47
  }
48
+ function parseLastVerification(raw) {
49
+ if (raw === null) {
50
+ return null;
51
+ }
52
+ try {
53
+ const parsed = JSON.parse(raw);
54
+ if (typeof parsed.at === 'string' &&
55
+ typeof parsed.green === 'boolean' &&
56
+ (parsed.kind === null || typeof parsed.kind === 'string')) {
57
+ return parsed;
58
+ }
59
+ return null;
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ export function serializeGoal(goal) {
66
+ return {
67
+ id: goal.id,
68
+ project_id: goal.projectId,
69
+ objective: goal.objective,
70
+ status: goal.status,
71
+ verification_surface: goal.verificationSurface,
72
+ constraints: goal.constraints,
73
+ boundaries: goal.boundaries,
74
+ iteration_policy: goal.iterationPolicy,
75
+ stop_condition: goal.stopCondition,
76
+ budget: goal.budget,
77
+ last_verification: parseLastVerification(goal.lastVerification),
78
+ created_at: goal.createdAt.toISOString(),
79
+ updated_at: goal.updatedAt.toISOString(),
80
+ };
81
+ }
48
82
  export function serializeTask(task, tags) {
49
83
  return {
50
84
  id: task.id,
51
85
  project_id: task.projectId,
86
+ goal_id: task.goalId,
52
87
  label: task.label,
53
88
  status: task.status,
54
89
  description: task.description,
package/dist/server.js CHANGED
@@ -12,17 +12,20 @@ import { createNotesRouter } from './routes/notes.js';
12
12
  import { createEventsRouter } from './routes/events.js';
13
13
  import { createTokensRouter } from './routes/tokens.js';
14
14
  import { createAgentRunsRouter } from './routes/agent-runs.js';
15
+ import { createGoalsRouter } from './routes/goals.js';
16
+ import { createSubmissionsRouter } from './routes/submissions.js';
15
17
  import { mountStatic } from './static.js';
16
18
  import { createServices } from './services/index.js';
17
19
  export function createApp(deps) {
18
20
  const services = deps.services ?? createServices({ db: deps.db, eventBus: deps.eventBus });
19
- const { eventBus, projectService, taskService, tagService, canvasService, documentService, folderService, noteService, commentService, agentRunService, tokenService, } = services;
21
+ const { eventBus, projectService, goalService, taskService, tagService, canvasService, documentService, folderService, noteService, commentService, agentRunService, tokenService, syncService, } = services;
20
22
  const app = new Hono();
21
23
  if (deps.authPassword !== undefined && deps.authPassword.length > 0) {
22
24
  app.use('*', createAuthMiddleware(deps.authPassword));
23
25
  }
24
26
  app.route('/api/v1', healthRouter);
25
27
  app.route('/api/v1', createProjectsRouter(projectService, taskService));
28
+ app.route('/api/v1', createGoalsRouter(goalService));
26
29
  app.route('/api/v1', createTasksRouter(taskService));
27
30
  app.route('/api/v1', createTagsRouter(tagService));
28
31
  app.route('/api/v1', createCanvasRouter(canvasService));
@@ -32,6 +35,7 @@ export function createApp(deps) {
32
35
  app.route('/api/v1', createCommentsRouter(commentService));
33
36
  app.route('/api/v1', createTokensRouter(tokenService));
34
37
  app.route('/api/v1', createAgentRunsRouter(agentRunService));
38
+ app.route('/api/v1', createSubmissionsRouter(syncService, projectService));
35
39
  app.route('/api/v1', createEventsRouter(eventBus));
36
40
  mountStatic(app);
37
41
  if (deps.mcp) {
@@ -33,6 +33,7 @@ export declare function createCanvasService(deps: CanvasServiceDeps): {
33
33
  tags?: import("../serialize.js").SerializedTag[] | undefined;
34
34
  id: string;
35
35
  project_id: string;
36
+ goal_id: string;
36
37
  label: string;
37
38
  status: "scope" | "todo" | "in_progress" | "done" | "backlog";
38
39
  description: string | null;
@@ -75,6 +76,7 @@ export declare function createCanvasService(deps: CanvasServiceDeps): {
75
76
  tags?: import("../serialize.js").SerializedTag[] | undefined;
76
77
  id: string;
77
78
  project_id: string;
79
+ goal_id: string;
78
80
  label: string;
79
81
  status: "scope" | "todo" | "in_progress" | "done" | "backlog";
80
82
  description: string | null;
@@ -1,4 +1,4 @@
1
- import { createEdge, createTask, deleteEdge as dbDeleteEdge, getEdgeByProjectAndId, getProject, getTask, listEdges, listTasks, updateEdge, updateProject, updateTask, } from '@plandesk/db';
1
+ import { createEdge, createTask, getOrCreateDefaultGoal, deleteEdge as dbDeleteEdge, getEdgeByProjectAndId, getProject, getTask, listEdges, listTasks, updateEdge, updateProject, updateTask, } from '@plandesk/db';
2
2
  import { serializeEdge, serializeTask } from '../serialize.js';
3
3
  export class InvalidCanvasError extends Error {
4
4
  constructor(message) {
@@ -93,6 +93,7 @@ export function createCanvasService(deps) {
93
93
  }
94
94
  const created = createTask(tx, {
95
95
  projectId,
96
+ goalId: getOrCreateDefaultGoal(tx, projectId).id,
96
97
  id: node.id,
97
98
  label: node.label,
98
99
  status: 'todo',