@mean-weasel/lineage 0.1.14 → 0.1.16

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.
@@ -8,6 +8,32 @@ import { dirname as dirname6, join as join10, resolve as resolve8 } from "node:p
8
8
  import { spawn } from "node:child_process";
9
9
  import { fileURLToPath as fileURLToPath3 } from "node:url";
10
10
 
11
+ // src/shared/edgeSummary.ts
12
+ var edgeSummaryWordLimit = 2;
13
+ var EdgeSummaryValidationError = class extends Error {
14
+ constructor(code, message) {
15
+ super(message);
16
+ this.code = code;
17
+ this.name = "EdgeSummaryValidationError";
18
+ }
19
+ code;
20
+ };
21
+ function normalizeEdgeSummary(value) {
22
+ if (value === void 0 || value === null) return void 0;
23
+ if (typeof value !== "string") throw new EdgeSummaryValidationError("not_text", "Edge summary must be text");
24
+ const words = value.trim().split(/\s+/u).filter(Boolean);
25
+ if (words.length === 0) return void 0;
26
+ if (words.length > edgeSummaryWordLimit) {
27
+ throw new EdgeSummaryValidationError("too_many_words", `Edge summary must contain at most ${edgeSummaryWordLimit} words`);
28
+ }
29
+ return words.join(" ");
30
+ }
31
+ function requireEdgeSummary(value) {
32
+ const summary = normalizeEdgeSummary(value);
33
+ if (!summary) throw new EdgeSummaryValidationError("required", "Edge summary is required");
34
+ return summary;
35
+ }
36
+
11
37
  // src/server/agentClaims.ts
