@mean-weasel/lineage 0.1.11 → 0.1.12

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,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/lineageCli.ts
4
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
5
- import { homedir, platform } from "node:os";
6
- import { dirname as dirname3, join as join7, resolve as resolve5 } from "node:path";
4
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "node:fs";
5
+ import { homedir as homedir2, platform as platform2 } from "node:os";
6
+ import { dirname as dirname5, join as join10, resolve as resolve8 } from "node:path";
7
7
  import { spawn } from "node:child_process";
8
8
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9
9
 
10
10
  // src/server/agentClaims.ts
11
- import { createHash as createHash2, randomBytes, timingSafeEqual } from "node:crypto";
11
+ import { createHash as createHash3, randomBytes, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
12
12
 
13
13
  // src/server/assetLineageDb.ts
14
- import { createRequire } from "node:module";
15
- import { mkdirSync as mkdirSync3 } from "node:fs";
16
- import { join as join4 } from "node:path";
14
+ import { createRequire as createRequire2 } from "node:module";
15
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "node:fs";
16
+ import { join as join6, resolve as resolve5 } from "node:path";
17
17
 
18
18
  // src/server/assetCore.ts
19
19
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "node:fs";
@@ -587,20 +587,580 @@ function validateProject(project = defaultProject) {
587
587
  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 };
588
588
  }
589
589
 
590
- // src/server/assetLineageDb.ts
590
+ // src/server/lineageProfiles.ts
591
+ import { createRequire } from "node:module";
592
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
593
+ import { homedir, platform } from "node:os";
594
+ import { dirname as dirname3, isAbsolute, join as join4, resolve as resolve3 } from "node:path";
595
+
596
+ // src/shared/lineageProfileTypes.ts
597
+ var lineageProfileSchemaVersion = "lineage.profile.v1";
598
+ var lineageProfileDoctorSchemaVersion = "lineage.profile_doctor.v1";
599
+
600
+ // src/server/lineageProfiles.ts
591
601
  var require2 = createRequire(import.meta.url);
