youmd 0.8.29 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/orchestrate.d.ts +12 -0
- package/dist/commands/orchestrate.d.ts.map +1 -0
- package/dist/commands/orchestrate.js +273 -0
- package/dist/commands/orchestrate.js.map +1 -0
- package/dist/commands/remote.d.ts +8 -0
- package/dist/commands/remote.d.ts.map +1 -1
- package/dist/commands/remote.js +8 -0
- package/dist/commands/remote.js.map +1 -1
- package/dist/commands/stack.d.ts.map +1 -1
- package/dist/commands/stack.js +37 -3
- package/dist/commands/stack.js.map +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/storage.d.ts +7 -0
- package/dist/commands/storage.d.ts.map +1 -0
- package/dist/commands/storage.js +220 -0
- package/dist/commands/storage.js.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/api.d.ts +32 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +21 -0
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/config.d.ts +15 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/daemon.d.ts +19 -0
- package/dist/lib/daemon.d.ts.map +1 -1
- package/dist/lib/daemon.js +72 -1
- package/dist/lib/daemon.js.map +1 -1
- package/dist/lib/foldermd.d.ts +71 -0
- package/dist/lib/foldermd.d.ts.map +1 -0
- package/dist/lib/foldermd.js +244 -0
- package/dist/lib/foldermd.js.map +1 -0
- package/dist/lib/orchestrator/loop.d.ts +55 -0
- package/dist/lib/orchestrator/loop.d.ts.map +1 -0
- package/dist/lib/orchestrator/loop.js +248 -0
- package/dist/lib/orchestrator/loop.js.map +1 -0
- package/dist/lib/orchestrator/supervisor.d.ts +80 -0
- package/dist/lib/orchestrator/supervisor.d.ts.map +1 -0
- package/dist/lib/orchestrator/supervisor.js +315 -0
- package/dist/lib/orchestrator/supervisor.js.map +1 -0
- package/dist/lib/orchestrator/tools.d.ts +16 -0
- package/dist/lib/orchestrator/tools.d.ts.map +1 -0
- package/dist/lib/orchestrator/tools.js +258 -0
- package/dist/lib/orchestrator/tools.js.map +1 -0
- package/dist/lib/remote-executor.d.ts +15 -1
- package/dist/lib/remote-executor.d.ts.map +1 -1
- package/dist/lib/remote-executor.js +195 -0
- package/dist/lib/remote-executor.js.map +1 -1
- package/dist/mcp/registry.d.ts.map +1 -1
- package/dist/mcp/registry.js +89 -0
- package/dist/mcp/registry.js.map +1 -1
- package/package.json +1 -1
- package/scripts/skillstack-sync/com.you.orchestrator-watch.plist +38 -0
- package/scripts/skillstack-sync/install-daemons-linux.sh +168 -0
- package/scripts/skillstack-sync/install-daemons.sh +1 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// loop.ts — the iterative agent loop for the You agent orchestrator.
|
|
3
|
+
//
|
|
4
|
+
// This is the one genuinely net-new piece the audit identified: every existing path in the
|
|
5
|
+
// CLI is single-hop (route -> run -> template-summarize -> stop). This closes the loop:
|
|
6
|
+
// the model picks a tool, we run it, feed the result back, and repeat until the model calls
|
|
7
|
+
// `finish`. It is model-agnostic (the model caller is injected) and tool-agnostic (tools are
|
|
8
|
+
// a registry of typed handlers), so the same loop drives orchestrator tools, brain tools, or
|
|
9
|
+
// remote-machine tools without change.
|
|
10
|
+
//
|
|
11
|
+
// The loop deliberately stays small and uses a JSON tool-call convention over plain chat
|
|
12
|
+
// completions, so it works against the existing /api/v1/chat proxy without needing native
|
|
13
|
+
// provider tool_use. That keeps the You agent harness/model-agnostic by construction.
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.parseToolCall = parseToolCall;
|
|
16
|
+
exports.runAgentLoop = runAgentLoop;
|
|
17
|
+
const FINISH_TOOL = "finish";
|
|
18
|
+
function buildSystemPrompt(goal, tools, context) {
|
|
19
|
+
const toolDocs = tools
|
|
20
|
+
.map((t) => {
|
|
21
|
+
const params = Object.entries(t.parameters)
|
|
22
|
+
.map(([k, v]) => ` - ${k}: ${v}`)
|
|
23
|
+
.join("\n");
|
|
24
|
+
return ` - ${t.name}: ${t.description}${params ? `\n${params}` : ""}`;
|
|
25
|
+
})
|
|
26
|
+
.join("\n");
|
|
27
|
+
return [
|
|
28
|
+
"You are U — Houston's personal master ORCHESTRATOR agent (U = the ultimate orchestrator, a role, not a person).",
|
|
29
|
+
"You do NOT write code or do the work yourself. You are model- and harness-agnostic: you",
|
|
30
|
+
"route work to the best worker agent (Claude Code for coding, etc.), launch and monitor",
|
|
31
|
+
"them across machines and projects, and report back. You are project-goal and task oriented.",
|
|
32
|
+
context ? `\nContext:\n${context}` : "",
|
|
33
|
+
"\nYou work in a loop. On EACH turn you call exactly one tool by replying with ONLY a JSON",
|
|
34
|
+
"object (no prose, no markdown fences) of the form:",
|
|
35
|
+
' {"thought": "<one short sentence of reasoning>", "tool": "<tool_name>", "args": { ... }}',
|
|
36
|
+
"\nAvailable tools:",
|
|
37
|
+
toolDocs,
|
|
38
|
+
` - ${FINISH_TOOL}: end the loop and report back. args: { summary: "what you did / found, for Houston" }`,
|
|
39
|
+
"\nRules:",
|
|
40
|
+
"- Reply with ONE json object and nothing else.",
|
|
41
|
+
"- Prefer delegating to a worker harness over doing work yourself.",
|
|
42
|
+
`- When the goal is met (or blocked), call ${FINISH_TOOL} with a clear summary.`,
|
|
43
|
+
`- Do not loop forever; if stuck, call ${FINISH_TOOL} and explain what is blocked.`,
|
|
44
|
+
`\nGoal: ${goal}`,
|
|
45
|
+
]
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join("\n");
|
|
48
|
+
}
|
|
49
|
+
function toolCallFromObject(obj) {
|
|
50
|
+
if (typeof obj.tool !== "string")
|
|
51
|
+
return null;
|
|
52
|
+
const args = obj.args && typeof obj.args === "object" ? obj.args : {};
|
|
53
|
+
return {
|
|
54
|
+
thought: typeof obj.thought === "string" ? obj.thought : undefined,
|
|
55
|
+
tool: obj.tool,
|
|
56
|
+
args,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Extract a tool call from a model reply, tolerating prose around/between JSON, code fences, and
|
|
61
|
+
* multiple objects. Scans for every balanced `{…}` span and returns the first that parses to an
|
|
62
|
+
* object with a string `tool` — so "Sure! {…prose…} {\"tool\":…}" still works (the naive
|
|
63
|
+
* first-`{`-to-last-`}` slice would fail on that).
|
|
64
|
+
*/
|
|
65
|
+
/** Reject absurdly large candidate JSON before parsing — a sane tool call is small, and a
|
|
66
|
+
* multi-hundred-KB span is either junk or a runaway response we don't want to parse/echo. */
|
|
67
|
+
const MAX_TOOL_CALL_CHARS = 256000;
|
|
68
|
+
function parseToolCall(raw) {
|
|
69
|
+
if (!raw)
|
|
70
|
+
return null;
|
|
71
|
+
let text = raw.trim();
|
|
72
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
73
|
+
if (fence)
|
|
74
|
+
text = fence[1].trim();
|
|
75
|
+
// Whole-string fast path.
|
|
76
|
+
if (text.length <= MAX_TOOL_CALL_CHARS) {
|
|
77
|
+
try {
|
|
78
|
+
const obj = JSON.parse(text);
|
|
79
|
+
const call = toolCallFromObject(obj);
|
|
80
|
+
if (call)
|
|
81
|
+
return call;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* fall through to span scan */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Scan for balanced {…} spans (string-aware) and try each.
|
|
88
|
+
let depth = 0;
|
|
89
|
+
let startIdx = -1;
|
|
90
|
+
let inStr = false;
|
|
91
|
+
let escaped = false;
|
|
92
|
+
for (let i = 0; i < text.length; i++) {
|
|
93
|
+
const ch = text[i];
|
|
94
|
+
if (inStr) {
|
|
95
|
+
if (escaped)
|
|
96
|
+
escaped = false;
|
|
97
|
+
else if (ch === "\\")
|
|
98
|
+
escaped = true;
|
|
99
|
+
else if (ch === '"')
|
|
100
|
+
inStr = false;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (ch === '"')
|
|
104
|
+
inStr = true;
|
|
105
|
+
else if (ch === "{") {
|
|
106
|
+
if (depth === 0)
|
|
107
|
+
startIdx = i;
|
|
108
|
+
depth++;
|
|
109
|
+
}
|
|
110
|
+
else if (ch === "}") {
|
|
111
|
+
if (depth > 0) {
|
|
112
|
+
depth--;
|
|
113
|
+
if (depth === 0 && startIdx !== -1) {
|
|
114
|
+
const candidate = text.slice(startIdx, i + 1);
|
|
115
|
+
if (candidate.length > MAX_TOOL_CALL_CHARS) {
|
|
116
|
+
startIdx = -1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const obj = JSON.parse(candidate);
|
|
121
|
+
const call = toolCallFromObject(obj);
|
|
122
|
+
if (call)
|
|
123
|
+
return call;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
/* try next span */
|
|
127
|
+
}
|
|
128
|
+
startIdx = -1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
function truncate(s, max = 4000) {
|
|
136
|
+
return s.length > max ? s.slice(0, max) + `\n…[truncated ${s.length - max} chars]` : s;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Run the orchestrator loop until the model calls `finish` or maxSteps is hit.
|
|
140
|
+
* Each step: ask the model for a tool call → run it → append the result → repeat.
|
|
141
|
+
*/
|
|
142
|
+
async function runAgentLoop(options) {
|
|
143
|
+
const { goal, tools, callModel, context } = options;
|
|
144
|
+
const maxSteps = options.maxSteps ?? 12;
|
|
145
|
+
const maxModelRetries = options.maxModelRetries ?? 2;
|
|
146
|
+
const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
147
|
+
const toolByName = new Map(tools.map((t) => [t.name, t]));
|
|
148
|
+
// Absorb transient model-call failures (timeouts, 5xx, socket resets) with bounded
|
|
149
|
+
// exponential backoff. Real LLM/proxy endpoints hiccup; a single blip shouldn't abandon
|
|
150
|
+
// the whole goal mid-run. Throws the last error only after every attempt fails.
|
|
151
|
+
const callModelWithRetry = async (msgs) => {
|
|
152
|
+
let lastErr;
|
|
153
|
+
for (let attempt = 0; attempt <= maxModelRetries; attempt++) {
|
|
154
|
+
try {
|
|
155
|
+
return await callModel(msgs);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
lastErr = err;
|
|
159
|
+
if (attempt < maxModelRetries)
|
|
160
|
+
await sleep(Math.min(250 * 2 ** attempt, 4000));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
164
|
+
};
|
|
165
|
+
const messages = [
|
|
166
|
+
{ role: "system", content: buildSystemPrompt(goal, tools, context) },
|
|
167
|
+
{ role: "user", content: `Begin. Goal: ${goal}` },
|
|
168
|
+
];
|
|
169
|
+
const steps = [];
|
|
170
|
+
let consecutiveParseFailures = 0;
|
|
171
|
+
const MAX_PARSE_FAILURES = 2;
|
|
172
|
+
// Safety net for high maxSteps: each step appends 1-2 messages, so a long/stuck run could
|
|
173
|
+
// grow the context unboundedly. Stop cleanly well before that becomes a token problem.
|
|
174
|
+
const MAX_MESSAGES = 256;
|
|
175
|
+
for (let i = 0; i < maxSteps; i++) {
|
|
176
|
+
if (messages.length > MAX_MESSAGES) {
|
|
177
|
+
return {
|
|
178
|
+
finished: false,
|
|
179
|
+
summary: `Stopped: conversation grew past ${MAX_MESSAGES} messages without finishing (likely stuck). Narrow the goal or run again.`,
|
|
180
|
+
steps,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
let reply;
|
|
184
|
+
try {
|
|
185
|
+
reply = await callModelWithRetry(messages);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
return {
|
|
189
|
+
finished: false,
|
|
190
|
+
summary: `Model call failed at step ${i + 1} after ${maxModelRetries + 1} attempt(s): ${err.message}`,
|
|
191
|
+
steps,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const call = parseToolCall(reply);
|
|
195
|
+
if (!call) {
|
|
196
|
+
// Model didn't emit a valid tool call — nudge, but bail after a couple of failures so a
|
|
197
|
+
// model that can't produce JSON doesn't silently burn the whole step budget.
|
|
198
|
+
consecutiveParseFailures++;
|
|
199
|
+
if (consecutiveParseFailures >= MAX_PARSE_FAILURES) {
|
|
200
|
+
return {
|
|
201
|
+
finished: false,
|
|
202
|
+
summary: `Stopped: the model did not return a valid tool call after ${MAX_PARSE_FAILURES} tries. Last reply: ${reply.slice(0, 200)}`,
|
|
203
|
+
steps,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
messages.push({ role: "assistant", content: reply });
|
|
207
|
+
messages.push({
|
|
208
|
+
role: "user",
|
|
209
|
+
content: 'That was not a valid tool call. Reply with ONLY a json object: {"thought":"...","tool":"...","args":{...}}.',
|
|
210
|
+
});
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
consecutiveParseFailures = 0;
|
|
214
|
+
messages.push({ role: "assistant", content: JSON.stringify(call) });
|
|
215
|
+
if (call.tool === FINISH_TOOL) {
|
|
216
|
+
const summary = typeof call.args.summary === "string" && call.args.summary.trim()
|
|
217
|
+
? call.args.summary
|
|
218
|
+
: "Done.";
|
|
219
|
+
return { finished: true, summary, steps };
|
|
220
|
+
}
|
|
221
|
+
const tool = toolByName.get(call.tool);
|
|
222
|
+
let result;
|
|
223
|
+
if (!tool) {
|
|
224
|
+
result = `error: unknown tool "${call.tool}". Available: ${tools.map((t) => t.name).join(", ")}, ${FINISH_TOOL}.`;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
try {
|
|
228
|
+
result = await tool.run(call.args);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
result = `error running ${call.tool}: ${err.message}`;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const step = { index: i, tool: call.tool, args: call.args, result, thought: call.thought };
|
|
235
|
+
steps.push(step);
|
|
236
|
+
options.onStep?.(step);
|
|
237
|
+
messages.push({
|
|
238
|
+
role: "user",
|
|
239
|
+
content: `Result of ${call.tool}:\n${truncate(result)}\n\nChoose the next tool (or ${FINISH_TOOL}).`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
finished: false,
|
|
244
|
+
summary: `Reached the ${maxSteps}-step limit without finishing. Last activity is in the step log; consider a narrower goal or running again.`,
|
|
245
|
+
steps,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=loop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loop.js","sourceRoot":"","sources":["../../../src/lib/orchestrator/loop.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export type WorkerHarness = "claude" | "codex" | "cursor" | "custom";
|
|
2
|
+
export type WorkerStatus = "running" | "exited" | "failed" | "stopped" | "unknown";
|
|
3
|
+
export interface WorkerRecord {
|
|
4
|
+
id: string;
|
|
5
|
+
harness: WorkerHarness;
|
|
6
|
+
/** You.md project name / slug this worker is operating on (for reporting). */
|
|
7
|
+
project?: string;
|
|
8
|
+
/** Working directory the harness was launched in. */
|
|
9
|
+
cwd: string;
|
|
10
|
+
/** The task prompt handed to the worker. */
|
|
11
|
+
goal: string;
|
|
12
|
+
pid?: number;
|
|
13
|
+
status: WorkerStatus;
|
|
14
|
+
exitCode?: number;
|
|
15
|
+
startedAt: string;
|
|
16
|
+
endedAt?: string;
|
|
17
|
+
logFile: string;
|
|
18
|
+
/** Host that launched this worker (for cross-machine fleet views). */
|
|
19
|
+
host: string;
|
|
20
|
+
/** True once this worker's terminal transition has been reported (watch loop). */
|
|
21
|
+
reported?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Terminal worker states — the worker is no longer doing work. */
|
|
24
|
+
export declare const TERMINAL_WORKER_STATUSES: WorkerStatus[];
|
|
25
|
+
export declare function isTerminalWorkerStatus(status: WorkerStatus): boolean;
|
|
26
|
+
export declare function loadWorkers(): WorkerRecord[];
|
|
27
|
+
/** True if a pid is alive (signal 0 probe). */
|
|
28
|
+
export declare function isPidAlive(pid: number | undefined): boolean;
|
|
29
|
+
/** Reconcile registry status against live pids (a running worker whose pid died → exited). */
|
|
30
|
+
export declare function refreshWorkers(workers?: WorkerRecord[]): WorkerRecord[];
|
|
31
|
+
export interface SpawnWorkerInput {
|
|
32
|
+
harness: WorkerHarness;
|
|
33
|
+
goal: string;
|
|
34
|
+
cwd: string;
|
|
35
|
+
project?: string;
|
|
36
|
+
host: string;
|
|
37
|
+
/** Required when harness === "custom": full argv, with {prompt} substituted in any element. */
|
|
38
|
+
customCommand?: string[];
|
|
39
|
+
/**
|
|
40
|
+
* Environment for the worker process. Defaults to the parent env for LOCAL spawns. Remote
|
|
41
|
+
* (bus-triggered) spawns MUST pass a minimized env so a remotely-launched agent does not
|
|
42
|
+
* inherit the daemon's secrets (you.md token, OPENROUTER_API_KEY, vault/folder keys).
|
|
43
|
+
*/
|
|
44
|
+
env?: NodeJS.ProcessEnv;
|
|
45
|
+
}
|
|
46
|
+
export interface SpawnWorkerResult {
|
|
47
|
+
ok: boolean;
|
|
48
|
+
worker?: WorkerRecord;
|
|
49
|
+
error?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Launch a worker harness detached, streaming its output to a per-worker log file, and record
|
|
53
|
+
* it in the registry. Detached + unref so the worker outlives this CLI process (an orchestrator
|
|
54
|
+
* dispatches and moves on; it does not block on the worker).
|
|
55
|
+
*/
|
|
56
|
+
export declare function spawnWorker(input: SpawnWorkerInput): SpawnWorkerResult;
|
|
57
|
+
export declare function getWorker(id: string): WorkerRecord | undefined;
|
|
58
|
+
/** Tail the last `lines` of a worker's captured output. */
|
|
59
|
+
export declare function getWorkerOutput(id: string, lines?: number): {
|
|
60
|
+
ok: boolean;
|
|
61
|
+
output?: string;
|
|
62
|
+
error?: string;
|
|
63
|
+
};
|
|
64
|
+
/** Stop a running worker (SIGTERM the process group). */
|
|
65
|
+
export declare function stopWorker(id: string): {
|
|
66
|
+
ok: boolean;
|
|
67
|
+
error?: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Reconcile status, then return workers that just reached a terminal state and have NOT yet been
|
|
71
|
+
* reported. Does NOT flip the `reported` flag — the caller marks each id reported only AFTER a
|
|
72
|
+
* successful post (via markReported), so a failed/transient post is retried next pass instead of
|
|
73
|
+
* silently dropping the completion. The "always on, report back" primitive for the watch loop.
|
|
74
|
+
*/
|
|
75
|
+
export declare function collectUnreportedCompletions(): WorkerRecord[];
|
|
76
|
+
/** Mark the given worker ids as reported (call only after the completion was successfully posted). */
|
|
77
|
+
export declare function markReported(ids: string[]): void;
|
|
78
|
+
/** Drop exited/stopped workers older than `maxAgeMs` from the registry (keeps running ones). */
|
|
79
|
+
export declare function pruneWorkers(maxAgeMs?: number): number;
|
|
80
|
+
//# sourceMappingURL=supervisor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor.d.ts","sourceRoot":"","sources":["../../../src/lib/orchestrator/supervisor.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AACrE,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;AAEnF,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,aAAa,CAAC;IACvB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,mEAAmE;AACnE,eAAO,MAAM,wBAAwB,EAAE,YAAY,EAAoC,CAAC;AAExF,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAEpE;AAmCD,wBAAgB,WAAW,IAAI,YAAY,EAAE,CAiB5C;AAaD,+CAA+C;AAC/C,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAS3D;AAED,8FAA8F;AAC9F,wBAAgB,cAAc,CAAC,OAAO,GAAE,YAAY,EAAkB,GAAG,YAAY,EAAE,CAWtF;AASD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,aAAa,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,+FAA+F;IAC/F,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAoFtE;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAE9D;AAED,2DAA2D;AAC3D,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAUxG;AAED,yDAAyD;AACzD,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAqBtE;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,IAAI,YAAY,EAAE,CAG7D;AAED,sGAAsG;AACtG,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAYhD;AAED,gGAAgG;AAChG,wBAAgB,YAAY,CAAC,QAAQ,SAAsB,GAAG,MAAM,CAWnE"}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// supervisor.ts — process supervisor for the You agent orchestrator.
|
|
3
|
+
//
|
|
4
|
+
// The You agent is a MASTER ORCHESTRATOR, not a worker/coding agent. It launches, monitors,
|
|
5
|
+
// and stops other agentic harnesses (Claude Code, Codex, Cursor, etc.) — each best at a
|
|
6
|
+
// given task — in real project directories, captures their output, and reports back. This
|
|
7
|
+
// module is the deterministic substrate for that: spawn / list / tail / stop, persisted in a
|
|
8
|
+
// registry so an always-on daemon (or the next CLI invocation) can see the whole fleet.
|
|
9
|
+
//
|
|
10
|
+
// No LLM in this file. It is pure, testable process management; the loop (loop.ts) drives it.
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.TERMINAL_WORKER_STATUSES = void 0;
|
|
46
|
+
exports.isTerminalWorkerStatus = isTerminalWorkerStatus;
|
|
47
|
+
exports.loadWorkers = loadWorkers;
|
|
48
|
+
exports.isPidAlive = isPidAlive;
|
|
49
|
+
exports.refreshWorkers = refreshWorkers;
|
|
50
|
+
exports.spawnWorker = spawnWorker;
|
|
51
|
+
exports.getWorker = getWorker;
|
|
52
|
+
exports.getWorkerOutput = getWorkerOutput;
|
|
53
|
+
exports.stopWorker = stopWorker;
|
|
54
|
+
exports.collectUnreportedCompletions = collectUnreportedCompletions;
|
|
55
|
+
exports.markReported = markReported;
|
|
56
|
+
exports.pruneWorkers = pruneWorkers;
|
|
57
|
+
const child_process = __importStar(require("child_process"));
|
|
58
|
+
const fs = __importStar(require("fs"));
|
|
59
|
+
const path = __importStar(require("path"));
|
|
60
|
+
const config_1 = require("../config");
|
|
61
|
+
/** Terminal worker states — the worker is no longer doing work. */
|
|
62
|
+
exports.TERMINAL_WORKER_STATUSES = ["exited", "stopped", "failed"];
|
|
63
|
+
function isTerminalWorkerStatus(status) {
|
|
64
|
+
return exports.TERMINAL_WORKER_STATUSES.includes(status);
|
|
65
|
+
}
|
|
66
|
+
const HARNESS_SPECS = {
|
|
67
|
+
// Claude Code headless print mode.
|
|
68
|
+
claude: { bin: "claude", args: (p) => ["-p", p], overrideEnv: "YOU_HARNESS_CLAUDE" },
|
|
69
|
+
// Codex non-interactive exec mode.
|
|
70
|
+
codex: { bin: "codex", args: (p) => ["exec", p], overrideEnv: "YOU_HARNESS_CODEX" },
|
|
71
|
+
// Cursor agent CLI headless mode.
|
|
72
|
+
cursor: { bin: "cursor-agent", args: (p) => ["-p", p], overrideEnv: "YOU_HARNESS_CURSOR" },
|
|
73
|
+
};
|
|
74
|
+
function orchestratorDir() {
|
|
75
|
+
const dir = path.join((0, config_1.getWritableHomeBundleDir)(), "orchestrator");
|
|
76
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
77
|
+
return dir;
|
|
78
|
+
}
|
|
79
|
+
function logsDir() {
|
|
80
|
+
const dir = path.join(orchestratorDir(), "logs");
|
|
81
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
82
|
+
return dir;
|
|
83
|
+
}
|
|
84
|
+
function registryPath() {
|
|
85
|
+
return path.join(orchestratorDir(), "workers.json");
|
|
86
|
+
}
|
|
87
|
+
function loadWorkers() {
|
|
88
|
+
try {
|
|
89
|
+
const raw = fs.readFileSync(registryPath(), "utf-8");
|
|
90
|
+
const parsed = JSON.parse(raw);
|
|
91
|
+
if (!Array.isArray(parsed))
|
|
92
|
+
return [];
|
|
93
|
+
// Drop malformed records (partial write, hand-edit, schema drift) instead of letting a
|
|
94
|
+
// single bad entry throw downstream — a corrupt row shouldn't hide every healthy worker.
|
|
95
|
+
return parsed.filter((w) => !!w &&
|
|
96
|
+
typeof w === "object" &&
|
|
97
|
+
typeof w.id === "string" &&
|
|
98
|
+
typeof w.status === "string");
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function saveWorkers(workers) {
|
|
105
|
+
// Atomic write (tmp + rename) so a concurrent reader never sees a half-written file.
|
|
106
|
+
// NOTE: this does not prevent lost updates under concurrent writers (the registry is a
|
|
107
|
+
// single JSON file with no lock); spawn/watch/remote runs are low-frequency so the window
|
|
108
|
+
// is small, but a future hardening is per-record files or a lockfile.
|
|
109
|
+
const target = registryPath();
|
|
110
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
111
|
+
fs.writeFileSync(tmp, JSON.stringify(workers, null, 2));
|
|
112
|
+
fs.renameSync(tmp, target);
|
|
113
|
+
}
|
|
114
|
+
/** True if a pid is alive (signal 0 probe). */
|
|
115
|
+
function isPidAlive(pid) {
|
|
116
|
+
if (!pid || pid <= 0)
|
|
117
|
+
return false;
|
|
118
|
+
try {
|
|
119
|
+
process.kill(pid, 0);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
// ESRCH = no such process; EPERM = exists but not ours (still alive).
|
|
124
|
+
return err.code === "EPERM";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Reconcile registry status against live pids (a running worker whose pid died → exited). */
|
|
128
|
+
function refreshWorkers(workers = loadWorkers()) {
|
|
129
|
+
let changed = false;
|
|
130
|
+
for (const w of workers) {
|
|
131
|
+
if (w.status === "running" && !isPidAlive(w.pid)) {
|
|
132
|
+
w.status = "exited";
|
|
133
|
+
w.endedAt = w.endedAt ?? new Date().toISOString();
|
|
134
|
+
changed = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (changed)
|
|
138
|
+
saveWorkers(workers);
|
|
139
|
+
return workers;
|
|
140
|
+
}
|
|
141
|
+
function shortId() {
|
|
142
|
+
// ULID-ish without external deps: time + random suffix. Not crypto; just a unique handle.
|
|
143
|
+
const rand = Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, "0");
|
|
144
|
+
const stamp = Date.now().toString(36);
|
|
145
|
+
return `w_${stamp}${rand}`;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Launch a worker harness detached, streaming its output to a per-worker log file, and record
|
|
149
|
+
* it in the registry. Detached + unref so the worker outlives this CLI process (an orchestrator
|
|
150
|
+
* dispatches and moves on; it does not block on the worker).
|
|
151
|
+
*/
|
|
152
|
+
function spawnWorker(input) {
|
|
153
|
+
const { harness, goal, cwd, project, host } = input;
|
|
154
|
+
if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
|
|
155
|
+
return { ok: false, error: `working directory does not exist: ${cwd}` };
|
|
156
|
+
}
|
|
157
|
+
let bin;
|
|
158
|
+
let argv;
|
|
159
|
+
if (harness === "custom") {
|
|
160
|
+
if (!input.customCommand || input.customCommand.length === 0) {
|
|
161
|
+
return { ok: false, error: "custom harness requires customCommand argv" };
|
|
162
|
+
}
|
|
163
|
+
const subst = input.customCommand.map((part) => part.replace(/\{prompt\}/g, goal));
|
|
164
|
+
bin = subst[0];
|
|
165
|
+
argv = subst.slice(1);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const spec = HARNESS_SPECS[harness];
|
|
169
|
+
const override = process.env[spec.overrideEnv];
|
|
170
|
+
bin = override && override.trim() ? override.trim() : spec.bin;
|
|
171
|
+
argv = spec.args(goal);
|
|
172
|
+
}
|
|
173
|
+
const id = shortId();
|
|
174
|
+
const logFile = path.join(logsDir(), `${id}.log`);
|
|
175
|
+
let pid;
|
|
176
|
+
try {
|
|
177
|
+
const out = fs.openSync(logFile, "a");
|
|
178
|
+
fs.writeSync(out, `# you orchestrator worker ${id}\n# harness=${harness} project=${project ?? "-"} cwd=${cwd}\n# goal: ${goal}\n# started: ${new Date().toISOString()}\n\n`);
|
|
179
|
+
const child = child_process.spawn(bin, argv, {
|
|
180
|
+
cwd,
|
|
181
|
+
detached: true,
|
|
182
|
+
stdio: ["ignore", out, out],
|
|
183
|
+
env: input.env ?? process.env,
|
|
184
|
+
});
|
|
185
|
+
pid = child.pid;
|
|
186
|
+
// A missing/unlaunchable binary surfaces ASYNCHRONOUSLY via 'error' (ENOENT), after spawn
|
|
187
|
+
// returns. Without this handler Node throws it as an uncaught exception, and the registry
|
|
188
|
+
// would keep a phantom "running" record. Mark the worker failed and never let it crash us.
|
|
189
|
+
child.on("error", (err) => {
|
|
190
|
+
try {
|
|
191
|
+
fs.appendFileSync(logFile, `\n# launch error: ${err.message}\n`);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
/* ignore */
|
|
195
|
+
}
|
|
196
|
+
const all = loadWorkers();
|
|
197
|
+
const rec = all.find((w) => w.id === id);
|
|
198
|
+
if (rec && rec.status === "running") {
|
|
199
|
+
rec.status = "failed";
|
|
200
|
+
rec.endedAt = new Date().toISOString();
|
|
201
|
+
saveWorkers(all);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
child.unref();
|
|
205
|
+
fs.closeSync(out);
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
error: `failed to launch ${bin}: ${err.message} (is the harness installed and on PATH?)`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const worker = {
|
|
214
|
+
id,
|
|
215
|
+
harness,
|
|
216
|
+
project,
|
|
217
|
+
cwd,
|
|
218
|
+
goal,
|
|
219
|
+
pid,
|
|
220
|
+
status: pid ? "running" : "failed",
|
|
221
|
+
startedAt: new Date().toISOString(),
|
|
222
|
+
logFile,
|
|
223
|
+
host,
|
|
224
|
+
};
|
|
225
|
+
const workers = loadWorkers();
|
|
226
|
+
workers.push(worker);
|
|
227
|
+
saveWorkers(workers);
|
|
228
|
+
return { ok: true, worker };
|
|
229
|
+
}
|
|
230
|
+
function getWorker(id) {
|
|
231
|
+
return refreshWorkers().find((w) => w.id === id);
|
|
232
|
+
}
|
|
233
|
+
/** Tail the last `lines` of a worker's captured output. */
|
|
234
|
+
function getWorkerOutput(id, lines = 40) {
|
|
235
|
+
const worker = getWorker(id);
|
|
236
|
+
if (!worker)
|
|
237
|
+
return { ok: false, error: `no worker with id ${id}` };
|
|
238
|
+
try {
|
|
239
|
+
const raw = fs.readFileSync(worker.logFile, "utf-8");
|
|
240
|
+
const tail = raw.split(/\r?\n/).slice(-lines).join("\n");
|
|
241
|
+
return { ok: true, output: tail };
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
return { ok: false, error: `could not read log: ${err.message}` };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Stop a running worker (SIGTERM the process group). */
|
|
248
|
+
function stopWorker(id) {
|
|
249
|
+
const workers = refreshWorkers();
|
|
250
|
+
const worker = workers.find((w) => w.id === id);
|
|
251
|
+
if (!worker)
|
|
252
|
+
return { ok: false, error: `no worker with id ${id}` };
|
|
253
|
+
if (worker.status !== "running")
|
|
254
|
+
return { ok: true };
|
|
255
|
+
try {
|
|
256
|
+
if (worker.pid && isPidAlive(worker.pid)) {
|
|
257
|
+
// Negative pid → kill the detached process group.
|
|
258
|
+
try {
|
|
259
|
+
process.kill(-worker.pid, "SIGTERM");
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
process.kill(worker.pid, "SIGTERM");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
worker.status = "stopped";
|
|
266
|
+
worker.endedAt = new Date().toISOString();
|
|
267
|
+
saveWorkers(workers);
|
|
268
|
+
return { ok: true };
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
return { ok: false, error: `failed to stop: ${err.message}` };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Reconcile status, then return workers that just reached a terminal state and have NOT yet been
|
|
276
|
+
* reported. Does NOT flip the `reported` flag — the caller marks each id reported only AFTER a
|
|
277
|
+
* successful post (via markReported), so a failed/transient post is retried next pass instead of
|
|
278
|
+
* silently dropping the completion. The "always on, report back" primitive for the watch loop.
|
|
279
|
+
*/
|
|
280
|
+
function collectUnreportedCompletions() {
|
|
281
|
+
const workers = refreshWorkers();
|
|
282
|
+
return workers.filter((w) => isTerminalWorkerStatus(w.status) && !w.reported);
|
|
283
|
+
}
|
|
284
|
+
/** Mark the given worker ids as reported (call only after the completion was successfully posted). */
|
|
285
|
+
function markReported(ids) {
|
|
286
|
+
if (ids.length === 0)
|
|
287
|
+
return;
|
|
288
|
+
const set = new Set(ids);
|
|
289
|
+
const workers = loadWorkers();
|
|
290
|
+
let changed = false;
|
|
291
|
+
for (const w of workers) {
|
|
292
|
+
if (set.has(w.id) && !w.reported) {
|
|
293
|
+
w.reported = true;
|
|
294
|
+
changed = true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (changed)
|
|
298
|
+
saveWorkers(workers);
|
|
299
|
+
}
|
|
300
|
+
/** Drop exited/stopped workers older than `maxAgeMs` from the registry (keeps running ones). */
|
|
301
|
+
function pruneWorkers(maxAgeMs = 24 * 60 * 60 * 1000) {
|
|
302
|
+
const now = Date.now();
|
|
303
|
+
const workers = refreshWorkers();
|
|
304
|
+
const kept = workers.filter((w) => {
|
|
305
|
+
if (w.status === "running")
|
|
306
|
+
return true;
|
|
307
|
+
const ended = w.endedAt ? Date.parse(w.endedAt) : Date.parse(w.startedAt);
|
|
308
|
+
return now - ended < maxAgeMs;
|
|
309
|
+
});
|
|
310
|
+
const removed = workers.length - kept.length;
|
|
311
|
+
if (removed > 0)
|
|
312
|
+
saveWorkers(kept);
|
|
313
|
+
return removed;
|
|
314
|
+
}
|
|
315
|
+
//# sourceMappingURL=supervisor.js.map
|