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.
Files changed (2) hide show
  1. package/agents.mjs +19 -10
  2. 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
- releaseHeldTasks(repoPath);
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.0",
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": {