@mean-weasel/lineage 0.1.15 → 0.1.17

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +249 -19
  3. package/dist/cli/lineage-channel.js +25 -5
  4. package/dist/cli/lineage-channel.js.map +2 -2
  5. package/dist/cli/lineage-dev.js +891 -52
  6. package/dist/cli/lineage-dev.js.map +4 -4
  7. package/dist/cli/lineage-preview.js +891 -52
  8. package/dist/cli/lineage-preview.js.map +4 -4
  9. package/dist/cli/lineage.js +891 -52
  10. package/dist/cli/lineage.js.map +4 -4
  11. package/dist/cli/managed-service.js +19 -5
  12. package/dist/cli/managed-service.js.map +2 -2
  13. package/dist/runtime-build.json +4 -4
  14. package/dist/server.js +883 -89
  15. package/dist/server.js.map +4 -4
  16. package/dist/web/assets/agent-shared-state-DOfpTLiw.webp +0 -0
  17. package/dist/web/assets/agent-to-canvas-DtIu_cPq.mp4 +0 -0
  18. package/dist/web/assets/{app-CDxSvoD3.css → app-B-fAyNsU.css} +1 -1
  19. package/dist/web/assets/app-DMogkUoS.js +16 -0
  20. package/dist/web/assets/attempt-history-D6wdic6n.webp +0 -0
  21. package/dist/web/assets/branching-tree-B8I5_S6D.png +0 -0
  22. package/dist/web/assets/canvas-cli-DHBacq7r.png +0 -0
  23. package/dist/web/assets/hero-agent-sync-CSyh0tHy.mp4 +0 -0
  24. package/dist/web/assets/hero-board-DG3ED4AW.webp +0 -0
  25. package/dist/web/assets/hero-lineage-growth-Bsl9tiq-.mp4 +0 -0
  26. package/dist/web/assets/hero-trace-connections-Bb52ABBc.mp4 +0 -0
  27. package/dist/web/assets/human-selection-BdDBDE-I.webp +0 -0
  28. package/dist/web/assets/human-to-agent-D1z92ZO9.mp4 +0 -0
  29. package/dist/web/assets/{jsx-runtime-_Rdg6et1.js → jsx-runtime-DAFSxiwi.js} +1 -1
  30. package/dist/web/assets/landing-Bn4qKbIO.css +1 -0
  31. package/dist/web/assets/landing-CoyGmxBo.js +1 -0
  32. package/dist/web/assets/reroll-history-DQcEH42R.mp4 +0 -0
  33. package/dist/web/index.html +3 -3
  34. package/dist/web/landing/index.html +3 -3
  35. package/fixtures/demo-project/lineage/swissifier-rich-demo.json +13 -13
  36. package/package.json +4 -3
  37. package/dist/web/assets/app-CYUgf6rX.js +0 -16
  38. package/dist/web/assets/landing-D3Y679_B.css +0 -1
  39. package/dist/web/assets/landing-ii8AH_DG.js +0 -1
@@ -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
 
@@ -619,6 +645,7 @@ import { dirname as dirname3, isAbsolute, join as join4, relative as relative2,
619
645
 
620
646
  // src/shared/lineageProfileTypes.ts
621
647
  var lineageProfileSchemaVersion = "lineage.profile.v1";
648
+ var lineageProfileInitSchemaVersion = "lineage.profile_init.v1";
622
649
  var lineageProfileDoctorSchemaVersion = "lineage.profile_doctor.v1";
623
650
  var lineageProfileCloneReceiptSchemaVersion = "lineage.profile_clone_receipt.v1";
624
651
  var lineageProfileAssetsCloneReceiptSchemaVersion = "lineage.profile_assets_clone_receipt.v1";
@@ -654,13 +681,13 @@ function profileManifestPath(selector) {
654
681
  if (!profileIdPattern.test(value)) throw new Error(`Invalid profile ID: ${value}`);
655
682
  return { manifestPath: join4(lineageProfileRoot(), value, "profile.json"), namedProfileId: value };
656
683
  }
657
- function requiredString(record, key) {
658
- const value = record[key];
684
+ function requiredString(record2, key) {
685
+ const value = record2[key];
659
686
  if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest requires a non-empty ${key}`);
660
687
  return value.trim();
661
688
  }
662
- function optionalString(record, key) {
663
- const value = record[key];
689
+ function optionalString(record2, key) {
690
+ const value = record2[key];
664
691
  if (value === void 0) return void 0;
665
692
  if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest ${key} must be a non-empty string`);
666
693
  return value.trim();
@@ -711,15 +738,15 @@ function resolveLineageProfile(selector) {
711
738
  throw new Error(`Could not parse profile manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
712
739
  }
713
740
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Profile manifest must be a JSON object");
714
- const record = raw;
715
- const schemaVersion = requiredString(record, "schema_version");
741
+ const record2 = raw;
742
+ const schemaVersion = requiredString(record2, "schema_version");
716
743
  if (schemaVersion !== lineageProfileSchemaVersion) throw new Error(`Unsupported profile schema_version: ${schemaVersion}`);
717
- const profileId = requiredString(record, "profile_id");
744
+ const profileId = requiredString(record2, "profile_id");
718
745
  if (!profileIdPattern.test(profileId)) throw new Error(`Invalid profile ID: ${profileId}`);
719
746
  if (namedProfileId && namedProfileId !== profileId) {
720
747
  throw new Error(`Named profile ${namedProfileId} does not match immutable manifest profile_id ${profileId}`);
721
748
  }
722
- const expectedRaw = record.expected_runtime;
749
+ const expectedRaw = record2.expected_runtime;
723
750
  if (expectedRaw !== void 0 && (!expectedRaw || typeof expectedRaw !== "object" || Array.isArray(expectedRaw))) {
724
751
  throw new Error("Profile expected_runtime must be an object");
725
752
  }
@@ -728,18 +755,18 @@ function resolveLineageProfile(selector) {
728
755
  const expectedVersion = expected ? optionalString(expected, "version") : void 0;
729
756
  const expectedCodeFingerprint = validateFingerprint(expected ? optionalString(expected, "code_fingerprint") : void 0, "expected_runtime.code_fingerprint");
730
757
  const expectedCodeOrigin = validateCodeOrigin(expected?.code_origin);
731
- const migrationsRaw = record.required_schema_migrations;
758
+ const migrationsRaw = record2.required_schema_migrations;
732
759
  if (migrationsRaw !== void 0 && (!Array.isArray(migrationsRaw) || migrationsRaw.some((value) => typeof value !== "string" || !value.trim()))) {
733
760
  throw new Error("Profile required_schema_migrations must be an array of non-empty strings");
734
761
  }
735
- const environment = validateEnvironment(requiredString(record, "environment"));
762
+ const environment = validateEnvironment(requiredString(record2, "environment"));
736
763
  const expectedChannel = validateChannel(expected?.channel);
737
764
  if (expectedChannel && expectedChannel !== environmentChannel(environment)) {
738
765
  throw new Error(`Profile environment ${environment} conflicts with expected runtime channel ${expectedChannel}`);
739
766
  }
740
767
  const manifest = {
741
- asset_root: resolveManifestPath(manifestPath, requiredString(record, "asset_root")),
742
- database_path: resolveManifestPath(manifestPath, requiredString(record, "database_path")),
768
+ asset_root: resolveManifestPath(manifestPath, requiredString(record2, "asset_root")),
769
+ database_path: resolveManifestPath(manifestPath, requiredString(record2, "database_path")),
743
770
  environment,
744
771
  ...expected ? {
745
772
  expected_runtime: {
@@ -753,7 +780,7 @@ function resolveLineageProfile(selector) {
753
780
  profile_id: profileId,
754
781
  ...migrationsRaw ? { required_schema_migrations: migrationsRaw.map((value) => value.trim()) } : {},
755
782
  schema_version: lineageProfileSchemaVersion,
756
- service_origin: validateServiceOrigin(requiredString(record, "service_origin"))
783
+ service_origin: validateServiceOrigin(requiredString(record2, "service_origin"))
757
784
  };
758
785
  return { ...manifest, manifest_path: manifestPath, profile_fingerprint: lineageProfileFingerprint(manifest) };
759
786
  }
@@ -767,6 +794,120 @@ function lineageProfileFingerprint(profile) {
767
794
  service_origin: profile.service_origin
768
795
  })).digest("hex");
769
796
  }
797
+ function initializeLineageProfile(profileId, serviceOrigin, runtime, confirmWrite, withWriterLease) {
798
+ if (!confirmWrite) throw new Error("Profile init requires --confirm-write");
799
+ if (!profileIdPattern.test(profileId)) throw new Error(`Invalid profile ID: ${profileId}`);
800
+ if (!runtime.code?.verified) throw new Error("Profile init requires a verified runtime code identity");
801
+ if (runtime.code.origin !== "checkout" && runtime.code.origin !== "package") {
802
+ throw new Error(`Profile init requires checkout or package code, not ${runtime.code.origin}`);
803
+ }
804
+ if (!runtime.code.fingerprint || !/^[a-f0-9]{64}$/i.test(runtime.code.fingerprint)) {
805
+ throw new Error("Profile init requires a valid executing code fingerprint");
806
+ }
807
+ const validatedServiceOrigin = validateServiceOrigin(serviceOrigin);
808
+ const root = lineageProfileRoot();
809
+ mkdirSync3(root, { recursive: true, mode: 448 });
810
+ const profileDirectory = join4(root, profileId);
811
+ try {
812
+ mkdirSync3(profileDirectory, { mode: 448 });
813
+ } catch (error) {
814
+ if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") {
815
+ const doctor = doctorLineageProfile(profileId, runtime);
816
+ if (doctor.ok) {
817
+ throw new Error(`Profile ${profileId} already exists and passes doctor. No files were changed. Reuse it with \`start --profile ${profileId}\`; verify it first with \`profile doctor --profile ${profileId} --json\`.`, { cause: error });
818
+ }
819
+ const failures = doctor.checks.filter((check) => check.status === "fail").map((check) => `${check.id}: ${check.message}`).join("; ");
820
+ throw new Error(`Profile ${profileId} already exists but did not pass doctor. No files were changed. Run \`profile doctor --profile ${profileId} --json\`, then inspect ${profileDirectory} manually before retrying. ${failures}`, { cause: error });
821
+ }
822
+ throw error;
823
+ }
824
+ const manifestPath = join4(profileDirectory, "profile.json");
825
+ const databasePath = join4(profileDirectory, "lineage.sqlite");
826
+ const assetRoot = join4(profileDirectory, "media");
827
+ const environment = channelEnvironment(runtime.channel);
828
+ const manifest = {
829
+ asset_root: assetRoot,
830
+ database_path: databasePath,
831
+ environment,
832
+ expected_runtime: {
833
+ channel: runtime.channel,
834
+ code_fingerprint: runtime.code.fingerprint.toLowerCase(),
835
+ code_origin: runtime.code.origin
836
+ },
837
+ profile_id: profileId,
838
+ schema_version: lineageProfileSchemaVersion,
839
+ service_origin: validatedServiceOrigin
840
+ };
841
+ const profile = {
842
+ ...manifest,
843
+ manifest_path: manifestPath,
844
+ profile_fingerprint: lineageProfileFingerprint(manifest)
845
+ };
846
+ try {
847
+ assertProfileChannel(profile, runtime.channel);
848
+ assertProfileRuntimePin(profile, runtime);
849
+ const identity = withWriterLease(profile, () => {
850
+ mkdirSync3(assetRoot, { mode: 448 });
851
+ const databaseFd = openSync(databasePath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_RDWR, 384);
852
+ closeSync(databaseFd);
853
+ const { DatabaseSync } = require2("node:sqlite");
854
+ const database = new DatabaseSync(databasePath);
855
+ let boundIdentity;
856
+ try {
857
+ boundIdentity = bindOpenDatabase(database, profile, false);
858
+ } finally {
859
+ database.close();
860
+ }
861
+ const manifestPayload = {
862
+ asset_root: "./media",
863
+ database_path: "./lineage.sqlite",
864
+ environment: profile.environment,
865
+ expected_runtime: profile.expected_runtime,
866
+ profile_id: profile.profile_id,
867
+ schema_version: profile.schema_version,
868
+ service_origin: profile.service_origin
869
+ };
870
+ writeFileSync2(manifestPath, `${JSON.stringify(manifestPayload, null, 2)}
871
+ `, { encoding: "utf8", flag: "wx", mode: 384 });
872
+ const manifestFd = openSync(manifestPath, "r");
873
+ try {
874
+ fsyncSync(manifestFd);
875
+ } finally {
876
+ closeSync(manifestFd);
877
+ }
878
+ const directoryFd = openSync(profileDirectory, "r");
879
+ try {
880
+ fsyncSync(directoryFd);
881
+ } finally {
882
+ closeSync(directoryFd);
883
+ }
884
+ const published = resolveLineageProfile(profileId);
885
+ if (published.profile_fingerprint !== profile.profile_fingerprint) {
886
+ throw new Error("Initialized profile fingerprint does not match its published manifest");
887
+ }
888
+ return boundIdentity;
889
+ });
890
+ return {
891
+ asset_root: profile.asset_root,
892
+ database_path: profile.database_path,
893
+ environment: profile.environment,
894
+ identity,
895
+ manifest_path: profile.manifest_path,
896
+ profile_fingerprint: profile.profile_fingerprint,
897
+ profile_id: profile.profile_id,
898
+ runtime: {
899
+ channel: runtime.channel,
900
+ code_fingerprint: runtime.code.fingerprint,
901
+ code_origin: runtime.code.origin
902
+ },
903
+ schema_version: lineageProfileInitSchemaVersion,
904
+ service_origin: profile.service_origin
905
+ };
906
+ } catch (error) {
907
+ rmSync(profileDirectory, { force: true, recursive: true });
908
+ throw error;
909
+ }
910
+ }
770
911
  function sha256(value) {
771
912
  return createHash2("sha256").update(value).digest("hex");
772
913
  }
