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

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
@@ -1213,6 +1213,7 @@ function isProcessAlive(pid) {
1213
1213
  }
1214
1214
 
1215
1215
  // src/orchestrator/run-store.ts
1216
+ var ORPHAN_GRACE_PERIOD_MS = 3e4;
1216
1217
  var RunStore = class {
1217
1218
  runsDir;
1218
1219
  createdDirs = /* @__PURE__ */ new Set();
@@ -1282,6 +1283,8 @@ var RunStore = class {
1282
1283
  const run = JSON.parse(content);
1283
1284
  if (run.status !== "running") return null;
1284
1285
  if (run.pid && isProcessAlive(run.pid)) return null;
1286
+ const ageMs = Date.now() - new Date(run.createdAt).getTime();
1287
+ if (ageMs < ORPHAN_GRACE_PERIOD_MS) return null;
1285
1288
  run.status = "failed";
1286
1289
  run.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1287
1290
  await writeFile2(filePath, JSON.stringify(run, null, 2), "utf-8");
@@ -3605,13 +3608,16 @@ neo memory list --type fact
3605
3608
  neo log <type> "<message>" # visible in TUI only
3606
3609
  \`\`\``;
3607
3610
  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
3611
+ 1. **Check work queue FIRST** \u2014 if you have pending tasks, work on the next one before looking for new work
3612
+ 2. Process incoming events (messages, run completions)
3613
+ 3. Follow up on pending work (CI checks, deferred dispatches) with \`neo runs\` or \`gh pr checks\`
3614
+ 4. Make decisions and dispatch agents
3615
+ 5. Update task status (\`neo memory update <id> --outcome in_progress|done|blocked\`) and log decisions
3616
+ 6. Yield \u2014 each heartbeat should take seconds, not minutes
3613
3617
 
3614
- After dispatching with \`neo run\`, note the runId in your focus and yield. Do NOT poll in a loop.
3618
+ **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.
3619
+
3620
+ 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
3621
  Completion events arrive at future heartbeats \u2014 react then.
3616
3622
  If you deferred work (e.g. "CI pending"), you MUST check it at the next heartbeat.`;
3617
3623
  var REPORTING_RULES = `### Reporting
@@ -3782,56 +3788,71 @@ var DONE_OUTCOMES = /* @__PURE__ */ new Set(["done", "abandoned"]);
3782
3788
  var MAX_TASKS = 15;
3783
3789
  function buildWorkQueueSection(memories) {
3784
3790
  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 = [];
3791
+ const doneCount = countDoneTasks(memories);
3792
+ if (tasks.length === 0) {
3793
+ if (doneCount > 0) {
3794
+ return `Work queue (0 remaining, ${doneCount} done) \u2014 all tasks complete. Pick up new work or wait for events.`;
3795
+ }
3796
+ return "";
3797
+ }
3798
+ const groups = groupTasksByInitiative(tasks);
3799
+ const lines = renderTaskGroups(groups);
3800
+ if (tasks.length > MAX_TASKS) {
3801
+ lines.push(` ... and ${tasks.length - MAX_TASKS} more pending`);
3802
+ }
3803
+ const header = `Work queue (${tasks.length} remaining, ${doneCount} done) \u2014 dispatch the next eligible task:`;
3804
+ return `${header}
3805
+ ${lines.join("\n")}`;
3806
+ }
3807
+ function countDoneTasks(memories) {
3808
+ return memories.filter((m) => m.type === "task" && DONE_OUTCOMES.has(m.outcome ?? "")).length;
3809
+ }
3810
+ function groupTasksByInitiative(tasks) {
3790
3811
  const initiativeMap = /* @__PURE__ */ new Map();
3791
3812
  const noInitiative = [];
3792
3813
  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) ?? [];
3814
+ const tag = task.tags.find((t) => t.startsWith("initiative:"));
3815
+ if (tag) {
3816
+ const key = tag.slice("initiative:".length);
3817
+ const group = initiativeMap.get(key) ?? [];
3797
3818
  group.push(task);
3798
- initiativeMap.set(initiative, group);
3819
+ initiativeMap.set(key, group);
3799
3820
  } else {
3800
3821
  noInitiative.push(task);
3801
3822
  }
3802
3823
  }
3824
+ const groups = [];
3803
3825
  for (const [initiative, taskList] of initiativeMap) {
3804
3826
  groups.push({ initiative, tasks: taskList });
3805
3827
  }
3806
3828
  if (noInitiative.length > 0) {
3807
3829
  groups.push({ initiative: null, tasks: noInitiative });
3808
3830
  }
3831
+ return groups;
3832
+ }
3833
+ function renderTaskGroups(groups) {
3809
3834
  const lines = [];
3810
3835
  let rendered = 0;
3811
- const remaining = tasks.length;
3812
3836
  for (const group of groups) {
3813
3837
  if (rendered >= MAX_TASKS) break;
3814
- if (group.initiative && initiativeMap.size > 0) {
3838
+ if (group.initiative && groups.length > 1) {
3815
3839
  lines.push(` [${group.initiative}]`);
3816
3840
  }
3817
3841
  for (const task of group.tasks) {
3818
3842
  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}`);
3843
+ lines.push(` ${formatTaskLine(task)}`);
3825
3844
  rendered++;
3826
3845
  }
