@neotx/core 0.1.0-alpha.19 → 0.1.0-alpha.20

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
@@ -149,6 +149,9 @@ declare const globalConfigSchema: z.ZodObject<{
149
149
  compactionIntervalMs: z.ZodDefault<z.ZodNumber>;
150
150
  eventTimeoutMs: z.ZodDefault<z.ZodNumber>;
151
151
  instructions: z.ZodOptional<z.ZodString>;
152
+ idleSkipMax: z.ZodDefault<z.ZodNumber>;
153
+ activeWorkSkipMax: z.ZodDefault<z.ZodNumber>;
154
+ autoDecide: z.ZodDefault<z.ZodBoolean>;
152
155
  }, z.core.$strip>>;
153
156
  memory: z.ZodDefault<z.ZodObject<{
154
157
  embeddings: z.ZodDefault<z.ZodBoolean>;
@@ -220,6 +223,9 @@ declare const neoConfigSchema: z.ZodObject<{
220
223
  compactionIntervalMs: z.ZodDefault<z.ZodNumber>;
221
224
  eventTimeoutMs: z.ZodDefault<z.ZodNumber>;
222
225
  instructions: z.ZodOptional<z.ZodString>;
226
+ idleSkipMax: z.ZodDefault<z.ZodNumber>;
227
+ activeWorkSkipMax: z.ZodDefault<z.ZodNumber>;
228
+ autoDecide: z.ZodDefault<z.ZodBoolean>;
223
229
  }, z.core.$strip>>;
224
230
  memory: z.ZodDefault<z.ZodObject<{
225
231
  embeddings: z.ZodDefault<z.ZodBoolean>;
@@ -1635,6 +1641,7 @@ declare class HeartbeatLoop {
1635
1641
  private checkBudgetExceeded;
1636
1642
  /**
1637
1643
  * Handle skip logic for idle and active-work scenarios.
1644
+ * Uses IdleDetector to make skip decisions based on context.
1638
1645
  */
1639
1646
  private handleSkipLogic;
1640
1647
  /**
package/dist/index.js CHANGED
@@ -518,7 +518,13 @@ var supervisorConfigSchema = z2.object({
518
518
  compactionIntervalMs: z2.number().default(36e5),
519
519
  /** Safety timeout for waitForWork (ms) */
520
520
  eventTimeoutMs: z2.number().default(3e5),
521
- instructions: z2.string().optional()
521
+ instructions: z2.string().optional(),
522
+ /** Max consecutive idle loop iterations before supervisor pauses polling */
523
+ idleSkipMax: z2.number().default(20),
524
+ /** Max consecutive active-work loop iterations before supervisor yields */
525
+ activeWorkSkipMax: z2.number().default(3),
526
+ /** When true, supervisor answers pending decisions autonomously instead of waiting for human input */
527
+ autoDecide: z2.boolean().default(false)
522
528
  }).default({
523
529
  port: 7777,
524
530
  heartbeatTimeoutMs: 3e5,
@@ -527,7 +533,10 @@ var supervisorConfigSchema = z2.object({
527
533
  dailyCapUsd: 50,
528
534
  consolidationIntervalMs: 3e5,
529
535
  compactionIntervalMs: 36e5,
530
- eventTimeoutMs: 3e5
536
+ eventTimeoutMs: 3e5,
537
+ idleSkipMax: 20,
538
+ activeWorkSkipMax: 3,
539
+ autoDecide: false
531
540
  });
532
541
  var globalConfigSchema = z2.object({
533
542
  repos: z2.array(repoConfigSchema).default([]),
@@ -598,7 +607,10 @@ var defaultConfig = {
598
607
  dailyCapUsd: 50,
599
608
  consolidationIntervalMs: 3e5,
600
609
  compactionIntervalMs: 36e5,
601
- eventTimeoutMs: 3e5
610
+ eventTimeoutMs: 3e5,
611
+ idleSkipMax: 20,
612
+ activeWorkSkipMax: 3,
613
+ autoDecide: false
602
614
  },
603
615
  memory: { embeddings: true }
604
616
  };
@@ -3811,6 +3823,57 @@ import { readdir as readdir4, readFile as readFile11, writeFile as writeFile6 }
3811
3823
  import { homedir as homedir4 } from "os";
3812
3824
  import path14 from "path";
3813
3825
 
3826
+ // src/supervisor/idle-detector.ts
3827
+ import { z as z6 } from "zod";
3828
+ var idleDetectorConfigSchema = z6.object({
3829
+ idleSkipMax: z6.number(),
3830
+ activeWorkSkipMax: z6.number()
3831
+ });
3832
+ var idleContextSchema = z6.object({
3833
+ eventCount: z6.number(),
3834
+ activeRuns: z6.number(),
3835
+ hasPendingConsolidation: z6.boolean(),
3836
+ hasExpiredDecisions: z6.boolean(),
3837
+ timeSinceLastHeartbeatMs: z6.number(),
3838
+ idleSkipCount: z6.number(),
3839
+ activeWorkSkipCount: z6.number()
3840
+ });
3841
+ var skipResultSchema = z6.object({
3842
+ shouldSkip: z6.boolean(),
3843
+ reason: z6.string()
3844
+ });
3845
+ var IdleDetector = class {
3846
+ config;
3847
+ constructor(config) {
3848
+ this.config = config;
3849
+ }
3850
+ /**
3851
+ * Evaluate whether the current heartbeat should be skipped.
3852
+ * @returns SkipResult with shouldSkip flag and reason
3853
+ */
3854
+ shouldSkip(context) {
3855
+ if (context.eventCount > 0) {
3856
+ return { shouldSkip: false, reason: "events pending" };
3857
+ }
3858
+ if (context.hasPendingConsolidation) {
3859
+ return { shouldSkip: false, reason: "pending consolidation" };
3860
+ }
3861
+ if (context.hasExpiredDecisions) {
3862
+ return { shouldSkip: false, reason: "expired decisions" };
3863
+ }
3864
+ if (context.activeRuns > 0) {
3865
+ if (context.activeWorkSkipCount >= this.config.activeWorkSkipMax) {
3866
+ return { shouldSkip: false, reason: "active work skip threshold exceeded" };
3867
+ }
3868
+ return { shouldSkip: true, reason: "active runs, within threshold" };
3869
+ }
3870
+ if (context.idleSkipCount >= this.config.idleSkipMax) {
3871
+ return { shouldSkip: false, reason: "idle skip threshold exceeded" };
3872
+ }
3873
+ return { shouldSkip: true, reason: "idle, within threshold" };
3874
+ }
3875
+ };
3876
+
3814
3877
  // src/supervisor/log-buffer.ts
3815
3878
  import { appendFile as appendFile6, readFile as readFile10, stat as stat2, writeFile as writeFile5 } from "fs/promises";
3816
3879
  import path13 from "path";
@@ -4140,7 +4203,7 @@ ${lines}
4140
4203
  }
4141
4204
  return "<focus>\n(empty \u2014 use neo memory write --type focus to set working context)\n</focus>";
4142
4205
  }
4143
- function buildPendingDecisionsSection(decisions) {
4206
+ function buildPendingDecisionsSection(decisions, autoDecide = false) {
4144
4207
  if (!decisions || decisions.length === 0) {
4145
4208
  return "";
4146
4209
  }
@@ -4159,13 +4222,22 @@ function buildPendingDecisionsSection(decisions) {
4159
4222
  lines.push(` Context: ${d.context}`);
4160
4223
  }
4161
4224
  }
4162
- return `Pending decisions (${decisions.length}):
4163
- ${lines.join("\n")}
4225
+ const instruction = autoDecide ? `You are in **autoDecide** mode \u2014 answer each pending decision yourself based on available context, project knowledge, and best engineering judgment.
4226
+
4227
+ \`\`\`bash
4228
+ neo decision answer <decision_id> <answer>
4229
+ \`\`\`
4230
+
4231
+ For each decision: analyze the options, consider the project context and risk, then answer decisively. Prefer safe, incremental choices when uncertain. Log your reasoning before answering.
4164
4232
 
4165
- To answer a decision, emit a \`decision:answer\` event:
4233
+ **Merge authority:** In autoDecide mode you MAY merge branches when the PR is ready (CI green, reviews approved). Use \`gh pr merge\` with the appropriate merge strategy.` : `To answer a decision, emit a \`decision:answer\` event:
4166
4234
  \`\`\`bash
4167
4235
  neo event emit decision:answer --data '{"id":"<decision_id>","answer":"<option_key>"}'
4168
4236
  \`\`\``;
4237
+ return `Pending decisions (${decisions.length}):
4238
+ ${lines.join("\n")}
4239
+
4240
+ ${instruction}`;
4169
4241
  }
4170
4242
  function buildAnsweredDecisionsSection(decisions) {
4171
4243
  if (!decisions || decisions.length === 0) {
@@ -4193,7 +4265,7 @@ ${opts.activeRuns.map((r) => `- ${r}`).join("\n")}`);
4193
4265
  if (recentActions) {
4194
4266
  parts.push(recentActions);
4195
4267
  }
4196
- const pendingDecisions = buildPendingDecisionsSection(opts.pendingDecisions);
4268
+ const pendingDecisions = buildPendingDecisionsSection(opts.pendingDecisions, opts.autoDecide);
4197
4269
  if (pendingDecisions) {
4198
4270
  parts.push(pendingDecisions);
4199
4271
  }
@@ -4519,7 +4591,28 @@ Nothing to do. Run \`neo log discovery "idle"\` and yield. Do not produce any ot
4519
4591
  }
4520
4592
  const repoList = opts.repos.map((r) => `- ${r.path} (branch: ${r.defaultBranch})`).join("\n");
4521
4593
  if (hasPendingDecisions) {
4522
- const pendingSection = buildPendingDecisionsSection(opts.pendingDecisions);
4594
+ const pendingSection = buildPendingDecisionsSection(opts.pendingDecisions, opts.autoDecide);
4595
+ if (opts.autoDecide) {
4596
+ return `${buildRoleSection(opts.heartbeatCount)}
4597
+
4598
+ <context>
4599
+ No events. No active runs. No pending tasks.
4600
+ ${budgetLine}
4601
+
4602
+ ${pendingSection}
4603
+
4604
+ Repositories:
4605
+ ${repoList}
4606
+ </context>
4607
+
4608
+ <reference>
4609
+ ${getCommandsSection(opts.heartbeatCount)}
4610
+ </reference>
4611
+
4612
+ <directive>
4613
+ Idle \u2014 but there are pending decisions to resolve. You are in **autoDecide** mode: answer each pending decision now using your best engineering judgment, then yield. You MAY merge branches when PRs are ready (CI green, reviews approved).
4614
+ </directive>`;
4615
+ }
4523
4616
  return `${buildRoleSection(opts.heartbeatCount)}
4524
4617
 
4525
4618
  <context>
@@ -4638,50 +4731,50 @@ neo memory forget <stale-id>
4638
4731
  }
4639
4732
 
4640
4733
  // src/supervisor/webhookEvents.ts
4641
- import { z as z6 } from "zod";
4642
- var supervisorStartedEventSchema = z6.object({
4643
- type: z6.literal("supervisor_started"),
4644
- supervisorId: z6.string(),
4645
- startedAt: z6.string().datetime()
4734
+ import { z as z7 } from "zod";
4735
+ var supervisorStartedEventSchema = z7.object({
4736
+ type: z7.literal("supervisor_started"),
4737
+ supervisorId: z7.string(),
4738
+ startedAt: z7.string().datetime()
4646
4739
  });
4647
- var heartbeatEventSchema = z6.object({
4648
- type: z6.literal("heartbeat"),
4649
- supervisorId: z6.string(),
4650
- heartbeatNumber: z6.number().int().min(0),
4651
- timestamp: z6.string().datetime(),
4652
- runsActive: z6.number().int().min(0),
4653
- budget: z6.object({
4654
- todayUsd: z6.number().min(0),
4655
- limitUsd: z6.number().min(0)
4740
+ var heartbeatEventSchema = z7.object({
4741
+ type: z7.literal("heartbeat"),
4742
+ supervisorId: z7.string(),
4743
+ heartbeatNumber: z7.number().int().min(0),
4744
+ timestamp: z7.string().datetime(),
4745
+ runsActive: z7.number().int().min(0),
4746
+ budget: z7.object({
4747
+ todayUsd: z7.number().min(0),
4748
+ limitUsd: z7.number().min(0)
4656
4749
  })
4657
4750
  });
4658
- var runDispatchedEventSchema = z6.object({
4659
- type: z6.literal("run_dispatched"),
4660
- supervisorId: z6.string(),
4661
- runId: z6.string(),
4662
- agent: z6.string(),
4663
- repo: z6.string(),
4664
- branch: z6.string(),
4665
- prompt: z6.string().max(500)
4751
+ var runDispatchedEventSchema = z7.object({
4752
+ type: z7.literal("run_dispatched"),
4753
+ supervisorId: z7.string(),
4754
+ runId: z7.string(),
4755
+ agent: z7.string(),
4756
+ repo: z7.string(),
4757
+ branch: z7.string(),
4758
+ prompt: z7.string().max(500)
4666
4759
  // truncated
4667
4760
  });
4668
- var runCompletedEventSchema = z6.object({
4669
- type: z6.literal("run_completed"),
4670
- supervisorId: z6.string(),
4671
- runId: z6.string(),
4672
- status: z6.enum(["completed", "failed", "cancelled"]),
4673
- output: z6.string().max(1e3).optional(),
4761
+ var runCompletedEventSchema = z7.object({
4762
+ type: z7.literal("run_completed"),
4763
+ supervisorId: z7.string(),
4764
+ runId: z7.string(),
4765
+ status: z7.enum(["completed", "failed", "cancelled"]),
4766
+ output: z7.string().max(1e3).optional(),
4674
4767
  // truncated
4675
- costUsd: z6.number().min(0),
4676
- durationMs: z6.number().int().min(0)
4768
+ costUsd: z7.number().min(0),
4769
+ durationMs: z7.number().int().min(0)
4677
4770
  });
4678
- var supervisorStoppedEventSchema = z6.object({
4679
- type: z6.literal("supervisor_stopped"),
4680
- supervisorId: z6.string(),
4681
- stoppedAt: z6.string().datetime(),
4682
- reason: z6.enum(["shutdown", "budget_exceeded", "error", "manual"])
4771
+ var supervisorStoppedEventSchema = z7.object({
4772
+ type: z7.literal("supervisor_stopped"),
4773
+ supervisorId: z7.string(),
4774
+ stoppedAt: z7.string().datetime(),
4775
+ reason: z7.enum(["shutdown", "budget_exceeded", "error", "manual"])
4683
4776
  });
4684
- var supervisorWebhookEventSchema = z6.discriminatedUnion("type", [
4777
+ var supervisorWebhookEventSchema = z7.discriminatedUnion("type", [
4685
4778
  supervisorStartedEventSchema,
4686
4779
  heartbeatEventSchema,
4687
4780
  runDispatchedEventSchema,
@@ -4690,8 +4783,6 @@ var supervisorWebhookEventSchema = z6.discriminatedUnion("type", [
4690
4783
  ]);
4691
4784
 
4692
4785
  // src/supervisor/heartbeat.ts
4693
- var DEFAULT_IDLE_SKIP_MAX = 20;
4694
- var DEFAULT_ACTIVE_WORK_SKIP_MAX = 3;
4695
4786
  var DEFAULT_CONSOLIDATION_INTERVAL = 5;
4696
4787
  function shouldConsolidate(heartbeatCount, lastConsolidationHeartbeat, consolidationInterval, hasPendingEntries) {
4697
4788
  const since = heartbeatCount - lastConsolidationHeartbeat;
@@ -4856,13 +4947,18 @@ var HeartbeatLoop = class {
4856
4947
  const activeRuns = await this.getActiveRuns();
4857
4948
  const decisionStore = this.getDecisionStore();
4858
4949
  await this.processDecisionAnswers(rawEvents, decisionStore);
4859
- await decisionStore.expire();
4860
- const _pendingDecisions = await decisionStore.pending();
4861
- void _pendingDecisions;
4950
+ const expiredDecisions = await decisionStore.expire();
4951
+ const hasExpiredDecisions = expiredDecisions.length > 0;
4952
+ const pendingDecisions = this.config.supervisor.autoDecide ? await decisionStore.pending() : [];
4953
+ const answeredDecisions = this.config.supervisor.autoDecide ? await decisionStore.answered(state?.lastHeartbeat) : [];
4954
+ const unconsolidatedEntries = await readUnconsolidated(this.supervisorDir);
4955
+ const hasPendingConsolidation = unconsolidatedEntries.length > 0;
4862
4956
  const skipResult = await this.handleSkipLogic({
4863
4957
  state,
4864
4958
  totalEventCount,
4865
- activeRuns
4959
+ activeRuns,
4960
+ hasPendingConsolidation,
4961
+ hasExpiredDecisions
4866
4962
  });
4867
4963
  if (skipResult.shouldSkip) return;
4868
4964
  if (skipResult.resetCounters) {
@@ -4877,6 +4973,8 @@ var HeartbeatLoop = class {
4877
4973
  isCompaction: modeResult.isCompaction,
4878
4974
  isConsolidation: modeResult.isConsolidation,
4879
4975
  activeRuns,
4976
+ pendingDecisions,
4977
+ answeredDecisions,
4880
4978
  lastHeartbeat: state?.lastHeartbeat,
4881
4979
  lastConsolidationTimestamp: modeResult.lastConsolidationTs
4882
4980
  });
@@ -4972,35 +5070,50 @@ var HeartbeatLoop = class {
4972
5070
  }
4973
5071
  /**
4974
5072
  * Handle skip logic for idle and active-work scenarios.
5073
+ * Uses IdleDetector to make skip decisions based on context.
4975
5074
  */
4976
5075
  async handleSkipLogic(opts) {
4977
- const { state, totalEventCount, activeRuns } = opts;
5076
+ const { state, totalEventCount, activeRuns, hasPendingConsolidation, hasExpiredDecisions } = opts;
4978
5077
  const idleSkipCount = state?.idleSkipCount ?? 0;
4979
5078
  const activeWorkSkipCount = state?.activeWorkSkipCount ?? 0;
4980
5079
  const hasActiveWork = activeRuns.length > 0;
4981
- if (totalEventCount === 0) {
5080
+ const lastHeartbeatMs = state?.lastHeartbeat ? new Date(state.lastHeartbeat).getTime() : Date.now();
5081
+ const timeSinceLastHeartbeatMs = Date.now() - lastHeartbeatMs;
5082
+ const context = {
5083
+ eventCount: totalEventCount,
5084
+ activeRuns: activeRuns.length,
5085
+ hasPendingConsolidation,
5086
+ hasExpiredDecisions,
5087
+ timeSinceLastHeartbeatMs,
5088
+ idleSkipCount,
5089
+ activeWorkSkipCount
5090
+ };
5091
+ const detector = new IdleDetector({
5092
+ idleSkipMax: this.config.supervisor.idleSkipMax,
5093
+ activeWorkSkipMax: this.config.supervisor.activeWorkSkipMax
5094
+ });
5095
+ const result = detector.shouldSkip(context);
5096
+ if (result.shouldSkip) {
4982
5097
  if (hasActiveWork) {
4983
- if (activeWorkSkipCount < DEFAULT_ACTIVE_WORK_SKIP_MAX) {
4984
- await this.updateState({
4985
- activeWorkSkipCount: activeWorkSkipCount + 1,
4986
- idleSkipCount: 0
4987
- });
4988
- await this.activityLog.log(
4989
- "heartbeat",
4990
- `Active-work skip #${activeWorkSkipCount + 1}/${DEFAULT_ACTIVE_WORK_SKIP_MAX} \u2014 ${activeRuns.length} runs active, no events`
4991
- );
4992
- return { shouldSkip: true, resetCounters: false };
4993
- }
5098
+ await this.updateState({
5099
+ activeWorkSkipCount: activeWorkSkipCount + 1,
5100
+ idleSkipCount: 0
5101
+ });
5102
+ await this.activityLog.log(
5103
+ "heartbeat",
5104
+ `Active-work skip #${activeWorkSkipCount + 1}/${this.config.supervisor.activeWorkSkipMax} \u2014 ${result.reason}`
5105
+ );
4994
5106
  } else {
4995
- if (idleSkipCount < DEFAULT_IDLE_SKIP_MAX) {
4996
- await this.updateState({
4997
- idleSkipCount: idleSkipCount + 1,
4998
- activeWorkSkipCount: 0
4999
- });
5000
- await this.activityLog.log("heartbeat", `Idle skip #${idleSkipCount + 1} \u2014 no events`);
5001
- return { shouldSkip: true, resetCounters: false };
5002
- }
5107
+ await this.updateState({
5108
+ idleSkipCount: idleSkipCount + 1,
5109
+ activeWorkSkipCount: 0
5110
+ });
5111
+ await this.activityLog.log(
5112
+ "heartbeat",
5113
+ `Idle skip #${idleSkipCount + 1}/${this.config.supervisor.idleSkipMax} \u2014 ${result.reason}`
5114
+ );
5003
5115
  }
5116
+ return { shouldSkip: true, resetCounters: false };
5004
5117
  }
5005
5118
  const needsReset = idleSkipCount > 0 || activeWorkSkipCount > 0;
5006
5119
  return { shouldSkip: false, resetCounters: needsReset };
@@ -5075,7 +5188,10 @@ var HeartbeatLoop = class {
5075
5188
  customInstructions: this.customInstructions,
5076
5189
  supervisorDir: this.supervisorDir,
5077
5190
  memories,
5078
- recentActions
5191
+ recentActions,
5192
+ pendingDecisions: opts.pendingDecisions,
5193
+ answeredDecisions: opts.answeredDecisions,
5194
+ autoDecide: this.config.supervisor.autoDecide
5079
5195
  };
5080
5196
  if (opts.isCompaction) {
5081
5197
  return {
@@ -5899,16 +6015,16 @@ import path17 from "path";
5899
6015
  import { existsSync as existsSync10 } from "fs";
5900
6016
  import { mkdir as mkdir8, readFile as readFile15, writeFile as writeFile9 } from "fs/promises";
5901
6017
  import path18 from "path";
5902
- import { z as z7 } from "zod";
5903
- var webhookEntrySchema = z7.object({
5904
- url: z7.string().url(),
5905
- events: z7.array(z7.string()).optional(),
5906
- secret: z7.string().optional(),
5907
- timeoutMs: z7.number().default(5e3),
5908
- createdAt: z7.string().default(() => (/* @__PURE__ */ new Date()).toISOString())
6018
+ import { z as z8 } from "zod";
6019
+ var webhookEntrySchema = z8.object({
6020
+ url: z8.string().url(),
6021
+ events: z8.array(z8.string()).optional(),
6022
+ secret: z8.string().optional(),
6023
+ timeoutMs: z8.number().default(5e3),
6024
+ createdAt: z8.string().default(() => (/* @__PURE__ */ new Date()).toISOString())
5909
6025
  });
5910
- var webhooksConfigSchema = z7.object({
5911
- webhooks: z7.array(webhookEntrySchema).default([])
6026
+ var webhooksConfigSchema = z8.object({
6027
+ webhooks: z8.array(webhookEntrySchema).default([])
5912
6028
  });
5913
6029
  function getWebhooksConfigPath() {
5914
6030
  return path18.join(getDataDir(), "webhooks.json");