midsummer-sol 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/index.js +200 -6
  2. package/package.json +1 -1
  3. package/sol-mcp.js +1743 -164
  4. package/sol.js +2240 -299
package/sol-mcp.js CHANGED
@@ -60,6 +60,11 @@ function verifyAtt(key, op, sig = op.att) {
60
60
  var init_attest = () => {};
61
61
 
62
62
  // src/errors.ts
63
+ var exports_errors = {};
64
+ __export(exports_errors, {
65
+ CorruptRepoError: () => CorruptRepoError,
66
+ CorruptObjectError: () => CorruptObjectError
67
+ });
63
68
  var CorruptRepoError, CorruptObjectError;
64
69
  var init_errors = __esm(() => {
65
70
  CorruptRepoError = class CorruptRepoError extends Error {
@@ -822,16 +827,74 @@ var init_sign = __esm(() => {
822
827
  });
823
828
 
824
829
  // src/prov.ts
830
+ function encodeTool(p) {
831
+ const surface = p.tool?.trim() || "";
832
+ if (!p.toolCall && !p.testRunId)
833
+ return surface || undefined;
834
+ const segs = [surface, TC_TAG];
835
+ const tc = p.toolCall;
836
+ segs.push(`name=${tc?.name ?? ""}`, `args=${tc?.argsHash ?? ""}`, `inv=${tc?.invocationId ?? ""}`, `test=${p.testRunId ?? ""}`);
837
+ return segs.join(TC_SEP);
838
+ }
839
+ function decodeTool(tool) {
840
+ if (!tool)
841
+ return {};
842
+ const parts = tool.split(TC_SEP);
843
+ if (parts.length < 2 || parts[1] !== TC_TAG)
844
+ return { tool: tool || undefined };
845
+ const surface = parts[0] || undefined;
846
+ const kv = new Map;
847
+ for (const seg of parts.slice(2)) {
848
+ const eq = seg.indexOf("=");
849
+ if (eq >= 0)
850
+ kv.set(seg.slice(0, eq), seg.slice(eq + 1));
851
+ }
852
+ const name = kv.get("name") || "";
853
+ const argsHash = kv.get("args") || "";
854
+ const invocationId = kv.get("inv") || "";
855
+ const testRunId = kv.get("test") || undefined;
856
+ const out = { tool: surface };
857
+ if (name || argsHash || invocationId)
858
+ out.toolCall = { name, argsHash, invocationId };
859
+ if (testRunId)
860
+ out.testRunId = testRunId;
861
+ return out;
862
+ }
863
+ function parseToolCall(raw) {
864
+ if (!raw)
865
+ return;
866
+ let v;
867
+ try {
868
+ v = JSON.parse(raw);
869
+ } catch {
870
+ return;
871
+ }
872
+ if (!v || typeof v !== "object")
873
+ return;
874
+ const o = v;
875
+ const name = typeof o.name === "string" ? o.name : "";
876
+ const argsHash = typeof o.argsHash === "string" ? o.argsHash : "";
877
+ const invocationId = typeof o.invocationId === "string" ? o.invocationId : "";
878
+ if (!name || !argsHash || !invocationId)
879
+ return;
880
+ return { name, argsHash, invocationId };
881
+ }
825
882
  function provenanceFromEnv(env = process.env) {
826
883
  const kind = env.SOL_KIND === "agent" ? "agent" : "human";
884
+ const tool = encodeTool({
885
+ tool: env.SOL_TOOL,
886
+ toolCall: parseToolCall(env.SOL_TOOL_CALL),
887
+ testRunId: env.SOL_TEST_RUN_ID || undefined
888
+ });
827
889
  const node = buildProvNode({
828
890
  agentId: env.SOL_AGENT_ID || (kind === "agent" ? env.SOL_ACTOR : undefined),
829
891
  sessionId: env.SOL_SESSION,
830
892
  model: env.SOL_MODEL,
831
893
  intent: env.SOL_INTENT,
832
- tool: env.SOL_TOOL
894
+ tool
833
895
  });
834
- return node ? { kind, node } : { kind };
896
+ const opRunId = env.SOL_OPERATION_ID || undefined;
897
+ return { kind, ...node ? { node } : {}, ...opRunId ? { opRunId } : {} };
835
898
  }
836
899
  function buildProvNode(fields) {
837
900
  const clean = { kind: "prov" };
@@ -860,6 +923,9 @@ function authorLabel(op, reader) {
860
923
  const bits = [];
861
924
  if (p?.sessionId)
862
925
  bits.push(`session ${p.sessionId}`);
926
+ const tp = p ? decodeTool(p.tool) : {};
927
+ if (tp.toolCall)
928
+ bits.push(`via ${tp.toolCall.name}`);
863
929
  const tail = p?.intent ? ` — intent: ${p.intent}` : "";
864
930
  return `${who}${bits.length ? ` (${bits.join(", ")})` : ""}${tail}`;
865
931
  }
@@ -869,10 +935,21 @@ function provJson(op, reader, key, trust) {
869
935
  return;
870
936
  const out = { kind: op.kind ?? "human" };
871
937
  if (p) {
872
- for (const k of ["agentId", "sessionId", "model", "intent", "tool", "rationale", "parent"])
938
+ for (const k of ["agentId", "sessionId", "model", "intent", "rationale", "parent"])
873
939
  if (p[k])
874
940
  out[k] = p[k];
875
941
  }
942
+ if (p?.tool) {
943
+ const tp = decodeTool(p.tool);
944
+ if (tp.tool)
945
+ out.tool = tp.tool;
946
+ if (tp.toolCall)
947
+ out.toolCall = tp.toolCall;
948
+ if (tp.testRunId)
949
+ out.testRunId = tp.testRunId;
950
+ }
951
+ if (op.opRunId !== undefined)
952
+ out.opRunId = op.opRunId;
876
953
  if (op.sig !== undefined) {
877
954
  const t = trustAuthor(op, trust);
878
955
  out.signed = true;
@@ -930,7 +1007,7 @@ function signTrailerValue(op, trust) {
930
1007
  return `${t.fingerprint} verified`;
931
1008
  }
932
1009
  }
933
- function provTrailers(op, reader, key, trust) {
1010
+ function provTrailers(op, reader, key, trust, reviewCount) {
934
1011
  const p = reader ? provOf(op, reader) : undefined;
935
1012
  const lines = [];
936
1013
  const signed = trustAuthor(op, trust);
@@ -944,6 +1021,13 @@ function provTrailers(op, reader, key, trust) {
944
1021
  lines.push(`Sol-Session: ${p.sessionId}`);
945
1022
  if (p?.model)
946
1023
  lines.push(`Sol-Model: ${p.model}`);
1024
+ const tp = decodeTool(p?.tool);
1025
+ if (tp.toolCall)
1026
+ lines.push(`Sol-ToolCall: ${tp.toolCall.name} args=${tp.toolCall.argsHash} inv=${tp.toolCall.invocationId}`);
1027
+ if (tp.testRunId)
1028
+ lines.push(`Sol-TestRun: ${tp.testRunId}`);
1029
+ if (op.opRunId)
1030
+ lines.push(`Sol-Operation: ${op.opRunId}`);
947
1031
  const intent = p?.intent || (isAgent ? op.message?.trim() || undefined : undefined);
948
1032
  if (intent)
949
1033
  lines.push(`Sol-Intent: ${intent}`);
@@ -952,17 +1036,22 @@ function provTrailers(op, reader, key, trust) {
952
1036
  lines.push(`Sol-Sign: ${sign}`);
953
1037
  if (op.att !== undefined && !(key && !verifyAtt(key, op)))
954
1038
  lines.push(`Sol-Attested: pushedBy=${op.pushedBy ?? "?"}`);
1039
+ if (reviewCount && reviewCount > 0)
1040
+ lines.push(`Sol-Attestations: ${reviewCount}`);
955
1041
  return lines;
956
1042
  }
957
1043
  async function captureProv(sink, env = process.env) {
958
- const { kind, node } = provenanceFromEnv(env);
1044
+ const { kind, node, opRunId } = provenanceFromEnv(env);
959
1045
  const out = {};
960
1046
  if (kind === "agent")
961
1047
  out.kind = "agent";
962
1048
  if (node)
963
1049
  out.prov = await sink.put(node);
1050
+ if (opRunId)
1051
+ out.opRunId = opRunId;
964
1052
  return out;
965
1053
  }
1054
+ var TC_SEP = "\x01", TC_TAG = "tc1";
966
1055
  var init_prov = __esm(() => {
967
1056
  init_attest();
968
1057
  init_sign();
@@ -994,10 +1083,25 @@ class AsyncRepo {
994
1083
  await this.log.append(by === this.actor ? this.sign(entry) : entry);
995
1084
  }
996
1085
  provFor;
1086
+ pendingToolCall;
1087
+ setToolCall(tc) {
1088
+ this.pendingToolCall = tc;
1089
+ }
997
1090
  async provenance(by) {
998
1091
  if (by !== this.actor)
999
1092
  return {};
1000
- return this.provFor ??= captureProv(this.store);
1093
+ const base = await (this.provFor ??= captureProv(this.store));
1094
+ const tc = this.pendingToolCall;
1095
+ if (!tc)
1096
+ return base;
1097
+ this.pendingToolCall = undefined;
1098
+ const { kind, node } = provenanceFromEnv();
1099
+ const session = node ?? { kind: "prov" };
1100
+ const surface = decodeTool(session.tool);
1101
+ const tool = encodeTool({ tool: surface.tool, toolCall: tc, testRunId: surface.testRunId });
1102
+ const merged = { ...session, kind: "prov", tool };
1103
+ const prov = await this.store.put(merged);
1104
+ return { ...kind === "agent" ? { kind: "agent" } : {}, prov, ...base.opRunId ? { opRunId: base.opRunId } : {} };
1001
1105
  }
1002
1106
  async currentRoot() {
1003
1107
  const head = await this.log.head();
@@ -1305,6 +1409,9 @@ class SolWorkspace {
1305
1409
  this.actor = actor;
1306
1410
  this.repo = new AsyncRepo(store, log, actor);
1307
1411
  }
1412
+ setToolCall(tc) {
1413
+ this.repo.setToolCall(tc);
1414
+ }
1308
1415
  async write(path, content) {
1309
1416
  return this.repo.writeFile(safePath(path), content);
1310
1417
  }
@@ -2239,6 +2346,15 @@ var init_diff = __esm(() => {
2239
2346
  });
2240
2347
 
2241
2348
  // src/file-store.ts
2349
+ var exports_file_store = {};
2350
+ __export(exports_file_store, {
2351
+ openLocal: () => openLocal,
2352
+ initLocal: () => initLocal,
2353
+ encodeObject: () => encodeObject2,
2354
+ decodeObject: () => decodeObject2,
2355
+ FileStore: () => FileStore2,
2356
+ FileOpLog: () => FileOpLog2
2357
+ });
2242
2358
  import { appendFileSync as appendFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
2243
2359
  import { join as join2 } from "node:path";
2244
2360
  import { gunzipSync as gunzipSync2, gzipSync as gzipSync2 } from "node:zlib";
@@ -2328,6 +2444,17 @@ class FileOpLog2 {
2328
2444
  return pageOps(all, opts);
2329
2445
  }
2330
2446
  }
2447
+ function initLocal(dir, actor = "anon") {
2448
+ const solDir = join2(dir, ".sol");
2449
+ mkdirSync2(solDir, { recursive: true });
2450
+ const store = new FileStore2(solDir);
2451
+ const log = new FileOpLog2(solDir);
2452
+ return { repo: new AsyncRepo(store, log, actor), store, log, solDir };
2453
+ }
2454
+ function openLocal(dir, actor = "anon") {
2455
+ const solDir = join2(dir, ".sol");
2456
+ return new AsyncRepo(new FileStore2(solDir), new FileOpLog2(solDir), actor);
2457
+ }
2331
2458
  var init_file_store2 = __esm(() => {
2332
2459
  init_async_repo();
2333
2460
  init_chain();
@@ -2845,6 +2972,74 @@ var init_keyring_at_rest = __esm(() => {
2845
2972
  });
2846
2973
 
2847
2974
  // src/bin/lib.ts
2975
+ var exports_lib = {};
2976
+ __export(exports_lib, {
2977
+ writeWorkingIndexAt: () => writeWorkingIndexAt,
2978
+ writeWorkingIndex: () => writeWorkingIndex,
2979
+ workingChanges: () => workingChanges,
2980
+ workingChangeRows: () => workingChangeRows,
2981
+ walkFiles: () => walkFiles,
2982
+ walkEntries: () => walkEntries,
2983
+ viewMetaPath: () => viewMetaPath,
2984
+ unresolvedConflictPaths: () => unresolvedConflictPaths,
2985
+ truncateOpLog: () => truncateOpLog,
2986
+ solDir: () => solDir,
2987
+ snapshotTree: () => snapshotTree,
2988
+ snapshotFile: () => snapshotFile,
2989
+ setSealedDecryptor: () => setSealedDecryptor,
2990
+ setOpLogHead: () => setOpLogHead,
2991
+ saveTrust: () => saveTrust,
2992
+ saveRefs: () => saveRefs,
2993
+ saveKeyRing: () => saveKeyRing,
2994
+ revealedHeadForRender: () => revealedHeadForRender,
2995
+ revealedHead: () => revealedHead,
2996
+ resolveRefSoft: () => resolveRefSoft,
2997
+ resolveRef: () => resolveRef,
2998
+ repoRoot: () => repoRoot,
2999
+ repoRel: () => repoRel,
3000
+ refsPath: () => refsPath,
3001
+ readViewMeta: () => readViewMeta,
3002
+ procCwd: () => procCwd,
3003
+ printWorkingDiff: () => printWorkingDiff,
3004
+ printTreeDiff: () => printTreeDiff,
3005
+ persistTree: () => persistTree,
3006
+ parentSolDir: () => parentSolDir,
3007
+ opsDir: () => opsDir,
3008
+ open: () => open,
3009
+ objectsDir: () => objectsDir,
3010
+ materializeTree: () => materializeTree,
3011
+ materializeInto: () => materializeInto,
3012
+ materializeDiff: () => materializeDiff,
3013
+ materialize: () => materialize,
3014
+ lockDir: () => lockDir,
3015
+ loadWorkingIndex: () => loadWorkingIndex,
3016
+ loadTrust: () => loadTrust,
3017
+ loadStore: () => loadStore,
3018
+ loadSigner: () => loadSigner,
3019
+ loadRefs: () => loadRefs,
3020
+ loadKeyRing: () => loadKeyRing,
3021
+ lexists: () => lexists,
3022
+ isWorkingPath: () => isWorkingPath,
3023
+ isIgnored: () => isIgnored,
3024
+ ignorePatterns: () => ignorePatterns,
3025
+ forceIncluded: () => forceIncluded,
3026
+ fileChange: () => fileChange,
3027
+ die: () => die,
3028
+ cwd: () => cwd,
3029
+ countChanges: () => countChanges,
3030
+ conflictHunks: () => conflictHunks,
3031
+ cfgDir: () => cfgDir,
3032
+ blameFile: () => blameFile,
3033
+ appendFileSync: () => appendFileSync3,
3034
+ appendCommit: () => appendCommit,
3035
+ appendCapture: () => appendCapture,
3036
+ allLocalNodes: () => allLocalNodes,
3037
+ addInclude: () => addInclude,
3038
+ actor: () => actor,
3039
+ acquireLock: () => acquireLock,
3040
+ LazyStore: () => LazyStore,
3041
+ DEFAULT_IGNORE: () => DEFAULT_IGNORE
3042
+ });
2848
3043
  import {
2849
3044
  appendFileSync as appendFileSync3,
2850
3045
  chmodSync as chmodSync3,
@@ -2854,6 +3049,7 @@ import {
2854
3049
  readdirSync as readdirSync3,
2855
3050
  readFileSync as readFileSync5,
2856
3051
  readlinkSync,
3052
+ renameSync as renameSync3,
2857
3053
  symlinkSync,
2858
3054
  unlinkSync,
2859
3055
  writeFileSync as writeFileSync5
@@ -2884,6 +3080,9 @@ function parentSolDir(dir = solDir) {
2884
3080
  const m = readViewMeta(dir);
2885
3081
  return m ? resolve(dir, m.parent) : undefined;
2886
3082
  }
3083
+ function cfgDir(dir = solDir) {
3084
+ return parentSolDir(dir) ?? dir;
3085
+ }
2887
3086
  function repoRel(p) {
2888
3087
  return relative(cwd, resolve(procCwd, p));
2889
3088
  }
@@ -3806,6 +4005,35 @@ function setOpLogHead(head) {
3806
4005
  const cur = existsSync5(hf) ? JSON.parse(readFileSync5(hf, "utf8")) : { seq: 0 };
3807
4006
  writeFileSync5(hf, JSON.stringify({ ...cur, head }));
3808
4007
  }
4008
+ function truncateOpLog(toSeq) {
4009
+ const opsFile = join5(solDir, "ops.jsonl");
4010
+ if (!existsSync5(opsFile))
4011
+ return;
4012
+ const kept = [];
4013
+ let last;
4014
+ for (const l of readFileSync5(opsFile, "utf8").split(`
4015
+ `)) {
4016
+ if (!l)
4017
+ continue;
4018
+ let op;
4019
+ try {
4020
+ op = JSON.parse(l);
4021
+ } catch {
4022
+ break;
4023
+ }
4024
+ if (op.seq > toSeq)
4025
+ break;
4026
+ kept.push(l);
4027
+ last = op;
4028
+ }
4029
+ const tmp = `${opsFile}.tmp`;
4030
+ writeFileSync5(tmp, kept.length ? kept.join(`
4031
+ `) + `
4032
+ ` : "");
4033
+ renameSync3(tmp, opsFile);
4034
+ const hf = join5(solDir, "HEAD");
4035
+ writeFileSync5(hf, JSON.stringify(last ? { head: last.rootAfter, seq: last.seq, logTip: last.entryHash } : { seq: 0 }));
4036
+ }
3809
4037
  async function persistTree(src, dst, head) {
3810
4038
  const node = src.get(head);
3811
4039
  if (!node)
@@ -3877,7 +4105,7 @@ function saveKeyRing(ring) {
3877
4105
  chmodSync3(keysPath(), 384);
3878
4106
  } catch {}
3879
4107
  }
3880
- var procCwd, repoRoot, cwd, solDir, actor, viewMetaPath = (dir = solDir) => join5(dir, "view.json"), viewParent, objectsDir = () => join5(viewParent ?? solDir, "objects"), lockDir = () => solDir, SYMLINK_MODE = 40960, EXEC_MODE = 493, DEFAULT_IGNORE, includePath = () => join5(solDir, "include"), LOCK_STALE_MS = 5000, LazyStore, sealedDecryptor, indent = (s) => s.split(`
4108
+ var procCwd, repoRoot, cwd, solDir, actor, viewMetaPath = (dir = solDir) => join5(dir, "view.json"), viewParent, objectsDir = () => join5(viewParent ?? solDir, "objects"), opsDir = () => solDir, lockDir = () => solDir, SYMLINK_MODE = 40960, EXEC_MODE = 493, DEFAULT_IGNORE, includePath = () => join5(solDir, "include"), LOCK_STALE_MS = 5000, LazyStore, sealedDecryptor, indent = (s) => s.split(`
3881
4109
  `).map((l) => " " + l).join(`
3882
4110
  `), indexPath = () => join5(solDir, "index.json"), linesOf = (content) => content === "" ? [] : content.replace(/\n$/, "").split(`
3883
4111
  `), refsPath = () => join5(solDir, "refs.json"), saveRefs = (refs) => writeFileSync5(refsPath(), JSON.stringify(refs, null, 2)), _signer, trustPath = () => join5(solDir, "trust.json"), keysPath = () => join5(solDir, "keys.json");
@@ -3930,9 +4158,13 @@ __export(exports_remote, {
3930
4158
  writeBundle: () => writeBundle,
3931
4159
  saveRemote: () => saveRemote,
3932
4160
  remoteRefs: () => remoteRefs,
4161
+ remotePushFinalize: () => remotePushFinalize,
4162
+ remotePushChunked: () => remotePushChunked,
4163
+ remotePushBatch: () => remotePushBatch,
3933
4164
  remotePush: () => remotePush,
3934
4165
  remotePromote: () => remotePromote,
3935
4166
  remoteHead: () => remoteHead,
4167
+ remoteGate: () => remoteGate,
3936
4168
  remoteExport: () => remoteExport,
3937
4169
  remoteEnvPush: () => remoteEnvPush,
3938
4170
  remoteEnvPull: () => remoteEnvPull,
@@ -3943,6 +4175,7 @@ __export(exports_remote, {
3943
4175
  policyRemove: () => policyRemove,
3944
4176
  policyGet: () => policyGet,
3945
4177
  policyCheck: () => policyCheck,
4178
+ ownerSet: () => ownerSet,
3946
4179
  mrOpen: () => mrOpen,
3947
4180
  mrList: () => mrList,
3948
4181
  mrGet: () => mrGet,
@@ -3956,6 +4189,7 @@ __export(exports_remote, {
3956
4189
  });
3957
4190
  import { appendFileSync as appendFileSync4, existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
3958
4191
  import { join as join6 } from "node:path";
4192
+ import { gzipSync as gzipSync3 } from "node:zlib";
3959
4193
  async function call(cfg, token, path, init) {
3960
4194
  const res = await fetch(endpoint(cfg, path), {
3961
4195
  ...init,
@@ -3965,6 +4199,44 @@ async function call(cfg, token, path, init) {
3965
4199
  throw new Error(`remote ${path} -> ${res.status}: ${(await res.text().catch(() => "")).slice(0, 200)}`);
3966
4200
  return res.json();
3967
4201
  }
4202
+ function splitNodes(nodes, batchSizeBytes) {
4203
+ const batches = [];
4204
+ let cur = [];
4205
+ let curBytes = 0;
4206
+ for (const node of nodes) {
4207
+ const nb = sizeOf(node);
4208
+ if (cur.length && curBytes + nb > batchSizeBytes) {
4209
+ batches.push(cur);
4210
+ cur = [];
4211
+ curBytes = 0;
4212
+ }
4213
+ cur.push(node);
4214
+ curBytes += nb;
4215
+ }
4216
+ if (cur.length)
4217
+ batches.push(cur);
4218
+ return batches.length ? batches : [[]];
4219
+ }
4220
+ async function remotePushChunked(cfg, token, body, batchSizeBytes = DEFAULT_BATCH_BYTES) {
4221
+ const totalBytes = sizeOf(body.nodes) + sizeOf(body.ops);
4222
+ if (totalBytes <= batchSizeBytes)
4223
+ return remotePush(cfg, token, body);
4224
+ const stamp = `${body.head ?? "h"}-${Date.now()}`;
4225
+ const nodeBatches = splitNodes(body.nodes, batchSizeBytes);
4226
+ const batchIds = [];
4227
+ try {
4228
+ for (let i = 0;i < nodeBatches.length; i++) {
4229
+ const batchId = `${stamp}-${i}`;
4230
+ batchIds.push(batchId);
4231
+ await remotePushBatch(cfg, token, batchId, nodeBatches[i], i === 0 ? body.ops : [], i);
4232
+ }
4233
+ } catch (e) {
4234
+ if (e instanceof Error && /-> 404/.test(e.message))
4235
+ return remotePush(cfg, token, body);
4236
+ throw e;
4237
+ }
4238
+ return remotePushFinalize(cfg, token, batchIds, body.branch, body.head, body.expectedHead);
4239
+ }
3968
4240
  function loadRemote(solDir2) {
3969
4241
  const p = join6(solDir2, "remote.json");
3970
4242
  return existsSync6(p) ? JSON.parse(readFileSync6(p, "utf8")) : undefined;
@@ -3985,9 +4257,10 @@ async function writeBundle(solDir2, bundle, from = 0) {
3985
4257
  writeFileSync6(join6(solDir2, "HEAD"), JSON.stringify({ head: bundle.head, seq: bundle.seq, logTip: bundle.tip }));
3986
4258
  return fresh.length;
3987
4259
  }
3988
- var endpoint = (cfg, path) => `${cfg.url.replace(/\/+$/, "")}${path}${path.includes("?") ? "&" : "?"}repo=${encodeURIComponent(cfg.repo)}`, remoteHead = (cfg, token) => call(cfg, token, "/head"), remoteExport = (cfg, token) => call(cfg, token, "/export"), remoteRefs = (cfg, token) => call(cfg, token, "/refs"), remotePush = (cfg, token, body) => call(cfg, token, "/push", { method: "POST", body: JSON.stringify(body) }), remotePromote = (cfg, token, branch) => call(cfg, token, "/promote", { method: "POST", body: JSON.stringify({ branch }) }), mrOpen = (cfg, token, body) => call(cfg, token, "/mr", { method: "POST", body: JSON.stringify(body) }), mrList = (cfg, token) => call(cfg, token, "/mrs"), mrDiff = (cfg, token, id) => call(cfg, token, `/mr/diff?id=${id}`), mrGet = (cfg, token, id) => call(cfg, token, `/mr?id=${id}`), mrAction = (cfg, token, action, body) => call(cfg, token, `/mr/${action}`, { method: "POST", body: JSON.stringify(body) }), accessGet = (cfg, token) => call(cfg, token, "/access"), accessSet = (cfg, token, body) => call(cfg, token, "/access", { method: "POST", body: JSON.stringify(body) }), policyCheck = (cfg, token, paths) => call(cfg, token, "/policy/check", { method: "POST", body: JSON.stringify({ paths }) }), policyGet = (cfg, token) => call(cfg, token, "/policy"), policyUpsert = (cfg, token, rule) => call(cfg, token, "/policy?write=1", { method: "POST", body: JSON.stringify({ op: "upsert", rule }) }), policyRemove = (cfg, token, pattern) => call(cfg, token, "/policy?write=1", { method: "POST", body: JSON.stringify({ op: "remove", pattern }) }), recipientsForPath = (cfg, token, path) => call(cfg, token, `/recipients?path=${encodeURIComponent(path)}`), recipientsForAudience = (cfg, token, audience, recovery) => call(cfg, token, `/recipients?audience=${encodeURIComponent(JSON.stringify(audience))}${recovery?.length ? `&recovery=${encodeURIComponent(recovery.join(","))}` : ""}`), remoteEnvPush = (cfg, token, bundle) => call(cfg, token, "/env/push", { method: "POST", body: JSON.stringify(bundle) }), remoteEnvAnchor = (cfg, token) => call(cfg, token, "/env/anchor"), remoteEnvPull = (cfg, token) => call(cfg, token, "/env/pull"), forkMeta = (cfg, token, parent) => call(cfg, token, "/fork-meta", { method: "POST", body: JSON.stringify({ parent }) }), forksList = (cfg, token) => call(cfg, token, "/forks"), saveRemote = (solDir2, cfg) => writeFileSync6(join6(solDir2, "remote.json"), JSON.stringify(cfg, null, 2));
4260
+ var endpoint = (cfg, path) => `${cfg.url.replace(/\/+$/, "")}${path}${path.includes("?") ? "&" : "?"}repo=${encodeURIComponent(cfg.repo)}`, remoteHead = (cfg, token) => call(cfg, token, "/head"), remoteGate = (cfg, token, branch) => call(cfg, token, `/head?gateState=true${branch ? `&branch=${encodeURIComponent(branch)}` : ""}`), remoteExport = (cfg, token) => call(cfg, token, "/export"), remoteRefs = (cfg, token) => call(cfg, token, "/refs"), remotePush = (cfg, token, body) => call(cfg, token, "/push", { method: "POST", body: JSON.stringify(body) }), remotePromote = (cfg, token, branch) => call(cfg, token, "/promote", { method: "POST", body: JSON.stringify({ branch }) }), remotePushBatch = (cfg, token, batchId, nodes, ops, seq) => call(cfg, token, "/push/batch", { method: "POST", body: gzipSync3(JSON.stringify({ batchId, nodes, ops, seq })), headers: { "content-encoding": "gzip" } }), remotePushFinalize = (cfg, token, batchIds, branch, head, expectedHead) => call(cfg, token, "/push/finalize", { method: "POST", body: JSON.stringify({ batchIds, branch, head, expectedHead }) }), DEFAULT_BATCH_BYTES, sizeOf = (v) => JSON.stringify(v).length, mrOpen = (cfg, token, body) => call(cfg, token, "/mr", { method: "POST", body: JSON.stringify(body) }), mrList = (cfg, token) => call(cfg, token, "/mrs"), mrDiff = (cfg, token, id) => call(cfg, token, `/mr/diff?id=${id}`), mrGet = (cfg, token, id) => call(cfg, token, `/mr?id=${id}`), mrAction = (cfg, token, action, body) => call(cfg, token, `/mr/${action}`, { method: "POST", body: JSON.stringify(body) }), accessGet = (cfg, token) => call(cfg, token, "/access"), ownerSet = (cfg, token, ownerHandle) => call(cfg, token, "/owner", { method: "POST", body: JSON.stringify({ ownerHandle }) }), accessSet = (cfg, token, body) => call(cfg, token, "/access", { method: "POST", body: JSON.stringify(body) }), policyCheck = (cfg, token, paths) => call(cfg, token, "/policy/check", { method: "POST", body: JSON.stringify({ paths }) }), policyGet = (cfg, token) => call(cfg, token, "/policy"), policyUpsert = (cfg, token, rule) => call(cfg, token, "/policy?write=1", { method: "POST", body: JSON.stringify({ op: "upsert", rule }) }), policyRemove = (cfg, token, pattern) => call(cfg, token, "/policy?write=1", { method: "POST", body: JSON.stringify({ op: "remove", pattern }) }), recipientsForPath = (cfg, token, path) => call(cfg, token, `/recipients?path=${encodeURIComponent(path)}`), recipientsForAudience = (cfg, token, audience, recovery) => call(cfg, token, `/recipients?audience=${encodeURIComponent(JSON.stringify(audience))}${recovery?.length ? `&recovery=${encodeURIComponent(recovery.join(","))}` : ""}`), remoteEnvPush = (cfg, token, bundle) => call(cfg, token, "/env/push", { method: "POST", body: JSON.stringify(bundle) }), remoteEnvAnchor = (cfg, token) => call(cfg, token, "/env/anchor"), remoteEnvPull = (cfg, token) => call(cfg, token, "/env/pull"), forkMeta = (cfg, token, parent) => call(cfg, token, "/fork-meta", { method: "POST", body: JSON.stringify({ parent }) }), forksList = (cfg, token) => call(cfg, token, "/forks"), saveRemote = (solDir2, cfg) => writeFileSync6(join6(solDir2, "remote.json"), JSON.stringify(cfg, null, 2));
3989
4261
  var init_remote = __esm(() => {
3990
4262
  init_file_store2();
4263
+ DEFAULT_BATCH_BYTES = 5 * 1024 * 1024;
3991
4264
  });
3992
4265
 
3993
4266
  // src/bin/test-gate.ts
@@ -6034,6 +6307,314 @@ var init_sealed_client = __esm(() => {
6034
6307
  init_crypto();
6035
6308
  });
6036
6309
 
6310
+ // src/bin/admin-repair.ts
6311
+ var exports_admin_repair = {};
6312
+ __export(exports_admin_repair, {
6313
+ planRepair: () => planRepair,
6314
+ makeResolves: () => makeResolves,
6315
+ applyRepair: () => applyRepair
6316
+ });
6317
+ async function makeResolves(store2) {
6318
+ const cache = new Map;
6319
+ const walk = async (h, seen) => {
6320
+ if (cache.get(h) === true)
6321
+ return true;
6322
+ if (seen.has(h))
6323
+ return true;
6324
+ seen.add(h);
6325
+ const node = await store2.get(h);
6326
+ if (!node)
6327
+ return false;
6328
+ if (node.kind === "tree") {
6329
+ for (const e of Object.values(node.entries))
6330
+ if (!await walk(e.hash, seen))
6331
+ return false;
6332
+ }
6333
+ return true;
6334
+ };
6335
+ return async (root) => {
6336
+ if (cache.has(root))
6337
+ return cache.get(root);
6338
+ const ok = await walk(root, new Set);
6339
+ cache.set(root, ok);
6340
+ return ok;
6341
+ };
6342
+ }
6343
+ async function lastResolvableRoot(ops, upToIdx, resolves) {
6344
+ for (let i = Math.min(upToIdx, ops.length - 1);i >= 0; i--) {
6345
+ if (await resolves(ops[i].rootAfter))
6346
+ return { op: ops[i], idx: i };
6347
+ }
6348
+ return;
6349
+ }
6350
+ function firstBrokenIdx(ops) {
6351
+ let prev;
6352
+ for (let i = 0;i < ops.length; i++) {
6353
+ if (ops[i].seq !== i + 1)
6354
+ return i;
6355
+ try {
6356
+ verifyChain(ops.slice(0, i + 1));
6357
+ } catch {
6358
+ return i;
6359
+ }
6360
+ prev = ops[i].entryHash;
6361
+ }
6362
+ return -1;
6363
+ }
6364
+ async function planRepair(ops, head, resolves) {
6365
+ const broken = firstBrokenIdx(ops);
6366
+ if (broken === -1) {
6367
+ if (ops.length === 0)
6368
+ return { kind: "noop", note: "", reason: "empty repo — nothing to repair" };
6369
+ const headResolves = head !== undefined && await resolves(head);
6370
+ if (headResolves)
6371
+ return { kind: "noop", note: "", reason: "repo is healthy — chain valid, head resolves" };
6372
+ const found2 = await lastResolvableRoot(ops, ops.length - 1, resolves);
6373
+ if (!found2) {
6374
+ return { kind: "unfixable", note: "", reason: head === undefined ? "head pointer lost and no historical root fully resolves — content is missing locally; rehydrate with `sol recover --from-remote <url> <repo>`" : `head ${head} dangles and no historical root fully resolves — a deep object is missing locally; rehydrate with \`sol recover --from-remote <url> <repo>\`` };
6375
+ }
6376
+ return {
6377
+ kind: "repoint-head",
6378
+ repointHead: found2.op.rootAfter,
6379
+ note: `repair: re-pointed head to ${found2.op.rootAfter} (rootAfter of seq ${found2.op.seq})`,
6380
+ reason: head === undefined ? "head pointer lost — recovered from the op-log" : `head ${head} dangles — recovered to the last fully-resolvable root`
6381
+ };
6382
+ }
6383
+ const keepIdx = broken - 1;
6384
+ if (keepIdx < 0) {
6385
+ return { kind: "truncate-chain", truncateTo: 0, toEmptyTree: true, note: "repair: chain broken at genesis — truncated to an empty repo", reason: `op-log chain broken at seq ${ops[0]?.seq ?? 1} (the first op)` };
6386
+ }
6387
+ const prefix = ops.slice(0, keepIdx + 1);
6388
+ const found = await lastResolvableRoot(prefix, keepIdx, resolves);
6389
+ const truncateTo = prefix[keepIdx].seq;
6390
+ if (!found) {
6391
+ return { kind: "truncate-chain", truncateTo, toEmptyTree: true, note: `repair: truncated to seq ${truncateTo}; no resolvable root in the kept prefix — head -> empty tree`, reason: `op-log chain broken at seq ${ops[broken].seq}; kept prefix has no resolvable root` };
6392
+ }
6393
+ return {
6394
+ kind: "truncate-chain",
6395
+ truncateTo,
6396
+ repointHead: found.op.rootAfter,
6397
+ note: `repair: truncated to seq ${truncateTo} (chain broke at seq ${ops[broken].seq}); re-pointed head to ${found.op.rootAfter}`,
6398
+ reason: `op-log chain broken at seq ${ops[broken].seq} — truncated to the last verified link (seq ${found.op.seq})`
6399
+ };
6400
+ }
6401
+ async function applyRepair(exec, by = "sol-doctor", at = Date.now()) {
6402
+ const ops = await exec.log.history();
6403
+ const head = await exec.log.head();
6404
+ const resolves = await makeResolves(exec.store);
6405
+ const plan = await planRepair(ops, head, resolves);
6406
+ if (plan.kind === "noop" || plan.kind === "unfixable")
6407
+ return plan;
6408
+ if (plan.kind === "truncate-chain" && plan.truncateTo !== undefined)
6409
+ await exec.truncate(plan.truncateTo);
6410
+ const newHead = plan.toEmptyTree ? await emptyRoot(exec.store) : plan.repointHead;
6411
+ if (newHead !== undefined)
6412
+ await exec.setHead(newHead);
6413
+ const repaired = newHead ?? head;
6414
+ const seq = await exec.log.seq() + 1;
6415
+ await exec.log.append({ seq, type: "checkpoint", path: "", rootAfter: repaired ?? await emptyRoot(exec.store), at, by, message: plan.note });
6416
+ return plan;
6417
+ }
6418
+ var init_admin_repair = __esm(() => {
6419
+ init_chain();
6420
+ init_async_tree();
6421
+ });
6422
+
6423
+ // src/bin/admin.ts
6424
+ var exports_admin = {};
6425
+ __export(exports_admin, {
6426
+ runDoctor: () => runDoctor,
6427
+ renderDoctorIntegrity: () => renderDoctorIntegrity,
6428
+ checkOpLog: () => checkOpLog,
6429
+ checkObjects: () => checkObjects
6430
+ });
6431
+ function checkOpLog(ops) {
6432
+ try {
6433
+ for (let i = 0;i < ops.length; i++)
6434
+ if (ops[i].seq !== i + 1)
6435
+ throw new Error(`gap before seq ${i + 1}`);
6436
+ const tip = verifyChain(ops);
6437
+ return { count: ops.length, chainOk: true, tip };
6438
+ } catch (e) {
6439
+ return { count: ops.length, chainOk: false, chainError: String(e?.message ?? e) };
6440
+ }
6441
+ }
6442
+ async function checkObjects(store2, head, ops) {
6443
+ const missing = [];
6444
+ const seen = new Set;
6445
+ if (head === undefined)
6446
+ return { reachable: 0, missing };
6447
+ const seqOfRoot = new Map;
6448
+ for (const op of ops)
6449
+ if (!seqOfRoot.has(op.rootAfter))
6450
+ seqOfRoot.set(op.rootAfter, op.seq);
6451
+ const walk = async (h, refBy) => {
6452
+ if (seen.has(h))
6453
+ return;
6454
+ seen.add(h);
6455
+ const node = await store2.get(h);
6456
+ if (!node) {
6457
+ missing.push({ hash: h, refBy });
6458
+ return;
6459
+ }
6460
+ if (node.kind === "tree")
6461
+ for (const e of Object.values(node.entries))
6462
+ await walk(e.hash, refBy);
6463
+ };
6464
+ await walk(head, seqOfRoot.get(head));
6465
+ return { reachable: seen.size - missing.length, missing };
6466
+ }
6467
+ async function runDoctor(store2, log) {
6468
+ const ops = await log.history();
6469
+ const head = await log.head();
6470
+ const seq = await log.seq();
6471
+ const oplog = checkOpLog(ops);
6472
+ const objects = await checkObjects(store2, head, ops);
6473
+ const headLost = head === undefined && seq > 0;
6474
+ const headDangling = head !== undefined && objects.missing.some((m) => m.hash === head);
6475
+ const ok = oplog.chainOk && objects.missing.length === 0 && !headLost;
6476
+ return { ok, oplog, objects, head, seq, headLost, headDangling };
6477
+ }
6478
+ function renderDoctorIntegrity(r) {
6479
+ const lines = [];
6480
+ const chain = r.oplog.chainOk ? `chain valid${r.oplog.tip ? `, tip: ${r.oplog.tip.slice(0, 16)}` : ""}` : `chain BROKEN — ${r.oplog.chainError}`;
6481
+ lines.push(` op-log: ${r.oplog.count} op(s), ${chain}`);
6482
+ if (r.headLost) {
6483
+ lines.push(` head: LOST (${r.seq} op(s) but no head pointer) — run \`sol fsck --repair\``);
6484
+ } else if (r.headDangling) {
6485
+ lines.push(` head: DANGLING (${r.head?.slice(0, 16)} missing from store) — run \`sol fsck --repair\``);
6486
+ } else {
6487
+ lines.push(` objects: ${r.objects.reachable} reachable, ${r.objects.missing.length} missing${r.objects.missing.length ? " (run: sol fsck --repair)" : ""}`);
6488
+ }
6489
+ for (const m of r.objects.missing)
6490
+ lines.push(` missing ${m.hash}${m.refBy !== undefined ? ` (op seq ${m.refBy})` : ""}`);
6491
+ return lines;
6492
+ }
6493
+ var init_admin = __esm(() => {
6494
+ init_chain();
6495
+ });
6496
+
6497
+ // src/bin/admin-reflog.ts
6498
+ var exports_admin_reflog = {};
6499
+ __export(exports_admin_reflog, {
6500
+ renderReflog: () => renderReflog,
6501
+ formatReflogLine: () => formatReflogLine,
6502
+ filterReflog: () => filterReflog,
6503
+ buildReflog: () => buildReflog
6504
+ });
6505
+ function buildReflog(ops) {
6506
+ const rows = [];
6507
+ let prevRoot;
6508
+ for (const op of ops) {
6509
+ rows.push({ seq: op.seq, by: op.by ?? "?", at: op.at, type: op.type, from: prevRoot, to: op.rootAfter, message: op.message, merge: op.parent2 });
6510
+ prevRoot = op.rootAfter;
6511
+ }
6512
+ return rows;
6513
+ }
6514
+ function filterReflog(rows, f = {}) {
6515
+ return rows.filter((r) => {
6516
+ if (f.actor !== undefined && r.by !== f.actor)
6517
+ return false;
6518
+ if (f.since !== undefined && r.at < f.since)
6519
+ return false;
6520
+ if (f.type !== undefined && r.type !== f.type)
6521
+ return false;
6522
+ return true;
6523
+ });
6524
+ }
6525
+ function formatReflogLine(r, branch) {
6526
+ const when = new Date(r.at).toISOString().replace("T", " ").slice(0, 19);
6527
+ const verb = r.merge ? "merge" : r.type;
6528
+ const move = `${short(r.from)} -> ${short(r.to)}`;
6529
+ const label = branch ? `${branch}: ` : "";
6530
+ const body = r.message ? `${verb} ${label}${r.message}` : `${verb} ${label}`.trimEnd();
6531
+ return `${r.seq} ${r.by} ${when} ${body} (${move})`;
6532
+ }
6533
+ function short(h) {
6534
+ if (!h)
6535
+ return "(empty)";
6536
+ return h.length > 14 ? h.slice(0, 14) : h;
6537
+ }
6538
+ function renderReflog(ops, opts = {}) {
6539
+ let rows = filterReflog(buildReflog(ops), opts);
6540
+ if (opts.branchFilter) {
6541
+ const b = opts.branchFilter;
6542
+ rows = rows.filter((r) => r.type === "checkpoint" && (r.message?.includes(b) ?? false));
6543
+ }
6544
+ return rows.reverse().map((r) => formatReflogLine(r, opts.branch));
6545
+ }
6546
+
6547
+ // src/bin/admin-recover.ts
6548
+ var exports_admin_recover = {};
6549
+ __export(exports_admin_recover, {
6550
+ restoreFileFromHistory: () => restoreFileFromHistory,
6551
+ listAllRoots: () => listAllRoots,
6552
+ dumpLog: () => dumpLog,
6553
+ RECOVER_HELP: () => RECOVER_HELP
6554
+ });
6555
+ async function listAllRoots(ops, has) {
6556
+ const byRoot = new Map;
6557
+ for (const op of ops) {
6558
+ byRoot.set(op.rootAfter, { root: op.rootAfter, seq: op.seq, at: op.at, message: op.message });
6559
+ }
6560
+ const out = [...byRoot.values()].sort((a, b) => b.seq - a.seq);
6561
+ if (has)
6562
+ for (const e of out)
6563
+ e.resolvable = await has(e.root);
6564
+ return out;
6565
+ }
6566
+ async function restoreFileFromHistory(store2, ops, path, from) {
6567
+ let window = ops;
6568
+ if (typeof from === "number")
6569
+ window = ops.filter((o) => o.seq <= from);
6570
+ else if (typeof from === "string") {
6571
+ const bare = from.startsWith("h_") ? from : "h_" + from;
6572
+ const idx = ops.findIndex((o) => o.rootAfter === bare || o.rootAfter.startsWith(bare));
6573
+ window = idx >= 0 ? ops.slice(0, idx + 1) : [];
6574
+ if (idx < 0)
6575
+ return { found: false, path, reason: `no op produced root ${from}` };
6576
+ }
6577
+ const tried = new Set;
6578
+ for (let i = window.length - 1;i >= 0; i--) {
6579
+ const root = window[i].rootAfter;
6580
+ if (tried.has(root))
6581
+ continue;
6582
+ tried.add(root);
6583
+ if (!await store2.has(root))
6584
+ continue;
6585
+ const leaf = await nodeAt(store2, root, path);
6586
+ if (!leaf)
6587
+ continue;
6588
+ if (leaf.kind === "sealed")
6589
+ return { found: true, path, sealed: true, box: leaf.box, fromSeq: window[i].seq, fromRoot: root };
6590
+ const blob = await fileAt(store2, root, path);
6591
+ return { found: true, path, content: leaf.content, encoding: blob?.encoding, fromSeq: window[i].seq, fromRoot: root };
6592
+ }
6593
+ return { found: false, path, reason: from !== undefined ? `path ${path} not found at or before ${from}` : `path ${path} never existed in history` };
6594
+ }
6595
+ function dumpLog(ops, fromSeq, toSeq) {
6596
+ const window = ops.filter((o) => (fromSeq === undefined || o.seq >= fromSeq) && (toSeq === undefined || o.seq <= toSeq));
6597
+ return JSON.stringify(window, null, 2);
6598
+ }
6599
+ var RECOVER_HELP = `sol recover — guide through recovery scenarios
6600
+
6601
+ 1. Rehydrate from the cloud (download ops + objects from the backend)
6602
+ sol recover --from-remote <url> <repo>
6603
+
6604
+ 2. List every root the repo ever landed on (find a lost head)
6605
+ sol recover --list-all-roots
6606
+
6607
+ 3. Restore a file from op history (even if the current tree dropped it)
6608
+ sol recover --file <path> [--from <seq|hash>]
6609
+
6610
+ 4. Dump raw op-log entries as JSON (manual inspection / salvage)
6611
+ sol recover --dump-log [<from-seq> [<to-seq>]]
6612
+
6613
+ after a structural repair, run \`sol doctor\` to confirm the repo is healthy.`;
6614
+ var init_admin_recover = __esm(() => {
6615
+ init_async_tree();
6616
+ });
6617
+
6037
6618
  // src/bin/secret-scrub.ts
6038
6619
  var exports_secret_scrub = {};
6039
6620
  __export(exports_secret_scrub, {
@@ -6440,6 +7021,140 @@ var init_merge = __esm(() => {
6440
7021
  init_tree();
6441
7022
  });
6442
7023
 
7024
+ // src/bin/errors-friendly.ts
7025
+ var exports_errors_friendly = {};
7026
+ __export(exports_errors_friendly, {
7027
+ friendlyError: () => friendlyError,
7028
+ formatSealedError: () => formatSealedError,
7029
+ formatMissingObjectError: () => formatMissingObjectError,
7030
+ formatChainError: () => formatChainError,
7031
+ formatAuthError: () => formatAuthError,
7032
+ classifyAuthError: () => classifyAuthError
7033
+ });
7034
+ function classifyAuthError(status, message = "") {
7035
+ if (status === 401 || / -> 401\b/.test(message) || /\b401\b/.test(message))
7036
+ return "expired";
7037
+ if (status === 403 || / -> 403\b/.test(message) || /\b403\b/.test(message))
7038
+ return "forbidden";
7039
+ if (/invalid token|malformed|not a jwt|jwt/i.test(message))
7040
+ return "invalid";
7041
+ return "unknown";
7042
+ }
7043
+ function formatAuthError(kind, repo) {
7044
+ switch (kind) {
7045
+ case "expired":
7046
+ return "session expired — run `sol auth login` (or set SOL_TOKEN)";
7047
+ case "forbidden":
7048
+ return `you lack access${repo ? ` to ${repo}` : " to this repo"} — ask the owner to grant you access, or check you're logged in as the right account (\`sol auth status\`)`;
7049
+ case "invalid":
7050
+ return "invalid token format — SOL_TOKEN is not a valid session token; re-run `sol auth login` to mint a fresh one";
7051
+ default:
7052
+ return "authentication failed — run `sol auth login` (or set SOL_TOKEN), then try again";
7053
+ }
7054
+ }
7055
+ function formatSealedError(path) {
7056
+ return `sealed file ${path} — you lack a decryption key.
7057
+ -> \`sol keys\` to see your keyring
7058
+ -> ask the owner to re-seal this path to your account (\`sol seal ${path} <you>\`)`;
7059
+ }
7060
+ function formatChainError(err) {
7061
+ const m = err.message;
7062
+ const seq = /seq (\d+)/.exec(m)?.[1];
7063
+ const where = seq ? `op-log chain broken at seq ${seq}` : "op-log chain broken";
7064
+ return `${where}: ${m.replace(/^corrupt repo: /, "")}
7065
+ -> \`sol doctor\` for a full diagnosis
7066
+ -> \`sol fsck --repair\` to recover (truncates to the last verified link)`;
7067
+ }
7068
+ function formatMissingObjectError(hash, seq) {
7069
+ const ref = seq !== undefined ? ` referenced by op seq ${seq}` : "";
7070
+ return `object ${hash}${ref} not found in store.
7071
+ -> \`sol doctor\` then \`sol fsck --repair\` to recover from backup
7072
+ -> or \`sol recover --from-remote <url> <repo>\` to rehydrate from the cloud`;
7073
+ }
7074
+ function friendlyError(err, ctx) {
7075
+ if (err instanceof CorruptObjectError)
7076
+ return formatMissingObjectError(err.hash);
7077
+ if (err instanceof CorruptRepoError) {
7078
+ if (/dangling head|head is lost|missing from the store/.test(err.message)) {
7079
+ return `${err.message.replace(/^corrupt repo: /, "")}
7080
+ -> \`sol doctor\` then \`sol fsck --repair\` to re-point the head`;
7081
+ }
7082
+ return formatChainError(err);
7083
+ }
7084
+ const msg = err instanceof Error ? err.message : String(err);
7085
+ const kind = classifyAuthError(undefined, msg);
7086
+ if (kind !== "unknown")
7087
+ return formatAuthError(kind, ctx?.repo);
7088
+ return msg;
7089
+ }
7090
+ var init_errors_friendly = __esm(() => {
7091
+ init_errors();
7092
+ });
7093
+
7094
+ // src/bin/admin-remote-verify.ts
7095
+ var exports_admin_remote_verify = {};
7096
+ __export(exports_admin_remote_verify, {
7097
+ renderRemoteVerify: () => renderRemoteVerify,
7098
+ remoteVerify: () => remoteVerify,
7099
+ classifyRemoteFailure: () => classifyRemoteFailure
7100
+ });
7101
+ function classifyRemoteFailure(status, message, repo) {
7102
+ if (status === 401 || status === 403)
7103
+ return { kind: "auth", message: formatAuthError(classifyAuthError(status, message), repo) };
7104
+ if (status !== undefined)
7105
+ return { kind: "backend", status, message: `backend error (${status}) — transient, try again: ${message}`.trim() };
7106
+ return { kind: "network", message: `could not reach the remote — connectivity issue: ${message}` };
7107
+ }
7108
+ async function remoteVerify(fetchLike, url, repo, token) {
7109
+ const endpoint2 = `${url.replace(/\/+$/, "")}/fsck?repo=${encodeURIComponent(repo)}`;
7110
+ let res;
7111
+ try {
7112
+ res = await fetchLike(endpoint2, { headers: { authorization: `Bearer ${token}`, "content-type": "application/json" } });
7113
+ } catch (e) {
7114
+ return classifyRemoteFailure(undefined, String(e?.message ?? e), repo);
7115
+ }
7116
+ if (!res.ok) {
7117
+ const body = await res.text().catch(() => "");
7118
+ return classifyRemoteFailure(res.status, body.slice(0, 200), repo);
7119
+ }
7120
+ const fsck = await res.json();
7121
+ return fsck.ok ? { kind: "ok", fsck } : { kind: "corrupt", fsck };
7122
+ }
7123
+ function renderRemoteVerify(r) {
7124
+ switch (r.kind) {
7125
+ case "ok":
7126
+ return {
7127
+ lines: [
7128
+ " checking remote repository...",
7129
+ ` op-log: chain ${r.fsck.chainOk ? "valid" : "BROKEN"}${r.fsck.head ? `, tip: ${r.fsck.head.slice(0, 16)}` : ""}`,
7130
+ ` objects: ${r.fsck.checked} reachable, ${r.fsck.missing.length} missing`,
7131
+ " status: OK"
7132
+ ],
7133
+ exitCode: 0
7134
+ };
7135
+ case "corrupt":
7136
+ return {
7137
+ lines: [
7138
+ " checking remote repository...",
7139
+ ` op-log: chain ${r.fsck.chainOk ? "valid" : `BROKEN — ${r.fsck.chainError ?? "?"}`}`,
7140
+ ` objects: ${r.fsck.checked} reachable, ${r.fsck.missing.length} missing`,
7141
+ ...r.fsck.missing.map((h) => ` missing ${h}`),
7142
+ " status: PROBLEMS FOUND — the remote repository is corrupted"
7143
+ ],
7144
+ exitCode: 1
7145
+ };
7146
+ case "auth":
7147
+ return { lines: [` ${r.message}`], exitCode: 1 };
7148
+ case "backend":
7149
+ return { lines: [` ${r.message}`], exitCode: 1 };
7150
+ case "network":
7151
+ return { lines: [` ${r.message}`], exitCode: 1 };
7152
+ }
7153
+ }
7154
+ var init_admin_remote_verify = __esm(() => {
7155
+ init_errors_friendly();
7156
+ });
7157
+
6443
7158
  // src/bin/local-peer.ts
6444
7159
  var exports_local_peer = {};
6445
7160
  __export(exports_local_peer, {
@@ -6690,12 +7405,333 @@ var init_converge = __esm(() => {
6690
7405
  EMPTY_TREE2 = { kind: "tree", entries: {} };
6691
7406
  });
6692
7407
 
7408
+ // src/bin/sync-watch-fs.ts
7409
+ var exports_sync_watch_fs = {};
7410
+ __export(exports_sync_watch_fs, {
7411
+ buildFsSyncEnv: () => buildFsSyncEnv
7412
+ });
7413
+ import { existsSync as existsSync14, readFileSync as readFileSync14 } from "node:fs";
7414
+ function resolveRemote() {
7415
+ const cfg = loadRemote(solDir);
7416
+ if (!cfg)
7417
+ return;
7418
+ return cfg.url ? cfg : { ...cfg, url: DEFAULT_REMOTE_URL };
7419
+ }
7420
+ function readRefs() {
7421
+ return existsSync14(refsPath()) ? JSON.parse(readFileSync14(refsPath(), "utf8")) : undefined;
7422
+ }
7423
+ async function buildFsSyncEnv(opts) {
7424
+ if (!existsSync14(solDir))
7425
+ throw new Error("not a sol repo — run `sol init` first");
7426
+ const { repo, log } = open();
7427
+ const cfg = resolveRemote() || (() => {
7428
+ throw new Error("no remote — set one with `sol remote <url> <repo>`");
7429
+ })();
7430
+ const token = opts.token;
7431
+ const replica = { store: new FileStore2(solDir, objectsDir()), log };
7432
+ const currentBranch = () => readRefs()?.current ?? "main";
7433
+ const local = {
7434
+ replica,
7435
+ async capture() {
7436
+ const snap = await snapshotTree(repo);
7437
+ if (snap.changed)
7438
+ writeWorkingIndex(await repo.list());
7439
+ return snap.changed;
7440
+ },
7441
+ head: () => log.head(),
7442
+ seq: () => log.seq(),
7443
+ logTip: () => log.logTip(),
7444
+ history: () => log.history(),
7445
+ async nodes() {
7446
+ return allLocalNodes();
7447
+ },
7448
+ branch: currentBranch,
7449
+ forkBase() {
7450
+ const refs = readRefs();
7451
+ return refs?.branches[refs.current]?.remote;
7452
+ },
7453
+ async dirty() {
7454
+ const wc = workingChanges(loadStore(), await log.head() ?? "");
7455
+ return wc.added.length + wc.modified.length + wc.removed.length > 0;
7456
+ },
7457
+ async absorbCanonical(bundle, fromHead) {
7458
+ const head = bundle.head ?? fromHead;
7459
+ const canonBundle = {
7460
+ head,
7461
+ seq: bundle.ops.length ? bundle.ops[bundle.ops.length - 1].seq : 0,
7462
+ tip: bundle.ops.length ? bundle.ops[bundle.ops.length - 1].entryHash : undefined,
7463
+ ops: bundle.ops,
7464
+ nodes: bundle.nodes,
7465
+ refs: bundle.branches ? { branches: bundle.branches, production: readRefs()?.current ?? "main" } : undefined
7466
+ };
7467
+ await writeBundle(solDir, canonBundle, 0);
7468
+ await local.adopt(fromHead, head, bundle.branches);
7469
+ },
7470
+ async adopt(from, to, branches) {
7471
+ const refs = readRefs();
7472
+ if (refs) {
7473
+ if (branches) {
7474
+ for (const [name, h] of Object.entries(branches)) {
7475
+ refs.branches[name] = { head: refs.branches[name]?.head ?? h, base: refs.branches[name]?.base ?? h, remote: h };
7476
+ }
7477
+ }
7478
+ const cur = refs.current;
7479
+ if (refs.branches[cur]) {
7480
+ refs.branches[cur].head = to;
7481
+ refs.branches[cur].remote = to;
7482
+ }
7483
+ saveRefs(refs);
7484
+ }
7485
+ setOpLogHead(to);
7486
+ if (to && to !== from)
7487
+ materializeDiff(loadStore(), from, to);
7488
+ }
7489
+ };
7490
+ const remote = {
7491
+ async head() {
7492
+ const rh = await remoteHead(cfg, token);
7493
+ return { head: rh.head, tip: rh.tip, seq: rh.seq };
7494
+ },
7495
+ async export() {
7496
+ const b = await remoteExport(cfg, token);
7497
+ return { head: b.head, tip: b.tip, seq: b.seq, ops: b.ops, nodes: b.nodes, branches: b.refs?.branches };
7498
+ },
7499
+ async push(body) {
7500
+ const res = await remotePush(cfg, token, body);
7501
+ return { head: res.head, applied: res.applied, merged: res.merged, conflicts: res.conflicts, branches: res.refs?.branches };
7502
+ }
7503
+ };
7504
+ if (!loadRemote(solDir)?.url && cfg.repo)
7505
+ saveRemote(solDir, cfg);
7506
+ const logger = {
7507
+ info: (line) => console.log(line),
7508
+ warn: (line) => console.error("sol: " + line)
7509
+ };
7510
+ const runLocked = async (fn) => {
7511
+ const release = acquireLock();
7512
+ try {
7513
+ return await fn();
7514
+ } finally {
7515
+ release();
7516
+ }
7517
+ };
7518
+ return { local, remote, log: logger, actor, dryRun: !!opts.dryRun, runLocked };
7519
+ }
7520
+ var DEFAULT_REMOTE_URL;
7521
+ var init_sync_watch_fs = __esm(() => {
7522
+ init_file_store2();
7523
+ init_lib();
7524
+ init_remote();
7525
+ DEFAULT_REMOTE_URL = (process.env.SOL_REMOTE || "https://sol.midsummer.new").replace(/\/+$/, "");
7526
+ });
7527
+
7528
+ // src/bin/sync-watch.ts
7529
+ var exports_sync_watch = {};
7530
+ __export(exports_sync_watch, {
7531
+ runSyncWatch: () => runSyncWatch,
7532
+ pushTick: () => pushTick,
7533
+ pollTick: () => pollTick,
7534
+ isFatalSyncError: () => isFatalSyncError,
7535
+ formatTick: () => formatTick,
7536
+ captureTick: () => captureTick
7537
+ });
7538
+ function isFatalSyncError(e) {
7539
+ const m = (e instanceof Error ? e.message : String(e)).toLowerCase();
7540
+ if (/-> 401|-> 403|unauthor|forbidden|session expired|invalid token/.test(m))
7541
+ return true;
7542
+ if (/corrupt|broken chain|chain mismatch|not a sol repo|tamper/.test(m))
7543
+ return true;
7544
+ return false;
7545
+ }
7546
+ async function captureTick(env) {
7547
+ if (env.dryRun)
7548
+ return { changed: 0 };
7549
+ const changed = await env.local.capture();
7550
+ return { changed };
7551
+ }
7552
+ async function pushTick(env) {
7553
+ const { local, remote } = env;
7554
+ const localHead = await local.head();
7555
+ if (!localHead)
7556
+ return { pushed: 0, merged: false, conflicts: [] };
7557
+ const rh = await remote.head();
7558
+ const ops = await local.history();
7559
+ const localSeq = ops.length ? ops[ops.length - 1].seq : 0;
7560
+ const localTip = await local.logTip();
7561
+ if (localSeq === rh.seq && localTip === rh.tip)
7562
+ return { pushed: 0, merged: false, conflicts: [], head: localHead };
7563
+ const branch = local.branch();
7564
+ const forkBase = local.forkBase();
7565
+ const baseOp = forkBase ? ops.find((o) => o.rootAfter === forkBase) : undefined;
7566
+ const fromSeq = baseOp ? baseOp.seq : rh.seq;
7567
+ const delta = ops.filter((o) => o.seq > fromSeq);
7568
+ if (env.dryRun) {
7569
+ env.log.info(`[dry-run] would push ${delta.length} op(s) (branch ${branch} @ ${short2(localHead)})`);
7570
+ return { pushed: 0, merged: false, conflicts: [], head: localHead };
7571
+ }
7572
+ const res = await remote.push({ nodes: await local.nodes(), ops: delta, branch, head: localHead, expectedHead: forkBase });
7573
+ const canon = await remote.export();
7574
+ const convergedHead = res.head ?? canon.branches?.[branch] ?? localHead;
7575
+ await local.absorbCanonical({ head: convergedHead, ops: canon.ops, nodes: canon.nodes, branches: canon.branches }, localHead);
7576
+ return {
7577
+ pushed: res.applied,
7578
+ merged: !!res.merged,
7579
+ conflicts: (res.conflicts ?? []).map((c) => c.path),
7580
+ head: convergedHead
7581
+ };
7582
+ }
7583
+ async function pollTick(env) {
7584
+ const { local, remote } = env;
7585
+ const rh = await remote.head();
7586
+ const localTip = await local.logTip();
7587
+ const localSeq = await local.seq();
7588
+ if (rh.seq <= localSeq && rh.tip === localTip)
7589
+ return { pulled: 0, merged: false, conflicts: [], deferred: false };
7590
+ if (await local.dirty()) {
7591
+ env.log.info("remote moved but the working tree is dirty — pull deferred to the next poll (your edits captured first)");
7592
+ return { pulled: 0, merged: false, conflicts: [], deferred: true };
7593
+ }
7594
+ if (env.dryRun) {
7595
+ env.log.info(`[dry-run] would pull remote @ ${short2(rh.head)} (seq ${rh.seq})`);
7596
+ return { pulled: 0, merged: false, conflicts: [], deferred: false };
7597
+ }
7598
+ const bundle = await remote.export();
7599
+ const prevHead = await local.head() ?? "";
7600
+ const before = (await local.history()).length;
7601
+ const res = await converge(local.replica, {
7602
+ nodes: bundle.nodes,
7603
+ ops: bundle.ops,
7604
+ incomingHead: bundle.head,
7605
+ actor: env.actor
7606
+ });
7607
+ const after = (await local.history()).length;
7608
+ if (res.head !== prevHead)
7609
+ await local.adopt(prevHead, res.head, bundle.branches);
7610
+ return {
7611
+ pulled: after - before,
7612
+ merged: res.merged,
7613
+ conflicts: res.conflicts.map((c) => c.path),
7614
+ deferred: false,
7615
+ head: res.head
7616
+ };
7617
+ }
7618
+ function formatTick(parts) {
7619
+ const segs = [];
7620
+ if (parts.captured)
7621
+ segs.push(`auto-captured ${parts.captured} change${parts.captured === 1 ? "" : "s"}`);
7622
+ if (parts.push && parts.push.pushed) {
7623
+ const note = parts.push.merged ? " (converged)" : "";
7624
+ const conf = parts.push.conflicts.length ? ` [${parts.push.conflicts.length} conflict(s): ${parts.push.conflicts.join(", ")}]` : "";
7625
+ segs.push(`pushed ${parts.push.pushed} op${parts.push.pushed === 1 ? "" : "s"} -> remote${note}${conf}`);
7626
+ }
7627
+ if (parts.poll && parts.poll.pulled) {
7628
+ const note = parts.poll.merged ? " (merged)" : "";
7629
+ const conf = parts.poll.conflicts.length ? ` [${parts.poll.conflicts.length} conflict(s): ${parts.poll.conflicts.join(", ")}]` : "";
7630
+ segs.push(`pulled ${parts.poll.pulled} op${parts.poll.pulled === 1 ? "" : "s"} from remote${note}${conf}`);
7631
+ }
7632
+ if (!segs.length)
7633
+ return;
7634
+ const t = new Date().toLocaleTimeString();
7635
+ return `[${t}] ${segs.join(" | ")}`;
7636
+ }
7637
+ async function runSyncWatch(opts) {
7638
+ const { buildFsSyncEnv: buildFsSyncEnv2 } = await Promise.resolve().then(() => (init_sync_watch_fs(), exports_sync_watch_fs));
7639
+ const env = await buildFsSyncEnv2(opts);
7640
+ const { watch } = await import("node:fs");
7641
+ const { sep: sep2 } = await import("node:path");
7642
+ const { DEFAULT_IGNORE: DEFAULT_IGNORE2 } = await Promise.resolve().then(() => (init_lib(), exports_lib));
7643
+ const pollInterval = opts.pollInterval ?? 5000;
7644
+ const pushImmediate = opts.pushImmediate ?? true;
7645
+ let exiting = false;
7646
+ const fatal = (where, e) => {
7647
+ if (exiting)
7648
+ return;
7649
+ exiting = true;
7650
+ env.log.warn(`FATAL (${where}): ${e instanceof Error ? e.message : e} — exiting. fix it and re-run \`sol sync --watch\`.`);
7651
+ process.exit(1);
7652
+ };
7653
+ let running = false;
7654
+ let pending = false;
7655
+ const captureAndPush = async () => {
7656
+ if (running) {
7657
+ pending = true;
7658
+ return;
7659
+ }
7660
+ running = true;
7661
+ try {
7662
+ do {
7663
+ pending = false;
7664
+ try {
7665
+ const cap = await env.runLocked(async () => {
7666
+ const c = await captureTick(env);
7667
+ let push;
7668
+ if (c.changed && pushImmediate) {
7669
+ try {
7670
+ push = await pushTick(env);
7671
+ } catch (e) {
7672
+ if (isFatalSyncError(e))
7673
+ return fatal("push", e);
7674
+ env.log.warn(`push failed (will retry next cycle): ${e instanceof Error ? e.message : e}`);
7675
+ }
7676
+ }
7677
+ return { captured: c.changed, push };
7678
+ });
7679
+ const line = formatTick(cap);
7680
+ if (line)
7681
+ env.log.info(line);
7682
+ } catch (e) {
7683
+ if (isFatalSyncError(e))
7684
+ return fatal("capture", e);
7685
+ env.log.warn(`sync tick error (retrying): ${e?.message || e}`);
7686
+ }
7687
+ } while (pending);
7688
+ } finally {
7689
+ running = false;
7690
+ }
7691
+ };
7692
+ const pollOnce = async () => {
7693
+ try {
7694
+ const res = await env.runLocked(() => pollTick(env));
7695
+ const line = formatTick({ poll: res });
7696
+ if (line)
7697
+ env.log.info(line);
7698
+ } catch (e) {
7699
+ if (isFatalSyncError(e))
7700
+ return fatal("poll", e);
7701
+ env.log.warn(`poll failed (will retry in ${Math.round(pollInterval / 1000)}s): ${e?.message || e}`);
7702
+ }
7703
+ };
7704
+ await captureAndPush();
7705
+ env.log.info(`sol sync --watch on ${opts.repoDir}
7706
+ every edit is auto-captured + pushed${pushImmediate ? "" : " (push disabled — capture only)"}; safe remote updates pull in every ${Math.round(pollInterval / 1000)}s.
7707
+ ` + ` nothing is lost — all edits are durable locally first. Ctrl-C to stop.${opts.dryRun ? `
7708
+ [DRY RUN — logging intent, mutating nothing]` : ""}`);
7709
+ let timer;
7710
+ watch(opts.repoDir, { recursive: true }, (_e, filename) => {
7711
+ if (!filename)
7712
+ return;
7713
+ const top = String(filename).split(sep2)[0];
7714
+ if (top === ".sol" || DEFAULT_IGNORE2.includes(top))
7715
+ return;
7716
+ clearTimeout(timer);
7717
+ timer = setTimeout(() => void captureAndPush(), 400);
7718
+ });
7719
+ const poller = setInterval(() => void pollOnce(), pollInterval);
7720
+ poller.unref?.();
7721
+ await new Promise(() => {});
7722
+ }
7723
+ var short2 = (h) => (h ?? "").slice(0, 12);
7724
+ var init_sync_watch = __esm(() => {
7725
+ init_converge();
7726
+ });
7727
+
6693
7728
  // src/mr.ts
6694
7729
  var exports_mr = {};
6695
7730
  __export(exports_mr, {
6696
7731
  mrSummary: () => mrSummary,
6697
7732
  mergeGate: () => mergeGate,
6698
- latestVerdicts: () => latestVerdicts
7733
+ latestVerdicts: () => latestVerdicts,
7734
+ isAdvisoryGate: () => isAdvisoryGate
6699
7735
  });
6700
7736
  function latestVerdicts(mr) {
6701
7737
  const m = new Map;
@@ -6703,18 +7739,23 @@ function latestVerdicts(mr) {
6703
7739
  m.set(r.actor, r.verdict);
6704
7740
  return m;
6705
7741
  }
6706
- function mergeGate(mr) {
7742
+ function mergeGate(mr, branchGate) {
6707
7743
  const reasons = [];
6708
7744
  if (mr.status !== "open")
6709
7745
  reasons.push(`MR is ${mr.status}`);
6710
7746
  for (const c of mr.checks)
6711
7747
  if (c.status === "fail")
6712
7748
  reasons.push(`check '${c.name}' failed`);
7749
+ if (branchGate?.verdict === "fail")
7750
+ reasons.push(`branch head gate failed${branchGate.summary ? `: ${branchGate.summary}` : ""}`);
6713
7751
  for (const [actor2, v] of latestVerdicts(mr))
6714
7752
  if (v === "request-changes")
6715
7753
  reasons.push(`${actor2} requested changes`);
6716
7754
  return reasons;
6717
7755
  }
7756
+ function isAdvisoryGate(reason) {
7757
+ return reason.startsWith("check '") || reason.startsWith("branch head gate");
7758
+ }
6718
7759
  function mrSummary(mr) {
6719
7760
  const approvals = [...latestVerdicts(mr).values()].filter((v) => v === "approve").length;
6720
7761
  const checks = mr.checks.length ? `${mr.checks.filter((c) => c.status === "pass").length}/${mr.checks.length} checks pass` : "no checks";
@@ -6729,7 +7770,7 @@ __export(exports_runtime, {
6729
7770
  hydrate: () => hydrate2,
6730
7771
  capture: () => capture
6731
7772
  });
6732
- import { chmodSync as chmodSync4, existsSync as existsSync14, lstatSync as lstatSync2, mkdirSync as mkdirSync8, readdirSync as readdirSync7, readFileSync as readFileSync14, readlinkSync as readlinkSync2, symlinkSync as symlinkSync2, unlinkSync as unlinkSync5, writeFileSync as writeFileSync13 } from "node:fs";
7773
+ import { chmodSync as chmodSync4, existsSync as existsSync15, lstatSync as lstatSync2, mkdirSync as mkdirSync8, readdirSync as readdirSync7, readFileSync as readFileSync15, readlinkSync as readlinkSync2, symlinkSync as symlinkSync2, unlinkSync as unlinkSync5, writeFileSync as writeFileSync13 } from "node:fs";
6733
7774
  import { platform } from "node:os";
6734
7775
  import { dirname as dirname3, join as join14, relative as relative2 } from "node:path";
6735
7776
  function isolateCommand(command, scratch) {
@@ -6793,7 +7834,7 @@ async function capture(repo, dir, keep = []) {
6793
7834
  const pats = ignorePatterns();
6794
7835
  const files = new Set(walkDir(dir, dir, pats));
6795
7836
  for (const k of keep)
6796
- if (existsSync14(join14(dir, k)))
7837
+ if (existsSync15(join14(dir, k)))
6797
7838
  files.add(k);
6798
7839
  const written = [];
6799
7840
  for (const f of files) {
@@ -6808,7 +7849,7 @@ async function capture(repo, dir, keep = []) {
6808
7849
  }
6809
7850
  continue;
6810
7851
  }
6811
- const buf = readFileSync14(abs);
7852
+ const buf = readFileSync15(abs);
6812
7853
  if (buf.includes(0)) {
6813
7854
  const cur = await repo.readBytes(f);
6814
7855
  if (cur === undefined || cur === SEALED || !Buffer.from(cur).equals(buf)) {
@@ -6825,7 +7866,7 @@ async function capture(repo, dir, keep = []) {
6825
7866
  }
6826
7867
  const deleted = [];
6827
7868
  for (const t of await repo.list()) {
6828
- if (!existsSync14(join14(dir, t))) {
7869
+ if (!existsSync15(join14(dir, t))) {
6829
7870
  await repo.deleteFile(t);
6830
7871
  deleted.push(t);
6831
7872
  }
@@ -6939,7 +7980,7 @@ __export(exports_git_adapter, {
6939
7980
  exportHistoryToGit: () => exportHistoryToGit
6940
7981
  });
6941
7982
  import { execFileSync } from "node:child_process";
6942
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync15, unlinkSync as unlinkSync6, writeFileSync as writeFileSync14 } from "node:fs";
7983
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, readFileSync as readFileSync16, unlinkSync as unlinkSync6, writeFileSync as writeFileSync14 } from "node:fs";
6943
7984
  import { join as join15 } from "node:path";
6944
7985
  import { deflateSync } from "node:zlib";
6945
7986
  async function importGitHead(gitPath, ws) {
@@ -7009,7 +8050,7 @@ async function applyGitFile(repo, path, mode, content) {
7009
8050
  }
7010
8051
  function setHead(fdir, head) {
7011
8052
  const hf = join15(fdir, "HEAD");
7012
- const cur = existsSync15(hf) ? JSON.parse(readFileSync15(hf, "utf8")) : { seq: 0 };
8053
+ const cur = existsSync16(hf) ? JSON.parse(readFileSync16(hf, "utf8")) : { seq: 0 };
7013
8054
  writeFileSync14(hf, JSON.stringify({ ...cur, head }));
7014
8055
  }
7015
8056
  async function importGitRepo(gitPath, fdir) {
@@ -7076,14 +8117,14 @@ function exportToGit(store2, head, gitPath, message) {
7076
8117
  }
7077
8118
  function gitObjectsDir(gitPath) {
7078
8119
  for (const c of [join15(gitPath, ".git", "objects"), join15(gitPath, "objects")])
7079
- if (existsSync15(c))
8120
+ if (existsSync16(c))
7080
8121
  return c;
7081
8122
  return;
7082
8123
  }
7083
8124
  function writeLooseObject(objectsDir2, o) {
7084
8125
  const dir = join15(objectsDir2, o.oid.slice(0, 2));
7085
8126
  const file = join15(dir, o.oid.slice(2));
7086
- if (existsSync15(file))
8127
+ if (existsSync16(file))
7087
8128
  return;
7088
8129
  const header = Buffer.from(`${o.type} ${o.content.length}\x00`);
7089
8130
  const framed = Buffer.concat([header, Buffer.from(o.content)]);
@@ -7200,14 +8241,14 @@ __export(exports_views, {
7200
8241
  loadViewsRegistry: () => loadViewsRegistry,
7201
8242
  createView: () => createView
7202
8243
  });
7203
- import { existsSync as existsSync16, mkdirSync as mkdirSync10, readdirSync as readdirSync8, readFileSync as readFileSync16, rmSync, writeFileSync as writeFileSync15 } from "node:fs";
8244
+ import { existsSync as existsSync17, mkdirSync as mkdirSync10, readdirSync as readdirSync8, readFileSync as readFileSync17, rmSync, writeFileSync as writeFileSync15 } from "node:fs";
7204
8245
  import { join as join16, relative as relative3, resolve as resolve3 } from "node:path";
7205
8246
  function loadViewsRegistry(parentSol) {
7206
8247
  const p = viewsRegistryPath(parentSol);
7207
- if (!existsSync16(p))
8248
+ if (!existsSync17(p))
7208
8249
  return { views: [] };
7209
8250
  try {
7210
- return JSON.parse(readFileSync16(p, "utf8"));
8251
+ return JSON.parse(readFileSync17(p, "utf8"));
7211
8252
  } catch {
7212
8253
  return { views: [] };
7213
8254
  }
@@ -7240,7 +8281,7 @@ async function viewStatuses(parentSol) {
7240
8281
  const out = [];
7241
8282
  for (const v of reg.views) {
7242
8283
  const viewSol = join16(v.dir, ".sol");
7243
- const exists = existsSync16(join16(viewSol, "view.json"));
8284
+ const exists = existsSync17(join16(viewSol, "view.json"));
7244
8285
  let head;
7245
8286
  if (exists) {
7246
8287
  head = await new FileOpLog2(viewSol).head();
@@ -7264,7 +8305,7 @@ function pruneViews(parentSol, opts = {}) {
7264
8305
  const reg = loadViewsRegistry(parentSol);
7265
8306
  const removed = [];
7266
8307
  reg.views = reg.views.filter((v) => {
7267
- const present = existsSync16(join16(v.dir, ".sol", "view.json"));
8308
+ const present = existsSync17(join16(v.dir, ".sol", "view.json"));
7268
8309
  const target = opts.name ? v.name === opts.name : !present;
7269
8310
  if (!target)
7270
8311
  return true;
@@ -7281,7 +8322,7 @@ function pruneViews(parentSol, opts = {}) {
7281
8322
  }
7282
8323
  function sharedObjectCount(parentSol) {
7283
8324
  const dir = join16(parentSol, "objects");
7284
- if (!existsSync16(dir))
8325
+ if (!existsSync17(dir))
7285
8326
  return 0;
7286
8327
  return readdirSync8(dir).filter((n) => !n.endsWith(".tmp")).length;
7287
8328
  }
@@ -7293,6 +8334,169 @@ var init_views = __esm(() => {
7293
8334
  init_lib();
7294
8335
  });
7295
8336
 
8337
+ // src/bin/agent-sessions.ts
8338
+ var exports_agent_sessions = {};
8339
+ __export(exports_agent_sessions, {
8340
+ stopSession: () => stopSession,
8341
+ sessions: () => sessions,
8342
+ sessionPath: () => sessionPath,
8343
+ sessionDir: () => sessionDir,
8344
+ saveSessionsRegistry: () => saveSessionsRegistry,
8345
+ pruneSession: () => pruneSession,
8346
+ loadSessionsRegistry: () => loadSessionsRegistry,
8347
+ fingerprintOfSeed: () => fingerprintOfSeed,
8348
+ createSession: () => createSession
8349
+ });
8350
+ import { chmodSync as chmodSync5, existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync18, rmSync as rmSync2, writeFileSync as writeFileSync16 } from "node:fs";
8351
+ import { randomBytes as randomBytes5 } from "node:crypto";
8352
+ import { basename, dirname as dirname4, join as join17, resolve as resolve4 } from "node:path";
8353
+ function sessionPath(parentSol) {
8354
+ return join17(parentSol, "sessions.json");
8355
+ }
8356
+ function sessionDir(parentSol, sessionId) {
8357
+ return join17(parentSol, "sessions", sessionId);
8358
+ }
8359
+ function loadSessionsRegistry(parentSol) {
8360
+ const p = sessionPath(parentSol);
8361
+ if (!existsSync18(p))
8362
+ return { sessions: [] };
8363
+ try {
8364
+ return JSON.parse(readFileSync18(p, "utf8"));
8365
+ } catch {
8366
+ return { sessions: [] };
8367
+ }
8368
+ }
8369
+ function saveSessionsRegistry(parentSol, reg) {
8370
+ writeFileSync16(sessionPath(parentSol), JSON.stringify(reg, null, 2));
8371
+ }
8372
+ function newSessionId(name) {
8373
+ const slug = name.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 24) || "agent";
8374
+ return `${slug}-${Date.now().toString(36)}-${randomBytes5(3).toString("hex")}`;
8375
+ }
8376
+ function createSession(opts) {
8377
+ const { parentSol, name, actor: actor2, check, metadata } = opts;
8378
+ if (!existsSync18(parentSol))
8379
+ throw new Error(`not a sol repo: ${parentSol}`);
8380
+ const sessionId = opts.sessionId ?? newSessionId(name);
8381
+ const sdir = sessionDir(parentSol, sessionId);
8382
+ if (existsSync18(sdir)) {
8383
+ if (opts.onExistingKey)
8384
+ opts.onExistingKey(sessionId);
8385
+ else
8386
+ throw new Error(`session already exists: ${sessionId} (${sdir})`);
8387
+ }
8388
+ const repoRoot2 = dirname4(parentSol);
8389
+ const viewDir = opts.dir ? resolve4(opts.dir) : join17(dirname4(repoRoot2), `${basename(repoRoot2)}-${name}`);
8390
+ if (existsSync18(join17(viewDir, ".sol")))
8391
+ throw new Error(`already a sol repo/view: ${viewDir}`);
8392
+ const branch = `view/${name}`;
8393
+ const startHead = headOf(parentSol);
8394
+ createView({ parentSol, viewDir, name, branch, actor: actor2, startHead });
8395
+ const kp = generateKeypair();
8396
+ mkdirSync11(sdir, { recursive: true });
8397
+ const signingKeyPath = join17(sdir, "signing.key");
8398
+ writeFileSync16(signingKeyPath, kp.seed, { mode: 384 });
8399
+ try {
8400
+ chmodSync5(signingKeyPath, 384);
8401
+ } catch {}
8402
+ if (check) {
8403
+ const cfg = loadSolConfig(parentSol);
8404
+ cfg.check = check;
8405
+ saveSolConfig(parentSol, cfg);
8406
+ }
8407
+ const record = {
8408
+ sessionId,
8409
+ name,
8410
+ viewDir: resolve4(viewDir),
8411
+ branch,
8412
+ actor: actor2,
8413
+ signingKeyPath: resolve4(signingKeyPath),
8414
+ fingerprint: kp.fingerprint,
8415
+ startHead,
8416
+ createdAt: Date.now(),
8417
+ ...metadata ? { metadata } : {}
8418
+ };
8419
+ const reg = loadSessionsRegistry(parentSol);
8420
+ reg.sessions = reg.sessions.filter((s) => s.sessionId !== sessionId);
8421
+ reg.sessions.push(record);
8422
+ saveSessionsRegistry(parentSol, reg);
8423
+ return record;
8424
+ }
8425
+ function headOf(parentSol) {
8426
+ const headFile = join17(parentSol, "HEAD");
8427
+ if (existsSync18(headFile)) {
8428
+ try {
8429
+ const h = JSON.parse(readFileSync18(headFile, "utf8"));
8430
+ if (h.head)
8431
+ return h.head;
8432
+ } catch {}
8433
+ }
8434
+ return emptyRoot2(new LazyStore(join17(parentSol, "objects")));
8435
+ }
8436
+ function matches(s, ref) {
8437
+ return s.sessionId === ref || s.name === ref;
8438
+ }
8439
+ function stopSession(parentSol, ref) {
8440
+ const reg = loadSessionsRegistry(parentSol);
8441
+ const s = reg.sessions.find((x) => x.sessionId === ref) ?? reg.sessions.find((x) => matches(x, ref));
8442
+ if (!s)
8443
+ return false;
8444
+ if (!s.stopAt) {
8445
+ s.stopAt = Date.now();
8446
+ saveSessionsRegistry(parentSol, reg);
8447
+ }
8448
+ return true;
8449
+ }
8450
+ async function sessions(parentSol) {
8451
+ const reg = loadSessionsRegistry(parentSol);
8452
+ const out = [];
8453
+ for (const s of reg.sessions) {
8454
+ const viewSol = join17(s.viewDir, ".sol");
8455
+ const exists = existsSync18(join17(viewSol, "view.json"));
8456
+ let head;
8457
+ if (exists)
8458
+ head = await new FileOpLog2(viewSol).head();
8459
+ const state = !exists ? "stale" : s.stopAt ? "stopped" : "running";
8460
+ out.push({ ...s, head, exists, state });
8461
+ }
8462
+ return out;
8463
+ }
8464
+ function pruneSession(parentSol, ref, deleteDir = false) {
8465
+ const reg = loadSessionsRegistry(parentSol);
8466
+ const removed = [];
8467
+ const drop = [];
8468
+ reg.sessions = reg.sessions.filter((s) => {
8469
+ const present = existsSync18(join17(s.viewDir, ".sol", "view.json"));
8470
+ const target = ref ? matches(s, ref) : !present;
8471
+ if (!target)
8472
+ return true;
8473
+ drop.push(s);
8474
+ removed.push(s.sessionId);
8475
+ return false;
8476
+ });
8477
+ for (const s of drop) {
8478
+ try {
8479
+ pruneViews(parentSol, { name: s.name, deleteDir });
8480
+ } catch {}
8481
+ try {
8482
+ rmSync2(sessionDir(parentSol, s.sessionId), { recursive: true, force: true });
8483
+ } catch {}
8484
+ }
8485
+ saveSessionsRegistry(parentSol, reg);
8486
+ return removed;
8487
+ }
8488
+ function fingerprintOfSeed(seed) {
8489
+ return fingerprintOf(publicOf(loadPrivateKey(seed)));
8490
+ }
8491
+ var init_agent_sessions = __esm(() => {
8492
+ init_file_store2();
8493
+ init_sign();
8494
+ init_tree();
8495
+ init_lib();
8496
+ init_test_gate();
8497
+ init_views();
8498
+ });
8499
+
7296
8500
  // src/bin/secret.ts
7297
8501
  var exports_secret2 = {};
7298
8502
  __export(exports_secret2, {
@@ -7300,7 +8504,7 @@ __export(exports_secret2, {
7300
8504
  runEnv: () => runEnv,
7301
8505
  resolveReference: () => resolveReference
7302
8506
  });
7303
- import { readFileSync as readFileSync17, readSync, writeSync } from "node:fs";
8507
+ import { readFileSync as readFileSync19, readSync, writeSync } from "node:fs";
7304
8508
  import { spawnSync as spawnSync2 } from "node:child_process";
7305
8509
  function flag(args, name) {
7306
8510
  const i = args.indexOf(name);
@@ -7672,7 +8876,7 @@ async function envValidate(ctx, args, json) {
7672
8876
  }
7673
8877
  function readSchemaText(solDir2) {
7674
8878
  try {
7675
- return readFileSync17(schemaLockPath(solDir2), "utf8");
8879
+ return readFileSync19(schemaLockPath(solDir2), "utf8");
7676
8880
  } catch {
7677
8881
  return;
7678
8882
  }
@@ -8354,8 +9558,8 @@ var exports_sol_secret_mcp = {};
8354
9558
  __export(exports_sol_secret_mcp, {
8355
9559
  startSecretMcp: () => startSecretMcp
8356
9560
  });
8357
- import { existsSync as existsSync17 } from "node:fs";
8358
- import { join as join17 } from "node:path";
9561
+ import { existsSync as existsSync19 } from "node:fs";
9562
+ import { join as join18 } from "node:path";
8359
9563
  async function buildSecretServer(solDir2) {
8360
9564
  const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
8361
9565
  const { CallToolRequestSchema, ListToolsRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
@@ -8391,8 +9595,8 @@ async function buildSecretServer(solDir2) {
8391
9595
  return server;
8392
9596
  }
8393
9597
  async function startSecretMcp(opts = {}) {
8394
- const solDir2 = opts.solDir || process.env.SOL_DIR || join17(process.cwd(), ".sol");
8395
- if (!existsSync17(solDir2)) {
9598
+ const solDir2 = opts.solDir || process.env.SOL_DIR || join18(process.cwd(), ".sol");
9599
+ if (!existsSync19(solDir2)) {
8396
9600
  process.stderr.write(`sol-secret-mcp: no .sol at ${solDir2} — run \`sol init\` first (or set SOL_DIR)
8397
9601
  `);
8398
9602
  process.exit(1);
@@ -8420,9 +9624,9 @@ __export(exports_dispatch, {
8420
9624
  dispatch: () => dispatch
8421
9625
  });
8422
9626
  import { execFileSync as execFileSync2 } from "node:child_process";
8423
- import { existsSync as existsSync18, mkdirSync as mkdirSync11, mkdtempSync, readdirSync as readdirSync9, readFileSync as readFileSync18, rmSync as rmSync2, unlinkSync as unlinkSync7, watch, writeFileSync as writeFileSync16 } from "node:fs";
9627
+ import { accessSync, constants as fsConstants, existsSync as existsSync20, mkdirSync as mkdirSync12, mkdtempSync, readdirSync as readdirSync9, readFileSync as readFileSync20, rmSync as rmSync3, unlinkSync as unlinkSync7, watch, writeFileSync as writeFileSync17 } from "node:fs";
8424
9628
  import { homedir as homedir2, hostname, platform as platform2, tmpdir } from "node:os";
8425
- import { basename, dirname as dirname4, join as join18, resolve as resolve4, sep as sep2 } from "node:path";
9629
+ import { basename as basename2, dirname as dirname5, join as join19, resolve as resolve5, sep as sep2 } from "node:path";
8426
9630
  function globCovers(pattern, path) {
8427
9631
  let re = "";
8428
9632
  for (let i = 0;i < pattern.length; i++) {
@@ -8442,6 +9646,14 @@ function globCovers(pattern, path) {
8442
9646
  }
8443
9647
  return new RegExp(`^${re}$`).test(path);
8444
9648
  }
9649
+ function writableTag(path) {
9650
+ try {
9651
+ accessSync(path, fsConstants.R_OK | fsConstants.W_OK);
9652
+ return "RW";
9653
+ } catch {
9654
+ return "READ-ONLY";
9655
+ }
9656
+ }
8445
9657
  function tokenClaims(token) {
8446
9658
  try {
8447
9659
  return JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString());
@@ -8450,11 +9662,11 @@ function tokenClaims(token) {
8450
9662
  }
8451
9663
  }
8452
9664
  async function loadStoredToken() {
8453
- if (!existsSync18(CRED_PATH))
9665
+ if (!existsSync20(CRED_PATH))
8454
9666
  return;
8455
9667
  let creds;
8456
9668
  try {
8457
- creds = JSON.parse(readFileSync18(CRED_PATH, "utf8"));
9669
+ creds = JSON.parse(readFileSync20(CRED_PATH, "utf8"));
8458
9670
  } catch {
8459
9671
  return;
8460
9672
  }
@@ -8469,7 +9681,7 @@ async function loadStoredToken() {
8469
9681
  if (res.ok) {
8470
9682
  const r = await res.json();
8471
9683
  if (r.accessToken) {
8472
- writeFileSync16(CRED_PATH, JSON.stringify({ ...creds, accessToken: r.accessToken, refreshToken: r.refreshToken ?? creds.refreshToken }, null, 2), { mode: 384 });
9684
+ writeFileSync17(CRED_PATH, JSON.stringify({ ...creds, accessToken: r.accessToken, refreshToken: r.refreshToken ?? creds.refreshToken }, null, 2), { mode: 384 });
8473
9685
  return r.accessToken;
8474
9686
  }
8475
9687
  }
@@ -8490,7 +9702,7 @@ function authHost() {
8490
9702
  if (process.env.SOL_AUTH)
8491
9703
  return process.env.SOL_AUTH.replace(/\/+$/, "");
8492
9704
  try {
8493
- const w = JSON.parse(readFileSync18(CRED_PATH, "utf8")).webUrl;
9705
+ const w = JSON.parse(readFileSync20(CRED_PATH, "utf8")).webUrl;
8494
9706
  if (w)
8495
9707
  return w.replace(/\/+$/, "");
8496
9708
  } catch {}
@@ -8514,11 +9726,11 @@ async function resolveMcpHttp(a, label) {
8514
9726
  die(`invalid --port: ${portRaw}`);
8515
9727
  return { token, port, host: flagVal("--host"), label: label ?? (a.includes("--secret") || a.includes("--secrets") ? "sol-secrets" : "sol") };
8516
9728
  }
8517
- function resolveRemote(solDir2) {
9729
+ function resolveRemote2(solDir2) {
8518
9730
  const cfg = loadRemote(solDir2);
8519
9731
  if (!cfg)
8520
9732
  return;
8521
- return cfg.url ? cfg : { ...cfg, url: DEFAULT_REMOTE_URL };
9733
+ return cfg.url ? cfg : { ...cfg, url: DEFAULT_REMOTE_URL2 };
8522
9734
  }
8523
9735
  async function pushEnvState(solDirPath, cfg, token) {
8524
9736
  const { readEnvStateBundle: readEnvStateBundle2 } = await Promise.resolve().then(() => (init_anchor(), exports_anchor));
@@ -8537,6 +9749,17 @@ ENV ANCHOR REJECTED by the remote: ${e?.message ?? String(e)}
8537
9749
  process.exitCode = 1;
8538
9750
  }
8539
9751
  }
9752
+ async function surfaceOwner(cfg, token) {
9753
+ const slash = cfg.repo.indexOf("/");
9754
+ if (slash < 0)
9755
+ return;
9756
+ const owner = cfg.repo.slice(0, slash);
9757
+ try {
9758
+ const res = await ownerSet(cfg, token, owner);
9759
+ if (res.error)
9760
+ console.error(`note: could not set repo owner to '${owner}': ${res.error}`);
9761
+ } catch {}
9762
+ }
8540
9763
  async function pullEnvState(solDirPath, cfg, token) {
8541
9764
  try {
8542
9765
  const { bundle } = await remoteEnvPull(cfg, token);
@@ -8569,7 +9792,7 @@ function cliVersion() {
8569
9792
  if (typeof __SOL_COMPILED_VERSION__ === "string" && __SOL_COMPILED_VERSION__)
8570
9793
  return __SOL_COMPILED_VERSION__;
8571
9794
  try {
8572
- return JSON.parse(readFileSync18(new URL("./package.json", import.meta.url), "utf8")).version || "dev";
9795
+ return JSON.parse(readFileSync20(new URL("./package.json", import.meta.url), "utf8")).version || "dev";
8573
9796
  } catch {
8574
9797
  return "dev";
8575
9798
  }
@@ -8616,7 +9839,7 @@ examples:
8616
9839
  sol commit "add login route"
8617
9840
  sol commit -m "fix parser" src/parse.ts src/lex.ts`,
8618
9841
  push: `sol push push the current branch to the configured remote (converging — never FF-rejected)
8619
- sol push <repo> if no remote is set, use the hosted Sol (${DEFAULT_REMOTE_URL}) + this repo name, then push
9842
+ sol push <repo> if no remote is set, use the hosted Sol (${DEFAULT_REMOTE_URL2}) + this repo name, then push
8620
9843
  sol push --create <repo> same, explicit form (creates the repo on first push)
8621
9844
  sol push --public <repo> create + push + make it public in ONE step (else new repos are private)
8622
9845
 
@@ -8639,6 +9862,21 @@ examples:
8639
9862
  notes:
8640
9863
  refuses to run over a dirty tree — commit or discard first. conflicts land in the working tree with markers.
8641
9864
  refuses to run while UNRESOLVED <<<<<<< markers remain — resolve them and \`sol commit\`, then pull again.`,
9865
+ sync: `sol sync --watch HANDS-OFF bidirectional sync daemon — auto-capture + push every edit,
9866
+ and converge safe remote updates in on a timer. no CLI invocations needed:
9867
+ the working tree stays in sync while every local edit is durably captured.
9868
+ --poll-interval <ms> how often to poll the remote for updates (default 5000)
9869
+ --no-push-immediately capture locally but do NOT auto-push (push later with \`sol push\`)
9870
+ --dry-run log what it WOULD do, mutate nothing
9871
+
9872
+ how it works (three loops over the same core as watch/push/pull):
9873
+ 1. every file change is auto-captured into the op-log (nothing lost, even offline)
9874
+ 2. after each capture it pushes the delta; the server may converge a concurrent commit in
9875
+ 3. it polls the remote; a moved head pulls in via converge() — NEVER clobbering an in-flight edit
9876
+ notes:
9877
+ needs a remote (\`sol remote <url> <repo>\` or \`sol push <repo>\` once) + auth (\`sol auth login\` / SOL_TOKEN).
9878
+ a dirty tree DEFERS a pull to the next poll (your edits are captured first). Ctrl-C exits cleanly.
9879
+ transient network errors are logged + retried; a fatal error (corrupt repo / auth) exits loud.`,
8642
9880
  hide: `sol hide <pattern> [--role write|admin] [--team <id>] [--users a,b] [--escrow] [--no-list] [--hide-names] [--hide-existence] [--strict]
8643
9881
  add/update a VisibilityPolicy RULE — the HEADLINE verb. binds a glob over tree
8644
9882
  paths to an AUDIENCE (who may DECRYPT). the rule is host-visible METADATA (no
@@ -8752,6 +9990,9 @@ examples:
8752
9990
  sol keys verify alice 1a2b-3c4d-... # confirm alice's key out-of-band before sealing to her`,
8753
9991
  remote: `sol remote show the configured remote
8754
9992
  sol remote <url> <repo> set the remote (url + repo name)
9993
+ sol remote verify REMOTE-side health check (the backend's /fsck) — differentiates auth
9994
+ (session expired / no access), backend (5xx, transient), connectivity,
9995
+ and a 200-but-corrupt remote repo. needs SOL_TOKEN (or \`sol auth login\`).
8755
9996
 
8756
9997
  tip: \`sol push <repo>\` configures the hosted remote for you in one step.`,
8757
9998
  branch: `sol branch list branches (* = current)
@@ -8816,6 +10057,27 @@ what it's for (TEST-GATED CONVERGENCE):
8816
10057
  hydrate the current tree to a sandbox, run the command, capture produced files back into a commit.
8817
10058
  --isolate confines the run (no network, writes stay in the sandbox).`,
8818
10059
  promote: "sol promote [branch] point the remote's production branch at <branch> (default: current branch)",
10060
+ doctor: `sol doctor — comprehensive LOCAL health check (exit 0 = OK, 1 = problems found)
10061
+
10062
+ checks: op-log chain integrity + contiguity, object reachability from head, auth (SOL_TOKEN / stored
10063
+ credentials + expiry), .sol file permissions, and the sealed keyring. read-only — it diagnoses but never
10064
+ changes anything. to RECOVER from what it finds: \`sol fsck --repair\` (op-log/head) or \`sol recover\` (cloud/backup).`,
10065
+ fsck: `sol fsck verify the op-log chain + object reachability (exit 0 = OK, 1 = problems)
10066
+ sol fsck --repair recover a corrupt repo:
10067
+ - head pointer lost -> re-point to the last resolvable historical root
10068
+ - head dangling -> re-point to the newest root the store still has
10069
+ - op-log chain broken -> truncate to the last verified link, re-point head
10070
+
10071
+ every repair appends an explicit CHECKPOINT recording what happened (visible in \`sol log\`/\`sol reflog\`),
10072
+ so the recovery is itself in the tamper-evident history. idempotent: re-running on a healthy repo is a no-op.`,
10073
+ recover: `sol recover the recovery guide (the menu below)
10074
+ sol recover --from-remote <url> <repo> rehydrate from the cloud
10075
+ sol recover --list-all-roots every root the repo ever landed on (find a lost head)
10076
+ sol recover --file <path> [--from <seq|hash>] restore a file from history (even if the tree dropped it)
10077
+ sol recover --dump-log [<from> [<to>]] dump raw op-log entries as JSON (manual salvage)`,
10078
+ reflog: `sol reflog [<branch>] [--by <actor>] [--since <YYYY-MM-DD>]
10079
+ the op-log as a ref-move history (git reflog): each line is a head move (from -> to) with seq, actor, time,
10080
+ and message. a merge shows as "merge". a <branch> arg keeps only checkpoints whose message labels it.`,
8819
10081
  git: `sol git import <repo> [dir] import a git repo's HEAD into a new Sol repo
8820
10082
  sol git export <repo> [-b branch] replay the Sol commit DAG as git history (+ provenance trailers), then \`git push\``,
8821
10083
  mr: `sol mr open [--from <branch>] [--to <branch>] [--upstream <repo>] -t "title" [-m body]
@@ -9105,7 +10367,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9105
10367
  try {
9106
10368
  const { loadSelfIdentity: loadSelfIdentity2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
9107
10369
  const { openContent: openContent2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
9108
- const ring = existsSync18(solDir) ? loadKeyRing() : new (await Promise.resolve().then(() => (init_crypto(), exports_crypto))).KeyRing;
10370
+ const ring = existsSync20(solDir) ? loadKeyRing() : new (await Promise.resolve().then(() => (init_crypto(), exports_crypto))).KeyRing;
9109
10371
  const self = loadSelfIdentity2();
9110
10372
  setSealedDecryptor((boxStr) => {
9111
10373
  const box = JSON.parse(boxStr);
@@ -9115,19 +10377,19 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9115
10377
  } catch {}
9116
10378
  }
9117
10379
  const servesMcp = cmd === "mcp" || cmd === "secret" && args[0] === "mcp";
9118
- const release = !servesMcp && new Set(["add", "track", "commit", "checkpoint", "rm", "gc", "branch", "tag", "switch", "merge", "undo", "revert", "pull", "push", "restore", "checkout", "run", "seal", "view", "env", "secret"]).has(cmd) && existsSync18(solDir) ? acquireLock() : undefined;
10380
+ const release = !servesMcp && new Set(["add", "track", "commit", "checkpoint", "rm", "gc", "branch", "tag", "switch", "merge", "undo", "revert", "pull", "push", "restore", "checkout", "run", "seal", "view", "agent", "env", "secret"]).has(cmd) && existsSync20(solDir) ? acquireLock() : undefined;
9119
10381
  try {
9120
10382
  switch (cmd) {
9121
10383
  case "init": {
9122
- const here = join18(procCwd, ".sol");
9123
- if (existsSync18(here))
10384
+ const here = join19(procCwd, ".sol");
10385
+ if (existsSync20(here))
9124
10386
  die("already a sol repo: " + procCwd);
9125
10387
  if (repoRoot && repoRoot !== procCwd && !args.includes("--force")) {
9126
10388
  die(`already inside a Sol repo at ${repoRoot}
9127
10389
  -> just commit into it: \`sol commit ...\` works from here (sol walks up to find the repo)
9128
10390
  -> to nest a NEW repo here anyway: \`sol init --force\``);
9129
10391
  }
9130
- mkdirSync11(here, { recursive: true });
10392
+ mkdirSync12(here, { recursive: true });
9131
10393
  new FileStore2(here);
9132
10394
  console.log(`initialized empty sol repo in ${here}`);
9133
10395
  break;
@@ -9157,7 +10419,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9157
10419
  const h = tok ? identityFromToken(tok)?.handle : undefined;
9158
10420
  return h || die("no account identity — run `sol auth login` (or set SOL_ACCOUNT for self-host)");
9159
10421
  };
9160
- const dirUrl = (process.env.SOL_REMOTE || DEFAULT_REMOTE_URL).replace(/\/+$/, "");
10422
+ const dirUrl = (process.env.SOL_REMOTE || DEFAULT_REMOTE_URL2).replace(/\/+$/, "");
9161
10423
  const sub = args[0];
9162
10424
  const wantJson = args.includes("--json");
9163
10425
  if (sub === "init") {
@@ -9243,7 +10505,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9243
10505
  if (sub === "import") {
9244
10506
  const passphrase = process.env.SOL_KEYSTORE_PASSPHRASE || die("set SOL_KEYSTORE_PASSPHRASE to decrypt the bundle");
9245
10507
  const file = args[1] || die("usage: sol keys import <bundle.json> (set SOL_KEYSTORE_PASSPHRASE)");
9246
- const bundle = JSON.parse(readFileSync18(file, "utf8"));
10508
+ const bundle = JSON.parse(readFileSync20(file, "utf8"));
9247
10509
  let recovered;
9248
10510
  try {
9249
10511
  recovered = importBundle2(bundle, passphrase);
@@ -9274,7 +10536,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9274
10536
  break;
9275
10537
  }
9276
10538
  case "trust": {
9277
- if (!existsSync18(solDir))
10539
+ if (!existsSync20(solDir))
9278
10540
  die("not a sol repo — run `sol init` first");
9279
10541
  const map = loadTrust();
9280
10542
  if (args[0] === "--remove" || args[0] === "-r") {
@@ -9321,7 +10583,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9321
10583
  }
9322
10584
  case "track":
9323
10585
  case "add": {
9324
- if (!existsSync18(solDir))
10586
+ if (!existsSync20(solDir))
9325
10587
  die("not a sol repo — run `sol init` first");
9326
10588
  const files = args.filter((a) => a !== "." && !a.startsWith("-"));
9327
10589
  if (!files.length) {
@@ -9332,7 +10594,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9332
10594
  let n = 0;
9333
10595
  for (const f of files) {
9334
10596
  const rf = repoRel(f);
9335
- if (!existsSync18(join18(cwd, rf))) {
10597
+ if (!existsSync20(join19(cwd, rf))) {
9336
10598
  console.error("skip (not on disk): " + f);
9337
10599
  continue;
9338
10600
  }
@@ -9361,14 +10623,14 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9361
10623
  if (!message)
9362
10624
  die('commit needs a message: sol commit "what you did" (scoped: sol commit -m "msg" file1 file2)');
9363
10625
  const parentHead = await repo.head();
9364
- const mergeHeadPath = join18(solDir, "MERGE_HEAD");
9365
- const parent2 = existsSync18(mergeHeadPath) ? readFileSync18(mergeHeadPath, "utf8").trim() || undefined : undefined;
10626
+ const mergeHeadPath = join19(solDir, "MERGE_HEAD");
10627
+ const parent2 = existsSync20(mergeHeadPath) ? readFileSync20(mergeHeadPath, "utf8").trim() || undefined : undefined;
9366
10628
  let changed = 0;
9367
10629
  let commitRoot = parentHead;
9368
10630
  if (paths.length) {
9369
10631
  for (const p of paths) {
9370
10632
  const rp = repoRel(p);
9371
- if (existsSync18(join18(cwd, rp))) {
10633
+ if (existsSync20(join19(cwd, rp))) {
9372
10634
  if (await snapshotFile(repo, rp))
9373
10635
  changed++;
9374
10636
  } else if ((await repo.list()).includes(rp)) {
@@ -9423,7 +10685,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9423
10685
  }
9424
10686
  case "status": {
9425
10687
  const { repo, log } = open();
9426
- const refs = existsSync18(refsPath()) ? await loadRefs(log) : undefined;
10688
+ const refs = existsSync20(refsPath()) ? await loadRefs(log) : undefined;
9427
10689
  const head = await repo.head();
9428
10690
  const headOp = head ? [...await log.history()].reverse().find((o) => o.rootAfter === head) : undefined;
9429
10691
  const headBy = headOp?.by ?? "?";
@@ -9513,7 +10775,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9513
10775
  const { listAll: listAll3 } = await Promise.resolve().then(() => (init_tree(), exports_tree));
9514
10776
  const { loadSelfIdentity: loadSelfIdentity2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
9515
10777
  const { openContent: openContent2, UNREADABLE: UNREADABLE2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
9516
- const ring = existsSync18(solDir) ? loadKeyRing() : new (await Promise.resolve().then(() => (init_crypto(), exports_crypto))).KeyRing;
10778
+ const ring = existsSync20(solDir) ? loadKeyRing() : new (await Promise.resolve().then(() => (init_crypto(), exports_crypto))).KeyRing;
9517
10779
  const self = loadSelfIdentity2();
9518
10780
  const decrypt = (boxStr) => {
9519
10781
  try {
@@ -9561,7 +10823,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9561
10823
  const { readFile: readTree, entryKindAt: kindAt } = await Promise.resolve().then(() => (init_tree(), exports_tree));
9562
10824
  const { loadSelfIdentity: loadSelfIdentity2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
9563
10825
  const { openContent: openContent2, UNREADABLE: UNREADABLE2, KeyRing: KeyRing3 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
9564
- const ring = existsSync18(solDir) ? loadKeyRing() : new KeyRing3;
10826
+ const ring = existsSync20(solDir) ? loadKeyRing() : new KeyRing3;
9565
10827
  const self = loadSelfIdentity2();
9566
10828
  const decrypt = (boxStr) => {
9567
10829
  try {
@@ -9660,7 +10922,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9660
10922
  };
9661
10923
  const live = await log.head() ?? "";
9662
10924
  const refArg = args.find((a) => !a.startsWith("-"));
9663
- const lrefs = existsSync18(refsPath()) ? JSON.parse(readFileSync18(refsPath(), "utf8")) : null;
10925
+ const lrefs = existsSync20(refsPath()) ? JSON.parse(readFileSync20(refsPath(), "utf8")) : null;
9664
10926
  let tipRoot = live;
9665
10927
  if (refArg) {
9666
10928
  tipRoot = lrefs?.branches[refArg]?.head ?? refArg;
@@ -9713,7 +10975,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9713
10975
  const path = args[0] || die("rm needs a path");
9714
10976
  let onDisk = false;
9715
10977
  try {
9716
- unlinkSync7(join18(cwd, path));
10978
+ unlinkSync7(join19(cwd, path));
9717
10979
  onDisk = true;
9718
10980
  } catch {}
9719
10981
  if (onDisk) {
@@ -9911,15 +11173,167 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9911
11173
  };
9912
11174
  walk(head);
9913
11175
  }
9914
- const ok = chainOk && missing.length === 0;
11176
+ const headLost = head === undefined && ops.length > 0;
11177
+ const ok = chainOk && missing.length === 0 && !headLost;
11178
+ if (args.includes("--repair")) {
11179
+ if (ok) {
11180
+ console.log("fsck --repair: nothing to repair — the repo is OK");
11181
+ process.exitCode = 0;
11182
+ break;
11183
+ }
11184
+ const { applyRepair: applyRepair2 } = await Promise.resolve().then(() => (init_admin_repair(), exports_admin_repair));
11185
+ const { FileStore: FileStore3 } = await Promise.resolve().then(() => (init_file_store2(), exports_file_store));
11186
+ const fileStore = new FileStore3(solDir, objectsDir());
11187
+ const plan = await applyRepair2({
11188
+ store: fileStore,
11189
+ log,
11190
+ truncate: async (toSeq) => truncateOpLog(toSeq),
11191
+ setHead: async (h) => setOpLogHead(h)
11192
+ }, "sol-doctor");
11193
+ console.log(`fsck --repair: ${plan.reason}`);
11194
+ if (plan.kind === "noop")
11195
+ console.log(" (already healthy — nothing changed)");
11196
+ else if (plan.kind === "unfixable") {
11197
+ console.log(" status: UNFIXABLE — local repair cannot recover this; run `sol recover --from-remote <url> <repo>` to rehydrate");
11198
+ process.exitCode = 1;
11199
+ break;
11200
+ } else {
11201
+ console.log(` ${plan.note}`);
11202
+ console.log(" status: repaired — run `sol status` (or `sol fsck`) to verify");
11203
+ }
11204
+ process.exitCode = 0;
11205
+ break;
11206
+ }
9915
11207
  console.log(`fsck: ${ok ? "OK" : "PROBLEMS FOUND"}`);
9916
11208
  console.log(` op-log: ${ops.length} op(s), chain ${chainOk ? "valid + contiguous" : "BROKEN — " + chainErr}`);
9917
- console.log(` head: ${head ? head.slice(0, 16) : "(empty)"}, ${missing.length} missing object(s)`);
11209
+ if (headLost)
11210
+ console.log(` head: LOST (${ops.length} op(s) but no head pointer) — run \`sol fsck --repair\``);
11211
+ else
11212
+ console.log(` head: ${head ? head.slice(0, 16) : "(empty)"}, ${missing.length} missing object(s)`);
9918
11213
  for (const mh of missing)
9919
11214
  console.log(" missing " + mh);
11215
+ if (!ok)
11216
+ console.log(" -> `sol doctor` for a full diagnosis, `sol fsck --repair` to recover");
9920
11217
  process.exitCode = ok ? 0 : 1;
9921
11218
  break;
9922
11219
  }
11220
+ case "doctor": {
11221
+ const { runDoctor: runDoctor2, renderDoctorIntegrity: renderDoctorIntegrity2 } = await Promise.resolve().then(() => (init_admin(), exports_admin));
11222
+ const { log } = open();
11223
+ const store2 = loadStore();
11224
+ const asyncStore = { get: async (h) => store2.get(h), put: async () => die("doctor is read-only"), has: async (h) => store2.has(h) };
11225
+ const report = await runDoctor2(asyncStore, log);
11226
+ console.log("sol doctor");
11227
+ for (const l of renderDoctorIntegrity2(report))
11228
+ console.log(l);
11229
+ const envTok = process.env.SOL_TOKEN;
11230
+ if (envTok) {
11231
+ const id = identityFromToken(envTok);
11232
+ console.log(` auth: SOL_TOKEN set${id?.handle ? ` (@${id.handle})` : id?.email ? ` (${id.email})` : ""}`);
11233
+ } else if (existsSync20(CRED_PATH)) {
11234
+ try {
11235
+ const creds = JSON.parse(readFileSync20(CRED_PATH, "utf8"));
11236
+ const c = creds.accessToken ? tokenClaims(creds.accessToken) : {};
11237
+ const exp = typeof c.exp === "number" ? new Date(c.exp * 1000).toISOString().slice(0, 10) : "?";
11238
+ const stale = typeof c.exp === "number" && c.exp * 1000 <= Date.now();
11239
+ console.log(` auth: logged in${c.handle ? ` as @${c.handle}` : ""}${stale ? " (EXPIRED — run `sol auth login`)" : ` (expires ${exp})`}`);
11240
+ } catch {
11241
+ console.log(" auth: stored credentials unreadable — run `sol auth login`");
11242
+ }
11243
+ } else {
11244
+ console.log(" auth: not logged in — `sol auth login` (or set SOL_TOKEN) for remote commands");
11245
+ }
11246
+ const objDir = join19(solDir, "objects");
11247
+ const opsFile = join19(solDir, "ops.jsonl");
11248
+ console.log(` files: .sol/objects/ ${writableTag(objDir)}, .sol/ops.jsonl ${existsSync20(opsFile) ? writableTag(opsFile) : "(none yet)"}`);
11249
+ const keyringPath = join19(homedir2(), ".sol", "keyring.json");
11250
+ console.log(` sealed: ${existsSync20(keyringPath) ? "keyring found, ready" : "no keyring (only needed for sealed paths)"}`);
11251
+ console.log(`status: ${report.ok ? "OK" : "PROBLEMS FOUND"}`);
11252
+ if (!report.ok)
11253
+ console.log(" -> `sol fsck --repair` to recover the op-log/head; `sol recover` for cloud/backup rehydrate");
11254
+ process.exitCode = report.ok ? 0 : 1;
11255
+ break;
11256
+ }
11257
+ case "reflog": {
11258
+ const { renderReflog: renderReflog2 } = await Promise.resolve().then(() => exports_admin_reflog);
11259
+ const { log } = open();
11260
+ const ops = await log.history();
11261
+ const flag2 = (name) => {
11262
+ const i = args.indexOf(name);
11263
+ return i >= 0 ? args[i + 1] : undefined;
11264
+ };
11265
+ const branchFilter = args[0] && !args[0].startsWith("--") ? args[0] : undefined;
11266
+ const by = flag2("--by");
11267
+ const sinceStr = flag2("--since");
11268
+ const since = sinceStr ? Date.parse(sinceStr) : undefined;
11269
+ if (sinceStr && Number.isNaN(since))
11270
+ die(`--since: not a date: ${sinceStr} (try YYYY-MM-DD)`);
11271
+ const lines2 = renderReflog2(ops, { branchFilter, actor: by, since });
11272
+ if (!lines2.length)
11273
+ console.log("(no matching ops)");
11274
+ for (const l of lines2)
11275
+ console.log(l);
11276
+ break;
11277
+ }
11278
+ case "recover": {
11279
+ const { RECOVER_HELP: RECOVER_HELP2, listAllRoots: listAllRoots2, restoreFileFromHistory: restoreFileFromHistory2, dumpLog: dumpLog2 } = await Promise.resolve().then(() => (init_admin_recover(), exports_admin_recover));
11280
+ const flag2 = (name) => {
11281
+ const i = args.indexOf(name);
11282
+ return i >= 0 ? args[i + 1] : undefined;
11283
+ };
11284
+ if (!args.length || args[0] === "--help") {
11285
+ console.log(RECOVER_HELP2);
11286
+ break;
11287
+ }
11288
+ if (args.includes("--from-remote")) {
11289
+ const url = args[args.indexOf("--from-remote") + 1];
11290
+ const repoName = args[args.indexOf("--from-remote") + 2];
11291
+ if (!url || !repoName)
11292
+ die("usage: sol recover --from-remote <url> <repo>");
11293
+ console.log(`to rehydrate from the cloud, clone the remote into a fresh dir:
11294
+ sol clone ${url} ${repoName}
11295
+ then copy your uncommitted working files in. (a destructive in-place rehydrate is intentionally manual.)`);
11296
+ break;
11297
+ }
11298
+ const { log } = open();
11299
+ const store2 = loadStore();
11300
+ const ops = await log.history();
11301
+ const asyncStore = { get: async (h) => store2.get(h), put: async () => die("recover is read-only"), has: async (h) => store2.has(h) };
11302
+ if (args.includes("--list-all-roots")) {
11303
+ const roots = await listAllRoots2(ops, async (h) => store2.has(h));
11304
+ for (const r of roots)
11305
+ console.log(` ${r.root.slice(0, 16)} seq ${r.seq} ${r.resolvable ? "resolvable" : "MISSING"}${r.message ? ` ${r.message}` : ""}`);
11306
+ if (!roots.length)
11307
+ console.log(" (no roots — empty repo)");
11308
+ break;
11309
+ }
11310
+ if (args.includes("--file")) {
11311
+ const path = flag2("--file") || die("usage: sol recover --file <path> [--from <seq|hash>]");
11312
+ const fromStr = flag2("--from");
11313
+ const from = fromStr ? /^\d+$/.test(fromStr) ? Number(fromStr) : fromStr : undefined;
11314
+ const rec = await restoreFileFromHistory2(asyncStore, ops, path, from);
11315
+ if (!rec.found)
11316
+ die(rec.reason);
11317
+ if ("sealed" in rec) {
11318
+ console.log(`${path} is sealed at seq ${rec.fromSeq} — recovered the opaque box (open it with your key):`);
11319
+ console.log(rec.box);
11320
+ } else {
11321
+ const out = rec.encoding === "base64" ? Buffer.from(rec.content, "base64") : rec.content;
11322
+ writeFileSync17(join19(cwd, path), out);
11323
+ console.log(`restored ${path} from seq ${rec.fromSeq} (root ${rec.fromRoot.slice(0, 16)}) -> wrote to disk; \`sol add ${path}\` to re-track`);
11324
+ }
11325
+ break;
11326
+ }
11327
+ if (args.includes("--dump-log")) {
11328
+ const i = args.indexOf("--dump-log");
11329
+ const fromSeq = args[i + 1] && /^\d+$/.test(args[i + 1]) ? Number(args[i + 1]) : undefined;
11330
+ const toSeq = args[i + 2] && /^\d+$/.test(args[i + 2]) ? Number(args[i + 2]) : undefined;
11331
+ console.log(dumpLog2(ops, fromSeq, toSeq));
11332
+ break;
11333
+ }
11334
+ console.log(RECOVER_HELP2);
11335
+ break;
11336
+ }
9923
11337
  case "gc": {
9924
11338
  const { log } = open();
9925
11339
  const store2 = loadStore();
@@ -9941,16 +11355,16 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9941
11355
  if (op.prov)
9942
11356
  walk(op.prov);
9943
11357
  }
9944
- const objDir = join18(solDir, "objects");
11358
+ const objDir = join19(solDir, "objects");
9945
11359
  let removed = 0;
9946
11360
  for (const name of readdirSync9(objDir)) {
9947
11361
  if (name.endsWith(".tmp") || !reachable.has(name)) {
9948
- unlinkSync7(join18(objDir, name));
11362
+ unlinkSync7(join19(objDir, name));
9949
11363
  removed++;
9950
11364
  }
9951
11365
  }
9952
11366
  console.log(`gc: kept ${reachable.size} object(s), removed ${removed} unreachable`);
9953
- if (existsSync18(join18(solDir, "env", "seal"))) {
11367
+ if (existsSync20(join19(solDir, "env", "seal"))) {
9954
11368
  const { gcStaleStanzas: gcStaleStanzas2 } = await Promise.resolve().then(() => (init_secret(), exports_secret));
9955
11369
  const st = gcStaleStanzas2(solDir);
9956
11370
  if (st.removed)
@@ -9965,8 +11379,8 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9965
11379
  console.log(p);
9966
11380
  break;
9967
11381
  }
9968
- const f = join18(cwd, ".solignore");
9969
- const lead = existsSync18(f) && !readFileSync18(f, "utf8").endsWith(`
11382
+ const f = join19(cwd, ".solignore");
11383
+ const lead = existsSync20(f) && !readFileSync20(f, "utf8").endsWith(`
9970
11384
  `) ? `
9971
11385
  ` : "";
9972
11386
  appendFileSync3(f, lead + pat + `
@@ -9977,7 +11391,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
9977
11391
  case "hide": {
9978
11392
  const { repo } = open();
9979
11393
  const wantJson = args.includes("--json");
9980
- const cfg = resolveRemote(solDir) || die("no remote — `sol push <repo>` (or `sol remote <url> <repo>`) first; the policy lives on the repo");
11394
+ const cfg = resolveRemote2(solDir) || die("no remote — `sol push <repo>` (or `sol remote <url> <repo>`) first; the policy lives on the repo");
9981
11395
  const token = process.env.SOL_TOKEN || authExpired();
9982
11396
  const sub = args[0];
9983
11397
  const flag2 = (name) => {
@@ -10082,12 +11496,12 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
10082
11496
  let removed = 0;
10083
11497
  {
10084
11498
  const res = await scrubHistory2(solDir, ops, targets);
10085
- writeFileSync16(join18(solDir, "ops.jsonl"), res.ops.map((o) => JSON.stringify(o)).join(`
11499
+ writeFileSync17(join19(solDir, "ops.jsonl"), res.ops.map((o) => JSON.stringify(o)).join(`
10086
11500
  `) + (res.ops.length ? `
10087
11501
  ` : ""));
10088
- writeFileSync16(join18(solDir, "HEAD"), JSON.stringify({ head: res.newHead, seq: res.newSeq, logTip: res.newLogTip }));
10089
- if (existsSync18(refsPath())) {
10090
- const refs = JSON.parse(readFileSync18(refsPath(), "utf8"));
11502
+ writeFileSync17(join19(solDir, "HEAD"), JSON.stringify({ head: res.newHead, seq: res.newSeq, logTip: res.newLogTip }));
11503
+ if (existsSync20(refsPath())) {
11504
+ const refs = JSON.parse(readFileSync20(refsPath(), "utf8"));
10091
11505
  for (const b of Object.values(refs.branches)) {
10092
11506
  if (b.head && res.rootMap.has(b.head))
10093
11507
  b.head = res.rootMap.get(b.head);
@@ -10110,7 +11524,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
10110
11524
  break;
10111
11525
  }
10112
11526
  if (reapply) {
10113
- const cfg = resolveRemote(solDir) || die("no remote — the policy lives on the repo; `sol remote <url> <repo>` first");
11527
+ const cfg = resolveRemote2(solDir) || die("no remote — the policy lives on the repo; `sol remote <url> <repo>` first");
10114
11528
  const token = process.env.SOL_TOKEN || authExpired();
10115
11529
  const { SealedClient: SealedClient3 } = await Promise.resolve().then(() => (init_sealed_client(), exports_sealed_client));
10116
11530
  const { UNREADABLE: UNREADABLE2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
@@ -10177,7 +11591,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
10177
11591
  const { SealedClient: SealedClient3 } = await Promise.resolve().then(() => (init_sealed_client(), exports_sealed_client));
10178
11592
  const { parseRecipient: parseRecipient3, resolveRecipient: resolveRecipient3, recordAudience: recordAudience3 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
10179
11593
  const { pubFingerprint: pubFingerprint2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
10180
- const dirUrl2 = (process.env.SOL_REMOTE || DEFAULT_REMOTE_URL).replace(/\/+$/, "");
11594
+ const dirUrl2 = (process.env.SOL_REMOTE || DEFAULT_REMOTE_URL2).replace(/\/+$/, "");
10181
11595
  const explicit = positional.slice(1);
10182
11596
  const recipientPubKeys2 = {};
10183
11597
  const audienceAccounts2 = [];
@@ -10185,7 +11599,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
10185
11599
  const crossAccount2 = [];
10186
11600
  let policyApplied2 = false;
10187
11601
  if (!explicit.length) {
10188
- const cfg = resolveRemote(solDir);
11602
+ const cfg = resolveRemote2(solDir);
10189
11603
  const token = process.env.SOL_TOKEN;
10190
11604
  if (cfg && token) {
10191
11605
  const resolved = await recipientsForPath(cfg, token, dir).catch(() => {
@@ -10245,15 +11659,15 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
10245
11659
  if (content === SEALED && !args.includes("--hide-names"))
10246
11660
  die("already sealed: " + path);
10247
11661
  if (content === undefined) {
10248
- const abs = join18(cwd, path);
10249
- if (!existsSync18(abs))
11662
+ const abs = join19(cwd, path);
11663
+ if (!existsSync20(abs))
10250
11664
  die("no such file: " + path);
10251
- content = readFileSync18(abs, "utf8");
11665
+ content = readFileSync20(abs, "utf8");
10252
11666
  }
10253
11667
  const ring = loadKeyRing();
10254
11668
  const { SealedClient: SealedClient2 } = await Promise.resolve().then(() => (init_sealed_client(), exports_sealed_client));
10255
11669
  const { parseRecipient: parseRecipient2, resolveRecipient: resolveRecipient2, recordAudience: recordAudience2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
10256
- const dirUrl = (process.env.SOL_REMOTE || DEFAULT_REMOTE_URL).replace(/\/+$/, "");
11670
+ const dirUrl = (process.env.SOL_REMOTE || DEFAULT_REMOTE_URL2).replace(/\/+$/, "");
10257
11671
  const recipientPubKeys = {};
10258
11672
  const audienceAccounts = [];
10259
11673
  const localRecipients = new Set([actor]);
@@ -10266,7 +11680,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
10266
11680
  let hideNamesFromPolicy = false;
10267
11681
  let hideExistenceFromPolicy = false;
10268
11682
  if (!rawRecipients.length) {
10269
- const cfg = resolveRemote(solDir);
11683
+ const cfg = resolveRemote2(solDir);
10270
11684
  const token = process.env.SOL_TOKEN;
10271
11685
  if (cfg && token) {
10272
11686
  const { pubFingerprint: pubFingerprint2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
@@ -10295,7 +11709,7 @@ refused for a non-recipient (host/agent-blind). a missing or malformed reference
10295
11709
  }
10296
11710
  }
10297
11711
  if (wantEscrow && !escrowSlots.length) {
10298
- const cfg = resolveRemote(solDir);
11712
+ const cfg = resolveRemote2(solDir);
10299
11713
  const token = process.env.SOL_TOKEN;
10300
11714
  if (cfg && token) {
10301
11715
  const { recipientsForAudience: recipientsForAudience2 } = await Promise.resolve().then(() => (init_remote(), exports_remote));
@@ -10501,7 +11915,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
10501
11915
  const { repo } = open();
10502
11916
  const wantJson = args.includes("--json");
10503
11917
  if (args.includes("--check")) {
10504
- const cfg = resolveRemote(solDir) || die("no remote — drift is checked against the policy on the repo; `sol remote <url> <repo>` first");
11918
+ const cfg = resolveRemote2(solDir) || die("no remote — drift is checked against the policy on the repo; `sol remote <url> <repo>` first");
10505
11919
  const token = process.env.SOL_TOKEN || authExpired();
10506
11920
  const { policyCheck: policyCheck2 } = await Promise.resolve().then(() => (init_remote(), exports_remote));
10507
11921
  const states = [];
@@ -10558,7 +11972,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
10558
11972
  const { openContent: openContent2, UNREADABLE: UNREADABLE2 } = await Promise.resolve().then(() => (init_crypto(), exports_crypto));
10559
11973
  const { parseStruct: parseStruct2 } = await Promise.resolve().then(() => (init_struct(), exports_struct));
10560
11974
  const audiences = loadAudiences2(solDir);
10561
- const ring = existsSync18(solDir) ? loadKeyRing() : new (await Promise.resolve().then(() => (init_crypto(), exports_crypto))).KeyRing;
11975
+ const ring = existsSync20(solDir) ? loadKeyRing() : new (await Promise.resolve().then(() => (init_crypto(), exports_crypto))).KeyRing;
10562
11976
  const self = loadSelfIdentity2();
10563
11977
  const levelOf = (boxStr) => {
10564
11978
  try {
@@ -10753,7 +12167,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
10753
12167
  const result = merge2({ store: store2 }, other.base, ours, other.head);
10754
12168
  if (result.conflicts.length) {
10755
12169
  materializeTree(store2, result.head);
10756
- writeFileSync16(join18(solDir, "MERGE_HEAD"), other.head);
12170
+ writeFileSync17(join19(solDir, "MERGE_HEAD"), other.head);
10757
12171
  saveMergeConflicts(solDir, result.conflicts);
10758
12172
  console.log(`merge ${name} -> ${result.conflicts.length} conflict(s), left in your working tree (uncommitted):`);
10759
12173
  for (const c of result.conflicts)
@@ -10854,8 +12268,19 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
10854
12268
  break;
10855
12269
  }
10856
12270
  case "remote": {
10857
- if (!existsSync18(solDir))
12271
+ if (!existsSync20(solDir))
10858
12272
  die("not a sol repo");
12273
+ if (args[0] === "verify") {
12274
+ const { remoteVerify: remoteVerify2, renderRemoteVerify: renderRemoteVerify2 } = await Promise.resolve().then(() => (init_admin_remote_verify(), exports_admin_remote_verify));
12275
+ const cfg2 = resolveRemote2(solDir) || die("no remote — `sol remote <url> <repo>` (or `sol clone`) first");
12276
+ const token = process.env.SOL_TOKEN || await loadStoredToken() || authExpired();
12277
+ const result = await remoteVerify2((url, init) => fetch(url, init), cfg2.url, cfg2.repo, token);
12278
+ const { lines: lines2, exitCode } = renderRemoteVerify2(result);
12279
+ for (const l of lines2)
12280
+ console.log(l);
12281
+ process.exitCode = exitCode;
12282
+ break;
12283
+ }
10859
12284
  if (args[0]) {
10860
12285
  const repoName = args[1] || die("usage: sol remote <url> <repo>");
10861
12286
  saveRemote(solDir, { url: args[0], repo: repoName });
@@ -10900,15 +12325,15 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
10900
12325
  }
10901
12326
  if (!tokens)
10902
12327
  die("timed out waiting for approval");
10903
- mkdirSync11(dirname4(CRED_PATH), { recursive: true });
10904
- writeFileSync16(CRED_PATH, JSON.stringify({ webUrl, ...tokens }, null, 2), { mode: 384 });
12328
+ mkdirSync12(dirname5(CRED_PATH), { recursive: true });
12329
+ writeFileSync17(CRED_PATH, JSON.stringify({ webUrl, ...tokens }, null, 2), { mode: 384 });
10905
12330
  const c = tokenClaims(tokens.accessToken);
10906
12331
  console.log(`
10907
12332
  Logged in as ${c.handle ? `@${c.handle}` : c.email || "user"}.`);
10908
12333
  if (!c.handle)
10909
12334
  console.log(" (no handle yet — set one in the web app to get your <handle>/<repo> namespace)");
10910
12335
  } else if (sub === "logout") {
10911
- if (existsSync18(CRED_PATH))
12336
+ if (existsSync20(CRED_PATH))
10912
12337
  unlinkSync7(CRED_PATH);
10913
12338
  console.log("logged out");
10914
12339
  } else if (sub === "status" || !sub) {
@@ -10917,11 +12342,11 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
10917
12342
  console.log(`authenticated via SOL_TOKEN (env)${c2.handle ? ` as @${c2.handle}` : ""}`);
10918
12343
  break;
10919
12344
  }
10920
- if (!existsSync18(CRED_PATH)) {
12345
+ if (!existsSync20(CRED_PATH)) {
10921
12346
  console.log("not logged in — run `sol auth login` (or set SOL_TOKEN)");
10922
12347
  break;
10923
12348
  }
10924
- const creds = JSON.parse(readFileSync18(CRED_PATH, "utf8"));
12349
+ const creds = JSON.parse(readFileSync20(CRED_PATH, "utf8"));
10925
12350
  const c = tokenClaims(creds.accessToken || "");
10926
12351
  const stale = typeof c.exp === "number" && c.exp * 1000 < Date.now();
10927
12352
  console.log(`logged in as ${c.handle ? `@${c.handle}` : c.email || "user"} via ${creds.webUrl}${stale ? " (token stale — refreshes on next use)" : ""}`);
@@ -10971,9 +12396,9 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
10971
12396
  console.log("heads-up: changing your handle re-namespaces your repos under the new <handle>/<repo>.");
10972
12397
  console.log(`handle set to @${out.handle}`);
10973
12398
  } else if (sub === "pat") {
10974
- if (!existsSync18(CRED_PATH))
12399
+ if (!existsSync20(CRED_PATH))
10975
12400
  die("run `sol auth login` first");
10976
- const creds = JSON.parse(readFileSync18(CRED_PATH, "utf8"));
12401
+ const creds = JSON.parse(readFileSync20(CRED_PATH, "utf8"));
10977
12402
  const token = await loadStoredToken();
10978
12403
  if (!token || !creds.webUrl)
10979
12404
  die("run `sol auth login` first");
@@ -11031,11 +12456,11 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11031
12456
  const { converge: converge2 } = await Promise.resolve().then(() => (init_converge(), exports_converge));
11032
12457
  const peer = openPeer2(localSrc);
11033
12458
  const peerHead = await peer.log.head();
11034
- const dest = resolve4(procCwd, args[1] || (args[0].replace(/\/+$/, "").split("/").pop() || "clone") + "-clone");
11035
- const ddir = join18(dest, ".sol");
11036
- if (existsSync18(ddir))
12459
+ const dest = resolve5(procCwd, args[1] || (args[0].replace(/\/+$/, "").split("/").pop() || "clone") + "-clone");
12460
+ const ddir = join19(dest, ".sol");
12461
+ if (existsSync20(ddir))
11037
12462
  die("already a sol repo: " + dest);
11038
- mkdirSync11(ddir, { recursive: true });
12463
+ mkdirSync12(ddir, { recursive: true });
11039
12464
  const peerOps = await peer.log.history();
11040
12465
  const res = await converge2({ store: new FileStore2(ddir), log: new FileOpLog2(ddir) }, { nodes: await peerNodes2(peer, peerHead, peerOps), ops: peerOps, incomingHead: peerHead, actor });
11041
12466
  const dstore = new Store;
@@ -11045,7 +12470,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11045
12470
  const files = (res.head ? listAll2(dstore, res.head) : []).filter((f) => !f.split("/").some(isReservedKey2));
11046
12471
  for (const f of files)
11047
12472
  materializeInto(dstore, res.head, dest, f);
11048
- writeFileSync16(join18(ddir, "refs.json"), JSON.stringify({ current: "main", branches: { main: { head: res.head, base: res.head, remote: res.head } }, tags: {} }, null, 2));
12473
+ writeFileSync17(join19(ddir, "refs.json"), JSON.stringify({ current: "main", branches: { main: { head: res.head, base: res.head, remote: res.head } }, tags: {} }, null, 2));
11049
12474
  writeWorkingIndexAt(ddir, dest, files);
11050
12475
  console.log(`cloned local peer ${args[0]} -> ${dest} (${(await peer.log.history()).length} ops, ${files.length} files)`);
11051
12476
  break;
@@ -11053,13 +12478,13 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11053
12478
  const [url, rest] = remoteUrlArg(args);
11054
12479
  const repoName = rest[0] || die("usage: sol clone [<url>] <owner>/<repo> [dir]");
11055
12480
  const token = process.env.SOL_TOKEN || die("set SOL_TOKEN to the backend bearer token");
11056
- const target = resolve4(cwd, rest[1] || repoName.split("/").pop() || repoName);
11057
- const fdir = join18(target, ".sol");
11058
- if (existsSync18(fdir))
12481
+ const target = resolve5(cwd, rest[1] || repoName.split("/").pop() || repoName);
12482
+ const fdir = join19(target, ".sol");
12483
+ if (existsSync20(fdir))
11059
12484
  die("already a sol repo: " + target);
11060
12485
  const cfg = { url, repo: repoName };
11061
12486
  const bundle = await remoteExport(cfg, token);
11062
- mkdirSync11(fdir, { recursive: true });
12487
+ mkdirSync12(fdir, { recursive: true });
11063
12488
  await writeBundle(fdir, bundle, 0);
11064
12489
  saveRemote(fdir, cfg);
11065
12490
  await pullEnvState(fdir, cfg, token);
@@ -11072,8 +12497,8 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11072
12497
  cloneBranches[name] = { head: h, base: h, remote: h };
11073
12498
  if (!cloneBranches[onBranch])
11074
12499
  cloneBranches[onBranch] = { head: checkoutHead, base: checkoutHead, remote: checkoutHead };
11075
- writeFileSync16(join18(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
11076
- writeFileSync16(join18(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: bundle.seq, logTip: bundle.tip }));
12500
+ writeFileSync17(join19(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
12501
+ writeFileSync17(join19(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: bundle.seq, logTip: bundle.tip }));
11077
12502
  const store2 = new Store;
11078
12503
  for (const node of bundle.nodes)
11079
12504
  store2.put(node);
@@ -11094,11 +12519,11 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11094
12519
  if (!loadRemote(solDir)) {
11095
12520
  const repoName = args.find((a) => !a.startsWith("-"));
11096
12521
  if (repoName) {
11097
- saveRemote(solDir, { url: DEFAULT_REMOTE_URL, repo: repoName });
11098
- console.log(`remote set: ${DEFAULT_REMOTE_URL} (repo ${repoName})`);
12522
+ saveRemote(solDir, { url: DEFAULT_REMOTE_URL2, repo: repoName });
12523
+ console.log(`remote set: ${DEFAULT_REMOTE_URL2} (repo ${repoName})`);
11099
12524
  }
11100
12525
  }
11101
- const cfg = resolveRemote(solDir) || die("no remote — set one with `sol remote <url> <repo>`, or `sol push <repo>` to use the hosted Sol");
12526
+ const cfg = resolveRemote2(solDir) || die("no remote — set one with `sol remote <url> <repo>`, or `sol push <repo>` to use the hosted Sol");
11102
12527
  {
11103
12528
  const { pendingScrubPaths: pendingScrubPaths2, historyHasCleartext: historyHasCleartext2 } = await Promise.resolve().then(() => (init_secret_scrub(), exports_secret_scrub));
11104
12529
  const ops0 = await log.history();
@@ -11112,7 +12537,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11112
12537
  const ops = await log.history();
11113
12538
  const localSeq = ops.length ? ops[ops.length - 1].seq : 0;
11114
12539
  const localTip = await log.logTip();
11115
- const localRefs = existsSync18(refsPath()) ? JSON.parse(readFileSync18(refsPath(), "utf8")) : undefined;
12540
+ const localRefs = existsSync20(refsPath()) ? JSON.parse(readFileSync20(refsPath(), "utf8")) : undefined;
11116
12541
  const branch = localRefs?.current ?? "main";
11117
12542
  const branchHead = await log.head() ?? "";
11118
12543
  const remoteWasEmpty = rh.seq === 0 && !rh.tip;
@@ -11124,7 +12549,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11124
12549
  const forkBase = localRefs?.branches[branch]?.remote;
11125
12550
  const baseOp = forkBase ? ops.find((o) => o.rootAfter === forkBase) : undefined;
11126
12551
  const fromSeq = baseOp ? baseOp.seq : rh.seq;
11127
- const res = await remotePush(cfg, token, {
12552
+ const res = await remotePushChunked(cfg, token, {
11128
12553
  nodes: allLocalNodes(),
11129
12554
  ops: ops.filter((o) => o.seq > fromSeq),
11130
12555
  branch,
@@ -11135,8 +12560,8 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11135
12560
  const prevHead = await log.head() ?? "";
11136
12561
  await writeBundle(solDir, canon, 0);
11137
12562
  const convergedHead = res.head ?? canon.refs?.branches?.[branch] ?? branchHead;
11138
- if (existsSync18(refsPath())) {
11139
- const refs = JSON.parse(readFileSync18(refsPath(), "utf8"));
12563
+ if (existsSync20(refsPath())) {
12564
+ const refs = JSON.parse(readFileSync20(refsPath(), "utf8"));
11140
12565
  if (canon.refs) {
11141
12566
  for (const [name, h] of Object.entries(canon.refs.branches)) {
11142
12567
  refs.branches[name] = { head: refs.branches[name]?.head ?? h, base: refs.branches[name]?.base ?? h, remote: h };
@@ -11160,6 +12585,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11160
12585
  console.log(`pushed ${res.applied} op(s) -> remote (branch ${branch} @ ${(convergedHead || "").slice(0, 12)})${mergedNote}${conflictNote}${prod ? `; production=${prod}` : ""}`);
11161
12586
  }
11162
12587
  await pushEnvState(solDir, cfg, token);
12588
+ await surfaceOwner(cfg, token);
11163
12589
  if (wantPublic) {
11164
12590
  const a = await accessSet(cfg, token, { visibility: "public" });
11165
12591
  console.log(`${cfg.repo} is now ${a.visibility}`);
@@ -11200,8 +12626,8 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11200
12626
  const ownMeta = readViewMeta(solDir);
11201
12627
  const base2 = peerMeta?.startHead ?? ownMeta?.startHead;
11202
12628
  const res = await converge2({ store: new FileStore2(solDir, objectsDir()), log }, { nodes: await peerNodes2(peer, peerHead, peerOps), ops: peerOps, incomingHead: peerHead, actor, ...base2 ? { base: base2 } : {} });
11203
- if (existsSync18(refsPath())) {
11204
- const refs = JSON.parse(readFileSync18(refsPath(), "utf8"));
12629
+ if (existsSync20(refsPath())) {
12630
+ const refs = JSON.parse(readFileSync20(refsPath(), "utf8"));
11205
12631
  if (refs.branches[refs.current])
11206
12632
  refs.branches[refs.current].head = res.head;
11207
12633
  saveRefs(refs);
@@ -11224,7 +12650,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11224
12650
  }
11225
12651
  break;
11226
12652
  }
11227
- const cfg = resolveRemote(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
12653
+ const cfg = resolveRemote2(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
11228
12654
  const token = process.env.SOL_TOKEN || authExpired();
11229
12655
  await surfaceEnvDivergence(solDir, cfg, token);
11230
12656
  await pullEnvState(solDir, cfg, token);
@@ -11232,11 +12658,11 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11232
12658
  const ops = await log.history();
11233
12659
  const localSeq = ops.length ? ops[ops.length - 1].seq : 0;
11234
12660
  const localTip = await log.logTip();
11235
- const curBranch = existsSync18(refsPath()) ? JSON.parse(readFileSync18(refsPath(), "utf8")).current : bundle.refs?.production || "main";
12661
+ const curBranch = existsSync20(refsPath()) ? JSON.parse(readFileSync20(refsPath(), "utf8")).current : bundle.refs?.production || "main";
11236
12662
  const remoteCurHead = bundle.refs?.branches?.[curBranch] ?? bundle.head ?? "";
11237
12663
  if (bundle.seq === localSeq && bundle.tip === localTip) {
11238
- if (bundle.refs && existsSync18(refsPath())) {
11239
- const lr = JSON.parse(readFileSync18(refsPath(), "utf8"));
12664
+ if (bundle.refs && existsSync20(refsPath())) {
12665
+ const lr = JSON.parse(readFileSync20(refsPath(), "utf8"));
11240
12666
  const before = lr.branches[lr.current]?.head;
11241
12667
  for (const [name, h] of Object.entries(bundle.refs.branches))
11242
12668
  lr.branches[name] = { head: h, base: lr.branches[name]?.base ?? h, remote: h };
@@ -11260,9 +12686,9 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11260
12686
  break;
11261
12687
  }
11262
12688
  const syncRefHead = (h) => {
11263
- if (!existsSync18(refsPath()))
12689
+ if (!existsSync20(refsPath()))
11264
12690
  return;
11265
- const refs = JSON.parse(readFileSync18(refsPath(), "utf8"));
12691
+ const refs = JSON.parse(readFileSync20(refsPath(), "utf8"));
11266
12692
  if (refs.branches[refs.current])
11267
12693
  refs.branches[refs.current].head = h;
11268
12694
  saveRefs(refs);
@@ -11280,8 +12706,8 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11280
12706
  syncRefHead(remoteCurHead);
11281
12707
  setOpLogHead(remoteCurHead);
11282
12708
  materializeTree(loadStore(), remoteCurHead);
11283
- if (bundle.refs && existsSync18(refsPath())) {
11284
- const lr = JSON.parse(readFileSync18(refsPath(), "utf8"));
12709
+ if (bundle.refs && existsSync20(refsPath())) {
12710
+ const lr = JSON.parse(readFileSync20(refsPath(), "utf8"));
11285
12711
  for (const [name, h] of Object.entries(bundle.refs.branches))
11286
12712
  lr.branches[name] = { head: h, base: lr.branches[name]?.base ?? h, remote: h };
11287
12713
  saveRefs(lr);
@@ -11329,9 +12755,9 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11329
12755
  for (const c of result.conflicts) {
11330
12756
  const blob = fileAt2(store2, result.head, c.path);
11331
12757
  if (blob)
11332
- writeFileSync16(join18(cwd, c.path), blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
12758
+ writeFileSync17(join19(cwd, c.path), blob.encoding === "base64" ? Buffer.from(blob.content, "base64") : blob.content);
11333
12759
  }
11334
- writeFileSync16(join18(solDir, "MERGE_HEAD"), remoteHead2);
12760
+ writeFileSync17(join19(solDir, "MERGE_HEAD"), remoteHead2);
11335
12761
  saveMergeConflicts(solDir, result.conflicts);
11336
12762
  console.log(`pulled + merged WITH ${result.conflicts.length} conflict(s), left uncommitted in your working tree:`);
11337
12763
  for (const c of result.conflicts)
@@ -11345,12 +12771,36 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11345
12771
  }
11346
12772
  break;
11347
12773
  }
12774
+ case "sync": {
12775
+ if (!existsSync20(solDir))
12776
+ die("not a sol repo — run `sol init` first");
12777
+ if (!args.includes("--watch"))
12778
+ die("usage: sol sync --watch [--poll-interval <ms>] [--no-push-immediately] [--dry-run]");
12779
+ resolveRemote2(solDir) || die("no remote — set one with `sol remote <url> <repo>`, or `sol push <repo>` first");
12780
+ const token = process.env.SOL_TOKEN || await loadStoredToken() || authExpired();
12781
+ const intFlag = (name, def) => {
12782
+ const i = args.indexOf(name);
12783
+ if (i < 0 || i + 1 >= args.length)
12784
+ return def;
12785
+ const n = Number(args[i + 1]);
12786
+ return Number.isFinite(n) && n > 0 ? n : def;
12787
+ };
12788
+ const { runSyncWatch: runSyncWatch2 } = await Promise.resolve().then(() => (init_sync_watch(), exports_sync_watch));
12789
+ await runSyncWatch2({
12790
+ repoDir: cwd,
12791
+ token,
12792
+ pollInterval: intFlag("--poll-interval", 5000),
12793
+ pushImmediate: !args.includes("--no-push-immediately"),
12794
+ dryRun: args.includes("--dry-run")
12795
+ });
12796
+ break;
12797
+ }
11348
12798
  case "promote": {
11349
- if (!existsSync18(solDir))
12799
+ if (!existsSync20(solDir))
11350
12800
  die("not a sol repo");
11351
- const cfg = resolveRemote(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
12801
+ const cfg = resolveRemote2(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
11352
12802
  const token = process.env.SOL_TOKEN || authExpired();
11353
- const cur = existsSync18(refsPath()) ? JSON.parse(readFileSync18(refsPath(), "utf8")).current : "main";
12803
+ const cur = existsSync20(refsPath()) ? JSON.parse(readFileSync20(refsPath(), "utf8")).current : "main";
11354
12804
  const branch = args[0] || cur;
11355
12805
  const refs = await remotePromote(cfg, token, branch);
11356
12806
  console.log(`promoted '${branch}' -> production '${refs.production}' now at ${(refs.branches[refs.production] ?? "").slice(0, 12)}`);
@@ -11361,9 +12811,9 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11361
12811
  const parent = frest[0] || die("usage: sol fork [<url>] <parent-repo> <new-repo> [dir]");
11362
12812
  const newRepo = frest[1] || die("usage: sol fork [<url>] <parent-repo> <new-repo> [dir]");
11363
12813
  const token = process.env.SOL_TOKEN || authExpired();
11364
- const target = resolve4(cwd, frest[2] || newRepo);
11365
- const fdir = join18(target, ".sol");
11366
- if (existsSync18(fdir))
12814
+ const target = resolve5(cwd, frest[2] || newRepo);
12815
+ const fdir = join19(target, ".sol");
12816
+ if (existsSync20(fdir))
11367
12817
  die("already a sol repo: " + target);
11368
12818
  const parentCfg = { url, repo: parent };
11369
12819
  const newCfg = { url, repo: newRepo, forkParent: parent };
@@ -11379,7 +12829,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11379
12829
  }
11380
12830
  await forkMeta(newCfg, token, parent);
11381
12831
  const canon = await remoteExport(newCfg, token);
11382
- mkdirSync11(fdir, { recursive: true });
12832
+ mkdirSync12(fdir, { recursive: true });
11383
12833
  await writeBundle(fdir, canon, 0);
11384
12834
  const srvRefs = canon.refs ?? { branches: { main: canon.head ?? "" }, production: "main" };
11385
12835
  const checkout = canon.checkout ?? { branch: srvRefs.production || "main", head: srvRefs.branches[srvRefs.production] ?? canon.head };
@@ -11388,8 +12838,8 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11388
12838
  const cloneBranches = {};
11389
12839
  for (const [name, h] of Object.entries(srvRefs.branches))
11390
12840
  cloneBranches[name] = { head: h, base: h, remote: h };
11391
- writeFileSync16(join18(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
11392
- writeFileSync16(join18(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: canon.seq, logTip: canon.tip }));
12841
+ writeFileSync17(join19(fdir, "refs.json"), JSON.stringify({ current: onBranch, branches: cloneBranches, tags: {} }, null, 2));
12842
+ writeFileSync17(join19(fdir, "HEAD"), JSON.stringify({ head: checkoutHead, seq: canon.seq, logTip: canon.tip }));
11393
12843
  saveRemote(fdir, newCfg);
11394
12844
  const store2 = new Store;
11395
12845
  for (const node of canon.nodes)
@@ -11407,7 +12857,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11407
12857
  }
11408
12858
  case "access": {
11409
12859
  const token = process.env.SOL_TOKEN || authExpired();
11410
- const cfg = resolveRemote(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
12860
+ const cfg = resolveRemote2(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
11411
12861
  const sub = args[0];
11412
12862
  if (!sub || sub === "show") {
11413
12863
  const a = await accessGet(cfg, token);
@@ -11459,7 +12909,7 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11459
12909
  break;
11460
12910
  }
11461
12911
  case "forks": {
11462
- const cfg = resolveRemote(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
12912
+ const cfg = resolveRemote2(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
11463
12913
  const token = process.env.SOL_TOKEN || authExpired();
11464
12914
  const { forks } = await forksList(cfg, token);
11465
12915
  if (!forks.length)
@@ -11472,11 +12922,11 @@ WARNING: "${path}" was committed as PLAINTEXT before this seal — the pre-seal
11472
12922
  break;
11473
12923
  }
11474
12924
  case "mr": {
11475
- const cfg = resolveRemote(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
12925
+ const cfg = resolveRemote2(solDir) || die("no remote — set one with `sol remote <url> <repo>`");
11476
12926
  const token = process.env.SOL_TOKEN || authExpired();
11477
12927
  const { mrSummary: mrSummary2 } = await Promise.resolve().then(() => exports_mr);
11478
12928
  const sub = args[0];
11479
- const localRefs = () => existsSync18(refsPath()) ? JSON.parse(readFileSync18(refsPath(), "utf8")) : { current: "main", branches: {}, tags: {} };
12929
+ const localRefs = () => existsSync20(refsPath()) ? JSON.parse(readFileSync20(refsPath(), "utf8")) : { current: "main", branches: {}, tags: {} };
11480
12930
  const flag2 = (name) => {
11481
12931
  const i = args.indexOf(name);
11482
12932
  return i >= 0 ? args[i + 1] : undefined;
@@ -11654,7 +13104,7 @@ ${mrSummary2(pr)}`);
11654
13104
  die("usage: sol run [--keep <path>] [--isolate] <command...>");
11655
13105
  const { capture: capture3, hydrate: hydrate3, isolateCommand: isolateCommand2 } = await Promise.resolve().then(() => (init_runtime(), exports_runtime));
11656
13106
  const head = await repo.head();
11657
- const dir = mkdtempSync(join18(tmpdir(), "sol-run-"));
13107
+ const dir = mkdtempSync(join19(tmpdir(), "sol-run-"));
11658
13108
  try {
11659
13109
  const hn = hydrate3(loadStore(), head, dir);
11660
13110
  console.log(`hydrated ${hn} file(s) -> sandbox${isolate ? " (isolated: no network, writes confined to the sandbox)" : ""}`);
@@ -11674,8 +13124,8 @@ ${mrSummary2(pr)}`);
11674
13124
  const { written, deleted } = await capture3(repo, dir, keep);
11675
13125
  if (written.length || deleted.length) {
11676
13126
  await appendCommit(log, await repo.head(), `run: ${command.join(" ")}`, head);
11677
- if (existsSync18(refsPath())) {
11678
- const refs = JSON.parse(readFileSync18(refsPath(), "utf8"));
13127
+ if (existsSync20(refsPath())) {
13128
+ const refs = JSON.parse(readFileSync20(refsPath(), "utf8"));
11679
13129
  if (refs.branches[refs.current]) {
11680
13130
  refs.branches[refs.current].head = await repo.head();
11681
13131
  saveRefs(refs);
@@ -11687,7 +13137,7 @@ ${mrSummary2(pr)}`);
11687
13137
  materialize(synced, nh, f);
11688
13138
  for (const f of deleted) {
11689
13139
  try {
11690
- unlinkSync7(join18(cwd, f));
13140
+ unlinkSync7(join19(cwd, f));
11691
13141
  } catch {}
11692
13142
  }
11693
13143
  console.log(`captured ${written.length} written, ${deleted.length} deleted file(s):`);
@@ -11699,7 +13149,7 @@ ${mrSummary2(pr)}`);
11699
13149
  console.log("no files captured (the run produced no tracked changes)");
11700
13150
  }
11701
13151
  } finally {
11702
- rmSync2(dir, { recursive: true, force: true });
13152
+ rmSync3(dir, { recursive: true, force: true });
11703
13153
  }
11704
13154
  break;
11705
13155
  }
@@ -11707,12 +13157,12 @@ ${mrSummary2(pr)}`);
11707
13157
  const [{ exportHistoryToGit: exportHistoryToGit2, importGitRepo: importGitRepo2 }, { hydrate: hydrate3 }] = await Promise.all([Promise.resolve().then(() => (init_git_adapter(), exports_git_adapter)), Promise.resolve().then(() => (init_runtime(), exports_runtime))]);
11708
13158
  const sub = args[0];
11709
13159
  if (sub === "import") {
11710
- const gitPath = resolve4(cwd, args[1] || die("usage: sol git import <git-repo> [dir]"));
11711
- const target = resolve4(cwd, args[2] || basename(gitPath));
11712
- const fdir = join18(target, ".sol");
11713
- if (existsSync18(fdir))
13160
+ const gitPath = resolve5(cwd, args[1] || die("usage: sol git import <git-repo> [dir]"));
13161
+ const target = resolve5(cwd, args[2] || basename2(gitPath));
13162
+ const fdir = join19(target, ".sol");
13163
+ if (existsSync20(fdir))
11714
13164
  die("already a sol repo: " + target);
11715
- mkdirSync11(fdir, { recursive: true });
13165
+ mkdirSync12(fdir, { recursive: true });
11716
13166
  const { commits, branches, head, current } = await importGitRepo2(gitPath, fdir);
11717
13167
  const refsBranches = {};
11718
13168
  for (const b of branches)
@@ -11720,20 +13170,20 @@ ${mrSummary2(pr)}`);
11720
13170
  if (!refsBranches[current])
11721
13171
  refsBranches[current] = { head, base: head };
11722
13172
  refsBranches[current].head = head;
11723
- writeFileSync16(join18(fdir, "refs.json"), JSON.stringify({ current, branches: refsBranches, tags: {} }, null, 2));
13173
+ writeFileSync17(join19(fdir, "refs.json"), JSON.stringify({ current, branches: refsBranches, tags: {} }, null, 2));
11724
13174
  const store2 = new Store;
11725
- for (const name of readdirSync9(join18(fdir, "objects"))) {
13175
+ for (const name of readdirSync9(join19(fdir, "objects"))) {
11726
13176
  if (name.endsWith(".tmp"))
11727
13177
  continue;
11728
13178
  try {
11729
- store2.put(decodeObject2(readFileSync18(join18(fdir, "objects", name))));
13179
+ store2.put(decodeObject2(readFileSync20(join19(fdir, "objects", name))));
11730
13180
  } catch {}
11731
13181
  }
11732
13182
  const onDisk = hydrate3(store2, head, target);
11733
- console.log(`imported ${commits} commit(s), ${branches.length} branch(es) from git -> ${args[2] || basename(gitPath)} (${onDisk} files; on branch ${current})`);
13183
+ console.log(`imported ${commits} commit(s), ${branches.length} branch(es) from git -> ${args[2] || basename2(gitPath)} (${onDisk} files; on branch ${current})`);
11734
13184
  } else if (sub === "export") {
11735
13185
  const { log } = open();
11736
- const gitPath = resolve4(cwd, args[1] || die("usage: sol git export <git-repo> [-b branch]"));
13186
+ const gitPath = resolve5(cwd, args[1] || die("usage: sol git export <git-repo> [-b branch]"));
11737
13187
  const store2 = loadStore();
11738
13188
  const head = await log.head() ?? "";
11739
13189
  if (!head || !listAll2(store2, head).length) {
@@ -11752,15 +13202,111 @@ ${mrSummary2(pr)}`);
11752
13202
  }
11753
13203
  break;
11754
13204
  }
13205
+ case "agent": {
13206
+ if (!existsSync20(solDir))
13207
+ die("not a sol repo — run `sol init` first");
13208
+ const parentSol = parentSolDir(solDir) ?? solDir;
13209
+ const { createSession: createSession2, stopSession: stopSession2, sessions: sessions2, pruneSession: pruneSession2 } = await Promise.resolve().then(() => (init_agent_sessions(), exports_agent_sessions));
13210
+ const sub = args[0];
13211
+ const json = args.includes("--json");
13212
+ const flagVal = (name) => {
13213
+ const i = args.indexOf(name);
13214
+ return i >= 0 ? args[i + 1] : undefined;
13215
+ };
13216
+ switch (sub) {
13217
+ case "start": {
13218
+ const name = args.slice(1).find((a) => !a.startsWith("-")) || die('usage: sol agent start <name> [dir] [--actor <name>] [--check "<cmd>"]');
13219
+ const positionals2 = args.slice(1).filter((a) => !a.startsWith("-"));
13220
+ const sessionActor = flagVal("--actor") || actor;
13221
+ const metaRaw = flagVal("--meta");
13222
+ let metadata;
13223
+ if (metaRaw) {
13224
+ try {
13225
+ metadata = JSON.parse(metaRaw);
13226
+ } catch {
13227
+ die("--meta must be valid JSON");
13228
+ }
13229
+ }
13230
+ const rec = createSession2({
13231
+ parentSol,
13232
+ name,
13233
+ actor: sessionActor,
13234
+ dir: positionals2[1] ? resolve5(procCwd, positionals2[1]) : undefined,
13235
+ check: flagVal("--check"),
13236
+ metadata
13237
+ });
13238
+ if (json) {
13239
+ console.log(JSON.stringify(rec));
13240
+ break;
13241
+ }
13242
+ console.log(`started agent session '${rec.name}' (id ${rec.sessionId})`);
13243
+ console.log(` view ${rec.viewDir} (branch ${rec.branch} @ ${(rec.startHead || "empty").slice(0, 12)})`);
13244
+ console.log(` actor ${rec.actor}`);
13245
+ console.log(` identity ${rec.fingerprint}`);
13246
+ console.log(` -> wire the agent: cd ${rec.viewDir} && export SOL_ACTOR=${rec.actor} SOL_SIGNING_KEY=$(cat ${rec.signingKeyPath})`);
13247
+ console.log(` then edit + \`sol commit -m "..." <files>\`; converge with \`sol pull ${rec.viewDir}\` from the parent.`);
13248
+ console.log(` stop/clean: \`sol agent stop ${rec.sessionId}\` | \`sol agent cleanup ${rec.sessionId} --delete\``);
13249
+ break;
13250
+ }
13251
+ case "stop": {
13252
+ const id = args.slice(1).find((a) => !a.startsWith("-")) || die("usage: sol agent stop <sessionId>");
13253
+ const ok = stopSession2(parentSol, id);
13254
+ if (json)
13255
+ console.log(JSON.stringify({ stopped: ok, sessionId: id }));
13256
+ else
13257
+ console.log(ok ? `stopped session ${id} (the view + key remain — \`sol agent cleanup\` to remove)` : `no such session: ${id}`);
13258
+ if (!ok)
13259
+ process.exitCode = 1;
13260
+ break;
13261
+ }
13262
+ case "status":
13263
+ case undefined: {
13264
+ const id = args.slice(1).find((a) => !a.startsWith("-"));
13265
+ const all = await sessions2(parentSol);
13266
+ const list = id ? all.filter((s) => s.sessionId === id || s.name === id) : all;
13267
+ if (json) {
13268
+ console.log(JSON.stringify({ repo: parentSol, sessions: list }));
13269
+ break;
13270
+ }
13271
+ if (!list.length) {
13272
+ console.log(id ? `no such session: ${id}` : "no agent sessions — start one with `sol agent start <name>`");
13273
+ break;
13274
+ }
13275
+ console.log(`agent sessions of ${parentSol}:`);
13276
+ for (const s of list) {
13277
+ console.log(` ${s.name.padEnd(16)} ${s.sessionId.padEnd(28)} ${(s.head || s.startHead || "empty").slice(0, 12)} ${s.actor.padEnd(12)} ${s.fingerprint} [${s.state}]`);
13278
+ console.log(` ${s.viewDir}`);
13279
+ }
13280
+ console.log(" stop: `sol agent stop <id>` | cleanup: `sol agent cleanup <id> [--delete]` (or bare `cleanup` to prune stale)");
13281
+ break;
13282
+ }
13283
+ case "cleanup": {
13284
+ const id = args.slice(1).find((a) => !a.startsWith("-"));
13285
+ const removed = pruneSession2(parentSol, id, args.includes("--delete"));
13286
+ if (json)
13287
+ console.log(JSON.stringify({ removed }));
13288
+ else
13289
+ console.log(removed.length ? `cleaned ${removed.length} session(s): ${removed.join(", ")}` : id ? `no such session: ${id}` : "no stale sessions to clean");
13290
+ break;
13291
+ }
13292
+ default:
13293
+ die(`unknown: sol agent ${sub}
13294
+ sol agent start <name> [dir] [--actor N] [--check "<cmd>"]
13295
+ sol agent status [<id>]
13296
+ sol agent stop <id>
13297
+ sol agent cleanup [<id>] [--delete]`);
13298
+ }
13299
+ break;
13300
+ }
11755
13301
  case "view": {
11756
13302
  const { log } = open();
11757
13303
  if (readViewMeta(solDir))
11758
13304
  die("already inside a view — create views from the parent repo (its `.sol` owns the shared store + op-log).");
11759
13305
  const name = args.find((a) => !a.startsWith("-")) || die("usage: sol view <name> [dir]");
11760
13306
  const rest = args.filter((a) => !a.startsWith("-"));
11761
- const defaultDir = join18(dirname4(cwd), `${basename(cwd)}-${name}`);
11762
- const viewDir = rest[1] ? resolve4(procCwd, rest[1]) : defaultDir;
11763
- if (existsSync18(join18(viewDir, ".sol")))
13307
+ const defaultDir = join19(dirname5(cwd), `${basename2(cwd)}-${name}`);
13308
+ const viewDir = rest[1] ? resolve5(procCwd, rest[1]) : defaultDir;
13309
+ if (existsSync20(join19(viewDir, ".sol")))
11764
13310
  die("already a sol repo/view: " + viewDir);
11765
13311
  const branch = `view/${name}`;
11766
13312
  const startHead = await log.head() ?? emptyRoot2(loadStore());
@@ -11774,7 +13320,7 @@ ${mrSummary2(pr)}`);
11774
13320
  break;
11775
13321
  }
11776
13322
  case "views": {
11777
- if (!existsSync18(solDir))
13323
+ if (!existsSync20(solDir))
11778
13324
  die("not a sol repo");
11779
13325
  const parentSol = parentSolDir(solDir) ?? solDir;
11780
13326
  const { pruneViews: pruneViews2, viewStatuses: viewStatuses2, sharedObjectCount: sharedObjectCount2 } = await Promise.resolve().then(() => (init_views(), exports_views));
@@ -11870,12 +13416,12 @@ ${mrSummary2(pr)}`);
11870
13416
  await startSecretMcp2({ solDir, http });
11871
13417
  break;
11872
13418
  }
11873
- if (!existsSync18(solDir))
13419
+ if (!existsSync20(solDir))
11874
13420
  die("not a sol repo — run `sol init` first");
11875
13421
  const { runEnv: runEnv2, runSecret: runSecret2, resolveReference: resolveReference2 } = await Promise.resolve().then(() => (init_secret2(), exports_secret2));
11876
13422
  const { loadSelfIdentity: loadSelfIdentity2, loadManageIdentity: loadManageIdentity2, fetchKey: fetchKey2 } = await Promise.resolve().then(() => (init_seal_audience(), exports_seal_audience));
11877
13423
  const { loadIdentity: loadIdentity2 } = await Promise.resolve().then(() => (init_identity_store(), exports_identity_store));
11878
- const dirUrl = (process.env.SOL_REMOTE || DEFAULT_REMOTE_URL).replace(/\/+$/, "");
13424
+ const dirUrl = (process.env.SOL_REMOTE || DEFAULT_REMOTE_URL2).replace(/\/+$/, "");
11879
13425
  const selfPub = (account) => {
11880
13426
  const id = loadIdentity2();
11881
13427
  return id && id.accountId === account ? id.x25519Pub : undefined;
@@ -11887,7 +13433,7 @@ ${mrSummary2(pr)}`);
11887
13433
  };
11888
13434
  const dirEdPub = async (account) => (await fetchKey2(dirUrl, account))?.edPub;
11889
13435
  const remoteAnchorVerify = async () => {
11890
- const rcfg = resolveRemote(solDir);
13436
+ const rcfg = resolveRemote2(solDir);
11891
13437
  const token = process.env.SOL_TOKEN || await loadStoredToken();
11892
13438
  if (!rcfg || !token)
11893
13439
  return;
@@ -11937,6 +13483,11 @@ everyday (examples are copy-safe — use real filenames):
11937
13483
  sol rm old.txt delete a file (from the repo and disk)
11938
13484
  sol ignore "*.tmp" add an ignore pattern (no arg lists the active patterns)
11939
13485
  sol fsck / sol gc verify integrity / drop unreachable objects
13486
+ sol doctor full health check (op-log + objects + auth + files + sealed keyring)
13487
+ sol fsck --repair recover a corrupt repo (re-point a lost head / truncate to the last verified link)
13488
+ sol recover guide through recovery (rehydrate from cloud, list roots, restore a file from history)
13489
+ sol reflog the op-log as a ref-move history (head moves, by actor/branch/date)
13490
+ sol remote verify REMOTE-side health check (the backend's /fsck) with auth/network differentiation
11940
13491
  sol check --set "bun test" gate convergence on a test command: a merge that line-converges but FAILS
11941
13492
  the check is flagged as a SEMANTIC conflict (status state:"SEMANTIC"), not
11942
13493
  silently accepted. \`sol check\` runs it on demand. (sol check --help)
@@ -11973,6 +13524,16 @@ clone-free agent views (the worktree killer — N agents, one shared store on di
11973
13524
  \`sol pull <view>\` converges losslessly. (sol view --help)
11974
13525
  sol views list every view (name, dir, branch, head, author, active/stale) + --prune
11975
13526
 
13527
+ agent sessions (one atomic command provisions a working tree, a signing identity, an actor, and the test-gate):
13528
+ sol agent start <name> create a view + MINT a per-session Ed25519 signing key + wire the actor +
13529
+ inherit/set the test-gate, all at once. prints SOL_SIGNING_KEY / SOL_ACTOR to
13530
+ export so every op the agent authors is cryptographically attributable.
13531
+ (--actor <name>, --check "<cmd>", [dir], --json)
13532
+ sol agent status [<id>] list sessions (name, id, head, actor, fingerprint, running/stopped/stale)
13533
+ sol agent stop <id> mark a session stopped (presence signal; view + key remain)
13534
+ sol agent cleanup [<id>] remove the session record + its key (--delete also removes the view dir;
13535
+ bare \`cleanup\` prunes every stale session)
13536
+
11976
13537
  branches & tags:
11977
13538
  sol branch list branches (sol branch feature creates one at HEAD)
11978
13539
  sol switch feature switch to a branch (captures current work first, never loses it)
@@ -11989,6 +13550,7 @@ auth (sign in once; remote commands then use the cached token, no SOL_TOKEN need
11989
13550
  remotes (self-hostable backend; token in SOL_TOKEN or via sol auth login):
11990
13551
  sol clone [<url>] <owner>/<repo> [dir] clone a remote repo (checks out PRODUCTION); default dir = <repo>
11991
13552
  sol push / sol pull sync your commits with the remote (push registers your branch's head)
13553
+ sol sync --watch hands-off bidirectional sync daemon: auto-capture + push edits, converge safe pulls in
11992
13554
  sol push <repo> one-step share: no remote set? use the hosted Sol + <repo>, then push
11993
13555
  sol push --public <repo> create + push + make public in one step (new repos are private by default)
11994
13556
  sol promote [branch] point the remote's production branch at <branch> (default: current)
@@ -12028,10 +13590,15 @@ async function runCli(argv) {
12028
13590
  const msg = e?.message || String(e);
12029
13591
  if (/ -> 401\b/.test(msg) || /\b401 unauthorized\b/i.test(msg))
12030
13592
  authExpired();
13593
+ const { CorruptObjectError: CorruptObjectError2, CorruptRepoError: CorruptRepoError2 } = await Promise.resolve().then(() => (init_errors(), exports_errors));
13594
+ if (e instanceof CorruptObjectError2 || e instanceof CorruptRepoError2) {
13595
+ const { friendlyError: friendlyError2 } = await Promise.resolve().then(() => (init_errors_friendly(), exports_errors_friendly));
13596
+ die(friendlyError2(e));
13597
+ }
12031
13598
  die(msg);
12032
13599
  }
12033
13600
  }
12034
- var CRED_PATH, DEFAULT_REMOTE_URL, ATTEST_KEY, remoteUrlArg = (a) => /^https?:\/\//.test(a[0] ?? "") ? [a[0].replace(/\/+$/, ""), a.slice(1)] : [DEFAULT_REMOTE_URL, a], cfgDir = () => parentSolDir(solDir) ?? solDir;
13601
+ var CRED_PATH, DEFAULT_REMOTE_URL2, ATTEST_KEY, remoteUrlArg = (a) => /^https?:\/\//.test(a[0] ?? "") ? [a[0].replace(/\/+$/, ""), a.slice(1)] : [DEFAULT_REMOTE_URL2, a];
12035
13602
  var init_dispatch = __esm(() => {
12036
13603
  init_chain();
12037
13604
  init_diff();
@@ -12043,8 +13610,8 @@ var init_dispatch = __esm(() => {
12043
13610
  init_remote();
12044
13611
  init_lib();
12045
13612
  init_test_gate();
12046
- CRED_PATH = join18(homedir2(), ".sol", "credentials");
12047
- DEFAULT_REMOTE_URL = (process.env.SOL_REMOTE || "https://sol.midsummer.new").replace(/\/+$/, "");
13613
+ CRED_PATH = join19(homedir2(), ".sol", "credentials");
13614
+ DEFAULT_REMOTE_URL2 = (process.env.SOL_REMOTE || "https://sol.midsummer.new").replace(/\/+$/, "");
12048
13615
  ATTEST_KEY = process.env.SOL_ATTEST_KEY || undefined;
12049
13616
  });
12050
13617
 
@@ -12224,10 +13791,16 @@ var init_mcp_vcs_tools = __esm(() => {
12224
13791
  // src/bin/sol-mcp.ts
12225
13792
  var exports_sol_mcp = {};
12226
13793
  __export(exports_sol_mcp, {
13794
+ toolCallFor: () => toolCallFor,
12227
13795
  startWorkspaceMcp: () => startWorkspaceMcp
12228
13796
  });
12229
- import { mkdirSync as mkdirSync12 } from "fs";
12230
- import { dirname as dirname5, join as join19 } from "path";
13797
+ import { createHash as createHash7, randomUUID as randomUUID3 } from "crypto";
13798
+ import { mkdirSync as mkdirSync13 } from "fs";
13799
+ import { dirname as dirname6, join as join20 } from "path";
13800
+ function toolCallFor(name, args) {
13801
+ const argsHash = "h_" + createHash7("sha256").update(JSON.stringify(args ?? {})).digest("hex").slice(0, 16);
13802
+ return { name, argsHash, invocationId: randomUUID3() };
13803
+ }
12231
13804
  async function handle(ws, name, a) {
12232
13805
  switch (name) {
12233
13806
  case "sol_write":
@@ -12268,8 +13841,8 @@ async function handle(ws, name, a) {
12268
13841
  async function buildWorkspaceServer(solDir2) {
12269
13842
  const { Server } = await import("@modelcontextprotocol/sdk/server/index.js");
12270
13843
  const { CallToolRequestSchema, ListToolsRequestSchema } = await import("@modelcontextprotocol/sdk/types.js");
12271
- mkdirSync12(solDir2, { recursive: true });
12272
- const repoRoot2 = dirname5(solDir2);
13844
+ mkdirSync13(solDir2, { recursive: true });
13845
+ const repoRoot2 = dirname6(solDir2);
12273
13846
  if (process.cwd() !== repoRoot2) {
12274
13847
  try {
12275
13848
  process.chdir(repoRoot2);
@@ -12283,16 +13856,20 @@ async function buildWorkspaceServer(solDir2) {
12283
13856
  const r = await callVcsTool(req.params.name, req.params.arguments ?? {});
12284
13857
  return { content: [{ type: "text", text: r.text }], isError: r.isError };
12285
13858
  }
13859
+ if (AUTHORING_MUTATORS.has(req.params.name)) {
13860
+ ws.setToolCall(toolCallFor(req.params.name, req.params.arguments ?? {}));
13861
+ }
12286
13862
  try {
12287
13863
  return await handle(ws, req.params.name, req.params.arguments ?? {});
12288
13864
  } catch (e) {
13865
+ ws.setToolCall(undefined);
12289
13866
  return { content: [{ type: "text", text: "sol error: " + (e?.message ?? e) }], isError: true };
12290
13867
  }
12291
13868
  });
12292
13869
  return server;
12293
13870
  }
12294
13871
  async function startWorkspaceMcp(opts = {}) {
12295
- const solDir2 = opts.solDir || process.env.SOL_DIR || join19(process.cwd(), ".sol");
13872
+ const solDir2 = opts.solDir || process.env.SOL_DIR || join20(process.cwd(), ".sol");
12296
13873
  if (opts.http) {
12297
13874
  const { serveMcpHttp: serveMcpHttp3 } = await Promise.resolve().then(() => (init_mcp_http(), exports_mcp_http));
12298
13875
  await serveMcpHttp3(() => buildWorkspaceServer(solDir2), opts.http);
@@ -12302,12 +13879,13 @@ async function startWorkspaceMcp(opts = {}) {
12302
13879
  const server = await buildWorkspaceServer(solDir2);
12303
13880
  await server.connect(new StdioServerTransport);
12304
13881
  }
12305
- var authoringTools, VCS_TOOL_NAMES, tools, text = (s) => ({ content: [{ type: "text", text: s }] });
13882
+ var AUTHORING_MUTATORS, authoringTools, VCS_TOOL_NAMES, tools, text = (s) => ({ content: [{ type: "text", text: s }] });
12306
13883
  var init_sol_mcp = __esm(() => {
12307
13884
  init_file_store();
12308
13885
  init_workspace();
12309
13886
  init_mcp_http();
12310
13887
  init_mcp_vcs_tools();
13888
+ AUTHORING_MUTATORS = new Set(["sol_write", "sol_edit", "sol_move", "sol_rm", "sol_commit", "sol_checkpoint"]);
12311
13889
  authoringTools = [
12312
13890
  { name: "sol_write", description: "Create or overwrite a text file in the sol workspace. Authoring goes here \u2014 not to a disk.", inputSchema: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] } },
12313
13891
  { name: "sol_read", description: "Read a text file from the sol workspace.", inputSchema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } },
@@ -12332,5 +13910,6 @@ var init_sol_mcp = __esm(() => {
12332
13910
  init_sol_mcp();
12333
13911
 
12334
13912
  export {
13913
+ toolCallFor,
12335
13914
  startWorkspaceMcp
12336
13915
  };