claude-maestro 0.1.18 → 0.1.19
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/out/main/index.js +928 -56
- package/out/preload/index.js +10 -0
- package/out/renderer/assets/{index-BTPlkF9b.js → index-1Z03T0zz.js} +2 -2
- package/out/renderer/assets/{index-DSDf1IPU.js → index-9AHdXE8U.js} +2 -2
- package/out/renderer/assets/{index-DVz1TdHX.js → index-B59uuZRU.js} +4 -4
- package/out/renderer/assets/{index-BuCQKc-Z.js → index-Bg4ondS2.js} +2 -2
- package/out/renderer/assets/{index-CA5CvYDB.js → index-BkOzhsuz.js} +1 -1
- package/out/renderer/assets/{index-3GYmITDo.js → index-C0rsWi9C.js} +2 -2
- package/out/renderer/assets/{index-D2J11DOI.js → index-C479DZmL.js} +5 -5
- package/out/renderer/assets/{index-H1mXv84m.js → index-CNNAMsV1.js} +2 -2
- package/out/renderer/assets/{index-DTKHKVCm.js → index-CTxGDYbk.js} +997 -112
- package/out/renderer/assets/{index-C9LaSnRB.js → index-CVWvgy2Y.js} +2 -2
- package/out/renderer/assets/{index-CYds92PN.js → index-CWk6CwGd.js} +3 -3
- package/out/renderer/assets/{index-DYixubwQ.js → index-CXeHg_Qc.js} +2 -2
- package/out/renderer/assets/{index-DLIiZzc_.js → index-CZP8wVw-.js} +2 -2
- package/out/renderer/assets/{index-DypnZCas.js → index-CoyUYEik.js} +3 -3
- package/out/renderer/assets/{index-CIscupyH.js → index-Cq5xQaOf.js} +5 -5
- package/out/renderer/assets/{index-dBdOx5at.js → index-CuHjzw7d.js} +5 -5
- package/out/renderer/assets/{index-C27Lfjw4.js → index-D9GPva9-.js} +5 -5
- package/out/renderer/assets/{index-C3tXFLVi.js → index-DI2ly48w.js} +2 -2
- package/out/renderer/assets/{index-D14Kkf05.js → index-DJwKAmOm.js} +2 -2
- package/out/renderer/assets/{index-BW8lIWcc.css → index-Dgaj6c_K.css} +4809 -4514
- package/out/renderer/assets/{index-tMx4AmRy.js → index-Dhxn3JIv.js} +2 -2
- package/out/renderer/assets/{index-CngVaxBn.js → index-JMVyecfQ.js} +5 -5
- package/out/renderer/assets/{index-D48Zxs0r.js → index-LW-gCnC-.js} +2 -2
- package/out/renderer/assets/{index-CrDDqySQ.js → index-jAA5WJm3.js} +5 -5
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
package/out/main/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const electron = require("electron");
|
|
3
3
|
const path = require("path");
|
|
4
|
-
const crypto = require("crypto");
|
|
5
4
|
const fs = require("fs");
|
|
6
|
-
const child_process = require("child_process");
|
|
7
5
|
const os = require("os");
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
const child_process = require("child_process");
|
|
8
8
|
const pty = require("node-pty");
|
|
9
9
|
const chokidar = require("chokidar");
|
|
10
10
|
const promises = require("fs/promises");
|
|
@@ -25,6 +25,278 @@ function _interopNamespaceDefault(e) {
|
|
|
25
25
|
return Object.freeze(n);
|
|
26
26
|
}
|
|
27
27
|
const pty__namespace = /* @__PURE__ */ _interopNamespaceDefault(pty);
|
|
28
|
+
const USER_AGENTS_DIR = path.join(os.homedir(), ".claude", "agents");
|
|
29
|
+
const WATCH_DEBOUNCE_MS = 400;
|
|
30
|
+
class AgentRegistryService {
|
|
31
|
+
constructor(persistence2, getWin2) {
|
|
32
|
+
this.persistence = persistence2;
|
|
33
|
+
this.getWin = getWin2;
|
|
34
|
+
}
|
|
35
|
+
watchers = /* @__PURE__ */ new Map();
|
|
36
|
+
debounce = null;
|
|
37
|
+
/** Paths the renderer may read/reveal — everything the last snapshot listed. */
|
|
38
|
+
knownPaths = /* @__PURE__ */ new Set();
|
|
39
|
+
disposed = false;
|
|
40
|
+
dispose() {
|
|
41
|
+
this.disposed = true;
|
|
42
|
+
if (this.debounce) clearTimeout(this.debounce);
|
|
43
|
+
for (const w of this.watchers.values()) w.close();
|
|
44
|
+
this.watchers.clear();
|
|
45
|
+
}
|
|
46
|
+
registryPath() {
|
|
47
|
+
return this.persistence.state.settings.agentRegistryPath.trim();
|
|
48
|
+
}
|
|
49
|
+
/** Unique session repo folders (project-local agents may live in each). */
|
|
50
|
+
projectDirs() {
|
|
51
|
+
return [...new Set(this.persistence.state.sessions.map((s) => s.folder).filter(Boolean))];
|
|
52
|
+
}
|
|
53
|
+
/** Build a fresh snapshot and re-arm the watchers for the dirs it covers. */
|
|
54
|
+
snapshot() {
|
|
55
|
+
const registryPath = this.registryPath();
|
|
56
|
+
const registry = readRegistry(registryPath);
|
|
57
|
+
const agents = scanAgentsDir(USER_AGENTS_DIR, "user", null);
|
|
58
|
+
for (const dir of this.projectDirs()) {
|
|
59
|
+
agents.push(...scanAgentsDir(path.join(dir, ".claude", "agents"), "project", dir));
|
|
60
|
+
}
|
|
61
|
+
agents.sort((a, b) => a.name.localeCompare(b.name));
|
|
62
|
+
const byName = /* @__PURE__ */ new Map();
|
|
63
|
+
for (const e of registry.entries) byName.set(e.name.toLowerCase(), e);
|
|
64
|
+
const matched = /* @__PURE__ */ new Set();
|
|
65
|
+
for (const agent of agents) {
|
|
66
|
+
const entry = byName.get(agent.name.toLowerCase());
|
|
67
|
+
if (entry) {
|
|
68
|
+
agent.registry = entry;
|
|
69
|
+
matched.add(entry);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const missing = registry.entries.filter((e) => e.fileMissing && !matched.has(e));
|
|
73
|
+
const registryDir = path.dirname(registryPath);
|
|
74
|
+
const snapshot = {
|
|
75
|
+
agents,
|
|
76
|
+
missing,
|
|
77
|
+
registryPath,
|
|
78
|
+
registryError: registry.error,
|
|
79
|
+
registryVersion: registry.version,
|
|
80
|
+
registryUpdated: registry.updated,
|
|
81
|
+
factoryRunning: fs.existsSync(path.join(registryDir, ".factory.lock"))
|
|
82
|
+
};
|
|
83
|
+
this.knownPaths = /* @__PURE__ */ new Set();
|
|
84
|
+
for (const a of agents) this.knownPaths.add(a.filePath);
|
|
85
|
+
for (const e of registry.entries) if (e.filePath) this.knownPaths.add(e.filePath);
|
|
86
|
+
this.armWatchers(registryDir);
|
|
87
|
+
return snapshot;
|
|
88
|
+
}
|
|
89
|
+
/** Re-snapshot now and push it to the renderer (manual refresh / settings change). */
|
|
90
|
+
refresh() {
|
|
91
|
+
const snapshot = this.snapshot();
|
|
92
|
+
this.getWin()?.webContents.send("agents:changed", snapshot);
|
|
93
|
+
return snapshot;
|
|
94
|
+
}
|
|
95
|
+
/** Read an agent file the last snapshot listed (null when unknown/unreadable). */
|
|
96
|
+
readAgentFile(filePath) {
|
|
97
|
+
if (this.knownPaths.size === 0) this.snapshot();
|
|
98
|
+
if (!this.knownPaths.has(filePath)) return null;
|
|
99
|
+
try {
|
|
100
|
+
return fs.readFileSync(filePath, "utf8");
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Reveal an agent file from the last snapshot in the OS file manager. */
|
|
106
|
+
revealAgentFile(filePath) {
|
|
107
|
+
if (this.knownPaths.has(filePath) && fs.existsSync(filePath)) electron.shell.showItemInFolder(filePath);
|
|
108
|
+
}
|
|
109
|
+
// ---------- file watching ----------
|
|
110
|
+
/**
|
|
111
|
+
* Watch the user agents dir, every project agents dir and the registry dir
|
|
112
|
+
* (the latter covers registry.json writes AND .factory.lock create/delete).
|
|
113
|
+
* Idempotent: only the delta of dirs is (un)watched.
|
|
114
|
+
*/
|
|
115
|
+
armWatchers(registryDir) {
|
|
116
|
+
if (this.disposed) return;
|
|
117
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
118
|
+
if (fs.existsSync(USER_AGENTS_DIR)) wanted.add(USER_AGENTS_DIR);
|
|
119
|
+
if (fs.existsSync(registryDir)) wanted.add(registryDir);
|
|
120
|
+
for (const dir of this.projectDirs()) {
|
|
121
|
+
const agentsDir = path.join(dir, ".claude", "agents");
|
|
122
|
+
if (fs.existsSync(agentsDir)) wanted.add(agentsDir);
|
|
123
|
+
}
|
|
124
|
+
for (const [dir, watcher] of this.watchers) {
|
|
125
|
+
if (!wanted.has(dir)) {
|
|
126
|
+
watcher.close();
|
|
127
|
+
this.watchers.delete(dir);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const dir of wanted) {
|
|
131
|
+
if (this.watchers.has(dir)) continue;
|
|
132
|
+
try {
|
|
133
|
+
const watcher = fs.watch(dir, () => this.scheduleRefresh());
|
|
134
|
+
watcher.on("error", () => {
|
|
135
|
+
watcher.close();
|
|
136
|
+
this.watchers.delete(dir);
|
|
137
|
+
});
|
|
138
|
+
this.watchers.set(dir, watcher);
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
scheduleRefresh() {
|
|
144
|
+
if (this.disposed) return;
|
|
145
|
+
if (this.debounce) clearTimeout(this.debounce);
|
|
146
|
+
this.debounce = setTimeout(() => {
|
|
147
|
+
this.debounce = null;
|
|
148
|
+
this.refresh();
|
|
149
|
+
}, WATCH_DEBOUNCE_MS);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function parseAgentFrontmatter(md) {
|
|
153
|
+
const m = /^?---\r?\n([\s\S]*?)\r?\n---/.exec(md);
|
|
154
|
+
if (!m) return {};
|
|
155
|
+
const out = {};
|
|
156
|
+
const lines = m[1].split(/\r?\n/);
|
|
157
|
+
for (let i = 0; i < lines.length; i++) {
|
|
158
|
+
const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(lines[i]);
|
|
159
|
+
if (!kv) continue;
|
|
160
|
+
const key = kv[1];
|
|
161
|
+
let value = kv[2].trim();
|
|
162
|
+
if (value === "|" || value === ">" || value === "|-" || value === ">-") {
|
|
163
|
+
const block = [];
|
|
164
|
+
while (i + 1 < lines.length && /^\s+\S/.test(lines[i + 1])) {
|
|
165
|
+
block.push(lines[++i].trim());
|
|
166
|
+
}
|
|
167
|
+
value = block.join(" ").trim();
|
|
168
|
+
} else if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
169
|
+
value = value.slice(1, -1);
|
|
170
|
+
}
|
|
171
|
+
if (key === "name") out.name = value;
|
|
172
|
+
else if (key === "description") out.description = value;
|
|
173
|
+
else if (key === "model") out.model = value;
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
function scanAgentsDir(dir, scope, projectDir) {
|
|
178
|
+
if (!fs.existsSync(dir)) return [];
|
|
179
|
+
let entries;
|
|
180
|
+
try {
|
|
181
|
+
entries = fs.readdirSync(dir);
|
|
182
|
+
} catch {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
const out = [];
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
if (!entry.endsWith(".md")) continue;
|
|
188
|
+
const filePath = path.join(dir, entry);
|
|
189
|
+
const base = entry.replace(/\.md$/, "");
|
|
190
|
+
let fm = {};
|
|
191
|
+
try {
|
|
192
|
+
fm = parseAgentFrontmatter(fs.readFileSync(filePath, "utf8"));
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
out.push({
|
|
196
|
+
name: fm.name?.trim() || base,
|
|
197
|
+
description: fm.description ?? "",
|
|
198
|
+
model: fm.model?.trim() || null,
|
|
199
|
+
scope,
|
|
200
|
+
projectDir,
|
|
201
|
+
filePath,
|
|
202
|
+
registry: null
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
function readRegistry(registryPath) {
|
|
208
|
+
if (!registryPath) return { entries: [], error: "No registry path configured.", version: null, updated: null };
|
|
209
|
+
if (!fs.existsSync(registryPath)) {
|
|
210
|
+
return { entries: [], error: `Registry not found: ${registryPath}`, version: null, updated: null };
|
|
211
|
+
}
|
|
212
|
+
let parsed;
|
|
213
|
+
try {
|
|
214
|
+
parsed = JSON.parse(fs.readFileSync(registryPath, "utf8"));
|
|
215
|
+
} catch (err) {
|
|
216
|
+
return {
|
|
217
|
+
entries: [],
|
|
218
|
+
error: `Registry unreadable: ${err.message || String(err)}`,
|
|
219
|
+
version: null,
|
|
220
|
+
updated: null
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const root2 = parsed ?? {};
|
|
224
|
+
const meta = root2._meta ?? {};
|
|
225
|
+
const rawAgents = Array.isArray(root2.agents) ? root2.agents : [];
|
|
226
|
+
const entries = [];
|
|
227
|
+
for (const raw of rawAgents) {
|
|
228
|
+
if (!raw || typeof raw !== "object") continue;
|
|
229
|
+
const entry = parseEntry(raw, registryPath);
|
|
230
|
+
if (entry) entries.push(entry);
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
entries,
|
|
234
|
+
error: null,
|
|
235
|
+
version: typeof meta.version === "string" ? meta.version : null,
|
|
236
|
+
updated: typeof meta.last_updated === "string" ? meta.last_updated : null
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function parseEntry(r, registryPath) {
|
|
240
|
+
const name = String(r.name ?? "").trim();
|
|
241
|
+
if (!name) return null;
|
|
242
|
+
const filePath = resolveEntryPath(r.file_path, registryPath);
|
|
243
|
+
return {
|
|
244
|
+
name,
|
|
245
|
+
filePath,
|
|
246
|
+
type: optString(r.type),
|
|
247
|
+
status: optString(r.status),
|
|
248
|
+
archetype: optString(r.archetype),
|
|
249
|
+
model: optString(r.model),
|
|
250
|
+
scope: optString(r.scope),
|
|
251
|
+
description: String(r.description ?? "").trim(),
|
|
252
|
+
topics: toStringArray$1(r.topics),
|
|
253
|
+
keywords: toStringArray$1(r.keywords),
|
|
254
|
+
relatedAgents: toStringArray$1(r.related_agents),
|
|
255
|
+
confluencePages: [
|
|
256
|
+
.../* @__PURE__ */ new Set([...toStringArray$1(r.confluence_source), ...toStringArray$1(r.confluence_pages)])
|
|
257
|
+
],
|
|
258
|
+
sourceVerified: r.source_verified === true,
|
|
259
|
+
githubRepos: toGithubRepos(r.github_repos),
|
|
260
|
+
githubVerified: r.github_verified === true,
|
|
261
|
+
knowledgeNotes: toStringArray$1(r.knowledge_note),
|
|
262
|
+
factoryMade: typeof r.factory_made === "boolean" ? r.factory_made : null,
|
|
263
|
+
created: optString(r.created),
|
|
264
|
+
lastUpdated: optString(r.last_updated),
|
|
265
|
+
fileMissing: filePath !== null && !fs.existsSync(filePath)
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function resolveEntryPath(raw, registryPath) {
|
|
269
|
+
const fp = String(raw ?? "").trim();
|
|
270
|
+
if (!fp) return null;
|
|
271
|
+
if (path.isAbsolute(fp)) return fp;
|
|
272
|
+
const registryDir = path.dirname(registryPath);
|
|
273
|
+
const fromRepoRoot = path.resolve(path.dirname(registryDir), fp);
|
|
274
|
+
if (fs.existsSync(fromRepoRoot)) return fromRepoRoot;
|
|
275
|
+
const fromRegistryDir = path.resolve(registryDir, fp);
|
|
276
|
+
return fs.existsSync(fromRegistryDir) ? fromRegistryDir : fromRepoRoot;
|
|
277
|
+
}
|
|
278
|
+
function optString(v) {
|
|
279
|
+
if (typeof v !== "string") return null;
|
|
280
|
+
const s = v.trim();
|
|
281
|
+
return s || null;
|
|
282
|
+
}
|
|
283
|
+
function toStringArray$1(v) {
|
|
284
|
+
if (typeof v === "string") return v.trim() ? [v.trim()] : [];
|
|
285
|
+
if (!Array.isArray(v)) return [];
|
|
286
|
+
return [...new Set(v.map((x) => String(x).trim()).filter(Boolean))];
|
|
287
|
+
}
|
|
288
|
+
function toGithubRepos(v) {
|
|
289
|
+
if (!Array.isArray(v)) return [];
|
|
290
|
+
const out = [];
|
|
291
|
+
for (const raw of v) {
|
|
292
|
+
if (!raw || typeof raw !== "object") continue;
|
|
293
|
+
const r = raw;
|
|
294
|
+
const repo = String(r.repo ?? "").trim();
|
|
295
|
+
if (!repo) continue;
|
|
296
|
+
out.push({ repo, ref: optString(r.ref), paths: toStringArray$1(r.paths) });
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
28
300
|
function git(cwd, args, env) {
|
|
29
301
|
return new Promise((resolve2, reject) => {
|
|
30
302
|
child_process.execFile(
|
|
@@ -1035,7 +1307,7 @@ function extractJson(text) {
|
|
|
1035
1307
|
return null;
|
|
1036
1308
|
}
|
|
1037
1309
|
}
|
|
1038
|
-
const TICK_MS$
|
|
1310
|
+
const TICK_MS$2 = 3e4;
|
|
1039
1311
|
const AGENT_TIMEOUT_MS = 5 * 6e4;
|
|
1040
1312
|
const MAX_RUNS_PER_SESSION$1 = 30;
|
|
1041
1313
|
const IDEA_COUNT = 4;
|
|
@@ -1064,7 +1336,7 @@ class AutoExpandService {
|
|
|
1064
1336
|
runs = /* @__PURE__ */ new Map();
|
|
1065
1337
|
start() {
|
|
1066
1338
|
if (this.timer) return;
|
|
1067
|
-
this.timer = setInterval(() => this.tick(), TICK_MS$
|
|
1339
|
+
this.timer = setInterval(() => this.tick(), TICK_MS$2);
|
|
1068
1340
|
this.tick();
|
|
1069
1341
|
}
|
|
1070
1342
|
dispose() {
|
|
@@ -1431,6 +1703,14 @@ class ConductorService {
|
|
|
1431
1703
|
/** The in-flight planner child, so dispose()/a new turn can cancel it. */
|
|
1432
1704
|
inFlight = null;
|
|
1433
1705
|
busy = false;
|
|
1706
|
+
/** Observer fired after each completed (non-error) turn — wired to the Factory's
|
|
1707
|
+
* self-growth detector in index.ts. A post-construction setter avoids a
|
|
1708
|
+
* constructor cycle (conductor is built before the factory). */
|
|
1709
|
+
turnCompleteCb = null;
|
|
1710
|
+
/** Register an observer notified after each successful turn (e.g. the Factory). */
|
|
1711
|
+
onTurnComplete(cb) {
|
|
1712
|
+
this.turnCompleteCb = cb;
|
|
1713
|
+
}
|
|
1434
1714
|
list() {
|
|
1435
1715
|
return this.messages;
|
|
1436
1716
|
}
|
|
@@ -1508,6 +1788,12 @@ class ConductorService {
|
|
|
1508
1788
|
this.inFlight = null;
|
|
1509
1789
|
this.busy = false;
|
|
1510
1790
|
this.persistAndBroadcast();
|
|
1791
|
+
if (!assistantMsg.error) {
|
|
1792
|
+
try {
|
|
1793
|
+
this.turnCompleteCb?.(this.messages);
|
|
1794
|
+
} catch {
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1511
1797
|
}
|
|
1512
1798
|
}
|
|
1513
1799
|
/**
|
|
@@ -2101,12 +2387,21 @@ function scanAgents() {
|
|
|
2101
2387
|
}
|
|
2102
2388
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
2103
2389
|
}
|
|
2104
|
-
const EMPTY = { artifacts: [], topics: [], lessons: [] };
|
|
2390
|
+
const EMPTY = { artifacts: [], topics: [], lessons: [], suggestions: [] };
|
|
2391
|
+
const EMPTY_GROWTH = {
|
|
2392
|
+
lastScannedAt: {},
|
|
2393
|
+
lastAutoProposeAt: 0,
|
|
2394
|
+
lastDetectAt: 0,
|
|
2395
|
+
turnsSinceDetect: 0,
|
|
2396
|
+
judgeDay: "",
|
|
2397
|
+
judgeCallsToday: 0
|
|
2398
|
+
};
|
|
2105
2399
|
class FactoryStore {
|
|
2106
2400
|
file = path.join(electron.app.getPath("userData"), "factory.json");
|
|
2107
2401
|
timer = null;
|
|
2108
2402
|
state = { ...EMPTY };
|
|
2109
2403
|
runs = [];
|
|
2404
|
+
growth = { ...EMPTY_GROWTH };
|
|
2110
2405
|
/** Load the saved registry (best-effort; an empty registry on any error). */
|
|
2111
2406
|
load() {
|
|
2112
2407
|
try {
|
|
@@ -2114,12 +2409,19 @@ class FactoryStore {
|
|
|
2114
2409
|
this.state = {
|
|
2115
2410
|
artifacts: Array.isArray(raw?.artifacts) ? raw.artifacts : [],
|
|
2116
2411
|
topics: Array.isArray(raw?.topics) ? raw.topics : [],
|
|
2117
|
-
lessons: Array.isArray(raw?.lessons) ? raw.lessons : []
|
|
2412
|
+
lessons: Array.isArray(raw?.lessons) ? raw.lessons : [],
|
|
2413
|
+
// Back-compat: older factory.json files have no `suggestions` key.
|
|
2414
|
+
suggestions: Array.isArray(raw?.suggestions) ? raw.suggestions : []
|
|
2118
2415
|
};
|
|
2119
2416
|
this.runs = Array.isArray(raw?.runs) ? raw.runs : [];
|
|
2417
|
+
this.growth = raw?.growth && typeof raw.growth === "object" ? { ...EMPTY_GROWTH, ...raw.growth } : { ...EMPTY_GROWTH };
|
|
2418
|
+
if (!this.growth.lastScannedAt || typeof this.growth.lastScannedAt !== "object" || Array.isArray(this.growth.lastScannedAt)) {
|
|
2419
|
+
this.growth.lastScannedAt = {};
|
|
2420
|
+
}
|
|
2120
2421
|
} catch {
|
|
2121
2422
|
this.state = { ...EMPTY };
|
|
2122
2423
|
this.runs = [];
|
|
2424
|
+
this.growth = { ...EMPTY_GROWTH };
|
|
2123
2425
|
}
|
|
2124
2426
|
return this.state;
|
|
2125
2427
|
}
|
|
@@ -2130,6 +2432,10 @@ class FactoryStore {
|
|
|
2130
2432
|
loadRuns() {
|
|
2131
2433
|
return this.runs;
|
|
2132
2434
|
}
|
|
2435
|
+
/** Self-growth bookkeeping (call after load()). */
|
|
2436
|
+
loadGrowth() {
|
|
2437
|
+
return this.growth;
|
|
2438
|
+
}
|
|
2133
2439
|
/** Replace the registry and schedule a save. */
|
|
2134
2440
|
set(state) {
|
|
2135
2441
|
this.state = state;
|
|
@@ -2140,6 +2446,11 @@ class FactoryStore {
|
|
|
2140
2446
|
this.runs = runs;
|
|
2141
2447
|
this.scheduleSave();
|
|
2142
2448
|
}
|
|
2449
|
+
/** Replace the self-growth bookkeeping and schedule a save. */
|
|
2450
|
+
setGrowth(growth) {
|
|
2451
|
+
this.growth = growth;
|
|
2452
|
+
this.scheduleSave();
|
|
2453
|
+
}
|
|
2143
2454
|
scheduleSave() {
|
|
2144
2455
|
if (this.timer) clearTimeout(this.timer);
|
|
2145
2456
|
this.timer = setTimeout(() => this.saveNow(), 500);
|
|
@@ -2152,7 +2463,11 @@ class FactoryStore {
|
|
|
2152
2463
|
try {
|
|
2153
2464
|
fs.mkdirSync(path.dirname(this.file), { recursive: true });
|
|
2154
2465
|
const tmp = this.file + ".tmp";
|
|
2155
|
-
fs.writeFileSync(
|
|
2466
|
+
fs.writeFileSync(
|
|
2467
|
+
tmp,
|
|
2468
|
+
JSON.stringify({ ...this.state, runs: this.runs, growth: this.growth }, null, 2),
|
|
2469
|
+
"utf8"
|
|
2470
|
+
);
|
|
2156
2471
|
fs.renameSync(tmp, this.file);
|
|
2157
2472
|
} catch (err) {
|
|
2158
2473
|
console.error("Failed to persist factory registry:", err);
|
|
@@ -2166,6 +2481,22 @@ const MAX_CANDIDATES = 8;
|
|
|
2166
2481
|
const MAX_RUNS = 25;
|
|
2167
2482
|
const MAX_TOPICS = 60;
|
|
2168
2483
|
const MAX_LESSONS = 40;
|
|
2484
|
+
const TICK_MS$1 = 6e4;
|
|
2485
|
+
const AUTO_PROPOSE_INTERVAL_MS = 6 * 60 * 6e4;
|
|
2486
|
+
const AUTO_PROPOSE_BOOT_GRACE_MS = 10 * 6e4;
|
|
2487
|
+
const DETECT_DEBOUNCE_MS = 4e3;
|
|
2488
|
+
const DETECT_TIMEOUT_MS = 45e3;
|
|
2489
|
+
const MIN_TURNS_SINCE_DETECT = 3;
|
|
2490
|
+
const DETECT_MIN_INTERVAL_MS = 20 * 6e4;
|
|
2491
|
+
const DETECT_CONTEXT_MAX = 6e3;
|
|
2492
|
+
const MIN_CONFIDENCE = 0.6;
|
|
2493
|
+
const MAX_SUGGESTIONS_PER_DETECT = 2;
|
|
2494
|
+
const JUDGE_DAILY_CAP = 12;
|
|
2495
|
+
const MAX_SUGGESTIONS = 60;
|
|
2496
|
+
function localDay$1() {
|
|
2497
|
+
const d = /* @__PURE__ */ new Date();
|
|
2498
|
+
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
|
|
2499
|
+
}
|
|
2169
2500
|
const KNOWN_LABELS = {
|
|
2170
2501
|
claude_ai_Atlassian: "Atlassian (Confluence / Jira)",
|
|
2171
2502
|
claude_ai_Figma: "Figma",
|
|
@@ -2177,16 +2508,40 @@ class FactoryService {
|
|
|
2177
2508
|
this.getWin = getWin2;
|
|
2178
2509
|
this.state = this.store.load();
|
|
2179
2510
|
this.runs = restoreRuns(this.store.loadRuns());
|
|
2511
|
+
for (const s of this.state.suggestions) {
|
|
2512
|
+
if (s.status === "creating") {
|
|
2513
|
+
s.status = "open";
|
|
2514
|
+
s.result = void 0;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2180
2517
|
}
|
|
2181
2518
|
store = new FactoryStore();
|
|
2182
2519
|
state;
|
|
2183
2520
|
runs = [];
|
|
2184
2521
|
sources = null;
|
|
2185
|
-
/** The in-flight agent child, so dispose()/cancel() can kill it. */
|
|
2522
|
+
/** The in-flight cancellable agent child (scan/author/judge), so dispose()/cancel() can kill it. */
|
|
2186
2523
|
inFlight = null;
|
|
2524
|
+
/** Source-discovery child — a SEPARATE slot from `inFlight` so discovery (which
|
|
2525
|
+
* can run while `busy` is false) never nulls a live scan/author/judge child. */
|
|
2526
|
+
discoverChild = null;
|
|
2527
|
+
/** In-flight discovery promise; concurrent callers join it instead of spawning
|
|
2528
|
+
* a second discovery agent (the single-headless-child invariant). */
|
|
2529
|
+
discovering = null;
|
|
2187
2530
|
busy = false;
|
|
2531
|
+
/** Resolves when the current heavy op (scan/author/judge) releases the lock —
|
|
2532
|
+
* the public listSources() awaits it before starting a discovery agent so the
|
|
2533
|
+
* two never run concurrently. Null while idle. */
|
|
2534
|
+
busyPromise = null;
|
|
2535
|
+
busyResolve = null;
|
|
2188
2536
|
/** Set by cancel(); the in-flight scan/author reports 'cancelled' instead of 'error'. */
|
|
2189
2537
|
cancelRequested = false;
|
|
2538
|
+
/** Self-growth background timer; null until start(). */
|
|
2539
|
+
timer = null;
|
|
2540
|
+
/** Debounce timer for the conversation detector. */
|
|
2541
|
+
detectTimer = null;
|
|
2542
|
+
/** Claimed synchronously while an auto-propose pass is dispatched (incl. its
|
|
2543
|
+
* pre-scan discovery await), so the 60s timer can't double-dispatch it. */
|
|
2544
|
+
autoProposing = false;
|
|
2190
2545
|
getState() {
|
|
2191
2546
|
return this.state;
|
|
2192
2547
|
}
|
|
@@ -2194,13 +2549,451 @@ class FactoryService {
|
|
|
2194
2549
|
return this.runs;
|
|
2195
2550
|
}
|
|
2196
2551
|
dispose() {
|
|
2552
|
+
if (this.timer) clearInterval(this.timer);
|
|
2553
|
+
this.timer = null;
|
|
2554
|
+
if (this.detectTimer) clearTimeout(this.detectTimer);
|
|
2555
|
+
this.detectTimer = null;
|
|
2197
2556
|
try {
|
|
2198
2557
|
this.inFlight?.kill();
|
|
2199
2558
|
} catch {
|
|
2200
2559
|
}
|
|
2560
|
+
try {
|
|
2561
|
+
this.discoverChild?.kill();
|
|
2562
|
+
} catch {
|
|
2563
|
+
}
|
|
2201
2564
|
this.inFlight = null;
|
|
2565
|
+
this.discoverChild = null;
|
|
2202
2566
|
this.store.saveNow();
|
|
2203
2567
|
}
|
|
2568
|
+
// ---------- self-growth: background timer ----------
|
|
2569
|
+
/** Begin the self-growth loop: an infrequent token-spending auto-propose pass. */
|
|
2570
|
+
start() {
|
|
2571
|
+
if (this.timer) return;
|
|
2572
|
+
const g = this.store.loadGrowth();
|
|
2573
|
+
const earliest = Date.now() - AUTO_PROPOSE_INTERVAL_MS + AUTO_PROPOSE_BOOT_GRACE_MS;
|
|
2574
|
+
if (g.lastAutoProposeAt < earliest) {
|
|
2575
|
+
this.store.setGrowth({ ...g, lastAutoProposeAt: earliest });
|
|
2576
|
+
}
|
|
2577
|
+
this.timer = setInterval(() => this.tick(), TICK_MS$1);
|
|
2578
|
+
}
|
|
2579
|
+
tick() {
|
|
2580
|
+
if (this.busy || this.autoProposing) return;
|
|
2581
|
+
const g = this.store.loadGrowth();
|
|
2582
|
+
if (Date.now() - g.lastAutoProposeAt >= AUTO_PROPOSE_INTERVAL_MS) {
|
|
2583
|
+
void this.autoProposePass().catch(() => {
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Infrequent token-spending pass: scan the connected MCP source that's gone
|
|
2589
|
+
* longest without a scan (round-robin), then convert that scan's proposed
|
|
2590
|
+
* candidates into suggestions (never auto-installed). A dedicated
|
|
2591
|
+
* `autoProposing` guard (claimed synchronously) prevents a second tick from
|
|
2592
|
+
* dispatching while this one is parked in discovery; the budget/rotation slot
|
|
2593
|
+
* is only consumed when a scan actually runs, and we harvest exactly the run
|
|
2594
|
+
* this pass produced (never a stale older one).
|
|
2595
|
+
*/
|
|
2596
|
+
async autoProposePass() {
|
|
2597
|
+
if (this.busy || this.autoProposing) return;
|
|
2598
|
+
this.autoProposing = true;
|
|
2599
|
+
try {
|
|
2600
|
+
const sources = await this.listSources().catch(() => []);
|
|
2601
|
+
if (sources.length === 0) return;
|
|
2602
|
+
const g = this.store.loadGrowth();
|
|
2603
|
+
const next = [...sources].sort(
|
|
2604
|
+
(a, b) => (g.lastScannedAt[a.server] ?? 0) - (g.lastScannedAt[b.server] ?? 0)
|
|
2605
|
+
)[0];
|
|
2606
|
+
const runId = await this.scan(
|
|
2607
|
+
next.server,
|
|
2608
|
+
"Automated background scan: surface only a few high-value, clearly groundable candidates."
|
|
2609
|
+
);
|
|
2610
|
+
if (!runId) return;
|
|
2611
|
+
const g2 = this.store.loadGrowth();
|
|
2612
|
+
this.store.setGrowth({
|
|
2613
|
+
...g2,
|
|
2614
|
+
lastScannedAt: { ...g2.lastScannedAt, [next.server]: Date.now() },
|
|
2615
|
+
lastAutoProposeAt: Date.now()
|
|
2616
|
+
});
|
|
2617
|
+
const run = this.runs.find((r) => r.id === runId);
|
|
2618
|
+
if (run && run.status === "done") this.absorbScanSuggestions(run);
|
|
2619
|
+
} finally {
|
|
2620
|
+
this.autoProposing = false;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
absorbScanSuggestions(run) {
|
|
2624
|
+
let newest = null;
|
|
2625
|
+
let added = 0;
|
|
2626
|
+
for (const c of run.candidates) {
|
|
2627
|
+
if (c.status !== "proposed") continue;
|
|
2628
|
+
if (this.suggestionDuplicate(c.kind, c.name, c.description)) continue;
|
|
2629
|
+
newest = this.enqueueSuggestion({
|
|
2630
|
+
suggestedKind: c.kind,
|
|
2631
|
+
name: c.name,
|
|
2632
|
+
title: c.description,
|
|
2633
|
+
description: c.description,
|
|
2634
|
+
rationale: c.rationale,
|
|
2635
|
+
origin: "scan",
|
|
2636
|
+
sourceRef: run.source,
|
|
2637
|
+
sourceLabel: run.sourceLabel,
|
|
2638
|
+
source: run.source,
|
|
2639
|
+
context: run.summary,
|
|
2640
|
+
topics: c.topics,
|
|
2641
|
+
keywords: c.keywords,
|
|
2642
|
+
existing: c.existing,
|
|
2643
|
+
confidence: 0.8
|
|
2644
|
+
});
|
|
2645
|
+
added++;
|
|
2646
|
+
}
|
|
2647
|
+
if (added > 0) {
|
|
2648
|
+
this.persist();
|
|
2649
|
+
if (newest) this.getWin()?.webContents.send("factory:suggestion", newest);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
// ---------- self-growth: conversation detector ----------
|
|
2653
|
+
/**
|
|
2654
|
+
* Called (fire-and-forget) after each completed Conductor turn. Debounced and
|
|
2655
|
+
* heavily rate-limited; schedules a short headless judge that may queue
|
|
2656
|
+
* skill/agent suggestions. Never blocks or throws into the caller.
|
|
2657
|
+
*/
|
|
2658
|
+
considerConversation(messages) {
|
|
2659
|
+
const g = this.store.loadGrowth();
|
|
2660
|
+
this.store.setGrowth({ ...g, turnsSinceDetect: (g.turnsSinceDetect ?? 0) + 1 });
|
|
2661
|
+
if (this.detectTimer) clearTimeout(this.detectTimer);
|
|
2662
|
+
this.detectTimer = setTimeout(() => {
|
|
2663
|
+
this.detectTimer = null;
|
|
2664
|
+
void this.maybeRunJudge(messages).catch(() => {
|
|
2665
|
+
});
|
|
2666
|
+
}, DETECT_DEBOUNCE_MS);
|
|
2667
|
+
}
|
|
2668
|
+
async maybeRunJudge(messages) {
|
|
2669
|
+
if (this.busy || this.discovering) return;
|
|
2670
|
+
const g = this.store.loadGrowth();
|
|
2671
|
+
const now = Date.now();
|
|
2672
|
+
if (g.turnsSinceDetect < MIN_TURNS_SINCE_DETECT && now - g.lastDetectAt < DETECT_MIN_INTERVAL_MS) {
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
const turnsAtStart = g.turnsSinceDetect;
|
|
2676
|
+
const day = localDay$1();
|
|
2677
|
+
const callsToday = g.judgeDay === day ? g.judgeCallsToday : 0;
|
|
2678
|
+
if (callsToday >= JUDGE_DAILY_CAP) return;
|
|
2679
|
+
const recent = messages.filter((m) => !m.pending && m.text?.trim()).slice(-6);
|
|
2680
|
+
if (recent.length === 0) return;
|
|
2681
|
+
this.setBusy(true);
|
|
2682
|
+
this.cancelRequested = false;
|
|
2683
|
+
try {
|
|
2684
|
+
const convo = recent.map((m) => `${m.role === "user" ? "USER" : "ASSISTANT"}: ${m.text.trim()}`).join("\n\n");
|
|
2685
|
+
const out = await runHeadlessClaude({
|
|
2686
|
+
cwd: process.cwd(),
|
|
2687
|
+
prompt: this.judgePrompt(convo),
|
|
2688
|
+
allowedTools: ["Read"],
|
|
2689
|
+
timeoutMs: DETECT_TIMEOUT_MS,
|
|
2690
|
+
onSpawn: (child) => this.inFlight = child
|
|
2691
|
+
});
|
|
2692
|
+
this.absorbChatSuggestions(
|
|
2693
|
+
this.parseJudge(out),
|
|
2694
|
+
recent[recent.length - 1].id,
|
|
2695
|
+
convo.slice(0, DETECT_CONTEXT_MAX)
|
|
2696
|
+
);
|
|
2697
|
+
} catch {
|
|
2698
|
+
} finally {
|
|
2699
|
+
this.inFlight = null;
|
|
2700
|
+
this.setBusy(false);
|
|
2701
|
+
const g2 = this.store.loadGrowth();
|
|
2702
|
+
const day2 = localDay$1();
|
|
2703
|
+
this.store.setGrowth({
|
|
2704
|
+
...g2,
|
|
2705
|
+
lastDetectAt: Date.now(),
|
|
2706
|
+
turnsSinceDetect: Math.max(0, (g2.turnsSinceDetect ?? 0) - turnsAtStart),
|
|
2707
|
+
judgeDay: day2,
|
|
2708
|
+
judgeCallsToday: (g2.judgeDay === day2 ? g2.judgeCallsToday : 0) + 1
|
|
2709
|
+
});
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
judgePrompt(convo) {
|
|
2713
|
+
const existing = this.existingNamesSnapshot();
|
|
2714
|
+
return [
|
|
2715
|
+
"You are the talent scout for an Agent & Skill Factory inside Maestro. You read a recent",
|
|
2716
|
+
"slice of the user's Conductor chat and decide whether it reveals a REUSABLE workflow or",
|
|
2717
|
+
"body of knowledge worth capturing as a Claude Code SKILL (a repeatable procedure the user",
|
|
2718
|
+
"invokes) or SUB-AGENT (a specialist for one bounded domain).",
|
|
2719
|
+
"",
|
|
2720
|
+
"Be conservative: most chats are one-offs and should yield NOTHING. Only suggest when the",
|
|
2721
|
+
"same kind of task would plausibly recur. Never suggest something that overlaps an artifact",
|
|
2722
|
+
"that already exists below — neither a near-duplicate name nor the same purpose.",
|
|
2723
|
+
"",
|
|
2724
|
+
`Artifacts that already exist (do NOT duplicate):
|
|
2725
|
+
${JSON.stringify(existing, null, 2)}`,
|
|
2726
|
+
"",
|
|
2727
|
+
"Recent Conductor conversation (oldest first):",
|
|
2728
|
+
convo,
|
|
2729
|
+
"",
|
|
2730
|
+
"Respond with ONLY one JSON object — no markdown fences, no prose:",
|
|
2731
|
+
'{"suggest": <true|false>,',
|
|
2732
|
+
' "items": [{"kind":"skill|agent",',
|
|
2733
|
+
' "name":"<kebab-case-slug>",',
|
|
2734
|
+
' "title":"<short human title>",',
|
|
2735
|
+
' "description":"<one line: when to use it>",',
|
|
2736
|
+
' "rationale":"<why this conversation shows it would recur>",',
|
|
2737
|
+
' "confidence": <0..1>}]}',
|
|
2738
|
+
'Return {"suggest": false, "items": []} when nothing is worth capturing.'
|
|
2739
|
+
].join("\n");
|
|
2740
|
+
}
|
|
2741
|
+
/** Existing skills/agents (registered + on-disk), for judge prompts and dedupe. */
|
|
2742
|
+
existingNamesSnapshot() {
|
|
2743
|
+
return [
|
|
2744
|
+
...this.state.artifacts.map((a) => ({ kind: a.kind, name: a.name, description: a.description })),
|
|
2745
|
+
...scanSkills().map((s) => ({ kind: "skill", name: s.name, description: s.description ?? "" })),
|
|
2746
|
+
...scanAgents().map((a) => ({ kind: "agent", name: a.name, description: a.description ?? "" }))
|
|
2747
|
+
];
|
|
2748
|
+
}
|
|
2749
|
+
parseJudge(out) {
|
|
2750
|
+
const parsed = extractJson(out);
|
|
2751
|
+
if (!parsed || parsed.suggest === false) return [];
|
|
2752
|
+
const raw = Array.isArray(parsed.items) ? parsed.items : [];
|
|
2753
|
+
const items = [];
|
|
2754
|
+
for (const r of raw) {
|
|
2755
|
+
if (!r || typeof r !== "object") continue;
|
|
2756
|
+
const o = r;
|
|
2757
|
+
const kind = o.kind === "agent" ? "agent" : "skill";
|
|
2758
|
+
const name = slugify(String(o.name ?? ""));
|
|
2759
|
+
const description = String(o.description ?? "").trim();
|
|
2760
|
+
if (!name || !description) continue;
|
|
2761
|
+
let confidence = Number(o.confidence);
|
|
2762
|
+
if (!Number.isFinite(confidence)) confidence = 0;
|
|
2763
|
+
items.push({
|
|
2764
|
+
kind,
|
|
2765
|
+
name,
|
|
2766
|
+
title: String(o.title ?? description).trim() || description,
|
|
2767
|
+
description,
|
|
2768
|
+
rationale: String(o.rationale ?? "").trim(),
|
|
2769
|
+
confidence: Math.max(0, Math.min(1, confidence))
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
return items;
|
|
2773
|
+
}
|
|
2774
|
+
absorbChatSuggestions(items, sourceMsgId, context) {
|
|
2775
|
+
let newest = null;
|
|
2776
|
+
let added = 0;
|
|
2777
|
+
for (const it of items) {
|
|
2778
|
+
if (added >= MAX_SUGGESTIONS_PER_DETECT) break;
|
|
2779
|
+
if (it.confidence < MIN_CONFIDENCE) continue;
|
|
2780
|
+
if (this.suggestionDuplicate(it.kind, it.name, it.title)) continue;
|
|
2781
|
+
newest = this.enqueueSuggestion({
|
|
2782
|
+
suggestedKind: it.kind,
|
|
2783
|
+
name: it.name,
|
|
2784
|
+
title: it.title,
|
|
2785
|
+
description: it.description,
|
|
2786
|
+
rationale: it.rationale,
|
|
2787
|
+
origin: "chat",
|
|
2788
|
+
sourceRef: sourceMsgId,
|
|
2789
|
+
sourceLabel: "Maestro chat",
|
|
2790
|
+
source: null,
|
|
2791
|
+
context,
|
|
2792
|
+
topics: [],
|
|
2793
|
+
keywords: [],
|
|
2794
|
+
existing: null,
|
|
2795
|
+
confidence: it.confidence
|
|
2796
|
+
});
|
|
2797
|
+
added++;
|
|
2798
|
+
}
|
|
2799
|
+
if (added > 0) {
|
|
2800
|
+
this.persist();
|
|
2801
|
+
if (newest) this.getWin()?.webContents.send("factory:suggestion", newest);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
// ---------- self-growth: suggestion queue ----------
|
|
2805
|
+
enqueueSuggestion(input) {
|
|
2806
|
+
const now = Date.now();
|
|
2807
|
+
const s = { id: crypto.randomUUID(), status: "open", createdAt: now, updatedAt: now, ...input };
|
|
2808
|
+
this.state.suggestions.push(s);
|
|
2809
|
+
this.capSuggestions();
|
|
2810
|
+
return s;
|
|
2811
|
+
}
|
|
2812
|
+
/**
|
|
2813
|
+
* Keep the queue bounded by pruning ONLY the oldest terminal (created/dismissed)
|
|
2814
|
+
* entries. Actionable suggestions (open/creating/error) are never dropped — if
|
|
2815
|
+
* those alone exceed the cap we let the queue run over rather than silently
|
|
2816
|
+
* losing work the user still has to act on.
|
|
2817
|
+
*/
|
|
2818
|
+
capSuggestions() {
|
|
2819
|
+
if (this.state.suggestions.length <= MAX_SUGGESTIONS) return;
|
|
2820
|
+
const terminalOldestFirst = this.state.suggestions.filter((s) => s.status === "created" || s.status === "dismissed").sort((a, b) => a.updatedAt - b.updatedAt);
|
|
2821
|
+
const drop = /* @__PURE__ */ new Set();
|
|
2822
|
+
for (const s of terminalOldestFirst) {
|
|
2823
|
+
if (this.state.suggestions.length - drop.size <= MAX_SUGGESTIONS) break;
|
|
2824
|
+
drop.add(s.id);
|
|
2825
|
+
}
|
|
2826
|
+
if (drop.size > 0) {
|
|
2827
|
+
this.state.suggestions = this.state.suggestions.filter((x) => !drop.has(x.id));
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* Deterministic, token-free dedupe: is this idea already installed/registered,
|
|
2832
|
+
* or already in the open queue, or semantically the same as one of those?
|
|
2833
|
+
*/
|
|
2834
|
+
suggestionDuplicate(kind, slug, title) {
|
|
2835
|
+
const key = `${kind}:${slug}`;
|
|
2836
|
+
const installed = /* @__PURE__ */ new Set([
|
|
2837
|
+
...this.state.artifacts.map((a) => `${a.kind}:${a.name}`),
|
|
2838
|
+
...listInstalled().map((i) => `${i.kind}:${i.name}`)
|
|
2839
|
+
]);
|
|
2840
|
+
if (installed.has(key)) return "artifact";
|
|
2841
|
+
const words = (t2) => new Set(t2.toLowerCase().replace(/[^a-z0-9 ]+/g, " ").split(/\s+/).filter(Boolean));
|
|
2842
|
+
const jaccard = (a, b) => {
|
|
2843
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
2844
|
+
let inter = 0;
|
|
2845
|
+
for (const w of a) if (b.has(w)) inter++;
|
|
2846
|
+
return inter / (a.size + b.size - inter);
|
|
2847
|
+
};
|
|
2848
|
+
const t = words(title);
|
|
2849
|
+
for (const s of this.state.suggestions) {
|
|
2850
|
+
if (s.status !== "open" && s.status !== "creating") continue;
|
|
2851
|
+
if (s.suggestedKind === kind && s.name === slug) return "suggestion";
|
|
2852
|
+
if (jaccard(t, words(s.title)) >= 0.6) return "suggestion";
|
|
2853
|
+
}
|
|
2854
|
+
for (const a of this.state.artifacts) {
|
|
2855
|
+
if (jaccard(t, words(`${a.name} ${a.description}`)) >= 0.6) return "artifact";
|
|
2856
|
+
}
|
|
2857
|
+
return null;
|
|
2858
|
+
}
|
|
2859
|
+
/** Author + write + register the artifact for a suggestion (the only way one is built). */
|
|
2860
|
+
async createFromSuggestion(id, kind) {
|
|
2861
|
+
const s = this.state.suggestions.find((x) => x.id === id);
|
|
2862
|
+
if (!s || s.status !== "open" && s.status !== "error") return;
|
|
2863
|
+
if (this.busy) return;
|
|
2864
|
+
const useKind = kind ?? s.suggestedKind;
|
|
2865
|
+
this.setBusy(true);
|
|
2866
|
+
this.cancelRequested = false;
|
|
2867
|
+
s.status = "creating";
|
|
2868
|
+
s.suggestedKind = useKind;
|
|
2869
|
+
s.result = void 0;
|
|
2870
|
+
s.updatedAt = Date.now();
|
|
2871
|
+
this.persist();
|
|
2872
|
+
try {
|
|
2873
|
+
await this.discovering?.catch(() => {
|
|
2874
|
+
});
|
|
2875
|
+
const slug = slugify(s.name);
|
|
2876
|
+
let authored;
|
|
2877
|
+
if (s.origin === "scan") {
|
|
2878
|
+
const sources = await this.discoverCache().catch(() => []);
|
|
2879
|
+
const source = sources.find((x) => x.server === s.source) ?? { server: s.source ?? "mcp", label: s.sourceLabel, toolPrefix: `mcp__${s.source ?? ""}`, readTools: [] };
|
|
2880
|
+
const candidate = {
|
|
2881
|
+
id: s.id,
|
|
2882
|
+
kind: useKind,
|
|
2883
|
+
name: slug,
|
|
2884
|
+
description: s.description,
|
|
2885
|
+
topics: s.topics,
|
|
2886
|
+
keywords: s.keywords,
|
|
2887
|
+
rationale: s.rationale,
|
|
2888
|
+
existing: s.existing,
|
|
2889
|
+
status: "authoring"
|
|
2890
|
+
};
|
|
2891
|
+
const allowedTools = ["Read", "Grep", "Glob", source.toolPrefix, ...source.readTools];
|
|
2892
|
+
const out = await runHeadlessClaude({
|
|
2893
|
+
cwd: process.cwd(),
|
|
2894
|
+
prompt: this.authorPrompt(source, candidate, slug),
|
|
2895
|
+
allowedTools,
|
|
2896
|
+
timeoutMs: AUTHOR_TIMEOUT_MS,
|
|
2897
|
+
onSpawn: (child) => this.inFlight = child
|
|
2898
|
+
});
|
|
2899
|
+
authored = this.parseAuthor(out);
|
|
2900
|
+
} else {
|
|
2901
|
+
const out = await runHeadlessClaude({
|
|
2902
|
+
cwd: process.cwd(),
|
|
2903
|
+
prompt: this.conversationAuthorPrompt(s, useKind, slug),
|
|
2904
|
+
allowedTools: ["Read", "Grep", "Glob"],
|
|
2905
|
+
timeoutMs: AUTHOR_TIMEOUT_MS,
|
|
2906
|
+
onSpawn: (child) => this.inFlight = child
|
|
2907
|
+
});
|
|
2908
|
+
authored = this.parseAuthor(out);
|
|
2909
|
+
}
|
|
2910
|
+
if (!authored) throw new Error("The author agent did not return usable file content.");
|
|
2911
|
+
const filePath = useKind === "skill" ? writeSkill(slug, authored.content) : writeAgent(slug, authored.content);
|
|
2912
|
+
this.registerArtifact({
|
|
2913
|
+
kind: useKind,
|
|
2914
|
+
name: slug,
|
|
2915
|
+
filePath,
|
|
2916
|
+
description: authored.description || s.description,
|
|
2917
|
+
topics: authored.topics.length ? authored.topics : s.topics,
|
|
2918
|
+
keywords: authored.keywords.length ? authored.keywords : s.keywords,
|
|
2919
|
+
source: s.origin === "scan" ? s.source ?? "scan" : "conversation",
|
|
2920
|
+
related: authored.related
|
|
2921
|
+
});
|
|
2922
|
+
const artifact = this.state.artifacts.find((a) => a.kind === useKind && a.name === slug);
|
|
2923
|
+
s.status = "created";
|
|
2924
|
+
s.artifactId = artifact?.id;
|
|
2925
|
+
s.filePath = filePath;
|
|
2926
|
+
s.result = `Wrote ${useKind} to ${filePath}`;
|
|
2927
|
+
} catch (err) {
|
|
2928
|
+
if (this.cancelRequested) {
|
|
2929
|
+
s.status = "open";
|
|
2930
|
+
s.result = void 0;
|
|
2931
|
+
} else {
|
|
2932
|
+
s.status = "error";
|
|
2933
|
+
s.result = err.message || String(err);
|
|
2934
|
+
}
|
|
2935
|
+
} finally {
|
|
2936
|
+
s.updatedAt = Date.now();
|
|
2937
|
+
this.inFlight = null;
|
|
2938
|
+
this.setBusy(false);
|
|
2939
|
+
this.persist();
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
/** Dismiss a suggestion without building it (kept as history; may resurface later). */
|
|
2943
|
+
dismissSuggestion(id) {
|
|
2944
|
+
const s = this.state.suggestions.find((x) => x.id === id);
|
|
2945
|
+
if (!s || s.status !== "open" && s.status !== "error") return;
|
|
2946
|
+
s.status = "dismissed";
|
|
2947
|
+
s.updatedAt = Date.now();
|
|
2948
|
+
this.persist();
|
|
2949
|
+
}
|
|
2950
|
+
conversationAuthorPrompt(s, kind, slug) {
|
|
2951
|
+
const related = this.state.artifacts.map((a) => ({ name: a.name, kind: a.kind, description: a.description }));
|
|
2952
|
+
const lessons = this.state.lessons.map((l) => l.text);
|
|
2953
|
+
const isSkill = kind === "skill";
|
|
2954
|
+
return [
|
|
2955
|
+
`You are authoring a Claude Code ${isSkill ? "SKILL" : "SUB-AGENT"} that captures a reusable`,
|
|
2956
|
+
"workflow the user demonstrated in a Maestro Conductor conversation. You run unattended.",
|
|
2957
|
+
"",
|
|
2958
|
+
`Target artifact:
|
|
2959
|
+
${JSON.stringify({ kind, name: slug, description: s.description, topics: s.topics, rationale: s.rationale }, null, 2)}`,
|
|
2960
|
+
"",
|
|
2961
|
+
"The conversation that motivated this artifact — capture the GENERAL, reusable procedure it",
|
|
2962
|
+
"shows; strip one-off specifics (particular file names, ids, values) and keep the repeatable",
|
|
2963
|
+
"method:",
|
|
2964
|
+
s.context || "(no excerpt was captured; rely on the target description above)",
|
|
2965
|
+
"",
|
|
2966
|
+
"Write the COMPLETE file content as Markdown with a YAML frontmatter block.",
|
|
2967
|
+
isSkill ? [
|
|
2968
|
+
"For a SKILL the file is SKILL.md. Frontmatter MUST be exactly:",
|
|
2969
|
+
"---",
|
|
2970
|
+
`name: ${slug}`,
|
|
2971
|
+
"description: <one line describing WHEN to use this skill>",
|
|
2972
|
+
"---",
|
|
2973
|
+
"Then the body: a focused, step-by-step procedure the agent can follow."
|
|
2974
|
+
].join("\n") : [
|
|
2975
|
+
"For a SUB-AGENT the file is an agent definition. Frontmatter MUST be exactly:",
|
|
2976
|
+
"---",
|
|
2977
|
+
`name: ${slug}`,
|
|
2978
|
+
"description: <one line: when to route to this agent — be specific with trigger terms>",
|
|
2979
|
+
"model: claude-sonnet-4-6",
|
|
2980
|
+
"---",
|
|
2981
|
+
"Then the body: the agent's system prompt — its scope, what it knows, and what it does NOT cover."
|
|
2982
|
+
].join("\n"),
|
|
2983
|
+
"",
|
|
2984
|
+
`Other artifacts that exist (name any genuinely related):
|
|
2985
|
+
${JSON.stringify(related, null, 2)}`,
|
|
2986
|
+
lessons.length ? `
|
|
2987
|
+
Lessons learned (respect these):
|
|
2988
|
+
- ${lessons.join("\n- ")}` : "",
|
|
2989
|
+
"",
|
|
2990
|
+
"Respond with ONLY one JSON object — no markdown fences around the whole object, no prose:",
|
|
2991
|
+
'{"content":"<the FULL file content, frontmatter + body, as a single string>",',
|
|
2992
|
+
' "description":"<final one-line description>",',
|
|
2993
|
+
' "topics":["..."], "keywords":["..."],',
|
|
2994
|
+
' "related":["<names of related existing artifacts>"]}'
|
|
2995
|
+
].filter((l) => l !== "").join("\n");
|
|
2996
|
+
}
|
|
2204
2997
|
/** Cancel the in-flight scan/author agent, if any (the run reports 'cancelled'). */
|
|
2205
2998
|
cancel() {
|
|
2206
2999
|
if (!this.inFlight) return;
|
|
@@ -2221,24 +3014,48 @@ class FactoryService {
|
|
|
2221
3014
|
* connectors are not in ~/.claude.json, so a no-tool headless agent reports
|
|
2222
3015
|
* what it can see; we merge that with the user-scope servers from
|
|
2223
3016
|
* ~/.claude.json. Cached for the app run; `refresh` forces a re-discovery.
|
|
3017
|
+
*
|
|
3018
|
+
* PUBLIC entry point (IPC / renderer refresh / auto-propose pre-scan): if a
|
|
3019
|
+
* heavy op holds the lock, WAIT for it before starting a discovery agent, so
|
|
3020
|
+
* we never run two headless `claude -p` children at once. Lock-holders
|
|
3021
|
+
* (scan/approve/createFromSuggestion) must call discoverCache() instead — they
|
|
3022
|
+
* already hold the lock and would otherwise wait on themselves (deadlock).
|
|
2224
3023
|
*/
|
|
2225
3024
|
async listSources(refresh = false) {
|
|
2226
3025
|
if (this.sources && !refresh) return this.sources;
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
3026
|
+
while (this.busy && this.busyPromise) await this.busyPromise;
|
|
3027
|
+
return this.discoverCache(refresh);
|
|
3028
|
+
}
|
|
3029
|
+
/**
|
|
3030
|
+
* The actual cached/memoized discovery, with NO busy-wait. Safe to call from a
|
|
3031
|
+
* lock-holder because discovery runs before that op spawns its own child, so
|
|
3032
|
+
* only one headless child is ever alive. Concurrent callers join one discovery.
|
|
3033
|
+
*/
|
|
3034
|
+
discoverCache(refresh = false) {
|
|
3035
|
+
if (this.sources && !refresh) return Promise.resolve(this.sources);
|
|
3036
|
+
if (this.discovering) return this.discovering;
|
|
3037
|
+
this.discovering = (async () => {
|
|
3038
|
+
try {
|
|
3039
|
+
const discovered = await this.discoverSources().catch(() => []);
|
|
3040
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
3041
|
+
for (const s of discovered) byKey.set(s.server, s);
|
|
3042
|
+
for (const m of readUserMcpServers()) {
|
|
3043
|
+
if (!byKey.has(m.name)) {
|
|
3044
|
+
byKey.set(m.name, {
|
|
3045
|
+
server: m.name,
|
|
3046
|
+
label: KNOWN_LABELS[m.name] ?? m.name,
|
|
3047
|
+
toolPrefix: `mcp__${m.name}`,
|
|
3048
|
+
readTools: []
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
this.sources = [...byKey.values()].sort((a, b) => a.label.localeCompare(b.label));
|
|
3053
|
+
return this.sources;
|
|
3054
|
+
} finally {
|
|
3055
|
+
this.discovering = null;
|
|
2238
3056
|
}
|
|
2239
|
-
}
|
|
2240
|
-
this.
|
|
2241
|
-
return this.sources;
|
|
3057
|
+
})();
|
|
3058
|
+
return this.discovering;
|
|
2242
3059
|
}
|
|
2243
3060
|
async discoverSources() {
|
|
2244
3061
|
const prompt = [
|
|
@@ -2265,8 +3082,8 @@ class FactoryService {
|
|
|
2265
3082
|
prompt,
|
|
2266
3083
|
allowedTools: ["Read"],
|
|
2267
3084
|
timeoutMs: DISCOVER_TIMEOUT_MS,
|
|
2268
|
-
onSpawn: (child) => this.
|
|
2269
|
-
}).finally(() => this.
|
|
3085
|
+
onSpawn: (child) => this.discoverChild = child
|
|
3086
|
+
}).finally(() => this.discoverChild = null);
|
|
2270
3087
|
const parsed = extractJson(out);
|
|
2271
3088
|
const list = Array.isArray(parsed?.servers) ? parsed.servers : [];
|
|
2272
3089
|
const sources = [];
|
|
@@ -2286,30 +3103,37 @@ class FactoryService {
|
|
|
2286
3103
|
return sources;
|
|
2287
3104
|
}
|
|
2288
3105
|
// ---------- scan (phase 1) ----------
|
|
2289
|
-
/**
|
|
3106
|
+
/**
|
|
3107
|
+
* Explore a source and propose skill/agent candidates. Claims the single
|
|
3108
|
+
* `busy` lock SYNCHRONOUSLY (before any await) so two headless agents can
|
|
3109
|
+
* never run at once. Returns the created run's id, or null if it bailed on
|
|
3110
|
+
* the busy guard (no run, no tokens spent) — callers (auto-propose) use this
|
|
3111
|
+
* to harvest exactly this run and to only consume budget when a scan ran.
|
|
3112
|
+
* Throws (releasing the lock) only for an unknown source / discovery failure.
|
|
3113
|
+
*/
|
|
2290
3114
|
async scan(serverKey, guidance) {
|
|
2291
|
-
if (this.busy) return;
|
|
2292
|
-
|
|
2293
|
-
const source = sources.find((s) => s.server === serverKey);
|
|
2294
|
-
if (!source) throw new Error(`Unknown source: ${serverKey}`);
|
|
2295
|
-
this.busy = true;
|
|
3115
|
+
if (this.busy) return null;
|
|
3116
|
+
this.setBusy(true);
|
|
2296
3117
|
this.cancelRequested = false;
|
|
2297
|
-
|
|
2298
|
-
id: crypto.randomUUID(),
|
|
2299
|
-
source: source.server,
|
|
2300
|
-
sourceLabel: source.label,
|
|
2301
|
-
guidance: guidance.trim(),
|
|
2302
|
-
startedAt: Date.now(),
|
|
2303
|
-
finishedAt: null,
|
|
2304
|
-
status: "running",
|
|
2305
|
-
phase: "discovering",
|
|
2306
|
-
candidates: [],
|
|
2307
|
-
summary: ""
|
|
2308
|
-
};
|
|
2309
|
-
this.pushRun(run);
|
|
3118
|
+
let run = null;
|
|
2310
3119
|
try {
|
|
3120
|
+
const sources = await this.discoverCache();
|
|
3121
|
+
const source = sources.find((s) => s.server === serverKey);
|
|
3122
|
+
if (!source) throw new Error(`Unknown source: ${serverKey}`);
|
|
3123
|
+
run = {
|
|
3124
|
+
id: crypto.randomUUID(),
|
|
3125
|
+
source: source.server,
|
|
3126
|
+
sourceLabel: source.label,
|
|
3127
|
+
guidance: guidance.trim(),
|
|
3128
|
+
startedAt: Date.now(),
|
|
3129
|
+
finishedAt: null,
|
|
3130
|
+
status: "running",
|
|
3131
|
+
phase: "discovering",
|
|
3132
|
+
candidates: [],
|
|
3133
|
+
summary: ""
|
|
3134
|
+
};
|
|
3135
|
+
this.pushRun(run);
|
|
2311
3136
|
const allowedTools = ["Read", "Grep", "Glob", source.toolPrefix, ...source.readTools];
|
|
2312
|
-
run.phase = "discovering";
|
|
2313
3137
|
this.broadcastRuns();
|
|
2314
3138
|
const out = await runHeadlessClaude({
|
|
2315
3139
|
cwd: process.cwd(),
|
|
@@ -2325,15 +3149,24 @@ class FactoryService {
|
|
|
2325
3149
|
run.status = "done";
|
|
2326
3150
|
this.absorbTopics(parsed.newTopics, source.server);
|
|
2327
3151
|
} catch (err) {
|
|
2328
|
-
run
|
|
2329
|
-
|
|
2330
|
-
|
|
3152
|
+
if (run) {
|
|
3153
|
+
run.status = this.cancelRequested ? "cancelled" : "error";
|
|
3154
|
+
run.phase = "done";
|
|
3155
|
+
run.summary = this.cancelRequested ? "Cancelled." : err.message || String(err);
|
|
3156
|
+
} else {
|
|
3157
|
+
this.inFlight = null;
|
|
3158
|
+
this.setBusy(false);
|
|
3159
|
+
throw err;
|
|
3160
|
+
}
|
|
2331
3161
|
} finally {
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
3162
|
+
if (run) {
|
|
3163
|
+
run.finishedAt = Date.now();
|
|
3164
|
+
this.inFlight = null;
|
|
3165
|
+
this.setBusy(false);
|
|
3166
|
+
this.broadcastRuns();
|
|
3167
|
+
}
|
|
2336
3168
|
}
|
|
3169
|
+
return run ? run.id : null;
|
|
2337
3170
|
}
|
|
2338
3171
|
scanPrompt(source, guidance) {
|
|
2339
3172
|
const existing = [
|
|
@@ -2432,13 +3265,13 @@ Lessons learned (respect these):
|
|
|
2432
3265
|
const candidate = run?.candidates.find((c) => c.id === candidateId);
|
|
2433
3266
|
if (!run || !candidate || candidate.status !== "proposed" && candidate.status !== "error") return;
|
|
2434
3267
|
if (this.busy) return;
|
|
2435
|
-
this.
|
|
3268
|
+
this.setBusy(true);
|
|
2436
3269
|
this.cancelRequested = false;
|
|
2437
3270
|
candidate.status = "authoring";
|
|
2438
3271
|
candidate.result = void 0;
|
|
2439
3272
|
this.broadcastRuns();
|
|
2440
3273
|
try {
|
|
2441
|
-
const source = (await this.
|
|
3274
|
+
const source = (await this.discoverCache()).find((s) => s.server === run.source) ?? { server: run.source, label: run.sourceLabel, toolPrefix: `mcp__${run.source}`, readTools: [] };
|
|
2442
3275
|
const allowedTools = ["Read", "Grep", "Glob", source.toolPrefix, ...source.readTools];
|
|
2443
3276
|
const slug = slugify(candidate.name);
|
|
2444
3277
|
const out = await runHeadlessClaude({
|
|
@@ -2474,7 +3307,7 @@ Lessons learned (respect these):
|
|
|
2474
3307
|
}
|
|
2475
3308
|
} finally {
|
|
2476
3309
|
this.inFlight = null;
|
|
2477
|
-
this.
|
|
3310
|
+
this.setBusy(false);
|
|
2478
3311
|
this.broadcastRuns();
|
|
2479
3312
|
this.broadcastState();
|
|
2480
3313
|
}
|
|
@@ -2722,6 +3555,28 @@ Lessons learned (respect these):
|
|
|
2722
3555
|
broadcastState() {
|
|
2723
3556
|
this.getWin()?.webContents.send("factory:changed", this.state);
|
|
2724
3557
|
}
|
|
3558
|
+
/**
|
|
3559
|
+
* Single source of truth for the headless lock. Setting it broadcasts so the
|
|
3560
|
+
* renderer can reflect background work (judge / author / scan) that doesn't
|
|
3561
|
+
* create a visible FactoryRun — preventing buttons that would silently no-op
|
|
3562
|
+
* against the lock from staying enabled.
|
|
3563
|
+
*/
|
|
3564
|
+
setBusy(v) {
|
|
3565
|
+
if (this.busy === v) return;
|
|
3566
|
+
this.busy = v;
|
|
3567
|
+
if (v) {
|
|
3568
|
+
this.busyPromise = new Promise((resolve) => this.busyResolve = resolve);
|
|
3569
|
+
} else {
|
|
3570
|
+
this.busyResolve?.();
|
|
3571
|
+
this.busyResolve = null;
|
|
3572
|
+
this.busyPromise = null;
|
|
3573
|
+
}
|
|
3574
|
+
this.getWin()?.webContents.send("factory:busy", v);
|
|
3575
|
+
}
|
|
3576
|
+
/** Current headless-lock state (for the initial renderer fetch). */
|
|
3577
|
+
isBusy() {
|
|
3578
|
+
return this.busy;
|
|
3579
|
+
}
|
|
2725
3580
|
broadcastRuns() {
|
|
2726
3581
|
this.store.setRuns(this.runs);
|
|
2727
3582
|
this.getWin()?.webContents.send("factory:runs", this.runs);
|
|
@@ -3401,7 +4256,7 @@ function tokenize(template) {
|
|
|
3401
4256
|
while (m = re.exec(template)) tokens.push(m[1] ?? m[2]);
|
|
3402
4257
|
return tokens;
|
|
3403
4258
|
}
|
|
3404
|
-
function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpand, conductor, factory, tokenEff, getWin2) {
|
|
4259
|
+
function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpand, conductor, factory, agents, tokenEff, getWin2) {
|
|
3405
4260
|
const rootOf = (id) => {
|
|
3406
4261
|
const config = sessions.getConfig(id);
|
|
3407
4262
|
if (!config) throw new Error(`Unknown session: ${id}`);
|
|
@@ -3580,6 +4435,7 @@ function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpan
|
|
|
3580
4435
|
electron.ipcMain.handle("factory:listSources", (_e, refresh) => factory.listSources(refresh));
|
|
3581
4436
|
electron.ipcMain.handle("factory:state", () => factory.getState());
|
|
3582
4437
|
electron.ipcMain.handle("factory:runs", () => factory.listRuns());
|
|
4438
|
+
electron.ipcMain.handle("factory:isBusy", () => factory.isBusy());
|
|
3583
4439
|
electron.ipcMain.handle(
|
|
3584
4440
|
"factory:scan",
|
|
3585
4441
|
(_e, serverKey, guidance) => factory.scan(serverKey, guidance)
|
|
@@ -3608,6 +4464,15 @@ function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpan
|
|
|
3608
4464
|
electron.ipcMain.handle("factory:dismissTopic", (_e, id) => factory.dismissTopic(id));
|
|
3609
4465
|
electron.ipcMain.handle("factory:addLesson", (_e, text) => factory.addLesson(text));
|
|
3610
4466
|
electron.ipcMain.handle("factory:deleteLesson", (_e, id) => factory.deleteLesson(id));
|
|
4467
|
+
electron.ipcMain.handle(
|
|
4468
|
+
"factory:createFromSuggestion",
|
|
4469
|
+
(_e, id, kind) => factory.createFromSuggestion(id, kind)
|
|
4470
|
+
);
|
|
4471
|
+
electron.ipcMain.handle("factory:dismissSuggestion", (_e, id) => factory.dismissSuggestion(id));
|
|
4472
|
+
electron.ipcMain.handle("agents:get", () => agents.snapshot());
|
|
4473
|
+
electron.ipcMain.handle("agents:refresh", () => agents.refresh());
|
|
4474
|
+
electron.ipcMain.handle("agents:read", (_e, filePath) => agents.readAgentFile(filePath));
|
|
4475
|
+
electron.ipcMain.handle("agents:reveal", (_e, filePath) => agents.revealAgentFile(filePath));
|
|
3611
4476
|
electron.ipcMain.handle("actions:list", () => sessions.actions);
|
|
3612
4477
|
electron.ipcMain.handle("actions:save", (_e, actions) => sessions.saveActions(actions));
|
|
3613
4478
|
electron.ipcMain.handle(
|
|
@@ -3763,6 +4628,7 @@ function registerIpc(sessions, fs2, persistence2, sentinels, features, autoExpan
|
|
|
3763
4628
|
Object.assign(persistence2.state.settings, patch);
|
|
3764
4629
|
persistence2.scheduleSave();
|
|
3765
4630
|
getWin2()?.webContents.send("session:changed");
|
|
4631
|
+
if (patch.agentRegistryPath !== void 0) agents.refresh();
|
|
3766
4632
|
});
|
|
3767
4633
|
}
|
|
3768
4634
|
const DEFAULT_TOKEN_EFFICIENCY = {
|
|
@@ -3789,7 +4655,8 @@ const DEFAULT_SETTINGS = {
|
|
|
3789
4655
|
watchdogStallMinutes: 10,
|
|
3790
4656
|
watchdogUnansweredMinutes: 5,
|
|
3791
4657
|
tokenEfficiency: DEFAULT_TOKEN_EFFICIENCY,
|
|
3792
|
-
tokenEfficiencyRepoOverrides: {}
|
|
4658
|
+
tokenEfficiencyRepoOverrides: {},
|
|
4659
|
+
agentRegistryPath: "C:\\repos\\agent-factory\\registry\\registry.json"
|
|
3793
4660
|
};
|
|
3794
4661
|
const DEFAULT_CATEGORIES = [
|
|
3795
4662
|
{
|
|
@@ -6430,6 +7297,7 @@ if (!gotLock) {
|
|
|
6430
7297
|
const autoExpand = new AutoExpandService(persistence, features, getWin);
|
|
6431
7298
|
const conductor = new ConductorService(persistence, sessions, features, autoExpand, getWin);
|
|
6432
7299
|
const factory = new FactoryService(getWin);
|
|
7300
|
+
const agentRegistry = new AgentRegistryService(persistence, getWin);
|
|
6433
7301
|
registerIpc(
|
|
6434
7302
|
sessions,
|
|
6435
7303
|
fsService,
|
|
@@ -6439,6 +7307,7 @@ if (!gotLock) {
|
|
|
6439
7307
|
autoExpand,
|
|
6440
7308
|
conductor,
|
|
6441
7309
|
factory,
|
|
7310
|
+
agentRegistry,
|
|
6442
7311
|
tokenEff,
|
|
6443
7312
|
getWin
|
|
6444
7313
|
);
|
|
@@ -6448,11 +7317,14 @@ if (!gotLock) {
|
|
|
6448
7317
|
sentinels.start();
|
|
6449
7318
|
autoExpand.start();
|
|
6450
7319
|
tokenEff.start();
|
|
7320
|
+
factory.start();
|
|
7321
|
+
conductor.onTurnComplete((messages) => factory.considerConversation(messages));
|
|
6451
7322
|
electron.app.on("activate", () => {
|
|
6452
7323
|
if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
6453
7324
|
});
|
|
6454
7325
|
electron.app.on("before-quit", () => {
|
|
6455
7326
|
tokenEff.dispose();
|
|
7327
|
+
agentRegistry.dispose();
|
|
6456
7328
|
factory.dispose();
|
|
6457
7329
|
conductor.dispose();
|
|
6458
7330
|
autoExpand.dispose();
|