@konductro/claude-plugin 2.1.1 → 2.3.0-rc.1

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/README.md CHANGED
@@ -50,6 +50,73 @@ Tools are called automatically by Claude based on natural language. You don't in
50
50
  | `start_work` | Start work on a ticket — creates branch via ADO, pushes spec file, sets in_progress |
51
51
  | `create_pr` | Create a pull request for a ticket — auto-assigns reviewer, moves ticket to pr_open |
52
52
 
53
+ ### Work items
54
+
55
+ | Tool | What it does |
56
+ |---|---|
57
+ | `create_work_item` | Create a story, task or bug in the project this repo belongs to |
58
+ | `update_work_item` | Change the title, description or acceptance criteria of a work item you own |
59
+ | `delete_work_item` | Delete a task or bug that has not been started |
60
+
61
+ The project is resolved from the repository's git remote, so you are never asked for a
62
+ project identifier.
63
+
64
+ **Everything created lands in Draft** and still needs an SDM's approval before it can be
65
+ worked on.
66
+
67
+ **`update_work_item` can change three fields and no others.** Status, sprint, assignee and
68
+ everything else are deliberately not on the schema — change those in Konductro. Sending
69
+ `acceptanceCriteria` replaces the whole list rather than appending to it. The `changed`
70
+ list it reports back is the fields that were written, not the ones that turned out to
71
+ differ, so re-sending a value unchanged still shows up in it.
72
+
73
+ **A task is created in the repository you are standing in.** Its parent story is the only
74
+ thing you need to name. Set `repositoryId` yourself only for a task that will be built in a
75
+ different repository on the same project.
76
+
77
+ **Only a task may have a parent.** A story or a bug sent with a `parentKey` is refused and
78
+ nothing is created, rather than being filed top-level as though it had worked. A bug is
79
+ always standalone here — Konductro parents one to a story only when QA files it against a
80
+ failed test criterion — so name the story in the description instead. A story is top-level
81
+ by definition: the hierarchy is story → task, and work that belongs under a story is a task.
82
+
83
+ **`create_work_item` returns the new item's id.** Use it for the follow-up call. Ticket
84
+ keys are prefix plus number and the prefix is not returned, so a guessed key resolves to a
85
+ different project's item of the same number instead of failing.
86
+
87
+ #### Who can create and change stories
88
+
89
+ Each project has a story access setting, which an SDM controls under Settings → General:
90
+
91
+ | Setting | What a developer can do |
92
+ |---|---|
93
+ | **Planning roles only** | Cannot create or change stories. Filing bugs and editing tasks are unaffected. |
94
+ | **Developers can, with review** | Changes apply straight away, then wait for an SDM to approve or reject. |
95
+ | **Developers can, no review** | Creates and changes apply with nothing held for approval. |
96
+
97
+ A new project starts on **planning roles only**.
98
+
99
+ Under *with review*, a change to a story is applied and then held: the story will not move
100
+ to Ready for QA until an SDM resolves it, even once every task is done. If it is rejected,
101
+ the previous values are restored and you are notified.
102
+
103
+ #### What a refusal means
104
+
105
+ These are answers, not errors — none of them is worth retrying as-is.
106
+
107
+ | You will see | What to do |
108
+ |---|---|
109
+ | The project's story access is set to planning roles only | A project setting. Ask an SDM to make the change, or to open story access for developers. |
110
+ | You can only change work items assigned to you, that you are the developer on, or that you created | Ask whoever owns it, or ask an SDM. |
111
+ | Stories cannot be deleted from the CLI | Deleting a story removes its tasks with it. Archive it in Konductro instead. |
112
+ | QA filed this bug against a failed test criterion | It is QA's record of a failure, not yours to remove. |
113
+ | Only a task can be created under a parent | Nothing was created. For a bug, re-run without `parentKey` and name the story in the description. For a story, you probably wanted `kind: "task"`. |
114
+ | Work has already started on this item | It has a branch, or has moved past sprint planning. Only items still in draft, approved or in_sprint can be deleted. |
115
+ | Your seat is a viewer seat | Read-only across the whole platform, not a project setting. Ask an admin for a contributor seat. |
116
+ | Your role on this project is read-only | Ask an SDM for a writing role on the project. |
117
+ | You are not a member of this project | Ask an SDM to add you to the project team. |
118
+ | Not authenticated | A setup problem rather than a permissions one — your `KONDUCTRO_CLI_TOKEN` is missing, expired or revoked. Create a new one in Konductro under your profile. |
119
+
53
120
  ### Technical analysis
