engineering-memory 1.11.18 → 1.11.20

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.
@@ -0,0 +1,374 @@
1
+ import * as z from 'zod/v4';
2
+ import { languageTag } from '../runtime/questionnaire-store.js';
3
+ import { assertSafeToPersist } from '../runtime/offline-outbox.js';
4
+ import { sha256, stableStringify } from '../utilities/hash.js';
5
+ import { answerChoice, answerData, askQuestionnaire } from './questionnaire-tools.js';
6
+ const field = z.enum(['meaning', 'entryRule']);
7
+ const navigation = z.tuple([z.string().trim().min(1).max(120), z.string().trim().min(1).max(200)]);
8
+ const wording = z.strictObject({
9
+ fields: z.record(field, z.strictObject({
10
+ title: z.string().trim().min(1).max(80),
11
+ question: z.string().trim().min(1).max(240),
12
+ example: z.string().trim().min(1).max(400),
13
+ inputTitle: z.string().trim().min(1).max(120),
14
+ })),
15
+ context: z.string().trim().min(1).max(400),
16
+ current: z.string().trim().min(1).max(80),
17
+ proposed: z.string().trim().min(1).max(80),
18
+ empty: z.string().trim().min(1).max(80),
19
+ page: z.string().trim().min(1).max(80),
20
+ keep: navigation,
21
+ write: navigation,
22
+ clear: navigation,
23
+ next: navigation,
24
+ defer: navigation,
25
+ save: navigation,
26
+ review: z.strictObject({
27
+ message: z.string().trim().min(1).max(240),
28
+ context: z.string().trim().min(1).max(400),
29
+ example: z.string().trim().min(1).max(400),
30
+ }),
31
+ });
32
+ const inputSchema = z.strictObject({
33
+ repoRoot: z.string().min(1),
34
+ projectId: z.string().uuid(),
35
+ externalTaskId: z.string().trim().min(2).max(160),
36
+ statusId: z.string().uuid(),
37
+ expectedVersion: z.number().int().min(1).max(2147483647),
38
+ field,
39
+ draft: z.string().trim().max(4000).optional(),
40
+ recommendation: z.enum(['keep', 'write', 'clear']),
41
+ reason: z.string().trim().min(1).max(180),
42
+ requestKey: z.string().regex(/^[A-Za-z0-9_-]{1,80}$/),
43
+ language: languageTag,
44
+ copy: wording.optional(),
45
+ decisionAttempt: z.number().int().min(1).max(10000).optional(),
46
+ presentation: z.literal('host_native').optional(),
47
+ });
48
+ const definition = z.object({
49
+ id: z.string().uuid(),
50
+ name: z.string().min(1).max(120),
51
+ slug: z.string().min(1).max(64),
52
+ meaning: z.string().max(4000),
53
+ entryRule: z.string().max(4000),
54
+ archivedAt: z.string().nullable(),
55
+ lockVersion: z.number().int().min(1).max(2147483647),
56
+ });
57
+ const pageSchema = z.object({
58
+ snapshot: z.string().regex(/^[a-f0-9]{64}$/),
59
+ items: z.array(definition).max(100),
60
+ total: z.number().int().min(0),
61
+ offset: z.number().int().min(0),
62
+ limit: z.literal(100),
63
+ });
64
+ const copies = {
65
+ tr: {
66
+ fields: {
67
+ meaning: {
68
+ title: 'Kolonun anlamı',
69
+ question: 'Bu kolondaki iş ne aşamada olur?',
70
+ inputTitle: 'Yazmak istediğin anlam',
71
+ example: 'İncelemede: geliştirme bitti; ekip arkadaşının kodu incelemesi bekleniyor.',
72
+ },
73
+ entryRule: {
74
+ title: 'Kolona giriş koşulu',
75
+ question: 'Bir iş bu kolona hangi koşulda alınır?',
76
+ inputTitle: 'Yazmak istediğin giriş koşulu',
77
+ example: 'İncelemeye geçmek için kod hazır ve ilgili testler başarılı olmalı.',
78
+ },
79
+ },
80
+ context: 'Bu adım isteğe bağlı. Yorum veya açıklama isteği kaydedilmez; yazdığın tanımı son incelemeden sonra kaydederiz.',
81
+ current: 'Mevcut metin',
82
+ proposed: 'Yeni metin',
83
+ empty: '(boş)',
84
+ page: 'Sayfa',
85
+ keep: ['Mevcut kalsın', 'Bu alanı ve mevcut işleri değiştirme.'],
86
+ write: [
87
+ 'Tanımı yaz',
88
+ 'Metin alanına kendi tanımını yaz; kaydetmeden önce birlikte gözden geçir.',
89
+ ],
90
+ clear: [
91
+ 'Bu alanı boşalt',
92
+ 'Boş bırakma tercihini son incelemede doğrula; diğer alanlar aynı kalır.',
93
+ ],
94
+ next: ['Devamını incele', 'Metnin sonraki kısmını göster; henüz hiçbir şey kaydetme.'],
95
+ defer: ['Şimdi kaydetme', 'Bu denemeyi ertele; kolon tanımını ve işleri değiştirme.'],
96
+ save: [
97
+ 'Bu tanımı kaydet',
98
+ 'İncelediğin alanı kaydet; diğer alanlara ve mevcut işlere dokunma.',
99
+ ],
100
+ review: {
101
+ message: 'Bu alanın değişikliğini incele',
102
+ context: 'Mevcut ve yeni metin tümüyle gösterilir. Uzunsa sonraki sayfaya geç; kaydetme yalnızca son sayfadadır. Bu işlem bir işi taşımaz veya olay çalıştırmaz.',
103
+ example: 'İncelemede kolonunu “kod hazır, inceleme bekleniyor” diye tanımlarsın. Kolondaki işler aynı yerde kalır.',
104
+ },
105
+ },
106
+ en: {
107
+ fields: {
108
+ meaning: {
109
+ title: 'Stage meaning',
110
+ question: 'What does work in this stage mean?',
111
+ inputTitle: 'Your stage meaning',
112
+ example: 'In Review: development is complete and a teammate needs to review the code.',
113
+ },
114
+ entryRule: {
115
+ title: 'Stage entry condition',
116
+ question: 'When should work enter this stage?',
117
+ inputTitle: 'Your entry condition',
118
+ example: 'Code must be ready and the relevant tests must pass before review.',
119
+ },
120
+ },
121
+ context: 'This step is optional. Feedback or requests for explanation do not save anything; a written definition is reviewed before saving.',
122
+ current: 'Current text',
123
+ proposed: 'New text',
124
+ empty: '(empty)',
125
+ page: 'Page',
126
+ keep: ['Keep the current value', 'Leave this field and existing work unchanged.'],
127
+ write: ['Write a definition', 'Enter your own text and review it before saving.'],
128
+ clear: ['Clear this field', 'Review an explicit empty value; other fields remain unchanged.'],
129
+ next: ['Review the next part', 'Show the next part of the text without saving anything yet.'],
130
+ defer: ['Do not save now', 'Defer this attempt without changing the stage or existing work.'],
131
+ save: [
132
+ 'Save this definition',
133
+ 'Save the reviewed field; other fields and existing work remain unchanged.',
134
+ ],
135
+ review: {
136
+ message: 'Review this field change',
137
+ context: 'All current and proposed text is shown. Continue through longer text; only the last page can save it. This does not move work or execute an event.',
138
+ example: 'Define In Review as “code is ready and awaiting review.” Tasks in that stage remain where they are.',
139
+ },
140
+ },
141
+ };
142
+ export function registerStatusMeaningTools(server, service) {
143
+ server.registerTool('work_item.describe_status', {
144
+ description: 'Optionally review a user-authored meaning or entry condition for one status at its exact version. Resume the bound write task first. Never infer text from the name or resurvey owner-authored defaults. Supply a deliberate keep/write/clear recommendation and reason in the conversation language. Without draft, an editable native field collects up to2000 characters; explicitly authored draft arguments support the full4000-character metadata contract through complete review pages. Feedback alone never writes. Every saved change requires exact final mode-governed review, and existing metadata authority/version checks. Retry identical input to resume; reconsider a skipped/deferred attempt only when authorized. Turkish/English copy is built in; other languages require complete copy. Does not move work or fire events.',
145
+ inputSchema,
146
+ }, (input, context) => describeStatus(server, service, input, context));
147
+ }
148
+ async function describeStatus(server, service, input, context) {
149
+ const scope = await service.workItemSetupContext(input);
150
+ if (!scope.ok)
151
+ return output(scope);
152
+ const { repoRoot: _repoRoot, ...retryArguments } = input;
153
+ try {
154
+ assertSafeToPersist(retryArguments, '', '', true);
155
+ }
156
+ catch {
157
+ return refusal('Remove sensitive text from the description and its retry arguments.', 'work_item.describe_status');
158
+ }
159
+ const primary = input.language.split('-')[0];
160
+ const copy = input.copy ?? (primary === 'tr' || primary === 'en' ? copies[primary] : undefined);
161
+ if (!copy)
162
+ return refusal('Supply every copy field in the conversation language.', 'work_item.describe_status');
163
+ let target;
164
+ let snapshot;
165
+ let total;
166
+ for (let offset = 0; total === undefined || offset < total; offset += 100) {
167
+ const response = await service.workItemWorkflow({
168
+ projectId: input.projectId,
169
+ offset,
170
+ limit: 100,
171
+ includeArchived: true,
172
+ ...(snapshot ? { snapshot } : {}),
173
+ });
174
+ if (!response.ok)
175
+ return output(response);
176
+ const parsed = pageSchema.safeParse(response.data);
177
+ if (!parsed.success)
178
+ return refusal('Reload the complete status definition before reviewing it.', 'work_item.statuses');
179
+ const page = parsed.data;
180
+ if (page.offset !== offset ||
181
+ (snapshot !== undefined && snapshot !== page.snapshot) ||
182
+ (total !== undefined && total !== page.total) ||
183
+ page.items.length !== Math.min(100, Math.max(0, page.total - offset)) ||
184
+ new Set(page.items.map((item) => item.id)).size !== page.items.length)
185
+ return refusal('The status pages changed or were incomplete. Reload the definition.', 'work_item.statuses');
186
+ snapshot = page.snapshot;
187
+ total = page.total;
188
+ target = page.items.find((item) => item.id === input.statusId);
189
+ if (target)
190
+ break;
191
+ }
192
+ if (!target || target.archivedAt !== null || target.lockVersion !== input.expectedVersion)
193
+ return refusal('The definition is unavailable, archived or changed. Read its current version before reviewing.', 'work_item.statuses');
194
+ try {
195
+ assertSafeToPersist({ label: target.name, slug: target.slug, value: target[input.field] }, '', '', true);
196
+ }
197
+ catch {
198
+ return refusal('The stored definition contains content that cannot be included in a durable question.', 'work_item.statuses');
199
+ }
200
+ const namespace = scope.data.namespace;
201
+ const { presentation: _presentation, ...decisionInput } = retryArguments;
202
+ const setupId = sha256(stableStringify({
203
+ namespace,
204
+ decisionInput,
205
+ current: target[input.field],
206
+ name: target.name,
207
+ slug: target.slug,
208
+ copy,
209
+ }));
210
+ const owner = {
211
+ tool: 'work_item.describe_status',
212
+ externalTaskId: input.externalTaskId,
213
+ decisionAttempt: input.decisionAttempt,
214
+ retryArguments,
215
+ };
216
+ let value = input.draft;
217
+ let inputQuestionnaireId = null;
218
+ if (value === undefined) {
219
+ inputQuestionnaireId = 'status-description-input-' + setupId;
220
+ const options = ['keep', 'write', 'clear'].map((id) => ({
221
+ id,
222
+ label: copy[id][0],
223
+ description: copy[id][1] + (id === input.recommendation ? ' ' + input.reason : ''),
224
+ }));
225
+ options.sort((a, b) => Number(b.id === input.recommendation) - Number(a.id === input.recommendation));
226
+ const question = {
227
+ questionnaireId: inputQuestionnaireId,
228
+ language: input.language,
229
+ impact: 'critical',
230
+ message: copy.fields[input.field].question + '\n' + target.name + ' (' + target.slug + ')',
231
+ context: copy.context,
232
+ example: copy.fields[input.field].example,
233
+ options,
234
+ textField: {
235
+ title: copy.fields[input.field].inputTitle,
236
+ maxLength: 2000,
237
+ requiredForChoice: 'write',
238
+ },
239
+ binding: {
240
+ setupId,
241
+ namespace,
242
+ projectId: input.projectId,
243
+ statusId: input.statusId,
244
+ expectedVersion: input.expectedVersion,
245
+ field: input.field,
246
+ },
247
+ };
248
+ const answer = await askQuestionnaire(server, service, { ...question, repoRoot: input.repoRoot, presentation: input.presentation }, context, [], owner);
249
+ const choice = answerChoice(answer);
250
+ if (!choice)
251
+ return answer;
252
+ if (choice === 'keep')
253
+ return deferred(input, 'skipped');
254
+ if (choice === 'clear')
255
+ value = '';
256
+ else if (choice === 'write')
257
+ value = answerData(answer)?.text;
258
+ if (value === undefined)
259
+ return refusal('Resume the exact editable description question.', 'questionnaire.resume');
260
+ }
261
+ if (value === target[input.field])
262
+ return output({ ok: true, data: { status: 'unchanged', applied: false } });
263
+ const proposal = {
264
+ projectId: input.projectId,
265
+ statusId: input.statusId,
266
+ expectedVersion: input.expectedVersion,
267
+ field: input.field,
268
+ value,
269
+ };
270
+ const proposalDigest = sha256(stableStringify(proposal));
271
+ const reviewText = copy.fields[input.field].title +
272
+ '\n' +
273
+ target.name +
274
+ ' (' +
275
+ target.slug +
276
+ ')\n\n' +
277
+ copy.current +
278
+ '\n' +
279
+ (target[input.field] || copy.empty) +
280
+ '\n\n' +
281
+ copy.proposed +
282
+ '\n' +
283
+ (value || copy.empty);
284
+ const pages = [];
285
+ for (let start = 0; start < reviewText.length;) {
286
+ let end = Math.min(start + 1400, reviewText.length);
287
+ const last = reviewText.charCodeAt(end - 1);
288
+ if (end < reviewText.length && last >= 0xd800 && last <= 0xdbff)
289
+ end--;
290
+ pages.push(reviewText.slice(start, end));
291
+ start = end;
292
+ }
293
+ const reviewQuestionnaireIds = pages.map((_, index) => 'status-description-review-' + sha256(stableStringify({ setupId, proposalDigest, index })));
294
+ for (let index = 0; index < pages.length; index++) {
295
+ if (context.mcpReq.signal.aborted)
296
+ return output({ ok: true, data: { status: 'pending', reason: 'interrupted' } });
297
+ const final = index === pages.length - 1;
298
+ const action = final ? copy.save : copy.next;
299
+ const question = {
300
+ questionnaireId: reviewQuestionnaireIds[index],
301
+ language: input.language,
302
+ impact: 'critical',
303
+ message: copy.review.message +
304
+ '\n\n' +
305
+ pages[index] +
306
+ '\n\n' +
307
+ copy.page +
308
+ ' ' +
309
+ (index + 1) +
310
+ '/' +
311
+ pages.length,
312
+ context: copy.review.context,
313
+ example: copy.review.example,
314
+ options: [
315
+ { id: final ? 'save' : 'continue', label: action[0], description: action[1] },
316
+ { id: 'defer', label: copy.defer[0], description: copy.defer[1] },
317
+ ],
318
+ binding: {
319
+ setupId,
320
+ namespace,
321
+ proposalDigest,
322
+ inputQuestionnaireId,
323
+ reviewQuestionnaireIds,
324
+ index,
325
+ },
326
+ };
327
+ const answer = await askQuestionnaire(server, service, { ...question, repoRoot: input.repoRoot, presentation: input.presentation }, context, [], owner);
328
+ const choice = answerChoice(answer);
329
+ if (choice === 'defer')
330
+ return deferred(input, 'deferred');
331
+ if (choice !== (final ? 'save' : 'continue'))
332
+ return answer;
333
+ }
334
+ return output(await service.workItemApplyStatusDescription({
335
+ ...proposal,
336
+ repoRoot: input.repoRoot,
337
+ externalTaskId: input.externalTaskId,
338
+ inputQuestionnaireId,
339
+ reviewQuestionnaireIds,
340
+ }));
341
+ }
342
+ function deferred(input, status) {
343
+ return output({
344
+ ok: true,
345
+ data: {
346
+ status,
347
+ applied: false,
348
+ ...(input.decisionAttempt === 10000
349
+ ? {}
350
+ : {
351
+ reconsider: {
352
+ tool: 'work_item.describe_status',
353
+ arguments: { ...input, decisionAttempt: (input.decisionAttempt ?? 0) + 1 },
354
+ },
355
+ }),
356
+ nextAction: input.decisionAttempt === 10000
357
+ ? 'This attempt remains deferred. Use a new requestKey only after reconsideration is explicitly authorized.'
358
+ : 'Identical retries preserve this decision. Reconsider only after explicit user intent or a reasoned current delegated decision.',
359
+ },
360
+ });
361
+ }
362
+ function refusal(message, recovery) {
363
+ return output({
364
+ ok: false,
365
+ error: { kind: 'status_description', message, recovery, retryable: false },
366
+ });
367
+ }
368
+ function output(result) {
369
+ return {
370
+ content: [{ type: 'text', text: JSON.stringify(result) }],
371
+ isError: !result.ok,
372
+ };
373
+ }
374
+ //# sourceMappingURL=status-meaning-tools.js.map
@@ -18,6 +18,7 @@ const outward = {
18
18
  export const toolAnnotations = {
19
19
  'project.git_preferences': read,
20
20
  'project.set_git_preferences': write,
21
+ 'worktree.prepare_files': repeatableWrite,
21
22
  'worktree.policy': read,
22
23
  'worktree.set_policy': write,
23
24
  'worktree.list': read,
@@ -34,6 +35,8 @@ export const toolAnnotations = {
34
35
  'questionnaire.answer_from_host': repeatableWrite,
35
36
  'questionnaire.withdraw': destructive,
36
37
  'session.entry': read,
38
+ 'release_notes.presented': repeatableWrite,
39
+ 'release_notes.show': read,
37
40
  'session.set_decision': repeatableWrite,
38
41
  'session.decline_update': repeatableWrite,
39
42
  'session.answer_shadow_notice': repeatableWrite,
@@ -104,6 +107,15 @@ export const toolAnnotations = {
104
107
  'project.update': write,
105
108
  'work_item.list': read,
106
109
  'work_item.statuses': read,
110
+ 'work_item.workflow': read,
111
+ 'work_item.configure_workflow': write,
112
+ 'work_item.setup_workflow': write,
113
+ 'work_item.describe_status': write,
114
+ 'work_item.update_status': write,
115
+ 'work_item.create_status': write,
116
+ 'work_item.archive_status': write,
117
+ 'work_item.restore_status': write,
118
+ 'work_item.set_initial_status': write,
107
119
  'work_item.get': read,
108
120
  'project.clone': outward,
109
121
  'project.link': write,
@@ -1,3 +1,7 @@
1
+ import { registerDeliveryTools } from './delivery-tools.js';
2
+ import { registerStatusMeaningTools } from './status-meaning-tools.js';
3
+ import { registerWorkflowTools } from './workflow-tools.js';
4
+ import { registerReleaseTools } from './release-tools.js';
1
5
  import { ensureDecisionMode, registerDecisionTools } from './decision-tools.js';
2
6
  import * as z from 'zod/v4';
3
7
  import { validationIds } from '../runtime/bridge-service.js';
@@ -48,6 +52,8 @@ const checkpointBase = {
48
52
  idempotencyKey: z.string().uuid().optional(),
49
53
  };
50
54
  export const engineeringMemoryToolNames = [
55
+ 'release_notes.presented',
56
+ 'release_notes.show',
51
57
  'decision.mode',
52
58
  'decision.mode_status',
53
59
  'questionnaire.decide',
@@ -57,6 +63,7 @@ export const engineeringMemoryToolNames = [
57
63
  'worktree.set_policy',
58
64
  'worktree.list',
59
65
  'worktree.reconcile',
66
+ 'worktree.prepare_files',
60
67
  'task.heartbeat',
61
68
  'task.pause',
62
69
  'worktree.release',
@@ -135,6 +142,15 @@ export const engineeringMemoryToolNames = [
135
142
  'work_item.confirm_plan',
136
143
  'project.update',
137
144
  'work_item.statuses',
145
+ 'work_item.workflow',
146
+ 'work_item.configure_workflow',
147
+ 'work_item.setup_workflow',
148
+ 'work_item.describe_status',
149
+ 'work_item.update_status',
150
+ 'work_item.create_status',
151
+ 'work_item.archive_status',
152
+ 'work_item.restore_status',
153
+ 'work_item.set_initial_status',
138
154
  'work_item.list',
139
155
  'work_item.get',
140
156
  'project.clone',
@@ -195,7 +211,10 @@ const waiverCopy = {
195
211
  },
196
212
  };
197
213
  export function registerQuestionnaireTools(server, service) {
214
+ registerReleaseTools(server, service);
198
215
  registerDecisionTools(server, service);
216
+ registerWorkflowTools(server, service);
217
+ registerStatusMeaningTools(server, service);
199
218
  server.registerTool('questionnaire.answer_from_host', {
200
219
  description: 'Relay an actual answer from a host-permitted blocking native AskUserQuestion or request_user_input control to the exact durable question. First read hostFallback from the pending operation or questionnaire.resume with presentation host_native, display its unchanged question/options/notices and await the real native result. Never infer answers, use chat consent, asynchronous controls, defaults or a declined/dismissed form. The hostTool field is agent-reported provenance, not transport-verified attestation. Bind requestKey and contentHash exactly; changed scope, withdrawn questions, invalid or conflicting answers are refused. This only saves the answer; retry the owning operation for current authority and version checks.',
201
220
  inputSchema: hostAnswerSchema.extend({
@@ -751,13 +770,7 @@ export function registerEngineeringMemoryTools(server, service) {
751
770
  }
752
771
  return toolResult(await service.taskVerify(request));
753
772
  });
754
- server.registerTool('task.close', {
755
- description: 'Close a task only when backend verification still matches the current Git diff hash.',
756
- inputSchema: z.object({
757
- repoRoot: optionalRepoRoot,
758
- taskId: z.string().min(1),
759
- }),
760
- }, async (input) => toolResult(await service.taskClose(input)));
773
+ registerDeliveryTools(server, service);
761
774
  server.registerTool('task.abandon', {
762
775
  description: "Abandon a task the user has said they are giving up on, so it stops holding its proposals and its local pointer. It has no effect on any other task: nobody else's work waits on this, and nothing already verified or closed is undone. Ask the user first; never abandon a task on your own judgement.",
763
776
  inputSchema: z.object({
@@ -1038,6 +1051,134 @@ export function registerEngineeringMemoryTools(server, service) {
1038
1051
  includeArchived: z.boolean().optional(),
1039
1052
  }),
1040
1053
  }, async (input) => toolResult(await service.workItemStatuses(input)));
1054
+ server.registerTool('work_item.workflow', {
1055
+ description: 'Read a coherent page of project stages and the six stored lifecycle event rules without seeding or changing anything. Pass the returned snapshot on every later page; a conflict requires rereading from the start. Page through all statuses before reviewing workflow choices. Every event target includes its human definition or null with targetUnavailable for a missing/foreign target; archived targets are unavailable. Do not infer meanings from names or show snapshot hashes as user choices.',
1056
+ inputSchema: z
1057
+ .object({
1058
+ projectId: z.string().uuid(),
1059
+ offset: z.number().int().min(0).optional(),
1060
+ limit: z.number().int().min(1).max(100).optional(),
1061
+ includeArchived: z.boolean().optional(),
1062
+ snapshot: z
1063
+ .string()
1064
+ .regex(/^[a-f0-9]{64}$/)
1065
+ .optional(),
1066
+ })
1067
+ .strict(),
1068
+ }, async (input) => toolResult(await service.workItemWorkflow(input)));
1069
+ server.registerTool('work_item.configure_workflow', {
1070
+ description: 'Apply explicit project workflow choices together against the complete snapshot from work_item.workflow. Requires current owner, maintainer or organization administration. Supply each of work_started, pr_opened, all_prs_merged, test_failed and blocked_declared exactly once, with an active same-project target and explicit automatic/confirm/ignore mode. The blocked target becomes the active parked stage. Optional draft_pr_opened preserves its stored rule when omitted; a missing draft rule starts automatic with no target. Only draft may have a null target. Existing work and the initial stage stay unchanged; no event fires. Resolve choices under the task decision mode, use human stage names, and never silently substitute a newer snapshot after conflict.',
1071
+ inputSchema: z
1072
+ .object({
1073
+ projectId: z.string().uuid(),
1074
+ expectedSnapshot: z.string().regex(/^[a-f0-9]{64}$/),
1075
+ rules: z
1076
+ .array(z
1077
+ .object({
1078
+ event: z.enum([
1079
+ 'work_started',
1080
+ 'draft_pr_opened',
1081
+ 'pr_opened',
1082
+ 'all_prs_merged',
1083
+ 'test_failed',
1084
+ 'blocked_declared',
1085
+ ]),
1086
+ toStatusId: z.string().uuid().nullable(),
1087
+ mode: z.enum(['automatic', 'confirm', 'ignore']),
1088
+ })
1089
+ .strict())
1090
+ .min(5)
1091
+ .max(6)
1092
+ .refine((rules) => new Set(rules.map((rule) => rule.event)).size === rules.length, {
1093
+ message: 'Each event may be supplied only once.',
1094
+ }),
1095
+ })
1096
+ .strict(),
1097
+ }, async (input) => toolResult(await service.workItemConfigureWorkflow(input)));
1098
+ server.registerTool('work_item.create_status', {
1099
+ description: 'Create one project status definition with an explicit stable slug, name, category and position. Requires current owner, maintainer or organization administration. Read work_item.statuses including archives first; a duplicate slug is refused, never silently renamed or restored. Meanings and entry conditions come from the project user. New definitions are not initial or parked; omitted actionable/terminal flags are false. This does not move work or configure events.',
1100
+ inputSchema: z
1101
+ .object({
1102
+ projectId: z.string().uuid(),
1103
+ slug: z
1104
+ .string()
1105
+ .trim()
1106
+ .min(1)
1107
+ .max(64)
1108
+ .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
1109
+ name: z
1110
+ .string()
1111
+ .trim()
1112
+ .min(1)
1113
+ .max(120)
1114
+ .regex(/^[^\r\n]+$/),
1115
+ category: z.enum(['new', 'indeterminate', 'done']),
1116
+ position: z.number().int().min(0).max(2_147_483_647),
1117
+ meaning: z.string().trim().max(4000).optional(),
1118
+ entryRule: z.string().trim().max(4000).optional(),
1119
+ isActionable: z.boolean().optional(),
1120
+ isTerminal: z.boolean().optional(),
1121
+ })
1122
+ .strict(),
1123
+ }, async (input) => toolResult(await service.workItemCreateStatus(input)));
1124
+ server.registerTool('work_item.archive_status', {
1125
+ description: 'Archive a non-initial project status at its current lockVersion from work_item.statuses. Existing items stay in that column; its metadata and event targets remain unchanged. Current owner, maintainer or organization administration required. Initial definitions need separate role setup and are refused here. Never silently retry a stale version.',
1126
+ inputSchema: z
1127
+ .object({
1128
+ projectId: z.string().uuid(),
1129
+ statusId: z.string().uuid(),
1130
+ expectedVersion: z.number().int().min(1).max(2_147_483_647),
1131
+ })
1132
+ .strict(),
1133
+ }, async (input) => toolResult(await service.workItemSetStatusArchived({ ...input, archived: true })));
1134
+ server.registerTool('work_item.restore_status', {
1135
+ description: 'Restore a non-initial project status at its current lockVersion from work_item.statuses with includeArchived:true. Existing work and metadata stay intact. Current owner, maintainer or organization administration required. Restoring an initial definition requires separate role setup and is refused here. Never silently retry a stale version.',
1136
+ inputSchema: z
1137
+ .object({
1138
+ projectId: z.string().uuid(),
1139
+ statusId: z.string().uuid(),
1140
+ expectedVersion: z.number().int().min(1).max(2_147_483_647),
1141
+ })
1142
+ .strict(),
1143
+ }, async (input) => toolResult(await service.workItemSetStatusArchived({ ...input, archived: false })));
1144
+ server.registerTool('work_item.set_initial_status', {
1145
+ description: 'Choose the active project stage where newly created work starts. Read all pages of work_item.statuses first. Pass the target lockVersion and the observed active initial status id and lockVersion, or explicit null for both prior-initial fields when none exists. Requires current owner, maintainer or organization administration. Existing work, stage meanings and event targets stay unchanged; this is separate from work_started. A stale selection returns a conflict; reread instead of silently retrying.',
1146
+ inputSchema: z
1147
+ .object({
1148
+ projectId: z.string().uuid(),
1149
+ statusId: z.string().uuid(),
1150
+ expectedVersion: z.number().int().min(1).max(2_147_483_647),
1151
+ expectedInitialStatusId: z.string().uuid().nullable(),
1152
+ expectedInitialVersion: z.number().int().min(1).max(2_147_483_647).nullable(),
1153
+ })
1154
+ .strict()
1155
+ .refine((input) => (input.expectedInitialStatusId === null) === (input.expectedInitialVersion === null), {
1156
+ message: 'The prior initial status and its version must both be present or both null.',
1157
+ }),
1158
+ }, async (input) => toolResult(await service.workItemSetInitialStatus(input)));
1159
+ server.registerTool('work_item.update_status', {
1160
+ description: 'Edit one active project status definition at its current lockVersion from work_item.statuses. Only project owners, maintainers and organization administrators may edit. Use meanings and entry conditions supplied by the project user; never invent them from a name. Omitted fields stay unchanged and empty text clears a meaning or entry condition. Show human status names when reviewing changes. This never moves work items, changes the stable slug, configures event roles or fires an event. Creation, archival and role setup are not part of this tool.',
1161
+ inputSchema: z
1162
+ .object({
1163
+ projectId: z.string().uuid(),
1164
+ statusId: z.string().uuid(),
1165
+ expectedVersion: z.number().int().min(1).max(2_147_483_647),
1166
+ name: z
1167
+ .string()
1168
+ .trim()
1169
+ .min(1)
1170
+ .max(120)
1171
+ .regex(/^[^\r\n]+$/)
1172
+ .optional(),
1173
+ category: z.enum(['new', 'indeterminate', 'done']).optional(),
1174
+ meaning: z.string().trim().max(4000).optional(),
1175
+ entryRule: z.string().trim().max(4000).optional(),
1176
+ position: z.number().int().min(0).max(2_147_483_647).optional(),
1177
+ isActionable: z.boolean().optional(),
1178
+ isTerminal: z.boolean().optional(),
1179
+ })
1180
+ .strict(),
1181
+ }, async (input) => toolResult(await service.workItemUpdateStatus(input)));
1041
1182
  server.registerTool('work_item.list', {
1042
1183
  description: 'List selectable work items for a project before opening an engineering run in this chat. Read work_item.statuses for the actual project catalogue slugs and user-defined meanings; never infer workflow from status names. The status filter matches an actual slug, including a retained archived status; an unknown slug returns an empty page.',
1043
1184
  inputSchema: z.object({