@@ -1498,7 +1639,7 @@ function acquireProfileWriterLease(profile, channel, role = "service") {
1498
1639
  }
1499
1640
  function assertProfileWriterLeaseHeld() {
1500
1641
  if (!process.env.LINEAGE_PROFILE) {
1501
- throw new Error("Persistent writes require a selected named Lineage profile and its writer lease; legacy-unbound access is read-only");
1642
+ throw new Error("Persistent writes require a selected named Lineage profile and its writer lease; legacy-unbound access is read-only. Create a fresh profile with `profile init --profile <id> --confirm-write`, then pass that profile to the command");
1502
1643
  }
1503
1644
  const profileId = process.env.LINEAGE_PROFILE_ID;
1504
1645
  const profileFingerprint = process.env.LINEAGE_PROFILE_FINGERPRINT;
@@ -1622,6 +1763,10 @@ function lineageDb() {
1622
1763
  child_asset_id text not null references assets(id),
1623
1764
  relation_type text not null check (relation_type in ('derived_from')),
1624
1765
  created_at text not null,
1766
+ summary text,
1767
+ summary_created_by text check (summary_created_by in ('human', 'agent', 'system')),
1768
+ summary_updated_by text check (summary_updated_by in ('human', 'agent', 'system')),
1769
+ summary_updated_at text,
1625
1770
  unique (project_id, parent_asset_id, child_asset_id, relation_type)
1626
1771
  );
1627
1772
  create index if not exists edges_parent on asset_edges(project_id, parent_asset_id);
@@ -1905,6 +2050,7 @@ function lineageDb() {
1905
2050
  imported_asset_id text not null references assets(id),
1906
2051
  parent_asset_id text not null references assets(id),
1907
2052
  imported_at text not null,
2053
+ edge_summary text,
1908
2054
  unique(job_id, output_index),
1909
2055
  unique(job_id, file_path)
1910
2056
  );
@@ -1993,6 +2139,11 @@ function lineageDb() {
1993
2139
  );
1994
2140
  create index if not exists agent_claim_events_claim_created on agent_claim_events(claim_id, created_at);
1995
2141
  `);
2142
+ ensureColumn(database, "asset_edges", "summary", "text");
2143
+ ensureColumn(database, "asset_edges", "summary_created_by", "text check (summary_created_by in ('human', 'agent', 'system'))");
2144
+ ensureColumn(database, "asset_edges", "summary_updated_by", "text check (summary_updated_by in ('human', 'agent', 'system'))");
2145
+ ensureColumn(database, "asset_edges", "summary_updated_at", "text");
2146
+ ensureColumn(database, "generation_job_outputs", "edge_summary", "text");
1996
2147
  migrateAssetSelections(database);
1997
2148
  dropLegacyAssetSelectionRootUnique(database);
1998
2149
  ensureColumn(database, "asset_selections", "notes", "text");
@@ -3118,6 +3269,14 @@ function cancelLineageTask(project, fields) {
3118
3269
  database.close();
3119
3270
  }
3120
3271
  }
3272
+ function cancelLineageIterateTasksForAssets(project, fields) {
3273
+ const targetIds = fields.assetIds ? new Set(fields.assetIds) : void 0;
3274
+ 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, {
3275
+ actor: fields.actor,
3276
+ confirmWrite: fields.confirmWrite,
3277
+ taskId: task.id
3278
+ }));
3279
+ }
3121
3280
  function resolveLineageTask(project, fields) {
3122
3281
  const normalizedProject = normalizeProject(project);
3123
3282
  const actor = normalizeActor(fields.actor, "Resolve actor");
@@ -3483,6 +3642,23 @@ function rerollRequestId(project, root, node, timestamp) {
3483
3642
  function rowString(value) {
3484
3643
  return typeof value === "string" && value.length > 0 ? value : void 0;
3485
3644
  }
3645
+ function lineageEdgeFromRow(row) {
3646
+ const summary = rowString(row.summary);
3647
+ const summaryCreatedBy = rowString(row.summary_created_by);
3648
+ const summaryUpdatedBy = rowString(row.summary_updated_by);
3649
+ const summaryUpdatedAt = rowString(row.summary_updated_at);
3650
+ return {
3651
+ id: String(row.id),
3652
+ parent_asset_id: String(row.parent_asset_id),
3653
+ child_asset_id: String(row.child_asset_id),
3654
+ relation_type: row.relation_type,
3655
+ created_at: String(row.created_at),
3656
+ ...summary ? { summary } : {},
3657
+ ...summaryCreatedBy ? { summary_created_by: summaryCreatedBy } : {},
3658
+ ...summaryUpdatedBy ? { summary_updated_by: summaryUpdatedBy } : {},
3659
+ ...summaryUpdatedAt ? { summary_updated_at: summaryUpdatedAt } : {}
3660
+ };
3661
+ }
3486
3662
  function rerollRequestFrom(row) {
3487
3663
  return {
3488
3664
  id: String(row.id),
@@ -3582,6 +3758,9 @@ function localPreviewUrl(project, localPath) {
3582
3758
  return `/api/assets/local-preview?${params.toString()}`;
3583
3759
  }
3584
3760
  function linkLineageAssets(project, fields) {
3761
+ const summary = normalizeEdgeSummary(fields.summary);
3762
+ if (summary && !fields.summaryActor) throw new LineageError("Lineage edge summary requires an explicit author");
3763
+ if (!summary && fields.summaryActor) throw new LineageError("Lineage edge summary author requires a summary");
3585
3764
  const database = lineageDb();
3586
3765
  requireAsset2(database, project, fields.parentAssetId);
3587
3766
  requireAsset2(database, project, fields.childAssetId);
@@ -3600,22 +3779,66 @@ function linkLineageAssets(project, fields) {
3600
3779
  database.close();
3601
3780
  throw error;
3602
3781
  }
3782
+ const createdAt = nowIso();
3603
3783
  const edge = {
3604
3784
  id: edgeId(project, fields.parentAssetId, fields.childAssetId),
3605
3785
  parent_asset_id: fields.parentAssetId,
3606
3786
  child_asset_id: fields.childAssetId,
3607
3787
  relation_type: "derived_from",
3608
- created_at: nowIso()
3788
+ created_at: createdAt,
3789
+ ...summary ? {
3790
+ summary,
3791
+ summary_created_by: fields.summaryActor,
3792
+ summary_updated_by: fields.summaryActor,
3793
+ summary_updated_at: createdAt
3794
+ } : {}
3609
3795
  };
3610
3796
  if (!fields.confirmWrite) {
3611
3797
  database.close();
3612
3798
  return { ok: true, dryRun: true, edge };
3613
3799
  }
3614
- database.prepare(`
3615
- insert into asset_edges (id, project_id, parent_asset_id, child_asset_id, relation_type, created_at)
3616
- values (?, ?, ?, ?, 'derived_from', ?)
3800
+ const inserted = database.prepare(`
3801
+ insert into asset_edges (
3802
+ id, project_id, parent_asset_id, child_asset_id, relation_type, created_at,
3803
+ summary, summary_created_by, summary_updated_by, summary_updated_at
3804
+ )
3805
+ values (?, ?, ?, ?, 'derived_from', ?, ?, ?, ?, ?)
3617
3806
  on conflict(project_id, parent_asset_id, child_asset_id, relation_type) do nothing
3618
- `).run(edge.id, project, edge.parent_asset_id, edge.child_asset_id, edge.created_at);
3807
+ `).run(
3808
+ edge.id,
3809
+ project,
3810
+ edge.parent_asset_id,
3811
+ edge.child_asset_id,
3812
+ edge.created_at,
3813
+ edge.summary || null,
3814
+ edge.summary_created_by || null,
3815
+ edge.summary_updated_by || null,
3816
+ edge.summary_updated_at || null
3817
+ );
3818
+ if (inserted.changes === 0) {
3819
+ const existingRow = database.prepare(`
3820
+ select id, parent_asset_id, child_asset_id, relation_type, created_at,
3821
+ summary, summary_created_by, summary_updated_by, summary_updated_at
3822
+ from asset_edges
3823
+ where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
3824
+ `).get(project, edge.parent_asset_id, edge.child_asset_id);
3825
+ if (!existingRow) {
3826
+ database.close();
3827
+ throw new LineageError("Lineage edge conflict could not be resolved", 409);
3828
+ }
3829
+ const existingEdge = lineageEdgeFromRow(existingRow);
3830
+ if (summary && (existingEdge.summary !== summary || existingEdge.summary_created_by !== fields.summaryActor || existingEdge.summary_updated_by !== fields.summaryActor || !existingEdge.summary_updated_at)) {
3831
+ database.close();
3832
+ throw new LineageError("Lineage edge already exists with a different summary or provenance", 409);
3833
+ }
3834
+ database.close();
3835
+ return {
3836
+ ok: true,
3837
+ idempotent: true,
3838
+ message: `Already linked ${existingEdge.child_asset_id} from ${existingEdge.parent_asset_id}`,
3839
+ edge: existingEdge
3840
+ };
3841
+ }
3619
3842
  database.close();
3620
3843
  return { ok: true, message: `Linked ${edge.child_asset_id} from ${edge.parent_asset_id}`, edge };
3621
3844
  }
@@ -3627,7 +3850,13 @@ function descendants(database, project, root) {
3627
3850
  const parent = queue.shift();
3628
3851
  if (seen.has(parent)) continue;
3629
3852
  seen.add(parent);
3630
- 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);
3853
+ const rows = database.prepare(`
3854
+ select id, parent_asset_id, child_asset_id, relation_type, created_at,
3855
+ summary, summary_created_by, summary_updated_by, summary_updated_at
3856
+ from asset_edges
3857
+ where project_id = ? and parent_asset_id = ?
3858
+ order by created_at
3859
+ `).all(project, parent).map(lineageEdgeFromRow);
3631
3860
  edges.push(...rows);
3632
3861
  queue.push(...rows.map((row) => row.child_asset_id));
3633
3862
  }
@@ -4013,7 +4242,7 @@ function lineageCommand(command, project, rootAssetId) {
4013
4242
  return `${lineagePublicPackageCommand()} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} ${lineageRuntimeSelector()} --json`;
4014
4243
  }
4015
4244
  function linkChildCommand(project, rootAssetId) {
4016
- return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write ${lineageRuntimeSelector()} --json`;
4245
+ return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --summary "<one-or-two-words>" --confirm-write ${lineageRuntimeSelector()} --json`;
4017
4246
  }
4018
4247
  function rerollImportGuidance(rootAssetId, targetAssetId) {
4019
4248
  return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
@@ -4062,6 +4291,7 @@ function getLineageBrief(project, rootAssetId) {
4062
4291
  };
4063
4292
  }
4064
4293
  function linkSelectedLineageChild(project, fields) {
4294
+ const summary = fields.summaryActor ? requireEdgeSummary(fields.summary) : fields.summary;
4065
4295
  const next = getLineageNextAsset(project, fields.rootAssetId);
4066
4296
  if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
4067
4297
  const rerollRequests = listLineageRerollRequests(project, next.root_asset_id).requests;
@@ -4090,7 +4320,9 @@ function linkSelectedLineageChild(project, fields) {
4090
4320
  childAssetId: fields.childAssetId,
4091
4321
  confirmWrite: fields.confirmWrite,
4092
4322
  claimToken: fields.claimToken,
4093
- parentAssetId: next.next_asset.asset_id
4323
+ parentAssetId: next.next_asset.asset_id,
4324
+ summary,
4325
+ summaryActor: fields.summaryActor
4094
4326
  });
4095
4327
  return {
4096
4328
  ...result,
@@ -4110,7 +4342,141 @@ function linkSelectedLineageChild(project, fields) {
4110
4342
  import { existsSync as existsSync8, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
4111
4343
  import { relative as relative3, resolve as resolve5 } from "node:path";
4112
4344
  import { randomUUID as randomUUID3 } from "node:crypto";
4113
- var adapterVersion = "generation-receipts-v1";
4345
+
4346
+ // src/shared/generationOutputManifest.ts
4347
+ var generationOutputManifestSchemaVersion = "lineage.generation_output_manifest.v1";
4348
+ var GenerationOutputManifestError = class extends Error {
4349
+ constructor(message) {
4350
+ super(message);
4351
+ this.name = "GenerationOutputManifestError";
4352
+ }
4353
+ };
4354
+ function record(value, label) {
4355
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4356
+ throw new GenerationOutputManifestError(`${label} must be an object`);
4357
+ }
4358
+ return value;
4359
+ }
4360
+ function exactKeys(value, allowed, label) {
4361
+ const allowedKeys = new Set(allowed);
4362
+ const unsupported = Object.keys(value).filter((key) => !allowedKeys.has(key)).sort();
4363
+ if (unsupported.length > 0) {
4364
+ throw new GenerationOutputManifestError(`${label} contains unsupported field: ${unsupported[0]}`);
4365
+ }
4366
+ }
4367
+ function nonEmptyText(value, label) {
4368
+ if (typeof value !== "string" || !value.trim()) throw new GenerationOutputManifestError(`${label} is required`);
4369
+ return value.trim();
4370
+ }
4371
+ function expectedGenerationOutputParents(job) {
4372
+ if (!Number.isInteger(job.expected_output_count) || job.expected_output_count <= 0) {
4373
+ throw new GenerationOutputManifestError("Generation job expected output count must be a positive integer");
4374
+ }
4375
+ const parents = job.inputs.filter((input) => input.role === "lineage_next_base").sort((left, right) => left.position - right.position);
4376
+ if (parents.length === 0) throw new GenerationOutputManifestError("Generation job has no lineage_next_base input");
4377
+ const parentIds = /* @__PURE__ */ new Set();
4378
+ const parentPositions = /* @__PURE__ */ new Set();
4379
+ for (const parent of parents) {
4380
+ if (typeof parent.asset_id !== "string" || !parent.asset_id || parent.asset_id.trim() !== parent.asset_id) {
4381
+ throw new GenerationOutputManifestError("Generation job has invalid lineage parent asset id");
4382
+ }
4383
+ if (!Number.isInteger(parent.position) || parent.position < 0 || parentPositions.has(parent.position)) {
4384
+ throw new GenerationOutputManifestError("Generation job has invalid lineage parent positions");
4385
+ }
4386
+ if (parentIds.has(parent.asset_id)) throw new GenerationOutputManifestError(`Generation job has duplicate lineage parent: ${parent.asset_id}`);
4387
+ parentIds.add(parent.asset_id);
4388
+ parentPositions.add(parent.position);
4389
+ }
4390
+ if (job.expected_output_count % parents.length !== 0) {
4391
+ throw new GenerationOutputManifestError("Generation job has invalid parent mapping");
4392
+ }
4393
+ const outputsPerParent = job.expected_output_count / parents.length;
4394
+ return Array.from({ length: job.expected_output_count }, (_value, outputIndex) => {
4395
+ return parents[Math.floor(outputIndex / outputsPerParent)].asset_id;
4396
+ });
4397
+ }
4398
+ function createGenerationOutputManifestDraft(job) {
4399
+ return {
4400
+ schema_version: generationOutputManifestSchemaVersion,
4401
+ job_id: job.id,
4402
+ outputs: expectedGenerationOutputParents(job).map((parentAssetId, outputIndex) => ({
4403
+ output_index: outputIndex,
4404
+ file_path: "",
4405
+ parent_asset_id: parentAssetId,
4406
+ edge_summary: ""
4407
+ }))
4408
+ };
4409
+ }
4410
+ function parseGenerationOutputManifest(value, job, options) {
4411
+ if (!options || typeof options.resolveFilePath !== "function") {
4412
+ throw new GenerationOutputManifestError("Generation output manifest requires a file-path resolver");
4413
+ }
4414
+ const manifest = record(value, "Generation output manifest");
4415
+ exactKeys(manifest, ["schema_version", "job_id", "outputs"], "Generation output manifest");
4416
+ if (manifest.schema_version !== generationOutputManifestSchemaVersion) {
4417
+ throw new GenerationOutputManifestError(`Generation output manifest schema_version must be ${generationOutputManifestSchemaVersion}`);
4418
+ }
4419
+ if (manifest.job_id !== job.id) throw new GenerationOutputManifestError(`Generation output manifest job_id must be ${job.id}`);
4420
+ if (!Array.isArray(manifest.outputs)) throw new GenerationOutputManifestError("Generation output manifest outputs must be an array");
4421
+ const expectedParents = expectedGenerationOutputParents(job);
4422
+ if (manifest.outputs.length !== expectedParents.length) {
4423
+ throw new GenerationOutputManifestError(`Generation output manifest requires ${expectedParents.length} outputs, received ${manifest.outputs.length}`);
4424
+ }
4425
+ const seenIndexes = /* @__PURE__ */ new Set();
4426
+ const outputs = manifest.outputs.map((value2, position) => {
4427
+ const output = record(value2, `Generation output at position ${position}`);
4428
+ exactKeys(output, ["output_index", "file_path", "parent_asset_id", "edge_summary"], `Generation output at position ${position}`);
4429
+ const outputIndex = output.output_index;
4430
+ if (!Number.isInteger(outputIndex) || Number(outputIndex) < 0) {
4431
+ throw new GenerationOutputManifestError(`Generation output at position ${position} requires a non-negative integer output_index`);
4432
+ }
4433
+ const normalizedIndex = Number(outputIndex);
4434
+ if (seenIndexes.has(normalizedIndex)) throw new GenerationOutputManifestError(`Duplicate generation output_index: ${normalizedIndex}`);
4435
+ seenIndexes.add(normalizedIndex);
4436
+ const expectedParent = expectedParents[normalizedIndex];
4437
+ if (!expectedParent) throw new GenerationOutputManifestError(`Unknown generation output_index: ${normalizedIndex}`);
4438
+ const filePath = nonEmptyText(output.file_path, `Generation output ${normalizedIndex} file_path`);
4439
+ const parentAssetId = nonEmptyText(output.parent_asset_id, `Generation output ${normalizedIndex} parent_asset_id`);
4440
+ if (parentAssetId !== expectedParent) {
4441
+ throw new GenerationOutputManifestError(`Generation output ${normalizedIndex} must use parent_asset_id ${expectedParent}`);
4442
+ }
4443
+ let edgeSummary;
4444
+ try {
4445
+ edgeSummary = requireEdgeSummary(output.edge_summary);
4446
+ } catch (error) {
4447
+ if (error instanceof EdgeSummaryValidationError) {
4448
+ throw new GenerationOutputManifestError(`Generation output ${normalizedIndex}: ${error.message}`);
4449
+ }
4450
+ throw error;
4451
+ }
4452
+ return {
4453
+ output_index: normalizedIndex,
4454
+ file_path: filePath,
4455
+ parent_asset_id: parentAssetId,
4456
+ edge_summary: edgeSummary
4457
+ };
4458
+ });
4459
+ for (const outputIndex of expectedParents.keys()) {
4460
+ if (!seenIndexes.has(outputIndex)) throw new GenerationOutputManifestError(`Missing generation output_index: ${outputIndex}`);
4461
+ }
4462
+ outputs.sort((left, right) => left.output_index - right.output_index);
4463
+ const seenPaths = /* @__PURE__ */ new Set();
4464
+ const resolvedOutputs = outputs.map((output) => {
4465
+ const filePath = nonEmptyText(options.resolveFilePath(output.file_path), `Generation output ${output.output_index} resolved file_path`);
4466
+ if (seenPaths.has(filePath)) throw new GenerationOutputManifestError(`Duplicate generation output file_path: ${filePath}`);
4467
+ seenPaths.add(filePath);
4468
+ return { ...output, file_path: filePath };
4469
+ });
4470
+ return {
4471
+ schema_version: generationOutputManifestSchemaVersion,
4472
+ job_id: job.id,
4473
+ outputs: resolvedOutputs
4474
+ };
4475
+ }
4476
+
4477
+ // src/server/generationReceipts.ts
4478
+ var legacyAdapterVersion = "generation-receipts-v1";
4479
+ var manifestAdapterVersion = "generation-receipts-v2";
4114
4480
  var provider = "codex-handoff";
4115
4481
  var GenerationReceiptError = class extends Error {
4116
4482
  constructor(message, status = 400) {
@@ -4129,6 +4495,70 @@ function parseJson(value, fallback) {
4129
4495
  if (!value) return fallback;
4130
4496
  return JSON.parse(value);
4131
4497
  }
4498
+ function positiveInteger(value) {
4499
+ return Number.isInteger(value) && Number(value) > 0;
4500
+ }
4501
+ function resolveLineageSelection(project) {
4502
+ const rootAssetId = activeLineageWorkspaceRoot(project);
4503
+ if (!rootAssetId) throw new GenerationReceiptError("No active lineage workspace for generation planning");
4504
+ const next = getLineageNextAsset(project, rootAssetId);
4505
+ if (!next.next_asset) throw new GenerationReceiptError(`No clear lineage next asset: ${next.reason}`);
4506
+ if (next.strategy !== "selected") throw new GenerationReceiptError("Generation v1 requires an explicit selected lineage next base");
4507
+ 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");
4508
+ return next;
4509
+ }
4510
+ function selectedParents(next) {
4511
+ const parents = next.next_assets.length > 0 ? next.next_assets : next.next_asset ? [next.next_asset] : [];
4512
+ if (parents.length === 0) throw new GenerationReceiptError("Missing lineage next base");
4513
+ return parents;
4514
+ }
4515
+ function parentMappings(next, perBaseCount) {
4516
+ return selectedParents(next).map((parent, parentIndex) => ({
4517
+ parent,
4518
+ output_indexes: Array.from({ length: perBaseCount }, (_value, index) => parentIndex * perBaseCount + index)
4519
+ }));
4520
+ }
4521
+ function buildHandoff(project, id, prompt, count, perBaseCount, next, inputs) {
4522
+ const parent = next.next_asset;
4523
+ if (!parent) throw new GenerationReceiptError("Missing lineage next base");
4524
+ const parents = parentMappings(next, perBaseCount);
4525
+ const outputManifest = createGenerationOutputManifestDraft({ id, expected_output_count: count, inputs });
4526
+ 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`;
4527
+ return {
4528
+ schema_version: "lineage.generation_handoff.v2",
4529
+ provider,
4530
+ project,
4531
+ job_id: id,
4532
+ prompt,
4533
+ expected_output_count: count,
4534
+ per_base_count: next.selection_mode === "multiple" ? perBaseCount : void 0,
4535
+ lineage: {
4536
+ root_asset_id: next.root_asset_id,
4537
+ parent_asset_id: parent.asset_id,
4538
+ selection_strategy: next.strategy,
4539
+ parent_title: parent.title,
4540
+ parent_local_path: parent.local_path,
4541
+ parent_s3_key: parent.s3_key,
4542
+ parents: parents.length > 1 ? parents.map((mapping) => ({
4543
+ parent_asset_id: mapping.parent.asset_id,
4544
+ parent_title: mapping.parent.title,
4545
+ parent_local_path: mapping.parent.local_path,
4546
+ parent_s3_key: mapping.parent.s3_key,
4547
+ output_indexes: mapping.output_indexes
4548
+ })) : void 0
4549
+ },
4550
+ instructions: [
4551
+ "Use Codex image generation outside Lineage server code.",
4552
+ "Write generated output files under .asset-scratch before import.",
4553
+ "Do not call live provider APIs from the CLI or server.",
4554
+ "Fill every output_manifest entry with its generated file path and a one- or two-word edge summary.",
4555
+ "Import the completed manifest with --confirm-write to persist every lineage child."
4556
+ ],
4557
+ import_command: importCommand,
4558
+ output_manifest: outputManifest,
4559
+ guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
4560
+ };
4561
+ }
4132
4562
  function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
