@cueloop/daemon 0.1.0-alpha.101 → 0.1.0-alpha.103
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/package.json +3 -3
- package/src/agent-tools.ts +6 -0
- package/src/api.ts +63 -0
- package/src/client-reliability.test.ts +2 -0
- package/src/client.ts +24 -2
- package/src/curate.ts +2 -0
- package/src/daemon-sdk.ts +8 -0
- package/src/diff-hot-reload.test.ts +4 -0
- package/src/diff-watcher.test.ts +2 -0
- package/src/diff-watcher.ts +33 -0
- package/src/examples/daemon-sdk.ts +2 -0
- package/src/gh.ts +4 -0
- package/src/ghostty-thread-surface-store.ts +3 -0
- package/src/ghostty-thread-surface.test.ts +2 -0
- package/src/ghostty-thread-surface.ts +6 -0
- package/src/harness-diagnostics.ts +1 -0
- package/src/harness-handoff.test.ts +725 -0
- package/src/harness-handoff.ts +27 -0
- package/src/harness-state-store.ts +5 -0
- package/src/herdr-split.ts +5 -0
- package/src/herdr-thread-surface.ts +14 -0
- package/src/interruptible-wait.ts +3 -0
- package/src/jj-working-tree.ts +9 -0
- package/src/main.ts +1 -0
- package/src/operation-receipts.ts +6 -0
- package/src/pr-poller.ts +7 -0
- package/src/project-files.ts +4 -0
- package/src/protocol.ts +4 -0
- package/src/retention.ts +8 -0
- package/src/server.ts +36 -7
- package/src/share-blob.test.ts +2 -0
- package/src/single-instance.test.ts +3 -0
- package/src/store.ts +11 -0
- package/src/thread-agent-validation.ts +20 -0
- package/src/thread-agent.test.ts +14 -1
- package/src/thread-agent.ts +361 -25
- package/src/thread-review.test.ts +4 -0
- package/src/thread-review.ts +8 -0
- package/src/thread-subscription.test.ts +3 -0
- package/src/thread-subscription.ts +9 -0
- package/src/validate.test.ts +4 -0
- package/src/validate.ts +3 -0
- package/src/vcs-source.ts +6 -0
- package/src/working-tree.ts +4 -0
|
@@ -59,6 +59,7 @@ describe("session.refreshDiff", () => {
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
if (init.exitCode !== 0) throw new Error(init.stderr.toString());
|
|
62
|
+
|
|
62
63
|
writeFileSync(join(repo, "a.ts"), "export const a = 2;\n");
|
|
63
64
|
const first = await core.repoDiff(repo);
|
|
64
65
|
const workspace = await resolveWorkspace(repo);
|
|
@@ -81,6 +82,7 @@ describe("session.refreshDiff", () => {
|
|
|
81
82
|
});
|
|
82
83
|
|
|
83
84
|
if (describe.exitCode !== 0) throw new Error(describe.stderr.toString());
|
|
85
|
+
|
|
84
86
|
const result = await core.sessionRefreshDiff(session.id);
|
|
85
87
|
const current = core.sessionGet(session.id);
|
|
86
88
|
|
|
@@ -100,6 +102,7 @@ describe("session.refreshDiff", () => {
|
|
|
100
102
|
});
|
|
101
103
|
|
|
102
104
|
if (init.exitCode !== 0) throw new Error(init.stderr.toString());
|
|
105
|
+
|
|
103
106
|
writeFileSync(join(repo, "a.ts"), "export const a = 2;\n");
|
|
104
107
|
const first = await core.repoDiff(repo);
|
|
105
108
|
const workspace = await resolveWorkspace(repo);
|
|
@@ -339,6 +342,7 @@ describe("the fs watcher drives hot-reload", () => {
|
|
|
339
342
|
});
|
|
340
343
|
|
|
341
344
|
if (init.exitCode !== 0) throw new Error(init.stderr.toString());
|
|
345
|
+
|
|
342
346
|
const session = await core.workbenchSession(repo);
|
|
343
347
|
|
|
344
348
|
expect(session.artifact.meta.vcs).toBe("jj");
|
package/src/diff-watcher.test.ts
CHANGED
|
@@ -18,6 +18,7 @@ test("startup reconciliation watches a directory whose creation event was missed
|
|
|
18
18
|
const change = Promise.withResolvers<void>();
|
|
19
19
|
const watcher = new DiffWatcher(() => {
|
|
20
20
|
refreshes++;
|
|
21
|
+
|
|
21
22
|
if (refreshes === 1) startup.resolve();
|
|
22
23
|
else change.resolve();
|
|
23
24
|
});
|
|
@@ -91,6 +92,7 @@ test("replacing a watch during reconciliation stops the old directory traversal"
|
|
|
91
92
|
const watcher = new DiffWatcher(() => {});
|
|
92
93
|
const spy = spyOn(directoryReads, "readdir").mockImplementation((path) => {
|
|
93
94
|
scanned.push(String(path));
|
|
95
|
+
|
|
94
96
|
if (path === first) {
|
|
95
97
|
scanStarted.resolve();
|
|
96
98
|
|
package/src/diff-watcher.ts
CHANGED
|
@@ -38,6 +38,7 @@ function ignoredDirectories(repoRoot: string): Set<string> {
|
|
|
38
38
|
);
|
|
39
39
|
|
|
40
40
|
if (!result || result.exitCode !== 0) return ignored;
|
|
41
|
+
|
|
41
42
|
for (const line of result.stdout.toString().split("\n")) {
|
|
42
43
|
const relative = line.trim().replace(/\/$/, "");
|
|
43
44
|
|
|
@@ -77,9 +78,11 @@ function resolveGitDirs(repoRoot: string): { gitDir: string; commonDir: string }
|
|
|
77
78
|
const result = spawnGit(["rev-parse", "--absolute-git-dir", "--git-common-dir"], repoRoot);
|
|
78
79
|
|
|
79
80
|
if (!result || result.exitCode !== 0) return null;
|
|
81
|
+
|
|
80
82
|
const [gitDir, commonRaw] = result.stdout.toString().trim().split("\n");
|
|
81
83
|
|
|
82
84
|
if (!gitDir) return null;
|
|
85
|
+
|
|
83
86
|
const commonDir = commonRaw
|
|
84
87
|
? isAbsolute(commonRaw)
|
|
85
88
|
? commonRaw
|
|
@@ -117,10 +120,12 @@ export class DiffWatcher {
|
|
|
117
120
|
|
|
118
121
|
if (existing) {
|
|
119
122
|
existing.sessionIds.add(sessionId);
|
|
123
|
+
|
|
120
124
|
if (vcs === "jj") this.startJjPoll(existing, repoRoot, sessionId);
|
|
121
125
|
|
|
122
126
|
return;
|
|
123
127
|
}
|
|
128
|
+
|
|
124
129
|
const repoWatch: RepoWatch = {
|
|
125
130
|
handles: [],
|
|
126
131
|
sessionIds: new Set([sessionId]),
|
|
@@ -133,6 +138,7 @@ export class DiffWatcher {
|
|
|
133
138
|
};
|
|
134
139
|
|
|
135
140
|
this.repoWatches.set(repoRoot, repoWatch);
|
|
141
|
+
|
|
136
142
|
if (vcs === "jj") this.startJjPoll(repoWatch, repoRoot, sessionId);
|
|
137
143
|
|
|
138
144
|
// one watcher per non-ignored working-tree directory; a tracked-file change re-captures
|
|
@@ -157,6 +163,7 @@ export class DiffWatcher {
|
|
|
157
163
|
this.watchInto(repoWatch.handles, join(gitDirs.commonDir, "refs"), { recursive: true }, () =>
|
|
158
164
|
this.scheduleRepoRefresh(repoRoot),
|
|
159
165
|
);
|
|
166
|
+
|
|
160
167
|
if (gitDirs.commonDir !== gitDirs.gitDir)
|
|
161
168
|
this.watchInto(repoWatch.handles, gitDirs.commonDir, { recursive: false }, (filename) => {
|
|
162
169
|
if (isRefChange(filename)) this.scheduleRepoRefresh(repoRoot);
|
|
@@ -169,7 +176,9 @@ export class DiffWatcher {
|
|
|
169
176
|
|
|
170
177
|
private startJjPoll(repoWatch: RepoWatch, repoRoot: string, sessionId: string): void {
|
|
171
178
|
repoWatch.jjSessionIds.add(sessionId);
|
|
179
|
+
|
|
172
180
|
if (repoWatch.jjPoll !== null) return;
|
|
181
|
+
|
|
173
182
|
repoWatch.jjPoll = setInterval(() => this.scheduleRepoRefresh(repoRoot), 2000);
|
|
174
183
|
}
|
|
175
184
|
|
|
@@ -183,7 +192,9 @@ export class DiffWatcher {
|
|
|
183
192
|
const repoWatch = this.repoWatches.get(repoRoot);
|
|
184
193
|
|
|
185
194
|
if (!repoWatch) return false;
|
|
195
|
+
|
|
186
196
|
if (repoWatch.watchedDirs.has(dir)) return true;
|
|
197
|
+
|
|
187
198
|
if (ALWAYS_IGNORED_DIRS.has(basename(dir)) || repoWatch.ignored.has(dir)) return true;
|
|
188
199
|
|
|
189
200
|
const opened = this.watchInto(repoWatch.handles, dir, { recursive: false }, (filename) => {
|
|
@@ -193,6 +204,7 @@ export class DiffWatcher {
|
|
|
193
204
|
});
|
|
194
205
|
|
|
195
206
|
if (!opened) return false;
|
|
207
|
+
|
|
196
208
|
repoWatch.watchedDirs.add(dir);
|
|
197
209
|
|
|
198
210
|
let entries: Dirent[];
|
|
@@ -223,8 +235,11 @@ export class DiffWatcher {
|
|
|
223
235
|
// the path is already gone (a transient temp dir); nothing to watch
|
|
224
236
|
return;
|
|
225
237
|
}
|
|
238
|
+
|
|
226
239
|
if (ALWAYS_IGNORED_DIRS.has(basename(path))) return;
|
|
240
|
+
|
|
227
241
|
if (isGitIgnored(repoRoot, path)) return;
|
|
242
|
+
|
|
228
243
|
this.watchTree(repoRoot, path);
|
|
229
244
|
}
|
|
230
245
|
|
|
@@ -233,11 +248,13 @@ export class DiffWatcher {
|
|
|
233
248
|
const repoWatch = this.repoWatches.get(repoRoot);
|
|
234
249
|
|
|
235
250
|
if (!repoWatch) return;
|
|
251
|
+
|
|
236
252
|
if (!repoWatch.watchedDirs.has(dir)) {
|
|
237
253
|
this.extendWatch(repoRoot, dir);
|
|
238
254
|
|
|
239
255
|
return;
|
|
240
256
|
}
|
|
257
|
+
|
|
241
258
|
let entries: Dirent[];
|
|
242
259
|
|
|
243
260
|
try {
|
|
@@ -245,9 +262,12 @@ export class DiffWatcher {
|
|
|
245
262
|
} catch {
|
|
246
263
|
return;
|
|
247
264
|
}
|
|
265
|
+
|
|
248
266
|
if (this.repoWatches.get(repoRoot) !== repoWatch) return;
|
|
267
|
+
|
|
249
268
|
for (const entry of entries) {
|
|
250
269
|
if (this.repoWatches.get(repoRoot) !== repoWatch) return;
|
|
270
|
+
|
|
251
271
|
if (entry.isDirectory() && !entry.isSymbolicLink())
|
|
252
272
|
// eslint-disable-next-line no-await-in-loop
|
|
253
273
|
await this.reconcileWatchTree(repoRoot, join(dir, entry.name));
|
|
@@ -285,15 +305,21 @@ export class DiffWatcher {
|
|
|
285
305
|
const repoWatch = this.repoWatches.get(repoRoot);
|
|
286
306
|
|
|
287
307
|
if (!repoWatch) return;
|
|
308
|
+
|
|
288
309
|
repoWatch.sessionIds.delete(sessionId);
|
|
289
310
|
repoWatch.jjSessionIds.delete(sessionId);
|
|
311
|
+
|
|
290
312
|
if (repoWatch.jjSessionIds.size === 0 && repoWatch.jjPoll !== null) {
|
|
291
313
|
clearInterval(repoWatch.jjPoll);
|
|
292
314
|
repoWatch.jjPoll = null;
|
|
293
315
|
}
|
|
316
|
+
|
|
294
317
|
if (repoWatch.sessionIds.size > 0) return;
|
|
318
|
+
|
|
295
319
|
if (repoWatch.debounce !== null) clearTimeout(repoWatch.debounce);
|
|
320
|
+
|
|
296
321
|
if (repoWatch.jjPoll !== null) clearInterval(repoWatch.jjPoll);
|
|
322
|
+
|
|
297
323
|
for (const handle of repoWatch.handles) handle.close();
|
|
298
324
|
this.repoWatches.delete(repoRoot);
|
|
299
325
|
}
|
|
@@ -302,14 +328,19 @@ export class DiffWatcher {
|
|
|
302
328
|
const repoWatch = this.repoWatches.get(repoRoot);
|
|
303
329
|
|
|
304
330
|
if (!repoWatch) return;
|
|
331
|
+
|
|
305
332
|
if (repoWatch.debounce !== null) clearTimeout(repoWatch.debounce);
|
|
333
|
+
|
|
306
334
|
repoWatch.debounce = setTimeout(async () => {
|
|
307
335
|
repoWatch.debounce = null;
|
|
336
|
+
|
|
308
337
|
if (repoWatch.reconcilePending) {
|
|
309
338
|
repoWatch.reconcilePending = false;
|
|
310
339
|
await this.reconcileWatchTree(repoRoot, repoRoot);
|
|
311
340
|
}
|
|
341
|
+
|
|
312
342
|
if (this.repoWatches.get(repoRoot) !== repoWatch) return;
|
|
343
|
+
|
|
313
344
|
this.onRepoChange(repoRoot);
|
|
314
345
|
}, DIFF_REFRESH_DEBOUNCE_MS);
|
|
315
346
|
}
|
|
@@ -318,7 +349,9 @@ export class DiffWatcher {
|
|
|
318
349
|
close(): void {
|
|
319
350
|
for (const repoWatch of this.repoWatches.values()) {
|
|
320
351
|
if (repoWatch.debounce !== null) clearTimeout(repoWatch.debounce);
|
|
352
|
+
|
|
321
353
|
if (repoWatch.jjPoll !== null) clearInterval(repoWatch.jjPoll);
|
|
354
|
+
|
|
322
355
|
for (const handle of repoWatch.handles) handle.close();
|
|
323
356
|
}
|
|
324
357
|
this.repoWatches.clear();
|
|
@@ -28,6 +28,7 @@ const reviewConversation = Effect.gen(function* () {
|
|
|
28
28
|
|
|
29
29
|
if (completed.outcome !== "completed")
|
|
30
30
|
throw new Error(`SDK example agent finished: ${completed.outcome}`);
|
|
31
|
+
|
|
31
32
|
const comments = yield* sdk.comments.list(thread.id);
|
|
32
33
|
|
|
33
34
|
if (comments[0])
|
|
@@ -36,6 +37,7 @@ const reviewConversation = Effect.gen(function* () {
|
|
|
36
37
|
commentId: comments[0].id,
|
|
37
38
|
body: "The explanation is clear.",
|
|
38
39
|
});
|
|
40
|
+
|
|
39
41
|
yield* sdk.sessions.sendMessage({
|
|
40
42
|
threadId: thread.id,
|
|
41
43
|
operationId: sdk.ids.operation(randomUUID()),
|
package/src/gh.ts
CHANGED
|
@@ -26,6 +26,7 @@ export async function prRefs(pr: string): Promise<PullRequestRefs | null> {
|
|
|
26
26
|
const { code, stdout } = await gh(["pr", "view", pr, "--json", "baseRefOid,headRefOid"]);
|
|
27
27
|
|
|
28
28
|
if (code !== 0) return null;
|
|
29
|
+
|
|
29
30
|
let input: unknown;
|
|
30
31
|
|
|
31
32
|
try {
|
|
@@ -60,12 +61,15 @@ export async function prSnapshot(
|
|
|
60
61
|
const before = await readRefs(pr);
|
|
61
62
|
|
|
62
63
|
if (before === null) return null;
|
|
64
|
+
|
|
63
65
|
const patch = await readDiff(pr);
|
|
64
66
|
|
|
65
67
|
if (patch === null) return null;
|
|
68
|
+
|
|
66
69
|
const after = await readRefs(pr);
|
|
67
70
|
|
|
68
71
|
if (after === null) return null;
|
|
72
|
+
|
|
69
73
|
if (before.baseSha === after.baseSha && before.headSha === after.headSha)
|
|
70
74
|
return { patch, ...after };
|
|
71
75
|
}
|
|
@@ -65,6 +65,7 @@ export class GhosttyThreadSurfaceStore {
|
|
|
65
65
|
const previous = this.surfaces.get(threadId);
|
|
66
66
|
|
|
67
67
|
if (previous?.opening) return false;
|
|
68
|
+
|
|
68
69
|
this.surfaces.set(threadId, { ...previous, opening: true });
|
|
69
70
|
|
|
70
71
|
try {
|
|
@@ -83,8 +84,10 @@ export class GhosttyThreadSurfaceStore {
|
|
|
83
84
|
const record = this.surfaces.get(threadId);
|
|
84
85
|
|
|
85
86
|
if (!record?.opening) return;
|
|
87
|
+
|
|
86
88
|
if (record.terminalId) this.surfaces.set(threadId, { terminalId: record.terminalId });
|
|
87
89
|
else this.surfaces.delete(threadId);
|
|
90
|
+
|
|
88
91
|
this.persist();
|
|
89
92
|
}
|
|
90
93
|
|
|
@@ -54,6 +54,7 @@ function persistence(initial: GhosttyThreadSurfaceHandle | null = null) {
|
|
|
54
54
|
const port: GhosttyThreadSurfacePersistence = {
|
|
55
55
|
ghosttyClaimThreadSurface: async () => {
|
|
56
56
|
if (claimed) return false;
|
|
57
|
+
|
|
57
58
|
claimed = true;
|
|
58
59
|
|
|
59
60
|
return true;
|
|
@@ -316,6 +317,7 @@ describe("openGhosttyThreadSurface", () => {
|
|
|
316
317
|
const store: GhosttyThreadSurfacePersistence = {
|
|
317
318
|
ghosttyClaimThreadSurface: async () => {
|
|
318
319
|
if (claimed) return false;
|
|
320
|
+
|
|
319
321
|
claimed = true;
|
|
320
322
|
|
|
321
323
|
return true;
|
|
@@ -139,10 +139,12 @@ export function resolveCueloopLaunchCommand(
|
|
|
139
139
|
|
|
140
140
|
for (const directory of path?.split(delimiter) ?? []) {
|
|
141
141
|
if (!isAbsolute(directory)) continue;
|
|
142
|
+
|
|
142
143
|
const candidate = join(directory, "cueloop");
|
|
143
144
|
|
|
144
145
|
try {
|
|
145
146
|
accessSync(candidate, constants.X_OK);
|
|
147
|
+
|
|
146
148
|
if (statSync(candidate).isFile()) return shellQuote(candidate);
|
|
147
149
|
} catch {
|
|
148
150
|
// A PATH entry without an executable cannot launch the Thread.
|
|
@@ -161,6 +163,7 @@ export function supportsGhosttyAppleScript(version: string | null): boolean {
|
|
|
161
163
|
const parsed = version?.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
162
164
|
|
|
163
165
|
if (!parsed) return false;
|
|
166
|
+
|
|
164
167
|
const major = Number(parsed[1]);
|
|
165
168
|
const minor = Number(parsed[2]);
|
|
166
169
|
|
|
@@ -183,7 +186,9 @@ export async function openGhosttyThreadSurface(
|
|
|
183
186
|
cueloopBinPath?: string | null,
|
|
184
187
|
): Promise<ThreadSurfaceOpenStatus> {
|
|
185
188
|
if (platform !== "darwin" || !insideGhostty(env)) return "unavailable";
|
|
189
|
+
|
|
186
190
|
if (mode === "none") return "disabled";
|
|
191
|
+
|
|
187
192
|
if (!supportsGhosttyAppleScript(runAppleScript(binPath, VERSION_SCRIPT))) return "failed";
|
|
188
193
|
|
|
189
194
|
try {
|
|
@@ -236,6 +241,7 @@ async function openClaimedGhosttyThreadSurface(
|
|
|
236
241
|
const result = runAppleScript(binPath, FOCUS_SCRIPT, [recorded.terminalId]);
|
|
237
242
|
|
|
238
243
|
if (result === "focused") return { status: "focused", retainClaim: false };
|
|
244
|
+
|
|
239
245
|
if (result !== "closed") return { status: "failed", retainClaim: false };
|
|
240
246
|
}
|
|
241
247
|
|
|
@@ -20,6 +20,7 @@ export function writeHarnessDiagnostic(
|
|
|
20
20
|
|
|
21
21
|
try {
|
|
22
22
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
23
|
+
|
|
23
24
|
if (existsSync(path) && statSync(path).size + Buffer.byteLength(record) > 256 * 1024)
|
|
24
25
|
writeFileSync(path, record, { mode: 0o600 });
|
|
25
26
|
else appendFileSync(path, record, { mode: 0o600 });
|