engineering-memory 1.11.21 → 1.11.23

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,6 @@
1
1
  import { ignoredRuntimeFiles, localRelativePath } from './worktree-preparation.js';
2
2
  import { decisionModeSchema } from './decision-mode-store.js';
3
- import { delegatedReasonSchema } from './questionnaire-store.js';
3
+ import { answerSourceName, delegatedReasonSchema } from './questionnaire-store.js';
4
4
  import { unreadableWorktree, } from './worktree-pool.js';
5
5
  import { validWorktreePolicy } from './worktree-policy.js';
6
6
  import { resolveTaskBase } from './branch-preferences.js';
@@ -689,6 +689,18 @@ export class BridgeService {
689
689
  (repository.projectId && repository.projectId !== projectId)) {
690
690
  throw refuse('The live task belongs to a different project than the one this repository is bound to.', 'project.resolve');
691
691
  }
692
+ if (pointer?.closedAt &&
693
+ pointer.worktreeGeneration &&
694
+ this.dependencies.worktreePool &&
695
+ !(await this.dependencies.worktreePool.find(projectId, taskSlug, repository.repoRoot)))
696
+ return asJsonValue({
697
+ taskClosed: true,
698
+ folderReleased: true,
699
+ branch: pointer.branch ?? null,
700
+ ...pendingQuestionnairesFields(pendingQuestionnaires),
701
+ repository: publicRepository(repository),
702
+ nextAction: 'This task is closed and its folder has been released, so there is nothing to resume or release. Its branch keeps the committed work; start new work with task.branch.',
703
+ });
692
704
  const adoptable = pointer &&
693
705
  !pointer.worktreeGeneration &&
694
706
  normalizeTaskMode(pointer.mode) !== 'read_only' &&
@@ -1677,10 +1689,31 @@ export class BridgeService {
1677
1689
  catch (error) {
1678
1690
  throw refuse(error instanceof Error ? error.message : 'The resulting commit must be inspected.', 'memory.sync_start');
1679
1691
  }
1692
+ if (!proof)
1693
+ throw refuse('This task has nothing to publish: its project had no inspected source snapshot when the task was verified.', 'memory.sync_start');
1680
1694
  const response = await this.dependencies.client.request('/memory/sources/publish-task', { method: 'POST', body: proof });
1681
1695
  return response.data;
1682
1696
  });
1683
1697
  }
