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.
- package/LICENSE +202 -0
- package/README.md +51 -0
- package/THIRD_PARTY_NOTICES.md +24 -0
- package/apps/web/dist/assets/FileCode-CZZH52o7.js +5 -0
- package/apps/web/dist/assets/FileCode-DovDO7Jh.css +1 -0
- package/apps/web/dist/assets/SettingsPage-CRvOwdjt.js +4 -0
- package/apps/web/dist/assets/SettingsPage-UbYSCwJj.css +1 -0
- package/apps/web/dist/assets/TerminalPanel-0sw-J6YS.css +1 -0
- package/apps/web/dist/assets/TerminalPanel-BqUjPWav.js +16 -0
- package/apps/web/dist/assets/index-BhqNNFja.js +54 -0
- package/apps/web/dist/assets/index-DD8VXy8H.css +1 -0
- package/apps/web/dist/assets/presubmit-mark-BBQ19Fmr.svg +7 -0
- package/apps/web/dist/assets/presubmit-mark-busy-CwBC-7on.svg +8 -0
- package/apps/web/dist/assets/react-vendor-wiHys0m2.js +9 -0
- package/apps/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/apps/web/dist/index.html +6 -0
- package/apps/web/dist/service-worker.js +11 -0
- package/dist/apps/server/src/agent-builder.js +204 -0
- package/dist/apps/server/src/attach.js +163 -0
- package/dist/apps/server/src/attachments.js +86 -0
- package/dist/apps/server/src/browser-access.js +131 -0
- package/dist/apps/server/src/check-provenance.js +95 -0
- package/dist/apps/server/src/check-supervisor.js +112 -0
- package/dist/apps/server/src/checks.js +402 -0
- package/dist/apps/server/src/child-collaboration-guidance.js +2 -0
- package/dist/apps/server/src/child-sessions.js +191 -0
- package/dist/apps/server/src/cli.js +162 -0
- package/dist/apps/server/src/conversation-search.js +12 -0
- package/dist/apps/server/src/coordinator-messages.js +72 -0
- package/dist/apps/server/src/dev-auth.js +54 -0
- package/dist/apps/server/src/dev-server.js +201 -0
- package/dist/apps/server/src/dev-worker.js +48 -0
- package/dist/apps/server/src/diagnostics.js +105 -0
- package/dist/apps/server/src/fast-mode.js +8 -0
- package/dist/apps/server/src/folder-picker.js +54 -0
- package/dist/apps/server/src/git-blobs.js +62 -0
- package/dist/apps/server/src/git-handoff.js +127 -0
- package/dist/apps/server/src/history-sources.js +59 -0
- package/dist/apps/server/src/hub.js +159 -0
- package/dist/apps/server/src/internal-transport.js +38 -0
- package/dist/apps/server/src/local-embeddings.js +70 -0
- package/dist/apps/server/src/ollama-setup.js +56 -0
- package/dist/apps/server/src/planning.js +30 -0
- package/dist/apps/server/src/project-agents.js +276 -0
- package/dist/apps/server/src/project-memory.js +94 -0
- package/dist/apps/server/src/projects.js +106 -0
- package/dist/apps/server/src/queued-launch.js +13 -0
- package/dist/apps/server/src/reply-search.js +172 -0
- package/dist/apps/server/src/review-context.js +60 -0
- package/dist/apps/server/src/review-refresh.js +14 -0
- package/dist/apps/server/src/reviewers.js +75 -0
- package/dist/apps/server/src/search-settings.js +22 -0
- package/dist/apps/server/src/semantic-history.js +186 -0
- package/dist/apps/server/src/server.js +4338 -0
- package/dist/apps/server/src/session-changes.js +181 -0
- package/dist/apps/server/src/session-router-settings.js +198 -0
- package/dist/apps/server/src/session-workspaces.js +135 -0
- package/dist/apps/server/src/sessions.js +579 -0
- package/dist/apps/server/src/shared-auth.js +70 -0
- package/dist/apps/server/src/shell-evaluator-settings.js +53 -0
- package/dist/apps/server/src/store.js +203 -0
- package/dist/apps/server/src/tailscale-process.js +115 -0
- package/dist/apps/server/src/tailscale-serve-worker.js +33 -0
- package/dist/apps/server/src/tailscale-serve.js +276 -0
- package/dist/apps/server/src/task-capacity.js +92 -0
- package/dist/apps/server/src/task-graph.js +190 -0
- package/dist/apps/server/src/task-logs.js +74 -0
- package/dist/apps/server/src/task-numbers.js +25 -0
- package/dist/apps/server/src/task-orchestrator.js +1088 -0
- package/dist/apps/server/src/terminal.js +182 -0
- package/dist/apps/server/src/terminals.js +80 -0
- package/dist/apps/server/src/tool-safety.js +2 -0
- package/dist/apps/server/src/workspace-lock.js +140 -0
- package/dist/apps/server/src/workspace-watcher.js +65 -0
- package/dist/apps/server/src/workspace.js +517 -0
- package/dist/packages/agent-pi/src/agent-builder-tool.js +32 -0
- package/dist/packages/agent-pi/src/agent-capabilities.js +44 -0
- package/dist/packages/agent-pi/src/ask-user.js +86 -0
- package/dist/packages/agent-pi/src/auth.js +510 -0
- package/dist/packages/agent-pi/src/check-tools.js +72 -0
- package/dist/packages/agent-pi/src/command-policy.js +246 -0
- package/dist/packages/agent-pi/src/coordinator-tools.js +122 -0
- package/dist/packages/agent-pi/src/fast-mode.js +67 -0
- package/dist/packages/agent-pi/src/file-tools.js +163 -0
- package/dist/packages/agent-pi/src/history-tool.js +40 -0
- package/dist/packages/agent-pi/src/history.js +34 -0
- package/dist/packages/agent-pi/src/index.js +177 -0
- package/dist/packages/agent-pi/src/ipc.js +314 -0
- package/dist/packages/agent-pi/src/jev-session-router.js +408 -0
- package/dist/packages/agent-pi/src/jev-tools.js +71 -0
- package/dist/packages/agent-pi/src/local-model.js +277 -0
- package/dist/packages/agent-pi/src/memory-tools.js +119 -0
- package/dist/packages/agent-pi/src/model.js +25 -0
- package/dist/packages/agent-pi/src/planning.js +206 -0
- package/dist/packages/agent-pi/src/resource-loader.js +23 -0
- package/dist/packages/agent-pi/src/review-tools.js +158 -0
- package/dist/packages/agent-pi/src/review-transition.js +73 -0
- package/dist/packages/agent-pi/src/review.js +40 -0
- package/dist/packages/agent-pi/src/routing.js +27 -0
- package/dist/packages/agent-pi/src/session-setup.js +118 -0
- package/dist/packages/agent-pi/src/shell-context.js +51 -0
- package/dist/packages/agent-pi/src/shell-policy.js +281 -0
- package/dist/packages/agent-pi/src/shell-safety.js +205 -0
- package/dist/packages/agent-pi/src/shell.js +121 -0
- package/dist/packages/agent-pi/src/snapshot-permissions.js +12 -0
- package/dist/packages/agent-pi/src/spike.js +41 -0
- package/dist/packages/agent-pi/src/task-tools.js +53 -0
- package/dist/packages/agent-pi/src/types.js +1 -0
- package/dist/packages/agent-pi/src/usage.js +30 -0
- package/dist/packages/agent-pi/src/worker.js +602 -0
- package/dist/packages/protocol/src/activity-summary.js +77 -0
- package/dist/packages/protocol/src/agents.js +301 -0
- package/dist/packages/protocol/src/browser-access.js +1 -0
- package/dist/packages/protocol/src/checks.js +40 -0
- package/dist/packages/protocol/src/child-sessions.js +28 -0
- package/dist/packages/protocol/src/command-policy.js +3 -0
- package/dist/packages/protocol/src/coordinator.js +135 -0
- package/dist/packages/protocol/src/delegation.js +40 -0
- package/dist/packages/protocol/src/git-diff.js +71 -0
- package/dist/packages/protocol/src/git-handoff.js +1 -0
- package/dist/packages/protocol/src/index.js +1 -0
- package/dist/packages/protocol/src/memory.js +46 -0
- package/dist/packages/protocol/src/metrics.js +70 -0
- package/dist/packages/protocol/src/planning.js +138 -0
- package/dist/packages/protocol/src/retrieval.js +36 -0
- package/dist/packages/protocol/src/reviewers.js +17 -0
- package/dist/packages/protocol/src/search.js +16 -0
- package/dist/packages/protocol/src/session-changes.js +29 -0
- package/dist/packages/protocol/src/session-router.js +123 -0
- package/dist/packages/protocol/src/shell-evaluator.js +43 -0
- package/dist/packages/protocol/src/task-graph.js +1 -0
- package/dist/packages/protocol/src/token-usage.js +17 -0
- package/dist/packages/protocol/src/user-questions.js +106 -0
- package/package.json +71 -0
- package/vendor/lancedb/NODEJS_THIRD_PARTY_LICENSES.md +668 -0
- package/vendor/lancedb/RUST_THIRD_PARTY_LICENSES.html +14607 -0
- package/vendor/lancedb/dist/arrow.d.ts +296 -0
- package/vendor/lancedb/dist/arrow.js +1145 -0
- package/vendor/lancedb/dist/arrow_type.d.ts +9 -0
- package/vendor/lancedb/dist/arrow_type.js +29 -0
- package/vendor/lancedb/dist/connection.d.ts +508 -0
- package/vendor/lancedb/dist/connection.js +298 -0
- package/vendor/lancedb/dist/embedding/embedding_function.d.ts +103 -0
- package/vendor/lancedb/dist/embedding/embedding_function.js +192 -0
- package/vendor/lancedb/dist/embedding/index.d.ts +37 -0
- package/vendor/lancedb/dist/embedding/index.js +108 -0
- package/vendor/lancedb/dist/embedding/openai.d.ts +16 -0
- package/vendor/lancedb/dist/embedding/openai.js +81 -0
- package/vendor/lancedb/dist/embedding/registry.d.ts +94 -0
- package/vendor/lancedb/dist/embedding/registry.js +221 -0
- package/vendor/lancedb/dist/embedding/transformers.d.ts +36 -0
- package/vendor/lancedb/dist/embedding/transformers.js +110 -0
- package/vendor/lancedb/dist/header.d.ts +162 -0
- package/vendor/lancedb/dist/header.js +217 -0
- package/vendor/lancedb/dist/index.d.ts +246 -0
- package/vendor/lancedb/dist/index.js +176 -0
- package/vendor/lancedb/dist/indices.d.ts +721 -0
- package/vendor/lancedb/dist/indices.js +166 -0
- package/vendor/lancedb/dist/materialized_view.d.ts +69 -0
- package/vendor/lancedb/dist/materialized_view.js +112 -0
- package/vendor/lancedb/dist/merge.d.ts +104 -0
- package/vendor/lancedb/dist/merge.js +120 -0
- package/vendor/lancedb/dist/native.d.ts +1193 -0
- package/vendor/lancedb/dist/native.js +688 -0
- package/vendor/lancedb/dist/oauth.d.ts +66 -0
- package/vendor/lancedb/dist/oauth.js +15 -0
- package/vendor/lancedb/dist/otel.d.ts +26 -0
- package/vendor/lancedb/dist/otel.js +114 -0
- package/vendor/lancedb/dist/permutation.d.ts +143 -0
- package/vendor/lancedb/dist/permutation.js +184 -0
- package/vendor/lancedb/dist/query.d.ts +663 -0
- package/vendor/lancedb/dist/query.js +966 -0
- package/vendor/lancedb/dist/rerankers/index.d.ts +5 -0
- package/vendor/lancedb/dist/rerankers/index.js +19 -0
- package/vendor/lancedb/dist/rerankers/rrf.d.ts +14 -0
- package/vendor/lancedb/dist/rerankers/rrf.js +28 -0
- package/vendor/lancedb/dist/sanitize.d.ts +32 -0
- package/vendor/lancedb/dist/sanitize.js +544 -0
- package/vendor/lancedb/dist/scannable.d.ts +92 -0
- package/vendor/lancedb/dist/scannable.js +200 -0
- package/vendor/lancedb/dist/schema.d.ts +16 -0
- package/vendor/lancedb/dist/schema.js +387 -0
- package/vendor/lancedb/dist/table.d.ts +1069 -0
- package/vendor/lancedb/dist/table.js +498 -0
- package/vendor/lancedb/dist/util.d.ts +14 -0
- package/vendor/lancedb/dist/util.js +77 -0
- package/vendor/lancedb/license_header.txt +2 -0
- package/vendor/lancedb/package.json +56 -0
- package/vendor/transformers/LICENSE +202 -0
- package/vendor/transformers/dist/transformers.cjs +30844 -0
- package/vendor/transformers/dist/transformers.mjs +31404 -0
- package/vendor/transformers/package.json +55 -0
- package/vendor/transformers/types/backends/onnx.d.ts +30 -0
- package/vendor/transformers/types/configs.d.ts +85 -0
- package/vendor/transformers/types/env.d.ts +110 -0
- package/vendor/transformers/types/generation/configuration_utils.d.ts +320 -0
- package/vendor/transformers/types/generation/logits_process.d.ts +354 -0
- package/vendor/transformers/types/generation/logits_sampler.d.ts +51 -0
- package/vendor/transformers/types/generation/parameters.d.ts +47 -0
- package/vendor/transformers/types/generation/stopping_criteria.d.ts +81 -0
- package/vendor/transformers/types/generation/streamers.d.ts +81 -0
- package/vendor/transformers/types/models/whisper/common_whisper.d.ts +8 -0
- package/vendor/transformers/types/models/whisper/generation_whisper.d.ts +76 -0
- package/vendor/transformers/types/models.d.ts +3684 -0
- package/vendor/transformers/types/ops/registry.d.ts +11 -0
- package/vendor/transformers/types/pipelines.d.ts +2402 -0
- package/vendor/transformers/types/processors.d.ts +924 -0
- package/vendor/transformers/types/tokenizers.d.ts +990 -0
- package/vendor/transformers/types/transformers.d.ts +13 -0
- package/vendor/transformers/types/utils/audio.d.ts +130 -0
- package/vendor/transformers/types/utils/constants.d.ts +2 -0
- package/vendor/transformers/types/utils/core.d.ts +98 -0
- package/vendor/transformers/types/utils/data-structures.d.ts +236 -0
- package/vendor/transformers/types/utils/devices.d.ts +18 -0
- package/vendor/transformers/types/utils/dtypes.d.ts +19 -0
- package/vendor/transformers/types/utils/generic.d.ts +11 -0
- package/vendor/transformers/types/utils/hub.d.ts +152 -0
- package/vendor/transformers/types/utils/image.d.ts +119 -0
- package/vendor/transformers/types/utils/maths.d.ts +280 -0
- package/vendor/transformers/types/utils/tensor.d.ts +418 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { fork } from 'node:child_process';
|
|
2
|
+
import { request } from 'node:http';
|
|
3
|
+
import { connect } from 'node:net';
|
|
4
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
/** Private streaming proxy; frontend crashes never restart the authoritative session host. */
|
|
9
|
+
export async function startDevServer(root, tailscaleOrigin) {
|
|
10
|
+
const directory = await mkdtemp(join(tmpdir(), 'ps-vite-'));
|
|
11
|
+
const socketPath = join(directory, 'http.sock');
|
|
12
|
+
const worker = new URL(import.meta.url.endsWith('.ts') ? './dev-worker.ts' : './dev-worker.js', import.meta.url);
|
|
13
|
+
let child;
|
|
14
|
+
let ready = false;
|
|
15
|
+
let closed = false;
|
|
16
|
+
let retry;
|
|
17
|
+
let failures = 0;
|
|
18
|
+
let exited = Promise.resolve();
|
|
19
|
+
const connections = new Set();
|
|
20
|
+
const track = (socket) => {
|
|
21
|
+
// The HTTP agent reuses sockets across requests; install cleanup only once.
|
|
22
|
+
if (connections.has(socket))
|
|
23
|
+
return socket;
|
|
24
|
+
connections.add(socket);
|
|
25
|
+
socket.once('close', () => connections.delete(socket));
|
|
26
|
+
return socket;
|
|
27
|
+
};
|
|
28
|
+
async function launch() {
|
|
29
|
+
await rm(socketPath, { force: true });
|
|
30
|
+
if (closed)
|
|
31
|
+
return;
|
|
32
|
+
// Do not inherit provider credentials or the application's account/data configuration.
|
|
33
|
+
const env = {};
|
|
34
|
+
for (const key of [
|
|
35
|
+
'PATH',
|
|
36
|
+
'TMPDIR',
|
|
37
|
+
'TMP',
|
|
38
|
+
'TEMP',
|
|
39
|
+
'SystemRoot',
|
|
40
|
+
'NODE_ENV',
|
|
41
|
+
'FORCE_COLOR',
|
|
42
|
+
'PRESUBMIT_DIAGNOSTICS',
|
|
43
|
+
])
|
|
44
|
+
if (process.env[key] !== undefined)
|
|
45
|
+
env[key] = process.env[key];
|
|
46
|
+
const current = fork(fileURLToPath(worker), [root, socketPath, tailscaleOrigin ?? ''], {
|
|
47
|
+
env,
|
|
48
|
+
execArgv: worker.pathname.endsWith('.ts') ? ['--import', import.meta.resolve('tsx')] : [],
|
|
49
|
+
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
|
|
50
|
+
});
|
|
51
|
+
child = current;
|
|
52
|
+
exited = new Promise((resolve) => {
|
|
53
|
+
current.once('exit', () => resolve());
|
|
54
|
+
current.once('error', () => {
|
|
55
|
+
if (!current.pid)
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
await new Promise((resolve, reject) => {
|
|
60
|
+
let started = false;
|
|
61
|
+
const timeout = setTimeout(() => {
|
|
62
|
+
reject(new Error('Frontend worker startup timed out'));
|
|
63
|
+
current.kill('SIGKILL');
|
|
64
|
+
}, 30_000);
|
|
65
|
+
current.on('message', (message) => {
|
|
66
|
+
if (message?.type !== 'ready' || closed || started)
|
|
67
|
+
return;
|
|
68
|
+
started = true;
|
|
69
|
+
ready = true;
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
resolve();
|
|
72
|
+
});
|
|
73
|
+
current.once('error', (error) => {
|
|
74
|
+
clearTimeout(timeout);
|
|
75
|
+
reject(error);
|
|
76
|
+
});
|
|
77
|
+
current.once('exit', () => {
|
|
78
|
+
clearTimeout(timeout);
|
|
79
|
+
ready = false;
|
|
80
|
+
for (const connection of connections)
|
|
81
|
+
connection.destroy();
|
|
82
|
+
if (!started)
|
|
83
|
+
reject(new Error('Frontend worker exited before becoming ready'));
|
|
84
|
+
if (!closed && started)
|
|
85
|
+
scheduleRestart();
|
|
86
|
+
});
|
|
87
|
+
}).catch(async (error) => {
|
|
88
|
+
current.kill('SIGKILL');
|
|
89
|
+
await exited;
|
|
90
|
+
throw error;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function scheduleRestart() {
|
|
94
|
+
if (closed)
|
|
95
|
+
return;
|
|
96
|
+
const wait = Math.min(30_000, 1000 * 2 ** Math.min(failures++, 5));
|
|
97
|
+
console.error(`Frontend worker stopped. Retrying in ${wait / 1000}s; agent sessions remain active.`);
|
|
98
|
+
retry = setTimeout(() => {
|
|
99
|
+
void launch().catch(() => scheduleRestart());
|
|
100
|
+
}, wait);
|
|
101
|
+
}
|
|
102
|
+
function headers(req) {
|
|
103
|
+
const { cookie: _cookie, authorization: _authorization, ...rest } = req.headers;
|
|
104
|
+
// The public server has already checked authentication and exact Host/Origin.
|
|
105
|
+
// Vite uses a private socket; normalize its transport host so enabling a remote
|
|
106
|
+
// origin at runtime needs no frontend or authoritative host restart.
|
|
107
|
+
return { ...rest, host: '127.0.0.1' };
|
|
108
|
+
}
|
|
109
|
+
function assetPath(req) {
|
|
110
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
111
|
+
// Browser location keeps the bootstrap token; transforms never need to receive it.
|
|
112
|
+
url.searchParams.delete('token');
|
|
113
|
+
return url.pathname + url.search;
|
|
114
|
+
}
|
|
115
|
+
let closing;
|
|
116
|
+
function close() {
|
|
117
|
+
return (closing ??= (async () => {
|
|
118
|
+
closed = true;
|
|
119
|
+
ready = false;
|
|
120
|
+
clearTimeout(retry);
|
|
121
|
+
for (const connection of connections)
|
|
122
|
+
connection.destroy();
|
|
123
|
+
const current = child;
|
|
124
|
+
const kill = setTimeout(() => current?.kill('SIGKILL'), 3000);
|
|
125
|
+
current?.kill('SIGTERM');
|
|
126
|
+
await exited;
|
|
127
|
+
clearTimeout(kill);
|
|
128
|
+
await rm(directory, { recursive: true, force: true });
|
|
129
|
+
})());
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
await launch();
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
await close();
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
close,
|
|
140
|
+
// Used by diagnostics and lifecycle tests; not exposed over HTTP.
|
|
141
|
+
get pid() {
|
|
142
|
+
return child?.pid;
|
|
143
|
+
},
|
|
144
|
+
request(req, res) {
|
|
145
|
+
if (!ready) {
|
|
146
|
+
res.writeHead(503, {
|
|
147
|
+
'Content-Type': 'text/plain',
|
|
148
|
+
'Retry-After': '1',
|
|
149
|
+
'Cache-Control': 'no-store',
|
|
150
|
+
});
|
|
151
|
+
res.end('Frontend is restarting. Agent sessions are still running. Reload shortly.');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const upstream = request({ socketPath, path: assetPath(req), method: req.method, headers: headers(req) }, (response) => {
|
|
155
|
+
res.writeHead(response.statusCode ?? 502, {
|
|
156
|
+
...response.headers,
|
|
157
|
+
'cache-control': 'no-store',
|
|
158
|
+
'referrer-policy': 'no-referrer',
|
|
159
|
+
});
|
|
160
|
+
response.on('error', () => res.destroy());
|
|
161
|
+
response.pipe(res);
|
|
162
|
+
});
|
|
163
|
+
upstream.on('socket', track);
|
|
164
|
+
upstream.on('error', () => {
|
|
165
|
+
if (res.headersSent)
|
|
166
|
+
res.destroy();
|
|
167
|
+
else {
|
|
168
|
+
res.writeHead(503, { 'Cache-Control': 'no-store' });
|
|
169
|
+
res.end('Frontend unavailable. Reload shortly.');
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
req.once('aborted', () => upstream.destroy());
|
|
173
|
+
res.once('close', () => upstream.destroy());
|
|
174
|
+
req.pipe(upstream);
|
|
175
|
+
},
|
|
176
|
+
upgrade(req, socket, head) {
|
|
177
|
+
if (!ready) {
|
|
178
|
+
socket.end('HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const upstream = track(connect(socketPath));
|
|
182
|
+
track(socket);
|
|
183
|
+
upstream.on('error', () => socket.destroy());
|
|
184
|
+
socket.on('error', () => upstream.destroy());
|
|
185
|
+
upstream.once('close', () => socket.destroy());
|
|
186
|
+
socket.once('close', () => upstream.destroy());
|
|
187
|
+
upstream.once('connect', () => {
|
|
188
|
+
upstream.write(`${req.method} ${req.url} HTTP/${req.httpVersion}\r\n`);
|
|
189
|
+
for (const [key, value] of Object.entries(headers(req))) {
|
|
190
|
+
for (const item of Array.isArray(value) ? value : [value])
|
|
191
|
+
if (item !== undefined)
|
|
192
|
+
upstream.write(`${key}: ${item}\r\n`);
|
|
193
|
+
}
|
|
194
|
+
upstream.write('\r\n');
|
|
195
|
+
if (head.length)
|
|
196
|
+
upstream.write(head);
|
|
197
|
+
socket.pipe(upstream).pipe(socket);
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { createServer as createViteServer } from 'vite';
|
|
3
|
+
import { recordHttp, startDiagnostics } from './diagnostics.js';
|
|
4
|
+
// This process owns frontend transforms only. No application stores or agent SDK are loaded.
|
|
5
|
+
const [root, socketPath, tailscaleOrigin] = process.argv.slice(2);
|
|
6
|
+
if (!root || !socketPath || !process.send)
|
|
7
|
+
throw new Error('Missing dev worker options');
|
|
8
|
+
// If the host crashes, this frontend has no owner and must exit even during startup.
|
|
9
|
+
process.once('disconnect', () => process.exit(0));
|
|
10
|
+
const stopDiagnostics = startDiagnostics('vite');
|
|
11
|
+
const server = createServer((req, res) => {
|
|
12
|
+
recordHttp(req, res, true);
|
|
13
|
+
vite.middlewares(req, res, () => {
|
|
14
|
+
res.writeHead(404);
|
|
15
|
+
res.end('Not found');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
const vite = await createViteServer({
|
|
19
|
+
configFile: false,
|
|
20
|
+
root,
|
|
21
|
+
appType: 'spa',
|
|
22
|
+
server: {
|
|
23
|
+
middlewareMode: true,
|
|
24
|
+
allowedHosts: tailscaleOrigin ? [new URL(tailscaleOrigin).hostname] : [],
|
|
25
|
+
// Let the browser select its own port and ws/wss protocol behind Serve as well as locally.
|
|
26
|
+
hmr: { server, path: '/__vite_hmr' },
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
let stopping = false;
|
|
30
|
+
async function stop() {
|
|
31
|
+
if (stopping)
|
|
32
|
+
return;
|
|
33
|
+
stopping = true;
|
|
34
|
+
stopDiagnostics();
|
|
35
|
+
await vite.close();
|
|
36
|
+
server.closeAllConnections();
|
|
37
|
+
server.close(() => process.exit(0));
|
|
38
|
+
}
|
|
39
|
+
for (const signal of ['SIGTERM', 'SIGINT'])
|
|
40
|
+
process.once(signal, () => void stop());
|
|
41
|
+
await new Promise((resolve, reject) => {
|
|
42
|
+
server.once('error', reject);
|
|
43
|
+
server.listen(socketPath, resolve);
|
|
44
|
+
});
|
|
45
|
+
if (!process.connected)
|
|
46
|
+
await stop();
|
|
47
|
+
else
|
|
48
|
+
process.send({ type: 'ready' });
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { PerformanceMetrics } from '../../../packages/protocol/src/metrics.js';
|
|
2
|
+
import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
|
|
3
|
+
import { getHeapStatistics } from 'node:v8';
|
|
4
|
+
export const diagnosticsEnabled = process.env.PRESUBMIT_DIAGNOSTICS === '1';
|
|
5
|
+
export const performanceMetrics = new PerformanceMetrics();
|
|
6
|
+
export const diagnosticGauges = new Map();
|
|
7
|
+
const collectionStarted = performance.now();
|
|
8
|
+
const initialCpu = process.cpuUsage();
|
|
9
|
+
const initialLoop = performance.eventLoopUtilization();
|
|
10
|
+
export function timed(name, operation) {
|
|
11
|
+
if (!diagnosticsEnabled)
|
|
12
|
+
return operation();
|
|
13
|
+
const start = performance.now();
|
|
14
|
+
try {
|
|
15
|
+
return operation();
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
performanceMetrics.observe(name, performance.now() - start);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function recordHttp(req, res, internal) {
|
|
22
|
+
if (!diagnosticsEnabled)
|
|
23
|
+
return;
|
|
24
|
+
const start = performance.now();
|
|
25
|
+
const kind = req.url?.startsWith('/api/') ? 'api' : 'assets';
|
|
26
|
+
const scope = internal ? 'internal' : 'public';
|
|
27
|
+
res.once('close', () => {
|
|
28
|
+
performanceMetrics.observe(`http.${scope}.${kind}`, performance.now() - start);
|
|
29
|
+
if (!res.writableFinished)
|
|
30
|
+
performanceMetrics.count('http.aborted');
|
|
31
|
+
else if (res.statusCode >= 400)
|
|
32
|
+
performanceMetrics.count('http.errors');
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** Opt-in process metrics only: no prompts, paths, credentials, or heap contents. */
|
|
36
|
+
export function startDiagnostics(role) {
|
|
37
|
+
if (process.env.PRESUBMIT_DIAGNOSTICS !== '1')
|
|
38
|
+
return () => { };
|
|
39
|
+
const delay = monitorEventLoopDelay({ resolution: 20 });
|
|
40
|
+
delay.enable();
|
|
41
|
+
let lastTime = collectionStarted;
|
|
42
|
+
let lastCpu = initialCpu;
|
|
43
|
+
let lastLoop = initialLoop;
|
|
44
|
+
const report = () => {
|
|
45
|
+
const now = performance.now();
|
|
46
|
+
const intervalMs = now - lastTime;
|
|
47
|
+
const cpu = process.cpuUsage();
|
|
48
|
+
const loop = performance.eventLoopUtilization();
|
|
49
|
+
const utilization = performance.eventLoopUtilization(loop, lastLoop);
|
|
50
|
+
const memory = process.memoryUsage();
|
|
51
|
+
const mib = (value) => Math.round(value / 1024 / 1024);
|
|
52
|
+
const report = {
|
|
53
|
+
role,
|
|
54
|
+
intervalMs: Math.round(intervalMs),
|
|
55
|
+
cpuPercent: intervalMs > 0
|
|
56
|
+
? Math.round((cpu.user - lastCpu.user + cpu.system - lastCpu.system) / (intervalMs * 10))
|
|
57
|
+
: 0,
|
|
58
|
+
eventLoopUtilization: Math.round(utilization.utilization * 1000) / 1000,
|
|
59
|
+
...performanceMetrics.drain(),
|
|
60
|
+
gauges: Object.fromEntries(diagnosticGauges),
|
|
61
|
+
pid: process.pid,
|
|
62
|
+
rssMiB: mib(memory.rss),
|
|
63
|
+
heapUsedMiB: mib(memory.heapUsed),
|
|
64
|
+
heapLimitMiB: mib(getHeapStatistics().heap_size_limit),
|
|
65
|
+
externalMiB: mib(memory.external),
|
|
66
|
+
eventLoopP99Ms: Math.round(delay.percentile(99) / 1e6),
|
|
67
|
+
eventLoopMaxMs: Math.round(delay.max / 1e6),
|
|
68
|
+
};
|
|
69
|
+
if (process.env.PRESUBMIT_DIAGNOSTICS_FORMAT === 'json')
|
|
70
|
+
console.error('[presubmit metrics]', JSON.stringify(report));
|
|
71
|
+
else
|
|
72
|
+
console.error(formatDiagnostics(report));
|
|
73
|
+
lastTime = now;
|
|
74
|
+
lastCpu = cpu;
|
|
75
|
+
lastLoop = loop;
|
|
76
|
+
delay.reset();
|
|
77
|
+
};
|
|
78
|
+
report();
|
|
79
|
+
const timer = setInterval(report, 30_000);
|
|
80
|
+
timer.unref();
|
|
81
|
+
return () => {
|
|
82
|
+
clearInterval(timer);
|
|
83
|
+
delay.disable();
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function formatDiagnostics(r) {
|
|
87
|
+
const memory = (mib) => (mib >= 1024 ? `${(mib / 1024).toFixed(2)} GiB` : `${mib} MiB`);
|
|
88
|
+
const duration = (ms) => ms >= 1000 ? `${(ms / 1000).toFixed(2)} s` : `${ms.toFixed(2)} ms`;
|
|
89
|
+
const lines = [
|
|
90
|
+
`[presubmit metrics] ${r.role} · PID ${r.pid} · last ${(r.intervalMs / 1000).toFixed(1)} s`,
|
|
91
|
+
` CPU ${r.cpuPercent}% (${(r.cpuPercent / 100).toFixed(2)} cores) · event loop ${(r.eventLoopUtilization * 100).toFixed(1)}% busy`,
|
|
92
|
+
` Memory: RSS ${memory(r.rssMiB)} · JS heap ${memory(r.heapUsedMiB)} / ${memory(r.heapLimitMiB)} limit · external ${memory(r.externalMiB)}`,
|
|
93
|
+
` Event-loop delay: p99 ${r.eventLoopP99Ms} ms · max ${r.eventLoopMaxMs} ms`,
|
|
94
|
+
];
|
|
95
|
+
if (r.gauges['workspace.watchers'] !== undefined)
|
|
96
|
+
lines.push(` File watching: ${r.gauges['workspace.watchers']} shared watchers · ${r.gauges['workspace.subscribers']} session subscribers · ${r.counters['workspace.fileEvents'] ?? 0} events · ${r.counters['workspace.changeBatches'] ?? 0} batches`);
|
|
97
|
+
if (r.counters['ws.messages'] !== undefined || r.counters['ws.bytes'] !== undefined)
|
|
98
|
+
lines.push(` WebSocket: ${r.counters['ws.messages'] ?? 0} messages · ${((r.counters['ws.bytes'] ?? 0) / 1024 / 1024).toFixed(2)} MiB enqueued (includes internal relays)`);
|
|
99
|
+
for (const [name, value] of Object.entries(r.timings).sort(([a], [b]) => a.localeCompare(b)))
|
|
100
|
+
lines.push(` ${name}: ${value.count} calls · avg ${duration(value.meanMs)} · p95 ≤ ${duration(value.p95UpperMs)} · max ${duration(value.maxMs)} · total ${duration(value.totalMs)}`);
|
|
101
|
+
for (const [name, count] of Object.entries(r.counters))
|
|
102
|
+
if (!['ws.messages', 'ws.bytes', 'workspace.fileEvents', 'workspace.changeBatches'].includes(name))
|
|
103
|
+
lines.push(` ${name}: ${count}`);
|
|
104
|
+
return lines.join('\n');
|
|
105
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Catalog capabilities are server-owned; clients cannot opt unsupported models into paid tiers. */
|
|
2
|
+
export function parseFastMode(value, model) {
|
|
3
|
+
if (value !== undefined && typeof value !== 'boolean')
|
|
4
|
+
throw new Error('Fast mode must be a boolean.');
|
|
5
|
+
if (value === true && model?.supportsFastMode !== true)
|
|
6
|
+
throw new Error('Fast mode is unavailable for the selected model.');
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const exec = promisify(execFile);
|
|
4
|
+
export async function pickProjectFolder(platform = process.platform, run = (file, args) => exec(file, args, { maxBuffer: 64 * 1024 })) {
|
|
5
|
+
try {
|
|
6
|
+
let stdout;
|
|
7
|
+
if (platform === 'darwin') {
|
|
8
|
+
({ stdout } = await run('osascript', [
|
|
9
|
+
'-e',
|
|
10
|
+
'try\nactivate\nreturn POSIX path of (choose folder with prompt "Choose a project folder")\non error number -128\nreturn ""\nend try',
|
|
11
|
+
]));
|
|
12
|
+
}
|
|
13
|
+
else if (platform === 'win32') {
|
|
14
|
+
({ stdout } = await run('powershell.exe', [
|
|
15
|
+
'-NoProfile',
|
|
16
|
+
'-STA',
|
|
17
|
+
'-Command',
|
|
18
|
+
'Add-Type -AssemblyName System.Windows.Forms; $picker = New-Object System.Windows.Forms.FolderBrowserDialog; $picker.Description = "Choose a project folder"; $picker.ShowNewFolderButton = $false; try { if ($picker.ShowDialog() -eq "OK") { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8; [Console]::WriteLine($picker.SelectedPath) } } finally { $picker.Dispose() }',
|
|
19
|
+
]));
|
|
20
|
+
}
|
|
21
|
+
else if (platform === 'linux') {
|
|
22
|
+
try {
|
|
23
|
+
({ stdout } = await run('zenity', [
|
|
24
|
+
'--file-selection',
|
|
25
|
+
'--directory',
|
|
26
|
+
'--title=Choose a project folder',
|
|
27
|
+
]));
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code !== 'ENOENT')
|
|
31
|
+
throw error;
|
|
32
|
+
({ stdout } = await run('kdialog', [
|
|
33
|
+
'--getexistingdirectory',
|
|
34
|
+
'.',
|
|
35
|
+
'--title',
|
|
36
|
+
'Choose a project folder',
|
|
37
|
+
]));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
throw new Error('Folder picking is not supported on this operating system.');
|
|
42
|
+
}
|
|
43
|
+
// Remove the command's line ending without trimming valid spaces in folder names.
|
|
44
|
+
return stdout.replace(/\r?\n$/, '') || null;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (platform === 'linux' && error.code === 1)
|
|
48
|
+
return null;
|
|
49
|
+
throw new Error('Could not open the folder picker on this computer. ' +
|
|
50
|
+
(platform === 'linux'
|
|
51
|
+
? 'A desktop session with Zenity or KDialog is required.'
|
|
52
|
+
: 'Please try again.'), { cause: error });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
/** Read immutable objects in bounded batches instead of spawning Git for every file. */
|
|
3
|
+
export async function* readTreeBlobs(cwd, tree, maxFileBytes) {
|
|
4
|
+
const entries = tree
|
|
5
|
+
.split('\0')
|
|
6
|
+
.filter(Boolean)
|
|
7
|
+
.map((entry) => {
|
|
8
|
+
const match = /^(\d+) (\w+) ([a-f0-9]{40,64})\s+(\d+|-)\t([\s\S]+)$/.exec(entry);
|
|
9
|
+
if (!match)
|
|
10
|
+
throw new Error('Invalid Git tree entry');
|
|
11
|
+
const [, mode, type, object, size, name] = match;
|
|
12
|
+
return {
|
|
13
|
+
name: name,
|
|
14
|
+
object: object,
|
|
15
|
+
size: Number(size),
|
|
16
|
+
...(type !== 'blob' || mode === '120000' || Number(size) > maxFileBytes
|
|
17
|
+
? { excluded: 'non-text, symlink, or oversized' }
|
|
18
|
+
: {}),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
let offset = 0;
|
|
22
|
+
while (offset < entries.length) {
|
|
23
|
+
const batch = [];
|
|
24
|
+
let size = 0;
|
|
25
|
+
while (offset < entries.length && batch.length < 128) {
|
|
26
|
+
const entry = entries[offset];
|
|
27
|
+
const bytes = entry.excluded ? 0 : entry.size;
|
|
28
|
+
if (batch.length && size + bytes > maxFileBytes)
|
|
29
|
+
break;
|
|
30
|
+
batch.push(entry);
|
|
31
|
+
size += bytes;
|
|
32
|
+
offset++;
|
|
33
|
+
}
|
|
34
|
+
const requested = batch.filter((entry) => !entry.excluded);
|
|
35
|
+
const output = requested.length
|
|
36
|
+
? await new Promise((resolve, reject) => {
|
|
37
|
+
const child = execFile('git', ['cat-file', '--batch'], { cwd, encoding: 'buffer', maxBuffer: size + requested.length * 128 }, (error, stdout) => (error ? reject(error) : resolve(stdout)));
|
|
38
|
+
child.stdin.on('error', reject);
|
|
39
|
+
child.stdin.end(requested.map((entry) => entry.object + '\n').join(''));
|
|
40
|
+
})
|
|
41
|
+
: Buffer.alloc(0);
|
|
42
|
+
let cursor = 0;
|
|
43
|
+
for (const entry of batch) {
|
|
44
|
+
if (entry.excluded) {
|
|
45
|
+
yield { name: entry.name, excluded: entry.excluded };
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const end = output.indexOf(10, cursor);
|
|
49
|
+
const header = output.subarray(cursor, end).toString('ascii');
|
|
50
|
+
if (end < cursor || header !== `${entry.object} blob ${entry.size}`)
|
|
51
|
+
throw new Error('Invalid Git blob response');
|
|
52
|
+
cursor = end + 1;
|
|
53
|
+
const content = output.subarray(cursor, cursor + entry.size);
|
|
54
|
+
cursor += entry.size;
|
|
55
|
+
if (content.length !== entry.size || output[cursor++] !== 10)
|
|
56
|
+
throw new Error('Incomplete Git blob response');
|
|
57
|
+
yield { name: entry.name, content };
|
|
58
|
+
}
|
|
59
|
+
if (cursor !== output.length)
|
|
60
|
+
throw new Error('Unexpected Git blob response');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { access, realpath } from 'node:fs/promises';
|
|
4
|
+
import { listSessionBranches, validateSessionWorkspace } from './session-workspaces.js';
|
|
5
|
+
const execute = promisify(execFile);
|
|
6
|
+
export const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
7
|
+
async function git(cwd, ...args) {
|
|
8
|
+
const { stdout } = await execute('git', args, {
|
|
9
|
+
cwd,
|
|
10
|
+
encoding: 'utf8',
|
|
11
|
+
timeout: 15_000,
|
|
12
|
+
maxBuffer: 1024 * 1024,
|
|
13
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: '0', GIT_TERMINAL_PROMPT: '0' },
|
|
14
|
+
});
|
|
15
|
+
return stdout.trim();
|
|
16
|
+
}
|
|
17
|
+
async function inspect(path) {
|
|
18
|
+
const [branch, commit, status, unmerged, gitDir] = await Promise.all([
|
|
19
|
+
git(path, 'rev-parse', '--abbrev-ref', 'HEAD'),
|
|
20
|
+
git(path, 'rev-parse', '--verify', 'HEAD^{commit}'),
|
|
21
|
+
git(path, 'status', '--porcelain=v1', '-z', '--untracked-files=all'),
|
|
22
|
+
git(path, 'ls-files', '--unmerged'),
|
|
23
|
+
git(path, 'rev-parse', '--absolute-git-dir'),
|
|
24
|
+
]);
|
|
25
|
+
const operations = await Promise.all([
|
|
26
|
+
'MERGE_HEAD',
|
|
27
|
+
'CHERRY_PICK_HEAD',
|
|
28
|
+
'REVERT_HEAD',
|
|
29
|
+
'rebase-merge',
|
|
30
|
+
'rebase-apply',
|
|
31
|
+
'sequencer',
|
|
32
|
+
].map(async (name) => {
|
|
33
|
+
try {
|
|
34
|
+
await access(`${gitDir}/${name}`);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}));
|
|
41
|
+
return {
|
|
42
|
+
path,
|
|
43
|
+
branch: branch === 'HEAD' ? null : branch,
|
|
44
|
+
commit,
|
|
45
|
+
dirty: !!status,
|
|
46
|
+
conflict: !!unmerged || operations.some(Boolean),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Read-only snapshot. Never executes any suggested mutation or predicts conflict-free merging. */
|
|
50
|
+
export async function inspectGitHandoff(repository, session, sessions, destination) {
|
|
51
|
+
const result = {
|
|
52
|
+
checkedAt: new Date().toISOString(),
|
|
53
|
+
branches: [],
|
|
54
|
+
blockers: [],
|
|
55
|
+
inspection: '',
|
|
56
|
+
};
|
|
57
|
+
if (!session.branch || session.workspace === repository) {
|
|
58
|
+
result.blockers.push('Guided Git handoff is only available for worktree sessions.');
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
result.branches = (await listSessionBranches(repository)).branches;
|
|
63
|
+
await validateSessionWorkspace(repository, session.workspace, session.branch);
|
|
64
|
+
const source = await inspect(session.workspace);
|
|
65
|
+
result.source = source;
|
|
66
|
+
result.inspection = `git -C ${shellQuote(source.path)} status\ngit -C ${shellQuote(source.path)} log -5 --oneline`;
|
|
67
|
+
if (source.branch !== session.branch)
|
|
68
|
+
result.blockers.push('Source branch changed or HEAD is detached. Restore the session branch, then refresh; do not switch with unsaved work.');
|
|
69
|
+
if (source.dirty)
|
|
70
|
+
result.blockers.push('Source has uncommitted or untracked work. Review and commit intended changes yourself, then refresh. Only committed changes can be integrated.');
|
|
71
|
+
if (source.conflict)
|
|
72
|
+
result.blockers.push('Source has conflicts or an unfinished Git operation. Resolve it manually before continuing.');
|
|
73
|
+
if (!destination) {
|
|
74
|
+
result.blockers.push('Choose a destination branch.');
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
if (!result.branches.includes(destination)) {
|
|
78
|
+
result.blockers.push('Destination branch is unavailable.');
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
if (destination === source.branch) {
|
|
82
|
+
result.blockers.push('Choose a different destination branch.');
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
const entries = (await git(repository, 'worktree', 'list', '--porcelain', '-z')).split('\0\0');
|
|
86
|
+
const entry = entries.find((item) => item.split('\0').includes(`branch refs/heads/${destination}`));
|
|
87
|
+
const destinationPath = entry
|
|
88
|
+
?.split('\0')
|
|
89
|
+
.find((line) => line.startsWith('worktree '))
|
|
90
|
+
?.slice(9);
|
|
91
|
+
if (!destinationPath) {
|
|
92
|
+
result.blockers.push('Destination is not checked out. In a clean, idle checkout, switch to the chosen branch (or create a worktree yourself), then refresh. Inspect git worktree list first.');
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
const target = await inspect(destinationPath);
|
|
96
|
+
result.destination = { ...target, branch: destination };
|
|
97
|
+
if (target.branch !== destination)
|
|
98
|
+
result.blockers.push('Destination changed during inspection. Refresh.');
|
|
99
|
+
if (target.dirty)
|
|
100
|
+
result.blockers.push('Destination has uncommitted or untracked work. Preserve it with a commit or stash yourself before integrating.');
|
|
101
|
+
if (target.conflict)
|
|
102
|
+
result.blockers.push('Destination has conflicts or an unfinished Git operation. Resolve it manually before continuing.');
|
|
103
|
+
const paths = await Promise.all([realpath(source.path), realpath(target.path)]);
|
|
104
|
+
for (const peer of sessions) {
|
|
105
|
+
if (!['running', 'waiting', 'unavailable'].includes(peer.status))
|
|
106
|
+
continue;
|
|
107
|
+
const peerPath = await realpath(peer.workspace).catch(() => peer.workspace);
|
|
108
|
+
if (paths.includes(peerPath))
|
|
109
|
+
result.blockers.push(`Session “${peer.title}” is ${peer.status}. Stop/wait for all sessions using these workspaces, including child tasks, before Git operations.`);
|
|
110
|
+
}
|
|
111
|
+
const ancestor = await git(repository, 'merge-base', source.commit, target.commit).catch(() => '');
|
|
112
|
+
result.contained = ancestor === source.commit;
|
|
113
|
+
const prefix = `git -C ${shellQuote(target.path)}`;
|
|
114
|
+
result.inspection += `\n${prefix} status\n${prefix} log --oneline ${shellQuote(`${target.commit}..${source.commit}`)}\n${prefix} diff --stat ${shellQuote(target.commit)} ${shellQuote(source.commit)}`;
|
|
115
|
+
if (!result.blockers.length && !result.contained) {
|
|
116
|
+
// Preconditions fail closed if either HEAD or the working files changed since inspection.
|
|
117
|
+
result.integration = `test "$(git -C ${shellQuote(source.path)} symbolic-ref --quiet HEAD)" = ${shellQuote(`refs/heads/${session.branch}`)} &&\ntest "$(git -C ${shellQuote(source.path)} rev-parse HEAD)" = ${shellQuote(source.commit)} &&\nhandoff_status=$(git -C ${shellQuote(source.path)} status --porcelain) && test -z "$handoff_status" &&\ntest "$(${prefix} symbolic-ref --quiet HEAD)" = ${shellQuote(`refs/heads/${destination}`)} &&\ntest "$(${prefix} rev-parse HEAD)" = ${shellQuote(target.commit)} &&\nhandoff_status=$(${prefix} status --porcelain) && test -z "$handoff_status" &&\n${prefix} merge --no-edit ${shellQuote(source.commit)}`;
|
|
118
|
+
}
|
|
119
|
+
if (!result.blockers.length && result.contained) {
|
|
120
|
+
result.cleanup = `git -C ${shellQuote(repository)} worktree remove -- ${shellQuote(source.path)}\n# Optional, only after verifying the branch is no longer needed:\ngit -C ${shellQuote(target.path)} branch -d -- ${shellQuote(session.branch)}`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
result.blockers.push('Worktree or Git status is unavailable. Restore missing worktrees or repository access and refresh. Nothing was changed.');
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/** Cache source extraction by visible reply sequence and completed prompts. */
|
|
3
|
+
export function historySources(sessions) {
|
|
4
|
+
const cache = new WeakMap();
|
|
5
|
+
return () => sessions().flatMap((session) => {
|
|
6
|
+
session.store.replySearch.sync();
|
|
7
|
+
const runs = session.runs.filter((run) => run.status !== 'running');
|
|
8
|
+
const latest = session.store.db
|
|
9
|
+
.prepare('SELECT max(seq) AS seq FROM reply_search_chunks')
|
|
10
|
+
.get();
|
|
11
|
+
const signature = JSON.stringify([
|
|
12
|
+
session.title,
|
|
13
|
+
latest?.seq,
|
|
14
|
+
runs.map((r) => [r.id, r.prompt, r.createdAt]),
|
|
15
|
+
]);
|
|
16
|
+
const saved = cache.get(session.store);
|
|
17
|
+
if (saved?.signature === signature)
|
|
18
|
+
return saved.docs;
|
|
19
|
+
const docs = [];
|
|
20
|
+
const byId = new Map(runs.map((run) => [run.id, run]));
|
|
21
|
+
function add(run, kind, block, text) {
|
|
22
|
+
// Keep model inputs well below the local embedding model's context window.
|
|
23
|
+
for (let offset = 0; offset < text.length; offset += 1040) {
|
|
24
|
+
let start = offset;
|
|
25
|
+
let end = Math.min(text.length, offset + 1200);
|
|
26
|
+
// Do not split an emoji or other surrogate pair at either chunk boundary.
|
|
27
|
+
if (start && /[\uDC00-\uDFFF]/.test(text[start]))
|
|
28
|
+
start--;
|
|
29
|
+
if (end < text.length && /[\uDC00-\uDFFF]/.test(text[end]))
|
|
30
|
+
end--;
|
|
31
|
+
const part = text.slice(start, end);
|
|
32
|
+
if (!part.trim())
|
|
33
|
+
continue;
|
|
34
|
+
docs.push({
|
|
35
|
+
sourceId: createHash('sha256')
|
|
36
|
+
.update(JSON.stringify([session.id, run.id, kind, block, offset]))
|
|
37
|
+
.digest('hex'),
|
|
38
|
+
sessionId: session.id,
|
|
39
|
+
sessionTitle: session.title,
|
|
40
|
+
runId: run.id,
|
|
41
|
+
kind,
|
|
42
|
+
createdAt: run.createdAt,
|
|
43
|
+
text: part,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const run of runs)
|
|
48
|
+
add(run, 'prompt', 'prompt', run.prompt);
|
|
49
|
+
for (const row of session.store.db
|
|
50
|
+
.prepare('SELECT id,run_id,json_quote(text) AS text FROM reply_search_chunks')
|
|
51
|
+
.iterate()) {
|
|
52
|
+
const run = byId.get(String(row.run_id));
|
|
53
|
+
if (run)
|
|
54
|
+
add(run, 'reply', String(row.id), JSON.parse(String(row.text)));
|
|
55
|
+
}
|
|
56
|
+
cache.set(session.store, { signature, docs });
|
|
57
|
+
return docs;
|
|
58
|
+
});
|
|
59
|
+
}
|