@we-scrum/cli 6.8.3 → 6.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@we-scrum/cli",
3
- "version": "6.8.3",
3
+ "version": "6.9.3",
4
4
  "description": "Cli tool for we-scrum application",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
@@ -38,8 +38,8 @@
38
38
  "tsup": "^8.5.1",
39
39
  "typescript": "^6.0.3",
40
40
  "@my-devkit/cli": "2.1.0",
41
- "@we-scrum/commands": "1.0.0",
42
41
  "@we-scrum/enums": "1.0.0",
42
+ "@we-scrum/commands": "1.0.0",
43
43
  "@we-scrum/models": "1.0.0",
44
44
  "@my-devkit/core": "1.0.0",
45
45
  "@we-scrum/utils": "1.0.0"
package/src/cli.ts CHANGED
@@ -4,6 +4,7 @@ import { Command } from 'commander';
4
4
  import { version } from '../package.json';
5
5
  import {
6
6
  registerAuthCommands,
7
+ registerChangeThreadCommands,
7
8
  registerIterationCommands,
8
9
  registerPolicyCommands,
9
10
  registerProjectCommands,
@@ -23,5 +24,6 @@ registerStoryCommands(program);
23
24
  registerTaskCommands(program);
24
25
  registerPolicyCommands(program);
25
26
  registerIterationCommands(program);
27
+ registerChangeThreadCommands(program);
26
28
 
27
29
  program.parse(process.argv);
@@ -0,0 +1,53 @@
1
+ import { WeScrumHelper } from '@helpers';
2
+ import { Logger } from '@my-devkit/core';
3
+ import { runProjectCommand } from '@utils';
4
+ import { ChangeThreadStatus } from '@we-scrum/enums';
5
+ import { Command, Option } from 'commander';
6
+
7
+ export function registerChangeThreadCommands(program: Command): void {
8
+ program
9
+ .command('get-change-threads')
10
+ .description('Get the open change threads for a change')
11
+ .requiredOption('-c, --changeId <id>', 'Unique identifier of the change')
12
+ .action(
13
+ runProjectCommand<{ changeId: string }>(async (options) => {
14
+ return WeScrumHelper.getOpenChangeThreads(options.changeId);
15
+ }),
16
+ );
17
+
18
+ program
19
+ .command('create-change-thread')
20
+ .description('Open a new change thread with a first comment')
21
+ .requiredOption('-c, --changeId <id>', 'Unique identifier of the change')
22
+ .requiredOption('-m, --comment <comment>', 'Comment body (markdown supported)')
23
+ .action(
24
+ runProjectCommand<{ changeId: string; comment: string }>(async (options) => {
25
+ const changeThreadId = await WeScrumHelper.createChangeThread(options.changeId, options.comment);
26
+ Logger.info(`Change thread "${changeThreadId}" created successfully.`);
27
+ }),
28
+ );
29
+
30
+ program
31
+ .command('update-change-thread-status')
32
+ .description('Update the status of a change thread')
33
+ .requiredOption('-t, --changeThreadId <id>', 'Unique identifier of the change thread')
34
+ .addOption(new Option('-s, --status <status>', 'New status').choices(ChangeThreadStatus.helper.members).makeOptionMandatory())
35
+ .action(
36
+ runProjectCommand<{ changeThreadId: string; status: ChangeThreadStatus }>(async (options) => {
37
+ await WeScrumHelper.updateChangeThreadStatus(options.changeThreadId, options.status);
38
+ Logger.info('Change thread status updated successfully.');
39
+ }),
40
+ );
41
+
42
+ program
43
+ .command('add-comment-to-change-thread')
44
+ .description('Reply to an existing change thread')
45
+ .requiredOption('-t, --changeThreadId <id>', 'Unique identifier of the change thread')
46
+ .requiredOption('-m, --comment <comment>', 'Comment body (markdown supported)')
47
+ .action(
48
+ runProjectCommand<{ changeThreadId: string; comment: string }>(async (options) => {
49
+ await WeScrumHelper.addCommentToChangeThread(options.changeThreadId, options.comment);
50
+ Logger.info('Comment added successfully.');
51
+ }),
52
+ );
53
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './auth';
2
+ export * from './change-thread';
2
3
  export * from './iteration';
3
4
  export * from './policy';
4
5
  export * from './project';
@@ -32,4 +32,14 @@ export function registerStoryCommands(program: Command): void {
32
32
  return WeScrumHelper.getNextTask(options.identificationNumber);
33
33
  }),
34
34
  );
