@mean-weasel/lineage 0.1.5 → 0.1.7

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.
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/lineageCli.ts
4
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "node:fs";
4
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "node:fs";
5
5
  import { homedir, platform } from "node:os";
6
- import { dirname as dirname3, join as join5, resolve as resolve3 } from "node:path";
6
+ import { dirname as dirname3, join as join6, resolve as resolve4 } from "node:path";
7
7
  import { spawn } from "node:child_process";
8
8
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9
9
 
@@ -40,12 +40,92 @@ var mimeByExt = {
40
40
  ".webp": "image/webp"
41
41
  };
42
42
  var localReviewExts = new Set(Object.keys(mimeByExt));
43
+ var channelFromName = /\b(linkedin|meta|tiktok|youtube|x-twitter|x)\b/i;
44
+ var campaignFromPath = /\b(20\d{2}-\d{2}-[a-z0-9-]+)\b/i;
45
+ function localReviewRoot(repoRoot2) {
46
+ return join(repoRoot2, ".asset-scratch");
47
+ }
43
48
  function contentTypeFor(file) {
44
49
  return mimeByExt[extname(file).toLowerCase()] || "application/octet-stream";
45
50
  }
46
51
  function fileSha256(file) {
47
52
  return createHash("sha256").update(readFileSync(file)).digest("hex");
48
53
  }
54
+ function walkLocalReviewFiles(dir, files = []) {
55
+ if (!existsSync(dir)) return files;
56
+ let entries;
57
+ try {
58
+ entries = readdirSync(dir, { withFileTypes: true });
59
+ } catch (error) {
60
+ if (error.code === "ENOENT") return files;
61
+ throw error;
62
+ }
63
+ for (const entry of entries) {
64
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "studio-uploads" || entry.name === "playwright-results" || entry.name === "lineage-demo") continue;
65
+ if (process.env.NODE_ENV !== "test" && /^vitest-/.test(entry.name)) continue;
66
+ const path = join(dir, entry.name);
67
+ if (entry.isDirectory()) walkLocalReviewFiles(path, files);
68
+ else if (entry.isFile() && localReviewExts.has(extname(entry.name).toLowerCase())) files.push(path);
69
+ }
70
+ return files;
71
+ }
72
+ function inferCampaign(relativePath) {
73
+ return campaignFromPath.exec(relativePath)?.[1] || "local-review";
74
+ }
75
+ function inferChannel(relativePath) {
76
+ const match = channelFromName.exec(relativePath);
77
+ if (!match) return "local";
78
+ return match[1].toLowerCase() === "x" ? "x-twitter" : match[1].toLowerCase();
79
+ }
80
+ function inferContentType(file) {
81
+ const mime = contentTypeFor(file);
82
+ if (mime.startsWith("image/gif")) return "gif";
83
+ if (mime.startsWith("image/")) return "image";
84
+ if (mime.startsWith("video/")) return "video";
85
+ return "other";
86
+ }
87
+ function listLocalReviewAssets(repoRoot2, project, catalog) {
88
+ const catalogChecksums = new Set(catalog.assets.map((asset) => asset.s3?.checksum_sha256).filter(Boolean));
89
+ const root = localReviewRoot(repoRoot2);
90
+ return walkLocalReviewFiles(root).flatMap((file) => {
91
+ try {
92
+ const stats = statSync(file);
93
+ const checksum = fileSha256(file);
94
+ const relativePath = relative(root, file);
95
+ return [{ checksum, file, relativePath, stats }];
96
+ } catch (error) {
97
+ if (error.code === "ENOENT") return [];
98
+ throw error;
99
+ }
100
+ }).filter((item) => !catalogChecksums.has(item.checksum)).map((item) => {
101
+ const fileSlug = basename(item.file, extname(item.file));
102
+ const channel = inferChannel(item.relativePath);
103
+ return {
104
+ asset_id: `local-${item.checksum.slice(0, 12)}`,
105
+ project,
106
+ product: project,
107
+ source: "local",
108
+ campaign: inferCampaign(item.relativePath),
109
+ channel,
110
+ audience: "local-review",
111
+ status: "planned",
112
+ content_type: inferContentType(item.file),
113
+ title: fileSlug.replace(/[-_]+/g, " "),
114
+ hook: item.relativePath,
115
+ cta: "Review before upload",
116
+ utm_content: fileSlug.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toLowerCase() || "local_review",
117
+ notes: "Local pre-push asset. Review and refine before uploading to S3.",
118
+ local: {
119
+ relative_path: item.relativePath,
120
+ absolute_path: item.file,
121
+ size_bytes: item.stats.size,
122
+ content_type: contentTypeFor(item.file),
123
+ checksum_sha256: item.checksum,
124
+ updated_at: item.stats.mtime.toISOString()
125
+ }
126
+ };
127
+ });
128
+ }
49
129
 
50
130
  // src/server/adapters/storage/s3StorageAdapter.ts
