chatroom-cli 1.132.1 → 1.133.0

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.
package/dist/index.js CHANGED
@@ -20416,6 +20416,10 @@ var init_agent_role = __esm(() => {
20416
20416
  reviewer: {
20417
20417
  tags: ["ephemeral" /* Ephemeral */],
20418
20418
  teams: [{ teamId: "duo", order: 35 }]
20419
+ },
20420
+ "game-asset-designer": {
20421
+ tags: ["ephemeral" /* Ephemeral */],
20422
+ teams: [{ teamId: "duo", order: 42 }]
20419
20423
  }
20420
20424
  };
20421
20425
  AGENT_ROLE_DEFINITIONS = Object.fromEntries(Object.entries(roleMetadata).map(([role, metadata]) => [role, { role, ...metadata }]));
@@ -35274,6 +35278,25 @@ var init_assigned_task = __esm(() => {
35274
35278
  ACTIVE_TASK_STATUSES = ["pending", "acknowledged", "in_progress"];
35275
35279
  });
35276
35280
 
35281
+ // src/daemon/domain/usecase/compute-backoff-delay.ts
35282
+ function computeBackoffDelay(attempt, opts, random3 = Math.random) {
35283
+ const exp = Math.min(opts.maxMs, opts.baseMs * 2 ** (attempt - 1));
35284
+ const jitterRatio = opts.jitter ?? 0;
35285
+ if (jitterRatio <= 0)
35286
+ return Math.max(0, Math.round(exp));
35287
+ const jitter = exp * jitterRatio * (random3() * 2 - 1);
35288
+ return Math.max(0, Math.round(exp + jitter));
35289
+ }
35290
+ var REDELIVERY_BACKOFF_BASE_MS = 5000, REDELIVERY_BACKOFF_MAX_MS, REDELIVERY_BACKOFF_OPTIONS;
35291
+ var init_compute_backoff_delay = __esm(() => {
35292
+ REDELIVERY_BACKOFF_MAX_MS = 5 * 60000;
35293
+ REDELIVERY_BACKOFF_OPTIONS = {
35294
+ baseMs: REDELIVERY_BACKOFF_BASE_MS,
35295
+ maxMs: REDELIVERY_BACKOFF_MAX_MS,
35296
+ jitter: 0.2
35297
+ };
35298
+ });
35299
+
35277
35300
  // src/daemon/infrastructure/local/harness/services/spawn-prompt.ts
35278
35301
  function createSpawnPrompt(raw, opts) {
35279
35302
  if (opts?.nativeBootstrap) {
@@ -36881,6 +36904,26 @@ var init_native_task_injector = __esm(() => {
36881
36904
  };
36882
36905
  });
36883
36906
 
36907
+ // src/daemon/services/agent-process-service/domain/usecase/is-cli-driven-harness.ts
36908
+ function isCliDrivenHarness(harness) {
36909
+ if (harness == null)
36910
+ return false;
36911
+ const capabilities = getHarnessCapabilities(harness);
36912
+ return capabilities?.runtimeKind === "cli";
36913
+ }
36914
+ var init_is_cli_driven_harness = __esm(() => {
36915
+ init_types();
36916
+ });
36917
+
36918
+ // src/daemon/services/agent-process-service/domain/usecase/restart-retry-interval.ts
36919
+ function restartRetryMaxIntervalMs(cliDriven) {
36920
+ return cliDriven ? CLI_RESTART_RETRY_MAX_INTERVAL_MS : AGENT_RESTART_RETRY_MAX_INTERVAL_MS;
36921
+ }
36922
+ var AGENT_RESTART_RETRY_MAX_INTERVAL_MS, CLI_RESTART_RETRY_MAX_INTERVAL_MS = 30000;
36923
+ var init_restart_retry_interval = __esm(() => {
36924
+ AGENT_RESTART_RETRY_MAX_INTERVAL_MS = 10 * 60000;
36925
+ });
36926
+
36884
36927
  // src/daemon/services/agent-process-service/service/agent-work-manager.ts
36885
36928
  class AgentWorkManager {
36886
36929
  deps;
@@ -36895,6 +36938,8 @@ class AgentWorkManager {
36895
36938
  recoveryGates = new Map;
36896
36939
  respawnStartRetries = new Map;
36897
36940
  respawnRestartSeq = 0;
36941
+ turnFailureAccountingFailures = new Map;
36942
+ turnFailureReleaseTimers = new Map;
36898
36943
  disposed = false;
36899
36944
  constructor(deps) {
36900
36945
  this.deps = deps;
@@ -36927,11 +36972,14 @@ class AgentWorkManager {
36927
36972
  recordDeliveryFailure: deps.taskService.recordDeliveryFailure,
36928
36973
  clearDeliveryFailure: deps.taskService.clearDeliveryFailure,
36929
36974
  recordTurnFailure: deps.taskService.recordTurnFailure,
36975
+ parkTurnFailure: deps.taskService.parkTurnFailure,
36930
36976
  decideUncoveredTurnEnd: deps.taskService.decideUncoveredTurnEnd,
36931
36977
  confirmNudgeSent: deps.taskService.confirmNudgeSent,
36932
36978
  resetTurnTracking: deps.taskService.resetTurnTracking,
36933
36979
  isRedeliveryExhausted: deps.taskService.isRedeliveryExhausted,
36934
36980
  clearRedeliveryTracking: deps.taskService.clearRedeliveryTracking,
36981
+ markTurnFailureReleasePending: deps.taskService.markTurnFailureReleasePending,
36982
+ clearTurnFailureReleasePending: deps.taskService.clearTurnFailureReleasePending,
36935
36983
  forgetStaleTask: deps.taskService.forgetStaleTask,
36936
36984
  loadAssignedTaskForAction: deps.taskService.loadAssignedTaskForAction,
36937
36985
  deliverNativeTask: (task, harnessSessionId, onTaskDelivered, isCurrent) => this.nativeTaskDeliveryQueue.enqueue({
@@ -36964,6 +37012,9 @@ class AgentWorkManager {
36964
37012
  role: event.role
36965
37013
  });
36966
37014
  }
37015
+ if (event.reason === "user.start" || event.reason === "user.restart") {
37016
+ this.cancelTurnFailureReleasesForRole(event.chatroomId, event.role);
37017
+ }
36967
37018
  await this.requestReconcile({
36968
37019
  chatroomId: event.chatroomId,
36969
37020
  role: event.role,
@@ -37021,14 +37072,29 @@ class AgentWorkManager {
37021
37072
  }
37022
37073
  async recoverStoppedRole(args2) {
37023
37074
  const key = recoveryKey(args2.chatroomId, args2.role);
37024
- const gate = this.recoveryGates.get(key);
37075
+ let gate = this.recoveryGates.get(key);
37025
37076
  if (!gate)
37026
37077
  throw new Error(`role recovery gate not prepared for ${args2.role}@${args2.chatroomId}`);
37027
- if (gate.recoveryPromise)
37028
- return gate.recoveryPromise;
37078
+ while (gate.recoveryPromise) {
37079
+ if (args2.mode !== "explicit")
37080
+ return gate.recoveryPromise;
37081
+ await gate.recoveryPromise.catch(() => {
37082
+ return;
37083
+ });
37084
+ if (!this.recoveryGates.has(key)) {
37085
+ await this.prepareRoleRecovery({ chatroomId: args2.chatroomId, role: args2.role });
37086
+ }
37087
+ const nextGate = this.recoveryGates.get(key);
37088
+ if (!nextGate)
37089
+ throw new Error(`role recovery gate not prepared for ${args2.role}@${args2.chatroomId}`);
37090
+ gate = nextGate;
37091
+ }
37029
37092
  gate.recoveryPromise = (async () => {
37030
37093
  await gate.drainPromise;
37031
37094
  const result3 = await this.deps.taskService.recoverInFlightTasks(args2);
37095
+ if (args2.mode === "explicit") {
37096
+ this.cancelTurnFailureReleasesForRole(args2.chatroomId, args2.role);
37097
+ }
37032
37098
  if (this.recoveryGates.get(key) === gate) {
37033
37099
  if (gate.retryTimer)
37034
37100
  clearTimeout(gate.retryTimer);
@@ -37064,7 +37130,7 @@ class AgentWorkManager {
37064
37130
  const gate = this.recoveryGates.get(recoveryKey(chatroomId, role));
37065
37131
  if (!gate || gate.retryTimer || this.disposed)
37066
37132
  return;
37067
- const delay3 = Math.min(30000, 1000 * 2 ** gate.retryAttempts++);
37133
+ const delay3 = computeBackoffDelay(gate.retryAttempts++ + 1, RECOVERY_RETRY_BACKOFF);
37068
37134
  gate.retryTimer = setTimeout(() => {
37069
37135
  gate.retryTimer = undefined;
37070
37136
  this.recoverStoppedRole({ chatroomId, role, mode: "automatic" }).catch((error51) => {
@@ -37073,26 +37139,115 @@ class AgentWorkManager {
37073
37139
  });
37074
37140
  }, delay3);
37075
37141
  }
37076
- async recoverTaskAfterTurnFailure(event, activeTask, phase) {
37142
+ async recoverTaskAfterTurnFailure(event, activeTask) {
37077
37143
  const { chatroomId, role } = event;
37078
- const { exceeded } = await this.deps.taskService.recordTurnFailure({
37144
+ const { attempts, exceeded } = await this.deps.taskService.recordTurnFailure({
37079
37145
  chatroomId,
37080
37146
  role,
37081
37147
  taskId: activeTask.taskId
37082
37148
  });
37083
- if (exceeded)
37084
- return "exceeded";
37085
- try {
37086
- await this.deps.taskService.releaseTaskAfterTurnFailure({
37149
+ if (exceeded) {
37150
+ this.cancelTurnFailureRelease({
37087
37151
  chatroomId,
37088
37152
  role,
37089
37153
  taskId: activeTask.taskId
37090
37154
  });
37155
+ return "exceeded";
37156
+ }
37157
+ this.scheduleTurnFailureRelease({ chatroomId, role, taskId: activeTask.taskId }, attempts, event.completion.turnId, "turn-failure");
37158
+ return "scheduled";
37159
+ }
37160
+ scheduleTurnFailureRelease(key, attempt, turnId, source) {
37161
+ if (this.disposed)
37162
+ return;
37163
+ const keyStr = turnFailureReleaseKey(key);
37164
+ if (source === "turn-failure")
37165
+ this.turnFailureAccountingFailures.delete(keyStr);
37166
+ this.cancelTurnFailureRelease(key);
37167
+ const delay3 = computeBackoffDelay(attempt, REDELIVERY_BACKOFF_OPTIONS);
37168
+ this.deps.taskService.markTurnFailureReleasePending(key);
37169
+ const timer = setTimeout(() => {
37170
+ this.turnFailureReleaseTimers.delete(keyStr);
37171
+ this.deps.taskService.clearTurnFailureReleasePending(key);
37172
+ if (this.disposed)
37173
+ return;
37174
+ this.releaseAfterTurnFailureBackoff(key, attempt);
37175
+ }, delay3);
37176
+ this.turnFailureReleaseTimers.set(keyStr, { key, timer });
37177
+ console.log(`[NativeDelivery:turn-failure-backoff] chatroom=${key.chatroomId} role=${key.role} task=${key.taskId} turn=${turnId} attempt=${attempt} delayMs=${delay3}`);
37178
+ }
37179
+ async releaseAfterTurnFailureBackoff(key, attempt) {
37180
+ const current = this.deps.taskInboxState.getForRole(key.chatroomId, key.role, key.taskId);
37181
+ if (!current || current.status !== "in_progress") {
37182
+ console.log(`[NativeDelivery:turn-failure-release-skipped] chatroom=${key.chatroomId} role=${key.role} task=${key.taskId} reason=task_not_current status=${current?.status ?? "missing"}`);
37183
+ return;
37184
+ }
37185
+ const activeTask = this.deps.agentTaskState.get({
37186
+ chatroomId: key.chatroomId,
37187
+ role: key.role
37188
+ });
37189
+ if (activeTask?.taskId === key.taskId) {
37190
+ console.log(`[NativeDelivery:turn-failure-release-skipped] chatroom=${key.chatroomId} role=${key.role} task=${key.taskId} reason=task_active`);
37191
+ return;
37192
+ }
37193
+ try {
37194
+ await this.deps.taskService.releaseTaskAfterTurnFailure({
37195
+ chatroomId: key.chatroomId,
37196
+ role: key.role,
37197
+ taskId: key.taskId
37198
+ });
37199
+ this.turnFailureAccountingFailures.delete(turnFailureReleaseKey(key));
37091
37200
  } catch (error51) {
37092
- console.error(`[NativeDelivery:${phase}-recovery-error] chatroom=${chatroomId} role=${role} task=${activeTask.taskId} turn=${event.completion.turnId} error=${error51 instanceof Error ? error51.stack ?? error51.message : String(error51)}`);
37093
- return "recovery-failed";
37201
+ console.error(`[NativeDelivery:turn-failed-release-error] chatroom=${key.chatroomId} role=${key.role} task=${key.taskId} error=${error51 instanceof Error ? error51.stack ?? error51.message : String(error51)}`);
37202
+ await this.retryReleaseAfterFailure(key, attempt);
37203
+ }
37204
+ }
37205
+ async retryReleaseAfterFailure(key, previousAttempt) {
37206
+ const keyStr = turnFailureReleaseKey(key);
37207
+ try {
37208
+ const { attempts, exceeded } = await this.deps.taskService.recordTurnFailure(key);
37209
+ this.turnFailureAccountingFailures.delete(keyStr);
37210
+ if (exceeded)
37211
+ return;
37212
+ this.scheduleTurnFailureRelease(key, attempts, "release-retry", "release-retry");
37213
+ } catch (error51) {
37214
+ console.error(`[NativeDelivery:turn-failed-release-retry-error] chatroom=${key.chatroomId} role=${key.role} task=${key.taskId} error=${error51 instanceof Error ? error51.stack ?? error51.message : String(error51)}`);
37215
+ const failures = (this.turnFailureAccountingFailures.get(keyStr) ?? 0) + 1;
37216
+ this.turnFailureAccountingFailures.set(keyStr, failures);
37217
+ if (failures >= MAX_TURN_FAILURE_ACCOUNTING_FAILURES) {
37218
+ this.turnFailureAccountingFailures.delete(keyStr);
37219
+ this.cancelTurnFailureRelease(key);
37220
+ console.error(`[NativeDelivery:turn-failure-accounting-stalled] chatroom=${key.chatroomId} role=${key.role} task=${key.taskId} failures=${failures}`);
37221
+ try {
37222
+ await this.deps.taskService.parkTurnFailure(key);
37223
+ } catch (parkError) {
37224
+ console.error(`[NativeDelivery:turn-failure-park-error] chatroom=${key.chatroomId} role=${key.role} task=${key.taskId} error=${parkError instanceof Error ? parkError.stack ?? parkError.message : String(parkError)}`);
37225
+ }
37226
+ return;
37227
+ }
37228
+ this.scheduleTurnFailureRelease(key, previousAttempt, "release-retry", "release-retry");
37229
+ }
37230
+ }
37231
+ cancelTurnFailureRelease(key) {
37232
+ const keyStr = turnFailureReleaseKey(key);
37233
+ const entry = this.turnFailureReleaseTimers.get(keyStr);
37234
+ if (entry === undefined)
37235
+ return;
37236
+ clearTimeout(entry.timer);
37237
+ this.turnFailureReleaseTimers.delete(keyStr);
37238
+ this.turnFailureAccountingFailures.delete(keyStr);
37239
+ this.deps.taskService.clearTurnFailureReleasePending(key);
37240
+ }
37241
+ cancelTurnFailureReleasesForRole(chatroomId, role) {
37242
+ const prefix = `${chatroomId}:${role.toLowerCase()}:`;
37243
+ for (const [keyStr, entry] of [...this.turnFailureReleaseTimers]) {
37244
+ if (!keyStr.startsWith(prefix))
37245
+ continue;
37246
+ clearTimeout(entry.timer);
37247
+ this.turnFailureReleaseTimers.delete(keyStr);
37248
+ this.turnFailureAccountingFailures.delete(keyStr);
37249
+ this.deps.taskService.clearTurnFailureReleasePending(entry.key);
37094
37250
  }
37095
- return "released";
37096
37251
  }
37097
37252
  async handleAgentTurnEnded(event) {
37098
37253
  const completion = event.completion;
@@ -37104,14 +37259,11 @@ class AgentWorkManager {
37104
37259
  const errorDetail = completion.error;
37105
37260
  console.error(`[NativeDelivery:turn-failed] chatroom=${event.chatroomId} role=${event.role} task=${activeTask2?.taskId ?? "none"} turn=${completion.turnId} status=${completion.status} source=${completion.source} error=${errorDetail ?? "none"}`);
37106
37261
  if (activeTask2) {
37107
- const recovery = await this.recoverTaskAfterTurnFailure(event, activeTask2, "turn-failed");
37262
+ const recovery = await this.recoverTaskAfterTurnFailure(event, activeTask2);
37108
37263
  if (recovery === "exceeded") {
37109
37264
  this.deps.agentTaskState.clear({ chatroomId: event.chatroomId, role: event.role });
37110
37265
  return { kind: "release-slot" };
37111
37266
  }
37112
- if (recovery === "recovery-failed") {
37113
- return { kind: "hold-slot", reason: "task-recovery-failed" };
37114
- }
37115
37267
  this.deps.agentTaskState.clear({ chatroomId: event.chatroomId, role: event.role });
37116
37268
  }
37117
37269
  const fact = {
@@ -37300,7 +37452,9 @@ class AgentWorkManager {
37300
37452
  return;
37301
37453
  }
37302
37454
  if (outcome.kind === "start-failed") {
37303
- console.error(`[NativeDelivery:uncovered-restart-failed] chatroom=${chatroomId} role=${role} task=${task.taskId} attempt=1 of ${MAX_RESPAWN_START_ATTEMPTS} error=${outcome.error}`);
37455
+ const cliDriven = isCliDrivenHarness(startInput.agentHarness);
37456
+ const attemptLabel = cliDriven ? `1 of ${MAX_RESPAWN_START_ATTEMPTS}` : "1";
37457
+ console.error(`[NativeDelivery:uncovered-restart-failed] chatroom=${chatroomId} role=${role} task=${task.taskId} attempt=${attemptLabel} error=${outcome.error}`);
37304
37458
  this.beginRespawnStartRetries({ chatroomId, role, workingDir, startInput });
37305
37459
  }
37306
37460
  }
@@ -37315,9 +37469,13 @@ class AgentWorkManager {
37315
37469
  };
37316
37470
  }
37317
37471
  }
37318
- beginRespawnStartRetries(request3) {
37472
+ beginRespawnStartRetries(input) {
37319
37473
  if (this.disposed)
37320
37474
  return;
37475
+ const request3 = {
37476
+ ...input,
37477
+ cliDriven: isCliDrivenHarness(input.startInput.agentHarness)
37478
+ };
37321
37479
  const key = recoveryKey(request3.chatroomId, request3.role);
37322
37480
  this.cancelRespawnStart(key, "superseded_by_new_restart");
37323
37481
  this.respawnRestartSeq += 1;
@@ -37332,11 +37490,27 @@ class AgentWorkManager {
37332
37490
  armRespawnStartRetry(key, pending) {
37333
37491
  if (this.disposed)
37334
37492
  return;
37335
- const delay3 = Math.min(30000, 1000 * 2 ** (pending.attempts - 1));
37493
+ const delay3 = computeBackoffDelay(pending.attempts, {
37494
+ baseMs: RESPAWN_RETRY_BASE_MS,
37495
+ maxMs: restartRetryMaxIntervalMs(pending.request.cliDriven),
37496
+ jitter: 0
37497
+ });
37336
37498
  pending.timer = setTimeout(() => {
37337
37499
  pending.timer = undefined;
37338
37500
  this.retryRespawnStart(key, pending.restartId).catch((error51) => {
37339
- console.error(`[NativeDelivery:uncovered-restart-error] chatroom=${pending.request.chatroomId} role=${pending.request.role} error=${error51 instanceof Error ? error51.stack ?? error51.message : String(error51)}`);
37501
+ const current = this.respawnStartRetries.get(key);
37502
+ const message = error51 instanceof Error ? error51.stack ?? error51.message : String(error51);
37503
+ console.error(`[NativeDelivery:uncovered-restart-error] chatroom=${pending.request.chatroomId} role=${pending.request.role} error=${message}`);
37504
+ if (!current || current.restartId !== pending.restartId || this.disposed)
37505
+ return;
37506
+ current.attempts += 1;
37507
+ if (current.request.cliDriven && current.attempts >= MAX_RESPAWN_START_ATTEMPTS) {
37508
+ this.respawnStartRetries.delete(key);
37509
+ console.error(`[NativeDelivery:uncovered-restart-failed] attempts exhausted chatroom=${current.request.chatroomId} role=${current.request.role} attempts=${current.attempts} error=${message}`);
37510
+ return;
37511
+ }
37512
+ console.error(`[NativeDelivery:uncovered-restart-error-retry] chatroom=${current.request.chatroomId} role=${current.request.role} attempt=${current.attempts} error=${message}`);
37513
+ this.armRespawnStartRetry(key, current);
37340
37514
  });
37341
37515
  }, delay3);
37342
37516
  }
@@ -37344,7 +37518,7 @@ class AgentWorkManager {
37344
37518
  const pending = this.respawnStartRetries.get(key);
37345
37519
  if (!pending || pending.restartId !== restartId || this.disposed)
37346
37520
  return;
37347
- const { chatroomId, role, workingDir, startInput } = pending.request;
37521
+ const { chatroomId, role } = pending.request;
37348
37522
  if (this.deps.agentMgr.isStopRequested(chatroomId, role)) {
37349
37523
  this.cancelRespawnStart(key, "stop_requested");
37350
37524
  return;
@@ -37354,6 +37528,33 @@ class AgentWorkManager {
37354
37528
  this.cancelRespawnStart(key, "agent_already_running");
37355
37529
  return;
37356
37530
  }
37531
+ const config3 = this.deps.configurationService.get(chatroomId, role);
37532
+ if (!config3) {
37533
+ if (this.deps.configurationService.state?.(chatroomId, role) === "syncing") {
37534
+ console.log(`[NativeDelivery:uncovered-restart-config-syncing] chatroom=${chatroomId} role=${role} attempts=${pending.attempts}`);
37535
+ this.armRespawnStartRetry(key, pending);
37536
+ return;
37537
+ }
37538
+ this.cancelRespawnStart(key, "config_removed");
37539
+ return;
37540
+ }
37541
+ const previous = pending.request;
37542
+ if (config3.agentHarness !== previous.startInput.agentHarness || config3.model !== previous.startInput.model || config3.workingDir !== previous.workingDir) {
37543
+ const startInput2 = {
37544
+ ...previous.startInput,
37545
+ agentHarness: config3.agentHarness,
37546
+ model: config3.model,
37547
+ workingDir: config3.workingDir
37548
+ };
37549
+ pending.request = {
37550
+ ...previous,
37551
+ workingDir: config3.workingDir,
37552
+ startInput: startInput2,
37553
+ cliDriven: isCliDrivenHarness(config3.agentHarness)
37554
+ };
37555
+ console.log(`[NativeDelivery:uncovered-restart-config-changed] chatroom=${chatroomId} role=${role} harness=${config3.agentHarness} model=${config3.model} workingDir=${config3.workingDir}`);
37556
+ }
37557
+ const { workingDir, startInput } = pending.request;
37357
37558
  const outcome = await this.deps.runSerializedForAgent({ chatroomId, role, workingDir }, { timeoutMs: AGENT_LIFECYCLE_OPERATION_TIMEOUT_MS }, async (ops, context3) => {
37358
37559
  if (this.respawnStartRetries.get(key)?.restartId !== restartId) {
37359
37560
  return { kind: "skipped", reason: "superseded" };
@@ -37379,12 +37580,13 @@ class AgentWorkManager {
37379
37580
  return;
37380
37581
  }
37381
37582
  current.attempts += 1;
37382
- if (current.attempts >= MAX_RESPAWN_START_ATTEMPTS) {
37583
+ if (current.request.cliDriven && current.attempts >= MAX_RESPAWN_START_ATTEMPTS) {
37383
37584
  this.respawnStartRetries.delete(key);
37384
37585
  console.error(`[NativeDelivery:uncovered-restart-failed] attempts exhausted chatroom=${chatroomId} role=${role} attempts=${current.attempts} error=${outcome.error}`);
37385
37586
  return;
37386
37587
  }
37387
- console.error(`[NativeDelivery:uncovered-restart-failed] chatroom=${chatroomId} role=${role} attempt=${current.attempts} of ${MAX_RESPAWN_START_ATTEMPTS} error=${outcome.error}`);
37588
+ const attemptLabel = current.request.cliDriven ? `${current.attempts} of ${MAX_RESPAWN_START_ATTEMPTS}` : `${current.attempts}`;
37589
+ console.error(`[NativeDelivery:uncovered-restart-failed] chatroom=${chatroomId} role=${role} attempt=${attemptLabel} error=${outcome.error}`);
37388
37590
  this.armRespawnStartRetry(key, current);
37389
37591
  }
37390
37592
  cancelRespawnStart(key, reason) {
@@ -37436,6 +37638,12 @@ class AgentWorkManager {
37436
37638
  }
37437
37639
  dispose() {
37438
37640
  this.disposed = true;
37641
+ for (const entry of this.turnFailureReleaseTimers.values()) {
37642
+ clearTimeout(entry.timer);
37643
+ this.deps.taskService.clearTurnFailureReleasePending(entry.key);
37644
+ }
37645
+ this.turnFailureReleaseTimers.clear();
37646
+ this.turnFailureAccountingFailures.clear();
37439
37647
  for (const pending of this.respawnStartRetries.values()) {
37440
37648
  if (pending.timer)
37441
37649
  clearTimeout(pending.timer);
@@ -37500,6 +37708,20 @@ class AgentWorkManager {
37500
37708
  if (notification.event.eventType === "task_deleted" /* TaskDeleted */) {
37501
37709
  await this.cancelTaskWork(notification.event.chatroomId, notification.event.role, notification.event.taskId);
37502
37710
  }
37711
+ if (notification.event.eventType === "task_assigned" /* TaskAssigned */) {
37712
+ this.cancelTurnFailureRelease({
37713
+ chatroomId: notification.event.chatroomId,
37714
+ role: notification.event.role,
37715
+ taskId: notification.event.taskId
37716
+ });
37717
+ }
37718
+ if (notification.event.task?.status === "completed") {
37719
+ this.cancelTurnFailureRelease({
37720
+ chatroomId: notification.event.chatroomId,
37721
+ role: notification.event.role,
37722
+ taskId: notification.event.taskId
37723
+ });
37724
+ }
37503
37725
  if (notification.event.eventType === "task_deleted" /* TaskDeleted */) {
37504
37726
  return { handledEventIds: [notification.event.eventId] };
37505
37727
  }
@@ -37545,6 +37767,7 @@ class AgentWorkManager {
37545
37767
  }
37546
37768
  }
37547
37769
  async cancelTaskWork(chatroomId, role, taskId) {
37770
+ this.cancelTurnFailureRelease({ chatroomId, role, taskId });
37548
37771
  const activeTask = this.deps.agentTaskState.get({ chatroomId, role });
37549
37772
  if (activeTask?.taskId !== taskId)
37550
37773
  return;
@@ -37653,7 +37876,7 @@ class AgentWorkManager {
37653
37876
  this.deps.agentTaskState.start(args2);
37654
37877
  }
37655
37878
  }
37656
- var MAX_RESPAWN_START_ATTEMPTS = 3, MAX_UNCOVERED_NUDGE_SEND_ATTEMPTS = 3, UNCOVERED_NUDGE_RETRY_MS = 250, recoveryKey = (chatroomId, role) => `${chatroomId}:${role.toLowerCase()}`, sessionKeyFor = (pid, harnessSessionId) => `${pid}:${harnessSessionId ?? "none"}`;
37879
+ var MAX_RESPAWN_START_ATTEMPTS = 3, MAX_UNCOVERED_NUDGE_SEND_ATTEMPTS = 3, UNCOVERED_NUDGE_RETRY_MS = 250, MAX_TURN_FAILURE_ACCOUNTING_FAILURES = 5, RECOVERY_RETRY_BACKOFF, RESPAWN_RETRY_BASE_MS = 1000, recoveryKey = (chatroomId, role) => `${chatroomId}:${role.toLowerCase()}`, turnFailureReleaseKey = ({ chatroomId, role, taskId }) => `${chatroomId}:${role.toLowerCase()}:${taskId}`, sessionKeyFor = (pid, harnessSessionId) => `${pid}:${harnessSessionId ?? "none"}`;
37657
37880
  var init_agent_work_manager = __esm(() => {
37658
37881
  init_reliability();
37659
37882
  init_agent();
@@ -37663,11 +37886,15 @@ var init_agent_work_manager = __esm(() => {
37663
37886
  init_agent_slot();
37664
37887
  init_assigned_task();
37665
37888
  init_check_agent_slot();
37889
+ init_compute_backoff_delay();
37666
37890
  init_native_task_injector_logic();
37667
37891
  init_convex_native_task_delivery_gateway();
37668
37892
  init_daemon_audit_port();
37669
37893
  init_task_delivery_processor();
37670
37894
  init_native_task_injector();
37895
+ init_is_cli_driven_harness();
37896
+ init_restart_retry_interval();
37897
+ RECOVERY_RETRY_BACKOFF = { baseMs: 1000, maxMs: 30000, jitter: 0 };
37671
37898
  });
37672
37899
 
37673
37900
  // src/daemon/services/agent-process-service/service/index.ts
@@ -39636,11 +39863,6 @@ class AgentProcessManager {
39636
39863
  }
39637
39864
  };
39638
39865
  const dispositions = await Promise.all([...this.agentTurnEndedHandlers].map((handler) => handler(event)));
39639
- const held = dispositions.find((disposition) => disposition?.kind === "hold-slot");
39640
- if (held?.kind === "hold-slot") {
39641
- console.warn(`[AgentProcessManager] Native turn slot held after ${completionStatus}: role=${opts.role} pid=${opts.pid} reason=${held.reason}`);
39642
- return;
39643
- }
39644
39866
  if (completionStatus !== "completed" && !dispositions.some((disposition) => disposition?.kind === "release-slot")) {
39645
39867
  console.warn(`[AgentProcessManager] Native turn slot held: no task-service disposition returned role=${opts.role} pid=${opts.pid} status=${completionStatus}`);
39646
39868
  return;
@@ -40799,14 +41021,80 @@ function createTaskService(deps) {
40799
41021
  let bootstrapRecoveryAttempts = 0;
40800
41022
  let bootstrapRecoveryTimer;
40801
41023
  let bootstrapRecoveryPromise;
40802
- const TURN_FAILURE_RETRY_CAP = 3;
41024
+ const TURN_FAILURE_RETRY_CAP = 8;
40803
41025
  const turnFailureCounts = new Map;
40804
41026
  const uncoveredLadder = new Map;
40805
41027
  const redeliveryExhaustedKeys = new Set;
41028
+ const rearmCycleCounts = new Map;
41029
+ const rearmTimers = new Map;
41030
+ const pendingTurnFailureReleases = new Set;
40806
41031
  const redeliveryKey = (chatroomId, role, taskId) => `${chatroomId}:${role.toLowerCase()}:${taskId}`;
40807
- const parkTask = async (args2, attempts) => {
40808
- const key = redeliveryKey(args2.chatroomId, args2.role, args2.taskId);
40809
- redeliveryExhaustedKeys.add(key);
41032
+ const clearRearmTimer = (key) => {
41033
+ const timer = rearmTimers.get(key);
41034
+ if (timer === undefined)
41035
+ return;
41036
+ clearTimeout(timer);
41037
+ rearmTimers.delete(key);
41038
+ };
41039
+ const resetRearmBudget = (key) => {
41040
+ clearRearmTimer(key);
41041
+ rearmCycleCounts.delete(key);
41042
+ };
41043
+ const scheduleRedeliveryRearm = (args2) => {
41044
+ if (inboxStopped)
41045
+ return;
41046
+ const { key } = args2;
41047
+ const cycles = rearmCycleCounts.get(key) ?? 0;
41048
+ if (cycles >= MAX_REARM_CYCLES) {
41049
+ console.log(`[NativeDelivery:redelivery-rearm-exhausted] chatroom=${args2.chatroomId} role=${args2.role} task=${args2.taskId} cycles=${MAX_REARM_CYCLES}`);
41050
+ return;
41051
+ }
41052
+ clearRearmTimer(key);
41053
+ const delay3 = REARM_DELAYS_MS[cycles];
41054
+ const timer = setTimeout(() => {
41055
+ rearmTimers.delete(key);
41056
+ runRedeliveryRearm({ ...args2, delay: delay3, cycle: cycles + 1 });
41057
+ }, delay3);
41058
+ rearmTimers.set(key, timer);
41059
+ };
41060
+ const runRedeliveryRearm = async (args2) => {
41061
+ const { chatroomId, role, taskId, key } = args2;
41062
+ const current = taskInboxState.getForRole(chatroomId, role, taskId);
41063
+ if (!current || current.status !== "in_progress" || !redeliveryExhaustedKeys.has(key)) {
41064
+ console.log(`[NativeDelivery:redelivery-rearm-skipped] reason=task_not_current chatroom=${chatroomId} role=${role} task=${taskId} status=${current?.status ?? "missing"}`);
41065
+ return;
41066
+ }
41067
+ rearmCycleCounts.set(key, args2.cycle);
41068
+ redeliveryExhaustedKeys.delete(key);
41069
+ turnFailureCounts.delete(key);
41070
+ try {
41071
+ await gateway.clearDeliveryFailure({
41072
+ sessionId: deps.sessionId,
41073
+ machineId: deps.machineId,
41074
+ taskId
41075
+ });
41076
+ } catch (error51) {
41077
+ console.warn(`[TaskService] redelivery-rearm clear failure failed chatroom=${chatroomId} role=${role} task=${taskId}: ${error51 instanceof Error ? error51.message : String(error51)}`);
41078
+ }
41079
+ let released = false;
41080
+ try {
41081
+ const result3 = await releaseTask({ chatroomId, role, taskId });
41082
+ released = result3.released;
41083
+ if (!released) {
41084
+ console.warn(`[TaskService] redelivery-rearm release skipped chatroom=${chatroomId} role=${role} task=${taskId} cycle=${args2.cycle}`);
41085
+ }
41086
+ } catch (error51) {
41087
+ console.error(`[TaskService] redelivery-rearm release failed chatroom=${chatroomId} role=${role} task=${taskId} cycle=${args2.cycle}: ${error51 instanceof Error ? error51.message : String(error51)}`);
41088
+ }
41089
+ if (!released) {
41090
+ redeliveryExhaustedKeys.add(key);
41091
+ await recordExhaustedOnBackend({ chatroomId, role, taskId });
41092
+ scheduleRedeliveryRearm(args2);
41093
+ return;
41094
+ }
41095
+ console.log(`[NativeDelivery:redelivery-rearm] cycle=${args2.cycle}/${MAX_REARM_CYCLES} chatroom=${chatroomId} role=${role} task=${taskId} delayMs=${args2.delay}`);
41096
+ };
41097
+ const recordExhaustedOnBackend = async (args2) => {
40810
41098
  try {
40811
41099
  await gateway.recordDeliveryFailure({
40812
41100
  sessionId: deps.sessionId,
@@ -40817,19 +41105,31 @@ function createTaskService(deps) {
40817
41105
  } catch (error51) {
40818
41106
  console.warn(`[TaskService] redelivery-exhausted failure record failed chatroom=${args2.chatroomId} role=${args2.role} task=${args2.taskId}: ${error51 instanceof Error ? error51.message : String(error51)}`);
40819
41107
  }
41108
+ };
41109
+ const parkTask = async (args2, reason, attempts) => {
41110
+ const key = redeliveryKey(args2.chatroomId, args2.role, args2.taskId);
41111
+ redeliveryExhaustedKeys.add(key);
41112
+ await recordExhaustedOnBackend(args2);
40820
41113
  const attemptsSuffix = attempts === undefined ? "" : ` attempts=${attempts}`;
40821
- console.log(`[NativeDelivery:redelivery-exhausted] chatroom=${args2.chatroomId} role=${args2.role} task=${args2.taskId}${attemptsSuffix} — release/redelivery stopped; agent restart (user.start) resets`);
41114
+ const rearmCycles = rearmCycleCounts.get(key) ?? 0;
41115
+ const rearmDelay = reason === "provider_failure" && rearmCycles < MAX_REARM_CYCLES ? REARM_DELAYS_MS[rearmCycles] : undefined;
41116
+ console.log(`[NativeDelivery:redelivery-exhausted] chatroom=${args2.chatroomId} role=${args2.role} task=${args2.taskId}${attemptsSuffix} reason=${reason} rearm=${rearmDelay ?? "none"} — release/redelivery stopped; user.start or explicit recovery resets`);
41117
+ if (reason === "provider_failure") {
41118
+ scheduleRedeliveryRearm({ ...args2, key });
41119
+ }
40822
41120
  };
40823
41121
  const recordTurnFailure = async (args2) => {
40824
41122
  const key = redeliveryKey(args2.chatroomId, args2.role, args2.taskId);
40825
- if (redeliveryExhaustedKeys.has(key))
40826
- return { exceeded: true };
40827
- const attempts = (turnFailureCounts.get(key) ?? 0) + 1;
41123
+ const recordedAttempts = turnFailureCounts.get(key) ?? 0;
41124
+ if (redeliveryExhaustedKeys.has(key)) {
41125
+ return { attempts: recordedAttempts, exceeded: true };
41126
+ }
41127
+ const attempts = recordedAttempts + 1;
40828
41128
  turnFailureCounts.set(key, attempts);
40829
41129
  if (attempts < TURN_FAILURE_RETRY_CAP)
40830
- return { exceeded: false };
40831
- await parkTask(args2, attempts);
40832
- return { exceeded: true };
41130
+ return { attempts, exceeded: false };
41131
+ await parkTask(args2, "provider_failure", attempts);
41132
+ return { attempts, exceeded: true };
40833
41133
  };
40834
41134
  const decideUncoveredTurnEnd2 = async (args2) => {
40835
41135
  const key = redeliveryKey(args2.chatroomId, args2.role, args2.taskId);
@@ -40843,7 +41143,7 @@ function createTaskService(deps) {
40843
41143
  if (decision.action === "restart-session") {
40844
41144
  uncoveredLadder.set(key, withSessionRestart(entry));
40845
41145
  } else if (decision.action === "park") {
40846
- await parkTask(args2);
41146
+ await parkTask(args2, "uncovered_turn");
40847
41147
  }
40848
41148
  return decision;
40849
41149
  };
@@ -40862,6 +41162,7 @@ function createTaskService(deps) {
40862
41162
  turnFailureCounts.delete(key);
40863
41163
  uncoveredLadder.delete(key);
40864
41164
  redeliveryExhaustedKeys.delete(key);
41165
+ resetRearmBudget(key);
40865
41166
  };
40866
41167
  const forgetStaleTask = ({
40867
41168
  chatroomId,
@@ -40902,11 +41203,19 @@ function createTaskService(deps) {
40902
41203
  if (key.startsWith(prefix))
40903
41204
  redeliveryExhaustedKeys.delete(key);
40904
41205
  }
41206
+ for (const key of [...rearmTimers.keys()]) {
41207
+ if (key.startsWith(prefix))
41208
+ resetRearmBudget(key);
41209
+ }
41210
+ for (const key of [...rearmCycleCounts.keys()]) {
41211
+ if (key.startsWith(prefix))
41212
+ rearmCycleCounts.delete(key);
41213
+ }
40905
41214
  };
40906
41215
  const scheduleBootstrapRecovery = () => {
40907
41216
  if (inboxStopped || bootstrapRecoveryTimer || !bootstrapRecoveryPending)
40908
41217
  return;
40909
- const delay3 = Math.min(30000, 1000 * 2 ** bootstrapRecoveryAttempts++);
41218
+ const delay3 = computeBackoffDelay(++bootstrapRecoveryAttempts, BOOTSTRAP_RECOVERY_BACKOFF);
40910
41219
  bootstrapRecoveryTimer = setTimeout(() => {
40911
41220
  bootstrapRecoveryTimer = undefined;
40912
41221
  if (!inboxStopped)
@@ -41123,6 +41432,11 @@ function createTaskService(deps) {
41123
41432
  },
41124
41433
  stopTaskInbox: () => {
41125
41434
  inboxStopped = true;
41435
+ for (const timer of rearmTimers.values())
41436
+ clearTimeout(timer);
41437
+ rearmTimers.clear();
41438
+ rearmCycleCounts.clear();
41439
+ pendingTurnFailureReleases.clear();
41126
41440
  if (bootstrapRecoveryTimer)
41127
41441
  clearTimeout(bootstrapRecoveryTimer);
41128
41442
  bootstrapRecoveryTimer = undefined;
@@ -41213,7 +41527,8 @@ function createTaskService(deps) {
41213
41527
  }
41214
41528
  if (mode === "automatic") {
41215
41529
  const tracking = { chatroomId: task.chatroomId, role: taskRole, taskId: task.taskId };
41216
- if (isRedeliveryExhausted(tracking)) {
41530
+ const key = redeliveryKey(task.chatroomId, taskRole, task.taskId);
41531
+ if (isRedeliveryExhausted(tracking) || rearmTimers.has(key) || pendingTurnFailureReleases.has(key)) {
41217
41532
  skipped += 1;
41218
41533
  continue;
41219
41534
  }
@@ -41299,6 +41614,7 @@ function createTaskService(deps) {
41299
41614
  ...expectedReason ? { expectedReason } : {}
41300
41615
  }),
41301
41616
  recordTurnFailure,
41617
+ parkTurnFailure: (args2) => parkTask(args2, "provider_failure"),
41302
41618
  decideUncoveredTurnEnd: decideUncoveredTurnEnd2,
41303
41619
  confirmNudgeSent,
41304
41620
  resetTurnTracking,
@@ -41306,18 +41622,29 @@ function createTaskService(deps) {
41306
41622
  clearRedeliveryTracking: ({ chatroomId, role }) => {
41307
41623
  clearRoleRedeliveryTracking(chatroomId, role);
41308
41624
  },
41625
+ markTurnFailureReleasePending: ({ chatroomId, role, taskId }) => {
41626
+ pendingTurnFailureReleases.add(redeliveryKey(chatroomId, role, taskId));
41627
+ },
41628
+ clearTurnFailureReleasePending: ({ chatroomId, role, taskId }) => {
41629
+ pendingTurnFailureReleases.delete(redeliveryKey(chatroomId, role, taskId));
41630
+ },
41309
41631
  forgetStaleTask
41310
41632
  };
41311
41633
  return service4;
41312
41634
  }
41635
+ var BOOTSTRAP_RECOVERY_BACKOFF, REARM_DELAYS_MS, MAX_REARM_CYCLES;
41313
41636
  var init_task_service = __esm(() => {
41314
41637
  init_chatroom_workspace_task_inbox();
41315
41638
  init_pending_task_reconciliation_watcher();
41316
41639
  init_task_inbox_acknowledgement_retry_watcher();
41317
41640
  init_task_inbox_delivery_retry_watcher();
41318
41641
  init_api3();
41642
+ init_compute_backoff_delay();
41319
41643
  init_uncovered_turn_ladder();
41320
41644
  init_convex_native_task_delivery_gateway();
41645
+ BOOTSTRAP_RECOVERY_BACKOFF = { baseMs: 1000, maxMs: 30000, jitter: 0 };
41646
+ REARM_DELAYS_MS = [15 * 60000, 30 * 60000, 60 * 60000];
41647
+ MAX_REARM_CYCLES = REARM_DELAYS_MS.length;
41321
41648
  });
41322
41649
 
41323
41650
  // src/daemon/services/task-service/infrastructure/repository/task-handoff-repository.ts
@@ -100031,7 +100358,7 @@ var init_team_presets = __esm(() => {
100031
100358
  duo: {
100032
100359
  structureId: TEAM_STRUCTURE_IDS.duo,
100033
100360
  name: "Duo",
100034
- description: "A planner and builder working as a pair, planner as coordinator, with optional ephemeral architect, researcher, triage, reviewer, uiux-engineer, and verifier roles",
100361
+ description: "A planner and builder working as a pair, planner as coordinator, with optional ephemeral architect, researcher, triage, reviewer, uiux-engineer, game-asset-designer, and verifier roles",
100035
100362
  roles: getBuiltinRolesForTeam("duo"),
100036
100363
  entryPoint: "planner"
100037
100364
  },
@@ -100810,6 +101137,166 @@ var init_builder = __esm(() => {
100810
101137
  };
100811
101138
  });
100812
101139
 
101140
+ // ../../services/backend/prompts/teams/game-asset-designer-evidence.ts
101141
+ var GAME_ASSET_EVIDENCE_LABELS, GAME_ASSET_EVIDENCE_LABEL_LIST, GAME_ASSET_NEVER_CLAIM_RULE = "Never claim a polycount, texel density, loudness, color space, or visual-quality result without `measured` or `inspected` evidence. Numeric results (polycount, texel density, resolution, loudness, true peak, sample rate, bit depth) and color-space slot checks require `measured`; `inspected` covers only visual or audible judgments. A budget or target you write into the spec is a requirement and is labelled `specified`, not a result.", GAME_ASSET_RESULT_RULE = "A checklist item is `pass` or `fail` only with `measured` or `inspected` evidence. Otherwise record it as `not checked` and keep its label (`specified` or `unverified`).", GAME_ASSET_EVIDENCE_DEFINITIONS = `- \`measured\` — a tool or script you ran on the actual file produced the number or read the metadata field; cite the command and its output value (this includes header or JSON values read by command).
101142
+ - \`inspected\` — you opened the actual file or a render and made a qualitative observation (silhouette, seam, palette, style match); describe what you saw and give no numbers.
101143
+ - \`specified\` — a target or requirement from the brief or your own spec; no file was checked. A budget you write into a spec is \`specified\`.
101144
+ - \`unverified\` — a result that should have been checked but could not be (no file access, no tool, unreadable file), or a value someone else stated that you did not check; say why and name the source.`;
101145
+ var init_game_asset_designer_evidence = __esm(() => {
101146
+ GAME_ASSET_EVIDENCE_LABELS = ["measured", "inspected", "specified", "unverified"];
101147
+ GAME_ASSET_EVIDENCE_LABEL_LIST = GAME_ASSET_EVIDENCE_LABELS.map((label) => `\`${label}\``).join(", ");
101148
+ });
101149
+
101150
+ // ../../services/backend/prompts/teams/game-asset-designer-handoff-templates.ts
101151
+ function getEntryPointToGameAssetDesignerHandoffTemplate(query) {
101152
+ const entryPoint = query.fromRole;
101153
+ return `Instructions (not part of the message): Pass the asset request as a brief the designer can act on without guessing. Provide every field below; write \`Not specified.\` for a field you cannot fill and the designer will ask or apply a stated default. Give each asset a stable ID (\`A01\`, \`A02\`…). Use \`spec only\` only when you want a specification with no files; the default is \`spec and generate\`.
101154
+
101155
+ ## Asset request
101156
+ <one to three sentences: what the assets are for and the user-visible goal>
101157
+
101158
+ ## Target
101159
+ <engine and version; genre and camera; platform tier (mobile, PC, console, VR); render pipeline if known>
101160
+
101161
+ ## Assets requested
101162
+ <numbered list with stable IDs \`A01\`, \`A02\`…; each item: asset name, category (3D model, PBR textures, 2D sprite/UI, audio, other), purpose, quantity or variants>
101163
+
101164
+ ## Budgets and conventions
101165
+ <triangle budget per LOD, texel density or pixel grid, texture resolution and atlas limits, loudness target, naming convention, folder layout; or \`Not specified.\`>
101166
+
101167
+ ## Style reference
101168
+ <repository paths, URLs, or a written style sheet; or \`Not specified.\`>
101169
+
101170
+ ## Spec consumer
101171
+ <who produces or integrates these assets from the spec: \`builder\`, \`uiux-engineer\`, an external artist, or \`user\`>
101172
+
101173
+ ## Existing assets
101174
+ <repository paths the designer must reuse or must not overwrite; or \`Not specified.\`>
101175
+
101176
+ ## Deliverable mode
101177
+ <\`spec and generate\` (default when you name an output location and allowed tools) or \`spec only\`>
101178
+
101179
+ ## Allowed tools
101180
+ <the generators the designer may run, or \`Any tool available in the designer's shell.\`; say which>
101181
+
101182
+ ## Allowed edit paths
101183
+ <repository-relative paths the designer may edit; \`None.\` for none>
101184
+
101185
+ ## Commit
101186
+ <\`None.\` or the commit message and the paths to commit>
101187
+
101188
+ ## Output location
101189
+ <the exact repository directory the designer may write to; required for \`spec and generate\`; \`None.\` for \`spec only\`>
101190
+
101191
+ ## Acceptance criteria
101192
+ <what must be true for the planner to accept the delivery>
101193
+
101194
+ Instructions after the body (not part of the message): run the normal handoff command to \`game-asset-designer\` with the complete body above, then stop. Use this template only under the conditions below; the body above ends at \`## Acceptance criteria\`.
101195
+
101196
+ ### When to use the game asset designer
101197
+ - the request creates or changes game art or audio assets (models, textures, sprites, UI art, sound effects);
101198
+ - assets must meet engine import conventions or budgets before a builder integrates them;
101199
+ - the user asks for an asset spec, style sheet, or asset audit.
101200
+
101201
+ Skip it for code-only work, for a trivial asset rename, and for non-game UI icons or illustrations (use \`uiux-engineer\`).
101202
+
101203
+ \`\`\`bash
101204
+ ${handoffCommand({
101205
+ chatroomId: "<chatroom-id>",
101206
+ role: entryPoint,
101207
+ nextRole: "game-asset-designer",
101208
+ cliEnvPrefix: ""
101209
+ })}
101210
+ \`\`\``;
101211
+ }
101212
+ function getGameAssetDesignerToEntryPointHandoffTemplate(query) {
101213
+ const entryPoint = query.toRole;
101214
+ return `Instructions (not part of the message): Return a complete asset delivery report for the request the planner named. State only what you established. Label every checked property with one evidence label: ${GAME_ASSET_EVIDENCE_LABEL_LIST}. Definitions: ${GAME_ASSET_EVIDENCE_DEFINITIONS} ${GAME_ASSET_NEVER_CLAIM_RULE} ${GAME_ASSET_RESULT_RULE} Write only under the named output location and the named edit paths. In \`spec only\` you write no files. Use the exact sections below, in this order. If you are blocked or need a clarification, send the same sections with Summary starting \`Blocked:\` and every section you could not reach written as \`Not started.\`
101215
+
101216
+ ## Summary
101217
+ <one to three sentences: what was delivered, whether it is spec only or includes files, and the count of \`fail\` and \`not checked\` items>
101218
+
101219
+ ## Target and assumptions
101220
+ <each assumption you made and why; each deviation from the planner's target or budgets; or \`None.\`>
101221
+
101222
+ ## Asset manifest
101223
+ <one entry per asset, keyed by the planner's stable ID (\`A01\`…): name, category, file format, path (or \`Specified only.\`), dimensions/units/scale, pivot, naming, LOD list with triangle budgets, texture set with resolution, color space and channel packing, atlas/pixel grid/frame data, audio format and loudness target, import settings, dependencies and variants; for each budget, the acceptance check (exact command and expected value); for each generated file, the generator (tool + version + command or seed)>
101224
+
101225
+ ## Quality checklist results
101226
+ <per asset: each applicable checklist item as \`pass\`, \`fail\`, \`n/a\`, or \`not checked\`, with its evidence label and, for \`measured\`, the command and value; write exactly \`None failed.\` after the list if no item failed>
101227
+
101228
+ ## Acceptance criteria results
101229
+ <each planner acceptance criterion as \`met\`, \`not met\`, or \`not checked\`, with its evidence label>
101230
+
101231
+ ## Style consistency
101232
+ <how the set matches the style reference; deviations; what was compared and how>
101233
+
101234
+ ## Created and edited paths
101235
+ <repository-relative paths and a one-line description each; write exactly \`None.\` for spec only>
101236
+
101237
+ ## Commits
101238
+ <write exactly \`None.\` if none; otherwise one line per commit: \`<sha> <message>\`>
101239
+
101240
+ ## Sources
101241
+ <URLs for engine or platform rules relied on, with version or date checked>
101242
+
101243
+ ## Open questions
101244
+ <each clarification needed from the planner with a recommended default; write exactly \`None.\` if none>
101245
+
101246
+ ## Not verified
101247
+ <every property you could not measure or inspect; write exactly \`None.\` if none>
101248
+
101249
+ ## Recommended next step
101250
+ <exactly one target: \`game-asset-designer\` (re-run with the listed asset IDs), \`builder\` (integrate, or apply the listed import settings), \`uiux-engineer\` (place the listed UI art in a screen design), or \`planner\` (decision needed on an open question); then the affected asset IDs>
101251
+
101252
+ Instructions after the body (not part of the message): run the normal handoff command to \`${entryPoint}\` with the complete body above, then stop.
101253
+
101254
+ \`\`\`bash
101255
+ ${handoffCommand({
101256
+ chatroomId: "<chatroom-id>",
101257
+ role: "game-asset-designer",
101258
+ nextRole: entryPoint,
101259
+ cliEnvPrefix: ""
101260
+ })}
101261
+ \`\`\``;
101262
+ }
101263
+ var init_game_asset_designer_handoff_templates = __esm(() => {
101264
+ init_game_asset_designer_evidence();
101265
+ init_command2();
101266
+ });
101267
+
101268
+ // ../../services/backend/prompts/teams/duo/handoff-templates/game-asset-designer-planner-code.ts
101269
+ function getGameAssetDesignerPlannerCodeTemplate(query) {
101270
+ return getGameAssetDesignerToEntryPointHandoffTemplate(query);
101271
+ }
101272
+ var init_game_asset_designer_planner_code = __esm(() => {
101273
+ init_game_asset_designer_handoff_templates();
101274
+ });
101275
+
101276
+ // ../../services/backend/prompts/teams/duo/handoff-templates/game-asset-designer-planner-chat.ts
101277
+ var init_game_asset_designer_planner_chat = __esm(() => {
101278
+ init_game_asset_designer_planner_code();
101279
+ });
101280
+
101281
+ // ../../services/backend/prompts/teams/duo/handoff-templates/game-asset-designer.ts
101282
+ var duoGameAssetDesignerHandoffContract;
101283
+ var init_game_asset_designer = __esm(() => {
101284
+ init_game_asset_designer_planner_chat();
101285
+ init_game_asset_designer_planner_code();
101286
+ init_contracts();
101287
+ duoGameAssetDesignerHandoffContract = {
101288
+ role: "game-asset-designer",
101289
+ receivesFrom: ["planner"],
101290
+ returnsTo: ["planner"],
101291
+ outboundTemplates: {
101292
+ planner: handoffTemplateByMode({
101293
+ chat: getGameAssetDesignerPlannerCodeTemplate,
101294
+ code: getGameAssetDesignerPlannerCodeTemplate
101295
+ })
101296
+ }
101297
+ };
101298
+ });
101299
+
100813
101300
  // ../../services/backend/prompts/teams/duo/handoff-templates/planner-architect-code.ts
100814
101301
  function getPlannerArchitectCodeTemplate(query) {
100815
101302
  return getEntryPointToArchitectHandoffTemplate(query);
@@ -100911,6 +101398,19 @@ var init_planner_builder_chat = __esm(() => {
100911
101398
  init_planner_builder_code();
100912
101399
  });
100913
101400
 
101401
+ // ../../services/backend/prompts/teams/duo/handoff-templates/planner-game-asset-designer-code.ts
101402
+ function getPlannerGameAssetDesignerCodeTemplate(query) {
101403
+ return getEntryPointToGameAssetDesignerHandoffTemplate(query);
101404
+ }
101405
+ var init_planner_game_asset_designer_code = __esm(() => {
101406
+ init_game_asset_designer_handoff_templates();
101407
+ });
101408
+
101409
+ // ../../services/backend/prompts/teams/duo/handoff-templates/planner-game-asset-designer-chat.ts
101410
+ var init_planner_game_asset_designer_chat = __esm(() => {
101411
+ init_planner_game_asset_designer_code();
101412
+ });
101413
+
100914
101414
  // ../../services/backend/prompts/teams/researcher-handoff-templates.ts
100915
101415
  function getEntryPointToResearcherHandoffTemplate(query) {
100916
101416
  const entryPoint = query.fromRole;
@@ -101562,6 +102062,8 @@ var init_planner = __esm(() => {
101562
102062
  init_planner_architect_code();
101563
102063
  init_planner_builder_chat();
101564
102064
  init_planner_builder_code();
102065
+ init_planner_game_asset_designer_chat();
102066
+ init_planner_game_asset_designer_code();
101565
102067
  init_planner_researcher_chat();
101566
102068
  init_planner_researcher_code();
101567
102069
  init_planner_reviewer_chat();
@@ -101585,6 +102087,7 @@ var init_planner = __esm(() => {
101585
102087
  "triage",
101586
102088
  "researcher",
101587
102089
  "reviewer",
102090
+ "game-asset-designer",
101588
102091
  "verifier"
101589
102092
  ],
101590
102093
  returnsTo: [
@@ -101595,6 +102098,7 @@ var init_planner = __esm(() => {
101595
102098
  "triage",
101596
102099
  "researcher",
101597
102100
  "reviewer",
102101
+ "game-asset-designer",
101598
102102
  "verifier"
101599
102103
  ],
101600
102104
  outboundTemplates: {
@@ -101622,6 +102126,10 @@ var init_planner = __esm(() => {
101622
102126
  chat: getPlannerReviewerCodeTemplate,
101623
102127
  code: getPlannerReviewerCodeTemplate
101624
102128
  }),
102129
+ "game-asset-designer": handoffTemplateByMode({
102130
+ chat: getPlannerGameAssetDesignerCodeTemplate,
102131
+ code: getPlannerGameAssetDesignerCodeTemplate
102132
+ }),
101625
102133
  verifier: handoffTemplateByMode({
101626
102134
  chat: getPlannerVerifierCodeTemplate,
101627
102135
  code: getPlannerVerifierCodeTemplate
@@ -101802,6 +102310,7 @@ var DUO_ROLE_HANDOFF_CATALOG, DUO_ROLE_HANDOFF_CONTRACTS, validatedDuoRoleHandof
101802
102310
  var init_handoff_templates = __esm(() => {
101803
102311
  init_architect();
101804
102312
  init_builder();
102313
+ init_game_asset_designer();
101805
102314
  init_planner();
101806
102315
  init_researcher();
101807
102316
  init_reviewer();
@@ -101816,6 +102325,7 @@ var init_handoff_templates = __esm(() => {
101816
102325
  researcher: duoResearcherHandoffContract,
101817
102326
  reviewer: duoReviewerHandoffContract,
101818
102327
  "uiux-engineer": duoUiuxEngineerHandoffContract,
102328
+ "game-asset-designer": duoGameAssetDesignerHandoffContract,
101819
102329
  triage: duoTriageHandoffContract,
101820
102330
  verifier: duoVerifierHandoffContract
101821
102331
  };
@@ -102628,6 +103138,79 @@ var init_builder2 = __esm(() => {
102628
103138
  init_sections();
102629
103139
  });
102630
103140
 
103141
+ // ../../services/backend/prompts/cli/roles/game-asset-designer.ts
103142
+ var GAME_ASSET_DESIGNER_GUIDANCE;
103143
+ var init_game_asset_designer2 = __esm(() => {
103144
+ init_game_asset_designer_evidence();
103145
+ init_sections();
103146
+ GAME_ASSET_DESIGNER_GUIDANCE = `## Game Asset Designer Operating Model
103147
+
103148
+ You are the game asset designer responsible for specifying production-quality game assets, and for producing them only when the planner's request and your available tools allow it. You own the asset specification, generate the asset files yourself, and edit only the asset-integration paths the brief allows (engine import or config files and the code that references the asset files). You do not implement application logic outside those paths. In \`spec only\` mode you write no files. You do not act as the builder or planner.
103149
+
103150
+ ${"{SESSION_CONTINUITY}"}
103151
+
103152
+ ${getInterRoleHandoffRule()}
103153
+
103154
+ 1. Recover the authoritative request from the injected task and the \`<handoff-templates>\` brief. Recover relevant chatroom history if the brief refers to earlier assets. Production quality is relative to a target, so confirm these inputs: target engine and version, genre and camera, platform tier (mobile, PC, console, VR), art style reference, asset categories, and budgets (triangles per LOD, texel density, texture resolution, atlas size, loudness). If an input is missing and the repository does not answer it, ask \`{ENTRY_POINT_ROLE}\` through the handoff with a clarification request. Do not guess a target engine or platform. For a non-blocking gap, apply the stated default and list it under assumptions. If you are blocked, send the same report to \`{ENTRY_POINT_ROLE}\` with Summary starting \`Blocked:\` and every section you could not reach written as \`Not started.\`
103155
+ 2. Inspect the repository for existing assets, naming and folder conventions, engine and import configuration, and style references. Match what exists unless the planner says otherwise. Cite paths.
103156
+ 3. Classify each asset as 3D model, PBR material or texture set, 2D sprite or UI art, audio, or other, and write one manifest entry per asset using the fields in the handoff template.
103157
+ 4. Specify against the target engine's documented import conventions. Prefer official engine documentation, the Khronos glTF 2.0 specification, and platform audio guidance. Cite the source for any rule you rely on. Treat community figures (albedo ranges, texel density tiers, polycount tiers) as starting points that the request's declared budgets override, and say so.
103158
+ 5. Apply the quality checklist below to every asset before handoff.
103159
+ 6. Say exactly how each claim was established, using only these evidence labels (${GAME_ASSET_EVIDENCE_LABEL_LIST}):
103160
+ ${GAME_ASSET_EVIDENCE_DEFINITIONS}
103161
+ ${GAME_ASSET_NEVER_CLAIM_RULE} ${GAME_ASSET_RESULT_RULE}
103162
+ 7. Generate the asset files yourself when the brief names an output location and allowed tools (default mode \`spec and generate\`). Use only the tools the brief allows, and use tools already available in your shell. Record the tool, its version, and the command or seed for every generated file in the manifest. Write only under the named output location. Edit files only at the paths named under \`Allowed edit paths\`. Never overwrite or delete an existing file unless the brief names it. Commit only when the brief names the paths and the commit message; otherwise leave changes uncommitted and say so. If \`Deliverable mode\` is \`spec only\`, write no files. If the output location is missing or ambiguous, deliver \`spec only\`, state that in Summary, and list the gap under Open questions. Describe generated output as production-ready only when every checklist item for it passes with \`measured\` or \`inspected\` evidence. Run each numeric check against the generated file itself, not against your own generator's settings.
103163
+
103164
+ In \`spec only\` the deliverable is a \`Specified only.\` manifest. Each entry has: a stable ID (\`A01\`, \`A02\`…); category; format; units, scale and pivot; name and path; budgets per property; import settings; dependencies and variants; and, for each budget, an acceptance check that gives the exact measuring command and expected value (for example \`ffmpeg -i <file> -af ebur128=peak=true -f null -\`, \`gltf-transform inspect <file>\`, \`ffprobe <file>\`).
103165
+ 8. Follow the injected \`<handoff-templates>\` asset delivery report exactly, in its order.
103166
+ 9. Hand the report to \`{ENTRY_POINT_ROLE}\` with the normal command, then stop:
103167
+
103168
+ \`\`\`bash
103169
+ chatroom handoff --chatroom-id="<chatroom-id>" --role="game-asset-designer" --next-role="{ENTRY_POINT_ROLE}"
103170
+ \`\`\`
103171
+
103172
+ ### Quality checklist (apply to each asset; mark each item pass, fail, or not applicable, with its evidence label)
103173
+
103174
+ **All assets**
103175
+ - Name follows the project convention (type prefix, descriptive name, variant, LOD suffix); ASCII, no spaces; folder matches the asset category.
103176
+ - File format is the one the target engine imports (glTF 2.0/GLB, FBX, PNG, WAV/OGG as named in the request); no unused data (hidden meshes, unused materials, history).
103177
+ - Style matches the named style reference: silhouette, value range, palette, level of detail, and texel density or pixel grid.
103178
+ - Budgets in the request are met; any exceedance is called out with the measured value.
103179
+
103180
+ **3D models**
103181
+ - Units and axes match the target (glTF: meters, +Y up, front +Z); scale is applied (identity scale) and a real-world size is stated.
103182
+ - Pivot/origin follows the declared policy; LODs share the base pivot and alignment.
103183
+ - Clean topology: no n-gons or non-manifold edges unless declared, no flipped normals or inverted winding (glTF: counter-clockwise front faces), consistent normals and tangents (MikkTSpace).
103184
+ - Triangle count per LOD is within budget; LOD naming matches the engine (Unity \`_LOD0\`, \`_LOD1\`; Unreal LODs share the base pivot and alignment and are imported per the FBX import options; Godot generates LODs at import, so declare the Generate LODs setting instead of a suffix).
103185
+ - UVs: non-overlapping unique layout where textures are unique, island padding, consistent texel density, no stretched islands; a second UV set if the engine needs baked lighting.
103186
+ - Collision and sockets use the engine's naming (Unreal \`UBX_\`/\`UCP_\`/\`USP_\`/\`UCX_\` + mesh name and \`SOCKET_\` + mesh name; Godot \`-col\`/\`-convcolonly\` suffixes).
103187
+ - Skinned meshes: bone count within budget, influences per vertex within the declared budget (commonly 4, marked \`unverified\`; glTF allows more via \`JOINTS_1\`), no leftover IK helpers, a named rest pose.
103188
+
103189
+ **PBR materials and textures**
103190
+ - Metallic-roughness workflow with documented channel packing (glTF: roughness in G, metallic in B, occlusion in R).
103191
+ - Color space: base color and emissive sRGB; normal, roughness, metallic, occlusion, masks linear. State the normal map Y convention. In glTF, color space is set by the material slot that references the image, not by embedded PNG chunks. Verify the slot for each map: \`baseColorTexture\` and \`emissiveTexture\` are sRGB; \`metallicRoughnessTexture\`, \`occlusionTexture\` and \`normalTexture\` are linear.
103192
+ - Base color values stay in a physically plausible range (community starting range roughly 30–240 sRGB per channel for non-metals; the request's budget overrides it); metallic is near binary.
103193
+ - Resolution matches the texel-density budget and surface size; dimensions meet the engine/platform rule (power of two where the engine needs mipmaps or streaming, for example Unreal; confirm for the target engine version); compression format named.
103194
+ - Tiling textures wrap without a visible seam; no baked-in lighting or text in base color unless declared.
103195
+ - Maps in one set share texel density, value range, and naming (\`T_Name_BaseColor\`, \`_Normal\`, \`_ORM\`).
103196
+
103197
+ **2D sprites and UI art**
103198
+ - Fixed grid and pixels-per-unit declared; sprite sizes are multiples of the grid; no sub-pixel offsets.
103199
+ - Palette size and colors declared; transparent background with clean alpha edges.
103200
+ - Animation: uniform frame size per clip, frame count, frame rate, loop flag, and pivot per clip.
103201
+ - Atlas: padding of at least the engine default (Unity default 4 px), atlas size within the platform max, filter mode Point for pixel art and no mip maps.
103202
+ - Export PNG (lossless with alpha); name states size and frame count where relevant.
103203
+
103204
+ **Audio**
103205
+ - Loudness measured against the platform target stated in the request (for example console -24 LUFS, mobile -18 LUFS, true peak at or below -1 dBTP); no clipping. Measure with a tool: \`ffmpeg -i <file> -af ebur128=peak=true -f null -\` for integrated loudness and true peak; \`ffprobe <file>\` for sample rate, bit depth and channels; for glTF, \`gltf-transform inspect <file>\` (quote the command's actual output). Cite the platform source. If the request does not state the target, mark the loudness target \`unverified\` and name the source. Mobile targets vary (-18 vs -16 LUFS).
103206
+ - Sample rate, bit depth, channel count, loop points, and format stated; short noisy SFX use PCM or ADPCM, long audio uses compressed or streamed formats, per the engine's guidance.
103207
+
103208
+ ### Scope boundary
103209
+ You may specify, name, organize, generate, validate, and integrate assets within the paths the brief allows. You must not claim fidelity you did not establish. If you cannot open or measure a file, write \`unverified\`. Do not present alternative art directions beyond what the request needs; recommend one.
103210
+
103211
+ The handoff must contain every required report section, the manifest with one entry per asset, checklist results with evidence labels, assumptions, created and edited paths (or \`None.\`), and open questions for \`{ENTRY_POINT_ROLE}\`.`;
103212
+ });
103213
+
102631
103214
  // ../../services/backend/prompts/cli/roles/planner.ts
102632
103215
  var init_planner2 = __esm(() => {
102633
103216
  init_env();
@@ -102783,6 +103366,7 @@ var init_fromContext = __esm(() => {
102783
103366
  var init_fromContext2 = __esm(() => {
102784
103367
  init_architect3();
102785
103368
  init_builder2();
103369
+ init_game_asset_designer2();
102786
103370
  init_planner2();
102787
103371
  init_researcher2();
102788
103372
  init_reviewer2();
@@ -102892,6 +103476,19 @@ var init_templates = __esm(() => {
102892
103476
  ],
102893
103477
  defaultHandoffTarget: "planner"
102894
103478
  },
103479
+ "game-asset-designer": {
103480
+ role: "game-asset-designer",
103481
+ title: "Game Asset Designer",
103482
+ description: "You are the game asset designer responsible for specifying production-quality game assets (and producing them when the planner allows); you do not implement application code.",
103483
+ responsibilities: [
103484
+ "Recover the target, asset request, style reference, and budgets from the planner brief",
103485
+ "Inspect the repository for existing assets, naming and folder conventions, and engine import configuration",
103486
+ "Write one manifest entry per asset covering format, units, pivot, LODs, textures, atlases, and audio",
103487
+ "Self-check each asset against the quality checklist and label every claim with its evidence label",
103488
+ "Hand one asset delivery report to the planner, separating assumptions and unverified properties from established results"
103489
+ ],
103490
+ defaultHandoffTarget: "planner"
103491
+ },
102895
103492
  verifier: {
102896
103493
  role: "verifier",
102897
103494
  title: "Verifier",
@@ -139302,4 +139899,4 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
139302
139899
  });
139303
139900
  program2.parse();
139304
139901
 
139305
- //# debugId=BCEF986EF291738664756E2164756E21
139902
+ //# debugId=71E0747DD56ECDB664756E2164756E21