35
+
36
+ program
37
+ .command('remove-automation-breakpoint')
38
+ .description('Remove automation breakpoint')
39
+ .requiredOption('-i, --automationBreakpointId <id>', 'Automation breakpoint identification number')
40
+ .action(
41
+ runProjectCommand<{ automationBreakpointId: string }>(async (options) => {
42
+ await WeScrumHelper.removeAutomationBreakpoint(options.automationBreakpointId);
43
+ }),
44
+ );
35
45
  }
@@ -8,10 +8,10 @@ export function registerTaskCommands(program: Command): void {
8
8
  .command('start-task')
9
9
  .description('Mark a specific task as in progress')
10
10
  .requiredOption('-i, --identificationNumber <id>', 'Story identification number')
11
- .requiredOption('-t, --taskId <id>', 'Unique identifier of the change to act on')
11
+ .requiredOption('-c, --changeId <id>', 'Unique identifier of the change to act on')
12
12
  .action(
13
- runProjectCommand<{ identificationNumber: string; taskId: string }>(async (options) => {
14
- await WeScrumHelper.startTask(options.identificationNumber, options.taskId);
13
+ runProjectCommand<{ identificationNumber: string; changeId: string }>(async (options) => {
14
+ await WeScrumHelper.startTask(options.identificationNumber, options.changeId);
15
15
  Logger.info('Task started successfully. You can now make changes to the codebase.');
16
16
  }),
17
17
  );
@@ -20,10 +20,10 @@ export function registerTaskCommands(program: Command): void {
20
20
  .command('complete-task')
21
21
  .description('Mark the current in-progress task as done')
22
22
  .requiredOption('-i, --identificationNumber <id>', 'Story identification number')
23
- .requiredOption('-t, --taskId <id>', 'Unique identifier of the change to act on')
23
+ .requiredOption('-c, --changeId <id>', 'Unique identifier of the change to act on')
24
24
  .action(
25
- runProjectCommand<{ identificationNumber: string; taskId: string }>(async (options) => {
26
- await WeScrumHelper.completeTask(options.identificationNumber, options.taskId);
25
+ runProjectCommand<{ identificationNumber: string; changeId: string }>(async (options) => {
26
+ await WeScrumHelper.completeTask(options.identificationNumber, options.changeId);
27
27
  Logger.info('Task completed successfully.');
28
28
  }),
29
29
  );
@@ -1,6 +1,7 @@
1
1
  import Handlebars from 'handlebars';
2
2
  import analysisDslLegend from '../../templates/analysis-dsl-legend.hbs';
3
3
  import developStorySkill from '../../templates/develop-story-skill.hbs';
4
+ import getNextTaskBreakpoint from '../../templates/get-next-task-breakpoint.hbs';
4
5
  import getNextTask from '../../templates/get-next-task.hbs';
5
6
  import reviewAnalysisSkill from '../../templates/review-analysis-skill.hbs';
6
7
 
@@ -9,6 +10,7 @@ Handlebars.registerPartial('analysis-dsl-legend', analysisDslLegend);
9
10
  const sources = {
10
11
  'develop-story-skill': developStorySkill,
11
12
  'get-next-task': getNextTask,
13
+ 'get-next-task-breakpoint': getNextTaskBreakpoint,
12
14
  'review-analysis-skill': reviewAnalysisSkill,
13
15
  } as const;
14
16
 
@@ -1,20 +1,25 @@
1
- import { _sortBy, assert, Command, serialize, TypeHelper } from '@my-devkit/core';
1
+ import { _sortBy, assert, Command, ContributionOrigin, serialize, TypeHelper } from '@my-devkit/core';
2
2
  import {
3
+ AddCommentToChangeThreadCommand,
4
+ CreateChangeThreadCommand,
3
5
  MarkEnumerationChangeAsDoneCommand,
4
6
  MarkObjectChangeAsDoneCommand,
5
7
  MarkRelationChangeAsDoneCommand,
6
8
  MarkRouteChangeAsDoneCommand,
7
9
  MarkTaskAsDoneCommand,
10
+ RemoveAutomationBreakpointCommand,
8
11
  TakeEnumerationChangeCommand,
9
12
  TakeObjectChangeCommand,
10
13
  TakeRelationChangeCommand,
11
14
  TakeRouteChangeCommand,
12
15
  TakeTaskCommand,
16
+ UpdateChangeThreadStatusCommand,
13
17
  UpdateIterationCommand,
14
18
  } from '@we-scrum/commands';
15
- import { ContentType, ProgressStatus } from '@we-scrum/enums';
19
+ import { ChangeThreadStatus, ContentType, ProgressStatus } from '@we-scrum/enums';
16
20
  import {
17
21
  DevelopmentPolicyModel,
22
+ ProjectChangeThreadModel,
18
23
  ProjectIterationModel,
19
24
  ProjectStoryModel,
20
25
  ProjectStorySectionModel,
@@ -55,6 +60,15 @@ export class WeScrumHelper {
55
60
  return `## Story ${identificationNumber}\n\nAll tasks are complete.`;
56
61
  }
57
62
 
63
+ if (nextChange.automationBreakpointId) {
64
+ return renderTemplate('get-next-task-breakpoint', {
65
+ identificationNumber,
66
+ automationBreakpointId: nextChange.automationBreakpointId,
67
+ });
68
+ }
69
+
70
+ this.appendOpenThreadsHint(nextChange);
71
+
58
72
  const analysisChange = StorySectionHelper.mapSingleChange(nextChange);
59
73
  const isTodo = nextChange.progress.status === ProgressStatus.ToDo;
60
74
  const developmentPolicy = await this.getChangeDevelopmentPolicy(nextChange);
@@ -66,7 +80,7 @@ export class WeScrumHelper {
66
80
  unitTestsPath: developmentPolicy?.areUnitTestsMandatory ? unitTestsPath : null,
67
81
  isTodo,
68
82
  identificationNumber,
69
- taskId: analysisChange.id,
83
+ changeId: analysisChange.id,
70
84
  });
71
85
  }
72
86
 
@@ -77,11 +91,7 @@ export class WeScrumHelper {
77
91
 
78
92
  const sections = await this.getStorySections(story.storyId);
79
93
 
80
- const task = _sortBy(sections, (s) => s.name)
81
- .flatMap((s) => [...s.enumerationChanges, ...s.objectChanges, ...s.routeChanges, ...s.relationChanges, ...s.tasks])
82
- .find((t) => t.progress?.status !== ProgressStatus.Done);
83
-
84
- return task;
94
+ return this.getAllChanges(sections).find((t) => t.progress?.status !== ProgressStatus.Done);
85
95
  }
86
96
 
87
97
  public static async getChangeById(identificationNumber: string, changeId: string) {
@@ -90,23 +100,14 @@ export class WeScrumHelper {
90
100
 
91
101
  const sections = await this.getStorySections(story.storyId);
92
102
 
93
- const change = _sortBy(sections, (s) => s.name)
94
- .flatMap((s) => [...s.enumerationChanges, ...s.objectChanges, ...s.routeChanges, ...s.relationChanges, ...s.tasks])
95
- .find((t) => {
96
- if (t instanceof ProjectStorySectionModelEnumerationChange) return t.enumerationChangeId === changeId;
97
- if (t instanceof ProjectStorySectionModelObjectChange) return t.objectChangeId === changeId;
98
- if (t instanceof ProjectStorySectionModelRouteChange) return t.routeChangeId === changeId;
99
- if (t instanceof ProjectStorySectionModelRelationChange) return t.relationChangeId === changeId;
100
- if (t instanceof ProjectStorySectionModelTask) return t.taskId === changeId;
101
- return false;
102
- });
103
+ const change = this.getAllChanges(sections).find((t) => this.getChangeIdentifier(t) === changeId);
103
104
 
104
105
  assert(!!change, `Change "${changeId}" not found in story "${identificationNumber}".`);
105
106
  return change;
106
107
  }
107
108
 
108
- public static async startTask(identificationNumber: string, taskId: string): Promise<void> {
109
- const change = await this.getChangeById(identificationNumber, taskId);
109
+ public static async startTask(identificationNumber: string, changeId: string): Promise<void> {
110
+ const change = await this.getChangeById(identificationNumber, changeId);
110
111
 
111
112
  assert(change.progress.status === ProgressStatus.ToDo, `Task is not in ToDo status (current status: ${change.progress.status}).`);
112
113
 
@@ -135,8 +136,8 @@ export class WeScrumHelper {
135
136
  }
136
137
  }
137
138
 
138
- public static async completeTask(identificationNumber: string, taskId: string): Promise<void> {
139
- const change = await this.getChangeById(identificationNumber, taskId);
139
+ public static async completeTask(identificationNumber: string, changeId: string): Promise<void> {
140
+ const change = await this.getChangeById(identificationNumber, changeId);
140
141
 
141
142
  assert(change.progress.status === ProgressStatus.Doing, `Task is not in progress (current status: ${change.progress.status}).`);
142
143
 
@@ -165,6 +166,13 @@ export class WeScrumHelper {
165
166
  }
166
167
  }
167
168
 
169
+ public static async removeAutomationBreakpoint(automationBreakpointId: string): Promise<void> {
170
+ await this.post(
171
+ 'project-management/remove-automation-breakpoint',
172
+ TypeHelper.transform(RemoveAutomationBreakpointCommand, { automationBreakpointId }),
173
+ );
174
+ }
175
+
168
176
  public static async updateIteration(
169
177
  name: string,
170
178
  options: { capacity?: number; comment?: string; startDate?: Date; endDate?: Date },
@@ -222,11 +230,87 @@ export class WeScrumHelper {
222
230
  assert(!!story, `Story "${identificationNumber}" not found in the active project.`);
223
231
 
224
232
  const sections = await this.getStorySections(story.storyId);
233
+ this.getAllChanges(sections).forEach((change) => this.appendOpenThreadsHint(change));
234
+
225
235
  const ast = StorySectionHelper.mapSectionsToAnalysisAst(sections);
226
236
 
227
237
  return AnalysisDsl.stringify(ast);
228
238
  }
229
239
 
240
+ public static async getOpenChangeThreads(changeId: string): Promise<string> {
241
+ const threads = await FirebaseHelper.getCollection<ProjectChangeThreadModel>(`/projects/${this.projectId}/change-threads`, {
242
+ where: [
243
+ ['changeId', '==', changeId],
244
+ ['status', '==', ChangeThreadStatus.Open],
245
+ ],
246
+ });
247
+
248
+ if (threads.length === 0) {
249
+ return `No open threads for change "${changeId}".`;
250
+ }
251
+
252
+ return threads.map((thread) => this.formatChangeThread(thread)).join('\n\n---\n\n');
253
+ }
254
+
255
+ public static async createChangeThread(changeId: string, comment: string): Promise<string> {
256
+ return this.post(
257
+ 'project-management/create-change-thread',
258
+ TypeHelper.transform(CreateChangeThreadCommand, { changeId, commentBody: comment }),
259
+ );
260
+ }
261
+
262
+ public static async addCommentToChangeThread(changeThreadId: string, comment: string): Promise<void> {
263
+ await this.post(
264
+ 'project-management/add-comment-to-change-thread',
265
+ TypeHelper.transform(AddCommentToChangeThreadCommand, { changeThreadId, commentBody: comment }),
266
+ );
267
+ }
268
+
269
+ public static async updateChangeThreadStatus(changeThreadId: string, status: ChangeThreadStatus): Promise<void> {
270
+ await this.post(
271
+ 'project-management/update-change-thread-status',
272
+ TypeHelper.transform(UpdateChangeThreadStatusCommand, { changeThreadId, status }),
273
+ );
274
+ }
275
+
276
+ private static formatChangeThread(thread: ProjectChangeThreadModel): string {
277
+ const comments = thread.comments
278
+ .map((comment) => {
279
+ const author = comment.contributionOrigin === ContributionOrigin.Cli ? 'Claude Code' : comment.createdByUserFullName;
280
+ return `**${author}** (${comment.createdAt.toISOString()}):\n${comment.body}`;
281
+ })
282
+ .join('\n\n');
283
+
284
+ return `## Thread ${thread.changeThreadId} (${thread.status})\n\n${comments}`;
285
+ }
286
+
287
+ private static getAllChanges(sections: ProjectStorySectionModel[]): DevelopmentPolicyHelper.Change[] {
288
+ return _sortBy(sections, (s) => s.name).flatMap((s) => [
289
+ ...s.enumerationChanges,
290
+ ...s.objectChanges,
291
+ ...s.routeChanges,
292
+ ...s.relationChanges,
293
+ ...s.tasks,
294
+ ]);
295
+ }
296
+
297
+ private static getChangeIdentifier(change: DevelopmentPolicyHelper.Change): string {
298
+ if (change instanceof ProjectStorySectionModelEnumerationChange) return change.enumerationChangeId;
299
+ if (change instanceof ProjectStorySectionModelObjectChange) return change.objectChangeId;
300
+ if (change instanceof ProjectStorySectionModelRouteChange) return change.routeChangeId;
301
+ if (change instanceof ProjectStorySectionModelRelationChange) return change.relationChangeId;
302
+ return change.taskId;
303
+ }
304
+
305
+ private static appendOpenThreadsHint(change: DevelopmentPolicyHelper.Change): void {
306
+ const openThreadCount = change.threads.filter((t) => t.status === ChangeThreadStatus.Open).length;
307
+ if (openThreadCount === 0) return;
308
+
309
+ const changeId = this.getChangeIdentifier(change);
310
+ const hint = `${openThreadCount} open ${openThreadCount === 1 ? 'thread' : 'threads'} on this change — run \`we-scrum get-change-threads --changeId "${changeId}"\` to view them.`;
311
+ change.description = change.description ? `${change.description}\n\n${hint}` : hint;
312
+ }
313
+
230
314
  private static async findStoryByIterationNumber(identificationNumber: string) {
231
315
  return FirebaseHelper.findDocument<ProjectStoryModel>(`/projects/${this.projectId}/stories`, [
232
316
  ['identificationNumber', '==', identificationNumber],
@@ -272,6 +356,7 @@ export class WeScrumHelper {
272
356
  headers: {
273
357
  'Content-Type': 'application/json',
274
358
  Authorization: `Bearer ${userIdToken}`,
359
+ 'X-Contribution-Origin': ContributionOrigin.Cli,
275
360
  },
276
361
  signal: AbortSignal.timeout(60 * 1000),
277
362
  });
@@ -0,0 +1,11 @@
1
+ ## Story {{identificationNumber}}
2
+
3
+ Development is paused: an automation breakpoint is set on the next change.
4
+
5
+ The story is partially developed and waiting for resumption. Do not proceed until the user explicitly confirms it is fine to continue.
6
+
7
+ ## Next steps
8
+
9
+ 1. Wait for the user to confirm they want to resume development.
10
+ 2. Once confirmed, run `we-scrum remove-automation-breakpoint --automationBreakpointId "{{automationBreakpointId}}"` to clear the breakpoint.
11
+ 3. Call `get_next_task` again to resume.
@@ -12,14 +12,25 @@
12
12
 
13
13
  ## Next steps
14
14
  {{#if isTodo}}
15
- 1. Run `we-scrum start-task --identificationNumber "{{identificationNumber}}" --taskId "{{taskId}}"` before making any changes.
16
- 2. Implement the task described above.
17
- 3. Commit your changes.
18
- 4. Run `we-scrum complete-task --identificationNumber "{{identificationNumber}}" --taskId "{{taskId}}"` when done.
19
- 5. Call `get_next_task` to continue with the next task.
20
- {{else}}
21
- 1. Implement the task described above.
22
- 2. Commit your changes.
23
- 3. Run `we-scrum complete-task --identificationNumber "{{identificationNumber}}" --taskId "{{taskId}}"` when done.
24
- 4. Call `get_next_task` to continue with the next task.
15
+ 1. Run `we-scrum start-task --identificationNumber "{{identificationNumber}}" --changeId "{{changeId}}"` before making any changes.
25
16
  {{/if}}
17
+ 1. If the task above mentions open threads, run `we-scrum get-change-threads --changeId "{{changeId}}"` before implementing. If a thread requests an adjustment to this task, make it part of the implementation below, then resolve the thread once done:
18
+ ```bash
19
+ we-scrum add-comment-to-change-thread --changeThreadId <changeThreadId> --comment "<markdown explaining what was done>"
20
+ we-scrum update-change-thread-status --changeThreadId <changeThreadId> --status Resolved
21
+ ```
22
+ Leave a thread open (with a reply explaining why) if it does not call for a change to this task, or if you disagree with the request.
23
+ 1. Implement the task described above.
24
+ 1. If your implementation deviates in any way from what this task describes (different naming, different location, a sub-step skipped or altered, a workaround for a blocker, a mistake you had to correct mid-way, etc.), post a comment explaining the deviation and why, before completing the task:
25
+ ```bash
26
+ we-scrum create-change-thread --changeId "{{changeId}}" --comment "<markdown explaining the deviation>"
27
+ ```
28
+ Group related deviations into a single thread. If there are several unrelated deviations, a few separate threads is fine, but do not open one thread per minor deviation — use judgment to keep the count reasonable. Skip this step entirely if the task was implemented exactly as described.
29
+ 1. If the development guidelines or the unit tests guidelines referenced above were incomplete, outdated, or missing for this kind of task — meaning you had to spend extra effort figuring out conventions they should have covered — open a change thread with a concrete suggested addition or correction:
30
+ ```bash
31
+ we-scrum create-change-thread --changeId "{{changeId}}" --comment "<markdown: what was missing/outdated, and a suggested fix>"
32
+ ```
33
+ Use at most one thread for all development guidelines feedback, and, separately, at most one thread for all unit tests guidelines feedback — do not mix the two guidelines together, and do not mix guideline feedback into the deviation thread above. Skip whichever guideline was accurate and sufficient.
34
+ 1. Commit your changes.
35
+ 1. Run `we-scrum complete-task --identificationNumber "{{identificationNumber}}" --changeId "{{changeId}}"` when done.
36
+ 1. Call `get_next_task` to continue with the next task.
@@ -34,7 +34,27 @@ we-scrum get-story-analysis --identificationNumber <identificationNumber>
34
34
 
35
35
  ---
36
36
 
37
- ## Step 2 — Review and return comments
37
+ ## Step 2 — Review existing threads
38
+
39
+ The analysis DSL may annotate a change with a note such as _"N open thread(s) — run `we-scrum get-change-threads --changeId <id>` to view them."_ This means earlier review comments are already open on that change.
40
+
41
+ For every change annotated this way:
42
+
43
+ ```bash
44
+ we-scrum get-change-threads --changeId <changeId>
45
+ ```
46
+
47
+ For each thread returned:
48
+ - If the current state of the analysis addresses the concern raised in the thread, resolve it and explain why:
49
+ ```bash
50
+ we-scrum add-comment-to-change-thread --changeThreadId <changeThreadId> --comment "<markdown explaining why this is resolved>"
51
+ we-scrum update-change-thread-status --changeThreadId <changeThreadId> --status Resolved
52
+ ```
53
+ - If the concern still stands, leave the thread open. Only add a follow-up comment via `add-comment-to-change-thread` if you have new information to add — do not just repeat the original comment.
54
+
55
+ ---
56
+
57
+ ## Step 3 — Review the analysis
38
58
 
39
59
  {{> analysis-dsl-legend}}
40
60
 
@@ -60,4 +80,18 @@ Review the story description and analysis DSL together. Check for:
60
80
  - Any change that adds, removes, or renames a field on an Aggregate or a Model must be accompanied by a migration script task in the analysis
61
81
  - If such a structural change is present but no corresponding migration task exists, raise it as a blocking issue — shipping without a migration will corrupt existing data or break reads
62
82
 
63
- Return your comments to the user.
83
+ ---
84
+
85
+ ## Step 4 — Post findings as change threads
86
+
87
+ Do not just report new findings in the chat — open a change thread on the change each finding concerns:
88
+
89
+ ```bash
90
+ we-scrum create-change-thread --changeId <changeId> --comment "<markdown-formatted finding>"
91
+ ```
92
+
93
+ - The comment body supports Markdown (headings, lists, bold, code spans) — it renders as such in the we-scrum UI.
94
+ - Group every finding for a given change into a single thread/comment rather than opening one thread per bullet point.
95
+ - Skip changes that already have an open thread covering the same concern (see Step 2) — reply on the existing thread instead of duplicating it.
96
+
97
+ Once threads are created or resolved, give the user a short summary in the chat: what was found, what was posted or resolved, and why — do not skip this summary just because the details live in we-scrum.