engineering-memory 1.11.19 → 1.11.21

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.
Files changed (31) hide show
  1. package/bin/engineering-memory.mjs +14 -24
  2. package/install/localization.generated.mjs +18 -0
  3. package/package.json +1 -1
  4. package/runtime/build.json +1 -1
  5. package/runtime/dist/src/auth/browser-auth.js +15 -4
  6. package/runtime/dist/src/config.js +1 -0
  7. package/runtime/dist/src/git/git-inspector.js +10 -0
  8. package/runtime/dist/src/localization/catalogue.generated.js +786 -0
  9. package/runtime/dist/src/mcp/delivery-tools.js +15 -48
  10. package/runtime/dist/src/mcp/onboarding-tools.js +289 -618
  11. package/runtime/dist/src/mcp/questionnaire-tools.js +25 -26
  12. package/runtime/dist/src/mcp/status-meaning-tools.js +12 -86
  13. package/runtime/dist/src/mcp/status-remap-tools.js +252 -0
  14. package/runtime/dist/src/mcp/tool-annotations.js +1 -0
  15. package/runtime/dist/src/mcp/tool-definitions.js +33 -55
  16. package/runtime/dist/src/mcp/workflow-tools.js +17 -108
  17. package/runtime/dist/src/mcp/worktree-tools.js +168 -250
  18. package/runtime/dist/src/runtime/api-client.js +8 -2
  19. package/runtime/dist/src/runtime/bridge-service.js +100 -94
  20. package/runtime/dist/src/runtime/create-bridge-service.js +4 -2
  21. package/runtime/dist/src/runtime/language-store.js +6 -0
  22. package/runtime/dist/src/runtime/release-notes.js +9 -7
  23. package/runtime/dist/src/runtime/release-report.js +30 -30
  24. package/runtime/dist/src/runtime/task-start.js +75 -73
  25. package/runtime/dist/src/runtime/texts.js +135 -0
  26. package/runtime/dist/src/runtime/worktree-pool.js +30 -5
  27. package/runtime/dist/src/runtime/worktree-preparation.js +3 -2
  28. package/runtime/dist/src/utilities/process.js +7 -0
  29. package/skill/references/lifecycle.md +21 -3
  30. package/skill/references/project-onboarding.md +5 -2
  31. package/skill/references/questionnaires.md +5 -1
@@ -1,4 +1,6 @@
1
+ import * as z from 'zod/v4';
1
2
  import { acceptedContent, CLIENT_CAPABILITIES_META_KEY, inputRequired, inputResponse, } from '@modelcontextprotocol/server';
3
+ import { texts } from '../runtime/texts.js';
2
4
  import { questionnaireAnswerSchema, } from '../runtime/questionnaire-store.js';