12
38
  import { createHash as createHash4, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
13
39
 
@@ -17,7 +43,7 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "node:fs";
17
43
  import { join as join6 } from "node:path";
18
44
 
19
45
  // src/server/assetCore.ts
20
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "node:fs";
46
+ import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "node:fs";
21
47
  import { dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
22
48
  import { spawnSync } from "node:child_process";
23
49
  import { fileURLToPath } from "node:url";
@@ -590,21 +616,26 @@ function validateProject(project = defaultProject) {
590
616
 
591
617
  // src/server/profileWriterLease.ts
592
618
  import { createHash as createHash3, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
593
- import { existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
619
+ import { existsSync as existsSync5, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
594
620
  import { dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
595
621
 
596
622
  // src/server/lineageProfiles.ts
597
623
  import { createHash as createHash2, randomUUID } from "node:crypto";
598
624
  import { createRequire } from "node:module";
599
625
  import {
600
- chmodSync,
626
+ chmodSync as chmodSync2,
627
+ closeSync,
601
628
  constants as fsConstants,
602
629
  copyFileSync as copyFileSync2,
603
630
  existsSync as existsSync4,
631
+ fsyncSync,
604
632
  linkSync,
633
+ lstatSync,
605
634
  mkdirSync as mkdirSync3,
635
+ openSync,
606
636
  readFileSync as readFileSync3,
607
637
  realpathSync,
638
+ renameSync,
608
639
  rmSync,
609
640
  statSync as statSync3,
610
641
  writeFileSync as writeFileSync2
@@ -617,6 +648,7 @@ var lineageProfileSchemaVersion = "lineage.profile.v1";
617
648
  var lineageProfileDoctorSchemaVersion = "lineage.profile_doctor.v1";
618
649
  var lineageProfileCloneReceiptSchemaVersion = "lineage.profile_clone_receipt.v1";
619
650
  var lineageProfileAssetsCloneReceiptSchemaVersion = "lineage.profile_assets_clone_receipt.v1";
651
+ var lineageProfileRuntimeRepinReceiptSchemaVersion = "lineage.profile_runtime_repin_receipt.v1";
620
652
 
621
653
  // src/server/lineageProfiles.ts
622
654
  var require2 = createRequire(import.meta.url);
@@ -648,13 +680,13 @@ function profileManifestPath(selector) {
648
680
  if (!profileIdPattern.test(value)) throw new Error(`Invalid profile ID: ${value}`);
649
681
  return { manifestPath: join4(lineageProfileRoot(), value, "profile.json"), namedProfileId: value };
650
682
  }
651
- function requiredString(record, key) {
652
- const value = record[key];
683
+ function requiredString(record2, key) {
684
+ const value = record2[key];
653
685
  if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest requires a non-empty ${key}`);
654
686
  return value.trim();
655
687
  }
656
- function optionalString(record, key) {
657
- const value = record[key];
688
+ function optionalString(record2, key) {
689
+ const value = record2[key];
658
690
  if (value === void 0) return void 0;
659
691
  if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest ${key} must be a non-empty string`);
660
692
  return value.trim();
@@ -705,15 +737,15 @@ function resolveLineageProfile(selector) {
705
737
  throw new Error(`Could not parse profile manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
706
738
  }
707
739
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Profile manifest must be a JSON object");
708
- const record = raw;
709
- const schemaVersion = requiredString(record, "schema_version");
740
+ const record2 = raw;
741
+ const schemaVersion = requiredString(record2, "schema_version");
710
742
  if (schemaVersion !== lineageProfileSchemaVersion) throw new Error(`Unsupported profile schema_version: ${schemaVersion}`);
711
- const profileId = requiredString(record, "profile_id");
743
+ const profileId = requiredString(record2, "profile_id");
712
744
  if (!profileIdPattern.test(profileId)) throw new Error(`Invalid profile ID: ${profileId}`);
713
745
  if (namedProfileId && namedProfileId !== profileId) {
714
746
  throw new Error(`Named profile ${namedProfileId} does not match immutable manifest profile_id ${profileId}`);
715
747
  }
716
- const expectedRaw = record.expected_runtime;
748
+ const expectedRaw = record2.expected_runtime;
717
749
  if (expectedRaw !== void 0 && (!expectedRaw || typeof expectedRaw !== "object" || Array.isArray(expectedRaw))) {
718
750
  throw new Error("Profile expected_runtime must be an object");
719
751
  }
@@ -722,18 +754,18 @@ function resolveLineageProfile(selector) {
722
754
  const expectedVersion = expected ? optionalString(expected, "version") : void 0;
723
755
  const expectedCodeFingerprint = validateFingerprint(expected ? optionalString(expected, "code_fingerprint") : void 0, "expected_runtime.code_fingerprint");
724
756
  const expectedCodeOrigin = validateCodeOrigin(expected?.code_origin);
725
- const migrationsRaw = record.required_schema_migrations;
757
+ const migrationsRaw = record2.required_schema_migrations;
726
758
  if (migrationsRaw !== void 0 && (!Array.isArray(migrationsRaw) || migrationsRaw.some((value) => typeof value !== "string" || !value.trim()))) {
727
759
  throw new Error("Profile required_schema_migrations must be an array of non-empty strings");
728
760
  }
729
- const environment = validateEnvironment(requiredString(record, "environment"));
761
+ const environment = validateEnvironment(requiredString(record2, "environment"));
730
762
  const expectedChannel = validateChannel(expected?.channel);
731
763
  if (expectedChannel && expectedChannel !== environmentChannel(environment)) {
732
764
  throw new Error(`Profile environment ${environment} conflicts with expected runtime channel ${expectedChannel}`);
733
765
  }
734
766
  const manifest = {
735
- asset_root: resolveManifestPath(manifestPath, requiredString(record, "asset_root")),
736
- database_path: resolveManifestPath(manifestPath, requiredString(record, "database_path")),
767
+ asset_root: resolveManifestPath(manifestPath, requiredString(record2, "asset_root")),
768
+ database_path: resolveManifestPath(manifestPath, requiredString(record2, "database_path")),
737
769
  environment,
738
770
  ...expected ? {
739
771
  expected_runtime: {
@@ -747,7 +779,7 @@ function resolveLineageProfile(selector) {
747
779
  profile_id: profileId,
748
780
  ...migrationsRaw ? { required_schema_migrations: migrationsRaw.map((value) => value.trim()) } : {},
749
781
  schema_version: lineageProfileSchemaVersion,
750
- service_origin: validateServiceOrigin(requiredString(record, "service_origin"))
782
+ service_origin: validateServiceOrigin(requiredString(record2, "service_origin"))
751
783
  };
752
784
  return { ...manifest, manifest_path: manifestPath, profile_fingerprint: lineageProfileFingerprint(manifest) };
753
785
  }
@@ -761,6 +793,129 @@ function lineageProfileFingerprint(profile) {
761
793
  service_origin: profile.service_origin
762
794
  })).digest("hex");
763
795
  }
796
+ function sha256(value) {
797
+ return createHash2("sha256").update(value).digest("hex");
798
+ }
799
+ function assertOwnerOnlyPath(path, kind) {
800
+ const stats = lstatSync(path);
801
+ const expectedType = kind === "manifest" ? stats.isFile() : stats.isDirectory();
802
+ if (!expectedType || stats.isSymbolicLink()) throw new Error(`Profile ${kind} must be a nonsymlink ${kind === "manifest" ? "regular file" : "directory"}: ${path}`);
803
+ if ((stats.mode & 63) !== 0) throw new Error(`Profile ${kind} must be owner-only: ${path}`);
804
+ if (typeof process.getuid === "function" && stats.uid !== process.getuid()) throw new Error(`Profile ${kind} must be owned by the current user: ${path}`);
805
+ return stats;
806
+ }
807
+ function runtimeRepinInvariant(profile) {
808
+ return JSON.stringify({
809
+ asset_root: profile.asset_root,
810
+ database_path: profile.database_path,
811
+ environment: profile.environment,
812
+ profile_fingerprint: profile.profile_fingerprint,
813
+ profile_id: profile.profile_id,
814
+ required_schema_migrations: profile.required_schema_migrations || [],
815
+ schema_version: profile.schema_version,
816
+ service_origin: profile.service_origin
817
+ });
818
+ }
819
+ function sameFileIdentity(left, right) {
820
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
821
+ }
822
+ function repinLineageDevelopmentProfileRuntime(selector, checkoutRoot, runtime, confirmWrite) {
823
+ if (!confirmWrite) throw new Error("Profile runtime repin requires --confirm-write");
824
+ if (runtime.channel !== "dev") throw new Error(`Profile runtime repin requires dev code, not ${runtime.channel}`);
825
+ if (!runtime.code?.verified) throw new Error("Profile runtime repin requires a verified checkout runtime");
826
+ if (runtime.code.origin !== "checkout") throw new Error(`Profile runtime repin requires checkout code, not ${runtime.code.origin}`);
827
+ if (!runtime.code.fingerprint || !/^[a-f0-9]{64}$/i.test(runtime.code.fingerprint)) {
828
+ throw new Error("Profile runtime repin requires a valid executing code fingerprint");
829
+ }
830
+ if (!checkoutRoot.trim()) throw new Error("Profile runtime repin requires --checkout-root");
831
+ const intendedRoot = realpathSync(resolve3(checkoutRoot));
832
+ if (!statSync3(intendedRoot).isDirectory()) throw new Error(`Profile runtime repin checkout root is not a directory: ${intendedRoot}`);
833
+ const executingRoot = realpathSync(runtime.code.root);
834
+ if (intendedRoot !== executingRoot) {
835
+ throw new Error(`Profile runtime repin checkout root ${intendedRoot} does not match executing code root ${executingRoot}`);
836
+ }
837
+ const profile = resolveLineageProfile(selector);
838
+ if (profile.environment !== "development") throw new Error(`Profile runtime repin requires a development profile, not ${profile.environment}`);
839
+ if (profile.expected_runtime?.channel !== "dev") throw new Error("Development profile runtime repin requires an existing dev channel pin");
840
+ if (profile.expected_runtime.code_origin !== "checkout") throw new Error("Development profile runtime repin requires an existing checkout origin pin");
841
+ if (!profile.expected_runtime.code_fingerprint) throw new Error("Development profile runtime repin requires an existing code fingerprint pin");
842
+ const manifestPath = profile.manifest_path;
843
+ assertOwnerOnlyPath(dirname3(manifestPath), "profile directory");
844
+ const beforeStats = assertOwnerOnlyPath(manifestPath, "manifest");
845
+ const beforeText = readFileSync3(manifestPath, "utf8");
846
+ const beforeHash = sha256(beforeText);
847
+ let raw;
848
+ try {
849
+ raw = JSON.parse(beforeText);
850
+ } catch (error) {
851
+ throw new Error(`Could not parse profile manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
852
+ }
853
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Profile manifest must be a JSON object");
854
+ const previousFingerprint = profile.expected_runtime.code_fingerprint;
855
+ const nextExpectedRuntime = {
856
+ channel: "dev",
857
+ code_fingerprint: runtime.code.fingerprint,
858
+ code_origin: "checkout",
859
+ ...runtime.gitSha ? { git_sha: runtime.gitSha } : {},
860
+ version: runtime.version
861
+ };
862
+ const updated = { ...raw, expected_runtime: nextExpectedRuntime };
863
+ const afterText = `${JSON.stringify(updated, null, 2)}
864
+ `;
865
+ const afterHash = sha256(afterText);
866
+ const result = {
867
+ changed: beforeText !== afterText,
868
+ checkout_root: intendedRoot,
869
+ manifest_after_sha256: afterHash,
870
+ manifest_before_sha256: beforeHash,
871
+ manifest_path: manifestPath,
872
+ new_code_fingerprint: runtime.code.fingerprint,
873
+ previous_code_fingerprint: previousFingerprint,
874
+ profile_fingerprint: profile.profile_fingerprint,
875
+ profile_id: profile.profile_id,
876
+ schema_version: lineageProfileRuntimeRepinReceiptSchemaVersion
877
+ };
878
+ if (!result.changed) return result;
879
+ const temporaryPath = join4(dirname3(manifestPath), `.profile.runtime-repin-${randomUUID()}.tmp`);
880
+ let temporaryExists = false;
881
+ try {
882
+ writeFileSync2(temporaryPath, afterText, { encoding: "utf8", flag: "wx", mode: 384 });
883
+ temporaryExists = true;
884
+ chmodSync2(temporaryPath, 384);
885
+ const temporaryFd = openSync(temporaryPath, "r");
886
+ try {
887
+ fsyncSync(temporaryFd);
888
+ } finally {
889
+ closeSync(temporaryFd);
890
+ }
891
+ const preparedReplacement = resolveLineageProfile(temporaryPath);
892
+ if (runtimeRepinInvariant(preparedReplacement) !== runtimeRepinInvariant(profile)) {
893
+ throw new Error("Profile runtime repin would change immutable profile routing identity");
894
+ }
895
+ const currentStats = assertOwnerOnlyPath(manifestPath, "manifest");
896
+ const currentText = readFileSync3(manifestPath, "utf8");
897
+ if (!sameFileIdentity(beforeStats, currentStats) || sha256(currentText) !== beforeHash) {
898
+ throw new Error("Profile manifest changed while runtime repin was being prepared; refusing replacement");
899
+ }
900
+ renameSync(temporaryPath, manifestPath);
901
+ temporaryExists = false;
902
+ chmodSync2(manifestPath, 384);
903
+ const directoryFd = openSync(dirname3(manifestPath), "r");
904
+ try {
905
+ fsyncSync(directoryFd);
906
+ } finally {
907
+ closeSync(directoryFd);
908
+ }
909
+ const replacement = resolveLineageProfile(manifestPath);
910
+ if (runtimeRepinInvariant(replacement) !== runtimeRepinInvariant(profile)) {
911
+ throw new Error("Profile runtime repin changed immutable profile routing identity");
912
+ }
913
+ if (readFileSync3(manifestPath, "utf8") !== afterText) throw new Error("Profile runtime repin replacement bytes do not match the prepared manifest");
914
+ return result;
915
+ } finally {
916
+ if (temporaryExists) rmSync(temporaryPath, { force: true });
917
+ }
918
+ }
764
919
  function assertProfileRuntimePin(profile, runtime) {
765
920
  if (!profile.expected_runtime?.code_fingerprint || !profile.expected_runtime.code_origin) {
766
921
  throw new Error(`Profile ${profile.profile_id} must pin expected_runtime.code_fingerprint and expected_runtime.code_origin before binding or writing`);
@@ -1051,6 +1206,7 @@ async function cloneLineageProfileDatabase(sourceDatabasePath, targetSelector, r
1051
1206
  const source = new DatabaseSync(sourcePath, { readOnly: true });
1052
1207
  try {
1053
1208
  const pagesCopied = await backup(source, temporaryPath);
1209
+ chmodSync2(temporaryPath, 384);
1054
1210
  const cloned = new DatabaseSync(temporaryPath);
1055
1211
  let identity2;
1056
1212
  try {
@@ -1188,7 +1344,7 @@ function cloneLineageProfileAssets(sourceAssetRoot, targetSelector, runtime, con
1188
1344
  const destinationPath = join4(targetRoot, relativePath);
1189
1345
  mkdirSync3(dirname3(destinationPath), { recursive: true, mode: 448 });
1190
1346
  copyFileSync2(sourcePath, destinationPath, fsConstants.COPYFILE_EXCL);
1191
- chmodSync(destinationPath, 384);
1347
+ chmodSync2(destinationPath, 384);
1192
1348
  const sourceHash = fileSha2562(sourcePath);
1193
1349
  const destinationHash = fileSha2562(destinationPath);
1194
1350
  if (sourceHash !== destinationHash) throw new Error("A source asset changed or failed checksum verification during clone");
@@ -1198,7 +1354,7 @@ function cloneLineageProfileAssets(sourceAssetRoot, targetSelector, runtime, con
1198
1354
  treeHash.update(destinationHash);
1199
1355
  treeHash.update("\0");
1200
1356
  }
1201
- chmodSync(targetRoot, 448);
1357
+ chmodSync2(targetRoot, 448);
1202
1358
  const receiptPath = join4(targetRoot, ".lineage-profile-assets.json");
1203
1359
  const result = {
1204
1360
  asset_root: targetRoot,
@@ -1251,7 +1407,7 @@ function errorCode(error) {
1251
1407
  return error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
1252
1408
  }
1253
1409
  function readOwner(lockPath) {
1254
- const stat = lstatSync(lockPath);
1410
+ const stat = lstatSync2(lockPath);
1255
1411
  if (!stat.isDirectory() || stat.isSymbolicLink()) {
1256
1412
  throw new Error(`Writer lease path is not a safe directory: ${lockPath}; manual inspection is required`);
1257
1413
  }
@@ -1300,7 +1456,7 @@ function reclaimDeadOwner(lockPath, owner) {
1300
1456
  const tokenFence = createHash3("sha256").update(owner.token).digest("hex").slice(0, 24);
1301
1457
  const tombstone = `${lockPath}.stale-${owner.pid}-${tokenFence}`;
1302
1458
  try {
1303
- renameSync(lockPath, tombstone);
1459
+ renameSync2(lockPath, tombstone);
1304
1460
  } catch (error) {
1305
1461
  if (errorCode(error) === "ENOENT") return;
1306
1462
  throw error;
@@ -1492,6 +1648,10 @@ function lineageDb() {
1492
1648
  child_asset_id text not null references assets(id),
1493
1649
  relation_type text not null check (relation_type in ('derived_from')),
1494
1650
  created_at text not null,
1651
+ summary text,
1652
+ summary_created_by text check (summary_created_by in ('human', 'agent', 'system')),
1653
+ summary_updated_by text check (summary_updated_by in ('human', 'agent', 'system')),
1654
+ summary_updated_at text,
1495
1655
  unique (project_id, parent_asset_id, child_asset_id, relation_type)
1496
1656
  );
1497
1657
  create index if not exists edges_parent on asset_edges(project_id, parent_asset_id);
@@ -1775,6 +1935,7 @@ function lineageDb() {
1775
1935
  imported_asset_id text not null references assets(id),
1776
1936
  parent_asset_id text not null references assets(id),
1777
1937
  imported_at text not null,
1938
+ edge_summary text,
1778
1939
  unique(job_id, output_index),
1779
1940
  unique(job_id, file_path)
1780
1941
  );
@@ -1863,6 +2024,11 @@ function lineageDb() {
1863
2024
  );
1864
2025
  create index if not exists agent_claim_events_claim_created on agent_claim_events(claim_id, created_at);
1865
2026
  `);
2027
+ ensureColumn(database, "asset_edges", "summary", "text");
2028
+ ensureColumn(database, "asset_edges", "summary_created_by", "text check (summary_created_by in ('human', 'agent', 'system'))");
2029
+ ensureColumn(database, "asset_edges", "summary_updated_by", "text check (summary_updated_by in ('human', 'agent', 'system'))");
2030
+ ensureColumn(database, "asset_edges", "summary_updated_at", "text");
2031
+ ensureColumn(database, "generation_job_outputs", "edge_summary", "text");
1866
2032
  migrateAssetSelections(database);
1867
2033
  dropLegacyAssetSelectionRootUnique(database);
1868
2034
  ensureColumn(database, "asset_selections", "notes", "text");
@@ -2988,6 +3154,14 @@ function cancelLineageTask(project, fields) {
2988
3154
  database.close();
2989
3155
  }
2990
3156
  }
3157
+ function cancelLineageIterateTasksForAssets(project, fields) {
3158
+ const targetIds = fields.assetIds ? new Set(fields.assetIds) : void 0;
3159
+ return listLineageTasks(project, fields.rootAssetId, ["pending"]).tasks.filter((task) => task.task_type === "iterate").filter((task) => !targetIds || targetIds.has(task.target_asset_id)).map((task) => cancelLineageTask(project, {
3160
+ actor: fields.actor,
3161
+ confirmWrite: fields.confirmWrite,
3162
+ taskId: task.id
3163
+ }));
3164
+ }
2991
3165
  function resolveLineageTask(project, fields) {
2992
3166
  const normalizedProject = normalizeProject(project);
2993
3167
  const actor = normalizeActor(fields.actor, "Resolve actor");
@@ -3353,6 +3527,23 @@ function rerollRequestId(project, root, node, timestamp) {
3353
3527
  function rowString(value) {
3354
3528
  return typeof value === "string" && value.length > 0 ? value : void 0;
3355
3529
  }
3530
+ function lineageEdgeFromRow(row) {
3531
+ const summary = rowString(row.summary);
3532
+ const summaryCreatedBy = rowString(row.summary_created_by);
3533
+ const summaryUpdatedBy = rowString(row.summary_updated_by);
3534
+ const summaryUpdatedAt = rowString(row.summary_updated_at);
3535
+ return {
3536
+ id: String(row.id),
3537
+ parent_asset_id: String(row.parent_asset_id),
3538
+ child_asset_id: String(row.child_asset_id),
3539
+ relation_type: row.relation_type,
3540
+ created_at: String(row.created_at),
3541
+ ...summary ? { summary } : {},
3542
+ ...summaryCreatedBy ? { summary_created_by: summaryCreatedBy } : {},
3543
+ ...summaryUpdatedBy ? { summary_updated_by: summaryUpdatedBy } : {},
3544
+ ...summaryUpdatedAt ? { summary_updated_at: summaryUpdatedAt } : {}
3545
+ };
3546
+ }
3356
3547
  function rerollRequestFrom(row) {
3357
3548
  return {
3358
3549
  id: String(row.id),
@@ -3452,6 +3643,9 @@ function localPreviewUrl(project, localPath) {
3452
3643
  return `/api/assets/local-preview?${params.toString()}`;
3453
3644
  }
3454
3645
  function linkLineageAssets(project, fields) {
3646
+ const summary = normalizeEdgeSummary(fields.summary);
3647
+ if (summary && !fields.summaryActor) throw new LineageError("Lineage edge summary requires an explicit author");
3648
+ if (!summary && fields.summaryActor) throw new LineageError("Lineage edge summary author requires a summary");
3455
3649
  const database = lineageDb();
3456
3650
  requireAsset2(database, project, fields.parentAssetId);
3457
3651
  requireAsset2(database, project, fields.childAssetId);
@@ -3470,22 +3664,66 @@ function linkLineageAssets(project, fields) {
3470
3664
  database.close();
3471
3665
  throw error;
3472
3666
  }
3667
+ const createdAt = nowIso();
3473
3668
  const edge = {
3474
3669
  id: edgeId(project, fields.parentAssetId, fields.childAssetId),
3475
3670
  parent_asset_id: fields.parentAssetId,
3476
3671
  child_asset_id: fields.childAssetId,
3477
3672
  relation_type: "derived_from",
3478
- created_at: nowIso()
3673
+ created_at: createdAt,
3674
+ ...summary ? {
3675
+ summary,
3676
+ summary_created_by: fields.summaryActor,
3677
+ summary_updated_by: fields.summaryActor,
3678
+ summary_updated_at: createdAt
3679
+ } : {}
3479
3680
  };
3480
3681
  if (!fields.confirmWrite) {
3481
3682
  database.close();
3482
3683
  return { ok: true, dryRun: true, edge };
3483
3684
  }
3484
- database.prepare(`
3485
- insert into asset_edges (id, project_id, parent_asset_id, child_asset_id, relation_type, created_at)
3486
- values (?, ?, ?, ?, 'derived_from', ?)
3685
+ const inserted = database.prepare(`
3686
+ insert into asset_edges (
3687
+ id, project_id, parent_asset_id, child_asset_id, relation_type, created_at,
3688
+ summary, summary_created_by, summary_updated_by, summary_updated_at
3689
+ )
3690
+ values (?, ?, ?, ?, 'derived_from', ?, ?, ?, ?, ?)
3487
3691
  on conflict(project_id, parent_asset_id, child_asset_id, relation_type) do nothing
3488
- `).run(edge.id, project, edge.parent_asset_id, edge.child_asset_id, edge.created_at);
3692
+ `).run(
3693
+ edge.id,
3694
+ project,
3695
+ edge.parent_asset_id,
3696
+ edge.child_asset_id,
3697
+ edge.created_at,
3698
+ edge.summary || null,
3699
+ edge.summary_created_by || null,
3700
+ edge.summary_updated_by || null,
3701
+ edge.summary_updated_at || null
3702
+ );
3703
+ if (inserted.changes === 0) {
3704
+ const existingRow = database.prepare(`
3705
+ select id, parent_asset_id, child_asset_id, relation_type, created_at,
3706
+ summary, summary_created_by, summary_updated_by, summary_updated_at
3707
+ from asset_edges
3708
+ where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
3709
+ `).get(project, edge.parent_asset_id, edge.child_asset_id);
3710
+ if (!existingRow) {
3711
+ database.close();
3712
+ throw new LineageError("Lineage edge conflict could not be resolved", 409);
3713
+ }
3714
+ const existingEdge = lineageEdgeFromRow(existingRow);
3715
+ if (summary && (existingEdge.summary !== summary || existingEdge.summary_created_by !== fields.summaryActor || existingEdge.summary_updated_by !== fields.summaryActor || !existingEdge.summary_updated_at)) {
3716
+ database.close();
3717
+ throw new LineageError("Lineage edge already exists with a different summary or provenance", 409);
3718
+ }
3719
+ database.close();
3720
+ return {
3721
+ ok: true,
3722
+ idempotent: true,
3723
+ message: `Already linked ${existingEdge.child_asset_id} from ${existingEdge.parent_asset_id}`,
3724
+ edge: existingEdge
3725
+ };
3726
+ }
3489
3727
  database.close();
3490
3728
  return { ok: true, message: `Linked ${edge.child_asset_id} from ${edge.parent_asset_id}`, edge };
3491
3729
  }
@@ -3497,7 +3735,13 @@ function descendants(database, project, root) {
3497
3735
  const parent = queue.shift();
3498
3736
  if (seen.has(parent)) continue;
3499
3737
  seen.add(parent);
3500
- const rows = database.prepare("select id, parent_asset_id, child_asset_id, relation_type, created_at from asset_edges where project_id = ? and parent_asset_id = ? order by created_at").all(project, parent);
3738
+ const rows = database.prepare(`
3739
+ select id, parent_asset_id, child_asset_id, relation_type, created_at,
3740
+ summary, summary_created_by, summary_updated_by, summary_updated_at
3741
+ from asset_edges
3742
+ where project_id = ? and parent_asset_id = ?
3743
+ order by created_at
3744
+ `).all(project, parent).map(lineageEdgeFromRow);
3501
3745
  edges.push(...rows);
3502
3746
  queue.push(...rows.map((row) => row.child_asset_id));
3503
3747
  }
@@ -3883,7 +4127,7 @@ function lineageCommand(command, project, rootAssetId) {
3883
4127
  return `${lineagePublicPackageCommand()} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} ${lineageRuntimeSelector()} --json`;
3884
4128
  }
3885
4129
  function linkChildCommand(project, rootAssetId) {
3886
- return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write ${lineageRuntimeSelector()} --json`;
4130
+ return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --summary "<one-or-two-words>" --confirm-write ${lineageRuntimeSelector()} --json`;
3887
4131
  }
3888
4132
  function rerollImportGuidance(rootAssetId, targetAssetId) {
3889
4133
  return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
@@ -3932,6 +4176,7 @@ function getLineageBrief(project, rootAssetId) {
3932
4176
  };
3933
4177
  }
3934
4178
  function linkSelectedLineageChild(project, fields) {
4179
+ const summary = fields.summaryActor ? requireEdgeSummary(fields.summary) : fields.summary;
3935
4180
  const next = getLineageNextAsset(project, fields.rootAssetId);
3936
4181
  if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
3937
4182
  const rerollRequests = listLineageRerollRequests(project, next.root_asset_id).requests;
@@ -3960,7 +4205,9 @@ function linkSelectedLineageChild(project, fields) {
3960
4205
  childAssetId: fields.childAssetId,
3961
4206
  confirmWrite: fields.confirmWrite,
3962
4207
  claimToken: fields.claimToken,
3963
- parentAssetId: next.next_asset.asset_id
4208
+ parentAssetId: next.next_asset.asset_id,
4209
+ summary,
4210
+ summaryActor: fields.summaryActor
3964
4211
  });
3965
4212
  return {
3966
4213
  ...result,
@@ -3980,7 +4227,141 @@ function linkSelectedLineageChild(project, fields) {
3980
4227
  import { existsSync as existsSync8, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
3981
4228
  import { relative as relative3, resolve as resolve5 } from "node:path";
3982
4229
  import { randomUUID as randomUUID3 } from "node:crypto";
3983
- var adapterVersion = "generation-receipts-v1";
4230
+
4231
+ // src/shared/generationOutputManifest.ts
4232
+ var generationOutputManifestSchemaVersion = "lineage.generation_output_manifest.v1";
4233
+ var GenerationOutputManifestError = class extends Error {
4234
+ constructor(message) {
4235
+ super(message);
4236
+ this.name = "GenerationOutputManifestError";
4237
+ }
4238
+ };
4239
+ function record(value, label) {
4240
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4241
+ throw new GenerationOutputManifestError(`${label} must be an object`);
4242
+ }
4243
+ return value;
4244
+ }
4245
+ function exactKeys(value, allowed, label) {
4246
+ const allowedKeys = new Set(allowed);
4247
+ const unsupported = Object.keys(value).filter((key) => !allowedKeys.has(key)).sort();
4248
+ if (unsupported.length > 0) {
4249
+ throw new GenerationOutputManifestError(`${label} contains unsupported field: ${unsupported[0]}`);
4250
+ }
4251
+ }
4252
+ function nonEmptyText(value, label) {
4253
+ if (typeof value !== "string" || !value.trim()) throw new GenerationOutputManifestError(`${label} is required`);
4254
+ return value.trim();
4255
+ }
4256
+ function expectedGenerationOutputParents(job) {
4257
+ if (!Number.isInteger(job.expected_output_count) || job.expected_output_count <= 0) {
4258
+ throw new GenerationOutputManifestError("Generation job expected output count must be a positive integer");
4259
+ }
4260
+ const parents = job.inputs.filter((input) => input.role === "lineage_next_base").sort((left, right) => left.position - right.position);
4261
+ if (parents.length === 0) throw new GenerationOutputManifestError("Generation job has no lineage_next_base input");
4262
+ const parentIds = /* @__PURE__ */ new Set();
4263
+ const parentPositions = /* @__PURE__ */ new Set();
4264
+ for (const parent of parents) {
4265
+ if (typeof parent.asset_id !== "string" || !parent.asset_id || parent.asset_id.trim() !== parent.asset_id) {
4266
+ throw new GenerationOutputManifestError("Generation job has invalid lineage parent asset id");
4267
+ }
4268
+ if (!Number.isInteger(parent.position) || parent.position < 0 || parentPositions.has(parent.position)) {
4269
+ throw new GenerationOutputManifestError("Generation job has invalid lineage parent positions");
4270
+ }
4271
+ if (parentIds.has(parent.asset_id)) throw new GenerationOutputManifestError(`Generation job has duplicate lineage parent: ${parent.asset_id}`);
4272
+ parentIds.add(parent.asset_id);
4273
+ parentPositions.add(parent.position);
4274
+ }
4275
+ if (job.expected_output_count % parents.length !== 0) {
4276
+ throw new GenerationOutputManifestError("Generation job has invalid parent mapping");
4277
+ }
4278
+ const outputsPerParent = job.expected_output_count / parents.length;
4279
+ return Array.from({ length: job.expected_output_count }, (_value, outputIndex) => {
4280
+ return parents[Math.floor(outputIndex / outputsPerParent)].asset_id;
4281
+ });
4282
+ }
4283
+ function createGenerationOutputManifestDraft(job) {
4284
+ return {
4285
+ schema_version: generationOutputManifestSchemaVersion,
4286
+ job_id: job.id,
4287
+ outputs: expectedGenerationOutputParents(job).map((parentAssetId, outputIndex) => ({
4288
+ output_index: outputIndex,
4289
+ file_path: "",
4290
+ parent_asset_id: parentAssetId,
4291
+ edge_summary: ""
4292
+ }))
4293
+ };
4294
+ }
4295
+ function parseGenerationOutputManifest(value, job, options) {
4296
+ if (!options || typeof options.resolveFilePath !== "function") {
4297
+ throw new GenerationOutputManifestError("Generation output manifest requires a file-path resolver");
4298
+ }
4299
+ const manifest = record(value, "Generation output manifest");
4300
+ exactKeys(manifest, ["schema_version", "job_id", "outputs"], "Generation output manifest");
4301
+ if (manifest.schema_version !== generationOutputManifestSchemaVersion) {
4302
+ throw new GenerationOutputManifestError(`Generation output manifest schema_version must be ${generationOutputManifestSchemaVersion}`);
4303
+ }
4304
+ if (manifest.job_id !== job.id) throw new GenerationOutputManifestError(`Generation output manifest job_id must be ${job.id}`);
4305
+ if (!Array.isArray(manifest.outputs)) throw new GenerationOutputManifestError("Generation output manifest outputs must be an array");
4306
+ const expectedParents = expectedGenerationOutputParents(job);
4307
+ if (manifest.outputs.length !== expectedParents.length) {
4308
+ throw new GenerationOutputManifestError(`Generation output manifest requires ${expectedParents.length} outputs, received ${manifest.outputs.length}`);
4309
+ }
4310
+ const seenIndexes = /* @__PURE__ */ new Set();
4311
+ const outputs = manifest.outputs.map((value2, position) => {
4312
+ const output = record(value2, `Generation output at position ${position}`);
4313
+ exactKeys(output, ["output_index", "file_path", "parent_asset_id", "edge_summary"], `Generation output at position ${position}`);
4314
+ const outputIndex = output.output_index;
4315
+ if (!Number.isInteger(outputIndex) || Number(outputIndex) < 0) {
4316
+ throw new GenerationOutputManifestError(`Generation output at position ${position} requires a non-negative integer output_index`);
4317
+ }
4318
+ const normalizedIndex = Number(outputIndex);
4319
+ if (seenIndexes.has(normalizedIndex)) throw new GenerationOutputManifestError(`Duplicate generation output_index: ${normalizedIndex}`);
4320
+ seenIndexes.add(normalizedIndex);
4321
+ const expectedParent = expectedParents[normalizedIndex];
4322
+ if (!expectedParent) throw new GenerationOutputManifestError(`Unknown generation output_index: ${normalizedIndex}`);
4323
+ const filePath = nonEmptyText(output.file_path, `Generation output ${normalizedIndex} file_path`);
4324
+ const parentAssetId = nonEmptyText(output.parent_asset_id, `Generation output ${normalizedIndex} parent_asset_id`);
4325
+ if (parentAssetId !== expectedParent) {
4326
+ throw new GenerationOutputManifestError(`Generation output ${normalizedIndex} must use parent_asset_id ${expectedParent}`);
4327
+ }
4328
+ let edgeSummary;
4329
+ try {
4330
+ edgeSummary = requireEdgeSummary(output.edge_summary);
4331
+ } catch (error) {
4332
+ if (error instanceof EdgeSummaryValidationError) {
4333
+ throw new GenerationOutputManifestError(`Generation output ${normalizedIndex}: ${error.message}`);
4334
+ }
4335
+ throw error;
4336
+ }
4337
+ return {
4338
+ output_index: normalizedIndex,
4339
+ file_path: filePath,
4340
+ parent_asset_id: parentAssetId,
4341
+ edge_summary: edgeSummary
4342
+ };
4343
+ });
4344
+ for (const outputIndex of expectedParents.keys()) {
4345
+ if (!seenIndexes.has(outputIndex)) throw new GenerationOutputManifestError(`Missing generation output_index: ${outputIndex}`);
4346
+ }
4347
+ outputs.sort((left, right) => left.output_index - right.output_index);
4348
+ const seenPaths = /* @__PURE__ */ new Set();
4349
+ const resolvedOutputs = outputs.map((output) => {
4350
+ const filePath = nonEmptyText(options.resolveFilePath(output.file_path), `Generation output ${output.output_index} resolved file_path`);
4351
+ if (seenPaths.has(filePath)) throw new GenerationOutputManifestError(`Duplicate generation output file_path: ${filePath}`);
4352
+ seenPaths.add(filePath);
4353
+ return { ...output, file_path: filePath };
4354
+ });
4355
+ return {
4356
+ schema_version: generationOutputManifestSchemaVersion,
4357
+ job_id: job.id,
4358
+ outputs: resolvedOutputs
4359
+ };
4360
+ }
4361
+
4362
+ // src/server/generationReceipts.ts
4363
+ var legacyAdapterVersion = "generation-receipts-v1";
4364
+ var manifestAdapterVersion = "generation-receipts-v2";
3984
4365
  var provider = "codex-handoff";
3985
4366
  var GenerationReceiptError = class extends Error {
3986
4367
  constructor(message, status = 400) {
@@ -3999,6 +4380,70 @@ function parseJson(value, fallback) {
3999
4380
  if (!value) return fallback;
4000
4381
  return JSON.parse(value);
4001
4382
  }
4383
+ function positiveInteger(value) {
4384
+ return Number.isInteger(value) && Number(value) > 0;
4385
+ }
4386
+ function resolveLineageSelection(project) {
4387
+ const rootAssetId = activeLineageWorkspaceRoot(project);
4388
+ if (!rootAssetId) throw new GenerationReceiptError("No active lineage workspace for generation planning");
4389
+ const next = getLineageNextAsset(project, rootAssetId);
4390
+ if (!next.next_asset) throw new GenerationReceiptError(`No clear lineage next asset: ${next.reason}`);
4391
+ if (next.strategy !== "selected") throw new GenerationReceiptError("Generation v1 requires an explicit selected lineage next base");
4392
+ if (next.selection_mode !== "multiple" && next.selection?.asset_id !== next.next_asset.asset_id) throw new GenerationReceiptError("Generation v1 requires one explicit selected lineage next base");
4393
+ return next;
4394
+ }
4395
+ function selectedParents(next) {
4396
+ const parents = next.next_assets.length > 0 ? next.next_assets : next.next_asset ? [next.next_asset] : [];
4397
+ if (parents.length === 0) throw new GenerationReceiptError("Missing lineage next base");
4398
+ return parents;
4399
+ }
4400
+ function parentMappings(next, perBaseCount) {
4401
+ return selectedParents(next).map((parent, parentIndex) => ({
4402
+ parent,
4403
+ output_indexes: Array.from({ length: perBaseCount }, (_value, index) => parentIndex * perBaseCount + index)
4404
+ }));
4405
+ }
4406
+ function buildHandoff(project, id, prompt, count, perBaseCount, next, inputs) {
4407
+ const parent = next.next_asset;
4408
+ if (!parent) throw new GenerationReceiptError("Missing lineage next base");
4409
+ const parents = parentMappings(next, perBaseCount);
4410
+ const outputManifest = createGenerationOutputManifestDraft({ id, expected_output_count: count, inputs });
4411
+ const importCommand = `${lineagePublicPackageCommand()} generate image import --project ${quote(project)} --job-id ${quote(id)} --manifest ${quote(".asset-scratch/generation-output-manifest.json")} --confirm-write ${lineageRuntimeSelector()} --json`;
4412
+ return {
4413
+ schema_version: "lineage.generation_handoff.v2",
4414
+ provider,
4415
+ project,
4416
+ job_id: id,
4417
+ prompt,
4418
+ expected_output_count: count,
4419
+ per_base_count: next.selection_mode === "multiple" ? perBaseCount : void 0,
4420
+ lineage: {
4421
+ root_asset_id: next.root_asset_id,
4422
+ parent_asset_id: parent.asset_id,
4423
+ selection_strategy: next.strategy,
4424
+ parent_title: parent.title,
4425
+ parent_local_path: parent.local_path,
4426
+ parent_s3_key: parent.s3_key,
4427
+ parents: parents.length > 1 ? parents.map((mapping) => ({
4428
+ parent_asset_id: mapping.parent.asset_id,
4429
+ parent_title: mapping.parent.title,
4430
+ parent_local_path: mapping.parent.local_path,
4431
+ parent_s3_key: mapping.parent.s3_key,
4432
+ output_indexes: mapping.output_indexes
4433
+ })) : void 0
4434
+ },
4435
+ instructions: [
4436
+ "Use Codex image generation outside Lineage server code.",
4437
+ "Write generated output files under .asset-scratch before import.",
4438
+ "Do not call live provider APIs from the CLI or server.",
4439
+ "Fill every output_manifest entry with its generated file path and a one- or two-word edge summary.",
4440
+ "Import the completed manifest with --confirm-write to persist every lineage child."
4441
+ ],
4442
+ import_command: importCommand,
4443
+ output_manifest: outputManifest,
4444
+ guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
4445
+ };
4446
+ }
4002
4447
  function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
4003
4448
  const importCommand = `${lineagePublicPackageCommand()} reroll import --project ${quote(project)} --job-id ${quote(id)} --file <.asset-scratch-file> --confirm-write ${lineageRuntimeSelector()} --json`;
4004
4449
  return {
@@ -4027,6 +4472,19 @@ function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
4027
4472
  guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
4028
4473
  };
4029
4474
  }
4475
+ function inputsFrom(jobIdValue, project, next) {
4476
+ return selectedParents(next).map((parent, position) => ({
4477
+ id: `${jobIdValue}:input:${position}`,
4478
+ job_id: jobIdValue,
4479
+ project_id: project,
4480
+ asset_id: parent.asset_id,
4481
+ root_asset_id: next.root_asset_id,
4482
+ role: "lineage_next_base",
4483
+ position,
4484
+ selection_strategy: next.strategy,
4485
+ selection_snapshot: next
4486
+ }));
4487
+ }
4030
4488
  function receiptFrom(row) {
4031
4489
  return {
4032
4490
  id: String(row.id),
@@ -4039,6 +4497,7 @@ function receiptFrom(row) {
4039
4497
  };
4040
4498
  }
4041
4499
  function outputFrom(row) {
4500
+ const edgeSummary = typeof row.edge_summary === "string" && row.edge_summary.length > 0 ? row.edge_summary : void 0;
4042
4501
  return {
4043
4502
  id: String(row.id),
4044
4503
  job_id: String(row.job_id),
@@ -4050,7 +4509,8 @@ function outputFrom(row) {
4050
4509
  content_type: String(row.content_type),
4051
4510
  imported_asset_id: String(row.imported_asset_id),
4052
4511
  parent_asset_id: String(row.parent_asset_id),
4053
- imported_at: String(row.imported_at)
4512
+ imported_at: String(row.imported_at),
4513
+ ...edgeSummary ? { edge_summary: edgeSummary } : {}
4054
4514
  };
4055
4515
  }
4056
4516
  function loadGenerationJob(database, project, id) {
@@ -4096,6 +4556,72 @@ function insertReceipt(database, id, type, command, payload) {
4096
4556
  values (?, ?, ?, 'ok', ?, ?, ?)
4097
4557
  `).run(`${id}:receipt:${type}:${Date.now()}`, id, type, command, JSON.stringify(payload), nowIso());
4098
4558
  }
4559
+ function planImageGeneration(project = defaultProject, fields) {
4560
+ const prompt = fields.prompt.trim();
4561
+ if (!prompt) throw new GenerationReceiptError("Missing --prompt");
4562
+ if (!fields.fromLineageSelection) throw new GenerationReceiptError("Generation v1 requires --from-lineage-selection");
4563
+ const next = resolveLineageSelection(project);
4564
+ const parentCount = selectedParents(next).length;
4565
+ if (parentCount > 1 && !positiveInteger(fields.perBaseCount)) throw new GenerationReceiptError("Multi-parent generation requires --per-base-count");
4566
+ const perBaseCount = parentCount > 1 ? Number(fields.perBaseCount) : Number(fields.count ?? fields.perBaseCount);
4567
+ const count = parentCount * perBaseCount;
4568
+ if (!positiveInteger(perBaseCount)) throw new GenerationReceiptError("Generation count must be a positive integer");
4569
+ if (fields.count !== void 0 && fields.count !== count) throw new GenerationReceiptError(`Generation count mismatch: expected ${count} from selected bases, received ${fields.count}`);
4570
+ const id = jobId();
4571
+ const inputs = inputsFrom(id, project, next);
4572
+ const handoff = buildHandoff(project, id, prompt, count, perBaseCount, next, inputs);
4573
+ const mappings = parentMappings(next, perBaseCount).map((mapping) => ({ parent_asset_id: mapping.parent.asset_id, output_indexes: mapping.output_indexes }));
4574
+ const timestamp = nowIso();
4575
+ const preview2 = {
4576
+ id,
4577
+ project_id: project,
4578
+ provider,
4579
+ adapter_version: manifestAdapterVersion,
4580
+ source_mode: "lineage_selection",
4581
+ root_asset_id: next.root_asset_id,
4582
+ prompt,
4583
+ expected_output_count: count,
4584
+ status: "planned",
4585
+ output_dir: ".asset-scratch",
4586
+ handoff,
4587
+ created_at: timestamp,
4588
+ updated_at: timestamp,
4589
+ inputs,
4590
+ outputs: [],
4591
+ receipts: [{
4592
+ id: `${id}:receipt:plan:preview`,
4593
+ job_id: id,
4594
+ receipt_type: "plan",
4595
+ status: "ok",
4596
+ command: "generate image plan",
4597
+ payload: { prompt, expected_output_count: count, per_base_count: parentCount > 1 ? perBaseCount : void 0, lineage: handoff.lineage, parent_mappings: mappings },
4598
+ created_at: timestamp
4599
+ }]
4600
+ };
4601
+ if (fields.dryRun) return { ok: true, command: "generate image plan", project, dryRun: true, wouldWrite: true, job: preview2 };
4602
+ const database = lineageDb();
4603
+ try {
4604
+ database.exec("BEGIN IMMEDIATE");
4605
+ try {
4606
+ database.prepare(`
4607
+ insert into generation_jobs (
4608
+ id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
4609
+ expected_output_count, status, output_dir, handoff_json, created_at, updated_at
4610
+ ) values (?, ?, ?, ?, 'lineage_selection', ?, ?, ?, 'planned', ?, ?, ?, ?)
4611
+ `).run(id, project, provider, manifestAdapterVersion, next.root_asset_id, prompt, count, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
4612
+ const insertInput = database.prepare("insert into generation_job_inputs (id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json) values (?, ?, ?, ?, ?, ?, ?, ?, ?)");
4613
+ for (const input of inputs) insertInput.run(input.id, id, project, input.asset_id, input.root_asset_id, input.role, input.position, input.selection_strategy, JSON.stringify(next));
4614
+ insertReceipt(database, id, "plan", "generate image plan", preview2.receipts[0].payload);
4615
+ database.exec("COMMIT");
4616
+ } catch (error) {
4617
+ database.exec("ROLLBACK");
4618
+ throw error;
4619
+ }
4620
+ return { ok: true, command: "generate image plan", project, job: loadGenerationJob(database, project, id) };
4621
+ } finally {
4622
+ database.close();
4623
+ }
4624
+ }
4099
4625
  function planImageReroll(project = defaultProject, fields) {
4100
4626
  const prompt = fields.prompt.trim();
4101
4627
  if (!prompt) throw new GenerationReceiptError("Missing --prompt");
@@ -4140,7 +4666,7 @@ function planImageReroll(project = defaultProject, fields) {
4140
4666
  id,
4141
4667
  project_id: project,
4142
4668
  provider,
4143
- adapter_version: adapterVersion,
4669
+ adapter_version: legacyAdapterVersion,
4144
4670
  source_mode: "lineage_reroll",
4145
4671
  root_asset_id: snapshot.root_asset_id,
4146
4672
  prompt,
@@ -4172,7 +4698,7 @@ function planImageReroll(project = defaultProject, fields) {
4172
4698
  id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
4173
4699
  expected_output_count, status, output_dir, handoff_json, created_at, updated_at
4174
4700
  ) values (?, ?, ?, ?, 'lineage_reroll', ?, ?, 1, 'planned', ?, ?, ?, ?)
4175
- `).run(id, project, provider, adapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
4701
+ `).run(id, project, provider, legacyAdapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
4176
4702
  database.prepare("insert into generation_job_inputs (id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json) values (?, ?, ?, ?, ?, ?, ?, ?, ?)").run(input.id, id, project, input.asset_id, input.root_asset_id, input.role, input.position, input.selection_strategy, JSON.stringify(input.selection_snapshot));
4177
4703
  insertReceipt(database, id, "plan", "reroll plan", preview2.receipts[0].payload);
4178
4704
  database.exec("COMMIT");
@@ -4185,14 +4711,66 @@ function planImageReroll(project = defaultProject, fields) {
4185
4711
  database.close();
4186
4712
  }
4187
4713
  }
4714
+ function parentForOutput(job, outputIndex) {
4715
+ const inputs = job.inputs.filter((input) => input.role === "lineage_next_base");
4716
+ if (inputs.length === 0) throw new GenerationReceiptError("Generation job has no lineage_next_base input");
4717
+ if (inputs.length === 1) return inputs[0].asset_id;
4718
+ if (job.expected_output_count % inputs.length !== 0) throw new GenerationReceiptError("Generation job has invalid parent mapping");
4719
+ return inputs[Math.floor(outputIndex / (job.expected_output_count / inputs.length))]?.asset_id || inputs[inputs.length - 1].asset_id;
4720
+ }
4721
+ function parentInputs(job) {
4722
+ const inputs = job.inputs.filter((input) => input.role === "lineage_next_base");
4723
+ if (inputs.length === 0) throw new GenerationReceiptError("Generation job has no lineage_next_base input");
4724
+ return inputs;
4725
+ }
4726
+ function parentFilesFor(job, parentFiles) {
4727
+ const inputs = parentInputs(job);
4728
+ const expectedPerParent = job.expected_output_count / inputs.length;
4729
+ if (!Number.isInteger(expectedPerParent)) throw new GenerationReceiptError("Generation job has invalid parent mapping");
4730
+ const allowedParents = new Set(inputs.map((input) => input.asset_id));
4731
+ const seenParents = /* @__PURE__ */ new Set();
4732
+ const mapped = [];
4733
+ for (const parentAssetId of Object.keys(parentFiles)) {
4734
+ if (!allowedParents.has(parentAssetId)) throw new GenerationReceiptError(`Unknown generation parent mapping: ${parentAssetId}`);
4735
+ if (seenParents.has(parentAssetId)) throw new GenerationReceiptError(`Duplicate generation parent mapping: ${parentAssetId}`);
4736
+ seenParents.add(parentAssetId);
4737
+ }
4738
+ for (const input of inputs) {
4739
+ const files = (parentFiles[input.asset_id] || []).map((file) => file.trim()).filter(Boolean);
4740
+ if (files.length === 0) throw new GenerationReceiptError(`Missing generation parent mapping for ${input.asset_id}`);
4741
+ if (files.length !== expectedPerParent) throw new GenerationReceiptError(`Parent ${input.asset_id} requires ${expectedPerParent} output file${expectedPerParent === 1 ? "" : "s"}, received ${files.length}`);
4742
+ for (const file of files) mapped.push({ file, parentAssetId: input.asset_id });
4743
+ }
4744
+ return mapped;
4745
+ }
4746
+ function orderedFilesFor(job, files) {
4747
+ return files.map((file, index) => ({ file, parentAssetId: parentForOutput(job, index) }));
4748
+ }
4749
+ function inspectImageGeneration(project = defaultProject, jobIdValue) {
4750
+ if (!jobIdValue) throw new GenerationReceiptError("Missing --job-id");
4751
+ const database = lineageDb();
4752
+ try {
4753
+ return { ok: true, command: "generate image inspect", project, job: loadGenerationJob(database, project, jobIdValue) };
4754
+ } finally {
4755
+ database.close();
4756
+ }
4757
+ }
4188
4758
  function isPathInside(child, parent) {
4189
4759
  const rel = relative3(parent, child);
4190
4760
  return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
4191
4761
  }
4192
- function resolveScratchFile(file) {
4762
+ function scratchCandidate(file) {
4193
4763
  const scratchRoot = resolve5(repoRoot, ".asset-scratch");
4194
4764
  const candidate = file.startsWith(".asset-scratch/") || resolve5(file).startsWith(scratchRoot) ? resolve5(repoRoot, file) : resolve5(scratchRoot, file);
4195
4765
  if (!isPathInside(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
4766
+ return { candidate, scratchRoot };
4767
+ }
4768
+ function resolveScratchManifestPath(file) {
4769
+ const { candidate, scratchRoot } = scratchCandidate(file);
4770
+ return relative3(scratchRoot, candidate);
4771
+ }
4772
+ function resolveScratchFile(file) {
4773
+ const { candidate, scratchRoot } = scratchCandidate(file);
4196
4774
  if (!existsSync8(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
4197
4775
  const realScratchRoot = realpathSync2(scratchRoot);
4198
4776
  const realCandidate = realpathSync2(candidate);
@@ -4208,6 +4786,165 @@ function resolveScratchFile(file) {
4208
4786
  assetId: `local-${checksum.slice(0, 12)}`
4209
4787
  };
4210
4788
  }
4789
+ function generationManifestConflict() {
4790
+ throw new GenerationReceiptError("Generation import already exists with different output, summary, or provenance", 409);
4791
+ }
4792
+ function confirmManifestRetry(project, job, manifest) {
4793
+ if (job.outputs.length !== manifest.outputs.length) generationManifestConflict();
4794
+ const database = lineageDb();
4795
+ try {
4796
+ for (const expected of manifest.outputs) {
4797
+ const recorded = job.outputs.find((output) => output.output_index === expected.output_index);
4798
+ if (!recorded || recorded.file_path !== expected.file_path || recorded.parent_asset_id !== expected.parent_asset_id || recorded.edge_summary !== expected.edge_summary) generationManifestConflict();
4799
+ const edge = database.prepare(`
4800
+ select summary, summary_created_by, summary_updated_by, summary_updated_at
4801
+ from asset_edges
4802
+ where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
4803
+ `).get(project, recorded.parent_asset_id, recorded.imported_asset_id);
4804
+ if (!edge || edge.summary !== expected.edge_summary || edge.summary_created_by !== "agent" || edge.summary_updated_by !== "agent" || typeof edge.summary_updated_at !== "string" || edge.summary_updated_at.length === 0) generationManifestConflict();
4805
+ }
4806
+ return { ok: true, command: "generate image import", project, job, imported: job.outputs, idempotent: true };
4807
+ } finally {
4808
+ database.close();
4809
+ }
4810
+ }
4811
+ function importImageGenerationOutputs(project = defaultProject, fields) {
4812
+ if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
4813
+ if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
4814
+ const database = lineageDb();
4815
+ let job;
4816
+ try {
4817
+ job = loadGenerationJob(database, project, fields.jobId);
4818
+ } finally {
4819
+ database.close();
4820
+ }
4821
+ if (job.source_mode !== "lineage_selection") throw new GenerationReceiptError(`Generation job is not an image-selection job: ${job.source_mode}`);
4822
+ const hasManifestInput = fields.manifest !== void 0;
4823
+ const hasParentFilesInput = fields.parentFiles !== void 0;
4824
+ const hasFilesInput = fields.files !== void 0;
4825
+ if (hasManifestInput && (hasParentFilesInput || hasFilesInput)) {
4826
+ throw new GenerationReceiptError("Use --manifest or legacy --files/--parent-files, not both");
4827
+ }
4828
+ if (hasParentFilesInput && hasFilesInput) throw new GenerationReceiptError("Use --files or --parent-files, not both");
4829
+ let manifest;
4830
+ let parentFileRows;
4831
+ let mappingStrategy;
4832
+ if (job.adapter_version === manifestAdapterVersion) {
4833
+ if (!hasManifestInput) throw new GenerationReceiptError("New generation jobs require --manifest");
4834
+ manifest = parseGenerationOutputManifest(fields.manifest, job, { resolveFilePath: resolveScratchManifestPath });
4835
+ if (job.status === "imported") return confirmManifestRetry(project, job, manifest);
4836
+ if (job.status !== "planned") throw new GenerationReceiptError(`Generation job is not importable from status: ${job.status}`);
4837
+ parentFileRows = manifest.outputs.map((output) => ({
4838
+ edgeSummary: output.edge_summary,
4839
+ file: output.file_path,
4840
+ parentAssetId: output.parent_asset_id
4841
+ }));
4842
+ mappingStrategy = "generation_output_manifest_v1";
4843
+ } else if (job.adapter_version === legacyAdapterVersion) {
4844
+ if (hasManifestInput) throw new GenerationReceiptError("Already-planned legacy generation jobs require --files or --parent-files");
4845
+ if (job.status !== "planned") throw new GenerationReceiptError(`Generation job is not importable from status: ${job.status}`);
4846
+ const hasExplicitParentFiles = Boolean(fields.parentFiles && Object.keys(fields.parentFiles).length > 0);
4847
+ parentFileRows = hasExplicitParentFiles ? parentFilesFor(job, fields.parentFiles || {}) : orderedFilesFor(job, (fields.files || []).map((file) => file.trim()).filter(Boolean));
4848
+ mappingStrategy = hasExplicitParentFiles ? "explicit_parent_files" : "ordered_per_base";
4849
+ } else {
4850
+ throw new GenerationReceiptError(`Unsupported generation adapter version: ${job.adapter_version}`);
4851
+ }
4852
+ if (parentFileRows.length === 0) throw new GenerationReceiptError("Generation import requires --files or --parent-files");
4853
+ if (parentFileRows.length !== job.expected_output_count) {
4854
+ throw new GenerationReceiptError(`Output count mismatch: expected ${job.expected_output_count}, received ${parentFileRows.length}`);
4855
+ }
4856
+ const resolved = parentFileRows.map((row) => ({ ...resolveScratchFile(row.file), edgeSummary: row.edgeSummary, parentAssetId: row.parentAssetId }));
4857
+ const uniquePaths = new Set(resolved.map((file) => file.relativePath));
4858
+ if (uniquePaths.size !== resolved.length) throw new GenerationReceiptError("Generation import files must be unique");
4859
+ cancelLineageIterateTasksForAssets(project, {
4860
+ actor: "system",
4861
+ confirmWrite: false,
4862
+ rootAssetId: job.root_asset_id
4863
+ });
4864
+ indexLineageAssets(project);
4865
+ const writeDb = lineageDb();
4866
+ try {
4867
+ const timestamp = nowIso();
4868
+ writeDb.exec("BEGIN IMMEDIATE");
4869
+ try {
4870
+ for (const [index, file] of resolved.entries()) {
4871
+ const assetRow = writeDb.prepare("select id from assets where project_id = ? and id = ?").get(project, file.assetId);
4872
+ if (!assetRow) throw new GenerationReceiptError(`Indexed local asset was not found: ${file.relativePath}`);
4873
+ const outputId = `${fields.jobId}:output:${index}`;
4874
+ writeDb.prepare(`insert into generation_job_outputs (
4875
+ id, job_id, project_id, output_index, file_path, checksum_sha256, size_bytes, content_type,
4876
+ imported_asset_id, parent_asset_id, imported_at, edge_summary
4877
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
4878
+ outputId,
4879
+ fields.jobId,
4880
+ project,
4881
+ index,
4882
+ file.relativePath,
4883
+ file.checksum,
4884
+ file.size,
4885
+ file.contentType,
4886
+ file.assetId,
4887
+ file.parentAssetId,
4888
+ timestamp,
4889
+ file.edgeSummary || null
4890
+ );
4891
+ const insertedEdge = writeDb.prepare(`insert into asset_edges (
4892
+ id, project_id, parent_asset_id, child_asset_id, relation_type, created_at,
4893
+ summary, summary_created_by, summary_updated_by, summary_updated_at
4894
+ ) values (?, ?, ?, ?, 'derived_from', ?, ?, ?, ?, ?)
4895
+ on conflict(project_id, parent_asset_id, child_asset_id, relation_type) do nothing`).run(
4896
+ `${project}:${file.parentAssetId}:derived_from:${file.assetId}`,
4897
+ project,
4898
+ file.parentAssetId,
4899
+ file.assetId,
4900
+ timestamp,
4901
+ file.edgeSummary || null,
4902
+ file.edgeSummary ? "agent" : null,
4903
+ file.edgeSummary ? "agent" : null,
4904
+ file.edgeSummary ? timestamp : null
4905
+ );
4906
+ if (file.edgeSummary && insertedEdge.changes === 0) {
4907
+ const edge = writeDb.prepare(`
4908
+ select summary, summary_created_by, summary_updated_by, summary_updated_at
4909
+ from asset_edges
4910
+ where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
4911
+ `).get(project, file.parentAssetId, file.assetId);
4912
+ if (!edge || edge.summary !== file.edgeSummary || edge.summary_created_by !== "agent" || edge.summary_updated_by !== "agent" || typeof edge.summary_updated_at !== "string" || edge.summary_updated_at.length === 0) generationManifestConflict();
4913
+ }
4914
+ }
4915
+ writeDb.prepare(`
4916
+ update generation_jobs
4917
+ set status = 'imported', imported_at = ?, updated_at = ?
4918
+ where project_id = ? and id = ?
4919
+ `).run(timestamp, timestamp, project, fields.jobId);
4920
+ insertReceipt(writeDb, fields.jobId, "import", "generate image import", {
4921
+ mapping_strategy: mappingStrategy,
4922
+ files: resolved.map((file, index) => ({
4923
+ output_index: index,
4924
+ file_path: file.relativePath,
4925
+ imported_asset_id: file.assetId,
4926
+ parent_asset_id: file.parentAssetId,
4927
+ ...file.edgeSummary ? { edge_summary: file.edgeSummary } : {}
4928
+ })),
4929
+ selection_reset: { root_asset_id: job.root_asset_id, cleared: true }
4930
+ });
4931
+ writeDb.prepare("delete from asset_selections where project_id = ? and root_asset_id = ?").run(project, job.root_asset_id);
4932
+ writeDb.exec("COMMIT");
4933
+ } catch (error) {
4934
+ writeDb.exec("ROLLBACK");
4935
+ throw error;
4936
+ }
4937
+ cancelLineageIterateTasksForAssets(project, {
4938
+ actor: "system",
4939
+ confirmWrite: true,
4940
+ rootAssetId: job.root_asset_id
4941
+ });
4942
+ const importedJob = loadGenerationJob(writeDb, project, fields.jobId);
4943
+ return { ok: true, command: "generate image import", project, job: importedJob, imported: importedJob.outputs };
4944
+ } finally {
4945
+ writeDb.close();
4946
+ }
4947
+ }
4211
4948
  function importImageRerollOutput(project = defaultProject, fields) {
4212
4949
  if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
4213
4950
  if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
@@ -4783,7 +5520,7 @@ function getLineageSelectionPacket(project, options = {}) {
4783
5520
  import { createHash as createHash6 } from "node:crypto";
4784
5521
  import { spawnSync as spawnSync2 } from "node:child_process";
4785
5522
  import { createRequire as createRequire4 } from "node:module";
4786
- import { existsSync as existsSync10, lstatSync as lstatSync2, readFileSync as readFileSync5, readdirSync as readdirSync3, readlinkSync, realpathSync as realpathSync3, statSync as statSync6 } from "node:fs";
5523
+ import { existsSync as existsSync10, lstatSync as lstatSync3, readFileSync as readFileSync5, readdirSync as readdirSync3, readlinkSync, realpathSync as realpathSync3, statSync as statSync6 } from "node:fs";
4787
5524
  import { dirname as dirname5, isAbsolute as isAbsolute3, join as join9, resolve as resolve7 } from "node:path";
4788
5525
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4789
5526
 
@@ -4810,7 +5547,7 @@ function executingCodeRoot() {
4810
5547
  return canonicalRoot(root);
4811
5548
  }
4812
5549
  var codeRoot = executingCodeRoot();
4813
- function sha256(value) {
5550
+ function sha2562(value) {
4814
5551
  return createHash6("sha256").update(value).digest("hex");
4815
5552
  }
4816
5553
  function canonicalRoot(root) {
@@ -4849,7 +5586,7 @@ function untrackedFingerprint(root, status) {
4849
5586
  hash.update(relativePath);
4850
5587
  const path = join9(root, relativePath);
4851
5588
  try {
4852
- const stat = lstatSync2(path);
5589
+ const stat = lstatSync3(path);
4853
5590
  if (stat.isSymbolicLink()) hash.update(readlinkSync(path));
4854
5591
  else if (stat.isFile()) hash.update(readFileSync5(path));
4855
5592
  else hash.update(`[${stat.mode}:${stat.size}]`);
@@ -4871,12 +5608,12 @@ function checkoutCodeIdentity(root, channel, version) {
4871
5608
  if (status === void 0 || diff === void 0) errors.push("Checkout dirty state could not be inspected");
4872
5609
  if (channel !== "dev") errors.push(`Checkout code may run only as dev, not ${channel}`);
4873
5610
  const dirty = Boolean(status);
4874
- const sourceFingerprint = sha256(`${gitSha || "unknown"}\0${sha256(diff || "")}\0${untrackedFingerprint(root, status || "")}`);
5611
+ const sourceFingerprint = sha2562(`${gitSha || "unknown"}\0${sha2562(diff || "")}\0${untrackedFingerprint(root, status || "")}`);
4875
5612
  return {
4876
5613
  channel,
4877
5614
  dirty,
4878
5615
  errors,
4879
- fingerprint: sha256(JSON.stringify({ channel, dirty, git_sha: gitSha, origin: "checkout", root, source_fingerprint: sourceFingerprint, version })),
5616
+ fingerprint: sha2562(JSON.stringify({ channel, dirty, git_sha: gitSha, origin: "checkout", root, source_fingerprint: sourceFingerprint, version })),
4880
5617
  git_sha: gitSha,
4881
5618
  origin: "checkout",
4882
5619
  package_version: version,
@@ -4886,7 +5623,7 @@ function checkoutCodeIdentity(root, channel, version) {
4886
5623
  };
4887
5624
  }
4888
5625
  function buildFingerprint(build) {
4889
- return sha256(JSON.stringify({
5626
+ return sha2562(JSON.stringify({
4890
5627
  package_name: build.package_name,
4891
5628
  package_version: build.package_version,
4892
5629
  schema_version: build.schema_version,
@@ -4986,7 +5723,7 @@ function packageCodeIdentity(root, channel, info, receiptPath) {
4986
5723
  channel,
4987
5724
  dirty: build?.source_dirty,
4988
5725
  errors,
4989
- fingerprint: sha256(JSON.stringify({
5726
+ fingerprint: sha2562(JSON.stringify({
4990
5727
  build_fingerprint: build?.build_fingerprint,
4991
5728
  channel,
4992
5729
  install_integrity: install?.package_integrity,
@@ -5014,7 +5751,7 @@ function getLineageCodeIdentity(channel, options = {}) {
5014
5751
  return {
5015
5752
  channel,
5016
5753
  errors,
5017
- fingerprint: sha256(JSON.stringify({ channel, origin: "unknown", root, version: info.version })),
5754
+ fingerprint: sha2562(JSON.stringify({ channel, origin: "unknown", root, version: info.version })),
5018
5755
  origin: "unknown",
5019
5756
  package_version: info.version,
5020
5757
  root,
@@ -5111,22 +5848,23 @@ var ManagedWriterRoutingError = class extends Error {
5111
5848
  status;
5112
5849
  };
5113
5850
  function managedWriterTimeoutMs(command, args) {
5114
- const subcommand = args.find((arg) => !arg.startsWith("-")) || "";
5115
- return command === "reroll" && subcommand === "import" ? 5 * 6e4 : 3e4;
5851
+ const positions = args.filter((arg) => !arg.startsWith("-"));
5852
+ const slowImport = command === "reroll" && positions[0] === "import" || command === "generate" && positions[0] === "image" && positions[1] === "import";
5853
+ return slowImport ? 5 * 6e4 : 3e4;
5116
5854
  }
5117
5855
  function errorMessage(body, fallback) {
5118
5856
  if (!body || typeof body !== "object" || Array.isArray(body)) return fallback;
5119
- const record = body;
5120
- if (typeof record.message === "string") return record.message;
5121
- if (typeof record.error === "string") return record.error;
5857
+ const record2 = body;
5858
+ if (typeof record2.message === "string") return record2.message;
5859
+ if (typeof record2.error === "string") return record2.error;
5122
5860
  return fallback;
5123
5861
  }
5124
5862
  function agentClaimError(body, status) {
5125
5863
  if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
5126
- const record = body;
5127
- if (typeof record.message !== "string" || typeof record.error !== "string") return void 0;
5128
- const conflicts = Array.isArray(record.conflicts) ? record.conflicts : [];
5129
- return new AgentClaimError(record.message, status, record.error, conflicts);
5864
+ const record2 = body;
5865
+ if (typeof record2.message !== "string" || typeof record2.error !== "string") return void 0;
5866
+ const conflicts = Array.isArray(record2.conflicts) ? record2.conflicts : [];
5867
+ return new AgentClaimError(record2.message, status, record2.error, conflicts);
5130
5868
  }
5131
5869
  function assertResponseIdentity(body, profile, channel) {
5132
5870
  if (!body || typeof body !== "object" || Array.isArray(body)) {
@@ -5224,6 +5962,26 @@ function readOptions(args, name) {
5224
5962
  }
5225
5963
  return values;
5226
5964
  }
5965
+ function readJsonFile(path, label) {
5966
+ try {
5967
+ return JSON.parse(readFileSync6(resolve8(path), "utf8"));
5968
+ } catch (error) {
5969
+ throw new Error(`${label} must be a readable JSON file: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
5970
+ }
5971
+ }
5972
+ function parentFilesOption(value) {
5973
+ if (value === void 0) return void 0;
5974
+ const mappings = {};
5975
+ for (const rawMapping of value.split(";")) {
5976
+ const separator = rawMapping.indexOf("=");
5977
+ const parent = separator >= 0 ? rawMapping.slice(0, separator).trim() : "";
5978
+ const files = separator >= 0 ? rawMapping.slice(separator + 1).split(",").map((file) => file.trim()).filter(Boolean) : [];
5979
+ if (!parent || files.length === 0) throw new Error("lineage generate image import --parent-files requires parent=file[,file][;parent=file]");
5980
+ if (Object.hasOwn(mappings, parent)) throw new Error(`Duplicate generation parent mapping: ${parent}`);
5981
+ mappings[parent] = files;
5982
+ }
5983
+ return mappings;
5984
+ }
5227
5985
  function resolveStartOptions(config, args) {
5228
5986
  const profile = prepareCliProfile(config, args);
5229
5987
  const serviceUrl = profile ? new URL(profile.service_origin) : void 0;
@@ -5309,13 +6067,18 @@ Usage:
5309
6067
  ${config.binName} profile bind --profile <id-or-manifest> --confirm-write [--json]
5310
6068
  ${config.binName} profile clone --source-db <snapshot-source> --target-profile <id-or-manifest> --confirm-write [--json]
5311
6069
  ${config.binName} profile clone-assets --source-asset-root <path> --target-profile <id-or-manifest> --confirm-write [--json]
6070
+ ${config.binName} profile repin-runtime --profile <development-profile> --checkout-root <path> --confirm-write [--json]
5312
6071
  ${config.binName} runtime info [--json]
5313
6072
  ${config.binName} runtime doctor [--json]
5314
6073
  ${config.binName} next [--project <project>] [--root <asset-id>] [--db <path>] [--json]
5315
6074
  ${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
5316
6075
  ${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
5317
6076
  ${config.binName} selection packet [--project <project>] [--workspace <id-or-root>|--root <asset-id>] [--channel <channel>] [--campaign <campaign>] [--context-notes <text>] [--label <label>] [--schema v2] [--out <path>] [--strict] [--db <path>] [--json]
5318
- ${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
6077
+ ${config.binName} link-child --root <asset-id> --child <asset-id> --summary "<one-or-two-words>" [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
6078
+ ${config.binName} generate image plan --prompt <text> --from-lineage-selection [--count <count>|--per-base-count <count>] [--project <project>] [--dry-run] [--db <path>] [--json]
6079
+ ${config.binName} generate image inspect --job-id <job-id> [--project <project>] [--db <path>] [--json]
6080
+ ${config.binName} generate image import --job-id <job-id> --manifest <json-file> --confirm-write [--project <project>] [--db <path>] [--json]
6081
+ ${config.binName} generate image import --job-id <legacy-job-id> (--files <file,file>|--parent-files <parent=file;parent=file>) --confirm-write [--project <project>] [--db <path>] [--json]
5319
6082
  ${config.binName} reroll list --root <asset-id> [--project <project>] [--db <path>] [--json]
5320
6083
  ${config.binName} reroll mark --root <asset-id> --target <asset-id> [--notes <text>] [--requested-by agent|human|system] [--project <project>] [--confirm-write] [--db <path>] [--json]
5321
6084
  ${config.binName} reroll cancel --root <asset-id> --target <asset-id> [--project <project>] [--confirm-write] [--db <path>] [--json]
@@ -5437,7 +6200,8 @@ function resolveDataCommandOptions(args) {
5437
6200
  dbPath: readOption(args, "--db"),
5438
6201
  json: args.includes("--json"),
5439
6202
  project: readOption(args, "--project") || process.env.LINEAGE_DEFAULT_PRODUCT || defaultProduct,
5440
- rootAssetId: readOption(args, "--root")
6203
+ rootAssetId: readOption(args, "--root"),
6204
+ summary: readOption(args, "--summary")
5441
6205
  };
5442
6206
  if (options.dbPath) process.env.LINEAGE_DB = options.dbPath;
5443
6207
  return options;
@@ -5480,13 +6244,57 @@ function runLineageDataCommand(command, args) {
5480
6244
  }
5481
6245
  if (command === "link-child") {
5482
6246
  if (!options.childAssetId) throw new Error("lineage link-child requires --child");
6247
+ const summary = requireEdgeSummary(options.summary);
5483
6248
  return linkSelectedLineageChild(options.project, {
5484
6249
  childAssetId: options.childAssetId,
5485
6250
  claimToken: options.claimToken,
5486
6251
  confirmWrite: options.confirmWrite,
5487
- rootAssetId: options.rootAssetId || options.assetId
6252
+ rootAssetId: options.rootAssetId || options.assetId,
6253
+ summary,
6254
+ summaryActor: "agent"
5488
6255
  });
5489
6256
  }
6257
+ if (command === "generate") {
6258
+ const [resource, subcommand] = positionalArgs(args);
6259
+ if (resource !== "image") throw new Error(`Unknown generate command: ${resource}`);
6260
+ if (subcommand === "plan") {
6261
+ const prompt = readOption(args, "--prompt");
6262
+ const rawCount = readOption(args, "--count");
6263
+ const rawPerBaseCount = readOption(args, "--per-base-count");
6264
+ if (!prompt) throw new Error("lineage generate image plan requires --prompt");
6265
+ return planImageGeneration(options.project, {
6266
+ count: rawCount === void 0 ? void 0 : Number(rawCount),
6267
+ dryRun: args.includes("--dry-run"),
6268
+ fromLineageSelection: args.includes("--from-lineage-selection"),
6269
+ perBaseCount: rawPerBaseCount === void 0 ? void 0 : Number(rawPerBaseCount),
6270
+ prompt
6271
+ });
6272
+ }
6273
+ if (subcommand === "inspect") {
6274
+ const jobId2 = readOption(args, "--job-id");
6275
+ if (!jobId2) throw new Error("lineage generate image inspect requires --job-id");
6276
+ return inspectImageGeneration(options.project, jobId2);
6277
+ }
6278
+ if (subcommand === "import") {
6279
+ const jobId2 = readOption(args, "--job-id");
6280
+ const manifestPath = readOption(args, "--manifest");
6281
+ const filesValue = readOption(args, "--files");
6282
+ const parentFilesValue = readOption(args, "--parent-files");
6283
+ if (!jobId2) throw new Error("lineage generate image import requires --job-id");
6284
+ if (manifestPath !== void 0 && (filesValue !== void 0 || parentFilesValue !== void 0)) {
6285
+ throw new Error("Use --manifest or legacy --files/--parent-files, not both");
6286
+ }
6287
+ if (filesValue !== void 0 && parentFilesValue !== void 0) throw new Error("Use --files or --parent-files, not both");
6288
+ return importImageGenerationOutputs(options.project, {
6289
+ confirmWrite: options.confirmWrite,
6290
+ files: filesValue === void 0 ? void 0 : filesValue.split(",").map((file) => file.trim()).filter(Boolean),
6291
+ jobId: jobId2,
6292
+ manifest: manifestPath === void 0 ? void 0 : readJsonFile(manifestPath, "Generation output manifest"),
6293
+ parentFiles: parentFilesOption(parentFilesValue)
6294
+ });
6295
+ }
6296
+ throw new Error(`Unknown generate image command: ${subcommand}`);
6297
+ }
5490
6298
  if (command === "reroll") {
5491
6299
  const subcommand = positionalArgs(args)[0] || "";
5492
6300
  if (subcommand === "list") {
@@ -5640,6 +6448,15 @@ function printDataResult(command, result, json) {
5640
6448
  if (link.warning) console.log(`Warning: ${link.warning}`);
5641
6449
  return;
5642
6450
  }
6451
+ if (command === "generate" && result && typeof result === "object" && "job" in result) {
6452
+ const generation = result;
6453
+ if (generation.imported) {
6454
+ console.log(`${generation.idempotent ? "Already imported" : "Imported"} ${generation.imported.length} output(s) for ${generation.job?.id || "job"}`);
6455
+ } else {
6456
+ console.log(`Generation job ${generation.job?.id || "job"} ${generation.job?.status || "unknown"}`);
6457
+ }
6458
+ return;
6459
+ }
5643
6460
  if (command === "reroll" && result && typeof result === "object") {
5644
6461
  if ("requests" in result) {
5645
6462
  const listed = result;
@@ -5715,6 +6532,18 @@ function runLineageProfileCommand(config, command, args) {
5715
6532
  const selector = profileSelector(args);
5716
6533
  if (!selector) throw new Error(`lineage profile ${command} requires --profile or LINEAGE_PROFILE`);
5717
6534
  if (command === "doctor") return doctorLineageProfile(selector, runtimeIdentity);
6535
+ if (command === "repin-runtime") {
6536
+ const checkoutRoot = readOption(args, "--checkout-root");
6537
+ if (!checkoutRoot) throw new Error("Profile runtime repin requires --checkout-root");
6538
+ if (!args.includes("--confirm-write")) throw new Error("Profile runtime repin requires --confirm-write");
6539
+ const profile = resolveLineageProfile(selector);
6540
+ const writerLease = acquireProfileWriterLease(profile, config.channel, "cli");
6541
+ try {
6542
+ return repinLineageDevelopmentProfileRuntime(selector, checkoutRoot, runtimeIdentity, true);
6543
+ } finally {
6544
+ writerLease.release();
6545
+ }
6546
+ }
5718
6547
  if (command === "bind") {
5719
6548
  if (!args.includes("--confirm-write")) throw new Error("Profile bind requires --confirm-write");
5720
6549
  const profile = resolveLineageProfile(selector);
@@ -5740,6 +6569,9 @@ function printProfileResult(result, json) {
5740
6569
  } else if (result.schema_version === "lineage.profile_clone_receipt.v1") {
5741
6570
  console.log(`Cloned ${result.source_database_path} to ${result.database_path} for ${result.target_identity.profile_id}`);
5742
6571
  console.log(`Receipt: ${result.receipt_path}`);
6572
+ } else if (result.schema_version === "lineage.profile_runtime_repin_receipt.v1") {
6573
+ console.log(`${result.changed ? "Repinned" : "Already pinned"} ${result.profile_id} to checkout ${result.checkout_root}`);
6574
+ console.log(`Code fingerprint: ${result.new_code_fingerprint}`);
5743
6575
  } else {
5744
6576
  console.log(`Cloned ${result.files_copied} referenced asset file(s) into ${result.asset_root}`);
5745
6577
  console.log(`Receipt: ${result.receipt_path}`);
@@ -5770,7 +6602,9 @@ function printRuntimeResult(result, json) {
5770
6602
  }
5771
6603
  function lineageCliRequiresWriterLease(command, args) {
5772
6604
  if (command === "next" || command === "brief" || command === "inspect" || command === "selection") return false;
5773
- const subcommand = positionalArgs(args)[0] || "";
6605
+ const positions = positionalArgs(args);
6606
+ const subcommand = positions[0] || "";
6607
+ if (command === "generate") return positions[0] !== "image" || positions[1] !== "inspect";
5774
6608
  if (command === "reroll") return subcommand !== "list";
5775
6609
  if (command === "tasks") return subcommand !== "list" && subcommand !== "inspect";
5776
6610
  if (command === "db") return subcommand !== "info";
@@ -6110,7 +6944,7 @@ async function runLineageCli(config, args = process.argv.slice(2)) {
6110
6944
  else console.error(`${config.binName}: ${message2}`);
6111
6945
  process.exit(1);
6112
6946
  }
6113
- if (command === "next" || command === "brief" || command === "inspect" || command === "selection" || command === "link-child" || command === "reroll" || command === "tasks") {
6947
+ if (command === "next" || command === "brief" || command === "inspect" || command === "selection" || command === "link-child" || command === "generate" || command === "reroll" || command === "tasks") {
6114
6948
  const commandArgs = normalizedArgs.slice(1);
6115
6949
  const json2 = commandArgs.includes("--json");
6116
6950
  try {