@mean-weasel/lineage 0.1.6 → 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.
@@ -615,6 +615,10 @@ function lineageDb() {
615
615
  create index if not exists assets_project_source_seen on assets(project_id, source, last_seen_at);
616
616
  create index if not exists assets_project_checksum on assets(project_id, checksum_sha256);
617
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
+ );
618
622
  create table if not exists asset_edges (
619
623
  id text primary key,
620
624
  project_id text not null references projects(id),
@@ -920,12 +924,49 @@ function lineageDb() {
920
924
  );
921
925
  create index if not exists generation_job_receipts_job on generation_job_receipts(job_id, created_at);
922
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);
923
964
  create table if not exists agent_claims (
924
965
  id text primary key,
925
966
  token_hash text not null,
926
967
  project_id text not null references projects(id),
927
968
  channel text,
928
- 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')),
929
970
  target_id text not null,
930
971
  target_title text,
931
972
  agent_id text,
@@ -959,13 +1000,76 @@ function lineageDb() {
959
1000
  migrateAssetSelections(database);
960
1001
  dropLegacyAssetSelectionRootUnique(database);
961
1002
  ensureColumn(database, "asset_selections", "notes", "text");
1003
+ backfillLineageTasks(database);
962
1004
  ensureColumn(database, "asset_ledger_records", "first_seen_at", "text");
963
1005
  ensureColumn(database, "asset_ledger_records", "indexed_by_run_id", "text");
964
1006
  ensureColumn(database, "asset_ledger_sources", "first_seen_at", "text");
965
1007
  ensureColumn(database, "asset_ledger_sources", "indexed_by_run_id", "text");
966
1008
  ensureReviewStateValues(database);
1009
+ ensureAgentClaimScopeValues(database);
1010
+ ensureGenerationReceiptCheckValues(database);
967
1011
  return database;
968
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
+ }
969
1073
  function ensureColumn(database, table, column, definition) {
970
1074
  const rows = database.prepare(`pragma table_info(${table})`).all();
971
1075
  if (!rows.some((row) => row.name === column)) database.exec(`alter table ${table} add column ${column} ${definition}`);
@@ -1029,12 +1133,138 @@ function ensureReviewStateValues(database) {
1029
1133
  drop table asset_reviews_old;
1030
1134
  `);
1031
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
+ }
1032
1262
 
1033
1263
  // src/server/agentClaims.ts
1034
1264
  var defaultTtlSeconds = 20 * 60;
1035
1265
  var idleAfterSeconds = 5 * 60;
1036
1266
  var staleAfterSeconds = 15 * 60;
1037
- 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"]);
1038
1268
  var claimTokenPattern = /claim_[a-z0-9_-]+\.[A-Za-z0-9_-]+/g;
1039
1269
  var AgentClaimError = class extends Error {
1040
1270
  constructor(message, status = 400, code = "agent_claim_error", conflicts = []) {
@@ -1339,6 +1569,19 @@ function revokeAgentClaim(project, claimId, fields) {
1339
1569
  database.close();
1340
1570
  }
1341
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
+ }
1342
1585
  function transferAgentClaim(project, claimId, fields) {
1343
1586
  if (!fields.confirmWrite) throw new AgentClaimError("Transferring an agent claim requires confirmWrite=true.", 400, "confirm_write_required");
1344
1587
  const toAgentName = fields.toAgentName.trim();
@@ -1356,6 +1599,19 @@ function transferAgentClaim(project, claimId, fields) {
1356
1599
  database.close();
1357
1600
  }
1358
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
+ }
1359
1615
  function validateAgentClaimForWrite(fields) {
1360
1616
  if (fields.dangerLevel === "danger" && !fields.confirmWrite) {
1361
1617
  return denied("human_confirmation_required", "Dangerous write requires explicit human confirmation.");
@@ -1376,11 +1632,13 @@ function validateAgentClaimForWrite(fields) {
1376
1632
  if (!scopeAllowsWrite(claim, fields.scopeType, fields.targetId, fields.writeKind)) {
1377
1633
  return denied("claim_scope_mismatch", `Claim does not cover ${fields.scopeType} ${fields.targetId}.`, [claim]);
1378
1634
  }
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
- });
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
+ }
1384
1642
  return { ok: true, claim, warnings: [] };
1385
1643
  } finally {
1386
1644
  database.close();
@@ -1400,6 +1658,507 @@ function selectedRows(database, project, root) {
1400
1658
  `).all(project, root);
1401
1659
  }
1402
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
+
1403
2162
  // src/server/assetLineageWorkspaces.ts
1404
2163
  function lineageWorkspaceId(project, rootAssetId) {
1405
2164
  return `${project}:lineage-workspace:${rootAssetId}`;
@@ -1586,7 +2345,7 @@ function indexLineageAssets(project = defaultProject) {
1586
2345
  database.close();
1587
2346
  return { catalog: catalog.length, local: local.length, total: catalog.length + local.length, database: lineageDbPath() };
1588
2347
  }
1589
- function requireAsset(database, project, assetId) {
2348
+ function requireAsset2(database, project, assetId) {
1590
2349
  const row = database.prepare("select id from assets where project_id = ? and id = ?").get(project, assetId);
1591
2350
  if (!row) throw new LineageError(`Unknown indexed asset: ${assetId}`, 404);
1592
2351
  }
@@ -1606,7 +2365,7 @@ function rootFor(database, project, assetId) {
1606
2365
  return assetId;
1607
2366
  }
1608
2367
  function assertCanonicalRoot(database, project, root) {
1609
- requireAsset(database, project, root);
2368
+ requireAsset2(database, project, root);
1610
2369
  const canonicalRoot = rootFor(database, project, root);
1611
2370
  if (canonicalRoot !== root) throw new LineageError(`Asset ${root} is not a lineage root`, 400);
1612
2371
  }
@@ -1643,7 +2402,7 @@ function lineageWriteClaimContext(database, project, assetId) {
1643
2402
  function getLineageWriteClaimContext(project, assetId) {
1644
2403
  const database = lineageDb();
1645
2404
  try {
1646
- requireAsset(database, project, assetId);
2405
+ requireAsset2(database, project, assetId);
1647
2406
  return lineageWriteClaimContext(database, project, assetId);
1648
2407
  } finally {
1649
2408
  database.close();
@@ -1655,12 +2414,12 @@ function latestSelectedRoot(database, project) {
1655
2414
  }
1656
2415
  function resolveRoot(database, project, rootAssetId) {
1657
2416
  if (rootAssetId) {
1658
- requireAsset(database, project, rootAssetId);
2417
+ requireAsset2(database, project, rootAssetId);
1659
2418
  return rootAssetId;
1660
2419
  }
1661
2420
  const root = activeLineageWorkspaceRoot(project) || latestSelectedRoot(database, project);
1662
2421
  if (!root) throw new LineageError("Lineage command requires --root unless a project selection exists");
1663
- requireAsset(database, project, root);
2422
+ requireAsset2(database, project, root);
1664
2423
  return root;
1665
2424
  }
1666
2425
  function edgeId(project, parent, child) {
@@ -1685,6 +2444,33 @@ function rerollRequestFrom(row) {
1685
2444
  resolved_at: rowString(row.resolved_at)
1686
2445
  };
1687
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
+ }
1688
2474
  function attemptFrom(row) {
1689
2475
  return {
1690
2476
  id: String(row.id),
@@ -1724,7 +2510,7 @@ function withImplicitAttempt(physicalAttempts, row) {
1724
2510
  }
1725
2511
  function assertNodeInRoot(database, project, root, node) {
1726
2512
  assertCanonicalRoot(database, project, root);
1727
- requireAsset(database, project, node);
2513
+ requireAsset2(database, project, node);
1728
2514
  const nodeRoot = rootFor(database, project, node);
1729
2515
  if (nodeRoot !== root) throw new LineageError(`Asset ${node} is not in lineage rooted at ${root}`);
1730
2516
  }
@@ -1745,8 +2531,8 @@ function localPreviewUrl(project, localPath) {
1745
2531
  }
1746
2532
  function linkLineageAssets(project, fields) {
1747
2533
  const database = lineageDb();
1748
- requireAsset(database, project, fields.parentAssetId);
1749
- requireAsset(database, project, fields.childAssetId);
2534
+ requireAsset2(database, project, fields.parentAssetId);
2535
+ requireAsset2(database, project, fields.childAssetId);
1750
2536
  if (fields.parentAssetId === fields.childAssetId) throw new LineageError("Lineage link cannot point to itself");
1751
2537
  const claimContext = lineageWriteClaimContext(database, project, fields.parentAssetId);
1752
2538
  try {
@@ -1797,10 +2583,17 @@ function descendants(database, project, root) {
1797
2583
  }
1798
2584
  function getLineageSnapshot(project, assetId) {
1799
2585
  const database = lineageDb();
1800
- requireAsset(database, project, assetId);
2586
+ requireAsset2(database, project, assetId);
1801
2587
  const root = explicitWorkspaceRoot(database, project, assetId) || rootFor(database, project, assetId);
1802
2588
  const edges = descendants(database, project, root);
1803
- 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
+ ])];
1804
2597
  const placeholders = ids.map(() => "?").join(",");
1805
2598
  const rows = database.prepare(`
1806
2599
  select a.id asset_id, a.project_id project, a.source, a.title, a.media_type, a.status, a.channel, a.campaign,
@@ -1815,12 +2608,12 @@ function getLineageSnapshot(project, assetId) {
1815
2608
  for (const attempt of attemptRows.map(attemptFrom)) {
1816
2609
  attemptsByNode.set(attempt.node_asset_id, [...attemptsByNode.get(attempt.node_asset_id) || [], attempt]);
1817
2610
  }
2611
+ const lineageTasksByNode = tasksByTarget(tasks);
1818
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) : [];
1819
2613
  const rerollsByNode = new Map(rerollRows.map((row) => {
1820
2614
  const request = rerollRequestFrom(row);
1821
- return [request.node_asset_id, request];
2615
+ return [request.node_asset_id, withRerollTask(request, lineageTasksByNode.get(request.node_asset_id)?.reroll)];
1822
2616
  }));
1823
- const selected = selectedRows(database, project, root);
1824
2617
  const childIds = new Set(edges.map((edge) => edge.parent_asset_id));
1825
2618
  const selectedIds = new Set(selected.map((row) => row.asset_id));
1826
2619
  const selections = selected.map((row) => ({
@@ -1837,11 +2630,13 @@ function getLineageSnapshot(project, assetId) {
1837
2630
  const attempts = withImplicitAttempt(attemptsByNode.get(row.asset_id) || [], row);
1838
2631
  const currentAttempt = attempts.find((attempt) => attempt.is_current) || attempts[0];
1839
2632
  const previewPath = currentAttempt?.file_path || row.local_path;
2633
+ const lineageTasks = lineageTasksByNode.get(row.asset_id);
1840
2634
  return {
1841
2635
  ...node,
1842
2636
  attempt_count: attempts.length,
1843
2637
  current_attempt: currentAttempt,
1844
2638
  is_latest: !childIds.has(row.asset_id),
2639
+ lineage_tasks: lineageTasks && Object.keys(lineageTasks).length > 0 ? lineageTasks : void 0,
1845
2640
  position,
1846
2641
  preview_url: canPreviewLocally(row.media_type, previewPath) ? localPreviewUrl(project, previewPath) : void 0,
1847
2642
  reroll_request: rerollsByNode.get(row.asset_id),
@@ -1857,6 +2652,7 @@ function getLineageSnapshot(project, assetId) {
1857
2652
  selected: selections.map((row) => row.asset_id),
1858
2653
  selection,
1859
2654
  selections,
2655
+ tasks,
1860
2656
  latest: nodes.filter((node) => node.is_latest).map((node) => node.asset_id),
1861
2657
  nodes,
1862
2658
  edges,
@@ -1868,7 +2664,25 @@ function getLineageNextAsset(project, rootAssetId) {
1868
2664
  const root = resolveRoot(database, project, rootAssetId);
1869
2665
  database.close();
1870
2666
  const snapshot = getLineageSnapshot(project, root);
1871
- 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;
1872
2686
  const latestNodes = snapshot.nodes.filter((node) => snapshot.latest.includes(node.asset_id));
1873
2687
  const warnings = [];
1874
2688
  for (const selectedNode of selectedNodes) {
@@ -1886,9 +2700,9 @@ function getLineageNextAsset(project, rootAssetId) {
1886
2700
  next_asset: selectedNodes[0],
1887
2701
  next_assets: selectedNodes,
1888
2702
  latest: snapshot.latest,
1889
- selected: snapshot.selected,
1890
- selection: snapshot.selection,
1891
- selections: snapshot.selections,
2703
+ selected: selectedIds,
2704
+ selection: selectedSelection,
2705
+ selections: selectedSelections,
1892
2706
  candidates: latestNodes,
1893
2707
  warnings,
1894
2708
  fetchedAt: nowIso()
@@ -1905,9 +2719,9 @@ function getLineageNextAsset(project, rootAssetId) {
1905
2719
  next_asset: latestNodes[0],
1906
2720
  next_assets: [latestNodes[0]],
1907
2721
  latest: snapshot.latest,
1908
- selected: snapshot.selected,
1909
- selection: snapshot.selection,
1910
- selections: snapshot.selections,
2722
+ selected: selectedIds,
2723
+ selection: selectedSelection,
2724
+ selections: selectedSelections,
1911
2725
  candidates: latestNodes,
1912
2726
  warnings,
1913
2727
  fetchedAt: nowIso()
@@ -1923,9 +2737,9 @@ function getLineageNextAsset(project, rootAssetId) {
1923
2737
  next_asset: null,
1924
2738
  next_assets: [],
1925
2739
  latest: snapshot.latest,
1926
- selected: snapshot.selected,
1927
- selection: snapshot.selection,
1928
- selections: snapshot.selections,
2740
+ selected: selectedIds,
2741
+ selection: selectedSelection,
2742
+ selections: selectedSelections,
1929
2743
  candidates: latestNodes,
1930
2744
  warnings,
1931
2745
  fetchedAt: nowIso()
@@ -1935,12 +2749,23 @@ function listLineageRerollRequests(project, rootAssetId) {
1935
2749
  const database = lineageDb();
1936
2750
  try {
1937
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]));
1938
2755
  const rows = database.prepare(`
1939
2756
  select * from asset_reroll_requests
1940
2757
  where project_id = ? and root_asset_id = ? and status = 'pending'
1941
2758
  order by created_at, id
1942
2759
  `).all(project, rootAssetId);
1943
- return { project, root_asset_id: rootAssetId, requests: rows.map(rerollRequestFrom), fetchedAt: nowIso() };
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() };
1944
2769
  } finally {
1945
2770
  database.close();
1946
2771
  }
@@ -1949,7 +2774,7 @@ function recordLineageRerollAttempt(project, fields) {
1949
2774
  const database = lineageDb();
1950
2775
  try {
1951
2776
  assertNodeInRoot(database, project, fields.rootAssetId, fields.nodeAssetId);
1952
- requireAsset(database, project, fields.assetId);
2777
+ requireAsset2(database, project, fields.assetId);
1953
2778
  assertAttemptAssetNotVisibleLineageNode(database, project, fields.rootAssetId, fields.assetId);
1954
2779
  const timestamp = nowIso();
1955
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);
@@ -2038,6 +2863,13 @@ function markLineageRerollRequest(project, fields) {
2038
2863
  created_at: timestamp
2039
2864
  };
2040
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
+ });
2041
2873
  if (existing) {
2042
2874
  database.prepare(`
2043
2875
  update asset_reroll_requests
@@ -2057,7 +2889,12 @@ function markLineageRerollRequest(project, fields) {
2057
2889
  review_state = excluded.review_state, reviewed_at = excluded.reviewed_at,
2058
2890
  ignored_at = excluded.ignored_at, notes = coalesce(excluded.notes, asset_reviews.notes), updated_at = excluded.updated_at
2059
2891
  `).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)) };
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
+ };
2061
2898
  } finally {
2062
2899
  database.close();
2063
2900
  }
@@ -2075,12 +2912,23 @@ function clearLineageRerollRequest(project, fields) {
2075
2912
  const timestamp = nowIso();
2076
2913
  const request = { ...rerollRequestFrom(existing), status: "cancelled", resolved_at: timestamp };
2077
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;
2078
2921
  database.prepare(`
2079
2922
  update asset_reroll_requests
2080
2923
  set status = 'cancelled', resolved_at = ?
2081
2924
  where id = ?
2082
2925
  `).run(timestamp, request.id);
2083
- return { ok: true, request };
2926
+ return {
2927
+ ok: true,
2928
+ request: withRerollTask(request, cancelledTask),
2929
+ task_id: cancelledTask?.id,
2930
+ task: cancelledTask
2931
+ };
2084
2932
  } finally {
2085
2933
  database.close();
2086
2934
  }
@@ -2097,6 +2945,9 @@ function lineageCommand(command, project, rootAssetId) {
2097
2945
  function linkChildCommand(project, rootAssetId) {
2098
2946
  return `${publicPackageCommand} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write --db ${shellQuote(lineageDbPath())} --json`;
2099
2947
  }
2948
+ function rerollImportGuidance(rootAssetId, targetAssetId) {
2949
+ return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
2950
+ }
2100
2951
  function getLineageBrief(project, rootAssetId) {
2101
2952
  const next = getLineageNextAsset(project, rootAssetId);
2102
2953
  const assets = next.next_assets;
@@ -2143,6 +2994,14 @@ function getLineageBrief(project, rootAssetId) {
2143
2994
  function linkSelectedLineageChild(project, fields) {
2144
2995
  const next = getLineageNextAsset(project, fields.rootAssetId);
2145
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
+ }
2146
3005
  if (fields.confirmWrite) {
2147
3006
  const claimContext = getLineageWriteClaimContext(project, next.next_asset.asset_id);
2148
3007
  const validation = validateAgentClaimForWrite({
@@ -2169,7 +3028,11 @@ function linkSelectedLineageChild(project, fields) {
2169
3028
  parent_asset_id: next.next_asset.asset_id,
2170
3029
  child_asset_id: fields.childAssetId,
2171
3030
  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
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
2173
3036
  };
2174
3037
  }
2175
3038
 
@@ -2359,7 +3222,7 @@ function planImageReroll(project = defaultProject, fields) {
2359
3222
  created_at: timestamp
2360
3223
  }]
2361
3224
  };
2362
- if (fields.dryRun) return { ok: true, command: "generate image plan", project, dryRun: true, wouldWrite: true, job: preview };
3225
+ if (fields.dryRun) return { ok: true, command: "reroll plan", project, dryRun: true, wouldWrite: true, job: preview };
2363
3226
  const database = lineageDb();
2364
3227
  try {
2365
3228
  database.exec("BEGIN IMMEDIATE");
@@ -2377,7 +3240,7 @@ function planImageReroll(project = defaultProject, fields) {
2377
3240
  database.exec("ROLLBACK");
2378
3241
  throw error;
2379
3242
  }
2380
- return { ok: true, command: "generate image plan", project, job: loadGenerationJob(database, project, id) };
3243
+ return { ok: true, command: "reroll plan", project, job: loadGenerationJob(database, project, id) };
2381
3244
  } finally {
2382
3245
  database.close();
2383
3246
  }
@@ -2456,8 +3319,18 @@ function importImageRerollOutput(project = defaultProject, fields) {
2456
3319
  checksumSha256: resolved.checksum,
2457
3320
  confirmWrite: true
2458
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
+ }
2459
3332
  const importedJob = loadGenerationJob(writeDb, project, fields.jobId);
2460
- return { ok: true, command: "generate image import", project, job: importedJob, imported: importedJob.outputs };
3333
+ return { ok: true, command: "reroll import", project, job: importedJob, imported: importedJob.outputs };
2461
3334
  } finally {
2462
3335
  writeDb.close();
2463
3336
  }
@@ -2508,8 +3381,8 @@ function resolveStartOptions(config, args) {
2508
3381
  port
2509
3382
  };
2510
3383
  }
2511
- function printHelp(config) {
2512
- console.log(`${config.binName} ${packageVersion()}
3384
+ function formatLineageHelp(config) {
3385
+ return `${config.binName} ${packageVersion()}
2513
3386
 
2514
3387
  Usage:
2515
3388
  ${config.binName} start [--port <port>] [--host <host>] [--db <path>] [--open] [--json]
@@ -2522,6 +3395,14 @@ Usage:
2522
3395
  ${config.binName} reroll cancel --root <asset-id> --target <asset-id> [--project <project>] [--confirm-write] [--db <path>] [--json]
2523
3396
  ${config.binName} reroll plan --root <asset-id> --target <asset-id> --prompt <text> [--project <project>] [--db <path>] [--json]
2524
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]
2525
3406
  ${config.binName} agent claim --project <project> --scope <scope> --target <target-id> --agent-name <name> [--channel <channel>] [--ttl 20m] [--json]
2526
3407
  ${config.binName} agent graph --root <asset-id> [--project <project>] [--db <path>] [--json]
2527
3408
  ${config.binName} agent status [--project <project>] [--json]
@@ -2533,7 +3414,14 @@ Usage:
2533
3414
  ${config.binName} --help
2534
3415
  ${config.binName} --version
2535
3416
 
2536
- ${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));
2537
3425
  }
2538
3426
  function openBrowser(url) {
2539
3427
  const command = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
@@ -2636,6 +3524,66 @@ function runLineageDataCommand(command, args) {
2636
3524
  }
2637
3525
  throw new Error(`Unknown reroll command: ${subcommand}`);
2638
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
+ }
2639
3587
  throw new Error(`Unknown command: ${command}`);
2640
3588
  }
2641
3589
  function rerollRequestedBy(value) {
@@ -2668,6 +3616,7 @@ function printDataResult(command, result, json) {
2668
3616
  if (command === "link-child" && result && typeof result === "object") {
2669
3617
  const link = result;
2670
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}`);
2671
3620
  return;
2672
3621
  }
2673
3622
  if (command === "reroll" && result && typeof result === "object") {
@@ -2688,6 +3637,22 @@ function printDataResult(command, result, json) {
2688
3637
  return;
2689
3638
  }
2690
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
+ }
2691
3656
  console.log(String(result));
2692
3657
  }
2693
3658
  function runLineageAgentCommand(command, args) {
@@ -2871,7 +3836,7 @@ function runLineageCli(config, args = process.argv.slice(2)) {
2871
3836
  start(config, normalizedArgs.slice(1));
2872
3837
  return;
2873
3838
  }
2874
- if (command === "next" || command === "brief" || command === "inspect" || command === "link-child" || command === "reroll") {
3839
+ if (command === "next" || command === "brief" || command === "inspect" || command === "link-child" || command === "reroll" || command === "tasks") {
2875
3840
  const commandArgs = normalizedArgs.slice(1);
2876
3841
  const json2 = commandArgs.includes("--json");
2877
3842
  try {