negotium 0.3.0 → 0.3.2

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 (61) hide show
  1. package/dist/agent-helpers.js +107 -6
  2. package/dist/agent-helpers.js.map +11 -11
  3. package/dist/background-bash.js +3 -2
  4. package/dist/background-bash.js.map +4 -4
  5. package/dist/browser-runtime.js +3 -2
  6. package/dist/browser-runtime.js.map +4 -4
  7. package/dist/chunk-s2gez3wg.js.map +1 -1
  8. package/dist/{chunk-1y2xw0xe.js → chunk-vvfwqxnh.js} +33 -3
  9. package/dist/{chunk-1y2xw0xe.js.map → chunk-vvfwqxnh.js.map} +8 -8
  10. package/dist/hosted-agent.js +34 -4
  11. package/dist/hosted-agent.js.map +8 -8
  12. package/dist/main.js +1179 -366
  13. package/dist/main.js.map +24 -21
  14. package/dist/mcp-catalog.js +2 -1
  15. package/dist/mcp-catalog.js.map +3 -3
  16. package/dist/mcp-factories.js +635 -256
  17. package/dist/mcp-factories.js.map +15 -13
  18. package/dist/mcp-servers.js +3 -1
  19. package/dist/mcp-servers.js.map +3 -3
  20. package/dist/prompts.js +8 -2
  21. package/dist/prompts.js.map +5 -5
  22. package/dist/query-runtime.js +3 -2
  23. package/dist/query-runtime.js.map +4 -4
  24. package/dist/registry.js +3 -3
  25. package/dist/registry.js.map +2 -2
  26. package/dist/rollout.js +1 -1
  27. package/dist/runtime/src/mcp/canonical-bridge-config.ts +1 -1
  28. package/dist/runtime/src/mcp/canonical-proxy-server.ts +74 -1
  29. package/dist/runtime/src/mcp/decision-server.ts +21 -0
  30. package/dist/runtime/src/mcp/factories/decision.ts +178 -0
  31. package/dist/runtime/src/mcp/factories/index.ts +6 -0
  32. package/dist/runtime/src/mcp/runtime-spec.ts +1 -0
  33. package/dist/runtime/src/node-host.ts +1 -0
  34. package/dist/runtime/src/platform/config.ts +2 -1
  35. package/dist/runtime/src/platform/mcp-catalog-policy.ts +1 -0
  36. package/dist/runtime/src/platform/mcp-config.ts +31 -1
  37. package/dist/runtime/src/prompts/builders.ts +5 -0
  38. package/dist/runtime/src/runtime/visual-html.ts +68 -2
  39. package/dist/runtime/src/storage/decisions.ts +231 -0
  40. package/dist/runtime/src/storage/storage-public.ts +2 -0
  41. package/dist/runtime/src/types.ts +13 -0
  42. package/dist/runtime/src/version.ts +1 -1
  43. package/dist/runtime-helpers.js +71 -4
  44. package/dist/runtime-helpers.js.map +5 -5
  45. package/dist/storage.js +261 -56
  46. package/dist/storage.js.map +5 -4
  47. package/dist/types/apps/negotium/src/mcp-servers.d.ts +1 -1
  48. package/dist/types/packages/core/src/mcp/canonical-bridge-config.d.ts +1 -1
  49. package/dist/types/packages/core/src/mcp/factories/decision.d.ts +16 -0
  50. package/dist/types/packages/core/src/mcp/factories/index.d.ts +1 -0
  51. package/dist/types/packages/core/src/mcp/runtime-spec.d.ts +1 -1
  52. package/dist/types/packages/core/src/platform/config.d.ts +2 -1
  53. package/dist/types/packages/core/src/platform/mcp-catalog-policy.d.ts +4 -0
  54. package/dist/types/packages/core/src/storage/decisions.d.ts +45 -0
  55. package/dist/types/packages/core/src/storage/storage-public.d.ts +2 -0
  56. package/dist/types/packages/core/src/types.d.ts +12 -0
  57. package/dist/types/packages/core/src/version.d.ts +1 -1
  58. package/dist/vault.js +3 -2
  59. package/dist/vault.js.map +4 -4
  60. package/install-browser-rs.mjs +3 -3
  61. package/package.json +2 -2
@@ -0,0 +1,231 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { logger } from "#platform/logger";
4
+ import { sanitizeFileName } from "#security/sanitize";
5
+ import { resolveStorageDataDir } from "#storage/storage-host";
6
+ import type { AgentKind, DecisionSnapshot } from "#types";
7
+
8
+ export type StoredDecision = DecisionSnapshot;
9
+
10
+ export const DECISION_STATUS_VALUES = [
11
+ "proposed",
12
+ "accepted",
13
+ "executed",
14
+ "rejected",
15
+ "superseded",
16
+ ] as const;
17
+
18
+ interface DecisionFileShape {
19
+ version: 1;
20
+ decisions: StoredDecision[];
21
+ }
22
+
23
+ function safeDecisionScopeKey(scopeKey: string): string {
24
+ const safe = sanitizeFileName(scopeKey);
25
+ if (!safe || safe === "." || safe === "..") {
26
+ throw new Error(`decisions: refusing unsafe scope key: ${scopeKey}`);
27
+ }
28
+ return safe;
29
+ }
30
+
31
+ export function decisionScopeKey(opts: { topicId?: string; session: string }): string {
32
+ return opts.topicId?.trim() || opts.session || "default";
33
+ }
34
+
35
+ export function getDecisionFilePath(userId: number | string, scopeKey: string): string {
36
+ void userId;
37
+ return join(resolveStorageDataDir(), "decisions", `${safeDecisionScopeKey(scopeKey)}.json`);
38
+ }
39
+
40
+ export function getDecisionGraphSvgPath(userId: number | string, scopeKey: string): string {
41
+ void userId;
42
+ return join(
43
+ resolveStorageDataDir(),
44
+ "decision-renders",
45
+ safeDecisionScopeKey(scopeKey),
46
+ "latest.svg",
47
+ );
48
+ }
49
+
50
+ export function writeDecisionGraphSvg(
51
+ userId: number | string,
52
+ scopeKey: string,
53
+ svg: string,
54
+ ): string {
55
+ const path = getDecisionGraphSvgPath(userId, scopeKey);
56
+ mkdirSync(dirname(path), { recursive: true });
57
+ const tmp = `${path}.${process.pid}.tmp`;
58
+ writeFileSync(tmp, svg, "utf-8");
59
+ renameSync(tmp, path);
60
+ return path;
61
+ }
62
+
63
+ export function readDecisions(userId: number | string, scopeKey: string): StoredDecision[] {
64
+ const path = getDecisionFilePath(userId, scopeKey);
65
+ if (!existsSync(path)) return [];
66
+ try {
67
+ const parsed = JSON.parse(readFileSync(path, "utf-8")) as DecisionFileShape;
68
+ return Array.isArray(parsed?.decisions) ? parsed.decisions : [];
69
+ } catch (error) {
70
+ logger.warn({ err: error, path }, "decisions: failed to read decision store");
71
+ return [];
72
+ }
73
+ }
74
+
75
+ export function writeDecisions(
76
+ userId: number | string,
77
+ scopeKey: string,
78
+ decisions: StoredDecision[],
79
+ ): void {
80
+ const path = getDecisionFilePath(userId, scopeKey);
81
+ mkdirSync(dirname(path), { recursive: true });
82
+ const payload: DecisionFileShape = { version: 1, decisions };
83
+ const tmp = `${path}.${process.pid}.tmp`;
84
+ writeFileSync(tmp, JSON.stringify(payload, null, 2), "utf-8");
85
+ renameSync(tmp, path);
86
+ }
87
+
88
+ export interface DecisionCreateInput {
89
+ action: string;
90
+ reasoning: string;
91
+ agent: AgentKind;
92
+ model?: string;
93
+ status?: StoredDecision["status"];
94
+ causedBy?: string[];
95
+ timestamp?: number;
96
+ }
97
+
98
+ export interface DecisionUpdateInput {
99
+ id: string;
100
+ action?: string;
101
+ reasoning?: string;
102
+ status?: StoredDecision["status"];
103
+ causedBy?: string[];
104
+ }
105
+
106
+ function nextDecisionId(decisions: StoredDecision[]): number {
107
+ let max = 0;
108
+ for (const decision of decisions) {
109
+ const id = Number(decision.id);
110
+ if (Number.isInteger(id) && id > max) max = id;
111
+ }
112
+ return max + 1;
113
+ }
114
+
115
+ function normalizedIds(ids: string[] | undefined): string[] | undefined {
116
+ if (!ids) return undefined;
117
+ const unique = [...new Set(ids.map((id) => id.trim()).filter(Boolean))];
118
+ return unique.length > 0 ? unique : undefined;
119
+ }
120
+
121
+ export function validateDecisionGraph(decisions: StoredDecision[]): void {
122
+ const ids = new Set(decisions.map((decision) => decision.id));
123
+ for (const decision of decisions) {
124
+ for (const upstream of decision.causedBy ?? []) {
125
+ if (!ids.has(upstream)) {
126
+ throw new Error(`Decision #${decision.id} references missing decision #${upstream}.`);
127
+ }
128
+ if (upstream === decision.id) {
129
+ throw new Error(`Decision #${decision.id} cannot cause itself.`);
130
+ }
131
+ }
132
+ }
133
+
134
+ const visiting = new Set<string>();
135
+ const visited = new Set<string>();
136
+ const byId = new Map(decisions.map((decision) => [decision.id, decision]));
137
+ const visit = (id: string): void => {
138
+ if (visited.has(id)) return;
139
+ if (visiting.has(id)) throw new Error(`Decision graph contains a cycle at #${id}.`);
140
+ visiting.add(id);
141
+ for (const upstream of byId.get(id)?.causedBy ?? []) visit(upstream);
142
+ visiting.delete(id);
143
+ visited.add(id);
144
+ };
145
+ for (const id of ids) visit(id);
146
+ }
147
+
148
+ export function createDecisions(
149
+ decisions: StoredDecision[],
150
+ inputs: DecisionCreateInput[],
151
+ ): { decisions: StoredDecision[]; created: StoredDecision[] } {
152
+ const out = [...decisions];
153
+ const created: StoredDecision[] = [];
154
+ let id = nextDecisionId(out);
155
+ for (const input of inputs) {
156
+ const decision: StoredDecision = {
157
+ id: String(id++),
158
+ action: input.action.trim(),
159
+ reasoning: input.reasoning.trim(),
160
+ agent: input.agent,
161
+ status: input.status ?? "accepted",
162
+ timestamp: input.timestamp ?? Date.now(),
163
+ ...(input.model ? { model: input.model } : {}),
164
+ ...(normalizedIds(input.causedBy) ? { causedBy: normalizedIds(input.causedBy) } : {}),
165
+ };
166
+ out.push(decision);
167
+ created.push(decision);
168
+ }
169
+ validateDecisionGraph(out);
170
+ return { decisions: out, created };
171
+ }
172
+
173
+ export function updateDecisions(
174
+ decisions: StoredDecision[],
175
+ updates: DecisionUpdateInput[],
176
+ ): { decisions: StoredDecision[]; missing: string[] } {
177
+ const out = decisions.map((decision) => ({
178
+ ...decision,
179
+ ...(decision.causedBy ? { causedBy: [...decision.causedBy] } : {}),
180
+ }));
181
+ const byId = new Map(out.map((decision) => [decision.id, decision]));
182
+ const missing: string[] = [];
183
+ for (const update of updates) {
184
+ const decision = byId.get(update.id);
185
+ if (!decision) {
186
+ missing.push(update.id);
187
+ continue;
188
+ }
189
+ if (update.action !== undefined) decision.action = update.action.trim();
190
+ if (update.reasoning !== undefined) decision.reasoning = update.reasoning.trim();
191
+ if (update.status !== undefined) decision.status = update.status;
192
+ if (update.causedBy !== undefined) {
193
+ const ids = normalizedIds(update.causedBy);
194
+ if (ids) decision.causedBy = ids;
195
+ else delete decision.causedBy;
196
+ }
197
+ }
198
+ validateDecisionGraph(out);
199
+ return { decisions: out, missing };
200
+ }
201
+
202
+ export function deleteDecisions(
203
+ decisions: StoredDecision[],
204
+ opts: { ids?: string[]; all?: boolean },
205
+ ): { decisions: StoredDecision[]; removed: number } {
206
+ if (opts.all) return { decisions: [], removed: decisions.length };
207
+ const ids = new Set(opts.ids ?? []);
208
+ const kept = decisions
209
+ .filter((decision) => !ids.has(decision.id))
210
+ .map((decision) => {
211
+ const causedBy = decision.causedBy?.filter((id) => !ids.has(id));
212
+ const next = { ...decision };
213
+ if (causedBy && causedBy.length > 0) next.causedBy = causedBy;
214
+ else delete next.causedBy;
215
+ return next;
216
+ });
217
+ return { decisions: kept, removed: decisions.length - kept.length };
218
+ }
219
+
220
+ export function renderDecisionList(decisions: StoredDecision[]): string {
221
+ if (decisions.length === 0) return "Decisions (0 recorded)";
222
+ return [
223
+ `Decisions (${decisions.length} recorded)`,
224
+ ...decisions.map((decision) => {
225
+ const causes = decision.causedBy?.length
226
+ ? ` <- ${decision.causedBy.map((id) => `#${id}`).join(", ")}`
227
+ : "";
228
+ return `[${decision.status}] #${decision.id} ${decision.action}${causes}\n ${decision.reasoning}`;
229
+ }),
230
+ ].join("\n");
231
+ }
@@ -20,6 +20,8 @@ export * as askUserGates from "#storage/ask-user-gates";
20
20
  export * from "#storage/ask-user-gates";
21
21
  export * as conversations from "#storage/conversations";
22
22
  export * from "#storage/conversations";
23
+ export * as decisions from "#storage/decisions";
24
+ export * from "#storage/decisions";
23
25
  export type {
24
26
  ForumTopicInfo,
25
27
  TopicRow,
@@ -97,6 +97,19 @@ export interface TaskSnapshot {
97
97
  owner?: string;
98
98
  }
99
99
 
100
+ /** One topic-scoped decision and its incoming causal edges. */
101
+ export interface DecisionSnapshot {
102
+ id: string;
103
+ action: string;
104
+ reasoning: string;
105
+ agent: AgentKind;
106
+ model?: string;
107
+ status: "proposed" | "accepted" | "executed" | "rejected" | "superseded";
108
+ /** Upstream decision ids. Each entry forms a directed upstream -> this edge. */
109
+ causedBy?: string[];
110
+ timestamp: number;
111
+ }
112
+
100
113
  export type UnifiedEvent =
101
114
  | {
102
115
  type: "user_message";
@@ -1 +1 @@
1
- export const NEGOTIUM_VERSION = "0.3.0";
1
+ export const NEGOTIUM_VERSION = "0.3.2";
@@ -535,7 +535,7 @@ var DM_WORKSPACE_DIR = resolve(STATE_DIR, "data", "dm");
535
535
  var SESSION_WORKSPACE_DIR = resolve(STATE_DIR, "data", "sessions");
536
536
  var CLAUDE_EXECUTABLE_ENV = envText("NEGOTIUM_CLAUDE_EXECUTABLE");
537
537
  var CLAUDE_EXECUTABLE = CLAUDE_EXECUTABLE_ENV ? resolve(CLAUDE_EXECUTABLE_ENV) : undefined;
538
- var BROWSER_RS_VERSION = "v0.1.19";
538
+ var BROWSER_RS_VERSION = "v0.1.21";
539
539
  var BROWSER_RS_MIN_SECURE_VERSION = "0.1.15";
540
540
  function versionAtLeast(actualVersion, minimumVersion) {
541
541
  const actual = actualVersion.split(".").map(Number);
@@ -605,6 +605,7 @@ var TSX_LOADER = createRequire(import.meta.url).resolve("tsx");
605
605
  var TSCONFIG_PATH = resolve(PROJECT_ROOT, "tsconfig.json");
606
606
  var SESSION_COMM_SERVER = resolve(PROJECT_ROOT, "src/mcp/session-comm/server.ts");
607
607
  var TASK_SERVER = resolve(PROJECT_ROOT, "src/mcp/task-server.ts");
608
+ var DECISION_SERVER = resolve(PROJECT_ROOT, "src/mcp/decision-server.ts");
608
609
  var BROWSER_MCP_SSE_PROXY_SERVER = resolve(PROJECT_ROOT, "src/mcp/browser-sse-proxy-server.ts");
609
610
  var CANONICAL_MCP_PROXY_SERVER = resolve(PROJECT_ROOT, "src/mcp/canonical-proxy-server.ts");
610
611
  var WIKI_SERVER = resolve(PROJECT_ROOT, "src/mcp/wiki-server.ts");
@@ -1439,6 +1440,10 @@ function buildMermaidHtml(code, theme, scriptUrl = MERMAID_BROWSER_ASSET_RELATIV
1439
1440
  .controls button:focus-visible{outline:2px solid var(--celadon);outline-offset:1px}
1440
1441
  .zoom-value{min-width:46px;color:var(--graphite);font:500 11px/1 Geist,system-ui,sans-serif;text-align:center;font-variant-numeric:tabular-nums}
1441
1442
  .error{margin:0;white-space:pre-wrap;color:#7D2E2E;background:#FFF5F2;border:1px solid #E4B9B1;border-radius:6px;padding:14px;font:13px ui-monospace,SFMono-Regular,Menlo,monospace}
1443
+ .failure{max-width:56ch;margin:0 auto}
1444
+ .failure p{margin:0 0 12px;color:var(--graphite);font:14px/1.6 Geist,system-ui,sans-serif}
1445
+ .failure details{color:var(--graphite);font:12px/1.5 Geist,system-ui,sans-serif}
1446
+ .failure summary{cursor:pointer;padding:4px 0}
1442
1447
  @media(max-width:600px){.viewport{padding:54px 14px 18px}.controls{top:10px;right:10px}}
1443
1448
  </style>
1444
1449
  </head>
@@ -1453,11 +1458,58 @@ function buildMermaidHtml(code, theme, scriptUrl = MERMAID_BROWSER_ASSET_RELATIV
1453
1458
  <script data-otium-mermaid-runtime src="${safeScriptUrl}"></script>
1454
1459
  <script>
1455
1460
  (async () => {
1461
+ // An unrendered document reports itself in more than one voice: Mermaid's
1462
+ // own 0x0 guard, and the browser refusing geometry on a path that was
1463
+ // never laid out. Both mean the same thing, so both are worth one retry
1464
+ // and, if it still fails, the same explanation.
1465
+ const unrendered = (error) => {
1466
+ const message = String(error && error.message ? error.message : error);
1467
+ return message.indexOf("not in render tree") !== -1 || message.indexOf("path is empty") !== -1;
1468
+ };
1456
1469
  try {
1457
1470
  const runtime = globalThis.mermaid;
1458
1471
  if (!runtime) throw new Error("Mermaid renderer failed to load.");
1459
1472
  runtime.initialize({ startOnLoad: false, securityLevel: "strict", theme: ${safeTheme} });
1460
- await runtime.run({ querySelector: ".mermaid" });
1473
+ const host = document.querySelector(".mermaid");
1474
+ // Mermaid sizes every label by appending a probe <svg> to the body and
1475
+ // reading getBBox(), and it throws "svg element not in render tree" the
1476
+ // moment that comes back 0x0. That is what a hidden panel looks like from
1477
+ // in here: the document exists but nothing is in the render tree, so the
1478
+ // measurement has no geometry to report. Ask the same question Mermaid
1479
+ // will ask, and only start once it has an answer.
1480
+ const measurable = () => {
1481
+ const probe = document.createElementNS("http://www.w3.org/2000/svg", "svg");
1482
+ const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
1483
+ text.textContent = "M";
1484
+ probe.appendChild(text);
1485
+ document.body.appendChild(probe);
1486
+ let box = { width: 0, height: 0 };
1487
+ try { box = text.getBBox(); } catch (ignored) {}
1488
+ probe.remove();
1489
+ return box.width > 0 || box.height > 0;
1490
+ };
1491
+ // requestAnimationFrame is the right clock here: a hidden document stops
1492
+ // being animated, so this waits without spinning and resumes on the frame
1493
+ // the panel is shown. The cap counts rendered frames, not wall time.
1494
+ const waitUntilMeasurable = async (maxFrames) => {
1495
+ for (let frame = 0; frame < maxFrames; frame += 1) {
1496
+ if (measurable()) return true;
1497
+ await new Promise((next) => requestAnimationFrame(next));
1498
+ }
1499
+ return measurable();
1500
+ };
1501
+ await waitUntilMeasurable(600);
1502
+ try {
1503
+ await runtime.run({ querySelector: ".mermaid" });
1504
+ } catch (firstAttempt) {
1505
+ if (!unrendered(firstAttempt)) throw firstAttempt;
1506
+ // The panel can be hidden again between the probe and the real measure.
1507
+ // Clear the marker Mermaid leaves behind so the retry is not skipped as
1508
+ // already done, then wait for the render tree once more.
1509
+ host.removeAttribute("data-processed");
1510
+ await waitUntilMeasurable(600);
1511
+ await runtime.run({ querySelector: ".mermaid" });
1512
+ }
1461
1513
  const viewport = document.querySelector(".viewport");
1462
1514
  const svg = document.querySelector(".mermaid svg");
1463
1515
  const value = document.querySelector(".zoom-value");
@@ -1484,7 +1536,22 @@ function buildMermaidHtml(code, theme, scriptUrl = MERMAID_BROWSER_ASSET_RELATIV
1484
1536
  applyScale(scale, false);
1485
1537
  } catch (error) {
1486
1538
  document.querySelector(".controls")?.remove();
1487
- document.querySelector(".viewport").innerHTML = '<pre class="error">' + String(error && error.message ? error.message : error).replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c])) + '</pre>';
1539
+ const raw = String(error && error.message ? error.message : error);
1540
+ const escape = (value) => value.replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
1541
+ // Mermaid aborts the whole render when it cannot place an edge label:
1542
+ // cardinality markers ask for a point a fixed distance along the edge,
1543
+ // and a relation laid out shorter than that walks off the end. Nothing
1544
+ // is wrong with the diagram, so the raw message sends authors looking
1545
+ // for a syntax error that does not exist. Say what actually moves it.
1546
+ const note = raw.indexOf("Could not find a suitable point") !== -1
1547
+ ? "Two nodes ended up too close together for Mermaid to fit a label on the edge between them. Renaming a node, adding another, or setting an explicit direction usually spreads the layout enough to render."
1548
+ : unrendered(error)
1549
+ ? "The panel stayed hidden long enough that there was never a laid-out page to measure the diagram against. Reopening the panel renders it."
1550
+ : "This diagram could not be rendered.";
1551
+ document.querySelector(".viewport").innerHTML =
1552
+ '<div class="failure"><p>' + escape(note) +
1553
+ '</p><details><summary>Technical detail</summary><pre class="error">' +
1554
+ escape(raw) + '</pre></details></div>';
1488
1555
  }
1489
1556
  })();
1490
1557
  </script>
@@ -1596,4 +1663,4 @@ export {
1596
1663
  CLAUDE_EFFORT_VALUES
1597
1664
  };
1598
1665
 
1599
- //# debugId=4D0763B626E155F864756E2164756E21
1666
+ //# debugId=002587304D7533BD64756E2164756E21