chatroom-cli 1.138.2 → 1.139.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
@@ -10016,6 +10016,33 @@ var init_rigging = __esm(() => {
10016
10016
  init_model_registry();
10017
10017
  });
10018
10018
 
10019
+ // src/commands/image-shared/writable-path.ts
10020
+ function dryRunOverwriteHint(dryRun) {
10021
+ return dryRun ? DRY_RUN_OVERWRITE_HINT : undefined;
10022
+ }
10023
+ function directoryRefusal(target) {
10024
+ return `${target.label} is a directory: ${target.path}. Pass a file path.`;
10025
+ }
10026
+ function fileRefusal(target) {
10027
+ return `${target.label} already exists: ${target.path}. ${target.hint ?? OVERWRITE_HINT}`;
10028
+ }
10029
+ function blockedMessage(target, status, force) {
10030
+ if (status === "directory")
10031
+ return directoryRefusal(target);
10032
+ if (status === "file" && !force)
10033
+ return fileRefusal(target);
10034
+ return null;
10035
+ }
10036
+ async function findUnwritablePathMessage(deps, targets, force) {
10037
+ for (const target of targets) {
10038
+ const message = blockedMessage(target, await deps.pathStatus(target.path), force);
10039
+ if (message !== null)
10040
+ return message;
10041
+ }
10042
+ return null;
10043
+ }
10044
+ var OVERWRITE_HINT = "Pass --force to overwrite.", DRY_RUN_OVERWRITE_HINT = "A real run would also refuse this. Add --force to print the plan; a dry run writes nothing; do not carry --force into the real run unless overwriting is intended.";
10045
+
10019
10046
  // src/commands/generate-3d/generate-3d.ts
10020
10047
  var exports_generate_3d = {};
