@mean-weasel/lineage 0.1.6 → 0.1.8

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 existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "node:fs";
4
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
5
5
  import { homedir, platform } from "node:os";
6
- import { dirname as dirname3, join as join6, resolve as resolve4 } from "node:path";
6
+ import { dirname as dirname3, join as join6, resolve as resolve5 } from "node:path";
7
7
  import { spawn } from "node:child_process";
8
8
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9
9
 
@@ -328,6 +328,13 @@ function catalogPath(project = defaultProject) {
328
328
  function fixtureCatalogPath(project = defaultProject) {
329
329
  return join3(repoRoot, "fixtures", cleanProject(project), "assets", "catalog.json");
330
330
  }
331
+ function resolvedCatalogPath(project = defaultProject) {
332
+ const path = catalogPath(project);
333
+ if (existsSync3(path)) return path;
334
+ const clean = cleanProject(project);
335
+ if (clean === defaultProject && existsSync3(fixtureCatalogPath(clean))) return fixtureCatalogPath(clean);
336
+ return path;
337
+ }
331
338
  function normalizeCatalog(catalog, fallbackProject = defaultProject) {
332
339
  const project = cleanProject(catalog.project || catalog.product || fallbackProject);
333
340
  const product = catalog.product || project;
@@ -570,6 +577,10 @@ function listAssets(project = defaultProject, options = {}) {
570
577
  error
571
578
  };
572
579
  }
580
+ function validateProject(project = defaultProject) {
581
+ const catalog = loadCatalog(project);
582
+ return { project: catalog.project, product: catalog.product, catalogPath: resolvedCatalogPath(project), default_bucket: catalog.default_bucket, default_region: catalog.default_region, asset_count: catalog.assets.length };
583
+ }
573
584
 
574
585
  // src/server/assetLineageDb.ts
575
586
  var require2 = createRequire(import.meta.url);
@@ -615,6 +626,10 @@ function lineageDb() {
615
626
  create index if not exists assets_project_source_seen on assets(project_id, source, last_seen_at);
616
627
  create index if not exists assets_project_checksum on assets(project_id, checksum_sha256);
617
628
  create index if not exists assets_project_channel_campaign on assets(project_id, channel, campaign);
629
+ create table if not exists lineage_schema_migrations (
630
+ key text primary key,
631
+ applied_at text not null
632
+ );
618
633
  create table if not exists asset_edges (
619
634
  id text primary key,
620
635
  project_id text not null references projects(id),
@@ -920,12 +935,49 @@ function lineageDb() {
920
935
  );
921
936
  create index if not exists generation_job_receipts_job on generation_job_receipts(job_id, created_at);
922
937
  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);
938
+ create table if not exists lineage_tasks (
939
+ id text primary key,
940
+ project_id text not null references projects(id),
941
+ root_asset_id text not null references assets(id),
942
+ target_asset_id text not null references assets(id),
943
+ task_type text not null check (task_type in ('iterate', 'reroll')),
944
+ status text not null check (status in ('pending', 'claimed', 'in_progress', 'resolved', 'cancelled')),
945
+ instructions text,
946
+ created_by text not null check (created_by in ('human', 'agent', 'system')),
947
+ created_at text not null,
948
+ updated_at text not null,
949
+ claimed_at text,
950
+ started_at text,
951
+ resolved_at text,
952
+ cancelled_at text,
953
+ resolved_generation_job_id text,
954
+ resolved_asset_id text references assets(id),
955
+ metadata_json text
956
+ );
957
+ create unique index if not exists lineage_tasks_one_open
958
+ on lineage_tasks(project_id, root_asset_id, target_asset_id, task_type)
959
+ where status in ('pending', 'claimed', 'in_progress');
960
+ create index if not exists lineage_tasks_root_status
961
+ on lineage_tasks(project_id, root_asset_id, status, updated_at);
962
+ create index if not exists lineage_tasks_target
963
+ on lineage_tasks(project_id, root_asset_id, target_asset_id, task_type, status);
964
+ create table if not exists lineage_task_events (
965
+ id text primary key,
966
+ task_id text not null references lineage_tasks(id) on delete cascade,
967
+ event_type text not null,
968
+ actor text,
969
+ message text,
970
+ created_at text not null,
971
+ metadata_json text
972
+ );
973
+ create index if not exists lineage_task_events_task_created
974
+ on lineage_task_events(task_id, created_at);
923
975
  create table if not exists agent_claims (
924
976
  id text primary key,
925
977
  token_hash text not null,
926
978
  project_id text not null references projects(id),
927
979
  channel text,
928
- scope_type text not null check (scope_type in ('lineage_workspace', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
980
+ scope_type text not null check (scope_type in ('lineage_workspace', 'lineage_task', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
929
981
  target_id text not null,
930
982
  target_title text,
931
983
  agent_id text,
@@ -959,13 +1011,76 @@ function lineageDb() {
959
1011
  migrateAssetSelections(database);
960
1012
  dropLegacyAssetSelectionRootUnique(database);
961
1013
  ensureColumn(database, "asset_selections", "notes", "text");
1014
+ backfillLineageTasks(database);
962
1015
  ensureColumn(database, "asset_ledger_records", "first_seen_at", "text");
963
1016
  ensureColumn(database, "asset_ledger_records", "indexed_by_run_id", "text");
964
1017
  ensureColumn(database, "asset_ledger_sources", "first_seen_at", "text");
965
1018
  ensureColumn(database, "asset_ledger_sources", "indexed_by_run_id", "text");
966
1019
  ensureReviewStateValues(database);
1020
+ ensureAgentClaimScopeValues(database);
1021
+ ensureGenerationReceiptCheckValues(database);
967
1022
  return database;
968
1023
  }
1024
+ function backfillLineageTasks(database) {
1025
+ database.exec(`
1026
+ create table if not exists lineage_schema_migrations (
1027
+ key text primary key,
1028
+ applied_at text not null
1029
+ )
1030
+ `);
1031
+ const marker = database.prepare("select key from lineage_schema_migrations where key = 'lineage_tasks_backfilled_v1'").get();
1032
+ if (marker) return;
1033
+ database.exec("BEGIN IMMEDIATE");
1034
+ try {
1035
+ database.exec(`
1036
+ insert or ignore into lineage_tasks (
1037
+ id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
1038
+ created_by, created_at, updated_at, metadata_json
1039
+ )
1040
+ select
1041
+ s.project_id || ':' || s.root_asset_id || ':lineage-task:iterate:' || s.asset_id,
1042
+ s.project_id,
1043
+ s.root_asset_id,
1044
+ s.asset_id,
1045
+ 'iterate',
1046
+ 'pending',
1047
+ s.notes,
1048
+ 'human',
1049
+ s.selected_at,
1050
+ s.selected_at,
1051
+ null
1052
+ from asset_selections s;
1053
+
1054
+ insert or ignore into lineage_tasks (
1055
+ id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
1056
+ created_by, created_at, updated_at, metadata_json
1057
+ )
1058
+ select
1059
+ r.project_id || ':' || r.root_asset_id || ':lineage-task:reroll:' || r.node_asset_id,
1060
+ r.project_id,
1061
+ r.root_asset_id,
1062
+ r.node_asset_id,
1063
+ 'reroll',
1064
+ 'pending',
1065
+ r.notes,
1066
+ r.requested_by,
1067
+ r.created_at,
1068
+ r.created_at,
1069
+ null
1070
+ from asset_reroll_requests r
1071
+ where r.status = 'pending';
1072
+ `);
1073
+ database.prepare(`
1074
+ insert into lineage_schema_migrations (key, applied_at)
1075
+ values ('lineage_tasks_backfilled_v1', ?)
1076
+ on conflict(key) do nothing
1077
+ `).run(nowIso());
1078
+ database.exec("COMMIT");
1079
+ } catch (error) {
1080
+ database.exec("ROLLBACK");
1081
+ throw error;
1082
+ }
1083
+ }
969
1084
  function ensureColumn(database, table, column, definition) {
970
1085
  const rows = database.prepare(`pragma table_info(${table})`).all();
971
1086
  if (!rows.some((row) => row.name === column)) database.exec(`alter table ${table} add column ${column} ${definition}`);
@@ -1029,12 +1144,138 @@ function ensureReviewStateValues(database) {
1029
1144
  drop table asset_reviews_old;
1030
1145
  `);
1031
1146
  }
1147
+ function ensureAgentClaimScopeValues(database) {
1148
+ const createSql = database.prepare("select sql from sqlite_master where type = 'table' and name = 'agent_claims'").get();
1149
+ if (createSql?.sql?.includes("'lineage_task'")) return;
1150
+ database.exec("PRAGMA foreign_keys = OFF; PRAGMA legacy_alter_table = ON; BEGIN IMMEDIATE");
1151
+ try {
1152
+ database.exec(`
1153
+ alter table agent_claims rename to agent_claims_old;
1154
+ create table agent_claims (
1155
+ id text primary key,
1156
+ token_hash text not null,
1157
+ project_id text not null references projects(id),
1158
+ channel text,
1159
+ scope_type text not null check (scope_type in ('lineage_workspace', 'lineage_task', 'content_post', 'content_queue_lane', 'selection_set', 'project_channel')),
1160
+ target_id text not null,
1161
+ target_title text,
1162
+ agent_id text,
1163
+ agent_name text not null,
1164
+ agent_kind text not null,
1165
+ thread_id text,
1166
+ status text not null check (status in ('active', 'expired', 'released', 'revoked', 'transferred')),
1167
+ created_at text not null,
1168
+ heartbeat_at text not null,
1169
+ expires_at text not null,
1170
+ released_at text,
1171
+ revoked_at text,
1172
+ revoked_by text,
1173
+ override_reason text,
1174
+ metadata_json text
1175
+ );
1176
+ insert into agent_claims
1177
+ select * from agent_claims_old;
1178
+ drop table agent_claims_old;
1179
+ create unique index if not exists agent_claims_token_hash on agent_claims(token_hash);
1180
+ create index if not exists agent_claims_project_status on agent_claims(project_id, status, heartbeat_at);
1181
+ create index if not exists agent_claims_target on agent_claims(project_id, channel, scope_type, target_id, status);
1182
+ `);
1183
+ const violations = database.prepare("pragma foreign_key_check").all();
1184
+ if (violations.length > 0) throw new Error(`Agent claim scope migration failed foreign key check: ${JSON.stringify(violations)}`);
1185
+ database.exec("COMMIT");
1186
+ } catch (error) {
1187
+ database.exec("ROLLBACK");
1188
+ throw error;
1189
+ } finally {
1190
+ database.exec("PRAGMA legacy_alter_table = OFF; PRAGMA foreign_keys = ON");
1191
+ }
1192
+ }
1193
+ function tableCreateSql(database, table) {
1194
+ const row = database.prepare("select sql from sqlite_master where type = 'table' and name = ?").get(table);
1195
+ return row?.sql || "";
1196
+ }
1197
+ function ensureGenerationReceiptCheckValues(database) {
1198
+ const jobsSql = tableCreateSql(database, "generation_jobs");
1199
+ const inputsSql = tableCreateSql(database, "generation_job_inputs");
1200
+ const needsJobsMigration = Boolean(jobsSql && !jobsSql.includes("'lineage_reroll'"));
1201
+ const needsInputsMigration = Boolean(inputsSql && !inputsSql.includes("'reroll_target'"));
1202
+ if (!needsJobsMigration && !needsInputsMigration) return;
1203
+ database.exec("PRAGMA foreign_keys = OFF; PRAGMA legacy_alter_table = ON; BEGIN IMMEDIATE");
1204
+ try {
1205
+ if (needsJobsMigration) migrateGenerationJobsCheck(database);
1206
+ if (needsInputsMigration) migrateGenerationJobInputsCheck(database);
1207
+ const violations = database.prepare("pragma foreign_key_check").all();
1208
+ if (violations.length > 0) throw new Error(`Generation receipt migration failed foreign key check: ${JSON.stringify(violations)}`);
1209
+ database.exec("COMMIT");
1210
+ } catch (error) {
1211
+ database.exec("ROLLBACK");
1212
+ throw error;
1213
+ } finally {
1214
+ database.exec("PRAGMA legacy_alter_table = OFF; PRAGMA foreign_keys = ON");
1215
+ }
1216
+ }
1217
+ function migrateGenerationJobsCheck(database) {
1218
+ database.exec(`
1219
+ alter table generation_jobs rename to generation_jobs_legacy_check;
1220
+ create table generation_jobs (
1221
+ id text primary key,
1222
+ project_id text not null references projects(id),
1223
+ provider text not null default 'codex-handoff',
1224
+ adapter_version text not null,
1225
+ source_mode text not null check (source_mode in ('lineage_selection', 'lineage_reroll')),
1226
+ root_asset_id text not null references assets(id),
1227
+ prompt text not null,
1228
+ expected_output_count integer not null check (expected_output_count > 0),
1229
+ status text not null check (status in ('planned', 'imported', 'failed', 'cancelled')),
1230
+ output_dir text,
1231
+ handoff_json text,
1232
+ created_at text not null,
1233
+ updated_at text not null,
1234
+ imported_at text
1235
+ );
1236
+ insert into generation_jobs (
1237
+ id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
1238
+ expected_output_count, status, output_dir, handoff_json, created_at, updated_at, imported_at
1239
+ )
1240
+ select
1241
+ id, project_id, provider, adapter_version, source_mode, root_asset_id, prompt,
1242
+ expected_output_count, status, output_dir, handoff_json, created_at, updated_at, imported_at
1243
+ from generation_jobs_legacy_check;
1244
+ drop table generation_jobs_legacy_check;
1245
+ create index if not exists generation_jobs_project_created on generation_jobs(project_id, created_at);
1246
+ `);
1247
+ }
1248
+ function migrateGenerationJobInputsCheck(database) {
1249
+ database.exec(`
1250
+ alter table generation_job_inputs rename to generation_job_inputs_legacy_check;
1251
+ create table generation_job_inputs (
1252
+ id text primary key,
1253
+ job_id text not null references generation_jobs(id) on delete cascade,
1254
+ project_id text not null references projects(id),
1255
+ asset_id text not null references assets(id),
1256
+ root_asset_id text not null references assets(id),
1257
+ role text not null check (role in ('lineage_next_base', 'reference', 'reroll_target')),
1258
+ position integer not null,
1259
+ selection_strategy text not null,
1260
+ selection_snapshot_json text not null,
1261
+ unique(job_id, asset_id, role)
1262
+ );
1263
+ insert into generation_job_inputs (
1264
+ id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json
1265
+ )
1266
+ select
1267
+ id, job_id, project_id, asset_id, root_asset_id, role, position, selection_strategy, selection_snapshot_json
1268
+ from generation_job_inputs_legacy_check;
1269
+ drop table generation_job_inputs_legacy_check;
1270
+ create index if not exists generation_job_inputs_job on generation_job_inputs(job_id, position);
1271
+ `);
1272
+ }
1032
1273
 
1033
1274
  // src/server/agentClaims.ts
1034
1275
  var defaultTtlSeconds = 20 * 60;
1035
1276
  var idleAfterSeconds = 5 * 60;
1036
1277
  var staleAfterSeconds = 15 * 60;
1037
- var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
1278
+ var scopes = /* @__PURE__ */ new Set(["lineage_workspace", "lineage_task", "content_post", "content_queue_lane", "selection_set", "project_channel"]);
1038
1279
  var claimTokenPattern = /claim_[a-z0-9_-]+\.[A-Za-z0-9_-]+/g;
1039
1280
  var AgentClaimError = class extends Error {
1040
1281
  constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
@@ -1339,6 +1580,19 @@ function revokeAgentClaim(project, claimId, fields) {
1339
1580
  database.close();
1340
1581
  }
1341
1582
  }
1583
+ function revokeAgentClaimInDatabase(database, project, claimId, fields) {
1584
+ expireActiveClaims(database);
1585
+ const claim = findClaimById(database, claimId, project);
1586
+ if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1587
+ if (claim.status !== "active") return claim;
1588
+ const timestamp = nowIso();
1589
+ database.prepare(`
1590
+ update agent_claims set status = 'revoked', revoked_at = ?, revoked_by = ?, override_reason = ?
1591
+ where id = ? and status = 'active'
1592
+ `).run(timestamp, fields.actor || "human", fields.reason || null, claim.id);
1593
+ recordEvent(database, claim.id, "revoked", fields.actor || "human", fields.reason);
1594
+ return findClaimById(database, claim.id, project);
1595
+ }
1342
1596
  function transferAgentClaim(project, claimId, fields) {
1343
1597
  if (!fields.confirmWrite) throw new AgentClaimError("Transferring an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
1344
1598
  const toAgentName = fields.toAgentName.trim();
@@ -1356,6 +1610,19 @@ function transferAgentClaim(project, claimId, fields) {
1356
1610
  database.close();
1357
1611
  }
1358
1612
  }
1613
+ function recordAgentClaimWriteAllowed(claim, fields) {
1614
+ const database = lineageDb();
1615
+ try {
1616
+ recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
1617
+ danger_level: fields.dangerLevel,
1618
+ target_id: fields.targetId,
1619
+ write_kind: fields.writeKind
1620
+ });
1621
+ return { ok: true };
1622
+ } finally {
1623
+ database.close();
1624
+ }
1625
+ }
1359
1626
  function validateAgentClaimForWrite(fields) {
1360
1627
  if (fields.dangerLevel === "danger" && !fields.confirmWrite) {
1361
1628
  return denied("human_confirmation_required", "Dangerous write requires explicit human confirmation.");
@@ -1376,11 +1643,13 @@ function validateAgentClaimForWrite(fields) {
1376
1643
  if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
1377
1644
  return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
1378
1645
  }
1379
- recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
1380
- danger_level: fields.dangerLevel,
1381
- target_id: fields.targetId,
1382
- write_kind: fields.writeKind
1383
- });
1646
+ if (fields.recordEvent !== false) {
1647
+ recordEvent(database, claim.id, "write_allowed", claim.agent_name, `${fields.writeKind} allowed.`, {
1648
+ danger_level: fields.dangerLevel,
1649
+ target_id: fields.targetId,
1650
+ write_kind: fields.writeKind
1651
+ });
1652
+ }
1384
1653
  return { ok: true, claim, warnings: [] };
1385
1654
  } finally {
1386
1655
  database.close();
@@ -1400,6 +1669,507 @@ function selectedRows(database, project, root) {
1400
1669
  `).all(project, root);
1401
1670
  }
1402
1671
 
1672
+ // src/server/assetLineageTasks.ts
1673
+ import { randomBytes as randomBytes2 } from "node:crypto";
1674
+ var LineageTaskError = class extends Error {
1675
+ constructor(message, status = 400) {
1676
+ super(message);
1677
+ this.status = status;
1678
+ }
1679
+ status;
1680
+ };
1681
+ var activeStatuses = ["pending", "claimed", "in_progress"];
1682
+ var taskTypes = /* @__PURE__ */ new Set(["iterate", "reroll"]);
1683
+ var taskStatuses = /* @__PURE__ */ new Set(["pending", "claimed", "in_progress", "resolved", "cancelled"]);
1684
+ function taskIdFor(project, rootAssetId, targetAssetId, taskType) {
1685
+ return `${project}:${rootAssetId}:lineage-task:${taskType}:${targetAssetId}`;
1686
+ }
1687
+ function randomId2(prefix) {
1688
+ return `${prefix}_${Date.now().toString(36)}_${randomBytes2(6).toString("base64url").toLowerCase()}`;
1689
+ }
1690
+ function metadataJson2(metadata) {
1691
+ return metadata ? JSON.stringify(metadata) : null;
1692
+ }
1693
+ function metadataWithoutClaim(metadata) {
1694
+ if (!metadata) return void 0;
1695
+ const next = { ...metadata };
1696
+ delete next.claim_id;
1697
+ return Object.keys(next).length > 0 ? next : void 0;
1698
+ }
1699
+ function parseMetadata2(value) {
1700
+ if (typeof value !== "string" || !value) return void 0;
1701
+ try {
1702
+ const parsed = JSON.parse(value);
1703
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
1704
+ } catch {
1705
+ return void 0;
1706
+ }
1707
+ }
1708
+ function normalizeProject(project) {
1709
+ const trimmed = project.trim();
1710
+ if (!trimmed) throw new LineageTaskError("Lineage task requires project");
1711
+ return trimmed;
1712
+ }
1713
+ function normalizeTaskType(taskType) {
1714
+ if (!taskTypes.has(taskType)) throw new LineageTaskError(`Unsupported lineage task type: ${taskType}`);
1715
+ return taskType;
1716
+ }
1717
+ function normalizeStatus(status) {
1718
+ if (!taskStatuses.has(status)) throw new LineageTaskError(`Unsupported lineage task status: ${status}`);
1719
+ return status;
1720
+ }
1721
+ function normalizeActor(actor, label) {
1722
+ const trimmed = actor.trim();
1723
+ if (!trimmed) throw new LineageTaskError(`${label} is required`);
1724
+ return trimmed;
1725
+ }
1726
+ function requireAsset(database, project, assetId) {
1727
+ const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
1728
+ if (!row) throw new LineageTaskError(`Unknown indexed asset: ${assetId}`, 404);
1729
+ }
1730
+ function taskFromRow(row) {
1731
+ const metadata = parseMetadata2(row.metadata_json);
1732
+ const claimId = typeof metadata?.claim_id === "string" ? metadata.claim_id : void 0;
1733
+ return {
1734
+ id: String(row.id),
1735
+ project_id: String(row.project_id),
1736
+ root_asset_id: String(row.root_asset_id),
1737
+ target_asset_id: String(row.target_asset_id),
1738
+ task_type: String(row.task_type),
1739
+ status: String(row.status),
1740
+ instructions: typeof row.instructions === "string" ? row.instructions : void 0,
1741
+ created_by: String(row.created_by),
1742
+ created_at: String(row.created_at),
1743
+ updated_at: String(row.updated_at),
1744
+ claimed_at: typeof row.claimed_at === "string" ? row.claimed_at : void 0,
1745
+ started_at: typeof row.started_at === "string" ? row.started_at : void 0,
1746
+ resolved_at: typeof row.resolved_at === "string" ? row.resolved_at : void 0,
1747
+ cancelled_at: typeof row.cancelled_at === "string" ? row.cancelled_at : void 0,
1748
+ resolved_generation_job_id: typeof row.resolved_generation_job_id === "string" ? row.resolved_generation_job_id : void 0,
1749
+ resolved_asset_id: typeof row.resolved_asset_id === "string" ? row.resolved_asset_id : void 0,
1750
+ claimed_by_claim_id: claimId,
1751
+ metadata
1752
+ };
1753
+ }
1754
+ function eventFromRow(row) {
1755
+ return {
1756
+ id: String(row.id),
1757
+ task_id: String(row.task_id),
1758
+ event_type: String(row.event_type),
1759
+ actor: typeof row.actor === "string" ? row.actor : void 0,
1760
+ message: typeof row.message === "string" ? row.message : void 0,
1761
+ created_at: String(row.created_at),
1762
+ metadata: parseMetadata2(row.metadata_json)
1763
+ };
1764
+ }
1765
+ function findTask(database, project, taskId) {
1766
+ const row = database.prepare("select * from lineage_tasks where project_id = ? and id = ?").get(project, taskId);
1767
+ return row ? taskFromRow(row) : null;
1768
+ }
1769
+ function requireTask(database, project, taskId) {
1770
+ const task = findTask(database, project, taskId);
1771
+ if (!task) throw new LineageTaskError(`Unknown lineage task: ${taskId}`, 404);
1772
+ return task;
1773
+ }
1774
+ function taskEvents(database, taskId) {
1775
+ const rows = database.prepare("select * from lineage_task_events where task_id = ? order by created_at, rowid").all(taskId);
1776
+ return rows.map(eventFromRow);
1777
+ }
1778
+ function recordEvent2(database, taskId, eventType, actor, message, metadata) {
1779
+ database.prepare(`
1780
+ insert into lineage_task_events (id, task_id, event_type, actor, message, created_at, metadata_json)
1781
+ values (?, ?, ?, ?, ?, ?, ?)
1782
+ `).run(randomId2("lineage_task_event"), taskId, eventType, actor || null, message || null, nowIso(), metadataJson2(metadata));
1783
+ }
1784
+ function taskWithEvents(database, project, taskId) {
1785
+ return { project, ok: true, task: requireTask(database, project, taskId), events: taskEvents(database, taskId) };
1786
+ }
1787
+ function assertChanged(result, message) {
1788
+ if (Number(result.changes) !== 1) throw new LineageTaskError(message, 409);
1789
+ }
1790
+ function transaction(database, callback) {
1791
+ database.exec("BEGIN IMMEDIATE");
1792
+ try {
1793
+ const value = callback();
1794
+ database.exec("COMMIT");
1795
+ return value;
1796
+ } catch (error) {
1797
+ database.exec("ROLLBACK");
1798
+ throw error;
1799
+ }
1800
+ }
1801
+ function taskReadWithEvents(database, project, taskId) {
1802
+ return { task: requireTask(database, project, taskId), events: taskEvents(database, taskId) };
1803
+ }
1804
+ function listLineageTasks(project, rootAssetId, statuses = activeStatuses) {
1805
+ const normalizedProject = normalizeProject(project);
1806
+ const normalizedStatuses = statuses.map(normalizeStatus);
1807
+ const database = lineageDb();
1808
+ try {
1809
+ requireAsset(database, normalizedProject, rootAssetId);
1810
+ if (normalizedStatuses.length === 0) {
1811
+ return { project: normalizedProject, root_asset_id: rootAssetId, tasks: [], fetchedAt: nowIso() };
1812
+ }
1813
+ const placeholders = normalizedStatuses.map(() => "?").join(",");
1814
+ const rows = database.prepare(`
1815
+ select * from lineage_tasks
1816
+ where project_id = ? and root_asset_id = ? and status in (${placeholders})
1817
+ order by updated_at desc, created_at desc, id
1818
+ `).all(normalizedProject, rootAssetId, ...normalizedStatuses);
1819
+ return { project: normalizedProject, root_asset_id: rootAssetId, tasks: rows.map(taskFromRow), fetchedAt: nowIso() };
1820
+ } finally {
1821
+ database.close();
1822
+ }
1823
+ }
1824
+ function getLineageTask(project, taskId) {
1825
+ const normalizedProject = normalizeProject(project);
1826
+ const database = lineageDb();
1827
+ try {
1828
+ return { project: normalizedProject, ...taskReadWithEvents(database, normalizedProject, taskId) };
1829
+ } finally {
1830
+ database.close();
1831
+ }
1832
+ }
1833
+ function upsertLineageTask(project, fields) {
1834
+ const normalizedProject = normalizeProject(project);
1835
+ const taskType = normalizeTaskType(fields.taskType);
1836
+ const taskId = taskIdFor(normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType);
1837
+ const database = lineageDb();
1838
+ try {
1839
+ requireAsset(database, normalizedProject, fields.rootAssetId);
1840
+ requireAsset(database, normalizedProject, fields.targetAssetId);
1841
+ const existing = database.prepare(`
1842
+ select * from lineage_tasks
1843
+ where project_id = ? and root_asset_id = ? and target_asset_id = ? and task_type = ?
1844
+ and status in ('pending', 'claimed', 'in_progress')
1845
+ order by created_at desc limit 1
1846
+ `).get(normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType);
1847
+ const instructions = fields.instructions?.trim() || void 0;
1848
+ if (existing) {
1849
+ const task = taskFromRow(existing);
1850
+ if (task.status !== "pending" && instructions !== void 0 && instructions !== task.instructions) {
1851
+ throw new LineageTaskError("Only pending lineage tasks can update instructions.", 409);
1852
+ }
1853
+ if (task.status === "pending" && instructions !== void 0 && instructions !== task.instructions) {
1854
+ const timestamp2 = nowIso();
1855
+ transaction(database, () => {
1856
+ const result = database.prepare(`
1857
+ update lineage_tasks
1858
+ set instructions = ?, updated_at = ?
1859
+ where id = ? and status = 'pending'
1860
+ `).run(instructions, timestamp2, task.id);
1861
+ assertChanged(result, "Only pending lineage tasks can update instructions.");
1862
+ recordEvent2(database, task.id, "instructions_updated", fields.createdBy, "Instructions updated.");
1863
+ });
1864
+ }
1865
+ return taskWithEvents(database, normalizedProject, task.id);
1866
+ }
1867
+ const closedTask = findTask(database, normalizedProject, taskId);
1868
+ if (closedTask) {
1869
+ const timestamp2 = nowIso();
1870
+ transaction(database, () => {
1871
+ const result = database.prepare(`
1872
+ update lineage_tasks
1873
+ set status = 'pending', instructions = ?, created_by = ?, updated_at = ?,
1874
+ claimed_at = null, started_at = null, resolved_at = null, cancelled_at = null,
1875
+ resolved_generation_job_id = null, resolved_asset_id = null, metadata_json = null
1876
+ where id = ? and status not in ('pending', 'claimed', 'in_progress')
1877
+ `).run(instructions || null, fields.createdBy, timestamp2, taskId);
1878
+ assertChanged(result, "Lineage task changed while reopening.");
1879
+ recordEvent2(database, taskId, "created", fields.createdBy, "Lineage task created.");
1880
+ });
1881
+ return taskWithEvents(database, normalizedProject, taskId);
1882
+ }
1883
+ const timestamp = nowIso();
1884
+ transaction(database, () => {
1885
+ database.prepare(`
1886
+ insert into lineage_tasks (
1887
+ id, project_id, root_asset_id, target_asset_id, task_type, status, instructions,
1888
+ created_by, created_at, updated_at, metadata_json
1889
+ ) values (?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, null)
1890
+ `).run(taskId, normalizedProject, fields.rootAssetId, fields.targetAssetId, taskType, instructions || null, fields.createdBy, timestamp, timestamp);
1891
+ recordEvent2(database, taskId, "created", fields.createdBy, "Lineage task created.");
1892
+ });
1893
+ return taskWithEvents(database, normalizedProject, taskId);
1894
+ } finally {
1895
+ database.close();
1896
+ }
1897
+ }
1898
+ function updateLineageTaskInstructions(project, fields) {
1899
+ const normalizedProject = normalizeProject(project);
1900
+ const instructions = fields.instructions.trim();
1901
+ const database = lineageDb();
1902
+ try {
1903
+ const task = requireTask(database, normalizedProject, fields.taskId);
1904
+ if (task.status !== "pending") throw new LineageTaskError("Only pending lineage tasks can update instructions.", 409);
1905
+ const timestamp = nowIso();
1906
+ transaction(database, () => {
1907
+ const result = database.prepare(`
1908
+ update lineage_tasks
1909
+ set instructions = ?, updated_at = ?
1910
+ where id = ? and status = 'pending'
1911
+ `).run(instructions || null, timestamp, task.id);
1912
+ assertChanged(result, "Only pending lineage tasks can update instructions.");
1913
+ recordEvent2(database, task.id, "instructions_updated", "human", "Instructions updated.");
1914
+ });
1915
+ return taskWithEvents(database, normalizedProject, task.id);
1916
+ } finally {
1917
+ database.close();
1918
+ }
1919
+ }
1920
+ function addLineageTaskComment(project, fields) {
1921
+ const normalizedProject = normalizeProject(project);
1922
+ const actor = normalizeActor(fields.actor, "Comment actor");
1923
+ const message = fields.message.trim();
1924
+ if (!message) throw new LineageTaskError("Comment message is required");
1925
+ const database = lineageDb();
1926
+ try {
1927
+ const task = requireTask(database, normalizedProject, fields.taskId);
1928
+ transaction(database, () => {
1929
+ recordEvent2(database, task.id, "comment_added", actor, message);
1930
+ });
1931
+ return taskWithEvents(database, normalizedProject, task.id);
1932
+ } finally {
1933
+ database.close();
1934
+ }
1935
+ }
1936
+ function claimLineageTask(project, fields) {
1937
+ const normalizedProject = normalizeProject(project);
1938
+ const agentName = normalizeActor(fields.agentName, "Agent name");
1939
+ const beforeClaimDb = lineageDb();
1940
+ try {
1941
+ const task = requireTask(beforeClaimDb, normalizedProject, fields.taskId);
1942
+ if (task.status !== "pending") throw new LineageTaskError("Only pending lineage tasks can be claimed.", 409);
1943
+ } finally {
1944
+ beforeClaimDb.close();
1945
+ }
1946
+ const claimResult = createAgentClaim({
1947
+ agentName,
1948
+ project: normalizedProject,
1949
+ scopeType: "lineage_task",
1950
+ targetId: fields.taskId,
1951
+ targetTitle: fields.taskId
1952
+ });
1953
+ if (!claimResult.claim) throw new LineageTaskError("Unable to create lineage task claim.", 500);
1954
+ const claim = claimResult.claim;
1955
+ const database = lineageDb();
1956
+ try {
1957
+ const task = requireTask(database, normalizedProject, fields.taskId);
1958
+ if (task.status !== "pending") {
1959
+ releaseAgentClaim(claimResult.claim_token);
1960
+ throw new LineageTaskError("Only pending lineage tasks can be claimed.", 409);
1961
+ }
1962
+ const timestamp = nowIso();
1963
+ const metadata = { ...task.metadata || {}, claim_id: claim.id };
1964
+ try {
1965
+ transaction(database, () => {
1966
+ const result = database.prepare(`
1967
+ update lineage_tasks
1968
+ set status = 'claimed', claimed_at = ?, updated_at = ?, metadata_json = ?
1969
+ where id = ? and status = 'pending'
1970
+ `).run(timestamp, timestamp, metadataJson2(metadata), task.id);
1971
+ assertChanged(result, "Only pending lineage tasks can be claimed.");
1972
+ recordEvent2(database, task.id, "claimed", agentName, "Lineage task claimed.", { claim_id: claim.id });
1973
+ });
1974
+ } catch (error) {
1975
+ releaseAgentClaim(claimResult.claim_token);
1976
+ throw error;
1977
+ }
1978
+ return {
1979
+ ...taskWithEvents(database, normalizedProject, task.id),
1980
+ claim,
1981
+ claim_token: claimResult.claim_token
1982
+ };
1983
+ } finally {
1984
+ database.close();
1985
+ }
1986
+ }
1987
+ function startLineageTask(project, fields) {
1988
+ const normalizedProject = normalizeProject(project);
1989
+ const precheckDatabase = lineageDb();
1990
+ try {
1991
+ const task = requireTask(precheckDatabase, normalizedProject, fields.taskId);
1992
+ if (task.status !== "claimed") throw new LineageTaskError("Only claimed lineage tasks can be started.", 409);
1993
+ } finally {
1994
+ precheckDatabase.close();
1995
+ }
1996
+ const validation = validateAgentClaimForWrite({
1997
+ claimToken: fields.claimToken,
1998
+ dangerLevel: "enforce",
1999
+ project: normalizedProject,
2000
+ recordEvent: false,
2001
+ scopeType: "lineage_task",
2002
+ targetId: fields.taskId,
2003
+ writeKind: "lineage_task_start"
2004
+ });
2005
+ if (!validation.ok) throw new LineageTaskError(validation.message, 409);
2006
+ const database = lineageDb();
2007
+ try {
2008
+ const task = requireTask(database, normalizedProject, fields.taskId);
2009
+ if (task.status !== "claimed") throw new LineageTaskError("Only claimed lineage tasks can be started.", 409);
2010
+ if (task.claimed_by_claim_id && task.claimed_by_claim_id !== validation.claim.id) {
2011
+ throw new LineageTaskError("Claim token does not match the task claim.", 409);
2012
+ }
2013
+ const timestamp = nowIso();
2014
+ const metadata = { ...task.metadata || {}, claim_id: validation.claim.id };
2015
+ transaction(database, () => {
2016
+ const result = database.prepare(`
2017
+ update lineage_tasks
2018
+ set status = 'in_progress', started_at = ?, updated_at = ?, metadata_json = ?
2019
+ where id = ? and status = 'claimed'
2020
+ `).run(timestamp, timestamp, metadataJson2(metadata), task.id);
2021
+ assertChanged(result, "Only claimed lineage tasks can be started.");
2022
+ recordEvent2(database, task.id, "started", validation.claim.agent_name, "Lineage task started.", { claim_id: validation.claim.id });
2023
+ });
2024
+ recordAgentClaimWriteAllowed(validation.claim, {
2025
+ dangerLevel: "enforce",
2026
+ targetId: fields.taskId,
2027
+ writeKind: "lineage_task_start"
2028
+ });
2029
+ return taskWithEvents(database, normalizedProject, task.id);
2030
+ } finally {
2031
+ database.close();
2032
+ }
2033
+ }
2034
+ function overrideLineageTask(project, fields) {
2035
+ const normalizedProject = normalizeProject(project);
2036
+ const actor = normalizeActor(fields.actor, "Override actor");
2037
+ const reason = fields.reason.trim();
2038
+ if (!reason) throw new LineageTaskError("Override reason is required");
2039
+ const database = lineageDb();
2040
+ try {
2041
+ const task = requireTask(database, normalizedProject, fields.taskId);
2042
+ if (!["claimed", "in_progress"].includes(task.status)) {
2043
+ throw new LineageTaskError(`Only claimed or in-progress lineage tasks can be overridden; task is ${task.status}.`, 409);
2044
+ }
2045
+ const timestamp = nowIso();
2046
+ const instructions = fields.instructions === void 0 ? task.instructions : fields.instructions.trim() || void 0;
2047
+ const metadata = metadataWithoutClaim(task.metadata);
2048
+ transaction(database, () => {
2049
+ const result = database.prepare(`
2050
+ update lineage_tasks
2051
+ set status = 'pending', instructions = ?, claimed_at = null, started_at = null,
2052
+ updated_at = ?, metadata_json = ?
2053
+ where project_id = ? and id = ? and status in ('claimed', 'in_progress')
2054
+ `).run(instructions || null, timestamp, metadataJson2(metadata), normalizedProject, task.id);
2055
+ assertChanged(result, `Only active lineage task ${task.id} could be overridden.`);
2056
+ if (task.claimed_by_claim_id) {
2057
+ revokeAgentClaimInDatabase(database, normalizedProject, task.claimed_by_claim_id, {
2058
+ actor,
2059
+ reason
2060
+ });
2061
+ }
2062
+ recordEvent2(database, task.id, "human_override", actor, reason, {
2063
+ previous_claim_id: task.claimed_by_claim_id,
2064
+ previous_status: task.status
2065
+ });
2066
+ });
2067
+ return taskWithEvents(database, normalizedProject, task.id);
2068
+ } finally {
2069
+ database.close();
2070
+ }
2071
+ }
2072
+ function cancelLineageTask(project, fields) {
2073
+ const normalizedProject = normalizeProject(project);
2074
+ const actor = normalizeActor(fields.actor, "Cancel actor");
2075
+ const database = lineageDb();
2076
+ try {
2077
+ const task = requireTask(database, normalizedProject, fields.taskId);
2078
+ if (task.status === "cancelled") return { project: normalizedProject, ok: true, task, events: taskEvents(database, task.id) };
2079
+ if (!["pending", "claimed", "in_progress"].includes(task.status)) {
2080
+ throw new LineageTaskError(`Only open lineage tasks can be cancelled; task is ${task.status}.`, 409);
2081
+ }
2082
+ if (task.status !== "pending" && !fields.override) {
2083
+ throw new LineageTaskError("Cancelling an active lineage task requires override=true.", 409);
2084
+ }
2085
+ const overridingActiveTask = fields.override === true && task.status !== "pending";
2086
+ const metadata = overridingActiveTask ? metadataWithoutClaim(task.metadata) : task.metadata;
2087
+ const timestamp = nowIso();
2088
+ const cancelledTask = {
2089
+ ...task,
2090
+ cancelled_at: timestamp,
2091
+ claimed_at: overridingActiveTask ? void 0 : task.claimed_at,
2092
+ claimed_by_claim_id: overridingActiveTask ? void 0 : task.claimed_by_claim_id,
2093
+ started_at: overridingActiveTask ? void 0 : task.started_at,
2094
+ status: "cancelled",
2095
+ updated_at: timestamp,
2096
+ metadata
2097
+ };
2098
+ if (!fields.confirmWrite) return { project: normalizedProject, ok: true, dryRun: true, task: cancelledTask, events: taskEvents(database, task.id) };
2099
+ transaction(database, () => {
2100
+ const result = database.prepare(`
2101
+ update lineage_tasks
2102
+ set status = 'cancelled', cancelled_at = ?, claimed_at = ?, started_at = ?,
2103
+ updated_at = ?, metadata_json = ?
2104
+ where id = ? and status = ?
2105
+ `).run(
2106
+ timestamp,
2107
+ overridingActiveTask ? null : task.claimed_at || null,
2108
+ overridingActiveTask ? null : task.started_at || null,
2109
+ timestamp,
2110
+ metadataJson2(metadata),
2111
+ task.id,
2112
+ task.status
2113
+ );
2114
+ assertChanged(result, `Only ${task.status} lineage task ${task.id} could be cancelled.`);
2115
+ if (overridingActiveTask) {
2116
+ if (task.claimed_by_claim_id) {
2117
+ revokeAgentClaimInDatabase(database, normalizedProject, task.claimed_by_claim_id, {
2118
+ actor,
2119
+ reason: "Lineage task cancelled by human override."
2120
+ });
2121
+ }
2122
+ recordEvent2(database, task.id, "human_override", actor, "Lineage task cancelled by human override.", {
2123
+ previous_claim_id: task.claimed_by_claim_id,
2124
+ previous_status: task.status
2125
+ });
2126
+ }
2127
+ recordEvent2(database, task.id, "cancelled", actor, "Lineage task cancelled.");
2128
+ });
2129
+ return taskWithEvents(database, normalizedProject, task.id);
2130
+ } finally {
2131
+ database.close();
2132
+ }
2133
+ }
2134
+ function resolveLineageTask(project, fields) {
2135
+ const normalizedProject = normalizeProject(project);
2136
+ const actor = normalizeActor(fields.actor, "Resolve actor");
2137
+ const database = lineageDb();
2138
+ try {
2139
+ const task = requireTask(database, normalizedProject, fields.taskId);
2140
+ if (fields.resolvedAssetId) requireAsset(database, normalizedProject, fields.resolvedAssetId);
2141
+ if (task.status === "resolved") return { project: normalizedProject, ok: true, task, events: taskEvents(database, task.id) };
2142
+ if (!["pending", "claimed", "in_progress"].includes(task.status)) {
2143
+ throw new LineageTaskError(`Only open lineage tasks can be resolved; task is ${task.status}.`, 409);
2144
+ }
2145
+ const timestamp = nowIso();
2146
+ const resolvedTask = {
2147
+ ...task,
2148
+ status: "resolved",
2149
+ resolved_asset_id: fields.resolvedAssetId,
2150
+ resolved_at: timestamp,
2151
+ resolved_generation_job_id: fields.resolvedGenerationJobId,
2152
+ updated_at: timestamp
2153
+ };
2154
+ if (!fields.confirmWrite) return { project: normalizedProject, ok: true, dryRun: true, task: resolvedTask, events: taskEvents(database, task.id) };
2155
+ transaction(database, () => {
2156
+ const result = database.prepare(`
2157
+ update lineage_tasks
2158
+ set status = 'resolved', resolved_at = ?, resolved_generation_job_id = ?, resolved_asset_id = ?, updated_at = ?
2159
+ where id = ? and status = ?
2160
+ `).run(timestamp, fields.resolvedGenerationJobId || null, fields.resolvedAssetId || null, timestamp, task.id, task.status);
2161
+ assertChanged(result, `Only ${task.status} lineage task ${task.id} could be resolved.`);
2162
+ recordEvent2(database, task.id, "resolved", actor, "Lineage task resolved.", {
2163
+ resolved_asset_id: fields.resolvedAssetId,
2164
+ resolved_generation_job_id: fields.resolvedGenerationJobId
2165
+ });
2166
+ });
2167
+ return taskWithEvents(database, normalizedProject, task.id);
2168
+ } finally {
2169
+ database.close();
2170
+ }
2171
+ }
2172
+
1403
2173
  // src/server/assetLineageWorkspaces.ts
1404
2174
  function lineageWorkspaceId(project, rootAssetId) {
1405
2175
  return `${project}:lineage-workspace:${rootAssetId}`;
@@ -1464,6 +2234,17 @@ function seedLegacyWorkspaces(database, project) {
1464
2234
  );
1465
2235
  }
1466
2236
  }
2237
+ function listRows(database, project) {
2238
+ return database.prepare(`
2239
+ select * from lineage_workspaces
2240
+ where project_id = ?
2241
+ order by
2242
+ case status when 'active' then 0 when 'paused' then 1 else 2 end,
2243
+ active_at desc nulls last,
2244
+ updated_at desc,
2245
+ title
2246
+ `).all(project).map(rowToWorkspace);
2247
+ }
1467
2248
  function activeWorkspace(database, project) {
1468
2249
  const row = database.prepare(`
1469
2250
  select * from lineage_workspaces
@@ -1473,6 +2254,20 @@ function activeWorkspace(database, project) {
1473
2254
  `).get(project);
1474
2255
  return row ? rowToWorkspace(row) : null;
1475
2256
  }
2257
+ function listLineageWorkspaces(project) {
2258
+ const database = lineageDb();
2259
+ try {
2260
+ seedLegacyWorkspaces(database, project);
2261
+ return {
2262
+ project,
2263
+ active_workspace: activeWorkspace(database, project),
2264
+ workspaces: listRows(database, project),
2265
+ fetchedAt: nowIso()
2266
+ };
2267
+ } finally {
2268
+ database.close();
2269
+ }
2270
+ }
1476
2271
  function activeLineageWorkspaceRoot(project) {
1477
2272
  const database = lineageDb();
1478
2273
  try {
@@ -1586,7 +2381,7 @@ function indexLineageAssets(project = defaultProject) {
1586
2381
  database.close();
1587
2382
  return { catalog: catalog.length, local: local.length, total: catalog.length + local.length, database: lineageDbPath() };
1588
2383
  }
1589
- function requireAsset(database, project, assetId) {
2384
+ function requireAsset2(database, project, assetId) {
1590
2385
  const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
1591
2386
  if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
1592
2387
  }
@@ -1606,7 +2401,7 @@ function rootFor(database, project, assetId) {
1606
2401
  return assetId;
1607
2402
  }
1608
2403
  function assertCanonicalRoot(database, project, root) {
1609
- requireAsset(database, project, root);
2404
+ requireAsset2(database, project, root);
1610
2405
  const canonicalRoot = rootFor(database, project, root);
1611
2406
  if (canonicalRoot !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
1612
2407
  }
@@ -1643,7 +2438,7 @@ function lineageWriteClaimContext(database, project, assetId) {
1643
2438
  function getLineageWriteClaimContext(project, assetId) {
1644
2439
  const database = lineageDb();
1645
2440
  try {
1646
- requireAsset(database, project, assetId);
2441
+ requireAsset2(database, project, assetId);
1647
2442
  return lineageWriteClaimContext(database, project, assetId);
1648
2443
  } finally {
1649
2444
  database.close();
@@ -1655,12 +2450,12 @@ function latestSelectedRoot(database, project) {
1655
2450
  }
1656
2451
  function resolveRoot(database, project, rootAssetId) {
1657
2452
  if (rootAssetId) {
1658
- requireAsset(database, project, rootAssetId);
2453
+ requireAsset2(database, project, rootAssetId);
1659
2454
  return rootAssetId;
1660
2455
  }
1661
2456
  const root = activeLineageWorkspaceRoot(project) || latestSelectedRoot(database, project);
1662
2457
  if (!root) throw new LineageError("Lineage command requires --root unless a project selection exists");
1663
- requireAsset(database, project, root);
2458
+ requireAsset2(database, project, root);
1664
2459
  return root;
1665
2460
  }
1666
2461
  function edgeId(project, parent, child) {
@@ -1685,6 +2480,33 @@ function rerollRequestFrom(row) {
1685
2480
  resolved_at: rowString(row.resolved_at)
1686
2481
  };
1687
2482
  }
2483
+ function tasksByTarget(tasks) {
2484
+ const byTarget = /* @__PURE__ */ new Map();
2485
+ for (const task of tasks) {
2486
+ if (task.task_type !== "iterate" && task.task_type !== "reroll") continue;
2487
+ const existing = byTarget.get(task.target_asset_id) || {};
2488
+ existing[task.task_type] = task;
2489
+ byTarget.set(task.target_asset_id, existing);
2490
+ }
2491
+ return byTarget;
2492
+ }
2493
+ function withRerollTask(request, task) {
2494
+ return task ? { ...request, task_id: task.id, task } : request;
2495
+ }
2496
+ function taskBackedRerollRequest(project, rootAssetId, task) {
2497
+ return {
2498
+ id: `${task.id}:request`,
2499
+ project_id: project,
2500
+ root_asset_id: rootAssetId,
2501
+ node_asset_id: task.target_asset_id,
2502
+ status: "pending",
2503
+ requested_by: task.created_by,
2504
+ notes: task.instructions,
2505
+ created_at: task.created_at,
2506
+ task_id: task.id,
2507
+ task
2508
+ };
2509
+ }
1688
2510
  function attemptFrom(row) {
1689
2511
  return {
1690
2512
  id: String(row.id),
@@ -1724,7 +2546,7 @@ function withImplicitAttempt(physicalAttempts, row) {
1724
2546
  }
1725
2547
  function assertNodeInRoot(database, project, root, node) {
1726
2548
  assertCanonicalRoot(database, project, root);
1727
- requireAsset(database, project, node);
2549
+ requireAsset2(database, project, node);
1728
2550
  const nodeRoot = rootFor(database, project, node);
1729
2551
  if (nodeRoot !== root) throw new LineageError(`Asset ${node} is not in lineage rooted at ${root}`);
1730
2552
  }
@@ -1745,8 +2567,8 @@ function localPreviewUrl(project, localPath) {
1745
2567
  }
1746
2568
  function linkLineageAssets(project, fields) {
1747
2569
  const database = lineageDb();
1748
- requireAsset(database, project, fields.parentAssetId);
1749
- requireAsset(database, project, fields.childAssetId);
2570
+ requireAsset2(database, project, fields.parentAssetId);
2571
+ requireAsset2(database, project, fields.childAssetId);
1750
2572
  if (fields.parentAssetId === fields.childAssetId) throw new LineageError("Lineage link cannot point to itself");
1751
2573
  const claimContext = lineageWriteClaimContext(database, project, fields.parentAssetId);
1752
2574
  try {
@@ -1797,10 +2619,17 @@ function descendants(database, project, root) {
1797
2619
  }
1798
2620
  function getLineageSnapshot(project, assetId) {
1799
2621
  const database = lineageDb();
1800
- requireAsset(database, project, assetId);
2622
+ requireAsset2(database, project, assetId);
1801
2623
  const root = explicitWorkspaceRoot(database, project, assetId) || rootFor(database, project, assetId);
1802
2624
  const edges = descendants(database, project, root);
1803
- const ids = [.../* @__PURE__ */ new Set([root, ...edges.flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id])])];
2625
+ const selected = selectedRows(database, project, root);
2626
+ const tasks = listLineageTasks(project, root).tasks;
2627
+ const ids = [.../* @__PURE__ */ new Set([
2628
+ root,
2629
+ ...edges.flatMap((edge) => [edge.parent_asset_id, edge.child_asset_id]),
2630
+ ...selected.map((row) => row.asset_id),
2631
+ ...tasks.map((task) => task.target_asset_id)
2632
+ ])];
1804
2633
  const placeholders = ids.map(() => "?").join(",");
1805
2634
  const rows = database.prepare(`
1806
2635
  select a.id asset_id, a.project_id project, a.source, a.title, a.media_type, a.status, a.channel, a.campaign,
@@ -1815,12 +2644,12 @@ function getLineageSnapshot(project, assetId) {
1815
2644
  for (const attempt of attemptRows.map(attemptFrom)) {
1816
2645
  attemptsByNode.set(attempt.node_asset_id, [...attemptsByNode.get(attempt.node_asset_id) || [], attempt]);
1817
2646
  }
2647
+ const lineageTasksByNode = tasksByTarget(tasks);
1818
2648
  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) : [];
1819
2649
  const rerollsByNode = new Map(rerollRows.map((row) => {
1820
2650
  const request = rerollRequestFrom(row);
1821
- return [request.node_asset_id, request];
2651
+ return [request.node_asset_id, withRerollTask(request, lineageTasksByNode.get(request.node_asset_id)?.reroll)];
1822
2652
  }));
1823
- const selected = selectedRows(database, project, root);
1824
2653
  const childIds = new Set(edges.map((edge) => edge.parent_asset_id));
1825
2654
  const selectedIds = new Set(selected.map((row) => row.asset_id));
1826
2655
  const selections = selected.map((row) => ({
@@ -1837,11 +2666,13 @@ function getLineageSnapshot(project, assetId) {
1837
2666
  const attempts = withImplicitAttempt(attemptsByNode.get(row.asset_id) || [], row);
1838
2667
  const currentAttempt = attempts.find((attempt) => attempt.is_current) || attempts[0];
1839
2668
  const previewPath = currentAttempt?.file_path || row.local_path;
2669
+ const lineageTasks = lineageTasksByNode.get(row.asset_id);
1840
2670
  return {
1841
2671
  ...node,
1842
2672
  attempt_count: attempts.length,
1843
2673
  current_attempt: currentAttempt,
1844
2674
  is_latest: !childIds.has(row.asset_id),
2675
+ lineage_tasks: lineageTasks && Object.keys(lineageTasks).length > 0 ? lineageTasks : void 0,
1845
2676
  position,
1846
2677
  preview_url: canPreviewLocally(row.media_type, previewPath) ? localPreviewUrl(project, previewPath) : void 0,
1847
2678
  reroll_request: rerollsByNode.get(row.asset_id),
@@ -1857,6 +2688,7 @@ function getLineageSnapshot(project, assetId) {
1857
2688
  selected: selections.map((row) => row.asset_id),
1858
2689
  selection,
1859
2690
  selections,
2691
+ tasks,
1860
2692
  latest: nodes.filter((node) => node.is_latest).map((node) => node.asset_id),
1861
2693
  nodes,
1862
2694
  edges,
@@ -1868,7 +2700,25 @@ function getLineageNextAsset(project, rootAssetId) {
1868
2700
  const root = resolveRoot(database, project, rootAssetId);
1869
2701
  database.close();
1870
2702
  const snapshot = getLineageSnapshot(project, root);
1871
- const selectedNodes = snapshot.selected.map((assetId) => snapshot.nodes.find((node) => node.asset_id === assetId)).filter((node) => Boolean(node));
2703
+ const pendingIterateTasks = (snapshot.tasks || []).filter((task) => task.task_type === "iterate" && task.status === "pending");
2704
+ const taskIdsInSelectionOrder = [
2705
+ ...snapshot.selections.map((selection) => selection.asset_id).filter((assetId) => pendingIterateTasks.some((task) => task.target_asset_id === assetId)),
2706
+ ...pendingIterateTasks.map((task) => task.target_asset_id).filter((assetId) => !snapshot.selections.some((selection) => selection.asset_id === assetId))
2707
+ ];
2708
+ const taskSelectedIds = [...new Set(taskIdsInSelectionOrder)];
2709
+ const selectedIds = taskSelectedIds.length > 0 ? taskSelectedIds : snapshot.selected;
2710
+ const selectedNodes = selectedIds.map((assetId) => snapshot.nodes.find((node) => node.asset_id === assetId)).filter((node) => Boolean(node));
2711
+ const taskSelections = taskSelectedIds.map((assetId, position) => {
2712
+ const task = pendingIterateTasks.find((item) => item.target_asset_id === assetId);
2713
+ return {
2714
+ asset_id: assetId,
2715
+ notes: task?.instructions,
2716
+ position,
2717
+ selected_at: task?.created_at || nowIso()
2718
+ };
2719
+ });
2720
+ const selectedSelection = taskSelections.length > 0 ? taskSelections[0] : snapshot.selection;
2721
+ const selectedSelections = taskSelections.length > 0 ? taskSelections : snapshot.selections;
1872
2722
  const latestNodes = snapshot.nodes.filter((node) => snapshot.latest.includes(node.asset_id));
1873
2723
  const warnings = [];
1874
2724
  for (const selectedNode of selectedNodes) {
@@ -1886,9 +2736,9 @@ function getLineageNextAsset(project, rootAssetId) {
1886
2736
  next_asset: selectedNodes[0],
1887
2737
  next_assets: selectedNodes,
1888
2738
  latest: snapshot.latest,
1889
- selected: snapshot.selected,
1890
- selection: snapshot.selection,
1891
- selections: snapshot.selections,
2739
+ selected: selectedIds,
2740
+ selection: selectedSelection,
2741
+ selections: selectedSelections,
1892
2742
  candidates: latestNodes,
1893
2743
  warnings,
1894
2744
  fetchedAt: nowIso()
@@ -1905,9 +2755,9 @@ function getLineageNextAsset(project, rootAssetId) {
1905
2755
  next_asset: latestNodes[0],
1906
2756
  next_assets: [latestNodes[0]],
1907
2757
  latest: snapshot.latest,
1908
- selected: snapshot.selected,
1909
- selection: snapshot.selection,
1910
- selections: snapshot.selections,
2758
+ selected: selectedIds,
2759
+ selection: selectedSelection,
2760
+ selections: selectedSelections,
1911
2761
  candidates: latestNodes,
1912
2762
  warnings,
1913
2763
  fetchedAt: nowIso()
@@ -1923,9 +2773,9 @@ function getLineageNextAsset(project, rootAssetId) {
1923
2773
  next_asset: null,
1924
2774
  next_assets: [],
1925
2775
  latest: snapshot.latest,
1926
- selected: snapshot.selected,
1927
- selection: snapshot.selection,
1928
- selections: snapshot.selections,
2776
+ selected: selectedIds,
2777
+ selection: selectedSelection,
2778
+ selections: selectedSelections,
1929
2779
  candidates: latestNodes,
1930
2780
  warnings,
1931
2781
  fetchedAt: nowIso()
@@ -1935,12 +2785,23 @@ function listLineageRerollRequests(project, rootAssetId) {
1935
2785
  const database = lineageDb();
1936
2786
  try {
1937
2787
  assertCanonicalRoot(database, project, rootAssetId);
2788
+ const rerollTasks = listLineageTasks(project, rootAssetId).tasks.filter((task) => task.task_type === "reroll");
2789
+ const pendingRerollTasks = listLineageTasks(project, rootAssetId, ["pending"]).tasks.filter((task) => task.task_type === "reroll");
2790
+ const rerollTaskByTarget = new Map(rerollTasks.map((task) => [task.target_asset_id, task]));
1938
2791
  const rows = database.prepare(`
1939
2792
  select * from asset_reroll_requests
1940
2793
  where project_id = ? and root_asset_id = ? and status = 'pending'
1941
2794
  order by created_at, id
1942
2795
  `).all(project, rootAssetId);
1943
- return { project, root_asset_id: rootAssetId, requests: rows.map(rerollRequestFrom), fetchedAt: nowIso() };
2796
+ const requests = rows.map((row) => {
2797
+ const request = rerollRequestFrom(row);
2798
+ return withRerollTask(request, rerollTaskByTarget.get(request.node_asset_id));
2799
+ });
2800
+ const legacyTargets = new Set(requests.map((request) => request.node_asset_id));
2801
+ for (const task of pendingRerollTasks) {
2802
+ if (!legacyTargets.has(task.target_asset_id)) requests.push(taskBackedRerollRequest(project, rootAssetId, task));
2803
+ }
2804
+ return { project, root_asset_id: rootAssetId, requests, fetchedAt: nowIso() };
1944
2805
  } finally {
1945
2806
  database.close();
1946
2807
  }
@@ -1949,7 +2810,7 @@ function recordLineageRerollAttempt(project, fields) {
1949
2810
  const database = lineageDb();
1950
2811
  try {
1951
2812
  assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
1952
- requireAsset(database, project, fields.assetId);
2813
+ requireAsset2(database, project, fields.assetId);
1953
2814
  assertAttemptAssetNotVisibleLineageNode(database, project, fields.rootAssetId, fields.assetId);
1954
2815
  const timestamp = nowIso();
1955
2816
  const maxRow = database.prepare("select max(attempt_index) max_index from asset_attempts where project_id = ? and node_asset_id = ?").get(project, fields.nodeAssetId);
@@ -2038,6 +2899,13 @@ function markLineageRerollRequest(project, fields) {
2038
2899
  created_at: timestamp
2039
2900
  };
2040
2901
  if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
2902
+ const taskResult = upsertLineageTask(project, {
2903
+ createdBy: request.requested_by,
2904
+ instructions: request.notes,
2905
+ rootAssetId: fields.rootAssetId,
2906
+ targetAssetId: fields.nodeAssetId,
2907
+ taskType: "reroll"
2908
+ });
2041
2909
  if (existing) {
2042
2910
  database.prepare(`
2043
2911
  update asset_reroll_requests
@@ -2057,7 +2925,12 @@ function markLineageRerollRequest(project, fields) {
2057
2925
  review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
2058
2926
  ignored_at = excluded.ignored_at, notes = coalesce(excluded.notes, asset_reviews.notes), updated_at = excluded.updated_at
2059
2927
  `).run(fields.nodeAssetId, timestamp, request.notes || null, timestamp);
2060
- return { ok: true, request: rerollRequestFrom(database.prepare("select * from asset_reroll_requests where id = ?").get(request.id)) };
2928
+ return {
2929
+ ok: true,
2930
+ request: withRerollTask(rerollRequestFrom(database.prepare("select * from asset_reroll_requests where id = ?").get(request.id)), taskResult.task),
2931
+ task_id: taskResult.task.id,
2932
+ task: taskResult.task
2933
+ };
2061
2934
  } finally {
2062
2935
  database.close();
2063
2936
  }
@@ -2075,12 +2948,23 @@ function clearLineageRerollRequest(project, fields) {
2075
2948
  const timestamp = nowIso();
2076
2949
  const request = { ...rerollRequestFrom(existing), status: "cancelled", resolved_at: timestamp };
2077
2950
  if (!fields.confirmWrite) return { ok: true, dryRun: true, request };
2951
+ const task = listLineageTasks(project, fields.rootAssetId, ["pending"]).tasks.find((item) => item.task_type === "reroll" && item.target_asset_id === fields.nodeAssetId);
2952
+ const cancelledTask = task ? cancelLineageTask(project, {
2953
+ actor: "human",
2954
+ confirmWrite: true,
2955
+ taskId: task.id
2956
+ }).task : void 0;
2078
2957
  database.prepare(`
2079
2958
  update asset_reroll_requests
2080
2959
  set status = 'cancelled', resolved_at = ?
2081
2960
  where id = ?
2082
2961
  `).run(timestamp, request.id);
2083
- return { ok: true, request };
2962
+ return {
2963
+ ok: true,
2964
+ request: withRerollTask(request, cancelledTask),
2965
+ task_id: cancelledTask?.id,
2966
+ task: cancelledTask
2967
+ };
2084
2968
  } finally {
2085
2969
  database.close();
2086
2970
  }
@@ -2097,6 +2981,9 @@ function lineageCommand(command, project, rootAssetId) {
2097
2981
  function linkChildCommand(project, rootAssetId) {
2098
2982
  return `${publicPackageCommand} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write --db ${shellQuote(lineageDbPath())} --json`;
2099
2983
  }
2984
+ function rerollImportGuidance(rootAssetId, targetAssetId) {
2985
+ return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
2986
+ }
2100
2987
  function getLineageBrief(project, rootAssetId) {
2101
2988
  const next = getLineageNextAsset(project, rootAssetId);
2102
2989
  const assets = next.next_assets;
@@ -2143,6 +3030,14 @@ function getLineageBrief(project, rootAssetId) {
2143
3030
  function linkSelectedLineageChild(project, fields) {
2144
3031
  const next = getLineageNextAsset(project, fields.rootAssetId);
2145
3032
  if (!next.next_asset) throw new LineageError("Cannot link child until a next base is selected or unambiguous");
3033
+ const rerollRequests = listLineageRerollRequests(project, next.root_asset_id).requests;
3034
+ const pendingRerollForParent = rerollRequests.find((request) => request.node_asset_id === next.next_asset?.asset_id);
3035
+ if (pendingRerollForParent && fields.confirmWrite) {
3036
+ throw new LineageError(
3037
+ `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.`,
3038
+ 409
3039
+ );
3040
+ }
2146
3041
  if (fields.confirmWrite) {
2147
3042
  const claimContext = getLineageWriteClaimContext(project, next.next_asset.asset_id);
2148
3043
  const validation = validateAgentClaimForWrite({
@@ -2169,7 +3064,11 @@ function linkSelectedLineageChild(project, fields) {
2169
3064
  parent_asset_id: next.next_asset.asset_id,
2170
3065
  child_asset_id: fields.childAssetId,
2171
3066
  reference_asset_ids: next.next_assets.map((asset) => asset.asset_id),
2172
- 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
3067
+ warning: [
3068
+ 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,
3069
+ 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,
3070
+ 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
3071
+ ].filter(Boolean).join(" ") || void 0
2173
3072
  };
2174
3073
  }
2175
3074
 
@@ -2359,7 +3258,7 @@ function planImageReroll(project = defaultProject, fields) {
2359
3258
  created_at: timestamp
2360
3259
  }]
2361
3260
  };
2362
- if (fields.dryRun) return { ok: true, command: "generate image plan", project, dryRun: true, wouldWrite: true, job: preview };
3261
+ if (fields.dryRun) return { ok: true, command: "reroll plan", project, dryRun: true, wouldWrite: true, job: preview };
2363
3262
  const database = lineageDb();
2364
3263
  try {
2365
3264
  database.exec("BEGIN IMMEDIATE");
@@ -2377,7 +3276,7 @@ function planImageReroll(project = defaultProject, fields) {
2377
3276
  database.exec("ROLLBACK");
2378
3277
  throw error;
2379
3278
  }
2380
- return { ok: true, command: "generate image plan", project, job: loadGenerationJob(database, project, id) };
3279
+ return { ok: true, command: "reroll plan", project, job: loadGenerationJob(database, project, id) };
2381
3280
  } finally {
2382
3281
  database.close();
2383
3282
  }
@@ -2456,20 +3355,251 @@ function importImageRerollOutput(project = defaultProject, fields) {
2456
3355
  checksumSha256: resolved.checksum,
2457
3356
  confirmWrite: true
2458
3357
  });
3358
+ const rerollTask = listLineageTasks(project, job.root_asset_id).tasks.find((task) => task.task_type === "reroll" && task.target_asset_id === target[0].asset_id);
3359
+ if (rerollTask) {
3360
+ resolveLineageTask(project, {
3361
+ actor: "agent",
3362
+ confirmWrite: true,
3363
+ resolvedAssetId: resolved.assetId,
3364
+ resolvedGenerationJobId: fields.jobId,
3365
+ taskId: rerollTask.id
3366
+ });
3367
+ }
2459
3368
  const importedJob = loadGenerationJob(writeDb, project, fields.jobId);
2460
- return { ok: true, command: "generate image import", project, job: importedJob, imported: importedJob.outputs };
3369
+ return { ok: true, command: "reroll import", project, job: importedJob, imported: importedJob.outputs };
2461
3370
  } finally {
2462
3371
  writeDb.close();
2463
3372
  }
2464
3373
  }
2465
3374
 
3375
+ // src/server/lineageSelectionPacket.ts
3376
+ import { createHash as createHash3 } from "node:crypto";
3377
+ import { existsSync as existsSync5, statSync as statSync4 } from "node:fs";
3378
+ import { isAbsolute, resolve as resolve4 } from "node:path";
3379
+ var LineageSelectionPacketError = class extends Error {
3380
+ constructor(message, warnings = [], errors = []) {
3381
+ super(message);
3382
+ this.warnings = warnings;
3383
+ this.errors = errors;
3384
+ }
3385
+ warnings;
3386
+ errors;
3387
+ };
3388
+ function listAllAssets(project) {
3389
+ const pageSize = 100;
3390
+ const first = listAssets(project, { page: 1, pageSize, source: "all" });
3391
+ const assets = [...first.assets];
3392
+ for (let page = 2; page <= first.pagination.totalPages; page += 1) {
3393
+ assets.push(...listAssets(project, { page, pageSize, source: "all" }).assets);
3394
+ }
3395
+ return assets;
3396
+ }
3397
+ function resolveWorkspace(project, options) {
3398
+ if (options.rootAssetId && options.workspaceId) {
3399
+ throw new LineageSelectionPacketError("Use either --root or --workspace, not both.", [], ["ambiguous_workspace"]);
3400
+ }
3401
+ const snapshot = listLineageWorkspaces(project);
3402
+ const target = options.workspaceId || options.rootAssetId;
3403
+ const workspace = target ? snapshot.workspaces.find((item) => item.id === target || item.root_asset_id === target) : snapshot.active_workspace;
3404
+ if (workspace) return workspace;
3405
+ if (options.rootAssetId) {
3406
+ return {
3407
+ id: lineageWorkspaceId(project, options.rootAssetId),
3408
+ project,
3409
+ root_asset_id: options.rootAssetId,
3410
+ title: `${options.rootAssetId} lineage`,
3411
+ status: "active",
3412
+ created_by: "system",
3413
+ created_at: "",
3414
+ updated_at: ""
3415
+ };
3416
+ }
3417
+ const message = options.workspaceId ? `Unknown lineage workspace: ${options.workspaceId}` : "No active lineage workspace. Pass --root or activate a workspace first.";
3418
+ throw new LineageSelectionPacketError(message, [], [options.workspaceId ? "unknown_workspace" : "missing_active_workspace"]);
3419
+ }
3420
+ function resolveLocalReference(reference) {
3421
+ if (!reference) return void 0;
3422
+ if (isAbsolute(reference)) return reference;
3423
+ const repoRelative = resolve4(repoRoot, reference);
3424
+ if (existsSync5(repoRelative)) return repoRelative;
3425
+ return resolve4(repoRoot, ".asset-scratch", reference);
3426
+ }
3427
+ function fileSize(path) {
3428
+ if (!path || !existsSync5(path)) return void 0;
3429
+ try {
3430
+ return statSync4(path).size;
3431
+ } catch {
3432
+ return void 0;
3433
+ }
3434
+ }
3435
+ function storageState(hasLocal, hasS3) {
3436
+ if (hasLocal && hasS3) return "local_and_s3";
3437
+ if (hasLocal) return "local_only";
3438
+ if (hasS3) return "s3_backed";
3439
+ return "unresolved";
3440
+ }
3441
+ function currentAttemptFor(node) {
3442
+ if (!node.current_attempt) return void 0;
3443
+ return {
3444
+ asset_id: node.current_attempt.asset_id,
3445
+ checksum_sha256: node.current_attempt.checksum_sha256,
3446
+ file_path: node.current_attempt.file_path,
3447
+ generation_job_id: node.current_attempt.generation_job_id,
3448
+ id: node.current_attempt.id,
3449
+ is_current: node.current_attempt.is_current,
3450
+ source: node.current_attempt.source
3451
+ };
3452
+ }
3453
+ function assetForNode(node, catalogAsset, warnings) {
3454
+ const localReference = catalogAsset?.local?.absolute_path || node.current_attempt?.file_path || node.local_path || catalogAsset?.local?.relative_path;
3455
+ const absolutePath = catalogAsset?.local?.absolute_path || resolveLocalReference(localReference);
3456
+ const localExists = absolutePath ? existsSync5(absolutePath) : false;
3457
+ const hasLocalClaim = Boolean(absolutePath || node.local_path || catalogAsset?.local?.relative_path);
3458
+ const s3Key = catalogAsset?.s3?.key || node.s3_key;
3459
+ const hasS3 = Boolean(s3Key);
3460
+ const checksum = catalogAsset?.local?.checksum_sha256 || node.current_attempt?.checksum_sha256 || node.checksum_sha256 || catalogAsset?.s3?.checksum_sha256;
3461
+ const mimeType = catalogAsset?.local?.content_type || catalogAsset?.s3?.content_type;
3462
+ const mediaType = catalogAsset?.content_type || node.media_type;
3463
+ if (hasLocalClaim && !localExists) warnings.push(`Selected asset ${node.asset_id} has a local path but the file is missing: ${absolutePath || node.local_path}`);
3464
+ if (!hasLocalClaim && !hasS3) warnings.push(`Selected asset ${node.asset_id} has neither a local file nor S3 key.`);
3465
+ if (mediaType && mediaType !== "image" && mediaType !== "gif") warnings.push(`Selected asset ${node.asset_id} is ${mediaType}, not an image/gif asset.`);
3466
+ return {
3467
+ asset_id: node.asset_id,
3468
+ campaign: catalogAsset?.campaign || node.campaign,
3469
+ channel: catalogAsset?.channel || node.channel,
3470
+ checksum_sha256: checksum,
3471
+ current_attempt: currentAttemptFor(node),
3472
+ local: {
3473
+ absolute_path: absolutePath,
3474
+ content_type: catalogAsset?.local?.content_type,
3475
+ exists: localExists,
3476
+ relative_path: catalogAsset?.local?.relative_path || node.local_path || node.current_attempt?.file_path,
3477
+ size_bytes: catalogAsset?.local?.size_bytes || fileSize(absolutePath)
3478
+ },
3479
+ media_type: mediaType,
3480
+ mime_type: mimeType,
3481
+ review_notes: node.review_notes,
3482
+ review_state: node.review_state,
3483
+ s3: {
3484
+ bucket: catalogAsset?.s3?.bucket,
3485
+ checksum_sha256: catalogAsset?.s3?.checksum_sha256,
3486
+ content_type: catalogAsset?.s3?.content_type,
3487
+ etag: catalogAsset?.s3?.etag,
3488
+ key: s3Key,
3489
+ region: catalogAsset?.s3?.region,
3490
+ size_bytes: catalogAsset?.s3?.size_bytes,
3491
+ version_id: catalogAsset?.s3?.version_id
3492
+ },
3493
+ selection_note: node.selection_note,
3494
+ source: catalogAsset?.source || node.source,
3495
+ status: catalogAsset?.status || node.status,
3496
+ storage_state: storageState(hasLocalClaim, hasS3),
3497
+ title: catalogAsset?.title || node.title || node.asset_id
3498
+ };
3499
+ }
3500
+ function packetId(packetIdentity) {
3501
+ const digest = createHash3("sha256").update(JSON.stringify(packetIdentity)).digest("hex").slice(0, 24);
3502
+ return `lineage_packet_${digest}`;
3503
+ }
3504
+ function getLineageSelectionPacket(project, options = {}) {
3505
+ const workspace = resolveWorkspace(project, options);
3506
+ const snapshot = getLineageSnapshot(project, workspace.root_asset_id);
3507
+ const catalogSummary = validateProject(project);
3508
+ const catalogAssets = listAllAssets(project);
3509
+ const catalogById = new Map(catalogAssets.map((asset) => [asset.asset_id, asset]));
3510
+ const warnings = [];
3511
+ const errors = [];
3512
+ const nodeById = new Map(snapshot.nodes.map((node) => [node.asset_id, node]));
3513
+ const selectedItems = snapshot.selections.map((selection) => ({
3514
+ asset_id: selection.asset_id,
3515
+ position: selection.position,
3516
+ selected_at: selection.selected_at,
3517
+ selection_note: selection.notes
3518
+ }));
3519
+ if (selectedItems.length === 0) warnings.push(`Lineage workspace ${workspace.id} has no selected assets.`);
3520
+ const assets = selectedItems.flatMap((item) => {
3521
+ const node = nodeById.get(item.asset_id);
3522
+ if (!node) {
3523
+ const message = `Selected asset ${item.asset_id} is not present in the resolved workspace snapshot.`;
3524
+ warnings.push(message);
3525
+ errors.push("selected_asset_outside_workspace");
3526
+ return [];
3527
+ }
3528
+ return [assetForNode(node, catalogById.get(item.asset_id), warnings)];
3529
+ });
3530
+ if (assets.some((asset) => !asset.dimensions)) warnings.push("Image dimensions are unavailable for one or more selected assets.");
3531
+ if (options.strict) {
3532
+ const strictErrors = [
3533
+ ...selectedItems.length === 0 ? ["empty_selection"] : [],
3534
+ ...assets.filter((asset) => asset.local.absolute_path && !asset.local.exists).map((asset) => `missing_local_file:${asset.asset_id}`),
3535
+ ...errors
3536
+ ];
3537
+ if (strictErrors.length > 0) {
3538
+ throw new LineageSelectionPacketError(`Lineage selection packet strict mode failed: ${strictErrors.join(", ")}`, warnings, strictErrors);
3539
+ }
3540
+ }
3541
+ const context = {
3542
+ campaign: options.campaign,
3543
+ channel: options.channel,
3544
+ labels: options.labels || [],
3545
+ notes: options.contextNotes
3546
+ };
3547
+ const identity = {
3548
+ assets: assets.map((asset) => ({
3549
+ asset_id: asset.asset_id,
3550
+ checksum_sha256: asset.checksum_sha256,
3551
+ local_path: asset.local.absolute_path,
3552
+ s3_key: asset.s3.key,
3553
+ storage_state: asset.storage_state
3554
+ })),
3555
+ context,
3556
+ project,
3557
+ root_asset_id: workspace.root_asset_id,
3558
+ schema_version: "lineage.selection_packet.v1",
3559
+ selection: selectedItems,
3560
+ workspace_id: workspace.id
3561
+ };
3562
+ return {
3563
+ assets,
3564
+ context,
3565
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
3566
+ errors,
3567
+ kind: "lineage.selection_packet",
3568
+ packet_id: packetId(identity),
3569
+ product: catalogSummary.product,
3570
+ project,
3571
+ schema_version: "lineage.selection_packet.v1",
3572
+ selection: {
3573
+ asset_ids: selectedItems.map((item) => item.asset_id),
3574
+ count: selectedItems.length,
3575
+ items: selectedItems,
3576
+ root_asset_id: workspace.root_asset_id
3577
+ },
3578
+ source: {
3579
+ app: "lineage",
3580
+ command: options.command,
3581
+ db_path: options.dbPath || process.env.LINEAGE_DB || lineageDbPath(),
3582
+ package: options.packageVersion
3583
+ },
3584
+ warnings: [...new Set(warnings)],
3585
+ workspace: {
3586
+ active_at: workspace.active_at,
3587
+ id: workspace.id,
3588
+ notes: workspace.notes,
3589
+ root_asset_id: workspace.root_asset_id,
3590
+ status: workspace.status,
3591
+ title: workspace.title
3592
+ }
3593
+ };
3594
+ }
3595
+
2466
3596
  // src/cli/lineageCli.ts
2467
3597
  var signalExitCodes = {
2468
3598
  SIGINT: 130,
2469
3599
  SIGTERM: 143
2470
3600
  };
2471
3601
  function packageRoot() {
2472
- return resolve4(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
3602
+ return resolve5(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
2473
3603
  }
2474
3604
  function packageVersion() {
2475
3605
  try {
@@ -2493,6 +3623,19 @@ function readOption(args, name) {
2493
3623
  if (index >= 0) return args[index + 1];
2494
3624
  return void 0;
2495
3625
  }
3626
+ function readOptions(args, name) {
3627
+ const values = [];
3628
+ const prefix = `${name}=`;
3629
+ for (let index = 0; index < args.length; index += 1) {
3630
+ const arg = args[index];
3631
+ if (arg.startsWith(prefix)) values.push(arg.slice(prefix.length));
3632
+ else if (arg === name && args[index + 1] && !args[index + 1].startsWith("--")) {
3633
+ values.push(args[index + 1]);
3634
+ index += 1;
3635
+ }
3636
+ }
3637
+ return values;
3638
+ }
2496
3639
  function resolveStartOptions(config, args) {
2497
3640
  const runtimeDir = dataRoot(config.displayName);
2498
3641
  const rawPort = readOption(args, "--port") || process.env.PORT || String(config.defaultPort);
@@ -2508,20 +3651,29 @@ function resolveStartOptions(config, args) {
2508
3651
  port
2509
3652
  };
2510
3653
  }
2511
- function printHelp(config) {
2512
- console.log(`${config.binName} ${packageVersion()}
3654
+ function formatLineageHelp(config) {
3655
+ return `${config.binName} ${packageVersion()}
2513
3656
 
2514
3657
  Usage:
2515
3658
  ${config.binName} start [--port <port>] [--host <host>] [--db <path>] [--open] [--json]
2516
3659
  ${config.binName} next [--project <project>] [--root <asset-id>] [--db <path>] [--json]
2517
3660
  ${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
2518
3661
  ${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
3662
+ ${config.binName} selection packet [--project <project>] [--workspace <id-or-root>|--root <asset-id>] [--channel <channel>] [--campaign <campaign>] [--context-notes <text>] [--label <label>] [--out <path>] [--strict] [--db <path>] [--json]
2519
3663
  ${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
2520
3664
  ${config.binName} reroll list --root <asset-id> [--project <project>] [--db <path>] [--json]
2521
3665
  ${config.binName} reroll mark --root <asset-id> --target <asset-id> [--notes <text>] [--requested-by agent|human|system] [--project <project>] [--confirm-write] [--db <path>] [--json]
2522
3666
  ${config.binName} reroll cancel --root <asset-id> --target <asset-id> [--project <project>] [--confirm-write] [--db <path>] [--json]
2523
3667
  ${config.binName} reroll plan --root <asset-id> --target <asset-id> --prompt <text> [--project <project>] [--db <path>] [--json]
2524
3668
  ${config.binName} reroll import --job-id <job-id> --file <scratch-file> --confirm-write [--project <project>] [--db <path>] [--json]
3669
+ ${config.binName} tasks list --root <asset-id> [--project <project>] [--db <path>] [--json]
3670
+ ${config.binName} tasks inspect --task <task-id> [--project <project>] [--db <path>] [--json]
3671
+ ${config.binName} tasks claim --task <task-id> --agent-name <name> [--project <project>] [--db <path>] [--json]
3672
+ ${config.binName} tasks start --task <task-id> --claim-token <claim-id.secret> [--project <project>] [--db <path>] [--json]
3673
+ ${config.binName} tasks comment --task <task-id> --message <text> [--project <project>] [--db <path>] [--json]
3674
+ ${config.binName} tasks cancel --task <task-id> [--confirm-write] [--override] [--project <project>] [--db <path>] [--json]
3675
+ ${config.binName} tasks override --task <task-id> --reason <text> [--instructions <text>] [--project <project>] [--db <path>] [--json]
3676
+ ${config.binName} tasks instructions --task <task-id> --instructions <text> [--project <project>] [--db <path>] [--json]
2525
3677
  ${config.binName} agent claim --project <project> --scope <scope> --target <target-id> --agent-name <name> [--channel <channel>] [--ttl 20m] [--json]
2526
3678
  ${config.binName} agent graph --root <asset-id> [--project <project>] [--db <path>] [--json]
2527
3679
  ${config.binName} agent status [--project <project>] [--json]
@@ -2533,7 +3685,14 @@ Usage:
2533
3685
  ${config.binName} --help
2534
3686
  ${config.binName} --version
2535
3687
 
2536
- ${config.displayName} runs the bundled Lineage server for the ${config.channel} channel.`);
3688
+ ${config.displayName} runs the bundled Lineage server for the ${config.channel} channel.
3689
+
3690
+ Variation vs re-roll:
3691
+ link-child creates a new visible child variation edge.
3692
+ reroll mark -> reroll plan -> reroll import updates the same node with a new attempt.`;
3693
+ }
3694
+ function printHelp(config) {
3695
+ console.log(formatLineageHelp(config));
2537
3696
  }
2538
3697
  function openBrowser(url) {
2539
3698
  const command = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
@@ -2576,6 +3735,31 @@ function runLineageDataCommand(command, args) {
2576
3735
  if (!options.assetId) throw new Error("lineage inspect requires --asset-id");
2577
3736
  return getLineageSnapshot(options.project, options.assetId);
2578
3737
  }
3738
+ if (command === "selection") {
3739
+ const subcommand = positionalArgs(args)[0] || "";
3740
+ if (subcommand !== "packet") throw new Error(`Unknown selection command: ${subcommand}`);
3741
+ const labels = readOptions(args, "--label").flatMap((label) => label.split(",")).map((label) => label.trim()).filter(Boolean);
3742
+ const packet = getLineageSelectionPacket(options.project, {
3743
+ campaign: readOption(args, "--campaign"),
3744
+ channel: readOption(args, "--channel"),
3745
+ command: "lineage selection packet",
3746
+ contextNotes: readOption(args, "--context-notes") || readOption(args, "--notes"),
3747
+ dbPath: options.dbPath,
3748
+ labels,
3749
+ packageVersion: packageVersion(),
3750
+ rootAssetId: options.rootAssetId,
3751
+ strict: args.includes("--strict"),
3752
+ workspaceId: readOption(args, "--workspace") || readOption(args, "--workspace-id")
3753
+ });
3754
+ const out = readOption(args, "--out");
3755
+ if (out) {
3756
+ const outPath = resolve5(out);
3757
+ mkdirSync4(dirname3(outPath), { recursive: true });
3758
+ writeFileSync2(outPath, `${JSON.stringify(packet, null, 2)}
3759
+ `);
3760
+ }
3761
+ return packet;
3762
+ }
2579
3763
  if (command === "link-child") {
2580
3764
  if (!options.childAssetId) throw new Error("lineage link-child requires --child");
2581
3765
  return linkSelectedLineageChild(options.project, {
@@ -2636,6 +3820,66 @@ function runLineageDataCommand(command, args) {
2636
3820
  }
2637
3821
  throw new Error(`Unknown reroll command: ${subcommand}`);
2638
3822
  }
3823
+ if (command === "tasks") {
3824
+ const subcommand = positionalArgs(args)[0] || "";
3825
+ const taskId = readOption(args, "--task");
3826
+ if (subcommand === "list") {
3827
+ if (!options.rootAssetId) throw new Error("lineage tasks list requires --root");
3828
+ return listLineageTasks(options.project, options.rootAssetId);
3829
+ }
3830
+ if (subcommand === "inspect") {
3831
+ if (!taskId) throw new Error("lineage tasks inspect requires --task");
3832
+ return getLineageTask(options.project, taskId);
3833
+ }
3834
+ if (subcommand === "claim") {
3835
+ const agentName = readOption(args, "--agent-name");
3836
+ if (!taskId) throw new Error("lineage tasks claim requires --task");
3837
+ if (!agentName) throw new Error("lineage tasks claim requires --agent-name");
3838
+ return claimLineageTask(options.project, { taskId, agentName });
3839
+ }
3840
+ if (subcommand === "start") {
3841
+ if (!taskId) throw new Error("lineage tasks start requires --task");
3842
+ if (!options.claimToken) throw new Error("lineage tasks start requires --claim-token");
3843
+ return startLineageTask(options.project, { taskId, claimToken: options.claimToken });
3844
+ }
3845
+ if (subcommand === "comment") {
3846
+ const message = readOption(args, "--message");
3847
+ if (!taskId) throw new Error("lineage tasks comment requires --task");
3848
+ if (!message) throw new Error("lineage tasks comment requires --message");
3849
+ return addLineageTaskComment(options.project, {
3850
+ actor: readOption(args, "--actor") || "human",
3851
+ message,
3852
+ taskId
3853
+ });
3854
+ }
3855
+ if (subcommand === "cancel") {
3856
+ if (!taskId) throw new Error("lineage tasks cancel requires --task");
3857
+ return cancelLineageTask(options.project, {
3858
+ actor: readOption(args, "--actor") || "human",
3859
+ confirmWrite: options.confirmWrite,
3860
+ override: args.includes("--override"),
3861
+ taskId
3862
+ });
3863
+ }
3864
+ if (subcommand === "override") {
3865
+ const reason = readOption(args, "--reason");
3866
+ if (!taskId) throw new Error("lineage tasks override requires --task");
3867
+ if (!reason) throw new Error("lineage tasks override requires --reason");
3868
+ return overrideLineageTask(options.project, {
3869
+ actor: readOption(args, "--actor") || "human",
3870
+ instructions: readOption(args, "--instructions"),
3871
+ reason,
3872
+ taskId
3873
+ });
3874
+ }
3875
+ if (subcommand === "instructions") {
3876
+ const instructions = readOption(args, "--instructions");
3877
+ if (!taskId) throw new Error("lineage tasks instructions requires --task");
3878
+ if (instructions === void 0) throw new Error("lineage tasks instructions requires --instructions");
3879
+ return updateLineageTaskInstructions(options.project, { instructions, taskId });
3880
+ }
3881
+ throw new Error(`Unknown tasks command: ${subcommand}`);
3882
+ }
2639
3883
  throw new Error(`Unknown command: ${command}`);
2640
3884
  }
2641
3885
  function rerollRequestedBy(value) {
@@ -2665,9 +3909,17 @@ function printDataResult(command, result, json) {
2665
3909
  console.log(`Active: ${snapshot.active_asset_id}`);
2666
3910
  return;
2667
3911
  }
3912
+ if (command === "selection" && result && typeof result === "object" && "packet_id" in result) {
3913
+ const packet = result;
3914
+ console.log(`${packet.packet_id}: ${packet.selection?.count || packet.assets?.length || 0} selected asset(s)`);
3915
+ if (packet.workspace?.root_asset_id) console.log(`Root: ${packet.workspace.root_asset_id}`);
3916
+ for (const warning of packet.warnings || []) console.log(`Warning: ${warning}`);
3917
+ return;
3918
+ }
2668
3919
  if (command === "link-child" && result && typeof result === "object") {
2669
3920
  const link = result;
2670
3921
  console.log(link.message || `${link.dryRun ? "Dry run: " : ""}Link ${link.edge?.child_asset_id || "child"} from ${link.edge?.parent_asset_id || "parent"}`);
3922
+ if (link.warning) console.log(`Warning: ${link.warning}`);
2671
3923
  return;
2672
3924
  }
2673
3925
  if (command === "reroll" && result && typeof result === "object") {
@@ -2688,6 +3940,22 @@ function printDataResult(command, result, json) {
2688
3940
  return;
2689
3941
  }
2690
3942
  }
3943
+ if (command === "tasks" && result && typeof result === "object") {
3944
+ if ("tasks" in result) {
3945
+ const listed = result;
3946
+ console.log(`${listed.tasks.length} lineage task(s)`);
3947
+ for (const task of listed.tasks) console.log(`${task.id} ${task.task_type} ${task.status} ${task.target_asset_id}`);
3948
+ return;
3949
+ }
3950
+ if ("task" in result) {
3951
+ const mutation = result;
3952
+ const prefix = mutation.dryRun ? "Dry run: " : "";
3953
+ console.log(`${prefix}${mutation.task?.id || "task"} ${mutation.task?.task_type || "task"} ${mutation.task?.status || "unknown"} ${mutation.task?.target_asset_id || ""}`.trim());
3954
+ if ("claim_token" in mutation && typeof mutation.claim_token === "string") console.log(`Token: ${mutation.claim_token}`);
3955
+ if (mutation.events && mutation.events.length > 0) console.log(`Events: ${mutation.events.map((event) => event.event_type).join(", ")}`);
3956
+ return;
3957
+ }
3958
+ }
2691
3959
  console.log(String(result));
2692
3960
  }
2693
3961
  function runLineageAgentCommand(command, args) {
@@ -2817,7 +4085,7 @@ function start(config, args) {
2817
4085
  process.exit(1);
2818
4086
  }
2819
4087
  const serverPath = join6(packageRoot(), "dist", "server.js");
2820
- if (!existsSync5(serverPath)) {
4088
+ if (!existsSync6(serverPath)) {
2821
4089
  const message = `Missing bundled server at ${serverPath}. Run npm run build before using ${config.binName} start from a source checkout.`;
2822
4090
  if (options.json) console.error(JSON.stringify({ ok: false, error: message }, null, 2));
2823
4091
  else console.error(`${config.binName}: ${message}`);
@@ -2871,7 +4139,7 @@ function runLineageCli(config, args = process.argv.slice(2)) {
2871
4139
  start(config, normalizedArgs.slice(1));
2872
4140
  return;
2873
4141
  }
2874
- if (command === "next" || command === "brief" || command === "inspect" || command === "link-child" || command === "reroll") {
4142
+ if (command === "next" || command === "brief" || command === "inspect" || command === "selection" || command === "link-child" || command === "reroll" || command === "tasks") {
2875
4143
  const commandArgs = normalizedArgs.slice(1);
2876
4144
  const json2 = commandArgs.includes("--json");
2877
4145
  try {