@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.
- package/package.json +1 -1
- package/src/index.js +278 -37
package/package.json
CHANGED
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
929
|
-
// cleared, and blocked_by is a REPLACE so an explicit []
|
|
930
|
-
// API — which is why these are `!== undefined` rather than
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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 ${
|
|
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,
|
|
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,
|
|
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 ${
|
|
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
|
|
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
|
|
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 ${
|
|
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
|
};
|