@konductro/claude-plugin 2.3.0-rc.1 → 2.4.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
@@ -122,7 +122,7 @@ These are answers, not errors — none of them is worth retrying as-is.
122
122
  | Tool | What it does |
123
123
  |---|---|
124
124
  | `list_tech_analysis_tasks` | List your pending technical analysis assignments |
125
- | `get_task_context` | Load requirements and project context for a tech analysis task |
125
+ | `get_task_context` | Load requirements and project context for a tech analysis task, plus the requester's context message when a refresh was asked for |
126
126
  | `submit_tech_analysis` | Submit your analysis document back to Konductro |
127
127
 
128
128
  ### Story decomposition
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konductro/claude-plugin",
3
- "version": "2.3.0-rc.1",
3
+ "version": "2.4.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",
@@ -94,6 +94,14 @@ function refusalText(err) {
94
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
95
  case 'NOT_PROJECT_MEMBER':
96
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
+ // ── demand analysis (KON-606) ──
98
+ case 'NOT_YOUR_DEMAND':
99
+ return 'Refused: the technical analysis for this demand is assigned to somebody else. Only they, or the chair of the demand forum, can submit it. Ask the chair to reassign it if it should be yours.';
100
+ case 'WRONG_STAGE':
101
+ // Two different situations behind one code, and the backend says which in its
102
+ // message: the demand has moved past `pitched`, or nobody has been assigned the
103
+ // analysis yet. Neither is worth retrying, so pass its sentence through.
104
+ return null;
97
105
  default:
98
106
  return null;
99
107
  }
@@ -217,7 +225,7 @@ server.tool(
217
225
  }
218
226
 
219
227
  const taskList = tasks.map((t, i) =>
220
- `${i + 1}. **${t.projectName}** — ${t.phaseName}\n Client: ${t.clientName}\n Phase ID: ${t.phaseId}\n ${t.hasExistingDraft ? '(has existing draft)' : '(new analysis)'}`
228
+ `${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)'}`
221
229
  ).join('\n\n');
222
230
 
