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
@@ -1,405 +0,0 @@
1
- window.__ModuleLoader__.load({
2
- id: "weir-harness",
3
- chunk: "client.edit-lock-panel.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
- const jsx = react_jsx_runtime.jsx;
11
- const jsxs = react_jsx_runtime.jsxs;
12
- // ---- Per-session Edit Lock entry (conversation composer bar) ----
13
- // Visible only while the `edit-lock` command exists (feature enabled).
14
- // Everything the panel SHOWS comes from a structured, read-only view; the
15
- // panel never parses command text. Everything it DOES is one explicit
16
- // /edit-lock command, so each action stays on the conversation record.
17
- // Nothing polls: the view is read when the entry mounts, when the panel
18
- // opens, and after each action.
19
- const STATES = ["revoked", "unavailable", "stopped", "attention", "confirm", "holding", "editing", "idle"];
20
- /** Status-dot colour per state; only theme-defined tokens. */
21
- const dotColor = {
22
- idle: "var(--dsw-alias-label-tertiary)",
23
- editing: "var(--dsw-alias-state-business-primary)",
24
- holding: "var(--dsw-alias-state-business-primary)",
25
- confirm: "var(--dsw-alias-state-warn-primary)",
26
- stopped: "var(--dsw-alias-state-warn-primary)",
27
- attention: "var(--dsw-alias-state-error-primary)",
28
- revoked: "var(--dsw-alias-label-tertiary)",
29
- unavailable: "var(--dsw-alias-label-tertiary)"
30
- };
31
- /** Normalise a view from the host; an unknown state renders as unavailable. */
32
- function stateOf(view) {
33
- return view && STATES.includes(view.state) ? view.state : "unavailable";
34
- }
35
- const buttonStyle = {
36
- display: "inline-flex",
37
- alignItems: "center",
38
- gap: "6px",
39
- background: "none",
40
- border: "1px solid var(--dsw-alias-border-l2)",
41
- borderRadius: "var(--dsw-radius-sm)",
42
- cursor: "pointer",
43
- padding: "3px 8px",
44
- fontSize: "12px",
45
- lineHeight: "16px",
46
- color: "var(--dsw-alias-label-secondary)"
47
- };
48
- const primaryStyle = { ...buttonStyle, color: "var(--dsw-alias-label-primary)", borderColor: "var(--dsw-alias-label-secondary)", fontWeight: 600 };
49
- const dangerStyle = { ...buttonStyle, color: "var(--dsw-alias-state-error-primary)", borderColor: "var(--dsw-alias-state-error-primary)" };
50
- const linkStyle = { background: "none", border: "none", padding: 0, cursor: "pointer", fontSize: "12px", color: "var(--dsw-alias-label-secondary)", textDecoration: "underline" };
51
- // Popover surface: the theme's documented overlay token (light and dark).
52
- const panelStyle = {
53
- position: "absolute",
54
- bottom: "calc(100% + 6px)",
55
- right: 0,
56
- zIndex: 20,
57
- width: "min(440px, 80vw)",
58
- maxHeight: "50vh",
59
- overflow: "auto",
60
- background: "var(--dsw-alias-bg-overlay)",
61
- border: "1px solid var(--dsw-alias-border-l2)",
62
- borderRadius: "var(--dsw-radius-md, 8px)",
63
- boxShadow: "0 6px 24px var(--dsw-alias-border-l2)",
64
- padding: "10px 12px",
65
- color: "var(--dsw-alias-label-primary)",
66
- fontSize: "12px",
67
- lineHeight: "18px"
68
- };
69
- const mutedStyle = { color: "var(--dsw-alias-label-secondary)" };
70
- const errorStyle = { color: "var(--dsw-alias-state-error-primary)", marginTop: "6px" };
71
- const rowStyle = { display: "flex", flexWrap: "wrap", alignItems: "center", gap: "8px" };
72
- const nameStyle = { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: "var(--dsw-font-markdown-code-block-font-family, ui-monospace, monospace)" };
73
- const dot = (state) => jsx("span", { "aria-hidden": true, style: { display: "inline-block", width: "7px", height: "7px", borderRadius: "50%", background: dotColor[state], flex: "none" } });
74
- /** Clock time for a reservation expiry, in the user's locale. */
75
- function clock(at) {
76
- try { return new Date(at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } catch { return ""; }
77
- }
78
- /** The command a row action runs. Paths are the canonical ones from the view. */
79
- function rowCommand(file) {
80
- if (file.action === "release") return `release ${file.detail.path}`;
81
- if (file.action === "confirm") return `confirm ${file.detail.path}`;
82
- if (file.action === "unlock") return `unlock ${file.detail.path} ${file.detail.generation}`;
83
- return null;
84
- }
85
- /** The one primary action of a state, or null when nothing is needed. */
86
- function primaryOf(state, view) {
87
- if (state === "unavailable" && view.bindingFailure?.code === "binding-duplicate" && view.bindingFailure.retryable === true && view.bindingFailure.inFlight !== true) return { verb: "retry", label: "editLockRetry" };
88
- // Continue editing is one human action: restore authority, then confirm the
89
- // retained files (each through the ordinary per-file check).
90
- // confirm --all only when something waits, so no empty "Confirmed 0 of 0" is recorded.
91
- if (state === "stopped" || state === "revoked") return { verbs: view.files.some((file) => file.mine && file.status === "user-interrupted") ? ["resume", "confirm --all"] : ["resume"], label: "editLockResume" };
92
- if (state === "confirm") return { verb: "confirm --all", label: "editLockConfirmAll" };
93
- if (state === "holding" && view.ownCount > 0) return { verbs: view.files.filter((file) => file.mine && file.action === "release").map(rowCommand), label: "editLockReleaseAll" };
94
- return null;
95
- }
96
- function EditLockPanel(props) {
97
- const t = props.t;
98
- const sessionId = props.sessionId;
99
- const [available, setAvailable] = react.useState(null);
100
- const [open, setOpen] = react.useState(false);
101
- const [view, setView] = react.useState(null);
102
- const [error, setError] = react.useState(null);
103
- const [pending, setPending] = react.useState(false);
104
- const [armedRevoke, setArmedRevoke] = react.useState(false);
105
- // Unlocking takes another session's file away, so like revoke it needs a
106
- // second click; holds the path of the armed row.
107
- const [armedUnlock, setArmedUnlock] = react.useState(null);
108
- const [details, setDetails] = react.useState(false);
109
- const refresh = () => Promise.resolve(props.fetchView?.()).then(
110
- (next) => { if (next) setView(next); },
111
- (reason) => {
112
- const message = reason instanceof Error ? reason.message : String(reason);
113
- setError(message);
114
- setView({ state: "unavailable", reason: message, files: [] });
115
- }
116
- );
117
- react.useEffect(() => {
118
- if (!sessionId) {
119
- setAvailable(false);
120
- return undefined;
121
- }
122
- let alive = true;
123
- Promise.resolve(props.commandsList(sessionId)).then(
124
- (list) => {
125
- if (!alive) return;
126
- const present = Array.isArray(list) && list.some((entry) => entry?.name === "edit-lock");
127
- setAvailable(present);
128
- // One read so the dot is right before the panel is ever opened.
129
- if (present) refresh();
130
- },
131
- () => { if (alive) setAvailable(false); }
132
- );
133
- return () => { alive = false; };
134
- }, [sessionId]);
135
- // Close on outside click: any pointerdown outside this entry's wrapper
136
- // dismisses the panel (Escape on the panel still works as well).
137
- react.useEffect(() => {
138
- if (!open || typeof document === "undefined") return undefined;
139
- const onPointerDown = (event) => {
140
- if (event?.target?.closest?.("[data-weir-edit-lock-root]")) return;
141
- setOpen(false);
142
- setArmedRevoke(false);
143
- setArmedUnlock(null);
144
- };
145
- document.addEventListener("pointerdown", onPointerDown);
146
- return () => { document.removeEventListener("pointerdown", onPointerDown); };
147
- }, [open]);
148
- if (available !== true) return null;
149
- const state = stateOf(view);
150
- /** Run commands in order, stop at the first failure, then re-read the view. */
151
- const run = (verbs) => {
152
- if (pending) return;
153
- setPending(true);
154
- setError(null);
155
- setArmedRevoke(false);
156
- setArmedUnlock(null);
157
- const list = [].concat(verbs).filter(Boolean);
158
- (async () => {
159
- for (const verb of list) {
160
- const outcome = await Promise.resolve(props.runEditLock(verb));
161
- if (outcome?.kind !== "success") throw new Error(outcome?.text ?? "edit-lock failed");
162
- }
163
- })().then(
164
- () => refresh().finally(() => setPending(false)),
165
- (reason) => {
166
- setError(reason instanceof Error ? reason.message : String(reason));
167
- refresh().finally(() => setPending(false));
168
- }
169
- );
170
- };
171
- const toggle = () => {
172
- const next = !open;
173
- setOpen(next);
174
- setArmedRevoke(false);
175
- if (next) refresh();
176
- };
177
- const action = (key, label, onClick, style = buttonStyle) => jsx("button", {
178
- type: "button", style, disabled: pending, onClick,
179
- "data-weir-edit-lock-action": key, children: label
180
- }, key);
181
- const summary = (() => {
182
- if (!view) return t("editLockLoading");
183
- if (state === "unavailable" && view.bindingFailure?.inFlight === true) return t("editLockRetryPending");
184
- if (state === "unavailable" && view.reason) return t("editLockState_failed");
185
- const base = t(`editLockState_${state}`);
186
- if (state === "holding" && view.hold) return `${base} ${t("editLockHoldUntil").replace("{time}", clock(view.hold.until))}`;
187
- if (state === "confirm") return `${base} (${view.pendingCount})`;
188
- return base;
189
- })();
190
- // A cold view (restored session, no live agent) is read-only: every write
191
- // action needs the live agent's command channel, so no button renders.
192
- const cold = view?.cold === true;
193
- const primary = view && !cold ? primaryOf(state, view) : null;
194
- const files = view?.files ?? [];
195
- const canRevoke = !cold && state !== "stopped" && state !== "unavailable" && state !== "revoked";
196
- // Structured binding refusals have localized guidance; retain the complete
197
- // diagnostic on demand, even when no session technical view exists yet.
198
- const bindingRefusal = state === "unavailable" && view?.bindingFailure?.code === "binding-duplicate";
199
- const detailReason = bindingRefusal ? view.reason : null;
200
- const hasDetails = Boolean(view?.technical || detailReason);
201
- // Only the exact reason (or the command's fixed wrapper) is redundant.
202
- // Never infer failure identity or retry eligibility from prose.
203
- const duplicateError = detailReason && (error === detailReason || error === `edit-lock: ${detailReason}`);
204
- const children = [
205
- jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [
206
- dot(state),
207
- jsx("span", { style: { flex: 1, fontWeight: 600 }, "data-weir-edit-lock-summary": "", children: summary }),
208
- jsx("button", { type: "button", style: linkStyle, disabled: pending, onClick: () => refresh(), "data-weir-edit-lock-action": "refresh", title: t("editLockRefresh"), children: "\u21bb" })
209
- ] }, "head"),
210
- view?.reason && !bindingRefusal ? jsx("div", { style: { ...errorStyle, wordBreak: "break-word" }, "data-weir-edit-lock-reason": "", children: view.reason }, "reason") : null,
211
- view?.bindingFailure ? jsx("div", { style: mutedStyle, "data-weir-edit-lock-binding": "", children: t(`editLockBinding_${view.bindingFailure.holderKind ?? "unknown"}`) }, "binding") : null,
212
- state === "revoked" && !cold ? jsx("div", { style: { ...mutedStyle, marginTop: "4px" }, "data-weir-edit-lock-revoked": "", children: t("editLockRevokedHint") }, "revoked") : null,
213
- cold && state === "revoked" ? jsx("div", { style: { ...mutedStyle, marginTop: "4px" }, "data-weir-edit-lock-cold": "", children: t(view.autoResume === false ? "editLockColdRevokedManual" : "editLockColdRevokedAuto") }, "cold") : null,
214
- cold && state === "stopped" ? jsx("div", { style: { ...mutedStyle, marginTop: "4px" }, "data-weir-edit-lock-cold": "", children: t(view.autoResume === false ? "editLockColdResumeManual" : "editLockColdResumeAuto") }, "cold") : null,
215
- // Usage is historical; only the live driver's explicit deadline signals a pause.
216
- // Follow-up locale keys: editLockCleanupOnly, editLockRecoveryUsage,
217
- // editLockPauseUsageHint. Template-layer fallback stays English.
218
- view?.cleanupOnly ? jsx("div", { style: { ...mutedStyle, marginTop: "4px" }, "data-weir-edit-lock-cleanup": "", children: t("editLockCleanupOnly") }, "cleanup") : null,
219
- // Follow-up locale key: editLockPausedUntil, with {time}. Absolute clock,
220
- // not a countdown or a promise to release locks/restore editing at expiry.
221
- !cold && state === "attention" && view?.cleanupOnly && Number.isFinite(view.pausedUntil) && view.pausedUntil > Date.now() ? jsx("div", { style: { ...mutedStyle, marginTop: "4px" }, "data-weir-edit-lock-paused-until": "", children: t("editLockPausedUntil").replace("{time}", clock(view.pausedUntil)) }, "paused-until") : null,
222
- view?.recovery ? jsx("div", { style: { ...mutedStyle, marginTop: "4px" }, "data-weir-edit-lock-recovery": "", children: t("editLockRecoveryUsage").replace("{attempts}", String(view.recovery.attempts)).replace("{elapsed}", String(view.recovery.elapsedSeconds)).replace("{pause}", String(view.recovery.pauseMinutes)) }, "recovery") : null,
223
- view?.recovery?.pauseMinutes > 0 ? jsx("div", { style: { ...mutedStyle, marginTop: "4px" }, "data-weir-edit-lock-pause": "", children: t("editLockPauseUsageHint") }, "pause") : null,
224
- primary ? jsx("div", { style: { marginTop: "8px" }, children: action("primary", t(primary.label), () => run(primary.verbs ?? primary.verb), primaryStyle) }, "primary") : null,
225
- files.length ? jsx("div", { style: { marginTop: "8px" }, "data-weir-edit-lock-files": "", children: files.map((file) => jsxs("div", {
226
- style: { padding: "4px 0", borderTop: "1px solid var(--dsw-alias-border-l2)" },
227
- "data-weir-edit-lock-file": file.detail.path,
228
- children: [
229
- jsxs("div", { style: rowStyle, children: [
230
- jsx("span", { style: nameStyle, title: file.detail.path, children: file.name }),
231
- jsx("span", { style: mutedStyle, children: file.mine ? t(`editLockStatus_${file.status}`) : `${t("editLockOwnerOther")} · ${t(`editLockStatus_${file.status}`)}` }),
232
- cold || !rowCommand(file) ? null
233
- : file.action === "unlock" && armedUnlock !== file.detail.path
234
- ? action(`arm-unlock:${file.detail.path}`, t("editLockRow_unlock"), () => setArmedUnlock(file.detail.path))
235
- : action(`${file.action}:${file.detail.path}`, t(file.action === "unlock" ? "editLockRow_unlockConfirm" : `editLockRow_${file.action}`), () => run(rowCommand(file)), file.action === "unlock" ? dangerStyle : buttonStyle)
236
- ] }),
237
- file.reason ? jsx("div", { style: { ...mutedStyle, overflowWrap: "anywhere" }, "data-weir-edit-lock-file-reason": file.detail.path, children: file.reason }) : null
238
- ]
239
- }, file.detail.path)) }, "files") : null,
240
- // Follow-up locale key: editLockOwnershipHint. Outcomes remain in the
241
- // adjacent tool result; lock rows do not establish a publication result.
242
- files.length ? jsx("div", { style: { ...mutedStyle, marginTop: "6px" }, "data-weir-edit-lock-ownership-hint": "", children: t("editLockOwnershipHint") }, "ownership-hint") : null,
243
- error && !duplicateError ? jsx("div", { style: errorStyle, "data-weir-edit-lock-error": "", children: error }, "error") : null,
244
- jsxs("div", { style: { display: "flex", alignItems: "center", gap: "10px", marginTop: "10px" }, children: [
245
- canRevoke ? (armedRevoke
246
- ? action("stop", t("editLockRevokeConfirm"), () => run("stop"), dangerStyle)
247
- : jsx("button", { type: "button", style: linkStyle, disabled: pending, onClick: () => setArmedRevoke(true), "data-weir-edit-lock-action": "arm-stop", children: t("editLockStop") })) : null,
248
- jsx("span", { style: { flex: 1 } }),
249
- hasDetails ? jsx("button", { type: "button", style: linkStyle, onClick: () => setDetails(!details), "aria-expanded": details, "data-weir-edit-lock-action": "details", children: t("editLockDetails") }) : null
250
- ] }, "foot"),
251
- details && hasDetails ? jsx("pre", {
252
- style: { ...mutedStyle, whiteSpace: "pre-wrap", wordBreak: "break-all", margin: "6px 0 0", fontFamily: "var(--dsw-font-markdown-code-block-font-family, ui-monospace, monospace)" },
253
- "data-weir-edit-lock-details": "",
254
- children: [
255
- ...(detailReason ? [detailReason] : []),
256
- ...(view?.technical ? [
257
- `session ${view.technical.sessionId} \u00b7 epoch ${view.technical.executionEpoch ?? "-"}`,
258
- `root ${view.technical.root ?? "-"} (${view.technical.mode ?? "-"})`
259
- ] : []),
260
- ...files.map((file) => `${file.detail.path} \u00b7 ${file.detail.owner} \u00b7 generation ${file.detail.generation}${file.reason ? ` \u00b7 ${file.reason}` : ""}`)
261
- ].join("\n")
262
- }, "details") : null
263
- ];
264
- return jsxs("span", {
265
- style: { position: "relative", display: "inline-flex" },
266
- "data-weir-edit-lock-root": "",
267
- children: [
268
- jsxs("button", {
269
- type: "button",
270
- style: buttonStyle,
271
- onClick: toggle,
272
- "aria-expanded": open,
273
- "data-weir-edit-lock": "",
274
- "data-weir-edit-lock-state": state,
275
- title: view ? summary : t("editLockTitle"),
276
- children: [dot(state), jsx("span", { children: t("editLockLabel") })]
277
- }),
278
- open ? jsx("div", {
279
- role: "dialog",
280
- "aria-label": t("editLockPanelTitle"),
281
- style: panelStyle,
282
- "data-weir-edit-lock-panel": "",
283
- onKeyDown: (event) => { if (event?.key === "Escape") setOpen(false); },
284
- children
285
- }) : null
286
- ]
287
- });
288
- }
289
- // Locale data face (pure data, NO ctx calls — a chunk factory re-executes
290
- // under HMR, so a chunk-local register would throw on the duplicate
291
- // (namespace, locale) pair): the entry registers these under "settings.weir.edit-lock"
292
- // inside a ctx.effect when this chunk arrives (lib/client.js
293
- // registerChunkDictionaries). Pinned by test/client.test.js's dictionary
294
- // union pin.
295
- const dictionaries = {
296
- en: {
297
- editLockLabel: "Edit Lock",
298
- editLockTitle: "Files this session is editing",
299
- editLockPanelTitle: "Edit Lock",
300
- editLockLoading: "Loading…",
301
- editLockRefresh: "Refresh",
302
- editLockStop: "Revoke editing",
303
- editLockRevokeConfirm: "Revoke now — the assistant stops editing until you continue",
304
- editLockResume: "Continue editing",
305
- editLockRetry: "Retry connection",
306
- editLockRetryPending: "Retrying connection… Editing remains unavailable.",
307
- editLockBinding_serving: "This session is being edited in another attachment right now. Retry the connection when that turn finishes; retry does not continue editing.",
308
- editLockBinding_transferable: "This session's editing authority is held by an inactive attachment of this same session. It attaches here automatically when you send a message; Retry connection also takes it over now (retry does not continue editing).",
309
- editLockBinding_unknown: "The holder cannot be verified. Wait for its disconnect or confirmed process shutdown, then retry. Never remove a live publisher reservation.",
310
- "editLockBinding_cross-process": "The holder is in another process. Disconnect it or confirm that process has shut down before retrying.",
311
- "editLockBinding_revocation-failed": "Durable revocation was not acknowledged. Inspect Edit Lock maintenance; no replacement has been granted.",
312
- editLockConfirmAll: "Continue with these files",
313
- editLockReleaseAll: "Release all files now",
314
- editLockDetails: "Technical details",
315
- editLockOwnerOther: "Another session",
316
- editLockHoldUntil: "until {time}",
317
- editLockRecovery: "The last turn failed; cleaning up (attempt {n}).",
318
- editLockState_idle: "Not editing any file",
319
- editLockCleanupOnly: "Cleanup only: business edits and new ownership are blocked. Release files you no longer need; inspect the tool result before retrying edits.",
320
- editLockRecoveryUsage: "Recorded recovery: {attempts} attempts · {elapsed}s elapsed · {pause} min pause used.",
321
- editLockPauseUsageHint: "Pause usage is cumulative, not a live timer. Pause expiry re-checks recovery; it does not release locks or restore editing.",
322
- editLockPausedUntil: "Recovery paused until {time}.",
323
- editLockOwnershipHint: "Locks show ownership, not write outcomes. For failed edits, inspect the tool result; releasing locks does not undo changes.",
324
- editLockState_editing: "Editing",
325
- editLockState_holding: "Files kept for this session",
326
- editLockState_confirm: "Waiting for you to continue",
327
- editLockState_stopped: "Editing stopped",
328
- editLockState_attention: "Needs your attention",
329
- editLockState_revoked: "Editing revoked by an administrator",
330
- editLockRevokedHint: "An administrative recovery revoked this session's editing authority and released its locks. Continue editing here, or send any message — editing resumes automatically in a new execution epoch; the revocation record is preserved.",
331
- editLockColdRevokedAuto: "An administrative recovery revoked this session's editing authority and released its locks. Send any message in this session — the binding and editing resume automatically.",
332
- editLockColdRevokedManual: "An administrative recovery revoked this session's editing authority and released its locks. Send any message to activate the session, then continue editing from this panel.",
333
- editLockColdResumeAuto: "This session was restored after a restart and is stopped. Send any message — editing resumes automatically, with the files below confirmed again.",
334
- editLockColdResumeManual: "This session was restored after a restart and is stopped. Send any message to activate the session, then continue editing from this panel.",
335
- editLockState_unavailable: "Edit Lock is starting",
336
- editLockState_failed: "Edit Lock is unavailable; edits are refused",
337
- editLockStatus_active: "editing",
338
- "editLockStatus_user-interrupted": "stopped",
339
- "editLockStatus_pending-confirmation": "waiting",
340
- editLockStatus_abnormal: "needs attention",
341
- editLockRow_release: "Release",
342
- editLockRow_confirm: "Continue",
343
- editLockRow_unlock: "Unlock",
344
- editLockRow_unlockConfirm: "Unlock now — the other session loses this file",
345
- },
346
- zh: {
347
- editLockLabel: "编辑锁",
348
- editLockTitle: "本会话正在编辑的文件",
349
- editLockPanelTitle: "编辑锁",
350
- editLockLoading: "加载中…",
351
- editLockRefresh: "刷新",
352
- editLockStop: "收回编辑权",
353
- editLockRevokeConfirm: "确认收回——助手将停止编辑,直到你点继续",
354
- editLockResume: "继续编辑",
355
- editLockRetry: "重试连接",
356
- editLockRetryPending: "正在重试连接……编辑仍不可用。",
357
- editLockBinding_serving: "本会话正在另一个附着位置编辑中。请在该回合结束后重试连接;重试不等于继续编辑。",
358
- editLockBinding_transferable: "本会话的编辑权由同一会话的一个不活跃附着持有。你发送下一条消息时会自动接管;也可以立即点「重试连接」接管(重试不等于继续编辑)。",
359
- editLockBinding_unknown: "无法核实原持有者。请等待原连接断开或确认其进程已退出,再重试;勿删除存活 publisher 的预约。",
360
- "editLockBinding_cross-process": "原持有者位于另一进程。请先断开原连接或确认其进程已退出,再重试。",
361
- "editLockBinding_revocation-failed": "持久撤权尚未确认。请查看编辑锁维护;未授予替代绑定权限。",
362
- editLockConfirmAll: "继续编辑这些文件",
363
- editLockReleaseAll: "立即释放全部文件",
364
- editLockDetails: "技术细节",
365
- editLockOwnerOther: "其他会话",
366
- editLockHoldUntil: "保留至 {time}",
367
- editLockRecovery: "上一回合出错,正在清理(第 {n} 次)。",
368
- editLockState_idle: "未占用任何文件",
369
- editLockCleanupOnly: "仅允许清理:业务编辑和新占用都被拒绝。请释放不再需要的文件,重试前查看工具结果。",
370
- editLockRecoveryUsage: "已记录恢复:{attempts} 次 · 已用 {elapsed} 秒 · 已暂停 {pause} 分钟。",
371
- editLockPauseUsageHint: "暂停时长是累计用量,不是当前倒计时。暂停到期只会重新检查恢复,不释放锁,也不恢复编辑。",
372
- editLockPausedUntil: "恢复已暂停,至 {time} 后重新检查。",
373
- editLockOwnershipHint: "锁只表示归属,不表示写入结果。编辑失败请查看工具结果;释放锁不会撤销已发生的修改。",
374
- editLockState_editing: "正在编辑",
375
- editLockState_holding: "文件为本会话保留",
376
- editLockState_confirm: "等你确认继续",
377
- editLockState_stopped: "编辑已停止",
378
- editLockState_attention: "需要你处理",
379
- editLockState_revoked: "编辑权已被管理员吊销",
380
- editLockRevokedHint: "管理员恢复已吊销本会话的编辑权并释放其锁。点击「继续编辑」或发送任意消息,即在新执行纪元恢复编辑;吊销记录保留。",
381
- editLockColdRevokedAuto: "管理员恢复已吊销本会话的编辑权并释放其锁。在本会话发送任意消息——连接与编辑会自动恢复。",
382
- editLockColdRevokedManual: "管理员恢复已吊销本会话的编辑权并释放其锁。发送任意消息激活会话,然后回到此面板点「继续编辑」。",
383
- editLockColdResumeAuto: "重启后恢复的会话,编辑已停止。发送任意消息即自动恢复编辑(下列文件会一并确认)。",
384
- editLockColdResumeManual: "重启后恢复的会话,编辑已停止。发送任意消息激活会话,然后回到此面板点「继续编辑」。",
385
- editLockState_unavailable: "编辑锁启动中",
386
- editLockState_failed: "编辑锁不可用,编辑会被拒绝",
387
- editLockStatus_active: "编辑中",
388
- "editLockStatus_user-interrupted": "已停止",
389
- "editLockStatus_pending-confirmation": "待确认",
390
- editLockStatus_abnormal: "需处理",
391
- editLockRow_release: "释放",
392
- editLockRow_confirm: "继续",
393
- editLockRow_unlock: "解锁",
394
- editLockRow_unlockConfirm: "确认解锁——对方会失去这个文件",
395
- },
396
- };
397
-
398
- exports.dictionaries = dictionaries;
399
- exports.EditLockPanel = EditLockPanel;
400
- exports.stateOf = stateOf;
401
- exports.primaryOf = primaryOf;
402
- exports.rowCommand = rowCommand;
403
- return module.exports;
404
- }
405
- });
@@ -1,57 +0,0 @@
1
- import { createCallContexts } from './call-context.js'
2
-
3
- /** Trusted tool adapter. The host binds exec.agent to an authenticated execution;
4
- * model arguments never contain epochs, ownership tokens or effective policy.
5
- * @param {Awaited<ReturnType<typeof import('./runtime.js').openEditLockRuntime>>} runtime */
6
- export function createEditLockAdapter(runtime) {
7
- const calls = createCallContexts()
8
- const active = new Set()
9
- const used = new WeakSet()
10
- let closing = false
11
- return Object.freeze({
12
- /** Host-only ingress after effective per-call sandbox policy resolution.
13
- * @param {import('./state.js').Execution} execution
14
- * @param {Parameters<typeof calls.bind>[1]} context */
15
- bind(execution, context) {
16
- if (closing) throw new Error('edit lock adapter closing')
17
- const call = calls.bind(execution, context, origin => {
18
- try { void runtime.control.cancel(origin).catch(() => {}) } catch { /* stale origin cannot cancel a new execution */ }
19
- })
20
- active.add(call)
21
- return call
22
- },
23
- /** @param {object} call
24
- * @param {{tool: 'write'|'hash_edit'|'lsp_rename', filePath: string, args: unknown, content: string,
25
- * batchOwnership?: import('./state.js').Ownership,
26
- * expected: {kind: 'createIfAbsent'}|{kind: 'replaceIfVersion', version: string}}} request */
27
- async publish(call, request) {
28
- if (closing) throw new Error('edit lock adapter closing')
29
- if (used.has(call)) throw new Error('call already submitted')
30
- used.add(call)
31
- calls.inspect(call)
32
- try {
33
- const context = calls.requireLive(call)
34
- const prepared = await runtime.requests.prepare(context.execution, {
35
- ...request, operationId: context.callId, cwd: context.cwd, effectivePolicy: context.effectivePolicy,
36
- }, context.signal)
37
- const operation = prepared.kind === 'history' ? prepared.operation
38
- : prepared.submission ? await runtime.requests.commit(prepared.submission) : null
39
- if (!operation || !['created', 'updated'].includes(operation.phase)) {
40
- throw new Error(`publication not acknowledged: ${operation?.phase ?? 'missing'}`)
41
- }
42
- return operation
43
- } finally {
44
- calls.close(call)
45
- active.delete(call)
46
- }
47
- },
48
- /** Stop dispatch and retain interrupted ownership before host disposal. */
49
- async close() {
50
- closing = true
51
- for (const call of active) calls.revoke(call)
52
- await runtime.close()
53
- for (const call of active) calls.close(call)
54
- active.clear()
55
- },
56
- })
57
- }
@@ -1,149 +0,0 @@
1
- // An administrative disposition is authorization to accept risk, never evidence
2
- // of publication outcome or old-writer quiescence. It lives in the atomic image.
3
- import { createHash } from 'node:crypto'
4
- import { canonical } from './snapshot.js'
5
- import { revokedOwner } from './derived-facts.js'
6
- import { expectedRegistry, isRegistryUpgrade } from './incarnations.js'
7
-
8
- export const LATE_WRITER_RISK = 'Detached historic writers may still modify files after this override.'
9
- export const AUTOMATIC_RECOVERY_ACTOR = 'automatic-dead-process-recovery'
10
- export const ONLINE_RECOVERY_ACTOR = 'online-administrator'
11
- /** Ledger actor vocabulary: the offline settings administrator, the online
12
- * one-click administrator (design D4) and the dead-process automatic
13
- * settlement (design D3) are the only writers. */
14
- const ACTORS = ['authenticated-settings-administrator', AUTOMATIC_RECOVERY_ACTOR, ONLINE_RECOVERY_ACTOR]
15
- export const digest = value => createHash('sha256').update(canonical(value)).digest('hex')
16
- // Epoch-scoped revocation (`revokedOwner`) and the disposition lookup
17
- // (`dispositionFor`) live in derived-facts.js (change edit-lock-deepening,
18
- // design D1) — one pure home shared by the manager status path, the cold view
19
- // and the inspector; `revokedOwner` is imported above for this module's own
20
- // validation. The re-export keeps existing external importers working (the
21
- // integration scenario child imports dispositionFor from this module).
22
- export { dispositionFor, revokedOwner } from './derived-facts.js'
23
-
24
- function requireThat(condition, message) { if (!condition) throw new Error(`invalid administrative recovery: ${message}`) }
25
- function keys(value, names) {
26
- requireThat(value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === names.length && names.every(k => Object.hasOwn(value, k)), 'schema')
27
- }
28
- function text(value, max = 512) { return typeof value === 'string' && value.trim().length > 0 && value.length <= max && !value.includes('\0') }
29
- const natural = value => Number.isSafeInteger(value) && value >= 0
30
-
31
- export function validateAdminLedger(state) {
32
- if (state.version === 4) return
33
- requireThat(Array.isArray(state.adminRecoveries), 'ledger')
34
- // The v5 ledger exists only once an override lands; a v6 image may carry an
35
- // empty one (registry upgrade before any administrative recovery).
36
- if (state.adminRecoveries.length === 0) { requireThat(state.version === 6, 'nonempty v5 ledger'); return }
37
- const ids = new Set()
38
- // An owner may be revoked again after a repair: its rows must carry strictly
39
- // increasing revokedEpochs in ledger order (the epoch only ever advances).
40
- const lastRevokedEpoch = new Map()
41
- // Operations bound by an owner's earlier rows are already disposed; a later
42
- // row of the same owner never re-covers them.
43
- const covered = new Map()
44
- // "Still revoked" is judged against the owner's LATEST row at image time.
45
- const finalRevokedEpoch = new Map()
46
- for (const row of state.adminRecoveries) {
47
- if (natural(row?.revokedEpoch) && typeof row?.owner === 'string') {
48
- finalRevokedEpoch.set(row.owner, Math.max(row.revokedEpoch, finalRevokedEpoch.get(row.owner) ?? -1))
49
- }
50
- }
51
- // Rows committed in one transaction share one expected/committed pair;
52
- // across transactions both revisions are non-decreasing.
53
- let lastExpected = -1, lastCommitted = -1
54
- for (const row of state.adminRecoveries) {
55
- keys(row, ['recoveryId', 'root', 'owner', 'expectedRevision', 'committedRevision', 'at', 'actor', 'reason', 'risk', 'confirmation', 'backup', 'operations', 'releasedLocks', 'revokedEpoch'])
56
- requireThat(text(row.recoveryId, 128) && !ids.has(row.recoveryId) && text(row.root, 4096) && text(row.owner), 'identity')
57
- ids.add(row.recoveryId)
58
- requireThat(natural(row.revokedEpoch) && row.revokedEpoch > (lastRevokedEpoch.get(row.owner) ?? -1), 'revocation epoch progression')
59
- lastRevokedEpoch.set(row.owner, row.revokedEpoch)
60
- requireThat(natural(row.expectedRevision) && natural(row.committedRevision) && row.committedRevision === row.expectedRevision + 1 && row.expectedRevision >= lastExpected && row.committedRevision >= lastCommitted && natural(row.at) && ACTORS.includes(row.actor) && text(row.reason, 2000), 'audit')
61
- lastExpected = row.expectedRevision; lastCommitted = row.committedRevision
62
- requireThat(row.risk === LATE_WRITER_RISK && /^[a-f0-9]{64}$/.test(row.confirmation), 'risk acknowledgement')
63
- keys(row.backup, ['file', 'sha256', 'bytes'])
64
- requireThat(row.backup.file === `admin-backup-${digest([row.root, row.recoveryId])}.json` && /^[a-f0-9]{64}$/.test(row.backup.sha256) && natural(row.backup.bytes) && row.backup.bytes > 0, 'backup')
65
- const session = state.sessions.find(s => s.sessionId === row.owner)
66
- requireThat(session && session.executionEpoch >= row.revokedEpoch, 'revoked owner epoch')
67
- // The row-level invariants of an unsettled revocation bind only an owner
68
- // still revoked at image time; a repaired owner's historical rows stand on
69
- // their append-only bindings alone.
70
- const stillRevoked = session.executionEpoch <= (finalRevokedEpoch.get(row.owner) ?? -1)
71
- requireThat(!stillRevoked || (session.interrupted && !state.locks.some(l => l.owner === row.owner)), 'revoked owner')
72
- requireThat(Array.isArray(row.operations) && row.operations.length > 0, 'operations')
73
- const opIds = new Set()
74
- for (const item of row.operations) {
75
- keys(item, ['operationId', 'sha256'])
76
- requireThat(text(item.operationId, 1024) && !opIds.has(item.operationId), 'operation key')
77
- opIds.add(item.operationId)
78
- const op = state.operations.find(o => o.sessionId === row.owner && o.operationId === item.operationId)
79
- requireThat(op?.phase === 'unknown' && digest(op) === item.sha256, 'immutable operation binding')
80
- }
81
- // The row's responsibility is exactly the owner's unresolved operations
82
- // originating at or before its revokedEpoch (that epoch never ran, so none
83
- // originate AT it), excluding operations its earlier rows already cover.
84
- const disposed = covered.get(row.owner) ?? new Set()
85
- requireThat(state.operations.filter(o => o.sessionId === row.owner && (o.phase === 'unknown' || o.phase === 'publishing' || o.phase === 'prepared')
86
- && (typeof o.origin?.executionEpoch !== 'number' || o.origin.executionEpoch <= row.revokedEpoch)
87
- && !disposed.has(o.operationId)).every(o => opIds.has(o.operationId)), 'complete unresolved owner scope')
88
- covered.set(row.owner, new Set([...disposed, ...opIds]))
89
- requireThat(row.confirmation === digest({ root: row.root, owner: row.owner, expectedRevision: row.expectedRevision, operationIds: [...opIds].sort(), risk: LATE_WRITER_RISK }), 'scoped confirmation')
90
- requireThat(Array.isArray(row.releasedLocks), 'released locks')
91
- const resources = new Set()
92
- for (const lock of row.releasedLocks) {
93
- keys(lock, lock.status === 'abnormal' ? ['resourceId', 'owner', 'generation', 'status', 'reason'] : ['resourceId', 'owner', 'generation', 'status'])
94
- requireThat(['active', 'user-interrupted', 'pending-confirmation', 'abnormal'].includes(lock.status) && (lock.status !== 'abnormal' || text(lock.reason)), 'released lock shape')
95
- requireThat(text(lock.resourceId, 4096) && lock.owner === row.owner && natural(lock.generation) && lock.generation > 0 && !resources.has(lock.resourceId), 'released ownership')
96
- resources.add(lock.resourceId)
97
- requireThat(state.generations.some(g => g.resourceId === lock.resourceId && g.generation >= lock.generation), 'generation tombstone')
98
- }
99
- }
100
- }
101
-
102
- /** Exact coherent transition: only this owner loses ownership; history is unchanged. */
103
- export function administrativeState(before, record) {
104
- requireThat(!revokedOwner(before, record.owner), 'owner already revoked')
105
- const session = before.sessions.find(s => s.sessionId === record.owner)
106
- requireThat(session && Number.isSafeInteger(session.executionEpoch + 1) && record.revokedEpoch === session.executionEpoch + 1, 'revocation epoch')
107
- requireThat(session.interrupted, 'interrupted owner required')
108
- requireThat(canonical(record.releasedLocks) === canonical(before.locks.filter(l => l.owner === record.owner)), 'exact ownership scope')
109
- return { ...structuredClone(before), version: before.version === 6 ? 6 : 5,
110
- adminRecoveries: [...(before.adminRecoveries ?? []), structuredClone(record)],
111
- sessions: before.sessions.map(s => s.sessionId === record.owner ? { ...s, interrupted: true, executionEpoch: record.revokedEpoch } : { ...s }),
112
- locks: before.locks.filter(l => l.owner !== record.owner).map(l => ({ ...l })),
113
- holds: before.holds.map(h => h.sessionId === record.owner ? { ...h, holding: false, holdUntil: null, holdCumulativeMs: 0 } : { ...h }),
114
- }
115
- }
116
-
117
- /**
118
- * Ledger and image-version transition discipline. Without an append the only
119
- * allowed version change is the v6 registry upgrade; the registry itself may
120
- * only ever gain the current manager incarnation's entry (expectedRegistry).
121
- * With appends the transition must be administrative and exactly the fold of
122
- * the appended records — over `before` itself for the single-record offline
123
- * path, or over a caller-supplied `base` (the validated ordinary part of a
124
- * composed commit) for the automatic recovery commit (design D3).
125
- * @param {any} before @param {any} after @param {boolean} [administrative]
126
- * @param {any} [base]
127
- */
128
- export function validateAdminTransition(before, after, administrative = false, base = undefined) {
129
- const old = before.adminRecoveries ?? [], next = after.adminRecoveries ?? []
130
- requireThat(next.length >= old.length && old.every((row, i) => canonical(row) === canonical(next[i])), 'append-only ledger')
131
- if (next.length === old.length) {
132
- if (before.version === after.version) {
133
- requireThat(after.version !== 6 || canonical(after.incarnations) === canonical(expectedRegistry(before, after)), 'incarnation registry')
134
- return
135
- }
136
- requireThat(isRegistryUpgrade(before, after), 'version transition requires explicit override')
137
- return
138
- }
139
- requireThat(administrative, 'explicit administrative operation required')
140
- if (base === undefined) {
141
- requireThat(next.length === old.length + 1, 'explicit administrative operation required')
142
- requireThat(canonical(after) === canonical(administrativeState(before, next.at(-1))), 'coherent authority transition')
143
- return
144
- }
145
- requireThat(canonical(base.adminRecoveries ?? []) === canonical(old), 'administrative base ledger')
146
- let expected = base
147
- for (const record of next.slice(old.length)) expected = administrativeState(expected, record)
148
- requireThat(canonical(after) === canonical(expected), 'coherent authority transition')
149
- }