602
+ var profileIdPattern = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
603
+ function lineageDataRoot() {
604
+ if (process.env.LINEAGE_HOME) return resolve3(process.env.LINEAGE_HOME);
605
+ if (platform() === "darwin") return join4(homedir(), "Library", "Application Support", "Lineage");
606
+ if (platform() === "win32") return join4(process.env.APPDATA || join4(homedir(), "AppData", "Roaming"), "Lineage");
607
+ return join4(process.env.XDG_DATA_HOME || join4(homedir(), ".local", "share"), "lineage");
608
+ }
609
+ function lineageProfileRoot() {
610
+ return resolve3(process.env.LINEAGE_PROFILE_ROOT || join4(lineageDataRoot(), "profiles"));
611
+ }
612
+ function environmentChannel(environment) {
613
+ if (environment === "production") return "stable";
614
+ if (environment === "preview") return "preview";
615
+ return "dev";
616
+ }
617
+ function channelEnvironment(channel) {
618
+ if (channel === "stable") return "production";
619
+ if (channel === "preview") return "preview";
620
+ return "development";
621
+ }
622
+ function profileManifestPath(selector) {
623
+ const value = selector.trim();
624
+ if (!value) throw new Error("Profile selector must not be empty");
625
+ const looksLikePath = isAbsolute(value) || value.includes("/") || value.includes("\\") || value.endsWith(".json");
626
+ if (looksLikePath) return { manifestPath: resolve3(value) };
627
+ if (!profileIdPattern.test(value)) throw new Error(`Invalid profile ID: ${value}`);
628
+ return { manifestPath: join4(lineageProfileRoot(), value, "profile.json"), namedProfileId: value };
629
+ }
630
+ function requiredString(record, key) {
631
+ const value = record[key];
632
+ if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest requires a non-empty ${key}`);
633
+ return value.trim();
634
+ }
635
+ function optionalString(record, key) {
636
+ const value = record[key];
637
+ if (value === void 0) return void 0;
638
+ if (typeof value !== "string" || !value.trim()) throw new Error(`Profile manifest ${key} must be a non-empty string`);
639
+ return value.trim();
640
+ }
641
+ function validateEnvironment(value) {
642
+ if (value === "production" || value === "preview" || value === "development") return value;
643
+ throw new Error(`Invalid profile environment: ${value}`);
644
+ }
645
+ function validateChannel(value) {
646
+ if (value === void 0) return void 0;
647
+ if (value === "stable" || value === "preview" || value === "dev") return value;
648
+ throw new Error(`Invalid expected runtime channel: ${String(value)}`);
649
+ }
650
+ function resolveManifestPath(manifestPath, value) {
651
+ return resolve3(dirname3(manifestPath), value);
652
+ }
653
+ function validateServiceOrigin(value) {
654
+ let parsed;
655
+ try {
656
+ parsed = new URL(value);
657
+ } catch {
658
+ throw new Error(`Invalid profile service_origin: ${value}`);
659
+ }
660
+ if (parsed.protocol !== "http:") throw new Error("Profile service_origin must use http for the local Lineage service");
661
+ if (!parsed.port) throw new Error("Profile service_origin must include an explicit port");
662
+ if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash) {
663
+ throw new Error("Profile service_origin must contain only scheme, host, and port");
664
+ }
665
+ const hostname = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
666
+ const isLocal = hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
667
+ if (!isLocal) throw new Error("Profile service_origin must use a loopback or localhost host");
668
+ return parsed.origin;
669
+ }
670
+ function resolveLineageProfile(selector) {
671
+ const { manifestPath, namedProfileId } = profileManifestPath(selector);
672
+ if (!existsSync4(manifestPath)) throw new Error(`Profile manifest does not exist: ${manifestPath}`);
673
+ let raw;
674
+ try {
675
+ raw = JSON.parse(readFileSync3(manifestPath, "utf8"));
676
+ } catch (error) {
677
+ throw new Error(`Could not parse profile manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
678
+ }
679
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Profile manifest must be a JSON object");
680
+ const record = raw;
681
+ const schemaVersion = requiredString(record, "schema_version");
682
+ if (schemaVersion !== lineageProfileSchemaVersion) throw new Error(`Unsupported profile schema_version: ${schemaVersion}`);
683
+ const profileId = requiredString(record, "profile_id");
684
+ if (!profileIdPattern.test(profileId)) throw new Error(`Invalid profile ID: ${profileId}`);
685
+ if (namedProfileId && namedProfileId !== profileId) {
686
+ throw new Error(`Named profile ${namedProfileId} does not match immutable manifest profile_id ${profileId}`);
687
+ }
688
+ const expectedRaw = record.expected_runtime;
689
+ if (expectedRaw !== void 0 && (!expectedRaw || typeof expectedRaw !== "object" || Array.isArray(expectedRaw))) {
690
+ throw new Error("Profile expected_runtime must be an object");
691
+ }
692
+ const expected = expectedRaw;
693
+ const expectedGitSha = expected ? optionalString(expected, "git_sha") : void 0;
694
+ const expectedVersion = expected ? optionalString(expected, "version") : void 0;
695
+ const migrationsRaw = record.required_schema_migrations;
696
+ if (migrationsRaw !== void 0 && (!Array.isArray(migrationsRaw) || migrationsRaw.some((value) => typeof value !== "string" || !value.trim()))) {
697
+ throw new Error("Profile required_schema_migrations must be an array of non-empty strings");
698
+ }
699
+ const environment = validateEnvironment(requiredString(record, "environment"));
700
+ const expectedChannel = validateChannel(expected?.channel);
701
+ if (expectedChannel && expectedChannel !== environmentChannel(environment)) {
702
+ throw new Error(`Profile environment ${environment} conflicts with expected runtime channel ${expectedChannel}`);
703
+ }
704
+ const manifest = {
705
+ asset_root: resolveManifestPath(manifestPath, requiredString(record, "asset_root")),
706
+ database_path: resolveManifestPath(manifestPath, requiredString(record, "database_path")),
707
+ environment,
708
+ ...expected ? {
709
+ expected_runtime: {
710
+ ...expectedChannel ? { channel: expectedChannel } : {},
711
+ ...expectedGitSha ? { git_sha: expectedGitSha } : {},
712
+ ...expectedVersion ? { version: expectedVersion } : {}
713
+ }
714
+ } : {},
715
+ profile_id: profileId,
716
+ ...migrationsRaw ? { required_schema_migrations: migrationsRaw.map((value) => value.trim()) } : {},
717
+ schema_version: lineageProfileSchemaVersion,
718
+ service_origin: validateServiceOrigin(requiredString(record, "service_origin"))
719
+ };
720
+ return { ...manifest, manifest_path: manifestPath };
721
+ }
722
+ function bindLineageProfileDatabase(profile, confirmWrite) {
723
+ const result = {
724
+ database_path: profile.database_path,
725
+ environment: profile.environment,
726
+ ok: true,
727
+ profile_id: profile.profile_id,
728
+ schema_version: "lineage.profile_bind.v1"
729
+ };
730
+ if (!confirmWrite) return { ...result, dryRun: true };
731
+ mkdirSync3(dirname3(profile.database_path), { recursive: true });
732
+ const { DatabaseSync } = require2("node:sqlite");
733
+ const database = new DatabaseSync(profile.database_path);
734
+ try {
735
+ database.exec("BEGIN IMMEDIATE");
736
+ try {
737
+ database.exec(`
738
+ create table if not exists lineage_profile_identity (
739
+ profile_id text primary key,
740
+ environment text not null,
741
+ bound_at text not null
742
+ )
743
+ `);
744
+ const rows = database.prepare("select profile_id, environment, bound_at from lineage_profile_identity").all();
745
+ if (rows.length > 0) {
746
+ if (rows.length !== 1 || rows[0].profile_id !== profile.profile_id || rows[0].environment !== profile.environment) {
747
+ throw new Error(`Database ${profile.database_path} is already bound to a different Lineage profile identity`);
748
+ }
749
+ database.exec("COMMIT");
750
+ return { ...result, already_bound: true, bound_at: rows[0].bound_at };
751
+ }
752
+ const boundAt = (/* @__PURE__ */ new Date()).toISOString();
753
+ database.prepare("insert into lineage_profile_identity (profile_id, environment, bound_at) values (?, ?, ?)").run(profile.profile_id, profile.environment, boundAt);
754
+ database.exec("COMMIT");
755
+ return { ...result, bound_at: boundAt };
756
+ } catch (error) {
757
+ database.exec("ROLLBACK");
758
+ throw error;
759
+ }
760
+ } finally {
761
+ database.close();
762
+ }
763
+ }
764
+ function assertProfileChannel(profile, channel) {
765
+ const requiredChannel = environmentChannel(profile.environment);
766
+ if (channel === requiredChannel) return;
767
+ if (profile.environment === "production") {
768
+ throw new Error(`Refusing to open production profile ${profile.profile_id} from ${channel} code; use the stable channel`);
769
+ }
770
+ throw new Error(`Profile ${profile.profile_id} requires the ${requiredChannel} channel, not ${channel}`);
771
+ }
772
+ function tableExists(database, table) {
773
+ return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
774
+ }
775
+ function gitShasMatch(expected, actual) {
776
+ if (!actual) return false;
777
+ const normalizedExpected = expected.toLowerCase();
778
+ const normalizedActual = actual.toLowerCase();
779
+ const shorterLength = Math.min(normalizedExpected.length, normalizedActual.length);
780
+ return shorterLength >= 12 && normalizedExpected.slice(0, shorterLength) === normalizedActual.slice(0, shorterLength);
781
+ }
782
+ function inspectDatabase(profile) {
783
+ const result = {
784
+ exists: existsSync4(profile.database_path),
785
+ migration_keys: [],
786
+ path: profile.database_path
787
+ };
788
+ if (!result.exists) return result;
789
+ try {
790
+ const { DatabaseSync } = require2("node:sqlite");
791
+ const database = new DatabaseSync(profile.database_path, { readOnly: true });
792
+ try {
793
+ if (tableExists(database, "lineage_profile_identity")) {
794
+ const rows = database.prepare("select profile_id, environment, bound_at from lineage_profile_identity").all();
795
+ if (rows.length === 1) {
796
+ const identity = {
797
+ bound_at: typeof rows[0].bound_at === "string" ? rows[0].bound_at : void 0,
798
+ environment: String(rows[0].environment),
799
+ profile_id: String(rows[0].profile_id)
800
+ };
801
+ result.identity = identity;
802
+ } else {
803
+ result.error = `Expected exactly one lineage_profile_identity row, found ${rows.length}`;
804
+ }
805
+ }
806
+ if (tableExists(database, "lineage_schema_migrations")) {
807
+ result.migration_keys = database.prepare("select key from lineage_schema_migrations order by key").all().map((row) => row.key);
808
+ }
809
+ } finally {
810
+ database.close();
811
+ }
812
+ } catch (error) {
813
+ result.error = error instanceof Error ? error.message : String(error);
814
+ }
815
+ return result;
816
+ }
817
+ function doctorLineageProfile(selector, runtime) {
818
+ const checks = [];
819
+ const result = {
820
+ checks,
821
+ ok: false,
822
+ runtime: { channel: runtime.channel, git_sha: runtime.gitSha, version: runtime.version },
823
+ schema_version: lineageProfileDoctorSchemaVersion
824
+ };
825
+ let profile;
826
+ try {
827
+ profile = resolveLineageProfile(selector);
828
+ result.profile = profile;
829
+ result.manifest_path = profile.manifest_path;
830
+ checks.push({ id: "manifest", message: `Loaded ${profile.profile_id}`, status: "pass" });
831
+ } catch (error) {
832
+ checks.push({ id: "manifest", message: error instanceof Error ? error.message : String(error), status: "fail" });
833
+ return result;
834
+ }
835
+ try {
836
+ assertProfileChannel(profile, runtime.channel);
837
+ checks.push({ id: "runtime_channel", message: `${runtime.channel} code matches ${profile.environment}`, status: "pass" });
838
+ } catch (error) {
839
+ checks.push({ id: "runtime_channel", message: error instanceof Error ? error.message : String(error), status: "fail" });
840
+ }
841
+ if (profile.expected_runtime?.version && profile.expected_runtime.version !== runtime.version) {
842
+ checks.push({ id: "runtime_version", message: `Expected version ${profile.expected_runtime.version}, found ${runtime.version}`, status: "fail" });
843
+ } else {
844
+ checks.push({ id: "runtime_version", message: `Runtime version ${runtime.version}`, status: "pass" });
845
+ }
846
+ if (profile.expected_runtime?.git_sha && !gitShasMatch(profile.expected_runtime.git_sha, runtime.gitSha)) {
847
+ checks.push({ id: "runtime_git_sha", message: `Expected Git SHA ${profile.expected_runtime.git_sha}, found ${runtime.gitSha || "unavailable"}`, status: "fail" });
848
+ } else if (profile.expected_runtime?.git_sha) {
849
+ checks.push({ id: "runtime_git_sha", message: `Git SHA ${runtime.gitSha}`, status: "pass" });
850
+ }
851
+ const assetExists = existsSync4(profile.asset_root);
852
+ const assetIsDirectory = assetExists && statSync3(profile.asset_root).isDirectory();
853
+ result.asset_root = { exists: assetExists, is_directory: assetIsDirectory, path: profile.asset_root };
854
+ checks.push({
855
+ id: "asset_root",
856
+ message: assetIsDirectory ? `Asset root exists: ${profile.asset_root}` : `Asset root is missing or not a directory: ${profile.asset_root}`,
857
+ status: assetIsDirectory ? "pass" : "fail"
858
+ });
859
+ const database = inspectDatabase(profile);
860
+ result.database = database;
861
+ checks.push({
862
+ id: "database_exists",
863
+ message: database.exists ? `Database exists: ${profile.database_path}` : `Database does not exist: ${profile.database_path}`,
864
+ status: database.exists ? "pass" : "fail"
865
+ });
866
+ if (database.error) checks.push({ id: "database_read", message: database.error, status: "fail" });
867
+ if (!database.identity) {
868
+ checks.push({ id: "database_identity", message: "Database is not bound to a Lineage profile", status: "fail" });
869
+ } else if (database.identity.profile_id !== profile.profile_id || database.identity.environment !== profile.environment) {
870
+ checks.push({
871
+ id: "database_identity",
872
+ message: `Database identity ${database.identity.profile_id}/${database.identity.environment} does not match ${profile.profile_id}/${profile.environment}`,
873
+ status: "fail"
874
+ });
875
+ } else {
876
+ checks.push({ id: "database_identity", message: `Database is bound to ${profile.profile_id}`, status: "pass" });
877
+ }
878
+ const missingMigrations = (profile.required_schema_migrations || []).filter((key) => !database.migration_keys.includes(key));
879
+ checks.push({
880
+ id: "database_schema",
881
+ message: missingMigrations.length ? `Missing required schema migrations: ${missingMigrations.join(", ")}` : `${database.migration_keys.length} schema migration marker(s) available`,
882
+ status: missingMigrations.length ? "fail" : "pass"
883
+ });
884
+ result.ok = checks.every((check) => check.status !== "fail");
885
+ return result;
886
+ }
887
+ function runtimeProfileIdentity(channel) {
888
+ const selector = process.env.LINEAGE_PROFILE;
889
+ const id = process.env.LINEAGE_PROFILE_ID;
890
+ const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
891
+ if (selector && id && environment) {
892
+ return {
893
+ bound: true,
894
+ environment,
895
+ id,
896
+ manifest_path: process.env.LINEAGE_PROFILE_MANIFEST,
897
+ service_origin: process.env.LINEAGE_PROFILE_SERVICE_ORIGIN
898
+ };
899
+ }
900
+ return {
901
+ bound: false,
902
+ environment: channelEnvironment(channel),
903
+ id: "legacy-unbound",
904
+ warning: id || environment || process.env.LINEAGE_PROFILE_MANIFEST ? "Invalid unbound runtime: derived profile identity was supplied without a resolved LINEAGE_PROFILE selector." : "Legacy unbound runtime: database and asset paths are not protected by a named profile."
905
+ };
906
+ }
907
+ function assertUnselectedDatabaseIsUnbound(runtime) {
908
+ if (runtime.database.exists && runtime.database.error) {
909
+ throw new Error(
910
+ `Database ${runtime.database.path} identity could not be verified: ${runtime.database.error}; refusing unselected access`
911
+ );
912
+ }
913
+ if (runtime.schema.profile_identity_rows !== void 0 && runtime.schema.profile_identity_rows !== 1) {
914
+ throw new Error(
915
+ `Database ${runtime.database.path} has invalid Lineage profile identity row count ${runtime.schema.profile_identity_rows}; refusing unselected access`
916
+ );
917
+ }
918
+ if (!runtime.schema.profile_id) return;
919
+ const environment = runtime.schema.profile_environment || "unknown";
920
+ throw new Error(
921
+ `Database ${runtime.database.path} is bound to Lineage profile ${runtime.schema.profile_id}/${environment}; select that profile with --profile`
922
+ );
923
+ }
924
+
925
+ // src/server/profileWriterLease.ts
926
+ import { createHash as createHash2, randomUUID, timingSafeEqual } from "node:crypto";
927
+ import { existsSync as existsSync5, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync4, realpathSync, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
928
+ import { basename as basename3, dirname as dirname4, join as join5, resolve as resolve4 } from "node:path";
929
+ var writerLeaseSchemaVersion = "lineage.profile_writer_lease.v1";
930
+ var ownerFileName = "owner.json";
931
+ var ProfileWriterLeaseConflictError = class extends Error {
932
+ constructor(message, owner) {
933
+ super(message);
934
+ this.owner = owner;
935
+ this.name = "ProfileWriterLeaseConflictError";
936
+ }
937
+ owner;
938
+ };
939
+ function canonicalDatabasePath(databasePath) {
940
+ const resolved = resolve4(databasePath);
941
+ const missingSegments = [];
942
+ let candidate = resolved;
943
+ while (!existsSync5(candidate)) {
944
+ const parent = dirname4(candidate);
945
+ if (parent === candidate) return resolved;
946
+ missingSegments.unshift(basename3(candidate));
947
+ candidate = parent;
948
+ }
949
+ try {
950
+ return join5(realpathSync(candidate), ...missingSegments);
951
+ } catch {
952
+ return resolved;
953
+ }
954
+ }
955
+ function profileWriterLockPath(profile) {
956
+ return `${canonicalDatabasePath(profile.database_path)}.writer.lock`;
957
+ }
958
+ function ownerPath(lockPath) {
959
+ return join5(lockPath, ownerFileName);
960
+ }
961
+ function errorCode(error) {
962
+ return error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
963
+ }
964
+ function readOwner(lockPath) {
965
+ const stat = lstatSync(lockPath);
966
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
967
+ throw new Error(`Writer lease path is not a safe directory: ${lockPath}; manual inspection is required`);
968
+ }
969
+ let raw;
970
+ try {
971
+ raw = JSON.parse(readFileSync4(ownerPath(lockPath), "utf8"));
972
+ } catch (error) {
973
+ throw new Error(`Writer lease metadata is unreadable at ${lockPath}; refusing automatic recovery`, { cause: error });
974
+ }
975
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
976
+ throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
977
+ }
978
+ const owner = raw;
979
+ if (owner.schema_version !== writerLeaseSchemaVersion || typeof owner.profile_id !== "string" || owner.environment !== "production" && owner.environment !== "preview" && owner.environment !== "development" || owner.role !== "service" && owner.role !== "cli" || !Number.isSafeInteger(owner.pid) || Number(owner.pid) <= 0 || typeof owner.acquired_at !== "string" || Number.isNaN(Date.parse(owner.acquired_at)) || owner.role === "service" && (typeof owner.service_origin !== "string" || !owner.service_origin) || owner.role === "cli" && owner.service_origin !== void 0 || typeof owner.token !== "string" || owner.token.length < 16) {
980
+ throw new Error(`Writer lease metadata is invalid at ${lockPath}; refusing automatic recovery`);
981
+ }
982
+ return owner;
983
+ }
984
+ function processIsAlive(pid) {
985
+ try {
986
+ process.kill(pid, 0);
987
+ return true;
988
+ } catch (error) {
989
+ if (errorCode(error) === "ESRCH") return false;
990
+ return true;
991
+ }
992
+ }
993
+ function createLeaseDirectory(lockPath, owner) {
994
+ mkdirSync4(lockPath, { mode: 448 });
995
+ try {
996
+ writeFileSync2(ownerPath(lockPath), `${JSON.stringify(owner, null, 2)}
997
+ `, { encoding: "utf8", flag: "wx", mode: 384 });
998
+ } catch (error) {
999
+ rmSync(lockPath, { force: true, recursive: true });
1000
+ throw error;
1001
+ }
1002
+ }
1003
+ function reclaimDeadOwner(lockPath, owner) {
1004
+ if (processIsAlive(owner.pid)) {
1005
+ const { token: _token, ...publicOwner } = owner;
1006
+ throw new ProfileWriterLeaseConflictError(
1007
+ `Lineage profile ${owner.profile_id} already has an active ${owner.role} writer (pid ${owner.pid}); use that managed service or stop it before starting another writer`,
1008
+ publicOwner
1009
+ );
1010
+ }
1011
+ const tokenFence = createHash2("sha256").update(owner.token).digest("hex").slice(0, 24);
1012
+ const tombstone = `${lockPath}.stale-${owner.pid}-${tokenFence}`;
1013
+ try {
1014
+ renameSync(lockPath, tombstone);
1015
+ } catch (error) {
1016
+ if (errorCode(error) === "ENOENT" || errorCode(error) === "EEXIST" || errorCode(error) === "ENOTEMPTY") return;
1017
+ throw error;
1018
+ }
1019
+ }
1020
+ function acquireProfileWriterLease(profile, channel, role = "service") {
1021
+ assertProfileChannel(profile, channel);
1022
+ const lockPath = profileWriterLockPath(profile);
1023
+ const owner = {
1024
+ acquired_at: (/* @__PURE__ */ new Date()).toISOString(),
1025
+ environment: profile.environment,
1026
+ pid: process.pid,
1027
+ profile_id: profile.profile_id,
1028
+ role,
1029
+ schema_version: writerLeaseSchemaVersion,
1030
+ ...role === "service" ? { service_origin: profile.service_origin } : {},
1031
+ token: randomUUID()
1032
+ };
1033
+ for (let attempt = 0; attempt < 4; attempt += 1) {
1034
+ try {
1035
+ createLeaseDirectory(lockPath, owner);
1036
+ process.env.LINEAGE_WRITER_LEASE_TOKEN = owner.token;
1037
+ process.env.LINEAGE_WRITER_LOCK_PATH = lockPath;
1038
+ const { token: _token, ...publicOwner } = owner;
1039
+ let released = false;
1040
+ return {
1041
+ authenticate: (candidate) => {
1042
+ if (!candidate) return false;
1043
+ const expected = Buffer.from(owner.token);
1044
+ const actual = Buffer.from(candidate);
1045
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
1046
+ },
1047
+ lock_path: lockPath,
1048
+ owner: publicOwner,
1049
+ release: () => {
1050
+ if (released) return;
1051
+ released = true;
1052
+ try {
1053
+ const current = readOwner(lockPath);
1054
+ if (current.token === owner.token && current.pid === owner.pid) rmSync(lockPath, { force: true, recursive: true });
1055
+ } catch {
1056
+ }
1057
+ if (process.env.LINEAGE_WRITER_LEASE_TOKEN === owner.token) delete process.env.LINEAGE_WRITER_LEASE_TOKEN;
1058
+ if (process.env.LINEAGE_WRITER_LOCK_PATH === lockPath) delete process.env.LINEAGE_WRITER_LOCK_PATH;
1059
+ }
1060
+ };
1061
+ } catch (error) {
1062
+ if (errorCode(error) !== "EEXIST") throw error;
1063
+ let existing;
1064
+ try {
1065
+ existing = readOwner(lockPath);
1066
+ } catch (readError) {
1067
+ if (errorCode(readError) === "ENOENT") continue;
1068
+ throw readError;
1069
+ }
1070
+ if (existing.profile_id !== profile.profile_id || existing.environment !== profile.environment) {
1071
+ throw new Error(`Writer lease identity at ${lockPath} does not match ${profile.profile_id}/${profile.environment}; manual inspection is required`, { cause: error });
1072
+ }
1073
+ reclaimDeadOwner(lockPath, existing);
1074
+ }
1075
+ }
1076
+ throw new Error(`Could not acquire writer lease for Lineage profile ${profile.profile_id}; another writer is racing for ownership`);
1077
+ }
1078
+ function assertProfileWriterLeaseHeld() {
1079
+ if (!process.env.LINEAGE_PROFILE) return;
1080
+ const profileId = process.env.LINEAGE_PROFILE_ID;
1081
+ const environment = process.env.LINEAGE_PROFILE_ENVIRONMENT;
1082
+ const manifestPath = process.env.LINEAGE_PROFILE_MANIFEST;
1083
+ const databasePath = process.env.LINEAGE_DB;
1084
+ const lockPath = process.env.LINEAGE_WRITER_LOCK_PATH;
1085
+ const token = process.env.LINEAGE_WRITER_LEASE_TOKEN;
1086
+ if (!profileId || !environment || !manifestPath || !databasePath || !lockPath || !token) {
1087
+ throw new Error("Named Lineage profiles may write only through a process holding the profile writer lease");
1088
+ }
1089
+ const expectedLockPath = `${canonicalDatabasePath(databasePath)}.writer.lock`;
1090
+ if (resolve4(lockPath) !== expectedLockPath) throw new Error("Profile writer lease path does not match the selected profile database");
1091
+ const owner = readOwner(lockPath);
1092
+ if (owner.pid !== process.pid || owner.token !== token || owner.profile_id !== profileId || owner.environment !== environment) {
1093
+ throw new Error("Current process does not own the selected Lineage profile writer lease");
1094
+ }
1095
+ }
1096
+ function getProfileWriterDelegation(profile) {
1097
+ const lockPath = profileWriterLockPath(profile);
1098
+ if (!existsSync5(lockPath)) {
1099
+ throw new Error(
1100
+ `Lineage profile ${profile.profile_id} mutating commands require its managed service at ${profile.service_origin}; no active service writer lease was found`
1101
+ );
1102
+ }
1103
+ const owner = readOwner(lockPath);
1104
+ if (owner.profile_id !== profile.profile_id || owner.environment !== profile.environment) {
1105
+ throw new Error(`Writer lease identity at ${lockPath} does not match ${profile.profile_id}/${profile.environment}; refusing delegation`);
1106
+ }
1107
+ if (owner.role !== "service" || owner.service_origin !== profile.service_origin) {
1108
+ throw new Error(`Writer lease for Lineage profile ${profile.profile_id} is not the configured managed service at ${profile.service_origin}`);
1109
+ }
1110
+ if (!processIsAlive(owner.pid)) {
1111
+ throw new Error(`Managed service writer lease for Lineage profile ${profile.profile_id} is stale; refusing delegation`);
1112
+ }
1113
+ const { token, ...publicOwner } = owner;
1114
+ return { owner: publicOwner, service_origin: owner.service_origin, token };
1115
+ }
1116
+
1117
+ // src/server/assetLineageDb.ts
1118
+ var require3 = createRequire2(import.meta.url);
592
1119
  function nowIso() {
593
1120
  return (/* @__PURE__ */ new Date()).toISOString();
594
1121
  }
