@konductro/cursor-plugin 1.1.0 → 1.3.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/README.md +65 -1
- package/bin/setup.js +1 -0
- package/package.json +1 -1
- package/servers/konductro-api.js +601 -6
- package/skills/demand-analysis.mdc +107 -0
- package/skills/technical-analysis.mdc +20 -1
package/README.md
CHANGED
|
@@ -59,8 +59,11 @@ Tools are called automatically by Cursor's agent based on your requests:
|
|
|
59
59
|
| `get_ticket_context` | Load full context for a ticket |
|
|
60
60
|
| `start_work` | Create branch and start a ticket |
|
|
61
61
|
| `create_pr` | Create a pull request for a ticket |
|
|
62
|
+
| `create_work_item` | Create a story, task or bug in the project this repo belongs to |
|
|
63
|
+
| `update_work_item` | Change the title, description or acceptance criteria of a work item you own |
|
|
64
|
+
| `delete_work_item` | Delete a task or bug that has not been started |
|
|
62
65
|
| `list_tech_analysis_tasks` | List tech analysis assignments |
|
|
63
|
-
| `get_task_context` | Load tech analysis context |
|
|
66
|
+
| `get_task_context` | Load tech analysis context, plus the requester's message when a refresh was asked for |
|
|
64
67
|
| `submit_tech_analysis` | Submit analysis document |
|
|
65
68
|
| `list_decomposition_tasks` | List story decomposition assignments |
|
|
66
69
|
| `get_decomposition_context` | Load story context |
|
|
@@ -74,6 +77,67 @@ Tools are called automatically by Cursor's agent based on your requests:
|
|
|
74
77
|
| `profile_repository` | Submit repository profile |
|
|
75
78
|
| `find_project_prototypes` | Find UX prototypes for a project |
|
|
76
79
|
|
|
80
|
+
### Work items
|
|
81
|
+
|
|
82
|
+
The project is resolved from the repository's git remote, so you are never asked for a
|
|
83
|
+
project identifier.
|
|
84
|
+
|
|
85
|
+
**Everything created lands in Draft** and still needs an SDM's approval before it can be
|
|
86
|
+
worked on.
|
|
87
|
+
|
|
88
|
+
**`update_work_item` can change three fields and no others.** Status, sprint, assignee and
|
|
89
|
+
everything else are deliberately not on the schema — change those in Konductro. Sending
|
|
90
|
+
`acceptanceCriteria` replaces the whole list rather than appending to it. The `changed`
|
|
91
|
+
list it reports back is the fields that were written, not the ones that turned out to
|
|
92
|
+
differ, so re-sending a value unchanged still shows up in it.
|
|
93
|
+
|
|
94
|
+
**A task is created in the repository you are standing in.** Its parent story is the only
|
|
95
|
+
thing you need to name. Set `repositoryId` yourself only for a task that will be built in a
|
|
96
|
+
different repository on the same project.
|
|
97
|
+
|
|
98
|
+
**Only a task may have a parent.** A story or a bug sent with a `parentKey` is refused and
|
|
99
|
+
nothing is created, rather than being filed top-level as though it had worked. A bug is
|
|
100
|
+
always standalone here — Konductro parents one to a story only when QA files it against a
|
|
101
|
+
failed test criterion — so name the story in the description instead. A story is top-level
|
|
102
|
+
by definition: the hierarchy is story → task, and work that belongs under a story is a task.
|
|
103
|
+
|
|
104
|
+
**`create_work_item` returns the new item's id.** Use it for the follow-up call. Ticket
|
|
105
|
+
keys are prefix plus number and the prefix is not returned, so a guessed key resolves to a
|
|
106
|
+
different project's item of the same number instead of failing.
|
|
107
|
+
|
|
108
|
+
#### Who can create and change stories
|
|
109
|
+
|
|
110
|
+
Each project has a story access setting, which an SDM controls under Settings → General:
|
|
111
|
+
|
|
112
|
+
| Setting | What a developer can do |
|
|
113
|
+
|---|---|
|
|
114
|
+
| **Planning roles only** | Cannot create or change stories. Filing bugs and editing tasks are unaffected. |
|
|
115
|
+
| **Developers can, with review** | Changes apply straight away, then wait for an SDM to approve or reject. |
|
|
116
|
+
| **Developers can, no review** | Creates and changes apply with nothing held for approval. |
|
|
117
|
+
|
|
118
|
+
A new project starts on **planning roles only**.
|
|
119
|
+
|
|
120
|
+
Under *with review*, a change to a story is applied and then held: the story will not move
|
|
121
|
+
to Ready for QA until an SDM resolves it, even once every task is done. If it is rejected,
|
|
122
|
+
the previous values are restored and you are notified.
|
|
123
|
+
|
|
124
|
+
#### What a refusal means
|
|
125
|
+
|
|
126
|
+
These are answers, not errors — none of them is worth retrying as-is.
|
|
127
|
+
|
|
128
|
+
| You will see | What to do |
|
|
129
|
+
|---|---|
|
|
130
|
+
| 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. |
|
|
131
|
+
| 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. |
|
|
132
|
+
| Stories cannot be deleted from the CLI | Deleting a story removes its tasks with it. Archive it in Konductro instead. |
|
|
133
|
+
| QA filed this bug against a failed test criterion | It is QA's record of a failure, not yours to remove. |
|
|
134
|
+
| 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"`. |
|
|
135
|
+
| 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. |
|
|
136
|
+
| Your seat is a viewer seat | Read-only across the whole platform, not a project setting. Ask an admin for a contributor seat. |
|
|
137
|
+
| Your role on this project is read-only | Ask an SDM for a writing role on the project. |
|
|
138
|
+
| You are not a member of this project | Ask an SDM to add you to the project team. |
|
|
139
|
+
| Konductro is not configured | A setup problem rather than a permissions one — re-run `konductro-cursor-setup` with your URL and token. |
|
|
140
|
+
|
|
77
141
|
## Updating Your Token
|
|
78
142
|
|
|
79
143
|
Run the setup command again with the new token:
|
package/bin/setup.js
CHANGED
|
@@ -136,6 +136,7 @@ function main() {
|
|
|
136
136
|
console.log(' prototype — build a UX prototype');
|
|
137
137
|
console.log(' find-prototype — find and download prototypes');
|
|
138
138
|
console.log(' bug-enrich — enrich a bug report');
|
|
139
|
+
console.log(' demand-analysis — size a demand against the real code');
|
|
139
140
|
console.log('');
|
|
140
141
|
console.log('Reload Cursor to activate (Cmd+Shift+P → "Developer: Reload Window").');
|
|
141
142
|
console.log('');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@konductro/cursor-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Cursor IDE plugin for the Konductro platform: technical analysis, delivery tasks, repository profiling, and prototype workflows.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
package/servers/konductro-api.js
CHANGED
|
@@ -32,6 +32,24 @@ function loadFileConfig() {
|
|
|
32
32
|
const fileConfig = loadFileConfig();
|
|
33
33
|
const KONDUCTRO_URL = (process.env.KONDUCTRO_URL || fileConfig.url || fileConfig.konductroUrl || '').replace(/\/$/, '');
|
|
34
34
|
const CLI_TOKEN = process.env.KONDUCTRO_CLI_TOKEN || fileConfig.token || fileConfig.cliToken || '';
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* What this plugin tells the server it is, on every request.
|
|
38
|
+
*
|
|
39
|
+
* READ FROM package.json, never a constant. The server's refusal of an outdated plugin is
|
|
40
|
+
* based on this string, so a hand-maintained copy that drifts from the published version
|
|
41
|
+
* is worse than no header at all: it would be believed.
|
|
42
|
+
*
|
|
43
|
+
* `readFileSync` rather than a JSON import assertion, whose syntax differs between Node 18
|
|
44
|
+
* and 20 and would break for whichever developers are on the other one.
|
|
45
|
+
*
|
|
46
|
+
* The shape is `<plugin>/<version>` and is identical across all four plugins, so the
|
|
47
|
+
* server parses one format rather than four.
|
|
48
|
+
*/
|
|
49
|
+
const PLUGIN_VERSION = JSON.parse(
|
|
50
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
51
|
+
).version;
|
|
52
|
+
const PLUGIN_ID = `cursor/${PLUGIN_VERSION}`;
|
|
35
53
|
const NOT_CONFIGURED_MESSAGE =
|
|
36
54
|
'Konductro is not configured. Run `konductro-cursor-setup --url <URL> --token <TOKEN>`, or set KONDUCTRO_URL and KONDUCTRO_CLI_TOKEN in the MCP server environment.';
|
|
37
55
|
|
|
@@ -39,6 +57,7 @@ async function konductroFetch(path, options = {}) {
|
|
|
39
57
|
const url = `${KONDUCTRO_URL}${path}`;
|
|
40
58
|
const headers = {
|
|
41
59
|
'Authorization': `Bearer ${CLI_TOKEN}`,
|
|
60
|
+
'X-Konductro-Plugin': PLUGIN_ID,
|
|
42
61
|
...options.headers,
|
|
43
62
|
};
|
|
44
63
|
// Only set Content-Type and default body for methods that send JSON
|
|
@@ -52,13 +71,148 @@ async function konductroFetch(path, options = {}) {
|
|
|
52
71
|
});
|
|
53
72
|
|
|
54
73
|
if (!response.ok) {
|
|
55
|
-
const
|
|
56
|
-
|
|
74
|
+
const raw = await response.text().catch(() => '');
|
|
75
|
+
// KON-520: keep the machine-readable code and status on the error. The backend
|
|
76
|
+
// answers { error, code, details? }, and every tool should branch on CODE rather than
|
|
77
|
+
// on message text so a wording change server-side cannot break the plugin.
|
|
78
|
+
//
|
|
79
|
+
// NOTE this CHANGED err.message. It used to be the whole envelope —
|
|
80
|
+
// `Konductro API error (404): {"error":"Bug not found","code":"BUG_NOT_FOUND"}` — and
|
|
81
|
+
// is now just the `error` sentence. Anything that used to sniff the message for a
|
|
82
|
+
// status or a code no longer matches, so those call sites were moved onto err.code /
|
|
83
|
+
// err.status (get_bug_for_enrichment and submit_bug_enrichment). If you add a tool,
|
|
84
|
+
// read the code; do not go back to matching text.
|
|
85
|
+
let parsed = null;
|
|
86
|
+
try { parsed = JSON.parse(raw); } catch { /* not JSON — fall through to the raw body */ }
|
|
87
|
+
const err = new Error(
|
|
88
|
+
parsed?.error ? parsed.error : `Konductro API error (${response.status}): ${raw}`,
|
|
89
|
+
);
|
|
90
|
+
err.status = response.status;
|
|
91
|
+
err.code = parsed?.code ?? null;
|
|
92
|
+
// Zod's flatten() on a validation failure. Kept so a refusal can name the offending
|
|
93
|
+
// field instead of saying "Validation failed" and leaving the agent to guess.
|
|
94
|
+
err.details = parsed?.details ?? null;
|
|
95
|
+
throw err;
|
|
57
96
|
}
|
|
58
97
|
|
|
59
98
|
return response.json();
|
|
60
99
|
}
|
|
61
100
|
|
|
101
|
+
// ─── KON-520: work-item tools ────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Turn a backend refusal into a sentence a developer can act on.
|
|
105
|
+
*
|
|
106
|
+
* A bare 403 makes an agent retry or guess. Each of these says what to do instead, and
|
|
107
|
+
* the setting-related one NAMES the setting, because the developer's next move is to ask
|
|
108
|
+
* their SDM rather than to try again.
|
|
109
|
+
*
|
|
110
|
+
* Keyed on code, never on message text.
|
|
111
|
+
*/
|
|
112
|
+
function refusalText(err) {
|
|
113
|
+
switch (err.code) {
|
|
114
|
+
case 'STORY_ACCESS_DENIED':
|
|
115
|
+
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.";
|
|
116
|
+
case 'NOT_YOUR_WORK_ITEM':
|
|
117
|
+
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.';
|
|
118
|
+
case 'STORIES_NOT_DELETABLE':
|
|
119
|
+
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.';
|
|
120
|
+
case 'QA_FILED_BUG':
|
|
121
|
+
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.";
|
|
122
|
+
case 'WORK_ALREADY_STARTED':
|
|
123
|
+
// Mirrors DELETABLE_STATUSES in work-item-access.service.ts: draft, approved and
|
|
124
|
+
// in_sprint are all still deletable. Anything past those, or anything with a
|
|
125
|
+
// branch, is not.
|
|
126
|
+
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.';
|
|
127
|
+
case 'INSUFFICIENT_ROLE':
|
|
128
|
+
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.';
|
|
129
|
+
case 'READ_ONLY_ROLE':
|
|
130
|
+
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.';
|
|
131
|
+
case 'NOT_PROJECT_MEMBER':
|
|
132
|
+
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.';
|
|
133
|
+
default:
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A missing token is a setup problem, not a permission problem — say so differently.
|
|
140
|
+
*
|
|
141
|
+
* Uses this plugin's own NOT_CONFIGURED_MESSAGE, which names the `konductro-cursor-setup`
|
|
142
|
+
* command, rather than telling a Cursor user to go and set environment variables by hand.
|
|
143
|
+
* The `setup` flag is what workItemCall reads: the message is this repo's, so matching on
|
|
144
|
+
* its opening words the way the reference plugin does would not survive here.
|
|
145
|
+
*/
|
|
146
|
+
function assertAuthConfigured() {
|
|
147
|
+
if (!KONDUCTRO_URL || !CLI_TOKEN) {
|
|
148
|
+
const err = new Error(NOT_CONFIGURED_MESSAGE);
|
|
149
|
+
err.setup = true;
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Name the field a validation failure is actually about.
|
|
156
|
+
*
|
|
157
|
+
* The backend answers INVALID_BODY with Zod's flatten() attached. Dropping it leaves the
|
|
158
|
+
* agent holding "Validation failed" with nothing to correct, which it can only respond to
|
|
159
|
+
* by guessing — so the whole point of validating is lost on the way back.
|
|
160
|
+
*/
|
|
161
|
+
function validationText(err) {
|
|
162
|
+
const f = err.details?.fieldErrors ?? {};
|
|
163
|
+
const fields = Object.entries(f)
|
|
164
|
+
.map(([name, msgs]) => `${name} (${(msgs ?? []).join('; ')})`)
|
|
165
|
+
.join(', ');
|
|
166
|
+
const form = (err.details?.formErrors ?? []).join('; ');
|
|
167
|
+
const parts = [fields, form].filter(Boolean).join(' — ');
|
|
168
|
+
if (!parts) return err.message;
|
|
169
|
+
// The backend's sentence for this code is sometimes unpunctuated ('Validation failed'),
|
|
170
|
+
// so close it before appending rather than running the two together.
|
|
171
|
+
const lead = /[.!?]$/.test(err.message) ? err.message : `${err.message}.`;
|
|
172
|
+
return `${lead} Problem with: ${parts}.`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Every work-item tool answers refusals the same way, so the cases read alike.
|
|
177
|
+
*
|
|
178
|
+
* ON isError. A REFUSAL is not an error — it is a correct, final answer to a question the
|
|
179
|
+
* developer was entitled to ask, and flagging it as an error is what makes an agent retry
|
|
180
|
+
* it or escalate around it. A genuine FAILURE (bad token, unreachable server, a 500, a
|
|
181
|
+
* shape the backend rejected) is an error, and is flagged, because the agent should stop
|
|
182
|
+
* and surface it rather than carry on as if the write landed.
|
|
183
|
+
*
|
|
184
|
+
* This is deliberately NOT "whatever the rest of the file does": most tools here flag
|
|
185
|
+
* nothing at all and report failures as ordinary prose, which is the weaker half of the
|
|
186
|
+
* convention. The two bug-enrichment tools already make the distinction this follows.
|
|
187
|
+
*/
|
|
188
|
+
async function workItemCall(fn) {
|
|
189
|
+
try {
|
|
190
|
+
assertAuthConfigured();
|
|
191
|
+
return { content: [{ type: 'text', text: await fn() }] };
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (err.status === 401) {
|
|
194
|
+
return {
|
|
195
|
+
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, then re-run \`konductro-cursor-setup --url <URL> --token <TOKEN>\`. This is a setup problem, not a permissions one.` }],
|
|
196
|
+
isError: true,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const refusal = refusalText(err);
|
|
200
|
+
if (refusal) return { content: [{ type: 'text', text: refusal }] };
|
|
201
|
+
|
|
202
|
+
// Setup errors already read as full sentences; only wrap the ones that do not.
|
|
203
|
+
const text = err.setup
|
|
204
|
+
? err.message
|
|
205
|
+
: `Failed: ${err.code === 'INVALID_BODY' ? validationText(err) : err.message}`;
|
|
206
|
+
return { content: [{ type: 'text', text }], isError: true };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Resolve the project from the repo's git remote, so no project id is ever asked for. */
|
|
211
|
+
async function projectFromRepo(repoUrl) {
|
|
212
|
+
const repo = await konductroFetch(`/api/cli/repo-by-url?url=${encodeURIComponent(repoUrl)}`);
|
|
213
|
+
return repo;
|
|
214
|
+
}
|
|
215
|
+
|
|
62
216
|
const server = new McpServer({
|
|
63
217
|
name: 'konductro',
|
|
64
218
|
version: '1.0.0',
|
|
@@ -91,7 +245,7 @@ server.tool(
|
|
|
91
245
|
}
|
|
92
246
|
|
|
93
247
|
const taskList = tasks.map((t, i) =>
|
|
94
|
-
`${i + 1}. **${t.projectName}** — ${t.phaseName}\n Client: ${t.clientName}\n Phase ID: ${t.phaseId}\n ${t.hasExistingDraft ? '(has existing draft)' : '(new analysis)'}`
|
|
248
|
+
`${i + 1}. **${t.projectName}** — ${t.phaseName}\n Client: ${t.clientName}\n Phase ID: ${t.phaseId}\n ${t.refreshRequested ? '(refresh requested)' : t.hasExistingDraft ? '(has existing draft)' : '(new analysis)'}`
|
|
95
249
|
).join('\n\n');
|
|
96
250
|
|
|
97
251
|
return {
|
|
@@ -121,6 +275,25 @@ server.tool(
|
|
|
121
275
|
if (context.phase.description) contextText += `**Description:** ${context.phase.description}\n`;
|
|
122
276
|
contextText += `\n`;
|
|
123
277
|
|
|
278
|
+
// KON-321: a refresh was requested, so this is a revision of an approved
|
|
279
|
+
// analysis rather than a first pass. Sits above the repositories and the
|
|
280
|
+
// previous document deliberately — it changes what is being asked for, so
|
|
281
|
+
// it has to be read before the material it applies to. Absent on a normal
|
|
282
|
+
// task, in which case the output is exactly what it was before.
|
|
283
|
+
if (context.refreshContext) {
|
|
284
|
+
const { requestedBy, message } = context.refreshContext;
|
|
285
|
+
contextText += `### Refresh Requested\n\n`;
|
|
286
|
+
contextText += `A refresh of this technical analysis was requested`;
|
|
287
|
+
if (requestedBy) contextText += ` by ${requestedBy}`;
|
|
288
|
+
contextText += `.\n\n`;
|
|
289
|
+
if (message) {
|
|
290
|
+
contextText += `**Context from the requester:**\n\n${message}\n\n`;
|
|
291
|
+
} else {
|
|
292
|
+
contextText += `No further context was given.\n\n`;
|
|
293
|
+
}
|
|
294
|
+
contextText += `Revise the existing analysis below rather than starting over.\n\n`;
|
|
295
|
+
}
|
|
296
|
+
|
|
124
297
|
if (context.repositories.length > 0) {
|
|
125
298
|
contextText += `### Repositories\n\n`;
|
|
126
299
|
for (const repo of context.repositories) {
|
|
@@ -147,6 +320,8 @@ server.tool(
|
|
|
147
320
|
}
|
|
148
321
|
}
|
|
149
322
|
|
|
323
|
+
contextText += renderDocumentTemplate(context.documentTemplate);
|
|
324
|
+
|
|
150
325
|
return {
|
|
151
326
|
content: [{
|
|
152
327
|
type: 'text',
|
|
@@ -156,6 +331,51 @@ server.tool(
|
|
|
156
331
|
}
|
|
157
332
|
);
|
|
158
333
|
|
|
334
|
+
/**
|
|
335
|
+
* The document structure this workspace requires, as the agent must read it.
|
|
336
|
+
*
|
|
337
|
+
* APPENDED, never woven into the blob above. Four plugins share that server response and
|
|
338
|
+
* the agent's prompt is sensitive to its shape, so everything before this point is
|
|
339
|
+
* byte-identical to what it has always been. A workspace with no template gets an empty
|
|
340
|
+
* string here and therefore exactly today's context.
|
|
341
|
+
*
|
|
342
|
+
* THE WORDING IS IDENTICAL IN ALL FOUR PLUGINS, deliberately. The story this belongs to
|
|
343
|
+
* requires the same behaviour from Claude, Amazon Q, Codex and Cursor, and four separately
|
|
344
|
+
* worded prompts is precisely how that stops being true. Change it here and copy it, do
|
|
345
|
+
* not improve it in one place.
|
|
346
|
+
*
|
|
347
|
+
* Standing content is reproduced WORD FOR WORD rather than summarised. It is usually a
|
|
348
|
+
* compliance clause the workspace requires verbatim, which is the whole reason the field
|
|
349
|
+
* exists.
|
|
350
|
+
*/
|
|
351
|
+
function renderDocumentTemplate(template) {
|
|
352
|
+
if (!template) return '';
|
|
353
|
+
|
|
354
|
+
let text = `### Required Document Structure\n\n`;
|
|
355
|
+
text += `This workspace requires technical analysis documents to follow **${template.name}** `;
|
|
356
|
+
text += `(v${template.version}). Produce these sections, in this order, using these exact headings.\n\n`;
|
|
357
|
+
|
|
358
|
+
for (const [index, section] of template.sections.entries()) {
|
|
359
|
+
text += `${index + 1}. **${section.title}**`;
|
|
360
|
+
if (section.intent) text += ` — ${section.intent}`;
|
|
361
|
+
if (section.mustCover) text += ` _(must cover: do not produce the document until you can answer this)_`;
|
|
362
|
+
text += `\n`;
|
|
363
|
+
if (section.diagram && section.diagram !== 'none') {
|
|
364
|
+
const kind = section.diagram === 'any' ? 'a Mermaid diagram of whichever type fits' : `a Mermaid ${section.diagram} diagram`;
|
|
365
|
+
text += ` Include ${kind} inside this section.\n`;
|
|
366
|
+
}
|
|
367
|
+
if (section.standingContent) {
|
|
368
|
+
text += ` Reproduce this text inside the section, word for word:\n`;
|
|
369
|
+
text += ` > ${section.standingContent.split('\n').join('\n > ')}\n`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
text += `\nThis structure replaces any default you would otherwise use. `;
|
|
374
|
+
text += `The sections marked "must cover" are what your exploration and your questions `;
|
|
375
|
+
text += `must be able to answer before you produce the document.\n\n`;
|
|
376
|
+
return text;
|
|
377
|
+
}
|
|
378
|
+
|
|
159
379
|
// Tool: Submit tech analysis
|
|
160
380
|
server.tool(
|
|
161
381
|
'submit_tech_analysis',
|
|
@@ -180,6 +400,216 @@ server.tool(
|
|
|
180
400
|
}
|
|
181
401
|
);
|
|
182
402
|
|
|
403
|
+
// ══ DEMAND ANALYSIS ════════════════════════════════════════════════════════
|
|
404
|
+
//
|
|
405
|
+
// The demand-level counterpart of the three tools above. A DEMAND has no project yet —
|
|
406
|
+
// which projects it lands in is decided at IT Eval, from what this analysis finds — so
|
|
407
|
+
// the context carries the whole workspace's repositories and the submission reports
|
|
408
|
+
// which of them the change actually touches.
|
|
409
|
+
|
|
410
|
+
// Tool: List pending demand analyses
|
|
411
|
+
server.tool(
|
|
412
|
+
'list_demand_analyses',
|
|
413
|
+
'List demand technical analyses assigned to you in Konductro. A demand sits above projects: it is a business request that has passed the demand forum and now needs sizing against the real code.',
|
|
414
|
+
{},
|
|
415
|
+
async () => {
|
|
416
|
+
if (!KONDUCTRO_URL || !CLI_TOKEN) {
|
|
417
|
+
return {
|
|
418
|
+
content: [{
|
|
419
|
+
type: 'text',
|
|
420
|
+
text: 'Konductro is not configured. Please set your Konductro URL and CLI token in the plugin settings.',
|
|
421
|
+
}],
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const { tasks } = await konductroFetch('/api/cli/demand-analyses');
|
|
426
|
+
|
|
427
|
+
if (!tasks || tasks.length === 0) {
|
|
428
|
+
return {
|
|
429
|
+
content: [{ type: 'text', text: 'No demand analyses assigned to you.' }],
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const lines = tasks.map((t) => {
|
|
434
|
+
const parts = [`### ${t.key} — ${t.title}`];
|
|
435
|
+
if (t.businessUnit) parts.push(`**Business unit:** ${t.businessUnit}`);
|
|
436
|
+
if (t.nextSitting) parts.push(`**Back at the forum:** ${new Date(t.nextSitting).toISOString().slice(0, 10)}`);
|
|
437
|
+
if (t.startWith && t.startWith.length) parts.push(`**Start with:** ${t.startWith.join(', ')}`);
|
|
438
|
+
if (t.askedFor) parts.push(`**They asked:** ${t.askedFor}`);
|
|
439
|
+
parts.push(`\`get_demand_analysis_context\` with demandId ${t.key} to load it.`);
|
|
440
|
+
return parts.join('\n');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
content: [{
|
|
445
|
+
type: 'text',
|
|
446
|
+
text: `## Demand analyses assigned to you (${tasks.length})\n\n${lines.join('\n\n')}`,
|
|
447
|
+
}],
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
// Tool: Load one demand's analysis context
|
|
453
|
+
server.tool(
|
|
454
|
+
'get_demand_analysis_context',
|
|
455
|
+
'Load everything needed to run a demand technical analysis: the pitch as the business wrote it, the value case, what the forum asked to have answered, the repositories to start with, and every repository in the workspace.',
|
|
456
|
+
{
|
|
457
|
+
demandId: z.string().describe('The demand key (e.g. DEM-4) or its UUID'),
|
|
458
|
+
},
|
|
459
|
+
async ({ demandId }) => {
|
|
460
|
+
if (!KONDUCTRO_URL || !CLI_TOKEN) {
|
|
461
|
+
return {
|
|
462
|
+
content: [{
|
|
463
|
+
type: 'text',
|
|
464
|
+
text: 'Konductro is not configured. Please set your Konductro URL and CLI token in the plugin settings.',
|
|
465
|
+
}],
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const ctx = await konductroFetch(`/api/cli/demand-analyses/${demandId}/context`);
|
|
470
|
+
const d = ctx && ctx.demand;
|
|
471
|
+
if (!d) {
|
|
472
|
+
return {
|
|
473
|
+
content: [{
|
|
474
|
+
type: 'text',
|
|
475
|
+
text: `No demand context came back for ${demandId}. Check the key, and that the analysis is assigned to you.`,
|
|
476
|
+
}],
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const cost = Object.entries(d.cost || {})
|
|
481
|
+
.filter(([, v]) => v)
|
|
482
|
+
.map(([k, v]) => `- **${k}:** ${v}`)
|
|
483
|
+
.join('\n') || '- Not given';
|
|
484
|
+
|
|
485
|
+
// THE PROJECT ID IS THE POINT OF THIS LIST, not decoration. `submit_demand_analysis`
|
|
486
|
+
// takes a `projectId` per proposed phase, and its description says to take it from
|
|
487
|
+
// here. Rendering only the project NAME left the analyst nowhere to get it: either
|
|
488
|
+
// the phase is submitted with no project, so the green light creates no real Phase
|
|
489
|
+
// and IT Eval prefills with nothing, or the agent invents a uuid and the whole
|
|
490
|
+
// submission is refused with PROJECT_NOT_FOUND.
|
|
491
|
+
const repos = (ctx.repositories || [])
|
|
492
|
+
.map((r) => `- \`${r.name}\` (${r.projectName}${r.type ? `, ${r.type}` : ''}) — ${r.url}\n projectId: \`${r.projectId}\``)
|
|
493
|
+
.join('\n') || '- None linked to any project yet';
|
|
494
|
+
|
|
495
|
+
const conversation = (ctx.conversation || [])
|
|
496
|
+
.map((c) => `- **${c.by}**${c.kind === 'amendment' ? ' (amendment)' : ''}: ${c.body}`)
|
|
497
|
+
.join('\n');
|
|
498
|
+
|
|
499
|
+
// THE WORKSPACE'S OWN QUESTIONS, which the block above does not carry.
|
|
500
|
+
//
|
|
501
|
+
// The fields rendered above are the nine Konductro ships. A workspace that added
|
|
502
|
+
// "Which regulator requires this?" has that question and its answer here and
|
|
503
|
+
// NOWHERE ELSE in this payload, so without this the analysis never learns it was
|
|
504
|
+
// asked — the one thing the whole templating feature exists for.
|
|
505
|
+
//
|
|
506
|
+
// Read off the PINNED template, so a question a later template deleted still shows
|
|
507
|
+
// on the demands that answered it. Unanswered questions are kept rather than
|
|
508
|
+
// dropped: that the forum asked and got nothing is itself worth knowing.
|
|
509
|
+
const tpl = ctx.template;
|
|
510
|
+
const templateBlock = tpl
|
|
511
|
+
? [
|
|
512
|
+
`## The form they filled in — ${tpl.name} v${tpl.version}`,
|
|
513
|
+
'',
|
|
514
|
+
...(tpl.sections || []).flatMap((section) => [
|
|
515
|
+
`### ${section.name}`,
|
|
516
|
+
...(section.help ? ['', `_${section.help}_`] : []),
|
|
517
|
+
'',
|
|
518
|
+
...(section.questions || []).map((q) =>
|
|
519
|
+
`**${q.question}**\n\n${q.answer || '_Not answered._'}\n`
|
|
520
|
+
),
|
|
521
|
+
]),
|
|
522
|
+
].join('\n')
|
|
523
|
+
: '';
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
content: [{
|
|
527
|
+
type: 'text',
|
|
528
|
+
text: [
|
|
529
|
+
`# ${d.key} — ${d.title}`,
|
|
530
|
+
'',
|
|
531
|
+
`**Raised by:** ${d.raisedBy}${d.businessUnit ? ` (${d.businessUnit})` : ''}`,
|
|
532
|
+
'',
|
|
533
|
+
'## What they need',
|
|
534
|
+
d.need,
|
|
535
|
+
'',
|
|
536
|
+
'## What happens today',
|
|
537
|
+
d.currentState || 'Not given',
|
|
538
|
+
'',
|
|
539
|
+
'## Who it affects',
|
|
540
|
+
[d.affects, d.affectsWho].filter(Boolean).join(' — ') || 'Not given',
|
|
541
|
+
'',
|
|
542
|
+
'## What it costs to not have it',
|
|
543
|
+
cost,
|
|
544
|
+
'',
|
|
545
|
+
'## Tied to a date',
|
|
546
|
+
d.date?.type ? `${d.date.type}${d.date.by ? ` — ${String(d.date.by).slice(0, 10)}` : ''}${d.date.why ? ` (${d.date.why})` : ''}` : 'No fixed date',
|
|
547
|
+
'',
|
|
548
|
+
'## How they would know it worked',
|
|
549
|
+
d.successLooksLike || 'Not given',
|
|
550
|
+
'',
|
|
551
|
+
'## What the forum asked you to answer',
|
|
552
|
+
ctx.askedFor || 'Nothing specific. Answer the pitch.',
|
|
553
|
+
'',
|
|
554
|
+
'## Where to start looking',
|
|
555
|
+
(ctx.startWith && ctx.startWith.length) ? ctx.startWith.map((r) => `\`${r}\``).join(', ') : 'Nothing named — use your judgement.',
|
|
556
|
+
'',
|
|
557
|
+
'## Every repository in this workspace',
|
|
558
|
+
'The starting point above is a hint, not a boundary. Report what the change ACTUALLY touches.',
|
|
559
|
+
'',
|
|
560
|
+
repos,
|
|
561
|
+
templateBlock ? `\n${templateBlock}` : '',
|
|
562
|
+
conversation ? `\n## Conversation on the demand\n${conversation}` : '',
|
|
563
|
+
].join('\n'),
|
|
564
|
+
}],
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
// Tool: Submit a demand analysis
|
|
570
|
+
server.tool(
|
|
571
|
+
'submit_demand_analysis',
|
|
572
|
+
'Push a finished demand technical analysis back to Konductro: what you found, which repositories the change actually touches, and the phase breakdown you propose. The breakdown is a PROPOSAL — the demand forum decides at IT Eval.',
|
|
573
|
+
{
|
|
574
|
+
demandId: z.string().describe('The demand key (e.g. DEM-4) or its UUID'),
|
|
575
|
+
summary: z.string().describe('What the analysis found, in markdown. What is already there, what has to change, the risks, and the assumptions the estimate rests on.'),
|
|
576
|
+
// BOTH OF THESE ARE CLEARED WHEN OMITTED, server-side and deliberately: a
|
|
577
|
+
// resubmission that dropped them used to keep the OLD proposal while the repos
|
|
578
|
+
// beside it reset to empty, leaving an analysis half from one run and half from
|
|
579
|
+
// another — which IT Eval then prefills from. So a second submission has to send
|
|
580
|
+
// all three fields or lose the two it leaves out.
|
|
581
|
+
foundRepos: z.array(z.string()).optional().describe('The repositories the change actually touches, by name. Not the ones you were told to start with. Resubmitting without this CLEARS it.'),
|
|
582
|
+
proposedPhases: z.array(z.object({
|
|
583
|
+
name: z.string().describe('What this phase delivers'),
|
|
584
|
+
projectId: z.string().uuid().optional().describe('The project it lands in, from the context. Omit if it genuinely belongs nowhere yet.'),
|
|
585
|
+
points: z.number().int().min(0).max(1000).describe('Story points for this phase alone. Never send a total; Konductro derives it.'),
|
|
586
|
+
why: z.string().optional().describe('Why it is its own phase rather than part of another'),
|
|
587
|
+
})).optional().describe('The phases you propose. A phase belongs to ONE project; work spanning two projects is two phases. Resubmitting without this CLEARS it.'),
|
|
588
|
+
},
|
|
589
|
+
async ({ demandId, summary, foundRepos, proposedPhases }) => {
|
|
590
|
+
if (!KONDUCTRO_URL || !CLI_TOKEN) {
|
|
591
|
+
return {
|
|
592
|
+
content: [{
|
|
593
|
+
type: 'text',
|
|
594
|
+
text: 'Konductro is not configured. Please set your Konductro URL and CLI token in the plugin settings.',
|
|
595
|
+
}],
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const result = await konductroFetch(`/api/cli/demand-analyses/${demandId}/submit`, {
|
|
600
|
+
method: 'POST',
|
|
601
|
+
body: JSON.stringify({ summary, foundRepos, proposedPhases }),
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
content: [{
|
|
606
|
+
type: 'text',
|
|
607
|
+
text: `## Demand analysis submitted\n\n${result.message}\n\n- Demand: ${result.key}\n- Submitted: ${result.submittedAt}\n\nThe demand forum has been notified and can now record the IT Eval.`,
|
|
608
|
+
}],
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
);
|
|
612
|
+
|
|
183
613
|
// Tool: List pending decomposition tasks
|
|
184
614
|
server.tool(
|
|
185
615
|
'list_decomposition_tasks',
|
|
@@ -505,6 +935,12 @@ server.tool(
|
|
|
505
935
|
text += `### Context Pack\n\n${ctx.ticket.contextPack}\n\n`;
|
|
506
936
|
}
|
|
507
937
|
|
|
938
|
+
// Developer Notes — present when context is fetched on a story itself. For a
|
|
939
|
+
// task, the notes that matter are the parent story's, rendered below.
|
|
940
|
+
if (ctx.ticket.developerNotes) {
|
|
941
|
+
text += `### Developer Notes\n\n${ctx.ticket.developerNotes}\n\n`;
|
|
942
|
+
}
|
|
943
|
+
|
|
508
944
|
// Parent Story
|
|
509
945
|
if (ctx.parent) {
|
|
510
946
|
text += `### Parent Story: ${ctx.parent.ticketKey} — ${ctx.parent.title}\n\n`;
|
|
@@ -517,6 +953,10 @@ server.tool(
|
|
|
517
953
|
if (ctx.parent.contextPack) {
|
|
518
954
|
text += `**Story Context Pack:**\n${ctx.parent.contextPack}\n\n`;
|
|
519
955
|
}
|
|
956
|
+
// Story-level context accumulated by whoever worked the story's other tasks.
|
|
957
|
+
if (ctx.parent.developerNotes) {
|
|
958
|
+
text += `**Parent Story, Developer Notes:**\n${ctx.parent.developerNotes}\n\n`;
|
|
959
|
+
}
|
|
520
960
|
}
|
|
521
961
|
|
|
522
962
|
// Children tasks
|
|
@@ -562,6 +1002,153 @@ server.tool(
|
|
|
562
1002
|
}
|
|
563
1003
|
);
|
|
564
1004
|
|
|
1005
|
+
// Tool: Append a developer note to a story (KON-314)
|
|
1006
|
+
server.tool(
|
|
1007
|
+
'append_story_developer_note',
|
|
1008
|
+
"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.",
|
|
1009
|
+
{
|
|
1010
|
+
storyId: z.string().describe('The story to append to — key (e.g. KON-283) or UUID. Not a task.'),
|
|
1011
|
+
note: z.string().describe('The new note only, as markdown. No heading, timestamp or separator.'),
|
|
1012
|
+
taskKey: z.string().optional().describe("The key of the task this note came from (e.g. KON-314), when there is one"),
|
|
1013
|
+
},
|
|
1014
|
+
async ({ storyId, note, taskKey }) => {
|
|
1015
|
+
const result = await konductroFetch(`/api/cli/tickets/${storyId}/developer-notes`, {
|
|
1016
|
+
method: 'POST',
|
|
1017
|
+
body: JSON.stringify({ note, taskKey }),
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
// Not echoing result.developerNotes: it is the whole accumulated blob, and
|
|
1021
|
+
// repeating it here invites a later call to send it back as a "new" note.
|
|
1022
|
+
return {
|
|
1023
|
+
content: [{
|
|
1024
|
+
type: 'text',
|
|
1025
|
+
text: `Note appended to ${result.ticketKey}'s developer notes.`,
|
|
1026
|
+
}],
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
);
|
|
1030
|
+
|
|
1031
|
+
// Tool: Create a work item
|
|
1032
|
+
server.tool(
|
|
1033
|
+
'create_work_item',
|
|
1034
|
+
"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.",
|
|
1035
|
+
{
|
|
1036
|
+
repoUrl: z.string().describe('The git remote URL of this repository (run: git remote get-url origin)'),
|
|
1037
|
+
kind: z.enum(['story', 'task', 'bug']).describe('What to create'),
|
|
1038
|
+
title: z.string().describe('A short title, as a person would write it'),
|
|
1039
|
+
description: z.string().describe('What this is and why, in markdown'),
|
|
1040
|
+
acceptanceCriteria: z.array(z.string()).optional().describe('Observable behaviour, one per item. Omit for a bug unless you have them.'),
|
|
1041
|
+
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.'),
|
|
1042
|
+
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.'),
|
|
1043
|
+
},
|
|
1044
|
+
async ({ repoUrl, kind, title, description, acceptanceCriteria, parentKey, repositoryId }) => workItemCall(async () => {
|
|
1045
|
+
// The backend's body schema DECLARES parentKey, so .strict() does not reject it — it
|
|
1046
|
+
// is accepted and then dropped for anything that is not a task, because the route
|
|
1047
|
+
// reads it only inside the task branch and calls createBug / createStory without it
|
|
1048
|
+
// (routes/cli.ts). The item lands top-level and the 201 reports success, so asking
|
|
1049
|
+
// for it under a story and being told it worked is exactly what happens. Refuse
|
|
1050
|
+
// instead: a refusal is recoverable in the next turn, a misfiled item is found later
|
|
1051
|
+
// by someone else, if at all.
|
|
1052
|
+
//
|
|
1053
|
+
// The two cases are dropped by the same code path but for different reasons, so the
|
|
1054
|
+
// remedy differs and the message branches. A bug COULD have a parent — QA files bugs
|
|
1055
|
+
// against the tested story that way — this endpoint just does not wire it through. A
|
|
1056
|
+
// story could not: the hierarchy is story → task, so nothing sits above a story ever,
|
|
1057
|
+
// and an agent sending one almost certainly meant to create a task.
|
|
1058
|
+
if (kind !== 'task' && parentKey) {
|
|
1059
|
+
const why = kind === 'bug'
|
|
1060
|
+
? `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.`
|
|
1061
|
+
: `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.`;
|
|
1062
|
+
return `Refused: only a task can be created under a parent, and NOTHING WAS CREATED. ${why}`;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const repo = await projectFromRepo(repoUrl);
|
|
1066
|
+
|
|
1067
|
+
// Creating a task was a dead end without this. The backend requires repositoryId as a
|
|
1068
|
+
// UUID, and nothing a developer can reach hands one out — get_decomposition_context
|
|
1069
|
+
// is the only tool that returns one, and that needs an assigned decomposition task
|
|
1070
|
+
// you do not have. So the agent was asked for a value it could not obtain, and a
|
|
1071
|
+
// guess came back as a bare "Validation failed".
|
|
1072
|
+
//
|
|
1073
|
+
// It was in hand the whole time: repo-by-url returns the repository row (id, name,
|
|
1074
|
+
// repoUrl, repoType, stacks, projectId, project) and the id was being discarded.
|
|
1075
|
+
// Defaulting to it is also the right answer rather than merely an available one — the
|
|
1076
|
+
// task is being written from inside the repo it will be built in.
|
|
1077
|
+
//
|
|
1078
|
+
// Tasks only. createStory ignores it, and defaulting it on a bug would silently start
|
|
1079
|
+
// attributing bugs to a repository they were not attributed to before.
|
|
1080
|
+
const resolvedRepositoryId = kind === 'task' ? (repositoryId ?? repo.id) : repositoryId;
|
|
1081
|
+
|
|
1082
|
+
const created = await konductroFetch(`/api/cli/projects/${repo.projectId}/work-items`, {
|
|
1083
|
+
method: 'POST',
|
|
1084
|
+
body: JSON.stringify({ kind, title, description, acceptanceCriteria, parentKey, repositoryId: resolvedRepositoryId }),
|
|
1085
|
+
});
|
|
1086
|
+
// Returning the id rather than the number alone. A key is PREFIX-NUMBER, the prefix is
|
|
1087
|
+
// nowhere in this tool's reach (repo-by-url does not return ticketPrefix), so printing
|
|
1088
|
+
// the number and pointing at "its key" invited the agent to guess one. resolveTicketId
|
|
1089
|
+
// matches prefix+number across the whole TENANT, not within the project, so a guessed
|
|
1090
|
+
// prefix does not 404 — it resolves to another project's ticket of the same number,
|
|
1091
|
+
// and the next update_work_item edits that one instead. The id closes it off: the UUID
|
|
1092
|
+
// branch of resolveTicketId short-circuits with no lookup at all.
|
|
1093
|
+
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.`;
|
|
1094
|
+
})
|
|
1095
|
+
);
|
|
1096
|
+
|
|
1097
|
+
// Tool: Update a work item
|
|
1098
|
+
server.tool(
|
|
1099
|
+
'update_work_item',
|
|
1100
|
+
"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.",
|
|
1101
|
+
{
|
|
1102
|
+
key: z.string().describe('The work item key (e.g. KON-451) or UUID'),
|
|
1103
|
+
title: z.string().optional().describe('The new title'),
|
|
1104
|
+
description: z.string().optional().describe('The new description, in markdown'),
|
|
1105
|
+
acceptanceCriteria: z.array(z.string()).optional().describe('The FULL new list, not just the additions — it replaces what is there'),
|
|
1106
|
+
},
|
|
1107
|
+
async ({ key, title, description, acceptanceCriteria }) => workItemCall(async () => {
|
|
1108
|
+
const body = {};
|
|
1109
|
+
if (title !== undefined) body.title = title;
|
|
1110
|
+
if (description !== undefined) body.description = description;
|
|
1111
|
+
if (acceptanceCriteria !== undefined) body.acceptanceCriteria = acceptanceCriteria;
|
|
1112
|
+
if (Object.keys(body).length === 0) {
|
|
1113
|
+
return 'Nothing to change — pass at least one of title, description or acceptanceCriteria.';
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const result = await konductroFetch(`/api/cli/tickets/${encodeURIComponent(key)}/work-item`, {
|
|
1117
|
+
method: 'PATCH',
|
|
1118
|
+
body: JSON.stringify(body),
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
// Deliberately NOT echoing the updated values back. Returning the item invites the
|
|
1122
|
+
// next call to send them again as "new" values, re-submitting fields nobody changed
|
|
1123
|
+
// and raising a review for a no-op. Same reason append_story_developer_note does not
|
|
1124
|
+
// return the notes blob.
|
|
1125
|
+
// `changed` echoes the fields that were WRITTEN, not the ones that turned out to
|
|
1126
|
+
// differ — the server returns Object.keys of what it applied (routes/cli.ts). So
|
|
1127
|
+
// re-sending a title with its existing value still reports "changed: title". Do not
|
|
1128
|
+
// read it as a diff.
|
|
1129
|
+
const changed = (result.changed ?? Object.keys(body)).join(', ');
|
|
1130
|
+
let text = `Updated ${key} (#${result.ticketNumber}) — changed: ${changed}.`;
|
|
1131
|
+
if (result.awaitingReview) {
|
|
1132
|
+
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.';
|
|
1133
|
+
}
|
|
1134
|
+
return text;
|
|
1135
|
+
})
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
// Tool: Delete a work item
|
|
1139
|
+
server.tool(
|
|
1140
|
+
'delete_work_item',
|
|
1141
|
+
"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.",
|
|
1142
|
+
{
|
|
1143
|
+
key: z.string().describe('The task or bug key (e.g. KON-452) or UUID'),
|
|
1144
|
+
},
|
|
1145
|
+
async ({ key }) => workItemCall(async () => {
|
|
1146
|
+
const result = await konductroFetch(`/api/cli/tickets/${encodeURIComponent(key)}/work-item`, { method: 'DELETE' });
|
|
1147
|
+
const extra = result.deleted > 1 ? ` (${result.deleted} rows, including its linked records)` : '';
|
|
1148
|
+
return `Deleted ${key}${extra}. This cannot be undone.`;
|
|
1149
|
+
})
|
|
1150
|
+
);
|
|
1151
|
+
|
|
565
1152
|
// Tool: Start work on a ticket
|
|
566
1153
|
server.tool(
|
|
567
1154
|
'start_work',
|
|
@@ -958,7 +1545,13 @@ server.tool(
|
|
|
958
1545
|
|
|
959
1546
|
return { content: [{ type: 'text', text }] };
|
|
960
1547
|
} catch (err) {
|
|
961
|
-
|
|
1548
|
+
// KON-520: this used to sniff err.message for 'BUG_NOT_FOUND' / '404', which worked
|
|
1549
|
+
// only because the message was the whole raw envelope. konductroFetch now sets
|
|
1550
|
+
// message to the backend's `error` sentence, so that match silently stopped firing
|
|
1551
|
+
// and a missing bug reported as a generic failure. Read the code and the status
|
|
1552
|
+
// instead — both are on the error and neither depends on wording.
|
|
1553
|
+
const notABug = err.code === 'BUG_NOT_FOUND' || err.status === 404;
|
|
1554
|
+
const msg = notABug
|
|
962
1555
|
? `Ticket ${ticketId} not found or is not a bug.`
|
|
963
1556
|
: `Failed to load bug context: ${err.message}`;
|
|
964
1557
|
return { content: [{ type: 'text', text: msg }], isError: true };
|
|
@@ -994,9 +1587,11 @@ server.tool(
|
|
|
994
1587
|
: 'No new enrichment applied (fields may already exist on the ticket).';
|
|
995
1588
|
return { content: [{ type: 'text', text: applied }] };
|
|
996
1589
|
} catch (err) {
|
|
997
|
-
|
|
1590
|
+
// Same fix as get_bug_for_enrichment above — match on err.code / err.status, not
|
|
1591
|
+
// on message text, which konductroFetch no longer formats the way this expected.
|
|
1592
|
+
const msg = (err.code === 'BUG_NOT_FOUND' || err.status === 404)
|
|
998
1593
|
? `Ticket ${ticketId} not found or is not a bug.`
|
|
999
|
-
: err.
|
|
1594
|
+
: err.code === 'INVALID_ENRICHMENT'
|
|
1000
1595
|
? 'At least one enrichment field must be provided.'
|
|
1001
1596
|
: `Failed to submit enrichment: ${err.message}`;
|
|
1002
1597
|
return { content: [{ type: 'text', text: msg }], isError: true };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: USE WHEN an architect has been assigned a Konductro DEMAND technical analysis — a business request that has passed the demand forum and needs sizing against the real code before the forum will commit to it
|
|
3
|
+
globs:
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Konductro Demand Analysis
|
|
8
|
+
|
|
9
|
+
A demand is a business request that sits ABOVE projects. It has passed the demand forum,
|
|
10
|
+
and the forum will not commit to it until somebody has read the actual code and said what
|
|
11
|
+
it entails. That is this job.
|
|
12
|
+
|
|
13
|
+
## What makes this different from a phase's technical analysis
|
|
14
|
+
|
|
15
|
+
A phase already belongs to a project, so its repositories are known. **A demand does not.**
|
|
16
|
+
Which projects the work lands in is decided at IT Eval, FROM WHAT YOU FIND. So:
|
|
17
|
+
|
|
18
|
+
- The context lists **every repository in the workspace**, not a project's few.
|
|
19
|
+
- The "start with" list is a hint from whoever assigned it. It is not a boundary, and it
|
|
20
|
+
is regularly wrong: the work turns out to be in a repository nobody expected.
|
|
21
|
+
- You report which repositories the change **actually touches**. That is what the forum
|
|
22
|
+
reads, and it is stored separately from the hint you were given.
|
|
23
|
+
|
|
24
|
+
## Workflow
|
|
25
|
+
|
|
26
|
+
### Step 1: See what is assigned
|
|
27
|
+
|
|
28
|
+
Call `list_demand_analyses`. Present the list and ask which one to work on. Each shows
|
|
29
|
+
what the forum asked to have answered and when it is next sitting — that date is the
|
|
30
|
+
deadline, so say it out loud.
|
|
31
|
+
|
|
32
|
+
### Step 2: Load the context
|
|
33
|
+
|
|
34
|
+
Call `get_demand_analysis_context` with the demand key. Read the whole thing before
|
|
35
|
+
touching any code. It carries:
|
|
36
|
+
|
|
37
|
+
- The pitch, in the business's own words, and what happens today
|
|
38
|
+
- The value case: who it affects, and what it costs to not have it
|
|
39
|
+
- **The form they actually filled in**, which is the workspace's own template rather
|
|
40
|
+
than a fixed set of questions. A workspace that asks "Which regulator requires this?"
|
|
41
|
+
has that question and its answer here and nowhere else, and it is usually the one that
|
|
42
|
+
changes the estimate. Read it.
|
|
43
|
+
- What the forum specifically asked you to answer
|
|
44
|
+
- Every repository in the workspace, with its project
|
|
45
|
+
|
|
46
|
+
**Say the value case back to the user in one line.** An estimate has to be proportionate
|
|
47
|
+
to the thing being bought, and this is the only place that number appears.
|
|
48
|
+
|
|
49
|
+
### Step 3: Read the code
|
|
50
|
+
|
|
51
|
+
The repositories are on this machine. Use Glob, Grep and Read. Konductro never clones
|
|
52
|
+
anything and neither do you: if a repository named in the context is not present locally,
|
|
53
|
+
say so rather than guessing at it.
|
|
54
|
+
|
|
55
|
+
Start with the repositories you were pointed at, then follow the work wherever it goes.
|
|
56
|
+
|
|
57
|
+
What you are trying to establish, in this order:
|
|
58
|
+
|
|
59
|
+
1. **What already exists.** Half of every demand turns out to be built. Say what is there
|
|
60
|
+
before you say what is needed.
|
|
61
|
+
2. **What actually has to change**, per repository, concretely enough that somebody could
|
|
62
|
+
argue with it.
|
|
63
|
+
3. **What is in the way.** Missing data, an integration nobody has written, a migration
|
|
64
|
+
with no way back.
|
|
65
|
+
4. **What the estimate rests on.** Every assumption you make is a way the number could be
|
|
66
|
+
wrong, and the forum is told which one when it is.
|
|
67
|
+
|
|
68
|
+
### Step 4: Propose the phases
|
|
69
|
+
|
|
70
|
+
This is the part the forum needs most, and the part it cannot do itself.
|
|
71
|
+
|
|
72
|
+
- **A phase belongs to ONE project.** Work spanning two projects is two phases. This is
|
|
73
|
+
not a formality: each phase becomes a real Phase in that project when the demand is
|
|
74
|
+
green lit.
|
|
75
|
+
- **Order them by what has to land first.** If phase two cannot start until phase one is
|
|
76
|
+
deployed, say so in its `why`.
|
|
77
|
+
- **Points per phase, never a total.** Konductro derives the total. Sending your own is
|
|
78
|
+
how the headline number and the breakdown behind it end up disagreeing.
|
|
79
|
+
- **A phase with no obvious home** is allowed: omit `projectId` and say why in the `why`.
|
|
80
|
+
A visible gap beats a phase filed in whichever project came first.
|
|
81
|
+
|
|
82
|
+
### Step 5: Submit
|
|
83
|
+
|
|
84
|
+
Call `submit_demand_analysis` with:
|
|
85
|
+
|
|
86
|
+
- `summary` — what you found. Structure it as: what exists, what has to change, risks,
|
|
87
|
+
assumptions. Markdown.
|
|
88
|
+
- `foundRepos` — the repositories the change actually touches. Not the hint you were given.
|
|
89
|
+
- `proposedPhases` — the breakdown above.
|
|
90
|
+
|
|
91
|
+
The forum is notified and records the IT Eval from your proposal. They can change any of
|
|
92
|
+
it; you are advising, not deciding.
|
|
93
|
+
|
|
94
|
+
**If you submit a second time, send all three again.** `foundRepos` and `proposedPhases`
|
|
95
|
+
are cleared when they are left out, deliberately: a resubmission that kept the old
|
|
96
|
+
proposal beside freshly reset repositories would be half one run and half another, and
|
|
97
|
+
IT Eval prefills from it.
|
|
98
|
+
|
|
99
|
+
## The bar
|
|
100
|
+
|
|
101
|
+
**Never estimate something you have not read.** An unfamiliar name is a reason to go and
|
|
102
|
+
look, never a reason to assume it is small. If a repository you need is not on this
|
|
103
|
+
machine, the honest output is a summary that says which one and what it blocks — not a
|
|
104
|
+
number with a shrug behind it.
|
|
105
|
+
|
|
106
|
+
**Say what you did not check.** The forum is about to commit money against this. A gap you
|
|
107
|
+
name costs an hour; a gap you paper over costs the quarter.
|
|
@@ -20,11 +20,20 @@ Call `get_task_context` with the phase ID. This loads:
|
|
|
20
20
|
- The approved requirements document (what needs to be built)
|
|
21
21
|
- Project and client context
|
|
22
22
|
- Repository information
|
|
23
|
+
- A "Refresh Requested" section, when someone has asked for an existing analysis
|
|
24
|
+
to be redone. Read it first: it carries the requester's reason, and the job is
|
|
25
|
+
to revise the previous analysis rather than start from scratch.
|
|
23
26
|
|
|
24
27
|
Share a summary of the requirements with the user and confirm you're ready to begin.
|
|
25
28
|
|
|
26
29
|
### Step 3: Explore the Codebase
|
|
27
30
|
|
|
31
|
+
**If the context carried a "Required Document Structure", that is your brief.** Every section in it is something you must be able to answer by the end of this step, and the ones marked "must cover" are not optional. Explore with those sections in mind rather than working through the generic list below and hoping it covers them — a workspace that asks for a Regulatory Impact section is telling you what to go and find out, and nothing in the generic list will lead you there.
|
|
32
|
+
|
|
33
|
+
Where the structure asks for something the generic list does not cover, go and look for it. Where it omits something, you do not need to chase it.
|
|
34
|
+
|
|
35
|
+
If no structure was supplied, use the list below as-is.
|
|
36
|
+
|
|
28
37
|
Analyse the local codebase using file reading and search tools:
|
|
29
38
|
- **Project structure** — folder layout, module organisation
|
|
30
39
|
- **Tech stack** — frameworks, versions, build tools
|
|
@@ -40,6 +49,10 @@ Discuss findings with the user as you go. Ask clarifying questions.
|
|
|
40
49
|
|
|
41
50
|
### Step 4: Impact Analysis
|
|
42
51
|
|
|
52
|
+
**Before moving on, check yourself against the required structure.** Take each section, and each "must cover" section especially, and ask whether you could write it now from what you have found. Where you could not, that is a question for the user or another pass over the codebase — ask it now rather than discovering the gap while writing the document, when the honest options are guessing or going back.
|
|
53
|
+
|
|
54
|
+
This is the step that makes the structure mean something. Producing the right headings over content that does not answer them is worse than the default document, because it looks like compliance.
|
|
55
|
+
|
|
43
56
|
Based on the requirements and your codebase exploration, assess:
|
|
44
57
|
- **What needs to change** — which files, modules, or services
|
|
45
58
|
- **What needs to be created** — new components, services, APIs
|
|
@@ -49,7 +62,12 @@ Based on the requirements and your codebase exploration, assess:
|
|
|
49
62
|
|
|
50
63
|
### Step 5: Produce the Document
|
|
51
64
|
|
|
52
|
-
|
|
65
|
+
**If the context carried a "Required Document Structure", follow it exactly** — those sections, in that order, under those headings. It replaces the default list below rather than supplementing it: do not add the default sections alongside it, and do not reorder it to something you find more natural. Where a section carries standing content, reproduce that text word for word; it is usually a clause the workspace is required to publish, and paraphrasing it defeats the point of it being there. Where a section asks for a diagram, draw one, as Mermaid, inside that section.
|
|
66
|
+
|
|
67
|
+
A document produced here must be indistinguishable from one produced for the same workspace in Konductro itself. That is the whole purpose of the structure being sent.
|
|
68
|
+
|
|
69
|
+
**If no structure was supplied**, use this default list:
|
|
70
|
+
|
|
53
71
|
1. **Codebase Overview** — what exists today
|
|
54
72
|
2. **Technical Stack** — frameworks, versions, build tools, deployment
|
|
55
73
|
3. **Architecture Patterns** — structure, key design decisions
|
|
@@ -72,3 +90,4 @@ Ask the user to review the document. When approved, call `submit_tech_analysis`
|
|
|
72
90
|
- Be thorough but focused — analyse what's relevant to the requirements
|
|
73
91
|
- Ask the user questions — they know the codebase context you don't
|
|
74
92
|
- Produce a complete, standalone document an architect can use without additional context
|
|
93
|
+
- When a required document structure is supplied, it wins over anything in this file. It is the workspace's own standard, and your job is to meet it, not to improve on it
|