10021
10048
  __export(exports_generate_3d, {
@@ -10104,18 +10131,16 @@ async function validateInputImage(deps, inputImage) {
10104
10131
  }
10105
10132
  return { kind: "file", path: localPath, contentType };
10106
10133
  }
10107
- async function requireWritablePath(deps, path, label, force, hint = "Pass --force to overwrite.") {
10108
- const status = await deps.pathStatus(path);
10109
- if (status === "directory") {
10110
- throw new Generate3dError(`${label} is a directory: ${path}. Pass a file path.`);
10111
- }
10112
- if (status === "file" && !force) {
10113
- throw new Generate3dError(`${label} already exists: ${path}. ${hint}`);
10114
- }
10134
+ async function requireWritablePaths(deps, targets, force) {
10135
+ const message = await findUnwritablePathMessage(deps, targets, force);
10136
+ if (message !== null)
10137
+ throw new Generate3dError(message);
10115
10138
  }
10116
- async function requireWritableOutput(deps, outPath, manifestPath, force) {
10117
- await requireWritablePath(deps, outPath, "--out", force);
10118
- await requireWritablePath(deps, manifestPath, "Manifest", force);
10139
+ async function requireWritableOutput(deps, outPath, manifestPath, force, dryRun) {
10140
+ await requireWritablePaths(deps, [
10141
+ { path: outPath, label: "--out", hint: dryRunOverwriteHint(dryRun) },
10142
+ { path: manifestPath, label: "Manifest", hint: dryRunOverwriteHint(dryRun) }
10143
+ ], force);
10119
10144
  }
10120
10145
  function planInputImageUrl(target) {
10121
10146
  if (target === undefined)
@@ -10186,13 +10211,17 @@ function buildRiggingPlan(outPath, rig, animationActionIds) {
10186
10211
  extraCostLines: riggingCostLines(animationActionIds.length)
10187
10212
  };
10188
10213
  }
10189
- async function requireWritableRiggingPaths(deps, rigging, force) {
10214
+ async function requireWritableRiggingPaths(deps, rigging, force, dryRun) {
10190
10215
  if (rigging.riggedOutPath === null)
10191
10216
  return;
10192
- await requireWritablePath(deps, rigging.riggedOutPath, "Rigged GLB", force);
10193
- for (const animation of rigging.animationOutPaths) {
10194
- await requireWritablePath(deps, animation.path, `Animation clip ${animation.actionId} GLB`, force);
10195
- }
10217
+ await requireWritablePaths(deps, [
10218
+ { path: rigging.riggedOutPath, label: "Rigged GLB", hint: dryRunOverwriteHint(dryRun) },
10219
+ ...rigging.animationOutPaths.map((animation) => ({
10220
+ path: animation.path,
10221
+ label: `Animation clip ${animation.actionId} GLB`,
10222
+ hint: dryRunOverwriteHint(dryRun)
10223
+ }))
10224
+ ], force);
10196
10225
  }
10197
10226
  function packageFilePaths(packageDir, base) {
10198
10227
  return { zipPath: join2(packageDir, `${base}.zip`), htmlPath: join2(packageDir, `${base}.html`) };
@@ -10445,10 +10474,10 @@ function planReferenceStep(args) {
10445
10474
  costLines: [{ label: REFERENCE_IMAGE_COST_LABEL, usd: REFERENCE_IMAGE_COST_USD }]
10446
10475
  };
10447
10476
  }
10448
- async function requireWritableReference(deps, reference, force) {
10477
+ async function requireWritableReference(deps, reference, force, dryRun) {
10449
10478
  if (reference?.source !== "generated" || reference.localPath === null)
10450
10479
  return;
10451
- await requireWritablePath(deps, reference.localPath, "Reference image", force);
10480
+ await requireWritablePaths(deps, [{ path: reference.localPath, label: "Reference image", hint: dryRunOverwriteHint(dryRun) }], force);
10452
10481
  }
10453
10482
  function requireRigForClips(rig, animationActionIds) {
10454
10483
  if (animationActionIds.length > 0 && !rig) {
@@ -10456,13 +10485,16 @@ function requireRigForClips(rig, animationActionIds) {
10456
10485
  }
10457
10486
  }
10458
10487
  async function requireWritablePlanPaths(args) {
10459
- await requireWritableOutput(args.deps, args.outPath, args.manifestPath, args.force);
10460
- await requireWritableRiggingPaths(args.deps, args.rigging, args.force);
10461
- await requireWritablePackage(args.deps, args.naming, args.force);
10488
+ await requireWritableOutput(args.deps, args.outPath, args.manifestPath, args.force, args.dryRun);
10489
+ await requireWritableRiggingPaths(args.deps, args.rigging, args.force, args.dryRun);
10490
+ await requireWritablePackage(args.deps, args.naming, args.force, args.dryRun);
10462
10491
  }
10463
- async function requireWritablePackage(deps, paths, force) {
10464
- await requireWritablePath(deps, paths.zipPath, "Package", force, PACKAGE_COLLISION_HINT);
10465
- await requireWritablePath(deps, paths.htmlPath, "Preview", force, PACKAGE_COLLISION_HINT);
10492
+ async function requireWritablePackage(deps, paths, force, dryRun) {
10493
+ const hint = dryRun ? DRY_RUN_PACKAGE_HINT : PACKAGE_COLLISION_HINT;
10494
+ await requireWritablePaths(deps, [
10495
+ { path: paths.zipPath, label: "Package", hint },
10496
+ { path: paths.htmlPath, label: "Preview", hint }
10497
+ ], force);
10466
10498
  }
10467
10499
  function resolvePlanTarget(opts) {
10468
10500
  const model = requireModel(opts.model);
@@ -10496,13 +10528,14 @@ async function planGenerate3d(opts, deps) {
10496
10528
  await requireWritablePlanPaths({
10497
10529
  deps,
10498
10530
  force: opts.force === true,
10531
+ dryRun: opts.dryRun === true,
10499
10532
  outPath,
10500
10533
  manifestPath,
10501
10534
  rigging,
10502
10535
  naming
10503
10536
  });
10504
10537
  const referenceStep = planReferenceStep({ model, opts, outPath, inputTarget });
10505
- await requireWritableReference(deps, referenceStep.referenceImage, opts.force === true);
10538
+ await requireWritableReference(deps, referenceStep.referenceImage, opts.force === true, opts.dryRun === true);
10506
10539
  const input = model.buildInput({
10507
10540
  prompt: opts.prompt,
10508
10541
  imageUrl: referenceStep.imageUrl,
@@ -11448,7 +11481,7 @@ async function generate3d(opts, deps) {
11448
11481
  costLines: formatGenerate3dCost(plan.costLines, plan.model.id, plan.generationCostKnown)
11449
11482
  };
11450
11483
  }
11451
- var GLB_CONTENT_TYPE = "model/gltf-binary", GLB_EXTENSION = ".glb", PLANNED_MODEL_URL = "<generated mesh GLB URL>", RIGGING_HUMANOID_WARNING = "Rigging (Meshy) expects a humanoid mesh; non-humanoid meshes fail on fal.", WEB_VARIANT_TRIANGLE_TARGET = 30000, WEB_VARIANT_MAX_TEXTURE_PX = 1024, WEB_VARIANT_TEXTURE_QUALITY = 80, WEB_VARIANT_DATA_TEXTURE_QUALITY = 95, PACKAGE_MANIFEST_NAME = "manifest.json", PACKAGE_PRICING_NOTE = "list-price estimate, not a billed amount", HTTP_URL_PATTERN, INPUT_IMAGE_CONTENT_TYPES, INPUT_REQUIREMENTS, PACKAGE_COLLISION_HINT = "Another run used this name in the same second; wait a second, use a different --package-dir, or pass --force.", FLAG_CAPABILITY, INPUT_FLAG_NAMES, INPUT_PLACEHOLDERS, INPUT_CAPABILITY, RESERVED_INPUT_FLAGS, GENERATED_REFERENCE_URL = "<generated reference image URL>", WEB_VARIANT_OPTIONS, SILENT_LOGGER2;
11484
+ var GLB_CONTENT_TYPE = "model/gltf-binary", GLB_EXTENSION = ".glb", PLANNED_MODEL_URL = "<generated mesh GLB URL>", RIGGING_HUMANOID_WARNING = "Rigging (Meshy) expects a humanoid mesh; non-humanoid meshes fail on fal.", WEB_VARIANT_TRIANGLE_TARGET = 30000, WEB_VARIANT_MAX_TEXTURE_PX = 1024, WEB_VARIANT_TEXTURE_QUALITY = 80, WEB_VARIANT_DATA_TEXTURE_QUALITY = 95, PACKAGE_MANIFEST_NAME = "manifest.json", PACKAGE_PRICING_NOTE = "list-price estimate, not a billed amount", HTTP_URL_PATTERN, INPUT_IMAGE_CONTENT_TYPES, INPUT_REQUIREMENTS, PACKAGE_COLLISION_CAUSE = "Another run used this name in the same second; wait a second, use a different --package-dir", PACKAGE_COLLISION_HINT, DRY_RUN_PACKAGE_HINT, FLAG_CAPABILITY, INPUT_FLAG_NAMES, INPUT_PLACEHOLDERS, INPUT_CAPABILITY, RESERVED_INPUT_FLAGS, GENERATED_REFERENCE_URL = "<generated reference image URL>", WEB_VARIANT_OPTIONS, SILENT_LOGGER2;
11452
11485
  var init_generate_3d = __esm(() => {
11453
11486
  init_meshoptimizer();
11454
11487
  init_asset_analysis();
@@ -11469,6 +11502,8 @@ var init_generate_3d = __esm(() => {
11469
11502
  text: { flag: "--prompt", value: (opts) => opts.prompt },
11470
11503
  image: { flag: "--input-image", value: (opts) => opts.inputImage }
11471
11504
  };
11505
+ PACKAGE_COLLISION_HINT = `${PACKAGE_COLLISION_CAUSE}, or pass --force.`;
11506
+ DRY_RUN_PACKAGE_HINT = `${PACKAGE_COLLISION_CAUSE}. ${DRY_RUN_OVERWRITE_HINT}`;
11472
11507
  FLAG_CAPABILITY = {
11473
11508
  "--prompt": "prompt",
11474
11509
  "--input-image": "image",
@@ -11618,16 +11653,16 @@ function packageBase(zipName, zipPath) {
11618
11653
  }
11619
11654
  return zipName.slice(0, -".zip".length);
11620
11655
  }
11621
- async function requireWritablePreview(htmlPath, force) {
11622
- const stats = await stat2(htmlPath).catch(() => null);
11656
+ async function previewPathStatus(path) {
11657
+ const stats = await stat2(path).catch(() => null);
11623
11658
  if (stats === null)
11624
- return;
11625
- if (stats.isDirectory()) {
11626
- throw new Error(`Preview is a directory: ${htmlPath}. Pass a file path.`);
11627
- }
11628
- if (!force) {
11629
- throw new Error(`Preview already exists: ${htmlPath}. Pass --force to overwrite.`);
11630
- }
11659
+ return "missing";
11660
+ return stats.isDirectory() ? "directory" : "file";
11661
+ }
11662
+ async function requireWritablePreview(htmlPath, force) {
11663
+ const message = await findUnwritablePathMessage({ pathStatus: previewPathStatus }, [{ path: htmlPath, label: "Preview" }], force);
11664
+ if (message !== null)
11665
+ throw new Error(message);
11631
11666
  }
11632
11667
  async function writePreview(htmlPath, html, force) {
11633
11668
  await requireWritablePreview(htmlPath, force);
@@ -11852,16 +11887,17 @@ function readPngDimensions(bytes) {
11852
11887
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
11853
11888
  return { width: view.getUint32(16), height: view.getUint32(20) };
11854
11889
  }
11855
- function formatConflicts(conflicts) {
11890
+ function formatConflicts(conflicts, dryRun) {
11856
11891
  const shown = conflicts.slice(0, 10);
11857
11892
  const lines = shown.map((conflict) => `- ${conflict.path}: ${conflict.reason}`);
11858
11893
  if (conflicts.length > shown.length) {
11859
11894
  lines.push(`- and ${conflicts.length - shown.length} more`);
11860
11895
  }
11896
+ const hint = dryRunOverwriteHint(dryRun) ?? "Pass --force to overwrite.";
11861
11897
  return `Refusing to write ${conflicts.length} output file(s):
11862
11898
  ${lines.join(`
11863
11899
  `)}
11864
- Pass --force to overwrite.`;
11900
+ ${hint}`;
11865
11901
  }
11866
11902
  async function findOutputConflicts(deps, paths, sourcePaths, sourceIdentities, force) {
11867
11903
  const matchesSourceIdentity = (identity) => sourceIdentities.some((source) => source !== null && source.dev === identity.dev && source.ino === identity.ino);
@@ -12149,18 +12185,16 @@ function requireImage2(images, model) {
12149
12185
  }
12150
12186
  return image;
12151
12187
  }
12152
- async function requireWritablePath2(deps, path, label, force) {
12153
- const status = await deps.pathStatus(path);
12154
- if (status === "directory") {
12155
- throw new GenerateImageError(`${label} is a directory: ${path}. Pass a file path.`);
12156
- }
12157
- if (status === "file" && !force) {
12158
- throw new GenerateImageError(`${label} already exists: ${path}. Pass --force to overwrite.`);
12159
- }
12188
+ async function requireWritablePaths2(deps, targets, force) {
12189
+ const message = await findUnwritablePathMessage(deps, targets, force);
12190
+ if (message !== null)
12191
+ throw new GenerateImageError(message);
12160
12192
  }
12161
- async function requireWritableOutput2(deps, outPath, manifestPath, force) {
12162
- await requireWritablePath2(deps, outPath, "--out", force);
12163
- await requireWritablePath2(deps, manifestPath, "Manifest", force);
12193
+ async function requireWritableOutput2(deps, outPath, manifestPath, force, dryRun) {
12194
+ await requireWritablePaths2(deps, [
12195
+ { path: outPath, label: "--out", hint: dryRunOverwriteHint(dryRun) },
12196
+ { path: manifestPath, label: "Manifest", hint: dryRunOverwriteHint(dryRun) }
12197
+ ], force);
12164
12198
  }
12165
12199
  function recoveryMessage(action, err2, requestId, url) {
12166
12200
  const detail = err2 instanceof Error ? err2.message : String(err2);
@@ -12433,7 +12467,7 @@ async function planGenerateImage(opts, deps) {
12433
12467
  validateInputOption(opts);
12434
12468
  const inputTarget = await validateInputImage2(deps, inputImage);
12435
12469
  const manifestPath = `${outPath}.json`;
12436
- await requireWritableOutput2(deps, outPath, manifestPath, opts.force === true);
12470
+ await requireWritableOutput2(deps, outPath, manifestPath, opts.force === true, opts.dryRun === true);
12437
12471
  const input = buildInput(prompt, opts, planInputImageUrl2(inputTarget));
12438
12472
  const cost = await buildGenerateImageCost({ model, input, inputTarget, deps });
12439
12473
  return {
@@ -14763,18 +14797,16 @@ function requireMatchingDimensions(args) {
14763
14797
  }
14764
14798
  return result;
14765
14799
  }
14766
- async function requireWritablePath3(path, label, force, deps) {
14767
- const status = await deps.pathStatus(path);
14768
- if (status === "directory") {
14769
- throw new RemoveBackgroundFalError(`${label} is a directory: ${path}. Pass a file path.`);
14770
- }
14771
- if (status === "file" && !force) {
14772
- throw new RemoveBackgroundFalError(`${label} already exists: ${path}. Pass --force to overwrite.`);
14773
- }
14800
+ async function requireWritablePaths3(deps, targets, force) {
14801
+ const message = await findUnwritablePathMessage(deps, targets, force);
14802
+ if (message !== null)
14803
+ throw new RemoveBackgroundFalError(message);
14774
14804
  }
14775
14805
  async function requireWritableOutputs(outPath, manifestPath, force, deps) {
14776
- await requireWritablePath3(outPath, "--out", force, deps);
14777
- await requireWritablePath3(manifestPath, "Manifest", force, deps);
14806
+ await requireWritablePaths3(deps, [
14807
+ { path: outPath, label: "--out" },
14808
+ { path: manifestPath, label: "Manifest" }
14809
+ ], force);
14778
14810
  }
14779
14811
  function manifestSaveMessage(err2, requestId, outPath, manifestPath) {
14780
14812
  const detail = err2 instanceof Error ? err2.message : String(err2);
@@ -48679,6 +48711,8 @@ var init_errorCodes = __esm(() => {
48679
48711
  EMPTY_PROMPT: "EMPTY_PROMPT",
48680
48712
  INVALID_SCHEDULE: "INVALID_SCHEDULE",
48681
48713
  LIFECYCLE_DISABLED: "LIFECYCLE_DISABLED",
48714
+ NOTHING_TO_UPDATE: "NOTHING_TO_UPDATE",
48715
+ SCHEDULE_FINISHED: "SCHEDULE_FINISHED",
48682
48716
  TITLE_REQUIRED: "TITLE_REQUIRED",
48683
48717
  TITLE_TOO_LONG: "TITLE_TOO_LONG",
48684
48718
  HARNESS_SESSION_ALREADY_ASSOCIATED: "HARNESS_SESSION_ALREADY_ASSOCIATED",
@@ -48694,6 +48728,18 @@ var init_errorCodes = __esm(() => {
48694
48728
  INVITE_INVALID: "INVITE_INVALID",
48695
48729
  INVITE_EMAIL_MISMATCH: "INVITE_EMAIL_MISMATCH",
48696
48730
  SIGNUP_DISABLED: "SIGNUP_DISABLED",
48731
+ FILE_ATTACHMENT_NOT_FOUND: "FILE_ATTACHMENT_NOT_FOUND",
48732
+ FILE_ATTACHMENT_INVALID_STATE: "FILE_ATTACHMENT_INVALID_STATE",
48733
+ FILE_ATTACHMENT_INVALID: "FILE_ATTACHMENT_INVALID",
48734
+ FILE_ATTACHMENT_TOO_LARGE: "FILE_ATTACHMENT_TOO_LARGE",
48735
+ FILE_ATTACHMENT_TYPE_NOT_ALLOWED: "FILE_ATTACHMENT_TYPE_NOT_ALLOWED",
48736
+ FILE_ATTACHMENT_INVALID_FILENAME: "FILE_ATTACHMENT_INVALID_FILENAME",
48737
+ FILE_ATTACHMENT_EXCEEDS_QUOTA: "FILE_ATTACHMENT_EXCEEDS_QUOTA",
48738
+ FILE_ATTACHMENT_LIMIT_EXCEEDED: "FILE_ATTACHMENT_LIMIT_EXCEEDED",
48739
+ FILE_ATTACHMENT_TOO_MANY_PENDING: "FILE_ATTACHMENT_TOO_MANY_PENDING",
48740
+ FILE_ATTACHMENTS_REQUIRE_USER_TARGET: "FILE_ATTACHMENTS_REQUIRE_USER_TARGET",
48741
+ FILE_UPLOADS_UNAVAILABLE: "FILE_UPLOADS_UNAVAILABLE",
48742
+ WEB_SESSION_REQUIRED: "WEB_SESSION_REQUIRED",
48697
48743
  CONFLICT: "CONFLICT"
48698
48744
  };
48699
48745
  FATAL_ERROR_CODES = [
@@ -48788,6 +48834,8 @@ var init_errorCodes = __esm(() => {
48788
48834
  BACKEND_ERROR_CODES.EMPTY_PROMPT,
48789
48835
  BACKEND_ERROR_CODES.INVALID_SCHEDULE,
48790
48836
  BACKEND_ERROR_CODES.LIFECYCLE_DISABLED,
48837
+ BACKEND_ERROR_CODES.NOTHING_TO_UPDATE,
48838
+ BACKEND_ERROR_CODES.SCHEDULE_FINISHED,
48791
48839
  BACKEND_ERROR_CODES.TITLE_REQUIRED,
48792
48840
  BACKEND_ERROR_CODES.TITLE_TOO_LONG,
48793
48841
  BACKEND_ERROR_CODES.HARNESS_SESSION_ALREADY_ASSOCIATED,
@@ -48818,7 +48866,19 @@ var init_errorCodes = __esm(() => {
48818
48866
  BACKEND_ERROR_CODES.HARNESS_SESSION_INVALID_AGENT,
48819
48867
  BACKEND_ERROR_CODES.HARNESS_SESSION_INVALID_PROMPT,
48820
48868
  BACKEND_ERROR_CODES.HARNESS_SESSION_CLOSED,
48821
- BACKEND_ERROR_CODES.HARNESS_SESSION_UNKNOWN_AGENT
48869
+ BACKEND_ERROR_CODES.HARNESS_SESSION_UNKNOWN_AGENT,
48870
+ BACKEND_ERROR_CODES.FILE_ATTACHMENT_NOT_FOUND,
48871
+ BACKEND_ERROR_CODES.FILE_ATTACHMENT_INVALID_STATE,
48872
+ BACKEND_ERROR_CODES.FILE_ATTACHMENT_INVALID,
48873
+ BACKEND_ERROR_CODES.FILE_ATTACHMENT_TOO_LARGE,
48874
+ BACKEND_ERROR_CODES.FILE_ATTACHMENT_TYPE_NOT_ALLOWED,
48875
+ BACKEND_ERROR_CODES.FILE_ATTACHMENT_INVALID_FILENAME,
48876
+ BACKEND_ERROR_CODES.FILE_ATTACHMENT_EXCEEDS_QUOTA,
48877
+ BACKEND_ERROR_CODES.FILE_ATTACHMENT_LIMIT_EXCEEDED,
48878
+ BACKEND_ERROR_CODES.FILE_ATTACHMENT_TOO_MANY_PENDING,
48879
+ BACKEND_ERROR_CODES.FILE_ATTACHMENTS_REQUIRE_USER_TARGET,
48880
+ BACKEND_ERROR_CODES.FILE_UPLOADS_UNAVAILABLE,
48881
+ BACKEND_ERROR_CODES.WEB_SESSION_REQUIRED
48822
48882
  ];
48823
48883
  });
48824
48884
 
@@ -72693,7 +72753,9 @@ function getArchitectRequirements() {
72693
72753
  - Analyze concurrency and races: concurrent writers, idempotency, ordering and monotonicity, retries, transactions, and failure/recovery behavior.
72694
72754
  - Design read patterns first: primary read paths, indexes, projections/read models, query shapes, scan and transaction limits, and invalidation/subscription scope.
72695
72755
  - Account for Convex reactivity and bandwidth: query invalidation patterns, avoiding unnecessary reactive fanout, and separating high-frequency heartbeat/liveness/status fields from low-frequency identity/configuration fields.
72696
- - Specify concrete schema, query, and mutation changes; migration/backfill sequence; tests; and verification order.`;
72756
+ - Specify concrete schema, query, and mutation changes; migration/backfill sequence; tests; and verification order.
72757
+ - Make every new or changed interface complete: list each method or field with its callers, its inputs and outputs, and the failure modes it must signal to those callers. An interface with an unnamed caller or an unspecified failure mode is incomplete.
72758
+ - Apply the deletion test to every new module, class, or abstraction: name what complexity would reappear across its callers if it were deleted. A module that fails the test (its callers would be no more complex without it) must be removed from the design.`;
72697
72759
  }
72698
72760
  function getEntryPointToArchitectHandoffTemplate(query) {
72699
72761
  const entryPoint = query.fromRole;
@@ -72701,7 +72763,7 @@ function getEntryPointToArchitectHandoffTemplate(query) {
72701
72763
 
72702
72764
  Use this before any backend contract, API, or schema/persistence change.
72703
72765
 
72704
- Provide the request, relevant constraints, and the files or product surfaces that need inspection. Ask for one evidence-backed design focused on ${getArchitectFocus()}.
72766
+ Provide the request, relevant constraints, and the files or product surfaces that need inspection. Name any prior decision in \`memory/\` or \`docs/\` the design must respect, and name the known test constraints (for example, an existing fake, or a rule that tests run against a real database). Ask for one evidence-backed design focused on ${getArchitectFocus()}.
72705
72767
 
72706
72768
  After completing the design, the \`architect\` must hand it back to \`${entryPoint}\` with the normal handoff command, then stop; it must not implement the design or delegate further work.
72707
72769
 
@@ -72729,6 +72791,8 @@ function getArchitectToEntryPointHandoffTemplate(query) {
72729
72791
  <cite the inspected repository patterns and complete the architect checklist below; do not provide a high-level essay>
72730
72792
  - Produce exactly one recommended design for ${getArchitectFocus()}.
72731
72793
  - Name concrete repository paths, existing abstractions, APIs/types, state/data flow, acceptance criteria, risks, and verification steps.
72794
+ - Name every seam the design introduces. A test seam is the public interface where a test observes behavior. Substitution in a test is allowed only at a port or adapter boundary (an external dependency injected through a port). Do not mock internal collaborators. Each seam must have a named consumer: a test that substitutes it, or a second real adapter. Do not introduce a seam with neither.
72795
+ - Check \`memory/\` and \`docs/\` for prior decisions about the area being designed. Cite each relevant decision by path and state whether the design preserves it or overturns it, with the reason. Verify each path exists with \`ls\` before citing it.
72732
72796
  ${getArchitectRequirements()}
72733
72797
 
72734
72798
  ## Force Multipliers
@@ -73071,7 +73135,7 @@ ${getHandoffQualityPrinciplesSectionBlock()}
73071
73135
  ## Proof of Completion
73072
73136
  ${getDelegationBriefDisclosureBlock()}
73073
73137
  ${getFileReferenceProofOfCompletionExample()}
73074
- <evidence the goal was met — list every file you modified>
73138
+ <evidence the goal was met — list every file you modified; for each behavior the brief's \`## Test seams\` section names a seam for: the test name, the seam it calls, the failure output observed before the implementation, and the passing output after>
73075
73139
 
73076
73140
  ## Code Change Verification
73077
73141
  ${CODE_CHANGE_VERIFICATION_CONFIRMATION}
@@ -73082,7 +73146,7 @@ ${getHandoffNotApplicableSectionComment("List blockers")}
73082
73146
 
73083
73147
  ## Notes for review
73084
73148
  ${getHandoffNotApplicableSectionComment("List review notes")}
73085
- <specific areas for planner to check>
73149
+ <specific areas for planner to check; any test written at a seam other than the one the brief's \`## Test seams\` section named, and why>
73086
73150
  \`\`\``;
73087
73151
  }
73088
73152
  function getBuilderPlannerCodeTemplate(query) {
@@ -73143,35 +73207,30 @@ function formatProofOfPrinciples() {
73143
73207
  var GAME_ASSET_RULES;
73144
73208
  var init_game_asset_designer_workflow = __esm(() => {
73145
73209
  GAME_ASSET_RULES = [
73146
- "Generate 2D assets only with `chatroom generate-image --platform=fal --model <Model>`, where <Model> is the brief's Model: `fal-ai/flux-2/flash` (FLUX 2 Flash), or `fal-ai/flux-2/flash/edit` with --input-image for reference edits. Generate a 3D model only with `chatroom generate-3d --platform=fal --model <Model>`, where <Model> is the brief's Model and is a fal 3D model id (`chatroom generate-3d --help` lists the known ids and their flags); the default is `fal-ai/trellis-2`, where a prompt generates a reference image first and that image ships in the zip. Pass --rig and --animation-action-id only for `meshy/v7.1` models, only when the brief asks for them, and only after the user confirms the priced dry run (a real run needs --confirm-cost). Pass --confirm-cost only when ## Cost confirmation quotes the user's approval; otherwise run --dry-run, return the priced plan, and do not pass --confirm-cost. A 3D run writes `<base>.zip` (`<stem>.glb`, `web/<stem>.web.glb`, `manifest.json`, the reference image when one was generated or supplied, and the rigged and animation GLBs with their web versions when rigging ran) and `<base>.html`, a separate preview of the web variant; `<base>` is the `--out` stem plus the run's UTC timestamp, and `--package-dir <dir>` (default: the master's directory) moves both.",
73147
- "Process images only with `chatroom slice-image` and `chatroom remove-image-background`; no other tool, script, or remote service.",
73148
- "A sheet is at most the brief's `Sheet grid` (default 2x2); a larger grid needs a user request quoted in the brief.",
73149
- "Names come from looking at each tile, never from cell order or prompt order.",
73150
- "Never pass `--names` to `slice-image` except P8's fallback when `remove-image-background --help` fails; assign names only with `remove-image-background --map` or `--out`.",
73151
73210
  "Write only under `Output location` and `Allowed edit paths`; never overwrite or delete unless the brief names the path.",
73152
73211
  "Mark `pass` or `fail` only with `measured` or `inspected` evidence (labels below).",
73153
- "Run `<command> --help` before first use of `slice-image`, `remove-image-background`, or `generate-3d`.",
73154
- "Always pass `--trim` to `chatroom remove-image-background`; use `--trim-padding` only when the brief names a padding.",
73155
- "If `generate-image` or `generate-3d` is missing, `FAL_API_KEY` is not set, or the generator fails, stop generation, deliver `spec only`, quote the exact error in Summary, and list it under Open questions. Do not retry in a loop.",
73212
+ "If the generator is missing, `FAL_API_KEY` is not set, or generation fails, stop generation, deliver `spec only`, quote the exact error in Summary, and list it under Open questions. Do not retry in a loop.",
73156
73213
  "Do not install or run any remote tool, script, or skill, and do not fetch generator code.",
73157
73214
  "Never switch generators.",
73158
73215
  "Describe generated output as production-ready only when every checklist item for it passes with `measured` or `inspected` evidence.",
73159
73216
  "Commit only when the brief names the paths and the commit message.",
73160
73217
  "Generate only when the brief names an output location and allowed tools.",
73161
- "Before the first real call of each command shape (sheet, per-asset, reference edit, or 3D model), run the same command with --dry-run and apply every constraint it prints; take model-specific constraints only from the dry run, never from memory."
73218
+ "You own the lighting and palette anchor for the asset set: from the brief's `## Lighting contract` and `## Style reference`, define one shared anchor (palette, light direction, colour temperature, background, material treatment) and repeat it word for word in every prompt of the set. Write every prompt yourself; the brief states goals and constraints, not prompts.",
73219
+ 'Honour the `## Lighting contract`: no lighting may be baked into the base colour. If the generator\'s whole-surface metal warning (named in `game-asset-3d-ai`) remains after one regeneration, or any other material warning remains, report it under Open questions. The **rig reference render** is "the existing render of the target rig, attached by path under `## Lighting contract`, used as a style target before generation. It is input only and cannot support `pass`."',
73220
+ "Verify lighting under the target conditions, not the generator's. A `pass` also needs an **asset-in-rig render**: \"a render of the generated asset loaded in the project's real scene after generation, attached by path under `## Lighting verification`. Only this render supports `pass`.\" Producing it is never your job. Without it the lighting checklist item is `not checked`, which blocks delivery (the Summary starts `Delivered, lighting not verified:`). The lighting item is `n/a` when the brief's `## Lighting contract` is `Not applicable.`"
73162
73221
  ];
73163
73222
  });
73164
73223
 
73165
73224
  // ../../services/backend/prompts/teams/game-asset-designer-handoff-templates.ts
73166
73225
  function getEntryPointToGameAssetDesignerHandoffTemplate(query) {
73167
73226
  const entryPoint = query.fromRole;
73168
- 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\`. The \`## Allowed tools\` value is fixed: \`chatroom generate-image --platform=fal --model <Model> (dry run first), chatroom generate-3d --platform=fal --model <Model> (dry run first), chatroom slice-image, chatroom remove-image-background\`. Do not name any other generator; for a 3D model also fill \`## Output paths\`, whose \`--package-dir\` stays on the \`generate-3d\` entry. Before generating anything, the designer evaluates each \`## Free asset candidates\` entry and records a \`use\`, \`adapt\`, or \`reject\` decision; it generates only the assets that no candidate covers.
73227
+ 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, except \`## Lighting contract\`, which is required for a brief with any 3D asset and may be \`Not applicable.\` only when the brief has only 2D or audio assets. Pass goals and constraints as an open problem: do not author prompts, material treatment, style, or lighting art direction; the designer proposes the palette, lighting anchor, and prompts. 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\`. In \`## Allowed tools\`, keep the fixed line for each domain the brief contains and delete the others; name no other tool or generator. See the matching skill for how each tool is used. For a 3D model generated with AI also fill \`## Output paths\`; a procedural asset uses \`## Output location\` and the brief's allowed edit paths instead. Before generating anything, the designer evaluates each \`## Free asset candidates\` entry and records a \`use\`, \`adapt\`, or \`reject\` decision; it generates only the assets that no candidate covers. For a verification-only lighting brief, name the existing generated asset paths and the asset-in-rig render under \`## Existing assets\`; the designer applies only the lighting checklist item and does not generate, slice, or package.
73169
73228
 
73170
73229
  ## Asset request
73171
73230
  <one to three sentences: what the assets are for and the user-visible goal>
73172
73231
 
73173
73232
  ## Target
73174
- <engine and version; genre and camera; platform tier (mobile, PC, console, VR); render pipeline if known>
73233
+ <engine and version; genre and camera; platform tier (mobile, PC, console, VR); render pipeline (renderer, tone mapping) if known; lighting goes in the Lighting contract>
73175
73234
 
73176
73235
  ## Assets requested
73177
73236
  <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>
@@ -73182,26 +73241,29 @@ function getEntryPointToGameAssetDesignerHandoffTemplate(query) {
73182
73241
  ## Style reference
73183
73242
  <repository paths, URLs, or a written style sheet; or \`Not specified.\`>
73184
73243
 
73244
+ ## Lighting contract
73245
+ <required for a brief with any 3D asset; \`Not specified.\` is not allowed, and \`Not applicable.\` is only for a brief with only 2D or audio assets. State the goal ("consistent with the game's rig and palette") plus the rig facts the repo defines: lighting model (image-based / directional / unlit / toon); environment map present or absent; metalness policy; exposure and tone mapping; colour space; shadow setup; the repository paths that define the rig; and a **rig reference render** path (required for a 3D asset; the designer cannot produce it): "the existing render of the target rig, attached by path under \`## Lighting contract\`, used as a style target before generation. It is input only and cannot support \`pass\`." The **asset-in-rig render** is "a render of the generated asset loaded in the project's real scene after generation, attached by path under \`## Lighting verification\`. Only this render supports \`pass\`." If you do not know a fact write \`Unknown: designer to read <path>\`. Do not write prompts, material treatment, or art direction; the designer owns those.>
73246
+
73185
73247
  ## Perspective
73186
- <one camera perspective shared by every asset in the set, for example \`orthographic front view\`, \`orthographic isometric\`, \`dimetric 2:1\`, or \`side view\`; optionally \`reference: <repository path>\` of an existing asset to match (requires \`## Model\` = \`fal-ai/flux-2/flash/edit\`); a reference makes every asset an individual edit, with no sheets; \`Not Applicable.\` for 3D models and audio; or \`Not specified.\`>
73248
+ <one camera perspective shared by every asset in the set, for example \`orthographic front view\`, \`orthographic isometric\`, \`dimetric 2:1\`, or \`side view\`; optionally \`reference: <repository path>\` of an existing asset to match. A reference constrains \`## Model\` to a reference-edit model, and every asset is then generated individually; the matching skill states which. \`Not Applicable.\` for 3D models and audio; or \`Not specified.\`>
73187
73249
 
73188
73250
  ## Model
73189
- <for a 2D asset: \`fal-ai/flux-2/flash\` (FLUX 2 Flash), passed as --model; \`fal-ai/flux-2/flash/edit\` only when \`## Perspective\` names a reference asset. For a 3D model: one fal 3D model id; \`chatroom generate-3d --help\` lists the known ids and the flags each takes>
73251
+ <for a 2D asset: \`fal-ai/flux-2/flash\` (FLUX 2 Flash), passed as --model; \`fal-ai/flux-2/flash/edit\` only when \`## Perspective\` names a reference asset. For a 3D asset generated with AI: one fal 3D model id; \`chatroom generate-3d --help\` lists the known ids and the flags each takes. For a procedural 3D asset: \`procedural: <generator script path>\`. For a mixed brief, give one line per domain prefixed \`AI 2D:\` / \`AI 3D:\` / \`Procedural 3D:\`, matching the Allowed tools prefixes.>
73190
73252
 
73191
73253
  ## Model parameters
73192
- <a JSON object as text passed to \`generate-image --input\`, or to \`generate-3d --input\` for a 3D model; one object for every call of a set; its keys must pass the model's --dry-run check; \`Not specified.\` if none>
73254
+ <a JSON object of model parameters, one object for every call of a set; its keys must pass the model's parameter check. For a mixed brief, give one line per domain prefixed \`AI 2D:\` / \`AI 3D:\` / \`Procedural 3D:\`, matching the Allowed tools prefixes. \`Not specified.\` if none. \`Not applicable.\` for a procedural asset (its parameters live in the config file named in \`## Allowed edit paths\`).>
73193
73255
 
73194
73256
  ## Cost confirmation
73195
- <the user's quoted approval of the priced dry run, with the estimate it approved; or Not confirmed.>
73257
+ <the user's quoted approval of the priced generation plan, with the estimate it approved; or Not confirmed. \`Not applicable.\` for a procedural asset (no fal cost).>
73196
73258
 
73197
73259
  ## Output paths
73198
- <for a 3D model: the \`--out\` path, whose file name becomes the package base \`<out-stem>-<timestamp>\`, and the optional \`--package-dir\` directory (default: the master's directory). The run writes \`<base>.zip\` (\`<stem>.glb\`, \`web/<stem>.web.glb\`, \`manifest.json\`) and \`<base>.html\`, a separate preview of the web variant, beside the master or in that directory. \`Not Applicable.\` for a 2D-only request>
73260
+ <for a 3D model generated with AI: the \`--out\` path and the optional \`--package-dir\`; the run writes the package beside the master, and the matching skill names the files. \`Not Applicable.\` for a 2D-only request and for a procedural asset: its export path comes from \`## Output location\`, and its script, config, and output are named in \`## Allowed edit paths\`.>
73199
73261
 
73200
73262
  ## Spec consumer
73201
73263
  <who integrates these assets from the spec: \`builder\` (application logic outside the allowed edit paths) or \`user\`; asset files themselves are generated only by \`game-asset-designer\`>
73202
73264
 
73203
73265
  ## Existing assets
73204
- <repository paths the designer must reuse or must not overwrite; or \`Not specified.\`>
73266
+ <repository paths the designer must reuse or must not overwrite; for a verification-only lighting brief, the existing generated asset paths and the asset-in-rig render path; or \`Not specified.\`>
73205
73267
 
73206
73268
  ## Free asset candidates
73207
73269
  <one entry per third-party pack to evaluate before generating: pack name, source URL, expected licence (for example CC0), formats, and the asset IDs it could cover; or \`None.\`. The designer checks each pack's own licence file and file sizes and gives each candidate a \`use\`, \`adapt\`, or \`reject\` decision. It fetches only the URLs named here, inspects the pack as static files, and never runs its code.>
@@ -73210,7 +73272,10 @@ function getEntryPointToGameAssetDesignerHandoffTemplate(query) {
73210
73272
  <\`spec and generate\` (default when you name an output location and allowed tools) or \`spec only\`>
73211
73273
 
73212
73274
  ## Allowed tools
73213
- \`chatroom generate-image --platform=fal --model <Model> (dry run first), chatroom generate-3d --platform=fal --model <Model> (dry run first), chatroom slice-image, chatroom remove-image-background\`
73275
+ <keep the line for each domain the brief contains and delete the others; \`None.\` for \`spec only\`.
73276
+ AI 2D: \`chatroom generate-image --platform=fal --model <Model> (dry run first), chatroom slice-image, chatroom remove-image-background\`
73277
+ AI 3D: \`chatroom generate-3d --platform=fal --model <Model> (dry run first), gltf-transform inspect\`
73278
+ Procedural 3D: \`<generator script path> <run command>, git rev-parse HEAD, gltf-transform inspect\`>
73214
73279
 
73215
73280
  ## Allowed edit paths
73216
73281
  <repository-relative paths the designer may edit; \`None.\` for none>
@@ -73259,12 +73324,14 @@ function getGameAssetDesignerToEntryPointHandoffTemplate(query) {
73259
73324
 
73260
73325
  ## Proof of Principles
73261
73326
  ${PROOF_OF_PRINCIPLES_REPORT}
73327
+ - [ ] <append the domain Proof of Principles lines the matching skill lists, one per line>
73262
73328
 
73263
73329
  ## Proof of Completion
73264
73330
  - [ ] <one checkbox per brief acceptance criterion; state the evidence label required (measured or inspected)>
73265
73331
  - [ ] Every asset ID has a file or \`Specified only.\`
73266
- - [ ] Every sliced asset was isolated with \`remove-image-background\` and \`inspected\`.
73332
+ - [ ] Every sheet-derived asset was isolated and \`inspected\`.
73267
73333
  - [ ] Manifests are listed in the asset manifest.
73334
+ - [ ] Lighting verified under the brief's Lighting contract with an **asset-in-rig render**: "a render of the generated asset loaded in the project's real scene after generation, attached by path under \`## Lighting verification\`. Only this render supports \`pass\`." The **rig reference render** is "the existing render of the target rig, attached by path under \`## Lighting contract\`, used as a style target before generation. It is input only and cannot support \`pass\`."; otherwise the Summary starts \`Delivered, lighting not verified:\`.
73268
73335
  - [ ] Every candidate in the brief has a decision with its licence and file sizes labelled \`inspected\`.
73269
73336
 
73270
73337
  ## Target and assumptions
@@ -73274,7 +73341,7 @@ ${PROOF_OF_PRINCIPLES_REPORT}
73274
73341
  <one entry per candidate in the brief: pack name, licence quoted from the pack's own licence file, file sizes checked, decision (\`use\`, \`adapt\`, or \`reject\`), asset IDs covered, and the reason; for \`adapt\`, the edits made; write exactly \`None.\` if the brief named no candidates>
73275
73342
 
73276
73343
  ## Asset manifest
73277
- <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 command line (\`chatroom generate-image\` with its \`--platform\`, \`--model\`, \`--seed\`, \`--input\`, and \`--input-image\` values, or \`chatroom generate-3d\` with its \`--platform\`, \`--model\`, \`--prompt\`/\`--input-image\`, \`--input\`, \`--rig\`, and \`--animation-action-id\` values), the sheet and tile manifests, and the isolated asset manifest path>
73344
+ <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 command line the matching skill requires (model, seed, parameters, and any generator-specific flags), the sheet and tile manifests, and the isolated asset manifest path>
73278
73345
 
73279
73346
  ## Quality checklist results
73280
73347
  <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>
@@ -73282,6 +73349,9 @@ ${PROOF_OF_PRINCIPLES_REPORT}
73282
73349
  ## Style consistency
73283
73350
  <how the set matches the style reference; deviations; what was compared and how>
73284
73351
 
73352
+ ## Lighting verification
73353
+ <per 3D asset: the lighting contract you applied; the shared palette/lighting anchor you used; an **asset-in-rig render**: "a render of the generated asset loaded in the project's real scene after generation, attached by path under \`## Lighting verification\`. Only this render supports \`pass\`."; the **rig reference render** path: "the existing render of the target rig, attached by path under \`## Lighting contract\`, used as a style target before generation. It is input only and cannot support \`pass\`."; result \`pass\`, \`fail\`, or \`not checked\` with its evidence label. For an AI-generated 3D asset also record the preview inspection marked \`not checked\` with the reason "no allowed capture tool" (no allowed tool captures Directional or Unlit views; Studio is image-based lighting and is never evidence); a procedural asset has no preview. In a verification-only brief (existing asset paths and an asset-in-rig render named), apply only this lighting item to those files; do not regenerate, slice, or package. If any asset is \`not checked\`, the Summary must start \`Delivered, lighting not verified:\`. Write \`n/a\` for 2D-only or audio-only requests and for \`spec only\` deliverables (no asset is rendered).>
73354
+
73285
73355
  ## Naming map
73286
73356
  | cell stem | seen | assigned name | path | inspection note |
73287
73357
  | --- | --- | --- | --- | --- |
@@ -73322,7 +73392,7 @@ var init_game_asset_designer_handoff_templates = __esm(() => {
73322
73392
  init_game_asset_designer_evidence();
73323
73393
  init_game_asset_designer_workflow();
73324
73394
  init_command2();
73325
- PROOF_OF_PRINCIPLES_REPORT = GAME_ASSET_RULES.map((rule) => `- [ ] ${rule} — <evidence: command lines, grid values, or naming map reference>`).join(`
73395
+ PROOF_OF_PRINCIPLES_REPORT = GAME_ASSET_RULES.map((rule) => `- [ ] ${rule} — <evidence: command lines and output values; grid values when a sheet was generated; naming map reference when names came from a mapping>`).join(`
73326
73396
  `);
73327
73397
  });
73328
73398
 
@@ -73383,6 +73453,8 @@ function getPlannerBuilderCodeBody() {
73383
73453
 
73384
73454
  **Detail bar:** Specify down to **every file** the builder will create or modify (full repo paths). Include code snippets — types, signatures, stubs, or target implementations — until a competent builder **cannot misinterpret** what to write. Vague layers ("update the backend", "fix the component") are not acceptable.
73385
73455
 
73456
+ **Test seams:** Fill \`## Test seams\` in the brief body for each behavior the slice changes. See the \`test-seam\` glossary term for what a seam is.
73457
+
73386
73458
  Guidance for you (not part of the message): Keep one slice ≈ one focused review surface. Delegate slices incrementally — one at a time, not all at once.
73387
73459
 
73388
73460
  \`\`\`\`markdown
@@ -73437,8 +73509,15 @@ Cross-file types, interfaces, or patterns that apply beyond a single file. Omit
73437
73509
  - <verifiable outcome the builder can self-check>
73438
73510
  - Include at least one check that the feature is **verified end-to-end**. Unit tests alone are insufficient for new features.
73439
73511
 
73512
+ ## Test seams
73513
+ - <behavior>: seam <public interface or port>; catches <what>; misses <what>
73514
+ - or: no test seam — <reason>
73515
+ - For each named seam: write a failing test first, then make it pass (red-green). If the seam is \`no test seam\`, delete this line.
73516
+
73440
73517
  ## What to avoid
73441
73518
  - <anti-patterns, recurring mistakes, or scope creep for this slice — be explicit>
73519
+ - Do not test private methods or mock internal collaborators. A test that breaks on refactor while behavior is unchanged is wrong.
73520
+ - Do not derive expected values by re-running the code's own formula. Take them from a known-good literal, a worked example, or the spec.
73442
73521
  - <e.g. "Do not add new abstractions", "Do not refactor unrelated files", "Do not change existing public APIs">
73443
73522
 
73444
73523
  ## Skills to activate
@@ -73475,7 +73554,7 @@ var init_planner_game_asset_designer_chat = __esm(() => {
73475
73554
  // ../../services/backend/prompts/teams/researcher-handoff-templates.ts
73476
73555
  function getEntryPointToResearcherHandoffTemplate(query) {
73477
73556
  const entryPoint = query.fromRole;
73478
- return `The automatically injected \`<user-message>\` is the authoritative request. Pass the decisions the research must ground, the specific research questions, known constraints, and the files or areas already suspected. Ask the researcher for a report only: do not edit files, implement code, or design alternative architectures.
73557
+ return `The automatically injected \`<user-message>\` is the authoritative request. Pass the decisions the research must ground, the specific research questions, known constraints, and the files or areas already suspected. List the domain nouns the research must check for existing equivalents in the repository, and any source the researcher must prefer or avoid. Ask the researcher for a report only: do not edit files, implement code, or design alternative architectures.
73479
73558
 
73480
73559
  ### When to research
73481
73560
 
@@ -73500,7 +73579,7 @@ ${handoffCommand({
73500
73579
  }
73501
73580
  function getResearcherToEntryPointHandoffTemplate(query) {
73502
73581
  const entryPoint = query.toRole;
73503
- return `Instructions (not part of the message): Return a complete, evidence-backed research brief for the authoritative request. The researcher is advisory: do not edit files, implement code, or present alternative architectures beyond what grounds the decision. Answer only the questions asked. Separate every finding into confirmed (with evidence), inferred (with reasoning), or unknown. Never present a guess as fact. External claims must cite the source URL together with the version or date checked. Use the exact sections below, in this order.
73582
+ return `Instructions (not part of the message): Return a complete, evidence-backed research brief for the authoritative request. The researcher is advisory: do not edit files, implement code, or present alternative architectures beyond what grounds the decision. Answer only the questions asked. Separate every finding into confirmed (with evidence), inferred (with reasoning), or unknown. Never present a guess as fact. External claims must cite the source URL together with the version or date checked. Prefer official documentation and primary sources (the package's own source or changelog, the platform's release notes) over blog posts and summaries; if only a secondary source was found, label the claim as resting on it. Use the exact sections below, in this order.
73504
73583
 
73505
73584
  ## Summary
73506
73585
  <state the answer to the core question in two to four sentences and whether any decision remains open>
@@ -73512,10 +73591,10 @@ function getResearcherToEntryPointHandoffTemplate(query) {
73512
73591
  <one entry per question, each tagged \`confirmed\` (with evidence), \`inferred\` (with reasoning), or \`unknown\`>
73513
73592
 
73514
73593
  ## Repository evidence
73515
- <concrete repository paths and symbols with what each shows; quote the relevant lines briefly>
73594
+ <concrete repository paths and symbols with what each shows; quote the relevant lines briefly; before recommending any new library, type, module, or abstraction, search the repository for an existing concept that does the same job under another name (search the domain nouns and their synonyms), and list each match with whether it fits and why>
73516
73595
 
73517
73596
  ## External sources
73518
- <URL, version or date checked, and the specific claim each source supports; write exactly \`None consulted.\` if no external source was needed, and say why>
73597
+ <one line per source: URL, the version or date checked, whether it is primary (official documentation, source code, changelog, release notes) or secondary, and the specific claim it supports; write exactly \`None consulted.\` if no external source was needed, and say why>
73519
73598
 
73520
73599
  ## Grounded facts for the plan
73521
73600
  <the facts the planner can rely on when decomposing and delegating, each tagged confirmed or inferred>
@@ -73560,7 +73639,7 @@ var init_planner_researcher_chat = __esm(() => {
73560
73639
  // ../../services/backend/prompts/teams/reviewer-handoff-templates.ts
73561
73640
  function getEntryPointToReviewerHandoffTemplate(query) {
73562
73641
  const entryPoint = query.fromRole;
73563
- 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.
73642
+ 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. Name the spec the change must satisfy (the user's request, an issue, or a file under \`docs/\`), or write \`no spec available\` when there is none. Ask the reviewer for a report only: do not edit files, run builds or tests, or implement fixes.
73564
73643
 
73565
73644
  ### When to review
73566
73645
 
@@ -73584,16 +73663,35 @@ ${handoffCommand({
73584
73663
  }
73585
73664
  function getReviewerToEntryPointHandoffTemplate(query) {
73586
73665
  const entryPoint = query.toRole;
73587
- 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.
73666
+ return `Instructions (not part of the message): Return a complete review report for the target the \`${entryPoint}\` 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.
73667
+
73668
+ What a good review covers:
73669
+ - Read the actual files and the surrounding code the diff depends on, not summaries.
73670
+ - Judge the target on three axes: \`standards\` (the repository's documented standards, such as \`CONTRIBUTING.md\`, \`AGENTS.md\`, or \`CLAUDE.md\` when present, and the conventions of the surrounding code; cite the standard by file and rule), \`spec\` (the request, issue, or spec the planner named, recorded in \`## Review scope\`; quote the requirement), and \`defect\` (a demonstrable bug or security flaw, cited by reproduction or code path). Tag every issue with its axis.
73671
+ - Cite the repository-relative \`file:line\` or line range for every issue, give the evidence (the quoted line or the code path), and suggest a concrete, small fix.
73672
+ - Calibrate severity: \`blocker\` for exposure or modification of another user's data, a broken build, or a violated must-have spec requirement; \`major\` for a real defect or a non-blocking spec miss, including a missing or wrong auth check that exposes no other user's data (the endpoint audit's Reporting rule takes precedence for auth gaps); \`minor\` for a real but low-impact issue; \`nit\` for style only. Group the issues by severity with blockers first.
73673
+ - Report only what you observed; mark anything unverified as such and append \`(judgement)\` to a heuristic call that is neither a documented-standard breach nor a \`defect\`.
73674
+ - Quote the named spec's requirement for every \`spec\` issue. If no spec was named or found, write \`no spec available\` in \`## Review scope\`; do not invent one.
73675
+ - Stay within the stated focus, and do not duplicate issues.
73676
+ - Name what you did not check under \`## Not reviewed\`.
73677
+
73678
+ Audit authentication and authorization for every backend endpoint in review scope, regardless of the stated focus. An endpoint is any exported Convex query, mutation, or action, HTTP route, or API handler that the target adds or changes, plus any such endpoint the changed code calls or exposes. For each endpoint, check:
73679
+ - **Authentication:** the endpoint establishes the caller's identity before it reads or writes data, using the shared auth helper the sibling endpoints use. It does not trust a caller-supplied user id, role, or team id as proof of identity.
73680
+ - **Authorization:** the endpoint checks that the authenticated caller may act on the specific resource the request names (for example chatroom membership, ownership, or role) before it returns data or changes state. A login check alone is not sufficient.
73681
+ - **Coverage:** no code path returns or changes data before its check runs. An internal helper that skips checks may be called only from endpoints that already performed them.
73682
+ - **Intent:** an endpoint without a check is acceptable only when the code or the spec states why it is public. Otherwise report it.
73683
+ - **Reporting:** report each missing or wrong check as a \`defect\` axis issue. Use \`blocker\` when it exposes or changes another user's data, and \`major\` otherwise. Cite the endpoint's \`file:line\`; list every in-scope endpoint you did not read under \`## Not reviewed\`.
73684
+
73685
+ Use the exact sections below, in this order.
73588
73686
 
73589
73687
  ## Summary
73590
- <one to three sentences: overall state of the target and the count of blocker/major issues>
73688
+ <one to three sentences: overall state of the target; the pass/fail result for each of \`standards\`, \`spec\`, and \`defect\`; and the count of blocker/major issues>
73591
73689
 
73592
73690
  ## Review scope
73593
- <commits, paths, and PR/branch inspected; commands run, if any>
73691
+ <commits, paths, and PR/branch inspected; the spec source used for the spec axis, or exactly \`no spec available\` when none was named or found; the standards files read; commands run, if any>
73594
73692
 
73595
73693
  ## Issues
73596
- <numbered list grouped by severity (blocker, major, minor, nit); each item: \`file:line\` — summary — evidence — suggested fix; write exactly \`None observed.\` if none>
73694
+ <numbered list grouped by severity (blocker, major, minor, nit); each item: severity, axis (\`standards\`, \`spec\`, or \`defect\`), \`file:line\` — summary — evidence — suggested fix; append \`(judgement)\` to the summary when the issue is a heuristic call; write exactly \`None observed.\` if none>
73597
73695
 
73598
73696
  ## Not reviewed
73599
73697
  <anything out of scope or unreadable; write exactly \`None.\` if none>
@@ -73629,7 +73727,7 @@ var init_planner_reviewer_chat = __esm(() => {
73629
73727
  // ../../services/backend/prompts/teams/triage-handoff-templates.ts
73630
73728
  function getEntryPointToTriageHandoffTemplate(query) {
73631
73729
  const entryPoint = query.fromRole;
73632
- return `The automatically injected \`<user-message>\` is the authoritative request. Pass the reported bug, observed and expected behavior, reproduction details and commands, relevant constraints, and the files or symbols already suspected. Ask triage to investigate and report only. Triage may inspect code and attempt a focused automated regression test as evidence, but must not edit implementation files or implement a fix.
73730
+ return `The automatically injected \`<user-message>\` is the authoritative request. Pass the reported bug, observed and expected behavior with the source of the expectation (the reporter's words, a spec, or a documented decision), reproduction details and commands, relevant constraints, and the files or symbols already suspected. State the observable success condition: what the fixed behavior looks like to the reporter, so triage can build a loop that goes red on this bug and green once it is fixed. Ask triage to investigate and report only. Triage may inspect code and attempt a focused automated regression test as evidence, but must not edit implementation files or implement a fix.
73633
73731
 
73634
73732
  Use this for every reported bug (something broken or behaving differently than expected) before any fix is implemented.
73635
73733
 
@@ -73646,7 +73744,7 @@ ${handoffCommand({
73646
73744
  }
73647
73745
  function getTriageToEntryPointHandoffTemplate(query) {
73648
73746
  const entryPoint = query.toRole;
73649
- return `Instructions (not part of the message): Return a complete, evidence-backed investigation of the authoritative reported bug. Triage is advisory: do not edit implementation files or implement the proposed fix. Begin with the \`## Summary\` section, then use the remaining sections below in order. Use \`###\` for sub-items within a section. Cite concrete repository paths and symbols, observed behavior, commands, and test output. Distinguish confirmed findings from hypotheses; never claim a test proved behavior unless its failing assertion demonstrates the report.
73747
+ return `Instructions (not part of the message): Return a complete, evidence-backed investigation of the authoritative reported bug. Triage is advisory: do not edit implementation files or implement the proposed fix. Begin with the \`## Summary\` section, then use the remaining sections below in order. Use \`###\` for sub-items within a section. Cite concrete repository paths and symbols, observed behavior, commands, and test output. Distinguish confirmed findings from hypotheses; never claim a test proved behavior unless its failing assertion demonstrates the report. Redact every secret in quoted output as \`<REDACTED>\`, and quote only the lines that carry the signal.
73650
73748
 
73651
73749
  ## Summary
73652
73750
  <briefly state the diagnosis and whether the root cause was confirmed or remains a hypothesis>
@@ -73655,13 +73753,13 @@ function getTriageToEntryPointHandoffTemplate(query) {
73655
73753
  <state the reported symptoms and expected behavior, including reproduction details and constraints recovered from the request or relevant history>
73656
73754
 
73657
73755
  ## Root cause
73658
- <trace the relevant ownership, data, and control flow using concrete repository paths and symbols; explain the causal reasoning and explicitly label the finding as confirmed or a hypothesis>
73756
+ <first list 3 to 5 ranked hypotheses, or fewer when a stack trace or failing assertion already pins the cause, each as "If <X> is the cause, then <changing Y> removes the symptom (or <changing Z> makes it worse)", before testing any of them; then state the confirmed or hypothesized cause, tracing ownership, data, and control flow with concrete repository paths and symbols; for each hypothesis, say whether it was tested and the outcome; check decision records under \`memory/\` (start at \`memory/index.md\`) and docs/ for whether the behavior is documented as intended before calling it a defect; label the finding confirmed or hypothesis>
73659
73757
 
73660
73758
  ## Reproduction attempt
73661
- <give the focused test path and exact command, the failing assertion or exact blocker, and the outcome; if reproduction was blocked, state what was tried and what additional information would help; do not fabricate proof>
73759
+ <give the one command that drives the user's exact symptom, its observed output (redacted), and whether it is deterministic; when cheap and safe, record the run count, flake rate, and performance baseline; give the minimal scenario, listing each input, caller, or configuration removed and whether the symptom stayed; if a regression test was written, its path, command, and failing assertion; if no reproducing command could be built, say so, list what was tried, and name exactly what is needed to proceed (environment access, a redacted captured artifact, or permission to add temporary instrumentation); never fabricate proof>
73662
73760
 
73663
73761
  ## Proposed immediate fix
73664
- <recommend the smallest appropriate change, naming the likely files and the specific change; do not implement it>
73762
+ <recommend the smallest appropriate change, naming the likely files and the specific change; do not implement it; then name the regression-test seam. A test seam is the public interface where a test observes behavior. Substitution in a test is allowed only at a port or adapter boundary (an external dependency injected through a port). Do not mock internal collaborators. Give one line on what the seam catches and what it misses; if only a shallow seam exists (the bug needs several callers or a longer chain of calls to trigger), say so and add it under Systemic issues>
73665
73763
 
73666
73764
  ## Systemic issues
73667
73765
  <list each separate systemic concern with repository evidence, impact, and follow-up concern, or write exactly: None observed.>
@@ -75045,6 +75143,14 @@ var init_data_design = () => {};
75045
75143
 
75046
75144
  // ../../services/backend/src/domain/usecase/skills/modules/defragmentation/index.ts
75047
75145
  var init_defragmentation = () => {};
75146
+
75147
+ // ../../services/backend/src/domain/usecase/skills/modules/game-asset-2d-ai/index.ts
75148
+ var init_game_asset_2d_ai = () => {};
75149
+ // ../../services/backend/src/domain/usecase/skills/modules/game-asset-3d-ai/index.ts
75150
+ var init_game_asset_3d_ai = () => {};
75151
+
75152
+ // ../../services/backend/src/domain/usecase/skills/modules/game-asset-3d-procedural/index.ts
75153
+ var init_game_asset_3d_procedural = () => {};
75048
75154
  // ../../services/backend/src/domain/usecase/skills/modules/user-centricity/index.ts
75049
75155
  var init_user_centricity = () => {};
75050
75156
 
@@ -75054,6 +75160,9 @@ var init_registry3 = __esm(() => {
75054
75160
  init_code_review();
75055
75161
  init_data_design();
75056
75162
  init_defragmentation();
75163
+ init_game_asset_2d_ai();
75164
+ init_game_asset_3d_ai();
75165
+ init_game_asset_3d_procedural();
75057
75166
  init_user_centricity();
75058
75167
  });
75059
75168
 
@@ -75107,6 +75216,10 @@ var init_glossary = __esm(() => {
75107
75216
  {
75108
75217
  term: "structural-decisions",
75109
75218
  definition: "Meta-level architectural choices that persist in the codebase and influence consistency: " + "folder structure, file naming, interface definitions, and key abstraction names/locations " + "(e.g., Repository/Service layers)."
75219
+ },
75220
+ {
75221
+ term: "test-seam",
75222
+ definition: "The public interface where a test observes behavior. Substitution in a test is allowed only at a port or adapter boundary (an external dependency injected through a port); do not mock internal collaborators."
75110
75223
  }
75111
75224
  ];
75112
75225
  NATIVE_GLOSSARY_TERMS = [
@@ -75229,12 +75342,20 @@ var init_game_asset_designer2 = __esm(() => {
75229
75342
  init_sections();
75230
75343
  GAME_ASSET_DESIGNER_GUIDANCE = `## Game Asset Designer Operating Model
75231
75344
 
75232
- You specify and generate production-quality game and UI assets (generation is the default unless the brief says \`spec only\`), using \`chatroom generate-image --platform=fal\` for 2D assets and \`chatroom generate-3d --platform=fal\` for 3D models. A 3D run writes \`<base>.zip\` (contents: R1) and \`<base>.html\`, a separate preview of the web variant; \`<base>\` is the \`--out\` stem plus the run's UTC timestamp, and \`--package-dir <dir>\` (default: the master's directory) moves both. You are not the builder or planner; do not implement application logic outside the asset-integration paths the brief allows. Your delivery report uses Proof of Principles and Proof of Completion.
75345
+ You specify and generate production-quality game and UI assets (generation is the default unless the brief says \`spec only\`). You use the matching skill for every generation step. You are not the builder or planner; do not implement application logic outside the asset-integration paths the brief allows. Your delivery report uses Proof of Principles and Proof of Completion.
75233
75346
 
75234
75347
  ${"{SESSION_CONTINUITY}"}
75235
75348
 
75236
75349
  ${getInterRoleHandoffRule()}
75237
75350
 
75351
+ ### Skills
75352
+ Activate exactly the skill that matches each asset, using the command form for this chatroom and role:
75353
+ - 3D asset via AI generation → \`game-asset-3d-ai\`: \`chatroom skill activate game-asset-3d-ai --chatroom-id=<chatroom-id> --role=game-asset-designer\`
75354
+ - 2D asset via AI generation → \`game-asset-2d-ai\`: \`chatroom skill activate game-asset-2d-ai --chatroom-id=<chatroom-id> --role=game-asset-designer\`
75355
+ - 3D asset via a committed procedural generator → \`game-asset-3d-procedural\`: \`chatroom skill activate game-asset-3d-procedural --chatroom-id=<chatroom-id> --role=game-asset-designer\`
75356
+
75357
+ A brief that mixes asset types activates each skill it needs. Audio and non-game UI icons are not covered by these skills; for audio, apply the All assets and Audio checklists in this prompt.
75358
+
75238
75359
  ### RULES
75239
75360
  ${formatGameAssetRules()}
75240
75361
 
@@ -75243,16 +75364,13 @@ ${GAME_ASSET_EVIDENCE_DEFINITIONS}
75243
75364
  ${GAME_ASSET_NEVER_CLAIM_RULE} ${GAME_ASSET_RESULT_RULE}
75244
75365
 
75245
75366
  ### PROCEDURE
75246
- P1 Recover the brief. IF engine, tier, style, or budgets are missing and the repo cannot answer THEN ask \`{ENTRY_POINT_ROLE}\` (Summary "Blocked:", unreached "Not started") ELSE defaults+assumptions.
75367
+ P1 Recover the brief. IF \`## Lighting contract\` is missing, not specified, or not applicable for a 3D asset, or engine, tier, style, or budgets are missing and the repo cannot answer, THEN ask \`{ENTRY_POINT_ROLE}\` (Summary "Blocked:", unreached "Not started"); ELSE apply defaults+assumptions (\`Not applicable.\` only for 2D/audio).
75247
75368
  P2 Inspect assets, names, folders, engine config; cite paths.
75248
75369
  P3 Classify each asset; one manifest entry per ID (\`A01\`…).
75249
- P4 IF \`Deliverable mode\` = \`spec only\` OR \`Output location\`/\`Allowed tools\` missing/ambiguous THEN no files, deliver \`Specified only.\`, GOTO P10.
75250
- P5 IF the asset is 3D THEN \`generate-3d --platform=fal --model <Model>\` (\`--prompt\`/\`--input-image\`, \`--out <Output location>/<ascii-name>.glb\`, \`--rig\`/\`--animation-action-id\` only as R1 allows (approval quoted)), GOTO P9. ELSE Count N. IF N=1 or styles differ THEN per-asset \`generate-image\`, GOTO P8. ELSE use ceil(N / (C×R)) sheets of ≤(C×R) items per \`Sheet grid\` (2x2 default). Prompt: one item per cell, CxR grid, no lines/borders/frames/duplicates. Generate \`generate-image --platform=fal --model <Model> --prompt "<prompt>" --out <Output location>/_work/<sheet>.png\`.
75251
- P6 \`chatroom slice-image --in <sheet> --out-dir <Output location>/_work/tiles --cols C --rows R\`. IF refused regenerate once; IF still refused one \`generate-image\` per asset (P5).
75252
- P7 View each tile. IF any tile has 0/2+ items, a cut item, or count differs from the brief's requested count (extra cells OK) THEN regenerate once; IF still bad THEN one \`generate-image\` per asset (P5). Name from sight.
75253
- P8 IF \`remove-image-background --help\` succeeds THEN run \`remove-image-background --in-dir <tiles> --out-dir <Output location> --map <stem>=<name>,... --trim\` (single: \`--in <image> --out <Output location>/<name>.png --trim\`); view results. IF warnings/halo THEN rerun with \`--tolerance\`/\`--force\` max twice, ELSE accept. ELSE rerun \`slice-image --names\`; record trimming not delivered as a failed item/open question.
75254
- P9 Record model, seed, manifests, names.
75255
- P10 Apply checklist, label evidence, fill proofs.
75370
+ P4 IF existing assets and an asset-in-rig render are named THEN skip P5–P9 (no generation, slicing, or packaging), apply only the lighting item in this prompt's All assets checklist, GOTO P10. IF \`Deliverable mode\` = \`spec only\` OR \`Output location\`/\`Allowed tools\` missing THEN deliver \`Specified only.\`, lighting \`n/a\`, GOTO P10.
75371
+ P5 Generate: activate the matching skill and follow its generation procedure for each asset type; never use a generator the brief does not name.
75372
+ P9 Record the model or generator and tool, the seed or parameter set, the manifests, and the names.
75373
+ P10 Apply checklist, label evidence, fill proofs; apply the lighting item (\`n/a\` for \`spec only\`); if \`not checked\`, deliver as \`Delivered, lighting not verified:\`.
75256
75374
  P11 Hand off to \`{ENTRY_POINT_ROLE}\`, then stop.
75257
75375
 
75258
75376
  ### Quality checklist (apply to each asset; mark each item pass, fail, or not applicable, with its evidence label)
@@ -75262,43 +75380,15 @@ P11 Hand off to \`{ENTRY_POINT_ROLE}\`, then stop.
75262
75380
  - 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).
75263
75381
  - Style matches the named style reference: silhouette, value range, palette, level of detail, and texel density or pixel grid.
75264
75382
  - Budgets in the request are met; any exceedance is called out with the measured value.
75265
-
75266
- **3D models**
75267
- - Units and axes match the target (glTF: meters, +Y up, front +Z); scale is applied (identity scale) and a real-world size is stated.
75268
- - Pivot/origin follows the declared policy; LODs share the base pivot and alignment.
75269
- - 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).
75270
- - 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).
75271
- - 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.
75272
- - Collision and sockets use the engine's naming (Unreal \`UBX_\`/\`UCP_\`/\`USP_\`/\`UCX_\` + mesh name and \`SOCKET_\` + mesh name; Godot \`-col\`/\`-convcolonly\` suffixes).
75273
- - 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.
75274
-
75275
- **PBR materials and textures**
75276
- - Metallic-roughness workflow with documented channel packing (glTF: roughness in G, metallic in B, occlusion in R).
75277
- - 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.
75278
- - 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.
75279
- - 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.
75280
- - Tiling textures wrap without a visible seam; no baked-in lighting or text in base color unless declared.
75281
- - Maps in one set share texel density, value range, and naming (\`T_Name_BaseColor\`, \`_Normal\`, \`_ORM\`).
75282
-
75283
- **2D sprites and UI art**
75284
- - Fixed grid and pixels-per-unit declared; sprite sizes are multiples of the grid; no sub-pixel offsets.
75285
- - Palette size and colors declared; transparent background with clean alpha edges.
75286
- - Animation: uniform frame size per clip, frame count, frame rate, loop flag, and pivot per clip.
75287
- - 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.
75288
- - Export PNG (lossless with alpha); name states size and frame count where relevant.
75289
- - Camera perspective matches the perspective chosen for the set (the brief's \`Perspective\`, or the one recorded under assumptions) and every other asset in the set (compare them side by side); \`n/a\` for a single asset.
75383
+ - **Lighting**: renders correctly under the brief's \`## Lighting contract\` (visible, coloured and textured, not near-black). Pass needs the asset-in-rig render under \`## Lighting verification\`; the rig reference render and Studio mode never support pass.
75290
75384
 
75291
75385
  **Audio**
75292
- - 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).
75386
+ - 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. 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).
75293
75387
  - 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.
75294
75388
 
75295
- ### Perspective consistency (generated 2D sets)
75296
- Assets used together must share one camera perspective. The model's constraints (unsupported parameters, size defaults, what it does not guarantee) come only from the matching \`--dry-run\` (R16) — \`generate-image --dry-run\` for a 2D asset and \`generate-3d --dry-run\` for a 3D model; apply them, and never add a parameter the dry run reports as unsupported.
75297
- - Repeat the perspective term from the brief's \`Perspective\` word for word in every prompt of the set (never the \`reference:\` path), and add the same flattening terms to every prompt: \`flat 2D game asset, orthographic, centered, plain background, even lighting\`. Suggested perspective terms (\`unverified\`): side view, front view, orthographic top-down, orthographic isometric, dimetric 2:1, three-quarter view. If the field names only a reference, describe that reference's view in words and record the description under assumptions. If \`Perspective\` is \`Not specified.\`, pick one, record it under assumptions, and use it for the whole set. If the \`Style reference\` asks for a lighting style these terms contradict (for example dramatic lighting), keep the perspective term and drop only the conflicting flattening term.
75298
- - Use one \`--input\` JSON (from \`Model parameters\`) on every call of the set; if the dry run reports a size parameter as defaulted, set it once in that JSON and reuse it. Add \`--input '<JSON>'\` and the seed to every P5 command.
75299
- - Use one shared \`--seed\` for the first call of each item or sheet. For every regeneration (P6, P7, or a perspective mismatch), use a new seed and record it (P9). A shared seed does not keep perspective across different prompts.
75300
- - Pass \`--input-image <path>\` with \`--model fal-ai/flux-2/flash/edit\` only when the brief's \`Model\` is \`fal-ai/flux-2/flash/edit\` and \`Perspective\` names a reference path. In this mode generate every asset individually (the P5 per-asset path, one \`--input-image\` per asset); never use a sheet.
75301
- - Inspect the set side by side. A mismatched item is regenerated individually (the P5 per-asset path, with a new seed), at most twice per item, separately from the sheet regenerations of P6 and P7. If it still differs, mark the perspective checklist item \`fail\` with \`inspected\` evidence.
75389
+ ### Set consistency principles
75390
+ - **One camera perspective per set.** Repeat the perspective term from the brief's \`## Perspective\` word for word in every prompt of the set; \`n/a\` for 3D models and audio. The matching skill owns the full perspective rules.
75391
+ - **One lighting and palette anchor per set.** From the brief's \`## Lighting contract\` and \`## Style reference\`, choose one anchor (palette, light direction, colour temperature, background, material treatment) and repeat it word for word in every prompt of the set. If the contract and the style reference conflict, follow the contract and record the deviation under assumptions. The matching skill owns the full anchor rules.
75302
75392
 
75303
75393
  ### Scope boundary
75304
75394
  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.
@@ -75332,7 +75422,7 @@ ${getInterRoleHandoffRule()}
75332
75422
 
75333
75423
  1. Recover the authoritative request and the research questions from the injected task and chatroom history. If the questions are not explicit, derive the decisions the planner must make and answer those.
75334
75424
  2. Investigate the repository first: existing modules, APIs, types, patterns, tests, and conventions that bear on the request. Cite concrete paths and symbols.
75335
- 3. Investigate external sources when the request depends on libraries, APIs, platform behavior, versions, or recent changes. Prefer official documentation and primary sources. Record the URL, the version, and the date checked.
75425
+ 3. Investigate external sources when the request depends on libraries, APIs, platform behavior, versions, or recent changes. Prefer official documentation and primary sources (the package's own source or changelog, the platform's release notes). Record the URL, the version, and the date checked.
75336
75426
  4. Label every finding \`confirmed\`, \`inferred\`, or \`unknown\`. Never present a guess as fact.
75337
75427
  5. Resolve ambiguity where evidence allows. For the rest, state the open question, who must decide it, and a recommended default.
75338
75428
  6. Follow the injected \`<handoff-templates>\` research brief exactly, in its order. Answer only the questions asked. Do not present alternative architectures beyond what grounds the decision.
@@ -75358,9 +75448,15 @@ ${getInterRoleHandoffRule()}
75358
75448
  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.
75359
75449
  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.
75360
75450
  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.
75361
- 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.
75362
- 5. Follow the injected \`<handoff-templates>\` review report exactly, in its order.
75363
- 6. Hand the report to \`{ENTRY_POINT_ROLE}\` with the normal command, then stop:
75451
+ 4. Audit authentication and authorization for every backend endpoint in review scope. An endpoint is any exported Convex query, mutation, or action, HTTP route, or API handler that the target adds or changes, plus any such endpoint the changed code calls or exposes. For each endpoint, check:
75452
+ - **Authentication:** the endpoint establishes the caller's identity before it reads or writes data, using the shared auth helper the sibling endpoints use. It does not trust a caller-supplied user id, role, or team id as proof of identity.
75453
+ - **Authorization:** the endpoint checks that the authenticated caller may act on the specific resource the request names (for example chatroom membership, ownership, or role) before it returns data or changes state. A login check alone is not sufficient.
75454
+ - **Coverage:** no code path returns or changes data before its check runs. An internal helper that skips checks may be called only from endpoints that already performed them.
75455
+ - **Intent:** an endpoint without a check is acceptable only when the code or the spec states why it is public. Otherwise report it.
75456
+ Report each missing or wrong check as a \`defect\` axis issue. Use severity \`blocker\` when the gap exposes or changes another user's data, and \`major\` otherwise. Cite the endpoint's file and line. List any endpoint in scope that you did not read under \`## Not reviewed\`.
75457
+ 5. 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.
75458
+ 6. Follow the injected \`<handoff-templates>\` review report exactly, in its order.
75459
+ 7. Hand the report to \`{ENTRY_POINT_ROLE}\` with the normal command, then stop:
75364
75460
 
75365
75461
  \`\`\`bash
75366
75462
  chatroom handoff --chatroom-id="<chatroom-id>" --role="reviewer" --next-role="{ENTRY_POINT_ROLE}"
@@ -75381,7 +75477,7 @@ ${getInterRoleHandoffRule()}
75381
75477
 
75382
75478
  1. Recover the reported bug from the injected task and chatroom history: the observed behavior, the expected behavior, and any reproduction details. If the expected behavior is not explicit, state the assumption you made.
75383
75479
  2. Trace the relevant code paths with concrete repository paths and symbols. Label every finding \`confirmed\` or \`hypothesis\`. Never present a hypothesis as fact.
75384
- 3. Attempt a focused reproduction only when it is cheap and safe. A focused regression test may be written as evidence. Do not edit implementation files.
75480
+ 3. Attempt a focused reproduction, and when cheap and safe, record the run count, flake rate, and performance baseline. A focused regression test may be written as evidence. Do not edit implementation files.
75385
75481
  4. Write the report using the sections in the injected \`<handoff-templates>\`, in the order given, beginning with the \`## Summary\` section.
75386
75482
  5. Hand the report to \`{ENTRY_POINT_ROLE}\` with the normal command, then stop:
75387
75483
 
@@ -75748,6 +75844,218 @@ var init_generator = __esm(() => {
75748
75844
  init_review_guidelines();
75749
75845
  });
75750
75846
 
75847
+ // ../shared/src/domain/file-attachment.ts
75848
+ function basenameOf(path2) {
75849
+ return path2.split(PATH_SEPARATOR_PATTERN).pop() ?? "";
75850
+ }
75851
+ function hasForbiddenFilenameCharacter(filename) {
75852
+ return INVALID_FILENAME_CHARACTER_PATTERN.test(filename) || BIDI_CONTROL_CHARACTER_PATTERN.test(filename);
75853
+ }
75854
+ function resolveFileAttachmentType(filename) {
75855
+ const basename7 = basenameOf(filename);
75856
+ const dotIndex = basename7.lastIndexOf(".");
75857
+ if (dotIndex <= 0)
75858
+ return null;
75859
+ const mimeType = EXTENSION_TO_MIME_TYPE.get(basename7.slice(dotIndex).toLowerCase());
75860
+ if (!mimeType)
75861
+ return null;
75862
+ const info = FILE_ATTACHMENT_TYPES[mimeType];
75863
+ return { mimeType, previewKind: info.previewKind, activeContent: info.activeContent };
75864
+ }
75865
+ function isValidAttachmentFilename(filename) {
75866
+ return filename.length > 0 && filename.length <= FILE_ATTACHMENT_MAX_FILENAME_LENGTH && !RESERVED_FILENAME_PATTERN.test(filename) && !hasForbiddenFilenameCharacter(filename);
75867
+ }
75868
+ var FILE_ATTACHMENT_MAX_BYTES, FILE_ATTACHMENT_MAX_PER_HANDOFF = 10, FILE_ATTACHMENT_MAX_FILENAME_LENGTH = 255, FILE_ATTACHMENT_THUMBNAIL_AUTOLOAD_MAX_BYTES, FILE_ATTACHMENT_TYPES, FILE_ATTACHMENT_MIME_TYPES, EXTENSION_TO_MIME_TYPE, INVALID_FILENAME_CHARACTER_PATTERN, BIDI_CONTROL_CHARACTER_PATTERN, PATH_SEPARATOR_PATTERN, RESERVED_FILENAME_PATTERN;
75869
+ var init_file_attachment = __esm(() => {
75870
+ FILE_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
75871
+ FILE_ATTACHMENT_THUMBNAIL_AUTOLOAD_MAX_BYTES = 2 * 1024 * 1024;
75872
+ FILE_ATTACHMENT_TYPES = {
75873
+ "image/png": { extensions: [".png"], previewKind: "image", activeContent: false },
75874
+ "image/jpeg": { extensions: [".jpg", ".jpeg"], previewKind: "image", activeContent: false },
75875
+ "image/gif": { extensions: [".gif"], previewKind: "image", activeContent: false },
75876
+ "image/webp": { extensions: [".webp"], previewKind: "image", activeContent: false },
75877
+ "image/svg+xml": { extensions: [".svg"], previewKind: "image", activeContent: true },
75878
+ "application/pdf": { extensions: [".pdf"], previewKind: "pdf", activeContent: false },
75879
+ "text/plain": { extensions: [".txt", ".log"], previewKind: "text", activeContent: false },
75880
+ "text/markdown": { extensions: [".md"], previewKind: "markdown", activeContent: false },
75881
+ "text/csv": { extensions: [".csv"], previewKind: "text", activeContent: false },
75882
+ "application/json": { extensions: [".json"], previewKind: "text", activeContent: false },
75883
+ "text/html": { extensions: [".html", ".htm"], previewKind: "text", activeContent: true },
75884
+ "application/zip": { extensions: [".zip"], previewKind: "download", activeContent: false }
75885
+ };
75886
+ FILE_ATTACHMENT_MIME_TYPES = Object.keys(FILE_ATTACHMENT_TYPES);
75887
+ EXTENSION_TO_MIME_TYPE = new Map(Object.entries(FILE_ATTACHMENT_TYPES).flatMap(([mimeType, info]) => info.extensions.map((extension2) => [
75888
+ extension2.toLowerCase(),
75889
+ mimeType
75890
+ ])));
75891
+ INVALID_FILENAME_CHARACTER_PATTERN = /[/\\\u0000-\u001f\u007f]/;
75892
+ BIDI_CONTROL_CHARACTER_PATTERN = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/;
75893
+ PATH_SEPARATOR_PATTERN = /[/\\]/;
75894
+ RESERVED_FILENAME_PATTERN = /^\.{1,2}$/;
75895
+ });
75896
+
75897
+ // src/commands/handoff/deps.ts
75898
+ var FileAttachmentUploadError;
75899
+ var init_deps3 = __esm(() => {
75900
+ FileAttachmentUploadError = class FileAttachmentUploadError extends Error {
75901
+ code;
75902
+ httpStatus;
75903
+ constructor(args2) {
75904
+ super(args2.message);
75905
+ this.name = "FileAttachmentUploadError";
75906
+ this.code = args2.code;
75907
+ this.httpStatus = args2.httpStatus;
75908
+ }
75909
+ };
75910
+ });
75911
+
75912
+ // src/commands/handoff/attachments.ts
75913
+ import { readFile as readFile12, stat as stat8 } from "node:fs/promises";
75914
+ import { basename as basename7 } from "node:path";
75915
+ function attachmentInvalid(path2, reason) {
75916
+ return { _tag: "AttachmentInvalid", path: path2, reason };
75917
+ }
75918
+ function uploadFailed(filename, cause) {
75919
+ if (cause instanceof FileAttachmentUploadError) {
75920
+ return {
75921
+ _tag: "AttachmentUploadFailed",
75922
+ filename,
75923
+ code: cause.code,
75924
+ message: cause.message
75925
+ };
75926
+ }
75927
+ return {
75928
+ _tag: "AttachmentUploadFailed",
75929
+ filename,
75930
+ code: "UPLOAD_FAILED",
75931
+ message: cause instanceof Error ? cause.message : String(cause)
75932
+ };
75933
+ }
75934
+ function validateFileStats(path2, stats) {
75935
+ if (!stats.isFile()) {
75936
+ return exports_Effect.fail(attachmentInvalid(path2, "not a regular file"));
75937
+ }
75938
+ if (stats.size <= 0) {
75939
+ return exports_Effect.fail(attachmentInvalid(path2, "file is empty"));
75940
+ }
75941
+ if (stats.size > FILE_ATTACHMENT_MAX_BYTES) {
75942
+ return exports_Effect.fail(attachmentInvalid(path2, `file exceeds the ${FILE_ATTACHMENT_MAX_BYTES}-byte limit`));
75943
+ }
75944
+ return exports_Effect.void;
75945
+ }
75946
+ function resolveAttachmentType(path2, filename) {
75947
+ const type = resolveFileAttachmentType(filename);
75948
+ if (!type) {
75949
+ return exports_Effect.fail(attachmentInvalid(path2, "file type is not allowed"));
75950
+ }
75951
+ if (!isValidAttachmentFilename(filename)) {
75952
+ return exports_Effect.fail(attachmentInvalid(path2, "filename is not valid"));
75953
+ }
75954
+ return exports_Effect.succeed(type.mimeType);
75955
+ }
75956
+ function prepareOneAttachment(path2) {
75957
+ return exports_Effect.gen(function* () {
75958
+ const filename = basename7(path2);
75959
+ const stats = yield* exports_Effect.tryPromise({
75960
+ try: () => stat8(path2),
75961
+ catch: () => attachmentInvalid(path2, "file does not exist or cannot be read")
75962
+ });
75963
+ yield* validateFileStats(path2, stats);
75964
+ const mimeType = yield* resolveAttachmentType(path2, filename);
75965
+ return { path: path2, filename, sizeBytes: stats.size, mimeType };
75966
+ });
75967
+ }
75968
+ function validateAttachmentEnvelope(paths, nextRole) {
75969
+ const normalizedNextRole = nextRole.toLowerCase();
75970
+ if (normalizedNextRole !== "user") {
75971
+ return exports_Effect.fail({ _tag: "AttachmentsRequireUserTarget" });
75972
+ }
75973
+ if (paths.length > FILE_ATTACHMENT_MAX_PER_HANDOFF) {
75974
+ return exports_Effect.fail({
75975
+ _tag: "AttachmentLimitExceeded",
75976
+ max: FILE_ATTACHMENT_MAX_PER_HANDOFF,
75977
+ count: paths.length
75978
+ });
75979
+ }
75980
+ return exports_Effect.void;
75981
+ }
75982
+ function prepareHandoffAttachments(paths, nextRole) {
75983
+ return exports_Effect.gen(function* () {
75984
+ if (paths.length === 0)
75985
+ return [];
75986
+ yield* validateAttachmentEnvelope(paths, nextRole);
75987
+ const prepared = [];
75988
+ for (const path2 of paths) {
75989
+ prepared.push(yield* prepareOneAttachment(path2));
75990
+ }
75991
+ return prepared;
75992
+ });
75993
+ }
75994
+ function discardHandoffAttachments(uploader, sessionId, attachmentIds) {
75995
+ if (attachmentIds.length === 0)
75996
+ return exports_Effect.void;
75997
+ return exports_Effect.tryPromise({
75998
+ try: () => uploader.discardUploads({ sessionId, attachmentIds: [...attachmentIds] }),
75999
+ catch: () => {
76000
+ return;
76001
+ }
76002
+ }).pipe(exports_Effect.catch(() => exports_Effect.void));
76003
+ }
76004
+ function uploadOneFile(uploader, args2, file2) {
76005
+ return exports_Effect.gen(function* () {
76006
+ const bytes = yield* exports_Effect.tryPromise({
76007
+ try: () => readFile12(file2.path),
76008
+ catch: (cause) => uploadFailed(file2.filename, cause)
76009
+ });
76010
+ if (bytes.length > FILE_ATTACHMENT_MAX_BYTES) {
76011
+ return yield* exports_Effect.fail(attachmentInvalid(file2.path, `file exceeds the ${FILE_ATTACHMENT_MAX_BYTES}-byte limit`));
76012
+ }
76013
+ if (bytes.length !== file2.sizeBytes) {
76014
+ return yield* exports_Effect.fail(attachmentInvalid(file2.path, "file changed while it was being read"));
76015
+ }
76016
+ const begun = yield* exports_Effect.tryPromise({
76017
+ try: () => uploader.beginUpload({
76018
+ sessionId: args2.sessionId,
76019
+ chatroomId: args2.chatroomId,
76020
+ senderRole: args2.senderRole,
76021
+ filename: file2.filename,
76022
+ sizeBytes: bytes.length
76023
+ }),
76024
+ catch: (cause) => uploadFailed(file2.filename, cause)
76025
+ });
76026
+ yield* exports_Effect.tryPromise({
76027
+ try: () => uploader.uploadBytes({
76028
+ sessionId: args2.sessionId,
76029
+ attachmentId: begun.attachmentId,
76030
+ uploadUrl: begun.uploadUrl,
76031
+ bytes
76032
+ }),
76033
+ catch: (cause) => uploadFailed(file2.filename, cause)
76034
+ }).pipe(exports_Effect.tapError(() => discardHandoffAttachments(uploader, args2.sessionId, [begun.attachmentId])));
76035
+ return begun.attachmentId;
76036
+ });
76037
+ }
76038
+ function uploadHandoffAttachments(uploader, args2) {
76039
+ return exports_Effect.gen(function* () {
76040
+ const uploadedIds = [];
76041
+ const uploadAll = exports_Effect.gen(function* () {
76042
+ for (const file2 of args2.files) {
76043
+ uploadedIds.push(yield* uploadOneFile(uploader, args2, file2));
76044
+ }
76045
+ });
76046
+ yield* uploadAll.pipe(exports_Effect.catch((error51) => exports_Effect.gen(function* () {
76047
+ yield* discardHandoffAttachments(uploader, args2.sessionId, uploadedIds);
76048
+ return yield* exports_Effect.fail(error51);
76049
+ })));
76050
+ return uploadedIds;
76051
+ });
76052
+ }
76053
+ var init_attachments = __esm(() => {
76054
+ init_file_attachment();
76055
+ init_dist();
76056
+ init_deps3();
76057
+ });
76058
+
75751
76059
  // src/infrastructure/cli-gateway/local-handoff-gateway.ts
75752
76060
  function createLocalHandoffGateway() {
75753
76061
  return {
@@ -75781,6 +76089,113 @@ var init_create_convex_command_deps = __esm(() => {
75781
76089
  init_client2();
75782
76090
  });
75783
76091
 
76092
+ // src/infrastructure/file-attachments/convex-file-attachment-uploader.ts
76093
+ function createConvexFileAttachmentUploader(backend2, fetchFn = fetch) {
76094
+ const backendUrl = getConvexUrl();
76095
+ return {
76096
+ beginUpload: async (args2) => {
76097
+ const result3 = await backend2.mutation(api.fileAttachments.beginUpload, args2);
76098
+ return {
76099
+ attachmentId: result3.attachmentId,
76100
+ uploadUrl: result3.uploadUrl
76101
+ };
76102
+ },
76103
+ uploadBytes: async ({ sessionId, attachmentId, uploadUrl, bytes }) => {
76104
+ assertUploadUrlAllowed(uploadUrl, backendUrl);
76105
+ const response = await fetchFn(uploadUrl, {
76106
+ method: "POST",
76107
+ headers: {
76108
+ Authorization: `Bearer ${sessionId}`,
76109
+ [ATTACHMENT_ID_HEADER]: attachmentId,
76110
+ "Content-Type": "application/octet-stream",
76111
+ "Content-Length": String(bytes.byteLength)
76112
+ },
76113
+ body: bytes
76114
+ });
76115
+ if (response.ok)
76116
+ return;
76117
+ const { code: code2, message } = await readUploadError(response);
76118
+ throw new FileAttachmentUploadError({ code: code2, message, httpStatus: response.status });
76119
+ },
76120
+ discardUploads: async ({ sessionId, attachmentIds }) => {
76121
+ await backend2.mutation(api.fileAttachments.discardUploads, { sessionId, attachmentIds });
76122
+ }
76123
+ };
76124
+ }
76125
+ function rejectedUploadUrl(message) {
76126
+ return new FileAttachmentUploadError({
76127
+ code: REJECTED_UPLOAD_URL_CODE,
76128
+ message,
76129
+ httpStatus: 0
76130
+ });
76131
+ }
76132
+ function isLoopbackHostname(hostname5) {
76133
+ return hostname5 === "localhost" || hostname5 === "127.0.0.1" || hostname5 === "[::1]";
76134
+ }
76135
+ function isConvexLocalSitePort(uploadPort, backendPort) {
76136
+ return uploadPort !== "" && backendPort !== "" && Number(uploadPort) === Number(backendPort) + 1;
76137
+ }
76138
+ function parseUploadUrl(uploadUrl, backendUrl) {
76139
+ try {
76140
+ return { upload: new URL(uploadUrl), backend: new URL(backendUrl) };
76141
+ } catch {
76142
+ throw rejectedUploadUrl("Upload URL is not a valid URL");
76143
+ }
76144
+ }
76145
+ function isSupportedUploadProtocol(protocol4) {
76146
+ return protocol4 === "http:" || protocol4 === "https:";
76147
+ }
76148
+ function requiresHttps(upload) {
76149
+ return upload.protocol === "http:" && !isLoopbackHostname(upload.hostname);
76150
+ }
76151
+ function assertUploadProtocol(upload) {
76152
+ if (!isSupportedUploadProtocol(upload.protocol)) {
76153
+ throw rejectedUploadUrl(`Upload URL uses an unsupported protocol: ${upload.protocol}`);
76154
+ }
76155
+ if (requiresHttps(upload)) {
76156
+ throw rejectedUploadUrl("Upload URL must use HTTPS unless it targets loopback");
76157
+ }
76158
+ }
76159
+ function isConvexLocalSiteUrl(upload, backend2) {
76160
+ return isLoopbackHostname(upload.hostname) && isLoopbackHostname(backend2.hostname) && isConvexLocalSitePort(upload.port, backend2.port);
76161
+ }
76162
+ function isConvexCloudSiteUrl(upload, backend2) {
76163
+ if (backend2.protocol !== "https:" || backend2.port !== "")
76164
+ return false;
76165
+ if (!backend2.hostname.endsWith(CONVEX_CLOUD_SUFFIX))
76166
+ return false;
76167
+ const siteOrigin = `https://${backend2.hostname.slice(0, -CONVEX_CLOUD_SUFFIX.length)}${CONVEX_SITE_SUFFIX}`;
76168
+ return upload.origin === siteOrigin;
76169
+ }
76170
+ function assertUploadUrlAllowed(uploadUrl, backendUrl) {
76171
+ const { upload, backend: backend2 } = parseUploadUrl(uploadUrl, backendUrl);
76172
+ assertUploadProtocol(upload);
76173
+ if (upload.origin === backend2.origin || isConvexLocalSiteUrl(upload, backend2) || isConvexCloudSiteUrl(upload, backend2)) {
76174
+ return;
76175
+ }
76176
+ throw rejectedUploadUrl(`Upload URL origin ${upload.origin} does not match the configured backend ${backend2.origin}`);
76177
+ }
76178
+ async function readUploadError(response) {
76179
+ try {
76180
+ const body = await response.json();
76181
+ return {
76182
+ code: typeof body.code === "string" ? body.code : DEFAULT_UPLOAD_ERROR_CODE,
76183
+ message: typeof body.message === "string" ? body.message : `Upload failed with status ${response.status}`
76184
+ };
76185
+ } catch {
76186
+ return {
76187
+ code: DEFAULT_UPLOAD_ERROR_CODE,
76188
+ message: `Upload failed with status ${response.status}`
76189
+ };
76190
+ }
76191
+ }
76192
+ var ATTACHMENT_ID_HEADER = "X-Chatroom-Attachment-Id", DEFAULT_UPLOAD_ERROR_CODE = "FILE_ATTACHMENT_UPLOAD_FAILED", REJECTED_UPLOAD_URL_CODE = "FILE_ATTACHMENT_UPLOAD_URL_REJECTED", CONVEX_CLOUD_SUFFIX = ".convex.cloud", CONVEX_SITE_SUFFIX = ".convex.site";
76193
+ var init_convex_file_attachment_uploader = __esm(() => {
76194
+ init_api3();
76195
+ init_deps3();
76196
+ init_client2();
76197
+ });
76198
+
75784
76199
  // src/commands/handoff/index.ts
75785
76200
  var exports_handoff = {};
75786
76201
  __export(exports_handoff, {
@@ -75789,106 +76204,222 @@ __export(exports_handoff, {
75789
76204
  });
75790
76205
  async function createDefaultDeps10() {
75791
76206
  const deps = await createConvexCommandDeps();
75792
- return { backend: deps.backend, gateway: createLocalHandoffGateway(), session: deps.session };
76207
+ return {
76208
+ backend: deps.backend,
76209
+ gateway: createLocalHandoffGateway(),
76210
+ session: deps.session,
76211
+ attachmentUploader: createConvexFileAttachmentUploader(deps.backend)
76212
+ };
75793
76213
  }
75794
- function handleHandoffError(err2) {
75795
- return exports_Effect.sync(() => {
75796
- if (err2._tag === "NotAuthenticated") {
76214
+ function toHandoffFailure(cause) {
76215
+ let errorData;
76216
+ if (cause instanceof ConvexError) {
76217
+ errorData = cause.data;
76218
+ } else if (cause instanceof LocalDaemonServerError && cause.details) {
76219
+ errorData = cause.details;
76220
+ }
76221
+ return { _tag: "HandoffFailed", cause, errorData };
76222
+ }
76223
+ function callHandoffGateway(gateway, request3) {
76224
+ return exports_Effect.tryPromise({
76225
+ try: () => gateway(request3),
76226
+ catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
76227
+ }).pipe(exports_Effect.mapError(toHandoffFailure));
76228
+ }
76229
+ function discardAfterFailedHandoff(uploader, sessionId, attachedFileIds) {
76230
+ if (!uploader || !attachedFileIds || attachedFileIds.length === 0)
76231
+ return exports_Effect.void;
76232
+ return discardHandoffAttachments(uploader, sessionId, attachedFileIds);
76233
+ }
76234
+ function performHandoff(args2) {
76235
+ return exports_Effect.gen(function* () {
76236
+ const result3 = yield* callHandoffGateway(args2.gateway, args2.request);
76237
+ if (!result3.success && result3.error) {
76238
+ return yield* exports_Effect.fail({
76239
+ _tag: "HandoffRejected",
76240
+ error: result3.error
76241
+ });
76242
+ }
76243
+ return result3;
76244
+ }).pipe(exports_Effect.catch((error51) => exports_Effect.gen(function* () {
76245
+ yield* discardAfterFailedHandoff(args2.uploader, args2.sessionId, args2.attachedFileIds);
76246
+ return yield* exports_Effect.fail(error51);
76247
+ })));
76248
+ }
76249
+ function uploadHandoffFiles(uploader, args2) {
76250
+ return exports_Effect.gen(function* () {
76251
+ if (args2.attachFiles.length === 0)
76252
+ return;
76253
+ if (!uploader) {
76254
+ return yield* exports_Effect.fail({
76255
+ _tag: "AttachmentUploadFailed",
76256
+ filename: "",
76257
+ code: "UPLOAD_UNAVAILABLE",
76258
+ message: "File attachments are not available in this environment"
76259
+ });
76260
+ }
76261
+ const files = yield* prepareHandoffAttachments(args2.attachFiles, args2.nextRole);
76262
+ return yield* uploadHandoffAttachments(uploader, {
76263
+ sessionId: args2.sessionId,
76264
+ chatroomId: args2.chatroomId,
76265
+ senderRole: args2.senderRole,
76266
+ files
76267
+ });
76268
+ });
76269
+ }
76270
+ function handleSessionError(err2) {
76271
+ switch (err2._tag) {
76272
+ case "NotAuthenticated":
75797
76273
  formatAuthError(err2.convexUrl, err2.otherUrls);
75798
- process.exit(1);
75799
- } else if (err2._tag === "InvalidChatroomId") {
76274
+ return true;
76275
+ case "InvalidChatroomId":
75800
76276
  formatChatroomIdError(err2.id);
75801
- process.exit(1);
75802
- } else if (err2._tag === "HandoffFailed") {
75803
- console.error(`
75804
- ❌ ERROR: Handoff failed`);
75805
- if (err2.errorData) {
75806
- console.error(`
75807
- ${err2.errorData.message || "An unexpected error occurred"}`);
75808
- if (process.env.CHATROOM_DEBUG === "true") {
75809
- console.error(`
76277
+ return true;
76278
+ default:
76279
+ return false;
76280
+ }
76281
+ }
76282
+ function printDebugInfo(value2) {
76283
+ if (process.env.CHATROOM_DEBUG !== "true")
76284
+ return;
76285
+ console.error(`
75810
76286
  \uD83D\uDD0D Debug Info:`);
75811
- console.error(JSON.stringify(err2.errorData, null, 2));
75812
- }
75813
- if (err2.errorData.code === "AUTH_FAILED") {
75814
- console.error(`
76287
+ console.error(value2);
76288
+ }
76289
+ function printHandoffFailureHint(code2) {
76290
+ if (code2 === "AUTH_FAILED") {
76291
+ console.error(`
75815
76292
  \uD83D\uDCA1 Try authenticating again:`);
75816
- console.error(` chatroom auth login`);
75817
- } else if (err2.errorData.code === "INVALID_ROLE") {
75818
- console.error(`
76293
+ console.error(` chatroom auth login`);
76294
+ } else if (code2 === "INVALID_ROLE") {
76295
+ console.error(`
75819
76296
  \uD83D\uDCA1 Check your team configuration and use a valid role`);
75820
- }
75821
- } else {
75822
- console.error(`
76297
+ }
76298
+ }
76299
+ function printHandoffFailed(err2) {
76300
+ console.error(`
76301
+ ❌ ERROR: Handoff failed`);
76302
+ if (err2.errorData) {
76303
+ console.error(`
76304
+ ${err2.errorData.message || "An unexpected error occurred"}`);
76305
+ printDebugInfo(JSON.stringify(err2.errorData, null, 2));
76306
+ printHandoffFailureHint(err2.errorData.code);
76307
+ } else {
76308
+ console.error(`
75823
76309
  ${err2.cause instanceof Error ? err2.cause.message : String(err2.cause)}`);
75824
- if (process.env.CHATROOM_DEBUG === "true") {
75825
- console.error(`
75826
- \uD83D\uDD0D Debug Info:`);
75827
- console.error(err2.cause);
75828
- }
75829
- }
75830
- console.error(`
76310
+ printDebugInfo(err2.cause);
76311
+ }
76312
+ console.error(`
75831
76313
  \uD83D\uDCDA Need help? Check the docs or run:`);
75832
- console.error(` chatroom handoff --help`);
75833
- process.exit(1);
75834
- } else if (err2._tag === "HandoffRejected") {
75835
- console.error(`
75836
- ❌ ERROR: ${err2.error.message}`);
75837
- if (err2.error.code === "INVALID_TARGET_ROLE" && err2.error.suggestedTargets) {
75838
- console.error(`
76314
+ console.error(` chatroom handoff --help`);
76315
+ }
76316
+ function printSuggestedTargets(targets) {
76317
+ console.error(`
75839
76318
  \uD83D\uDCCB Available handoff targets for this team:`);
75840
- for (const target of err2.error.suggestedTargets) {
75841
- console.error(` • ${target}`);
75842
- }
75843
- console.error(`
76319
+ for (const target of targets) {
76320
+ console.error(` • ${target}`);
76321
+ }
76322
+ console.error(`
75844
76323
  \uD83D\uDCA1 Check your team's handoff rules in the system prompt for valid paths.`);
75845
- } else if (err2.error.suggestedTarget) {
75846
- console.error(`
76324
+ }
76325
+ function printSuggestedTarget(target) {
76326
+ console.error(`
75847
76327
  \uD83D\uDCA1 Try this instead:`);
75848
- console.error("```");
75849
- console.error(` chatroom handoff --next-role=${err2.error.suggestedTarget} ...`);
75850
- console.error("```");
75851
- }
76328
+ console.error("```");
76329
+ console.error(` chatroom handoff --next-role=${target} ...`);
76330
+ console.error("```");
76331
+ }
76332
+ function printHandoffRejected(err2) {
76333
+ console.error(`
76334
+ ❌ ERROR: ${err2.error.message}`);
76335
+ if (err2.error.code === "INVALID_TARGET_ROLE" && err2.error.suggestedTargets) {
76336
+ printSuggestedTargets(err2.error.suggestedTargets);
76337
+ } else if (err2.error.suggestedTarget) {
76338
+ printSuggestedTarget(err2.error.suggestedTarget);
76339
+ }
76340
+ }
76341
+ function printAttachmentTargetFailure(err2) {
76342
+ if (err2._tag === "AttachmentsRequireUserTarget") {
76343
+ console.error(`
76344
+ ❌ ERROR: --attach-file can only be used with --next-role=user. Agent-to-agent handoffs cannot carry files.`);
76345
+ return;
76346
+ }
76347
+ console.error(`
76348
+ ❌ ERROR: Too many files. At most ${err2.max} files can be attached to one handoff (got ${err2.count}).`);
76349
+ }
76350
+ function printAttachmentUploadFailure(err2) {
76351
+ if (err2._tag === "AttachmentInvalid") {
76352
+ console.error(`
76353
+ ❌ ERROR: Cannot attach "${err2.path}": ${err2.reason}`);
76354
+ return;
76355
+ }
76356
+ console.error(`
76357
+ ❌ ERROR: Failed to upload "${err2.filename}": ${err2.message}`);
76358
+ console.error(` Code: ${err2.code}`);
76359
+ }
76360
+ function printAttachmentFailure(err2) {
76361
+ if (err2._tag === "AttachmentsRequireUserTarget" || err2._tag === "AttachmentLimitExceeded") {
76362
+ printAttachmentTargetFailure(err2);
76363
+ return;
76364
+ }
76365
+ printAttachmentUploadFailure(err2);
76366
+ }
76367
+ function handleHandoffError(err2) {
76368
+ return exports_Effect.sync(() => {
76369
+ if (handleSessionError(err2)) {
75852
76370
  process.exit(1);
76371
+ return;
75853
76372
  }
76373
+ switch (err2._tag) {
76374
+ case "HandoffFailed":
76375
+ printHandoffFailed(err2);
76376
+ break;
76377
+ case "HandoffRejected":
76378
+ printHandoffRejected(err2);
76379
+ break;
76380
+ default:
76381
+ printAttachmentFailure(err2);
76382
+ break;
76383
+ }
76384
+ process.exit(1);
75854
76385
  });
75855
76386
  }
75856
76387
  async function handoff(chatroomId, options, deps) {
75857
76388
  const d = deps ?? await createDefaultDeps10();
75858
76389
  const layer = commandServicesLayerFromDeps(d);
75859
- await exports_Effect.runPromise(handoffEffect(chatroomId, options).pipe(exports_Effect.catch((err2) => handleHandoffError(err2)), exports_Effect.provide(layer)));
76390
+ await exports_Effect.runPromise(handoffEffect(chatroomId, options, d.attachmentUploader).pipe(exports_Effect.catch((err2) => handleHandoffError(err2)), exports_Effect.provide(layer)));
75860
76391
  }
75861
- var handoffEffect = (chatroomId, options) => exports_Effect.gen(function* () {
76392
+ var handoffEffect = (chatroomId, options, attachmentUploader) => exports_Effect.gen(function* () {
75862
76393
  const session2 = yield* SessionService;
75863
76394
  const gateway = yield* CliGatewayService;
75864
- const { role, message, nextRole } = options;
76395
+ const { role, message, nextRole, attachFiles = [] } = options;
75865
76396
  const sessionId = yield* requireSessionForChatroomEffect({ chatroomId });
75866
- const result3 = yield* exports_Effect.tryPromise({
75867
- try: () => gateway.handoff({
76397
+ const attachedFileIds = yield* uploadHandoffFiles(attachmentUploader, {
76398
+ sessionId,
76399
+ chatroomId,
76400
+ senderRole: role,
76401
+ nextRole,
76402
+ attachFiles
76403
+ });
76404
+ const result3 = yield* performHandoff({
76405
+ gateway: gateway.handoff,
76406
+ request: {
75868
76407
  sessionId,
75869
76408
  chatroomId,
75870
76409
  senderRole: role,
75871
76410
  content: message,
75872
- targetRole: nextRole
75873
- }),
75874
- catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
75875
- }).pipe(exports_Effect.mapError((cause) => {
75876
- let errorData;
75877
- if (cause instanceof ConvexError) {
75878
- errorData = cause.data;
75879
- } else if (cause instanceof LocalDaemonServerError && cause.details) {
75880
- errorData = cause.details;
75881
- }
75882
- return { _tag: "HandoffFailed", cause, errorData };
75883
- }));
75884
- if (!result3.success && result3.error) {
75885
- return yield* exports_Effect.fail({
75886
- _tag: "HandoffRejected",
75887
- error: result3.error
75888
- });
75889
- }
76411
+ targetRole: nextRole,
76412
+ ...attachedFileIds ? { attachedFileIds } : {}
76413
+ },
76414
+ uploader: attachmentUploader,
76415
+ sessionId,
76416
+ attachedFileIds
76417
+ });
75890
76418
  const convexUrl = yield* session2.getConvexUrl();
75891
76419
  yield* exports_Effect.sync(() => {
76420
+ if (result3.attachmentsDiscarded) {
76421
+ console.error("Warning: this handoff was already recorded for this message, so the attached files were not delivered. " + "Send the files in a new handoff to user after the current one is complete.");
76422
+ }
75892
76423
  console.log(generateHandoffOutput({
75893
76424
  role,
75894
76425
  nextRole,
@@ -75902,9 +76433,11 @@ var init_handoff = __esm(() => {
75902
76433
  init_generator();
75903
76434
  init_values();
75904
76435
  init_dist();
76436
+ init_attachments();
75905
76437
  init_local_daemon();
75906
76438
  init_local_handoff_gateway();
75907
76439
  init_create_convex_command_deps();
76440
+ init_convex_file_attachment_uploader();
75908
76441
  init_services();
75909
76442
  init_error_formatting();
75910
76443
  });
@@ -84266,7 +84799,7 @@ var init_parse_skill_file = __esm(() => {
84266
84799
  });
84267
84800
 
84268
84801
  // src/infrastructure/external-skills/discover-external-skills.ts
84269
- import { lstat as lstat2, readdir as readdir2, readFile as readFile13, realpath, stat as stat8 } from "node:fs/promises";
84802
+ import { lstat as lstat2, readdir as readdir2, readFile as readFile14, realpath, stat as stat9 } from "node:fs/promises";
84270
84803
  import os from "node:os";
84271
84804
  import { join as join22 } from "node:path";
84272
84805
  function errorCode(err2) {
@@ -84341,7 +84874,7 @@ async function scanEntry(entry, sourcePath, state) {
84341
84874
  }
84342
84875
  async function isSkillCandidate(sourcePath, issues) {
84343
84876
  try {
84344
- if (!(await stat8(sourcePath)).isDirectory())
84877
+ if (!(await stat9(sourcePath)).isDirectory())
84345
84878
  return false;
84346
84879
  } catch (err2) {
84347
84880
  issues.push(issue2(sourcePath, "unreadable", errorCode(err2)));
@@ -84349,7 +84882,7 @@ async function isSkillCandidate(sourcePath, issues) {
84349
84882
  }
84350
84883
  const skillPath = join22(sourcePath, SKILL_FILE_NAME);
84351
84884
  try {
84352
- const skillStat = await stat8(skillPath);
84885
+ const skillStat = await stat9(skillPath);
84353
84886
  if (!skillStat.isFile())
84354
84887
  return false;
84355
84888
  if (skillStat.size > MAX_SKILL_FILE_BYTES) {
@@ -84374,7 +84907,7 @@ async function isSymlink2(path2) {
84374
84907
  }
84375
84908
  async function readSkillFile(sourcePath, issues) {
84376
84909
  try {
84377
- return await readFile13(join22(sourcePath, SKILL_FILE_NAME), "utf8");
84910
+ return await readFile14(join22(sourcePath, SKILL_FILE_NAME), "utf8");
84378
84911
  } catch (err2) {
84379
84912
  issues.push(issue2(sourcePath, "unreadable", errorCode(err2)));
84380
84913
  return;
@@ -86865,7 +87398,7 @@ var init_orphan_tracker = __esm(() => {
86865
87398
  });
86866
87399
 
86867
87400
  // src/daemon/entry/handlers/process/output-store.ts
86868
- import { appendFile as appendFile2, mkdir as mkdir10, readFile as readFile14, rm as rm3 } from "node:fs/promises";
87401
+ import { appendFile as appendFile2, mkdir as mkdir10, readFile as readFile15, rm as rm3 } from "node:fs/promises";
86869
87402
  import { tmpdir } from "node:os";
86870
87403
  import { join as join25 } from "node:path";
86871
87404
 
@@ -86898,7 +87431,7 @@ class TempFileOutputStore {
86898
87431
  async getLastNLines(maxLines) {
86899
87432
  let source;
86900
87433
  try {
86901
- source = await readFile14(this.state.filePath, "utf-8");
87434
+ source = await readFile15(this.state.filePath, "utf-8");
86902
87435
  } catch {
86903
87436
  source = this.state.inMemory;
86904
87437
  }
@@ -86915,7 +87448,7 @@ class TempFileOutputStore {
86915
87448
  }
86916
87449
  async getFullOutput() {
86917
87450
  try {
86918
- return await readFile14(this.state.filePath, "utf-8");
87451
+ return await readFile15(this.state.filePath, "utf-8");
86919
87452
  } catch {
86920
87453
  return this.state.inMemory;
86921
87454
  }
@@ -88545,7 +89078,7 @@ var init_workspace_visibility_policy = __esm(() => {
88545
89078
 
88546
89079
  // src/infrastructure/services/workspace/workspace-ignore.ts
88547
89080
  import { promises as fsPromises } from "node:fs";
88548
- import { readFile as readFile15 } from "node:fs/promises";
89081
+ import { readFile as readFile16 } from "node:fs/promises";
88549
89082
  import { homedir as homedir11 } from "node:os";
88550
89083
  import path2 from "node:path";
88551
89084
  function normalizeRelativePath(relativePath) {
@@ -88610,7 +89143,7 @@ async function loadGitExtraExcludeRuleSets(rootDir) {
88610
89143
  }
88611
89144
  async function readIgnoreFile(filePath) {
88612
89145
  try {
88613
- return await readFile15(filePath, "utf-8");
89146
+ return await readFile16(filePath, "utf-8");
88614
89147
  } catch {
88615
89148
  return null;
88616
89149
  }
@@ -89582,7 +90115,7 @@ var init_git_workspace_hierarchy = __esm(() => {
89582
90115
  });
89583
90116
 
89584
90117
  // ../../node_modules/.pnpm/readdirp@5.1.1/node_modules/readdirp/index.js
89585
- import { lstat as lstat3, readdir as readdir3, realpath as realpath2, stat as stat9 } from "node:fs/promises";
90118
+ import { lstat as lstat3, readdir as readdir3, realpath as realpath2, stat as stat10 } from "node:fs/promises";
89586
90119
  import { join as pjoin, resolve as presolve, sep as psep } from "node:path";
89587
90120
  import { Readable } from "node:stream";
89588
90121
  function readdirp(root, options = {}) {
@@ -89679,7 +90212,7 @@ var init_readdirp = __esm(() => {
89679
90212
  const type = opts.type ?? defaultOptions.type;
89680
90213
  this._fileFilter = normalizeFilter(opts.fileFilter);
89681
90214
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
89682
- const statMethod = opts.lstat ? lstat3 : stat9;
90215
+ const statMethod = opts.lstat ? lstat3 : stat10;
89683
90216
  if (wantBigintFsStats) {
89684
90217
  this._stat = (path5) => statMethod(path5, { bigint: true });
89685
90218
  } else {
@@ -89767,9 +90300,9 @@ var init_readdirp = __esm(() => {
89767
90300
  return { files, depth, path: path5 };
89768
90301
  }
89769
90302
  _formatEntry(dirent, path5) {
89770
- const basename7 = this._isDirent ? dirent.name : dirent;
89771
- const fullPath = pjoin(path5, basename7);
89772
- const entry = { path: fullPath.slice(this._relStart), fullPath, basename: basename7 };
90303
+ const basename8 = this._isDirent ? dirent.name : dirent;
90304
+ const fullPath = pjoin(path5, basename8);
90305
+ const entry = { path: fullPath.slice(this._relStart), fullPath, basename: basename8 };
89773
90306
  if (this._isDirent) {
89774
90307
  entry.dirent = dirent;
89775
90308
  return entry;
@@ -89834,7 +90367,7 @@ var init_readdirp = __esm(() => {
89834
90367
 
89835
90368
  // ../../node_modules/.pnpm/chokidar@5.0.0/node_modules/chokidar/handler.js
89836
90369
  import { watch as fs_watch, unwatchFile, watchFile } from "node:fs";
89837
- import { realpath as fsrealpath, lstat as lstat4, open, stat as stat10 } from "node:fs/promises";
90370
+ import { realpath as fsrealpath, lstat as lstat4, open, stat as stat11 } from "node:fs/promises";
89838
90371
  import { type as osType } from "node:os";
89839
90372
  import * as sp from "node:path";
89840
90373
  function createFsWatchInstance(path5, options, listener, errHandler, emitRaw) {
@@ -89865,9 +90398,9 @@ class NodeFsHandler {
89865
90398
  _watchWithNodeFs(path5, listener) {
89866
90399
  const opts = this.fsw.options;
89867
90400
  const directory = sp.dirname(path5);
89868
- const basename8 = sp.basename(path5);
90401
+ const basename9 = sp.basename(path5);
89869
90402
  const parent = this.fsw._getWatchedDir(directory);
89870
- parent.add(basename8);
90403
+ parent.add(basename9);
89871
90404
  const absolutePath = sp.resolve(path5);
89872
90405
  const options = {
89873
90406
  persistent: opts.persistent
@@ -89877,7 +90410,7 @@ class NodeFsHandler {
89877
90410
  let closer;
89878
90411
  if (opts.usePolling) {
89879
90412
  const enableBin = opts.interval !== opts.binaryInterval;
89880
- options.interval = enableBin && isBinaryPath(basename8) ? opts.binaryInterval : opts.interval;
90413
+ options.interval = enableBin && isBinaryPath(basename9) ? opts.binaryInterval : opts.interval;
89881
90414
  closer = setFsWatchFileListener(path5, absolutePath, options, {
89882
90415
  listener,
89883
90416
  rawEmitter: this.fsw._emitRaw
@@ -89896,17 +90429,17 @@ class NodeFsHandler {
89896
90429
  return;
89897
90430
  }
89898
90431
  const dirname19 = sp.dirname(file2);
89899
- const basename8 = sp.basename(file2);
90432
+ const basename9 = sp.basename(file2);
89900
90433
  const parent = this.fsw._getWatchedDir(dirname19);
89901
90434
  let prevStats = stats;
89902
- if (parent.has(basename8))
90435
+ if (parent.has(basename9))
89903
90436
  return;
89904
90437
  const listener = async (path5, newStats) => {
89905
90438
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file2, 5))
89906
90439
  return;
89907
90440
  if (!newStats || newStats.mtimeMs === 0) {
89908
90441
  try {
89909
- const newStats2 = await stat10(file2);
90442
+ const newStats2 = await stat11(file2);
89910
90443
  if (this.fsw.closed)
89911
90444
  return;
89912
90445
  const at = newStats2.atimeMs;
@@ -89924,9 +90457,9 @@ class NodeFsHandler {
89924
90457
  prevStats = newStats2;
89925
90458
  }
89926
90459
  } catch (error51) {
89927
- this.fsw._remove(dirname19, basename8);
90460
+ this.fsw._remove(dirname19, basename9);
89928
90461
  }
89929
- } else if (parent.has(basename8)) {
90462
+ } else if (parent.has(basename9)) {
89930
90463
  const at = newStats.atimeMs;
89931
90464
  const mt = newStats.mtimeMs;
89932
90465
  if (!at || at <= mt || mt !== prevStats.mtimeMs) {
@@ -90262,7 +90795,7 @@ var init_handler = __esm(() => {
90262
90795
  ERROR: "error"
90263
90796
  };
90264
90797
  EV = EVENTS;
90265
- statMethods = { lstat: lstat4, stat: stat10 };
90798
+ statMethods = { lstat: lstat4, stat: stat11 };
90266
90799
  HANDLER_KEYS = [KEY_LISTENERS, KEY_ERR, KEY_RAW];
90267
90800
  binaryExtensions = new Set([
90268
90801
  "3dm",
@@ -90534,7 +91067,7 @@ var init_handler = __esm(() => {
90534
91067
  // ../../node_modules/.pnpm/chokidar@5.0.0/node_modules/chokidar/index.js
90535
91068
  import { EventEmitter } from "node:events";
90536
91069
  import { stat as statcb, Stats } from "node:fs";
90537
- import { readdir as readdir4, stat as stat11 } from "node:fs/promises";
91070
+ import { readdir as readdir4, stat as stat12 } from "node:fs/promises";
90538
91071
  import * as sp2 from "node:path";
90539
91072
  function arrify(item) {
90540
91073
  return Array.isArray(item) ? item : [item];
@@ -90988,7 +91521,7 @@ var init_chokidar = __esm(() => {
90988
91521
  const fullPath = opts.cwd ? sp2.join(opts.cwd, path5) : path5;
90989
91522
  let stats2;
90990
91523
  try {
90991
- stats2 = await stat11(fullPath);
91524
+ stats2 = await stat12(fullPath);
90992
91525
  } catch (err2) {}
90993
91526
  if (!stats2 || this.closed)
90994
91527
  return;
@@ -91092,8 +91625,8 @@ var init_chokidar = __esm(() => {
91092
91625
  }
91093
91626
  return this._userIgnored(path5, stats);
91094
91627
  }
91095
- _isntIgnored(path5, stat12) {
91096
- return !this._isIgnored(path5, stat12);
91628
+ _isntIgnored(path5, stat13) {
91629
+ return !this._isIgnored(path5, stat13);
91097
91630
  }
91098
91631
  _getWatchHelpers(path5) {
91099
91632
  return new WatchHelper(path5, this.options.followSymlinks, this);
@@ -93205,8 +93738,8 @@ function parseMakefileTargets(content) {
93205
93738
  var MAX_NAME_LENGTH2 = 256;
93206
93739
 
93207
93740
  // src/infrastructure/services/workspace/workspace-resolver.ts
93208
- import { readFile as readFile17, readdir as readdir5, stat as stat12 } from "node:fs/promises";
93209
- import { join as join30, basename as basename9, resolve as resolve11 } from "node:path";
93741
+ import { readFile as readFile18, readdir as readdir5, stat as stat13 } from "node:fs/promises";
93742
+ import { join as join30, basename as basename10, resolve as resolve11 } from "node:path";
93210
93743
  async function resolveGlobPatternStar(rootDir, cleaned) {
93211
93744
  const parentDir2 = join30(rootDir, cleaned.slice(0, -2));
93212
93745
  try {
@@ -93230,7 +93763,7 @@ async function resolveLiteralPath(rootDir, cleaned) {
93230
93763
  try {
93231
93764
  if (!resolve11(dir).startsWith(resolve11(rootDir)))
93232
93765
  return [];
93233
- const s = await stat12(dir);
93766
+ const s = await stat13(dir);
93234
93767
  if (s.isDirectory())
93235
93768
  return [dir];
93236
93769
  } catch {}
@@ -93246,10 +93779,10 @@ async function resolveGlobPattern(rootDir, pattern) {
93246
93779
  }
93247
93780
  async function readPackageJson(dir) {
93248
93781
  try {
93249
- const content = await readFile17(join30(dir, "package.json"), "utf-8");
93782
+ const content = await readFile18(join30(dir, "package.json"), "utf-8");
93250
93783
  const pkg = JSON.parse(content);
93251
93784
  return {
93252
- name: pkg.name || basename9(dir),
93785
+ name: pkg.name || basename10(dir),
93253
93786
  scripts: pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {}
93254
93787
  };
93255
93788
  } catch {
@@ -93258,7 +93791,7 @@ async function readPackageJson(dir) {
93258
93791
  }
93259
93792
  async function readPnpmWorkspacePatterns(rootDir) {
93260
93793
  try {
93261
- const content = await readFile17(join30(rootDir, "pnpm-workspace.yaml"), "utf-8");
93794
+ const content = await readFile18(join30(rootDir, "pnpm-workspace.yaml"), "utf-8");
93262
93795
  const patterns = [];
93263
93796
  let inPackages = false;
93264
93797
  for (const line of content.split(`
@@ -93285,7 +93818,7 @@ async function readPnpmWorkspacePatterns(rootDir) {
93285
93818
  }
93286
93819
  async function readPackageJsonWorkspacePatterns(rootDir) {
93287
93820
  try {
93288
- const content = await readFile17(join30(rootDir, "package.json"), "utf-8");
93821
+ const content = await readFile18(join30(rootDir, "package.json"), "utf-8");
93289
93822
  const pkg = JSON.parse(content);
93290
93823
  if (!pkg.workspaces)
93291
93824
  return [];
@@ -93333,8 +93866,8 @@ async function resolveSubWorkspaces(rootDir, pm) {
93333
93866
  var init_workspace_resolver = () => {};
93334
93867
 
93335
93868
  // src/infrastructure/services/workspace/command-discovery.ts
93336
- import { access as access2, readFile as readFile18 } from "node:fs/promises";
93337
- import { join as join31, relative as relative3, basename as basename10 } from "node:path";
93869
+ import { access as access2, readFile as readFile19 } from "node:fs/promises";
93870
+ import { join as join31, relative as relative3, basename as basename11 } from "node:path";
93338
93871
  async function detectPackageManager(workingDir) {
93339
93872
  for (const { file: file2, manager } of LOCKFILE_MAP) {
93340
93873
  try {
@@ -93382,7 +93915,7 @@ function isValidScriptEntry(name, script) {
93382
93915
  }
93383
93916
  async function readJsonFile(filePath, label) {
93384
93917
  try {
93385
- const content = await readFile18(filePath, "utf-8");
93918
+ const content = await readFile19(filePath, "utf-8");
93386
93919
  return parseJsonc2(content);
93387
93920
  } catch (error51) {
93388
93921
  if (error51.code !== "ENOENT") {
@@ -93406,7 +93939,7 @@ function collectRootScriptCommands(scripts, pm, scriptPrefix, rootSw) {
93406
93939
  return commands;
93407
93940
  }
93408
93941
  async function readRootPackageJson(workingDir, pm, scriptPrefix) {
93409
- let rootPackageName = basename10(workingDir);
93942
+ let rootPackageName = basename11(workingDir);
93410
93943
  const pkg = await readJsonFile(join31(workingDir, "package.json"), "root package.json");
93411
93944
  if (!pkg)
93412
93945
  return { commands: [], rootPackageName };
@@ -93439,7 +93972,7 @@ async function readDenoJson(workingDir) {
93439
93972
  const deno = await readJsonFile(join31(workingDir, fileName), fileName);
93440
93973
  if (!deno?.tasks || typeof deno.tasks !== "object")
93441
93974
  continue;
93442
- const pkgName = deno.name ?? basename10(workingDir);
93975
+ const pkgName = deno.name ?? basename11(workingDir);
93443
93976
  const subWorkspace = { type: "deno", path: ".", name: pkgName };
93444
93977
  for (const [taskName, taskCommand] of Object.entries(deno.tasks)) {
93445
93978
  if (!isValidScriptEntry(taskName, taskCommand))
@@ -93459,14 +93992,14 @@ async function readMakefile(workingDir) {
93459
93992
  const commands = [];
93460
93993
  for (const fileName of ["Makefile", "makefile"]) {
93461
93994
  try {
93462
- const content = await readFile18(join31(workingDir, fileName), "utf-8");
93995
+ const content = await readFile19(join31(workingDir, fileName), "utf-8");
93463
93996
  const targets = parseMakefileTargets(content);
93464
93997
  if (targets.length === 0)
93465
93998
  continue;
93466
93999
  const subWorkspace = {
93467
94000
  type: "make",
93468
94001
  path: ".",
93469
- name: basename10(workingDir)
94002
+ name: basename11(workingDir)
93470
94003
  };
93471
94004
  for (const target of targets) {
93472
94005
  commands.push({
@@ -93597,8 +94130,8 @@ function createWorkspaceGitProbe(args2) {
93597
94130
  }
93598
94131
  async function statFingerprint(absPath) {
93599
94132
  try {
93600
- const stat13 = await fs7.stat(absPath);
93601
- return `${stat13.mtimeMs}:${stat13.size}`;
94133
+ const stat14 = await fs7.stat(absPath);
94134
+ return `${stat14.mtimeMs}:${stat14.size}`;
93602
94135
  } catch {
93603
94136
  return "missing";
93604
94137
  }
@@ -94624,12 +95157,12 @@ async function readCommandsUnits(workingDir) {
94624
95157
  return [{ kind: "commands", commands: commands.map(toCommandEntry) }];
94625
95158
  }
94626
95159
  async function readWorktreeUnits(workingDir) {
94627
- const [dirty, stat13] = await Promise.all([isDirty(workingDir), getDiffStat(workingDir)]);
95160
+ const [dirty, stat14] = await Promise.all([isDirty(workingDir), getDiffStat(workingDir)]);
94628
95161
  return [
94629
95162
  {
94630
95163
  kind: "worktree",
94631
95164
  isDirty: dirty,
94632
- diffStat: stat13.status === "available" ? { ...stat13.diffStat } : { filesChanged: 0, insertions: 0, deletions: 0 }
95165
+ diffStat: stat14.status === "available" ? { ...stat14.diffStat } : { filesChanged: 0, insertions: 0, deletions: 0 }
94633
95166
  }
94634
95167
  ];
94635
95168
  }
@@ -95006,7 +95539,7 @@ var init_workspace_service2 = __esm(() => {
95006
95539
  });
95007
95540
 
95008
95541
  // src/daemon/entry/init-daemon.ts
95009
- import { stat as stat13 } from "node:fs/promises";
95542
+ import { stat as stat14 } from "node:fs/promises";
95010
95543
  async function discoverModelsForHarness(harness, service4) {
95011
95544
  const installed2 = await service4.isInstalled();
95012
95545
  if (!installed2) {
@@ -95041,7 +95574,7 @@ function createDefaultDeps22() {
95041
95574
  kill: (pid, signal) => process.kill(pid, signal)
95042
95575
  },
95043
95576
  fs: {
95044
- stat: stat13
95577
+ stat: stat14
95045
95578
  },
95046
95579
  machine: {
95047
95580
  clearAgentPid,
@@ -98881,7 +99414,7 @@ var init_git_clone = __esm(() => {
98881
99414
 
98882
99415
  // src/infrastructure/local-actions/git-clone.ts
98883
99416
  import { spawn as spawn5 } from "node:child_process";
98884
- import { readdir as readdir6, stat as stat14 } from "node:fs/promises";
99417
+ import { readdir as readdir6, stat as stat15 } from "node:fs/promises";
98885
99418
  function parseGitCloneProgressLine(line) {
98886
99419
  const match5 = PROGRESS_LINE_PATTERN.exec(line.trim());
98887
99420
  if (!match5?.[1] || !match5[2])
@@ -98895,7 +99428,7 @@ function classifyGitCloneFailure(stderrTail, spawnErrorCode) {
98895
99428
  }
98896
99429
  async function statOrNull(path8) {
98897
99430
  try {
98898
- return await stat14(path8);
99431
+ return await stat15(path8);
98899
99432
  } catch (error51) {
98900
99433
  if (error51.code === "ENOENT")
98901
99434
  return null;
@@ -99579,7 +100112,7 @@ function gunzipBase64Payload(base643, maxBytes) {
99579
100112
  var init_workspace_path_security = () => {};
99580
100113
 
99581
100114
  // src/daemon/entry/files/file-content-fulfillment.ts
99582
- import { readFile as readFile19 } from "node:fs/promises";
100115
+ import { readFile as readFile20 } from "node:fs/promises";
99583
100116
  import { gzipSync as gzipSync3 } from "node:zlib";
99584
100117
  function getErrorCause(error51) {
99585
100118
  if (typeof error51 === "object" && error51 !== null && "cause" in error51 && error51.cause !== undefined) {
@@ -99654,7 +100187,7 @@ var init_file_content_fulfillment = __esm(() => {
99654
100187
  continue;
99655
100188
  }
99656
100189
  let fileNotFound = false;
99657
- const buffer = yield* exports_Effect.catch(exports_Effect.tryPromise(() => readFile19(absolutePath)), (error51) => {
100190
+ const buffer = yield* exports_Effect.catch(exports_Effect.tryPromise(() => readFile20(absolutePath)), (error51) => {
99658
100191
  if (isENOENT(error51)) {
99659
100192
  fileNotFound = true;
99660
100193
  return exports_Effect.succeed(Buffer.alloc(0));
@@ -99899,10 +100432,10 @@ function parseAttachmentFileName(remainder) {
99899
100432
  if (underscore <= 0)
99900
100433
  return false;
99901
100434
  const uuid3 = remainder.slice(0, underscore);
99902
- const basename11 = remainder.slice(underscore + 1);
99903
- if (!UUID_V4_RE.test(uuid3) || !basename11)
100435
+ const basename12 = remainder.slice(underscore + 1);
100436
+ if (!UUID_V4_RE.test(uuid3) || !basename12)
99904
100437
  return false;
99905
- return sanitizeChatAttachmentFileName(basename11) === basename11;
100438
+ return sanitizeChatAttachmentFileName(basename12) === basename12;
99906
100439
  }
99907
100440
  function getInvalidChatAttachmentUploadPathReason(relativePath) {
99908
100441
  if (relativePath.includes("\\"))
@@ -99931,14 +100464,14 @@ function normalizeSegments(relativePath) {
99931
100464
  function hasBlockedSegment(lowerSegments) {
99932
100465
  return lowerSegments.some((segment) => BLOCKED_SEGMENT_NAMES.has(segment));
99933
100466
  }
99934
- function hasBlockedBasename(basename11) {
99935
- return /^\.env(?:\.|$)/.test(basename11) || /\.(?:pem|key)$/.test(basename11) || basename11 === "id_rsa" || basename11 === "credentials.json";
100467
+ function hasBlockedBasename(basename12) {
100468
+ return /^\.env(?:\.|$)/.test(basename12) || /\.(?:pem|key)$/.test(basename12) || basename12 === "id_rsa" || basename12 === "credentials.json";
99936
100469
  }
99937
100470
  function isBlockedUploadTargetPath(relativePath) {
99938
100471
  if (!relativePath)
99939
100472
  return false;
99940
- const { lowerSegments, basename: basename11 } = normalizeSegments(relativePath);
99941
- return hasBlockedSegment(lowerSegments) || hasBlockedBasename(basename11);
100473
+ const { lowerSegments, basename: basename12 } = normalizeSegments(relativePath);
100474
+ return hasBlockedSegment(lowerSegments) || hasBlockedBasename(basename12);
99942
100475
  }
99943
100476
  function getBlockedUploadTargetReason(relativePath) {
99944
100477
  if (!isBlockedUploadTargetPath(relativePath))
@@ -101830,7 +102363,7 @@ function createDaemonDeps(opts = {}) {
101830
102363
  })
101831
102364
  };
101832
102365
  }
101833
- var init_deps3 = __esm(() => {
102366
+ var init_deps4 = __esm(() => {
101834
102367
  init_publisher_registry();
101835
102368
  });
101836
102369
 
@@ -119329,7 +119862,7 @@ var init_start_daemon = __esm(() => {
119329
119862
  init_daemon_layers();
119330
119863
  init_daemon_runtime();
119331
119864
  init_default_router_deps();
119332
- init_deps3();
119865
+ init_deps4();
119333
119866
  init_execute_chatroom_stop_command();
119334
119867
  init_init_daemon();
119335
119868
  init_persistence_path();
@@ -120170,7 +120703,7 @@ async function runGenerate3dPreview(zip, options, command) {
120170
120703
  }
120171
120704
  }
120172
120705
  function registerGenerate3dCommand(program2) {
120173
- const generate3d2 = program2.command("generate-3d").description("Generate one 3D mesh (GLB) with a fal model, plus its JSON manifest and a zip/preview package").option("--model <id>", "fal endpoint id (default fal-ai/trellis-2); any id is accepted").option("--out <path>", "Output GLB path (required; manifest is written to <path>.json)").option("--platform <name>", "Generation platform: fal", parsePlatform, DEFAULT_GENERATE_3D_PLATFORM).option("--prompt <text>", "text prompt; with the default model, a reference image is generated first").option("--input-image <path-or-url>", "reference image (local file or URL); the image-to-3D input").option("--force", "replace existing output, reference, package, preview, rig, and clip files").option("--target-height <meters>", "Scale the model so its height is this many metres (positive, finite)", parseTargetHeight).option("--origin <origin>", "Where the origin goes: feet (bottom centre), center, or as-is (default)", parseOrigin, "as-is").option("--rig", "rig with Meshy 7.1 (requires --model meshy/v7.1/...); needs --confirm-cost on a real run").option("--animation-action-id <id>", "Meshy preset animation id (0-696), repeatable; requires --rig", collectRepeatable, []).option("--package-dir <dir>", "Directory for the package zip and preview (default: the directory of --out)").option("--input <json|@file>", "extra fal request fields as a JSON object, merged over the model defaults; reserved: prompt, image_url; @file reads JSON from a file").option("--confirm-cost", "confirm the priced plan for a real run that rigs or animates (Meshy 7.1 only), after the user confirms the priced dry run; not needed for --dry-run").option("--dry-run", "Validate and print the request and the static list-price estimate. No upload or fal generation call is made; FAL_API_KEY is not required").addHelpText("after", HELP_AFTER).action(runGenerate3d);
120706
+ const generate3d2 = program2.command("generate-3d").description("Generate one 3D mesh (GLB) with a fal model, plus its JSON manifest and a zip/preview package").option("--model <id>", "fal endpoint id (default fal-ai/trellis-2); any id is accepted").option("--out <path>", "Output GLB path (required; manifest is written to <path>.json)").option("--platform <name>", "Generation platform: fal", parsePlatform, DEFAULT_GENERATE_3D_PLATFORM).option("--prompt <text>", "text prompt; with the default model, a reference image is generated first").option("--input-image <path-or-url>", "reference image (local file or URL); the image-to-3D input").option("--force", "replace existing output, reference, package, preview, rig, and clip files").option("--target-height <meters>", "Scale the model so its height is this many metres (positive, finite)", parseTargetHeight).option("--origin <origin>", "Where the origin goes: feet (bottom centre), center, or as-is (default)", parseOrigin, "as-is").option("--rig", "rig with Meshy 7.1 (requires --model meshy/v7.1/...); needs --confirm-cost on a real run").option("--animation-action-id <id>", "Meshy preset animation id (0-696), repeatable; requires --rig", collectRepeatable, []).option("--package-dir <dir>", "Directory for the package zip and preview (default: the directory of --out)").option("--input <json|@file>", "extra fal request fields as a JSON object, merged over the model defaults; reserved: prompt, image_url; @file reads JSON from a file").option("--confirm-cost", "confirm the priced plan for a real run that rigs or animates (Meshy 7.1 only), after the user confirms the priced dry run; not needed for --dry-run").option("--dry-run", "Validate and print the request and the static list-price estimate. No upload or fal generation call is made; FAL_API_KEY is not required. Refuses an existing output like a real run; add --force to print the plan anyway (a dry run writes nothing)").addHelpText("after", HELP_AFTER).action(runGenerate3d);
120174
120707
  generate3d2.command("preview <zip>").description("Rebuild <base>.html from an existing package zip, offline").option("--force", "replace an existing <base>.html").action(runGenerate3dPreview);
120175
120708
  }
120176
120709
 
@@ -120237,7 +120770,8 @@ async function runGenerateImage(options) {
120237
120770
  seed: options.seed,
120238
120771
  input,
120239
120772
  inputImage: options.inputImage,
120240
- force: options.force
120773
+ force: options.force,
120774
+ dryRun: options.dryRun === true
120241
120775
  };
120242
120776
  if (options.dryRun === true) {
120243
120777
  const { formatGenerateImagePlan: formatGenerateImagePlan2, planGenerateImage: planGenerateImage2 } = await Promise.resolve().then(() => (init_image(), exports_image));
@@ -120255,7 +120789,7 @@ async function runGenerateImage(options) {
120255
120789
  }
120256
120790
  }
120257
120791
  function registerGenerateImageCommand(program2) {
120258
- program2.command("generate-image").description("Generate one image with a fal model and write a JSON manifest next to it").requiredOption("--prompt <text>", "Text prompt describing the image").requiredOption("--out <path>", "Output image path (manifest is written to <path>.json)").option("--platform <name>", "Generation platform: fal", parsePlatform2, DEFAULT_GENERATE_IMAGE_PLATFORM).requiredOption("--model <id>", `fal model id (required), for example ${FLUX_2_FLASH_MODEL}, or ${FLUX_2_FLASH_EDIT_MODEL} with --input-image`).option("--aspect-ratio <ratio>", "Aspect ratio for models that take one, e.g. 1:1, 16:9 (omitted = model default). Rejected for models the CLI knows do not support it").option("--output-format <format>", "Output format: png or webp (omitted = model default)", parseOutputFormat).option("--seed <n>", "Seed for the random number generator", parseSeed).option("--input <json|@file>", "Model-specific JSON input object merged into the fal request; @file reads JSON from a file").option("--input-image <path-or-url>", `Image to edit: a local .png, .jpg, .jpeg, .webp, or .gif (uploaded to fal) or an http(s) URL. Requires an edit model such as ${FLUX_2_FLASH_EDIT_MODEL}`).option("--force", "Overwrite existing output and manifest files").option("--dry-run", "Validate and print the request, the model constraints the CLI knows, and the estimated fal cost. No upload or fal generation call is made; one read-only pricing request may be made when FAL_API_KEY is set (the key is not required)").addHelpText("after", HELP_AFTER2).action(runGenerateImage);
120792
+ program2.command("generate-image").description("Generate one image with a fal model and write a JSON manifest next to it").requiredOption("--prompt <text>", "Text prompt describing the image").requiredOption("--out <path>", "Output image path (manifest is written to <path>.json)").option("--platform <name>", "Generation platform: fal", parsePlatform2, DEFAULT_GENERATE_IMAGE_PLATFORM).requiredOption("--model <id>", `fal model id (required), for example ${FLUX_2_FLASH_MODEL}, or ${FLUX_2_FLASH_EDIT_MODEL} with --input-image`).option("--aspect-ratio <ratio>", "Aspect ratio for models that take one, e.g. 1:1, 16:9 (omitted = model default). Rejected for models the CLI knows do not support it").option("--output-format <format>", "Output format: png or webp (omitted = model default)", parseOutputFormat).option("--seed <n>", "Seed for the random number generator", parseSeed).option("--input <json|@file>", "Model-specific JSON input object merged into the fal request; @file reads JSON from a file").option("--input-image <path-or-url>", `Image to edit: a local .png, .jpg, .jpeg, .webp, or .gif (uploaded to fal) or an http(s) URL. Requires an edit model such as ${FLUX_2_FLASH_EDIT_MODEL}`).option("--force", "Overwrite existing output and manifest files").option("--dry-run", "Validate and print the request, the model constraints the CLI knows, and the estimated fal cost. No upload or fal generation call is made; one read-only pricing request may be made when FAL_API_KEY is set (the key is not required). Refuses an existing output like a real run; add --force to print the plan anyway (a dry run writes nothing)").addHelpText("after", HELP_AFTER2).action(runGenerateImage);
120259
120793
  }
120260
120794
 
120261
120795
  // src/commands/register-chatroom-role-command.ts
@@ -120898,7 +121432,7 @@ function buildManifest3(asset, deps, tolerance, minAreaPercent, isolatedAt) {
120898
121432
  licensing: "unverified"
120899
121433
  };
120900
121434
  }
120901
- async function preflight(outputs, tiles, force, deps) {
121435
+ async function preflight(outputs, tiles, force, dryRun, deps) {
120902
121436
  const sourcePaths = new Set;
120903
121437
  const sourceIdentities = [];
120904
121438
  for (const tile of tiles) {
@@ -120909,7 +121443,7 @@ async function preflight(outputs, tiles, force, deps) {
120909
121443
  }
120910
121444
  const conflicts = await findOutputConflicts(deps, outputs, sourcePaths, sourceIdentities, force);
120911
121445
  if (conflicts.length > 0)
120912
- throw new RemoveBackgroundError(formatConflicts(conflicts));
121446
+ throw new RemoveBackgroundError(formatConflicts(conflicts, dryRun));
120913
121447
  }
120914
121448
  function encodeAsset(image, keyed, options, deps) {
120915
121449
  if (!options.trim) {
@@ -121031,7 +121565,7 @@ async function removeImageBackground(opts, deps) {
121031
121565
  }
121032
121566
  const tiles = inspected.map((tile) => decodeInput(tile, deps));
121033
121567
  const prepared = await prepareAssets(assignNames(mode, opts, tiles), mode, options, deps);
121034
- await preflight(prepared.flatMap((asset) => [asset.path, asset.manifestPath]), tiles, opts.force === true, deps);
121568
+ await preflight(prepared.flatMap((asset) => [asset.path, asset.manifestPath]), tiles, opts.force === true, opts.dryRun === true, deps);
121035
121569
  const result = {
121036
121570
  dryRun: opts.dryRun === true,
121037
121571
  assets: toAssets(prepared),
@@ -121387,10 +121921,10 @@ function parseSourceManifest(raw, sheetSha, manifestPath) {
121387
121921
  warnings
121388
121922
  };
121389
121923
  }
121390
- async function preflightOutputs(deps, paths, sourcePaths, sourceIdentities, force) {
121924
+ async function preflightOutputs(deps, paths, sourcePaths, sourceIdentities, force, dryRun) {
121391
121925
  const conflicts = await findOutputConflicts(deps, paths, sourcePaths, sourceIdentities, force);
121392
121926
  if (conflicts.length > 0) {
121393
- throw new SliceImageError(formatConflicts(conflicts));
121927
+ throw new SliceImageError(formatConflicts(conflicts, dryRun));
121394
121928
  }
121395
121929
  }
121396
121930
  var EDGE_CONTENT_THRESHOLD = 25;
@@ -121612,7 +122146,7 @@ async function sliceImage(opts, deps) {
121612
122146
  await deps.fileIdentity(inPath),
121613
122147
  await deps.fileIdentity(sourceManifestPath)
121614
122148
  ];
121615
- await preflightOutputs(deps, allOutputPaths, new Set([inPath, sourceManifestPath]), sourceIdentities, force);
122149
+ await preflightOutputs(deps, allOutputPaths, new Set([inPath, sourceManifestPath]), sourceIdentities, force, dryRun);
121616
122150
  const encoded = outputs.map((output) => {
121617
122151
  const cropped = cropTile(image, output.cell);
121618
122152
  let encodedBytes;
@@ -122023,7 +122557,7 @@ handoffCommandGroup.command("list-templates").description("Show the pair descrip
122023
122557
  legacyNextRole: parentOpts.nextRole
122024
122558
  });
122025
122559
  });
122026
- handoffCommandGroup.option("--chatroom-id <id>", "Chatroom identifier").option("--role <role>", "Your role").option("--next-role <nextRole>", "Role to hand off to").action(async (options) => {
122560
+ handoffCommandGroup.option("--chatroom-id <id>", "Chatroom identifier").option("--role <role>", "Your role").option("--next-role <nextRole>", "Role to hand off to").option("--attach-file <path>", "File to attach (only with --next-role=user; repeatable, max 10, 10 MiB each)", collectMultiValueOption, []).action(async (options) => {
122027
122561
  const missing = [
122028
122562
  ["--chatroom-id", options.chatroomId],
122029
122563
  ["--role", options.role],
@@ -122056,7 +122590,8 @@ handoffCommandGroup.option("--chatroom-id <id>", "Chatroom identifier").option("
122056
122590
  await handoff2(chatroomId, {
122057
122591
  role,
122058
122592
  message,
122059
- nextRole
122593
+ nextRole,
122594
+ attachFiles: options.attachFile ?? []
122060
122595
  });
122061
122596
  });
122062
122597
  program2.command("notify-user").description("Send the user an update while you keep working (does not complete or transition your task)").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").action(async (options) => {
@@ -122463,4 +122998,4 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
122463
122998
  });
122464
122999
  program2.parse();
122465
123000
 
122466
- //# debugId=874F17B509E159FE64756E2164756E21
123001
+ //# debugId=954BAD59D75CD6A764756E2164756E21