omgbase 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "146fc1dd",
3
- "built": "2026-10-26T16:59:23.671Z"
2
+ "commit": "6c51e9d7",
3
+ "built": "2026-10-27T04:58:37.867Z"
4
4
  }
@@ -1,5 +1,5 @@
1
1
  import { parseArgs } from "node:util";
2
- import { rebuildIndex, runGc, buildEmbedTasks, buildDocEmbedTasks, indexIdentity, requireSemantic, resolveSettings, workspaceSettings, repoOwnSettings, writeWorkspaceSettings, writeRepoSettings, ftsIndexedRowCount, liveLeafCount, sizeReport, activeAlerts, readCapability, openCoverageFrom, captureFor, enableSemantic, disableSemantic, semanticProviderOf, SemanticProviderMissing, withWriterLock, vacuum, reclaimableBytes, humanBytes, formatShare, } from "@omgbase/core";
2
+ import { historyProviderOf, NATIVE_PROVIDER, requireProvider, providerConfigOf, CapabilityUnavailable, rebuildIndex, runGc, buildEmbedTasks, buildDocEmbedTasks, indexIdentity, requireSemantic, resolveSettings, workspaceSettings, repoOwnSettings, writeWorkspaceSettings, writeRepoSettings, ftsIndexedRowCount, liveLeafCount, sizeReport, activeAlerts, refreshLegacyBytesAlert, readCapability, openCoverageFrom, captureFor, enableSemantic, disableSemantic, semanticProviderOf, SemanticProviderMissing, withWriterLock, vacuum, reclaimableBytes, humanBytes, formatShare, } from "@omgbase/core";
3
3
  import { CliUsageError, EngineErrorLike, EXIT_OK, EXIT_ERROR, emitMachine, renderHelp } from "../output.js";
4
4
  import { columns } from "../render.js";
5
5
  import { loadEmbedding, drainEmbeddings } from "./_embed.js";
