weir-harness 1.2.2 → 2.0.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.
Files changed (89) hide show
  1. package/cordis.patch.yml +7 -18
  2. package/lib/client.file-lease-badge.js +307 -0
  3. package/lib/client.js +20 -30
  4. package/lib/client.settings-page.js +10 -34
  5. package/package.json +2 -2
  6. package/src/capabilities/store/checked-contract.d.ts +2 -2
  7. package/src/capabilities/store/fs-adapter.js +2 -3
  8. package/src/context-guard/index.js +2 -2
  9. package/src/delegate/supervision-mount.js +5 -5
  10. package/src/delegate/tool.js +2 -10
  11. package/src/file-lease/client.js +1066 -0
  12. package/src/file-lease/index.js +30 -0
  13. package/src/file-lease/lineage.js +207 -0
  14. package/src/file-lease/node-contracts.d.ts +42 -0
  15. package/src/file-lease/plugin.js +789 -0
  16. package/src/file-lease/singleton.js +155 -0
  17. package/src/file-lease/spawn.js +62 -0
  18. package/src/file-lease/tools.js +459 -0
  19. package/src/file-lease/view.js +144 -0
  20. package/src/hashline-edit/index.js +76 -42
  21. package/src/hashline-edit/write-tool.js +90 -0
  22. package/src/intent-gate/matcher.js +27 -0
  23. package/src/lease-broker/entry.js +98 -0
  24. package/src/lease-broker/node-contracts.d.ts +63 -0
  25. package/src/lease-broker/protocol.js +236 -0
  26. package/src/lease-broker/server.js +399 -0
  27. package/src/lease-broker/state.js +425 -0
  28. package/src/lsp/admin.js +24 -13
  29. package/src/lsp/index.js +5 -2
  30. package/src/lsp/registry.js +83 -17
  31. package/src/lsp/tools.js +137 -27
  32. package/src/notify/commands.js +15 -2
  33. package/src/notify/index.js +48 -10
  34. package/src/notify/web-channel.js +26 -3
  35. package/src/process/index.js +11 -0
  36. package/src/settings/index.js +2 -44
  37. package/src/settings/sections.js +29 -28
  38. package/src/shared/audit.js +6 -1
  39. package/src/todo-driver/index.js +1 -6
  40. package/src/worktree/cards.js +6 -21
  41. package/src/worktree/lane-core.js +1 -19
  42. package/src/worktree/lane-workflows.js +11 -32
  43. package/lib/client.edit-lock-maintenance.js +0 -592
  44. package/lib/client.edit-lock-panel.js +0 -405
  45. package/src/edit-lock/adapter.js +0 -57
  46. package/src/edit-lock/admin-ledger.js +0 -149
  47. package/src/edit-lock/admin-recovery.js +0 -151
  48. package/src/edit-lock/admission.js +0 -82
  49. package/src/edit-lock/binding-attestation.js +0 -29
  50. package/src/edit-lock/binding-guidance.js +0 -72
  51. package/src/edit-lock/binding-machine.js +0 -312
  52. package/src/edit-lock/call-context.js +0 -65
  53. package/src/edit-lock/derived-facts.js +0 -67
  54. package/src/edit-lock/domain.js +0 -165
  55. package/src/edit-lock/domains.js +0 -170
  56. package/src/edit-lock/host.js +0 -149
  57. package/src/edit-lock/incarnations.js +0 -150
  58. package/src/edit-lock/index.js +0 -1303
  59. package/src/edit-lock/inspect.js +0 -219
  60. package/src/edit-lock/lifecycle.js +0 -579
  61. package/src/edit-lock/limits.js +0 -61
  62. package/src/edit-lock/maintenance.js +0 -310
  63. package/src/edit-lock/manager.js +0 -875
  64. package/src/edit-lock/negotiation.js +0 -132
  65. package/src/edit-lock/operation-history.js +0 -214
  66. package/src/edit-lock/peer-client.js +0 -86
  67. package/src/edit-lock/peer-transport.js +0 -76
  68. package/src/edit-lock/peer.js +0 -87
  69. package/src/edit-lock/publisher.js +0 -191
  70. package/src/edit-lock/read-authority.js +0 -101
  71. package/src/edit-lock/recovery.js +0 -139
  72. package/src/edit-lock/remote.js +0 -362
  73. package/src/edit-lock/request-binding.js +0 -42
  74. package/src/edit-lock/request-data.js +0 -40
  75. package/src/edit-lock/reservation.js +0 -301
  76. package/src/edit-lock/reserved-runtime.js +0 -45
  77. package/src/edit-lock/resource-identity.js +0 -130
  78. package/src/edit-lock/runtime.js +0 -145
  79. package/src/edit-lock/settle.js +0 -144
  80. package/src/edit-lock/snapshot.js +0 -175
  81. package/src/edit-lock/stale-sweep.js +0 -120
  82. package/src/edit-lock/state.js +0 -462
  83. package/src/edit-lock/store.js +0 -327
  84. package/src/edit-lock/tool-scope.js +0 -29
  85. package/src/edit-lock/view.js +0 -170
  86. package/src/edit-lock/write-scopes.js +0 -48
  87. package/src/edit-lock/write-tool.js +0 -45
  88. package/src/shared/delegate-edit-lifecycle.js +0 -36
  89. package/src/worktree/authority.js +0 -56
package/cordis.patch.yml CHANGED
@@ -40,12 +40,6 @@
40
40
  worktreeAutoSetup: true
41
41
  worktreeWatchTimeoutMinutes: 360
42
42
  worktreeAutoApprove: auto-clean
43
- editLockEnabled: true
44
- # Message-driven auto-resume (edit-lock-auto-resume-on-message): a genuine
45
- # user message resumes a stopped session and confirms its retained files.
46
- # Read live per message, so toggling applies without a restart.
47
- editLockAutoResume: true
48
- editLockStaleSweep: true
49
43
  robashEnabled: true
50
44
  # The five whitelist tables are NOT declared here on purpose. A patch
51
45
  # row's `config` is replaced WHOLE rather than deep-merged, so a default
@@ -185,10 +179,6 @@
185
179
  compaction: true
186
180
  toolResultPruner: true
187
181
  weirCompactionCoordinator: true
