symbiot 0.36.0 → 0.36.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/agents.mjs +19 -10
- package/package.json +1 -1
package/agents.mjs
CHANGED
|
@@ -59,31 +59,35 @@ const fillHandoff = (tmpl, repoPath) => tmpl.replace(/\{dir\}/g, shSingle(repoPa
|
|
|
59
59
|
// process; .symbiot/agent.pid catches another one (`symbiot push --open` while
|
|
60
60
|
// the app is up). Returns the job, null (no command set) or { busy, id, pid }.
|
|
61
61
|
// A preset that only opens a tab (Orca, editors) exits at once, so it holds
|
|
62
|
-
// the folder only that long.
|
|
62
|
+
// the folder only that long. A busy result's `auto` says whether the process
|
|
63
|
+
// that started that agent is still up to start one on the held tasks.
|
|
63
64
|
function runHandoff(repoPath) {
|
|
64
65
|
const tmpl = handoffCmd(); if (!tmpl || !repoPath) return null;
|
|
65
|
-
const busy = runningHandoff(repoPath); if (busy) return { busy: true, id: busy.id || "", pid: busy.pid };
|
|
66
|
+
const busy = runningHandoff(repoPath); if (busy) return { busy: true, id: busy.id || "", pid: busy.pid, auto: !!busy.auto };
|
|
66
67
|
releaseHeldTasks(repoPath); // held for an agent another process started, which has since exited
|
|
67
68
|
const lock = join(repoPath, ".symbiot", LOCK);
|
|
68
69
|
const e = track(repoPath.split("/").pop(), fillHandoff(tmpl, repoPath), repoPath, () => {
|
|
69
70
|
try { if (JSON.parse(readFileSync(lock, "utf8")).pid === e.pid) unlinkSync(lock); } catch {}
|
|
70
|
-
|
|
71
|
+
// tasks sent while it ran land now; start on them as that Send would have
|
|
72
|
+
if (releaseHeldTasks(repoPath) && /^\s*-\s*\[ \]/m.test(readSymbiot(repoPath, "TASKS.md"))) { const n = runHandoff(repoPath); if (n && !n.busy) n.fromHeld = true; }
|
|
71
73
|
});
|
|
72
74
|
if (!e) return null;
|
|
73
75
|
e.handoff = true;
|
|
74
|
-
if (e.pid) try { writeFileSync(lock, JSON.stringify({ pid: e.pid, id: e.id, startedAt: e.startedAt })); } catch {}
|
|
76
|
+
if (e.pid) try { writeFileSync(lock, JSON.stringify({ pid: e.pid, id: e.id, startedAt: e.startedAt, owner: process.pid })); } catch {}
|
|
75
77
|
return e;
|
|
76
78
|
}
|
|
77
79
|
const LOCK = "agent.pid";
|
|
78
80
|
const LOCK_MAX_AGE = 12 * 3600 * 1000; // older than this, the pid has probably been reused
|
|
79
81
|
const pidAlive = (pid) => { try { process.kill(pid, 0); return true; } catch (err) { return !!err && err.code === "EPERM"; } };
|
|
80
|
-
// The agent still running in this folder, if any.
|
|
82
|
+
// The agent still running in this folder, if any. `auto`: its exit will be
|
|
83
|
+
// seen (this process started it, or its owner — e.g. the app — is still up).
|
|
84
|
+
// `symbiot push --open` returns right after starting one, so it never is.
|
|
81
85
|
function runningHandoff(path) {
|
|
82
86
|
const e = HANDOFFS.find((x) => x.handoff && x.path === path && x.status === "running");
|
|
83
|
-
if (e) return { id: e.id, pid: e.pid, startedAt: e.startedAt };
|
|
87
|
+
if (e) return { id: e.id, pid: e.pid, startedAt: e.startedAt, auto: true };
|
|
84
88
|
try {
|
|
85
89
|
const l = JSON.parse(readSymbiot(path, LOCK));
|
|
86
|
-
if (l && Number.isInteger(l.pid) && Date.now() - l.startedAt < LOCK_MAX_AGE && pidAlive(l.pid)) return l;
|
|
90
|
+
if (l && Number.isInteger(l.pid) && Date.now() - l.startedAt < LOCK_MAX_AGE && pidAlive(l.pid)) return { ...l, auto: Number.isInteger(l.owner) && pidAlive(l.owner) };
|
|
87
91
|
} catch {}
|
|
88
92
|
return null;
|
|
89
93
|
}
|
|
@@ -277,7 +281,7 @@ function parseQuestions(md) {
|
|
|
277
281
|
if (/^#\s/.test(l)) continue;
|
|
278
282
|
const b = l.match(/^(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.+)$/);
|
|
279
283
|
if (sec === "s") { if (b) suggestions.push(clip(b[1])); continue; }
|
|
280
|
-
if (b) { if (cur) cur.options.push(clip(b[1])); else questions.push({ q: clip(b[1]), context: "", options: [] }); continue; }
|
|
284
|
+
if (b) { if (cur) cur.options.push(clip(b[1])); else if (/\?\s*$/.test(b[1])) questions.push({ q: clip(b[1]), context: "", options: [] }); continue; } // a bullet with no "### question" above it is a question only if it actually ends in "?" — otherwise it's preamble/prose (a file list, etc.)
|
|
281
285
|
if (cur) cur.context = clip((cur.context ? cur.context + " " : "") + l);
|
|
282
286
|
}
|
|
283
287
|
return { questions: questions.filter((x) => x.q).slice(0, 20).map((x) => ({ ...x, options: x.options.slice(0, 6) })), suggestions: suggestions.filter(Boolean).slice(0, 10) };
|
|
@@ -314,14 +318,19 @@ function answerQuestions(path, answers, opts = {}) {
|
|
|
314
318
|
if (opts.rerun) { const e = runHandoff(path); if (e && e.busy) out.note = "Answers saved. An agent is still running in that folder, so another wasn't started. Send them again once it finishes."; else if (e) out.rerun = e.id; else out.note = "Answers saved. Set an agent command in Settings to have the agent pick them up automatically."; }
|
|
315
319
|
return out;
|
|
316
320
|
}
|
|
321
|
+
// Open tasks in a held brief (TASKS.next.md), or null if nothing is held.
|
|
322
|
+
function heldTasks(path) {
|
|
323
|
+
const md = readSymbiot(path, HELD);
|
|
324
|
+
return md ? md.split("\n").filter((l) => /^\s*-\s*\[ \]/.test(l)).length : null;
|
|
325
|
+
}
|
|
317
326
|
// The Agents tab: every tracked job, its log tail, what it changed, and — on the
|
|
318
|
-
// newest job per folder — the questions/ideas it left.
|
|
327
|
+
// newest job per folder — the questions/ideas it left and any tasks held for it.
|
|
319
328
|
function agentsList() {
|
|
320
329
|
const seen = new Set();
|
|
321
330
|
return HANDOFFS.map((e) => {
|
|
322
331
|
let tail = ""; try { tail = readFileSync(e.log, "utf8").slice(-1200); } catch {}
|
|
323
332
|
const first = !seen.has(e.path); seen.add(e.path);
|
|
324
|
-
return { id: e.id, name: e.name, path: e.path, status: e.status, elapsed: (e.endedAt || Date.now()) - e.startedAt, exitCode: e.exitCode, tail, changed: agentChanges(e.path, e.startedAt), ask: first ? agentQuestions(e.path, e.name) : null };
|
|
333
|
+
return { id: e.id, name: e.name, path: e.path, status: e.status, elapsed: (e.endedAt || Date.now()) - e.startedAt, exitCode: e.exitCode, tail, changed: agentChanges(e.path, e.startedAt), ask: first ? agentQuestions(e.path, e.name) : null, held: first ? heldTasks(e.path) : null, fromHeld: !!e.fromHeld };
|
|
325
334
|
});
|
|
326
335
|
}
|
|
327
336
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.36.
|
|
3
|
+
"version": "0.36.1",
|
|
4
4
|
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice — Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|