@neotx/core 0.1.0-alpha.4 → 0.1.0-alpha.6

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/index.js CHANGED
@@ -1207,12 +1207,16 @@ function isProcessAlive(pid) {
1207
1207
  try {
1208
1208
  process.kill(pid, 0);
1209
1209
  return true;
1210
- } catch {
1210
+ } catch (error) {
1211
+ if (error instanceof Error && "code" in error && error.code === "EPERM") {
1212
+ return true;
1213
+ }
1211
1214
  return false;
1212
1215
  }
1213
1216
  }
1214
1217
 
1215
1218
  // src/orchestrator/run-store.ts
1219
+ var ORPHAN_GRACE_PERIOD_MS = 3e4;
1216
1220
  var RunStore = class {
1217
1221
  runsDir;
1218
1222
  createdDirs = /* @__PURE__ */ new Set();
@@ -1281,7 +1285,10 @@ var RunStore = class {
1281
1285
  const content = await readFile4(filePath, "utf-8");
1282
1286
  const run = JSON.parse(content);
1283
1287
  if (run.status !== "running") return null;
1288
+ if (run.pid && run.pid === process.pid) return null;
1284
1289
  if (run.pid && isProcessAlive(run.pid)) return null;
1290
+ const ageMs = Date.now() - new Date(run.createdAt).getTime();
1291
+ if (ageMs < ORPHAN_GRACE_PERIOD_MS) return null;
1285
1292
  run.status = "failed";
1286
1293
  run.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1287
1294
  await writeFile2(filePath, JSON.stringify(run, null, 2), "utf-8");
@@ -2599,6 +2606,7 @@ var Orchestrator = class extends NeoEventEmitter {
2599
2606
  workflow: input.workflow,
2600
2607
  repo: input.repo,
2601
2608
  prompt: input.prompt,
2609
+ pid: process.pid,
2602
2610
  status: "running",
2603
2611
  steps: {},
2604
2612
  createdAt: activeSession.startedAt,
@@ -2786,6 +2794,7 @@ var Orchestrator = class extends NeoEventEmitter {
2786
2794
  workflow: input.workflow,
2787
2795
  repo: input.repo,
2788
2796
  prompt: input.prompt,
2797
+ pid: process.pid,
2789
2798
  branch: taskResult.branch,
2790
2799
  status: taskResult.status === "success" ? "completed" : "failed",
2791
2800
  steps: taskResult.steps,
@@ -3605,13 +3614,16 @@ neo memory list --type fact
3605
3614
  neo log <type> "<message>" # visible in TUI only
3606
3615
  \`\`\``;
3607
3616
  var HEARTBEAT_RULES = `### Heartbeat lifecycle
3608
- 1. Process incoming events (messages, run completions)
3609
- 2. Follow up on pending work (CI checks, deferred dispatches) with \`neo runs\` or \`gh pr checks\`
3610
- 3. Make decisions and dispatch agents
3611
- 4. Update memory and log decisions
3612
- 5. Yield \u2014 each heartbeat should take seconds, not minutes
3617
+ 1. **Check work queue FIRST** \u2014 if you have pending tasks, work on the next one before looking for new work
3618
+ 2. Process incoming events (messages, run completions)
3619
+ 3. Follow up on pending work (CI checks, deferred dispatches) with \`neo runs\` or \`gh pr checks\`
3620
+ 4. Make decisions and dispatch agents
3621
+ 5. Update task status (\`neo memory update <id> --outcome in_progress|done|blocked\`) and log decisions
3622
+ 6. Yield \u2014 each heartbeat should take seconds, not minutes
3613
3623
 
3614
- After dispatching with \`neo run\`, note the runId in your focus and yield. Do NOT poll in a loop.
3624
+ **CRITICAL**: Your work queue IS your plan. Do not re-plan work that is already in the queue. When an planner agent produces tasks, create them with \`neo memory write --type task\`, then dispatch them one by one in subsequent heartbeats. Mark each task \`in_progress\` when dispatching, \`done\` when the run completes, \`blocked\` if stuck.
3625
+
3626
+ After dispatching with \`neo run\`, mark the task \`in_progress\`, note the runId in your focus, and yield. Do NOT poll in a loop.
3615
3627
  Completion events arrive at future heartbeats \u2014 react then.
3616
3628
  If you deferred work (e.g. "CI pending"), you MUST check it at the next heartbeat.`;
3617
3629
  var REPORTING_RULES = `### Reporting
@@ -3782,56 +3794,71 @@ var DONE_OUTCOMES = /* @__PURE__ */ new Set(["done", "abandoned"]);
3782
3794
  var MAX_TASKS = 15;
3783
3795
  function buildWorkQueueSection(memories) {
3784
3796
  const tasks = memories.filter((m) => m.type === "task" && !DONE_OUTCOMES.has(m.outcome ?? ""));
3785
- if (tasks.length === 0) return "";
3786
- const doneCount = memories.filter(
3787
- (m) => m.type === "task" && DONE_OUTCOMES.has(m.outcome ?? "")
3788
- ).length;
3789
- const groups = [];
3797
+ const doneCount = countDoneTasks(memories);
3798
+ if (tasks.length === 0) {
3799
+ if (doneCount > 0) {
3800
+ return `Work queue (0 remaining, ${doneCount} done) \u2014 all tasks complete. Pick up new work or wait for events.`;
3801
+ }
3802
+ return "";
3803
+ }
3804
+ const groups = groupTasksByInitiative(tasks);
3805
+ const lines = renderTaskGroups(groups);
3806
+ if (tasks.length > MAX_TASKS) {
3807
+ lines.push(` ... and ${tasks.length - MAX_TASKS} more pending`);
3808
+ }
3809
+ const header = `Work queue (${tasks.length} remaining, ${doneCount} done) \u2014 dispatch the next eligible task:`;
3810
+ return `${header}
3811
+ ${lines.join("\n")}`;
3812
+ }
3813
+ function countDoneTasks(memories) {
3814
+ return memories.filter((m) => m.type === "task" && DONE_OUTCOMES.has(m.outcome ?? "")).length;
3815
+ }
3816
+ function groupTasksByInitiative(tasks) {
3790
3817
  const initiativeMap = /* @__PURE__ */ new Map();
3791
3818
  const noInitiative = [];
3792
3819
  for (const task of tasks) {
3793
- const initiativeTag = task.tags.find((t) => t.startsWith("initiative:"));
3794
- if (initiativeTag) {
3795
- const initiative = initiativeTag.slice("initiative:".length);
3796
- const group = initiativeMap.get(initiative) ?? [];
3820
+ const tag = task.tags.find((t) => t.startsWith("initiative:"));
3821
+ if (tag) {
3822
+ const key = tag.slice("initiative:".length);
3823
+ const group = initiativeMap.get(key) ?? [];
3797
3824
  group.push(task);
3798
- initiativeMap.set(initiative, group);
3825
+ initiativeMap.set(key, group);
3799
3826
  } else {
3800
3827
  noInitiative.push(task);
3801
3828
  }
3802
3829
  }
3830
+ const groups = [];
3803
3831
  for (const [initiative, taskList] of initiativeMap) {
3804
3832
  groups.push({ initiative, tasks: taskList });
3805
3833
  }
3806
3834
  if (noInitiative.length > 0) {
3807
3835
  groups.push({ initiative: null, tasks: noInitiative });
3808
3836
  }
3837
+ return groups;
3838
+ }
3839
+ function renderTaskGroups(groups) {
3809
3840
  const lines = [];
3810
3841
  let rendered = 0;
3811
- const remaining = tasks.length;
3812
3842
  for (const group of groups) {
3813
3843
  if (rendered >= MAX_TASKS) break;
3814
- if (group.initiative && initiativeMap.size > 0) {
3844
+ if (group.initiative && groups.length > 1) {
3815
3845
  lines.push(` [${group.initiative}]`);
3816
3846
  }
3817
3847
  for (const task of group.tasks) {
3818
3848
  if (rendered >= MAX_TASKS) break;
3819
- const marker = formatTaskMarker(task.outcome);
3820
- const severity = task.severity ? `[${task.severity}] ` : "";
3821
- const scopeBasename = task.scope !== "global" ? ` (${getBasename(task.scope)})` : "";
3822
- const runRef = task.runId ? ` [run ${task.runId.slice(0, 8)}]` : "";
3823
- const categoryRef = task.category ? ` \u2192 ${task.category}` : "";
3824
- lines.push(` ${marker} ${severity}${task.content}${scopeBasename}${runRef}${categoryRef}`);
3849
+ lines.push(` ${formatTaskLine(task)}`);
3825
3850
  rendered++;
3826
3851
  }
3827
3852
  }
3828
- if (remaining > MAX_TASKS) {
3829
- const overflow = remaining - MAX_TASKS;
3830
- lines.push(` ... and ${overflow} more pending`);
3831
- }
3832
- const header = `Work queue (${remaining} remaining, ${doneCount} done):`;
3833
- return `${header}
3834
- ${lines.join("\n")}`;
3853
+ return lines;
3854
+ }
3855
+ function formatTaskLine(task) {
3856
+ const marker = formatTaskMarker(task.outcome);
3857
+ const severity = task.severity ? `[${task.severity}] ` : "";
3858
+ const scope = task.scope !== "global" ? ` (${getBasename(task.scope)})` : "";
3859
+ const run = task.runId ? ` [run ${task.runId.slice(0, 8)}]` : "";
3860
+ const cat = task.category ? ` \u2192 ${task.category}` : "";
3861
+ return `${marker} ${severity}${task.content}${scope}${run}${cat}`;
3835
3862
  }
3836
3863
  function formatTaskMarker(outcome) {
3837
3864
  switch (outcome) {
@@ -3911,16 +3938,16 @@ Heartbeat #${opts.heartbeatCount}
3911
3938
  ${COMMANDS}
3912
3939
  </commands>`);
3913
3940
  const contextParts = [];
3914
- contextParts.push(...buildContextSections(opts));
3941
+ const workQueue = buildWorkQueueSection(opts.memories);
3942
+ if (workQueue) {
3943
+ contextParts.push(workQueue);
3944
+ }
3915
3945
  if (opts.activeRuns.length > 0) {
3916
3946
  contextParts.push(`Active runs:
3917
3947
  ${opts.activeRuns.map((r) => `- ${r}`).join("\n")}`);
3918
3948
  }
3949
+ contextParts.push(...buildContextSections(opts));
3919
3950
  contextParts.push(buildMemorySection(opts.memories, opts.supervisorDir));
3920
- const workQueue = buildWorkQueueSection(opts.memories);
3921
- if (workQueue) {
3922
- contextParts.push(workQueue);
3923
- }
3924
3951
  const recentActions = buildRecentActionsSection(opts.recentActions);
3925
3952
  if (recentActions) {
3926
3953
  contextParts.push(recentActions);
@@ -3939,7 +3966,7 @@ ${contextParts.join("\n\n")}
3939
3966
  ${opts.customInstructions}`);
3940
3967
  }
3941
3968
  instructionParts.push(
3942
- "This is a standard heartbeat. Focus on processing events and dispatching work."
3969
+ "This is a standard heartbeat. Focus on processing events and dispatching work. If you have tasks in your work queue, dispatch the next eligible one."
3943
3970
  );
3944
3971
  sections.push(`<instructions>
3945
3972
  ${instructionParts.join("\n\n")}
@@ -3956,16 +3983,16 @@ Heartbeat #${opts.heartbeatCount} (CONSOLIDATION)
3956
3983
  ${COMMANDS}
3957
3984
  </commands>`);
3958
3985
  const contextParts = [];
3959
- contextParts.push(...buildContextSections(opts));
3986
+ const workQueueConsolidation = buildWorkQueueSection(opts.memories);
3987
+ if (workQueueConsolidation) {
3988
+ contextParts.push(workQueueConsolidation);
3989
+ }
3960
3990
  if (opts.activeRuns.length > 0) {
3961
3991
  contextParts.push(`Active runs:
3962
3992
  ${opts.activeRuns.map((r) => `- ${r}`).join("\n")}`);
3963
3993
  }
3994
+ contextParts.push(...buildContextSections(opts));
3964
3995
  contextParts.push(buildMemorySection(opts.memories, opts.supervisorDir));
3965
- const workQueueConsolidation = buildWorkQueueSection(opts.memories);
3966
- if (workQueueConsolidation) {
3967
- contextParts.push(workQueueConsolidation);
3968
- }
3969
3996
  const recentActions = buildRecentActionsSection(opts.recentActions);
3970
3997
  if (recentActions) {
3971
3998
  contextParts.push(recentActions);