595
1122
  function lineageDbPath() {
596
- return process.env.LINEAGE_DB || join4(packageRoot, ".lineage", "asset-lineage.sqlite");
1123
+ return process.env.LINEAGE_DB || join6(packageRoot, ".lineage", "asset-lineage.sqlite");
1124
+ }
1125
+ function assertOpenedProfileIdentity(database, databasePath) {
1126
+ const selector = process.env.LINEAGE_PROFILE;
1127
+ if (!selector) return;
1128
+ const profile = resolveLineageProfile(selector);
1129
+ if (resolve5(databasePath) !== profile.database_path) {
1130
+ throw new Error(`Profile ${profile.profile_id} requires database ${profile.database_path}, not ${databasePath}`);
1131
+ }
1132
+ const table = database.prepare("select 1 from sqlite_master where type = 'table' and name = 'lineage_profile_identity'").get();
1133
+ if (!table) throw new Error(`Profile database ${databasePath} is not bound to a Lineage profile identity`);
1134
+ const rows = database.prepare("select profile_id, environment from lineage_profile_identity").all();
1135
+ if (rows.length !== 1) {
1136
+ throw new Error(`Profile database ${databasePath} has invalid Lineage profile identity row count ${rows.length}`);
1137
+ }
1138
+ if (rows[0].profile_id !== profile.profile_id || rows[0].environment !== profile.environment) {
1139
+ throw new Error(
1140
+ `Profile database ${databasePath} is bound to ${rows[0].profile_id}/${rows[0].environment}, not ${profile.profile_id}/${profile.environment}`
1141
+ );
1142
+ }
597
1143
  }
598
1144
  function lineageDb() {
599
- mkdirSync3(join4(lineageDbPath(), ".."), { recursive: true });
600
- const { DatabaseSync } = require2("node:sqlite");
601
- const database = new DatabaseSync(lineageDbPath());
602
- database.exec("PRAGMA foreign_keys = ON");
603
- database.exec("PRAGMA busy_timeout = 5000");
1145
+ const readOnly = process.env.LINEAGE_DB_ACCESS === "read-only";
1146
+ const databasePath = lineageDbPath();
1147
+ if (!readOnly) assertProfileWriterLeaseHeld();
1148
+ if (process.env.LINEAGE_PROFILE && !existsSync6(databasePath)) {
1149
+ throw new Error(`Profile database does not exist: ${databasePath}; bind the profile before opening it`);
1150
+ }
1151
+ if (!readOnly) mkdirSync5(join6(databasePath, ".."), { recursive: true });
1152
+ const { DatabaseSync } = require3("node:sqlite");
1153
+ const database = readOnly ? new DatabaseSync(databasePath, { readOnly: true }) : new DatabaseSync(databasePath);
1154
+ try {
1155
+ assertOpenedProfileIdentity(database, databasePath);
1156
+ database.exec("PRAGMA foreign_keys = ON");
1157
+ database.exec("PRAGMA busy_timeout = 5000");
1158
+ if (readOnly) return database;
1159
+ } catch (error) {
1160
+ database.close();
1161
+ throw error;
1162
+ }
1163
+ database.exec("PRAGMA journal_mode = WAL");
604
1164
  database.exec(`
605
1165
  create table if not exists projects (
606
1166
  id text primary key,
@@ -1316,12 +1876,12 @@ function randomId(prefix) {
1316
1876
  return `${prefix}_${Date.now().toString(36)}_${randomBytes(6).toString("base64url").toLowerCase()}`;
1317
1877
  }
1318
1878
  function tokenHash(token) {
1319
- return createHash2("sha256").update(token).digest("hex");
1879
+ return createHash3("sha256").update(token).digest("hex");
1320
1880
  }
1321
1881
  function safeEqual(a, b) {
1322
1882
  const left = Buffer.from(a);
1323
1883
  const right = Buffer.from(b);
1324
- return left.length === right.length && timingSafeEqual(left, right);
1884
+ return left.length === right.length && timingSafeEqual2(left, right);
1325
1885
  }
1326
1886
  function expiresAtFrom(timestamp, ttlSeconds) {
1327
1887
  return new Date(new Date(timestamp).getTime() + ttlSeconds * 1e3).toISOString();
@@ -1348,6 +1908,12 @@ function derivedState(row, now = /* @__PURE__ */ new Date()) {
1348
1908
  }
1349
1909
  function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
1350
1910
  const heartbeatAt = String(row.heartbeat_at);
1911
+ const persistedStatus = String(row.status);
1912
+ const state = derivedState({
1913
+ expires_at: String(row.expires_at),
1914
+ heartbeat_at: heartbeatAt,
1915
+ status: persistedStatus
1916
+ }, now);
1351
1917
  return {
1352
1918
  id: String(row.id),
1353
1919
  project: String(row.project_id),
@@ -1359,7 +1925,7 @@ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
1359
1925
  agent_name: String(row.agent_name),
1360
1926
  agent_kind: String(row.agent_kind),
1361
1927
  thread_id: typeof row.thread_id === "string" ? row.thread_id : void 0,
1362
- status: String(row.status),
1928
+ status: persistedStatus === "active" && state === "expired" ? "expired" : persistedStatus,
1363
1929
  created_at: String(row.created_at),
1364
1930
  heartbeat_at: heartbeatAt,
1365
1931
  expires_at: String(row.expires_at),
@@ -1369,11 +1935,7 @@ function rowToClaim(row, now = /* @__PURE__ */ new Date()) {
1369
1935
  override_reason: typeof row.override_reason === "string" ? row.override_reason : void 0,
1370
1936
  metadata: parseMetadata(row.metadata_json),
1371
1937
  heartbeat_age_seconds: Math.max(0, Math.floor((now.getTime() - new Date(heartbeatAt).getTime()) / 1e3)),
1372
- derived_state: derivedState({
1373
- expires_at: String(row.expires_at),
1374
- heartbeat_at: heartbeatAt,
1375
- status: String(row.status)
1376
- }, now)
1938
+ derived_state: state
1377
1939
  };
1378
1940
  }
1379
1941
  function eventToRow(row) {
@@ -1515,7 +2077,7 @@ function createAgentClaim(fields) {
1515
2077
  function listAgentClaims(project) {
1516
2078
  const database = lineageDb();
1517
2079
  try {
1518
- expireActiveClaims(database);
2080
+ if (process.env.LINEAGE_DB_ACCESS !== "read-only") expireActiveClaims(database);
1519
2081
  const rows = project ? database.prepare("select * from agent_claims where project_id = ? order by status, heartbeat_at desc").all(project) : database.prepare("select * from agent_claims order by project_id, status, heartbeat_at desc").all();
1520
2082
  return { ok: true, claims: rows.map((row) => rowToClaim(row)), fetchedAt: nowIso() };
1521
2083
  } finally {
@@ -1525,7 +2087,7 @@ function listAgentClaims(project) {
1525
2087
  function inspectAgentClaim(claimId, project) {
1526
2088
  const database = lineageDb();
1527
2089
  try {
1528
- expireActiveClaims(database);
2090
+ if (process.env.LINEAGE_DB_ACCESS !== "read-only") expireActiveClaims(database);
1529
2091
  const claim = findClaimById(database, claimId, project);
1530
2092
  if (!claim) throw new AgentClaimError(`Unknown agent claim: ${claimId}`, 404, "claim_not_found");
1531
2093
  const events = database.prepare("select * from agent_claim_events where claim_id = ? order by created_at").all(claim.id);
@@ -1662,7 +2224,7 @@ function validateAgentClaimForWrite(fields) {
1662
2224
  }
1663
2225
 
1664
2226
  // src/server/assetLineage.ts
1665
- import { join as join5 } from "node:path";
2227
+ import { join as join7 } from "node:path";
1666
2228
 
1667
2229
  // src/server/assetLineageSelection.ts
1668
2230
  function selectedRows(database, project, root) {
@@ -2213,9 +2775,18 @@ function knownRoots(database, project) {
2213
2775
  and parent_asset_id not in (select child_asset_id from asset_edges where project_id = ?)
2214
2776
  group by parent_asset_id
2215
2777
  `).all(project, project, project, project);
2216
- return rows.map((row) => ({ root_asset_id: row.root_asset_id, selected_at: row.selected_at || void 0 }));
2778
+ const roots = /* @__PURE__ */ new Map();
2779
+ for (const row of rows) {
2780
+ const existing = roots.get(row.root_asset_id);
2781
+ roots.set(row.root_asset_id, {
2782
+ root_asset_id: row.root_asset_id,
2783
+ selected_at: row.selected_at || existing?.selected_at
2784
+ });
2785
+ }
2786
+ return [...roots.values()];
2217
2787
  }