1698
+ async publishDelivered(repoRoot, taskId) {
1699
+ try {
1700
+ const proof = await this.dependencies.gate.publicationProof(repoRoot, taskId);
1701
+ if (!proof)
1702
+ return null;
1703
+ const response = await this.dependencies.client.request('/memory/sources/publish-task', { method: 'POST', body: proof });
1704
+ const answer = objectValue(response.data);
1705
+ return answer?.published === false
1706
+ ? { published: false, reason: String(answer.reason) }
1707
+ : { published: true };
1708
+ }
1709
+ catch (error) {
1710
+ return {
1711
+ published: false,
1712
+ reason: error instanceof Error ? error.message : String(error),
1713
+ 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.',
1714
+ };
1715
+ }
1716
+ }
1684
1717
  async onboardingRequest(operation, body) {
1685
1718
  return this.execute(async () => {
1686
1719
  if (operation === 'next' || operation === 'submit') {
@@ -2256,11 +2289,7 @@ export class BridgeService {
2256
2289
  questionnaireId,
2257
2290
  reason: waiver.reason,
2258
2291
  paths,
2259
- answerSource: question.answerSource?.kind === 'delegated_agent'
2260
- ? 'delegated_agent'
2261
- : question.answerSource?.kind === 'host_native_relay'
2262
- ? question.answerSource.hostTool
2263
- : 'mcp_form',
2292
+ answerSource: answerSourceName(question.answerSource),
2264
2293
  ...(delegatedEvidence(question)
2265
2294
  ? { delegatedDecision: delegatedEvidence(question) }
2266
2295
  : {}),
@@ -2533,9 +2562,44 @@ export class BridgeService {
2533
2562
  currentCommit: repository.git.head,
2534
2563
  folder,
2535
2564
  suggestedName,
2565
+ ...(input.workItemId
2566
+ ? await this.workItemBranches(repository.repoRoot, repository.projectId, input.workItemProjectId ?? repository.projectId, input.workItemId, preferences.data)
2567
+ : { planBranch: null, continueBranches: [] }),
2536
2568
  });
2537
2569
  });
2538
2570
  }
2571
+ async workItemBranches(repoRoot, projectId, itemProjectId, workItemId, preferences) {
2572
+ const git = this.dependencies.repositories.git;
2573
+ const [plan, runs, reserved, openDeliveries] = await Promise.all([
2574
+ this.dependencies.client.request(endpoints.workItemPlan(itemProjectId, workItemId)),
2575
+ this.dependencies.client.request(`${endpoints.workItemRuns(itemProjectId, workItemId)}?offset=0&limit=50`),
2576
+ git.protectedBranchNames(repoRoot),
2577
+ this.dependencies.client
2578
+ .request(`${endpoints.projectDeliveries(projectId)}?state=open&limit=100`)
2579
+ .then((response) => response.data?.items ?? [], () => []),
2580
+ ]);
2581
+ for (const preference of Object.values(preferences.branches))
2582
+ if (preference.branch)
2583
+ reserved.add(preference.branch);
2584
+ const used = [
2585
+ ...new Set((runs.data?.items ?? [])
2586
+ .filter((run) => run.projectId === projectId && run.mode !== 'read_only')
2587
+ .map((run) => run.branch)
2588
+ .filter((branch) => !!branch && !reserved.has(branch))),
2589
+ ];
2590
+ const planBranch = plan.data?.confirmed ? plan.data.branch : null;
2591
+ const continueBranches = !planBranch
2592
+ ? used.slice(0, 2)
2593
+ : used.includes(planBranch) || (await git.branchExists(repoRoot, planBranch))
2594
+ ? [planBranch]
2595
+ : [];
2596
+ const open = new Set(openDeliveries.map((delivery) => delivery.taskId));
2597
+ return {
2598
+ planBranch,
2599
+ continueBranches,
2600
+ openDeliveryBranches: continueBranches.filter((branch) => (runs.data?.items ?? []).some((run) => run.branch === branch && open.has(run.id))),
2601
+ };
2602
+ }
2539
2603
  async suggestedBranchName(repoRoot, externalTaskId) {
2540
2604
  const candidates = suggestedBranchNames(externalTaskId);
2541
2605
  for (const candidate of candidates)
@@ -2730,6 +2794,17 @@ export class BridgeService {
2730
2794
  return asJsonValue({ operation, complete: true, removed: entry.repoRoot });
2731
2795
  }
2732
2796
  const entry = await pool.forPath(projectId, repository.repoRoot);
2797
+ if ((operation === 'release' || operation === 'cancel_delivery') &&
2798
+ !(await pool.find(projectId, input.externalTaskId, repository.repoRoot)) &&
2799
+ ((entry?.phase === 'released' && entry.externalTaskId === input.externalTaskId) ||
2800
+ (await this.dependencies.activeContexts.loadForSlug(repository.repoFingerprint, input.externalTaskId))?.worktreeGeneration === input.generation))
2801
+ return asJsonValue({
2802
+ operation,
2803
+ complete: true,
2804
+ repoRoot: repository.repoRoot,
2805
+ alreadyReleased: true,
2806
+ nextAction: "This task's folder was already released; there is nothing left to release.",
2807
+ });
2733
2808
  if (!entry ||
2734
2809
  entry.externalTaskId !== input.externalTaskId ||
2735
2810
  entry.generation !== input.generation)
@@ -2750,8 +2825,17 @@ export class BridgeService {
2750
2825
  }
2751
2826
  if (operation === 'release') {
2752
2827
  await this.requireDeliveredTaskWork(entry);
2828
+ const sourcePublication = outcome === 'delivered' && entry.taskId && entry.pendingDelivery
2829
+ ? await this.publishDelivered(repository.repoRoot, entry.taskId)
2830
+ : null;
2753
2831
  await pool.release(projectId, repository.repoRoot, input.generation, outcome === 'delivered' ? outcome : undefined);
2754
2832
  this.poolAllocations.delete(repository.repoRoot);
2833
+ return asJsonValue({
2834
+ operation,
2835
+ complete: true,
2836
+ repoRoot: repository.repoRoot,
2837
+ ...(sourcePublication ? { sourcePublication } : {}),
2838
+ });
2755
2839
  }
2756
2840
  if (operation === 'cancel_delivery') {
2757
2841
  if (!entry.pendingDelivery)
@@ -2859,7 +2943,8 @@ export class BridgeService {
2859
2943
  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');
2860
2944
  if (repository.git.changedPaths.length > 0)
2861
2945
  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');
2862
- if (await git.branchExists(repository.repoRoot, input.name))
2946
+ if (input.base.kind !== 'existing' &&
2947
+ (await git.branchExists(repository.repoRoot, input.name)))
2863
2948
  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');
2864
2949
  if (holder.generation) {
2865
2950
  await pool.moveOut(projectId, repository.repoRoot, holder.generation, holder.oid ?? undefined);
@@ -2891,6 +2976,7 @@ export class BridgeService {
2891
2976
  keepCurrent: input.keepCurrent,
2892
2977
  inPlace: !existing && input.inPlace,
2893
2978
  resume: Boolean(existing),
2979
+ continued: !existing && input.base?.kind === 'existing',
2894
2980
  }, allocationPolicy));
2895
2981
  allocationTimer.mark('pool_allocate');
2896
2982
  allocationTimer.finish();
@@ -3702,6 +3788,22 @@ export class BridgeService {
3702
3788
  return asJsonValue(response.data);
3703
3789
  });
3704
3790
  }
3791
+ async workItemComment(input) {
3792
+ return this.execute(async () => {
3793
+ const response = await this.dependencies.client.request(endpoints.workItemComments(input.projectId, input.workItemId), { method: 'POST', body: cleanJson(input.data) });
3794
+ return asJsonValue(response.data);
3795
+ });
3796
+ }
3797
+ async workItemComments(input) {
3798
+ return this.execute(async () => {
3799
+ const query = new URLSearchParams({
3800
+ offset: String(input.offset ?? 0),
3801
+ limit: String(input.limit ?? 50),
3802
+ });
3803
+ const response = await this.dependencies.client.request(`${endpoints.workItemComments(input.projectId, input.workItemId)}?${query}`);
3804
+ return asJsonValue(response.data);
3805
+ });
3806
+ }
3705
3807
  async workItemPlan(input) {
3706
3808
  return this.execute(async () => {
3707
3809
  const response = await this.dependencies.client.request(endpoints.workItemPlan(input.projectId, input.workItemId));
@@ -4000,7 +4102,7 @@ export class BridgeService {
4000
4102
  const livePointers = authenticated && state === 'bound'
4001
4103
  ? this.dependencies.activeContexts.list(repository.repoFingerprint)
4002
4104
  : Promise.resolve([]);
4003
- const [client, liveTasksRaw, workItems, gitPreferences, worktrees, pendingQuestionnaires, moved,] = await Promise.all([
4105
+ const [client, liveTasksRaw, workItems, gitPreferences, worktrees, pendingQuestionnaires, moved, deliveries,] = await Promise.all([
4004
4106
  this.clientUpdate(authenticated),
4005
4107
  livePointers,
4006
4108
  authenticated && state === 'bound' && projectId
@@ -4021,6 +4123,9 @@ export class BridgeService {
4021
4123
  authenticated && state === 'bound' && projectId
4022
4124
  ? this.movedAway(repository, projectId)
4023
4125
  : Promise.resolve({}),
4126
+ authenticated && state === 'bound' && projectId
4127
+ ? this.openDeliveries(projectId)
4128
+ : Promise.resolve(null),
4024
4129
  ]);
4025
4130
  timer.mark('independent_lookups');
4026
4131
  const liveTasks = liveTasksRaw.map(describePointer);
@@ -4060,6 +4165,7 @@ export class BridgeService {
4060
4165
  ...(pendingQuestionnaires.deferred.length > 0
4061
4166
  ? { deferredTaskStarts: pendingQuestionnaires.deferred }
4062
4167
  : {}),
4168
+ ...(deliveries ? { openDeliveries: deliveries } : {}),
4063
4169
  client,
4064
4170
  ...moved,
4065
4171
  ...(incomplete && state === 'bound'
@@ -4103,6 +4209,30 @@ export class BridgeService {
4103
4209
  return {};
4104
4210
  }
4105
4211
  }
4212
+ async openDeliveries(projectId) {
4213
+ try {
4214
+ const response = await this.dependencies.client.request(`${endpoints.projectDeliveries(projectId)}?state=open&limit=5`);
4215
+ const page = objectValue(response.data);
4216
+ const items = Array.isArray(page?.items) ? page.items : [];
4217
+ if (!items.length)
4218
+ return null;
4219
+ const pool = this.dependencies.worktreePool;
4220
+ return {
4221
+ total: page.total ?? items.length,
4222
+ items: await Promise.all(items.map(async (item) => {
4223
+ const folder = await pool?.forTask(String(objectValue(item)?.taskId));
4224
+ return {
4225
+ ...objectOrEmpty(item),
4226
+ folderHere: folder?.pendingDelivery ? folder.repoRoot : null,
4227
+ };
4228
+ })),
4229
+ 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.',
4230
+ };
4231
+ }
4232
+ catch {
4233
+ return null;
4234
+ }
4235
+ }
4106
4236
  async actionableWorkItems(projectId) {
4107
4237
  try {
4108
4238
  const response = await this.dependencies.client.request(`${endpoints.workItemList(projectId)}?limit=100&actionable=true`);
@@ -4408,6 +4538,44 @@ export class BridgeService {
4408
4538
  return asJsonValue({ queued: true, outboxId: queued.id, idempotencyKey });
4409
4539
  }
4410
4540
  }
4541
+ async taskDeliveryAnswer(input) {
4542
+ return this.execute(() => this.mutateWithOutbox('task.delivery', endpoints.taskDeliveryAnswer(input.projectId, input.taskId), cleanJson({
4543
+ expectedVersion: input.expectedVersion,
4544
+ choice: input.choice,
4545
+ baseBranch: input.baseBranch,
4546
+ answerSource: input.answerSource,
4547
+ })));
4548
+ }
4549
+ async taskDeliveryCancel(input) {
4550
+ return this.execute(() => this.mutateWithOutbox('task.delivery', endpoints.taskDeliveryCancel(input.projectId, input.taskId), { reason: input.reason }));
4551
+ }
4552
+ async taskDeliveryRecord(input) {
4553
+ return this.execute(async () => {
4554
+ const response = await this.dependencies.client.request(endpoints.taskDelivery(input.projectId, input.taskId));
4555
+ const folder = await this.dependencies.worktreePool?.forTask(input.taskId);
4556
+ return asJsonValue({
4557
+ record: response.data,
4558
+ folder: folder?.pendingDelivery ? folder.repoRoot : null,
4559
+ });
4560
+ });
4561
+ }
4562
+ async reportDeliveryOutcome(report) {
4563
+ const path = report.outcome === 'delivered'
4564
+ ? endpoints.taskDeliveryDeliver(report.projectId, report.taskId)
4565
+ : endpoints.taskDeliveryCancel(report.projectId, report.taskId);
4566
+ const body = report.outcome === 'delivered'
4567
+ ? { commit: report.commit, pushed: report.pushed }
4568
+ : { reason: report.reason };
4569
+ await this.dependencies.outbox.enqueue({
4570
+ operation: 'task.delivery',
4571
+ method: 'POST',
4572
+ path,
4573
+ body,
4574
+ projectId: report.projectId,
4575
+ idempotencyKey: sha256(stableStringify({ path, body })),
4576
+ });
4577
+ this.deliverInBackground();
4578
+ }
4411
4579
  async enqueueTaskReceipt(operation, path, taskId, repoRoot, content) {
4412
4580
  const repository = await this.dependencies.repositories.resolve(repoRoot);
4413
4581
  return this.taskExclusive(taskId, async () => {
@@ -4456,7 +4624,11 @@ export class BridgeService {
4456
4624
  for (const entry of entries) {
4457
4625
  const owner = taskIdFromDeliverySafe(entry);
4458
4626
  const body = objectOrEmpty(entry.body);
4459
- const key = owner ? 'task:' + owner : 'project:' + String(body.projectId ?? 'unscoped');
4627
+ const key = owner
4628
+ ? 'task:' + owner
4629
+ : entry.operation === 'task.delivery'
4630
+ ? 'delivery:' + entry.path.slice(0, entry.path.lastIndexOf('/'))
4631
+ : 'project:' + String(body.projectId ?? 'unscoped');
4460
4632
  const group = groups.get(key) ?? [];
4461
4633
  group.push(entry);
4462
4634
  groups.set(key, group);
@@ -4482,6 +4654,12 @@ export class BridgeService {
4482
4654
  synchronized.push(entry.id);
4483
4655
  }
4484
4656
  catch (error) {
4657
+ if (entry.operation === 'task.delivery' &&
4658
+ error instanceof ApiResponseError &&
4659
+ (error.httpStatus === 404 || error.httpStatus === 409)) {
4660
+ await this.dependencies.outbox.acknowledge(entry.id);
4661
+ continue;
4662
+ }
4485
4663
  const errorKind = classifyError(error);
4486
4664
  await this.dependencies.outbox.markAttempt(entry.id, errorKind);
4487
4665
  if (entry.journalRef)
@@ -4851,6 +5029,9 @@ export class BridgeService {
4851
5029
  response = await this.dependencies.client.request(endpoints.taskClose, {
4852
5030
  method: 'POST',
4853
5031
  body,
5032
+ ...(this.dependencies.clientVersion
5033
+ ? { headers: { 'x-client-version': this.dependencies.clientVersion } }
5034
+ : {}),
4854
5035
  });
4855
5036
  }
4856
5037
  catch (error) {
@@ -5348,7 +5529,7 @@ function deliveryQuestion(touchedContract, destination, publishApplicable) {
5348
5529
  : {}),
5349
5530
  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.',
5350
5531
  afterCommit: publishApplicable
5351
- ? 'Call memory.publish_task with this closed task id and repoRoot after the authorized commit is created.'
5532
+ ? "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."
5352
5533
  : 'This task has nothing to publish (see sourcePublication on the close response); do not call memory.publish_task.',
5353
5534
  });
5354
5535
  }
@@ -49,6 +49,7 @@ export function createBridgeService(options = {}) {
49
49
  const repositoryDecisions = new RepositoryDecisionStore(stateRoot);
50
50
  const updateChoices = new UpdateChoiceStore(stateRoot);
51
51
  const shadowNotices = new ShadowNoticeStore(stateRoot);
52
+ let service;
52
53
  const worktreePool = new WorktreePool(stateRoot, {
53
54
  ...(options.ownerId ? { ownerId: options.ownerId } : {}),
54
55
  pendingWork: async (entry) => {
@@ -59,11 +60,12 @@ export function createBridgeService(options = {}) {
59
60
  pointer?.closeIntent ||
60
61
  (await outbox.listForTask(entry.taskId)).length);
61
62
  },
63
+ onDeliveryOutcome: async (report) => service?.reportDeliveryOutcome(report),
62
64
  });
63
65
  const gate = new VerificationGate(stateRoot, git, outbox, undefined, worktreePool);
64
66
  const principalState = new PrincipalStateGuard(stateRoot, credentials, cache, outbox, activeContexts, gate);
65
67
  const languages = new LanguageStore(stateRoot);
66
- return new BridgeService({
68
+ return (service = new BridgeService({
67
69
  releaseNotes: new ReleaseNotes(stateRoot, client, credentials),
68
70
  worktreePool,
69
71
  worktreePolicy: new WorktreePolicyCache(stateRoot),
@@ -83,6 +85,6 @@ export function createBridgeService(options = {}) {
83
85
  onboarding: new OnboardingStore(stateRoot),
84
86
  clientVersion: options.clientVersion === undefined ? config.clientVersion : options.clientVersion,
85
87
  principalState,
86
- });
88
+ }));
87
89
  }
88
90
  //# sourceMappingURL=create-bridge-service.js.map
@@ -170,6 +170,13 @@ const answerSourceSchema = z.union([
170
170
  impact: z.enum(['routine', 'critical']),
171
171
  }),
172
172
  ]);
173
+ export function answerSourceName(source) {
174
+ return source?.kind === 'delegated_agent'
175
+ ? 'delegated_agent'
176
+ : source?.kind === 'host_native_relay'
177
+ ? source.hostTool
178
+ : 'mcp_form';
179
+ }
173
180
  const storedAnswerSchema = z.strictObject({
174
181
  requestKey: z.string().regex(/^questionnaire_[0-9a-f]{64}$/),
175
182
  answeredAt: z.string().datetime(),
@@ -18,6 +18,7 @@ const startCopy = {
18
18
  fetched: (role, source) => `The project's ${role} branch ${source} is fetched from the remote and the new branch starts from that exact commit.`,
19
19
  pinned: (source) => `The new branch starts where this folder is right now, ${source}. Nothing is fetched.`,
20
20
  typedBranch: 'Write a branch name on origin below. It is fetched and the new branch starts from it.',
21
+ continued: (branch) => `No new branch is created. The work continues from the newest commit of ${branch}, fetched from origin when this clone lacks it.`,
21
22
  suggested: 'The branch is created with this name.',
22
23
  typedName: 'Write the name below. A name that already exists is refused and the form is asked again.',
23
24
  },
@@ -37,6 +38,7 @@ const startCopy = {
37
38
  fetched: (role, source) => `Projenin ${role} dalı ${source} uzak sunucudan çekilir ve yeni dal tam o commit’ten başlar.`,
38
39
  pinned: (source) => `Yeni dal, bu klasörün şu an üzerinde olduğu ${source} noktasından başlar. Hiçbir şey çekilmez.`,
39
40
  typedBranch: 'origin üzerindeki dal adını aşağıya yaz. O dal çekilir ve yeni dal ondan başlar.',
41
+ continued: (branch) => `Yeni dal açılmaz. İş ${branch} dalının en yeni commit’inden sürer; bu klonda yoksa origin’den çekilir.`,
40
42
  suggested: 'Dal bu adla açılır.',
41
43
  typedName: 'Adı aşağıya yaz. Zaten var olan bir ad kabul edilmez, form yeniden sorulur.',
42
44
  },
@@ -47,6 +49,8 @@ const startWording = z.strictObject({
47
49
  message: text,
48
50
  keepContext: text,
49
51
  newBranch: text,
52
+ newBranchOrContinue: text,
53
+ continuePlan: text,
50
54
  example: text,
51
55
  stay: text,
52
56
  base: z.strictObject({
@@ -56,6 +60,9 @@ const startWording = z.strictObject({
56
60
  other: text,
57
61
  otherDetail: text,
58
62
  fetchDetail: text,
63
+ continue: text,
64
+ continueDetail: text,
65
+ continueOpenDetail: text,
59
66
  field: text,
60
67
  }),
61
68
  location: z.strictObject({
@@ -92,6 +99,8 @@ export function sourceLabel(base, currentBranch) {
92
99
  return base.remote + '/' + base.branch;
93
100
  if (base.kind === 'local')
94
101
  return base.ref;
102
+ if (base.kind === 'existing')
103
+ return base.branch;
95
104
  if (base.kind === 'current')
96
105
  return (currentBranch ?? 'HEAD') + (base.commit ? ' (' + base.commit.slice(0, 7) + ')' : '');
97
106
  return base.kind;
@@ -135,14 +144,20 @@ export function taskStartDefinition(facts) {
135
144
  ...option,
136
145
  label: base?.kind === 'remote'
137
146
  ? sourceLabel(base, facts.currentBranch)
147
+ : base?.kind === 'existing'
148
+ ? format(copy.base.continue, language, { branch: base.branch })
149
+ : option.id === 'current'
150
+ ? format(copy.base.local, language, { branch })
151
+ : copy.base.other,
152
+ description: base?.kind === 'existing'
153
+ ? facts.openDeliveryBranches?.includes(base.branch)
154
+ ? copy.base.continueOpenDetail
155
+ : copy.base.continueDetail
138
156
  : option.id === 'current'
139
- ? format(copy.base.local, language, { branch })
140
- : copy.base.other,
141
- description: option.id === 'current'
142
- ? copy.base.localDetail
143
- : option.id === 'other'
144
- ? copy.base.otherDetail
145
- : copy.base.fetchDetail,
157
+ ? copy.base.localDetail
158
+ : option.id === 'other'
159
+ ? copy.base.otherDetail
160
+ : copy.base.fetchDetail,
146
161
  };
147
162
  }),
148
163
  textField: { ...question.textField, title: copy.base.field },
@@ -184,8 +199,11 @@ export function taskStartDefinition(facts) {
184
199
  message: copy.message,
185
200
  context: keepOnly
186
201
  ? copy.keepContext
187
- : format(copy.newBranch, language, { name: binding.name }) +
188
- (binding.base ? ' · ' + sourceLabel(binding.base, facts.currentBranch) : ''),
202
+ : binding.base?.kind === 'existing'
203
+ ? format(copy.continuePlan, language, { branch: binding.base.branch })
204
+ : format(Object.values(binding.bases ?? {}).some((base) => base.kind === 'existing')
205
+ ? copy.newBranchOrContinue
206
+ : copy.newBranch, language, { name: binding.name }) + (binding.base ? ' · ' + sourceLabel(binding.base, facts.currentBranch) : ''),
189
207
  example: keepOnly ? format(copy.stay, language, { source: current }) : copy.example,
190
208
  questions,
191
209
  binding: JSON.parse(JSON.stringify(binding)),
@@ -269,17 +287,27 @@ function legacyTaskStartDefinition(facts) {
269
287
  label: base.remote + '/' + base.branch + ' (' + role + ')',
270
288
  description: copy.fetched(copy.roles[role], base.remote + '/' + base.branch),
271
289
  });
290
+ const continued = facts.continueBranches ?? [];
272
291
  binding.bases = {
273
292
  current: {
274
293
  kind: 'current',
275
294
  ...(facts.currentCommit ? { commit: facts.currentCommit } : {}),
276
295
  },
277
296
  ...Object.fromEntries(roles.map(({ role, base }) => [role, base])),
297
+ ...Object.fromEntries(continued.map((branch, index) => [
298
+ 'continue_' + (index + 1),
299
+ { kind: 'existing', branch },
300
+ ])),
278
301
  };
279
302
  questions.push({
280
303
  id: 'base',
281
304
  message: tr ? 'Hangi daldan başlasın?' : 'Which branch should it start from?',
282
305
  options: [
306
+ ...continued.map((branch, index) => ({
307
+ id: 'continue_' + (index + 1),
308
+ label: (tr ? 'Var olan dalı sürdür: ' : 'Continue the existing branch ') + branch,
309
+ description: copy.continued(branch),
310
+ })),
283
311
  ...roles.slice(0, 1).map(roleOption),
284
312
  {
285
313
  id: 'current',
@@ -422,12 +450,14 @@ export function taskStartChoice(record) {
422
450
  ? binding.bases?.[baseAnswer.choice]
423
451
  : undefined);
424
452
  const nameAnswer = record.answers.name;
425
- const name = binding.name ??
426
- (nameAnswer?.choice === 'custom'
427
- ? nameAnswer.text
428
- : nameAnswer?.choice === 'suggested'
429
- ? binding.suggestedName
430
- : undefined);
453
+ const name = base?.kind === 'existing'
454
+ ? base.branch
455
+ : (binding.name ??
456
+ (nameAnswer?.choice === 'custom'
457
+ ? nameAnswer.text
458
+ : nameAnswer?.choice === 'suggested'
459
+ ? binding.suggestedName
460
+ : undefined));
431
461
  if (!base || !name)
432
462
  return null;
433
463
  return {