3
5
  export async function askQuestionnaire(server, service, input, context, previousDefinitions = [], owner) {
4
6
  try {
@@ -233,30 +235,17 @@ function consumedResponses(context) {
233
235
  consumed.set(context.mcpReq, keys);
234
236
  return keys;
235
237
  }
236
- const questionnaireCopy = {
237
- en: {
238
- pending: 'Choose an answer to resolve this decision. Closing or cancelling the form leaves it pending.',
239
- freeText: ' You can type a different answer without choosing an option. Free text is not stored and cannot be replayed.',
240
- storedText: ' The bounded text field is stored as part of this exact decision and is replayed after restart.',
241
- everyQuestion: ' To record a decision, answer each question or choose an option that ends the form. You can send a comment without selecting any option.',
242
- example: 'Example',
243
- answer: 'Answer',
244
- freeAnswer: 'Write another answer (optional)',
245
- recommended: 'Recommended',
246
- feedback: 'Write an answer or comment (optional)',
247
- },
248
- tr: {
249
- pending: 'Kararını kaydetmek için bir seçenek seç. Formu kapatmak veya iptal etmek onay sayılmaz; soru beklemede kalır.',
250
- freeText: ' Bir seçenek seçmeden farklı yanıtını yazabilirsin. Serbest yanıt saklanmaz ve daha sonra geri getirilemez.',
251
- storedText: ' Bu metin alanındaki yanıt kararınla birlikte saklanır; yeniden başlattığında korunur.',
252
- everyQuestion: ' Karar vermek için soruları cevapla veya formu bitiren bir seçenek seç. Hiçbir seçenek seçmeden yorum da gönderebilirsin.',
253
- example: 'Örnek',
254
- answer: 'Yanıt',
255
- freeAnswer: 'Farklı yanıtını yaz (isteğe bağlı)',
256
- recommended: 'Önerilen',
257
- feedback: 'Yanıtını veya yorumunu yaz (isteğe bağlı)',
258
- },
259
- };
238
+ const questionnaireCopy = z.strictObject({
239
+ pending: z.string().min(1),
240
+ freeText: z.string().min(1),
241
+ storedText: z.string().min(1),
242
+ everyQuestion: z.string().min(1),
243
+ example: z.string().min(1),
244
+ answer: z.string().min(1),
245
+ freeAnswer: z.string().min(1),
246
+ recommended: z.string().min(1),
247
+ feedback: z.string().min(1),
248
+ });
260
249
  function formAnswers(record, content) {
261
250
  const answers = {};
262
251
  for (const sub of record.questions ?? []) {
@@ -274,6 +263,8 @@ function formAnswers(record, content) {
274
263
  }
275
264
  function presentationMessage(record, compact = false) {
276
265
  const copy = copyFor(record);
266
+ if (record.owner?.tool === 'decision.mode' && record.binding)
267
+ return [record.message, record.context].filter(Boolean).join('\n');
277
268
  if (compact)
278
269
  return [
279
270
  record.message,
@@ -292,13 +283,21 @@ function presentationMessage(record, compact = false) {
292
283
  return [
293
284
  record.message,
294
285
  ...explained(record),
295
- `${copy.pending}${record.questions ? copy.everyQuestion : ''}${record.allowFreeText ? copy.freeText : ''}${record.textField ? copy.storedText : ''}`,
286
+ [
287
+ copy.pending,
288
+ record.questions && copy.everyQuestion,
289
+ record.allowFreeText && copy.freeText,
290
+ record.textField && copy.storedText,
291
+ ]
292
+ .filter(Boolean)
293
+ .join(' '),
296
294
  ]
297
295
  .filter(Boolean)
298
296
  .join('\n\n');
299
297
  }
300
298
  function copyFor(record) {
301
- return questionnaireCopy[record.language?.startsWith('tr') ? 'tr' : 'en'];
299
+ return (questionnaireCopy.safeParse(texts('questionnaire', record.language ?? 'en')).data ??
300
+ questionnaireCopy.parse(texts('questionnaire', 'en')));
302
301
  }
303
302
  function optionLabels(options, copy) {
304
303
  const recommended = options.find((option) => option.label.includes(`(${copy.recommended})`))?.id ?? options[0].id;
@@ -1,10 +1,14 @@
1
1
  import * as z from 'zod/v4';
2
2
  import { languageTag } from '../runtime/questionnaire-store.js';
3
3
  import { assertSafeToPersist } from '../runtime/offline-outbox.js';
4
+ import { texts } from '../runtime/texts.js';
4
5
  import { sha256, stableStringify } from '../utilities/hash.js';
5
6
  import { answerChoice, answerData, askQuestionnaire } from './questionnaire-tools.js';
6
7
  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 navigation = z.strictObject({
9
+ label: z.string().trim().min(1).max(120),
10
+ description: z.string().trim().min(1).max(200),
11
+ });
8
12
  const wording = z.strictObject({
9
13
  fields: z.record(field, z.strictObject({
10
14
  title: z.string().trim().min(1).max(80),
@@ -61,87 +65,9 @@ const pageSchema = z.object({
61
65
  offset: z.number().int().min(0),
62
66
  limit: z.literal(100),
63
67
  });
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
68
  export function registerStatusMeaningTools(server, service) {
143
69
  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.',
70
+ 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. Copy comes from the product text catalogue when it has the conversation language; otherwise supply every copy field. Does not move work or fire events.',
145
71
  inputSchema,
146
72
  }, (input, context) => describeStatus(server, service, input, context));
147
73
  }
@@ -156,8 +82,8 @@ async function describeStatus(server, service, input, context) {
156
82
  catch {
157
83
  return refusal('Remove sensitive text from the description and its retry arguments.', 'work_item.describe_status');
158
84
  }
159
- const primary = input.language.split('-')[0];
160
- const copy = input.copy ?? (primary === 'tr' || primary === 'en' ? copies[primary] : undefined);
85
+ await service.language(input.language);
86
+ const copy = input.copy ?? wording.safeParse(texts('statusMeaning', input.language)).data;
161
87
  if (!copy)
162
88
  return refusal('Supply every copy field in the conversation language.', 'work_item.describe_status');
163
89
  let target;
@@ -219,8 +145,8 @@ async function describeStatus(server, service, input, context) {
219
145
  inputQuestionnaireId = 'status-description-input-' + setupId;
220
146
  const options = ['keep', 'write', 'clear'].map((id) => ({
221
147
  id,
222
- label: copy[id][0],
223
- description: copy[id][1] + (id === input.recommendation ? ' ' + input.reason : ''),
148
+ label: copy[id].label,
149
+ description: copy[id].description + (id === input.recommendation ? ' ' + input.reason : ''),
224
150
  }));
225
151
  options.sort((a, b) => Number(b.id === input.recommendation) - Number(a.id === input.recommendation));
226
152
  const question = {
@@ -312,8 +238,8 @@ async function describeStatus(server, service, input, context) {
312
238
  context: copy.review.context,
313
239
  example: copy.review.example,
314
240
  options: [
315
- { id: final ? 'save' : 'continue', label: action[0], description: action[1] },
316
- { id: 'defer', label: copy.defer[0], description: copy.defer[1] },
241
+ { id: final ? 'save' : 'continue', label: action.label, description: action.description },
242
+ { id: 'defer', label: copy.defer.label, description: copy.defer.description },
317
243
  ],
318
244
  binding: {
319
245
  setupId,
@@ -0,0 +1,252 @@
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 { texts } from '../runtime/texts.js';
5
+ import { sha256, stableStringify } from '../utilities/hash.js';
6
+ import { answerChoice, askQuestionnaire } from './questionnaire-tools.js';
7
+ const event = z.enum([
8
+ 'work_started',
9
+ 'draft_pr_opened',
10
+ 'pr_opened',
11
+ 'all_prs_merged',
12
+ 'test_failed',
13
+ 'blocked_declared',
14
+ ]);
15
+ const navigation = z.strictObject({
16
+ label: z.string().trim().min(1).max(120),
17
+ description: z.string().trim().min(1).max(200),
18
+ });
19
+ const wording = z.strictObject({
20
+ message: z.string().trim().min(1).max(240),
21
+ context: z.string().trim().min(1).max(400),
22
+ example: z.string().trim().min(1).max(400),
23
+ open: z.string().trim().min(1).max(40),
24
+ archived: z.string().trim().min(1).max(40),
25
+ rolesStay: z.string().trim().min(1).max(240),
26
+ roles: z.record(event, z.string().trim().min(1).max(80)),
27
+ reason: z.string().trim().min(1).max(40),
28
+ page: z.string().trim().min(1).max(40),
29
+ next: navigation,
30
+ defer: navigation,
31
+ apply: navigation,
32
+ });
33
+ const inputSchema = z.strictObject({
34
+ repoRoot: z.string().min(1),
35
+ projectId: z.string().uuid(),
36
+ externalTaskId: z.string().trim().min(2).max(160),
37
+ requestKey: z.string().regex(/^[A-Za-z0-9_-]{1,80}$/),
38
+ expectedSnapshot: z.string().regex(/^[a-f0-9]{64}$/),
39
+ moves: z
40
+ .array(z.strictObject({ fromStatusId: z.string().uuid(), toStatusId: z.string().uuid() }))
41
+ .min(1)
42
+ .max(40)
43
+ .refine((values) => new Set(values.map((value) => value.fromStatusId)).size === values.length),
44
+ reason: z.string().trim().min(1).max(400),
45
+ language: languageTag,
46
+ copy: wording.optional(),
47
+ decisionAttempt: z.number().int().min(1).max(10000).optional(),
48
+ presentation: z.literal('host_native').optional(),
49
+ });
50
+ const stage = z.object({
51
+ id: z.string().uuid(),
52
+ name: z.string().min(1).max(120),
53
+ slug: z.string().min(1).max(64),
54
+ archivedAt: z.string().nullable(),
55
+ activeItems: z.number().int().min(0),
56
+ archivedItems: z.number().int().min(0),
57
+ });
58
+ const pageSchema = z.object({
59
+ snapshot: z.string().regex(/^[a-f0-9]{64}$/),
60
+ items: z.array(stage).max(100),
61
+ total: z.number().int().min(0),
62
+ offset: z.number().int().min(0),
63
+ limit: z.literal(100),
64
+ eventRules: z.array(z.object({
65
+ event,
66
+ targetStatus: z.object({ id: z.string().uuid() }).nullable(),
67
+ })),
68
+ });
69
+ export function registerStatusRemapTools(server, service) {
70
+ server.registerTool('work_item.remap_statuses', {
71
+ description: 'Move all work in chosen project stages to other active stages in one confirmed operation. Resume the bound write task and read every work_item.workflow page first; supply that exact snapshot, the explicit from/to stage pairs the user decided and a short reason in the conversation language. Sources may be archived stages; targets must be active; a stage cannot be both a source and a target. The review shows each group with its open and archived counts and any roles still aimed at a source stage; approval is bound to those counts and applies only if they are unchanged. It is critical under the task mode; feedback never applies it. Retry identical input to resume; reconsider a deferred attempt only when authorized. Copy comes from the product text catalogue when it has the conversation language; otherwise supply every copy field. Roles, stage definitions, order and archive state are not changed.',
72
+ inputSchema,
73
+ }, (input, context) => remapStatuses(server, service, input, context));
74
+ }
75
+ async function remapStatuses(server, service, input, context) {
76
+ const scope = await service.workItemSetupContext(input);
77
+ if (!scope.ok)
78
+ return output(scope);
79
+ const { repoRoot: _repoRoot, ...retryArguments } = input;
80
+ try {
81
+ assertSafeToPersist(retryArguments, '', '', true);
82
+ }
83
+ catch {
84
+ return refusal('Remove sensitive text from the reason and its retry arguments.', 'work_item.remap_statuses');
85
+ }
86
+ await service.language(input.language);
87
+ const copy = input.copy ?? wording.safeParse(texts('statusRemap', input.language)).data;
88
+ if (!copy)
89
+ return refusal('Supply every copy field in the conversation language.', 'work_item.remap_statuses');
90
+ const stages = [];
91
+ let rules = [];
92
+ let total;
93
+ for (let offset = 0; total === undefined || offset < total; offset += 100) {
94
+ const response = await service.workItemWorkflow({
95
+ projectId: input.projectId,
96
+ snapshot: input.expectedSnapshot,
97
+ includeArchived: true,
98
+ limit: 100,
99
+ offset,
100
+ });
101
+ if (!response.ok)
102
+ return output(response);
103
+ const parsed = pageSchema.safeParse(response.data);
104
+ if (!parsed.success)
105
+ return refusal('The workflow response is incomplete. Reload it before reviewing the move.', 'work_item.workflow');
106
+ const page = parsed.data;
107
+ if (page.snapshot !== input.expectedSnapshot ||
108
+ page.offset !== offset ||
109
+ (total !== undefined && page.total !== total) ||
110
+ page.items.length !== Math.min(100, Math.max(0, page.total - offset)))
111
+ return refusal('The workflow pages changed or were incomplete. Reload every page before reviewing the move.', 'work_item.workflow');
112
+ total = page.total;
113
+ rules = page.eventRules;
114
+ stages.push(...page.items);
115
+ }
116
+ const find = (id) => stages.find((item) => item.id === id);
117
+ const sourceIds = input.moves.map((move) => move.fromStatusId);
118
+ const targetIds = input.moves.map((move) => move.toStatusId);
119
+ if (new Set(stages.map((item) => item.id)).size !== stages.length ||
120
+ sourceIds.some((id) => targetIds.includes(id) || !find(id)) ||
121
+ targetIds.some((id) => find(id)?.archivedAt !== null))
122
+ return refusal('Move work from stages of this project to its active stages, without using a stage as both a source and a target.', 'work_item.workflow');
123
+ const moves = input.moves.map((move) => ({
124
+ ...move,
125
+ expectedActiveItems: find(move.fromStatusId).activeItems,
126
+ expectedArchivedItems: find(move.fromStatusId).archivedItems,
127
+ }));
128
+ if (moves.every((move) => move.expectedActiveItems + move.expectedArchivedItems === 0))
129
+ return output({ ok: true, data: { status: 'unchanged', applied: false } });
130
+ const label = (id) => find(id).name + ' (' + find(id).slug + ')';
131
+ const kept = rules
132
+ .filter((rule) => rule.targetStatus && sourceIds.includes(rule.targetStatus.id))
133
+ .map((rule) => copy.roles[rule.event] + ' → ' + label(rule.targetStatus.id));
134
+ const lines = [
135
+ ...moves.map((move) => label(move.fromStatusId) +
136
+ ' → ' +
137
+ label(move.toStatusId) +
138
+ ': ' +
139
+ copy.open +
140
+ ' ' +
141
+ move.expectedActiveItems +
142
+ ', ' +
143
+ copy.archived +
144
+ ' ' +
145
+ move.expectedArchivedItems),
146
+ ...(kept.length ? ['', copy.rolesStay, ...kept] : []),
147
+ '',
148
+ copy.reason + ': ' + input.reason,
149
+ ];
150
+ try {
151
+ assertSafeToPersist(lines, '', '', true);
152
+ }
153
+ catch {
154
+ return refusal('A stage name contains content that cannot be included in a durable question.', 'work_item.workflow');
155
+ }
156
+ const pages = [];
157
+ for (const line of lines) {
158
+ const last = pages.length - 1;
159
+ if (last >= 0 && pages[last].length + 1 + line.length <= 1400)
160
+ pages[last] += '\n' + line;
161
+ else
162
+ pages.push(line);
163
+ }
164
+ const namespace = scope.data.namespace;
165
+ const { presentation: _presentation, ...decisionInput } = retryArguments;
166
+ const proposal = {
167
+ projectId: input.projectId,
168
+ expectedSnapshot: input.expectedSnapshot,
169
+ moves,
170
+ };
171
+ const proposalDigest = sha256(stableStringify(proposal));
172
+ const setupId = sha256(stableStringify({ namespace, decisionInput, proposalDigest, pages }));
173
+ const reviewQuestionnaireIds = pages.map((_, index) => 'status-remap-review-' + sha256(stableStringify({ setupId, index })));
174
+ const owner = {
175
+ tool: 'work_item.remap_statuses',
176
+ externalTaskId: input.externalTaskId,
177
+ decisionAttempt: input.decisionAttempt,
178
+ retryArguments,
179
+ };
180
+ for (let index = 0; index < pages.length; index++) {
181
+ if (context.mcpReq.signal.aborted)
182
+ return output({ ok: true, data: { status: 'pending', reason: 'interrupted' } });
183
+ const final = index === pages.length - 1;
184
+ const action = final ? copy.apply : copy.next;
185
+ const question = {
186
+ questionnaireId: reviewQuestionnaireIds[index],
187
+ language: input.language,
188
+ impact: 'critical',
189
+ message: copy.message +
190
+ '\n\n' +
191
+ pages[index] +
192
+ '\n\n' +
193
+ copy.page +
194
+ ' ' +
195
+ (index + 1) +
196
+ '/' +
197
+ pages.length,
198
+ context: copy.context,
199
+ example: copy.example,
200
+ options: [
201
+ { id: final ? 'apply' : 'continue', label: action.label, description: action.description },
202
+ { id: 'defer', label: copy.defer.label, description: copy.defer.description },
203
+ ],
204
+ binding: { setupId, namespace, proposalDigest, reviewQuestionnaireIds, index },
205
+ };
206
+ const answer = await askQuestionnaire(server, service, { ...question, repoRoot: input.repoRoot, presentation: input.presentation }, context, [], owner);
207
+ const choice = answerChoice(answer);
208
+ if (choice === 'defer')
209
+ return deferred(input);
210
+ if (choice !== (final ? 'apply' : 'continue'))
211
+ return answer;
212
+ }
213
+ return output(await service.workItemApplyStatusRemap({
214
+ ...proposal,
215
+ repoRoot: input.repoRoot,
216
+ externalTaskId: input.externalTaskId,
217
+ reviewQuestionnaireIds,
218
+ }));
219
+ }
220
+ function deferred(input) {
221
+ return output({
222
+ ok: true,
223
+ data: {
224
+ status: 'deferred',
225
+ applied: false,
226
+ ...(input.decisionAttempt === 10000
227
+ ? {}
228
+ : {
229
+ reconsider: {
230
+ tool: 'work_item.remap_statuses',
231
+ arguments: { ...input, decisionAttempt: (input.decisionAttempt ?? 0) + 1 },
232
+ },
233
+ }),
234
+ nextAction: input.decisionAttempt === 10000
235
+ ? 'This attempt remains deferred. Use a new requestKey only after reconsideration is explicitly authorized.'
236
+ : 'No work was moved. Identical retries keep this deferral. Reconsider only after explicit user intent or a reasoned current delegated decision.',
237
+ },
238
+ });
239
+ }
240
+ function refusal(message, recovery) {
241
+ return output({
242
+ ok: false,
243
+ error: { kind: 'status_remap', message, recovery, retryable: false },
244
+ });
245
+ }
246
+ function output(result) {
247
+ return {
248
+ content: [{ type: 'text', text: JSON.stringify(result) }],
249
+ isError: !result.ok,
250
+ };
251
+ }
252
+ //# sourceMappingURL=status-remap-tools.js.map
@@ -111,6 +111,7 @@ export const toolAnnotations = {
111
111
  'work_item.configure_workflow': write,
112
112
  'work_item.setup_workflow': write,
113
113
  'work_item.describe_status': write,
114
+ 'work_item.remap_statuses': write,
114
115
  'work_item.update_status': write,
115
116
  'work_item.create_status': write,
116
117
  'work_item.archive_status': write,
@@ -1,5 +1,6 @@
1
1
  import { registerDeliveryTools } from './delivery-tools.js';
2
2
  import { registerStatusMeaningTools } from './status-meaning-tools.js';
3
+ import { registerStatusRemapTools } from './status-remap-tools.js';
3
4
  import { registerWorkflowTools } from './workflow-tools.js';
4
5
  import { registerReleaseTools } from './release-tools.js';
5
6
  import { ensureDecisionMode, registerDecisionTools } from './decision-tools.js';
@@ -8,6 +9,7 @@ import { validationIds } from '../runtime/bridge-service.js';
8
9
  import { stableStringify } from '../utilities/hash.js';
9
10
  import { hostAnswerSchema, languageTag, questionnaireDefinitionSchema, } from '../runtime/questionnaire-store.js';
10
11
  import { answerChoice, answerQuestionnaireFromHost, askFailed, askQuestionnaire, resumeQuestionnaire, } from './questionnaire-tools.js';
12
+ import { copies, format } from '../runtime/texts.js';
11
13
  const optionalRepoRoot = z.string().min(1).optional();
12
14
  const stringList = z.array(z.string().min(1));
13
15
  const jsonValue = z.lazy(() => z.union([
@@ -146,6 +148,7 @@ export const engineeringMemoryToolNames = [
146
148
  'work_item.configure_workflow',
147
149
  'work_item.setup_workflow',
148
150
  'work_item.describe_status',
151
+ 'work_item.remap_statuses',
149
152
  'work_item.update_status',
150
153
  'work_item.create_status',
151
154
  'work_item.archive_status',
@@ -178,43 +181,23 @@ const reconciliationEntry = z.object({
178
181
  .describe('Optional for approved_revision; resolved from the approved proposal when omitted.'),
179
182
  reason: z.string().optional(),
180
183
  });
181
- const waiverCopy = {
182
- en: {
183
- ask: (task) => `${task}: skip the UI check (widget, golden or screenshot test) for this task?`,
184
- files: (listed) => `Changed files that draw the interface: ${listed}`,
185
- reason: (reason) => `The reason you gave: ${reason}`,
186
- context: 'The UI check is what proves the screens this task changed still render. Skipping it means nothing in this task opens them, and the skip is stored in the task audit trail with the reason above.',
187
- example: 'A layout this change broke would pass verification and first show up when someone opens the app.',
188
- waive: [
189
- 'Skip the UI check',
190
- 'The task verifies without a widget, golden or screenshot test of these files, and the skip is recorded.',
191
- ],
192
- require: [
193
- 'Keep the UI check required',
194
- 'Nothing is skipped. The task stays unverified until a widget, golden or screenshot test covers these files.',
195
- ],
196
- },
197
- tr: {
198
- ask: (task) => `${task}: bu görev için UI kontrolünü (widget, golden veya ekran görüntüsü testi) atlayalım mı?`,
199
- files: (listed) => `Arayüzü çizen değişen dosyalar: ${listed}`,
200
- reason: (reason) => `Verdiğin gerekçe: ${reason}`,
201
- context: 'UI kontrolü, bu görevde değişen ekranların hâlâ doğru çizildiğini gösteren testtir. Atlarsan bu görevde o ekranları açan hiçbir test kalmaz ve atlama kararı yukarıdaki gerekçesiyle görevin denetim kaydına yazılır.',
202
- example: 'Bu değişiklikte bozulan bir yerleşim doğrulamadan geçer ve ancak uygulamayı açan biri fark ettiğinde ortaya çıkar.',
203
- waive: [
204
- 'UI kontrolünü atla',
205
- 'Görev, bu dosyalar için widget, golden veya ekran görüntüsü testi olmadan doğrulanır ve atlama kaydedilir.',
206
- ],
207
- require: [
208
- 'UI kontrolü zorunlu kalsın',
209
- 'Hiçbir şey atlanmaz. Bu dosyaları kapsayan bir widget, golden veya ekran görüntüsü testi gelene kadar görev doğrulanmaz.',
210
- ],
211
- },
212
- };
184
+ const text = z.string().min(1);
185
+ const option = z.strictObject({ label: text, description: text });
186
+ const waiverWording = z.strictObject({
187
+ ask: text,
188
+ files: text,
189
+ reason: text,
190
+ context: text,
191
+ example: text,
192
+ waive: option,
193
+ require: option,
194
+ });
213
195
  export function registerQuestionnaireTools(server, service) {
214
196
  registerReleaseTools(server, service);
215
197
  registerDecisionTools(server, service);
216
198
  registerWorkflowTools(server, service);
217
199
  registerStatusMeaningTools(server, service);
200
+ registerStatusRemapTools(server, service);
218
201
  server.registerTool('questionnaire.answer_from_host', {
219
202
  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.',
220
203
  inputSchema: hostAnswerSchema.extend({
@@ -576,10 +559,7 @@ export function registerEngineeringMemoryTools(server, service) {
576
559
  .max(500),
577
560
  }),
578
561
  }, async (input, context) => {
579
- const language = (await service.language(input.language))?.startsWith('tr')
580
- ? 'tr'
581
- : 'en';
582
- const review = { ...input, language };
562
+ const review = { ...input, language: await service.language(input.language) };
583
563
  let questions;
584
564
  try {
585
565
  questions = await service.ruleDeviationQuestions(review);
@@ -709,7 +689,7 @@ export function registerEngineeringMemoryTools(server, service) {
709
689
  }),
710
690
  }, async (input, context) => {
711
691
  const { language: told, ...request } = input;
712
- const language = (await service.language(told))?.startsWith('tr') ? 'tr' : 'en';
692
+ const wording = copies(waiverWording, 'uiWaiver', await service.language(told));
713
693
  for (const waiver of request.waivers ?? []) {
714
694
  const subject = await service.validationWaiverSubject({
715
695
  repoRoot: request.repoRoot,
@@ -721,20 +701,21 @@ export function registerEngineeringMemoryTools(server, service) {
721
701
  return toolResult(subject);
722
702
  const { questionnaireId, externalTaskId, paths, reason } = subject.data;
723
703
  const listed = paths.slice(0, 20).join(', ') + (paths.length > 20 ? ` (+${paths.length - 20})` : '');
724
- const definitions = ['tr', 'en'].map((displayLanguage) => {
725
- const copy = waiverCopy[displayLanguage];
726
- return {
727
- questionnaireId,
728
- language: displayLanguage,
729
- message: [copy.ask(externalTaskId), copy.files(listed), copy.reason(reason)].join('\n'),
730
- context: copy.context,
731
- example: copy.example,
732
- options: [
733
- { id: 'waive', label: copy.waive[0], description: copy.waive[1] },
734
- { id: 'require', label: copy.require[0], description: copy.require[1] },
735
- ],
736
- };
737
- });
704
+ const definitions = wording.map(({ language, copy }) => ({
705
+ questionnaireId,
706
+ language,
707
+ message: [
708
+ format(copy.ask, language, { task: externalTaskId }),
709
+ format(copy.files, language, { files: listed }),
710
+ format(copy.reason, language, { reason }),
711
+ ].join('\n'),
712
+ context: copy.context,
713
+ example: copy.example,
714
+ options: [
715
+ { id: 'waive', label: copy.waive.label, description: copy.waive.description },
716
+ { id: 'require', label: copy.require.label, description: copy.require.description },
717
+ ],
718
+ }));
738
719
  const earlier = [
739
720
  {
740
721
  questionnaireId,
@@ -755,10 +736,7 @@ export function registerEngineeringMemoryTools(server, service) {
755
736
  ],
756
737
  },
757
738
  ];
758
- const form = await askQuestionnaire(server, service, {
759
- ...definitions.find((definition) => definition.language === language),
760
- repoRoot: request.repoRoot,
761
- }, context, [...definitions, ...earlier]);
739
+ const form = await askQuestionnaire(server, service, { ...definitions[0], repoRoot: request.repoRoot }, context, [...definitions, ...earlier]);
762
740
  if (askFailed(form))
763
741
  return form;
764
742
  const record = await service.questionnaireResume({