2218
2788
  function seedLegacyWorkspaces(database, project) {
2789
+ if (process.env.LINEAGE_DB_ACCESS === "read-only") return;
2219
2790
  ensureProject2(database, project);
2220
2791
  const timestamp = nowIso();
2221
2792
  const statement = database.prepare(`
@@ -2239,6 +2810,34 @@ function seedLegacyWorkspaces(database, project) {
2239
2810
  );
2240
2811
  }
2241
2812
  }
2813
+ function inferredLegacyWorkspaces(database, project) {
2814
+ if (process.env.LINEAGE_DB_ACCESS !== "read-only") return [];
2815
+ const persistedRoots = new Set(
2816
+ database.prepare("select root_asset_id from lineage_workspaces where project_id = ?").all(project).map((row) => row.root_asset_id)
2817
+ );
2818
+ const asset = database.prepare("select id, title from assets where project_id = ? and id = ?");
2819
+ return knownRoots(database, project).flatMap((root) => {
2820
+ if (persistedRoots.has(root.root_asset_id)) return [];
2821
+ const row = asset.get(project, root.root_asset_id);
2822
+ if (!row) return [];
2823
+ const timestamp = root.selected_at || nowIso();
2824
+ return [{
2825
+ active_at: root.selected_at,
2826
+ created_at: timestamp,
2827
+ created_by: "system",
2828
+ id: lineageWorkspaceId(project, root.root_asset_id),
2829
+ project,
2830
+ root_asset_id: root.root_asset_id,
2831
+ status: "active",
2832
+ title: `${row.title} lineage`,
2833
+ updated_at: timestamp
2834
+ }];
2835
+ });
2836
+ }
2837
+ function sortWorkspaces(workspaces) {
2838
+ const statusRank = { active: 0, paused: 1, archived: 2 };
2839
+ return workspaces.sort((left, right) => statusRank[left.status] - statusRank[right.status] || (right.active_at || "").localeCompare(left.active_at || "") || right.updated_at.localeCompare(left.updated_at) || left.title.localeCompare(right.title));
2840
+ }
2242
2841
  function listRows(database, project) {
2243
2842
  return database.prepare(`
2244
2843
  select * from lineage_workspaces
@@ -2250,23 +2849,15 @@ function listRows(database, project) {
2250
2849
  title
2251
2850
  `).all(project).map(rowToWorkspace);
2252
2851
  }
2253
- function activeWorkspace(database, project) {
2254
- const row = database.prepare(`
2255
- select * from lineage_workspaces
2256
- where project_id = ? and status != 'archived'
2257
- order by active_at desc nulls last, updated_at desc
2258
- limit 1
2259
- `).get(project);
2260
- return row ? rowToWorkspace(row) : null;
2261
- }
2262
2852
  function listLineageWorkspaces(project) {
2263
2853
  const database = lineageDb();
2264
2854
  try {
2265
2855
  seedLegacyWorkspaces(database, project);
2856
+ const workspaces = sortWorkspaces([...listRows(database, project), ...inferredLegacyWorkspaces(database, project)]);
2266
2857
  return {
2267
2858
  project,
2268
- active_workspace: activeWorkspace(database, project),
2269
- workspaces: listRows(database, project),
2859
+ active_workspace: workspaces.find((workspace) => workspace.status !== "archived") || null,
2860
+ workspaces,
2270
2861
  fetchedAt: nowIso()
2271
2862
  };
2272
2863
  } finally {
@@ -2277,7 +2868,7 @@ function activeLineageWorkspaceRoot(project) {
2277
2868
  const database = lineageDb();
2278
2869
  try {
2279
2870
  seedLegacyWorkspaces(database, project);
2280
- return activeWorkspace(database, project)?.root_asset_id;
2871
+ return sortWorkspaces([...listRows(database, project), ...inferredLegacyWorkspaces(database, project)]).find((workspace) => workspace.status !== "archived")?.root_asset_id;
2281
2872
  } finally {
2282
2873
  database.close();
2283
2874
  }
@@ -2336,7 +2927,7 @@ function upsertProject(database, project) {
2336
2927
  insert into projects (id, product, catalog_path, created_at, updated_at)
2337
2928
  values (?, ?, ?, ?, ?)
2338
2929
  on conflict(id) do update set product = excluded.product, updated_at = excluded.updated_at
2339
- `).run(project, project, join5(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
2930
+ `).run(project, project, join7(repoRoot, project, "assets", "catalog.json"), timestamp, timestamp);
2340
2931
  }
2341
2932
  function upsertAsset(database, project, asset) {
2342
2933
  const timestamp = nowIso();
@@ -2975,16 +3566,34 @@ function clearLineageRerollRequest(project, fields) {
2975
3566
  }
2976
3567
  }
2977
3568
 
2978
- // src/server/assetLineageHandoff.ts
2979
- var publicPackageCommand = "npx @mean-weasel/lineage";
3569
+ // src/server/lineageRuntimeCommand.ts
3570
+ import { existsSync as existsSync7 } from "node:fs";
3571
+ import { createRequire as createRequire3 } from "node:module";
3572
+ import { join as join8 } from "node:path";
3573
+ var require4 = createRequire3(import.meta.url);
3574
+ function lineagePublicPackageCommand() {
3575
+ if (process.env.LINEAGE_CHANNEL === "dev") {
3576
+ const sourceCli = join8(packageRoot, "src", "cli", "lineage-dev.ts");
3577
+ const builtCli = join8(packageRoot, "dist", "cli", "lineage-dev.js");
3578
+ return existsSync7(sourceCli) ? `${shellQuote(process.execPath)} --import ${shellQuote(require4.resolve("tsx"))} ${shellQuote(sourceCli)}` : `${shellQuote(process.execPath)} ${shellQuote(builtCli)}`;
3579
+ }
3580
+ if (process.env.LINEAGE_CHANNEL === "preview") return "LINEAGE_CHANNEL=preview npx --package @mean-weasel/lineage@next lineage-dev";
3581
+ return "npx @mean-weasel/lineage";
3582
+ }
2980
3583
  function shellQuote(value) {
2981
3584
  return `'${value.replace(/'/g, "'\\''")}'`;
2982
3585
  }
3586
+ function lineageRuntimeSelector() {
3587
+ const manifest = process.env.LINEAGE_PROFILE_MANIFEST?.trim();
3588
+ return manifest ? `--profile ${shellQuote(manifest)}` : `--db ${shellQuote(lineageDbPath())}`;
3589
+ }
3590
+
3591
+ // src/server/assetLineageHandoff.ts
2983
3592
  function lineageCommand(command, project, rootAssetId) {
2984
- return `${publicPackageCommand} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --db ${shellQuote(lineageDbPath())} --json`;
3593
+ return `${lineagePublicPackageCommand()} ${command} --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} ${lineageRuntimeSelector()} --json`;
2985
3594
  }
2986
3595
  function linkChildCommand(project, rootAssetId) {
2987
- return `${publicPackageCommand} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write --db ${shellQuote(lineageDbPath())} --json`;
3596
+ return `${lineagePublicPackageCommand()} link-child --project ${shellQuote(project)} --root ${shellQuote(rootAssetId)} --child <asset-id> --confirm-write ${lineageRuntimeSelector()} --json`;
2988
3597
  }
2989
3598
  function rerollImportGuidance(rootAssetId, targetAssetId) {
2990
3599
  return `Use lineage reroll plan --root ${rootAssetId} --target ${targetAssetId} and lineage reroll import instead.`;
@@ -3026,7 +3635,7 @@ function getLineageBrief(project, rootAssetId) {
3026
3635
  },
3027
3636
  handoff: {
3028
3637
  next_command: lineageCommand("next", project, next.root_asset_id),
3029
- inspect_command: asset ? `${publicPackageCommand} inspect --project ${shellQuote(project)} --asset-id ${shellQuote(asset.asset_id)} --db ${shellQuote(lineageDbPath())} --json` : void 0,
3638
+ inspect_command: asset ? `${lineagePublicPackageCommand()} inspect --project ${shellQuote(project)} --asset-id ${shellQuote(asset.asset_id)} ${lineageRuntimeSelector()} --json` : void 0,
3030
3639
  link_child_command: asset ? linkChildCommand(project, next.root_asset_id) : void 0
3031
3640
  },
3032
3641
  fetchedAt: nowIso()
@@ -3078,9 +3687,9 @@ function linkSelectedLineageChild(project, fields) {
3078
3687
  }
3079
3688
 
3080
3689
  // src/server/generationReceipts.ts
3081
- import { existsSync as existsSync4, realpathSync, statSync as statSync3 } from "node:fs";
3082
- import { relative as relative2, resolve as resolve3 } from "node:path";
3083
- import { randomUUID } from "node:crypto";
3690
+ import { existsSync as existsSync8, realpathSync as realpathSync2, statSync as statSync4 } from "node:fs";
3691
+ import { relative as relative2, resolve as resolve6 } from "node:path";
3692
+ import { randomUUID as randomUUID2 } from "node:crypto";
3084
3693
  var adapterVersion = "generation-receipts-v1";
3085
3694
  var provider = "codex-handoff";
3086
3695
  var GenerationReceiptError = class extends Error {
@@ -3091,7 +3700,7 @@ var GenerationReceiptError = class extends Error {
3091
3700
  status;
3092
3701
  };
3093
3702
  function jobId() {
3094
- return `gen-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
3703
+ return `gen-${Date.now().toString(36)}-${randomUUID2().slice(0, 8)}`;
3095
3704
  }
3096
3705
  function quote(value) {
3097
3706
  return JSON.stringify(value);
@@ -3101,7 +3710,7 @@ function parseJson(value, fallback) {
3101
3710
  return JSON.parse(value);
3102
3711
  }
3103
3712
  function buildRerollHandoff(project, id, prompt, rootAssetId, target, request) {
3104
- const importCommand = `npx lineage reroll import --project ${quote(project)} --job-id ${quote(id)} --file <.asset-scratch-file> --confirm-write --json`;
3713
+ const importCommand = `${lineagePublicPackageCommand()} reroll import --project ${quote(project)} --job-id ${quote(id)} --file <.asset-scratch-file> --confirm-write ${lineageRuntimeSelector()} --json`;
3105
3714
  return {
3106
3715
  schema_version: "lineage.generation_handoff.v1",
3107
3716
  provider,
@@ -3291,14 +3900,14 @@ function isPathInside(child, parent) {
3291
3900
  return Boolean(rel) && !rel.startsWith("..") && !rel.startsWith("/");
3292
3901
  }
3293
3902
  function resolveScratchFile(file) {
3294
- const scratchRoot = resolve3(repoRoot, ".asset-scratch");
3295
- const candidate = file.startsWith(".asset-scratch/") || resolve3(file).startsWith(scratchRoot) ? resolve3(repoRoot, file) : resolve3(scratchRoot, file);
3903
+ const scratchRoot = resolve6(repoRoot, ".asset-scratch");
3904
+ const candidate = file.startsWith(".asset-scratch/") || resolve6(file).startsWith(scratchRoot) ? resolve6(repoRoot, file) : resolve6(scratchRoot, file);
3296
3905
  if (!isPathInside(candidate, scratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
3297
- if (!existsSync4(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
3298
- const realScratchRoot = realpathSync(scratchRoot);
3299
- const realCandidate = realpathSync(candidate);
3906
+ if (!existsSync8(candidate)) throw new GenerationReceiptError(`Missing import file: ${file}`, 404);
3907
+ const realScratchRoot = realpathSync2(scratchRoot);
3908
+ const realCandidate = realpathSync2(candidate);
3300
3909
  if (!isPathInside(realCandidate, realScratchRoot)) throw new GenerationReceiptError(`Import file must be under .asset-scratch: ${file}`);
3301
- const stats = statSync3(candidate);
3910
+ const stats = statSync4(candidate);
3302
3911
  if (!stats.isFile()) throw new GenerationReceiptError(`Import path is not a file: ${file}`);
3303
3912
  const checksum = fileSha256(candidate);
3304
3913
  return {
@@ -3378,17 +3987,19 @@ function importImageRerollOutput(project = defaultProject, fields) {
3378
3987
  }
3379
3988
 
3380
3989
  // src/server/lineageSelectionPacket.ts
3381
- import { createHash as createHash3 } from "node:crypto";
3382
- import { existsSync as existsSync5, statSync as statSync4 } from "node:fs";
3383
- import { isAbsolute, resolve as resolve4 } from "node:path";
3990
+ import { createHash as createHash4 } from "node:crypto";
3991
+ import { existsSync as existsSync9, statSync as statSync5 } from "node:fs";
3992
+ import { isAbsolute as isAbsolute2, resolve as resolve7 } from "node:path";
3384
3993
  var LineageSelectionPacketError = class extends Error {
3385
- constructor(message, warnings = [], errors = []) {
3994
+ constructor(message, warnings = [], errors = [], diagnostics = []) {
3386
3995
  super(message);
3387
3996
  this.warnings = warnings;
3388
3997
  this.errors = errors;
3998
+ this.diagnostics = diagnostics;
3389
3999
  }
3390
4000
  warnings;
3391
4001
  errors;
4002
+ diagnostics;
3392
4003
  };
3393
4004
  function listAllAssets(project) {
3394
4005
  const pageSize = 100;
@@ -3424,15 +4035,15 @@ function resolveWorkspace(project, options) {
3424
4035
  }
3425
4036
  function resolveLocalReference(reference) {
3426
4037
  if (!reference) return void 0;
3427
- if (isAbsolute(reference)) return reference;
3428
- const repoRelative = resolve4(repoRoot, reference);
3429
- if (existsSync5(repoRelative)) return repoRelative;
3430
- return resolve4(repoRoot, ".asset-scratch", reference);
4038
+ if (isAbsolute2(reference)) return reference;
4039
+ const repoRelative = resolve7(repoRoot, reference);
4040
+ if (existsSync9(repoRelative)) return repoRelative;
4041
+ return resolve7(repoRoot, ".asset-scratch", reference);
3431
4042
  }
3432
4043
  function fileSize(path) {
3433
- if (!path || !existsSync5(path)) return void 0;
4044
+ if (!path || !existsSync9(path)) return void 0;
3434
4045
  try {
3435
- return statSync4(path).size;
4046
+ return statSync5(path).size;
3436
4047
  } catch {
3437
4048
  return void 0;
3438
4049
  }
@@ -3458,7 +4069,7 @@ function currentAttemptFor(node) {
3458
4069
  function assetForNode(node, catalogAsset, warnings) {
3459
4070
  const localReference = catalogAsset?.local?.absolute_path || node.current_attempt?.file_path || node.local_path || catalogAsset?.local?.relative_path;
3460
4071
  const absolutePath = catalogAsset?.local?.absolute_path || resolveLocalReference(localReference);
3461
- const localExists = absolutePath ? existsSync5(absolutePath) : false;
4072
+ const localExists = absolutePath ? existsSync9(absolutePath) : false;
3462
4073
  const hasLocalClaim = Boolean(absolutePath || node.local_path || catalogAsset?.local?.relative_path);
3463
4074
  const s3Key = catalogAsset?.s3?.key || node.s3_key;
3464
4075
  const hasS3 = Boolean(s3Key);
@@ -3503,10 +4114,192 @@ function assetForNode(node, catalogAsset, warnings) {
3503
4114
  };
3504
4115
  }
3505
4116
  function packetId(packetIdentity) {
3506
- const digest = createHash3("sha256").update(JSON.stringify(packetIdentity)).digest("hex").slice(0, 24);
4117
+ const digest = createHash4("sha256").update(JSON.stringify(packetIdentity)).digest("hex").slice(0, 24);
3507
4118
  return `lineage_packet_${digest}`;
3508
4119
  }
3509
- function getLineageSelectionPacket(project, options = {}) {
4120
+ var SHA256_PATTERN = /^[a-f0-9]{64}$/;
4121
+ function canonicalValue(value) {
4122
+ if (Array.isArray(value)) return value.map((item) => canonicalValue(item));
4123
+ if (value && typeof value === "object") {
4124
+ return Object.fromEntries(
4125
+ Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => [key, canonicalValue(item)])
4126
+ );
4127
+ }
4128
+ return value;
4129
+ }
4130
+ function canonicalLineageSelectionPacketIdentityJson(projection) {
4131
+ return JSON.stringify(canonicalValue(projection));
4132
+ }
4133
+ function lineageSelectionPacketV2IdentityProjection(packet) {
4134
+ const assetsById = new Map(packet.assets.map((asset) => [asset.asset_id, asset]));
4135
+ const selection = packet.selection.items.map((item) => {
4136
+ const asset = assetsById.get(item.asset_id);
4137
+ if (!asset) {
4138
+ const diagnostics = [{ asset_id: item.asset_id, code: "selected_asset_missing", severity: "error" }];
4139
+ throw new LineageSelectionPacketError(
4140
+ `Selected asset ${item.asset_id} is absent from the v2 packet asset envelope.`,
4141
+ [],
4142
+ ["selected_asset_missing"],
4143
+ diagnostics
4144
+ );
4145
+ }
4146
+ return {
4147
+ asset_id: item.asset_id,
4148
+ campaign: asset.campaign,
4149
+ channel: asset.channel,
4150
+ current_attempt: {
4151
+ asset_id: asset.current_attempt.asset_id,
4152
+ attempt_index: asset.current_attempt.attempt_index,
4153
+ checksum_sha256: asset.current_attempt.checksum_sha256,
4154
+ id: asset.current_attempt.id,
4155
+ source: asset.current_attempt.source
4156
+ },
4157
+ media_type: asset.media_type,
4158
+ mime_type: asset.mime_type,
4159
+ position: item.position,
4160
+ selection_note: item.selection_note,
4161
+ title: asset.title
4162
+ };
4163
+ });
4164
+ return {
4165
+ schema_version: "lineage.selection_packet.v2",
4166
+ project: packet.project,
4167
+ product: packet.product,
4168
+ workspace: {
4169
+ id: packet.workspace.id,
4170
+ root_asset_id: packet.workspace.root_asset_id
4171
+ },
4172
+ context: {
4173
+ campaign: packet.context.campaign,
4174
+ channel: packet.context.channel,
4175
+ labels: packet.context.labels,
4176
+ notes: packet.context.notes
4177
+ },
4178
+ selection,
4179
+ diagnostics: packet.diagnostics.map((diagnostic) => ({
4180
+ code: diagnostic.code,
4181
+ severity: diagnostic.severity,
4182
+ ...diagnostic.asset_id ? { asset_id: diagnostic.asset_id } : {}
4183
+ }))
4184
+ };
4185
+ }
4186
+ function lineageSelectionPacketV2IdentitySha256(packet) {
4187
+ const projection = lineageSelectionPacketV2IdentityProjection(packet);
4188
+ return createHash4("sha256").update(canonicalLineageSelectionPacketIdentityJson(projection)).digest("hex");
4189
+ }
4190
+ function addDiagnostic(diagnostics, diagnostic) {
4191
+ if (diagnostics.some((item) => item.code === diagnostic.code && item.severity === diagnostic.severity && item.asset_id === diagnostic.asset_id)) return;
4192
+ diagnostics.push(diagnostic);
4193
+ }
4194
+ function requireV2CurrentAttempt(node, warnings, diagnostics) {
4195
+ const attempt = node.current_attempt;
4196
+ if (!attempt) {
4197
+ const message = `Selected asset ${node.asset_id} has no current attempt identity.`;
4198
+ const diagnostic = { asset_id: node.asset_id, code: "current_attempt_missing", severity: "error" };
4199
+ throw new LineageSelectionPacketError(message, warnings, ["current_attempt_missing"], [...diagnostics, diagnostic]);
4200
+ }
4201
+ if (!attempt.id || !attempt.asset_id || !Number.isInteger(attempt.attempt_index) || attempt.attempt_index <= 0 || !attempt.source || attempt.is_current !== true) {
4202
+ const message = `Selected asset ${node.asset_id} has malformed current attempt identity.`;
4203
+ const diagnostic = { asset_id: node.asset_id, code: "current_attempt_invalid_identity", severity: "error" };
4204
+ throw new LineageSelectionPacketError(message, warnings, ["current_attempt_invalid_identity"], [...diagnostics, diagnostic]);
4205
+ }
4206
+ if (!attempt.checksum_sha256 || !SHA256_PATTERN.test(attempt.checksum_sha256)) {
4207
+ const message = `Selected asset ${node.asset_id} current attempt does not have a valid lowercase SHA-256 checksum.`;
4208
+ const diagnostic = { asset_id: node.asset_id, code: "current_attempt_invalid_checksum", severity: "error" };
4209
+ throw new LineageSelectionPacketError(message, warnings, ["current_attempt_invalid_checksum"], [...diagnostics, diagnostic]);
4210
+ }
4211
+ return {
4212
+ asset_id: attempt.asset_id,
4213
+ attempt_index: attempt.attempt_index,
4214
+ checksum_sha256: attempt.checksum_sha256,
4215
+ file_path: attempt.file_path,
4216
+ generation_job_id: attempt.generation_job_id,
4217
+ id: attempt.id,
4218
+ is_current: attempt.is_current,
4219
+ source: attempt.source
4220
+ };
4221
+ }
4222
+ function assetForNodeV2(node, visibleCatalogAsset, catalogById, warnings, errors, diagnostics) {
4223
+ const currentAttempt = requireV2CurrentAttempt(node, warnings, diagnostics);
4224
+ const currentAttemptCatalogAsset = catalogById.get(currentAttempt.asset_id);
4225
+ const isVisibleNodeAttempt = currentAttempt.asset_id === node.asset_id;
4226
+ const mediaCatalogAsset = currentAttemptCatalogAsset || (isVisibleNodeAttempt ? visibleCatalogAsset : void 0);
4227
+ const localReference = currentAttempt.file_path || mediaCatalogAsset?.local?.absolute_path || mediaCatalogAsset?.local?.relative_path || (isVisibleNodeAttempt ? node.local_path : void 0);
4228
+ const absolutePath = resolveLocalReference(localReference);
4229
+ const localExists = absolutePath ? existsSync9(absolutePath) : false;
4230
+ const hasLocalClaim = Boolean(localReference);
4231
+ const s3Key = mediaCatalogAsset?.s3?.key || (isVisibleNodeAttempt ? node.s3_key : void 0);
4232
+ const hasS3 = Boolean(s3Key);
4233
+ const mimeType = mediaCatalogAsset?.local?.content_type || mediaCatalogAsset?.s3?.content_type || visibleCatalogAsset?.local?.content_type || visibleCatalogAsset?.s3?.content_type || (localReference ? contentTypeFor(localReference) : void 0);
4234
+ const mediaType = mediaCatalogAsset?.content_type || visibleCatalogAsset?.content_type || node.media_type;
4235
+ if (localExists && absolutePath && fileSha256(absolutePath) !== currentAttempt.checksum_sha256) {
4236
+ const message = `Selected asset ${node.asset_id} current attempt checksum does not match its local file.`;
4237
+ const diagnostic = { asset_id: node.asset_id, code: "current_attempt_checksum_mismatch", severity: "error" };
4238
+ throw new LineageSelectionPacketError(message, [...warnings, message], ["current_attempt_checksum_mismatch"], [...diagnostics, diagnostic]);
4239
+ }
4240
+ const s3Checksum = mediaCatalogAsset?.s3?.checksum_sha256;
4241
+ if (hasS3 && s3Checksum && s3Checksum !== currentAttempt.checksum_sha256) {
4242
+ const message = `Selected asset ${node.asset_id} current attempt checksum does not match its S3 media envelope.`;
4243
+ const diagnostic = { asset_id: node.asset_id, code: "current_attempt_checksum_mismatch", severity: "error" };
4244
+ throw new LineageSelectionPacketError(message, [...warnings, message], ["current_attempt_checksum_mismatch"], [...diagnostics, diagnostic]);
4245
+ }
4246
+ if (hasLocalClaim && !localExists) {
4247
+ warnings.push(`Selected asset ${node.asset_id} current attempt has a local path but the file is missing: ${absolutePath || localReference}`);
4248
+ }
4249
+ if (!hasLocalClaim && !hasS3) {
4250
+ const message = `Selected asset ${node.asset_id} current attempt has neither a local file nor S3 key.`;
4251
+ warnings.push(message);
4252
+ errors.push(message);
4253
+ }
4254
+ if (mediaType && mediaType !== "image" && mediaType !== "gif") {
4255
+ warnings.push(`Selected asset ${node.asset_id} is ${mediaType}, not an image/gif asset.`);
4256
+ addDiagnostic(diagnostics, { asset_id: node.asset_id, code: "unsupported_media_type", severity: "warning" });
4257
+ }
4258
+ const conflictingChecksums = [
4259
+ currentAttemptCatalogAsset?.local?.checksum_sha256,
4260
+ currentAttemptCatalogAsset?.s3?.checksum_sha256,
4261
+ visibleCatalogAsset?.local?.checksum_sha256,
4262
+ visibleCatalogAsset?.s3?.checksum_sha256,
4263
+ node.checksum_sha256
4264
+ ].filter((checksum) => Boolean(checksum && checksum !== currentAttempt.checksum_sha256));
4265
+ if (conflictingChecksums.length > 0) {
4266
+ warnings.push(`Selected asset ${node.asset_id} catalog or visible-node checksum differs from its current attempt; current attempt checksum is authoritative.`);
4267
+ }
4268
+ return {
4269
+ asset_id: node.asset_id,
4270
+ campaign: visibleCatalogAsset?.campaign || node.campaign,
4271
+ channel: visibleCatalogAsset?.channel || node.channel,
4272
+ checksum_sha256: currentAttempt.checksum_sha256,
4273
+ current_attempt: currentAttempt,
4274
+ local: {
4275
+ absolute_path: absolutePath,
4276
+ content_type: mediaCatalogAsset?.local?.content_type,
4277
+ exists: localExists,
4278
+ relative_path: mediaCatalogAsset?.local?.relative_path || currentAttempt.file_path || (isVisibleNodeAttempt ? node.local_path : void 0),
4279
+ size_bytes: mediaCatalogAsset?.local?.size_bytes || fileSize(absolutePath)
4280
+ },
4281
+ media_type: mediaType,
4282
+ mime_type: mimeType,
4283
+ review_notes: node.review_notes,
4284
+ review_state: node.review_state,
4285
+ s3: {
4286
+ bucket: mediaCatalogAsset?.s3?.bucket,
4287
+ checksum_sha256: mediaCatalogAsset?.s3?.checksum_sha256,
4288
+ content_type: mediaCatalogAsset?.s3?.content_type,
4289
+ etag: mediaCatalogAsset?.s3?.etag,
4290
+ key: s3Key,
4291
+ region: mediaCatalogAsset?.s3?.region,
4292
+ size_bytes: mediaCatalogAsset?.s3?.size_bytes,
4293
+ version_id: mediaCatalogAsset?.s3?.version_id
4294
+ },
4295
+ selection_note: node.selection_note,
4296
+ source: mediaCatalogAsset?.source || visibleCatalogAsset?.source || node.source,
4297
+ status: visibleCatalogAsset?.status || node.status,
4298
+ storage_state: storageState(hasLocalClaim, hasS3),
4299
+ title: visibleCatalogAsset?.title || node.title || node.asset_id
4300
+ };
4301
+ }
4302
+ function getLineageSelectionPacketV1(project, options = {}) {
3510
4303
  const workspace = resolveWorkspace(project, options);
3511
4304
  const snapshot = getLineageSnapshot(project, workspace.root_asset_id);
3512
4305
  const catalogSummary = validateProject(project);
@@ -3597,16 +4390,114 @@ function getLineageSelectionPacket(project, options = {}) {
3597
4390
  }
3598
4391
  };
3599
4392
  }
4393
+ function getLineageSelectionPacketV2(project, options) {
4394
+ const workspace = resolveWorkspace(project, options);
4395
+ const snapshot = getLineageSnapshot(project, workspace.root_asset_id);
4396
+ const catalogSummary = validateProject(project);
4397
+ const catalogAssets = listAllAssets(project);
4398
+ const catalogById = new Map(catalogAssets.map((asset) => [asset.asset_id, asset]));
4399
+ const warnings = [];
4400
+ const errors = [];
4401
+ const diagnostics = [];
4402
+ const nodeById = new Map(snapshot.nodes.map((node) => [node.asset_id, node]));
4403
+ const selectedItems = snapshot.selections.map((selection) => ({
4404
+ asset_id: selection.asset_id,
4405
+ position: selection.position,
4406
+ selected_at: selection.selected_at,
4407
+ selection_note: selection.notes
4408
+ }));
4409
+ if (selectedItems.length === 0) {
4410
+ warnings.push(`Lineage workspace ${workspace.id} has no selected assets.`);
4411
+ addDiagnostic(diagnostics, { code: "empty_selection", severity: "warning" });
4412
+ }
4413
+ const assets = [];
4414
+ for (const item of selectedItems) {
4415
+ const node = nodeById.get(item.asset_id);
4416
+ if (!node) {
4417
+ const message = `Selected asset ${item.asset_id} is not present in the resolved workspace snapshot.`;
4418
+ warnings.push(message);
4419
+ errors.push(message);
4420
+ const diagnostic = { asset_id: item.asset_id, code: "selected_asset_outside_workspace", severity: "error" };
4421
+ addDiagnostic(diagnostics, diagnostic);
4422
+ throw new LineageSelectionPacketError(message, warnings, ["selected_asset_outside_workspace"], diagnostics);
4423
+ }
4424
+ assets.push(assetForNodeV2(node, catalogById.get(item.asset_id), catalogById, warnings, errors, diagnostics));
4425
+ }
4426
+ if (assets.some((asset) => !asset.dimensions)) {
4427
+ warnings.push("Image dimensions are unavailable for one or more selected assets.");
4428
+ }
4429
+ if (options.strict) {
4430
+ const strictErrors = [
4431
+ ...selectedItems.length === 0 ? ["empty_selection"] : [],
4432
+ ...assets.filter((asset) => asset.local.absolute_path && !asset.local.exists).map((asset) => `missing_local_file:${asset.asset_id}`),
4433
+ ...assets.filter((asset) => asset.storage_state === "unresolved").map((asset) => `unresolved_current_attempt_media:${asset.asset_id}`),
4434
+ ...diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => `${diagnostic.code}${diagnostic.asset_id ? `:${diagnostic.asset_id}` : ""}`)
4435
+ ];
4436
+ if (strictErrors.length > 0) {
4437
+ throw new LineageSelectionPacketError(
4438
+ `Lineage selection packet strict mode failed: ${strictErrors.join(", ")}`,
4439
+ warnings,
4440
+ strictErrors,
4441
+ diagnostics
4442
+ );
4443
+ }
4444
+ }
4445
+ const packet = {
4446
+ assets,
4447
+ context: {
4448
+ campaign: options.campaign,
4449
+ channel: options.channel,
4450
+ labels: options.labels || [],
4451
+ notes: options.contextNotes
4452
+ },
4453
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
4454
+ diagnostics,
4455
+ errors: [...new Set(errors)],
4456
+ identity_sha256: "",
4457
+ kind: "lineage.selection_packet",
4458
+ packet_id: "",
4459
+ product: catalogSummary.product,
4460
+ project,
4461
+ schema_version: "lineage.selection_packet.v2",
4462
+ selection: {
4463
+ asset_ids: selectedItems.map((item) => item.asset_id),
4464
+ count: selectedItems.length,
4465
+ items: selectedItems,
4466
+ root_asset_id: workspace.root_asset_id
4467
+ },
4468
+ source: {
4469
+ app: "lineage",
4470
+ command: options.command,
4471
+ db_path: options.dbPath || process.env.LINEAGE_DB || lineageDbPath(),
4472
+ package: options.packageVersion
4473
+ },
4474
+ warnings: [...new Set(warnings)],
4475
+ workspace: {
4476
+ active_at: workspace.active_at,
4477
+ id: workspace.id,
4478
+ notes: workspace.notes,
4479
+ root_asset_id: workspace.root_asset_id,
4480
+ status: workspace.status,
4481
+ title: workspace.title
4482
+ }
4483
+ };
4484
+ packet.identity_sha256 = lineageSelectionPacketV2IdentitySha256(packet);
4485
+ packet.packet_id = `lineage_packet_${packet.identity_sha256.slice(0, 24)}`;
4486
+ return packet;
4487
+ }
4488
+ function getLineageSelectionPacket(project, options = {}) {
4489
+ return options.schema === "v2" ? getLineageSelectionPacketV2(project, options) : getLineageSelectionPacketV1(project, options);
4490
+ }
3600
4491
 
3601
4492
  // src/server/runtimeInfo.ts
3602
4493
  import { spawnSync as spawnSync2 } from "node:child_process";
3603
- import { createRequire as createRequire2 } from "node:module";
3604
- import { existsSync as existsSync6, readFileSync as readFileSync3, statSync as statSync5 } from "node:fs";
3605
- import { join as join6 } from "node:path";
3606
- var require3 = createRequire2(import.meta.url);
4494
+ import { createRequire as createRequire4 } from "node:module";
4495
+ import { existsSync as existsSync10, readFileSync as readFileSync5, statSync as statSync6 } from "node:fs";
4496
+ import { join as join9 } from "node:path";
4497
+ var require5 = createRequire4(import.meta.url);
3607
4498
  function packageInfo() {
3608
4499
  try {
3609
- const info = JSON.parse(readFileSync3(join6(packageRoot, "package.json"), "utf8"));
4500
+ const info = JSON.parse(readFileSync5(join9(packageRoot, "package.json"), "utf8"));
3610
4501
  return { name: info.name || "@mean-weasel/lineage", version: info.version || "0.0.0" };
3611
4502
  } catch {
3612
4503
  return { name: "@mean-weasel/lineage", version: "0.0.0" };
@@ -3622,32 +4513,47 @@ function normalizeRuntimeChannel(value) {
3622
4513
  function gitSha() {
3623
4514
  const envSha = process.env.LINEAGE_GIT_SHA || process.env.GITHUB_SHA;
3624
4515
  if (envSha) return envSha.slice(0, 40);
3625
- if (!existsSync6(join6(packageRoot, ".git"))) return void 0;
4516
+ if (!existsSync10(join9(packageRoot, ".git"))) return void 0;
3626
4517
  const result = spawnSync2("git", ["rev-parse", "--short=12", "HEAD"], { cwd: packageRoot, encoding: "utf8" });
3627
4518
  return result.status === 0 ? result.stdout.trim() || void 0 : void 0;
3628
4519
  }
3629
- function tableExists(database, table) {
4520
+ function tableExists2(database, table) {
3630
4521
  return Boolean(database.prepare("select name from sqlite_master where type = 'table' and name = ?").get(table));
3631
4522
  }
3632
4523
  function tableCount(database, table) {
3633
- if (!tableExists(database, table)) return void 0;
4524
+ if (!tableExists2(database, table)) return void 0;
3634
4525
  const row = database.prepare(`select count(*) count from ${table}`).get();
3635
4526
  return typeof row?.count === "number" ? row.count : void 0;
3636
4527
  }
4528
+ function migrationKeys(database) {
4529
+ if (!tableExists2(database, "lineage_schema_migrations")) return [];
4530
+ return database.prepare("select key from lineage_schema_migrations order by key").all().map((row) => row.key);
4531
+ }
3637
4532
  function getLineageRuntimeInfo(options = {}) {
3638
4533
  const info = packageInfo();
3639
4534
  const dbPath = options.dbPath || lineageDbPath();
3640
- const databaseInfo = { exists: existsSync6(dbPath), path: dbPath };
4535
+ const channel = normalizeRuntimeChannel(options.channel || process.env.LINEAGE_CHANNEL || process.env.LINEAGE_RELEASE_CHANNEL);
4536
+ const databaseInfo = { exists: existsSync10(dbPath), path: dbPath };
4537
+ const schema = { migration_keys: [] };
3641
4538
  if (databaseInfo.exists) {
3642
4539
  try {
3643
- const stat = statSync5(dbPath);
4540
+ const stat = statSync6(dbPath);
3644
4541
  databaseInfo.modified_at = stat.mtime.toISOString();
3645
4542
  databaseInfo.size_bytes = stat.size;
3646
- const { DatabaseSync } = require3("node:sqlite");
4543
+ const { DatabaseSync } = require5("node:sqlite");
3647
4544
  const database = new DatabaseSync(dbPath, { readOnly: true });
3648
4545
  try {
3649
4546
  databaseInfo.projects = tableCount(database, "projects");
3650
4547
  databaseInfo.workspaces = tableCount(database, "lineage_workspaces");
4548
+ schema.migration_keys = migrationKeys(database);
4549
+ if (tableExists2(database, "lineage_profile_identity")) {
4550
+ const rows = database.prepare("select profile_id, environment from lineage_profile_identity").all();
4551
+ schema.profile_identity_rows = rows.length;
4552
+ if (rows.length === 1) {
4553
+ schema.profile_id = rows[0].profile_id;
4554
+ schema.profile_environment = rows[0].environment;
4555
+ }
4556
+ }
3651
4557
  } finally {
3652
4558
  database.close();
3653
4559
  }
@@ -3657,27 +4563,113 @@ function getLineageRuntimeInfo(options = {}) {
3657
4563
  }
3658
4564
  return {
3659
4565
  asset_root: repoRoot,
3660
- channel: normalizeRuntimeChannel(options.channel || process.env.LINEAGE_CHANNEL || process.env.LINEAGE_RELEASE_CHANNEL),
4566
+ channel,
3661
4567
  database: databaseInfo,
3662
4568
  fetchedAt: nowIso(),
3663
4569
  git_sha: gitSha(),
3664
4570
  node_env: process.env.NODE_ENV,
3665
4571
  package_name: info.name,
4572
+ profile: runtimeProfileIdentity(channel),
4573
+ schema,
3666
4574
  version: info.version
3667
4575
  };
3668
4576
  }
3669
4577
 
4578
+ // src/server/managedWriterRouting.ts
4579
+ var managedWriterRequestSchemaVersion = "lineage.managed_writer_request.v1";
4580
+ var managedWriterResponseSchemaVersion = "lineage.managed_writer_response.v1";
4581
+ var managedWriterRoute = "/api/managed-writer/execute";
4582
+ var delegationHeader = "X-Lineage-Writer-Delegation";
4583
+ var ManagedWriterRoutingError = class extends Error {
4584
+ constructor(message, status = 409, options) {
4585
+ super(message, options);
4586
+ this.status = status;
4587
+ this.name = "ManagedWriterRoutingError";
4588
+ }
4589
+ status;
4590
+ };
4591
+ function managedWriterTimeoutMs(command, args) {
4592
+ const subcommand = args.find((arg) => !arg.startsWith("-")) || "";
4593
+ return command === "reroll" && subcommand === "import" ? 5 * 6e4 : 3e4;
4594
+ }
4595
+ function errorMessage(body, fallback) {
4596
+ if (!body || typeof body !== "object" || Array.isArray(body)) return fallback;
4597
+ const record = body;
4598
+ if (typeof record.message === "string") return record.message;
4599
+ if (typeof record.error === "string") return record.error;
4600
+ return fallback;
4601
+ }
4602
+ function agentClaimError(body, status) {
4603
+ if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
4604
+ const record = body;
4605
+ if (typeof record.message !== "string" || typeof record.error !== "string") return void 0;
4606
+ const conflicts = Array.isArray(record.conflicts) ? record.conflicts : [];
4607
+ return new AgentClaimError(record.message, status, record.error, conflicts);
4608
+ }
4609
+ function assertResponseIdentity(body, profile, channel) {
4610
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
4611
+ throw new ManagedWriterRoutingError("Managed writer service returned a non-object response", 502);
4612
+ }
4613
+ const response = body;
4614
+ if (response.ok !== true || response.schema_version !== managedWriterResponseSchemaVersion || !response.service || response.service.profile_id !== profile.profile_id || response.service.environment !== profile.environment || response.service.service_origin !== profile.service_origin || response.service.channel !== channel) {
4615
+ throw new ManagedWriterRoutingError("Managed writer response identity does not match the selected profile/service", 502);
4616
+ }
4617
+ }
4618
+ async function executeManagedWriterCommand(profile, channel, command, args) {
4619
+ const delegation = getProfileWriterDelegation(profile);
4620
+ let response;
4621
+ try {
4622
+ response = await fetch(new URL(managedWriterRoute, delegation.service_origin), {
4623
+ body: JSON.stringify({
4624
+ args,
4625
+ channel,
4626
+ command,
4627
+ environment: profile.environment,
4628
+ profile_id: profile.profile_id,
4629
+ schema_version: managedWriterRequestSchemaVersion,
4630
+ service_origin: profile.service_origin
4631
+ }),
4632
+ headers: {
4633
+ "Content-Type": "application/json",
4634
+ [delegationHeader]: delegation.token
4635
+ },
4636
+ method: "POST",
4637
+ redirect: "error",
4638
+ signal: AbortSignal.timeout(managedWriterTimeoutMs(command, args))
4639
+ });
4640
+ } catch (error) {
4641
+ throw new ManagedWriterRoutingError(
4642
+ `Managed service for Lineage profile ${profile.profile_id} is unavailable or did not respond at ${profile.service_origin}; no direct fallback was attempted and the mutation outcome is unknown`,
4643
+ 503,
4644
+ { cause: error }
4645
+ );
4646
+ }
4647
+ let body;
4648
+ try {
4649
+ body = await response.json();
4650
+ } catch (error) {
4651
+ throw new ManagedWriterRoutingError("Managed writer service returned invalid JSON; mutation result is unknown", 502, { cause: error });
4652
+ }
4653
+ if (!response.ok) {
4654
+ const claimError = agentClaimError(body, response.status);
4655
+ if (claimError) throw claimError;
4656
+ throw new ManagedWriterRoutingError(errorMessage(body, `Managed writer service returned HTTP ${response.status}`), response.status);
4657
+ }
4658
+ assertResponseIdentity(body, profile, channel);
4659
+ return body.result;
4660
+ }
4661
+
3670
4662
  // src/cli/lineageCli.ts
3671
4663
  var signalExitCodes = {
3672
4664
  SIGINT: 130,
3673
4665
  SIGTERM: 143
3674
4666
  };
3675
4667
  function packageRoot2() {
3676
- return resolve5(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
4668
+ return resolve8(dirname5(fileURLToPath2(import.meta.url)), "..", "..");
3677
4669
  }
3678
4670
  function packageVersion() {
3679
4671
  try {
3680
- const packageInfo2 = JSON.parse(readFileSync4(join7(packageRoot2(), "package.json"), "utf8"));
4672
+ const packageInfo2 = JSON.parse(readFileSync6(join10(packageRoot2(), "package.json"), "utf8"));
3681
4673
  return packageInfo2.version || "0.0.0";
3682
4674
  } catch {
3683
4675
  return "0.0.0";
@@ -3685,9 +4677,9 @@ function packageVersion() {
3685
4677
  }
3686
4678
  function dataRoot(displayName) {
3687
4679
  if (process.env.LINEAGE_HOME) return process.env.LINEAGE_HOME;
3688
- if (platform() === "darwin") return join7(homedir(), "Library", "Application Support", displayName);
3689
- if (platform() === "win32") return join7(process.env.APPDATA || join7(homedir(), "AppData", "Roaming"), displayName);
3690
- return join7(process.env.XDG_DATA_HOME || join7(homedir(), ".local", "share"), displayName.toLowerCase().replace(/\s+/g, "-"));
4680
+ if (platform2() === "darwin") return join10(homedir2(), "Library", "Application Support", displayName);
4681
+ if (platform2() === "win32") return join10(process.env.APPDATA || join10(homedir2(), "AppData", "Roaming"), displayName);
4682
+ return join10(process.env.XDG_DATA_HOME || join10(homedir2(), ".local", "share"), displayName.toLowerCase().replace(/\s+/g, "-"));
3691
4683
  }
3692
4684
  function readOption(args, name) {
3693
4685
  const prefix = `${name}=`;
@@ -3711,22 +4703,37 @@ function readOptions(args, name) {
3711
4703
  return values;
3712
4704
  }
3713
4705
  function resolveStartOptions(config, args) {
3714
- const rawPort = readOption(args, "--port") || process.env.PORT || String(config.defaultPort);
4706
+ const profile = prepareCliProfile(config, args);
4707
+ const serviceUrl = profile ? new URL(profile.service_origin) : void 0;
4708
+ const rawPort = readOption(args, "--port") || process.env.PORT || serviceUrl?.port || String(config.defaultPort);
3715
4709
  const port = Number(rawPort);
3716
4710
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
3717
4711
  throw new Error(`Invalid port: ${rawPort}`);
3718
4712
  }
4713
+ const profileHost = serviceUrl ? normalizeListenHost(serviceUrl.hostname) : void 0;
4714
+ const host = normalizeListenHost(readOption(args, "--host") || process.env.HOST || profileHost || config.defaultHost);
4715
+ if (profile && serviceUrl && (host !== profileHost || port !== Number(serviceUrl.port || 80))) {
4716
+ throw new Error(`Profile ${profile.profile_id} service_origin ${profile.service_origin} conflicts with requested host/port ${host}:${port}`);
4717
+ }
3719
4718
  return {
3720
- assetRoot: resolveCliAssetRoot(args),
3721
- dbPath: resolveCliDbPath(config, args),
3722
- host: readOption(args, "--host") || process.env.HOST || config.defaultHost,
4719
+ assetRoot: profile?.asset_root || resolveCliAssetRoot(args),
4720
+ dbPath: profile?.database_path || resolveCliDbPath(config, args),
4721
+ host,
3723
4722
  json: args.includes("--json"),
3724
4723
  open: args.includes("--open"),
3725
- port
4724
+ port,
4725
+ profile
3726
4726
  };
3727
4727
  }
4728
+ function normalizeListenHost(host) {
4729
+ return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
4730
+ }
4731
+ function lineageServiceUrl(host, port) {
4732
+ const urlHost = host.includes(":") ? `[${normalizeListenHost(host)}]` : host;
4733
+ return `http://${urlHost}:${port}`;
4734
+ }
3728
4735
  function resolveCliAssetRoot(args) {
3729
- return resolve5(readOption(args, "--asset-root") || process.env.LINEAGE_ASSET_ROOT || packageRoot);
4736
+ return resolve8(readOption(args, "--asset-root") || process.env.LINEAGE_ASSET_ROOT || packageRoot);
3730
4737
  }
3731
4738
  function configureCliAssetRoot(args) {
3732
4739
  const assetRoot = resolveCliAssetRoot(args);
@@ -3734,17 +4741,68 @@ function configureCliAssetRoot(args) {
3734
4741
  return setLineageAssetRoot(assetRoot);
3735
4742
  }
3736
4743
  function resolveCliDbPath(config, args) {
3737
- return readOption(args, "--db") || process.env.LINEAGE_DB || join7(dataRoot(config.displayName), `${config.binName}.sqlite`);
4744
+ return readOption(args, "--db") || process.env.LINEAGE_DB || join10(dataRoot(config.displayName), `${config.binName}.sqlite`);
4745
+ }
4746
+ function hasOption(args, name) {
4747
+ return args.includes(name) || args.some((arg) => arg.startsWith(`${name}=`));
4748
+ }
4749
+ function profileSelector(args) {
4750
+ const option = readOption(args, "--profile");
4751
+ if (hasOption(args, "--profile") && !option?.trim()) {
4752
+ throw new Error("--profile requires a non-empty profile ID or manifest path");
4753
+ }
4754
+ if (option && process.env.LINEAGE_PROFILE) {
4755
+ const optionProfile = resolveLineageProfile(option);
4756
+ const environmentProfile = resolveLineageProfile(process.env.LINEAGE_PROFILE);
4757
+ if (optionProfile.manifest_path !== environmentProfile.manifest_path) {
4758
+ throw new Error(`--profile ${option} conflicts with LINEAGE_PROFILE ${process.env.LINEAGE_PROFILE}`);
4759
+ }
4760
+ }
4761
+ return option || process.env.LINEAGE_PROFILE;
4762
+ }
4763
+ function doctorFailures(result) {
4764
+ return result.checks.filter((check) => check.status === "fail").map((check) => `${check.id}: ${check.message}`).join("; ");
4765
+ }
4766
+ function prepareCliProfile(config, args) {
4767
+ const selector = profileSelector(args);
4768
+ if (!selector) {
4769
+ assertUnselectedDatabaseIsUnbound(getLineageRuntimeInfo({ channel: config.channel, dbPath: resolveCliDbPath(config, args) }));
4770
+ return void 0;
4771
+ }
4772
+ if (hasOption(args, "--db")) throw new Error("A named profile cannot be combined with --db");
4773
+ if (hasOption(args, "--asset-root")) throw new Error("A named profile cannot be combined with --asset-root");
4774
+ const profile = resolveLineageProfile(selector);
4775
+ if (process.env.LINEAGE_DB && resolve8(process.env.LINEAGE_DB) !== profile.database_path) {
4776
+ throw new Error(`Profile ${profile.profile_id} database_path conflicts with LINEAGE_DB`);
4777
+ }
4778
+ if (process.env.LINEAGE_ASSET_ROOT && resolve8(process.env.LINEAGE_ASSET_ROOT) !== profile.asset_root) {
4779
+ throw new Error(`Profile ${profile.profile_id} asset_root conflicts with LINEAGE_ASSET_ROOT`);
4780
+ }
4781
+ assertProfileChannel(profile, config.channel);
4782
+ const runtime = getLineageRuntimeInfo({ channel: config.channel, dbPath: profile.database_path });
4783
+ const doctor = doctorLineageProfile(selector, { channel: config.channel, gitSha: runtime.git_sha, version: runtime.version });
4784
+ if (!doctor.ok) throw new Error(`Profile ${profile.profile_id} failed doctor: ${doctorFailures(doctor)}`);
4785
+ process.env.LINEAGE_ASSET_ROOT = profile.asset_root;
4786
+ process.env.LINEAGE_DB = profile.database_path;
4787
+ process.env.LINEAGE_PROFILE = selector;
4788
+ process.env.LINEAGE_PROFILE_ENVIRONMENT = profile.environment;
4789
+ process.env.LINEAGE_PROFILE_ID = profile.profile_id;
4790
+ process.env.LINEAGE_PROFILE_MANIFEST = profile.manifest_path;
4791
+ process.env.LINEAGE_PROFILE_SERVICE_ORIGIN = profile.service_origin;
4792
+ setLineageAssetRoot(profile.asset_root);
4793
+ return profile;
3738
4794
  }
3739
4795
  function formatLineageHelp(config) {
3740
4796
  return `${config.binName} ${packageVersion()}
3741
4797
 
3742
4798
  Usage:
3743
- ${config.binName} start [--port <port>] [--host <host>] [--db <path>] [--asset-root <path>] [--open] [--json]
4799
+ ${config.binName} start [--profile <id-or-manifest>] [--port <port>] [--host <host>] [--db <path>] [--asset-root <path>] [--open] [--json]
4800
+ ${config.binName} profile doctor --profile <id-or-manifest> [--json]
4801
+ ${config.binName} profile bind --profile <id-or-manifest> [--confirm-write] [--json]
3744
4802
  ${config.binName} next [--project <project>] [--root <asset-id>] [--db <path>] [--json]
3745
4803
  ${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
3746
4804
  ${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
3747
- ${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]
4805
+ ${config.binName} selection packet [--project <project>] [--workspace <id-or-root>|--root <asset-id>] [--channel <channel>] [--campaign <campaign>] [--context-notes <text>] [--label <label>] [--schema v2] [--out <path>] [--strict] [--db <path>] [--json]
3748
4806
  ${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
3749
4807
  ${config.binName} reroll list --root <asset-id> [--project <project>] [--db <path>] [--json]
3750
4808
  ${config.binName} reroll mark --root <asset-id> --target <asset-id> [--notes <text>] [--requested-by agent|human|system] [--project <project>] [--confirm-write] [--db <path>] [--json]
@@ -3776,6 +4834,11 @@ ${config.displayName} runs the bundled Lineage server for the ${config.channel}
3776
4834
  Asset catalogs and local media default to the installed package root. Pass
3777
4835
  --asset-root <path> or LINEAGE_ASSET_ROOT to use an external project root.
3778
4836
 
4837
+ All commands accept --profile <id-or-manifest> or LINEAGE_PROFILE. Named
4838
+ profiles are authoritative for database, asset root, environment, and origin;
4839
+ they cannot be combined with direct --db or --asset-root overrides. Commands
4840
+ without a profile run in legacy-unbound compatibility mode.
4841
+
3779
4842
  Variation vs re-roll:
3780
4843
  link-child creates a new visible child variation edge.
3781
4844
  reroll mark -> reroll plan -> reroll import updates the same node with a new attempt.`;
@@ -3784,8 +4847,8 @@ function printHelp(config) {
3784
4847
  console.log(formatLineageHelp(config));
3785
4848
  }
3786
4849
  function openBrowser(url) {
3787
- const command = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
3788
- const args = platform() === "win32" ? ["/c", "start", "", url] : [url];
4850
+ const command = platform2() === "darwin" ? "open" : platform2() === "win32" ? "cmd" : "xdg-open";
4851
+ const args = platform2() === "win32" ? ["/c", "start", "", url] : [url];
3789
4852
  const opener = spawn(command, args, { detached: true, stdio: "ignore" });
3790
4853
  opener.unref();
3791
4854
  }
@@ -3828,6 +4891,8 @@ function runLineageDataCommand(command, args) {
3828
4891
  if (command === "selection") {
3829
4892
  const subcommand = positionalArgs(args)[0] || "";
3830
4893
  if (subcommand !== "packet") throw new Error(`Unknown selection command: ${subcommand}`);
4894
+ const schema = readOption(args, "--schema");
4895
+ if (schema && schema !== "v2") throw new Error(`Unsupported selection packet schema: ${schema}. Omit --schema for v1 or pass --schema v2.`);
3831
4896
  const labels = readOptions(args, "--label").flatMap((label) => label.split(",")).map((label) => label.trim()).filter(Boolean);
3832
4897
  const packet = getLineageSelectionPacket(options.project, {
3833
4898
  campaign: readOption(args, "--campaign"),
@@ -3838,14 +4903,15 @@ function runLineageDataCommand(command, args) {
3838
4903
  labels,
3839
4904
  packageVersion: packageVersion(),
3840
4905
  rootAssetId: options.rootAssetId,
4906
+ schema: schema === "v2" ? "v2" : void 0,
3841
4907
  strict: args.includes("--strict"),
3842
4908
  workspaceId: readOption(args, "--workspace") || readOption(args, "--workspace-id")
3843
4909
  });
3844
4910
  const out = readOption(args, "--out");
3845
4911
  if (out) {
3846
- const outPath = resolve5(out);
3847
- mkdirSync4(dirname3(outPath), { recursive: true });
3848
- writeFileSync2(outPath, `${JSON.stringify(packet, null, 2)}
4912
+ const outPath = resolve8(out);
4913
+ mkdirSync6(dirname5(outPath), { recursive: true });
4914
+ writeFileSync3(outPath, `${JSON.stringify(packet, null, 2)}
3849
4915
  `);
3850
4916
  }
3851
4917
  return packet;
@@ -4055,6 +5121,64 @@ function runLineageDbCommand(config, command, args) {
4055
5121
  if (command === "info") return getLineageRuntimeInfo({ channel: config.channel, dbPath });
4056
5122
  throw new Error(`Unknown db command: ${command}`);
4057
5123
  }
5124
+ function runLineageProfileCommand(config, command, args) {
5125
+ if (command !== "doctor" && command !== "bind") throw new Error(`Unknown profile command: ${command}`);
5126
+ if (hasOption(args, "--db")) throw new Error(`Profile ${command} cannot be combined with --db`);
5127
+ if (hasOption(args, "--asset-root")) throw new Error(`Profile ${command} cannot be combined with --asset-root`);
5128
+ const selector = profileSelector(args);
5129
+ if (!selector) throw new Error(`lineage profile ${command} requires --profile or LINEAGE_PROFILE`);
5130
+ if (command === "bind") {
5131
+ const profile = resolveLineageProfile(selector);
5132
+ assertProfileChannel(profile, config.channel);
5133
+ if (!args.includes("--confirm-write")) return bindLineageProfileDatabase(profile, false);
5134
+ mkdirSync6(dirname5(profile.database_path), { recursive: true });
5135
+ const writerLease = acquireProfileWriterLease(profile, config.channel, "cli");
5136
+ try {
5137
+ return bindLineageProfileDatabase(profile, true);
5138
+ } finally {
5139
+ writerLease.release();
5140
+ }
5141
+ }
5142
+ const runtime = getLineageRuntimeInfo({ channel: config.channel });
5143
+ return doctorLineageProfile(selector, { channel: config.channel, gitSha: runtime.git_sha, version: runtime.version });
5144
+ }
5145
+ function printProfileResult(result, json) {
5146
+ if (json) {
5147
+ console.log(JSON.stringify(result, null, 2));
5148
+ return;
5149
+ }
5150
+ if (result.schema_version === "lineage.profile_bind.v1") {
5151
+ console.log(`${result.dryRun ? "Would bind" : result.already_bound ? "Already bound" : "Bound"} ${result.profile_id} to ${result.database_path}`);
5152
+ return;
5153
+ }
5154
+ console.log(`Profile doctor: ${result.ok ? "ok" : "failed"}`);
5155
+ for (const check of result.checks) console.log(`${check.status.toUpperCase()} ${check.id}: ${check.message}`);
5156
+ }
5157
+ function lineageProfileDoctorExitCode(result) {
5158
+ return result.ok ? 0 : 1;
5159
+ }
5160
+ function lineageCliRequiresWriterLease(command, args) {
5161
+ if (command === "next" || command === "brief" || command === "inspect" || command === "selection") return false;
5162
+ const subcommand = positionalArgs(args)[0] || "";
5163
+ if (command === "reroll") return subcommand !== "list";
5164
+ if (command === "tasks") return subcommand !== "list" && subcommand !== "inspect";
5165
+ if (command === "db") return subcommand !== "info";
5166
+ if (command === "agent") return subcommand !== "status" && subcommand !== "graph" && subcommand !== "inspect";
5167
+ return true;
5168
+ }
5169
+ function argsWithoutProfileSelector(args) {
5170
+ const filtered = [];
5171
+ for (let index = 0; index < args.length; index += 1) {
5172
+ const arg = args[index];
5173
+ if (arg === "--profile") {
5174
+ index += 1;
5175
+ continue;
5176
+ }
5177
+ if (arg.startsWith("--profile=")) continue;
5178
+ filtered.push(arg);
5179
+ }
5180
+ return filtered;
5181
+ }
4058
5182
  function printDbResult(command, result, json) {
4059
5183
  if (json) {
4060
5184
  console.log(JSON.stringify(result, null, 2));
@@ -4205,19 +5329,21 @@ function start(config, args) {
4205
5329
  else console.error(`${config.binName}: ${message}`);
4206
5330
  process.exit(1);
4207
5331
  }
4208
- const serverPath = join7(packageRoot2(), "dist", "server.js");
4209
- if (!existsSync7(serverPath)) {
5332
+ const serverPath = join10(packageRoot2(), "dist", "server.js");
5333
+ if (!existsSync11(serverPath)) {
4210
5334
  const message = `Missing bundled server at ${serverPath}. Run npm run build before using ${config.binName} start from a source checkout.`;
4211
5335
  if (options.json) console.error(JSON.stringify({ ok: false, error: message }, null, 2));
4212
5336
  else console.error(`${config.binName}: ${message}`);
4213
5337
  process.exit(1);
4214
5338
  }
4215
- mkdirSync4(dirname3(options.dbPath), { recursive: true });
4216
- const url = `http://${options.host}:${options.port}`;
5339
+ mkdirSync6(dirname5(options.dbPath), { recursive: true });
5340
+ const url = lineageServiceUrl(options.host, options.port);
4217
5341
  if (options.json) {
4218
- console.log(JSON.stringify({ assetRoot: options.assetRoot, channel: config.channel, dbPath: options.dbPath, host: options.host, port: options.port, status: "starting", url }, null, 2));
5342
+ console.log(JSON.stringify({ assetRoot: options.assetRoot, channel: config.channel, dbPath: options.dbPath, host: options.host, port: options.port, profile: options.profile ? { environment: options.profile.environment, id: options.profile.profile_id } : { bound: false, id: "legacy-unbound" }, status: "starting", url }, null, 2));
4219
5343
  } else {
4220
5344
  console.log(`${config.displayName} starting at ${url}`);
5345
+ if (options.profile) console.log(`Profile: ${options.profile.profile_id} (${options.profile.environment})`);
5346
+ else console.warn("Warning: legacy-unbound runtime; database and asset paths are not protected by a named profile.");
4221
5347
  console.log(`SQLite: ${options.dbPath}`);
4222
5348
  console.log(`Assets: ${options.assetRoot}`);
4223
5349
  }
@@ -4229,6 +5355,12 @@ function start(config, args) {
4229
5355
  LINEAGE_ASSET_ROOT: options.assetRoot,
4230
5356
  LINEAGE_CHANNEL: config.channel,
4231
5357
  LINEAGE_DB: options.dbPath,
5358
+ LINEAGE_PROFILE: options.profile ? process.env.LINEAGE_PROFILE : void 0,
5359
+ LINEAGE_PROFILE_ENVIRONMENT: options.profile?.environment,
5360
+ LINEAGE_PROFILE_ID: options.profile?.profile_id,
5361
+ LINEAGE_PROFILE_MANIFEST: options.profile?.manifest_path,
5362
+ LINEAGE_PROFILE_SERVICE_ORIGIN: options.profile?.service_origin,
5363
+ LINEAGE_DB_ACCESS: void 0,
4232
5364
  NODE_ENV: "production",
4233
5365
  PORT: String(options.port)
4234
5366
  },
@@ -4247,7 +5379,8 @@ function start(config, args) {
4247
5379
  process.exit(1);
4248
5380
  });
4249
5381
  }
4250
- function runLineageCli(config, args = process.argv.slice(2)) {
5382
+ async function runLineageCli(config, args = process.argv.slice(2)) {
5383
+ process.env.LINEAGE_CHANNEL = config.channel;
4251
5384
  if (args.includes("--help") || args.includes("-h") || args.length === 0) {
4252
5385
  printHelp(config);
4253
5386
  process.exit(0);
@@ -4262,11 +5395,52 @@ function runLineageCli(config, args = process.argv.slice(2)) {
4262
5395
  start(config, normalizedArgs.slice(1));
4263
5396
  return;
4264
5397
  }
5398
+ if (command === "profile") {
5399
+ const commandArgs = normalizedArgs.slice(2);
5400
+ const profileCommand = normalizedArgs[1] || "";
5401
+ const json2 = commandArgs.includes("--json");
5402
+ try {
5403
+ const result = runLineageProfileCommand(config, profileCommand, commandArgs);
5404
+ printProfileResult(result, json2);
5405
+ process.exit(lineageProfileDoctorExitCode(result));
5406
+ } catch (error) {
5407
+ const message2 = error instanceof Error ? error.message : String(error);
5408
+ if (json2) console.error(JSON.stringify({ ok: false, command: `profile ${profileCommand}`, error: message2 }, null, 2));
5409
+ else console.error(`${config.binName}: ${message2}`);
5410
+ process.exit(1);
5411
+ }
5412
+ }
5413
+ let managedWriterProfile;
5414
+ try {
5415
+ const profile = prepareCliProfile(config, normalizedArgs.slice(1));
5416
+ if (profile) {
5417
+ if (lineageCliRequiresWriterLease(command, normalizedArgs.slice(1))) {
5418
+ delete process.env.LINEAGE_DB_ACCESS;
5419
+ try {
5420
+ const writerLease = acquireProfileWriterLease(profile, config.channel, "cli");
5421
+ process.once("exit", writerLease.release);
5422
+ } catch (error) {
5423
+ if (!(error instanceof ProfileWriterLeaseConflictError) || error.owner.role !== "service") throw error;
5424
+ process.env.LINEAGE_DB_ACCESS = "read-only";
5425
+ managedWriterProfile = profile;
5426
+ }
5427
+ } else {
5428
+ process.env.LINEAGE_DB_ACCESS = "read-only";
5429
+ }
5430
+ }
5431
+ } catch (error) {
5432
+ const message2 = error instanceof Error ? error.message : String(error);
5433
+ const json2 = normalizedArgs.includes("--json");
5434
+ if (json2) console.error(JSON.stringify({ ok: false, command, error: message2 }, null, 2));
5435
+ else console.error(`${config.binName}: ${message2}`);
5436
+ process.exit(1);
5437
+ }
4265
5438
  if (command === "next" || command === "brief" || command === "inspect" || command === "selection" || command === "link-child" || command === "reroll" || command === "tasks") {
4266
5439
  const commandArgs = normalizedArgs.slice(1);
4267
5440
  const json2 = commandArgs.includes("--json");
4268
5441
  try {
4269
- printDataResult(command, runLineageDataCommand(command, commandArgs), json2);
5442
+ const result = managedWriterProfile ? await executeManagedWriterCommand(managedWriterProfile, config.channel, command, argsWithoutProfileSelector(commandArgs)) : runLineageDataCommand(command, commandArgs);
5443
+ printDataResult(command, result, json2);
4270
5444
  } catch (error) {
4271
5445
  const message2 = redactAgentClaimTokens(error instanceof Error ? error.message : String(error));
4272
5446
  if (json2) {
@@ -4296,7 +5470,13 @@ function runLineageCli(config, args = process.argv.slice(2)) {
4296
5470
  const agentCommand = normalizedArgs[1] || "";
4297
5471
  const json2 = commandArgs.includes("--json");
4298
5472
  try {
4299
- printAgentResult(agentCommand, runLineageAgentCommand(agentCommand, commandArgs), json2);
5473
+ const result = managedWriterProfile ? await executeManagedWriterCommand(
5474
+ managedWriterProfile,
5475
+ config.channel,
5476
+ "agent",
5477
+ argsWithoutProfileSelector([agentCommand, ...commandArgs])
5478
+ ) : runLineageAgentCommand(agentCommand, commandArgs);
5479
+ printAgentResult(agentCommand, result, json2);
4300
5480
  } catch (error) {
4301
5481
  const message2 = redactAgentClaimTokens(error instanceof Error ? error.message : String(error));
4302
5482
  if (json2) {
@@ -4317,5 +5497,5 @@ function runLineageCli(config, args = process.argv.slice(2)) {
4317
5497
  }
4318
5498
 
4319
5499
  // src/cli/lineage.ts
4320
- runLineageCli({ binName: "lineage", channel: "stable", defaultHost: "lineage.localhost", defaultPort: 5197, displayName: "Lineage" });
5500
+ await runLineageCli({ binName: "lineage", channel: "stable", defaultHost: "lineage.localhost", defaultPort: 5197, displayName: "Lineage" });
4321
5501
  //# sourceMappingURL=lineage.js.map