chatroom-cli 1.132.0 → 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
@@ -20412,6 +20412,14 @@ var init_agent_role = __esm(() => {
20412
20412
  { teamId: "duo", order: 45 },
20413
20413
  { teamId: "solo", order: 45 }
20414
20414
  ]
20415
+ },
20416
+ reviewer: {
20417
+ tags: ["ephemeral" /* Ephemeral */],
20418
+ teams: [{ teamId: "duo", order: 35 }]
20419
+ },
20420
+ "game-asset-designer": {
20421
+ tags: ["ephemeral" /* Ephemeral */],
20422
+ teams: [{ teamId: "duo", order: 42 }]
20415
20423
  }
20416
20424
  };
20417
20425
  AGENT_ROLE_DEFINITIONS = Object.fromEntries(Object.entries(roleMetadata).map(([role, metadata]) => [role, { role, ...metadata }]));
@@ -35270,6 +35278,25 @@ var init_assigned_task = __esm(() => {
35270
35278
  ACTIVE_TASK_STATUSES = ["pending", "acknowledged", "in_progress"];
35271
35279
  });
35272
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
+
35273
35300
  // src/daemon/infrastructure/local/harness/services/spawn-prompt.ts
35274
35301
  function createSpawnPrompt(raw, opts) {
35275
35302
  if (opts?.nativeBootstrap) {
@@ -36877,6 +36904,26 @@ var init_native_task_injector = __esm(() => {
36877
36904
  };
36878
36905
  });
36879
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
+
36880
36927
  // src/daemon/services/agent-process-service/service/agent-work-manager.ts
36881
36928
  class AgentWorkManager {
36882
36929
  deps;
@@ -36891,6 +36938,8 @@ class AgentWorkManager {
36891
36938
  recoveryGates = new Map;
36892
36939
  respawnStartRetries = new Map;
36893
36940
  respawnRestartSeq = 0;
36941
+ turnFailureAccountingFailures = new Map;
36942
+ turnFailureReleaseTimers = new Map;
36894
36943
  disposed = false;
36895
36944
  constructor(deps) {
36896
36945
  this.deps = deps;
@@ -36923,11 +36972,14 @@ class AgentWorkManager {
36923
36972
  recordDeliveryFailure: deps.taskService.recordDeliveryFailure,
36924
36973
  clearDeliveryFailure: deps.taskService.clearDeliveryFailure,
36925
36974
  recordTurnFailure: deps.taskService.recordTurnFailure,
36975
+ parkTurnFailure: deps.taskService.parkTurnFailure,
36926
36976
  decideUncoveredTurnEnd: deps.taskService.decideUncoveredTurnEnd,
36927
36977
  confirmNudgeSent: deps.taskService.confirmNudgeSent,
36928
36978
  resetTurnTracking: deps.taskService.resetTurnTracking,
36929
36979
  isRedeliveryExhausted: deps.taskService.isRedeliveryExhausted,
36930
36980
  clearRedeliveryTracking: deps.taskService.clearRedeliveryTracking,
36981
+ markTurnFailureReleasePending: deps.taskService.markTurnFailureReleasePending,
36982
+ clearTurnFailureReleasePending: deps.taskService.clearTurnFailureReleasePending,
36931
36983
  forgetStaleTask: deps.taskService.forgetStaleTask,
36932
36984
  loadAssignedTaskForAction: deps.taskService.loadAssignedTaskForAction,
36933
36985
  deliverNativeTask: (task, harnessSessionId, onTaskDelivered, isCurrent) => this.nativeTaskDeliveryQueue.enqueue({
@@ -36960,6 +37012,9 @@ class AgentWorkManager {
36960
37012
  role: event.role
36961
37013
  });
36962
37014
  }
37015
+ if (event.reason === "user.start" || event.reason === "user.restart") {
37016
+ this.cancelTurnFailureReleasesForRole(event.chatroomId, event.role);
37017
+ }
36963
37018
  await this.requestReconcile({
36964
37019
  chatroomId: event.chatroomId,
36965
37020
  role: event.role,
@@ -37017,14 +37072,29 @@ class AgentWorkManager {
37017
37072
  }
37018
37073
  async recoverStoppedRole(args2) {
37019
37074
  const key = recoveryKey(args2.chatroomId, args2.role);
37020
- const gate = this.recoveryGates.get(key);
37075
+ let gate = this.recoveryGates.get(key);
37021
37076
  if (!gate)
37022
37077
  throw new Error(`role recovery gate not prepared for ${args2.role}@${args2.chatroomId}`);
37023
- if (gate.recoveryPromise)
37024
- 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
+ }
37025
37092
  gate.recoveryPromise = (async () => {
37026
37093
  await gate.drainPromise;
37027
37094
  const result3 = await this.deps.taskService.recoverInFlightTasks(args2);
37095
+ if (args2.mode === "explicit") {
37096
+ this.cancelTurnFailureReleasesForRole(args2.chatroomId, args2.role);
37097
+ }
37028
37098
  if (this.recoveryGates.get(key) === gate) {
37029
37099
  if (gate.retryTimer)
37030
37100
  clearTimeout(gate.retryTimer);
@@ -37060,7 +37130,7 @@ class AgentWorkManager {
37060
37130
  const gate = this.recoveryGates.get(recoveryKey(chatroomId, role));
37061
37131
  if (!gate || gate.retryTimer || this.disposed)
37062
37132
  return;
37063
- const delay3 = Math.min(30000, 1000 * 2 ** gate.retryAttempts++);
37133
+ const delay3 = computeBackoffDelay(gate.retryAttempts++ + 1, RECOVERY_RETRY_BACKOFF);
37064
37134
  gate.retryTimer = setTimeout(() => {
37065
37135
  gate.retryTimer = undefined;
37066
37136
  this.recoverStoppedRole({ chatroomId, role, mode: "automatic" }).catch((error51) => {
@@ -37069,26 +37139,115 @@ class AgentWorkManager {
37069
37139
  });
37070
37140
  }, delay3);
37071
37141
  }
37072
- async recoverTaskAfterTurnFailure(event, activeTask, phase) {
37142
+ async recoverTaskAfterTurnFailure(event, activeTask) {
37073
37143
  const { chatroomId, role } = event;
37074
- const { exceeded } = await this.deps.taskService.recordTurnFailure({
37144
+ const { attempts, exceeded } = await this.deps.taskService.recordTurnFailure({
37075
37145
  chatroomId,
37076
37146
  role,
37077
37147
  taskId: activeTask.taskId
37078
37148
  });
37079
- if (exceeded)
37080
- return "exceeded";
37081
- try {
37082
- await this.deps.taskService.releaseTaskAfterTurnFailure({
37149
+ if (exceeded) {
37150
+ this.cancelTurnFailureRelease({
37083
37151
  chatroomId,
37084
37152
  role,
37085
37153
  taskId: activeTask.taskId
37086
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));
37087
37200
  } catch (error51) {
37088
- 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)}`);
37089
- 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);
37090
37250
  }
37091
- return "released";
37092
37251
  }
37093
37252
  async handleAgentTurnEnded(event) {
37094
37253
  const completion = event.completion;
@@ -37100,14 +37259,11 @@ class AgentWorkManager {
37100
37259
  const errorDetail = completion.error;
37101
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"}`);
37102
37261
  if (activeTask2) {
37103
- const recovery = await this.recoverTaskAfterTurnFailure(event, activeTask2, "turn-failed");
37262
+ const recovery = await this.recoverTaskAfterTurnFailure(event, activeTask2);
37104
37263
  if (recovery === "exceeded") {
37105
37264
  this.deps.agentTaskState.clear({ chatroomId: event.chatroomId, role: event.role });
37106
37265
  return { kind: "release-slot" };
37107
37266
  }
37108
- if (recovery === "recovery-failed") {
37109
- return { kind: "hold-slot", reason: "task-recovery-failed" };
37110
- }
37111
37267
  this.deps.agentTaskState.clear({ chatroomId: event.chatroomId, role: event.role });
37112
37268
  }
37113
37269
  const fact = {
@@ -37296,7 +37452,9 @@ class AgentWorkManager {
37296
37452
  return;
37297
37453
  }
37298
37454
  if (outcome.kind === "start-failed") {
37299
- 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}`);
37300
37458
  this.beginRespawnStartRetries({ chatroomId, role, workingDir, startInput });
37301
37459
  }
37302
37460
  }
@@ -37311,9 +37469,13 @@ class AgentWorkManager {
37311
37469
  };
37312
37470
  }
37313
37471
  }
37314
- beginRespawnStartRetries(request3) {
37472
+ beginRespawnStartRetries(input) {
37315
37473
  if (this.disposed)
37316
37474
  return;
37475
+ const request3 = {
37476
+ ...input,
37477
+ cliDriven: isCliDrivenHarness(input.startInput.agentHarness)
37478
+ };
37317
37479
  const key = recoveryKey(request3.chatroomId, request3.role);
37318
37480
  this.cancelRespawnStart(key, "superseded_by_new_restart");
37319
37481
  this.respawnRestartSeq += 1;
@@ -37328,11 +37490,27 @@ class AgentWorkManager {
37328
37490
  armRespawnStartRetry(key, pending) {
37329
37491
  if (this.disposed)
37330
37492
  return;
37331
- 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
+ });
37332
37498
  pending.timer = setTimeout(() => {
37333
37499
  pending.timer = undefined;
37334
37500
  this.retryRespawnStart(key, pending.restartId).catch((error51) => {
37335
- 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);
37336
37514
  });
37337
37515
  }, delay3);
37338
37516
  }
@@ -37340,7 +37518,7 @@ class AgentWorkManager {
37340
37518
  const pending = this.respawnStartRetries.get(key);
37341
37519
  if (!pending || pending.restartId !== restartId || this.disposed)
37342
37520
  return;
37343
- const { chatroomId, role, workingDir, startInput } = pending.request;
37521
+ const { chatroomId, role } = pending.request;
37344
37522
  if (this.deps.agentMgr.isStopRequested(chatroomId, role)) {
37345
37523
  this.cancelRespawnStart(key, "stop_requested");
37346
37524
  return;
@@ -37350,6 +37528,33 @@ class AgentWorkManager {
37350
37528
  this.cancelRespawnStart(key, "agent_already_running");
37351
37529
  return;
37352
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;
37353
37558
  const outcome = await this.deps.runSerializedForAgent({ chatroomId, role, workingDir }, { timeoutMs: AGENT_LIFECYCLE_OPERATION_TIMEOUT_MS }, async (ops, context3) => {
37354
37559
  if (this.respawnStartRetries.get(key)?.restartId !== restartId) {
37355
37560
  return { kind: "skipped", reason: "superseded" };
@@ -37375,12 +37580,13 @@ class AgentWorkManager {
37375
37580
  return;
37376
37581
  }
37377
37582
  current.attempts += 1;
37378
- if (current.attempts >= MAX_RESPAWN_START_ATTEMPTS) {
37583
+ if (current.request.cliDriven && current.attempts >= MAX_RESPAWN_START_ATTEMPTS) {
37379
37584
  this.respawnStartRetries.delete(key);
37380
37585
  console.error(`[NativeDelivery:uncovered-restart-failed] attempts exhausted chatroom=${chatroomId} role=${role} attempts=${current.attempts} error=${outcome.error}`);
37381
37586
  return;
37382
37587
  }
37383
- 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}`);
37384
37590
  this.armRespawnStartRetry(key, current);
37385
37591
  }
37386
37592
  cancelRespawnStart(key, reason) {
@@ -37432,6 +37638,12 @@ class AgentWorkManager {
37432
37638
  }
37433
37639
  dispose() {
37434
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();
37435
37647
  for (const pending of this.respawnStartRetries.values()) {
37436
37648
  if (pending.timer)
37437
37649
  clearTimeout(pending.timer);
@@ -37496,6 +37708,20 @@ class AgentWorkManager {
37496
37708
  if (notification.event.eventType === "task_deleted" /* TaskDeleted */) {
37497
37709
  await this.cancelTaskWork(notification.event.chatroomId, notification.event.role, notification.event.taskId);
37498
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
+ }
37499
37725
  if (notification.event.eventType === "task_deleted" /* TaskDeleted */) {
37500
37726
  return { handledEventIds: [notification.event.eventId] };
37501
37727
  }
@@ -37541,6 +37767,7 @@ class AgentWorkManager {
37541
37767
  }
37542
37768
  }
37543
37769
  async cancelTaskWork(chatroomId, role, taskId) {
37770
+ this.cancelTurnFailureRelease({ chatroomId, role, taskId });
37544
37771
  const activeTask = this.deps.agentTaskState.get({ chatroomId, role });
37545
37772
  if (activeTask?.taskId !== taskId)
37546
37773
  return;
@@ -37649,7 +37876,7 @@ class AgentWorkManager {
37649
37876
  this.deps.agentTaskState.start(args2);
37650
37877
  }
37651
37878
  }
37652
- 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"}`;
37653
37880
  var init_agent_work_manager = __esm(() => {
37654
37881
  init_reliability();
37655
37882
  init_agent();
@@ -37659,11 +37886,15 @@ var init_agent_work_manager = __esm(() => {
37659
37886
  init_agent_slot();
37660
37887
  init_assigned_task();
37661
37888
  init_check_agent_slot();
37889
+ init_compute_backoff_delay();
37662
37890
  init_native_task_injector_logic();
37663
37891
  init_convex_native_task_delivery_gateway();
37664
37892
  init_daemon_audit_port();
37665
37893
  init_task_delivery_processor();
37666
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 };
37667
37898
  });
37668
37899
 
37669
37900
  // src/daemon/services/agent-process-service/service/index.ts
@@ -39632,11 +39863,6 @@ class AgentProcessManager {
39632
39863
  }
39633
39864
  };
39634
39865
  const dispositions = await Promise.all([...this.agentTurnEndedHandlers].map((handler) => handler(event)));
39635
- const held = dispositions.find((disposition) => disposition?.kind === "hold-slot");
39636
- if (held?.kind === "hold-slot") {
39637
- console.warn(`[AgentProcessManager] Native turn slot held after ${completionStatus}: role=${opts.role} pid=${opts.pid} reason=${held.reason}`);
39638
- return;
39639
- }
39640
39866
  if (completionStatus !== "completed" && !dispositions.some((disposition) => disposition?.kind === "release-slot")) {
39641
39867
  console.warn(`[AgentProcessManager] Native turn slot held: no task-service disposition returned role=${opts.role} pid=${opts.pid} status=${completionStatus}`);
39642
39868
  return;
@@ -40795,14 +41021,80 @@ function createTaskService(deps) {
40795
41021
  let bootstrapRecoveryAttempts = 0;
40796
41022
  let bootstrapRecoveryTimer;
40797
41023
  let bootstrapRecoveryPromise;
40798
- const TURN_FAILURE_RETRY_CAP = 3;
41024
+ const TURN_FAILURE_RETRY_CAP = 8;
40799
41025
  const turnFailureCounts = new Map;
40800
41026
  const uncoveredLadder = new Map;
40801
41027
  const redeliveryExhaustedKeys = new Set;
41028
+ const rearmCycleCounts = new Map;
41029
+ const rearmTimers = new Map;
41030
+ const pendingTurnFailureReleases = new Set;
40802
41031
  const redeliveryKey = (chatroomId, role, taskId) => `${chatroomId}:${role.toLowerCase()}:${taskId}`;
40803
- const parkTask = async (args2, attempts) => {
40804
- const key = redeliveryKey(args2.chatroomId, args2.role, args2.taskId);
40805
- 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) => {
40806
41098
  try {
40807
41099
  await gateway.recordDeliveryFailure({
40808
41100
  sessionId: deps.sessionId,
@@ -40813,19 +41105,31 @@ function createTaskService(deps) {
40813
41105
  } catch (error51) {
40814
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)}`);
40815
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);
40816
41113
  const attemptsSuffix = attempts === undefined ? "" : ` attempts=${attempts}`;
40817
- 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
+ }
40818
41120
  };
40819
41121
  const recordTurnFailure = async (args2) => {
40820
41122
  const key = redeliveryKey(args2.chatroomId, args2.role, args2.taskId);
40821
- if (redeliveryExhaustedKeys.has(key))
40822
- return { exceeded: true };
40823
- 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;
40824
41128
  turnFailureCounts.set(key, attempts);
40825
41129
  if (attempts < TURN_FAILURE_RETRY_CAP)
40826
- return { exceeded: false };
40827
- await parkTask(args2, attempts);
40828
- return { exceeded: true };
41130
+ return { attempts, exceeded: false };
41131
+ await parkTask(args2, "provider_failure", attempts);
41132
+ return { attempts, exceeded: true };
40829
41133
  };
40830
41134
  const decideUncoveredTurnEnd2 = async (args2) => {
40831
41135
  const key = redeliveryKey(args2.chatroomId, args2.role, args2.taskId);
@@ -40839,7 +41143,7 @@ function createTaskService(deps) {
40839
41143
  if (decision.action === "restart-session") {
40840
41144
  uncoveredLadder.set(key, withSessionRestart(entry));
40841
41145
  } else if (decision.action === "park") {
40842
- await parkTask(args2);
41146
+ await parkTask(args2, "uncovered_turn");
40843
41147
  }
40844
41148
  return decision;
40845
41149
  };
@@ -40858,6 +41162,7 @@ function createTaskService(deps) {
40858
41162
  turnFailureCounts.delete(key);
40859
41163
  uncoveredLadder.delete(key);
40860
41164
  redeliveryExhaustedKeys.delete(key);
41165
+ resetRearmBudget(key);
40861
41166
  };
40862
41167
  const forgetStaleTask = ({
40863
41168
  chatroomId,
@@ -40898,11 +41203,19 @@ function createTaskService(deps) {
40898
41203
  if (key.startsWith(prefix))
40899
41204
  redeliveryExhaustedKeys.delete(key);
40900
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
+ }
40901
41214
  };
40902
41215
  const scheduleBootstrapRecovery = () => {
40903
41216
  if (inboxStopped || bootstrapRecoveryTimer || !bootstrapRecoveryPending)
40904
41217
  return;
40905
- const delay3 = Math.min(30000, 1000 * 2 ** bootstrapRecoveryAttempts++);
41218
+ const delay3 = computeBackoffDelay(++bootstrapRecoveryAttempts, BOOTSTRAP_RECOVERY_BACKOFF);
40906
41219
  bootstrapRecoveryTimer = setTimeout(() => {
40907
41220
  bootstrapRecoveryTimer = undefined;
40908
41221
  if (!inboxStopped)
@@ -41119,6 +41432,11 @@ function createTaskService(deps) {
41119
41432
  },
41120
41433
  stopTaskInbox: () => {
41121
41434
  inboxStopped = true;
41435
+ for (const timer of rearmTimers.values())
41436
+ clearTimeout(timer);
41437
+ rearmTimers.clear();
41438
+ rearmCycleCounts.clear();
41439
+ pendingTurnFailureReleases.clear();
41122
41440
  if (bootstrapRecoveryTimer)
41123
41441
  clearTimeout(bootstrapRecoveryTimer);
41124
41442
  bootstrapRecoveryTimer = undefined;
@@ -41209,7 +41527,8 @@ function createTaskService(deps) {
41209
41527
  }
41210
41528
  if (mode === "automatic") {
41211
41529
  const tracking = { chatroomId: task.chatroomId, role: taskRole, taskId: task.taskId };
41212
- if (isRedeliveryExhausted(tracking)) {
41530
+ const key = redeliveryKey(task.chatroomId, taskRole, task.taskId);
41531
+ if (isRedeliveryExhausted(tracking) || rearmTimers.has(key) || pendingTurnFailureReleases.has(key)) {
41213
41532
  skipped += 1;
41214
41533
  continue;
41215
41534
  }
@@ -41295,6 +41614,7 @@ function createTaskService(deps) {
41295
41614
  ...expectedReason ? { expectedReason } : {}
41296
41615
  }),
41297
41616
  recordTurnFailure,
41617
+ parkTurnFailure: (args2) => parkTask(args2, "provider_failure"),
41298
41618
  decideUncoveredTurnEnd: decideUncoveredTurnEnd2,
41299
41619
  confirmNudgeSent,
41300
41620
  resetTurnTracking,
@@ -41302,18 +41622,29 @@ function createTaskService(deps) {
41302
41622
  clearRedeliveryTracking: ({ chatroomId, role }) => {
41303
41623
  clearRoleRedeliveryTracking(chatroomId, role);
41304
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
+ },
41305
41631
  forgetStaleTask
41306
41632
  };
41307
41633
  return service4;
41308
41634
  }
41635
+ var BOOTSTRAP_RECOVERY_BACKOFF, REARM_DELAYS_MS, MAX_REARM_CYCLES;
41309
41636
  var init_task_service = __esm(() => {
41310
41637
  init_chatroom_workspace_task_inbox();
41311
41638
  init_pending_task_reconciliation_watcher();
41312
41639
  init_task_inbox_acknowledgement_retry_watcher();
41313
41640
  init_task_inbox_delivery_retry_watcher();
41314
41641
  init_api3();
41642
+ init_compute_backoff_delay();
41315
41643
  init_uncovered_turn_ladder();
41316
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;
41317
41648
  });
41318
41649
 
41319
41650
  // src/daemon/services/task-service/infrastructure/repository/task-handoff-repository.ts
@@ -100027,7 +100358,7 @@ var init_team_presets = __esm(() => {
100027
100358
  duo: {
100028
100359
  structureId: TEAM_STRUCTURE_IDS.duo,
100029
100360
  name: "Duo",
100030
- description: "A planner and builder working as a pair, planner as coordinator, with optional ephemeral architect, researcher, triage, 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",
100031
100362
  roles: getBuiltinRolesForTeam("duo"),
100032
100363
  entryPoint: "planner"
100033
100364
  },
@@ -100806,6 +101137,166 @@ var init_builder = __esm(() => {
100806
101137
  };
100807
101138
  });
100808
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
+
100809
101300
  // ../../services/backend/prompts/teams/duo/handoff-templates/planner-architect-code.ts
100810
101301
  function getPlannerArchitectCodeTemplate(query) {
100811
101302
  return getEntryPointToArchitectHandoffTemplate(query);
@@ -100907,6 +101398,19 @@ var init_planner_builder_chat = __esm(() => {
100907
101398
  init_planner_builder_code();
100908
101399
  });
100909
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
+
100910
101414
  // ../../services/backend/prompts/teams/researcher-handoff-templates.ts
100911
101415
  function getEntryPointToResearcherHandoffTemplate(query) {
100912
101416
  const entryPoint = query.fromRole;
@@ -100992,6 +101496,75 @@ var init_planner_researcher_chat = __esm(() => {
100992
101496
  init_planner_researcher_code();
100993
101497
  });
100994
101498
 
101499
+ // ../../services/backend/prompts/teams/reviewer-handoff-templates.ts
101500
+ function getEntryPointToReviewerHandoffTemplate(query) {
101501
+ const entryPoint = query.fromRole;
101502
+ return `Pass the review target and the review focus. State the exact commit range, branch, pull request, or file list to inspect, the focus areas (for example security, correctness, or test quality), and any known context or constraints. Ask the reviewer for a report only: do not edit files, run builds or tests, or implement fixes.
101503
+
101504
+ ### When to review
101505
+
101506
+ Review before delivery for code-producing work:
101507
+ - a builder slice has just completed and is about to be delivered;
101508
+ - the user asks for a code review;
101509
+ - a change is large, risky, or touches security-sensitive or shared code.
101510
+
101511
+ Skip review for a trivial edit that has already been verified.
101512
+
101513
+ After completing the review, the reviewer must return its issue report to \`${entryPoint}\` with the normal handoff command.
101514
+
101515
+ \`\`\`bash
101516
+ ${handoffCommand({
101517
+ chatroomId: "<chatroom-id>",
101518
+ role: entryPoint,
101519
+ nextRole: "reviewer",
101520
+ cliEnvPrefix: ""
101521
+ })}
101522
+ \`\`\``;
101523
+ }
101524
+ function getReviewerToEntryPointHandoffTemplate(query) {
101525
+ const entryPoint = query.toRole;
101526
+ return `Instructions (not part of the message): Return a complete review report for the target the planner named. The reviewer is advisory: do not edit files, implement fixes, run builds or tests, or propose architecture rewrites beyond each issue's suggested fix. Report only what you observed and mark anything unverified as such. Label every issue with a severity: \`blocker\`, \`major\`, \`minor\`, or \`nit\`. Group the issues by severity with blockers first, and cite the repository-relative file and line or line range for each. Use the exact sections below, in this order.
101527
+
101528
+ ## Summary
101529
+ <one to three sentences: overall state of the target and the count of blocker/major issues>
101530
+
101531
+ ## Review scope
101532
+ <commits, paths, and PR/branch inspected; commands run, if any>
101533
+
101534
+ ## Issues
101535
+ <numbered list grouped by severity (blocker, major, minor, nit); each item: \`file:line\` — summary — evidence — suggested fix; write exactly \`None observed.\` if none>
101536
+
101537
+ ## Not reviewed
101538
+ <anything out of scope or unreadable; write exactly \`None.\` if none>
101539
+
101540
+ Instructions after the body (not part of the message): run the normal handoff command to \`${entryPoint}\` with the complete body above, then stop.
101541
+
101542
+ \`\`\`bash
101543
+ ${handoffCommand({
101544
+ chatroomId: "<chatroom-id>",
101545
+ role: "reviewer",
101546
+ nextRole: entryPoint,
101547
+ cliEnvPrefix: ""
101548
+ })}
101549
+ \`\`\``;
101550
+ }
101551
+ var init_reviewer_handoff_templates = __esm(() => {
101552
+ init_command2();
101553
+ });
101554
+
101555
+ // ../../services/backend/prompts/teams/duo/handoff-templates/planner-reviewer-code.ts
101556
+ function getPlannerReviewerCodeTemplate(query) {
101557
+ return getEntryPointToReviewerHandoffTemplate(query);
101558
+ }
101559
+ var init_planner_reviewer_code = __esm(() => {
101560
+ init_reviewer_handoff_templates();
101561
+ });
101562
+
101563
+ // ../../services/backend/prompts/teams/duo/handoff-templates/planner-reviewer-chat.ts
101564
+ var init_planner_reviewer_chat = __esm(() => {
101565
+ init_planner_reviewer_code();
101566
+ });
101567
+
100995
101568
  // ../../services/backend/prompts/teams/triage-handoff-templates.ts
100996
101569
  function getEntryPointToTriageHandoffTemplate(query) {
100997
101570
  const entryPoint = query.fromRole;
@@ -101489,8 +102062,12 @@ var init_planner = __esm(() => {
101489
102062
  init_planner_architect_code();
101490
102063
  init_planner_builder_chat();
101491
102064
  init_planner_builder_code();
102065
+ init_planner_game_asset_designer_chat();
102066
+ init_planner_game_asset_designer_code();
101492
102067
  init_planner_researcher_chat();
101493
102068
  init_planner_researcher_code();
102069
+ init_planner_reviewer_chat();
102070
+ init_planner_reviewer_code();
101494
102071
  init_planner_triage_chat();
101495
102072
  init_planner_triage_code();
101496
102073
  init_planner_uiux_engineer_chat();
@@ -101509,9 +102086,21 @@ var init_planner = __esm(() => {
101509
102086
  "uiux-engineer",
101510
102087
  "triage",
101511
102088
  "researcher",
102089
+ "reviewer",
102090
+ "game-asset-designer",
102091
+ "verifier"
102092
+ ],
102093
+ returnsTo: [
102094
+ "user",
102095
+ "builder",
102096
+ "architect",
102097
+ "uiux-engineer",
102098
+ "triage",
102099
+ "researcher",
102100
+ "reviewer",
102101
+ "game-asset-designer",
101512
102102
  "verifier"
101513
102103
  ],
101514
- returnsTo: ["user", "builder", "architect", "uiux-engineer", "triage", "researcher", "verifier"],
101515
102104
  outboundTemplates: {
101516
102105
  builder: handoffTemplateByMode({
101517
102106
  chat: getPlannerBuilderCodeTemplate,
@@ -101533,6 +102122,14 @@ var init_planner = __esm(() => {
101533
102122
  chat: getPlannerResearcherCodeTemplate,
101534
102123
  code: getPlannerResearcherCodeTemplate
101535
102124
  }),
102125
+ reviewer: handoffTemplateByMode({
102126
+ chat: getPlannerReviewerCodeTemplate,
102127
+ code: getPlannerReviewerCodeTemplate
102128
+ }),
102129
+ "game-asset-designer": handoffTemplateByMode({
102130
+ chat: getPlannerGameAssetDesignerCodeTemplate,
102131
+ code: getPlannerGameAssetDesignerCodeTemplate
102132
+ }),
101536
102133
  verifier: handoffTemplateByMode({
101537
102134
  chat: getPlannerVerifierCodeTemplate,
101538
102135
  code: getPlannerVerifierCodeTemplate
@@ -101577,6 +102174,38 @@ var init_researcher = __esm(() => {
101577
102174
  };
101578
102175
  });
101579
102176
 
102177
+ // ../../services/backend/prompts/teams/duo/handoff-templates/reviewer-planner-code.ts
102178
+ function getReviewerPlannerCodeTemplate(query) {
102179
+ return getReviewerToEntryPointHandoffTemplate(query);
102180
+ }
102181
+ var init_reviewer_planner_code = __esm(() => {
102182
+ init_reviewer_handoff_templates();
102183
+ });
102184
+
102185
+ // ../../services/backend/prompts/teams/duo/handoff-templates/reviewer-planner-chat.ts
102186
+ var init_reviewer_planner_chat = __esm(() => {
102187
+ init_reviewer_planner_code();
102188
+ });
102189
+
102190
+ // ../../services/backend/prompts/teams/duo/handoff-templates/reviewer.ts
102191
+ var duoReviewerHandoffContract;
102192
+ var init_reviewer = __esm(() => {
102193
+ init_reviewer_planner_chat();
102194
+ init_reviewer_planner_code();
102195
+ init_contracts();
102196
+ duoReviewerHandoffContract = {
102197
+ role: "reviewer",
102198
+ receivesFrom: ["planner"],
102199
+ returnsTo: ["planner"],
102200
+ outboundTemplates: {
102201
+ planner: handoffTemplateByMode({
102202
+ chat: getReviewerPlannerCodeTemplate,
102203
+ code: getReviewerPlannerCodeTemplate
102204
+ })
102205
+ }
102206
+ };
102207
+ });
102208
+
101580
102209
  // ../../services/backend/prompts/teams/duo/handoff-templates/triage-planner-code.ts
101581
102210
  function getTriagePlannerCodeTemplate(query) {
101582
102211
  return getTriageToEntryPointHandoffTemplate(query);
@@ -101681,8 +102310,10 @@ var DUO_ROLE_HANDOFF_CATALOG, DUO_ROLE_HANDOFF_CONTRACTS, validatedDuoRoleHandof
101681
102310
  var init_handoff_templates = __esm(() => {
101682
102311
  init_architect();
101683
102312
  init_builder();
102313
+ init_game_asset_designer();
101684
102314
  init_planner();
101685
102315
  init_researcher();
102316
+ init_reviewer();
101686
102317
  init_triage();
101687
102318
  init_uiux_engineer();
101688
102319
  init_verifier();
@@ -101692,7 +102323,9 @@ var init_handoff_templates = __esm(() => {
101692
102323
  builder: duoBuilderHandoffContract,
101693
102324
  architect: duoArchitectHandoffContract,
101694
102325
  researcher: duoResearcherHandoffContract,
102326
+ reviewer: duoReviewerHandoffContract,
101695
102327
  "uiux-engineer": duoUiuxEngineerHandoffContract,
102328
+ "game-asset-designer": duoGameAssetDesignerHandoffContract,
101696
102329
  triage: duoTriageHandoffContract,
101697
102330
  verifier: duoVerifierHandoffContract
101698
102331
  };
@@ -102505,6 +103138,79 @@ var init_builder2 = __esm(() => {
102505
103138
  init_sections();
102506
103139
  });
102507
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
+
102508
103214
  // ../../services/backend/prompts/cli/roles/planner.ts
102509
103215
  var init_planner2 = __esm(() => {
102510
103216
  init_env();
@@ -102536,6 +103242,30 @@ chatroom handoff --chatroom-id="<chatroom-id>" --role="researcher" --next-role="
102536
103242
  \`\`\``;
102537
103243
  });
102538
103244
 
103245
+ // ../../services/backend/prompts/cli/roles/reviewer.ts
103246
+ var REVIEWER_GUIDANCE;
103247
+ var init_reviewer2 = __esm(() => {
103248
+ init_sections();
103249
+ REVIEWER_GUIDANCE = `## Reviewer Operating Model
103250
+
103251
+ You are the reviewer responsible for inspecting the code artifacts the planner names and reporting the issues you observe. You are advisory only: do not edit files, implement fixes, or act as the builder or planner.
103252
+
103253
+ ${"{SESSION_CONTINUITY}"}
103254
+
103255
+ ${getInterRoleHandoffRule()}
103256
+
103257
+ 1. Recover the review target and focus from the injected task and the \`<handoff-templates>\` brief. If the target is not explicit, ask the planner through the handoff; do not guess a range.
103258
+ 2. Inspect the target: the diff or commits, the changed files, and the surrounding code they depend on. Read the actual files; do not review from summaries.
103259
+ 3. Look for correctness bugs, security issues, missing or weak tests, unclear ownership or error handling, dead code, duplication, and type drift. Stay within the stated focus when one is given.
103260
+ 4. Label every issue with a severity (\`blocker\`, \`major\`, \`minor\`, \`nit\`) and cite the file and line. Only report what you observed; mark anything unverified as such.
103261
+ 5. Follow the injected \`<handoff-templates>\` review report exactly, in its order.
103262
+ 6. Hand the report to \`{ENTRY_POINT_ROLE}\` with the normal command, then stop:
103263
+
103264
+ \`\`\`bash
103265
+ chatroom handoff --chatroom-id="<chatroom-id>" --role="reviewer" --next-role="{ENTRY_POINT_ROLE}"
103266
+ \`\`\``;
103267
+ });
103268
+
102539
103269
  // ../../services/backend/prompts/cli/roles/triage.ts
102540
103270
  var TRIAGE_GUIDANCE;
102541
103271
  var init_triage3 = __esm(() => {
@@ -102636,8 +103366,10 @@ var init_fromContext = __esm(() => {
102636
103366
  var init_fromContext2 = __esm(() => {
102637
103367
  init_architect3();
102638
103368
  init_builder2();
103369
+ init_game_asset_designer2();
102639
103370
  init_planner2();
102640
103371
  init_researcher2();
103372
+ init_reviewer2();
102641
103373
  init_triage3();
102642
103374
  init_uiux_engineer3();
102643
103375
  init_verifier3();
@@ -102731,6 +103463,32 @@ var init_templates = __esm(() => {
102731
103463
  ],
102732
103464
  defaultHandoffTarget: "planner"
102733
103465
  },
103466
+ reviewer: {
103467
+ role: "reviewer",
103468
+ title: "Reviewer",
103469
+ description: "You are the reviewer responsible for inspecting the code artifacts the planner names and reporting the issues you observe; you are advisory only and do not implement fixes.",
103470
+ responsibilities: [
103471
+ "Recover the review target and focus from the planner brief",
103472
+ "Inspect the diff or commits and the surrounding code they depend on",
103473
+ "Look for correctness, security, test quality, ownership, dead code, duplication, and type drift",
103474
+ "Label each issue with a severity and cite the file and line",
103475
+ "Hand one review report to the planner, separating reviewed scope from what was not reviewed"
103476
+ ],
103477
+ defaultHandoffTarget: "planner"
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
+ },
102734
103492
  verifier: {
102735
103493
  role: "verifier",
102736
103494
  title: "Verifier",
@@ -139141,4 +139899,4 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
139141
139899
  });
139142
139900
  program2.parse();
139143
139901
 
139144
- //# debugId=430CD3ADC73C742264756E2164756E21
139902
+ //# debugId=71E0747DD56ECDB664756E2164756E21