engineering-memory 1.11.17 → 1.11.19

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.
@@ -1,3 +1,6 @@
1
+ import { ignoredRuntimeFiles, localRelativePath } from './worktree-preparation.js';
2
+ import { decisionModeSchema } from './decision-mode-store.js';
3
+ import { delegatedReasonSchema } from './questionnaire-store.js';
1
4
  import { unreadableWorktree, } from './worktree-pool.js';
2
5
  import { validWorktreePolicy } from './worktree-policy.js';
3
6
  import { resolveTaskBase } from './branch-preferences.js';
@@ -57,6 +60,202 @@ export class BridgeService {
57
60
  await this.dependencies.languages.remember(principalHash, told);
58
61
  return told ?? (await this.dependencies.languages.read(principalHash));
59
62
  }
63
+ async decisionModeStatus(input) {
64
+ const scope = await this.questionnaireScope(input.repoRoot);
65
+ return this.dependencies.questionnaires.decisions.read(scope, this.dependencies.client.namespace, input.externalTaskId);
66
+ }
67
+ async decisionModeQuestion(input) {
68
+ const scope = await this.questionnaireScope(input.repoRoot);
69
+ if (!scope.projectId || !(await this.dependencies.credentials.get('access-token')))
70
+ throw refuse('Select a signed-in project before choosing a task decision mode.', 'session.entry');
71
+ const namespace = this.dependencies.client.namespace;
72
+ const id = 'decision-mode-' +
73
+ sha256(stableStringify({
74
+ scope,
75
+ namespace,
76
+ task: input.externalTaskId,
77
+ version: input.expectedVersion,
78
+ ...(input.decisionAttempt ? { decisionAttempt: input.decisionAttempt } : {}),
79
+ }));
80
+ const current = await this.decisionModeStatus(input);
81
+ if (current.version !== input.expectedVersion &&
82
+ !(current.version === input.expectedVersion + 1 && current.questionnaireId === id))
83
+ throw refuse('The mode changed. Read decision.mode_status before opening another mode question.', 'decision.mode_status');
84
+ const language = await this.language(input.language);
85
+ const existing = await this.dependencies.questionnaires.get(scope, id);
86
+ if (existing) {
87
+ if (existing.owner?.tool !== 'decision.mode' ||
88
+ existing.owner.externalTaskId !== input.externalTaskId)
89
+ throw refuse('This mode question has a different owner. Start a new decisionAttempt.', 'decision.mode');
90
+ if (existing.status === 'withdrawn')
91
+ throw refuse('The mode question was withdrawn. When the user resumes this choice, increment decisionAttempt without changing expectedVersion.', 'decision.mode');
92
+ return {
93
+ questionnaireId: id,
94
+ message: existing.message,
95
+ context: existing.context,
96
+ example: existing.example,
97
+ language: existing.language,
98
+ options: existing.options,
99
+ binding: existing.binding,
100
+ };
101
+ }
102
+ const tr = language?.startsWith('tr');
103
+ const definition = {
104
+ questionnaireId: id,
105
+ message: tr
106
+ ? input.expectedVersion
107
+ ? 'Hangi moda geçelim?'
108
+ : 'Bu taskta nasıl ilerleyelim?'
109
+ : input.expectedVersion
110
+ ? 'Which mode should we switch to?'
111
+ : 'How should we work on this task?',
112
+ context: tr
113
+ ? 'Seçim bu task için geçerli; istediğinde değiştirebilirsin.'
114
+ : 'This choice applies to this task; you can change it anytime.',
115
+ example: tr
116
+ ? 'Örneğin canlıya alma kararını ajana bırakabilir veya kendin onaylayabilirsin.'
117
+ : 'For example, you can delegate a release decision or approve it yourself.',
118
+ language: tr ? 'tr' : 'en',
119
+ options: tr
120
+ ? [
121
+ {
122
+ id: 'approve_for_me',
123
+ label: 'Benim için onayla',
124
+ description: 'Sıradan kararları ben veririm; çok kritik olanları sana sorarım.',
125
+ },
126
+ {
127
+ id: 'autonomous',
128
+ label: 'Otonom',
129
+ description: 'Seçenekleri senin hedeflerine göre değerlendirip kararları gerekçesiyle veririm.',
130
+ },
131
+ {
132
+ id: 'ask',
133
+ label: 'Onay iste',
134
+ description: 'Karar gereken her durumda sana sorarım.',
135
+ },
136
+ ]
137
+ : [
138
+ {
139
+ id: 'approve_for_me',
140
+ label: 'Approve for me',
141
+ description: 'I decide routine matters and ask you about critical ones.',
142
+ },
143
+ {
144
+ id: 'autonomous',
145
+ label: 'Autonomous',
146
+ description: 'I weigh the choices against your goals and record my reasoning.',
147
+ },
148
+ {
149
+ id: 'ask',
150
+ label: 'Ask for approval',
151
+ description: 'I ask you whenever a decision is needed.',
152
+ },
153
+ ],
154
+ binding: {
155
+ namespace,
156
+ externalTaskId: input.externalTaskId,
157
+ expectedVersion: input.expectedVersion,
158
+ },
159
+ };
160
+ return definition;
161
+ }
162
+ async decisionModeSelect(input) {
163
+ const record = await this.questionnaireResume(input);
164
+ const namespace = this.dependencies.client.namespace;
165
+ if (record.owner?.tool !== 'decision.mode' ||
166
+ record.owner.externalTaskId !== input.externalTaskId ||
167
+ record.binding?.namespace !== namespace ||
168
+ record.binding?.externalTaskId !== input.externalTaskId ||
169
+ record.binding?.expectedVersion !== input.expectedVersion ||
170
+ record.status !== 'answered' ||
171
+ !record.answerAvailable ||
172
+ record.answerSource?.kind === 'delegated_agent')
173
+ throw refuse('A mode can only be selected through its native task mode question.', 'decision.mode');
174
+ const mode = decisionModeSchema.parse(record.answer?.choice);
175
+ return this.dependencies.questionnaires.decisions.select(record.scope, namespace, input.externalTaskId, input.expectedVersion, mode, record.questionnaireId);
176
+ }
177
+ async questionnaireDecisionPolicy(record, answer) {
178
+ const externalTaskId = record.owner?.externalTaskId;
179
+ const state = externalTaskId
180
+ ? await this.dependencies.questionnaires.decisions.read(record.scope, this.dependencies.client.namespace, externalTaskId)
181
+ : null;
182
+ let impact = record.owner?.tool === 'questionnaire.ask' ? (record.impact ?? 'critical') : 'critical';
183
+ if (record.owner?.tool === 'task.branch' && record.binding) {
184
+ if (!answer)
185
+ impact = 'routine';
186
+ else {
187
+ const selected = taskStartChoice({
188
+ ...record,
189
+ status: 'answered',
190
+ answerAvailable: true,
191
+ ...(record.questions
192
+ ? { answers: answer }
193
+ : { answer: answer }),
194
+ });
195
+ if (selected?.status === 'deferred' ||
196
+ (selected?.status === 'start' && !selected.inPlace && !selected.keepCurrent))
197
+ impact = 'routine';
198
+ }
199
+ }
200
+ if (record.questionnaireId.startsWith('rule-deviation-') ||
201
+ record.questionnaireId.startsWith('validation-waiver-'))
202
+ impact = 'critical';
203
+ const userOnly = record.owner?.tool === 'decision.mode' || !record.scope.projectId || !externalTaskId;
204
+ const delegated = !userOnly &&
205
+ !!state?.configured &&
206
+ (state.mode === 'autonomous' || (state.mode === 'approve_for_me' && impact === 'routine'));
207
+ return { delegated, impact, state, userOnly };
208
+ }
209
+ async assertDecisionCurrent(record) {
210
+ const source = record.answerSource;
211
+ if (source?.kind !== 'delegated_agent')
212
+ return;
213
+ const policy = await this.questionnaireDecisionPolicy(record, record.questions ? record.answers : record.answer);
214
+ if (!policy.delegated ||
215
+ policy.state?.version !== source.modeVersion ||
216
+ policy.state.mode !== source.mode ||
217
+ record.owner?.externalTaskId !== source.externalTaskId ||
218
+ source.namespace !== this.dependencies.client.namespace)
219
+ throw refuse('The mode changed after this delegated answer. Re-evaluate using a new questionnaireId, or increment the owning operation decisionAttempt. Do not execute this answer.', 'questionnaire.resume');
220
+ }
221
+ async questionnaireDecide(input) {
222
+ const scope = await this.questionnaireScope(input.repoRoot);
223
+ const record = await this.dependencies.questionnaires.get(scope, input.questionnaireId);
224
+ if (!record ||
225
+ record.requestKey !== input.requestKey ||
226
+ record.contentHash !== input.contentHash)
227
+ throw refuse('Read the exact pending questionnaire before deciding.', 'questionnaire.resume');
228
+ const externalTaskId = record.owner?.externalTaskId;
229
+ if (!externalTaskId)
230
+ throw refuse('This question has no task delegation scope.', 'questionnaire.resume');
231
+ return this.dependencies.questionnaires.decisions.locked(scope, this.dependencies.client.namespace, externalTaskId, async () => {
232
+ const answer = input.answer ?? input.answers;
233
+ if (!answer || Boolean(input.answer) === Boolean(input.answers))
234
+ throw refuse('Exactly one of answer or answers is required. Resume the original question.', 'questionnaire.resume');
235
+ const policy = await this.questionnaireDecisionPolicy(record, answer);
236
+ if (!policy.delegated || !policy.state || policy.state.version !== input.modeVersion)
237
+ throw refuse('The current mode requires a native answer for this decision, or its version changed. Resume the question.', 'questionnaire.resume');
238
+ await this.assertDecisionCurrent(record);
239
+ const reasoning = delegatedReasonSchema.parse({
240
+ reason: input.reason,
241
+ alternativesConsidered: input.alternativesConsidered,
242
+ userInterestReview: input.userInterestReview,
243
+ });
244
+ const resolved = await this.dependencies.questionnaires.accept(scope, record.questionnaireId, record.requestKey, answer, {
245
+ kind: 'delegated_agent',
246
+ externalTaskId,
247
+ mode: policy.state.mode,
248
+ modeVersion: policy.state.version,
249
+ namespace: this.dependencies.client.namespace,
250
+ impact: policy.impact,
251
+ ...reasoning,
252
+ });
253
+ return {
254
+ ...resolved,
255
+ retry: this.questionnaireRetry(resolved.record, input.repoRoot ?? process.cwd()),
256
+ };
257
+ });
258
+ }
60
259
  async questionnaireAsk(input, previousDefinitions = [], owner) {
61
260
  const scope = await this.questionnaireScope(input.repoRoot, input.preparation);
62
261
  const { repoRoot: _repoRoot, preparation: _preparation, presentation: _presentation, ...definition } = input;
@@ -64,6 +263,7 @@ export class BridgeService {
64
263
  ? null
65
264
  : await this.checkoutTask(scope.repoFingerprint, await this.dependencies.repositories.git.findRoot(input.repoRoot ?? process.cwd()));
66
265
  const record = await this.dependencies.questionnaires.ask(scope, definition, previousDefinitions, owner ?? (askingTask ? { tool: 'questionnaire.ask', externalTaskId: askingTask } : undefined));
266
+ await this.assertDecisionCurrent(record);
67
267
  return { ...record, language: record.language ?? (await this.language()) };
68
268
  }
69
269
  async checkoutTask(repoFingerprint, repoRoot) {
@@ -80,6 +280,7 @@ export class BridgeService {
80
280
  const record = await this.dependencies.questionnaires.get(scope, input.questionnaireId);
81
281
  if (!record)
82
282
  throw refuse('No questionnaire exists for this account and repository binding.', 'session.entry');
283
+ await this.assertDecisionCurrent(record);
83
284
  return { ...record, language: record.language ?? (await this.language()) };
84
285
  }
85
286
  async questionnaireWithdraw(input) {
@@ -96,7 +297,7 @@ export class BridgeService {
96
297
  async acceptWithScope(scope, questionnaireId, requestKey, answer, answerSource) {
97
298
  return this.dependencies.questionnaires.accept(scope, questionnaireId, requestKey, answer, answerSource);
98
299
  }
99
- retryHint(record, repoRoot, attemptOffset = 0) {
300
+ questionnaireRetry(record, repoRoot, attemptOffset = 0) {
100
301
  if (!record.owner || record.owner.tool === 'questionnaire.ask')
101
302
  return undefined;
102
303
  const attempt = (record.owner.decisionAttempt ?? 0) + attemptOffset;
@@ -121,7 +322,7 @@ export class BridgeService {
121
322
  const resolved = await this.acceptWithScope(scope, input.questionnaireId, input.requestKey, (input.answer ?? input.answers), { kind: 'host_native_relay', hostTool: input.hostTool });
122
323
  return {
123
324
  ...resolved,
124
- retry: this.retryHint(resolved.record, input.repoRoot ?? process.cwd()),
325
+ retry: this.questionnaireRetry(resolved.record, input.repoRoot ?? process.cwd()),
125
326
  };
126
327
  }
127
328
  async questionnaireScope(repoRoot, preparation = false) {
@@ -227,7 +428,7 @@ export class BridgeService {
227
428
  .map(([externalTaskId, record]) => asJsonValue({
228
429
  externalTaskId,
229
430
  deferredAt: record.answeredAt ?? record.createdAt,
230
- reconsider: this.retryHint(record, repoRoot, 1),
431
+ reconsider: this.questionnaireRetry(record, repoRoot, 1),
231
432
  nextAction: `Starting ${externalTaskId} is on hold. Mention it only if the user brings that task up, and call reconsider only when they ask to start it.`,
232
433
  }));
233
434
  return { mine, other, deferred };
@@ -415,6 +616,10 @@ export class BridgeService {
415
616
  await this.seedResumeSnapshot(pointer, data);
416
617
  return asJsonValue({
417
618
  ...data,
619
+ decisionMode: await this.decisionModeStatus({
620
+ repoRoot: repository.repoRoot,
621
+ externalTaskId: input.externalTaskId,
622
+ }),
418
623
  checkpoint: checkpointView(checkpoint),
419
624
  repository: publicRepository(repository),
420
625
  localJournal: journalSummary(await this.dependencies.journal.load(projectId, input.externalTaskId)),
@@ -774,6 +979,10 @@ export class BridgeService {
774
979
  return asJsonValue({
775
980
  backend: resumeBackendView(backend, backendFresh || !localJournal.projection),
776
981
  requirements: backend.requirements ?? null,
982
+ decisionMode: await this.decisionModeStatus({
983
+ repoRoot: repository.repoRoot,
984
+ externalTaskId: taskSlug,
985
+ }),
777
986
  ...(contextRefreshRequired
778
987
  ? {
779
988
  staleReason: backend.staleReason ?? null,
@@ -1250,6 +1459,8 @@ export class BridgeService {
1250
1459
  const question = input.questionnaireId
1251
1460
  ? await this.dependencies.questionnaires.get(scope, input.questionnaireId)
1252
1461
  : null;
1462
+ if (question)
1463
+ await this.assertDecisionCurrent(question);
1253
1464
  const digest = sha256(stableStringify(input.choice));
1254
1465
  if (question?.answer?.choice !== 'approve' ||
1255
1466
  (question.binding?.choiceDigest !== digest &&
@@ -1341,6 +1552,8 @@ export class BridgeService {
1341
1552
  const digest = sha256(stableStringify(confirmedInput));
1342
1553
  const scope = await this.questionnaireScope(input.repoRoot);
1343
1554
  const question = await this.dependencies.questionnaires.get(scope, questionnaireId);
1555
+ if (question)
1556
+ await this.assertDecisionCurrent(question);
1344
1557
  if (questionnaireId !== 'sync-start-' + digest ||
1345
1558
  question?.answer?.choice !== 'approve' ||
1346
1559
  input.ref !== snapshot.commit)
@@ -1706,6 +1919,8 @@ export class BridgeService {
1706
1919
  return rule;
1707
1920
  const questionnaireId = ruleDeviationQuestionnaireId(input.taskId, path, rule);
1708
1921
  const question = await this.dependencies.questionnaires.get(scope, questionnaireId);
1922
+ if (question)
1923
+ await this.assertDecisionCurrent(question);
1709
1924
  const choice = question?.status === 'answered' ? question.answer?.choice : undefined;
1710
1925
  if (choice === 'change')
1711
1926
  throw refuse(`The user chose to change ${path} so it follows ${rule.resourceKey}. Change the code, then record task.self_review again with that rule as fixed.`, 'task.self_review');
@@ -1717,7 +1932,12 @@ export class BridgeService {
1717
1932
  deviationApproval: {
1718
1933
  questionnaireId,
1719
1934
  answeredAt: question.answeredAt,
1720
- hostTool: question.answerSource?.hostTool ?? null,
1935
+ hostTool: question.answerSource?.kind === 'host_native_relay'
1936
+ ? question.answerSource.hostTool
1937
+ : null,
1938
+ ...(delegatedEvidence(question)
1939
+ ? { delegatedDecision: delegatedEvidence(question) }
1940
+ : {}),
1721
1941
  },
1722
1942
  };
1723
1943
  })),
@@ -1934,6 +2154,11 @@ export class BridgeService {
1934
2154
  if (recoveredVerification) {
1935
2155
  return asJsonValue({
1936
2156
  ...recoveredVerification,
2157
+ ...(recoveredVerification.verified === true
2158
+ ? {
2159
+ nextAction: 'Call task.close now for this verified task. It opens the mode-governed Git delivery selector. Memory publication approval is separate; do not stop after publishing memory.',
2160
+ }
2161
+ : {}),
1937
2162
  repository: publicRepository(repository),
1938
2163
  });
1939
2164
  }
@@ -2043,6 +2268,8 @@ export class BridgeService {
2043
2268
  for (const waiver of waivers) {
2044
2269
  const questionnaireId = validationWaiverId(input.taskId, waiver.validationId, paths, waiver.reason);
2045
2270
  const question = await this.dependencies.questionnaires.get(scope, questionnaireId);
2271
+ if (question)
2272
+ await this.assertDecisionCurrent(question);
2046
2273
  if (question?.answer?.choice !== 'waive') {
2047
2274
  throw refuse(`The user has not agreed to skip the ${waiver.validationId} check for ${samplePaths(paths) || 'this change'} with this reason. Call task.verify with the waiver so the tool asks them, or provide the evidence.`, 'task.verify');
2048
2275
  }
@@ -2051,7 +2278,14 @@ export class BridgeService {
2051
2278
  questionnaireId,
2052
2279
  reason: waiver.reason,
2053
2280
  paths,
2054
- answerSource: question.answerSource?.hostTool ?? 'mcp_form',
2281
+ answerSource: question.answerSource?.kind === 'delegated_agent'
2282
+ ? 'delegated_agent'
2283
+ : question.answerSource?.kind === 'host_native_relay'
2284
+ ? question.answerSource.hostTool
2285
+ : 'mcp_form',
2286
+ ...(delegatedEvidence(question)
2287
+ ? { delegatedDecision: delegatedEvidence(question) }
2288
+ : {}),
2055
2289
  });
2056
2290
  }
2057
2291
  }
@@ -2129,6 +2363,11 @@ export class BridgeService {
2129
2363
  return asJsonValue({
2130
2364
  ...objectOrEmpty(response.data),
2131
2365
  mode: taskMode,
2366
+ ...(responseData?.verified === true
2367
+ ? {
2368
+ nextAction: 'Call task.close now for this verified task. It opens the mode-governed Git delivery selector. Memory publication approval is separate; do not stop after publishing memory.',
2369
+ }
2370
+ : {}),
2132
2371
  ...(foreignChangedPaths.length > 0
2133
2372
  ? { changedPathsOutsideThisTask: foreignChangedPaths }
2134
2373
  : {}),
@@ -2170,6 +2409,13 @@ export class BridgeService {
2170
2409
  });
2171
2410
  });
2172
2411
  }
2412
+ async taskDeliveryBase(input) {
2413
+ return this.execute(async () => {
2414
+ const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
2415
+ await this.dependencies.repositories.git.validateBranch(repository.repoRoot, input.branch);
2416
+ return { branch: input.branch };
2417
+ });
2418
+ }
2173
2419
  async taskClose(input) {
2174
2420
  return await this.execute(async () => {
2175
2421
  return await this.taskExclusive(input.taskId, async () => {
@@ -2226,6 +2472,47 @@ export class BridgeService {
2226
2472
  return asJsonValue({ allocation });
2227
2473
  });
2228
2474
  }
2475
+ async worktreePrepareFiles(input) {
2476
+ return this.execute(async () => {
2477
+ const repository = await this.dependencies.repositories.resolve(input.repoRoot);
2478
+ const pool = this.dependencies.worktreePool;
2479
+ if (!repository.projectId || !pool)
2480
+ throw refuse('Resume a bound managed worktree before preparing files.', 'session.resume');
2481
+ const allocation = await pool.assertOwnership(repository.projectId, repository.repoRoot, input.generation);
2482
+ if (!allocation.managed ||
2483
+ allocation.phase !== 'working' ||
2484
+ allocation.externalTaskId !== input.externalTaskId)
2485
+ throw refuse('Resume this task with its current worktree generation.', 'session.resume');
2486
+ const source = allocation.sourceRepoRoot;
2487
+ if (!source)
2488
+ throw refuse('Resume worktree preparation before inspecting runtime files.', 'session.resume');
2489
+ const inventory = await ignoredRuntimeFiles(source, allocation.sourceMainRoot ?? source, allocation.repoRoot);
2490
+ let prepared = allocation;
2491
+ if (input.paths !== undefined) {
2492
+ if (input.expectedInventory !== inventory.inventory || input.paths.length > 100)
2493
+ throw refuse('Inspect the current ignored-file inventory before selecting required files.', 'worktree.prepare_files');
2494
+ const paths = input.paths.map(localRelativePath);
2495
+ if (new Set(paths).size !== paths.length)
2496
+ throw refuse('Choose each required runtime path once.', 'worktree.prepare_files');
2497
+ prepared = await pool.prepare(allocation, { inventory: inventory.inventory, paths });
2498
+ this.poolAllocations.set(repository.repoRoot, prepared);
2499
+ }
2500
+ const offset = input.offset ?? 0, limit = input.limit ?? 100;
2501
+ return asJsonValue({
2502
+ repoRoot: prepared.repoRoot,
2503
+ generation: prepared.generation,
2504
+ inventory: inventory.inventory,
2505
+ candidates: inventory.paths.slice(offset, offset + limit),
2506
+ total: inventory.paths.length,
2507
+ offset,
2508
+ limit,
2509
+ nextOffset: offset + limit < inventory.paths.length ? offset + limit : null,
2510
+ selectedPaths: prepared.runtimeFiles?.paths ?? [],
2511
+ readiness: prepared.readiness,
2512
+ nextAction: 'Inspect every candidate page and tracked imports, build/configuration references and explicit user requirements. Select only necessary local runtime files with paths and expectedInventory; use an empty paths array when none are needed. A selection supplements recognized automatic files and acknowledges the reviewed inventory. Keep generated caches/artifacts excluded; paths inside them may be selected only on explicit runtime evidence. Contents remain local and must never be logged, uploaded or committed. Resolve missing/conflicting/unsafe files without overwriting them; a runtimeReview.required result is not a ready worktree.',
2513
+ });
2514
+ });
2515
+ }
2229
2516
  async projectGitPreferences(input) {
2230
2517
  return this.execute(async () => {
2231
2518
  const repository = await this.dependencies.repositories.resolveIdentity(input.repoRoot ?? process.cwd(), input.projectId);
@@ -2648,7 +2935,7 @@ export class BridgeService {
2648
2935
  return asJsonValue({
2649
2936
  ...allocation,
2650
2937
  kept: !allocation.managed,
2651
- nextAction: 'Run every file, terminal, lifecycle and delivery operation from this repoRoot. Inspect readiness separately: a file problem does not cancel the task, and an editor request does not prove its window is visible. Resolve reported local file issues without overwriting existing files, then retry task.branch or session.resume. Use task.heartbeat during long work and pause before handoff.',
2938
+ nextAction: 'Run every file, terminal, lifecycle and delivery operation from this repoRoot. Inspect readiness separately: a file problem does not cancel the task, and an editor request does not prove its window is visible. If files.runtimeReview.required, call worktree.prepare_files in this repoRoot with externalTaskId and generation, inspect all candidate pages against tracked imports/build configuration and user requirements, and select the necessary relative paths before claiming runtime readiness. Resolve reported local file issues without overwriting existing files, then retry task.branch or session.resume. Use task.heartbeat during long work and pause before handoff.',
2652
2939
  });
2653
2940
  }
2654
2941
  adoptOwned(repoRoot, entry) {
@@ -3090,6 +3377,218 @@ export class BridgeService {
3090
3377
  return asJsonValue(response.data);
3091
3378
  });
3092
3379
  }
3380
+ async workItemSetupScope(input) {
3381
+ const scope = await this.questionnaireScope(input.repoRoot);
3382
+ const root = await this.dependencies.repositories.git.findRoot(input.repoRoot);
3383
+ const task = await this.checkoutTask(scope.repoFingerprint, root);
3384
+ const pointer = await this.dependencies.activeContexts.loadForSlug(scope.repoFingerprint, input.externalTaskId);
3385
+ if (scope.projectId !== input.projectId ||
3386
+ task !== input.externalTaskId ||
3387
+ !pointer ||
3388
+ pointer.closedAt ||
3389
+ pointer.mode === 'read_only')
3390
+ throw refuse('Resume the active write task in its bound project before configuring its workflow.', 'session.resume');
3391
+ return scope;
3392
+ }
3393
+ async workItemSetupContext(input) {
3394
+ return this.execute(async () => {
3395
+ await this.workItemSetupScope(input);
3396
+ return {
3397
+ projectId: input.projectId,
3398
+ externalTaskId: input.externalTaskId,
3399
+ namespace: this.dependencies.client.namespace,
3400
+ };
3401
+ });
3402
+ }
3403
+ async workItemApplyWorkflowSetup(input) {
3404
+ return this.execute(async () => {
3405
+ const scope = await this.workItemSetupScope(input);
3406
+ return this.dependencies.questionnaires.decisions.locked(scope, this.dependencies.client.namespace, input.externalTaskId, async () => {
3407
+ const final = await this.questionnaireResume({
3408
+ repoRoot: input.repoRoot,
3409
+ questionnaireId: input.questionnaireId,
3410
+ });
3411
+ const proposal = {
3412
+ projectId: input.projectId,
3413
+ expectedSnapshot: input.expectedSnapshot,
3414
+ rules: input.rules,
3415
+ };
3416
+ if (final.owner?.tool !== 'work_item.setup_workflow' ||
3417
+ final.owner.externalTaskId !== input.externalTaskId ||
3418
+ final.status !== 'answered' ||
3419
+ !final.answerAvailable ||
3420
+ final.answer?.choice !== 'approve' ||
3421
+ final.binding?.namespace !== this.dependencies.client.namespace ||
3422
+ final.binding?.proposalDigest !== sha256(stableStringify(proposal)) ||
3423
+ stableStringify(final.binding?.selectionQuestionnaireIds) !==
3424
+ stableStringify(input.selectionQuestionnaireIds) ||
3425
+ input.selectionQuestionnaireIds.length !== 5 ||
3426
+ input.rules.length !== 5 ||
3427
+ new Set(input.rules.map((rule) => rule.event)).size !== 5 ||
3428
+ input.rules.some((rule) => rule.event === 'draft_pr_opened'))
3429
+ throw refuse('Review the exact five choices through work_item.setup_workflow before saving.', 'work_item.setup_workflow');
3430
+ for (let index = 0; index < input.rules.length; index++) {
3431
+ const rule = input.rules[index];
3432
+ const record = await this.questionnaireResume({
3433
+ repoRoot: input.repoRoot,
3434
+ questionnaireId: input.selectionQuestionnaireIds[index],
3435
+ });
3436
+ if (record.owner?.tool !== 'work_item.setup_workflow' ||
3437
+ record.owner.externalTaskId !== input.externalTaskId ||
3438
+ record.status !== 'answered' ||
3439
+ !record.answerAvailable ||
3440
+ record.answer?.choice !== rule.toStatusId ||
3441
+ record.binding?.setupId !== final.binding?.setupId ||
3442
+ record.binding?.projectId !== input.projectId ||
3443
+ record.binding?.expectedSnapshot !== input.expectedSnapshot ||
3444
+ record.binding?.event !== rule.event ||
3445
+ record.binding?.mode !== rule.mode)
3446
+ throw refuse('A role choice no longer matches the exact workflow review. Resume its original decision.', 'work_item.setup_workflow');
3447
+ }
3448
+ if (stableStringify(await this.workItemSetupScope(input)) !== stableStringify(scope))
3449
+ throw refuse('The task account or repository binding changed before saving.', 'session.resume');
3450
+ const response = await this.dependencies.client.request(endpoints.workItemList(input.projectId) + '/workflow', {
3451
+ method: 'PUT',
3452
+ body: cleanJson({ expectedSnapshot: input.expectedSnapshot, rules: input.rules }),
3453
+ });
3454
+ return asJsonValue(response.data);
3455
+ });
3456
+ });
3457
+ }
3458
+ async workItemApplyStatusDescription(input) {
3459
+ return this.execute(async () => {
3460
+ const scope = await this.workItemSetupScope(input);
3461
+ return this.dependencies.questionnaires.decisions.locked(scope, this.dependencies.client.namespace, input.externalTaskId, async () => {
3462
+ if (input.reviewQuestionnaireIds.length < 1 ||
3463
+ input.reviewQuestionnaireIds.length > 10 ||
3464
+ new Set(input.reviewQuestionnaireIds).size !== input.reviewQuestionnaireIds.length)
3465
+ throw refuse('Review the complete status description before saving.', 'work_item.describe_status');
3466
+ const final = await this.questionnaireResume({
3467
+ repoRoot: input.repoRoot,
3468
+ questionnaireId: input.reviewQuestionnaireIds.at(-1),
3469
+ });
3470
+ const proposal = {
3471
+ projectId: input.projectId,
3472
+ statusId: input.statusId,
3473
+ expectedVersion: input.expectedVersion,
3474
+ field: input.field,
3475
+ value: input.value,
3476
+ };
3477
+ const proposalDigest = sha256(stableStringify(proposal));
3478
+ if (final.owner?.tool !== 'work_item.describe_status' ||
3479
+ final.owner.externalTaskId !== input.externalTaskId ||
3480
+ final.binding?.namespace !== this.dependencies.client.namespace ||
3481
+ final.binding?.proposalDigest !== proposalDigest ||
3482
+ final.binding?.inputQuestionnaireId !== input.inputQuestionnaireId ||
3483
+ stableStringify(final.binding?.reviewQuestionnaireIds) !==
3484
+ stableStringify(input.reviewQuestionnaireIds))
3485
+ throw refuse('This approval does not match the exact description change.', 'work_item.describe_status');
3486
+ for (let index = 0; index < input.reviewQuestionnaireIds.length; index++) {
3487
+ const record = await this.questionnaireResume({
3488
+ repoRoot: input.repoRoot,
3489
+ questionnaireId: input.reviewQuestionnaireIds[index],
3490
+ });
3491
+ if (record.owner?.tool !== 'work_item.describe_status' ||
3492
+ record.owner.externalTaskId !== input.externalTaskId ||
3493
+ record.status !== 'answered' ||
3494
+ !record.answerAvailable ||
3495
+ record.answer?.choice !==
3496
+ (index === input.reviewQuestionnaireIds.length - 1 ? 'save' : 'continue') ||
3497
+ record.binding?.setupId !== final.binding?.setupId ||
3498
+ record.binding?.namespace !== this.dependencies.client.namespace ||
3499
+ record.binding?.proposalDigest !== proposalDigest ||
3500
+ record.binding?.index !== index ||
3501
+ record.binding?.inputQuestionnaireId !== input.inputQuestionnaireId ||
3502
+ stableStringify(record.binding?.reviewQuestionnaireIds) !==
3503
+ stableStringify(input.reviewQuestionnaireIds))
3504
+ throw refuse('A review page is missing or no longer matches this description.', 'work_item.describe_status');
3505
+ }
3506
+ if (input.inputQuestionnaireId) {
3507
+ const draft = await this.questionnaireResume({
3508
+ repoRoot: input.repoRoot,
3509
+ questionnaireId: input.inputQuestionnaireId,
3510
+ });
3511
+ if (draft.owner?.tool !== 'work_item.describe_status' ||
3512
+ draft.owner.externalTaskId !== input.externalTaskId ||
3513
+ draft.status !== 'answered' ||
3514
+ !draft.answerAvailable ||
3515
+ draft.binding?.setupId !== final.binding?.setupId ||
3516
+ draft.binding?.namespace !== this.dependencies.client.namespace ||
3517
+ draft.binding?.projectId !== input.projectId ||
3518
+ draft.binding?.statusId !== input.statusId ||
3519
+ draft.binding?.expectedVersion !== input.expectedVersion ||
3520
+ draft.binding?.field !== input.field ||
3521
+ !((draft.answer?.choice === 'clear' && input.value === '') ||
3522
+ (draft.answer?.choice === 'write' && draft.answer.text === input.value)))
3523
+ throw refuse('The saved draft does not match this exact editable answer.', 'work_item.describe_status');
3524
+ }
3525
+ else if (final.owner.retryArguments?.draft !== input.value) {
3526
+ throw refuse('The supplied draft changed after review.', 'work_item.describe_status');
3527
+ }
3528
+ if (stableStringify(await this.workItemSetupScope(input)) !== stableStringify(scope))
3529
+ throw refuse('The task account or binding changed before saving.', 'session.resume');
3530
+ const response = await this.dependencies.client.request(endpoints.workItemStatuses(input.projectId) + '/' + encodeURIComponent(input.statusId), {
3531
+ method: 'PATCH',
3532
+ body: { expectedVersion: input.expectedVersion, [input.field]: input.value },
3533
+ });
3534
+ return asJsonValue(response.data);
3535
+ });
3536
+ });
3537
+ }
3538
+ async workItemWorkflow(input) {
3539
+ return this.execute(async () => {
3540
+ const query = new URLSearchParams({
3541
+ offset: String(input.offset ?? 0),
3542
+ limit: String(input.limit ?? 50),
3543
+ });
3544
+ if (input.includeArchived !== undefined)
3545
+ query.set('includeArchived', String(input.includeArchived));
3546
+ if (input.snapshot !== undefined)
3547
+ query.set('snapshot', input.snapshot);
3548
+ const response = await this.dependencies.client.request(endpoints.workItemList(input.projectId) + '/workflow?' + query.toString());
3549
+ return asJsonValue(response.data);
3550
+ });
3551
+ }
3552
+ async workItemConfigureWorkflow(input) {
3553
+ return this.execute(async () => {
3554
+ const { projectId, ...body } = input;
3555
+ const response = await this.dependencies.client.request(endpoints.workItemList(projectId) + '/workflow', { method: 'PUT', body: cleanJson(body) });
3556
+ return asJsonValue(response.data);
3557
+ });
3558
+ }
3559
+ async workItemCreateStatus(input) {
3560
+ return this.execute(async () => {
3561
+ const { projectId, ...body } = input;
3562
+ const response = await this.dependencies.client.request(endpoints.workItemStatuses(projectId), { method: 'POST', body: cleanJson(body) });
3563
+ return asJsonValue(response.data);
3564
+ });
3565
+ }
3566
+ async workItemSetStatusArchived(input) {
3567
+ return this.execute(async () => {
3568
+ const response = await this.dependencies.client.request(endpoints.workItemStatuses(input.projectId) +
3569
+ '/' +
3570
+ encodeURIComponent(input.statusId) +
3571
+ (input.archived ? '' : '/restore'), {
3572
+ method: input.archived ? 'DELETE' : 'POST',
3573
+ body: { expectedVersion: input.expectedVersion },
3574
+ });
3575
+ return asJsonValue(response.data);
3576
+ });
3577
+ }
3578
+ async workItemSetInitialStatus(input) {
3579
+ return this.execute(async () => {
3580
+ const { projectId, statusId, ...body } = input;
3581
+ const response = await this.dependencies.client.request(endpoints.workItemStatuses(projectId) + '/' + encodeURIComponent(statusId) + '/initial', { method: 'PUT', body: cleanJson(body) });
3582
+ return asJsonValue(response.data);
3583
+ });
3584
+ }
3585
+ async workItemUpdateStatus(input) {
3586
+ return this.execute(async () => {
3587
+ const { projectId, statusId, ...body } = input;
3588
+ const response = await this.dependencies.client.request(endpoints.workItemStatuses(projectId) + '/' + encodeURIComponent(statusId), { method: 'PATCH', body: cleanJson(body) });
3589
+ return asJsonValue(response.data);
3590
+ });
3591
+ }
3093
3592
  async workItemList(input) {
3094
3593
  return await this.execute(async () => {
3095
3594
  const query = new URLSearchParams();
@@ -3446,7 +3945,7 @@ export class BridgeService {
3446
3945
  async sessionEntry(input = {}) {
3447
3946
  return await this.execute(async () => {
3448
3947
  const timer = startPhaseTimer('session.entry');
3449
- await this.language(input.language);
3948
+ const language = await this.language(input.language);
3450
3949
  const authentication = await this.dependencies.browserAuth.status();
3451
3950
  timer.mark('auth_status');
3452
3951
  let repository;
@@ -3514,6 +4013,9 @@ export class BridgeService {
3514
4013
  .map(([role]) => role)
3515
4014
  : [];
3516
4015
  const taskSelectionRequired = liveTasks.length > 0 || workItems.length > 0;
4016
+ const whatsNew = authenticated && state === 'bound' && !moved.nextAction
4017
+ ? await this.dependencies.releaseNotes?.prepare(this.dependencies.clientVersion, language)
4018
+ : null;
3517
4019
  timer.finish();
3518
4020
  return asJsonValue({
3519
4021
  authenticated,
@@ -3523,6 +4025,7 @@ export class BridgeService {
3523
4025
  liveTasks,
3524
4026
  workItems,
3525
4027
  taskSelectionRequired,
4028
+ ...(whatsNew ? { whatsNew } : {}),
3526
4029
  ...(worktrees ? { worktrees: worktrees.ok ? worktrees.data : worktrees.error } : {}),
3527
4030
  ...(gitPreferences
3528
4031
  ? {
@@ -3554,6 +4057,19 @@ export class BridgeService {
3554
4057
  });
3555
4058
  });
3556
4059
  }
4060
+ async releaseNotesShow(input) {
4061
+ return await this.execute(async () => {
4062
+ await this.questionnaireScope(input.repoRoot);
4063
+ const language = await this.language(input.language);
4064
+ const report = await this.dependencies.releaseNotes?.show(this.dependencies.clientVersion, language);
4065
+ return asJsonValue({ report: report ?? null });
4066
+ });
4067
+ }
4068
+ async releaseNotesPresented(input) {
4069
+ return await this.execute(async () => ({
4070
+ recorded: (await this.dependencies.releaseNotes?.presented(input.reportId, input.claimId)) ?? false,
4071
+ }));
4072
+ }
3557
4073
  async movedAway(repository, projectId) {
3558
4074
  try {
3559
4075
  const response = await this.dependencies.client.request(endpoints.projectResolve, {
@@ -4365,6 +4881,12 @@ export class BridgeService {
4365
4881
  ...closedTask,
4366
4882
  repository: publicRepository(repository),
4367
4883
  recoveredAfterResponseLoss,
4884
+ deliveryContext: {
4885
+ externalTaskId: closedPointer?.taskSlug ?? null,
4886
+ namespace: this.dependencies.client.namespace,
4887
+ diffHash: String(body.diffHash),
4888
+ mode: normalizeTaskMode(closedPointer?.mode),
4889
+ },
4368
4890
  delivery: deliveryQuestion(touchedContract, await this.dependencies.repositories.git.pushDestination(repository.repoRoot), objectValue(closedTask.sourcePublication)?.applicable === true),
4369
4891
  });
4370
4892
  }
@@ -5420,4 +5942,16 @@ function publicMembership(value) {
5420
5942
  }
5421
5943
  return result;
5422
5944
  }
5945
+ function delegatedEvidence(question) {
5946
+ const source = question.answerSource;
5947
+ if (source?.kind !== 'delegated_agent')
5948
+ return undefined;
5949
+ return {
5950
+ mode: source.mode,
5951
+ modeVersion: source.modeVersion,
5952
+ reason: source.reason,
5953
+ alternativesConsidered: source.alternativesConsidered,
5954
+ userInterestReview: source.userInterestReview,
5955
+ };
5956
+ }
5423
5957
  //# sourceMappingURL=bridge-service.js.map