188
- # 编辑锁服务:提供者 edit-lock 与消费者 todo-driver / hashline-edit /
189
- # lsp 同在本组。预设服务必须 isolate,否则预设加载以
190
- # "Preset services require isolate realms" 失败。
191
- weirEditLock: true
192
182
  # Worktree lanes service: provider (worktree) and consumer
193
183
  # (delegate) share this realm (S24 / S26 S-A).
194
184
  weirWorktreeLanes: true
@@ -241,10 +231,10 @@
241
231
  # F4 todo 续推:空转续推 + 用户打断/供应商错误语义 + 逃生舱
242
232
  - id: todo-driver
243
233
  name: 'weir-harness/todo-driver'
244
- # 编辑锁(默认开):显式保存 false 后重启才关闭;须在 hashline-edit / lsp 前挂载。
245
- # 两个消费者挂载时捕获隔离组内的锁服务。
246
- - id: edit-lock
247
- name: 'weir-harness/edit-lock'
234
+ # 文件租约(编辑锁新实现):进程内共享一个租约 client,发布五个所有权
235
+ # 工具;不发布 cordis 服务,因此不需要 isolate 组,也不再约束行序。
236
+ - id: file-lease
237
+ name: 'weir-harness/file-lease'
248
238
  # F7 锚点编辑:read 锚点增强 + fail-closed hash_edit 工具
249
239
  - id: hashline-edit
250
240
  name: 'weir-harness/hashline-edit'
@@ -378,7 +368,6 @@
378
368
  compaction: true
379
369
  toolResultPruner: true
380
370
  weirCompactionCoordinator: true
381
- weirEditLock: true
382
371
  weirWorktreeLanes: true
383
372
  weirBlackboard: true
384
373
  config:
@@ -418,9 +407,9 @@
418
407
  name: 'weir-harness/blackboard'
419
408
  - id: todo-driver
420
409
  name: 'weir-harness/todo-driver'
421
- # Must precede hashline-edit / lsp (mount-time capture).
422
- - id: edit-lock
423
- name: 'weir-harness/edit-lock'
410
+ # File leases (the edit-lock rebuild): no service, no ordering constraint.
411
+ - id: file-lease
412
+ name: 'weir-harness/file-lease'
424
413
  - id: hashline-edit
425
414
  name: 'weir-harness/hashline-edit'
426
415
  config:
@@ -0,0 +1,307 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "weir-harness",
3
+ chunk: "client.file-lease-badge.js",
4
+ factory: (require) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
+ let react = require("react");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ // ---- Edit-lock Badge (conversation composer bar) ----
11
+ // The ONLY user-visible surface of the edit-lock mechanism (design D13).
12
+ // It is read-only by construction, not by convention:
13
+ //
14
+ // - the badge has exactly three states — idle, holding N, unprotected
15
+ // — and each one resolves by itself (turn end, expiry, negotiation).
16
+ // None of them asks the user for anything. The badge names the
17
+ // surface, and appends a fact only when there is one: how many files
18
+ // this session holds, or that protection is off. An idle badge is
19
+ // just its name — never a status word standing in for "nothing";
20
+ // - the panel speaks only when there is something to say. With no
21
+ // claim and no degraded protection it is ONE line; each group's
22
+ // heading appears only above a non-empty list, and the protection
23
+ // notice only when protection is degraded. There are no headings over
24
+ // empty lists and no disclaimer about what cannot be clicked: a
25
+ // surface with no controls needs no sentence saying so;
26
+ // - it contains NO button besides its own toggle, NO command input, NO
27
+ // confirmation step and no clickable row. Expand/collapse is the one
28
+ // interaction in this chunk; every ownership change belongs to the
29
+ // model's tools. A control here would reintroduce the "some state
30
+ // needs a human" that the rebuild exists to delete;
31
+ // - it never writes to the conversation and never notifies. The read
32
+ // goes to the one read-only endpoint through the composition root's
33
+ // `fetchView` prop; nothing else is called.
34
+ //
35
+ // Everything shown arrives pre-shaped by the server-side view
36
+ // (`src/file-lease/view.js`): short path names, holder labels, and how
37
+ // long a retention has left. This chunk renders that and derives only
38
+ // the presentation state — it never parses prose and never invents a
39
+ // fact.
40
+ const STATE_NONE = "none";
41
+ const STATE_HOLDING = "holding";
42
+ const STATE_DEGRADED = "degraded";
43
+ /** Status-dot colour per state; only theme-defined tokens. */
44
+ const dotColor = {
45
+ none: "var(--dsw-alias-label-tertiary)",
46
+ holding: "var(--dsw-alias-state-business-primary)",
47
+ degraded: "var(--dsw-alias-state-warn-primary)"
48
+ };
49
+ const rootStyle = { position: "relative", display: "inline-flex" };
50
+ const badgeStyle = {
51
+ display: "inline-flex",
52
+ alignItems: "center",
53
+ gap: "6px",
54
+ background: "none",
55
+ border: "1px solid var(--dsw-alias-border-l2)",
56
+ borderRadius: "var(--dsw-radius-sm)",
57
+ cursor: "pointer",
58
+ padding: "3px 8px",
59
+ fontSize: "12px",
60
+ lineHeight: "16px",
61
+ color: "var(--dsw-alias-label-secondary)"
62
+ };
63
+ // Popover surface: the theme's documented overlay token (light and dark).
64
+ const panelStyle = {
65
+ position: "absolute",
66
+ bottom: "calc(100% + 6px)",
67
+ right: 0,
68
+ zIndex: 20,
69
+ width: "min(420px, 80vw)",
70
+ maxHeight: "50vh",
71
+ overflow: "auto",
72
+ background: "var(--dsw-alias-bg-overlay)",
73
+ border: "1px solid var(--dsw-alias-border-l2)",
74
+ borderRadius: "var(--dsw-radius-md, 8px)",
75
+ boxShadow: "0 6px 24px var(--dsw-alias-border-l2)",
76
+ padding: "10px 12px",
77
+ color: "var(--dsw-alias-label-primary)",
78
+ fontSize: "12px",
79
+ lineHeight: "18px"
80
+ };
81
+ const mutedStyle = { color: "var(--dsw-alias-label-secondary)" };
82
+ /** A group heading: bold, and spaced only when something precedes it. */
83
+ const headStyle = { fontWeight: 600 };
84
+ // The degraded notice: amber (the same token the dot uses) at the panel's
85
+ // own weight — a status line, not an alarm.
86
+ const noticeStyle = { color: "var(--dsw-alias-state-warn-primary)" };
87
+ const rowStyle = { display: "flex", flexWrap: "wrap", alignItems: "baseline", gap: "8px", padding: "3px 0", borderTop: "1px solid var(--dsw-alias-border-l2)" };
88
+ const nameStyle = { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: "var(--dsw-font-markdown-code-block-font-family, ui-monospace, monospace)" };
89
+ const dot = (state) => react_jsx_runtime.jsx("span", { "aria-hidden": true, style: { display: "inline-block", width: "7px", height: "7px", borderRadius: "50%", background: dotColor[state], flex: "none" } });
90
+ /**
91
+ * The badge state of a view. Anything unreadable is `degraded`: an
92
+ * unreadable surface must never be presented as "nothing held", which
93
+ * reads like a fact the panel cannot actually vouch for.
94
+ * @param {any} view the server-shaped view
95
+ * @returns {"none" | "holding" | "degraded"}
96
+ */
97
+ function stateOf(view) {
98
+ if (!view || typeof view !== "object") return STATE_DEGRADED;
99
+ if (view.arbiter !== "ok") return STATE_DEGRADED;
100
+ const mine = Array.isArray(view.mine) ? view.mine : [];
101
+ return mine.length > 0 ? STATE_HOLDING : STATE_NONE;
102
+ }
103
+ /** The claims of one section, tolerating a partial/degraded payload. */
104
+ function rowsOf(view, key) {
105
+ const rows = view && Array.isArray(view[key]) ? view[key] : [];
106
+ return rows.filter((row) => row && typeof row === "object" && typeof row.path === "string" && row.path !== "");
107
+ }
108
+ /** Remaining retention in whole minutes, or null when there is none. */
109
+ function retainedMinutes(holdUntil, at) {
110
+ if (typeof holdUntil !== "number" || !Number.isFinite(holdUntil)) return null;
111
+ const minutes = Math.max(0, Math.ceil((holdUntil - at) / 60000));
112
+ return minutes > 0 ? minutes : null;
113
+ }
114
+ function FileLeaseBadge(props) {
115
+ const t = props.t;
116
+ const sessionId = props.sessionId;
117
+ const [open, setOpen] = react.useState(false);
118
+ // `any`: the payload is the server-shaped view, narrowed on read.
119
+ const [view, setView] = react.useState(/** @type {any} */ (null));
120
+ // The one read: on mount so the badge is right before it is ever
121
+ // expanded, and again each time the panel opens (a snapshot, never a
122
+ // poll — ownership has no state to keep in sync).
123
+ const read = () => {
124
+ if (typeof props.fetchView !== "function") {
125
+ setView({ schemaVersion: 1, arbiter: "unavailable", mine: [], others: [] });
126
+ return Promise.resolve();
127
+ }
128
+ return Promise.resolve(props.fetchView()).then(
129
+ (next) => { if (next && typeof next === "object") setView(next); },
130
+ // A failed read is the degraded state: the panel cannot vouch
131
+ // for anything, so it says so instead of showing an empty list.
132
+ () => { setView({ schemaVersion: 1, arbiter: "unavailable", mine: [], others: [] }); }
133
+ );
134
+ };
135
+ react.useEffect(() => {
136
+ if (!sessionId) {
137
+ setView(null);
138
+ setOpen(false);
139
+ return undefined;
140
+ }
141
+ read();
142
+ return undefined;
143
+ }, [sessionId]);
144
+ // Close on Escape and on any pointerdown outside this entry's wrapper:
145
+ // the same dismissal the other composer surfaces use, and the only way
146
+ // out of the panel that needs no action model behind it.
147
+ react.useEffect(() => {
148
+ if (!open || typeof document === "undefined") return undefined;
149
+ const onPointerDown = (event) => {
150
+ if (event && typeof event.target?.closest === "function" && event.target.closest("[data-weir-file-lease-root]")) return;
151
+ setOpen(false);
152
+ };
153
+ const onKeyDown = (event) => {
154
+ if (event?.key === "Escape") setOpen(false);
155
+ };
156
+ document.addEventListener("pointerdown", onPointerDown);
157
+ document.addEventListener("keydown", onKeyDown);
158
+ return () => {
159
+ document.removeEventListener("pointerdown", onPointerDown);
160
+ document.removeEventListener("keydown", onKeyDown);
161
+ };
162
+ }, [open]);
163
+ if (!sessionId) return null;
164
+ // Before the first read settles the entry shows the idle state:
165
+ // ownership does not survive the process (D10), so "nothing held" is
166
+ // the honest answer for a session that has not been read yet.
167
+ const state = view === null ? STATE_NONE : stateOf(view);
168
+ const mine = rowsOf(view, "mine");
169
+ const others = rowsOf(view, "others");
170
+ // The badge suffix: a fact when there is one, nothing when there is
171
+ // not. An idle badge never says "not holding" — it simply is idle.
172
+ const suffix = state === STATE_HOLDING
173
+ ? t("fileLeaseState_holding").replace("{n}", String(mine.length))
174
+ : state === STATE_DEGRADED ? t("fileLeaseState_degraded") : "";
175
+ const arbiter = view && typeof view.arbiter === "string" ? view.arbiter : "unavailable";
176
+ const at = Date.now();
177
+ const toggle = () => {
178
+ const next = !open;
179
+ setOpen(next);
180
+ if (next) read();
181
+ };
182
+ const row = (entry, key, trailing) => react_jsx_runtime.jsxs("div", {
183
+ style: rowStyle,
184
+ "data-weir-file-lease-row": key,
185
+ children: [
186
+ react_jsx_runtime.jsx("span", { style: nameStyle, title: entry.path, children: entry.path }),
187
+ ...trailing
188
+ ]
189
+ }, `${key}:${entry.path}`);
190
+ const mineRows = mine.map((entry) => {
191
+ const trailing = [];
192
+ const minutes = retainedMinutes(entry.holdUntil, at);
193
+ if (minutes !== null) {
194
+ trailing.push(react_jsx_runtime.jsx("span", { style: mutedStyle, "data-weir-file-lease-retained": entry.path, children: t("fileLeaseRetainedFor").replace("{n}", String(minutes)) }, "retained"));
195
+ }
196
+ return row(entry, "mine", trailing);
197
+ });
198
+ const otherRows = others.map((entry) => {
199
+ const label = entry.holder && typeof entry.holder.label === "string" ? entry.holder.label : "";
200
+ return row(entry, "others", [react_jsx_runtime.jsx("span", { style: mutedStyle, "data-weir-file-lease-holder": entry.path, children: label }, "holder")]);
201
+ });
202
+ // The panel is assembled as blocks so that spacing, and the existence
203
+ // of a heading, both follow from what is actually there.
204
+ const blocks = [];
205
+ if (state === STATE_DEGRADED) {
206
+ blocks.push(react_jsx_runtime.jsx("div", {
207
+ style: noticeStyle,
208
+ "data-weir-file-lease-arbiter": arbiter,
209
+ children: t("fileLeaseDegradedHint")
210
+ }, "notice"));
211
+ }
212
+ const head = (key, text) => react_jsx_runtime.jsx("div", {
213
+ style: blocks.length === 0 ? headStyle : { ...headStyle, marginTop: "10px" },
214
+ children: text
215
+ }, `${key}-head`);
216
+ if (mineRows.length > 0) {
217
+ blocks.push(head("mine", t("fileLeaseMine")));
218
+ blocks.push(react_jsx_runtime.jsx("div", { "data-weir-file-lease-section": "mine", children: mineRows }, "mine"));
219
+ }
220
+ if (otherRows.length > 0) {
221
+ blocks.push(head("others", t("fileLeaseOthers")));
222
+ blocks.push(react_jsx_runtime.jsx("div", { "data-weir-file-lease-section": "others", children: otherRows }, "others"));
223
+ }
224
+ // Nothing held anywhere: one plain line, and (when protection is
225
+ // degraded) one notice above it — never a section skeleton.
226
+ if (mineRows.length === 0 && otherRows.length === 0) {
227
+ blocks.push(react_jsx_runtime.jsx("div", {
228
+ style: blocks.length === 0 ? mutedStyle : { ...mutedStyle, marginTop: "6px" },
229
+ "data-weir-file-lease-empty": "",
230
+ children: t("fileLeaseEmpty")
231
+ }, "empty"));
232
+ }
233
+ return react_jsx_runtime.jsxs("span", {
234
+ style: rootStyle,
235
+ "data-weir-file-lease-root": "",
236
+ children: [
237
+ react_jsx_runtime.jsxs("button", {
238
+ type: "button",
239
+ style: badgeStyle,
240
+ onClick: toggle,
241
+ "aria-expanded": open,
242
+ "data-weir-file-lease": "",
243
+ "data-weir-file-lease-state": state,
244
+ title: t("fileLeaseTitle"),
245
+ children: [
246
+ dot(state),
247
+ react_jsx_runtime.jsx("span", { children: t("fileLeaseLabel") }),
248
+ suffix === "" ? null : react_jsx_runtime.jsx("span", { "data-weir-file-lease-summary": "", children: suffix })
249
+ ]
250
+ }),
251
+ open ? react_jsx_runtime.jsx("div", {
252
+ role: "dialog",
253
+ "aria-label": t("fileLeasePanelTitle"),
254
+ style: panelStyle,
255
+ "data-weir-file-lease-panel": "",
256
+ onKeyDown: (event) => { if (event?.key === "Escape") setOpen(false); },
257
+ children: blocks
258
+ }) : null
259
+ ]
260
+ });
261
+ }
262
+ // Locale data face (pure data, NO ctx calls — a chunk factory re-executes
263
+ // under HMR, so a chunk-local register would throw on the duplicate
264
+ // (namespace, locale) pair): the entry registers these under
265
+ // "settings.weir.file-lease" inside a ctx.effect when this chunk arrives
266
+ // (lib/client.js registerChunkDictionaries). Pinned by test/client.test.js's
267
+ // dictionary union pin.
268
+ //
269
+ // Copy rules: plain user-facing words only — no internal vocabulary, no
270
+ // "read-only" disclaimers, no status word for the idle state. Badge
271
+ // suffixes carry their own leading separator so the badge reads as one
272
+ // string in both locales.
273
+ const dictionaries = {
274
+ en: {
275
+ fileLeaseLabel: "Edit Lock",
276
+ fileLeaseTitle: "Files in use",
277
+ fileLeasePanelTitle: "Files in use",
278
+ fileLeaseState_holding: " · {n}",
279
+ fileLeaseState_degraded: " · unprotected",
280
+ fileLeaseMine: "You are editing",
281
+ fileLeaseOthers: "Another session is editing",
282
+ fileLeaseEmpty: "No files in use.",
283
+ fileLeaseDegradedHint: "Protection is temporarily unavailable; editing continues.",
284
+ fileLeaseRetainedFor: "{n} min left"
285
+ },
286
+ zh: {
287
+ fileLeaseLabel: "编辑锁",
288
+ fileLeaseTitle: "文件占用",
289
+ fileLeasePanelTitle: "文件占用",
290
+ fileLeaseState_holding: " · {n}",
291
+ fileLeaseState_degraded: " · 未加锁",
292
+ fileLeaseMine: "你正在编辑",
293
+ fileLeaseOthers: "其他会话正在编辑",
294
+ fileLeaseEmpty: "暂无文件占用。",
295
+ fileLeaseDegradedHint: "保护暂不可用,编辑不受影响。",
296
+ fileLeaseRetainedFor: "保留 {n} 分钟"
297
+ }
298
+ };
299
+
300
+ exports.dictionaries = dictionaries;
301
+ exports.FileLeaseBadge = FileLeaseBadge;
302
+ exports.stateOf = stateOf;
303
+ exports.rowsOf = rowsOf;
304
+ exports.retainedMinutes = retainedMinutes;
305
+ return module.exports;
306
+ }
307
+ });
package/lib/client.js CHANGED
@@ -1,4 +1,4 @@
1
- // Weir client chunks: {"client.blackboard-model.js":"2e5abdd44edb599d10d9d547fa07133e84250d6007161c2c8704b58151896f93","client.blackboard-panel.js":"c9d6a0f02623075fc884f3519f982574ad33ad871f7bcd7e8c36f12512f0de39","client.capability-badge.js":"6ffd703f498e0aa5cc8ac78f244402ec4ceee56d22d2a0e74c246799565776cc","client.capability-manager.js":"d22e8116922f98e61bbbdf36fc78bd81e8b47b0ee5e75ddb06bea4e204749b89","client.capability-model.js":"d2aef1aa2daaecad6c9a1b9d9a0ecf6fb37dd7ec80efff5d532cf3b8a6efa712","client.capability-presets.js":"12ac8da3568d013abd76b67cfe473a329f4af2feaac9cd55a82d8881e5bc1017","client.chain-editor.js":"1e9a33bc68cabc43bc27b91ef83eab5028a33256596c147e15722d684687765b","client.chain-model.js":"2e885922cf8c2972725c6eef8bf20c7d1202d961edfb8ad41574dac062d74371","client.disabled-categories-editor.js":"f9103fd2e2691eda9afd199e1a5462189097a9f3dee0518be5fa3587d896e033","client.edit-lock-maintenance.js":"5e6933f3ad222ea99e890a06b94a223de1e864de058fca2a53954d62eaad9ab8","client.edit-lock-panel.js":"fa78a0762d2787e6322308dfff103aab3cdbd078d1ec22000d6deefe258153b1","client.hash-edit-model.js":"71727d47a6ef32ebd2abfb063fa93499a5e607d6716e3ec8983ae056c4fb317f","client.hash-edit-view.js":"fc0f4dabddab9a48527553ad4232b3715e46e6863aab75e02ae2dcd6687265c2","client.lsp-model.js":"30943ed1ed11c1f2770a724da855ea2417c11566c2778c79e17cfc2de499cd80","client.lsp-panel.js":"61629a4d317dae9ed01c1cc0355632fb752788dc23f7606257188050734cd030","client.lsp-toggle.js":"d6af81be5e5b854e98563bbb9349a6db89834a9d7dee9cf0e97759ecb32090e8","client.model-picker.js":"57f96826e00b37ae6d6474a6750f31b4e128e5fdb37e302875a41cdb2e18f14a","client.notify-permissions.js":"2be9bdbeffef96baa3cbe4ce430ab30a1d1bf797782453d287d54602ea47db8e","client.notify-web.js":"0943f67d5526ff0fb7cb4f69580e0a9ea54ed06465c513afc51a5a7fb1c55ccf","client.robash-editor.js":"cd6a283b72cb02598e32ca4ce03a1b8de5f328ecdd1f5f920377524f3bdcbe1c","client.robash-model.js":"f4395338a773e8bb48e4f96aeb737cd634edcca0b90af992a7dbbe47be42e451","client.settings-page.js":"b6524f04641793602e00bc42f1c86f7e8981b903e16a8493379126c359dbab96","client.worktree-model.js":"8bfbaaa080f20e90a0128afccb9b4e232514615a8f2fee3a32a42dde78b34a8f","client.worktree-view.js":"356c3f5f683b8509ae8679243a6460ca02957fd15e057a629b2ac4fdd12ed057"}
1
+ // Weir client chunks: {"client.blackboard-model.js":"2e5abdd44edb599d10d9d547fa07133e84250d6007161c2c8704b58151896f93","client.blackboard-panel.js":"c9d6a0f02623075fc884f3519f982574ad33ad871f7bcd7e8c36f12512f0de39","client.capability-badge.js":"6ffd703f498e0aa5cc8ac78f244402ec4ceee56d22d2a0e74c246799565776cc","client.capability-manager.js":"d22e8116922f98e61bbbdf36fc78bd81e8b47b0ee5e75ddb06bea4e204749b89","client.capability-model.js":"d2aef1aa2daaecad6c9a1b9d9a0ecf6fb37dd7ec80efff5d532cf3b8a6efa712","client.capability-presets.js":"12ac8da3568d013abd76b67cfe473a329f4af2feaac9cd55a82d8881e5bc1017","client.chain-editor.js":"1e9a33bc68cabc43bc27b91ef83eab5028a33256596c147e15722d684687765b","client.chain-model.js":"2e885922cf8c2972725c6eef8bf20c7d1202d961edfb8ad41574dac062d74371","client.disabled-categories-editor.js":"f9103fd2e2691eda9afd199e1a5462189097a9f3dee0518be5fa3587d896e033","client.file-lease-badge.js":"cd30546307c91c94fe05484557e46f58c53fe55657f62b0a0415a072dfb3b582","client.hash-edit-model.js":"71727d47a6ef32ebd2abfb063fa93499a5e607d6716e3ec8983ae056c4fb317f","client.hash-edit-view.js":"fc0f4dabddab9a48527553ad4232b3715e46e6863aab75e02ae2dcd6687265c2","client.lsp-model.js":"30943ed1ed11c1f2770a724da855ea2417c11566c2778c79e17cfc2de499cd80","client.lsp-panel.js":"61629a4d317dae9ed01c1cc0355632fb752788dc23f7606257188050734cd030","client.lsp-toggle.js":"d6af81be5e5b854e98563bbb9349a6db89834a9d7dee9cf0e97759ecb32090e8","client.model-picker.js":"57f96826e00b37ae6d6474a6750f31b4e128e5fdb37e302875a41cdb2e18f14a","client.notify-permissions.js":"2be9bdbeffef96baa3cbe4ce430ab30a1d1bf797782453d287d54602ea47db8e","client.notify-web.js":"0943f67d5526ff0fb7cb4f69580e0a9ea54ed06465c513afc51a5a7fb1c55ccf","client.robash-editor.js":"cd6a283b72cb02598e32ca4ce03a1b8de5f328ecdd1f5f920377524f3bdcbe1c","client.robash-model.js":"f4395338a773e8bb48e4f96aeb737cd634edcca0b90af992a7dbbe47be42e451","client.settings-page.js":"f3c04a5b33cdb963357bc653f7050e598f19efc4cc833694379a4ba055e949d3","client.worktree-model.js":"8bfbaaa080f20e90a0128afccb9b4e232514615a8f2fee3a32a42dde78b34a8f","client.worktree-view.js":"356c3f5f683b8509ae8679243a6460ca02957fd15e057a629b2ac4fdd12ed057"}
2
2
  window.__ModuleLoader__.load({
3
3
  id: "weir-harness",
4
4
  factory: (require) => {
@@ -122,7 +122,7 @@ window.__ModuleLoader__.load({
122
122
  register: null,
123
123
  lspToggle: null,
124
124
  hashEdit: null,
125
- editLock: null,
125
+ fileLease: null,
126
126
  worktree: null,
127
127
  };
128
128
  // ---- Package-local chunk wiring (composition root) ----
@@ -218,15 +218,15 @@ window.__ModuleLoader__.load({
218
218
  if (!arrival?.chunks) return null;
219
219
  return react_jsx_runtime.jsx(arrival.chunks.LspToggle, { ...props, settingsBus, t: chunkDictionaries.lspToggle ?? props.t });
220
220
  }
221
- const loadEditLockChunk = lazyChunks(() => require.async("./client.edit-lock-panel.js").then((chunk) => {
222
- chunkDictionaries.register?.("settings.weir.edit-lock", chunk.dictionaries);
221
+ const loadFileLeaseChunk = lazyChunks(() => require.async("./client.file-lease-badge.js").then((chunk) => {
222
+ chunkDictionaries.register?.("settings.weir.file-lease", chunk.dictionaries);
223
223
  return chunk;
224
224
  }));
225
- /** Composer-bar Edit Lock entry: nothing until its chunk arrives. */
226
- function EditLockWrapper(props) {
227
- const arrival = useChunkArrival(loadEditLockChunk, typeof props.sessionId === "string" && props.sessionId !== "");
225
+ /** Composer-bar file-lease entry: nothing until its chunk arrives. */
226
+ function FileLeaseBadgeWrapper(props) {
227
+ const arrival = useChunkArrival(loadFileLeaseChunk, typeof props.sessionId === "string" && props.sessionId !== "");
228
228
  if (!arrival?.chunks) return null;
229
- return react_jsx_runtime.jsx(arrival.chunks.EditLockPanel, { ...props, t: chunkDictionaries.editLock ?? props.t });
229
+ return react_jsx_runtime.jsx(arrival.chunks.FileLeaseBadge, { ...props, t: chunkDictionaries.fileLease ?? props.t });
230
230
  }
231
231
  // ---- Tab glyphs (ui-entries-and-icons) ----
232
232
  // Three 16x16 stroke glyphs drawn in currentColor, matching the host
@@ -427,7 +427,7 @@ window.__ModuleLoader__.load({
427
427
  chunkDictionaries.register = registerChunkDictionaries;
428
428
  chunkDictionaries.lspToggle = ctx.locale.bind("settings.weir.lsp-toggle");
429
429
  chunkDictionaries.hashEdit = ctx.locale.bind("settings.weir.hash-edit");
430
- chunkDictionaries.editLock = ctx.locale.bind("settings.weir.edit-lock");
430
+ chunkDictionaries.fileLease = ctx.locale.bind("settings.weir.file-lease");
431
431
  chunkDictionaries.worktree = ctx.locale.bind("settings.weir.worktree");
432
432
  const pageT = ctx.locale.bind("settings.weir.page");
433
433
  const capabilityT = ctx.locale.bind("settings.weir.capability");
@@ -619,28 +619,20 @@ window.__ModuleLoader__.load({
619
619
  key: "hash_edit",
620
620
  locale: NS
621
621
  }, HashEditToolView)), "ui-weir-settings: hash_edit toolview");
622
- // Edit Lock entry: renders only while the `edit-lock` command exists
623
- // (feature enabled). Each action is an explicit human /edit-lock run.
622
+ // File-lease ownership entry (edit-lock rebuild D13): the read-only
623
+ // badge of the file-lease feature. It carries NO action verb and no
624
+ // command bridge — every ownership state resolves by itself (turn end,
625
+ // expiry, negotiation) — and its single read is the read-only view
626
+ // endpoint, so nothing in this surface can write to the conversation.
624
627
  ctx.effect(() => ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
625
628
  name: "conversation.input.right",
626
- id: "weir-edit-lock",
629
+ id: "weir-file-lease",
627
630
  order: 101,
628
631
  locale: NS,
629
632
  inject: (sessionId) => {
630
633
  if (!sessionId) return {};
631
634
  return {
632
635
  sessionId,
633
- runEditLock: async (verb) => {
634
- if (!ctx.remote.commands?.execute) return { kind: "error", text: "unknown command: /edit-lock" };
635
- const result = await ctx.remote.commands.execute(sessionId, `/edit-lock ${verb}`, []);
636
- if (!result.ok) return { kind: "error", text: `${result.error.message} (${result.error.code})` };
637
- if (result.value === undefined) return { kind: "error", text: "unknown command: /edit-lock" };
638
- return result.value.result;
639
- },
640
- commandsList: (sid) => {
641
- if (!ctx.remote.commands?.list) return Promise.resolve([]);
642
- return ctx.remote.commands.list(sid).then((result) => (result.ok ? result.value : []));
643
- },
644
636
  // Structured view: read-only, never written to the conversation.
645
637
  fetchView: async () => {
646
638
  const result = await httpChannel("api/weir-edit-lock/view", { sessionId });
@@ -649,7 +641,7 @@ window.__ModuleLoader__.load({
649
641
  }
650
642
  };
651
643
  }
652
- }, EditLockWrapper)), "ui-weir-settings: edit lock entry");
644
+ }, FileLeaseBadgeWrapper)), "ui-weir-settings: file lease badge");
653
645
  // ---- Worktree lane surfaces ----
654
646
  // Each surface registers in its own effect so one failure cannot take
655
647
  // down the others (same discipline as the hash_edit view). Reads go to
@@ -1064,15 +1056,14 @@ window.__ModuleLoader__.load({
1064
1056
  require.async("./client.chain-model.js"),
1065
1057
  require.async("./client.robash-model.js"),
1066
1058
  require.async("./client.lsp-model.js"),
1067
- require.async("./client.notify-permissions.js"),
1068
- require.async("./client.edit-lock-maintenance.js")
1069
- ]).then(([settingsPage, chainEditor, robashEditor, disabledCategoriesEditor, lspPanel, chainModel, robashModel, lspModel, notifyPermissions, editLockMaintenance]) => {
1059
+ require.async("./client.notify-permissions.js")
1060
+ ]).then(([settingsPage, chainEditor, robashEditor, disabledCategoriesEditor, lspPanel, chainModel, robashModel, lspModel, notifyPermissions]) => {
1070
1061
  // The whole settings card tree shares one t (the page
1071
1062
  // namespace): every batch chunk's data face merges into ONE
1072
1063
  // registration — a duplicate (namespace, locale) pair would
1073
1064
  // throw, so per-feature namespaces are not an option here.
1074
1065
  const pageDictionaries = { en: {}, zh: {} };
1075
- for (const chunk of [settingsPage, chainModel, robashModel, lspModel, disabledCategoriesEditor, notifyPermissions, editLockMaintenance]) {
1066
+ for (const chunk of [settingsPage, chainModel, robashModel, lspModel, disabledCategoriesEditor, notifyPermissions]) {
1076
1067
  Object.assign(pageDictionaries.en, chunk.dictionaries?.en);
1077
1068
  Object.assign(pageDictionaries.zh, chunk.dictionaries?.zh);
1078
1069
  }
@@ -1094,8 +1085,7 @@ window.__ModuleLoader__.load({
1094
1085
  // The HTTP channel adapter reaches every fetch-talking editor
1095
1086
  // through the same prop (composition-root injection: a chunk
1096
1087
  // never builds its own carrier).
1097
- NotifyPermissionsField: (editorProps) => react_jsx_runtime.jsx(notifyPermissions.NotifyPermissionsField, { ...editorProps, channel: httpChannel }),
1098
- EditLockMaintenanceField: (editorProps) => react_jsx_runtime.jsx(editLockMaintenance.EditLockMaintenanceField, { ...editorProps, channel: httpChannel })
1088
+ NotifyPermissionsField: (editorProps) => react_jsx_runtime.jsx(notifyPermissions.NotifyPermissionsField, { ...editorProps, channel: httpChannel })
1099
1089
  };
1100
1090
  return { settingsPage, editors };
1101
1091
  }, (error) => {
@@ -47,7 +47,7 @@ window.__ModuleLoader__.load({
47
47
  // scatter). test/client-settings-page.test.js pins this list to
48
48
  // RESTART_KEYS from src/settings/sections.js, so a declaration
49
49
  // change on either side cannot drift the two sides.
50
- const RESTART_FIELDS = ["intentGateClassifier", "intentGateProvider", "intentGateModel", "intentGateReasoningEffort", "intentGateTimeoutMs", "jevEndpoint", "jevModel", "jevApiKeyEnv", "todoEnabled", "todoMaxConsecutive", "todoErrorRetryMax", "todoErrorBackoffBaseMs", "todoErrorBackoffCapMs", "hashlineHideStockEdit", "editLockEnabled"];
50
+ const RESTART_FIELDS = ["intentGateClassifier", "intentGateProvider", "intentGateModel", "intentGateReasoningEffort", "intentGateTimeoutMs", "jevEndpoint", "jevModel", "jevApiKeyEnv", "todoEnabled", "todoMaxConsecutive", "todoErrorRetryMax", "todoErrorBackoffBaseMs", "todoErrorBackoffCapMs", "hashlineHideStockEdit"];
51
51
  // ---- condition declaration shorthands (frozen pure data, D2) ----
52
52
  const enabledWhen = (key) => Object.freeze({ key, equals: true });
53
53
  const CLASSIFIER_LLM = Object.freeze({ key: "intentGateClassifier", equals: "llm" });
@@ -107,10 +107,8 @@ window.__ModuleLoader__.load({
107
107
  ] },
108
108
  { id: "editing", fields: [
109
109
  { field: "hashlineHideStockEdit", kind: "boolean" },
110
- { field: "editLockEnabled", kind: "boolean" },
111
- { field: "editLockAutoResume", kind: "boolean", parent: "editLockEnabled", when: enabledWhen("editLockEnabled") },
112
- { field: "editLockStaleSweep", kind: "boolean", parent: "editLockEnabled", when: enabledWhen("editLockEnabled") },
113
- // Diagnostics must remain reachable when enforcement is off.
110
+ // Edit Lock diagnostics seat: the composition root binds no editor
111
+ // for it (edit-lock rebuild), so the row renders nothing.
114
112
  { kind: "custom", slot: "editLockMaintenance" }
115
113
  ] },
116
114
  { id: "worktree", fields: [
@@ -194,14 +192,6 @@ window.__ModuleLoader__.load({
194
192
  guardSoftThreshold: 0.72,
195
193
  guardHardThreshold: 0.88,
196
194
  hashlineHideStockEdit: true,
197
- editLockEnabled: true,
198
- editLockHoldDefaultMinutes: 30,
199
- editLockHoldSingleMaxMinutes: 30,
200
- editLockHoldCumulativeMaxMinutes: 120,
201
- editLockNudgeAttempts: 2,
202
- editLockNudgeFallback: "release",
203
- editLockAutoResume: true,
204
- editLockStaleSweep: true,
205
195
  robashEnabled: true,
206
196
  lspEnabled: false,
207
197
  lspIdleMs: 600000,
@@ -597,7 +587,6 @@ window.__ModuleLoader__.load({
597
587
  const childLabelStyle = { ...labelStyle, fontSize: "13px" };
598
588
  const labelTextStyle = { display: "inline-flex", alignItems: "center", gap: "6px" };
599
589
  const hintStyle = { fontSize: "12px", lineHeight: "16px", color: "var(--dsw-alias-label-secondary)" };
600
- const optOutStyle = { ...hintStyle, color: "var(--dsw-alias-state-warn-primary)", borderLeft: "2px solid var(--dsw-alias-state-warn-primary)", paddingLeft: "8px", marginTop: "4px" };
601
590
  // Card-based group sections (design D7): layered background, l1 border,
602
591
  // radius, the group title inside the card top, hairline l2 separators
603
592
  // between rows. Colors only from theme tokens; geometry hardcoded.
@@ -637,9 +626,6 @@ window.__ModuleLoader__.load({
637
626
  react_jsx_runtime.jsxs("div", { style: labelGroupStyle, children: [
638
627
  labelWithTag(descriptor, t, depth > 0 ? childLabelStyle : labelStyle),
639
628
  react_jsx_runtime.jsx("span", { style: hintStyle, children: t(`${descriptor.field}Hint`) }),
640
- descriptor.field === "editLockEnabled" && resolveEffectiveValue(descriptor, field, field?.saved) !== true
641
- ? react_jsx_runtime.jsx("span", { style: optOutStyle, role: "alert", children: t("editLockOptOutWarning") })
642
- : null
643
629
  ] }),
644
630
  react_jsx_runtime.jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: "4px" }, children: [
645
631
  descriptor.kind === "boolean" ? react_jsx_runtime.jsx(primitives.Switch, {
@@ -925,9 +911,13 @@ window.__ModuleLoader__.load({
925
911
  }));
926
912
  }
927
913
  if (node.slot === "editLockMaintenance") {
928
- // Edit Lock maintenance: profile-wide switch status and
929
- // read-only authority diagnostics (read-only).
930
- return wrap(react_jsx_runtime.jsx(props.editors.EditLockMaintenanceField, { t }));
914
+ // The Edit Lock maintenance seat is GONE (edit-lock rebuild):
915
+ // the composition root no longer binds an editor for it, and
916
+ // the row disappears with the settings keys. An unbound seat
917
+ // must leave the card intact instead of rendering an undefined
918
+ // element type, so the row simply renders nothing here.
919
+ const MaintenanceField = props.editors?.EditLockMaintenanceField;
920
+ return typeof MaintenanceField === "function" ? wrap(react_jsx_runtime.jsx(MaintenanceField, { t })) : null;
931
921
  }
932
922
  // notifyPermissions: the macOS-only permission entry
933
923
  return wrap(react_jsx_runtime.jsx(props.editors.NotifyPermissionsField, { t }));
@@ -1113,13 +1103,6 @@ window.__ModuleLoader__.load({
1113
1103
  guardHardThresholdHint: "Above this fill ratio compaction is requested (default 0.88; not a completion guarantee).",
1114
1104
  hashlineHideStockEdit: "Anchor editing only",
1115
1105
  hashlineHideStockEditHint: "Hides the built-in edit tool; file edits go through hash_edit anchors only.",
1116
- editLockEnabled: "Edit Lock",
1117
- editLockEnabledHint: "Sessions take turns editing the same files instead of overwriting each other.",
1118
- editLockOptOutWarning: "Edit Lock is set to off. After saving, restart DeepSeek Harness to apply the change. Once enforcement is off, ordinary editing is UNPROTECTED and can overwrite another session's work. Old fences and history are retained and may block files when you re-enable Edit Lock.",
1119
- editLockAutoResume: "Message resumes editing",
1120
- editLockAutoResumeHint: "After a session is stopped, sending any message restores its editing right and re-confirms its kept files (default on).",
1121
- editLockStaleSweep: "Sweep stale locks",
1122
- editLockStaleSweepHint: "Locks whose target file was deleted or moved are re-verified and released as you send messages (default on).",
1123
1106
  worktreeEnabled: "Worktree lanes",
1124
1107
  worktreeEnabledHint: "Lets the assistant work in isolated git worktrees: lane tools, the /worktree command, and the lanes panel.",
1125
1108
  worktreeAutoSetup: "Auto-install in new lanes",
@@ -1248,13 +1231,6 @@ window.__ModuleLoader__.load({
1248
1231
  guardHardThresholdHint: "上下文用量超过该比例时请求立即压缩(默认 0.88,不保证压缩完成)。",
1249
1232
  hashlineHideStockEdit: "仅用锚点编辑",
1250
1233
  hashlineHideStockEditHint: "隐藏内置 edit 工具,文件修改一律走 hash_edit 锚点编辑。",
1251
- editLockEnabled: "编辑锁",
1252
- editLockEnabledHint: "多个会话轮流编辑同一批文件,而不是互相覆盖。",
1253
- editLockOptOutWarning: "编辑锁已设为关闭。保存后须重启 DeepSeek Harness 才会生效。停用强制执行后,普通编辑不受保护,可能覆盖其他会话的工作。旧围栏与历史仍会保留,重新启用编辑锁时可能再次阻塞文件。",
1254
- editLockAutoResume: "发消息即恢复编辑",
1255
- editLockAutoResumeHint: "会话被停止后,发送任意消息即自动恢复其编辑权并确认保留的文件(默认开启)。",
1256
- editLockStaleSweep: "自动清理失效锁",
1257
- editLockStaleSweepHint: "目标文件已被删除或移动的锁,在你发消息时自动复核并释放(默认开启)。",
1258
1234
  worktreeEnabled: "Worktree 车道",
1259
1235
  worktreeEnabledHint: "让助手在隔离的 git worktree 中工作:车道工具、/worktree 命令与车道面板。",
1260
1236
  worktreeAutoSetup: "新车道自动安装依赖",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weir-harness",
3
- "version": "1.2.2",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "description": "Weir \u2014 orchestrator-centric DSH agent preset: intent gate, category delegation, todo driver, pull-based background jobs, auto-compaction, hashline edit.",
6
6
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  "./todo-driver": "./src/todo-driver/index.js",
44
44
  "./context-guard": "./src/context-guard/index.js",
45
45
  "./hashline-edit": "./src/hashline-edit/index.js",
46
- "./edit-lock": "./src/edit-lock/index.js",
46
+ "./file-lease": "./src/file-lease/plugin.js",
47
47
  "./blackboard": "./src/blackboard/index.js",
48
48
  "./blackboard-remote": "./src/blackboard/remote.js",
49
49
  "./worktree": "./src/worktree/index.js",
@@ -1,6 +1,6 @@
1
1
  // Augmentations of the narrow checked-graph platform contract
2
- // (types/platform.d.ts) for the capability-store liveness/lock modules: the
3
- // edit-lock reservation reuses them, which pulls them into the checked graph.
2
+ // (types/platform.d.ts) for the capability-store liveness/lock modules: they
3
+ // pull these declarations into the checked graph.
4
4
  // Same discipline as the base contract — runtime modules remain supplied by
5
5
  // Node; these declarations only describe what the checked graph uses.
6
6
  declare module 'node:fs' {
@@ -21,9 +21,8 @@ import { link, mkdir, open, readFile, readdir, rename, unlink } from 'node:fs/pr
21
21
  // Directory fsync is a POSIX crash-durability idiom: on Windows the directory
22
22
  // handle opens but sync fails EPERM (verified on Node v24, NTFS). The win32
23
23
  // store row (SUPPORTED_PLATFORMS in store.js) therefore commits with
24
- // file-level fsync + atomic rename only — the same platform stance the
25
- // edit-lock reservation already takes (reservation.js SYNC_SUPPORTED). The
26
- // durability tradeoff is documented in docs/features/session-capability-manager.md.
24
+ // file-level fsync + atomic rename only. The durability tradeoff is documented
25
+ // in docs/features/session-capability-manager.md.
27
26
  const DIR_SYNC_SUPPORTED = process.platform === 'darwin' || process.platform === 'linux'
28
27
 
29
28
  // Windows file-locking etiquette: antivirus and the search indexer briefly