engineering-memory 1.11.22 → 1.11.24

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,6 +1,7 @@
1
+ import { liveQuestionKind } from './live-signals.js';
1
2
  import { ignoredRuntimeFiles, localRelativePath } from './worktree-preparation.js';
2
3
  import { decisionModeSchema } from './decision-mode-store.js';
3
- import { delegatedReasonSchema } from './questionnaire-store.js';
4
+ import { answerSourceName, delegatedReasonSchema } from './questionnaire-store.js';
4
5
  import { unreadableWorktree, } from './worktree-pool.js';
5
6
  import { validWorktreePolicy } from './worktree-policy.js';
6
7
  import { resolveTaskBase } from './branch-preferences.js';
@@ -56,6 +57,9 @@ export class BridgeService {
56
57
  managesWorktrees() {
57
58
  return Boolean(this.dependencies.worktreePool);
58
59
  }
60
+ get live() {
61
+ return this.dependencies.live;
62
+ }
59
63
  async language(told) {
60
64
  const accessToken = await this.dependencies.credentials.get('access-token');
61
65
  const principalHash = accessToken ? principalFingerprint(accessToken) : sha256('anonymous');
@@ -219,7 +223,7 @@ export class BridgeService {
219
223
  alternativesConsidered: input.alternativesConsidered,
220
224
  userInterestReview: input.userInterestReview,
221
225
  });
222
- const resolved = await this.dependencies.questionnaires.accept(scope, record.questionnaireId, record.requestKey, answer, {
226
+ const resolved = await this.acceptWithScope(scope, record.questionnaireId, record.requestKey, answer, {
223
227
  kind: 'delegated_agent',
224
228
  externalTaskId,
225
229
  mode: policy.state.mode,
@@ -242,7 +246,17 @@ export class BridgeService {
242
246
  : await this.checkoutTask(scope.repoFingerprint, await this.dependencies.repositories.git.findRoot(input.repoRoot ?? process.cwd()));
243
247
  const record = await this.dependencies.questionnaires.ask(scope, definition, previousDefinitions, owner ?? (askingTask ? { tool: 'questionnaire.ask', externalTaskId: askingTask } : undefined));
244
248
  await this.assertDecisionCurrent(record);
245
- return { ...record, language: record.language ?? (await this.language()) };
249
+ return await this.liveQuestion({
250
+ ...record,
251
+ language: record.language ?? (await this.language()),
252
+ });
253
+ }
254
+ async liveQuestion(record) {
255
+ if (record.status === 'pending')
256
+ await this.live.questionOpened(liveQuestionKind(record));
257
+ else
258
+ await this.live.questionClosed();
259
+ return record;
246
260
  }
247
261
  async checkoutTask(repoFingerprint, repoRoot) {
248
262
  const worktreeId = sha256(await canonicalPath(repoRoot));
@@ -259,11 +273,14 @@ export class BridgeService {
259
273
  if (!record)
260
274
  throw refuse('No questionnaire exists for this account and repository binding.', 'session.entry');
261
275
  await this.assertDecisionCurrent(record);
262
- return { ...record, language: record.language ?? (await this.language()) };
276
+ return await this.liveQuestion({
277
+ ...record,
278
+ language: record.language ?? (await this.language()),
279
+ });
263
280
  }
264
281
  async questionnaireWithdraw(input) {
265
282
  const scope = await this.questionnaireScope(input.repoRoot, input.preparation);
266
- return this.dependencies.questionnaires.withdraw(scope, input.questionnaireId);
283
+ return await this.liveQuestion(await this.dependencies.questionnaires.withdraw(scope, input.questionnaireId));
267
284
  }
268
285
  async questionnaireAccept(input) {
269
286
  const scope = await this.questionnaireScope(input.repoRoot, input.preparation);
@@ -273,7 +290,9 @@ export class BridgeService {
273
290
  return await this.acceptWithScope(scope, input.questionnaireId, input.requestKey, input.answer, input.answerSource);
274
291
  }
275
292
  async acceptWithScope(scope, questionnaireId, requestKey, answer, answerSource) {
276
- return this.dependencies.questionnaires.accept(scope, questionnaireId, requestKey, answer, answerSource);
293
+ const resolved = await this.dependencies.questionnaires.accept(scope, questionnaireId, requestKey, answer, answerSource);
294
+ await this.liveQuestion(resolved.record);
295
+ return resolved;
277
296
  }
278
297
  questionnaireRetry(record, repoRoot, attemptOffset = 0) {
279
298
  if (!record.owner || record.owner.tool === 'questionnaire.ask')
@@ -574,6 +593,7 @@ export class BridgeService {
574
593
  };
575
594
  await this.dependencies.activeContexts.save(pointer);
576
595
  this.workedTasks.set(sha256(repository.repoRoot), input.externalTaskId);
596
+ await this.live.taskStarted(task.id);
577
597
  await this.dependencies.journal.apply({
578
598
  eventId: checkpointId,
579
599
  taskId: task.id,
@@ -964,6 +984,7 @@ export class BridgeService {
964
984
  ? { closedAt: pointer?.closedAt ?? new Date().toISOString() }
965
985
  : {}),
966
986
  });
987
+ await this.live.taskStarted(backendTask.id);
967
988
  }
968
989
  const backendFresh = responseSource !== 'stale_cache';
969
990
  return asJsonValue({
@@ -1023,16 +1044,19 @@ export class BridgeService {
1023
1044
  }
1024
1045
  async contextPrepareChange(input) {
1025
1046
  const pointer = await this.dependencies.activeContexts.findBySessionId(input.sessionId);
1026
- if (!pointer)
1027
- return this.contextPrepareChangeUnlocked(input);
1028
- try {
1029
- return await this.taskExclusive(pointer.taskId, () => this.contextPrepareChangeUnlocked(input));
1030
- }
1031
- catch (error) {
1032
- return this.execute(async () => {
1047
+ const result = pointer
1048
+ ? await this.taskExclusive(pointer.taskId, () => this.contextPrepareChangeUnlocked(input)).catch((error) => this.execute(async () => {
1033
1049
  throw error;
1034
- });
1035
- }
1050
+ }))
1051
+ : await this.contextPrepareChangeUnlocked(input);
1052
+ return this.liveTaskCall(result);
1053
+ }
1054
+ liveTaskCall(result) {
1055
+ if (result.ok)
1056
+ this.live.settled();
1057
+ else if (result.error?.recovery)
1058
+ this.live.refused(result.error.recovery);
1059
+ return result;
1036
1060
  }
1037
1061
  async contextPrepareChangeUnlocked(input) {
1038
1062
  return await this.execute(async () => {
@@ -1689,10 +1713,31 @@ export class BridgeService {
1689
1713
  catch (error) {
1690
1714
  throw refuse(error instanceof Error ? error.message : 'The resulting commit must be inspected.', 'memory.sync_start');
1691
1715
  }
1716
+ if (!proof)
1717
+ throw refuse('This task has nothing to publish: its project had no inspected source snapshot when the task was verified.', 'memory.sync_start');
1692
1718
  const response = await this.dependencies.client.request('/memory/sources/publish-task', { method: 'POST', body: proof });
1693
1719
  return response.data;
1694
1720
  });
1695
1721
  }
1722
+ async publishDelivered(repoRoot, taskId) {
1723
+ try {
1724
+ const proof = await this.dependencies.gate.publicationProof(repoRoot, taskId);
1725
+ if (!proof)
1726
+ return null;
1727
+ const response = await this.dependencies.client.request('/memory/sources/publish-task', { method: 'POST', body: proof });
1728
+ const answer = objectValue(response.data);
1729
+ return answer?.published === false
1730
+ ? { published: false, reason: String(answer.reason) }
1731
+ : { published: true };
1732
+ }
1733
+ catch (error) {
1734
+ return {
1735
+ published: false,
1736
+ reason: error instanceof Error ? error.message : String(error),
1737
+ nextAction: 'The delivery is recorded; only the source memory of this task was not published. The member who opened the task can call memory.publish_task in this folder before another task reuses it; otherwise memory.sync_start inspects the result.',
1738
+ };
1739
+ }
1740
+ }
1696
1741
  async onboardingRequest(operation, body) {
1697
1742
  return this.execute(async () => {
1698
1743
  if (operation === 'next' || operation === 'submit') {
@@ -1778,7 +1823,7 @@ export class BridgeService {
1778
1823
  });
1779
1824
  }
1780
1825
  async taskCheckpoint(input) {
1781
- return await this.writeTaskEvent(input, false);
1826
+ return this.liveTaskCall(await this.writeTaskEvent(input, false));
1782
1827
  }
1783
1828
  async taskRecordCorrection(input) {
1784
1829
  const reference = input.correctionRef ?? input.summary;
@@ -1864,7 +1909,7 @@ export class BridgeService {
1864
1909
  }));
1865
1910
  }
1866
1911
  async taskSelfReview(input) {
1867
- return await this.execute(async () => {
1912
+ const reviewed = await this.execute(async () => {
1868
1913
  const files = await this.approvedDeviations(input);
1869
1914
  assertSafeToPersist(cleanJson({ files }));
1870
1915
  const checkout = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
@@ -1896,6 +1941,7 @@ export class BridgeService {
1896
1941
  },
1897
1942
  });
1898
1943
  });
1944
+ return this.liveTaskCall(reviewed);
1899
1945
  }
1900
1946
  async approvedDeviations(input) {
1901
1947
  const files = reviewedFiles(input);
@@ -1934,7 +1980,7 @@ export class BridgeService {
1934
1980
  })));
1935
1981
  }
1936
1982
  async taskReconcile(input) {
1937
- return await this.execute(async () => {
1983
+ const reconciled = await this.execute(async () => {
1938
1984
  const repoRoot = input.repoRoot ?? process.cwd();
1939
1985
  const entries = (input.entries ?? [
1940
1986
  {
@@ -1965,6 +2011,7 @@ export class BridgeService {
1965
2011
  });
1966
2012
  return asJsonValue({ reconciliations: response.data });
1967
2013
  });
2014
+ return this.liveTaskCall(reconciled);
1968
2015
  }
1969
2016
  async taskResolvePendingDelivery(input) {
1970
2017
  return await this.execute(async () => {
@@ -2075,7 +2122,7 @@ export class BridgeService {
2075
2122
  }, true);
2076
2123
  }
2077
2124
  async taskVerify(input) {
2078
- return await this.execute(async () => {
2125
+ const verified = await this.execute(async () => {
2079
2126
  const timer = startPhaseTimer('task.verify');
2080
2127
  return await this.taskExclusive(input.taskId, async () => {
2081
2128
  await this.recoverJournalOutbox();
@@ -2268,11 +2315,7 @@ export class BridgeService {
2268
2315
  questionnaireId,
2269
2316
  reason: waiver.reason,
2270
2317
  paths,
2271
- answerSource: question.answerSource?.kind === 'delegated_agent'
2272
- ? 'delegated_agent'
2273
- : question.answerSource?.kind === 'host_native_relay'
2274
- ? question.answerSource.hostTool
2275
- : 'mcp_form',
2318
+ answerSource: answerSourceName(question.answerSource),
2276
2319
  ...(delegatedEvidence(question)
2277
2320
  ? { delegatedDecision: delegatedEvidence(question) }
2278
2321
  : {}),
@@ -2365,6 +2408,7 @@ export class BridgeService {
2365
2408
  });
2366
2409
  });
2367
2410
  });
2411
+ return this.liveTaskCall(verified);
2368
2412
  }
2369
2413
  async validationWaiverSubject(input) {
2370
2414
  return await this.execute(async () => {
@@ -2407,7 +2451,7 @@ export class BridgeService {
2407
2451
  });
2408
2452
  }
2409
2453
  async taskClose(input) {
2410
- return await this.execute(async () => {
2454
+ const closed = await this.execute(async () => {
2411
2455
  return await this.taskExclusive(input.taskId, async () => {
2412
2456
  const checkout = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
2413
2457
  const pointer = await this.requireActivePointer(checkout.repoFingerprint, input.taskId, false, 'close');
@@ -2452,6 +2496,7 @@ export class BridgeService {
2452
2496
  return await this.deliverCloseIntent(repository, closeBody, taskChanges, false);
2453
2497
  });
2454
2498
  });
2499
+ return this.liveTaskCall(closed);
2455
2500
  }
2456
2501
  async worktreeTaskAllocation(input) {
2457
2502
  return this.execute(async () => {
@@ -2545,9 +2590,44 @@ export class BridgeService {
2545
2590
  currentCommit: repository.git.head,
2546
2591
  folder,
2547
2592
  suggestedName,
2593
+ ...(input.workItemId
2594
+ ? await this.workItemBranches(repository.repoRoot, repository.projectId, input.workItemProjectId ?? repository.projectId, input.workItemId, preferences.data)
2595
+ : { planBranch: null, continueBranches: [] }),
2548
2596
  });
2549
2597
  });
2550
2598
  }
2599
+ async workItemBranches(repoRoot, projectId, itemProjectId, workItemId, preferences) {
2600
+ const git = this.dependencies.repositories.git;
2601
+ const [plan, runs, reserved, openDeliveries] = await Promise.all([
2602
+ this.dependencies.client.request(endpoints.workItemPlan(itemProjectId, workItemId)),
2603
+ this.dependencies.client.request(`${endpoints.workItemRuns(itemProjectId, workItemId)}?offset=0&limit=50`),
2604
+ git.protectedBranchNames(repoRoot),
2605
+ this.dependencies.client
2606
+ .request(`${endpoints.projectDeliveries(projectId)}?state=open&limit=100`)
2607
+ .then((response) => response.data?.items ?? [], () => []),
2608
+ ]);
2609
+ for (const preference of Object.values(preferences.branches))
2610
+ if (preference.branch)
2611
+ reserved.add(preference.branch);
2612
+ const used = [
2613
+ ...new Set((runs.data?.items ?? [])
2614
+ .filter((run) => run.projectId === projectId && run.mode !== 'read_only')
2615
+ .map((run) => run.branch)
2616
+ .filter((branch) => !!branch && !reserved.has(branch))),
2617
+ ];
2618
+ const planBranch = plan.data?.confirmed ? plan.data.branch : null;
2619
+ const continueBranches = !planBranch
2620
+ ? used.slice(0, 2)
2621
+ : used.includes(planBranch) || (await git.branchExists(repoRoot, planBranch))
2622
+ ? [planBranch]
2623
+ : [];
2624
+ const open = new Set(openDeliveries.map((delivery) => delivery.taskId));
2625
+ return {
2626
+ planBranch,
2627
+ continueBranches,
2628
+ openDeliveryBranches: continueBranches.filter((branch) => (runs.data?.items ?? []).some((run) => run.branch === branch && open.has(run.id))),
2629
+ };
2630
+ }
2551
2631
  async suggestedBranchName(repoRoot, externalTaskId) {
2552
2632
  const candidates = suggestedBranchNames(externalTaskId);
2553
2633
  for (const candidate of candidates)
@@ -2770,11 +2850,22 @@ export class BridgeService {
2770
2850
  if (operation === 'pause') {
2771
2851
  await pool.pause(projectId, repository.repoRoot, input.generation);
2772
2852
  this.poolAllocations.delete(repository.repoRoot);
2853
+ await this.live.taskPaused(entry.taskId);
2773
2854
  }
2774
2855
  if (operation === 'release') {
2775
2856
  await this.requireDeliveredTaskWork(entry);
2857
+ const sourcePublication = outcome === 'delivered' && entry.taskId && entry.pendingDelivery
2858
+ ? await this.publishDelivered(repository.repoRoot, entry.taskId)
2859
+ : null;
2776
2860
  await pool.release(projectId, repository.repoRoot, input.generation, outcome === 'delivered' ? outcome : undefined);
2777
2861
  this.poolAllocations.delete(repository.repoRoot);
2862
+ await this.live.taskPaused(entry.taskId);
2863
+ return asJsonValue({
2864
+ operation,
2865
+ complete: true,
2866
+ repoRoot: repository.repoRoot,
2867
+ ...(sourcePublication ? { sourcePublication } : {}),
2868
+ });
2778
2869
  }
2779
2870
  if (operation === 'cancel_delivery') {
2780
2871
  if (!entry.pendingDelivery)
@@ -2795,6 +2886,7 @@ export class BridgeService {
2795
2886
  await this.requireDeliveredTaskWork(entry);
2796
2887
  const cancelled = await pool.cancelDelivery(projectId, repository.repoRoot, input.generation, choice);
2797
2888
  this.poolAllocations.delete(repository.repoRoot);
2889
+ await this.live.taskPaused(entry.taskId);
2798
2890
  return asJsonValue({
2799
2891
  operation,
2800
2892
  complete: true,
@@ -2882,7 +2974,8 @@ export class BridgeService {
2882
2974
  throw refuse(`This folder is held by task ${holder.externalTaskId}. Call task.branch again: its start form asks whether ${holder.externalTaskId} should move out of this folder before the new branch starts here, or opens the new task in a separate worktree.`, 'task.branch');
2883
2975
  if (repository.git.changedPaths.length > 0)
2884
2976
  throw refuse(`This folder has uncommitted changes, so the new branch cannot start here and ${holder.externalTaskId} stays in it. Call task.branch with a new decisionAttempt and open the task in a separate worktree.`, 'task.branch');
2885
- if (await git.branchExists(repository.repoRoot, input.name))
2977
+ if (input.base.kind !== 'existing' &&
2978
+ (await git.branchExists(repository.repoRoot, input.name)))
2886
2979
  throw refuse(`Branch ${input.name} already exists, so it cannot start here and ${holder.externalTaskId} stays in this folder. Call task.branch with a new decisionAttempt and choose a new branch name.`, 'task.branch');
2887
2980
  if (holder.generation) {
2888
2981
  await pool.moveOut(projectId, repository.repoRoot, holder.generation, holder.oid ?? undefined);
@@ -2914,6 +3007,7 @@ export class BridgeService {
2914
3007
  keepCurrent: input.keepCurrent,
2915
3008
  inPlace: !existing && input.inPlace,
2916
3009
  resume: Boolean(existing),
3010
+ continued: !existing && input.base?.kind === 'existing',
2917
3011
  }, allocationPolicy));
2918
3012
  allocationTimer.mark('pool_allocate');
2919
3013
  allocationTimer.finish();
@@ -3215,6 +3309,7 @@ export class BridgeService {
3215
3309
  .release(input.taskId);
3216
3310
  const forgotten = await this.dependencies.activeContexts.forget(repository.repoFingerprint, input.taskId);
3217
3311
  await this.dependencies.gate.invalidateTask(input.taskId);
3312
+ this.live.taskEnded(input.taskId);
3218
3313
  return asJsonValue({
3219
3314
  ...abandoned,
3220
3315
  localPointerRemoved: forgotten,
@@ -3725,6 +3820,22 @@ export class BridgeService {
3725
3820
  return asJsonValue(response.data);
3726
3821
  });
3727
3822
  }
3823
+ async workItemComment(input) {
3824
+ return this.execute(async () => {
3825
+ const response = await this.dependencies.client.request(endpoints.workItemComments(input.projectId, input.workItemId), { method: 'POST', body: cleanJson(input.data) });
3826
+ return asJsonValue(response.data);
3827
+ });
3828
+ }
3829
+ async workItemComments(input) {
3830
+ return this.execute(async () => {
3831
+ const query = new URLSearchParams({
3832
+ offset: String(input.offset ?? 0),
3833
+ limit: String(input.limit ?? 50),
3834
+ });
3835
+ const response = await this.dependencies.client.request(`${endpoints.workItemComments(input.projectId, input.workItemId)}?${query}`);
3836
+ return asJsonValue(response.data);
3837
+ });
3838
+ }
3728
3839
  async workItemPlan(input) {
3729
3840
  return this.execute(async () => {
3730
3841
  const response = await this.dependencies.client.request(endpoints.workItemPlan(input.projectId, input.workItemId));
@@ -4023,7 +4134,7 @@ export class BridgeService {
4023
4134
  const livePointers = authenticated && state === 'bound'
4024
4135
  ? this.dependencies.activeContexts.list(repository.repoFingerprint)
4025
4136
  : Promise.resolve([]);
4026
- const [client, liveTasksRaw, workItems, gitPreferences, worktrees, pendingQuestionnaires, moved,] = await Promise.all([
4137
+ const [client, liveTasksRaw, workItems, gitPreferences, worktrees, pendingQuestionnaires, moved, deliveries,] = await Promise.all([
4027
4138
  this.clientUpdate(authenticated),
4028
4139
  livePointers,
4029
4140
  authenticated && state === 'bound' && projectId
@@ -4044,6 +4155,9 @@ export class BridgeService {
4044
4155
  authenticated && state === 'bound' && projectId
4045
4156
  ? this.movedAway(repository, projectId)
4046
4157
  : Promise.resolve({}),
4158
+ authenticated && state === 'bound' && projectId
4159
+ ? this.openDeliveries(projectId)
4160
+ : Promise.resolve(null),
4047
4161
  ]);
4048
4162
  timer.mark('independent_lookups');
4049
4163
  const liveTasks = liveTasksRaw.map(describePointer);
@@ -4056,9 +4170,11 @@ export class BridgeService {
4056
4170
  .map(([role]) => role)
4057
4171
  : [];
4058
4172
  const taskSelectionRequired = liveTasks.length > 0 || workItems.length > 0;
4059
- const whatsNew = authenticated && state === 'bound' && !moved.nextAction
4173
+ const open = authenticated && state === 'bound' && !moved.nextAction;
4174
+ const whatsNew = open
4060
4175
  ? await this.dependencies.releaseNotes?.prepare(this.dependencies.clientVersion, language)
4061
4176
  : null;
4177
+ const liveStatusNotice = open && projectId ? await this.liveStatusNotice(projectId, language) : null;
4062
4178
  timer.finish();
4063
4179
  return asJsonValue({
4064
4180
  authenticated,
@@ -4069,6 +4185,12 @@ export class BridgeService {
4069
4185
  workItems,
4070
4186
  taskSelectionRequired,
4071
4187
  ...(whatsNew ? { whatsNew } : {}),
4188
+ ...(liveStatusNotice
4189
+ ? {
4190
+ liveStatusNotice,
4191
+ liveStatusNoticeAction: 'Show this text to the user once as a short paragraph in their language, then silently call live_status.notice_presented with this exact noticeVersion. It asks nothing, needs no answer and never blocks their work.',
4192
+ }
4193
+ : {}),
4072
4194
  ...(worktrees ? { worktrees: worktrees.ok ? worktrees.data : worktrees.error } : {}),
4073
4195
  ...(gitPreferences
4074
4196
  ? {
@@ -4083,6 +4205,7 @@ export class BridgeService {
4083
4205
  ...(pendingQuestionnaires.deferred.length > 0
4084
4206
  ? { deferredTaskStarts: pendingQuestionnaires.deferred }
4085
4207
  : {}),
4208
+ ...(deliveries ? { openDeliveries: deliveries } : {}),
4086
4209
  client,
4087
4210
  ...moved,
4088
4211
  ...(incomplete && state === 'bound'
@@ -4113,6 +4236,58 @@ export class BridgeService {
4113
4236
  recorded: (await this.dependencies.releaseNotes?.presented(input.reportId, input.claimId)) ?? false,
4114
4237
  }));
4115
4238
  }
4239
+ async liveStatusNotice(projectId, language) {
4240
+ const response = await this.dependencies.client
4241
+ .request(endpoints.liveStatusNotice(projectId), {
4242
+ networkTimeoutMs: 2000,
4243
+ retryRefresh: false,
4244
+ })
4245
+ .catch(() => null);
4246
+ const notice = liveStatusNoticeSchema.safeParse(response?.data);
4247
+ if (!notice.success || notice.data.presented)
4248
+ return null;
4249
+ const { copy } = copies(liveStatusNoticeWording, 'liveStatusNotice', language)[0];
4250
+ return {
4251
+ projectId: notice.data.projectId,
4252
+ organizationId: notice.data.organizationId,
4253
+ noticeVersion: notice.data.noticeVersion,
4254
+ sharing: notice.data.sharing,
4255
+ text: copy[notice.data.sharing],
4256
+ };
4257
+ }
4258
+ async liveStatusList(input) {
4259
+ return await this.execute(async () => {
4260
+ const scope = input.organizationId
4261
+ ? new URLSearchParams({ organizationId: input.organizationId })
4262
+ : new URLSearchParams({
4263
+ projectId: input.projectId ?? (await this.boundProject(input.repoRoot)),
4264
+ });
4265
+ const response = await this.dependencies.client.request(`${endpoints.liveStatusList}?${scope}`);
4266
+ return asJsonValue(response.data);
4267
+ });
4268
+ }
4269
+ async liveStatusNoticePresented(input) {
4270
+ return await this.execute(async () => {
4271
+ const projectId = input.projectId ?? (await this.boundProject(input.repoRoot));
4272
+ const response = await this.dependencies.client.request(endpoints.liveStatusNoticePresented(projectId), { method: 'POST', body: { noticeVersion: input.noticeVersion } });
4273
+ return asJsonValue(response.data);
4274
+ });
4275
+ }
4276
+ async liveStatusSetSharing(input) {
4277
+ return await this.execute(async () => {
4278
+ const response = await this.dependencies.client.request(endpoints.liveStatusSharing(input.organizationId), {
4279
+ method: 'PUT',
4280
+ body: { sharing: input.sharing, expectedSharing: input.expectedSharing },
4281
+ });
4282
+ return asJsonValue(response.data);
4283
+ });
4284
+ }
4285
+ async boundProject(repoRoot) {
4286
+ const repository = await this.dependencies.repositories.resolveIdentity(repoRoot ?? process.cwd());
4287
+ if (!repository.projectId)
4288
+ throw refuse('This repository has no selected project, so there is no default scope for live status. Name a projectId or an organizationId.', 'session.entry');
4289
+ return repository.projectId;
4290
+ }
4116
4291
  async movedAway(repository, projectId) {
4117
4292
  try {
4118
4293
  const response = await this.dependencies.client.request(endpoints.projectResolve, {
@@ -4126,6 +4301,30 @@ export class BridgeService {
4126
4301
  return {};
4127
4302
  }
4128
4303
  }
4304
+ async openDeliveries(projectId) {
4305
+ try {
4306
+ const response = await this.dependencies.client.request(`${endpoints.projectDeliveries(projectId)}?state=open&limit=5`);
4307
+ const page = objectValue(response.data);
4308
+ const items = Array.isArray(page?.items) ? page.items : [];
4309
+ if (!items.length)
4310
+ return null;
4311
+ const pool = this.dependencies.worktreePool;
4312
+ return {
4313
+ total: page.total ?? items.length,
4314
+ items: await Promise.all(items.map(async (item) => {
4315
+ const folder = await pool?.forTask(String(objectValue(item)?.taskId));
4316
+ return {
4317
+ ...objectOrEmpty(item),
4318
+ folderHere: folder?.pendingDelivery ? folder.repoRoot : null,
4319
+ };
4320
+ })),
4321
+ nextAction: 'These closed tasks have no concluded delivery. Mention them to the user in one line. For one whose folderHere is set, task.close with its taskId in that folder asks and performs the Git delivery there. Any other can be answered or concluded with task.delivery when the user asks.',
4322
+ };
4323
+ }
4324
+ catch {
4325
+ return null;
4326
+ }
4327
+ }
4129
4328
  async actionableWorkItems(projectId) {
4130
4329
  try {
4131
4330
  const response = await this.dependencies.client.request(`${endpoints.workItemList(projectId)}?limit=100&actionable=true`);
@@ -4431,6 +4630,44 @@ export class BridgeService {
4431
4630
  return asJsonValue({ queued: true, outboxId: queued.id, idempotencyKey });
4432
4631
  }
4433
4632
  }
4633
+ async taskDeliveryAnswer(input) {
4634
+ return this.execute(() => this.mutateWithOutbox('task.delivery', endpoints.taskDeliveryAnswer(input.projectId, input.taskId), cleanJson({
4635
+ expectedVersion: input.expectedVersion,
4636
+ choice: input.choice,
4637
+ baseBranch: input.baseBranch,
4638
+ answerSource: input.answerSource,
4639
+ })));
4640
+ }
4641
+ async taskDeliveryCancel(input) {
4642
+ return this.execute(() => this.mutateWithOutbox('task.delivery', endpoints.taskDeliveryCancel(input.projectId, input.taskId), { reason: input.reason }));
4643
+ }
4644
+ async taskDeliveryRecord(input) {
4645
+ return this.execute(async () => {
4646
+ const response = await this.dependencies.client.request(endpoints.taskDelivery(input.projectId, input.taskId));
4647
+ const folder = await this.dependencies.worktreePool?.forTask(input.taskId);
4648
+ return asJsonValue({
4649
+ record: response.data,
4650
+ folder: folder?.pendingDelivery ? folder.repoRoot : null,
4651
+ });
4652
+ });
4653
+ }
4654
+ async reportDeliveryOutcome(report) {
4655
+ const path = report.outcome === 'delivered'
4656
+ ? endpoints.taskDeliveryDeliver(report.projectId, report.taskId)
4657
+ : endpoints.taskDeliveryCancel(report.projectId, report.taskId);
4658
+ const body = report.outcome === 'delivered'
4659
+ ? { commit: report.commit, pushed: report.pushed }
4660
+ : { reason: report.reason };
4661
+ await this.dependencies.outbox.enqueue({
4662
+ operation: 'task.delivery',
4663
+ method: 'POST',
4664
+ path,
4665
+ body,
4666
+ projectId: report.projectId,
4667
+ idempotencyKey: sha256(stableStringify({ path, body })),
4668
+ });
4669
+ this.deliverInBackground();
4670
+ }
4434
4671
  async enqueueTaskReceipt(operation, path, taskId, repoRoot, content) {
4435
4672
  const repository = await this.dependencies.repositories.resolve(repoRoot);
4436
4673
  return this.taskExclusive(taskId, async () => {
@@ -4479,7 +4716,11 @@ export class BridgeService {
4479
4716
  for (const entry of entries) {
4480
4717
  const owner = taskIdFromDeliverySafe(entry);
4481
4718
  const body = objectOrEmpty(entry.body);
4482
- const key = owner ? 'task:' + owner : 'project:' + String(body.projectId ?? 'unscoped');
4719
+ const key = owner
4720
+ ? 'task:' + owner
4721
+ : entry.operation === 'task.delivery'
4722
+ ? 'delivery:' + entry.path.slice(0, entry.path.lastIndexOf('/'))
4723
+ : 'project:' + String(body.projectId ?? 'unscoped');
4483
4724
  const group = groups.get(key) ?? [];
4484
4725
  group.push(entry);
4485
4726
  groups.set(key, group);
@@ -4505,6 +4746,12 @@ export class BridgeService {
4505
4746
  synchronized.push(entry.id);
4506
4747
  }
4507
4748
  catch (error) {
4749
+ if (entry.operation === 'task.delivery' &&
4750
+ error instanceof ApiResponseError &&
4751
+ (error.httpStatus === 404 || error.httpStatus === 409)) {
4752
+ await this.dependencies.outbox.acknowledge(entry.id);
4753
+ continue;
4754
+ }
4508
4755
  const errorKind = classifyError(error);
4509
4756
  await this.dependencies.outbox.markAttempt(entry.id, errorKind);
4510
4757
  if (entry.journalRef)
@@ -4874,6 +5121,9 @@ export class BridgeService {
4874
5121
  response = await this.dependencies.client.request(endpoints.taskClose, {
4875
5122
  method: 'POST',
4876
5123
  body,
5124
+ ...(this.dependencies.clientVersion
5125
+ ? { headers: { 'x-client-version': this.dependencies.clientVersion } }
5126
+ : {}),
4877
5127
  });
4878
5128
  }
4879
5129
  catch (error) {
@@ -5139,6 +5389,17 @@ const modeWording = z.strictObject({
5139
5389
  autonomous: option,
5140
5390
  ask: option,
5141
5391
  });
5392
+ const liveStatusNoticeWording = z.strictObject({
5393
+ managers: text,
5394
+ off: text,
5395
+ });
5396
+ const liveStatusNoticeSchema = z.object({
5397
+ projectId: z.uuid(),
5398
+ organizationId: z.uuid(),
5399
+ sharing: z.enum(['managers', 'off']),
5400
+ noticeVersion: z.string().regex(/^[0-9a-f]{64}$/),
5401
+ presented: z.boolean(),
5402
+ });
5142
5403
  const ruleDeviationWording = z.strictObject({
5143
5404
  ask: text,
5144
5405
  rule: text,
@@ -5371,7 +5632,7 @@ function deliveryQuestion(touchedContract, destination, publishApplicable) {
5371
5632
  : {}),
5372
5633
  afterPullRequest: 'Do not end the turn once a pull request exists. Check whether it merges cleanly, report the conflicting files if it does not, and ask whether to resolve them before touching anything.',
5373
5634
  afterCommit: publishApplicable
5374
- ? 'Call memory.publish_task with this closed task id and repoRoot after the authorized commit is created.'
5635
+ ? "After the authorized delivery, call worktree.release in this folder with deliveryOutcome delivered: it publishes this task's source memory and then records the delivery for the project. Its sourcePublication says whether publication happened; call memory.publish_task only when it names a reason you can fix. A task without a worktree to release calls memory.publish_task after the commit instead."
5375
5636
  : 'This task has nothing to publish (see sourcePublication on the close response); do not call memory.publish_task.',
5376
5637
  });
5377
5638
  }
@@ -1,3 +1,4 @@
1
+ import { LiveSignals } from './live-signals.js';
1
2
  import { ReleaseNotes } from './release-notes.js';
2
3
  import { WorktreePool } from './worktree-pool.js';
3
4
  import { WorktreePolicyCache } from './worktree-policy.js';
@@ -42,13 +43,16 @@ export function createBridgeService(options = {}) {
42
43
  credentials,
43
44
  cache,
44
45
  refreshPath: endpoints.authRefresh,
46
+ liveHeader: (path) => live.header(path),
45
47
  });
48
+ const live = new LiveSignals(client);
46
49
  const git = new GitInspector();
47
50
  const outbox = new OfflineOutbox(stateRoot);
48
51
  const activeContexts = new ActiveContextStore(stateRoot);
49
52
  const repositoryDecisions = new RepositoryDecisionStore(stateRoot);
50
53
  const updateChoices = new UpdateChoiceStore(stateRoot);
51
54
  const shadowNotices = new ShadowNoticeStore(stateRoot);
55
+ let service;
52
56
  const worktreePool = new WorktreePool(stateRoot, {
53
57
  ...(options.ownerId ? { ownerId: options.ownerId } : {}),
54
58
  pendingWork: async (entry) => {
@@ -59,11 +63,13 @@ export function createBridgeService(options = {}) {
59
63
  pointer?.closeIntent ||
60
64
  (await outbox.listForTask(entry.taskId)).length);
61
65
  },
66
+ onDeliveryOutcome: async (report) => service?.reportDeliveryOutcome(report),
62
67
  });
63
68
  const gate = new VerificationGate(stateRoot, git, outbox, undefined, worktreePool);
64
69
  const principalState = new PrincipalStateGuard(stateRoot, credentials, cache, outbox, activeContexts, gate);
65
70
  const languages = new LanguageStore(stateRoot);
66
- return new BridgeService({
71
+ return (service = new BridgeService({
72
+ live,
67
73
  releaseNotes: new ReleaseNotes(stateRoot, client, credentials),
68
74
  worktreePool,
69
75
  worktreePolicy: new WorktreePolicyCache(stateRoot),
@@ -83,6 +89,6 @@ export function createBridgeService(options = {}) {
83
89
  onboarding: new OnboardingStore(stateRoot),
84
90
  clientVersion: options.clientVersion === undefined ? config.clientVersion : options.clientVersion,
85
91
  principalState,
86
- });
92
+ }));
87
93
  }
88
94
  //# sourceMappingURL=create-bridge-service.js.map