4133
4563
  const importCommand = `${lineagePublicPackageCommand()} reroll import --project ${quote(project)} --job-id ${quote(id)} --file <.asset-scratch-file> --confirm-write ${lineageRuntimeSelector()} --json`;
4134
4564
  return {
@@ -4157,6 +4587,19 @@ function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
4157
4587
  guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
4158
4588
  };
4159
4589
  }
4590
+ function inputsFrom(jobIdValue, project, next) {
4591
+ return selectedParents(next).map((parent, position) => ({
4592
+ id: `${jobIdValue}:input:${position}`,
4593
+ job_id: jobIdValue,
4594
+ project_id: project,
4595
+ asset_id: parent.asset_id,
4596
+ root_asset_id: next.root_asset_id,
4597
+ role: "lineage_next_base",
4598
+ position,
4599
+ selection_strategy: next.strategy,
4600
+ selection_snapshot: next
4601
+ }));
4602
+ }
4160
4603
  function receiptFrom(row) {
4161
4604
  return {
4162
4605
  id: String(row.id),
@@ -4169,6 +4612,7 @@ function receiptFrom(row) {
4169
4612
  };
4170
4613
  }
4171
4614
  function outputFrom(row) {
4615
+ const edgeSummary = typeof row.edge_summary === "string" && row.edge_summary.length > 0 ? row.edge_summary : void 0;
4172
4616
  return {
4173
4617
  id: String(row.id),
4174
4618
  job_id: String(row.job_id),
@@ -4180,7 +4624,8 @@ function outputFrom(row) {
4180
4624
  content_type: String(row.content_type),
4181
4625
  imported_asset_id: String(row.imported_asset_id),
4182
4626
  parent_asset_id: String(row.parent_asset_id),
4183
- imported_at: String(row.imported_at)
4627
+ imported_at: String(row.imported_at),
4628
+ ...edgeSummary ? { edge_summary: edgeSummary } : {}
4184
4629
  };
4185
4630
  }
4186
4631
  function loadGenerationJob(database, project, id) {
@@ -4226,6 +4671,72 @@ function insertReceipt(database, id, type, command, payload) {
4226
4671
  values (?, ?, ?, 'ok', ?, ?, ?)
4227
4672
  `).run(`${id}:receipt:${type}:${Date.now()}`, id, type, command, JSON.stringify(payload), nowIso());
4228
4673
  }
4674
+ function planImageGeneration(project = defaultProject, fields) {
4675
+ const prompt = fields.prompt.trim();
4676
+ if (!prompt) throw new GenerationReceiptError("Missing --prompt");
4677
+ if (!fields.fromLineageSelection) throw new GenerationReceiptError("Generation v1 requires --from-lineage-selection");
4678
+ const next = resolveLineageSelection(project);
4679
+ const parentCount = selectedParents(next).length;
4680
+ if (parentCount > 1 && !positiveInteger(fields.perBaseCount)) throw new GenerationReceiptError("Multi-parent generation requires --per-base-count");
4681
+ const perBaseCount = parentCount > 1 ? Number(fields.perBaseCount) : Number(fields.count ?? fields.perBaseCount);
4682
+ const count = parentCount * perBaseCount;
4683
+ if (!positiveInteger(perBaseCount)) throw new GenerationReceiptError("Generation count must be a positive integer");
4684
+ if (fields.count !== void 0 && fields.count !== count) throw new GenerationReceiptError(`Generation count mismatch: expected ${count} from selected bases, received ${fields.count}`);
4685
+ const id = jobId();
4686
+ const inputs = inputsFrom(id, project, next);
4687
+ const handoff = buildHandoff(project, id, prompt, count, perBaseCount, next, inputs);
4688
+ const mappings = parentMappings(next, perBaseCount).map((mapping) => ({ parent_asset_id: mapping.parent.asset_id, output_indexes: mapping.output_indexes }));
4689
+ const timestamp = nowIso();
4690
+ const preview = {
4691
+ id,
4692
+ project_id: project,
4693
+ provider,
4694
+ adapter_version: manifestAdapterVersion,
4695
+ source_mode: "lineage_selection",
4696
+ root_asset_id: next.root_asset_id,
4697
+ prompt,
4698
+ expected_output_count: count,
4699
+ status: "planned",
4700
+ output_dir: ".asset-scratch",
4701
+ handoff,
4702
+ created_at: timestamp,
4703
+ updated_at: timestamp,
4704
+ inputs,
4705
+ outputs: [],
4706
+ receipts: [{
4707
+ id: `${id}:receipt:plan:preview`,
4708
+ job_id: id,
4709
+ receipt_type: "plan",
4710
+ status: "ok",
4711
+ command: "generate image plan",
4712
+ payload: { prompt, expected_output_count: count, per_base_count: parentCount > 1 ? perBaseCount : void 0, lineage: handoff.lineage, parent_mappings: mappings },
4713
+ created_at: timestamp
4714
+ }]
4715
+ };
4716
+ if (fields.dryRun) return { ok: true, command: "generate image plan", project, dryRun: true, wouldWrite: true, job: preview };
4717
+ const database = lineageDb();
4718
+ try {
4719
+ database.exec("BEGIN IMMEDIATE");
4720
+ try {
4721
+ database.prepare(`
4722
+ insert into generation_jobs (
4723
+ id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
4724
+ expected_output_count, status, output_dir, handoff_json, created_at, updated_at
4725
+ ) values (?, ?, ?, ?, 'lineage_selection', ?, ?, ?, 'planned', ?, ?, ?, ?)
4726
+ `).run(id, project, provider, manifestAdapterVersion, next.root_asset_id, prompt, count, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
4727
+ 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 (?, ?, ?, ?, ?, ?, ?, ?, ?)");
4728
+ 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));
4729
+ insertReceipt(database, id, "plan", "generate image plan", preview.receipts[0].payload);
4730
+ database.exec("COMMIT");
4731
+ } catch (error) {
4732
+ database.exec("ROLLBACK");
4733
+ throw error;
4734
+ }
4735
+ return { ok: true, command: "generate image plan", project, job: loadGenerationJob(database, project, id) };
4736
+ } finally {
4737
+ database.close();
4738
+ }
4739
+ }
4229
4740
  function planImageReroll(project = defaultProject, fields) {
4230
4741
  const prompt = fields.prompt.trim();
4231
4742
  if (!prompt) throw new GenerationReceiptError("Missing --prompt");
@@ -4270,7 +4781,7 @@ function planImageReroll(project = defaultProject, fields) {
4270
4781
  id,
4271
4782
  project_id: project,
4272
4783
  provider,
4273
- adapter_version: adapterVersion,
4784
+ adapter_version: legacyAdapterVersion,
4274
4785
  source_mode: "lineage_reroll",
4275
4786
  root_asset_id: snapshot.root_asset_id,
4276
4787
  prompt,
@@ -4302,7 +4813,7 @@ function planImageReroll(project = defaultProject, fields) {
4302
4813
  id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
4303
4814
  expected_output_count, status, output_dir, handoff_json, created_at, updated_at
4304
4815
  ) values (?, ?, ?, ?, 'lineage_reroll', ?, ?, 1, 'planned', ?, ?, ?, ?)
4305
- `).run(id, project, provider, adapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
4816
+ `).run(id, project, provider, legacyAdapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
4306
4817
  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));
4307
4818
  insertReceipt(database, id, "plan", "reroll plan", preview.receipts[0].payload);
4308
4819
  database.exec("COMMIT");
@@ -4315,14 +4826,66 @@ function planImageReroll(project = defaultProject, fields) {
4315
4826
  database.close();
4316
4827
  }
4317
4828
  }
4829
+ function parentForOutput(job, outputIndex) {
4830
+ const inputs = job.inputs.filter((input) => input.role === "lineage_next_base");
4831
+ if (inputs.length === 0) throw new GenerationReceiptError("Generation job has no lineage_next_base input");
4832
+ if (inputs.length === 1) return inputs[0].asset_id;
4833
+ if (job.expected_output_count % inputs.length !== 0) throw new GenerationReceiptError("Generation job has invalid parent mapping");
4834
+ return inputs[Math.floor(outputIndex / (job.expected_output_count / inputs.length))]?.asset_id || inputs[inputs.length - 1].asset_id;
4835
+ }
4836
+ function parentInputs(job) {
4837
+ const inputs = job.inputs.filter((input) => input.role === "lineage_next_base");
4838
+ if (inputs.length === 0) throw new GenerationReceiptError("Generation job has no lineage_next_base input");
4839
+ return inputs;
4840
+ }
4841
+ function parentFilesFor(job, parentFiles) {
4842
+ const inputs = parentInputs(job);
4843
+ const expectedPerParent = job.expected_output_count / inputs.length;
4844
+ if (!Number.isInteger(expectedPerParent)) throw new GenerationReceiptError("Generation job has invalid parent mapping");
4845
+ const allowedParents = new Set(inputs.map((input) => input.asset_id));
4846
+ const seenParents = /* @__PURE__ */ new Set();
4847
+ const mapped = [];
4848
+ for (const parentAssetId of Object.keys(parentFiles)) {
4849
+ if (!allowedParents.has(parentAssetId)) throw new GenerationReceiptError(`Unknown generation parent mapping: ${parentAssetId}`);
4850
+ if (seenParents.has(parentAssetId)) throw new GenerationReceiptError(`Duplicate generation parent mapping: ${parentAssetId}`);
4851
+ seenParents.add(parentAssetId);
4852
+ }
4853
+ for (const input of inputs) {
4854
+ const files = (parentFiles[input.asset_id] || []).map((file) => file.trim()).filter(Boolean);
4855
+ if (files.length === 0) throw new GenerationReceiptError(`Missing generation parent mapping for ${input.asset_id}`);
4856
+ if (files.length !== expectedPerParent) throw new GenerationReceiptError(`Parent ${input.asset_id} requires ${expectedPerParent} output file${expectedPerParent === 1 ? "" : "s"}, received ${files.length}`);
4857
+ for (const file of files) mapped.push({ file, parentAssetId: input.asset_id });
4858
+ }
4859
+ return mapped;
4860
+ }
4861
+ function orderedFilesFor(job, files) {
4862
+ return files.map((file, index) => ({ file, parentAssetId: parentForOutput(job, index) }));
4863
+ }
4864
+ function inspectImageGeneration(project = defaultProject, jobIdValue) {
4865
+ if (!jobIdValue) throw new GenerationReceiptError("Missing --job-id");
4866
+ const database = lineageDb();
4867
+ try {
4868
+ return { ok: true, command: "generate image inspect", project, job: loadGenerationJob(database, project, jobIdValue) };
4869
+ } finally {
4870
+ database.close();
4871
+ }
4872
+ }
4318
4873
  function isPathInside(child, parent) {
4319
4874
  const rel = relative3(parent, child);
4320
4875
  return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
4321
4876
  }
4322
- function resolveScratchFile(file) {
4877
+ function scratchCandidate(file) {
4323
4878
  const scratchRoot = resolve5(repoRoot, ".asset-scratch");
4324
4879
  const candidate = file.startsWith(".asset-scratch/") || resolve5(file).startsWith(scratchRoot) ? resolve5(repoRoot, file) : resolve5(scratchRoot, file);
4325
4880
  if (!isPathInside(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
4881
+ return { candidate, scratchRoot };
4882
+ }
4883
+ function resolveScratchManifestPath(file) {
4884
+ const { candidate, scratchRoot } = scratchCandidate(file);
4885
+ return relative3(scratchRoot, candidate);
4886
+ }
4887
+ function resolveScratchFile(file) {
4888
+ const { candidate, scratchRoot } = scratchCandidate(file);
4326
4889
  if (!existsSync8(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
4327
4890
  const realScratchRoot = realpathSync2(scratchRoot);
4328
4891
  const realCandidate = realpathSync2(candidate);
@@ -4338,6 +4901,165 @@ function resolveScratchFile(file) {
4338
4901
  assetId: `local-${checksum.slice(0, 12)}`
4339
4902
  };
4340
4903
  }
4904
+ function generationManifestConflict() {
4905
+ throw new GenerationReceiptError("Generation import already exists with different output, summary, or provenance", 409);
4906
+ }
4907
+ function confirmManifestRetry(project, job, manifest) {
4908
+ if (job.outputs.length !== manifest.outputs.length) generationManifestConflict();
4909
+ const database = lineageDb();
4910
+ try {
4911
+ for (const expected of manifest.outputs) {
4912
+ const recorded = job.outputs.find((output) => output.output_index === expected.output_index);
4913
+ if (!recorded || recorded.file_path !== expected.file_path || recorded.parent_asset_id !== expected.parent_asset_id || recorded.edge_summary !== expected.edge_summary) generationManifestConflict();
4914
+ const edge = database.prepare(`
4915
+ select summary, summary_created_by, summary_updated_by, summary_updated_at
4916
+ from asset_edges
4917
+ where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
4918
+ `).get(project, recorded.parent_asset_id, recorded.imported_asset_id);
4919
+ 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();
4920
+ }
4921
+ return { ok: true, command: "generate image import", project, job, imported: job.outputs, idempotent: true };
4922
+ } finally {
4923
+ database.close();
4924
+ }
4925
+ }
4926
+ function importImageGenerationOutputs(project = defaultProject, fields) {
4927
+ if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
4928
+ if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
4929
+ const database = lineageDb();
4930
+ let job;
4931
+ try {
4932
+ job = loadGenerationJob(database, project, fields.jobId);
4933
+ } finally {
4934
+ database.close();
4935
+ }
4936
+ if (job.source_mode !== "lineage_selection") throw new GenerationReceiptError(`Generation job is not an image-selection job: ${job.source_mode}`);
4937
+ const hasManifestInput = fields.manifest !== void 0;
4938
+ const hasParentFilesInput = fields.parentFiles !== void 0;
4939
+ const hasFilesInput = fields.files !== void 0;
4940
+ if (hasManifestInput && (hasParentFilesInput || hasFilesInput)) {
4941
+ throw new GenerationReceiptError("Use --manifest or legacy --files/--parent-files, not both");
4942
+ }
4943
+ if (hasParentFilesInput && hasFilesInput) throw new GenerationReceiptError("Use --files or --parent-files, not both");
4944
+ let manifest;
4945
+ let parentFileRows;
4946
+ let mappingStrategy;
4947
+ if (job.adapter_version === manifestAdapterVersion) {
4948
+ if (!hasManifestInput) throw new GenerationReceiptError("New generation jobs require --manifest");
4949
+ manifest = parseGenerationOutputManifest(fields.manifest, job, { resolveFilePath: resolveScratchManifestPath });
4950
+ if (job.status === "imported") return confirmManifestRetry(project, job, manifest);
4951
+ if (job.status !== "planned") throw new GenerationReceiptError(`Generation job is not importable from status: ${job.status}`);
4952
+ parentFileRows = manifest.outputs.map((output) => ({
4953
+ edgeSummary: output.edge_summary,
4954
+ file: output.file_path,
4955
+ parentAssetId: output.parent_asset_id
4956
+ }));
4957
+ mappingStrategy = "generation_output_manifest_v1";
4958
+ } else if (job.adapter_version === legacyAdapterVersion) {
4959
+ if (hasManifestInput) throw new GenerationReceiptError("Already-planned legacy generation jobs require --files or --parent-files");
4960
+ if (job.status !== "planned") throw new GenerationReceiptError(`Generation job is not importable from status: ${job.status}`);
4961
+ const hasExplicitParentFiles = Boolean(fields.parentFiles && Object.keys(fields.parentFiles).length > 0);
4962
+ parentFileRows = hasExplicitParentFiles ? parentFilesFor(job, fields.parentFiles || {}) : orderedFilesFor(job, (fields.files || []).map((file) => file.trim()).filter(Boolean));
4963
+ mappingStrategy = hasExplicitParentFiles ? "explicit_parent_files" : "ordered_per_base";
4964
+ } else {
4965
+ throw new GenerationReceiptError(`Unsupported generation adapter version: ${job.adapter_version}`);
4966
+ }
4967
+ if (parentFileRows.length === 0) throw new GenerationReceiptError("Generation import requires --files or --parent-files");
4968
+ if (parentFileRows.length !== job.expected_output_count) {
4969
+ throw new GenerationReceiptError(`Output count mismatch: expected ${job.expected_output_count}, received ${parentFileRows.length}`);
4970
+ }
4971
+ const resolved = parentFileRows.map((row) => ({ ...resolveScratchFile(row.file), edgeSummary: row.edgeSummary, parentAssetId: row.parentAssetId }));
4972
+ const uniquePaths = new Set(resolved.map((file) => file.relativePath));
4973
+ if (uniquePaths.size !== resolved.length) throw new GenerationReceiptError("Generation import files must be unique");
4974
+ cancelLineageIterateTasksForAssets(project, {
4975
+ actor: "system",
4976
+ confirmWrite: false,
4977
+ rootAssetId: job.root_asset_id
4978
+ });
4979
+ indexLineageAssets(project);
4980
+ const writeDb = lineageDb();
4981
+ try {
4982
+ const timestamp = nowIso();
4983
+ writeDb.exec("BEGIN IMMEDIATE");
4984
+ try {
4985
+ for (const [index, file] of resolved.entries()) {
4986
+ const assetRow = writeDb.prepare("select id from assets where project_id = ? and id = ?").get(project, file.assetId);
4987
+ if (!assetRow) throw new GenerationReceiptError(`Indexed local asset was not found: ${file.relativePath}`);
4988
+ const outputId = `${fields.jobId}:output:${index}`;
4989
+ writeDb.prepare(`insert into generation_job_outputs (
4990
+ id, job_id, project_id, output_index, file_path, checksum_sha256, size_bytes, content_type,
4991
+ imported_asset_id, parent_asset_id, imported_at, edge_summary
4992
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
4993
+ outputId,
4994
+ fields.jobId,
4995
+ project,
4996
+ index,
4997
+ file.relativePath,
4998
+ file.checksum,
4999
+ file.size,
5000
+ file.contentType,
5001
+ file.assetId,
5002
+ file.parentAssetId,
5003
+ timestamp,
5004
+ file.edgeSummary || null
5005
+ );
5006
+ const insertedEdge = writeDb.prepare(`insert into asset_edges (
5007
+ id, project_id, parent_asset_id, child_asset_id, relation_type, created_at,
5008
+ summary, summary_created_by, summary_updated_by, summary_updated_at
5009
+ ) values (?, ?, ?, ?, 'derived_from', ?, ?, ?, ?, ?)
5010
+ on conflict(project_id, parent_asset_id, child_asset_id, relation_type) do nothing`).run(
5011
+ `${project}:${file.parentAssetId}:derived_from:${file.assetId}`,
5012
+ project,
5013
+ file.parentAssetId,
5014
+ file.assetId,
5015
+ timestamp,
5016
+ file.edgeSummary || null,
5017
+ file.edgeSummary ? "agent" : null,
5018
+ file.edgeSummary ? "agent" : null,
5019
+ file.edgeSummary ? timestamp : null
5020
+ );
5021
+ if (file.edgeSummary && insertedEdge.changes === 0) {
5022
+ const edge = writeDb.prepare(`
5023
+ select summary, summary_created_by, summary_updated_by, summary_updated_at
5024
+ from asset_edges
5025
+ where project_id = ? and parent_asset_id = ? and child_asset_id = ? and relation_type = 'derived_from'
5026
+ `).get(project, file.parentAssetId, file.assetId);
5027
+ 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();
5028
+ }
5029
+ }
5030
+ writeDb.prepare(`
5031
+ update generation_jobs
5032
+ set status = 'imported', imported_at = ?, updated_at = ?
5033
+ where project_id = ? and id = ?
5034
+ `).run(timestamp, timestamp, project, fields.jobId);
5035
+ insertReceipt(writeDb, fields.jobId, "import", "generate image import", {
5036
+ mapping_strategy: mappingStrategy,
5037
+ files: resolved.map((file, index) => ({
5038
+ output_index: index,
5039
+ file_path: file.relativePath,
5040
+ imported_asset_id: file.assetId,
5041
+ parent_asset_id: file.parentAssetId,
5042
+ ...file.edgeSummary ? { edge_summary: file.edgeSummary } : {}
5043
+ })),
5044
+ selection_reset: { root_asset_id: job.root_asset_id, cleared: true }
5045
+ });
5046
+ writeDb.prepare("delete from asset_selections where project_id = ? and root_asset_id = ?").run(project, job.root_asset_id);
5047
+ writeDb.exec("COMMIT");
5048
+ } catch (error) {
5049
+ writeDb.exec("ROLLBACK");
5050
+ throw error;
5051
+ }
5052
+ cancelLineageIterateTasksForAssets(project, {
5053
+ actor: "system",
5054
+ confirmWrite: true,
5055
+ rootAssetId: job.root_asset_id
5056
+ });
5057
+ const importedJob = loadGenerationJob(writeDb, project, fields.jobId);
5058
+ return { ok: true, command: "generate image import", project, job: importedJob, imported: importedJob.outputs };
5059
+ } finally {
5060
+ writeDb.close();
5061
+ }
5062
+ }
4341
5063
  function importImageRerollOutput(project = defaultProject, fields) {
4342
5064
  if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
4343
5065
  if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
@@ -5178,6 +5900,9 @@ function getLineageRuntimeInfo(options = {}) {
5178
5900
  const code = options.code || getLineageCodeIdentity(channel);
5179
5901
  const databaseInfo = { exists: existsSync10(dbPath), path: dbPath };
5180
5902
  const schema = { migration_keys: [] };
5903
+ const serviceInstanceId = process.env.LINEAGE_SERVICE_INSTANCE_ID;
5904
+ const launcherPid = process.env.LINEAGE_LAUNCHER_PID ? Number(process.env.LINEAGE_LAUNCHER_PID) : void 0;
5905
+ const isServiceProcess = Boolean(serviceInstanceId && Number.isInteger(launcherPid) && launcherPid > 0);
5181
5906
  if (databaseInfo.exists) {
5182
5907
  try {
5183
5908
  const stat = statSync6(dbPath);
@@ -5215,14 +5940,22 @@ function getLineageRuntimeInfo(options = {}) {
5215
5940
  git_sha: code.git_sha,
5216
5941
  node_env: process.env.NODE_ENV,
5217
5942
  package_name: info.name,
5218
- profile: runtimeProfileIdentity(channel),
5219
- schema,
5220
- service: {
5221
- instance_id: process.env.LINEAGE_SERVICE_INSTANCE_ID,
5222
- launcher_pid: process.env.LINEAGE_LAUNCHER_PID ? Number(process.env.LINEAGE_LAUNCHER_PID) : void 0,
5943
+ process: {
5223
5944
  pid: process.pid,
5945
+ role: isServiceProcess ? "service" : "command",
5224
5946
  started_at: processStartedAt
5225
5947
  },
5948
+ profile: runtimeProfileIdentity(channel),
5949
+ schema,
5950
+ ...isServiceProcess ? {
5951
+ service: {
5952
+ instance_id: serviceInstanceId,
5953
+ launcher_pid: launcherPid,
5954
+ mode: process.env.LINEAGE_SERVICE_MODE === "managed" ? "managed" : "foreground",
5955
+ pid: process.pid,
5956
+ started_at: processStartedAt
5957
+ }
5958
+ } : {},
5226
5959
  version: info.version
5227
5960
  };
5228
5961
  }
@@ -5241,22 +5974,23 @@ var ManagedWriterRoutingError = class extends Error {
5241
5974
  status;
5242
5975
  };
5243
5976
  function managedWriterTimeoutMs(command, args) {
5244
- const subcommand = args.find((arg) => !arg.startsWith("-")) || "";
5245
- return command === "reroll" && subcommand === "import" ? 5 * 6e4 : 3e4;
5977
+ const positions = args.filter((arg) => !arg.startsWith("-"));
5978
+ const slowImport = command === "reroll" && positions[0] === "import" || command === "generate" && positions[0] === "image" && positions[1] === "import";
5979
+ return slowImport ? 5 * 6e4 : 3e4;
5246
5980
  }
5247
5981
  function errorMessage(body, fallback) {
5248
5982
  if (!body || typeof body !== "object" || Array.isArray(body)) return fallback;
5249
- const record = body;
5250
- if (typeof record.message === "string") return record.message;
5251
- if (typeof record.error === "string") return record.error;
5983
+ const record2 = body;
5984
+ if (typeof record2.message === "string") return record2.message;
5985
+ if (typeof record2.error === "string") return record2.error;
5252
5986
  return fallback;
5253
5987
  }
5254
5988
  function agentClaimError(body, status) {
5255
5989
  if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
5256
- const record = body;
5257
- if (typeof record.message !== "string" || typeof record.error !== "string") return void 0;
5258
- const conflicts = Array.isArray(record.conflicts) ? record.conflicts : [];
5259
- return new AgentClaimError(record.message, status, record.error, conflicts);
5990
+ const record2 = body;
5991
+ if (typeof record2.message !== "string" || typeof record2.error !== "string") return void 0;
5992
+ const conflicts = Array.isArray(record2.conflicts) ? record2.conflicts : [];
5993
+ return new AgentClaimError(record2.message, status, record2.error, conflicts);
5260
5994
  }
5261
5995
  function assertResponseIdentity(body, profile, channel) {
5262
5996
  if (!body || typeof body !== "object" || Array.isArray(body)) {
@@ -5354,7 +6088,30 @@ function readOptions(args, name) {
5354
6088
  }
5355
6089
  return values;
5356
6090
  }
6091
+ function readJsonFile(path, label) {
6092
+ try {
6093
+ return JSON.parse(readFileSync6(resolve8(path), "utf8"));
6094
+ } catch (error) {
6095
+ throw new Error(`${label} must be a readable JSON file: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
6096
+ }
6097
+ }
6098
+ function parentFilesOption(value) {
6099
+ if (value === void 0) return void 0;
6100
+ const mappings = {};
6101
+ for (const rawMapping of value.split(";")) {
6102
+ const separator = rawMapping.indexOf("=");
6103
+ const parent = separator >= 0 ? rawMapping.slice(0, separator).trim() : "";
6104
+ const files = separator >= 0 ? rawMapping.slice(separator + 1).split(",").map((file) => file.trim()).filter(Boolean) : [];
6105
+ if (!parent || files.length === 0) throw new Error("lineage generate image import --parent-files requires parent=file[,file][;parent=file]");
6106
+ if (Object.hasOwn(mappings, parent)) throw new Error(`Duplicate generation parent mapping: ${parent}`);
6107
+ mappings[parent] = files;
6108
+ }
6109
+ return mappings;
6110
+ }
5357
6111
  function resolveStartOptions(config, args) {
6112
+ if (!profileSelector(args)) {
6113
+ throw new Error("Start requires --profile or LINEAGE_PROFILE. Create one with `profile init --profile <id> --confirm-write`, then start again with that profile");
6114
+ }
5358
6115
  const profile = prepareCliProfile(config, args);
5359
6116
  const serviceUrl = profile ? new URL(profile.service_origin) : void 0;
5360
6117
  const rawPort = readOption(args, "--port") || process.env.PORT || serviceUrl?.port || String(config.defaultPort);
@@ -5434,7 +6191,8 @@ function formatLineageHelp(config) {
5434
6191
  return `${config.binName} ${packageVersion()}
5435
6192
 
5436
6193
  Usage:
5437
- ${config.binName} start [--profile <id-or-manifest>] [--port <port>] [--host <host>] [--db <path>] [--asset-root <path>] [--open] [--json]
6194
+ ${config.binName} start --profile <id-or-manifest> [--open] [--json]
6195
+ ${config.binName} profile init --profile <new-profile-id> [--service-origin <http-origin>] --confirm-write [--json]
5438
6196
  ${config.binName} profile doctor --profile <id-or-manifest> [--json]
5439
6197
  ${config.binName} profile bind --profile <id-or-manifest> --confirm-write [--json]
5440
6198
  ${config.binName} profile clone --source-db <snapshot-source> --target-profile <id-or-manifest> --confirm-write [--json]
@@ -5446,7 +6204,11 @@ Usage:
5446
6204
  ${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
5447
6205
  ${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
5448
6206
  ${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]
5449
- ${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
6207
+ ${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]
6208
+ ${config.binName} generate image plan --prompt <text> --from-lineage-selection [--count <count>|--per-base-count <count>] [--project <project>] [--dry-run] [--db <path>] [--json]
6209
+ ${config.binName} generate image inspect --job-id <job-id> [--project <project>] [--db <path>] [--json]
6210
+ ${config.binName} generate image import --job-id <job-id> --manifest <json-file> --confirm-write [--project <project>] [--db <path>] [--json]
6211
+ ${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]
5450
6212
  ${config.binName} reroll list --root <asset-id> [--project <project>] [--db <path>] [--json]
5451
6213
  ${config.binName} reroll mark --root <asset-id> --target <asset-id> [--notes <text>] [--requested-by agent|human|system] [--project <project>] [--confirm-write] [--db <path>] [--json]
5452
6214
  ${config.binName} reroll cancel --root <asset-id> --target <asset-id> [--project <project>] [--confirm-write] [--db <path>] [--json]
@@ -5568,7 +6330,8 @@ function resolveDataCommandOptions(args) {
5568
6330
  dbPath: readOption(args, "--db"),
5569
6331
  json: args.includes("--json"),
5570
6332
  project: readOption(args, "--project") || process.env.LINEAGE_DEFAULT_PRODUCT || defaultProduct,
5571
- rootAssetId: readOption(args, "--root")
6333
+ rootAssetId: readOption(args, "--root"),
6334
+ summary: readOption(args, "--summary")
5572
6335
  };
5573
6336
  if (options.dbPath) process.env.LINEAGE_DB = options.dbPath;
5574
6337
  return options;
@@ -5611,13 +6374,57 @@ function runLineageDataCommand(command, args) {
5611
6374
  }
5612
6375
  if (command === "link-child") {
5613
6376
  if (!options.childAssetId) throw new Error("lineage link-child requires --child");
6377
+ const summary = requireEdgeSummary(options.summary);
5614
6378
  return linkSelectedLineageChild(options.project, {
5615
6379
  childAssetId: options.childAssetId,
5616
6380
  claimToken: options.claimToken,
5617
6381
  confirmWrite: options.confirmWrite,
5618
- rootAssetId: options.rootAssetId || options.assetId
6382
+ rootAssetId: options.rootAssetId || options.assetId,
6383
+ summary,
6384
+ summaryActor: "agent"
5619
6385
  });
5620
6386
  }
6387
+ if (command === "generate") {
6388
+ const [resource, subcommand] = positionalArgs(args);
6389
+ if (resource !== "image") throw new Error(`Unknown generate command: ${resource}`);
6390
+ if (subcommand === "plan") {
6391
+ const prompt = readOption(args, "--prompt");
6392
+ const rawCount = readOption(args, "--count");
6393
+ const rawPerBaseCount = readOption(args, "--per-base-count");
6394
+ if (!prompt) throw new Error("lineage generate image plan requires --prompt");
6395
+ return planImageGeneration(options.project, {
6396
+ count: rawCount === void 0 ? void 0 : Number(rawCount),
6397
+ dryRun: args.includes("--dry-run"),
6398
+ fromLineageSelection: args.includes("--from-lineage-selection"),
6399
+ perBaseCount: rawPerBaseCount === void 0 ? void 0 : Number(rawPerBaseCount),
6400
+ prompt
6401
+ });
6402
+ }
6403
+ if (subcommand === "inspect") {
6404
+ const jobId2 = readOption(args, "--job-id");
6405
+ if (!jobId2) throw new Error("lineage generate image inspect requires --job-id");
6406
+ return inspectImageGeneration(options.project, jobId2);
6407
+ }
6408
+ if (subcommand === "import") {
6409
+ const jobId2 = readOption(args, "--job-id");
6410
+ const manifestPath = readOption(args, "--manifest");
6411
+ const filesValue = readOption(args, "--files");
6412
+ const parentFilesValue = readOption(args, "--parent-files");
6413
+ if (!jobId2) throw new Error("lineage generate image import requires --job-id");
6414
+ if (manifestPath !== void 0 && (filesValue !== void 0 || parentFilesValue !== void 0)) {
6415
+ throw new Error("Use --manifest or legacy --files/--parent-files, not both");
6416
+ }
6417
+ if (filesValue !== void 0 && parentFilesValue !== void 0) throw new Error("Use --files or --parent-files, not both");
6418
+ return importImageGenerationOutputs(options.project, {
6419
+ confirmWrite: options.confirmWrite,
6420
+ files: filesValue === void 0 ? void 0 : filesValue.split(",").map((file) => file.trim()).filter(Boolean),
6421
+ jobId: jobId2,
6422
+ manifest: manifestPath === void 0 ? void 0 : readJsonFile(manifestPath, "Generation output manifest"),
6423
+ parentFiles: parentFilesOption(parentFilesValue)
6424
+ });
6425
+ }
6426
+ throw new Error(`Unknown generate image command: ${subcommand}`);
6427
+ }
5621
6428
  if (command === "reroll") {
5622
6429
  const subcommand = positionalArgs(args)[0] || "";
5623
6430
  if (subcommand === "list") {
@@ -5771,6 +6578,15 @@ function printDataResult(command, result, json) {
5771
6578
  if (link.warning) console.log(`Warning: ${link.warning}`);
5772
6579
  return;
5773
6580
  }
6581
+ if (command === "generate" && result && typeof result === "object" && "job" in result) {
6582
+ const generation = result;
6583
+ if (generation.imported) {
6584
+ console.log(`${generation.idempotent ? "Already imported" : "Imported"} ${generation.imported.length} output(s) for ${generation.job?.id || "job"}`);
6585
+ } else {
6586
+ console.log(`Generation job ${generation.job?.id || "job"} ${generation.job?.status || "unknown"}`);
6587
+ }
6588
+ return;
6589
+ }
5774
6590
  if (command === "reroll" && result && typeof result === "object") {
5775
6591
  if ("requests" in result) {
5776
6592
  const listed = result;
@@ -5817,6 +6633,25 @@ function runLineageDbCommand(config, command, args) {
5817
6633
  function runLineageProfileCommand(config, command, args) {
5818
6634
  const runtime = getLineageRuntimeInfo({ channel: config.channel });
5819
6635
  const runtimeIdentity = { channel: config.channel, code: runtime.code, gitSha: runtime.git_sha, version: runtime.version };
6636
+ if (command === "init") {
6637
+ if (hasOption(args, "--db")) throw new Error("Profile init cannot be combined with --db");
6638
+ if (hasOption(args, "--asset-root")) throw new Error("Profile init cannot be combined with --asset-root");
6639
+ if (!args.includes("--confirm-write")) throw new Error("Profile init requires --confirm-write");
6640
+ const profileId = readOption(args, "--profile");
6641
+ if (!profileId) throw new Error("lineage profile init requires --profile");
6642
+ if (process.env.LINEAGE_PROFILE && process.env.LINEAGE_PROFILE !== profileId) {
6643
+ throw new Error("Profile init requires an explicit --profile that does not conflict with LINEAGE_PROFILE");
6644
+ }
6645
+ const serviceOrigin = readOption(args, "--service-origin") || `http://${config.defaultHost}:${config.defaultPort}`;
6646
+ return initializeLineageProfile(profileId, serviceOrigin, runtimeIdentity, true, (profile, initialize) => {
6647
+ const writerLease = acquireProfileWriterLease(profile, config.channel, "cli");
6648
+ try {
6649
+ return initialize();
6650
+ } finally {
6651
+ writerLease.release();
6652
+ }
6653
+ });
6654
+ }
5820
6655
  if (command === "clone") {
5821
6656
  const source = readOption(args, "--source-db");
5822
6657
  const target = readOption(args, "--target-profile");
@@ -5875,7 +6710,10 @@ function printProfileResult(result, json) {
5875
6710
  console.log(JSON.stringify(result, null, 2));
5876
6711
  return;
5877
6712
  }
5878
- if (result.schema_version === "lineage.profile_doctor.v1") {
6713
+ if (result.schema_version === "lineage.profile_init.v1") {
6714
+ console.log(`Initialized ${result.profile_id} at ${result.manifest_path}`);
6715
+ console.log(`Service: ${result.service_origin}`);
6716
+ } else if (result.schema_version === "lineage.profile_doctor.v1") {
5879
6717
  console.log(`Profile doctor: ${result.ok ? "ok" : "failed"}`);
5880
6718
  for (const check of result.checks) console.log(`${check.status.toUpperCase()} ${check.id}: ${check.message}`);
5881
6719
  } else if (result.schema_version === "lineage.profile_bind.v1") {
@@ -5916,7 +6754,9 @@ function printRuntimeResult(result, json) {
5916
6754
  }
5917
6755
  function lineageCliRequiresWriterLease(command, args) {
5918
6756
  if (command === "next" || command === "brief" || command === "inspect" || command === "selection") return false;
5919
- const subcommand = positionalArgs(args)[0] || "";
6757
+ const positions = positionalArgs(args);
6758
+ const subcommand = positions[0] || "";
6759
+ if (command === "generate") return positions[0] !== "image" || positions[1] !== "inspect";
5920
6760
  if (command === "reroll") return subcommand !== "list";
5921
6761
  if (command === "tasks") return subcommand !== "list" && subcommand !== "inspect";
5922
6762
  if (command === "db") return subcommand !== "info";
@@ -6104,7 +6944,6 @@ function start(config, args) {
6104
6944
  } else {
6105
6945
  console.log(`${config.displayName} starting at ${url}`);
6106
6946
  if (options.profile) console.log(`Profile: ${options.profile.profile_id} (${options.profile.environment})`);
6107
- else console.warn("Warning: legacy-unbound runtime; database and asset paths are not protected by a named profile.");
6108
6947
  console.log(`SQLite: ${options.dbPath}`);
6109
6948
  console.log(`Assets: ${options.assetRoot}`);
6110
6949
  }
@@ -6117,7 +6956,7 @@ function start(config, args) {
6117
6956
  LINEAGE_ASSET_ROOT: options.assetRoot,
6118
6957
  LINEAGE_CHANNEL: config.channel,
6119
6958
  LINEAGE_DB: options.dbPath,
6120
- LINEAGE_PROFILE: options.profile ? process.env.LINEAGE_PROFILE : void 0,
6959
+ LINEAGE_PROFILE: process.env.LINEAGE_PROFILE,
6121
6960
  LINEAGE_PROFILE_ENVIRONMENT: options.profile?.environment,
6122
6961
  LINEAGE_PROFILE_FINGERPRINT: options.profile?.profile_fingerprint,
6123
6962
  LINEAGE_PROFILE_ID: options.profile?.profile_id,
@@ -6125,7 +6964,7 @@ function start(config, args) {
6125
6964
  LINEAGE_PROFILE_SERVICE_ORIGIN: options.profile?.service_origin,
6126
6965
  LINEAGE_LAUNCHER_PID: String(process.pid),
6127
6966
  LINEAGE_SERVICE_INSTANCE_ID: serviceInstanceId,
6128
- LINEAGE_DB_ACCESS: options.profile ? void 0 : "read-only",
6967
+ LINEAGE_DB_ACCESS: void 0,
6129
6968
  NODE_ENV: "production",
6130
6969
  PORT: String(options.port)
6131
6970
  },
@@ -6245,7 +7084,7 @@ async function runLineageCli(config, args = process.argv.slice(2)) {
6245
7084
  process.env.LINEAGE_DB_ACCESS = "read-only";
6246
7085
  }
6247
7086
  } else if (requiresWriter) {
6248
- throw new Error("Persistent writes require --profile; legacy-unbound access is read-only");
7087
+ throw new Error("Persistent writes require --profile; legacy-unbound access is read-only. Create a fresh profile with `profile init --profile <id> --confirm-write`, then pass that profile to the command");
6249
7088
  } else {
6250
7089
  process.env.LINEAGE_DB_ACCESS = "read-only";
6251
7090
  }
@@ -6256,7 +7095,7 @@ async function runLineageCli(config, args = process.argv.slice(2)) {
6256
7095
  else console.error(`${config.binName}: ${message2}`);
6257
7096
  process.exit(1);
6258
7097
  }
6259
- if (command === "next" || command === "brief" || command === "inspect" || command === "selection" || command === "link-child" || command === "reroll" || command === "tasks") {
7098
+ if (command === "next" || command === "brief" || command === "inspect" || command === "selection" || command === "link-child" || command === "generate" || command === "reroll" || command === "tasks") {
6260
7099
  const commandArgs = normalizedArgs.slice(1);
6261
7100
  const json2 = commandArgs.includes("--json");
6262
7101
  try {