presubmit 0.1.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 (220) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +51 -0
  3. package/THIRD_PARTY_NOTICES.md +24 -0
  4. package/apps/web/dist/assets/FileCode-CZZH52o7.js +5 -0
  5. package/apps/web/dist/assets/FileCode-DovDO7Jh.css +1 -0
  6. package/apps/web/dist/assets/SettingsPage-CRvOwdjt.js +4 -0
  7. package/apps/web/dist/assets/SettingsPage-UbYSCwJj.css +1 -0
  8. package/apps/web/dist/assets/TerminalPanel-0sw-J6YS.css +1 -0
  9. package/apps/web/dist/assets/TerminalPanel-BqUjPWav.js +16 -0
  10. package/apps/web/dist/assets/index-BhqNNFja.js +54 -0
  11. package/apps/web/dist/assets/index-DD8VXy8H.css +1 -0
  12. package/apps/web/dist/assets/presubmit-mark-BBQ19Fmr.svg +7 -0
  13. package/apps/web/dist/assets/presubmit-mark-busy-CwBC-7on.svg +8 -0
  14. package/apps/web/dist/assets/react-vendor-wiHys0m2.js +9 -0
  15. package/apps/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
  16. package/apps/web/dist/index.html +6 -0
  17. package/apps/web/dist/service-worker.js +11 -0
  18. package/dist/apps/server/src/agent-builder.js +204 -0
  19. package/dist/apps/server/src/attach.js +163 -0
  20. package/dist/apps/server/src/attachments.js +86 -0
  21. package/dist/apps/server/src/browser-access.js +131 -0
  22. package/dist/apps/server/src/check-provenance.js +95 -0
  23. package/dist/apps/server/src/check-supervisor.js +112 -0
  24. package/dist/apps/server/src/checks.js +402 -0
  25. package/dist/apps/server/src/child-collaboration-guidance.js +2 -0
  26. package/dist/apps/server/src/child-sessions.js +191 -0
  27. package/dist/apps/server/src/cli.js +162 -0
  28. package/dist/apps/server/src/conversation-search.js +12 -0
  29. package/dist/apps/server/src/coordinator-messages.js +72 -0
  30. package/dist/apps/server/src/dev-auth.js +54 -0
  31. package/dist/apps/server/src/dev-server.js +201 -0
  32. package/dist/apps/server/src/dev-worker.js +48 -0
  33. package/dist/apps/server/src/diagnostics.js +105 -0
  34. package/dist/apps/server/src/fast-mode.js +8 -0
  35. package/dist/apps/server/src/folder-picker.js +54 -0
  36. package/dist/apps/server/src/git-blobs.js +62 -0
  37. package/dist/apps/server/src/git-handoff.js +127 -0
  38. package/dist/apps/server/src/history-sources.js +59 -0
  39. package/dist/apps/server/src/hub.js +159 -0
  40. package/dist/apps/server/src/internal-transport.js +38 -0
  41. package/dist/apps/server/src/local-embeddings.js +70 -0
  42. package/dist/apps/server/src/ollama-setup.js +56 -0
  43. package/dist/apps/server/src/planning.js +30 -0
  44. package/dist/apps/server/src/project-agents.js +276 -0
  45. package/dist/apps/server/src/project-memory.js +94 -0
  46. package/dist/apps/server/src/projects.js +106 -0
  47. package/dist/apps/server/src/queued-launch.js +13 -0
  48. package/dist/apps/server/src/reply-search.js +172 -0
  49. package/dist/apps/server/src/review-context.js +60 -0
  50. package/dist/apps/server/src/review-refresh.js +14 -0
  51. package/dist/apps/server/src/reviewers.js +75 -0
  52. package/dist/apps/server/src/search-settings.js +22 -0
  53. package/dist/apps/server/src/semantic-history.js +186 -0
  54. package/dist/apps/server/src/server.js +4338 -0
  55. package/dist/apps/server/src/session-changes.js +181 -0
  56. package/dist/apps/server/src/session-router-settings.js +198 -0
  57. package/dist/apps/server/src/session-workspaces.js +135 -0
  58. package/dist/apps/server/src/sessions.js +579 -0
  59. package/dist/apps/server/src/shared-auth.js +70 -0
  60. package/dist/apps/server/src/shell-evaluator-settings.js +53 -0
  61. package/dist/apps/server/src/store.js +203 -0
  62. package/dist/apps/server/src/tailscale-process.js +115 -0
  63. package/dist/apps/server/src/tailscale-serve-worker.js +33 -0
  64. package/dist/apps/server/src/tailscale-serve.js +276 -0
  65. package/dist/apps/server/src/task-capacity.js +92 -0
  66. package/dist/apps/server/src/task-graph.js +190 -0
  67. package/dist/apps/server/src/task-logs.js +74 -0
  68. package/dist/apps/server/src/task-numbers.js +25 -0
  69. package/dist/apps/server/src/task-orchestrator.js +1088 -0
  70. package/dist/apps/server/src/terminal.js +182 -0
  71. package/dist/apps/server/src/terminals.js +80 -0
  72. package/dist/apps/server/src/tool-safety.js +2 -0
  73. package/dist/apps/server/src/workspace-lock.js +140 -0
  74. package/dist/apps/server/src/workspace-watcher.js +65 -0
  75. package/dist/apps/server/src/workspace.js +517 -0
  76. package/dist/packages/agent-pi/src/agent-builder-tool.js +32 -0
  77. package/dist/packages/agent-pi/src/agent-capabilities.js +44 -0
  78. package/dist/packages/agent-pi/src/ask-user.js +86 -0
  79. package/dist/packages/agent-pi/src/auth.js +510 -0
  80. package/dist/packages/agent-pi/src/check-tools.js +72 -0
  81. package/dist/packages/agent-pi/src/command-policy.js +246 -0
  82. package/dist/packages/agent-pi/src/coordinator-tools.js +122 -0
  83. package/dist/packages/agent-pi/src/fast-mode.js +67 -0
  84. package/dist/packages/agent-pi/src/file-tools.js +163 -0
  85. package/dist/packages/agent-pi/src/history-tool.js +40 -0
  86. package/dist/packages/agent-pi/src/history.js +34 -0
  87. package/dist/packages/agent-pi/src/index.js +177 -0
  88. package/dist/packages/agent-pi/src/ipc.js +314 -0
  89. package/dist/packages/agent-pi/src/jev-session-router.js +408 -0
  90. package/dist/packages/agent-pi/src/jev-tools.js +71 -0
  91. package/dist/packages/agent-pi/src/local-model.js +277 -0
  92. package/dist/packages/agent-pi/src/memory-tools.js +119 -0
  93. package/dist/packages/agent-pi/src/model.js +25 -0
  94. package/dist/packages/agent-pi/src/planning.js +206 -0
  95. package/dist/packages/agent-pi/src/resource-loader.js +23 -0
  96. package/dist/packages/agent-pi/src/review-tools.js +158 -0
  97. package/dist/packages/agent-pi/src/review-transition.js +73 -0
  98. package/dist/packages/agent-pi/src/review.js +40 -0
  99. package/dist/packages/agent-pi/src/routing.js +27 -0
  100. package/dist/packages/agent-pi/src/session-setup.js +118 -0
  101. package/dist/packages/agent-pi/src/shell-context.js +51 -0
  102. package/dist/packages/agent-pi/src/shell-policy.js +281 -0
  103. package/dist/packages/agent-pi/src/shell-safety.js +205 -0
  104. package/dist/packages/agent-pi/src/shell.js +121 -0
  105. package/dist/packages/agent-pi/src/snapshot-permissions.js +12 -0
  106. package/dist/packages/agent-pi/src/spike.js +41 -0
  107. package/dist/packages/agent-pi/src/task-tools.js +53 -0
  108. package/dist/packages/agent-pi/src/types.js +1 -0
  109. package/dist/packages/agent-pi/src/usage.js +30 -0
  110. package/dist/packages/agent-pi/src/worker.js +602 -0
  111. package/dist/packages/protocol/src/activity-summary.js +77 -0
  112. package/dist/packages/protocol/src/agents.js +301 -0
  113. package/dist/packages/protocol/src/browser-access.js +1 -0
  114. package/dist/packages/protocol/src/checks.js +40 -0
  115. package/dist/packages/protocol/src/child-sessions.js +28 -0
  116. package/dist/packages/protocol/src/command-policy.js +3 -0
  117. package/dist/packages/protocol/src/coordinator.js +135 -0
  118. package/dist/packages/protocol/src/delegation.js +40 -0
  119. package/dist/packages/protocol/src/git-diff.js +71 -0
  120. package/dist/packages/protocol/src/git-handoff.js +1 -0
  121. package/dist/packages/protocol/src/index.js +1 -0
  122. package/dist/packages/protocol/src/memory.js +46 -0
  123. package/dist/packages/protocol/src/metrics.js +70 -0
  124. package/dist/packages/protocol/src/planning.js +138 -0
  125. package/dist/packages/protocol/src/retrieval.js +36 -0
  126. package/dist/packages/protocol/src/reviewers.js +17 -0
  127. package/dist/packages/protocol/src/search.js +16 -0
  128. package/dist/packages/protocol/src/session-changes.js +29 -0
  129. package/dist/packages/protocol/src/session-router.js +123 -0
  130. package/dist/packages/protocol/src/shell-evaluator.js +43 -0
  131. package/dist/packages/protocol/src/task-graph.js +1 -0
  132. package/dist/packages/protocol/src/token-usage.js +17 -0
  133. package/dist/packages/protocol/src/user-questions.js +106 -0
  134. package/package.json +71 -0
  135. package/vendor/lancedb/NODEJS_THIRD_PARTY_LICENSES.md +668 -0
  136. package/vendor/lancedb/RUST_THIRD_PARTY_LICENSES.html +14607 -0
  137. package/vendor/lancedb/dist/arrow.d.ts +296 -0
  138. package/vendor/lancedb/dist/arrow.js +1145 -0
  139. package/vendor/lancedb/dist/arrow_type.d.ts +9 -0
  140. package/vendor/lancedb/dist/arrow_type.js +29 -0
  141. package/vendor/lancedb/dist/connection.d.ts +508 -0
  142. package/vendor/lancedb/dist/connection.js +298 -0
  143. package/vendor/lancedb/dist/embedding/embedding_function.d.ts +103 -0
  144. package/vendor/lancedb/dist/embedding/embedding_function.js +192 -0
  145. package/vendor/lancedb/dist/embedding/index.d.ts +37 -0
  146. package/vendor/lancedb/dist/embedding/index.js +108 -0
  147. package/vendor/lancedb/dist/embedding/openai.d.ts +16 -0
  148. package/vendor/lancedb/dist/embedding/openai.js +81 -0
  149. package/vendor/lancedb/dist/embedding/registry.d.ts +94 -0
  150. package/vendor/lancedb/dist/embedding/registry.js +221 -0
  151. package/vendor/lancedb/dist/embedding/transformers.d.ts +36 -0
  152. package/vendor/lancedb/dist/embedding/transformers.js +110 -0
  153. package/vendor/lancedb/dist/header.d.ts +162 -0
  154. package/vendor/lancedb/dist/header.js +217 -0
  155. package/vendor/lancedb/dist/index.d.ts +246 -0
  156. package/vendor/lancedb/dist/index.js +176 -0
  157. package/vendor/lancedb/dist/indices.d.ts +721 -0
  158. package/vendor/lancedb/dist/indices.js +166 -0
  159. package/vendor/lancedb/dist/materialized_view.d.ts +69 -0
  160. package/vendor/lancedb/dist/materialized_view.js +112 -0
  161. package/vendor/lancedb/dist/merge.d.ts +104 -0
  162. package/vendor/lancedb/dist/merge.js +120 -0
  163. package/vendor/lancedb/dist/native.d.ts +1193 -0
  164. package/vendor/lancedb/dist/native.js +688 -0
  165. package/vendor/lancedb/dist/oauth.d.ts +66 -0
  166. package/vendor/lancedb/dist/oauth.js +15 -0
  167. package/vendor/lancedb/dist/otel.d.ts +26 -0
  168. package/vendor/lancedb/dist/otel.js +114 -0
  169. package/vendor/lancedb/dist/permutation.d.ts +143 -0
  170. package/vendor/lancedb/dist/permutation.js +184 -0
  171. package/vendor/lancedb/dist/query.d.ts +663 -0
  172. package/vendor/lancedb/dist/query.js +966 -0
  173. package/vendor/lancedb/dist/rerankers/index.d.ts +5 -0
  174. package/vendor/lancedb/dist/rerankers/index.js +19 -0
  175. package/vendor/lancedb/dist/rerankers/rrf.d.ts +14 -0
  176. package/vendor/lancedb/dist/rerankers/rrf.js +28 -0
  177. package/vendor/lancedb/dist/sanitize.d.ts +32 -0
  178. package/vendor/lancedb/dist/sanitize.js +544 -0
  179. package/vendor/lancedb/dist/scannable.d.ts +92 -0
  180. package/vendor/lancedb/dist/scannable.js +200 -0
  181. package/vendor/lancedb/dist/schema.d.ts +16 -0
  182. package/vendor/lancedb/dist/schema.js +387 -0
  183. package/vendor/lancedb/dist/table.d.ts +1069 -0
  184. package/vendor/lancedb/dist/table.js +498 -0
  185. package/vendor/lancedb/dist/util.d.ts +14 -0
  186. package/vendor/lancedb/dist/util.js +77 -0
  187. package/vendor/lancedb/license_header.txt +2 -0
  188. package/vendor/lancedb/package.json +56 -0
  189. package/vendor/transformers/LICENSE +202 -0
  190. package/vendor/transformers/dist/transformers.cjs +30844 -0
  191. package/vendor/transformers/dist/transformers.mjs +31404 -0
  192. package/vendor/transformers/package.json +55 -0
  193. package/vendor/transformers/types/backends/onnx.d.ts +30 -0
  194. package/vendor/transformers/types/configs.d.ts +85 -0
  195. package/vendor/transformers/types/env.d.ts +110 -0
  196. package/vendor/transformers/types/generation/configuration_utils.d.ts +320 -0
  197. package/vendor/transformers/types/generation/logits_process.d.ts +354 -0
  198. package/vendor/transformers/types/generation/logits_sampler.d.ts +51 -0
  199. package/vendor/transformers/types/generation/parameters.d.ts +47 -0
  200. package/vendor/transformers/types/generation/stopping_criteria.d.ts +81 -0
  201. package/vendor/transformers/types/generation/streamers.d.ts +81 -0
  202. package/vendor/transformers/types/models/whisper/common_whisper.d.ts +8 -0
  203. package/vendor/transformers/types/models/whisper/generation_whisper.d.ts +76 -0
  204. package/vendor/transformers/types/models.d.ts +3684 -0
  205. package/vendor/transformers/types/ops/registry.d.ts +11 -0
  206. package/vendor/transformers/types/pipelines.d.ts +2402 -0
  207. package/vendor/transformers/types/processors.d.ts +924 -0
  208. package/vendor/transformers/types/tokenizers.d.ts +990 -0
  209. package/vendor/transformers/types/transformers.d.ts +13 -0
  210. package/vendor/transformers/types/utils/audio.d.ts +130 -0
  211. package/vendor/transformers/types/utils/constants.d.ts +2 -0
  212. package/vendor/transformers/types/utils/core.d.ts +98 -0
  213. package/vendor/transformers/types/utils/data-structures.d.ts +236 -0
  214. package/vendor/transformers/types/utils/devices.d.ts +18 -0
  215. package/vendor/transformers/types/utils/dtypes.d.ts +19 -0
  216. package/vendor/transformers/types/utils/generic.d.ts +11 -0
  217. package/vendor/transformers/types/utils/hub.d.ts +152 -0
  218. package/vendor/transformers/types/utils/image.d.ts +119 -0
  219. package/vendor/transformers/types/utils/maths.d.ts +280 -0
  220. package/vendor/transformers/types/utils/tensor.d.ts +418 -0
