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

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.d.ts CHANGED
@@ -835,6 +835,8 @@ interface OrchestratorOptions {
835
835
  journalDir?: string | undefined;
836
836
  builtInWorkflowDir?: string | undefined;
837
837
  customWorkflowDir?: string | undefined;
838
+ /** Skip orphan recovery on start — workers should set this to true to avoid false orphan detection on concurrent launches. */
839
+ skipOrphanRecovery?: boolean | undefined;
838
840
  }
839
841
  declare class Orchestrator extends NeoEventEmitter {
840
842
  private readonly config;
@@ -858,6 +860,7 @@ declare class Orchestrator extends NeoEventEmitter {
858
860
  private _costToday;
859
861
  private _startedAt;
860
862
  private _drainResolve;
863
+ private readonly skipOrphanRecovery;
861
864
  constructor(config: NeoConfig, options?: OrchestratorOptions);
862
865
  registerWorkflow(definition: WorkflowDefinition): void;
863
866
  registerAgent(agent: ResolvedAgent): void;
package/dist/index.js CHANGED
@@ -1207,7 +1207,10 @@ 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
  }
@@ -1282,6 +1285,7 @@ var RunStore = class {
1282
1285
  const content = await readFile4(filePath, "utf-8");
1283
1286
  const run = JSON.parse(content);
1284
1287
  if (run.status !== "running") return null;
1288
+ if (run.pid && run.pid === process.pid) return null;
1285
1289
  if (run.pid && isProcessAlive(run.pid)) return null;
1286
1290
  const ageMs = Date.now() - new Date(run.createdAt).getTime();
1287
1291
  if (ageMs < ORPHAN_GRACE_PERIOD_MS) return null;
@@ -2367,6 +2371,7 @@ var Orchestrator = class extends NeoEventEmitter {
2367
2371
  _costToday = 0;
2368
2372
  _startedAt = 0;
2369
2373
  _drainResolve = null;
2374
+ skipOrphanRecovery;
2370
2375
  constructor(config, options = {}) {
2371
2376
  super();
2372
2377
  this.config = config;
@@ -2374,6 +2379,7 @@ var Orchestrator = class extends NeoEventEmitter {
2374
2379
  this.journalDir = options.journalDir ?? getJournalsDir();
2375
2380
  this.builtInWorkflowDir = options.builtInWorkflowDir;
2376
2381
  this.customWorkflowDir = options.customWorkflowDir;
2382
+ this.skipOrphanRecovery = options.skipOrphanRecovery ?? false;
2377
2383
  for (const repo of config.repos) {
2378
2384
  const resolvedPath = path11.resolve(repo.path);
2379
2385
  const normalizedRepo = { ...repo, path: resolvedPath };
@@ -2491,7 +2497,9 @@ var Orchestrator = class extends NeoEventEmitter {
2491
2497
  this.registerWorkflow(workflow);
2492
2498
  }
2493
2499
  }
2494
- await this.recoverOrphanedRuns();
2500
+ if (!this.skipOrphanRecovery) {
2501
+ await this.recoverOrphanedRuns();
2502
+ }
2495
2503
  await mkdir6(this.config.sessions.dir, { recursive: true });
2496
2504
  }
2497
2505
  async shutdown() {
@@ -2602,6 +2610,7 @@ var Orchestrator = class extends NeoEventEmitter {
2602
2610
  workflow: input.workflow,
2603
2611
  repo: input.repo,
2604
2612
  prompt: input.prompt,
2613
+ pid: process.pid,
2605
2614
  status: "running",
2606
2615
  steps: {},
2607
2616
  createdAt: activeSession.startedAt,
@@ -2789,6 +2798,7 @@ var Orchestrator = class extends NeoEventEmitter {
2789
2798
  workflow: input.workflow,
2790
2799
  repo: input.repo,
2791
2800
  prompt: input.prompt,
2801
+ pid: process.pid,
2792
2802
  branch: taskResult.branch,
2793
2803
  status: taskResult.status === "success" ? "completed" : "failed",
2794
2804
  steps: taskResult.steps,
@@ -3567,7 +3577,16 @@ async function appendLogBuffer(dir, entry) {
3567
3577
  }
3568
3578
 
3569
3579
  // src/supervisor/prompt-builder.ts
3570
- var ROLE = `You are the neo autonomous supervisor. You orchestrate developer agents across repositories. You make decisions autonomously, act on events, and yield quickly.`;
3580
+ var ROLE = `You are the neo autonomous supervisor \u2014 a stateless dispatch controller.
3581
+
3582
+ You receive state (events, memory, work queue) and produce actions (tool calls).
3583
+
3584
+ <behavioral-contract>
3585
+ - Your ONLY visible output is \`neo log\` commands. The TUI shows these and nothing else.
3586
+ - Your text output is NEVER shown to anyone \u2014 every token of text is wasted cost.
3587
+ - Produce tool calls, not explanations. Do not narrate your reasoning.
3588
+ - You NEVER modify code \u2014 that is the agents' job.
3589
+ </behavioral-contract>`;
3571
3590
  var COMMANDS = `### Dispatching agents
3572
3591
  \`\`\`bash
3573
3592
  neo run <agent> --prompt "..." --repo <path> --branch <name> [--priority critical|high|medium|low] [--meta '<json>']
@@ -3598,6 +3617,8 @@ neo agents # list available agents
3598
3617
  neo memory write --type fact --scope /path "Stable fact about repo"
3599
3618
  neo memory write --type focus --expires 2h "Current working context"
3600
3619
  neo memory write --type procedure --scope /path "How to do X"
3620
+ neo memory write --type task --scope /path --severity high --category "neo runs <id>" "Task description"
3621
+ neo memory update <id> --outcome in_progress|done|blocked|abandoned
3601
3622
  neo memory forget <id>
3602
3623
  neo memory search "keyword"
3603
3624
  neo memory list --type fact
@@ -3607,107 +3628,109 @@ neo memory list --type fact
3607
3628
  \`\`\`bash
3608
3629
  neo log <type> "<message>" # visible in TUI only
3609
3630
  \`\`\``;
3631
+ var COMMANDS_COMPACT = `### Commands (reference)
3632
+ \`neo run <agent> --prompt "..." --repo <path> --branch <name> [--meta '<json>']\`
3633
+ \`neo runs [--short | <runId>]\` \xB7 \`neo cost --short\` \xB7 \`neo agents\`
3634
+ \`neo memory write|update|forget|search|list\` \xB7 \`neo log <type> "<msg>"\`
3635
+ Always read output after architect/refiner: \`neo runs <runId>\`.`;
3610
3636
  var HEARTBEAT_RULES = `### Heartbeat lifecycle
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
3617
3637
 
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.
3638
+ <decision-tree>
3639
+ 1. DEDUP FIRST \u2014 check focus for PROCESSED entries. Skip any runId already processed.
3640
+ 2. PENDING TASKS? \u2014 dispatch the next eligible task from work queue. Do not re-plan.
3641
+ 3. EVENTS? \u2014 process run completions, messages, webhooks. Parse agent JSON output.
3642
+ 4. FOLLOW-UPS? \u2014 check CI (\`gh pr checks\`), deferred dispatches (\`neo runs\`).
3643
+ 5. DISPATCH \u2014 route work to agents. Mark tasks \`in_progress\`, add ACTIVE to focus.
3644
+ 6. YIELD \u2014 log your decisions and yield. Do not poll. Completions arrive at future heartbeats.
3645
+ </decision-tree>
3619
3646
 
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.
3621
- Completion events arrive at future heartbeats \u2014 react then.
3622
- If you deferred work (e.g. "CI pending"), you MUST check it at the next heartbeat.`;
3647
+ <rules>
3648
+ - Work queue IS your plan. Never re-plan existing tasks.
3649
+ - Maximize parallelism: dispatch independent tasks in the same heartbeat.
3650
+ - After dispatch: update focus, yield immediately. Do NOT wait for results.
3651
+ - Deferred work (CI pending): MUST check at next heartbeat.
3652
+ </rules>`;
3623
3653
  var REPORTING_RULES = `### Reporting
3624
- \`neo log\` is your ONLY visible output \u2014 the TUI shows these and nothing else.
3625
- - \`neo log decision "..."\` \u2014 why you chose this route
3626
- - \`neo log action "..."\` \u2014 what you dispatched/did
3627
- - \`neo log discovery "..."\` \u2014 ephemeral observations
3628
- - 1-3 sentences per log. Pack maximum info: ticket, agent, branch, runId, cost, PR#. No markdown.
3629
3654
 
3630
- Your text output is NEVER shown to users.`;
3631
- var MEMORY_RULES = `### Memory \u2014 types and when to use each
3655
+ \`neo log\` is your ONLY visible output. Use telegraphic format.
3632
3656
 
3633
- | Type | What | When | TTL |
3634
- |------|------|------|-----|
3635
- | \`fact\` | Stable truth that affects decisions | After discovering something that changes how you dispatch or review | Permanent (decays if unused) |
3636
- | \`procedure\` | How-to recipe learned from failure | After the same issue occurs 3+ times | Permanent (decays if unused) |
3637
- | \`focus\` | Structured working context | After every dispatch, deferral, or priority change | Expires (always set --expires) |
3638
- | \`feedback\` | Recurring review pattern | After seeing the same reviewer complaint 3+ times | Permanent |
3639
- | \`episode\` | Run outcome | Auto-created on run completion \u2014 do NOT write manually | Permanent |
3640
- | \`task\` | Planned work item | After architect output or decomposition | Until done/abandoned |
3657
+ <log-format>
3658
+ neo log decision "<ticket> \u2192 <action> | <1-line reason>"
3659
+ neo log action "<agent> <repo>:<branch> run:<runId> | <context>"
3660
+ neo log discovery "<what> in <where>"
3661
+ </log-format>
3641
3662
 
3642
- #### What to store
3643
- - Architectural truths that affect future dispatch decisions (CI config, build requirements, tooling)
3644
- - Procedures learned from repeated failures (3+ occurrences of the same issue)
3645
- - Active working context in structured focus format (see below)
3663
+ <examples>
3664
+ <example type="good">
3665
+ neo log decision "YC-42 \u2192 developer | clear spec, complexity 3"
3666
+ neo log action "developer standards:feat/YC-42-auth run:5900a64a | task T1"
3667
+ neo log discovery "CI requires node 20 in api-service"
3668
+ </example>
3669
+ <example type="bad">
3670
+ neo log plan "Good! Now let me check the status and update things accordingly."
3671
+ neo log decision "Heartbeat #309: Idle cycle - no action required. All 4 repositories stable."
3672
+ neo log action "I've dispatched a developer agent to work on the authentication feature."
3673
+ </example>
3674
+ </examples>`;
3675
+ var MEMORY_RULES_CORE = `### Memory
3646
3676
 
3647
- #### What NOT to store
3648
- - File counts, line numbers, or structural details derivable from code (\`ls\` or \`cat package.json\` can answer it)
3649
- - Completed work details \u2014 once a PR is merged, forget the related facts unless they are reusable
3650
- - Agent output details already available via \`neo runs <id>\`
3651
- - Facts about repos where no work is currently planned
3677
+ <memory-types>
3678
+ | Type | Store when | TTL |
3679
+ |------|-----------|-----|
3680
+ | \`fact\` | Stable truth affecting dispatch decisions | Permanent (decays) |
3681
+ | \`procedure\` | Same failure 3+ times | Permanent |
3682
+ | \`focus\` | After every dispatch/deferral | --expires required |
3683
+ | \`task\` | Any planned work (tickets, decompositions, follow-ups) | Until done/abandoned |
3684
+ | \`feedback\` | Same review complaint 3+ times | Permanent |
3685
+ </memory-types>
3652
3686
 
3653
- #### Focus format (MANDATORY)
3654
- Focus entries MUST use this structured format \u2014 no free-form paragraphs:
3655
- \`\`\`
3656
- ACTIVE: <runId> <agent> "<task>" branch:<name>
3657
- PENDING: <taskId> "<description>" depends:<taskId>
3658
- WAITING: <what> since:HB<N>
3659
- PROCESSED: <runId> \u2192 <outcome> PR#<N>
3660
- \`\`\`
3687
+ <memory-rules>
3688
+ - Focus MUST use structured format: ACTIVE/PENDING/WAITING/PROCESSED lines only.
3689
+ - NEVER store: file counts, line numbers, completed work details, data available via \`neo runs <id>\`.
3690
+ - After PR merge: forget related facts unless they are reusable architectural truths.
3691
+ - Pattern escalation: same failure 3+ times \u2192 write a \`procedure\`.
3692
+ - Every memory that references external context MUST include a retrieval command (in \`--category\` for tasks, in content for facts/procedures). You are stateless \u2014 if you can't retrieve it later, don't store it.
3693
+ </memory-rules>
3661
3694
 
3662
- \`\`\`bash
3663
- # Focus: structured working context (always set --expires)
3664
- neo memory write --type focus --expires 2h "ACTIVE: 5900a64a developer 'T1: schema+store' branch:feat/task-queue
3665
- PENDING: T2 'CLI --outcome flag' depends:T1
3666
- PENDING: T3+T4 'prompt injection' depends:T1"
3695
+ <task-workflow>
3696
+ Tasks are your work queue. The work queue section above shows them with markers (\`\u25CB\` pending, \`[ACTIVE]\` in_progress, \`[BLOCKED]\` blocked).
3667
3697
 
3668
- # Facts: truths that change how you work (NOT trivia)
3669
- neo memory write --type fact --scope /path/to/repo "CI requires pnpm build before push \u2014 no auto-rebuild in pipeline"
3670
- neo memory write --type fact --scope /path/to/repo "Biome enforces complexity max 20 \u2014 extract helpers for large functions"
3698
+ Create a task for any planned work: incoming tickets, architect decompositions, refiner sub-tickets, follow-up actions, CI fixes.
3699
+ - \`--severity critical|high|medium|low\` \u2014 dispatch highest severity first
3700
+ - \`--tags "initiative:<name>"\` \u2014 groups related tasks (shown as [initiative] headers in queue)
3701
+ - \`--tags "depends:mem_<id>"\` \u2014 task cannot start until dependency is done
3702
+ - \`--category\` \u2014 **MANDATORY** \u2014 the command to retrieve context for this task (shown as \`\u2192 <command>\` in queue)
3671
3703
 
3672
- # Procedures: recipes learned from failure (write after 3+ occurrences)
3673
- neo memory write --type procedure --scope /path/to/repo "Before re-dispatching after orphan failure, check if PR already merged with gh pr view"
3674
- neo memory write --type procedure --scope /path/to/repo "Integration tests require DATABASE_URL env var \u2014 agent must set it"
3704
+ **Context retrieval rule**: every task and relevant memory MUST include a way for you to access its source context at a future heartbeat. You are stateless \u2014 without this, you lose the context.
3705
+ - Agent output: \`--category "neo runs <runId>"\`
3706
+ - Note/plan: \`--category "cat notes/plan-feature.md"\`
3707
+ - Notion ticket: \`--category "API-retrieve-a-page <notionPageId>"\`
3708
+ - Architect decomposition: \`--category "neo runs <architectRunId>"\` (contains milestones + tasks)
3675
3709
 
3676
- # Feedback: recurring reviewer complaints
3677
- neo memory write --type feedback --scope /path/to/repo --category input_validation "Always validate user input at controller boundaries"
3710
+ Lifecycle: create \u2192 \`neo memory update <id> --outcome in_progress\` (on dispatch) \u2192 \`done\` (on success) / \`blocked\` (on failure, will retry) / \`abandoned\` (terminal, won't retry)
3678
3711
 
3679
- # Tasks: work queue items from architect/refiner output
3680
- neo memory write --type task --scope /path/to/repo --severity high --category "neo runs abc123" "T1: Implement auth middleware"
3681
- neo memory write --type task --scope /path/to/repo --severity medium --tags "initiative:auth-v2,depends:mem_abc" --category "cat notes/plan-auth.md" "T2: Add JWT validation"
3712
+ Dispatch rule: pick the highest-severity task with no unmet dependencies. Dispatch independent tasks in parallel. Before dispatching, run the \`--category\` command to retrieve task context.
3713
+ </task-workflow>
3682
3714
 
3683
- # Update task status as you work
3684
- neo memory update <id> --outcome in_progress
3685
- neo memory update <id> --outcome done
3686
- neo memory update <id> --outcome blocked
3687
- neo memory update <id> --outcome abandoned
3715
+ <focus-format>
3716
+ ACTIVE: <runId> <agent> "<task>" branch:<name>
3717
+ PENDING: <taskId> "<description>" depends:<taskId>
3718
+ WAITING: <what> since:HB<N>
3719
+ PROCESSED: <runId> \u2192 <outcome> PR#<N>
3720
+ </focus-format>
3688
3721
 
3689
- # Forget stale entries
3722
+ **Notes** (\`notes/\`, via Bash): use for detailed multi-page plans that span multiple heartbeats. After creating a plan, write a focus summary with \`--category "cat notes/<file>"\`. Delete notes when done.`;
3723
+ var MEMORY_RULES_EXAMPLES = `<memory-commands>
3724
+ neo memory write --type focus --expires 2h "ACTIVE: 5900a64a developer 'T1' branch:feat/x"
3725
+ neo memory write --type fact --scope /repo "CI requires pnpm build \u2014 discovered in run abc123"
3726
+ neo memory write --type procedure --scope /repo "Check gh pr view before re-dispatch"
3727
+ neo memory write --type task --scope /repo --severity high --category "neo runs abc123" --tags "initiative:auth-v2,depends:mem_xyz" "T1: Auth middleware"
3728
+ neo memory update <id> --outcome in_progress|done|blocked|abandoned
3690
3729
  neo memory forget <id>
3691
-
3692
- # Search across all memories (semantic)
3693
- neo memory search "database setup"
3694
- \`\`\`
3695
-
3696
- #### Work queue workflow (Tasks)
3697
- After architect/refiner output, create tasks with \`neo memory write --type task\`:
3698
- - Include \`--category\` with the command to retrieve context (\`neo runs <id>\` or \`cat notes/<file>\`)
3699
- - Use \`--tags depends:mem_<id>\` for task dependencies
3700
- - Use \`--tags initiative:<name>\` to group tasks across repos
3701
- - Update status with \`neo memory update <id> --outcome in_progress|done|blocked|abandoned\`
3702
- - The queue is shown at every heartbeat \u2014 you will not lose track
3703
-
3704
- #### Pattern escalation
3705
- When you encounter the same failure or issue 3+ times, ALWAYS write a \`procedure\` memory so you handle it automatically next time. Do not re-discover the same problem repeatedly.
3706
-
3707
- #### Event deduplication
3708
- After processing a run completion, record the runId as PROCESSED in your focus. If the same runId appears in future events, skip it \u2014 do not re-analyze.
3709
-
3710
- **Notes** (\`notes/\`, via Bash): use for detailed multi-page plans, analysis, and checklists that span multiple heartbeats. After creating or reading a plan, write a focus summary: "Plan: <name> | Tasks: T1-T5 | Current: T1 | Next: T2 (depends T1) | Ref: cat notes/<file>". Delete notes when done.`;
3730
+ </memory-commands>`;
3731
+ function getCommandsSection(heartbeatCount) {
3732
+ return heartbeatCount <= 3 ? COMMANDS : COMMANDS_COMPACT;
3733
+ }
3711
3734
  function buildContextSections(opts) {
3712
3735
  const parts = [];
3713
3736
  if (opts.repos.length > 0) {
@@ -3868,7 +3891,7 @@ function getBasename(scopePath) {
3868
3891
  const parts = scopePath.split("/");
3869
3892
  return parts[parts.length - 1] || scopePath;
3870
3893
  }
3871
- var SIGNIFICANT_TYPES = /* @__PURE__ */ new Set(["decision", "action", "dispatch", "error", "plan"]);
3894
+ var SIGNIFICANT_TYPES = /* @__PURE__ */ new Set(["decision", "action", "dispatch", "error"]);
3872
3895
  function buildRecentActionsSection(entries) {
3873
3896
  const significant = entries.filter((e) => SIGNIFICANT_TYPES.has(e.type));
3874
3897
  if (significant.length === 0) return "";
@@ -3891,7 +3914,7 @@ function buildEventsSection(grouped) {
3891
3914
  const { messages, webhooks, runCompletions } = grouped;
3892
3915
  const totalEvents = messages.length + webhooks.length + runCompletions.length;
3893
3916
  if (totalEvents === 0) {
3894
- return "No new events. Idle heartbeat \u2014 check on active runs if any, or wait.";
3917
+ return "No new events.";
3895
3918
  }
3896
3919
  const parts = [];
3897
3920
  for (const msg of messages) {
@@ -3922,15 +3945,33 @@ ${JSON.stringify(event.data.payload ?? {}, null, 2)}
3922
3945
  return `Internal event: ${event.eventKind}`;
3923
3946
  }
3924
3947
  }
3948
+ function isIdleHeartbeat(opts) {
3949
+ const { messages, webhooks, runCompletions } = opts.grouped;
3950
+ const totalEvents = messages.length + webhooks.length + runCompletions.length;
3951
+ const hasWork = buildWorkQueueSection(opts.memories) !== "";
3952
+ return totalEvents === 0 && opts.activeRuns.length === 0 && !hasWork;
3953
+ }
3954
+ function buildIdlePrompt(opts) {
3955
+ return `<role>
3956
+ ${ROLE}
3957
+ Heartbeat #${opts.heartbeatCount}
3958
+ </role>
3959
+
3960
+ <context>
3961
+ No events. No active runs. No pending tasks.
3962
+ Budget: $${opts.budgetStatus.todayUsd.toFixed(2)} / $${opts.budgetStatus.capUsd.toFixed(2)} (${opts.budgetStatus.remainingPct.toFixed(0)}% remaining)
3963
+ </context>
3964
+
3965
+ <directive>
3966
+ Nothing to do. Run \`neo log discovery "idle"\` and yield. Do not produce any other output.
3967
+ </directive>`;
3968
+ }
3925
3969
  function buildStandardPrompt(opts) {
3926
3970
  const sections = [];
3927
3971
  sections.push(`<role>
3928
3972
  ${ROLE}
3929
3973
  Heartbeat #${opts.heartbeatCount}
3930
3974
  </role>`);
3931
- sections.push(`<commands>
3932
- ${COMMANDS}
3933
- </commands>`);
3934
3975
  const contextParts = [];
3935
3976
  const workQueue = buildWorkQueueSection(opts.memories);
3936
3977
  if (workQueue) {
@@ -3951,16 +3992,21 @@ ${buildEventsSection(opts.grouped)}`);
3951
3992
  sections.push(`<context>
3952
3993
  ${contextParts.join("\n\n")}
3953
3994
  </context>`);
3995
+ sections.push(`<reference>
3996
+ ${getCommandsSection(opts.heartbeatCount)}
3997
+ </reference>`);
3954
3998
  const instructionParts = [];
3955
3999
  instructionParts.push(HEARTBEAT_RULES);
3956
4000
  instructionParts.push(REPORTING_RULES);
3957
- instructionParts.push(MEMORY_RULES);
4001
+ instructionParts.push(MEMORY_RULES_CORE);
3958
4002
  if (opts.customInstructions) {
3959
4003
  instructionParts.push(`### Custom instructions
3960
4004
  ${opts.customInstructions}`);
3961
4005
  }
4006
+ const { messages, webhooks, runCompletions } = opts.grouped;
4007
+ const hasEvents = messages.length + webhooks.length + runCompletions.length > 0;
3962
4008
  instructionParts.push(
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."
4009
+ hasEvents ? "Process events, dispatch eligible work, yield. Each heartbeat costs ~$0.10 \u2014 be efficient." : "No events. If pending work exists, dispatch it. Otherwise yield immediately."
3964
4010
  );
3965
4011
  sections.push(`<instructions>
3966
4012
  ${instructionParts.join("\n\n")}
@@ -3973,9 +4019,6 @@ function buildConsolidationPrompt(opts) {
3973
4019
  ${ROLE}
3974
4020
  Heartbeat #${opts.heartbeatCount} (CONSOLIDATION)
3975
4021
  </role>`);
3976
- sections.push(`<commands>
3977
- ${COMMANDS}
3978
- </commands>`);
3979
4022
  const contextParts = [];
3980
4023
  const workQueueConsolidation = buildWorkQueueSection(opts.memories);
3981
4024
  if (workQueueConsolidation) {
@@ -3996,10 +4039,14 @@ ${buildEventsSection(opts.grouped)}`);
3996
4039
  sections.push(`<context>
3997
4040
  ${contextParts.join("\n\n")}
3998
4041
  </context>`);
4042
+ sections.push(`<reference>
4043
+ ${getCommandsSection(opts.heartbeatCount)}
4044
+ </reference>`);
3999
4045
  const instructionParts = [];
4000
4046
  instructionParts.push(HEARTBEAT_RULES);
4001
4047
  instructionParts.push(REPORTING_RULES);
4002
- instructionParts.push(MEMORY_RULES);
4048
+ instructionParts.push(MEMORY_RULES_CORE);
4049
+ instructionParts.push(MEMORY_RULES_EXAMPLES);
4003
4050
  if (opts.customInstructions) {
4004
4051
  instructionParts.push(`### Custom instructions
4005
4052
  ${opts.customInstructions}`);
@@ -4016,13 +4063,7 @@ If there IS active work, your job:
4016
4063
  2. **Update focus** \u2014 rewrite focus using the MANDATORY structured format (ACTIVE/PENDING/WAITING/PROCESSED). Remove resolved items. Add new context.
4017
4064
  3. **Pattern escalation** \u2014 if agents hit the same issue 3+ times (check recent actions), write a \`procedure\` to prevent recurrence.
4018
4065
  4. **Prune completed work** \u2014 if a PR is merged or an initiative is done, forget related facts that are no longer actionable. Keep only reusable architectural truths.
4019
-
4020
- \`\`\`bash
4021
- neo memory write --type procedure --scope /repo "Before re-dispatching after orphan, check gh pr view first"
4022
- neo memory write --type focus --expires 4h "ACTIVE: abc123 developer 'T3: prompt injection' branch:feat/task-queue
4023
- PROCESSED: 5900a64a \u2192 PR#70 APPROVED"
4024
- neo memory forget <stale-id>
4025
- \`\`\``
4066
+ 5. **Prune done tasks** \u2014 forget tasks with outcome \`done\` or \`abandoned\` older than 7 days.`
4026
4067
  );
4027
4068
  sections.push(`<instructions>
4028
4069
  ${instructionParts.join("\n\n")}
@@ -4035,9 +4076,6 @@ function buildCompactionPrompt(opts) {
4035
4076
  ${ROLE}
4036
4077
  Heartbeat #${opts.heartbeatCount} (COMPACTION)
4037
4078
  </role>`);
4038
- sections.push(`<commands>
4039
- ${COMMANDS}
4040
- </commands>`);
4041
4079
  const contextParts = [];
4042
4080
  contextParts.push(...buildContextSections(opts));
4043
4081
  contextParts.push(buildMemorySection(opts.memories, opts.supervisorDir));
@@ -4048,10 +4086,14 @@ ${COMMANDS}
4048
4086
  sections.push(`<context>
4049
4087
  ${contextParts.join("\n\n")}
4050
4088
  </context>`);
4089
+ sections.push(`<reference>
4090
+ ${getCommandsSection(opts.heartbeatCount)}
4091
+ </reference>`);
4051
4092
  const instructionParts = [];
4052
4093
  instructionParts.push(HEARTBEAT_RULES);
4053
4094
  instructionParts.push(REPORTING_RULES);
4054
- instructionParts.push(MEMORY_RULES);
4095
+ instructionParts.push(MEMORY_RULES_CORE);
4096
+ instructionParts.push(MEMORY_RULES_EXAMPLES);
4055
4097
  if (opts.customInstructions) {
4056
4098
  instructionParts.push(`### Custom instructions
4057
4099
  ${opts.customInstructions}`);
@@ -4064,8 +4106,9 @@ This is a COMPACTION heartbeat. Deep-clean your ENTIRE memory.
4064
4106
  3. **Remove trivial facts** \u2014 file counts, line numbers, structural details that \`ls\` or \`cat package.json\` can answer. These waste context.
4065
4107
  4. **Merge duplicates** \u2014 combine similar facts within the same scope into one.
4066
4108
  5. **Clean up focus** \u2014 forget resolved items, rewrite remaining in structured format.
4067
- 6. **Delete completed notes** from notes/ directory.
4068
- 7. **Stay under 15 facts per scope** \u2014 prioritize facts that affect dispatch decisions.
4109
+ 6. **Prune done tasks** \u2014 forget tasks with outcome \`done\` or \`abandoned\` older than 7 days.
4110
+ 7. **Delete completed notes** from notes/ directory.
4111
+ 8. **Stay under 15 facts per scope** \u2014 prioritize facts that affect dispatch decisions.
4069
4112
 
4070
4113
  Flag contradictions: if two facts contradict, keep the newer one.
4071
4114
 
@@ -4376,6 +4419,12 @@ var HeartbeatLoop = class {
4376
4419
  modeLabel: "consolidation"
4377
4420
  };
4378
4421
  }
4422
+ if (isIdleHeartbeat(sharedOpts)) {
4423
+ return {
4424
+ prompt: buildIdlePrompt(sharedOpts),
4425
+ modeLabel: "idle"
4426
+ };
4427
+ }
4379
4428
  return {
4380
4429
  prompt: buildStandardPrompt(sharedOpts),
4381
4430
  modeLabel: "standard"
@@ -4403,7 +4452,6 @@ var HeartbeatLoop = class {
4403
4452
  }
4404
4453
  const queryOptions = {
4405
4454
  cwd: homedir2(),
4406
- maxTurns: 15,
4407
4455
  allowedTools,
4408
4456
  permissionMode: "bypassPermissions",
4409
4457
  allowDangerouslySkipPermissions: true,