54
121
 
55
122
  | Tool | What it does |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konductro/claude-plugin",
3
- "version": "2.1.1",
3
+ "version": "2.3.0-rc.1",
4
4
  "description": "Claude Code plugin for the Konductro platform — technical analysis and delivery tasks",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -35,13 +35,156 @@ async function konductroFetch(path, options = {}) {
35
35
  });
36
36
 
37
37
  if (!response.ok) {
38
- const body = await response.text().catch(() => '');
39
- throw new Error(`Konductro API error (${response.status}): ${body}`);
38
+ const raw = await response.text().catch(() => '');
39
+ // KON-517: keep the machine-readable code and status on the error. The backend
40
+ // answers { error, code, details? }, and every tool should branch on CODE rather than
41
+ // on message text so a wording change server-side cannot break the plugin.
42
+ //
43
+ // NOTE this CHANGED err.message. It used to be the whole envelope —
44
+ // `Konductro API error (404): {"error":"Bug not found","code":"BUG_NOT_FOUND"}` — and
45
+ // is now just the `error` sentence. Anything that used to sniff the message for a
46
+ // status or a code no longer matches, so those call sites were moved onto err.code /
47
+ // err.status (get_bug_for_enrichment and submit_bug_enrichment). If you add a tool,
48
+ // read the code; do not go back to matching text.
49
+ let parsed = null;
50
+ try { parsed = JSON.parse(raw); } catch { /* not JSON — fall through to the raw body */ }
51
+ const err = new Error(
52
+ parsed?.error ? parsed.error : `Konductro API error (${response.status}): ${raw}`,
53
+ );
54
+ err.status = response.status;
55
+ err.code = parsed?.code ?? null;
56
+ // Zod's flatten() on a validation failure. Kept so a refusal can name the offending
57
+ // field instead of saying "Validation failed" and leaving the agent to guess.
58
+ err.details = parsed?.details ?? null;
59
+ throw err;
40
60
  }
41
61
 
42
62
  return response.json();
43
63
  }
44
64
 