223
231
  return {
@@ -247,6 +255,25 @@ server.tool(
247
255
  if (context.phase.description) contextText += `**Description:** ${context.phase.description}\n`;
248
256
  contextText += `\n`;
249
257
 
258
+ // KON-318: a refresh was requested, so this is a revision of an approved
259
+ // analysis rather than a first pass. Sits above the repositories and the
260
+ // previous document deliberately — it changes what is being asked for, so
261
+ // it has to be read before the material it applies to. Absent on a normal
262
+ // task, in which case the output is exactly what it was before.
263
+ if (context.refreshContext) {
264
+ const { requestedBy, message } = context.refreshContext;
265
+ contextText += `### Refresh Requested\n\n`;
266
+ contextText += `A refresh of this technical analysis was requested`;
267
+ if (requestedBy) contextText += ` by ${requestedBy}`;
268
+ contextText += `.\n\n`;
269
+ if (message) {
270
+ contextText += `**Context from the requester:**\n\n${message}\n\n`;
271
+ } else {
272
+ contextText += `No further context was given.\n\n`;
273
+ }
274
+ contextText += `Revise the existing analysis below rather than starting over.\n\n`;
275
+ }
276
+
250
277
  if (context.repositories.length > 0) {
251
278
  contextText += `### Repositories\n\n`;
252
279
  for (const repo of context.repositories) {
@@ -306,6 +333,177 @@ server.tool(
306
333
  }
307
334
  );
308
335
 
336
+ // ══ DEMAND ANALYSIS ════════════════════════════════════════════════════════
337
+ //
338
+ // The demand-level counterpart of the three tools above. A DEMAND has no project yet —
339
+ // which projects it lands in is decided at IT Eval, from what this analysis finds — so
340
+ // the context carries the whole workspace's repositories and the submission reports
341
+ // which of them the change actually touches.
342
+
343
+ // Tool: List pending demand analyses
344
+ server.tool(
345
+ 'list_demand_analyses',
346
+ '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.',
347
+ {},
348
+ // workItemCall, not a hand-rolled config check: it asserts the token, turns a 401 into
349
+ // a setup sentence, routes a refusal through refusalText keyed on CODE (KON-517), and
350
+ // FLAGS a genuine failure with isError so the agent stops rather than carrying on. The
351
+ // config check this replaces returned a plain success result, so an unconfigured plugin
352
+ // read to the agent as "no work assigned".
353
+ async () => workItemCall(async () => {
354
+ const { tasks } = await konductroFetch('/api/cli/demand-analyses');
355
+
356
+ if (!tasks || tasks.length === 0) {
357
+ return 'No demand analyses assigned to you.';
358
+ }
359
+
360
+ const lines = tasks.map((t) => {
361
+ const parts = [`### ${t.key} — ${t.title}`];
362
+ if (t.businessUnit) parts.push(`**Business unit:** ${t.businessUnit}`);
363
+ if (t.nextSitting) parts.push(`**Back at the forum:** ${new Date(t.nextSitting).toISOString().slice(0, 10)}`);
364
+ if (t.startWith && t.startWith.length) parts.push(`**Start with:** ${t.startWith.join(', ')}`);
365
+ if (t.askedFor) parts.push(`**They asked:** ${t.askedFor}`);
366
+ parts.push(`\`get_demand_analysis_context\` with demandId ${t.key} to load it.`);
367
+ return parts.join('\n');
368
+ });
369
+
370
+ return `## Demand analyses assigned to you (${tasks.length})\n\n${lines.join('\n\n')}`;
371
+ })
372
+ );
373
+
374
+ // Tool: Load one demand's analysis context
375
+ server.tool(
376
+ 'get_demand_analysis_context',
377
+ '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.',
378
+ {
379
+ demandId: z.string().describe('The demand key (e.g. DEM-4) or its UUID'),
380
+ },
381
+ async ({ demandId }) => workItemCall(async () => {
382
+ const ctx = await konductroFetch(`/api/cli/demand-analyses/${encodeURIComponent(demandId)}/context`);
383
+ const d = ctx && ctx.demand;
384
+ if (!d) {
385
+ return `No demand context came back for ${demandId}. Check the key, and that the analysis is assigned to you.`;
386
+ }
387
+
388
+ const tpl = ctx.template;
389
+
390
+ const cost = Object.entries(d.cost || {})
391
+ .filter(([, v]) => v)
392
+ .map(([k, v]) => `- **${k}:** ${v}`)
393
+ .join('\n') || '- Not given';
394
+
395
+ // THE PROJECT ID IS THE POINT OF THIS LIST, not decoration. `submit_demand_analysis`
396
+ // takes a `projectId` per proposed phase, and a phase without one creates no real
397
+ // Phase at green light. Rendering only the project NAME left the analyst with no way
398
+ // to supply it, so every phase would have landed homeless and the green light would
399
+ // have created nothing.
400
+ const repos = (ctx.repositories || [])
401
+ .map((r) => `- \`${r.name}\` (${r.projectName}${r.type ? `, ${r.type}` : ''}) — ${r.url}\n projectId: \`${r.projectId}\``)
402
+ .join('\n') || '- None linked to any project yet';
403
+
404
+ const conversation = (ctx.conversation || [])
405
+ .map((c) => `- **${c.by}**${c.kind === 'amendment' ? ' (amendment)' : ''}: ${c.body}`)
406
+ .join('\n');
407
+
408
+ return [
409
+ `# ${d.key} — ${d.title}`,
410
+ '',
411
+ `**Raised by:** ${d.raisedBy}${d.businessUnit ? ` (${d.businessUnit})` : ''}`,
412
+ '',
413
+ '## What they need',
414
+ d.need,
415
+ '',
416
+ '## What happens today',
417
+ d.currentState || 'Not given',
418
+ '',
419
+ '## Who it affects',
420
+ [d.affects, d.affectsWho].filter(Boolean).join(' — ') || 'Not given',
421
+ '',
422
+ '## What it costs to not have it',
423
+ cost,
424
+ '',
425
+ '## Tied to a date',
426
+ 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',
427
+ '',
428
+ '## How they would know it worked',
429
+ d.successLooksLike || 'Not given',
430
+ '',
431
+ // THE WORKSPACE'S OWN QUESTIONS. The endpoint returns the PINNED template —
432
+ // the one this demand was actually raised on, so a question a later template
433
+ // deleted still appears here. Dropping it meant a workspace that asks "Which
434
+ // regulator requires this?" had that answer invisible to the analyst, which is
435
+ // the entire point of the templating feature.
436
+ ...(tpl
437
+ ? [
438
+ '',
439
+ `## The form they filled in — ${tpl.name} v${tpl.version}`,
440
+ ...tpl.sections.flatMap((section) => [
441
+ '',
442
+ `### ${section.name}`,
443
+ ...(section.help ? [`_${section.help}_`] : []),
444
+ ...section.questions.map((q) =>
445
+ `- **${q.question}** ${q.answer ? `\n ${q.answer}` : '\n _Left blank._'}`),
446
+ ]),
447
+ ]
448
+ : []),
449
+ '',
450
+ '## What the forum asked you to answer',
451
+ ctx.askedFor || 'Nothing specific. Answer the pitch.',
452
+ '',
453
+ '## Where to start looking',
454
+ (ctx.startWith && ctx.startWith.length) ? ctx.startWith.map((r) => `\`${r}\``).join(', ') : 'Nothing named — use your judgement.',
455
+ '',
456
+ '## Every repository in this workspace',
457
+ 'The starting point above is a hint, not a boundary. Report what the change ACTUALLY touches.',
458
+ '',
459
+ repos,
460
+ conversation ? `\n## Conversation on the demand\n${conversation}` : '',
461
+ ].join('\n');
462
+ })
463
+ );
464
+
465
+ // Tool: Submit a demand analysis
466
+ server.tool(
467
+ 'submit_demand_analysis',
468
+ '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.',
469
+ {
470
+ demandId: z.string().describe('The demand key (e.g. DEM-4) or its UUID'),
471
+ 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.'),
472
+ foundRepos: z.array(z.string()).optional().describe('The repositories the change actually touches, by name. Not the ones you were told to start with.'),
473
+ proposedPhases: z.array(z.object({
474
+ name: z.string().describe('What this phase delivers'),
475
+ projectId: z.string().uuid().optional().describe('The project it lands in, from the context. Omit if it genuinely belongs nowhere yet.'),
476
+ points: z.number().int().min(0).max(1000).describe('Story points for this phase alone. Never send a total; Konductro derives it.'),
477
+ why: z.string().optional().describe('Why it is its own phase rather than part of another'),
478
+ })).optional().describe('The phases you propose. A phase belongs to ONE project; work spanning two projects is two phases.'),
479
+ },
480
+ async ({ demandId, summary, foundRepos, proposedPhases }) => workItemCall(async () => {
481
+ const result = await konductroFetch(`/api/cli/demand-analyses/${encodeURIComponent(demandId)}/submit`, {
482
+ method: 'POST',
483
+ body: JSON.stringify({ summary, foundRepos, proposedPhases }),
484
+ });
485
+
486
+ const homeless = (proposedPhases ?? []).filter((ph) => !ph.projectId).length;
487
+
488
+ return [
489
+ '## Demand analysis submitted',
490
+ '',
491
+ result.message,
492
+ '',
493
+ `- Demand: ${result.key}`,
494
+ `- Submitted: ${result.submittedAt}`,
495
+ // A phase with no project creates no real Phase at green light. That is allowed
496
+ // and is sometimes the honest answer, but it is worth saying out loud here rather
497
+ // than letting the forum discover it when nothing gets created.
498
+ ...(homeless
499
+ ? ['', `**${homeless} phase${homeless === 1 ? '' : 's'} named no project**, so ${homeless === 1 ? 'it' : 'they'} will not create real work at green light until the forum assigns one.`]
500
+ : []),
501
+ '',
502
+ 'The demand forum has been notified and can now record the IT Eval.',
503
+ ].join('\n');
504
+ })
505
+ );
506
+
309
507
  // Tool: List pending decomposition tasks
310
508
  server.tool(
311
509
  'list_decomposition_tasks',
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: demand-analysis
3
+ description: Size a Konductro DEMAND against the real code. Use when an architect has been assigned a demand technical analysis — a business request that has passed the demand forum and now needs to be broken into phases and estimated before the forum will commit to it.
4
+ allowed-tools: Read, Grep, Glob, Bash, mcp__konductro__list_demand_analyses, mcp__konductro__get_demand_analysis_context, mcp__konductro__submit_demand_analysis
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**, question by question, on the template the demand
40
+ was RAISED against — so a question this workspace added appears, and one a later
41
+ template deleted still appears on the demands that answered it
42
+ - What the forum specifically asked you to answer
43
+ - Every repository in the workspace, with its project and that project's `projectId`
44
+
45
+ **Say the value case back to the user in one line.** An estimate has to be proportionate
46
+ to the thing being bought, and this is the only place that number appears.
47
+
48
+ ### Step 3: Read the code
49
+
50
+ The repositories are on this machine. Use Glob, Grep and Read. Konductro never clones
51
+ anything and neither do you: if a repository named in the context is not present locally,
52
+ say so rather than guessing at it.
53
+
54
+ Start with the repositories you were pointed at, then follow the work wherever it goes.
55
+
56
+ What you are trying to establish, in this order:
57
+
58
+ 1. **What already exists.** Half of every demand turns out to be built. Say what is there
59
+ before you say what is needed.
60
+ 2. **What actually has to change**, per repository, concretely enough that somebody could
61
+ argue with it.
62
+ 3. **What is in the way.** Missing data, an integration nobody has written, a migration
63
+ with no way back.
64
+ 4. **What the estimate rests on.** Every assumption you make is a way the number could be
65
+ wrong, and the forum is told which one when it is.
66
+
67
+ ### Step 4: Propose the phases
68
+
69
+ This is the part the forum needs most, and the part it cannot do itself.
70
+
71
+ - **A phase belongs to ONE project.** Work spanning two projects is two phases. This is
72
+ not a formality: each phase becomes a real Phase in that project when the demand is
73
+ green lit.
74
+ - **Take `projectId` from the repository list in the context**, where it is printed under
75
+ each repository. A phase without one creates no real Phase at green light, so guessing
76
+ is worse than omitting.
77
+ - **Order them by what has to land first.** If phase two cannot start until phase one is
78
+ deployed, say so in its `why`.
79
+ - **Points per phase, never a total.** Konductro derives the total. Sending your own is
80
+ how the headline number and the breakdown behind it end up disagreeing.
81
+ - **A phase with no obvious home** is allowed: omit `projectId` and say why in the `why`.
82
+ A visible gap beats a phase filed in whichever project came first.
83
+
84
+ ### Step 5: Submit
85
+
86
+ Call `submit_demand_analysis` with:
87
+
88
+ - `summary` — what you found. Structure it as: what exists, what has to change, risks,
89
+ assumptions. Markdown.
90
+ - `foundRepos` — the repositories the change actually touches. Not the hint you were given.
91
+ - `proposedPhases` — the breakdown above.
92
+
93
+ The forum is notified and records the IT Eval from your proposal. They can change any of
94
+ it; you are advising, not deciding.
95
+
96
+ ## The bar
97
+
98
+ **Never estimate something you have not read.** An unfamiliar name is a reason to go and
99
+ look, never a reason to assume it is small. If a repository you need is not on this
100
+ machine, the honest output is a summary that says which one and what it blocks — not a
101
+ number with a shrug behind it.
102
+
103
+ **Say what you did not check.** The forum is about to commit money against this. A gap you
104
+ name costs an hour; a gap you paper over costs the quarter.
@@ -20,6 +20,9 @@ Once the user picks a task, call `get_task_context` with the phase ID. This load
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