omgbase 0.11.0 → 0.13.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": "0145153",
3
- "built": "2026-10-12T05:05:42.833Z"
2
+ "commit": "48b355b",
3
+ "built": "2026-10-12T21:39:26.285Z"
4
4
  }
@@ -1,4 +1,4 @@
1
- import { type Op } from "@omgbase/core";
1
+ import { type EffectTarget, type Plan, type Receipt, type Sugar } from "@omgbase/core";
2
2
  import type { Cli } from "../context.js";
3
3
  import type { RepoRow } from "@omgbase/core";
4
4
  import type { Workspace } from "@omgbase/core";
@@ -25,30 +25,65 @@ export declare function readStdin(): string;
25
25
  export declare function readIdsFromStdin(): string[];
26
26
  /** Expand a block-list argument list, substituting stdin ids for a bare "-". */
27
27
  export declare function expandBlockArgs(args: string[]): string[];
28
- export interface RunOpsOptions {
28
+ export declare function defaultActor(): string;
29
+ export interface RunOpts {
29
30
  reason?: string;
30
31
  actor?: string;
31
32
  }
32
33
  /**
33
- * Apply a set of kernel ops as one changeset and render the outcome. Honors
34
- * cli.flags.dryRun (per-file unified diffs to stdout, nothing committed) and
35
- * cli.flags.mode (--json emits ApplyResult verbatim). Typed conflicts are
36
- * rendered by the caller's catch via the output contract; we let apply throw.
34
+ * Run a builder's effects as one request (`applySugar`: plan + execute under the
35
+ * writer flock, the sugar's reason unless `--reason` overrides it) and render the
36
+ * outcome. Honors cli.flags.dryRun — the Plan: per-file unified diffs to stdout,
37
+ * nothing minted, nothing committed (§3.6) — and cli.flags.mode (--json emits
38
+ * the Receipt, or the Plan on a dry run, verbatim). An empty effect list is
39
+ * ` nothing to do`.
37
40
  */
38
- export declare function runOps(cli: Cli, ws: Workspace, repo: RepoRow, ops: Op[], opts?: RunOpsOptions): number;
41
+ export declare function runSugar(cli: Cli, ws: Workspace, repo: RepoRow, sugar: Sugar, opts?: RunOpts): number;
39
42
  /**
40
- * Remote peer of runOps: call the changeset-producing tool that mirrors this
41
- * command (blocks_insert, blocks_check, …) against the `--server` engine — the
42
- * server resolves refs + pins CAS + applies — then render the returned
43
- * ApplyResult with the SAME renderers as local. --dry-run is threaded as the
44
- * tool's dry_run so the preview comes back without committing.
43
+ * Remote peer of runSugar: call the sugar tool that mirrors this command
44
+ * (blocks_insert, blocks_check, …) against the `--server` engine — the server
45
+ * resolves refs + pins CAS + applies — and render the returned Receipt with the
46
+ * SAME renderers as local. The global --dry-run is the `plan` tool over the
47
+ * effects the verb would build (`effects` composes them from the refs as given:
48
+ * an id names its row, anything else a document path — the server resolves);
49
+ * no tool takes `dry_run` since surface 4.0.
45
50
  */
46
- export declare function runOpsRemote(cli: Cli, tool: string, toolArgs: Record<string, unknown>): Promise<number>;
51
+ export declare function runSugarRemote(cli: Cli, tool: string, toolArgs: Record<string, unknown>, effects: () => unknown[] | Promise<unknown[]>): Promise<number>;
52
+ /** A ref as an effect target for a remote `plan`: a `b_` id is the block, a `d_` id the document, anything else a document path. */
53
+ export declare function targetOf(ref: string): EffectTarget;
54
+ /**
55
+ * `apply` given `{ effects }`: plan + execute as one transaction
56
+ * (`applyEffects`, spec/mutate §11.7) under the writer flock, rendered as a
57
+ * Receipt. `--dry-run` plans only and renders the Plan (§3.6: planning runs
58
+ * under the symbolic minter and the ids it reports are `$new.n`). An empty
59
+ * list is `nothing to do`.
60
+ */
61
+ export declare function runEffects(cli: Cli, ws: Workspace, repo: RepoRow, effects: unknown[], opts?: RunOpts): number;
62
+ /** Remote peer of `runEffects`: the `apply` tool with `effects` (the Receipt), or on a dry run the `plan` tool (the Plan). */
63
+ export declare function runEffectsRemote(cli: Cli, effects: unknown[], reason?: string): Promise<number>;
64
+ /** `execute`: commit exactly a Plan (the plan form, spec/mutate §11.7 — admission, then the one transaction), rendered as a Receipt. */
65
+ export declare function runExecutePlan(cli: Cli, ws: Workspace, repo: RepoRow, plan: unknown, opts?: RunOpts): number;
66
+ /** Remote peer of `runExecutePlan`: the `execute` tool's plan form. */
67
+ export declare function runExecutePlanRemote(cli: Cli, plan: unknown, reason?: string): Promise<number>;
68
+ /** A Plan, dry-run style: `--json` the Plan verbatim (what `execute` takes back); human the §3.6 note and the per-file diffs of its preview. */
69
+ export declare function renderPlan(cli: Cli, plan: Plan): number;
70
+ /**
71
+ * A Receipt (any write tool's result since 4.0 — a `links_*` result carries its
72
+ * `hits`/`pairs` beside the receipt's keys): `--json` verbatim; human the
73
+ * commit confirmation of §4 (one revision per touched document) and, on
74
+ * stdout, the ids the write names — the plan's `preview.results[].ids` with
75
+ * every `$new.n` realized from `new_ids` when the plan is at hand with its
76
+ * preview (a local write, any `execute`); else (a plan echoed without
77
+ * `preview`, a remote write) every id the plan minted, the `new_ids` values in
78
+ * order — which includes the discarded mints of a re-parse (`spec/mutate` §10),
79
+ * ids that name no row.
80
+ */
81
+ export declare function renderReceipt(cli: Cli, receipt: Receipt, plan?: unknown): number;
82
+ export declare function receiptIds(receipt: Receipt, plan: unknown): string[];
47
83
  /**
48
84
  * The dry-run rendering every write verb shares (spec/cli §3.6): the note on
49
- * stderr, then per changed file the path, the unified diff of before → after
50
- * (`spec/surface` §3) and an empty line. Document verbs hand their
51
- * `DocOpResult.diffs` here; block verbs their `ApplyResult.diffs`.
85
+ * stderr, then per changed file of the Plan's `preview.diffs` the path, the
86
+ * unified diff of before → after (`spec/surface` §3) and an empty line.
52
87
  */
53
88
  export declare function renderDiffs(cli: Cli, diffs: Record<string, {
54
89
  before: string;
@@ -1,13 +1,17 @@
1
1
  import { readFileSync, readSync } from "node:fs";
2
2
  import { userInfo } from "node:os";
3
- import { apply, unifiedDiff, surfaceApplyResult } from "@omgbase/core";
3
+ import { applyEffectsDetailed, applySugar, executePlan, isValidId, substituteSymbols, unifiedDiff, surfacePlan, surfaceReceipt } from "@omgbase/core";
4
4
  import { CliUsageError, EXIT_OK, EXIT_ERROR } from "../output.js";
5
5
  import { remoteCall } from "./_remote.js";
6
- // Shared plumbing for every write command (11 §5.6, §5.7). Sugar commands build
7
- // their kernel Ops, then hand them here: this sets origin.actor (human:$USER by
8
- // default, override --actor), runs under the writer flock (omgbaseDir), threads
9
- // --dry-run (validate + render diffs, commit nothing), and renders results or
10
- // typed conflicts uniformly.
6
+ // Shared plumbing for every write command (spec/cli §3.6, §4; ADR-021's
7
+ // retirement release, cli 4.0). The sugar verbs build their effects with the
8
+ // shared builders of `@omgbase/core` (`mutate/sugar.ts` — the same table the
9
+ // MCP tools use), then hand them here: this sets origin.actor (human:$USER by
10
+ // default, override --actor), runs under the writer flock (omgbaseDir),
11
+ // threads the global --dry-run as a `plan` (render the preview's diffs, mint
12
+ // nothing, commit nothing), and renders the Receipt or the Plan uniformly.
13
+ // Typed conflicts are rendered by the caller's catch via the output contract;
14
+ // we let the planner throw.
11
15
  /** Read markdown content from -m, -f (file, or "-" = stdin), or bare stdin. */
12
16
  export function readContent(values, allowStdin = true) {
13
17
  if (values.m !== undefined)
@@ -108,7 +112,7 @@ export function expandBlockArgs(args) {
108
112
  }
109
113
  return out;
110
114
  }
111
- function defaultActor() {
115
+ export function defaultActor() {
112
116
  try {
113
117
  return `human:${userInfo().username}`;
114
118
  }
@@ -117,61 +121,149 @@ function defaultActor() {
117
121
  }
118
122
  }
119
123
  /**
120
- * Apply a set of kernel ops as one changeset and render the outcome. Honors
121
- * cli.flags.dryRun (per-file unified diffs to stdout, nothing committed) and
122
- * cli.flags.mode (--json emits ApplyResult verbatim). Typed conflicts are
123
- * rendered by the caller's catch via the output contract; we let apply throw.
124
+ * Run a builder's effects as one request (`applySugar`: plan + execute under the
125
+ * writer flock, the sugar's reason unless `--reason` overrides it) and render the
126
+ * outcome. Honors cli.flags.dryRun — the Plan: per-file unified diffs to stdout,
127
+ * nothing minted, nothing committed (§3.6) — and cli.flags.mode (--json emits
128
+ * the Receipt, or the Plan on a dry run, verbatim). An empty effect list is
129
+ * ` nothing to do`.
124
130
  */
125
- export function runOps(cli, ws, repo, ops, opts = {}) {
126
- if (ops.length === 0) {
131
+ export function runSugar(cli, ws, repo, sugar, opts = {}) {
132
+ if (sugar.effects.length === 0) {
127
133
  cli.io.err(cli.style.dim(" nothing to do"));
128
134
  return EXIT_OK;
129
135
  }
130
- const req = {
136
+ const { plan, receipt } = applySugar(ws.store, {
131
137
  repoId: repo.repoId,
132
138
  ...(repo.rootPath ? { rootPath: repo.rootPath } : {}),
133
- ops,
134
- origin: { actor: opts.actor ?? defaultActor(), ...(opts.reason ? { reason: opts.reason } : {}) },
135
139
  omgbaseDir: ws.omgbaseDir,
140
+ actor: opts.actor ?? defaultActor(),
141
+ ...(opts.reason ? { reason: opts.reason } : {}),
136
142
  ...(cli.flags.dryRun ? { dryRun: true } : {}),
137
- };
138
- // The reference form on every printed path (spec/surface §1 "Paths"): the
139
- // kernel's result is re-shaped exactly as the MCP server re-shapes it.
140
- const result = surfaceApplyResult(apply(ws.store, req));
141
- return renderApply(cli, result);
143
+ }, sugar);
144
+ if (!receipt)
145
+ return renderPlan(cli, surfacePlan(plan));
146
+ return renderReceipt(cli, surfaceReceipt(receipt), plan);
147
+ }
148
+ /**
149
+ * Remote peer of runSugar: call the sugar tool that mirrors this command
150
+ * (blocks_insert, blocks_check, …) against the `--server` engine — the server
151
+ * resolves refs + pins CAS + applies — and render the returned Receipt with the
152
+ * SAME renderers as local. The global --dry-run is the `plan` tool over the
153
+ * effects the verb would build (`effects` composes them from the refs as given:
154
+ * an id names its row, anything else a document path — the server resolves);
155
+ * no tool takes `dry_run` since surface 4.0.
156
+ */
157
+ export async function runSugarRemote(cli, tool, toolArgs, effects) {
158
+ if (cli.flags.dryRun)
159
+ return renderPlan(cli, await remoteCall(cli, "plan", { effects: await effects() }));
160
+ return renderReceipt(cli, await remoteCall(cli, tool, toolArgs));
161
+ }
162
+ /** A ref as an effect target for a remote `plan`: a `b_` id is the block, a `d_` id the document, anything else a document path. */
163
+ export function targetOf(ref) {
164
+ if (isValidId(ref, "b"))
165
+ return { kind: "block", id: ref };
166
+ if (isValidId(ref, "d"))
167
+ return { kind: "document", id: ref };
168
+ return { kind: "document", path: ref };
142
169
  }
170
+ // ---- the Effect layer (ADR-021; spec/cli 3.2 `apply -f { effects }`, `execute`) ----
143
171
  /**
144
- * Remote peer of runOps: call the changeset-producing tool that mirrors this
145
- * command (blocks_insert, blocks_check, …) against the `--server` engine — the
146
- * server resolves refs + pins CAS + applies — then render the returned
147
- * ApplyResult with the SAME renderers as local. --dry-run is threaded as the
148
- * tool's dry_run so the preview comes back without committing.
172
+ * `apply` given `{ effects }`: plan + execute as one transaction
173
+ * (`applyEffects`, spec/mutate §11.7) under the writer flock, rendered as a
174
+ * Receipt. `--dry-run` plans only and renders the Plan (§3.6: planning runs
175
+ * under the symbolic minter and the ids it reports are `$new.n`). An empty
176
+ * list is `nothing to do`.
149
177
  */
150
- export async function runOpsRemote(cli, tool, toolArgs) {
151
- const result = await remoteCall(cli, tool, {
152
- ...toolArgs,
153
- ...(cli.flags.dryRun ? { dry_run: true } : {}),
178
+ export function runEffects(cli, ws, repo, effects, opts = {}) {
179
+ if (effects.length === 0) {
180
+ cli.io.err(cli.style.dim(" nothing to do"));
181
+ return EXIT_OK;
182
+ }
183
+ const req = {
184
+ repoId: repo.repoId,
185
+ ...(repo.rootPath ? { rootPath: repo.rootPath } : {}),
186
+ effects,
187
+ origin: { actor: opts.actor ?? defaultActor(), ...(opts.reason ? { reason: opts.reason } : {}) },
188
+ omgbaseDir: ws.omgbaseDir,
189
+ };
190
+ const { plan, receipt } = applyEffectsDetailed(ws.store, { ...req, ...(cli.flags.dryRun ? { dryRun: true } : {}) });
191
+ if (!receipt)
192
+ return renderPlan(cli, surfacePlan(plan));
193
+ return renderReceipt(cli, surfaceReceipt(receipt), plan);
194
+ }
195
+ /** Remote peer of `runEffects`: the `apply` tool with `effects` (the Receipt), or on a dry run the `plan` tool (the Plan). */
196
+ export async function runEffectsRemote(cli, effects, reason) {
197
+ if (cli.flags.dryRun)
198
+ return renderPlan(cli, await remoteCall(cli, "plan", { effects }));
199
+ return renderReceipt(cli, await remoteCall(cli, "apply", { effects, ...(reason ? { reason } : {}) }));
200
+ }
201
+ /** `execute`: commit exactly a Plan (the plan form, spec/mutate §11.7 — admission, then the one transaction), rendered as a Receipt. */
202
+ export function runExecutePlan(cli, ws, repo, plan, opts = {}) {
203
+ const receipt = executePlan(ws.store, {
204
+ repoId: repo.repoId,
205
+ ...(repo.rootPath ? { rootPath: repo.rootPath } : {}),
206
+ plan,
207
+ origin: { actor: opts.actor ?? defaultActor(), ...(opts.reason ? { reason: opts.reason } : {}) },
208
+ omgbaseDir: ws.omgbaseDir,
154
209
  });
155
- return renderApply(cli, result);
210
+ return renderReceipt(cli, surfaceReceipt(receipt), plan);
156
211
  }
157
- /** Capture + render an ApplyResult (shared by the local and remote paths). */
158
- function renderApply(cli, result) {
159
- cli.capture?.(result); // shell: ApplyResult — its minted/affected ids are the frame
212
+ /** Remote peer of `runExecutePlan`: the `execute` tool's plan form. */
213
+ export async function runExecutePlanRemote(cli, plan, reason) {
214
+ return renderReceipt(cli, await remoteCall(cli, "execute", { plan, ...(reason ? { reason } : {}) }), plan);
215
+ }
216
+ /** A Plan, dry-run style: `--json` the Plan verbatim (what `execute` takes back); human the §3.6 note and the per-file diffs of its preview. */
217
+ export function renderPlan(cli, plan) {
218
+ cli.capture?.(plan);
160
219
  if (cli.flags.mode !== "human") {
161
- cli.io.out(JSON.stringify(result));
220
+ cli.io.out(JSON.stringify(plan));
162
221
  return EXIT_OK;
163
222
  }
164
- return cli.flags.dryRun ? renderDryRun(cli, result) : renderCommitted(cli, result);
223
+ renderDiffs(cli, plan.preview?.diffs ?? {});
224
+ return EXIT_OK;
165
225
  }
166
- function renderDryRun(cli, result) {
167
- renderDiffs(cli, result.diffs ?? {});
226
+ /**
227
+ * A Receipt (any write tool's result since 4.0 — a `links_*` result carries its
228
+ * `hits`/`pairs` beside the receipt's keys): `--json` verbatim; human the
229
+ * commit confirmation of §4 (one revision per touched document) and, on
230
+ * stdout, the ids the write names — the plan's `preview.results[].ids` with
231
+ * every `$new.n` realized from `new_ids` when the plan is at hand with its
232
+ * preview (a local write, any `execute`); else (a plan echoed without
233
+ * `preview`, a remote write) every id the plan minted, the `new_ids` values in
234
+ * order — which includes the discarded mints of a re-parse (`spec/mutate` §10),
235
+ * ids that name no row.
236
+ */
237
+ export function renderReceipt(cli, receipt, plan) {
238
+ const ids = receiptIds(receipt, plan);
239
+ cli.capture?.({ ...receipt, ids }); // shell: the ids the write names are the frame
240
+ if (cli.flags.mode !== "human") {
241
+ cli.io.out(JSON.stringify(receipt));
242
+ return EXIT_OK;
243
+ }
244
+ const { render, style, io } = cli;
245
+ const n = receipt.revisions.length;
246
+ io.err(style.dim(` ${style.ok(render.g.ok)} committed · ${n} document${n === 1 ? "" : "s"} touched`));
247
+ for (const id of ids)
248
+ io.out(id);
168
249
  return EXIT_OK;
169
250
  }
251
+ export function receiptIds(receipt, plan) {
252
+ const results = plan?.preview?.results;
253
+ if (!Array.isArray(results))
254
+ return Object.values(receipt.new_ids);
255
+ const ids = [];
256
+ for (const r of results)
257
+ if (Array.isArray(r.ids))
258
+ for (const id of r.ids)
259
+ if (typeof id === "string")
260
+ ids.push(id);
261
+ return substituteSymbols(ids, receipt.new_ids);
262
+ }
170
263
  /**
171
264
  * The dry-run rendering every write verb shares (spec/cli §3.6): the note on
172
- * stderr, then per changed file the path, the unified diff of before → after
173
- * (`spec/surface` §3) and an empty line. Document verbs hand their
174
- * `DocOpResult.diffs` here; block verbs their `ApplyResult.diffs`.
265
+ * stderr, then per changed file of the Plan's `preview.diffs` the path, the
266
+ * unified diff of before → after (`spec/surface` §3) and an empty line.
175
267
  */
176
268
  export function renderDiffs(cli, diffs) {
177
269
  const { style, io } = cli;
@@ -194,16 +286,5 @@ export function unifiedDiffLines(cli, before, after) {
194
286
  return style.dim(line);
195
287
  });
196
288
  }
197
- function renderCommitted(cli, result) {
198
- const { render, style, io } = cli;
199
- const g = render.g;
200
- const n = result.revisions.length;
201
- io.err(style.dim(` ${style.ok(g.ok)} committed · ${n} document${n === 1 ? "" : "s"} touched`));
202
- // stdout stays useful for pipes: the ids minted/affected, one per line.
203
- const ids = result.results.flatMap((r) => r.ids);
204
- for (const id of ids)
205
- io.out(id);
206
- return EXIT_OK;
207
- }
208
289
  export { EXIT_OK, EXIT_ERROR };
209
290
  //# sourceMappingURL=_mutate.js.map
Binary file
@@ -3,13 +3,14 @@ import { spawnSync } from "node:child_process";
3
3
  import { writeFileSync, readFileSync, mkdtempSync, rmSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
- import { resolveRef, nodesGet } from "@omgbase/core";
6
+ import { resolveRef, nodesGet, updateBlockEffects } from "@omgbase/core";
7
7
  import { CliUsageError, EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
8
- import { runOps } from "./_mutate.js";
8
+ import { runSugar } from "./_mutate.js";
9
9
  // `omg edit <block>` (11 §5.6, §5.7): read the block's raw markdown → open it in
10
- // $EDITOR → update with the PRE-READ hash pinned as CAS. A mid-edit change by
11
- // someone else lands as a clean stale_expectation (with current truth), never a
12
- // lost write. The human structural-edit loop.
10
+ // $EDITOR → `set markdown` with the PRE-READ hash pinned as `expect.content_hash`
11
+ // (the `blocks_update` builder; cli 4.0). A mid-edit change by someone else
12
+ // lands as a clean stale_expectation (with current truth), never a lost write.
13
+ // The human structural-edit loop.
13
14
  function rawHashOf(ws, blockId) {
14
15
  const row = ws.store.db
15
16
  .prepare("SELECT lower(hex(raw_hash)) h FROM blocks WHERE block_id = ? AND deleted_commit IS NULL")
@@ -63,8 +64,7 @@ function runEdit(cli, args) {
63
64
  cli.io.err(cli.style.dim(" no changes"));
64
65
  return EXIT_OK;
65
66
  }
66
- const op = { op: "update", block: blockId, markdown: after, ...(preHash ? { expect: { content_hash: preHash } } : {}) };
67
- return runOps(cli, ws, repo, [op], values.actor ? { actor: values.actor } : {});
67
+ return runSugar(cli, ws, repo, updateBlockEffects(blockId, { markdown: after, ...(preHash ? { expect: { content_hash: preHash } } : {}) }), values.actor ? { actor: values.actor } : {});
68
68
  }
69
69
  finally {
70
70
  rmSync(dir, { recursive: true, force: true });
@@ -7,7 +7,7 @@ const GROUPS = [
7
7
  { title: "orient & read", names: ["status", "ls", "outline", "cat", "show", "find"] },
8
8
  { title: "query", names: ["query", "run"] },
9
9
  { title: "history & links", names: ["log", "hist", "diff", "links"] },
10
- { title: "mutate", names: ["apply", "insert", "update", "edit", "move", "rm", "done", "append", "retarget", "split", "merge", "node"] },
10
+ { title: "mutate", names: ["apply", "execute", "insert", "update", "edit", "move", "rm", "done", "append", "retarget", "split", "merge", "node"] },
11
11
  { title: "documents", names: ["new", "mv", "meta"] },
12
12
  { title: "session", names: ["shell"] },
13
13
  { title: "sync & serve", names: ["sync", "mcp"] },
@@ -1,7 +1,7 @@
1
1
  import { parseArgs } from "node:util";
2
- import { serveStdio, Watcher, WatchLease, watchLeaseLive, freshnessSweep, awaitReady, WATCH_READY_PATIENCE_MS, EmbedDrainer, EngineError } from "@omgbase/core";
2
+ import { serveStdio, Watcher, WatchLease, watchLeaseLive, freshnessSweep, withWriterLock, awaitReady, WATCH_READY_PATIENCE_MS, EmbedDrainer, EngineError } from "@omgbase/core";
3
3
  import { EngineErrorLike, EXIT_OK, renderHelp } from "../output.js";
4
- import { installSpecSeams } from "../seams.js";
4
+ import { installSpecSeams, WRITER_LOCK_TIMEOUT_ENV } from "../seams.js";
5
5
  import { loadEmbedding } from "./_embed.js";
6
6
  import { openRepoSource } from "./_source.js";
7
7
  import { hostInfo } from "../build.js";
@@ -26,6 +26,9 @@ function announceSpecSeams(cli) {
26
26
  if (seams.clock !== null) {
27
27
  cli.io.err(cli.style.dim(`[mcp] spec seam: clock pinned to ${seams.clock} (OMGBASE_SPEC_CLOCK) — conformance run, not for production`));
28
28
  }
29
+ if (seams.writerLockTimeoutMs !== null) {
30
+ cli.io.err(cli.style.dim(`[mcp] test seam: writer lock waits ${seams.writerLockTimeoutMs} ms (${WRITER_LOCK_TIMEOUT_ENV}) — not for production`));
31
+ }
29
32
  }
30
33
  async function runMcp(cli, args) {
31
34
  const { values } = parseArgs({
@@ -115,7 +118,11 @@ async function runMcp(cli, args) {
115
118
  // is primed is caught by the sweep, one after it by the feed.
116
119
  source = await openRepoSource(ws.store, repo);
117
120
  if (source) {
121
+ // Every checkpoint and the priming sweep run under the workspace writer
122
+ // lock (spec/sync §7), as the Rust watcher's do: a verb in another
123
+ // process waits for the ingest instead of racing its file-CAS.
118
124
  watcher = new Watcher(ws.store, repo.repoId, source, {
125
+ omgbaseDir: ws.omgbaseDir,
119
126
  onCheckpoint: (r) => {
120
127
  if (r.ingested.length > 0 || r.deleted.length > 0) {
121
128
  cli.io.err(cli.style.dim(`[watch] checkpoint: +${r.ingested.length} -${r.deleted.length}`));
@@ -129,8 +136,9 @@ async function runMcp(cli, args) {
129
136
  if (!(await awaitReady(watcher.ready(), WATCH_READY_PATIENCE_MS))) {
130
137
  cli.io.err(cli.style.dim(`[watch] the fs adapter did not report ready within ${WATCH_READY_PATIENCE_MS / 1000}s — proceeding as if ready`));
131
138
  }
132
- if (repo.rootPath)
133
- freshnessSweep(ws.store, repo.repoId, repo.rootPath); // prime: anything that landed before the feed was ready
139
+ const root = repo.rootPath;
140
+ if (root)
141
+ withWriterLock(ws.omgbaseDir, () => freshnessSweep(ws.store, repo.repoId, root)); // prime: anything that landed before the feed was ready
134
142
  cli.io.err(cli.style.dim(`[mcp] serving ${repo.slug} on stdio · watcher live`));
135
143
  }
136
144
  else {
@@ -161,9 +169,13 @@ async function runMcp(cli, args) {
161
169
  : failed
162
170
  ? (_text) => Promise.reject(failed)
163
171
  : undefined;
172
+ // The workspace dir makes every write tool take the writer lock for its whole
173
+ // call (spec/sync §7) — `omg` verbs, the Rust `omgbase mcp` and the watcher
174
+ // all lock the same file, so the server must too.
164
175
  const handle = await serveStdio({
165
176
  store: ws.store,
166
177
  repoId: repo.repoId,
178
+ omgbaseDir: ws.omgbaseDir,
167
179
  ...(repo.rootPath ? { rootPath: repo.rootPath } : {}),
168
180
  ...(drainer ? { onMutation: () => drainer.schedule() } : {}),
169
181
  ...(embedQuery ? { embedQuery } : {}),
@@ -1,5 +1,6 @@
1
1
  import type { Command } from "../commands.js";
2
2
  export declare const cmdApply: Command;
3
+ export declare const cmdExecute: Command;
3
4
  export declare const cmdInsert: Command;
4
5
  export declare const cmdUpdate: Command;
5
6
  export declare const cmdMove: Command;