65
+ // ─── KON-517: work-item tools ────────────────────────────────────────────────
66
+
67
+ /**
68
+ * Turn a backend refusal into a sentence a developer can act on.
69
+ *
70
+ * A bare 403 makes an agent retry or guess. Each of these says what to do instead, and
71
+ * the setting-related one NAMES the setting, because the developer's next move is to ask
72
+ * their SDM rather than to try again.
73
+ *
74
+ * Keyed on code, never on message text.
75
+ */
76
+ function refusalText(err) {
77
+ switch (err.code) {
78
+ case 'STORY_ACCESS_DENIED':
79
+ return "Refused: this project's story access is set to planning roles only, so stories here can only be created or edited by an SDM, architect or UX lead. This is a project setting, not something to retry — ask an SDM to make the change, or to open story access for developers. Filing bugs and editing tasks are unaffected.";
80
+ case 'NOT_YOUR_WORK_ITEM':
81
+ return 'Refused: you can only change work items assigned to you, that you are the developer on, or that you created. Ask whoever owns it, or ask an SDM to make the change.';
82
+ case 'STORIES_NOT_DELETABLE':
83
+ return 'Refused: stories cannot be deleted from the CLI — deleting one removes its tasks with it and cannot be undone. Archive it in Konductro instead.';
84
+ case 'QA_FILED_BUG':
85
+ return "Refused: QA filed this bug against a failed test criterion, so it is their record of what went wrong and is not yours to remove. Fix it, or ask QA if it was raised in error.";
86
+ case 'WORK_ALREADY_STARTED':
87
+ // Mirrors DELETABLE_STATUSES in work-item-access.service.ts: draft, approved and
88
+ // in_sprint are all still deletable. Anything past those, or anything with a
89
+ // branch, is not.
90
+ return 'Refused: work has already started on this item — it has a branch, or has moved past sprint planning (only items still in draft, approved or in_sprint can be deleted). Deleting it would orphan the branch. Close it out in Konductro instead.';
91
+ case 'INSUFFICIENT_ROLE':
92
+ return 'Refused: your Konductro seat is a viewer seat, which is read-only across the whole platform. This is not a project setting — ask an admin to move you to a contributor seat.';
93
+ case 'READ_ONLY_ROLE':
94
+ return 'Refused: your role on this project is read-only (viewer or stakeholder), so you cannot create or change work items here. Ask an SDM to give you a writing role on the project.';
95
+ case 'NOT_PROJECT_MEMBER':
96
+ return 'Refused: you are not a member of this project, so you cannot create or change work items in it. Ask an SDM to add you to the project team.';
97
+ default:
98
+ return null;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * A missing token is a setup problem, not a permission problem — say so differently.
104
+ *
105
+ * The `setup` flag is what workItemCall branches on. It used to match these messages by
106
+ * their opening words, which meant a plugin whose setup wording differs (the codex and
107
+ * cursor ports name their own setup command) silently fell through and got the sentence
108
+ * wrapped in `Failed:`. Matching on our own wording is the same habit this change removes
109
+ * for BACKEND errors, so the flag is set explicitly here instead.
110
+ */
111
+ function assertAuthConfigured() {
112
+ if (!CLI_TOKEN) {
113
+ const err = new Error(
114
+ 'Not authenticated: KONDUCTRO_CLI_TOKEN is not set. Create a CLI token in Konductro under your profile, then set KONDUCTRO_CLI_TOKEN in this MCP server\'s environment. This is a setup problem, not a permissions one.',
115
+ );
116
+ err.setup = true;
117
+ throw err;
118
+ }
119
+ if (!KONDUCTRO_URL) {
120
+ const err = new Error('Not configured: KONDUCTRO_URL is not set. Point it at your Konductro instance.');
121
+ err.setup = true;
122
+ throw err;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Name the field a validation failure is actually about.
128
+ *
129
+ * The backend answers INVALID_BODY with Zod's flatten() attached. Dropping it leaves the
130
+ * agent holding "Validation failed" with nothing to correct, which it can only respond to
131
+ * by guessing — so the whole point of validating is lost on the way back.
132
+ */
133
+ function validationText(err) {
134
+ const f = err.details?.fieldErrors ?? {};
135
+ const fields = Object.entries(f)
136
+ .map(([name, msgs]) => `${name} (${(msgs ?? []).join('; ')})`)
137
+ .join(', ');
138
+ const form = (err.details?.formErrors ?? []).join('; ');
139
+ const parts = [fields, form].filter(Boolean).join(' — ');
140
+ if (!parts) return err.message;
141
+ // The backend's sentence for this code is sometimes unpunctuated ('Validation failed'),
142
+ // so close it before appending rather than running the two together.
143
+ const lead = /[.!?]$/.test(err.message) ? err.message : `${err.message}.`;
144
+ return `${lead} Problem with: ${parts}.`;
145
+ }
146
+
147
+ /**
148
+ * Every work-item tool answers refusals the same way, so the cases read alike.
149
+ *
150
+ * ON isError. A REFUSAL is not an error — it is a correct, final answer to a question the
151
+ * developer was entitled to ask, and flagging it as an error is what makes an agent retry
152
+ * it or escalate around it. A genuine FAILURE (bad token, unreachable server, a 500, a
153
+ * shape the backend rejected) is an error, and is flagged, because the agent should stop
154
+ * and surface it rather than carry on as if the write landed.
155
+ *
156
+ * This is deliberately NOT "whatever the rest of the file does": most tools here flag
157
+ * nothing at all and report failures as ordinary prose, which is the weaker half of the
158
+ * convention. The two bug-enrichment tools already make the distinction this follows.
159
+ */
160
+ async function workItemCall(fn) {
161
+ try {
162
+ assertAuthConfigured();
163
+ return { content: [{ type: 'text', text: await fn() }] };
164
+ } catch (err) {
165
+ if (err.status === 401) {
166
+ return {
167
+ content: [{ type: 'text', text: 'Not authenticated: Konductro rejected the CLI token. It may be expired or revoked — create a new one in Konductro under your profile and update KONDUCTRO_CLI_TOKEN. This is a setup problem, not a permissions one.' }],
168
+ isError: true,
169
+ };
170
+ }
171
+ const refusal = refusalText(err);
172
+ if (refusal) return { content: [{ type: 'text', text: refusal }] };
173
+
174
+ // Setup errors already read as full sentences; only wrap the ones that do not.
175
+ const text = err.setup
176
+ ? err.message
177
+ : `Failed: ${err.code === 'INVALID_BODY' ? validationText(err) : err.message}`;
178
+ return { content: [{ type: 'text', text }], isError: true };
179
+ }
180
+ }
181
+
182
+ /** Resolve the project from the repo's git remote, so no project id is ever asked for. */
183
+ async function projectFromRepo(repoUrl) {
184
+ const repo = await konductroFetch(`/api/cli/repo-by-url?url=${encodeURIComponent(repoUrl)}`);
185
+ return repo;
186
+ }
187
+
45
188
  const server = new McpServer({
46
189
  name: 'konductro',
47
190
  version: '1.0.0',
@@ -348,6 +491,30 @@ server.tool(
348
491
  }
349
492
  );
350
493
 
494
+ // Tool: Submit a regenerated developer brief (KON-325)
495
+ server.tool(
496
+ 'submit_regenerated_brief',
497
+ "Submit a freshly regenerated developer brief for a task to Konductro. It becomes the task's current brief and is appended to the brief history (the previous version is preserved). Regenerate the brief tailored to Claude Code conventions before calling this.",
498
+ {
499
+ ticketId: z.string().describe('The ticket ID or key (e.g. KON-325) to update'),
500
+ content: z.string().describe('The full regenerated developer brief (markdown)'),
501
+ toolset: z.enum(['claude', 'q', 'codex', 'cursor']).default('claude').describe("Coding toolset that produced this brief (this plugin: 'claude')"),
502
+ },
503
+ async ({ ticketId, content, toolset }) => {
504
+ const result = await konductroFetch(`/api/cli/tickets/${ticketId}/regenerate-brief`, {
505
+ method: 'POST',
506
+ body: JSON.stringify({ content, toolset }),
507
+ });
508
+
509
+ return {
510
+ content: [{
511
+ type: 'text',
512
+ text: `Developer brief regenerated for ${ticketId} — now version ${result.version} (${result.toolset}).`,
513
+ }],
514
+ };
515
+ }
516
+ );
517
+
351
518
  // Tool: List my tickets
352
519
  server.tool(
353
520
  'list_my_tickets',
@@ -464,6 +631,12 @@ server.tool(
464
631
  text += `### Context Pack\n\n${ctx.ticket.contextPack}\n\n`;
465
632
  }
466
633
 
634
+ // Developer Notes — present when context is fetched on a story itself. For a
635
+ // task, the notes that matter are the parent story's, rendered below.
636
+ if (ctx.ticket.developerNotes) {
637
+ text += `### Developer Notes\n\n${ctx.ticket.developerNotes}\n\n`;
638
+ }
639
+
467
640
  // Parent Story
468
641
  if (ctx.parent) {
469
642
  text += `### Parent Story: ${ctx.parent.ticketKey} — ${ctx.parent.title}\n\n`;
@@ -476,6 +649,10 @@ server.tool(
476
649
  if (ctx.parent.contextPack) {
477
650
  text += `**Story Context Pack:**\n${ctx.parent.contextPack}\n\n`;
478
651
  }
652
+ // Story-level context accumulated by whoever worked the story's other tasks.
653
+ if (ctx.parent.developerNotes) {
654
+ text += `**Parent Story, Developer Notes:**\n${ctx.parent.developerNotes}\n\n`;
655
+ }
479
656
  }
480
657
 
481
658
  // Children tasks
@@ -521,6 +698,155 @@ server.tool(
521
698
  }
522
699
  );
523
700
 
701
+ // Tool: Append a developer note to a story
702
+ server.tool(
703
+ 'append_story_developer_note',
704
+ "Append a note to a story's developer notes in Konductro, so context you worked out on one task is there for whoever picks up the next task on the same story. Notes live on the STORY, not the task: pass the parent story's key, which get_ticket_context prints in its Parent Story heading. Pass the current task's key as taskKey so the entry is attributable. Send only your new note — the endpoint appends it, and Konductro stamps the author, timestamp and task itself, so do not include a heading, a timestamp, a separator, or any notes you read earlier.",
705
+ {
706
+ storyId: z.string().describe('The story to append to — key (e.g. KON-283) or UUID. Not a task.'),
707
+ note: z.string().describe('The new note only, as markdown. No heading, timestamp or separator.'),
708
+ taskKey: z.string().optional().describe("The key of the task this note came from (e.g. KON-311), when there is one"),
709
+ },
710
+ async ({ storyId, note, taskKey }) => {
711
+ const result = await konductroFetch(`/api/cli/tickets/${storyId}/developer-notes`, {
712
+ method: 'POST',
713
+ body: JSON.stringify({ note, taskKey }),
714
+ });
715
+
716
+ // Deliberately not echoing result.developerNotes — it is the whole accumulated
717
+ // blob, and repeating it here invites a later call to send it back as a "new"
718
+ // note, which would duplicate every entry.
719
+ return {
720
+ content: [{
721
+ type: 'text',
722
+ text: `Note appended to ${result.ticketKey}'s developer notes.`,
723
+ }],
724
+ };
725
+ }
726
+ );
727
+
728
+ // Tool: Create a work item
729
+ server.tool(
730
+ 'create_work_item',
731
+ "Create a story, task or bug in the Konductro project this repository belongs to. Anything created this way lands in DRAFT and still needs an SDM's approval before it can be worked — say so when you report back, so nobody thinks it is ready to start. A task needs a parent story, and is created in the repository repoUrl resolves to; a bug needs no parent and is filed standalone. Creating STORIES is governed by the project's story access setting and may be refused; filing bugs and creating tasks are not affected by it. Pass the repository's git remote (git remote get-url origin) — the project is resolved from it, never asked for.",
732
+ {
733
+ repoUrl: z.string().describe('The git remote URL of this repository (run: git remote get-url origin)'),
734
+ kind: z.enum(['story', 'task', 'bug']).describe('What to create'),
735
+ title: z.string().describe('A short title, as a person would write it'),
736
+ description: z.string().describe('What this is and why, in markdown'),
737
+ acceptanceCriteria: z.array(z.string()).optional().describe('Observable behaviour, one per item. Omit for a bug unless you have them.'),
738
+ parentKey: z.string().optional().describe('For a TASK, the parent story key (e.g. KON-451). ONLY a task may have a parent. A story or a bug sent with a parentKey is REFUSED and nothing is created, rather than being filed top-level as though it had worked.'),
739
+ repositoryId: z.string().optional().describe('For a TASK, the repository UUID it will be built in. Leave this out — it defaults to the repository repoUrl resolves to, which is the one you are standing in. Only set it for a task that will be built in a DIFFERENT repository on the same project, and then you need that repo\'s UUID from Konductro.'),
740
+ },
741
+ async ({ repoUrl, kind, title, description, acceptanceCriteria, parentKey, repositoryId }) => workItemCall(async () => {
742
+ // KON-517 review: the backend's body schema DECLARES parentKey, so .strict() does not
743
+ // reject it — it is accepted and then dropped for anything that is not a task, because
744
+ // the route reads it only inside the task branch and calls createBug / createStory
745
+ // without it (routes/cli.ts). The item lands top-level and the 201 reports success, so
746
+ // asking for it under a story and being told it worked is exactly what happens. Refuse
747
+ // instead: a refusal is recoverable in the next turn, a misfiled item is found later
748
+ // by someone else, if at all.
749
+ //
750
+ // The two cases are dropped by the same code path but for different reasons, so the
751
+ // remedy differs and the message branches. A bug COULD have a parent — QA files bugs
752
+ // against the tested story that way — this endpoint just does not wire it through. A
753
+ // story could not: the hierarchy is story → task, so nothing sits above a story ever,
754
+ // and an agent sending one almost certainly meant to create a task.
755
+ if (kind !== 'task' && parentKey) {
756
+ const why = kind === 'bug'
757
+ ? `Konductro only parents a bug to a story when QA files it against a failed test criterion; a bug created from the CLI is always standalone in the project's Bugs phase. Re-run without parentKey, and name ${parentKey} in the description so the link to it is not lost.`
758
+ : `A story is top-level by definition — the hierarchy is story → task, so nothing sits above a story. If you meant to add work under ${parentKey}, re-run with kind "task". If you meant a new story, re-run without parentKey.`;
759
+ return `Refused: only a task can be created under a parent, and NOTHING WAS CREATED. ${why}`;
760
+ }
761
+
762
+ const repo = await projectFromRepo(repoUrl);
763
+
764
+ // KON-517 review, blocking: creating a task was a dead end. The backend requires
765
+ // repositoryId as a UUID, and nothing a developer can reach hands one out —
766
+ // get_decomposition_context is the only tool that returns one, and that needs an
767
+ // assigned decomposition task you do not have. So the agent was asked for a value it
768
+ // could not obtain, and a guess came back as a bare "Validation failed".
769
+ //
770
+ // It was in hand the whole time: repo-by-url returns the repository row (id, name,
771
+ // repoUrl, repoType, stacks, projectId, project) and the id was being discarded.
772
+ // Defaulting to it is also the right answer rather than merely an available one — the
773
+ // task is being written from inside the repo it will be built in.
774
+ //
775
+ // Tasks only. createStory ignores it, and defaulting it on a bug would silently start
776
+ // attributing bugs to a repository they were not attributed to before.
777
+ const resolvedRepositoryId = kind === 'task' ? (repositoryId ?? repo.id) : repositoryId;
778
+
779
+ const created = await konductroFetch(`/api/cli/projects/${repo.projectId}/work-items`, {
780
+ method: 'POST',
781
+ body: JSON.stringify({ kind, title, description, acceptanceCriteria, parentKey, repositoryId: resolvedRepositoryId }),
782
+ });
783
+ // KON-517 review, blocking: this used to print the ticketNumber alone and then say
784
+ // "use get_ticket_context with its key" — but a key is PREFIX-NUMBER, the prefix is
785
+ // nowhere in this tool's reach (repo-by-url does not return ticketPrefix), and so the
786
+ // agent was being invited to guess one. resolveTicketId matches prefix+number across
787
+ // the whole TENANT, not within the project, so a guessed prefix does not 404 — it
788
+ // resolves to another project's ticket of the same number, and the next
789
+ // update_work_item edits that one instead. The id closes it off: the UUID branch of
790
+ // resolveTicketId short-circuits with no lookup at all, so it can only be this row.
791
+ return `Created #${created.ticketNumber} — "${created.title}" (${created.kind}) in ${repo.project?.name ?? 'the project'}.\n\nid: ${created.id}\n\nIt is in ${created.status.toUpperCase()} and needs an SDM to approve it before it can be worked on. Use THAT id with get_ticket_context, update_work_item or delete_work_item. Do not build a key from #${created.ticketNumber} — the project's ticket prefix is not returned here, and a guessed prefix silently resolves to a different project's item of the same number rather than failing.`;
792
+ })
793
+ );
794
+
795
+ // Tool: Update a work item
796
+ server.tool(
797
+ 'update_work_item',
798
+ "Change the title, description or acceptance criteria of a work item you own in Konductro — one assigned to you, that you are the developer on, or that you created. THESE THREE FIELDS ARE ALL YOU CAN CHANGE: status, sprint, assignee and everything else are deliberately not available here and must be changed in Konductro. Editing a STORY is governed by the project's story access setting and may be refused, or may be applied and then held for an SDM to review; editing a task is not affected by it. Send only the fields you are actually changing.",
799
+ {
800
+ key: z.string().describe('The work item key (e.g. KON-451) or UUID'),
801
+ title: z.string().optional().describe('The new title'),
802
+ description: z.string().optional().describe('The new description, in markdown'),
803
+ acceptanceCriteria: z.array(z.string()).optional().describe('The FULL new list, not just the additions — it replaces what is there'),
804
+ },
805
+ async ({ key, title, description, acceptanceCriteria }) => workItemCall(async () => {
806
+ const body = {};
807
+ if (title !== undefined) body.title = title;
808
+ if (description !== undefined) body.description = description;
809
+ if (acceptanceCriteria !== undefined) body.acceptanceCriteria = acceptanceCriteria;
810
+ if (Object.keys(body).length === 0) {
811
+ return 'Nothing to change — pass at least one of title, description or acceptanceCriteria.';
812
+ }
813
+
814
+ const result = await konductroFetch(`/api/cli/tickets/${encodeURIComponent(key)}/work-item`, {
815
+ method: 'PATCH',
816
+ body: JSON.stringify(body),
817
+ });
818
+
819
+ // Deliberately NOT echoing the updated values back. Returning the item invites the
820
+ // next call to send them again as "new" values, re-submitting fields nobody changed
821
+ // and raising a review for a no-op. Same reason append_story_developer_note does not
822
+ // return the notes blob.
823
+ // `changed` echoes the fields that were WRITTEN, not the ones that turned out to
824
+ // differ — the server returns Object.keys of what it applied (routes/cli.ts). So
825
+ // re-sending a title with its existing value still reports "changed: title". Do not
826
+ // read it as a diff.
827
+ const changed = (result.changed ?? Object.keys(body)).join(', ');
828
+ let text = `Updated ${key} (#${result.ticketNumber}) — changed: ${changed}.`;
829
+ if (result.awaitingReview) {
830
+ text += '\n\nThis change is APPLIED but now waiting for an SDM to approve or reject it, because this project reviews story changes. Until it is resolved the story will not move to Ready for QA, even if every task is done. If it is rejected the previous values come back and you will be notified.';
831
+ }
832
+ return text;
833
+ })
834
+ );
835
+
836
+ // Tool: Delete a work item
837
+ server.tool(
838
+ 'delete_work_item',
839
+ "Delete a task or a bug from Konductro that has not been started. Bugs count: Konductro stores a bug as a task, so a developer's own unstarted bug can be deleted here. Stories cannot — deleting one takes its tasks with it and there is no restore, so archive a story in Konductro instead. Neither can a bug QA filed against a failed test criterion, which is part of that test record rather than yours. Nothing that has started can be deleted by anyone: that means anything carrying a branch, or anything past sprint planning, since only draft, approved and in_sprint items still qualify. Close those out in Konductro instead. Beyond that you may delete an item you are assigned to, are the developer on, or created; an SDM, architect or UX lead on the project may delete any item that qualifies. This is permanent and there is no undo, so be sure the developer asked for it.",
840
+ {
841
+ key: z.string().describe('The task or bug key (e.g. KON-452) or UUID'),
842
+ },
843
+ async ({ key }) => workItemCall(async () => {
844
+ const result = await konductroFetch(`/api/cli/tickets/${encodeURIComponent(key)}/work-item`, { method: 'DELETE' });
845
+ const extra = result.deleted > 1 ? ` (${result.deleted} rows, including its linked records)` : '';
846
+ return `Deleted ${key}${extra}. This cannot be undone.`;
847
+ })
848
+ );
849
+
524
850
  // Tool: Start work on a ticket
525
851
  server.tool(
526
852
  'start_work',
@@ -917,7 +1243,13 @@ server.tool(
917
1243
 
918
1244
  return { content: [{ type: 'text', text }] };
919
1245
  } catch (err) {
920
- const msg = err.message?.includes('BUG_NOT_FOUND') || err.message?.includes('404')
1246
+ // KON-517: these used to sniff err.message for 'BUG_NOT_FOUND' / '404', which
1247
+ // worked only because the message was the whole raw envelope. konductroFetch now
1248
+ // sets message to the backend's `error` sentence, so that match silently stopped
1249
+ // firing and a missing bug reported as a generic failure. Read the code and the
1250
+ // status instead — both are on the error and neither depends on wording.
1251
+ const notABug = err.code === 'BUG_NOT_FOUND' || err.status === 404;
1252
+ const msg = notABug
921
1253
  ? `Ticket ${ticketId} not found or is not a bug.`
922
1254
  : `Failed to load bug context: ${err.message}`;
923
1255
  return { content: [{ type: 'text', text: msg }], isError: true };
@@ -953,9 +1285,11 @@ server.tool(
953
1285
  : 'No new enrichment applied (fields may already exist on the ticket).';
954
1286
  return { content: [{ type: 'text', text: applied }] };
955
1287
  } catch (err) {
956
- const msg = err.message?.includes('BUG_NOT_FOUND') || err.message?.includes('404')
1288
+ // Same fix as get_bug_for_enrichment above — match on err.code / err.status, not
1289
+ // on message text, which konductroFetch no longer formats the way this expected.
1290
+ const msg = (err.code === 'BUG_NOT_FOUND' || err.status === 404)
957
1291
  ? `Ticket ${ticketId} not found or is not a bug.`
958
- : err.message?.includes('INVALID_ENRICHMENT')
1292
+ : err.code === 'INVALID_ENRICHMENT'
959
1293
  ? 'At least one enrichment field must be provided.'
960
1294
  : `Failed to submit enrichment: ${err.message}`;
961
1295
  return { content: [{ type: 'text', text: msg }], isError: true };
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: regenerate-brief
3
+ description: Regenerate a task's developer brief using Claude Code, tailored to the current codebase, and submit it back to Konductro. Use when a developer wants a fresh, tool-tailored brief for an existing task.
4
+ allowed-tools: Read, Grep, Glob, Bash, mcp__konductro__list_my_tickets, mcp__konductro__get_ticket_context, mcp__konductro__submit_regenerated_brief
5
+ ---
6
+
7
+ # Regenerate Developer Brief
8
+
9
+ Help a developer regenerate the developer brief for a Konductro task, tailored to Claude Code, then submit it back. The regenerated brief becomes the task's current brief; the previous version is preserved in the brief history (nothing is lost).
10
+
11
+ ## Workflow
12
+
13
+ ### Step 1: Pick the task
14
+
15
+ Call `list_my_tickets` to show assigned tickets, or take a ticket key the developer gives you (e.g. `KON-325`). Choose the task whose brief to regenerate.
16
+
17
+ ### Step 2: Load current context
18
+
19
+ Call `get_ticket_context` with the ticket ID. Review the **current brief** (context pack), description, acceptance criteria, architectural notes, and target repository — this is your starting point.
20
+
21
+ ### Step 3: Explore the codebase
22
+
23
+ Use Read / Grep / Glob against the **local** repository to ground the brief in the actual code as it stands now — relevant files, existing patterns, entry points, and tests. A regenerated brief should reflect the current state of the code, not just the original decomposition.
24
+
25
+ ### Step 4: Regenerate the brief
26
+
27
+ Write a fresh developer brief tailored to Claude Code conventions. Use clear, actionable sections:
28
+ - **Architecture Context** — how this task fits, key constraints
29
+ - **Files to Create / Files to Modify** — concrete paths + what changes
30
+ - **Implementation Steps** — a numbered, ordered approach
31
+ - **Out of Scope** — what this task explicitly does not cover
32
+
33
+ Keep it focused on THIS task. Prefer concrete file paths and specifics over generic advice.
34
+
35
+ ### Step 5: Submit
36
+
37
+ Call `submit_regenerated_brief` with the ticket ID and the regenerated brief as `content` (the `toolset` defaults to `claude` for this plugin). On success it becomes the current brief and is recorded as a new version.
38
+
39
+ ## Rules
40
+
41
+ - This does not change the task's status or branch — it only updates the brief. To start work, use `/start-ticket`.
42
+ - If `submit_regenerated_brief` fails (auth/validation), show the error clearly **and** show the brief you wrote so the developer can retry without losing it.
43
+ - Regenerate against the real local code — don't just reword the existing brief.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: start-ticket
3
3
  description: Pick up an assigned ticket and start work — creates branch, pushes ticket spec, updates status. Use when a developer wants to begin working on a ticket.
4
- allowed-tools: Read, Grep, Glob, Bash, mcp__konductro__list_my_tickets, mcp__konductro__get_ticket_context, mcp__konductro__start_work
4
+ allowed-tools: Read, Grep, Glob, Bash, mcp__konductro__list_my_tickets, mcp__konductro__get_ticket_context, mcp__konductro__submit_regenerated_brief, mcp__konductro__start_work
5
5
  ---
6
6
 
7
7
  # Start Ticket
@@ -24,6 +24,10 @@ Call `get_ticket_context` with the chosen ticket ID. Present a summary:
24
24
  - Target repository and default base branch
25
25
  - Any dependencies or architectural notes
26
26
 
27
+ ### Step 2.5 (optional): Regenerate the brief before starting
28
+
29
+ Offer the developer the option to **regenerate the developer brief first**, tailored to Claude Code, so they start from a fresh, code-grounded brief. If they accept: explore the local codebase, write an updated brief, and submit it with `submit_regenerated_brief` (toolset `claude`) before moving on. If they decline, continue with the existing brief. (For a standalone regeneration without starting work, use `/regenerate-brief`.)
30
+
27
31
  ### Step 3: Confirm Branch Details
28
32
 
29
33
  Ask the developer: