negotium 0.3.0 → 0.3.1

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 +57 -5
  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 +1129 -365
  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 +585 -255
  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 +18 -1
  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 +21 -3
  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.1";
@@ -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>
@@ -1484,7 +1489,20 @@ function buildMermaidHtml(code, theme, scriptUrl = MERMAID_BROWSER_ASSET_RELATIV
1484
1489
  applyScale(scale, false);
1485
1490
  } catch (error) {
1486
1491
  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>';
1492
+ const raw = String(error && error.message ? error.message : error);
1493
+ const escape = (value) => value.replace(/[&<>]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));
1494
+ // Mermaid aborts the whole render when it cannot place an edge label:
1495
+ // cardinality markers ask for a point a fixed distance along the edge,
1496
+ // and a relation laid out shorter than that walks off the end. Nothing
1497
+ // is wrong with the diagram, so the raw message sends authors looking
1498
+ // for a syntax error that does not exist. Say what actually moves it.
1499
+ const note = raw.indexOf("Could not find a suitable point") !== -1
1500
+ ? "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."
1501
+ : "This diagram could not be rendered.";
1502
+ document.querySelector(".viewport").innerHTML =
1503
+ '<div class="failure"><p>' + escape(note) +
1504
+ '</p><details><summary>Technical detail</summary><pre class="error">' +
1505
+ escape(raw) + '</pre></details></div>';
1488
1506
  }
1489
1507
  })();
1490
1508
  </script>
@@ -1596,4 +1614,4 @@ export {
1596
1614
  CLAUDE_EFFORT_VALUES
1597
1615
  };
1598
1616
 
1599
- //# debugId=4D0763B626E155F864756E2164756E21
1617
+ //# debugId=958B6D1C218A4DA464756E2164756E21