3827
3846
  }
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")}`;
3847
+ return lines;
3848
+ }
3849
+ function formatTaskLine(task) {
3850
+ const marker = formatTaskMarker(task.outcome);
3851
+ const severity = task.severity ? `[${task.severity}] ` : "";
3852
+ const scope = task.scope !== "global" ? ` (${getBasename(task.scope)})` : "";
3853
+ const run = task.runId ? ` [run ${task.runId.slice(0, 8)}]` : "";
3854
+ const cat = task.category ? ` \u2192 ${task.category}` : "";
3855
+ return `${marker} ${severity}${task.content}${scope}${run}${cat}`;
3835
3856
  }
3836
3857
  function formatTaskMarker(outcome) {
3837
3858
  switch (outcome) {
@@ -3911,16 +3932,16 @@ Heartbeat #${opts.heartbeatCount}
3911
3932
  ${COMMANDS}
3912
3933
  </commands>`);
3913
3934
  const contextParts = [];
3914
- contextParts.push(...buildContextSections(opts));
3935
+ const workQueue = buildWorkQueueSection(opts.memories);
3936
+ if (workQueue) {
3937
+ contextParts.push(workQueue);
3938
+ }
3915
3939
  if (opts.activeRuns.length > 0) {
3916
3940
  contextParts.push(`Active runs:
3917
3941
  ${opts.activeRuns.map((r) => `- ${r}`).join("\n")}`);
3918
3942
  }
3943
+ contextParts.push(...buildContextSections(opts));
3919
3944
  contextParts.push(buildMemorySection(opts.memories, opts.supervisorDir));
3920
- const workQueue = buildWorkQueueSection(opts.memories);
3921
- if (workQueue) {
3922
- contextParts.push(workQueue);
3923
- }
3924
3945
  const recentActions = buildRecentActionsSection(opts.recentActions);
3925
3946
  if (recentActions) {
3926
3947
  contextParts.push(recentActions);
@@ -3939,7 +3960,7 @@ ${contextParts.join("\n\n")}
3939
3960
  ${opts.customInstructions}`);
3940
3961
  }
3941
3962
  instructionParts.push(
3942
- "This is a standard heartbeat. Focus on processing events and dispatching work."
3963
+ "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
3964
  );
3944
3965
  sections.push(`<instructions>
3945
3966
  ${instructionParts.join("\n\n")}
@@ -3956,16 +3977,16 @@ Heartbeat #${opts.heartbeatCount} (CONSOLIDATION)
3956
3977
  ${COMMANDS}
3957
3978
  </commands>`);
3958
3979
  const contextParts = [];
3959
- contextParts.push(...buildContextSections(opts));
3980
+ const workQueueConsolidation = buildWorkQueueSection(opts.memories);
3981
+ if (workQueueConsolidation) {
3982
+ contextParts.push(workQueueConsolidation);
3983
+ }
3960
3984
  if (opts.activeRuns.length > 0) {
3961
3985
  contextParts.push(`Active runs:
3962
3986
  ${opts.activeRuns.map((r) => `- ${r}`).join("\n")}`);
3963
3987
  }
3988
+ contextParts.push(...buildContextSections(opts));
3964
3989
  contextParts.push(buildMemorySection(opts.memories, opts.supervisorDir));
3965
- const workQueueConsolidation = buildWorkQueueSection(opts.memories);
3966
- if (workQueueConsolidation) {
3967
- contextParts.push(workQueueConsolidation);
3968
- }
3969
3990
  const recentActions = buildRecentActionsSection(opts.recentActions);
3970
3991
  if (recentActions) {
3971
3992
  contextParts.push(recentActions);