@@ -0,0 +1,159 @@
1
+ import { streamAttachment } from './attach.js';
2
+ import { createHash } from 'node:crypto';
3
+ import { createConnection, createServer } from 'node:net';
4
+ import { chmod, lstat, unlink } from 'node:fs/promises';
5
+ import { acquireWorkspaceLock, WorkspaceAlreadyRunningError } from './workspace-lock.js';
6
+ import { tailscaleOrigin } from './browser-access.js';
7
+ export function hubAddress(home) {
8
+ return `/tmp/presubmit-hub-${process.getuid?.() ?? 'user'}-${createHash('sha256').update(home).digest('hex').slice(0, 20)}.sock`;
9
+ }
10
+ class HostStoppingError extends Error {
11
+ }
12
+ async function register(home, workspace, tailscale) {
13
+ return new Promise((resolve, reject) => {
14
+ const socket = createConnection(hubAddress(home));
15
+ let bytes = '';
16
+ socket.setTimeout(30000, () => socket.destroy(new Error('Presubmit did not finish opening the project. Please retry.')));
17
+ socket.on('connect', () => socket.write(JSON.stringify({ workspace, tailscale }) + '\n'));
18
+ socket.on('data', (chunk) => {
19
+ bytes += chunk.toString();
20
+ if (bytes.length > 16384)
21
+ socket.destroy(new Error('Invalid Presubmit response.'));
22
+ });
23
+ socket.on('error', reject);
24
+ socket.on('end', () => {
25
+ try {
26
+ if (!bytes)
27
+ throw Object.assign(new Error('Presubmit host disconnected.'), { code: 'ECONNRESET' });
28
+ const response = JSON.parse(bytes);
29
+ // Older hosts report the same transition without a structured code.
30
+ if (response.code === 'HOST_STOPPING' || response.error === 'Server is stopping.')
31
+ throw new HostStoppingError('The previous Presubmit host is still shutting down. Please retry shortly.');
32
+ if (response.error)
33
+ throw new Error(response.error);
34
+ const url = new URL(response.url);
35
+ if (url.username ||
36
+ url.password ||
37
+ (tailscale
38
+ ? url.origin !== tailscale
39
+ : url.protocol !== 'http:' || url.hostname !== '127.0.0.1'))
40
+ throw new Error(tailscale
41
+ ? 'The running host did not return the requested Tailscale URL. Restart it with this version and --tailscale <https-device-url>.'
42
+ : 'Invalid Presubmit server URL.');
43
+ resolve(url.toString());
44
+ }
45
+ catch (error) {
46
+ reject(error);
47
+ }
48
+ });
49
+ });
50
+ }
51
+ /** One CLI host per Presubmit home; private Unix IPC registers later repositories. */
52
+ export async function acquireHub(home, workspace, tailscale) {
53
+ if (tailscale !== undefined)
54
+ tailscale = tailscaleOrigin(tailscale);
55
+ let lock;
56
+ const until = Date.now() + 15000;
57
+ while (!lock) {
58
+ try {
59
+ lock = await acquireWorkspaceLock(`presubmit-home:${home}`);
60
+ }
61
+ catch (error) {
62
+ if (!(error instanceof WorkspaceAlreadyRunningError))
63
+ throw error;
64
+ try {
65
+ return { existingUrl: await register(home, workspace, tailscale) };
66
+ }
67
+ catch (error) {
68
+ if ((!(error instanceof HostStoppingError) &&
69
+ !['ENOENT', 'ECONNREFUSED', 'ECONNRESET'].includes(error.code ?? '')) ||
70
+ Date.now() >= until)
71
+ throw error;
72
+ }
73
+ // The old host keeps ownership until all workers and stores are closed.
74
+ // Re-acquire the lock after it leaves; never start a second host alongside it.
75
+ await new Promise((resolve) => setTimeout(resolve, 100));
76
+ }
77
+ }
78
+ let control;
79
+ const peers = new Set();
80
+ let stopping = false;
81
+ return {
82
+ async listen(url, openProject, readEvents) {
83
+ const path = hubAddress(home);
84
+ try {
85
+ const info = await lstat(path);
86
+ if (!info.isSocket())
87
+ throw new Error('Presubmit control path is not a socket.');
88
+ await unlink(path);
89
+ }
90
+ catch (error) {
91
+ if (error.code !== 'ENOENT')
92
+ throw error;
93
+ }
94
+ control = createServer((socket) => {
95
+ peers.add(socket);
96
+ socket.once('close', () => peers.delete(socket));
97
+ let bytes = '';
98
+ let handled = false;
99
+ socket.on('error', () => { });
100
+ socket.setTimeout(30000, () => socket.destroy());
101
+ socket.on('data', (chunk) => {
102
+ if (handled)
103
+ return;
104
+ bytes += chunk.toString();
105
+ if (bytes.length > 8192) {
106
+ socket.destroy();
107
+ return;
108
+ }
109
+ if (!bytes.includes('\n'))
110
+ return;
111
+ handled = true;
112
+ void (async () => {
113
+ try {
114
+ const input = JSON.parse(bytes);
115
+ if (stopping) {
116
+ socket.end(JSON.stringify({ code: 'HOST_STOPPING', error: 'Server is stopping.' }) + '\n');
117
+ return;
118
+ }
119
+ if (input.type === 'attach') {
120
+ if (!readEvents)
121
+ throw new Error('Host does not support attachment. Restart it with this version.');
122
+ streamAttachment(socket, input, readEvents);
123
+ return;
124
+ }
125
+ if (typeof input.workspace !== 'string')
126
+ throw new Error('Invalid project path.');
127
+ if (input.tailscale !== undefined && typeof input.tailscale !== 'string')
128
+ throw new Error('Invalid Tailscale URL.');
129
+ const remote = input.tailscale === undefined ? undefined : tailscaleOrigin(input.tailscale);
130
+ socket.end(JSON.stringify({ url: await openProject(input.workspace, remote) }));
131
+ }
132
+ catch (error) {
133
+ socket.end(JSON.stringify({
134
+ error: error instanceof Error ? error.message : 'Could not open project.',
135
+ }) + '\n');
136
+ }
137
+ })();
138
+ });
139
+ });
140
+ await new Promise((resolve, reject) => {
141
+ control.once('error', reject);
142
+ control.listen(path, resolve);
143
+ });
144
+ await chmod(path, 0o600);
145
+ lock.setUrl(url);
146
+ },
147
+ beginShutdown() {
148
+ stopping = true;
149
+ },
150
+ async close() {
151
+ stopping = true;
152
+ for (const peer of peers)
153
+ peer.destroy();
154
+ if (control?.listening)
155
+ await new Promise((resolve) => control.close(() => resolve()));
156
+ await lock.release();
157
+ },
158
+ };
159
+ }
@@ -0,0 +1,38 @@
1
+ import { request as httpRequest } from 'node:http';
2
+ import { WebSocket } from 'ws';
3
+ /** Child workspaces use private Unix sockets, never additional listening TCP ports. */
4
+ export function requestWorkspace(origin, socketPath, path, options = {}) {
5
+ if (!socketPath)
6
+ return fetch(origin + path, options);
7
+ return new Promise((resolve, reject) => {
8
+ const req = httpRequest({
9
+ socketPath,
10
+ signal: options.signal,
11
+ path,
12
+ method: options.method ?? 'GET',
13
+ headers: { ...options.headers, Host: new URL(origin).host },
14
+ }, (response) => {
15
+ const chunks = [];
16
+ response.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
17
+ response.on('error', reject);
18
+ response.on('end', () => {
19
+ const headers = new Headers();
20
+ for (let i = 0; i < response.rawHeaders.length; i += 2)
21
+ headers.append(response.rawHeaders[i], response.rawHeaders[i + 1]);
22
+ const status = response.statusCode ?? 502;
23
+ resolve(new Response([204, 205, 304].includes(status) ? null : Buffer.concat(chunks), {
24
+ status,
25
+ headers,
26
+ }));
27
+ });
28
+ });
29
+ req.on('error', reject);
30
+ req.end(options.body);
31
+ });
32
+ }
33
+ export function connectWorkspace(origin, socketPath, path, options = {}) {
34
+ return new WebSocket(socketPath ? `ws+unix://${socketPath}:${path}` : origin.replace('http:', 'ws:') + path, {
35
+ ...options,
36
+ headers: { ...options.headers, Host: new URL(origin).host },
37
+ });
38
+ }
@@ -0,0 +1,70 @@
1
+ /** Embeddings stay on loopback; redirecting requests to a remote provider is disallowed. */
2
+ export function ollamaEmbeddings(model = 'nomic-embed-text', address = 'http://127.0.0.1:11434') {
3
+ const url = new URL(address);
4
+ if (url.protocol !== 'http:' ||
5
+ !['127.0.0.1', '[::1]'].includes(url.hostname) ||
6
+ url.username ||
7
+ url.password ||
8
+ url.pathname !== '/' ||
9
+ url.search ||
10
+ url.hash)
11
+ throw new Error('The embedding service must use a loopback HTTP address.');
12
+ if (!model.trim() || model.length > 200)
13
+ throw new Error('Invalid embedding model.');
14
+ async function request(path, signal, body) {
15
+ let response;
16
+ try {
17
+ response = await fetch(new URL(path, url), {
18
+ method: body ? 'POST' : 'GET',
19
+ headers: body ? { 'Content-Type': 'application/json' } : undefined,
20
+ body: body ? JSON.stringify(body) : undefined,
21
+ redirect: 'error',
22
+ signal: AbortSignal.any([signal, AbortSignal.timeout(30000)]),
23
+ });
24
+ }
25
+ catch {
26
+ if (signal.aborted)
27
+ throw new Error('Retrieval cancelled.');
28
+ throw new Error('Local embeddings are unavailable. Start Ollama and install the configured model.');
29
+ }
30
+ if (!response.ok)
31
+ throw new Error('Local embeddings are unavailable. Check the configured Ollama model.');
32
+ return response.json();
33
+ }
34
+ return {
35
+ model,
36
+ async identity(signal) {
37
+ const data = await request('/api/tags', signal);
38
+ const found = Array.isArray(data.models) &&
39
+ data.models.find((item) => item.name === model || item.name === `${model}:latest`);
40
+ if (!found || typeof found.digest !== 'string')
41
+ throw new Error('Install the configured embedding model in Ollama before using related search.');
42
+ // A changed model file gets a fresh table, even when its user-facing tag is unchanged.
43
+ return `${model}:${found.digest}`;
44
+ },
45
+ async embed(texts, kind, signal) {
46
+ const prefix = model.startsWith('nomic-embed-text')
47
+ ? kind === 'query'
48
+ ? 'search_query: '
49
+ : 'search_document: '
50
+ : '';
51
+ const data = await request('/api/embed', signal, {
52
+ model,
53
+ input: texts.map((text) => prefix + text),
54
+ truncate: false,
55
+ keep_alive: '5m',
56
+ });
57
+ const vectors = data.embeddings;
58
+ if (!Array.isArray(vectors) ||
59
+ vectors.length !== texts.length ||
60
+ vectors.some((v) => !Array.isArray(v) ||
61
+ !v.length ||
62
+ v.length > 8192 ||
63
+ v.some((n) => typeof n !== 'number' || !Number.isFinite(n)) ||
64
+ !v.some((n) => n !== 0)) ||
65
+ vectors.some((v) => v.length !== vectors[0].length))
66
+ throw new Error('The local model returned invalid embeddings.');
67
+ return vectors;
68
+ },
69
+ };
70
+ }
@@ -0,0 +1,56 @@
1
+ import { constants } from 'node:fs';
2
+ import { access, stat } from 'node:fs/promises';
3
+ import { delimiter, isAbsolute, join } from 'node:path';
4
+ import { parseSearchSettings } from './search-settings.js';
5
+ function executablePaths() {
6
+ const paths = (process.env.PATH ?? '')
7
+ .split(delimiter)
8
+ .filter(isAbsolute)
9
+ .map((directory) => join(directory, 'ollama'));
10
+ if (process.platform === 'darwin')
11
+ paths.push('/Applications/Ollama.app/Contents/Resources/ollama', '/opt/homebrew/bin/ollama', '/usr/local/bin/ollama');
12
+ else if (process.platform === 'linux')
13
+ paths.push('/usr/local/bin/ollama', '/usr/bin/ollama');
14
+ return [...new Set(paths)];
15
+ }
16
+ /** Inspect local installation and model metadata without executing or downloading anything. */
17
+ export async function checkOllamaSetup(model, endpoint, options = {}) {
18
+ const settings = parseSearchSettings({ enabled: false, agentRetrieval: false, model, endpoint });
19
+ const signal = AbortSignal.any([options.signal, AbortSignal.timeout(2000)].filter((value) => value !== undefined));
20
+ const installed = (async () => {
21
+ for (const path of options.executablePaths ?? executablePaths()) {
22
+ if (signal.aborted)
23
+ break;
24
+ try {
25
+ await access(path, constants.X_OK);
26
+ if ((await stat(path)).isFile())
27
+ return true;
28
+ }
29
+ catch {
30
+ // An absent executable is expected on a server that still needs setup.
31
+ }
32
+ }
33
+ return false;
34
+ })();
35
+ let running = false;
36
+ let modelAvailable = null;
37
+ try {
38
+ const response = await fetch(new URL('/api/tags', settings.endpoint), {
39
+ redirect: 'error',
40
+ signal,
41
+ });
42
+ if (!response.ok)
43
+ throw new Error('Model list unavailable.');
44
+ const data = await response.json();
45
+ if (!data || typeof data !== 'object' || !('models' in data) || !Array.isArray(data.models))
46
+ throw new Error('Invalid model list.');
47
+ if (!data.models.every((item) => item && typeof item.name === 'string' && typeof item.digest === 'string'))
48
+ throw new Error('Invalid model list.');
49
+ running = true;
50
+ modelAvailable = data.models.some((item) => item.name === settings.model || item.name === `${settings.model}:latest`);
51
+ }
52
+ catch {
53
+ // Unreachable services, redirects and non-Ollama responses do not complete these steps.
54
+ }
55
+ return { installed: (await installed) || running, running, modelAvailable };
56
+ }
@@ -0,0 +1,30 @@
1
+ import { parsePlanningBoard } from '../../../packages/protocol/src/planning.js';
2
+ /** Pure validation before either model proposals or human edits mutate durable state. */
3
+ export function updatePlanningBoard(current, proposal, sourceRunIds) {
4
+ const next = parsePlanningBoard(proposal);
5
+ if (next.revision !== current.revision)
6
+ throw new Error('Planning board changed. Reload the latest board before saving.');
7
+ const sources = new Set(sourceRunIds);
8
+ for (const item of [...next.ideas, ...next.questions, ...next.tasks]) {
9
+ if (item.sourceRunIds?.some((id) => !sources.has(id)))
10
+ throw new Error('Unknown planning source message.');
11
+ }
12
+ for (const key of ['ideas', 'tasks', 'questions']) {
13
+ const ids = new Set(next[key].map((item) => item.id));
14
+ if (current[key].some((item) => !ids.has(item.id)))
15
+ throw new Error('Keep existing planning items. Defer or reject them instead of deleting their history.');
16
+ }
17
+ for (const key of ['ideas', 'questions', 'tasks']) {
18
+ for (const previous of current[key]) {
19
+ const item = next[key].find((item) => item.id === previous.id);
20
+ if (previous.sourceRunIds?.some((id) => !item.sourceRunIds?.includes(id)))
21
+ throw new Error('Preserve existing source message references.');
22
+ }
23
+ }
24
+ for (const previous of current.tasks) {
25
+ const task = next.tasks.find((task) => task.id === previous.id);
26
+ if (previous.sourceIdeaIds.some((id) => !task.sourceIdeaIds.includes(id)))
27
+ throw new Error('Preserve existing source idea references.');
28
+ }
29
+ return { ...next, revision: current.revision + 1 };
30
+ }
@@ -0,0 +1,276 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { join } from 'node:path';
3
+ import { agentId, agentText, parseAgentDefinition, parseAgentConnection, parseAgentInput, parseAgentInvocation, resolveAgentCapabilities, } from '../../../packages/protocol/src/agents.js';
4
+ /** Owned by a project root Store, never a session Store. No launch, scheduling or external effects. */
5
+ export class ProjectAgents {
6
+ store;
7
+ projectId;
8
+ constructor(store, projectId) {
9
+ this.store = store;
10
+ this.projectId = projectId;
11
+ agentId(projectId);
12
+ store.db
13
+ .exec(`CREATE TABLE IF NOT EXISTS agent_versions(kind TEXT NOT NULL, project TEXT NOT NULL, id TEXT NOT NULL, version INTEGER NOT NULL, payload TEXT NOT NULL, PRIMARY KEY(kind,project,id,version));
14
+ CREATE TABLE IF NOT EXISTS agent_invocations(project TEXT NOT NULL, id TEXT NOT NULL, payload TEXT NOT NULL, PRIMARY KEY(project,id));`);
15
+ }
16
+ saveDefinition(input) {
17
+ const definition = parseAgentDefinition(input);
18
+ this.saveVersion('definition', definition);
19
+ return definition;
20
+ }
21
+ saveConnection(input) {
22
+ const connection = parseAgentConnection(input);
23
+ this.assertProject(connection.projectId);
24
+ const definition = this.definition(connection.agent.id, connection.agent.version);
25
+ resolveAgentCapabilities(definition, connection, definition.requestedCapabilities);
26
+ this.saveVersion('connection', connection);
27
+ return connection;
28
+ }
29
+ definition(id, version) {
30
+ return parseAgentDefinition(this.readVersion('definition', id, version));
31
+ }
32
+ connection(id, version) {
33
+ return parseAgentConnection(this.readVersion('connection', id, version));
34
+ }
35
+ /** Bounded discovery for a future builder. Returns latest versions, not mutable aliases. */
36
+ listDefinitions(limit = 50) {
37
+ return this.latest('definition', limit).map(parseAgentDefinition);
38
+ }
39
+ definitionVersions(id) {
40
+ agentId(id);
41
+ return this.store.db
42
+ .prepare('SELECT payload FROM agent_versions WHERE kind=? AND project=? AND id=? ORDER BY version DESC')
43
+ .all('definition', this.projectId, id).map((row) => parseAgentDefinition(JSON.parse(row.payload)));
44
+ }
45
+ listConnections(limit = 50) {
46
+ return this.latest('connection', limit).map(parseAgentConnection);
47
+ }
48
+ connectionVersions(id) {
49
+ agentId(id);
50
+ return this.store.db
51
+ .prepare('SELECT payload FROM agent_versions WHERE kind=? AND project=? AND id=? ORDER BY version DESC')
52
+ .all('connection', this.projectId, id).map((row) => parseAgentConnection(JSON.parse(row.payload)));
53
+ }
54
+ latest(kind, limit) {
55
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
56
+ throw new Error('Invalid agent list limit.');
57
+ return this.store.db
58
+ .prepare(`SELECT payload FROM agent_versions AS v WHERE kind=? AND project=?
59
+ AND version=(SELECT MAX(version) FROM agent_versions WHERE kind=v.kind AND project=v.project AND id=v.id)
60
+ ORDER BY id LIMIT ?`)
61
+ .all(kind, this.projectId, limit).map((r) => JSON.parse(r.payload));
62
+ }
63
+ assertProject(projectId) {
64
+ if (projectId !== this.projectId)
65
+ throw new Error('Agent record belongs to another project.');
66
+ }
67
+ saveVersion(kind, value) {
68
+ this.assertProject(value.projectId);
69
+ this.store.transaction(() => {
70
+ const count = this.store.db
71
+ .prepare('SELECT COUNT(*) AS n FROM agent_versions WHERE project=?')
72
+ .get(this.projectId);
73
+ if (count.n >= 1000)
74
+ throw new Error('Project agent version limit reached (1000).');
75
+ const row = this.store.db
76
+ .prepare('SELECT MAX(version) AS v FROM agent_versions WHERE kind=? AND project=? AND id=?')
77
+ .get(kind, this.projectId, value.id);
78
+ if (value.version !== (row.v ?? 0) + 1)
79
+ throw new Error('Agent versions are immutable; save the next consecutive version.');
80
+ this.store.db
81
+ .prepare('INSERT INTO agent_versions VALUES(?,?,?,?,?)')
82
+ .run(kind, this.projectId, value.id, value.version, JSON.stringify(value));
83
+ });
84
+ }
85
+ readVersion(kind, id, version) {
86
+ agentId(id);
87
+ if (!Number.isSafeInteger(version) || version < 1)
88
+ throw new Error('Invalid agent version.');
89
+ const row = this.store.db
90
+ .prepare('SELECT payload FROM agent_versions WHERE kind=? AND project=? AND id=? AND version=?')
91
+ .get(kind, this.projectId, id, version);
92
+ if (!row)
93
+ throw new Error('Agent version not found in this project.');
94
+ return JSON.parse(row.payload);
95
+ }
96
+ /** Explicit caller action only. Captures inputs and both versions before any worker can start. */
97
+ prepare(connectionId, connectionVersion, input, host) {
98
+ const connection = this.connection(connectionId, connectionVersion);
99
+ const definition = this.definition(connection.agent.id, connection.agent.version);
100
+ return this.prepareFrozen(definition, connection, input, host);
101
+ }
102
+ /** Builder-only draft test: never publishes a definition or installs a connection. */
103
+ prepareTest(value, input, host) {
104
+ const definition = parseAgentDefinition(value);
105
+ this.assertProject(definition.projectId);
106
+ const snapshot = definition.contextRequirements.includes('snapshot');
107
+ const connection = parseAgentConnection({
108
+ schemaVersion: 1,
109
+ projectId: this.projectId,
110
+ id: `test-${randomUUID()}`,
111
+ version: 1,
112
+ agent: { id: definition.id, version: definition.version },
113
+ placement: snapshot ? 'snapshot-action' : 'conversation-action',
114
+ trigger: 'manual',
115
+ inputMapping: {
116
+ prompt: 'user-message',
117
+ ...(snapshot
118
+ ? { snapshot: 'captured-snapshot' }
119
+ : definition.contextRequirements.includes('workspace')
120
+ ? { workspace: 'project-workspace' }
121
+ : {}),
122
+ },
123
+ permissionCeiling: [...host],
124
+ resultDestination: definition.output,
125
+ });
126
+ return this.prepareFrozen(definition, connection, input, host);
127
+ }
128
+ prepareFrozen(definition, connection, input, host) {
129
+ const effectiveCapabilities = resolveAgentCapabilities(definition, connection, host);
130
+ const mappedInput = parseAgentInput(input, connection);
131
+ const now = new Date().toISOString();
132
+ const invocation = {
133
+ schemaVersion: 1,
134
+ projectId: this.projectId,
135
+ id: randomUUID(),
136
+ sessionId: randomUUID(),
137
+ runId: randomUUID(),
138
+ definition,
139
+ connection,
140
+ effectiveCapabilities,
141
+ input: mappedInput,
142
+ status: 'prepared',
143
+ revision: 1,
144
+ createdAt: now,
145
+ updatedAt: now,
146
+ };
147
+ this.store.transaction(() => {
148
+ const count = this.store.db
149
+ .prepare('SELECT COUNT(*) AS n FROM agent_invocations WHERE project=?')
150
+ .get(this.projectId);
151
+ if (count.n >= 1000)
152
+ throw new Error('Project invocation limit reached (1000).');
153
+ const active = this.store.db
154
+ .prepare(`SELECT COUNT(*) AS n FROM agent_invocations WHERE project=? AND json_extract(payload, '$.status') IN ('prepared','running','cancelling')`)
155
+ .get(this.projectId);
156
+ if (active.n >= 20)
157
+ throw new Error('Project active invocation limit reached (20).');
158
+ this.store.db
159
+ .prepare('INSERT INTO agent_invocations VALUES(?,?,?)')
160
+ .run(this.projectId, invocation.id, JSON.stringify(invocation));
161
+ });
162
+ return structuredClone(invocation);
163
+ }
164
+ read(id) {
165
+ agentId(id);
166
+ const row = this.store.db
167
+ .prepare('SELECT payload FROM agent_invocations WHERE project=? AND id=?')
168
+ .get(this.projectId, id);
169
+ if (!row)
170
+ throw new Error('Invocation not found in this project.');
171
+ const record = parseAgentInvocation(JSON.parse(row.payload));
172
+ this.assertProject(record.projectId);
173
+ if (record.id !== id)
174
+ throw new Error('Invocation identity does not match durable key.');
175
+ return record;
176
+ }
177
+ list(limit = 50) {
178
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000)
179
+ throw new Error('Invalid invocation limit.');
180
+ return this.store.db
181
+ .prepare('SELECT payload FROM agent_invocations WHERE project=? ORDER BY rowid DESC LIMIT ?')
182
+ .all(this.projectId, limit).map((r) => {
183
+ const record = parseAgentInvocation(JSON.parse(r.payload));
184
+ this.assertProject(record.projectId);
185
+ return record;
186
+ });
187
+ }
188
+ update(id, revision, change) {
189
+ return this.store.transaction(() => {
190
+ const record = this.read(id);
191
+ if (record.revision !== revision)
192
+ throw new Error('Invocation changed; reload before acting.');
193
+ change(record);
194
+ record.revision++;
195
+ record.updatedAt = new Date().toISOString();
196
+ this.store.db
197
+ .prepare('UPDATE agent_invocations SET payload=? WHERE project=? AND id=?')
198
+ .run(JSON.stringify(record), this.projectId, id);
199
+ return record;
200
+ });
201
+ }
202
+ /** One-shot claim. A crash after claiming interrupts, never replays. Recheck current host ceiling. */
203
+ claim(id, revision, host) {
204
+ return this.update(id, revision, (record) => {
205
+ if (record.status !== 'prepared')
206
+ throw new Error('Invocation is not prepared; replay is prohibited.');
207
+ const effective = resolveAgentCapabilities(record.definition, record.connection, host);
208
+ record.effectiveCapabilities = record.effectiveCapabilities.filter((c) => effective.includes(c));
209
+ if (record.definition.output === 'review-findings' &&
210
+ !record.effectiveCapabilities.includes('findings.report'))
211
+ throw new Error('Host no longer permits required findings output.');
212
+ record.status = 'running';
213
+ });
214
+ }
215
+ cancel(id, revision) {
216
+ return this.update(id, revision, (record) => {
217
+ if (record.status === 'prepared')
218
+ record.status = 'cancelled';
219
+ else if (record.status === 'running')
220
+ record.status = 'cancelling';
221
+ else
222
+ throw new Error('Invocation cannot be cancelled in its current state.');
223
+ });
224
+ }
225
+ /** Call only after the existing harness has exited/disposed (including cancellation). */
226
+ finish(id, revision, runId, outcome, error) {
227
+ return this.update(id, revision, (record) => {
228
+ if (record.runId !== runId || !['running', 'cancelling'].includes(record.status))
229
+ throw new Error('Stale or inactive invocation completion.');
230
+ if (!['completed', 'failed', 'cancelled'].includes(outcome))
231
+ throw new Error('Invalid invocation outcome.');
232
+ record.status = record.status === 'cancelling' ? 'cancelled' : outcome;
233
+ if (error !== undefined)
234
+ record.error = agentText(error, 'invocation error', 4000);
235
+ });
236
+ }
237
+ /** Startup only, after old workers are gone. No constructor-triggered execution or automatic resume. */
238
+ recoverInterrupted() {
239
+ let count = 0;
240
+ const rows = this.store.db
241
+ .prepare(`SELECT payload FROM agent_invocations WHERE project=? AND json_extract(payload, '$.status') IN ('prepared','running','cancelling')`)
242
+ .all(this.projectId);
243
+ for (const row of rows) {
244
+ const record = parseAgentInvocation(JSON.parse(row.payload));
245
+ this.update(record.id, record.revision, (item) => {
246
+ item.status = 'interrupted';
247
+ item.error = 'Interrupted by restart; start a new invocation explicitly.';
248
+ });
249
+ count++;
250
+ }
251
+ return count;
252
+ }
253
+ /** Server-owned paths/auth, never values from instructions or input mappings. Uses the shared PiAgent. */
254
+ startOptions(id, host) {
255
+ const record = this.read(id);
256
+ if (record.status !== 'running')
257
+ throw new Error('Claim invocation before constructing start options.');
258
+ const snapshot = record.input.snapshot;
259
+ return {
260
+ runId: record.runId,
261
+ cwd: host.cwd,
262
+ dataDir: join(host.dataDir, 'agent-invocations', record.sessionId),
263
+ prompt: record.input.prompt,
264
+ mode: snapshot ? 'review' : 'research',
265
+ ...(snapshot ? { snapshotOnly: true, snapshot: structuredClone(snapshot) } : {}),
266
+ auth: host.auth,
267
+ reasoningLevel: host.reasoningLevel,
268
+ fastMode: host.fastMode,
269
+ projectAgent: {
270
+ invocationId: record.id,
271
+ instructions: record.definition.instructions,
272
+ capabilities: [...record.effectiveCapabilities],
273
+ },
274
+ };
275
+ }
276
+ }