@@ -87,7 +87,7 @@ function runDoctor(cli, args) {
87
87
  if (values.help) {
88
88
  return renderHelp(cli, {
89
89
  name: "doctor",
90
- summary: "Invariant sweep: FTS rows vs live leaf blocks, head present, capture complete, active alerts, SQLite integrity — exit 1 on any failure (CI-able)",
90
+ summary: "Invariant sweep: FTS rows vs live leaf blocks, head present, bytes reproducible, capture complete, active alerts, SQLite integrity — exit 1 on any failure (CI-able)",
91
91
  usage: ["doctor [--json]", "doctor --size [--json]"],
92
92
  options: [
93
93
  ["--size", "instead of the checks: what the database is made of — bytes and rows per category (content, identity, recovery, history, search, sync — the change feed under sync) and table, the file's pages, the feed's position, what VACUUM would reclaim, advice"],
@@ -124,10 +124,33 @@ function runDoctor(cli, args) {
124
124
  // build without the module) owes none and the check says so in its detail.
125
125
  const headless = db.prepare("SELECT count(*) c FROM docs d WHERE d.deleted_at IS NULL AND d.head IS NULL").get().c;
126
126
  checks.push({ name: "head present", ok: headless === 0, detail: `${headless} without a head` });
127
+ // Store 17.4 (Phase 5a-ii; spec/store §5.11, §8 C9): every live document's
128
+ // rows reproduce its `file_hash` — the fidelity check the migration runs,
129
+ // re-derived here in full for the selected repository, and the `legacy_bytes`
130
+ // alert written to match (raised with the count and a sample of ids, or
131
+ // cleared), so the `alerts` check below sees the current condition. A
132
+ // document ingested before block trivia was stored fails it until `omg sync`
133
+ // repairs its rows from the file.
134
+ const legacy = refreshLegacyBytesAlert(db, repo.repoId, new Date().toISOString());
135
+ checks.push({ name: "bytes reproducible", ok: legacy.length === 0, detail: `${legacy.length} document(s) not reproducible from their rows` });
127
136
  const manifest = readCapability(db, repo.repoId, "history");
128
137
  const since = openCoverageFrom(manifest);
129
138
  const recorder = captureFor(db, repo.repoId);
130
- if (recorder && since !== null) {
139
+ const provider = historyProviderOf(manifest);
140
+ if (provider !== NATIVE_PROVIDER && since !== null) {
141
+ // Phase 6a (spec/cli §6, 5.5): a git-provider repository's capture is the worker's — the check reads how far the
142
+ // branch tip lags the heads; `provider git: <n> heads not yet checkpointed`, or `unavailable` when the provider cannot run.
143
+ try {
144
+ const uncaptured = requireProvider(db, repo.repoId, provider).reader(repo.repoId, providerConfigOf(manifest)).uncapturedHeads(repo.repoId, since);
145
+ checks.push({ name: "capture complete", ok: uncaptured === 0, detail: `provider ${provider}: ${uncaptured} heads not yet checkpointed` });
146
+ }
147
+ catch (e) {
148
+ if (!(e instanceof CapabilityUnavailable))
149
+ throw e;
150
+ checks.push({ name: "capture complete", ok: false, detail: `provider ${provider}: unavailable (${e.data().reason ?? e.message})` });
151
+ }
152
+ }
153
+ else if (recorder && since !== null) {
131
154
  const uncaptured = recorder.uncapturedHeads(repo.repoId, since);
132
155
  checks.push({ name: "capture complete", ok: uncaptured === 0, detail: `${uncaptured} heads without a revision` });
133
156
  }
@@ -228,8 +251,10 @@ function capabilityLine(capabilities, name) {
228
251
  const c = capabilities.find((x) => x.capability === name);
229
252
  if (!c || c.state === "disabled")
230
253
  return "disabled";
254
+ // Phase 6a: a provider row says which — `enabled since <ts> (git)`.
255
+ const provider = typeof c.config.provider === "string" ? ` (${c.config.provider})` : "";
231
256
  if (name === "history")
232
- return c.coverage_from ? `enabled since ${c.coverage_from}` : "enabled";
257
+ return c.coverage_from ? `enabled since ${c.coverage_from}${provider}` : `enabled${provider}`;
233
258
  const model = c.config.model;
234
259
  return typeof model === "string" ? `enabled (${model})` : "enabled";
235
260
  }
@@ -44,7 +44,7 @@ async function runDiff(cli, args) {
44
44
  // Through the recorder seam (Phase 4a): `capability_unavailable { history, unavailable | disabled }` when the build has no module or nothing was ever captured;
45
45
  // a revision the recorder does not hold answers by the spec/surface §2.1 decision table (Phase 4b).
46
46
  try {
47
- result = requireHistoryReader(ws.store.db).docDiffUnified(info.docId, {
47
+ result = requireHistoryReader(ws.store.db, repo.repoId).docDiffUnified(info.docId, {
48
48
  ...(values.from ? { fromRev: values.from } : {}),
49
49
  ...(values.to ? { toRev: values.to } : {}),
50
50
  label: ref,
@@ -62,8 +62,10 @@ async function runDiff(cli, args) {
62
62
  let entries;
63
63
  if (cli.flags.server)
64
64
  entries = await remoteCall(cli, "docs_diff", { ...docLocator(ref), from_rev: result.from, to_rev: result.to });
65
- else
66
- entries = requireHistoryReader(cli.workspace().store.db).diffBlocks(result._id, result.from, result.to);
65
+ else {
66
+ const ws = cli.workspace();
67
+ entries = requireHistoryReader(ws.store.db, cli.repo(ws).repoId).diffBlocks(result._id, result.from, result.to);
68
+ }
67
69
  if (cli.flags.mode !== "human")
68
70
  return emitMachine(cli, entries, { items: entries, ids: entries.map((e) => e._id) });
69
71
  if (entries.length === 0) {
@@ -33,7 +33,7 @@ async function runHist(cli, args) {
33
33
  if (!resolved || resolved.kind !== "block")
34
34
  throw new EngineErrorLike("block_missing", `hist needs a block id; got ${ref}`);
35
35
  // Through the recorder seam (Phase 4a): `capability_unavailable { history, unavailable | disabled }` when the build has no module or nothing was ever captured.
36
- changes = requireHistoryReader(ws.store.db).blocksHistory(resolved.blockId, values.n ? { limit: Number(values.n) } : {});
36
+ changes = requireHistoryReader(ws.store.db, repo.repoId).blocksHistory(resolved.blockId, values.n ? { limit: Number(values.n) } : {});
37
37
  }
38
38
  // A change carries no id of its own, so `--ids` is the `--json` document.
39
39
  if (cli.flags.mode !== "human")
@@ -1,7 +1,8 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
- import { evaluateRetention, retentionPolicy, exportHistory, attachArchive, detachArchive, listArchives, humanBytes, withWriterLock, enableHistory, disableHistory, purgeHistory, openCoverageFrom, RetentionPolicyError, NotASegment, StaleSegment, ARCHIVE_DIR, } from "@omgbase/core";
4
+ import { evaluateRetention, retentionPolicy, exportHistory, attachArchive, detachArchive, listArchives, humanBytes, withWriterLock, enableHistory, disableHistory, purgeHistory, openCoverageFrom, historyProviderOf, readCapability, requireProvider, providerConfigOf, NATIVE_PROVIDER, HistoryProviderConflict, RetentionPolicyError, NotASegment, StaleSegment, ARCHIVE_DIR, EngineError, fsVaultSink, } from "@omgbase/core";
5
+ import { parseCadence, GIT_PROVIDER, GitHistoryProvider, PUSH_POLICIES, FETCH_POLICIES, rewrittenMessage } from "@omgbase/git-adapter";
5
6
  import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
6
7
  import { columns } from "../render.js";
7
8
  // `omg history <enable|disable|purge|plan|export|attach|detach|list>` (spec/cli
@@ -16,15 +17,25 @@ import { columns } from "../render.js";
16
17
  // the segment registry). No MCP tool: the catalog's traces are
17
18
  // `version.capabilities.history`, the `revisions.archived` attribute,
18
19
  // `capability_unavailable` and `revision_archived`.
19
- const SUBCOMMANDS = ["enable", "disable", "purge", "plan", "export", "attach", "detach", "list"];
20
+ const SUBCOMMANDS = ["enable", "disable", "purge", "checkpoint", "push", "fetch", "remote", "plan", "export", "attach", "detach", "list"];
21
+ const REMOTE_SUBCOMMANDS = ["set", "unset", "status", "resolve", "reconcile"];
22
+ const RECONCILE_MODES = ["rebase", "merge", "adopt", "fork"];
23
+ const PROVIDERS = [NATIVE_PROVIDER, GIT_PROVIDER];
20
24
  function renderCard(cli) {
21
25
  return renderHelp(cli, {
22
26
  name: "history",
23
- summary: "History capture (enable/disable/purge per repository) and retention: plan what the policy would archive, export it to a segment, attach/detach/list segments",
27
+ summary: "History capture (enable/disable/purge per repository; native or git provider) and retention: plan what the policy would archive, export it to a segment, attach/detach/list segments",
24
28
  usage: [
25
- "history enable [--best-effort]",
29
+ "history enable [--best-effort] [--provider native|git] [--branch <b>] [--cadence every|<ms>]",
26
30
  "history disable",
27
31
  "history purge [--before <ts>]",
32
+ "history checkpoint",
33
+ "history remote set --url <url> [--name <remote>] [--branch <b>] [--push off|on-checkpoint|periodic] [--fetch off|periodic|webhook]",
34
+ "history remote unset | status",
35
+ "history push",
36
+ "history fetch",
37
+ "history remote resolve <path> --ours|--theirs",
38
+ "history remote reconcile --rebase|--merge|--adopt|--fork",
28
39
  "history plan [--before <ts>] [--policy <json|@file>]",
29
40
  "history export [--prune] [--dest <dir>] [--before <ts>] [--policy <json|@file>] [--dry-run]",
30
41
  "history attach <file|dir>",
@@ -35,7 +46,18 @@ function renderCard(cli) {
35
46
  ["enable", "turn capture on for the repository: one baseline commit (origin import) records every live document's current head, a coverage interval opens, every later write is captured; idempotent"],
36
47
  ["disable", "turn capture off: the coverage interval closes, later writes record no history (the feed still does); what was captured stays readable; idempotent"],
37
48
  ["--best-effort", "(enable) do not require capture: a writer without the history module may still write (default: such a writer is refused — writer_requires)"],
38
- ["purge", "delete the repository's captured history — every commit, or with --before those before an instant (a live head's revision stays while capture is on); the recorder's id holds are released; the history tables are dropped when no repository is enabled and nothing is left"],
49
+ ["--provider <p>", "(enable) who keeps the history: native (the store's own tables, captured in every commit — the default) or git (a bare repository at .omgbase/git, committed from the rows by the checkpoint worker); one provider per repository — switch with disable, then enable --provider"],
50
+ ["--branch <b>", "(enable --provider git) the branch the commits go on (default omgbase)"],
51
+ ["--cadence <c>", "(enable --provider git) every: one commit per feed row (default); <ms>: the rows pending when the worker runs fold into one commit"],
52
+ ["checkpoint", "(git provider) run the checkpoint worker once: the feed rows since the branch tip become commits; prints { committed, from_seq, to_seq, tip, rescanned }"],
53
+ ["remote set", "(git provider) bind a remote: its URL (never with credentials — git resolves access through the host), the remote branch (default: the local one), when to push (off — the default; on-checkpoint — after every checkpoint that committed; periodic — on every freshness sweep) and when to fetch (off; periodic — on every sweep; webhook — reserved: an external caller runs history fetch)"],
54
+ ["remote unset", "(git provider) forget the remote, its tracking refs and its alerts"],
55
+ ["remote status", "(git provider) local refs only, no network: { remote, branch, local_tip, remote_tip, pushed_through, state: in-sync|ahead|behind|diverged|unknown }"],
56
+ ["push", "(git provider) push the branch and the ids ref — never --force; a rejected push raises git_remote_diverged and exits 1 (run fetch to merge, then push again)"],
57
+ ["fetch", "(git provider) fetch the remote branch: fast-forward (the remote's changes enter the vault as external edits), a tree-level three-way merge when both sides moved (one merge commit), conflicts kept as <stem>.remote-<sha>.md beside yours with git_merge_conflict raised, or git_remote_diverged when the remote was rewritten (push and fetch pause)"],
58
+ ["remote resolve", "(git provider) settle one conflict: --ours keeps your bytes, --theirs takes the remote's; the sidecar leaves; the merge commit is written when none remains"],
59
+ ["remote reconcile", "(git provider; only while git_remote_diverged or git_remote_forked is active) --rebase replays your unpushed checkpoints onto the new remote head (trailers preserved, the ids ref rebased alongside); --merge one merge commit keeping both histories; --adopt the remote's head becomes truth (its tree into the vault, your edits kept beside as <stem>.local-<sha>.md, your old tip on refs/omgbase/backup/<ts>); --fork pushes your branch as <branch>-omgbase-<ts> for a human to merge with git"],
60
+ ["purge", "delete the repository's captured history — every commit, or with --before those before an instant (a live head's revision stays while capture is on); the recorder's id holds are released; the history tables are dropped when no repository is enabled and nothing is left; on a git-provider repository the bare repository and the worker's cursor are removed"],
39
61
  ["plan", "per document: revisions kept / to archive and the bytes only they reach; the policy in effect"],
40
62
  ["export", "write ONE new segment file (default dest .omgbase/archives/) holding the archived revisions' bodies; --prune then squashes the store and records the segment"],
41
63
  ["attach <path>", "record a segment file (or every segment of a directory that belongs to this repo) so reads at its revisions work again"],
@@ -240,13 +262,17 @@ function interval(i) {
240
262
  */
241
263
  function renderLifecycle(cli, verb, r) {
242
264
  const row = r.row;
265
+ const provider = historyProviderOf(row);
243
266
  if (cli.flags.mode !== "human") {
244
- return emitMachine(cli, { state: row.state, coverage: row.coverage, writer_requires: row.writer_requires, changed: r.changed, ...(r.baseline ? { baseline: r.baseline } : {}) });
267
+ // The git provider's enable (spec/cli §6, 5.5) adds `provider` and the branch `tip` of its baseline commit.
268
+ return emitMachine(cli, { state: row.state, coverage: row.coverage, writer_requires: row.writer_requires, changed: r.changed, ...(provider !== NATIVE_PROVIDER ? { provider } : {}), ...(r.baseline ? { baseline: r.baseline } : {}), ...(r.tip !== undefined ? { tip: r.tip } : {}) });
245
269
  }
246
270
  const { io, style } = cli;
247
271
  if (verb === "enable") {
248
272
  const since = openCoverageFrom(row);
249
- io.err(style.dim(` history ${r.changed ? "enabled" : "already enabled"}${since ? ` since ${since}` : ""}${r.baseline ? ` (baseline ${r.baseline.commit}: ${plural(r.baseline.revisions, "revision")})` : ""}`));
273
+ const via = provider === NATIVE_PROVIDER ? "" : ` via ${provider}`;
274
+ const baseline = r.baseline ? (provider === NATIVE_PROVIDER ? ` (baseline ${r.baseline.commit}: ${plural(r.baseline.revisions, "revision")})` : ` (baseline ${r.baseline.commit}: ${plural(r.baseline.revisions, "document")}${r.tip ? `, tip ${r.tip}` : ""})`) : "";
275
+ io.err(style.dim(` history ${r.changed ? "enabled" : "already enabled"}${since ? ` since ${since}` : ""}${via}${baseline}`));
250
276
  }
251
277
  else {
252
278
  io.err(style.dim(r.changed || row.coverage.length > 0 ? ` history ${r.changed ? "disabled" : "already disabled"} (captured ${row.coverage.map(interval).join(", ")})` : " history already disabled"));
@@ -265,8 +291,13 @@ function renderLifecycle(cli, verb, r) {
265
291
  */
266
292
  function renderPurge(cli, r) {
267
293
  if (cli.flags.mode !== "human")
268
- return emitMachine(cli, { purged: r.purged, claims_released: r.claims_released, coverage: r.coverage, tables_dropped: r.tables_dropped });
294
+ return emitMachine(cli, { purged: r.purged, claims_released: r.claims_released, coverage: r.coverage, tables_dropped: r.tables_dropped, ...(r.provider ? { provider: r.provider } : {}) });
269
295
  const { io, style } = cli;
296
+ // The git provider (spec/cli §6, 5.5): the bare repository and the worker's cursor leave; ` purged the git history (<path> removed)` / ` nothing to purge (<path> is already gone)`.
297
+ if (r.provider) {
298
+ io.err(style.dim(r.provider.removed ? ` purged the ${r.provider.name} history (${r.provider.path} removed, cursor cleared)` : ` nothing to purge (${r.provider.path} is already gone)`));
299
+ return EXIT_OK;
300
+ }
270
301
  const p = r.purged;
271
302
  const total = Object.values(p).reduce((n, v) => n + v, 0) + r.claims_released;
272
303
  if (total === 0 && !r.tables_dropped) {
@@ -283,16 +314,294 @@ function runPurge(cli, args) {
283
314
  const ws = cli.workspace();
284
315
  const repo = cli.repo(ws);
285
316
  const before = parseBefore(values.before);
317
+ // A git history is one line of commits: `--before` has no meaning there (spec/cli §6, 5.5).
318
+ if (before !== undefined && historyProviderOf(readCapability(ws.store.db, repo.repoId, "history")) !== NATIVE_PROVIDER)
319
+ throw new CliUsageError("--before has no meaning for the git provider: history purge removes the whole bare repository");
286
320
  const result = withWriterLock(ws.omgbaseDir, () => purgeHistory(ws.store.db, repo.repoId, { ...(before !== undefined ? { before } : {}) }));
287
321
  return renderPurge(cli, result);
288
322
  }
323
+ /**
324
+ * `history checkpoint` (spec/cli §6, 5.5; spec/store §5.12): the git provider's
325
+ * worker, once, under the writer lock; `--json` the receipt `{ committed,
326
+ * from_seq, to_seq, tip, rescanned }`, human ` checkpointed <n> commit(s)
327
+ * (seq <from>..<to>), tip <sha>` / ` nothing to checkpoint (tip <sha>)` /
328
+ * ` rescanned: 1 commit at seq <head>, tip <sha>`. On a native repository it
329
+ * is a usage error: the recorder captures in every commit.
330
+ */
331
+ function runCheckpoint(cli, args) {
332
+ parseArgs({ args, allowPositionals: false, options: {} });
333
+ const ws = cli.workspace();
334
+ const repo = cli.repo(ws);
335
+ const row = readCapability(ws.store.db, repo.repoId, "history");
336
+ const provider = historyProviderOf(row);
337
+ if (provider === NATIVE_PROVIDER)
338
+ throw new CliUsageError("history checkpoint is the git provider's: this repository's history is native (captured in every commit)", `run '${cli.prog} history enable --provider git' after '${cli.prog} history disable' to switch`);
339
+ if (!row || row.state !== "enabled")
340
+ throw new CliUsageError(`history is disabled for this repository — enable it with '${cli.prog} history enable --provider ${provider}'`);
341
+ const result = withWriterLock(ws.omgbaseDir, () => requireProvider(ws.store.db, repo.repoId, provider).checkpoint(repo.repoId, providerConfigOf(row)));
342
+ if (cli.flags.mode !== "human")
343
+ return emitMachine(cli, result);
344
+ const { io, style } = cli;
345
+ if (result.rescanned)
346
+ io.err(style.dim(` rescanned: 1 commit at seq ${result.to_seq}, tip ${result.tip}`));
347
+ else if (result.committed === 0)
348
+ io.err(style.dim(` nothing to checkpoint (tip ${result.tip ?? "none"})`));
349
+ else
350
+ io.err(style.dim(` checkpointed ${plural(result.committed, "commit")} (seq ${result.from_seq}..${result.to_seq}), tip ${result.tip}`));
351
+ // `push: "on-checkpoint"` (cli 5.6): the push that followed, or why it did not.
352
+ if (result.push)
353
+ io.err(style.dim("error" in result.push ? ` push failed: ${result.push.error}` : result.push.pushed ? ` pushed ${result.push.tip}${result.push.pushed_through === null ? "" : ` (through seq ${result.push.pushed_through})`}` : ` nothing to push`));
354
+ return EXIT_OK;
355
+ }
356
+ /** The selected repository as a git-provider repository, or the usage errors every remote verb shares. */
357
+ function gitContext(cli, verb) {
358
+ const ws = cli.workspace();
359
+ const repo = cli.repo(ws);
360
+ const row = readCapability(ws.store.db, repo.repoId, "history");
361
+ const provider = historyProviderOf(row);
362
+ if (provider === NATIVE_PROVIDER)
363
+ throw new CliUsageError(`history ${verb} is the git provider's: this repository's history is native (captured in every commit)`, `run '${cli.prog} history enable --provider git' after '${cli.prog} history disable' to switch`);
364
+ if (!row || row.state !== "enabled")
365
+ throw new CliUsageError(`history is disabled for this repository — enable it with '${cli.prog} history enable --provider ${provider}'`);
366
+ const p = requireProvider(ws.store.db, repo.repoId, provider);
367
+ if (!(p instanceof GitHistoryProvider))
368
+ throw new CliUsageError(`history ${verb} is the git provider's (this repository's provider is ${provider})`);
369
+ return { ws, repoId: repo.repoId, rootPath: repo.rootPath, row, provider: p };
370
+ }
371
+ /** The vault the remote half writes inbound content into: the repository's filesystem root through the observe path. A sourceless repository has none. */
372
+ function vaultOf(cli, ctx, verb) {
373
+ if (ctx.rootPath === null)
374
+ throw new CliUsageError(`history ${verb} needs a repository with a filesystem source: inbound content is written into its root as external edits`, `attach one with '${cli.prog} source add <dir> --repo <slug>'`);
375
+ return fsVaultSink(ctx.ws.store, ctx.repoId, ctx.rootPath);
376
+ }
377
+ /** A thrown `EngineError` of the remote half (`git_remote_diverged`, `git_merge_conflict`) renders as the engine error it is (exit 1); anything else propagates. */
378
+ function remoteError(e) {
379
+ if (e instanceof EngineError)
380
+ throw new EngineErrorLike(e.code, e.message, { data: e.data, retriable: e.retriable });
381
+ throw e;
382
+ }
383
+ function runRemote(cli, args) {
384
+ const [sub, ...rest] = args;
385
+ if (sub === undefined || !REMOTE_SUBCOMMANDS.includes(sub))
386
+ throw new CliUsageError(`history remote takes one of ${REMOTE_SUBCOMMANDS.join("|")}${sub === undefined ? "" : ` (got ${JSON.stringify(sub)})`}`, `run '${cli.prog} history --help'`);
387
+ switch (sub) {
388
+ case "set": return runRemoteSet(cli, rest);
389
+ case "unset": return runRemoteUnset(cli, rest);
390
+ case "status": return runRemoteStatus(cli, rest);
391
+ case "resolve": return runRemoteResolve(cli, rest);
392
+ case "reconcile": return runRemoteReconcile(cli, rest);
393
+ }
394
+ }
395
+ function runRemoteSet(cli, args) {
396
+ const { values } = parseArgs({ args, allowPositionals: false, options: { url: { type: "string" }, name: { type: "string" }, branch: { type: "string" }, push: { type: "string" }, fetch: { type: "string" } } });
397
+ if (values.url === undefined)
398
+ throw new CliUsageError("history remote set requires --url <url>");
399
+ if (values.push !== undefined && !PUSH_POLICIES.includes(values.push))
400
+ throw new CliUsageError(`--push must be one of ${PUSH_POLICIES.join("|")} (got ${JSON.stringify(values.push)})`);
401
+ if (values.fetch !== undefined && !FETCH_POLICIES.includes(values.fetch))
402
+ throw new CliUsageError(`--fetch must be one of ${FETCH_POLICIES.join("|")} (got ${JSON.stringify(values.fetch)})`);
403
+ const ctx = gitContext(cli, "remote set");
404
+ let config;
405
+ try {
406
+ config = withWriterLock(ctx.ws.omgbaseDir, () => ctx.provider.remoteSet(ctx.repoId, providerConfigOf(ctx.row), { url: values.url, ...(values.name !== undefined ? { name: values.name } : {}), ...(values.branch !== undefined ? { branch: values.branch } : {}), ...(values.push !== undefined ? { push: values.push } : {}), ...(values.fetch !== undefined ? { fetch: values.fetch } : {}) }));
407
+ }
408
+ catch (e) {
409
+ // The binding's own refusals (credentials in the URL, a bad name, branch or policy) are usage errors.
410
+ if (e instanceof Error && !(e instanceof EngineError) && /credentials|not a remote name|not a branch name|--push|--fetch|--url/.test(e.message))
411
+ throw new CliUsageError(e.message);
412
+ throw e;
413
+ }
414
+ const r = config.remote;
415
+ if (cli.flags.mode !== "human")
416
+ return emitMachine(cli, { remote: r });
417
+ cli.io.err(cli.style.dim(` remote ${r.name} → ${r.url} (branch ${r.branch}, push ${r.push}, fetch ${r.fetch})`));
418
+ return EXIT_OK;
419
+ }
420
+ function runRemoteUnset(cli, args) {
421
+ parseArgs({ args, allowPositionals: false, options: {} });
422
+ const ctx = gitContext(cli, "remote unset");
423
+ const had = ctx.row.config.remote?.name ?? null;
424
+ withWriterLock(ctx.ws.omgbaseDir, () => ctx.provider.remoteUnset(ctx.repoId, providerConfigOf(ctx.row)));
425
+ if (cli.flags.mode !== "human")
426
+ return emitMachine(cli, { remote: null });
427
+ cli.io.err(cli.style.dim(had === null ? " no remote was bound" : ` remote ${had} unbound`));
428
+ return EXIT_OK;
429
+ }
430
+ function renderRemoteStatus(cli, s) {
431
+ if (cli.flags.mode !== "human")
432
+ return emitMachine(cli, s);
433
+ const { io, style } = cli;
434
+ const rows = [
435
+ [style.dim("remote"), `${s.remote}/${s.branch}`],
436
+ [style.dim("state"), s.state],
437
+ [style.dim("local tip"), s.local_tip ?? "—"],
438
+ [style.dim("remote tip"), s.remote_tip ?? "—"],
439
+ [style.dim("pushed through"), s.pushed_through === null ? "—" : `seq ${s.pushed_through}`],
440
+ ];
441
+ for (const line of columns(rows, [{}, {}]))
442
+ io.out(` ${line}`);
443
+ return EXIT_OK;
444
+ }
445
+ function runRemoteStatus(cli, args) {
446
+ parseArgs({ args, allowPositionals: false, options: {} });
447
+ const ctx = gitContext(cli, "remote status");
448
+ if (!ctx.row.config.remote)
449
+ throw new CliUsageError("no remote is bound to this repository's git history", `bind one with '${cli.prog} history remote set --url <url>'`);
450
+ return renderRemoteStatus(cli, ctx.provider.remoteStatus(ctx.repoId, providerConfigOf(ctx.row)));
451
+ }
452
+ function runPush(cli, args) {
453
+ parseArgs({ args, allowPositionals: false, options: {} });
454
+ const ctx = gitContext(cli, "push");
455
+ if (!ctx.row.config.remote)
456
+ throw new CliUsageError("no remote is bound to this repository's git history", `bind one with '${cli.prog} history remote set --url <url>'`);
457
+ let r;
458
+ try {
459
+ r = withWriterLock(ctx.ws.omgbaseDir, () => ctx.provider.push(ctx.repoId, providerConfigOf(ctx.row)));
460
+ }
461
+ catch (e) {
462
+ remoteError(e);
463
+ }
464
+ if (cli.flags.mode !== "human")
465
+ return emitMachine(cli, r);
466
+ const remote = ctx.row.config.remote;
467
+ const ids = r.ids === "rejected" ? " (the ids ref was rejected: the remote's identity record is another engine's)" : "";
468
+ cli.io.err(cli.style.dim(r.pushed ? ` pushed ${r.tip} to ${remote.name}/${remote.branch}${r.pushed_through === null ? "" : ` (through seq ${r.pushed_through})`}${ids}` : ` nothing to push (${remote.name}/${remote.branch} at ${r.tip ?? "none"})${ids}`));
469
+ return EXIT_OK;
470
+ }
471
+ function renderFetch(cli, remote, f) {
472
+ if (f.state === "diverged") {
473
+ // Detect, stop, alert (spec/store §5.12 "Divergence"): the verb itself fails so the operator sees it; the alert line follows.
474
+ throw new EngineErrorLike("git_remote_diverged", rewrittenMessage(remote.name, remote.branch, f.remote_tip, f.old_remote_tip ?? null), { data: { remote: remote.name, branch: remote.branch, local_tip: f.tip, remote_tip: f.remote_tip, old_remote_tip: f.old_remote_tip ?? null }, retriable: true });
475
+ }
476
+ if (cli.flags.mode !== "human")
477
+ return emitMachine(cli, f);
478
+ const { io, style } = cli;
479
+ switch (f.state) {
480
+ case "nothing":
481
+ io.err(style.dim(` nothing to fetch from ${remote.name}/${remote.branch}${f.remote_tip ? ` (at ${f.remote_tip})` : ""}`));
482
+ break;
483
+ case "fast-forward":
484
+ io.err(style.dim(` fast-forwarded to ${f.tip}: ${plural(f.applied.length, "file")} written into the vault`));
485
+ break;
486
+ case "merged":
487
+ io.err(style.dim(` merged ${remote.name}/${remote.branch} at ${f.remote_tip}: ${plural(f.applied.length, "file")} written into the vault, merge commit ${f.tip}`));
488
+ break;
489
+ case "conflict":
490
+ io.err(style.dim(` ${plural(f.conflicts.length, "conflict")}: ${f.conflicts.join(", ")} — the remote's versions are beside yours; settle each with ${cli.prog} history remote resolve <path> --ours|--theirs`));
491
+ break;
492
+ }
493
+ return EXIT_OK;
494
+ }
495
+ function runFetch(cli, args) {
496
+ parseArgs({ args, allowPositionals: false, options: {} });
497
+ const ctx = gitContext(cli, "fetch");
498
+ if (!ctx.row.config.remote)
499
+ throw new CliUsageError("no remote is bound to this repository's git history", `bind one with '${cli.prog} history remote set --url <url>'`);
500
+ const vault = vaultOf(cli, ctx, "fetch");
501
+ let f;
502
+ try {
503
+ f = withWriterLock(ctx.ws.omgbaseDir, () => ctx.provider.fetch(ctx.repoId, providerConfigOf(ctx.row), vault));
504
+ }
505
+ catch (e) {
506
+ remoteError(e);
507
+ }
508
+ return renderFetch(cli, ctx.row.config.remote, f);
509
+ }
510
+ function runRemoteResolve(cli, args) {
511
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { ours: { type: "boolean" }, theirs: { type: "boolean" } } });
512
+ const path = positionals[0];
513
+ if (!path)
514
+ throw new CliUsageError("history remote resolve requires a <path>");
515
+ if (Boolean(values.ours) === Boolean(values.theirs))
516
+ throw new CliUsageError("history remote resolve takes exactly one of --ours or --theirs");
517
+ const ctx = gitContext(cli, "remote resolve");
518
+ const vault = vaultOf(cli, ctx, "remote resolve");
519
+ const storage = path.replace(/^\/+/, "");
520
+ let r;
521
+ try {
522
+ r = withWriterLock(ctx.ws.omgbaseDir, () => ctx.provider.resolve(ctx.repoId, providerConfigOf(ctx.row), vault, storage, values.ours ? "ours" : "theirs"));
523
+ }
524
+ catch (e) {
525
+ if (e instanceof Error && !(e instanceof EngineError) && /no merge conflict is pending|is not a pending conflict/.test(e.message))
526
+ throw new CliUsageError(e.message);
527
+ remoteError(e);
528
+ }
529
+ if (cli.flags.mode !== "human")
530
+ return emitMachine(cli, r);
531
+ cli.io.err(cli.style.dim(` resolved ${r.path} (--${r.choice}); ${plural(r.remaining.length, "conflict")} remaining${r.merged ? `; merge commit ${r.merged}` : ""}`));
532
+ return EXIT_OK;
533
+ }
534
+ function renderReconcile(cli, r) {
535
+ if (cli.flags.mode !== "human")
536
+ return emitMachine(cli, r);
537
+ const { io, style } = cli;
538
+ const conflicts = r.conflicts.length > 0 ? `; ${plural(r.conflicts.length, "conflict")}: ${r.conflicts.join(", ")} (settle each with ${cli.prog} history remote resolve <path> --ours|--theirs)` : "";
539
+ const applied = `${plural(r.applied.length, "file")} written into the vault`;
540
+ switch (r.mode) {
541
+ case "rebase":
542
+ io.err(style.dim(` rebased ${plural(r.rewritten, "commit")} onto the remote's head, tip ${r.tip}; the previous tip is kept on refs/omgbase/backup; ${applied}${conflicts}`));
543
+ break;
544
+ case "merge":
545
+ io.err(style.dim(r.rewritten === 1 ? ` merged the remote's head: merge commit ${r.tip}; the previous tip is kept on refs/omgbase/backup; ${applied}` : ` merge pending: ${applied}${conflicts}`));
546
+ break;
547
+ case "adopt":
548
+ io.err(style.dim(` adopted the remote's head ${r.tip}; the previous tip is kept on refs/omgbase/backup; ${applied}${conflicts}`));
549
+ break;
550
+ case "fork":
551
+ io.err(style.dim(` pushed the engine's branch as ${r.fork}; merge it with git, then run ${cli.prog} history remote reconcile --adopt`));
552
+ break;
553
+ }
554
+ return EXIT_OK;
555
+ }
556
+ function runRemoteReconcile(cli, args) {
557
+ const { values } = parseArgs({ args, allowPositionals: false, options: { rebase: { type: "boolean" }, merge: { type: "boolean" }, adopt: { type: "boolean" }, fork: { type: "boolean" } } });
558
+ const modes = RECONCILE_MODES.filter((m) => values[m]);
559
+ if (modes.length !== 1)
560
+ throw new CliUsageError(`history remote reconcile takes exactly one of ${RECONCILE_MODES.map((m) => `--${m}`).join(" | ")}`);
561
+ const ctx = gitContext(cli, "remote reconcile");
562
+ const vault = vaultOf(cli, ctx, "remote reconcile");
563
+ let r;
564
+ try {
565
+ r = withWriterLock(ctx.ws.omgbaseDir, () => ctx.provider.reconcile(ctx.repoId, providerConfigOf(ctx.row), vault, modes[0]));
566
+ }
567
+ catch (e) {
568
+ if (e instanceof Error && !(e instanceof EngineError) && /is not diverged|no remote is bound/.test(e.message))
569
+ throw new CliUsageError(e.message);
570
+ remoteError(e);
571
+ }
572
+ return renderReconcile(cli, r);
573
+ }
289
574
  function runEnable(cli, args) {
290
- const { values } = parseArgs({ args, allowPositionals: false, options: { "best-effort": { type: "boolean" } } });
575
+ const { values } = parseArgs({ args, allowPositionals: false, options: { "best-effort": { type: "boolean" }, provider: { type: "string" }, branch: { type: "string" }, cadence: { type: "string" } } });
576
+ const provider = values.provider ?? NATIVE_PROVIDER;
577
+ if (!PROVIDERS.includes(provider))
578
+ throw new CliUsageError(`--provider must be one of ${PROVIDERS.join("|")} (got ${JSON.stringify(provider)})`);
579
+ if (provider === NATIVE_PROVIDER && (values.branch !== undefined || values.cadence !== undefined))
580
+ throw new CliUsageError("--branch and --cadence go with --provider git");
581
+ const providerOptions = {};
582
+ if (values.branch !== undefined)
583
+ providerOptions.branch = values.branch;
584
+ if (values.cadence !== undefined) {
585
+ try {
586
+ providerOptions.cadence = parseCadence(values.cadence);
587
+ }
588
+ catch (e) {
589
+ throw new CliUsageError(e.message);
590
+ }
591
+ }
291
592
  const ws = cli.workspace();
292
593
  const repo = cli.repo(ws);
293
594
  // A write (the baseline commit): under the workspace writer lock like every other.
294
- const result = withWriterLock(ws.omgbaseDir, () => enableHistory(ws.store.db, repo.repoId, { writerRequires: !values["best-effort"] }));
295
- return renderLifecycle(cli, "enable", result);
595
+ try {
596
+ const result = withWriterLock(ws.omgbaseDir, () => enableHistory(ws.store.db, repo.repoId, { writerRequires: !values["best-effort"], ...(provider !== NATIVE_PROVIDER ? { provider, providerOptions } : {}) }));
597
+ return renderLifecycle(cli, "enable", result);
598
+ }
599
+ catch (e) {
600
+ // spec/store §5.12: one provider per repository — the switch is `disable` then `enable --provider`.
601
+ if (e instanceof HistoryProviderConflict)
602
+ throw new CliUsageError(e.message);
603
+ throw e;
604
+ }
296
605
  }
297
606
  function runDisable(cli, args) {
298
607
  parseArgs({ args, allowPositionals: false, options: {} });
@@ -312,6 +621,10 @@ function runHistory(cli, args) {
312
621
  case "enable": return runEnable(cli, rest);
313
622
  case "disable": return runDisable(cli, rest);
314
623
  case "purge": return runPurge(cli, rest);
624
+ case "checkpoint": return runCheckpoint(cli, rest);
625
+ case "push": return runPush(cli, rest);
626
+ case "fetch": return runFetch(cli, rest);
627
+ case "remote": return runRemote(cli, rest);
315
628
  case "plan": return runPlan(cli, rest);
316
629
  case "export": return runExport(cli, rest);
317
630
  case "attach": return runAttach(cli, rest);
@@ -319,5 +632,5 @@ function runHistory(cli, args) {
319
632
  case "list": return runList(cli, rest);
320
633
  }
321
634
  }
322
- export const cmdHistory = { name: "history", summary: "Capture: enable/disable/purge per repository; retention: plan/export archive segments, attach/detach/list", run: (cli, a) => runHistory(cli, a) };
635
+ export const cmdHistory = { name: "history", summary: "Capture: enable/disable/purge per repository (native or git provider), checkpoint, remote set/status, push/fetch, resolve/reconcile; retention: plan/export archive segments, attach/detach/list", run: (cli, a) => runHistory(cli, a) };
323
636
  //# sourceMappingURL=history.js.map
@@ -1,5 +1,6 @@
1
1
  import { parseArgs } from "node:util";
2
- import { serveStdio, Watcher, WatchLease, watchLeaseLive, freshnessSweep, withWriterLock, awaitReady, WATCH_READY_PATIENCE_MS, EmbedDrainer, EngineError, resolveSettings, parseToolsProfile, TOOLS_PROFILES, } from "@omgbase/core";
2
+ import { serveStdio, Watcher, WatchLease, watchLeaseLive, freshnessSweep, withWriterLock, awaitReady, WATCH_READY_PATIENCE_MS, EmbedDrainer, EngineError, resolveSettings, parseToolsProfile, TOOLS_PROFILES, readCapability, historyProviderOf, NATIVE_PROVIDER, runProviderCheckpoint, } from "@omgbase/core";
3
+ import { createCheckpointScheduler, readGitConfig } from "@omgbase/git-adapter";
3
4
  import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
4
5
  import { installSpecSeams, WRITER_LOCK_TIMEOUT_ENV } from "../seams.js";
5
6
  import { loadEmbedding } from "./_embed.js";
@@ -124,6 +125,26 @@ async function runMcp(cli, args) {
124
125
  onError: (err) => cli.io.err(cli.style.dim(`[mcp] embed drain failed: ${String(err)}`)),
125
126
  })
126
127
  : null;
128
+ // The git history provider's cadence (spec/store §5.12 "Cadence", 17.6 —
129
+ // Phase 6b): the mutation hook below also runs the checkpoint worker — under
130
+ // `every` synchronously after each successful write (the git history is
131
+ // complete without `omg sync`), under `{ debounce_ms }` once, `debounce_ms`
132
+ // after the last mutation (a burst folds into one commit). The worker runs
133
+ // under the workspace writer lock (re-entrant when the hook fires inside a
134
+ // locked tool call); the watcher's checkpoints notify it too.
135
+ const historyRow = readCapability(ws.store.db, repo.repoId, "history");
136
+ const gitHistory = historyRow !== null && historyRow.state === "enabled" && historyProviderOf(historyRow) !== NATIVE_PROVIDER;
137
+ const scheduler = gitHistory
138
+ ? createCheckpointScheduler({
139
+ cadence: readGitConfig({ ...historyRow.config, provider: historyProviderOf(historyRow) }).cadence,
140
+ run: () => { withWriterLock(ws.omgbaseDir, () => runProviderCheckpoint(ws.store, repo.repoId)); },
141
+ onError: (err) => cli.io.err(cli.style.dim(`[mcp] git checkpoint failed: ${String(err)}`)),
142
+ })
143
+ : null;
144
+ if (scheduler) {
145
+ const cadence = readGitConfig({ ...historyRow.config, provider: historyProviderOf(historyRow) }).cadence;
146
+ cli.io.err(cli.style.dim(`[mcp] git checkpoint on mutation: ${cadence === "every" ? "every write" : `${cadence.debounce_ms} ms after the last write`}`));
147
+ }
127
148
  // Decide whether to run the in-process watcher. Off if --no-watch, or if a
128
149
  // live watcher already holds the lease (another `omg watch`/`omg mcp`).
129
150
  const leaseHeld = watchLeaseLive(ws.omgbaseDir);
@@ -148,8 +169,9 @@ async function runMcp(cli, args) {
148
169
  onCheckpoint: (r) => {
149
170
  if (r.ingested.length > 0 || r.deleted.length > 0) {
150
171
  cli.io.err(cli.style.dim(`[watch] batch: +${r.ingested.length} -${r.deleted.length}`));
151
- // A file change the watcher ingested may also need (re-)embedding.
172
+ // A file change the watcher ingested may also need (re-)embedding — and a git checkpoint.
152
173
  drainer?.schedule();
174
+ scheduler?.notify();
153
175
  }
154
176
  },
155
177
  onError: (err) => cli.io.err(cli.style.dim(`[watch] error: ${String(err)}`)),
@@ -203,7 +225,7 @@ async function runMcp(cli, args) {
203
225
  repoId: repo.repoId,
204
226
  omgbaseDir: ws.omgbaseDir,
205
227
  ...(repo.rootPath ? { rootPath: repo.rootPath } : {}),
206
- ...(drainer ? { onMutation: () => drainer.schedule() } : {}),
228
+ ...(drainer || scheduler ? { onMutation: () => { drainer?.schedule(); scheduler?.notify(); } } : {}),
207
229
  ...(embedQuery ? { embedQuery } : {}),
208
230
  ...(loaded ? { embedProvider: loaded.providerName } : {}),
209
231
  ...(semanticUnavailable ? { semanticUnavailable } : {}),
@@ -230,6 +252,11 @@ async function runMcp(cli, args) {
230
252
  catch { /* reported via onError */ }
231
253
  await drainer.close();
232
254
  }
255
+ // A debounced git checkpoint still pending runs now (its error is reported, never thrown).
256
+ if (scheduler) {
257
+ scheduler.flush();
258
+ scheduler.close();
259
+ }
233
260
  if (embedding)
234
261
  await embedding.close();
235
262
  await handle.close();
@@ -1,5 +1,5 @@
1
1
  import { parseArgs } from "node:util";
2
- import { resolveRef, nodesGet, docsRead, docLinks, historyReader, docPropertiesMerged } from "@omgbase/core";
2
+ import { resolveRef, nodesGet, docsRead, docLinks, historyReaderIfAvailable, docPropertiesMerged } from "@omgbase/core";
3
3
  import { CliUsageError, EngineErrorLike, EXIT_OK, emitMachine, renderHelp } from "../output.js";
4
4
  import { expandBlockArgs } from "./_mutate.js";
5
5
  function showOne(cli, ref, include) {
@@ -27,8 +27,9 @@ function showOne(cli, ref, include) {
27
27
  const node = nodesGet(ws.store, resolved.docId, resolved.blockId, { resolution: "full" });
28
28
  if (!node)
29
29
  throw new EngineErrorLike("block_missing", `no block ${ref}`);
30
- // `--include history` on a store with nothing captured (Phase 4a): the card simply carries no history.
31
- const history = include.includes("history") ? historyReader(ws.store.db)?.blocksHistory(resolved.blockId, { limit: 5 }) ?? [] : undefined;
30
+ // `--include history` on a store with nothing captured (Phase 4a): the card simply carries no history; on a
31
+ // git-provider repository (Phase 6a) there is no block lineage to show — the same empty list.
32
+ const history = include.includes("history") ? blockHistoryOrNone(ws.store.db, repo.repoId, resolved.blockId) : undefined;
32
33
  return { payload: { ...node, ...(history ? { history } : {}) }, render: () => renderBlockCard(cli, node, history) };
33
34
  }
34
35
  function runShow(cli, args) {
@@ -117,5 +118,14 @@ function renderBlockCard(cli, node, history) {
117
118
  function fmt(v) {
118
119
  return typeof v === "string" ? v : JSON.stringify(v);
119
120
  }
121
+ /** A block's last five changes through the repository's reader, or `[]` when the reader has none or does not record block lineage. */
122
+ function blockHistoryOrNone(db, repoId, blockId) {
123
+ try {
124
+ return historyReaderIfAvailable(db, repoId)?.blocksHistory(blockId, { limit: 5 }) ?? [];
125
+ }
126
+ catch {
127
+ return [];
128
+ }
129
+ }
120
130
  export const cmdShow = { name: "show", summary: "Metadata card for a node", run: (cli, a) => runShow(cli, a) };
121
131
  //# sourceMappingURL=show.js.map
@@ -95,6 +95,13 @@ function runOneShot(cli, ws, repo) {
95
95
  for (const p of result.conflicted)
96
96
  io.out(` ${style.warn(p)}`);
97
97
  }
98
+ // spec/sync §4.3 step 5 (4.4): the legacy documents whose separator bytes
99
+ // were repaired from their files (spec/store §5.11) — no commit, no revision.
100
+ if (result.repaired.length > 0) {
101
+ io.out(` ${style.ok(g.ok)} repaired ${result.repaired.length}`);
102
+ for (const p of result.repaired)
103
+ io.out(` ${style.accent(p)}`);
104
+ }
98
105
  if (!result.changed)
99
106
  io.out(` ${style.dim("already up to date")}`);
100
107
  return EXIT_OK;
package/dist/src/seams.js CHANGED
@@ -1,4 +1,5 @@
1
- import { pinClock, sequentialMinter, setIdMinter, setWriterLockDefaults } from "@omgbase/core";
1
+ import { pinClock, registerHistoryProvider, sequentialMinter, setIdMinter, setWriterLockDefaults } from "@omgbase/core";
2
+ import { DEFAULT_IDENTITY, FIXTURE_IDENTITY, gitHistoryProvider } from "@omgbase/git-adapter";
2
3
  import { CliUsageError } from "./output.js";
3
4
  // The two conformance seams of spec/surface §7.1 (and spec/cli §2.6), read
4
5
  // from the environment at process start — BEFORE any workspace opens, so no id
@@ -63,6 +64,12 @@ export function installSpecSeams(env = process.env) {
63
64
  setWriterLockDefaults({ timeoutMs: ms });
64
65
  seams.writerLockTimeoutMs = ms;
65
66
  }
67
+ // The git history provider (modular-core Phase 6a; spec/store §5.12): the
68
+ // composition layer attaches it here, before any store opens — this binary
69
+ // is the host. Under either conformance seam its commits carry the fixture
70
+ // identity `omgbase <omgbase@fixture>` (spec/store §9.4), so the shas the
71
+ // fixtures pin follow from the inputs alone.
72
+ registerHistoryProvider("git", gitHistoryProvider({ identity: seams.minter || seams.clock !== null ? FIXTURE_IDENTITY : DEFAULT_IDENTITY }));
66
73
  installed = seams;
67
74
  return seams;
68
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omgbase",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "description": "Open Markdown Graph Base — CLI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,9 +45,10 @@
45
45
  "omg": "./dist/src/main.js"
46
46
  },
47
47
  "dependencies": {
48
- "@omgbase/core": "^0.31.0",
48
+ "@omgbase/core": "^0.33.0",
49
49
  "@omgbase/fs-adapter": "^0.2.0",
50
- "@omgbase/sync": "^0.7.1",
50
+ "@omgbase/git-adapter": "^0.1.0",
51
+ "@omgbase/sync": "^0.7.3",
51
52
  "yaml": "^2.9.0"
52
53
  },
53
54
  "devDependencies": {