@idea-base/mcp-server 1.3.0 → 2.1.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +278 -37
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idea-base/mcp-server",
3
- "version": "1.3.0",
3
+ "version": "2.1.0",
4
4
  "description": "MCP Server for IDEA Base - AI-powered project management. Integrates with Claude Code, Cursor, and Windsurf.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -53,6 +53,19 @@ function resolveBaseUrl() {
53
53
  const API_BASE_URL = resolveBaseUrl();
54
54
  const API_KEY = (process.env.IDEA_BASE_API_KEY || '').trim();
55
55
 
56
+ // The agent identity this server declares on every request (X-On-Behalf-Of).
57
+ //
58
+ // It answers "who did this?" and nothing else — it carries no permission, no
59
+ // delegation and no elevation. Authorization stays entirely with the API key and its
60
+ // owner. Without it, work done by an agent is recorded as the key owner, which makes
61
+ // the owner the actor of his own notifications and silently suppresses them.
62
+ //
63
+ // Freeform by design, so a session may declare a different agent. The identity must
64
+ // already exist server-side as a non-human user, or EVERY call fails closed with 403
65
+ // AGENT_IDENTITY_NOT_RESOLVABLE / AGENT_IDENTITY_NOT_PERMITTED — deliberately loud,
66
+ // never a silent fallback to the key owner.
67
+ const AGENT_ID = (process.env.IDEA_BASE_AGENT_ID || 'claude_ai').trim();
68
+
56
69
  if (!API_KEY) {
57
70
  console.error('Error: IDEA_BASE_API_KEY environment variable is required');
58
71
  process.exit(1);
@@ -190,6 +203,7 @@ async function apiRequest(endpoint, options = {}) {
190
203
  signal: controller.signal,
191
204
  headers: {
192
205
  'Authorization': `Bearer ${API_KEY}`,
206
+ 'X-On-Behalf-Of': AGENT_ID,
193
207
  'Content-Type': 'application/json',
194
208
  'Accept': 'application/json',
195
209
  ...options.headers,
@@ -419,12 +433,34 @@ const tools = [
419
433
  },
420
434
  assignee_user_id: {
421
435
  type: 'string',
422
- description: 'User ID to assign the task to. Must be a member of the same account. Omit to leave the task unassigned.',
436
+ description: 'User ID to assign the task to. Must be a member of the same account. Omit to leave the task unassigned. For a SECOND (or third, ...) assignee, do not call this again — use add_assignee after creation, since this field only ever sets a single owner at create time.',
423
437
  },
424
438
  parent_task_id: {
425
439
  type: 'number',
426
440
  description: 'Make this a SUBTASK of the given task. The parent must be in the same project, and a subtask cannot itself have subtasks (one level only). A parent with subtasks takes its status from them: any subtask in progress makes the parent in progress, and all subtasks done makes it ready to complete.',
427
441
  },
442
+ verification_mode: {
443
+ type: 'string',
444
+ enum: ['manual', 'ai_review', 'ci_required', 'all'],
445
+ description: 'Gate this task\'s own completion. "manual" (default) allows a plain status change to done. "ai_review" or "all" BLOCK marking the task done (403 VERIFICATION_REQUIRED) until ai_completion_score reaches 0.8 — run "Verify with AI" first. "ci_required" or "all" BLOCK it until a PR is linked (github_pr_url) with passing CI. Setting this arms a real check against your own future attempt to close the task.',
446
+ },
447
+ github_pr_url: {
448
+ type: 'string',
449
+ description: 'URL of the GitHub Pull Request implementing this task. Used to fetch the PR diff for AI verification and to match incoming CI status (check_run/check_suite/workflow_run webhooks) back to this task.',
450
+ },
451
+ sort_order: {
452
+ type: 'number',
453
+ description: 'Position among the project\'s tasks (lower sorts first). NOT wired on the REST create path — POST /api/projects/:id/tasks always assigns MAX(sort_order)+1 and ignores this field on create. Set it after creation with update_task instead.',
454
+ },
455
+ is_milestone: {
456
+ type: 'boolean',
457
+ description: 'Mark this task as a milestone, drawn differently on the Gantt view. NOT wired on the REST create path — POST /api/projects/:id/tasks does not accept this field; the column is left at its default (0). Set it after creation with update_task instead.',
458
+ },
459
+ required_ci_checks: {
460
+ type: 'array',
461
+ items: { type: 'string' },
462
+ description: 'Which named CI checks (matched case-insensitively as a substring of the check name) must pass for "ci_required"/"all" verification_mode to consider CI satisfied — e.g. ["build","test"]. Read by GET /api/tasks/:id/ci-status\'s resolveRequiredChecks (app/functions/api/tasks/[id]/ci-status.js:13-32), NOT by the GitHub webhook handler, which sets ci_status unconditionally from whichever single check/workflow event just arrived. Omit to leave unset (falls back to the project\'s own required_ci_checks, then to requiring every check). NOT wired on the REST create path — POST /api/projects/:id/tasks does not accept this field. Set it after creation with update_task instead.',
463
+ },
428
464
  },
429
465
  required: ['project_id', 'title'],
430
466
  },
@@ -469,7 +505,29 @@ const tools = [
469
505
  },
470
506
  assignee_user_id: {
471
507
  type: 'string',
472
- description: 'User ID to assign the task to, replacing any existing assignee. Must be a member of the same account. Pass an empty string to unassign.',
508
+ description: 'User ID to assign the task to, REPLACING THE WHOLE assignee set (every non-active assignment row is deleted, active workers spared) — use this only when you want exactly one owner. Must be a member of the same account. Pass an empty string to unassign everyone (active workers still spared). To ADD a second assignee without touching the first, use add_assignee instead; to remove just one, use remove_assignee.',
509
+ },
510
+ verification_mode: {
511
+ type: 'string',
512
+ enum: ['manual', 'ai_review', 'ci_required', 'all'],
513
+ description: 'Gate this task\'s own completion. "manual" (default) allows a plain status change to done. "ai_review" or "all" BLOCK marking the task done (403 VERIFICATION_REQUIRED) until ai_completion_score reaches 0.8 — run "Verify with AI" first. "ci_required" or "all" BLOCK it until a PR is linked (github_pr_url) with passing CI. Setting this arms a real check against your own future attempt to close the task.',
514
+ },
515
+ github_pr_url: {
516
+ type: 'string',
517
+ description: 'URL of the GitHub Pull Request implementing this task. Used to fetch the PR diff for AI verification and to match incoming CI status back to this task. Pass an empty string to clear it.',
518
+ },
519
+ sort_order: {
520
+ type: 'number',
521
+ description: 'Position among the project\'s tasks (lower sorts first). Drives the order returned by list_tasks and the board/list views.',
522
+ },
523
+ is_milestone: {
524
+ type: 'boolean',
525
+ description: 'Mark this task as a milestone, drawn differently on the Gantt view. Pass false to unmark it.',
526
+ },
527
+ required_ci_checks: {
528
+ type: 'array',
529
+ items: { type: 'string' },
530
+ description: 'Which named CI checks (matched case-insensitively as a substring of the check name) must pass for "ci_required"/"all" verification_mode to consider CI satisfied — e.g. ["build","test"]. Read by GET /api/tasks/:id/ci-status\'s resolveRequiredChecks (app/functions/api/tasks/[id]/ci-status.js:13-32), NOT by the GitHub webhook handler, which sets ci_status unconditionally from whichever single check/workflow event just arrived regardless of this field. Stored as a JSON array. Pass an empty array to require every check; this tool has no way to clear the column back to "inherit from project" (the REST API reserves that for an empty string, which this array-typed field cannot send).',
473
531
  },
474
532
  blocked_by: {
475
533
  type: 'array',
@@ -507,6 +565,90 @@ const tools = [
507
565
  required: ['task_id', 'status'],
508
566
  },
509
567
  },
568
+ {
569
+ name: 'add_assignee',
570
+ description: 'Assign a user to a task WITHOUT disturbing anyone already assigned. task_assignments holds one row per (task_id, user_id) — POST /api/tasks/:id/assignments upserts exactly that one row and leaves every other assignee alone, unlike update_task\'s assignee_user_id (which REPLACES the whole set — use that only when you actually want a single owner). This is the fix for the defect where a second MCP assignment silently dropped the first.\n\nIdempotent and is_active-safe: this checks the task\'s current assignees first, and if the user already holds a row (assigned OR actively working) it does nothing and says so, rather than re-POSTing. That matters because the REST endpoint\'s upsert OVERWRITES is_active on conflict — a blind re-assign of someone with a running timer (is_active=1, set by start_working) would silently stop their clock. Skipping the no-op re-POST is what keeps assignment and active-work presence independent.\n\nRejects a user who is not a member of this account (404, surfaced as an error) rather than creating a dangling assignment.',
571
+ inputSchema: {
572
+ type: 'object',
573
+ properties: {
574
+ task_id: {
575
+ type: 'number',
576
+ description: 'The ID of the task to assign the user to.',
577
+ },
578
+ user_id: {
579
+ type: 'string',
580
+ description: 'User ID to add as an assignee. Must be a member of the same account.',
581
+ },
582
+ },
583
+ required: ['task_id', 'user_id'],
584
+ },
585
+ },
586
+ {
587
+ name: 'remove_assignee',
588
+ description: 'Remove exactly one user\'s assignment from a task, leaving every other assignee untouched. DELETE /api/tasks/:id/assignments?user_id=... removes that one task_assignments row outright (both the assignment and any is_active flag on it) — this is a full unassign, not stop_working\'s "still assigned, no longer active" (use stop_working to end a work session without unassigning).',
589
+ inputSchema: {
590
+ type: 'object',
591
+ properties: {
592
+ task_id: {
593
+ type: 'number',
594
+ description: 'The ID of the task to remove the assignment from.',
595
+ },
596
+ user_id: {
597
+ type: 'string',
598
+ description: 'User ID to remove from the task\'s assignees.',
599
+ },
600
+ },
601
+ required: ['task_id', 'user_id'],
602
+ },
603
+ },
604
+ {
605
+ name: 'verify_task',
606
+ description: 'Run AI verification: grades this task\'s acceptance_criteria against the diff of its linked PR (a Haiku relevance pre-screen, then a Sonnet deep review) and PERSISTS the result to ai_completion_score / ai_completion_notes. This is exactly what update_task_status checks when the task\'s verification_mode is "ai_review" or "all" — call this to satisfy that gate yourself rather than repeatedly hitting 403 VERIFICATION_REQUIRED with no way through it.\n\nCOSTS MONEY — DO NOT LOOP THIS. Every call is metered via recordAIUsage and recorded to ai_generations (Sonnet, large PR-diff context) for audit. It runs FREE while your account is within its plan\'s included monthly verification cap; once over that cap each call is billed as AI credit overage — 2 credits per call on this account. Calling this repeatedly hoping for a higher score spends real credits for no guaranteed gain; if a genuine score lands below the 0.8 gate, that is a finding about the work, not a reason to retry.\n\nGRADES THE DIFF, NOT THE RUNNING SYSTEM. A high score means the PR\'s code changes look like they satisfy the criteria on paper — it is NOT proof the deployed/running behavior actually works. Never report this score as end-to-end verification; that is a human\'s job or an independent task-verify sub-agent\'s, not this tool\'s.\n\nRequires the task to already have non-empty acceptance_criteria (set via update_task) and a way to see the work: either the task\'s own github_pr_url (set via update_task) or a pr_url/work_description passed here. Fails with a clear message naming what is missing rather than grading an empty contract — refusal is free, no credits spent.',
607
+ inputSchema: {
608
+ type: 'object',
609
+ properties: {
610
+ task_id: {
611
+ type: 'number',
612
+ description: 'The ID of the task to verify. Must already have non-empty acceptance_criteria.',
613
+ },
614
+ pr_url: {
615
+ type: 'string',
616
+ description: 'GitHub PR URL to grade against, for this call only — does not overwrite the task\'s stored github_pr_url. Omit to use the task\'s own linked PR.',
617
+ },
618
+ work_description: {
619
+ type: 'string',
620
+ description: 'Free-text description of the completed work to grade against. Used ONLY as a fallback when no PR diff is available (no pr_url given here and the task has no github_pr_url); ignored whenever a PR diff can be fetched.',
621
+ },
622
+ },
623
+ required: ['task_id'],
624
+ },
625
+ },
626
+ {
627
+ name: 'record_verification_feedback',
628
+ description: 'Record whether the most recent verify_task verdict on this task was accurate — calibration data, not a correction. Attaches to the latest ai_generations row of type completion_review for this task, so verify_task must have run at least once first. Does NOT change the stored ai_completion_score or re-run any model — it costs nothing (no Anthropic call, just an audit-log row) and is safe to call as often as useful.',
629
+ inputSchema: {
630
+ type: 'object',
631
+ properties: {
632
+ task_id: {
633
+ type: 'number',
634
+ description: 'The ID of the task whose most recent verification you are giving feedback on.',
635
+ },
636
+ agreed: {
637
+ type: 'boolean',
638
+ description: 'Whether you agree the AI verdict (score + summary) was an accurate read of the work.',
639
+ },
640
+ actual_score: {
641
+ type: 'number',
642
+ description: 'Optional: the score (0.0-1.0) you believe was actually correct, if you disagree with the AI\'s score.',
643
+ },
644
+ notes: {
645
+ type: 'string',
646
+ description: 'Optional: why you agreed or disagreed.',
647
+ },
648
+ },
649
+ required: ['task_id', 'agreed'],
650
+ },
651
+ },
510
652
  {
511
653
  name: 'log_time',
512
654
  description: 'Log time spent on a task. This triggers a real-time notification showing time was logged.',
@@ -622,7 +764,7 @@ const tools = [
622
764
  },
623
765
  {
624
766
  name: 'stop_working',
625
- description: 'Mark that you have stopped working on a task (break or switching tasks). Optionally capture your state on the way out: pass `note` to append a work note and/or `resume_context` to overwrite the resume block, so the next session can pick up where you left off. Both are recorded as author_kind="ai".',
767
+ description: 'Mark that you have stopped working on a task (break or switching tasks). Optionally capture your state on the way out: pass `note` to append a work note and/or `resume_context` to overwrite the resume block, so the next session can pick up where you left off.',
626
768
  inputSchema: {
627
769
  type: 'object',
628
770
  properties: {
@@ -644,7 +786,7 @@ const tools = [
644
786
  },
645
787
  {
646
788
  name: 'add_work_note',
647
- description: 'Append a timestamped work note (status update) to a task\'s activity log. Use this to record progress so a future session can pick up where you left off (e.g. "finished auth handler, tests green, next: wire the callback"). Notes are append-only and never overwrite each other. Set author_kind="human" ONLY when you are relaying a note dictated by the human user; use "ai" (the default) when leaving your own status update.',
789
+ description: 'Append a timestamped work note (status update) to a task\'s activity log. Use this to record progress so a future session can pick up where you left off (e.g. "finished auth handler, tests green, next: wire the callback"). Notes are append-only and never overwrite each other.',
648
790
  inputSchema: {
649
791
  type: 'object',
650
792
  properties: {
@@ -656,11 +798,6 @@ const tools = [
656
798
  type: 'string',
657
799
  description: 'The work note / status update text',
658
800
  },
659
- author_kind: {
660
- type: 'string',
661
- enum: ['ai', 'human'],
662
- description: 'Who authored this note: "ai" (default, your own update) or "human" (you are relaying something the user said).',
663
- },
664
801
  is_internal: {
665
802
  type: 'boolean',
666
803
  description: 'Whether the note is internal-only (hidden from customers). Defaults to true for work notes.',
@@ -671,7 +808,7 @@ const tools = [
671
808
  },
672
809
  {
673
810
  name: 'add_comment',
674
- description: 'Add a comment to a task\'s discussion thread. Unlike work notes (progress journal), comments are for communication and are customer-visible by default. Set author_kind="human" when relaying the user\'s words; use "ai" (default) for your own.',
811
+ description: 'Add a comment to a task\'s discussion thread. Unlike work notes (progress journal), comments are for communication and are customer-visible by default.',
675
812
  inputSchema: {
676
813
  type: 'object',
677
814
  properties: {
@@ -683,11 +820,6 @@ const tools = [
683
820
  type: 'string',
684
821
  description: 'The comment text',
685
822
  },
686
- author_kind: {
687
- type: 'string',
688
- enum: ['ai', 'human'],
689
- description: 'Who authored this comment: "ai" (default) or "human" (relaying the user).',
690
- },
691
823
  is_internal: {
692
824
  type: 'boolean',
693
825
  description: 'Whether the comment is internal-only (hidden from customers). Defaults to false.',
@@ -698,7 +830,7 @@ const tools = [
698
830
  },
699
831
  {
700
832
  name: 'set_resume_context',
701
- description: 'Set (overwrite) the task\'s resume context — a single pinned block describing the current state, what is done, what is next, and where to look. This is read first when a work session restarts cold (e.g. via start_working) so you can re-orient immediately. Update it at the end of a work session. Set author_kind="human" only when relaying the user\'s words; default "ai".',
833
+ description: 'Set (overwrite) the task\'s resume context — a single pinned block describing the current state, what is done, what is next, and where to look. This is read first when a work session restarts cold (e.g. via start_working) so you can re-orient immediately. Update it at the end of a work session.',
702
834
  inputSchema: {
703
835
  type: 'object',
704
836
  properties: {
@@ -710,11 +842,6 @@ const tools = [
710
842
  type: 'string',
711
843
  description: 'The resume-context text (markdown ok): current state / next steps / where to look.',
712
844
  },
713
- author_kind: {
714
- type: 'string',
715
- enum: ['ai', 'human'],
716
- description: 'Who authored this context: "ai" (default) or "human".',
717
- },
718
845
  },
719
846
  required: ['task_id', 'context'],
720
847
  },
@@ -898,7 +1025,12 @@ const toolHandlers = {
898
1025
  };
899
1026
  },
900
1027
 
901
- async create_task({ project_id, title, description, acceptance_criteria, estimated_minutes, priority, start_date, due_date, assignee_user_id, parent_task_id }) {
1028
+ async create_task({ project_id, title, description, acceptance_criteria, estimated_minutes, priority, start_date, due_date, assignee_user_id, parent_task_id, verification_mode, github_pr_url, sort_order, is_milestone, required_ci_checks }) {
1029
+ // sort_order, is_milestone and required_ci_checks are forwarded for parity
1030
+ // with update_task, but POST /api/projects/:id/tasks does not destructure
1031
+ // any of the three — it always assigns MAX(sort_order)+1 and leaves
1032
+ // is_milestone/required_ci_checks at their column defaults. Sending them
1033
+ // here is a silent no-op on create; use update_task right after to set them.
902
1034
  const task = await apiRequest(`/projects/${project_id}/tasks`, {
903
1035
  method: 'POST',
904
1036
  body: JSON.stringify({
@@ -911,6 +1043,11 @@ const toolHandlers = {
911
1043
  due_date,
912
1044
  assignee_user_id,
913
1045
  parent_task_id,
1046
+ verification_mode,
1047
+ github_pr_url,
1048
+ sort_order,
1049
+ is_milestone,
1050
+ required_ci_checks,
914
1051
  }),
915
1052
  });
916
1053
  return {
@@ -923,11 +1060,13 @@ const toolHandlers = {
923
1060
  };
924
1061
  },
925
1062
 
926
- async update_task({ task_id, title, description, acceptance_criteria, estimated_minutes, priority, start_date, due_date, assignee_user_id, blocked_by, blocked_reason }) {
1063
+ async update_task({ task_id, title, description, acceptance_criteria, estimated_minutes, priority, start_date, due_date, assignee_user_id, blocked_by, blocked_reason, verification_mode, github_pr_url, sort_order, is_milestone, required_ci_checks }) {
927
1064
  // Only forward what the caller actually named. An absent key leaves the
928
- // field alone; an explicitly empty string is how a date or the assignee is
929
- // cleared, and blocked_by is a REPLACE so an explicit [] must reach the
930
- // API — which is why these are `!== undefined` rather than truthiness.
1065
+ // field alone; an explicitly empty string is how a date, the assignee or
1066
+ // the PR link is cleared, and blocked_by is a REPLACE so an explicit []
1067
+ // must reach the API — which is why these are `!== undefined` rather than
1068
+ // truthiness. is_milestone follows the same rule: `false` is a real value,
1069
+ // not an absence, and must still reach the API.
931
1070
  const updates = {};
932
1071
  if (title !== undefined) updates.title = title;
933
1072
  if (description !== undefined) updates.description = description;
@@ -939,6 +1078,11 @@ const toolHandlers = {
939
1078
  if (assignee_user_id !== undefined) updates.assignee_user_id = assignee_user_id;
940
1079
  if (blocked_by !== undefined) updates.blocked_by = blocked_by;
941
1080
  if (blocked_reason !== undefined) updates.blocked_reason = blocked_reason;
1081
+ if (verification_mode !== undefined) updates.verification_mode = verification_mode;
1082
+ if (github_pr_url !== undefined) updates.github_pr_url = github_pr_url;
1083
+ if (sort_order !== undefined) updates.sort_order = sort_order;
1084
+ if (is_milestone !== undefined) updates.is_milestone = is_milestone;
1085
+ if (required_ci_checks !== undefined) updates.required_ci_checks = required_ci_checks;
942
1086
 
943
1087
  const task = await apiRequest(`/tasks/${task_id}`, {
944
1088
  method: 'PUT',
@@ -969,6 +1113,103 @@ const toolHandlers = {
969
1113
  };
970
1114
  },
971
1115
 
1116
+ async add_assignee({ task_id, user_id }) {
1117
+ // Check first so an already-assigned (or already-active) user is never
1118
+ // re-POSTed: the REST upsert overwrites is_active on conflict, and a
1119
+ // blind re-assign would silently stop that person's running timer. See
1120
+ // POST /api/tasks/:id/assignments' ON CONFLICT clause.
1121
+ const current = await apiRequest(`/tasks/${task_id}/assignments`);
1122
+ const assigned = current.assigned || [];
1123
+ const active = current.active_workers || [];
1124
+ const existing = [...assigned, ...active].find(a => a.user_id === user_id);
1125
+
1126
+ if (existing) {
1127
+ const state = active.some(a => a.user_id === user_id) ? 'actively working' : 'assigned';
1128
+ return {
1129
+ content: [
1130
+ {
1131
+ type: 'text',
1132
+ text: `${user_id} is already ${state} on task ${task_id} — no change made. Other assignees are untouched.`,
1133
+ },
1134
+ ],
1135
+ };
1136
+ }
1137
+
1138
+ await apiRequest(`/tasks/${task_id}/assignments`, {
1139
+ method: 'POST',
1140
+ body: JSON.stringify({ user_id }),
1141
+ });
1142
+
1143
+ return {
1144
+ content: [
1145
+ {
1146
+ type: 'text',
1147
+ text: `Assigned ${user_id} to task ${task_id}. Existing assignees, if any, were left in place.`,
1148
+ },
1149
+ ],
1150
+ };
1151
+ },
1152
+
1153
+ async remove_assignee({ task_id, user_id }) {
1154
+ await apiRequest(`/tasks/${task_id}/assignments?user_id=${encodeURIComponent(user_id)}`, {
1155
+ method: 'DELETE',
1156
+ });
1157
+
1158
+ return {
1159
+ content: [
1160
+ {
1161
+ type: 'text',
1162
+ text: `Removed ${user_id} from task ${task_id}'s assignees. Other assignees are untouched.`,
1163
+ },
1164
+ ],
1165
+ };
1166
+ },
1167
+
1168
+ async verify_task({ task_id, pr_url, work_description }) {
1169
+ const body = {};
1170
+ if (pr_url !== undefined) body.pr_url = pr_url;
1171
+ if (work_description !== undefined) body.work_description = work_description;
1172
+
1173
+ const result = await apiRequest(`/tasks/${task_id}/verify`, {
1174
+ method: 'POST',
1175
+ body: JSON.stringify(body),
1176
+ });
1177
+
1178
+ const billingNote = result.billing?.type === 'ai_credit_overage'
1179
+ ? `Billed ${result.billing.credits_charged} AI credit(s) as overage (over your plan's included verification cap).`
1180
+ : 'Free platform feature — within your plan\'s included verification cap.';
1181
+
1182
+ return {
1183
+ content: [
1184
+ {
1185
+ type: 'text',
1186
+ text: `Verification score for task ${task_id}: ${result.score} (confidence: ${result.confidence}). ${billingNote} ` +
1187
+ `This grades the PR diff against the acceptance criteria, not the running system.\n\n${JSON.stringify(result, null, 2)}`,
1188
+ },
1189
+ ],
1190
+ };
1191
+ },
1192
+
1193
+ async record_verification_feedback({ task_id, agreed, actual_score, notes }) {
1194
+ const body = { agreed };
1195
+ if (actual_score !== undefined) body.actual_score = actual_score;
1196
+ if (notes !== undefined) body.notes = notes;
1197
+
1198
+ await apiRequest(`/tasks/${task_id}/verify-feedback`, {
1199
+ method: 'POST',
1200
+ body: JSON.stringify(body),
1201
+ });
1202
+
1203
+ return {
1204
+ content: [
1205
+ {
1206
+ type: 'text',
1207
+ text: `Feedback recorded for task ${task_id}'s most recent AI verification (agreed: ${agreed}).`,
1208
+ },
1209
+ ],
1210
+ };
1211
+ },
1212
+
972
1213
  async log_time({ task_id, minutes, notes, date }) {
973
1214
  const entry = await apiRequest(`/tasks/${task_id}/time`, {
974
1215
  method: 'POST',
@@ -1100,14 +1341,14 @@ const toolHandlers = {
1100
1341
  if (note && note.trim()) {
1101
1342
  await apiRequest(`/tasks/${task_id}/activity`, {
1102
1343
  method: 'POST',
1103
- body: JSON.stringify({ note, author_kind: 'ai', is_internal: true }),
1344
+ body: JSON.stringify({ note, is_internal: true }),
1104
1345
  });
1105
1346
  captured.push('work note');
1106
1347
  }
1107
1348
  if (resume_context !== undefined && resume_context !== null) {
1108
1349
  await apiRequest(`/tasks/${task_id}`, {
1109
1350
  method: 'PUT',
1110
- body: JSON.stringify({ resume_context, resume_context_author_kind: 'ai' }),
1351
+ body: JSON.stringify({ resume_context }),
1111
1352
  });
1112
1353
  captured.push('resume context');
1113
1354
  }
@@ -1127,46 +1368,46 @@ const toolHandlers = {
1127
1368
  };
1128
1369
  },
1129
1370
 
1130
- async add_work_note({ task_id, note, author_kind = 'ai', is_internal = true }) {
1371
+ async add_work_note({ task_id, note, is_internal = true }) {
1131
1372
  const saved = await apiRequest(`/tasks/${task_id}/activity`, {
1132
1373
  method: 'POST',
1133
- body: JSON.stringify({ note, author_kind, is_internal }),
1374
+ body: JSON.stringify({ note, is_internal }),
1134
1375
  });
1135
1376
  return {
1136
1377
  content: [
1137
1378
  {
1138
1379
  type: 'text',
1139
- text: `Work note added to task #${task_id} (as ${author_kind === 'ai' ? 'AI Agent' : 'human'}). Note id: ${saved.id}.`,
1380
+ text: `Work note added to task #${task_id} (as ${AGENT_ID}). Note id: ${saved.id}.`,
1140
1381
  },
1141
1382
  ],
1142
1383
  };
1143
1384
  },
1144
1385
 
1145
- async add_comment({ task_id, comment, author_kind = 'ai', is_internal = false }) {
1386
+ async add_comment({ task_id, comment, is_internal = false }) {
1146
1387
  const saved = await apiRequest(`/tasks/${task_id}/comments`, {
1147
1388
  method: 'POST',
1148
- body: JSON.stringify({ comment, author_kind, is_internal }),
1389
+ body: JSON.stringify({ comment, is_internal }),
1149
1390
  });
1150
1391
  return {
1151
1392
  content: [
1152
1393
  {
1153
1394
  type: 'text',
1154
- text: `Comment added to task #${task_id} (as ${author_kind === 'ai' ? 'AI Agent' : 'human'}). Comment id: ${saved.id}.`,
1395
+ text: `Comment added to task #${task_id} (as ${AGENT_ID}). Comment id: ${saved.id}.`,
1155
1396
  },
1156
1397
  ],
1157
1398
  };
1158
1399
  },
1159
1400
 
1160
- async set_resume_context({ task_id, context, author_kind = 'ai' }) {
1401
+ async set_resume_context({ task_id, context }) {
1161
1402
  await apiRequest(`/tasks/${task_id}`, {
1162
1403
  method: 'PUT',
1163
- body: JSON.stringify({ resume_context: context, resume_context_author_kind: author_kind }),
1404
+ body: JSON.stringify({ resume_context: context }),
1164
1405
  });
1165
1406
  return {
1166
1407
  content: [
1167
1408
  {
1168
1409
  type: 'text',
1169
- text: `Resume context updated for task #${task_id} (as ${author_kind === 'ai' ? 'AI Agent' : 'human'}). The next session that calls start_working will read this first.`,
1410
+ text: `Resume context updated for task #${task_id} (as ${AGENT_ID}). The next session that calls start_working will read this first.`,
1170
1411
  },
1171
1412
  ],
1172
1413
  };