51
131
  function parseAssetIdFromS3Key(key) {
@@ -227,6 +307,7 @@ var defaultProduct = process.env.LINEAGE_DEFAULT_PRODUCT || defaultProject;
227
307
  var publicFallbackBucket = "lineage-demo-assets";
228
308
  var publicFallbackRegion = "us-east-1";
229
309
  var contentTypes = /* @__PURE__ */ new Set(["image", "video", "gif", "audio", "doc", "other"]);
310
+ var baseChannels = ["linkedin", "meta", "tiktok", "x-twitter", "youtube"];
230
311
  var projectNamePattern = /^[a-z0-9][a-z0-9-]*$/;
231
312
  var LineageAssetError = class extends Error {
232
313
  constructor(message, status = 400) {
@@ -360,6 +441,9 @@ function defaultFallbackCatalog() {
360
441
  project: defaultProject
361
442
  }, defaultProject);
362
443
  }
444
+ function isDefaultFallbackCatalog(catalog) {
445
+ return catalog.project === defaultProject && !existsSync3(catalogPath(defaultProject));
446
+ }
363
447
  function loadCatalog(project = defaultProject) {
364
448
  const path = catalogPath(project);
365
449
  if (existsSync3(path)) {
@@ -425,6 +509,67 @@ var storageAdapter = createS3StorageAdapter({
425
509
  saveCatalog,
426
510
  supportedContentTypes: contentTypes
427
511
  });
512
+ function uniqueSorted(values) {
513
+ return Array.from(new Set(values.filter(Boolean))).sort();
514
+ }
515
+ function facetsFor(assets) {
516
+ return {
517
+ audiences: uniqueSorted(assets.map((asset) => asset.audience)),
518
+ campaigns: uniqueSorted(assets.map((asset) => asset.campaign)),
519
+ channels: uniqueSorted([...baseChannels, ...assets.map((asset) => asset.channel)]),
520
+ contentTypes: uniqueSorted(assets.map((asset) => asset.content_type)),
521
+ placementStatuses: uniqueSorted(assets.flatMap((asset) => asset.placements?.map((placement) => placement.status) || [])),
522
+ statuses: uniqueSorted(assets.map((asset) => asset.status)),
523
+ totalSizeBytes: assets.reduce((sum, asset) => sum + (asset.s3?.size_bytes || 0), 0)
524
+ };
525
+ }
526
+ function filteredAssets(assets, options) {
527
+ const query = options.query?.trim().toLowerCase();
528
+ return assets.filter((asset) => {
529
+ if (options.status && options.status !== "all" && asset.status !== options.status) return false;
530
+ if (options.channel && options.channel !== "all" && asset.channel !== options.channel) return false;
531
+ if (options.type && options.type !== "all" && asset.content_type !== options.type) return false;
532
+ if (options.placementStatus === "not-posted" && asset.placements?.some((placement) => placement.status === "posted")) return false;
533
+ if (options.placementStatus && !["all", "not-posted"].includes(options.placementStatus) && !asset.placements?.some((placement) => placement.status === options.placementStatus)) return false;
534
+ if (options.campaign && options.campaign !== "all" && asset.campaign !== options.campaign) return false;
535
+ if (options.audience && options.audience !== "all" && asset.audience !== options.audience) return false;
536
+ if (!query) return true;
537
+ return [asset.asset_id, asset.title, asset.campaign, asset.channel, asset.audience, asset.hook, asset.cta].join(" ").toLowerCase().includes(query);
538
+ });
539
+ }
540
+ function listAssets(project = defaultProject, options = {}) {
541
+ const catalog = loadCatalog(project);
542
+ const pageSize = Math.min(Math.max(Number(options.pageSize || 10), 1), 100);
543
+ const page = Math.max(Number(options.page || 1), 1);
544
+ const localAssets = listLocalReviewAssets(repoRoot, catalog.project, catalog);
545
+ const source = options.source || "catalog";
546
+ const sourceAssets = source === "local" ? localAssets : source === "all" ? [...localAssets, ...catalog.assets] : catalog.assets;
547
+ const filtered = filteredAssets(sourceAssets, options);
548
+ const totalPages = Math.max(Math.ceil(filtered.length / pageSize), 1);
549
+ const safePage = Math.min(page, totalPages);
550
+ const start2 = (safePage - 1) * pageSize;
551
+ const pageAssets = filtered.slice(start2, start2 + pageSize);
552
+ let liveObjects = [];
553
+ let error;
554
+ if (options.includeLive && !isDefaultFallbackCatalog(catalog)) {
555
+ try {
556
+ liveObjects = storageAdapter.listLiveObjects(catalog);
557
+ } catch (err) {
558
+ error = err instanceof Error ? err.message : String(err);
559
+ }
560
+ }
561
+ return {
562
+ catalog: { project: catalog.project, product: catalog.product, default_bucket: catalog.default_bucket, default_region: catalog.default_region, asset_count: catalog.assets.length },
563
+ assets: pageAssets,
564
+ facets: facetsFor(catalog.assets),
565
+ pagination: { page: safePage, pageSize, total: filtered.length, totalPages },
566
+ liveObjects,
567
+ orphanObjects: liveObjects.filter((object) => !object.cataloged),
568
+ identity: options.includeLive && !isDefaultFallbackCatalog(catalog) ? storageAdapter.getIdentity() : void 0,
569
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
570
+ error
571
+ };
572
+ }
428
573
 
429
574
  // src/server/assetLineageDb.ts
430
575
  var require2 = createRequire(import.meta.url);
@@ -470,6 +615,10 @@ function lineageDb() {
470
615
  create index if not exists assets_project_source_seen on assets(project_id, source, last_seen_at);
471
616
  create index if not exists assets_project_checksum on assets(project_id, checksum_sha256);
472
617
  create index if not exists assets_project_channel_campaign on assets(project_id, channel, campaign);
618
+ create table if not exists lineage_schema_migrations (
619
+ key text primary key,
620
+ applied_at text not null
621
+ );
473
622
  create table if not exists asset_edges (
474
623
  id text primary key,
475
624
  project_id text not null references projects(id),
@@ -481,6 +630,44 @@ function lineageDb() {
481
630
  );
482
631
  create index if not exists edges_parent on asset_edges(project_id, parent_asset_id);
483
632
  create index if not exists edges_child on asset_edges(project_id, child_asset_id);
633
+ create table if not exists asset_attempts (
634
+ id text primary key,
635
+ project_id text not null references projects(id),
636
+ node_asset_id text not null references assets(id),
637
+ asset_id text not null references assets(id),
638
+ attempt_index integer not null check (attempt_index > 0),
639
+ source text not null check (source in ('initial', 'generated_child', 'reroll')),
640
+ prompt text,
641
+ generation_job_id text,
642
+ file_path text,
643
+ checksum_sha256 text,
644
+ created_at text not null,
645
+ promoted_at text,
646
+ is_current integer not null check (is_current in (0, 1)),
647
+ unique(project_id, node_asset_id, attempt_index),
648
+ unique(project_id, node_asset_id, asset_id, source)
649
+ );
650
+ create unique index if not exists asset_attempts_one_current
651
+ on asset_attempts(project_id, node_asset_id)
652
+ where is_current = 1;
653
+ create index if not exists asset_attempts_node_created
654
+ on asset_attempts(project_id, node_asset_id, created_at);
655
+ create table if not exists asset_reroll_requests (
656
+ id text primary key,
657
+ project_id text not null references projects(id),
658
+ root_asset_id text not null references assets(id),
659
+ node_asset_id text not null references assets(id),
660
+ status text not null check (status in ('pending', 'resolved', 'cancelled')),
661
+ requested_by text not null check (requested_by in ('human', 'agent', 'system')),
662
+ notes text,
663
+ created_at text not null,
664
+ resolved_at text
665
+ );
666
+ create unique index if not exists asset_reroll_requests_one_pending
667
+ on asset_reroll_requests(project_id, root_asset_id, node_asset_id)
668
+ where status = 'pending';
669
+ create index if not exists asset_reroll_requests_root_status
670
+ on asset_reroll_requests(project_id, root_asset_id, status, created_at);
484
671
  create table if not exists asset_reviews (
485
672
  asset_id text primary key references assets(id),
486
673
  review_state text not null check (review_state in ('unreviewed', 'approved', 'needs_revision', 'rejected', 'ignored')),
@@ -685,7 +872,7 @@ function lineageDb() {
685
872
  project_id text not null references projects(id),
686
873
  provider text not null default 'codex-handoff',
687
874
  adapter_version text not null,
688
- source_mode text not null check (source_mode in ('lineage_selection')),
875
+ source_mode text not null check (source_mode in ('lineage_selection', 'lineage_reroll')),
689
876
  root_asset_id text not null references assets(id),
690
877
  prompt text not null,
691
878
  expected_output_count integer not null check (expected_output_count > 0),
@@ -703,7 +890,7 @@ function lineageDb() {
703
890
  project_id text not null references projects(id),
704
891
  asset_id text not null references assets(id),
705
892
  root_asset_id text not null references assets(id),
706
- role text not null check (role in ('lineage_next_base', 'reference')),
893
+ role text not null check (role in ('lineage_next_base', 'reference', 'reroll_target')),
707
894
  position integer not null,
708
895
  selection_strategy text not null,
709
896
  selection_snapshot_json text not null,
@@ -737,12 +924,49 @@ function lineageDb() {
737
924
  );
738
925
  create index if not exists generation_job_receipts_job on generation_job_receipts(job_id, created_at);
739
926
  create table if not exists adapter_settings (project_id text not null references projects(id), adapter_type text not null check (adapter_type in ('cloud', 'scheduler', 'image_generator')), provider text not null, enabled integer not null check (enabled in (0, 1)), secret_ref text, safe_config_json text not null, created_at text not null, updated_at text not null, primary key(project_id, adapter_type, provider)); create index if not exists adapter_settings_project_type on adapter_settings(project_id, adapter_type);
927
+ create table if not exists lineage_tasks (
928
+ id text primary key,
929
+ project_id text not null references projects(id),
930
+ root_asset_id text not null references assets(id),
931
+ target_asset_id text not null references assets(id),
932
+ task_type text not null check (task_type in ('iterate', 'reroll')),
933
+ status text not null check (status in ('pending', 'claimed', 'in_progress', 'resolved', 'cancelled')),
934
+ instructions text,
935
+ created_by text not null check (created_by in ('human', 'agent', 'system')),
936
+ created_at text not null,
937
+ updated_at text not null,
938
+ claimed_at text,
939
+ started_at text,
940
+ resolved_at text,
941
+ cancelled_at text,
942
+ resolved_generation_job_id text,
943
+ resolved_asset_id text references assets(id),
944
+ metadata_json text
945
+ );
946
+ create unique index if not exists lineage_tasks_one_open
947
+ on lineage_tasks(project_id, root_asset_id, target_asset_id, task_type)
948
+ where status in ('pending', 'claimed', 'in_progress');
949
+ create index if not exists lineage_tasks_root_status
950
+ on lineage_tasks(project_id, root_asset_id, status, updated_at);
951
+ create index if not exists lineage_tasks_target
952
+ on lineage_tasks(project_id, root_asset_id, target_asset_id, task_type, status);
953
+ create table if not exists lineage_task_events (
954
+ id text primary key,
955
+ task_id text not null references lineage_tasks(id) on delete cascade,
956
+ event_type text not null,
957
+ actor text,
958
+ message text,
959
+ created_at text not null,
960
+ metadata_json text
961
+ );
962
+ create index if not exists lineage_task_events_task_created
963
+ on lineage_task_events(task_id, created_at);
740
964
  create table if not exists agent_claims (
741
965
  id text primary key,
742
966
  token_hash text not null,
743
967
  project_id text not null references projects(id),
744
968
  channel text,
745
- scope_type text not null check (scope_type in ('lineage_workspace', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
969
+ scope_type text not null check (scope_type in ('lineage_workspace', 'lineage_task', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
746
970
  target_id text not null,
747
971
  target_title text,
748
972
  agent_id text,
@@ -776,13 +1000,76 @@ function lineageDb() {
776
1000
  migrateAssetSelections(database);
777
1001
  dropLegacyAssetSelectionRootUnique(database);
778
1002
  ensureColumn(database, "asset_selections", "notes", "text");
1003
+ backfillLineageTasks(database);
779
1004
  ensureColumn(database, "asset_ledger_records", "first_seen_at", "text");
780
1005
  ensureColumn(database, "asset_ledger_records", "indexed_by_run_id", "text");
781
1006
  ensureColumn(database, "asset_ledger_sources", "first_seen_at", "text");
782
1007
  ensureColumn(database, "asset_ledger_sources", "indexed_by_run_id", "text");
783
1008
  ensureReviewStateValues(database);
1009
+ ensureAgentClaimScopeValues(database);
1010
+ ensureGenerationReceiptCheckValues(database);
784
1011
  return database;
785
1012
  }
1013
+ function backfillLineageTasks(database) {
1014
+ database.exec(`
1015
+ create table if not exists lineage_schema_migrations (
1016
+ key text primary key,
1017
+ applied_at text not null
1018
+ )
1019
+ `);
1020
+ const marker = database.prepare("select key from lineage_schema_migrations where key = 'lineage_tasks_backfilled_v1'").get();
1021
+ if (marker) return;
1022
+ database.exec("BEGIN IMMEDIATE");
1023
+ try {
1024
+ database.exec(`
1025
+ insert or ignore into lineage_tasks (
1026
+ id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
1027
+ created_by, created_at, updated_at, metadata_json
1028
+ )
1029
+ select
1030
+ s.project_id || ':' || s.root_asset_id || ':lineage-task:iterate:' || s.asset_id,
1031
+ s.project_id,
1032
+ s.root_asset_id,
1033
+ s.asset_id,
1034
+ 'iterate',
1035
+ 'pending',
1036
+ s.notes,
1037
+ 'human',
1038
+ s.selected_at,
1039
+ s.selected_at,
1040
+ null
1041
+ from asset_selections s;
1042
+
1043
+ insert or ignore into lineage_tasks (
1044
+ id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
1045
+ created_by, created_at, updated_at, metadata_json
1046
+ )
1047
+ select
1048
+ r.project_id || ':' || r.root_asset_id || ':lineage-task:reroll:' || r.node_asset_id,
1049
+ r.project_id,
1050
+ r.root_asset_id,
1051
+ r.node_asset_id,
1052
+ 'reroll',
1053
+ 'pending',
1054
+ r.notes,
1055
+ r.requested_by,
1056
+ r.created_at,
1057
+ r.created_at,
1058
+ null
1059
+ from asset_reroll_requests r
1060
+ where r.status = 'pending';
1061
+ `);
1062
+ database.prepare(`
1063
+ insert into lineage_schema_migrations (key, applied_at)
1064
+ values ('lineage_tasks_backfilled_v1', ?)
1065
+ on conflict(key) do nothing
1066
+ `).run(nowIso());
1067
+ database.exec("COMMIT");
1068
+ } catch (error) {
1069
+ database.exec("ROLLBACK");
1070
+ throw error;
1071
+ }
1072
+ }
786
1073
  function ensureColumn(database, table, column, definition) {
787
1074
  const rows = database.prepare(`pragma table_info(${table})`).all();
788
1075
  if (!rows.some((row) => row.name === column)) database.exec(`alter table ${table} add column ${column} ${definition}`);
@@ -846,12 +1133,138 @@ function ensureReviewStateValues(database) {
846
1133
  drop table asset_reviews_old;
847
1134
  `);
848
1135
  }
1136
+ function ensureAgentClaimScopeValues(database) {
1137
+ const createSql = database.prepare("select sql from sqlite_master where type = 'table' and name = 'agent_claims'").get();
1138
+ if (createSql?.sql?.includes("'lineage_task'")) return;
1139
+ database.exec("PRAGMA foreign_keys = OFF; PRAGMA legacy_alter_table = ON; BEGIN IMMEDIATE");
1140
+ try {
1141
+ database.exec(`
1142
+ alter table agent_claims rename to agent_claims_old;
1143
+ create table agent_claims (
1144
+ id text primary key,
1145
+ token_hash text not null,
1146
+ project_id text not null references projects(id),
1147
+ channel text,
1148
+ scope_type text not null check (scope_type in ('lineage_workspace', 'lineage_task', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
1149
+ target_id text not null,
1150
+ target_title text,
1151
+ agent_id text,
1152
+ agent_name text not null,
1153
+ agent_kind text not null,
1154
+ thread_id text,
1155
+ status text not null check (status in ('active', 'expired', 'released', 'revoked', 'transferred')),
1156
+ created_at text not null,
1157
+ heartbeat_at text not null,
1158
+ expires_at text not null,
1159
+ released_at text,
1160
+ revoked_at text,
1161
+ revoked_by text,
1162
+ override_reason text,
1163
+ metadata_json text
1164
+ );
1165
+ insert into agent_claims
1166
+ select * from agent_claims_old;
1167
+ drop table agent_claims_old;
1168
+ create unique index if not exists agent_claims_token_hash on agent_claims(token_hash);
1169
+ create index if not exists agent_claims_project_status on agent_claims(project_id, status, heartbeat_at);
1170
+ create index if not exists agent_claims_target on agent_claims(project_id, channel, scope_type, target_id, status);
1171
+ `);
1172
+ const violations = database.prepare("pragma foreign_key_check").all();
1173
+ if (violations.length > 0) throw new Error(`Agent claim scope migration failed foreign key check: ${JSON.stringify(violations)}`);
1174
+ database.exec("COMMIT");
1175
+ } catch (error) {
1176
+ database.exec("ROLLBACK");
1177
+ throw error;
1178
+ } finally {
1179
+ database.exec("PRAGMA legacy_alter_table = OFF; PRAGMA foreign_keys = ON");
1180
+ }
1181
+ }
1182
+ function tableCreateSql(database, table) {
1183
+ const row = database.prepare("select sql from sqlite_master where type = 'table' and name = ?").get(table);
1184
+ return row?.sql || "";
1185
+ }
1186
+ function ensureGenerationReceiptCheckValues(database) {
1187
+ const jobsSql = tableCreateSql(database, "generation_jobs");
1188
+ const inputsSql = tableCreateSql(database, "generation_job_inputs");
1189
+ const needsJobsMigration = Boolean(jobsSql && !jobsSql.includes("'lineage_reroll'"));
1190
+ const needsInputsMigration = Boolean(inputsSql && !inputsSql.includes("'reroll_target'"));
1191
+ if (!needsJobsMigration && !needsInputsMigration) return;
1192
+ database.exec("PRAGMA foreign_keys = OFF; PRAGMA legacy_alter_table = ON; BEGIN IMMEDIATE");
1193
+ try {
1194
+ if (needsJobsMigration) migrateGenerationJobsCheck(database);
1195
+ if (needsInputsMigration) migrateGenerationJobInputsCheck(database);
1196
+ const violations = database.prepare("pragma foreign_key_check").all();
1197
+ if (violations.length > 0) throw new Error(`Generation receipt migration failed foreign key check: ${JSON.stringify(violations)}`);
1198
+ database.exec("COMMIT");
1199
+ } catch (error) {
1200
+ database.exec("ROLLBACK");
1201
+ throw error;
1202
+ } finally {
1203
+ database.exec("PRAGMA legacy_alter_table = OFF; PRAGMA foreign_keys = ON");
1204
+ }
1205
+ }
1206
+ function migrateGenerationJobsCheck(database) {
1207
+ database.exec(`
1208
+ alter table generation_jobs rename to generation_jobs_legacy_check;
1209
+ create table generation_jobs (
1210
+ id text primary key,
1211
+ project_id text not null references projects(id),
1212
+ provider text not null default 'codex-handoff',
1213
+ adapter_version text not null,
1214
+ source_mode text not null check (source_mode in ('lineage_selection', 'lineage_reroll')),
1215
+ root_asset_id text not null references assets(id),
1216
+ prompt text not null,
1217
+ expected_output_count integer not null check (expected_output_count > 0),
1218
+ status text not null check (status in ('planned', 'imported', 'failed', 'cancelled')),
1219
+ output_dir text,
1220
+ handoff_json text,
1221
+ created_at text not null,
1222
+ updated_at text not null,
1223
+ imported_at text
1224
+ );
1225
+ insert into generation_jobs (
1226
+ id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
1227
+ expected_output_count, status, output_dir, handoff_json, created_at, updated_at, imported_at
1228
+ )
1229
+ select
1230
+ id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
1231
+ expected_output_count, status, output_dir, handoff_json, created_at, updated_at, imported_at
1232
+ from generation_jobs_legacy_check;
1233
+ drop table generation_jobs_legacy_check;
1234
+ create index if not exists generation_jobs_project_created on generation_jobs(project_id, created_at);
1235
+ `);
1236
+ }
1237
+ function migrateGenerationJobInputsCheck(database) {
1238
+ database.exec(`
1239
+ alter table generation_job_inputs rename to generation_job_inputs_legacy_check;
1240
+ create table generation_job_inputs (
1241
+ id text primary key,
1242
+ job_id text not null references generation_jobs(id) on delete cascade,
1243
+ project_id text not null references projects(id),
1244
+ asset_id text not null references assets(id),
1245
+ root_asset_id text not null references assets(id),
1246
+ role text not null check (role in ('lineage_next_base', 'reference', 'reroll_target')),
1247
+ position integer not null,
1248
+ selection_strategy text not null,
1249
+ selection_snapshot_json text not null,
1250
+ unique(job_id, asset_id, role)
1251
+ );
1252
+ insert into generation_job_inputs (
1253
+ id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json
1254
+ )
1255
+ select
1256
+ id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json
1257
+ from generation_job_inputs_legacy_check;
1258
+ drop table generation_job_inputs_legacy_check;
1259
+ create index if not exists generation_job_inputs_job on generation_job_inputs(job_id, position);
1260
+ `);
1261
+ }
849
1262
 
850
1263
  // src/server/agentClaims.ts
851
1264
  var defaultTtlSeconds = 20 * 60;
852
1265
  var idleAfterSeconds = 5 * 60;
853
1266
  var staleAfterSeconds = 15 * 60;
854
- var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
1267
+ var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "lineage_task", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
855
1268
  var claimTokenPattern = /claim_[a-z0-9_-]+\.[A-Za-z0-9_-]+/g;
856
1269
  var AgentClaimError = class extends Error {
857
1270
  constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
@@ -1156,6 +1569,19 @@ function revokeAgentClaim(project, claimId, fields) {
1156
1569
  database.close();
1157
1570
  }
1158
1571
  }
1572
+ function revokeAgentClaimInDatabase(database, project, claimId, fields) {
1573
+ expireActiveClaims(database);
1574
+ const claim = findClaimById(database, claimId, project);
1575
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1576
+ if (claim.status !== "active") return claim;
1577
+ const timestamp = nowIso();
1578
+ database.prepare(`
1579
+ update agent_claims set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
1580
+ where id = ? and status = 'active'
1581
+ `).run(timestamp, fields.actor || "human", fields.reason || null, claim.id);
1582
+ recordEvent(database, claim.id, "revoked", fields.actor || "human", fields.reason);
1583
+ return findClaimById(database, claim.id, project);
1584
+ }
1159
1585
  function transferAgentClaim(project, claimId, fields) {
1160
1586
  if (!fields.confirmWrite) throw new AgentClaimError("Transferring an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
1161
1587
  const toAgentName = fields.toAgentName.trim();
@@ -1173,6 +1599,19 @@ function transferAgentClaim(project, claimId, fields) {
1173
1599
  database.close();
1174
1600
  }
1175
1601
  }
1602
+ function recordAgentClaimWriteAllowed(claim, fields) {
1603
+ const database = lineageDb();
1604
+ try {
1605
+ recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
1606
+ danger_level: fields.dangerLevel,
1607
+ target_id: fields.targetId,
1608
+ write_kind: fields.writeKind
1609
+ });
1610
+ return { ok: true };
1611
+ } finally {
1612
+ database.close();
1613
+ }
1614
+ }
1176
1615
  function validateAgentClaimForWrite(fields) {
1177
1616
  if (fields.dangerLevel === "danger" && !fields.confirmWrite) {
1178
1617
  return denied("human_confirmation_required", "Dangerous write requires explicit human confirmation.");
@@ -1193,17 +1632,22 @@ function validateAgentClaimForWrite(fields) {
1193
1632
  if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
1194
1633
  return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
1195
1634
  }
1196
- recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
1197
- danger_level: fields.dangerLevel,
1198
- target_id: fields.targetId,
1199
- write_kind: fields.writeKind
1200
- });
1635
+ if (fields.recordEvent !== false) {
1636
+ recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
1637
+ danger_level: fields.dangerLevel,
1638
+ target_id: fields.targetId,
1639
+ write_kind: fields.writeKind
1640
+ });
1641
+ }
1201
1642
  return { ok: true, claim, warnings: [] };
1202
1643
  } finally {
1203
1644
  database.close();
1204
1645
  }
1205
1646
  }
1206
1647
 
1648
+ // src/server/assetLineage.ts
1649
+ import { join as join5 } from "node:path";
1650
+
1207
1651
  // src/server/assetLineageSelection.ts
1208
1652
  function selectedRows(database, project, root) {
1209
1653
  return database.prepare(`
@@ -1214,6 +1658,507 @@ function selectedRows(database, project, root) {
1214
1658
  `).all(project, root);
1215
1659
  }
1216
1660
 
1661
+ // src/server/assetLineageTasks.ts
1662
+ import { randomBytes as randomBytes2 } from "node:crypto";
1663
+ var LineageTaskError = class extends Error {
1664
+ constructor(message, status = 400) {
1665
+ super(message);
1666
+ this.status = status;
1667
+ }
1668
+ status;
1669
+ };
1670
+ var activeStatuses = ["pending", "claimed", "in_progress"];
1671
+ var taskTypes = /* @__PURE__ */ new Set(["iterate", "reroll"]);
1672
+ var taskStatuses = /* @__PURE__ */ new Set(["pending", "claimed", "in_progress", "resolved", "cancelled"]);
1673
+ function taskIdFor(project, rootAssetId, targetAssetId, taskType) {
1674
+ return `${project}:${rootAssetId}:lineage-task:${taskType}:${targetAssetId}`;
1675
+ }
1676
+ function randomId2(prefix) {
1677
+ return `${prefix}_${Date.now().toString(36)}_${randomBytes2(6).toString("base64url").toLowerCase()}`;
1678
+ }
1679
+ function metadataJson2(metadata) {
1680
+ return metadata ? JSON.stringify(metadata) : null;
1681
+ }
1682
+ function metadataWithoutClaim(metadata) {
1683
+ if (!metadata) return void 0;
1684
+ const next = { ...metadata };
1685
+ delete next.claim_id;
1686
+ return Object.keys(next).length > 0 ? next : void 0;
1687
+ }
1688
+ function parseMetadata2(value) {
1689
+ if (typeof value !== "string" || !value) return void 0;
1690
+ try {
1691
+ const parsed = JSON.parse(value);
1692
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
1693
+ } catch {
1694
+ return void 0;
1695
+ }
1696
+ }
1697
+ function normalizeProject(project) {
1698
+ const trimmed = project.trim();
1699
+ if (!trimmed) throw new LineageTaskError("Lineage task requires project");
1700
+ return trimmed;
1701
+ }
1702
+ function normalizeTaskType(taskType) {
1703
+ if (!taskTypes.has(taskType)) throw new LineageTaskError(`Unsupported lineage task type: ${taskType}`);
1704
+ return taskType;
1705
+ }
1706
+ function normalizeStatus(status) {
1707
+ if (!taskStatuses.has(status)) throw new LineageTaskError(`Unsupported lineage task status: ${status}`);
1708
+ return status;
1709
+ }
1710
+ function normalizeActor(actor, label) {
1711
+ const trimmed = actor.trim();
1712
+ if (!trimmed) throw new LineageTaskError(`${label} is required`);
1713
+ return trimmed;
1714
+ }
1715
+ function requireAsset(database, project, assetId) {
1716
+ const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
1717
+ if (!row) throw new LineageTaskError(`Unknown indexed asset: ${assetId}`, 404);
1718
+ }
1719
+ function taskFromRow(row) {
1720
+ const metadata = parseMetadata2(row.metadata_json);
1721
+ const claimId = typeof metadata?.claim_id === "string" ? metadata.claim_id : void 0;
1722
+ return {
1723
+ id: String(row.id),
1724
+ project_id: String(row.project_id),
1725
+ root_asset_id: String(row.root_asset_id),
1726
+ target_asset_id: String(row.target_asset_id),
1727
+ task_type: String(row.task_type),
1728
+ status: String(row.status),
1729
+ instructions: typeof row.instructions === "string" ? row.instructions : void 0,
1730
+ created_by: String(row.created_by),
1731
+ created_at: String(row.created_at),
1732
+ updated_at: String(row.updated_at),
1733
+ claimed_at: typeof row.claimed_at === "string" ? row.claimed_at : void 0,
1734
+ started_at: typeof row.started_at === "string" ? row.started_at : void 0,
1735
+ resolved_at: typeof row.resolved_at === "string" ? row.resolved_at : void 0,
1736
+ cancelled_at: typeof row.cancelled_at === "string" ? row.cancelled_at : void 0,
1737
+ resolved_generation_job_id: typeof row.resolved_generation_job_id === "string" ? row.resolved_generation_job_id : void 0,
1738
+ resolved_asset_id: typeof row.resolved_asset_id === "string" ? row.resolved_asset_id : void 0,
1739
+ claimed_by_claim_id: claimId,
1740
+ metadata
1741
+ };
1742
+ }
1743
+ function eventFromRow(row) {
1744
+ return {
1745
+ id: String(row.id),
1746
+ task_id: String(row.task_id),
1747
+ event_type: String(row.event_type),
1748
+ actor: typeof row.actor === "string" ? row.actor : void 0,
1749
+ message: typeof row.message === "string" ? row.message : void 0,
1750
+ created_at: String(row.created_at),
1751
+ metadata: parseMetadata2(row.metadata_json)
1752
+ };
1753
+ }
1754
+ function findTask(database, project, taskId) {
1755
+ const row = database.prepare("select * from lineage_tasks where project_id = ? and id = ?").get(project, taskId);
1756
+ return row ? taskFromRow(row) : null;
1757
+ }
1758
+ function requireTask(database, project, taskId) {
1759
+ const task = findTask(database, project, taskId);
1760
+ if (!task) throw new LineageTaskError(`Unknown lineage task: ${taskId}`, 404);
1761
+ return task;
1762
+ }
1763
+ function taskEvents(database, taskId) {
1764
+ const rows = database.prepare("select * from lineage_task_events where task_id = ? order by created_at, rowid").all(taskId);
1765
+ return rows.map(eventFromRow);
1766
+ }
1767
+ function recordEvent2(database, taskId, eventType, actor, message, metadata) {
1768
+ database.prepare(`
1769
+ insert into lineage_task_events (id, task_id, event_type, actor, message, created_at, metadata_json)
1770
+ values (?, ?, ?, ?, ?, ?, ?)
1771
+ `).run(randomId2("lineage_task_event"), taskId, eventType, actor || null, message || null, nowIso(), metadataJson2(metadata));
1772
+ }
1773
+ function taskWithEvents(database, project, taskId) {
1774
+ return { project, ok: true, task: requireTask(database, project, taskId), events: taskEvents(database, taskId) };
1775
+ }
1776
+ function assertChanged(result, message) {
1777
+ if (Number(result.changes) !== 1) throw new LineageTaskError(message, 409);
1778
+ }
1779
+ function transaction(database, callback) {
1780
+ database.exec("BEGIN IMMEDIATE");
1781
+ try {
1782
+ const value = callback();
1783
+ database.exec("COMMIT");
1784
+ return value;
1785
+ } catch (error) {
1786
+ database.exec("ROLLBACK");
1787
+ throw error;
1788
+ }
1789
+ }
1790
+ function taskReadWithEvents(database, project, taskId) {
1791
+ return { task: requireTask(database, project, taskId), events: taskEvents(database, taskId) };
1792
+ }
1793
+ function listLineageTasks(project, rootAssetId, statuses = activeStatuses) {
1794
+ const normalizedProject = normalizeProject(project);
1795
+ const normalizedStatuses = statuses.map(normalizeStatus);
1796
+ const database = lineageDb();
1797
+ try {
1798
+ requireAsset(database, normalizedProject, rootAssetId);
1799
+ if (normalizedStatuses.length === 0) {
1800
+ return { project: normalizedProject, root_asset_id: rootAssetId, tasks: [], fetchedAt: nowIso() };
1801
+ }
1802
+ const placeholders = normalizedStatuses.map(() => "?").join(",");
1803
+ const rows = database.prepare(`
1804
+ select * from lineage_tasks
1805
+ where project_id = ? and root_asset_id = ? and status in (${placeholders})
1806
+ order by updated_at desc, created_at desc, id
1807
+ `).all(normalizedProject, rootAssetId, ...normalizedStatuses);
1808
+ return { project: normalizedProject, root_asset_id: rootAssetId, tasks: rows.map(taskFromRow), fetchedAt: nowIso() };
1809
+ } finally {
1810
+ database.close();
1811
+ }
1812
+ }
1813
+ function getLineageTask(project, taskId) {
1814
+ const normalizedProject = normalizeProject(project);
1815
+ const database = lineageDb();
1816
+ try {
1817
+ return { project: normalizedProject, ...taskReadWithEvents(database, normalizedProject, taskId) };
1818
+ } finally {
1819
+ database.close();
1820
+ }
1821
+ }
1822
+ function upsertLineageTask(project, fields) {
1823
+ const normalizedProject = normalizeProject(project);
1824
+ const taskType = normalizeTaskType(fields.taskType);
1825
+ const taskId = taskIdFor(normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType);
1826
+ const database = lineageDb();
1827
+ try {
1828
+ requireAsset(database, normalizedProject, fields.rootAssetId);
1829
+ requireAsset(database, normalizedProject, fields.targetAssetId);
1830
+ const existing = database.prepare(`
1831
+ select * from lineage_tasks
1832
+ where project_id = ? and root_asset_id = ? and target_asset_id = ? and task_type = ?
1833
+ and status in ('pending', 'claimed', 'in_progress')
1834
+ order by created_at desc limit 1
1835
+ `).get(normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType);
1836
+ const instructions = fields.instructions?.trim() || void 0;
1837
+ if (existing) {
1838
+ const task = taskFromRow(existing);
1839
+ if (task.status !== "pending" && instructions !== void 0 && instructions !== task.instructions) {
1840
+ throw new LineageTaskError("Only pending lineage tasks can update instructions.", 409);
1841
+ }
1842
+ if (task.status === "pending" && instructions !== void 0 && instructions !== task.instructions) {
1843
+ const timestamp2 = nowIso();
1844
+ transaction(database, () => {
1845
+ const result = database.prepare(`
1846
+ update lineage_tasks
1847
+ set instructions = ?, updated_at = ?
1848
+ where id = ? and status = 'pending'
1849
+ `).run(instructions, timestamp2, task.id);
1850
+ assertChanged(result, "Only pending lineage tasks can update instructions.");
1851
+ recordEvent2(database, task.id, "instructions_updated", fields.createdBy, "Instructions updated.");
1852
+ });
1853
+ }
1854
+ return taskWithEvents(database, normalizedProject, task.id);
1855
+ }
1856
+ const closedTask = findTask(database, normalizedProject, taskId);
1857
+ if (closedTask) {
1858
+ const timestamp2 = nowIso();
1859
+ transaction(database, () => {
1860
+ const result = database.prepare(`
1861
+ update lineage_tasks
1862
+ set status = 'pending', instructions = ?, created_by = ?, updated_at = ?,
1863
+ claimed_at = null, started_at = null, resolved_at = null, cancelled_at = null,
1864
+ resolved_generation_job_id = null, resolved_asset_id = null, metadata_json = null
1865
+ where id = ? and status not in ('pending', 'claimed', 'in_progress')
1866
+ `).run(instructions || null, fields.createdBy, timestamp2, taskId);
1867
+ assertChanged(result, "Lineage task changed while reopening.");
1868
+ recordEvent2(database, taskId, "created", fields.createdBy, "Lineage task created.");
1869
+ });
1870
+ return taskWithEvents(database, normalizedProject, taskId);
1871
+ }
1872
+ const timestamp = nowIso();
1873
+ transaction(database, () => {
1874
+ database.prepare(`
1875
+ insert into lineage_tasks (
1876
+ id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
1877
+ created_by, created_at, updated_at, metadata_json
1878
+ ) values (?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, null)
1879
+ `).run(taskId, normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType, instructions || null, fields.createdBy, timestamp, timestamp);
1880
+ recordEvent2(database, taskId, "created", fields.createdBy, "Lineage task created.");
1881
+ });
1882
+ return taskWithEvents(database, normalizedProject, taskId);
1883
+ } finally {
1884
+ database.close();
1885
+ }
1886
+ }
1887
+ function updateLineageTaskInstructions(project, fields) {
1888
+ const normalizedProject = normalizeProject(project);
1889
+ const instructions = fields.instructions.trim();
1890
+ const database = lineageDb();
1891
+ try {
1892
+ const task = requireTask(database, normalizedProject, fields.taskId);
1893
+ if (task.status !== "pending") throw new LineageTaskError("Only pending lineage tasks can update instructions.", 409);
1894
+ const timestamp = nowIso();
1895
+ transaction(database, () => {
1896
+ const result = database.prepare(`
1897
+ update lineage_tasks
1898
+ set instructions = ?, updated_at = ?
1899
+ where id = ? and status = 'pending'
1900
+ `).run(instructions || null, timestamp, task.id);
1901
+ assertChanged(result, "Only pending lineage tasks can update instructions.");
1902
+ recordEvent2(database, task.id, "instructions_updated", "human", "Instructions updated.");
1903
+ });
1904
+ return taskWithEvents(database, normalizedProject, task.id);
1905
+ } finally {
1906
+ database.close();
1907
+ }
1908
+ }
1909
+ function addLineageTaskComment(project, fields) {
1910
+ const normalizedProject = normalizeProject(project);
1911
+ const actor = normalizeActor(fields.actor, "Comment actor");
1912
+ const message = fields.message.trim();
1913
+ if (!message) throw new LineageTaskError("Comment message is required");
1914
+ const database = lineageDb();
1915
+ try {
1916
+ const task = requireTask(database, normalizedProject, fields.taskId);
1917
+ transaction(database, () => {
1918
+ recordEvent2(database, task.id, "comment_added", actor, message);
1919
+ });
1920
+ return taskWithEvents(database, normalizedProject, task.id);
1921
+ } finally {
1922
+ database.close();
1923
+ }
1924
+ }
1925
+ function claimLineageTask(project, fields) {
1926
+ const normalizedProject = normalizeProject(project);
1927
+ const agentName = normalizeActor(fields.agentName, "Agent name");
1928
+ const beforeClaimDb = lineageDb();
1929
+ try {
1930
+ const task = requireTask(beforeClaimDb, normalizedProject, fields.taskId);
1931
+ if (task.status !== "pending") throw new LineageTaskError("Only pending lineage tasks can be claimed.", 409);
1932
+ } finally {
1933
+ beforeClaimDb.close();
1934
+ }
1935
+ const claimResult = createAgentClaim({
1936
+ agentName,
1937
+ project: normalizedProject,
1938
+ scopeType: "lineage_task",
1939
+ targetId: fields.taskId,
1940
+ targetTitle: fields.taskId
1941
+ });
1942
+ if (!claimResult.claim) throw new LineageTaskError("Unable to create lineage task claim.", 500);
1943
+ const claim = claimResult.claim;
1944
+ const database = lineageDb();
1945
+ try {
1946
+ const task = requireTask(database, normalizedProject, fields.taskId);
1947
+ if (task.status !== "pending") {
1948
+ releaseAgentClaim(claimResult.claim_token);
1949
+ throw new LineageTaskError("Only pending lineage tasks can be claimed.", 409);
1950
+ }
1951
+ const timestamp = nowIso();
1952
+ const metadata = { ...task.metadata || {}, claim_id: claim.id };
1953
+ try {
1954
+ transaction(database, () => {
1955
+ const result = database.prepare(`
1956
+ update lineage_tasks
1957
+ set status = 'claimed', claimed_at = ?, updated_at = ?, metadata_json = ?
1958
+ where id = ? and status = 'pending'
1959
+ `).run(timestamp, timestamp, metadataJson2(metadata), task.id);
1960
+ assertChanged(result, "Only pending lineage tasks can be claimed.");
1961
+ recordEvent2(database, task.id, "claimed", agentName, "Lineage task claimed.", { claim_id: claim.id });
1962
+ });
1963
+ } catch (error) {
1964
+ releaseAgentClaim(claimResult.claim_token);
1965
+ throw error;
1966
+ }
1967
+ return {
1968
+ ...taskWithEvents(database, normalizedProject, task.id),
1969
+ claim,
1970
+ claim_token: claimResult.claim_token
1971
+ };
1972
+ } finally {
1973
+ database.close();
1974
+ }
1975
+ }
1976
+ function startLineageTask(project, fields) {
1977
+ const normalizedProject = normalizeProject(project);
1978
+ const precheckDatabase = lineageDb();
1979
+ try {
1980
+ const task = requireTask(precheckDatabase, normalizedProject, fields.taskId);
1981
+ if (task.status !== "claimed") throw new LineageTaskError("Only claimed lineage tasks can be started.", 409);
1982
+ } finally {
1983
+ precheckDatabase.close();
1984
+ }
1985
+ const validation = validateAgentClaimForWrite({
1986
+ claimToken: fields.claimToken,
1987
+ dangerLevel: "enforce",
1988
+ project: normalizedProject,
1989
+ recordEvent: false,
1990
+ scopeType: "lineage_task",
1991
+ targetId: fields.taskId,
1992
+ writeKind: "lineage_task_start"
1993
+ });
1994
+ if (!validation.ok) throw new LineageTaskError(validation.message, 409);
1995
+ const database = lineageDb();
1996
+ try {
1997
+ const task = requireTask(database, normalizedProject, fields.taskId);
1998
+ if (task.status !== "claimed") throw new LineageTaskError("Only claimed lineage tasks can be started.", 409);
1999
+ if (task.claimed_by_claim_id && task.claimed_by_claim_id !== validation.claim.id) {
2000
+ throw new LineageTaskError("Claim token does not match the task claim.", 409);
2001
+ }
2002
+ const timestamp = nowIso();
2003
+ const metadata = { ...task.metadata || {}, claim_id: validation.claim.id };
2004
+ transaction(database, () => {
2005
+ const result = database.prepare(`
2006
+ update lineage_tasks
2007
+ set status = 'in_progress', started_at = ?, updated_at = ?, metadata_json = ?
2008
+ where id = ? and status = 'claimed'
2009
+ `).run(timestamp, timestamp, metadataJson2(metadata), task.id);
2010
+ assertChanged(result, "Only claimed lineage tasks can be started.");
2011
+ recordEvent2(database, task.id, "started", validation.claim.agent_name, "Lineage task started.", { claim_id: validation.claim.id });
2012
+ });
2013
+ recordAgentClaimWriteAllowed(validation.claim, {
2014
+ dangerLevel: "enforce",
2015
+ targetId: fields.taskId,
2016
+ writeKind: "lineage_task_start"
2017
+ });
2018
+ return taskWithEvents(database, normalizedProject, task.id);
2019
+ } finally {
2020
+ database.close();
2021
+ }
2022
+ }
2023
+ function overrideLineageTask(project, fields) {
2024
+ const normalizedProject = normalizeProject(project);
2025
+ const actor = normalizeActor(fields.actor, "Override actor");
2026
+ const reason = fields.reason.trim();
2027
+ if (!reason) throw new LineageTaskError("Override reason is required");
2028
+ const database = lineageDb();
2029
+ try {
2030
+ const task = requireTask(database, normalizedProject, fields.taskId);
2031
+ if (!["claimed", "in_progress"].includes(task.status)) {
2032
+ throw new LineageTaskError(`Only claimed or in-progress lineage tasks can be overridden; task is ${task.status}.`, 409);
2033
+ }
2034
+ const timestamp = nowIso();
2035
+ const instructions = fields.instructions === void 0 ? task.instructions : fields.instructions.trim() || void 0;
2036
+ const metadata = metadataWithoutClaim(task.metadata);
2037
+ transaction(database, () => {
2038
+ const result = database.prepare(`
2039
+ update lineage_tasks
2040
+ set status = 'pending', instructions = ?, claimed_at = null, started_at = null,
2041
+ updated_at = ?, metadata_json = ?
2042
+ where project_id = ? and id = ? and status in ('claimed', 'in_progress')
2043
+ `).run(instructions || null, timestamp, metadataJson2(metadata), normalizedProject, task.id);
2044
+ assertChanged(result, `Only active lineage task ${task.id} could be overridden.`);
2045
+ if (task.claimed_by_claim_id) {
2046
+ revokeAgentClaimInDatabase(database, normalizedProject, task.claimed_by_claim_id, {
2047
+ actor,
2048
+ reason
2049
+ });
2050
+ }
2051
+ recordEvent2(database, task.id, "human_override", actor, reason, {
2052
+ previous_claim_id: task.claimed_by_claim_id,
2053
+ previous_status: task.status
2054
+ });
2055
+ });
2056
+ return taskWithEvents(database, normalizedProject, task.id);
2057
+ } finally {
2058
+ database.close();
2059
+ }
2060
+ }
2061
+ function cancelLineageTask(project, fields) {
2062
+ const normalizedProject = normalizeProject(project);
2063
+ const actor = normalizeActor(fields.actor, "Cancel actor");
2064
+ const database = lineageDb();
2065
+ try {
2066
+ const task = requireTask(database, normalizedProject, fields.taskId);
2067
+ if (task.status === "cancelled") return { project: normalizedProject, ok: true, task, events: taskEvents(database, task.id) };
2068
+ if (!["pending", "claimed", "in_progress"].includes(task.status)) {
2069
+ throw new LineageTaskError(`Only open lineage tasks can be cancelled; task is ${task.status}.`, 409);
2070
+ }
2071
+ if (task.status !== "pending" && !fields.override) {
2072
+ throw new LineageTaskError("Cancelling an active lineage task requires override=true.", 409);
2073
+ }
2074
+ const overridingActiveTask = fields.override === true && task.status !== "pending";
2075
+ const metadata = overridingActiveTask ? metadataWithoutClaim(task.metadata) : task.metadata;
2076
+ const timestamp = nowIso();
2077
+ const cancelledTask = {
2078
+ ...task,
2079
+ cancelled_at: timestamp,
2080
+ claimed_at: overridingActiveTask ? void 0 : task.claimed_at,
2081
+ claimed_by_claim_id: overridingActiveTask ? void 0 : task.claimed_by_claim_id,
2082
+ started_at: overridingActiveTask ? void 0 : task.started_at,
2083
+ status: "cancelled",
2084
+ updated_at: timestamp,
2085
+ metadata
2086
+ };
2087
+ if (!fields.confirmWrite) return { project: normalizedProject, ok: true, dryRun: true, task: cancelledTask, events: taskEvents(database, task.id) };
2088
+ transaction(database, () => {
2089
+ const result = database.prepare(`
2090
+ update lineage_tasks
2091
+ set status = 'cancelled', cancelled_at = ?, claimed_at = ?, started_at = ?,
2092
+ updated_at = ?, metadata_json = ?
2093
+ where id = ? and status = ?
2094
+ `).run(
2095
+ timestamp,
2096
+ overridingActiveTask ? null : task.claimed_at || null,
2097
+ overridingActiveTask ? null : task.started_at || null,
2098
+ timestamp,
2099
+ metadataJson2(metadata),
2100
+ task.id,
2101
+ task.status
2102
+ );
2103
+ assertChanged(result, `Only ${task.status} lineage task ${task.id} could be cancelled.`);
2104
+ if (overridingActiveTask) {
2105
+ if (task.claimed_by_claim_id) {
2106
+ revokeAgentClaimInDatabase(database, normalizedProject, task.claimed_by_claim_id, {
2107
+ actor,
2108
+ reason: "Lineage task cancelled by human override."
2109
+ });
2110
+ }
2111
+ recordEvent2(database, task.id, "human_override", actor, "Lineage task cancelled by human override.", {
2112
+ previous_claim_id: task.claimed_by_claim_id,
2113
+ previous_status: task.status
2114
+ });
2115
+ }
2116
+ recordEvent2(database, task.id, "cancelled", actor, "Lineage task cancelled.");
2117
+ });
2118
+ return taskWithEvents(database, normalizedProject, task.id);
2119
+ } finally {
2120
+ database.close();
2121
+ }
2122
+ }
2123
+ function resolveLineageTask(project, fields) {
2124
+ const normalizedProject = normalizeProject(project);
2125
+ const actor = normalizeActor(fields.actor, "Resolve actor");
2126
+ const database = lineageDb();
2127
+ try {
2128
+ const task = requireTask(database, normalizedProject, fields.taskId);
2129
+ if (fields.resolvedAssetId) requireAsset(database, normalizedProject, fields.resolvedAssetId);
2130
+ if (task.status === "resolved") return { project: normalizedProject, ok: true, task, events: taskEvents(database, task.id) };
2131
+ if (!["pending", "claimed", "in_progress"].includes(task.status)) {
2132
+ throw new LineageTaskError(`Only open lineage tasks can be resolved; task is ${task.status}.`, 409);
2133
+ }
2134
+ const timestamp = nowIso();
2135
+ const resolvedTask = {
2136
+ ...task,
2137
+ status: "resolved",
2138
+ resolved_asset_id: fields.resolvedAssetId,
2139
+ resolved_at: timestamp,
2140
+ resolved_generation_job_id: fields.resolvedGenerationJobId,
2141
+ updated_at: timestamp
2142
+ };
2143
+ if (!fields.confirmWrite) return { project: normalizedProject, ok: true, dryRun: true, task: resolvedTask, events: taskEvents(database, task.id) };
2144
+ transaction(database, () => {
2145
+ const result = database.prepare(`
2146
+ update lineage_tasks
2147
+ set status = 'resolved', resolved_at = ?, resolved_generation_job_id = ?, resolved_asset_id = ?, updated_at = ?
2148
+ where id = ? and status = ?
2149
+ `).run(timestamp, fields.resolvedGenerationJobId || null, fields.resolvedAssetId || null, timestamp, task.id, task.status);
2150
+ assertChanged(result, `Only ${task.status} lineage task ${task.id} could be resolved.`);
2151
+ recordEvent2(database, task.id, "resolved", actor, "Lineage task resolved.", {
2152
+ resolved_asset_id: fields.resolvedAssetId,
2153
+ resolved_generation_job_id: fields.resolvedGenerationJobId
2154
+ });
2155
+ });
2156
+ return taskWithEvents(database, normalizedProject, task.id);
2157
+ } finally {
2158
+ database.close();
2159
+ }
2160
+ }
2161
+
1217
2162
  // src/server/assetLineageWorkspaces.ts
1218
2163
  function lineageWorkspaceId(project, rootAssetId) {
1219
2164
  return `${project}:lineage-workspace:${rootAssetId}`;
@@ -1336,7 +2281,71 @@ var LineageError = class extends Error {
1336
2281
  }
1337
2282
  status;
1338
2283
  };
1339
- function requireAsset(database, project, assetId) {
2284
+ function collectAssets(project, source) {
2285
+ const first = listAssets(project, { source, page: 1, pageSize: 100 });
2286
+ const assets = [...first.assets];
2287
+ for (let page = 2; page <= first.pagination.totalPages; page += 1) {
2288
+ assets.push(...listAssets(project, { source, page, pageSize: 100 }).assets);
2289
+ }
2290
+ return assets;
2291
+ }
2292
+ function upsertProject(database, project) {
2293
+ const timestamp = nowIso();
2294
+ database.prepare(`
2295
+ insert into projects (id, product, catalog_path, created_at, updated_at)
2296
+ values (?, ?, ?, ?, ?)
2297
+ on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
2298
+ `).run(project, project, join5(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
2299
+ }
2300
+ function upsertAsset(database, project, asset) {
2301
+ const timestamp = nowIso();
2302
+ const source = asset.source === "local" ? "local" : "catalog";
2303
+ database.prepare(`
2304
+ insert into assets (
2305
+ id, project_id, source, local_path, s3_key, checksum_sha256, media_type, title, status,
2306
+ channel, campaign, audience, size_bytes, content_type, created_at, updated_at, last_seen_at
2307
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2308
+ on conflict(id) do update set
2309
+ source = excluded.source, local_path = excluded.local_path, s3_key = excluded.s3_key,
2310
+ checksum_sha256 = excluded.checksum_sha256, media_type = excluded.media_type,
2311
+ title = excluded.title, status = excluded.status, channel = excluded.channel,
2312
+ campaign = excluded.campaign, audience = excluded.audience, size_bytes = excluded.size_bytes,
2313
+ content_type = excluded.content_type, updated_at = excluded.updated_at, last_seen_at = excluded.last_seen_at
2314
+ `).run(
2315
+ asset.asset_id,
2316
+ project,
2317
+ source,
2318
+ asset.local?.relative_path || null,
2319
+ asset.s3?.key || null,
2320
+ asset.local?.checksum_sha256 || asset.s3?.checksum_sha256 || null,
2321
+ asset.content_type,
2322
+ asset.title,
2323
+ asset.status,
2324
+ asset.channel || null,
2325
+ asset.campaign || null,
2326
+ asset.audience || null,
2327
+ asset.local?.size_bytes || asset.s3?.size_bytes || null,
2328
+ asset.local?.content_type || asset.s3?.content_type || null,
2329
+ timestamp,
2330
+ timestamp,
2331
+ timestamp
2332
+ );
2333
+ database.prepare(`
2334
+ insert into asset_reviews (asset_id, review_state, updated_at)
2335
+ values (?, 'unreviewed', ?)
2336
+ on conflict(asset_id) do nothing
2337
+ `).run(asset.asset_id, timestamp);
2338
+ }
2339
+ function indexLineageAssets(project = defaultProject) {
2340
+ const database = lineageDb();
2341
+ const catalog = collectAssets(project, "catalog");
2342
+ const local = collectAssets(project, "local");
2343
+ upsertProject(database, project);
2344
+ for (const asset of [...catalog, ...local]) upsertAsset(database, project, asset);
2345
+ database.close();
2346
+ return { catalog: catalog.length, local: local.length, total: catalog.length + local.length, database: lineageDbPath() };
2347
+ }
2348
+ function requireAsset2(database, project, assetId) {
1340
2349
  const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
1341
2350
  if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
1342
2351
  }
@@ -1355,6 +2364,11 @@ function rootFor(database, project, assetId) {
1355
2364
  }
1356
2365
  return assetId;
1357
2366
  }
2367
+ function assertCanonicalRoot(database, project, root) {
2368
+ requireAsset2(database, project, root);
2369
+ const canonicalRoot = rootFor(database, project, root);
2370
+ if (canonicalRoot !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
2371
+ }
1358
2372
  function explicitWorkspaceRoot(database, project, assetId) {
1359
2373
  const row = database.prepare(`
1360
2374
  select root_asset_id from lineage_workspaces
@@ -1388,7 +2402,7 @@ function lineageWriteClaimContext(database, project, assetId) {
1388
2402
  function getLineageWriteClaimContext(project, assetId) {
1389
2403
  const database = lineageDb();
1390
2404
  try {
1391
- requireAsset(database, project, assetId);
2405
+ requireAsset2(database, project, assetId);
1392
2406
  return lineageWriteClaimContext(database, project, assetId);
1393
2407
  } finally {
1394
2408
  database.close();
@@ -1400,17 +2414,113 @@ function latestSelectedRoot(database, project) {
1400
2414
  }
1401
2415
  function resolveRoot(database, project, rootAssetId) {
1402
2416
  if (rootAssetId) {
1403
- requireAsset(database, project, rootAssetId);
2417
+ requireAsset2(database, project, rootAssetId);
1404
2418
  return rootAssetId;
1405
2419
  }
1406
2420
  const root = activeLineageWorkspaceRoot(project) || latestSelectedRoot(database, project);
1407
2421
  if (!root) throw new LineageError("Lineage command requires --root unless a project selection exists");
1408
- requireAsset(database, project, root);
2422
+ requireAsset2(database, project, root);
1409
2423
  return root;
1410
2424
  }
1411
2425
  function edgeId(project, parent, child) {
1412
2426
  return `${project}:${parent}:derived_from:${child}`;
1413
2427
  }
2428
+ function rerollRequestId(project, root, node, timestamp) {
2429
+ return `${project}:${root}:reroll:${node}:${Date.parse(timestamp) || Date.now()}`;
2430
+ }
2431
+ function rowString(value) {
2432
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2433
+ }
2434
+ function rerollRequestFrom(row) {
2435
+ return {
2436
+ id: String(row.id),
2437
+ project_id: String(row.project_id),
2438
+ root_asset_id: String(row.root_asset_id),
2439
+ node_asset_id: String(row.node_asset_id),
2440
+ status: row.status,
2441
+ requested_by: row.requested_by,
2442
+ notes: rowString(row.notes),
2443
+ created_at: String(row.created_at),
2444
+ resolved_at: rowString(row.resolved_at)
2445
+ };
2446
+ }
2447
+ function tasksByTarget(tasks) {
2448
+ const byTarget = /* @__PURE__ */ new Map();
2449
+ for (const task of tasks) {
2450
+ if (task.task_type !== "iterate" && task.task_type !== "reroll") continue;
2451
+ const existing = byTarget.get(task.target_asset_id) || {};
2452
+ existing[task.task_type] = task;
2453
+ byTarget.set(task.target_asset_id, existing);
2454
+ }
2455
+ return byTarget;
2456
+ }
2457
+ function withRerollTask(request, task) {
2458
+ return task ? { ...request, task_id: task.id, task } : request;
2459
+ }
2460
+ function taskBackedRerollRequest(project, rootAssetId, task) {
2461
+ return {
2462
+ id: `${task.id}:request`,
2463
+ project_id: project,
2464
+ root_asset_id: rootAssetId,
2465
+ node_asset_id: task.target_asset_id,
2466
+ status: "pending",
2467
+ requested_by: task.created_by,
2468
+ notes: task.instructions,
2469
+ created_at: task.created_at,
2470
+ task_id: task.id,
2471
+ task
2472
+ };
2473
+ }
2474
+ function attemptFrom(row) {
2475
+ return {
2476
+ id: String(row.id),
2477
+ project_id: String(row.project_id),
2478
+ node_asset_id: String(row.node_asset_id),
2479
+ asset_id: String(row.asset_id),
2480
+ attempt_index: Number(row.attempt_index),
2481
+ source: row.source,
2482
+ prompt: rowString(row.prompt),
2483
+ generation_job_id: rowString(row.generation_job_id),
2484
+ file_path: rowString(row.file_path),
2485
+ checksum_sha256: rowString(row.checksum_sha256),
2486
+ created_at: String(row.created_at),
2487
+ promoted_at: rowString(row.promoted_at),
2488
+ is_current: Boolean(Number(row.is_current))
2489
+ };
2490
+ }
2491
+ function implicitAttempt(row, isCurrent = true) {
2492
+ const createdAt = row.asset_created_at || nowIso();
2493
+ return {
2494
+ id: `${row.project}:${row.asset_id}:attempt:implicit`,
2495
+ project_id: row.project,
2496
+ node_asset_id: row.asset_id,
2497
+ asset_id: row.asset_id,
2498
+ attempt_index: 1,
2499
+ source: "initial",
2500
+ file_path: row.local_path,
2501
+ checksum_sha256: row.checksum_sha256,
2502
+ created_at: createdAt,
2503
+ promoted_at: createdAt,
2504
+ is_current: isCurrent
2505
+ };
2506
+ }
2507
+ function withImplicitAttempt(physicalAttempts, row) {
2508
+ if (physicalAttempts.some((attempt) => attempt.source === "initial")) return physicalAttempts;
2509
+ return [...physicalAttempts, implicitAttempt(row, !physicalAttempts.some((attempt) => attempt.is_current))];
2510
+ }
2511
+ function assertNodeInRoot(database, project, root, node) {
2512
+ assertCanonicalRoot(database, project, root);
2513
+ requireAsset2(database, project, node);
2514
+ const nodeRoot = rootFor(database, project, node);
2515
+ if (nodeRoot !== root) throw new LineageError(`Asset ${node} is not in lineage rooted at ${root}`);
2516
+ }
2517
+ function assertAttemptAssetNotVisibleLineageNode(database, project, root, assetId) {
2518
+ const visibleNodeIds = /* @__PURE__ */ new Set([
2519
+ root,
2520
+ ...descendants(database, project, root).flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id])
2521
+ ]);
2522
+ if (visibleNodeIds.has(assetId)) throw new LineageError(`Attempt asset ${assetId} is already a visible lineage node in ${root}`, 400);
2523
+ }
1414
2524
  function canPreviewLocally(mediaType, localPath) {
1415
2525
  return Boolean(localPath && ["image", "video", "gif"].includes(mediaType));
1416
2526
  }
@@ -1421,8 +2531,8 @@ function localPreviewUrl(project, localPath) {
1421
2531
  }
1422
2532
  function linkLineageAssets(project, fields) {
1423
2533
  const database = lineageDb();
1424
- requireAsset(database, project, fields.parentAssetId);
1425
- requireAsset(database, project, fields.childAssetId);
2534
+ requireAsset2(database, project, fields.parentAssetId);
2535
+ requireAsset2(database, project, fields.childAssetId);
1426
2536
  if (fields.parentAssetId === fields.childAssetId) throw new LineageError("Lineage link cannot point to itself");
1427
2537
  const claimContext = lineageWriteClaimContext(database, project, fields.parentAssetId);
1428
2538
  try {
@@ -1473,20 +2583,37 @@ function descendants(database, project, root) {
1473
2583
  }
1474
2584
  function getLineageSnapshot(project, assetId) {
1475
2585
  const database = lineageDb();
1476
- requireAsset(database, project, assetId);
2586
+ requireAsset2(database, project, assetId);
1477
2587
  const root = explicitWorkspaceRoot(database, project, assetId) || rootFor(database, project, assetId);
1478
2588
  const edges = descendants(database, project, root);
1479
- const ids = [.../* @__PURE__ */ new Set([root, ...edges.flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id])])];
2589
+ const selected = selectedRows(database, project, root);
2590
+ const tasks = listLineageTasks(project, root).tasks;
2591
+ const ids = [.../* @__PURE__ */ new Set([
2592
+ root,
2593
+ ...edges.flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id]),
2594
+ ...selected.map((row) => row.asset_id),
2595
+ ...tasks.map((task) => task.target_asset_id)
2596
+ ])];
1480
2597
  const placeholders = ids.map(() => "?").join(",");
1481
2598
  const rows = database.prepare(`
1482
2599
  select a.id asset_id, a.project_id project, a.source, a.title, a.media_type, a.status, a.channel, a.campaign,
1483
2600
  a.local_path, a.s3_key, a.checksum_sha256, coalesce(r.review_state, 'unreviewed') review_state,
1484
- r.notes review_notes, l.x layout_x, l.y layout_y
2601
+ r.notes review_notes, a.created_at asset_created_at, l.x layout_x, l.y layout_y
1485
2602
  from assets a left join asset_reviews r on r.asset_id = a.id
1486
2603
  left join asset_layouts l on l.project_id = a.project_id and l.root_asset_id = ? and l.asset_id = a.id
1487
2604
  where a.project_id = ? and a.id in (${placeholders})
1488
2605
  `).all(root, project, ...ids);
1489
- const selected = selectedRows(database, project, root);
2606
+ const attemptRows = ids.length > 0 ? database.prepare(`select * from asset_attempts where project_id = ? and node_asset_id in (${placeholders}) order by node_asset_id, attempt_index desc`).all(project, ...ids) : [];
2607
+ const attemptsByNode = /* @__PURE__ */ new Map();
2608
+ for (const attempt of attemptRows.map(attemptFrom)) {
2609
+ attemptsByNode.set(attempt.node_asset_id, [...attemptsByNode.get(attempt.node_asset_id) || [], attempt]);
2610
+ }
2611
+ const lineageTasksByNode = tasksByTarget(tasks);
2612
+ const rerollRows = ids.length > 0 ? database.prepare(`select * from asset_reroll_requests where project_id = ? and root_asset_id = ? and status = 'pending' and node_asset_id in (${placeholders}) order by created_at`).all(project, root, ...ids) : [];
2613
+ const rerollsByNode = new Map(rerollRows.map((row) => {
2614
+ const request = rerollRequestFrom(row);
2615
+ return [request.node_asset_id, withRerollTask(request, lineageTasksByNode.get(request.node_asset_id)?.reroll)];
2616
+ }));
1490
2617
  const childIds = new Set(edges.map((edge) => edge.parent_asset_id));
1491
2618
  const selectedIds = new Set(selected.map((row) => row.asset_id));
1492
2619
  const selections = selected.map((row) => ({
@@ -1498,13 +2625,21 @@ function getLineageSnapshot(project, assetId) {
1498
2625
  const selection = selections[0] || null;
1499
2626
  const nodes = rows.map((row) => {
1500
2627
  const position = typeof row.layout_x === "number" && typeof row.layout_y === "number" ? { x: row.layout_x, y: row.layout_y } : void 0;
1501
- const { layout_x: _layoutX, layout_y: _layoutY, ...node } = row;
2628
+ const { asset_created_at: _assetCreatedAt, layout_x: _layoutX, layout_y: _layoutY, ...node } = row;
1502
2629
  const nodeSelection = selections.find((item) => item.asset_id === row.asset_id);
2630
+ const attempts = withImplicitAttempt(attemptsByNode.get(row.asset_id) || [], row);
2631
+ const currentAttempt = attempts.find((attempt) => attempt.is_current) || attempts[0];
2632
+ const previewPath = currentAttempt?.file_path || row.local_path;
2633
+ const lineageTasks = lineageTasksByNode.get(row.asset_id);
1503
2634
  return {
1504
2635
  ...node,
2636
+ attempt_count: attempts.length,
2637
+ current_attempt: currentAttempt,
1505
2638
  is_latest: !childIds.has(row.asset_id),
2639
+ lineage_tasks: lineageTasks && Object.keys(lineageTasks).length > 0 ? lineageTasks : void 0,
1506
2640
  position,
1507
- preview_url: canPreviewLocally(row.media_type, row.local_path) ? localPreviewUrl(project, row.local_path) : void 0,
2641
+ preview_url: canPreviewLocally(row.media_type, previewPath) ? localPreviewUrl(project, previewPath) : void 0,
2642
+ reroll_request: rerollsByNode.get(row.asset_id),
1508
2643
  selection_note: nodeSelection?.notes,
1509
2644
  user_selected: selectedIds.has(row.asset_id)
1510
2645
  };
@@ -1517,6 +2652,7 @@ function getLineageSnapshot(project, assetId) {
1517
2652
  selected: selections.map((row) => row.asset_id),
1518
2653
  selection,
1519
2654
  selections,
2655
+ tasks,
1520
2656
  latest: nodes.filter((node) => node.is_latest).map((node) => node.asset_id),
1521
2657
  nodes,
1522
2658
  edges,
@@ -1528,7 +2664,25 @@ function getLineageNextAsset(project, rootAssetId) {
1528
2664
  const root = resolveRoot(database, project, rootAssetId);
1529
2665
  database.close();
1530
2666
  const snapshot = getLineageSnapshot(project, root);
1531
- const selectedNodes = snapshot.selected.map((assetId) => snapshot.nodes.find((node) => node.asset_id === assetId)).filter((node) => Boolean(node));
2667
+ const pendingIterateTasks = (snapshot.tasks || []).filter((task) => task.task_type === "iterate" && task.status === "pending");
2668
+ const taskIdsInSelectionOrder = [
2669
+ ...snapshot.selections.map((selection) => selection.asset_id).filter((assetId) => pendingIterateTasks.some((task) => task.target_asset_id === assetId)),
2670
+ ...pendingIterateTasks.map((task) => task.target_asset_id).filter((assetId) => !snapshot.selections.some((selection) => selection.asset_id === assetId))
2671
+ ];
2672
+ const taskSelectedIds = [...new Set(taskIdsInSelectionOrder)];
2673
+ const selectedIds = taskSelectedIds.length > 0 ? taskSelectedIds : snapshot.selected;
2674
+ const selectedNodes = selectedIds.map((assetId) => snapshot.nodes.find((node) => node.asset_id === assetId)).filter((node) => Boolean(node));
2675
+ const taskSelections = taskSelectedIds.map((assetId, position) => {
2676
+ const task = pendingIterateTasks.find((item) => item.target_asset_id === assetId);
2677
+ return {
2678
+ asset_id: assetId,
2679
+ notes: task?.instructions,
2680
+ position,
2681
+ selected_at: task?.created_at || nowIso()
2682
+ };
2683
+ });
2684
+ const selectedSelection = taskSelections.length > 0 ? taskSelections[0] : snapshot.selection;
2685
+ const selectedSelections = taskSelections.length > 0 ? taskSelections : snapshot.selections;
1532
2686
  const latestNodes = snapshot.nodes.filter((node) => snapshot.latest.includes(node.asset_id));
1533
2687
  const warnings = [];
1534
2688
  for (const selectedNode of selectedNodes) {
@@ -1546,9 +2700,9 @@ function getLineageNextAsset(project, rootAssetId) {
1546
2700
  next_asset: selectedNodes[0],
1547
2701
  next_assets: selectedNodes,
1548
2702
  latest: snapshot.latest,
1549
- selected: snapshot.selected,
1550
- selection: snapshot.selection,
1551
- selections: snapshot.selections,
2703
+ selected: selectedIds,
2704
+ selection: selectedSelection,
2705
+ selections: selectedSelections,
1552
2706
  candidates: latestNodes,
1553
2707
  warnings,
1554
2708
  fetchedAt: nowIso()
@@ -1565,9 +2719,9 @@ function getLineageNextAsset(project, rootAssetId) {
1565
2719
  next_asset: latestNodes[0],
1566
2720
  next_assets: [latestNodes[0]],
1567
2721
  latest: snapshot.latest,
1568
- selected: snapshot.selected,
1569
- selection: snapshot.selection,
1570
- selections: snapshot.selections,
2722
+ selected: selectedIds,
2723
+ selection: selectedSelection,
2724
+ selections: selectedSelections,
1571
2725
  candidates: latestNodes,
1572
2726
  warnings,
1573
2727
  fetchedAt: nowIso()
@@ -1583,14 +2737,202 @@ function getLineageNextAsset(project, rootAssetId) {
1583
2737
  next_asset: null,
1584
2738
  next_assets: [],
1585
2739
  latest: snapshot.latest,
1586
- selected: snapshot.selected,
1587
- selection: snapshot.selection,
1588
- selections: snapshot.selections,
2740
+ selected: selectedIds,
2741
+ selection: selectedSelection,
2742
+ selections: selectedSelections,
1589
2743
  candidates: latestNodes,
1590
2744
  warnings,
1591
2745
  fetchedAt: nowIso()
1592
2746
  };
1593
2747
  }
2748
+ function listLineageRerollRequests(project, rootAssetId) {
2749
+ const database = lineageDb();
2750
+ try {
2751
+ assertCanonicalRoot(database, project, rootAssetId);
2752
+ const rerollTasks = listLineageTasks(project, rootAssetId).tasks.filter((task) => task.task_type === "reroll");
2753
+ const pendingRerollTasks = listLineageTasks(project, rootAssetId, ["pending"]).tasks.filter((task) => task.task_type === "reroll");
2754
+ const rerollTaskByTarget = new Map(rerollTasks.map((task) => [task.target_asset_id, task]));
2755
+ const rows = database.prepare(`
2756
+ select * from asset_reroll_requests
2757
+ where project_id = ? and root_asset_id = ? and status = 'pending'
2758
+ order by created_at, id
2759
+ `).all(project, rootAssetId);
2760
+ const requests = rows.map((row) => {
2761
+ const request = rerollRequestFrom(row);
2762
+ return withRerollTask(request, rerollTaskByTarget.get(request.node_asset_id));
2763
+ });
2764
+ const legacyTargets = new Set(requests.map((request) => request.node_asset_id));
2765
+ for (const task of pendingRerollTasks) {
2766
+ if (!legacyTargets.has(task.target_asset_id)) requests.push(taskBackedRerollRequest(project, rootAssetId, task));
2767
+ }
2768
+ return { project, root_asset_id: rootAssetId, requests, fetchedAt: nowIso() };
2769
+ } finally {
2770
+ database.close();
2771
+ }
2772
+ }
2773
+ function recordLineageRerollAttempt(project, fields) {
2774
+ const database = lineageDb();
2775
+ try {
2776
+ assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
2777
+ requireAsset2(database, project, fields.assetId);
2778
+ assertAttemptAssetNotVisibleLineageNode(database, project, fields.rootAssetId, fields.assetId);
2779
+ const timestamp = nowIso();
2780
+ const maxRow = database.prepare("select max(attempt_index) max_index from asset_attempts where project_id = ? and node_asset_id = ?").get(project, fields.nodeAssetId);
2781
+ const attemptIndex = Number(maxRow.max_index || 1) + 1;
2782
+ const attempt = {
2783
+ id: `${project}:${fields.nodeAssetId}:attempt:${attemptIndex}`,
2784
+ project_id: project,
2785
+ node_asset_id: fields.nodeAssetId,
2786
+ asset_id: fields.assetId,
2787
+ attempt_index: attemptIndex,
2788
+ source: "reroll",
2789
+ prompt: fields.prompt,
2790
+ generation_job_id: fields.generationJobId,
2791
+ file_path: fields.filePath,
2792
+ checksum_sha256: fields.checksumSha256,
2793
+ created_at: timestamp,
2794
+ promoted_at: timestamp,
2795
+ is_current: true
2796
+ };
2797
+ if (!fields.confirmWrite) return { ok: true, dryRun: true, attempt };
2798
+ database.exec("BEGIN IMMEDIATE");
2799
+ try {
2800
+ database.prepare("update asset_attempts set is_current = 0 where project_id = ? and node_asset_id = ?").run(project, fields.nodeAssetId);
2801
+ database.prepare(`
2802
+ insert into asset_attempts (
2803
+ id, project_id, node_asset_id, asset_id, attempt_index, source, prompt, generation_job_id,
2804
+ file_path, checksum_sha256, created_at, promoted_at, is_current
2805
+ ) values (?, ?, ?, ?, ?, 'reroll', ?, ?, ?, ?, ?, ?, 1)
2806
+ `).run(
2807
+ attempt.id,
2808
+ project,
2809
+ fields.nodeAssetId,
2810
+ fields.assetId,
2811
+ attempt.attempt_index,
2812
+ fields.prompt,
2813
+ fields.generationJobId,
2814
+ fields.filePath,
2815
+ fields.checksumSha256,
2816
+ timestamp,
2817
+ timestamp
2818
+ );
2819
+ database.prepare(`
2820
+ update asset_reroll_requests
2821
+ set status = 'resolved', resolved_at = ?
2822
+ where project_id = ? and root_asset_id = ? and node_asset_id = ? and status = 'pending'
2823
+ `).run(timestamp, project, fields.rootAssetId, fields.nodeAssetId);
2824
+ database.prepare(`
2825
+ insert into asset_reviews (asset_id, review_state, reviewed_at, ignored_at, notes, updated_at)
2826
+ values (?, 'unreviewed', ?, null, null, ?)
2827
+ on conflict(asset_id) do update set
2828
+ review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
2829
+ ignored_at = excluded.ignored_at, updated_at = excluded.updated_at
2830
+ `).run(fields.nodeAssetId, timestamp, timestamp);
2831
+ database.exec("COMMIT");
2832
+ } catch (error) {
2833
+ database.exec("ROLLBACK");
2834
+ throw error;
2835
+ }
2836
+ return { ok: true, attempt };
2837
+ } finally {
2838
+ database.close();
2839
+ }
2840
+ }
2841
+ function markLineageRerollRequest(project, fields) {
2842
+ const database = lineageDb();
2843
+ try {
2844
+ assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
2845
+ const existing = database.prepare(`
2846
+ select * from asset_reroll_requests
2847
+ where project_id = ? and root_asset_id = ? and node_asset_id = ? and status = 'pending'
2848
+ order by created_at desc limit 1
2849
+ `).get(project, fields.rootAssetId, fields.nodeAssetId);
2850
+ const timestamp = nowIso();
2851
+ const request = existing ? {
2852
+ ...rerollRequestFrom(existing),
2853
+ notes: fields.notes || rerollRequestFrom(existing).notes,
2854
+ requested_by: fields.requestedBy || rerollRequestFrom(existing).requested_by
2855
+ } : {
2856
+ id: rerollRequestId(project, fields.rootAssetId, fields.nodeAssetId, timestamp),
2857
+ project_id: project,
2858
+ root_asset_id: fields.rootAssetId,
2859
+ node_asset_id: fields.nodeAssetId,
2860
+ status: "pending",
2861
+ requested_by: fields.requestedBy || "human",
2862
+ notes: fields.notes,
2863
+ created_at: timestamp
2864
+ };
2865
+ if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
2866
+ const taskResult = upsertLineageTask(project, {
2867
+ createdBy: request.requested_by,
2868
+ instructions: request.notes,
2869
+ rootAssetId: fields.rootAssetId,
2870
+ targetAssetId: fields.nodeAssetId,
2871
+ taskType: "reroll"
2872
+ });
2873
+ if (existing) {
2874
+ database.prepare(`
2875
+ update asset_reroll_requests
2876
+ set requested_by = ?, notes = ?
2877
+ where id = ?
2878
+ `).run(request.requested_by, request.notes || null, request.id);
2879
+ } else {
2880
+ database.prepare(`
2881
+ insert into asset_reroll_requests (id, project_id, root_asset_id, node_asset_id, status, requested_by, notes, created_at, resolved_at)
2882
+ values (?, ?, ?, ?, 'pending', ?, ?, ?, null)
2883
+ `).run(request.id, project, fields.rootAssetId, fields.nodeAssetId, request.requested_by, request.notes || null, request.created_at);
2884
+ }
2885
+ database.prepare(`
2886
+ insert into asset_reviews (asset_id, review_state, reviewed_at, ignored_at, notes, updated_at)
2887
+ values (?, 'needs_revision', ?, null, ?, ?)
2888
+ on conflict(asset_id) do update set
2889
+ review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
2890
+ ignored_at = excluded.ignored_at, notes = coalesce(excluded.notes, asset_reviews.notes), updated_at = excluded.updated_at
2891
+ `).run(fields.nodeAssetId, timestamp, request.notes || null, timestamp);
2892
+ return {
2893
+ ok: true,
2894
+ request: withRerollTask(rerollRequestFrom(database.prepare("select * from asset_reroll_requests where id = ?").get(request.id)), taskResult.task),
2895
+ task_id: taskResult.task.id,
2896
+ task: taskResult.task
2897
+ };
2898
+ } finally {
2899
+ database.close();
2900
+ }
2901
+ }
2902
+ function clearLineageRerollRequest(project, fields) {
2903
+ const database = lineageDb();
2904
+ try {
2905
+ assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
2906
+ const existing = database.prepare(`
2907
+ select * from asset_reroll_requests
2908
+ where project_id = ? and root_asset_id = ? and node_asset_id = ? and status = 'pending'
2909
+ order by created_at desc limit 1
2910
+ `).get(project, fields.rootAssetId, fields.nodeAssetId);
2911
+ if (!existing) throw new LineageError(`No pending re-roll request for ${fields.nodeAssetId}`, 404);
2912
+ const timestamp = nowIso();
2913
+ const request = { ...rerollRequestFrom(existing), status: "cancelled", resolved_at: timestamp };
2914
+ if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
2915
+ const task = listLineageTasks(project, fields.rootAssetId, ["pending"]).tasks.find((item) => item.task_type === "reroll" && item.target_asset_id === fields.nodeAssetId);
2916
+ const cancelledTask = task ? cancelLineageTask(project, {
2917
+ actor: "human",
2918
+ confirmWrite: true,
2919
+ taskId: task.id
2920
+ }).task : void 0;
2921
+ database.prepare(`
2922
+ update asset_reroll_requests
2923
+ set status = 'cancelled', resolved_at = ?
2924
+ where id = ?
2925
+ `).run(timestamp, request.id);
2926
+ return {
2927
+ ok: true,
2928
+ request: withRerollTask(request, cancelledTask),
2929
+ task_id: cancelledTask?.id,
2930
+ task: cancelledTask
2931
+ };
2932
+ } finally {
2933
+ database.close();
2934
+ }
2935
+ }
1594
2936
 
1595
2937
  // src/server/assetLineageHandoff.ts
1596
2938
  var publicPackageCommand = "npx @mean-weasel/lineage";
@@ -1603,6 +2945,9 @@ function lineageCommand(command, project, rootAssetId) {
1603
2945
  function linkChildCommand(project, rootAssetId) {
1604
2946
  return `${publicPackageCommand} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write --db ${shellQuote(lineageDbPath())} --json`;
1605
2947
  }
2948
+ function rerollImportGuidance(rootAssetId, targetAssetId) {
2949
+ return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
2950
+ }
1606
2951
  function getLineageBrief(project, rootAssetId) {
1607
2952
  const next = getLineageNextAsset(project, rootAssetId);
1608
2953
  const assets = next.next_assets;
@@ -1649,6 +2994,14 @@ function getLineageBrief(project, rootAssetId) {
1649
2994
  function linkSelectedLineageChild(project, fields) {
1650
2995
  const next = getLineageNextAsset(project, fields.rootAssetId);
1651
2996
  if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
2997
+ const rerollRequests = listLineageRerollRequests(project, next.root_asset_id).requests;
2998
+ const pendingRerollForParent = rerollRequests.find((request) => request.node_asset_id === next.next_asset?.asset_id);
2999
+ if (pendingRerollForParent && fields.confirmWrite) {
3000
+ throw new LineageError(
3001
+ `Pending re-roll exists for ${pendingRerollForParent.node_asset_id}. lineage link-child creates a visible child variation edge; it does not re-roll the same node. ${rerollImportGuidance(next.root_asset_id, pendingRerollForParent.node_asset_id)} Cancel the re-roll first if you intentionally want a new child variation.`,
3002
+ 409
3003
+ );
3004
+ }
1652
3005
  if (fields.confirmWrite) {
1653
3006
  const claimContext = getLineageWriteClaimContext(project, next.next_asset.asset_id);
1654
3007
  const validation = validateAgentClaimForWrite({
@@ -1675,21 +3028,325 @@ function linkSelectedLineageChild(project, fields) {
1675
3028
  parent_asset_id: next.next_asset.asset_id,
1676
3029
  child_asset_id: fields.childAssetId,
1677
3030
  reference_asset_ids: next.next_assets.map((asset) => asset.asset_id),
1678
- warning: next.next_assets.length > 1 ? "Linked child to the primary selected base; add explicit edges to other selected references if the child derives from them too." : void 0
3031
+ warning: [
3032
+ pendingRerollForParent ? `Pending re-roll exists for ${pendingRerollForParent.node_asset_id}. link-child would create a visible child variation; use reroll plan/import to update the same node attempt.` : void 0,
3033
+ rerollRequests.length > 0 && !pendingRerollForParent ? `This lineage has ${rerollRequests.length} pending re-roll target(s). link-child is only for new visible child variations, not re-roll attempts.` : void 0,
3034
+ next.next_assets.length > 1 ? "Linked child to the primary selected base; add explicit edges to other selected references if the child derives from them too." : void 0
3035
+ ].filter(Boolean).join(" ") || void 0
1679
3036
  };
1680
3037
  }
1681
3038
 
3039
+ // src/server/generationReceipts.ts
3040
+ import { existsSync as existsSync4, realpathSync, statSync as statSync3 } from "node:fs";
3041
+ import { relative as relative2, resolve as resolve3 } from "node:path";
3042
+ import { randomUUID } from "node:crypto";
3043
+ var adapterVersion = "generation-receipts-v1";
3044
+ var provider = "codex-handoff";
3045
+ var GenerationReceiptError = class extends Error {
3046
+ constructor(message, status = 400) {
3047
+ super(message);
3048
+ this.status = status;
3049
+ }
3050
+ status;
3051
+ };
3052
+ function jobId() {
3053
+ return `gen-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
3054
+ }
3055
+ function quote(value) {
3056
+ return JSON.stringify(value);
3057
+ }
3058
+ function parseJson(value, fallback) {
3059
+ if (!value) return fallback;
3060
+ return JSON.parse(value);
3061
+ }
3062
+ function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
3063
+ const importCommand = `npx lineage reroll import --project ${quote(project)} --job-id ${quote(id)} --file <.asset-scratch-file> --confirm-write --json`;
3064
+ return {
3065
+ schema_version: "lineage.generation_handoff.v1",
3066
+ provider,
3067
+ project,
3068
+ job_id: id,
3069
+ prompt,
3070
+ expected_output_count: 1,
3071
+ lineage: {
3072
+ root_asset_id: rootAssetId,
3073
+ parent_asset_id: target.asset_id,
3074
+ selection_strategy: "reroll_request",
3075
+ parent_title: target.title,
3076
+ parent_local_path: target.local_path,
3077
+ parent_s3_key: target.s3_key
3078
+ },
3079
+ instructions: [
3080
+ "Use Codex image generation outside Lineage server code.",
3081
+ "Write the regenerated output file under .asset-scratch before import.",
3082
+ "Do not call live provider APIs from the CLI or server.",
3083
+ "Import exactly one output with reroll import, not link-child or generate image import.",
3084
+ `Resolve re-roll request ${request.id}; do not create a visible lineage child edge.`
3085
+ ],
3086
+ import_command: importCommand,
3087
+ guardrails: { live_generation: false, external_services: false, output_root: ".asset-scratch", confirm_write_required: true }
3088
+ };
3089
+ }
3090
+ function receiptFrom(row) {
3091
+ return {
3092
+ id: String(row.id),
3093
+ job_id: String(row.job_id),
3094
+ receipt_type: row.receipt_type,
3095
+ status: row.status,
3096
+ command: String(row.command),
3097
+ payload: parseJson(String(row.payload_json), null),
3098
+ created_at: String(row.created_at)
3099
+ };
3100
+ }
3101
+ function outputFrom(row) {
3102
+ return {
3103
+ id: String(row.id),
3104
+ job_id: String(row.job_id),
3105
+ project_id: String(row.project_id),
3106
+ output_index: Number(row.output_index),
3107
+ file_path: String(row.file_path),
3108
+ checksum_sha256: String(row.checksum_sha256),
3109
+ size_bytes: Number(row.size_bytes),
3110
+ content_type: String(row.content_type),
3111
+ imported_asset_id: String(row.imported_asset_id),
3112
+ parent_asset_id: String(row.parent_asset_id),
3113
+ imported_at: String(row.imported_at)
3114
+ };
3115
+ }
3116
+ function loadGenerationJob(database, project, id) {
3117
+ const row = database.prepare("select * from generation_jobs where project_id = ? and id = ?").get(project, id);
3118
+ if (!row) throw new GenerationReceiptError(`Unknown generation job: ${id}`, 404);
3119
+ const inputRows = database.prepare("select * from generation_job_inputs where job_id = ? order by position").all(id);
3120
+ const inputs = inputRows.map((input) => ({
3121
+ id: String(input.id),
3122
+ job_id: String(input.job_id),
3123
+ project_id: String(input.project_id),
3124
+ asset_id: String(input.asset_id),
3125
+ root_asset_id: String(input.root_asset_id),
3126
+ role: input.role,
3127
+ position: Number(input.position),
3128
+ selection_strategy: String(input.selection_strategy),
3129
+ selection_snapshot: parseJson(String(input.selection_snapshot_json), {})
3130
+ }));
3131
+ const outputs = database.prepare("select * from generation_job_outputs where job_id = ? order by output_index").all(id).map(outputFrom);
3132
+ const receipts = database.prepare("select * from generation_job_receipts where job_id = ? order by created_at, id").all(id).map(receiptFrom);
3133
+ return {
3134
+ id: String(row.id),
3135
+ project_id: String(row.project_id),
3136
+ provider: row.provider,
3137
+ adapter_version: String(row.adapter_version),
3138
+ source_mode: String(row.source_mode),
3139
+ root_asset_id: String(row.root_asset_id),
3140
+ prompt: String(row.prompt),
3141
+ expected_output_count: Number(row.expected_output_count),
3142
+ status: row.status,
3143
+ output_dir: typeof row.output_dir === "string" ? row.output_dir : void 0,
3144
+ handoff: parseJson(String(row.handoff_json), {}),
3145
+ created_at: String(row.created_at),
3146
+ updated_at: String(row.updated_at),
3147
+ imported_at: typeof row.imported_at === "string" ? row.imported_at : void 0,
3148
+ inputs,
3149
+ outputs,
3150
+ receipts
3151
+ };
3152
+ }
3153
+ function insertReceipt(database, id, type, command, payload) {
3154
+ database.prepare(`
3155
+ insert into generation_job_receipts (id, job_id, receipt_type, status, command, payload_json, created_at)
3156
+ values (?, ?, ?, 'ok', ?, ?, ?)
3157
+ `).run(`${id}:receipt:${type}:${Date.now()}`, id, type, command, JSON.stringify(payload), nowIso());
3158
+ }
3159
+ function planImageReroll(project = defaultProject, fields) {
3160
+ const prompt = fields.prompt.trim();
3161
+ if (!prompt) throw new GenerationReceiptError("Missing --prompt");
3162
+ if (!fields.rootAssetId) throw new GenerationReceiptError("Missing --root");
3163
+ if (!fields.targetAssetId) throw new GenerationReceiptError("Missing --target");
3164
+ const snapshot = getLineageSnapshot(project, fields.rootAssetId);
3165
+ const target = snapshot.nodes.find((node) => node.asset_id === fields.targetAssetId);
3166
+ if (!target) throw new GenerationReceiptError(`Re-roll target is not in lineage: ${fields.targetAssetId}`, 404);
3167
+ const request = listLineageRerollRequests(project, snapshot.root_asset_id).requests.find((item) => item.node_asset_id === fields.targetAssetId);
3168
+ if (!request) throw new GenerationReceiptError(`No pending re-roll request for ${fields.targetAssetId}`);
3169
+ const id = jobId();
3170
+ const timestamp = nowIso();
3171
+ const handoff = buildRerollHandoff(project, id, prompt, snapshot.root_asset_id, target, request);
3172
+ const input = {
3173
+ id: `${id}:input:0`,
3174
+ job_id: id,
3175
+ project_id: project,
3176
+ asset_id: target.asset_id,
3177
+ root_asset_id: snapshot.root_asset_id,
3178
+ role: "reroll_target",
3179
+ position: 0,
3180
+ selection_strategy: "reroll_request",
3181
+ selection_snapshot: {
3182
+ project,
3183
+ root_asset_id: snapshot.root_asset_id,
3184
+ strategy: "selected",
3185
+ selection_mode: "single",
3186
+ recommended_action: "evolve_variations",
3187
+ reason: "user_selected",
3188
+ next_asset: target,
3189
+ next_assets: [target],
3190
+ latest: snapshot.latest,
3191
+ selected: [target.asset_id],
3192
+ selection: null,
3193
+ selections: [],
3194
+ candidates: snapshot.nodes,
3195
+ warnings: ["Re-roll target: import output as an attempt, not a lineage child."],
3196
+ fetchedAt: timestamp
3197
+ }
3198
+ };
3199
+ const preview = {
3200
+ id,
3201
+ project_id: project,
3202
+ provider,
3203
+ adapter_version: adapterVersion,
3204
+ source_mode: "lineage_reroll",
3205
+ root_asset_id: snapshot.root_asset_id,
3206
+ prompt,
3207
+ expected_output_count: 1,
3208
+ status: "planned",
3209
+ output_dir: ".asset-scratch",
3210
+ handoff,
3211
+ created_at: timestamp,
3212
+ updated_at: timestamp,
3213
+ inputs: [input],
3214
+ outputs: [],
3215
+ receipts: [{
3216
+ id: `${id}:receipt:plan:preview`,
3217
+ job_id: id,
3218
+ receipt_type: "plan",
3219
+ status: "ok",
3220
+ command: "reroll plan",
3221
+ payload: { prompt, expected_output_count: 1, lineage: handoff.lineage, reroll_request_id: request.id },
3222
+ created_at: timestamp
3223
+ }]
3224
+ };
3225
+ if (fields.dryRun) return { ok: true, command: "reroll plan", project, dryRun: true, wouldWrite: true, job: preview };
3226
+ const database = lineageDb();
3227
+ try {
3228
+ database.exec("BEGIN IMMEDIATE");
3229
+ try {
3230
+ database.prepare(`
3231
+ insert into generation_jobs (
3232
+ id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
3233
+ expected_output_count, status, output_dir, handoff_json, created_at, updated_at
3234
+ ) values (?, ?, ?, ?, 'lineage_reroll', ?, ?, 1, 'planned', ?, ?, ?, ?)
3235
+ `).run(id, project, provider, adapterVersion, snapshot.root_asset_id, prompt, ".asset-scratch", JSON.stringify(handoff), timestamp, timestamp);
3236
+ 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));
3237
+ insertReceipt(database, id, "plan", "reroll plan", preview.receipts[0].payload);
3238
+ database.exec("COMMIT");
3239
+ } catch (error) {
3240
+ database.exec("ROLLBACK");
3241
+ throw error;
3242
+ }
3243
+ return { ok: true, command: "reroll plan", project, job: loadGenerationJob(database, project, id) };
3244
+ } finally {
3245
+ database.close();
3246
+ }
3247
+ }
3248
+ function isPathInside(child, parent) {
3249
+ const rel = relative2(parent, child);
3250
+ return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
3251
+ }
3252
+ function resolveScratchFile(file) {
3253
+ const scratchRoot = resolve3(repoRoot, ".asset-scratch");
3254
+ const candidate = file.startsWith(".asset-scratch/") || resolve3(file).startsWith(scratchRoot) ? resolve3(repoRoot, file) : resolve3(scratchRoot, file);
3255
+ if (!isPathInside(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
3256
+ if (!existsSync4(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
3257
+ const realScratchRoot = realpathSync(scratchRoot);
3258
+ const realCandidate = realpathSync(candidate);
3259
+ if (!isPathInside(realCandidate, realScratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
3260
+ const stats = statSync3(candidate);
3261
+ if (!stats.isFile()) throw new GenerationReceiptError(`Import path is not a file: ${file}`);
3262
+ const checksum = fileSha256(candidate);
3263
+ return {
3264
+ relativePath: relative2(scratchRoot, candidate),
3265
+ checksum,
3266
+ size: stats.size,
3267
+ contentType: contentTypeFor(candidate),
3268
+ assetId: `local-${checksum.slice(0, 12)}`
3269
+ };
3270
+ }
3271
+ function importImageRerollOutput(project = defaultProject, fields) {
3272
+ if (!fields.jobId) throw new GenerationReceiptError("Missing --job-id");
3273
+ if (!fields.confirmWrite) throw new GenerationReceiptError("Generation import requires --confirm-write");
3274
+ const database = lineageDb();
3275
+ let job;
3276
+ try {
3277
+ job = loadGenerationJob(database, project, fields.jobId);
3278
+ } finally {
3279
+ database.close();
3280
+ }
3281
+ if (job.status !== "planned") throw new GenerationReceiptError(`Generation job is not importable from status: ${job.status}`);
3282
+ if (job.source_mode !== "lineage_reroll") throw new GenerationReceiptError(`Generation job is not a re-roll job: ${job.source_mode}`);
3283
+ const target = job.inputs.filter((input) => input.role === "reroll_target");
3284
+ if (target.length !== 1) throw new GenerationReceiptError("Re-roll import requires exactly one reroll_target input");
3285
+ const resolved = resolveScratchFile(fields.file);
3286
+ indexLineageAssets(project);
3287
+ const writeDb = lineageDb();
3288
+ try {
3289
+ const timestamp = nowIso();
3290
+ writeDb.exec("BEGIN IMMEDIATE");
3291
+ try {
3292
+ const assetRow = writeDb.prepare("select id from assets where project_id = ? and id = ?").get(project, resolved.assetId);
3293
+ if (!assetRow) throw new GenerationReceiptError(`Indexed local asset was not found: ${resolved.relativePath}`);
3294
+ const outputId = `${fields.jobId}:output:0`;
3295
+ writeDb.prepare(`insert into generation_job_outputs (
3296
+ id, job_id, project_id, output_index, file_path, checksum_sha256, size_bytes, content_type, imported_asset_id, parent_asset_id, imported_at
3297
+ ) values (?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?)`).run(outputId, fields.jobId, project, resolved.relativePath, resolved.checksum, resolved.size, resolved.contentType, resolved.assetId, target[0].asset_id, timestamp);
3298
+ writeDb.prepare(`
3299
+ update generation_jobs
3300
+ set status = 'imported', imported_at = ?, updated_at = ?
3301
+ where project_id = ? and id = ?
3302
+ `).run(timestamp, timestamp, project, fields.jobId);
3303
+ insertReceipt(writeDb, fields.jobId, "import", "reroll import", {
3304
+ file: { output_index: 0, file_path: resolved.relativePath, imported_asset_id: resolved.assetId, parent_asset_id: target[0].asset_id },
3305
+ reroll: { root_asset_id: job.root_asset_id, node_asset_id: target[0].asset_id }
3306
+ });
3307
+ writeDb.exec("COMMIT");
3308
+ } catch (error) {
3309
+ writeDb.exec("ROLLBACK");
3310
+ throw error;
3311
+ }
3312
+ recordLineageRerollAttempt(project, {
3313
+ rootAssetId: job.root_asset_id,
3314
+ nodeAssetId: target[0].asset_id,
3315
+ assetId: resolved.assetId,
3316
+ prompt: job.prompt,
3317
+ generationJobId: fields.jobId,
3318
+ filePath: resolved.relativePath,
3319
+ checksumSha256: resolved.checksum,
3320
+ confirmWrite: true
3321
+ });
3322
+ const rerollTask = listLineageTasks(project, job.root_asset_id).tasks.find((task) => task.task_type === "reroll" && task.target_asset_id === target[0].asset_id);
3323
+ if (rerollTask) {
3324
+ resolveLineageTask(project, {
3325
+ actor: "agent",
3326
+ confirmWrite: true,
3327
+ resolvedAssetId: resolved.assetId,
3328
+ resolvedGenerationJobId: fields.jobId,
3329
+ taskId: rerollTask.id
3330
+ });
3331
+ }
3332
+ const importedJob = loadGenerationJob(writeDb, project, fields.jobId);
3333
+ return { ok: true, command: "reroll import", project, job: importedJob, imported: importedJob.outputs };
3334
+ } finally {
3335
+ writeDb.close();
3336
+ }
3337
+ }
3338
+
1682
3339
  // src/cli/lineageCli.ts
1683
3340
  var signalExitCodes = {
1684
3341
  SIGINT: 130,
1685
3342
  SIGTERM: 143
1686
3343
  };
1687
3344
  function packageRoot() {
1688
- return resolve3(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
3345
+ return resolve4(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
1689
3346
  }
1690
3347
  function packageVersion() {
1691
3348
  try {
1692
- const packageInfo = JSON.parse(readFileSync3(join5(packageRoot(), "package.json"), "utf8"));
3349
+ const packageInfo = JSON.parse(readFileSync3(join6(packageRoot(), "package.json"), "utf8"));
1693
3350
  return packageInfo.version || "0.0.0";
1694
3351
  } catch {
1695
3352
  return "0.0.0";
@@ -1697,9 +3354,9 @@ function packageVersion() {
1697
3354
  }
1698
3355
  function dataRoot(displayName) {
1699
3356
  if (process.env.LINEAGE_HOME) return process.env.LINEAGE_HOME;
1700
- if (platform() === "darwin") return join5(homedir(), "Library", "Application Support", displayName);
1701
- if (platform() === "win32") return join5(process.env.APPDATA || join5(homedir(), "AppData", "Roaming"), displayName);
1702
- return join5(process.env.XDG_DATA_HOME || join5(homedir(), ".local", "share"), displayName.toLowerCase().replace(/\s+/g, "-"));
3357
+ if (platform() === "darwin") return join6(homedir(), "Library", "Application Support", displayName);
3358
+ if (platform() === "win32") return join6(process.env.APPDATA || join6(homedir(), "AppData", "Roaming"), displayName);
3359
+ return join6(process.env.XDG_DATA_HOME || join6(homedir(), ".local", "share"), displayName.toLowerCase().replace(/\s+/g, "-"));
1703
3360
  }
1704
3361
  function readOption(args, name) {
1705
3362
  const prefix = `${name}=`;
@@ -1717,15 +3374,15 @@ function resolveStartOptions(config, args) {
1717
3374
  throw new Error(`Invalid port: ${rawPort}`);
1718
3375
  }
1719
3376
  return {
1720
- dbPath: readOption(args, "--db") || process.env.LINEAGE_DB || join5(runtimeDir, `${config.binName}.sqlite`),
3377
+ dbPath: readOption(args, "--db") || process.env.LINEAGE_DB || join6(runtimeDir, `${config.binName}.sqlite`),
1721
3378
  host: readOption(args, "--host") || process.env.HOST || config.defaultHost,
1722
3379
  json: args.includes("--json"),
1723
3380
  open: args.includes("--open"),
1724
3381
  port
1725
3382
  };
1726
3383
  }
1727
- function printHelp(config) {
1728
- console.log(`${config.binName} ${packageVersion()}
3384
+ function formatLineageHelp(config) {
3385
+ return `${config.binName} ${packageVersion()}
1729
3386
 
1730
3387
  Usage:
1731
3388
  ${config.binName} start [--port <port>] [--host <host>] [--db <path>] [--open] [--json]
@@ -1733,6 +3390,19 @@ Usage:
1733
3390
  ${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
1734
3391
  ${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
1735
3392
  ${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
3393
+ ${config.binName} reroll list --root <asset-id> [--project <project>] [--db <path>] [--json]
3394
+ ${config.binName} reroll mark --root <asset-id> --target <asset-id> [--notes <text>] [--requested-by agent|human|system] [--project <project>] [--confirm-write] [--db <path>] [--json]
3395
+ ${config.binName} reroll cancel --root <asset-id> --target <asset-id> [--project <project>] [--confirm-write] [--db <path>] [--json]
3396
+ ${config.binName} reroll plan --root <asset-id> --target <asset-id> --prompt <text> [--project <project>] [--db <path>] [--json]
3397
+ ${config.binName} reroll import --job-id <job-id> --file <scratch-file> --confirm-write [--project <project>] [--db <path>] [--json]
3398
+ ${config.binName} tasks list --root <asset-id> [--project <project>] [--db <path>] [--json]
3399
+ ${config.binName} tasks inspect --task <task-id> [--project <project>] [--db <path>] [--json]
3400
+ ${config.binName} tasks claim --task <task-id> --agent-name <name> [--project <project>] [--db <path>] [--json]
3401
+ ${config.binName} tasks start --task <task-id> --claim-token <claim-id.secret> [--project <project>] [--db <path>] [--json]
3402
+ ${config.binName} tasks comment --task <task-id> --message <text> [--project <project>] [--db <path>] [--json]
3403
+ ${config.binName} tasks cancel --task <task-id> [--confirm-write] [--override] [--project <project>] [--db <path>] [--json]
3404
+ ${config.binName} tasks override --task <task-id> --reason <text> [--instructions <text>] [--project <project>] [--db <path>] [--json]
3405
+ ${config.binName} tasks instructions --task <task-id> --instructions <text> [--project <project>] [--db <path>] [--json]
1736
3406
  ${config.binName} agent claim --project <project> --scope <scope> --target <target-id> --agent-name <name> [--channel <channel>] [--ttl 20m] [--json]
1737
3407
  ${config.binName} agent graph --root <asset-id> [--project <project>] [--db <path>] [--json]
1738
3408
  ${config.binName} agent status [--project <project>] [--json]
@@ -1744,7 +3414,14 @@ Usage:
1744
3414
  ${config.binName} --help
1745
3415
  ${config.binName} --version
1746
3416
 
1747
- ${config.displayName} runs the bundled Lineage server for the ${config.channel} channel.`);
3417
+ ${config.displayName} runs the bundled Lineage server for the ${config.channel} channel.
3418
+
3419
+ Variation vs re-roll:
3420
+ link-child creates a new visible child variation edge.
3421
+ reroll mark -> reroll plan -> reroll import updates the same node with a new attempt.`;
3422
+ }
3423
+ function printHelp(config) {
3424
+ console.log(formatLineageHelp(config));
1748
3425
  }
1749
3426
  function openBrowser(url) {
1750
3427
  const command = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
@@ -1796,8 +3473,123 @@ function runLineageDataCommand(command, args) {
1796
3473
  rootAssetId: options.rootAssetId || options.assetId
1797
3474
  });
1798
3475
  }
3476
+ if (command === "reroll") {
3477
+ const subcommand = positionalArgs(args)[0] || "";
3478
+ if (subcommand === "list") {
3479
+ if (!options.rootAssetId) throw new Error("lineage reroll list requires --root");
3480
+ return listLineageRerollRequests(options.project, options.rootAssetId);
3481
+ }
3482
+ if (subcommand === "mark") {
3483
+ const targetAssetId = readOption(args, "--target");
3484
+ const requestedBy = rerollRequestedBy(readOption(args, "--requested-by") || "agent");
3485
+ if (!options.rootAssetId) throw new Error("lineage reroll mark requires --root");
3486
+ if (!targetAssetId) throw new Error("lineage reroll mark requires --target");
3487
+ return markLineageRerollRequest(options.project, {
3488
+ rootAssetId: options.rootAssetId,
3489
+ nodeAssetId: targetAssetId,
3490
+ notes: readOption(args, "--notes"),
3491
+ requestedBy,
3492
+ confirmWrite: options.confirmWrite
3493
+ });
3494
+ }
3495
+ if (subcommand === "cancel") {
3496
+ const targetAssetId = readOption(args, "--target");
3497
+ if (!options.rootAssetId) throw new Error("lineage reroll cancel requires --root");
3498
+ if (!targetAssetId) throw new Error("lineage reroll cancel requires --target");
3499
+ return clearLineageRerollRequest(options.project, {
3500
+ rootAssetId: options.rootAssetId,
3501
+ nodeAssetId: targetAssetId,
3502
+ confirmWrite: options.confirmWrite
3503
+ });
3504
+ }
3505
+ if (subcommand === "plan") {
3506
+ const targetAssetId = readOption(args, "--target");
3507
+ const prompt = readOption(args, "--prompt");
3508
+ if (!options.rootAssetId) throw new Error("lineage reroll plan requires --root");
3509
+ if (!targetAssetId) throw new Error("lineage reroll plan requires --target");
3510
+ if (!prompt) throw new Error("lineage reroll plan requires --prompt");
3511
+ return planImageReroll(options.project, {
3512
+ rootAssetId: options.rootAssetId,
3513
+ targetAssetId,
3514
+ prompt,
3515
+ dryRun: args.includes("--dry-run")
3516
+ });
3517
+ }
3518
+ if (subcommand === "import") {
3519
+ const jobId2 = readOption(args, "--job-id");
3520
+ const file = readOption(args, "--file");
3521
+ if (!jobId2) throw new Error("lineage reroll import requires --job-id");
3522
+ if (!file) throw new Error("lineage reroll import requires --file");
3523
+ return importImageRerollOutput(options.project, { jobId: jobId2, file, confirmWrite: options.confirmWrite });
3524
+ }
3525
+ throw new Error(`Unknown reroll command: ${subcommand}`);
3526
+ }
3527
+ if (command === "tasks") {
3528
+ const subcommand = positionalArgs(args)[0] || "";
3529
+ const taskId = readOption(args, "--task");
3530
+ if (subcommand === "list") {
3531
+ if (!options.rootAssetId) throw new Error("lineage tasks list requires --root");
3532
+ return listLineageTasks(options.project, options.rootAssetId);
3533
+ }
3534
+ if (subcommand === "inspect") {
3535
+ if (!taskId) throw new Error("lineage tasks inspect requires --task");
3536
+ return getLineageTask(options.project, taskId);
3537
+ }
3538
+ if (subcommand === "claim") {
3539
+ const agentName = readOption(args, "--agent-name");
3540
+ if (!taskId) throw new Error("lineage tasks claim requires --task");
3541
+ if (!agentName) throw new Error("lineage tasks claim requires --agent-name");
3542
+ return claimLineageTask(options.project, { taskId, agentName });
3543
+ }
3544
+ if (subcommand === "start") {
3545
+ if (!taskId) throw new Error("lineage tasks start requires --task");
3546
+ if (!options.claimToken) throw new Error("lineage tasks start requires --claim-token");
3547
+ return startLineageTask(options.project, { taskId, claimToken: options.claimToken });
3548
+ }
3549
+ if (subcommand === "comment") {
3550
+ const message = readOption(args, "--message");
3551
+ if (!taskId) throw new Error("lineage tasks comment requires --task");
3552
+ if (!message) throw new Error("lineage tasks comment requires --message");
3553
+ return addLineageTaskComment(options.project, {
3554
+ actor: readOption(args, "--actor") || "human",
3555
+ message,
3556
+ taskId
3557
+ });
3558
+ }
3559
+ if (subcommand === "cancel") {
3560
+ if (!taskId) throw new Error("lineage tasks cancel requires --task");
3561
+ return cancelLineageTask(options.project, {
3562
+ actor: readOption(args, "--actor") || "human",
3563
+ confirmWrite: options.confirmWrite,
3564
+ override: args.includes("--override"),
3565
+ taskId
3566
+ });
3567
+ }
3568
+ if (subcommand === "override") {
3569
+ const reason = readOption(args, "--reason");
3570
+ if (!taskId) throw new Error("lineage tasks override requires --task");
3571
+ if (!reason) throw new Error("lineage tasks override requires --reason");
3572
+ return overrideLineageTask(options.project, {
3573
+ actor: readOption(args, "--actor") || "human",
3574
+ instructions: readOption(args, "--instructions"),
3575
+ reason,
3576
+ taskId
3577
+ });
3578
+ }
3579
+ if (subcommand === "instructions") {
3580
+ const instructions = readOption(args, "--instructions");
3581
+ if (!taskId) throw new Error("lineage tasks instructions requires --task");
3582
+ if (instructions === void 0) throw new Error("lineage tasks instructions requires --instructions");
3583
+ return updateLineageTaskInstructions(options.project, { instructions, taskId });
3584
+ }
3585
+ throw new Error(`Unknown tasks command: ${subcommand}`);
3586
+ }
1799
3587
  throw new Error(`Unknown command: ${command}`);
1800
3588
  }
3589
+ function rerollRequestedBy(value) {
3590
+ if (value === "agent" || value === "human" || value === "system") return value;
3591
+ throw new Error(`Invalid re-roll requester: ${value}`);
3592
+ }
1801
3593
  function printDataResult(command, result, json) {
1802
3594
  if (json) {
1803
3595
  console.log(JSON.stringify(result, null, 2));
@@ -1824,8 +3616,43 @@ function printDataResult(command, result, json) {
1824
3616
  if (command === "link-child" && result && typeof result === "object") {
1825
3617
  const link = result;
1826
3618
  console.log(link.message || `${link.dryRun ? "Dry run: " : ""}Link ${link.edge?.child_asset_id || "child"} from ${link.edge?.parent_asset_id || "parent"}`);
3619
+ if (link.warning) console.log(`Warning: ${link.warning}`);
1827
3620
  return;
1828
3621
  }
3622
+ if (command === "reroll" && result && typeof result === "object") {
3623
+ if ("requests" in result) {
3624
+ const listed = result;
3625
+ console.log(`${listed.requests.length} pending re-roll target(s)`);
3626
+ for (const request of listed.requests) console.log(`${request.node_asset_id}${request.notes ? `: ${request.notes}` : ""}`);
3627
+ return;
3628
+ }
3629
+ if ("job" in result) {
3630
+ const planned = result;
3631
+ console.log(planned.imported ? `Imported re-roll for ${planned.job?.id || "job"}` : `Planned re-roll ${planned.job?.id || "job"}`);
3632
+ return;
3633
+ }
3634
+ if ("request" in result) {
3635
+ const mutation = result;
3636
+ console.log(`${mutation.dryRun ? "Dry run: " : ""}Re-roll ${mutation.request?.status || "request"} for ${mutation.request?.node_asset_id || "target"}`);
3637
+ return;
3638
+ }
3639
+ }
3640
+ if (command === "tasks" && result && typeof result === "object") {
3641
+ if ("tasks" in result) {
3642
+ const listed = result;
3643
+ console.log(`${listed.tasks.length} lineage task(s)`);
3644
+ for (const task of listed.tasks) console.log(`${task.id} ${task.task_type} ${task.status} ${task.target_asset_id}`);
3645
+ return;
3646
+ }
3647
+ if ("task" in result) {
3648
+ const mutation = result;
3649
+ const prefix = mutation.dryRun ? "Dry run: " : "";
3650
+ console.log(`${prefix}${mutation.task?.id || "task"} ${mutation.task?.task_type || "task"} ${mutation.task?.status || "unknown"} ${mutation.task?.target_asset_id || ""}`.trim());
3651
+ if ("claim_token" in mutation && typeof mutation.claim_token === "string") console.log(`Token: ${mutation.claim_token}`);
3652
+ if (mutation.events && mutation.events.length > 0) console.log(`Events: ${mutation.events.map((event) => event.event_type).join(", ")}`);
3653
+ return;
3654
+ }
3655
+ }
1829
3656
  console.log(String(result));
1830
3657
  }
1831
3658
  function runLineageAgentCommand(command, args) {
@@ -1954,8 +3781,8 @@ function start(config, args) {
1954
3781
  else console.error(`${config.binName}: ${message}`);
1955
3782
  process.exit(1);
1956
3783
  }
1957
- const serverPath = join5(packageRoot(), "dist", "server.js");
1958
- if (!existsSync4(serverPath)) {
3784
+ const serverPath = join6(packageRoot(), "dist", "server.js");
3785
+ if (!existsSync5(serverPath)) {
1959
3786
  const message = `Missing bundled server at ${serverPath}. Run npm run build before using ${config.binName} start from a source checkout.`;
1960
3787
  if (options.json) console.error(JSON.stringify({ ok: false, error: message }, null, 2));
1961
3788
  else console.error(`${config.binName}: ${message}`);
@@ -2009,7 +3836,7 @@ function runLineageCli(config, args = process.argv.slice(2)) {
2009
3836
  start(config, normalizedArgs.slice(1));
2010
3837
  return;
2011
3838
  }
2012
- if (command === "next" || command === "brief" || command === "inspect" || command === "link-child") {
3839
+ if (command === "next" || command === "brief" || command === "inspect" || command === "link-child" || command === "reroll" || command === "tasks") {
2013
3840
  const commandArgs = normalizedArgs.slice(1);
2014
3841
  const json2 = commandArgs.includes("--json");
2015
3842
  try {