nestor-sh 2.7.1 → 2.9.0

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/nestor.mjs CHANGED
@@ -6567,18 +6567,51 @@ var init_store = __esm({
6567
6567
  }
6568
6568
  /**
6569
6569
  * Search missions by objective text (LIKE query).
6570
+ * Supports optional filters for type, status, date range.
6571
+ * Returns results sorted by relevance (title matches ranked higher).
6570
6572
  */
6571
- searchMissions(query, limit = 5) {
6572
- const pattern = `%${query}%`;
6573
- const rows = queryAll(
6574
- this.db,
6575
- `SELECT * FROM missions
6576
- WHERE objective LIKE ? OR title LIKE ?
6577
- ORDER BY created_at DESC
6578
- LIMIT ?`,
6579
- [pattern, pattern, limit]
6580
- );
6581
- return rows.map((row) => this.rowToMission(row));
6573
+ searchMissions(query, opts) {
6574
+ let sql = "SELECT * FROM missions WHERE 1=1";
6575
+ const params = [];
6576
+ if (query) {
6577
+ sql += " AND (title LIKE ? OR objective LIKE ? OR findings LIKE ?)";
6578
+ const q = `%${query}%`;
6579
+ params.push(q, q, q);
6580
+ }
6581
+ if (opts?.type) {
6582
+ sql += " AND type = ?";
6583
+ params.push(opts.type);
6584
+ }
6585
+ if (opts?.status) {
6586
+ sql += " AND status = ?";
6587
+ params.push(opts.status);
6588
+ }
6589
+ if (opts?.from) {
6590
+ sql += " AND created_at >= ?";
6591
+ params.push(new Date(opts.from).getTime());
6592
+ }
6593
+ if (opts?.to) {
6594
+ sql += " AND created_at <= ?";
6595
+ params.push(new Date(opts.to).getTime());
6596
+ }
6597
+ sql += " ORDER BY created_at DESC";
6598
+ sql += ` LIMIT ${opts?.limit || 20}`;
6599
+ const rows = queryAll(this.db, sql, params);
6600
+ const missions = rows.map((row) => this.rowToMission(row));
6601
+ if (query) {
6602
+ const lowerQ = query.toLowerCase();
6603
+ missions.sort((a, b) => {
6604
+ const aInTitle = a.title.toLowerCase().includes(lowerQ) ? 1 : 0;
6605
+ const bInTitle = b.title.toLowerCase().includes(lowerQ) ? 1 : 0;
6606
+ if (aInTitle !== bInTitle) return bInTitle - aInTitle;
6607
+ return b.createdAt - a.createdAt;
6608
+ });
6609
+ }
6610
+ return missions.map((m) => ({
6611
+ ...m,
6612
+ findingCount: Array.isArray(m.findings) ? m.findings.length : 0,
6613
+ qualityScore: m.evaluation && typeof m.evaluation === "object" && "overall" in m.evaluation ? m.evaluation.overall : null
6614
+ }));
6582
6615
  }
6583
6616
  /**
6584
6617
  * List all missions, optionally filtered by tenant.
@@ -6589,6 +6622,44 @@ var init_store = __esm({
6589
6622
  const rows = queryAll(this.db, sql, params);
6590
6623
  return rows.map((row) => this.rowToMission(row));
6591
6624
  }
6625
+ /**
6626
+ * Get aggregate mission statistics.
6627
+ */
6628
+ getMissionStats(tenantId) {
6629
+ const baseSql = tenantId ? "SELECT * FROM missions WHERE tenant_id = ?" : "SELECT * FROM missions";
6630
+ const params = tenantId ? [tenantId] : [];
6631
+ const rows = queryAll(this.db, baseSql, params);
6632
+ const missions = rows.map((row) => this.rowToMission(row));
6633
+ const byType = {};
6634
+ const byStatus = {};
6635
+ let totalCost = 0;
6636
+ let totalFindings = 0;
6637
+ let qualitySum = 0;
6638
+ let qualityCount = 0;
6639
+ for (const m of missions) {
6640
+ byType[m.type] = (byType[m.type] || 0) + 1;
6641
+ byStatus[m.status] = (byStatus[m.status] || 0) + 1;
6642
+ totalCost += m.spentUsd || 0;
6643
+ totalFindings += Array.isArray(m.findings) ? m.findings.length : 0;
6644
+ if (m.evaluation && typeof m.evaluation === "object" && "overall" in m.evaluation) {
6645
+ const overall = m.evaluation.overall;
6646
+ if (typeof overall === "number") {
6647
+ qualitySum += overall;
6648
+ qualityCount++;
6649
+ }
6650
+ }
6651
+ }
6652
+ const topCategories = Object.entries(byType).sort((a, b) => b[1] - a[1]).map(([cat]) => cat);
6653
+ return {
6654
+ total: missions.length,
6655
+ byType,
6656
+ byStatus,
6657
+ totalCost: Math.round(totalCost * 100) / 100,
6658
+ totalFindings,
6659
+ avgQuality: qualityCount > 0 ? Math.round(qualitySum / qualityCount * 100) / 100 : 0,
6660
+ topCategories
6661
+ };
6662
+ }
6592
6663
  /**
6593
6664
  * Search findings across all missions (LIKE query).
6594
6665
  */
@@ -11430,7 +11501,7 @@ var SERVER_VERSION, startTime;
11430
11501
  var init_health = __esm({
11431
11502
  "../server/src/routes/health.ts"() {
11432
11503
  "use strict";
11433
- SERVER_VERSION = "2.7.1";
11504
+ SERVER_VERSION = "2.9.0";
11434
11505
  startTime = Date.now();
11435
11506
  }
11436
11507
  });
@@ -12843,7 +12914,7 @@ var init_system = __esm({
12843
12914
  init_error_handler();
12844
12915
  init_broadcaster();
12845
12916
  init_approval_service();
12846
- SERVER_VERSION2 = "2.7.1";
12917
+ SERVER_VERSION2 = "2.9.0";
12847
12918
  startTime2 = Date.now();
12848
12919
  UpdateConfigSchema = z9.object({
12849
12920
  server: z9.object({
@@ -20273,7 +20344,7 @@ function isNativeAvailable() {
20273
20344
  return nativeModule !== null;
20274
20345
  }
20275
20346
  function getNativeVersion() {
20276
- return nativeModule ? "2.7.1" : null;
20347
+ return nativeModule ? "2.9.0" : null;
20277
20348
  }
20278
20349
  function validateSsrf(url, allowPrivate = false) {
20279
20350
  if (nativeModule) {
@@ -102139,8 +102210,8 @@ function findChromePath() {
102139
102210
  for (const p8 of paths) {
102140
102211
  if (p8) {
102141
102212
  try {
102142
- const { existsSync: existsSync26 } = __require("node:fs");
102143
- if (existsSync26(p8)) return p8;
102213
+ const { existsSync: existsSync27 } = __require("node:fs");
102214
+ if (existsSync27(p8)) return p8;
102144
102215
  } catch {
102145
102216
  }
102146
102217
  }
@@ -102155,8 +102226,8 @@ function findChromePath() {
102155
102226
  ];
102156
102227
  for (const p8 of paths) {
102157
102228
  try {
102158
- const { existsSync: existsSync26 } = __require("node:fs");
102159
- if (existsSync26(p8)) return p8;
102229
+ const { existsSync: existsSync27 } = __require("node:fs");
102230
+ if (existsSync27(p8)) return p8;
102160
102231
  } catch {
102161
102232
  }
102162
102233
  }
@@ -102171,8 +102242,8 @@ function findChromePath() {
102171
102242
  ];
102172
102243
  for (const p8 of linuxPaths) {
102173
102244
  try {
102174
- const { existsSync: existsSync26 } = __require("node:fs");
102175
- if (existsSync26(p8)) return p8;
102245
+ const { existsSync: existsSync27 } = __require("node:fs");
102246
+ if (existsSync27(p8)) return p8;
102176
102247
  } catch {
102177
102248
  }
102178
102249
  }
@@ -103436,6 +103507,114 @@ var init_meta_tool_factory = __esm({
103436
103507
  }
103437
103508
  });
103438
103509
 
103510
+ // ../agent/src/memory/layered-memory.ts
103511
+ var LayeredMemory, EpisodicLayer, SemanticLayer;
103512
+ var init_layered_memory = __esm({
103513
+ "../agent/src/memory/layered-memory.ts"() {
103514
+ "use strict";
103515
+ LayeredMemory = class {
103516
+ constructor(store) {
103517
+ this.store = store;
103518
+ this.layers = [
103519
+ new EpisodicLayer(store),
103520
+ new SemanticLayer(store)
103521
+ ];
103522
+ }
103523
+ layers = [];
103524
+ /** Query all layers and merge results by relevance */
103525
+ async query(input, limit = 10) {
103526
+ const allResults = [];
103527
+ for (const layer of this.layers) {
103528
+ try {
103529
+ const results = await layer.query(input, limit);
103530
+ allResults.push(...results);
103531
+ } catch {
103532
+ }
103533
+ }
103534
+ return allResults.sort((a, b) => b.relevance - a.relevance).slice(0, limit);
103535
+ }
103536
+ /** Get stats about each memory layer */
103537
+ stats() {
103538
+ return [
103539
+ { name: "Working", type: "working", count: 0 },
103540
+ // managed externally
103541
+ { name: "Episodic", type: "episodic", count: this.getEpisodicCount() },
103542
+ { name: "Semantic", type: "semantic", count: this.getSemanticCount() }
103543
+ ];
103544
+ }
103545
+ getEpisodicCount() {
103546
+ try {
103547
+ const missions = this.store.listMissions("default");
103548
+ return missions.length;
103549
+ } catch {
103550
+ return 0;
103551
+ }
103552
+ }
103553
+ getSemanticCount() {
103554
+ try {
103555
+ const stats = this.store.getKgStats();
103556
+ return stats.totalEntities + stats.totalFacts;
103557
+ } catch {
103558
+ return 0;
103559
+ }
103560
+ }
103561
+ };
103562
+ EpisodicLayer = class {
103563
+ constructor(store) {
103564
+ this.store = store;
103565
+ }
103566
+ name = "Episodic";
103567
+ type = "episodic";
103568
+ async query(input, limit = 5) {
103569
+ try {
103570
+ const missions = this.store.searchMissions(input, { limit });
103571
+ return missions.map((m) => ({
103572
+ content: `Mission "${m.title}" (${m.type}, ${m.status}): ${(m.objective ?? "").substring(0, 200)}. ${m.findingCount} findings, score: ${m.qualityScore ?? "N/A"}`,
103573
+ source: `mission:${m.id}`,
103574
+ layer: "episodic",
103575
+ relevance: 0.6,
103576
+ timestamp: m.createdAt
103577
+ }));
103578
+ } catch {
103579
+ return [];
103580
+ }
103581
+ }
103582
+ };
103583
+ SemanticLayer = class {
103584
+ constructor(store) {
103585
+ this.store = store;
103586
+ }
103587
+ name = "Semantic";
103588
+ type = "semantic";
103589
+ async query(input, limit = 5) {
103590
+ const results = [];
103591
+ try {
103592
+ const entities = this.store.searchKgEntities(input);
103593
+ for (const e of entities.slice(0, limit)) {
103594
+ results.push({
103595
+ content: `Entity: ${e.canonicalName} (${e.type})${e.aliases?.length ? ` \u2014 aliases: ${e.aliases.join(", ")}` : ""}`,
103596
+ source: `kg:entity:${e.id}`,
103597
+ layer: "semantic",
103598
+ relevance: 0.7
103599
+ });
103600
+ }
103601
+ const facts = this.store.getKgFacts(input.substring(0, 50));
103602
+ for (const f of facts.slice(0, limit)) {
103603
+ results.push({
103604
+ content: `Fact: ${f.predicate} \u2192 ${f.object.substring(0, 150)} (confidence: ${f.confidence})`,
103605
+ source: `kg:fact:${f.sourceMissionId ?? "unknown"}`,
103606
+ layer: "semantic",
103607
+ relevance: 0.8 * (f.confidence || 0.5)
103608
+ });
103609
+ }
103610
+ } catch {
103611
+ }
103612
+ return results;
103613
+ }
103614
+ };
103615
+ }
103616
+ });
103617
+
103439
103618
  // ../agent/src/tools/system-tools.ts
103440
103619
  import { z as z28 } from "zod";
103441
103620
  import { randomUUID as randomUUID23 } from "node:crypto";
@@ -103827,11 +104006,130 @@ ${summary}`, isError: false };
103827
104006
  }
103828
104007
  }
103829
104008
  });
104009
+ const searchMissionsSchema = z28.object({
104010
+ query: z28.string().describe("Search query (searches titles, objectives, findings)"),
104011
+ type: z28.string().optional().describe("Filter by mission type (osint, research, code, audit, creation, analysis)"),
104012
+ limit: z28.number().optional().describe("Max results (default: 5)")
104013
+ });
104014
+ registry.register({
104015
+ name: "nestor_search_missions",
104016
+ description: "Search past missions by topic, type, or content. Returns matching missions with their findings count and quality scores. Use this to find relevant past work before starting a new mission.",
104017
+ inputSchema: searchMissionsSchema,
104018
+ handler: async (input, _ctx) => {
104019
+ const args2 = input;
104020
+ try {
104021
+ const results = store.searchMissions(args2.query, {
104022
+ type: args2.type,
104023
+ limit: args2.limit || 5
104024
+ });
104025
+ if (results.length === 0) {
104026
+ return { output: `No missions found matching "${args2.query}".`, isError: false };
104027
+ }
104028
+ const summary = results.map((m) => {
104029
+ const quality = m.qualityScore !== null ? ` \u2014 quality: ${(m.qualityScore * 100).toFixed(0)}%` : "";
104030
+ return `\u2022 [${m.status}] ${m.title} (${m.type}) \u2014 ${m.findingCount} findings, $${m.spentUsd.toFixed(4)}${quality}
104031
+ ID: ${m.id}
104032
+ Objective: ${m.objective.substring(0, 200)}`;
104033
+ }).join("\n\n");
104034
+ return {
104035
+ output: `Found ${results.length} mission(s) matching "${args2.query}":
104036
+
104037
+ ${summary}`,
104038
+ isError: false
104039
+ };
104040
+ } catch (err) {
104041
+ const msg = err instanceof Error ? err.message : String(err);
104042
+ return { output: `Error searching missions: ${msg}`, isError: true };
104043
+ }
104044
+ }
104045
+ });
104046
+ registry.register({
104047
+ name: "nestor_get_mission_report",
104048
+ description: "Get the full report of a specific past mission by ID. Returns the markdown report, evaluation scores, and key findings.",
104049
+ inputSchema: missionIdSchema,
104050
+ handler: async (input, _ctx) => {
104051
+ const args2 = input;
104052
+ try {
104053
+ const mission = store.getMission(args2.missionId);
104054
+ if (!mission) {
104055
+ return { output: `Mission "${args2.missionId}" not found.`, isError: true };
104056
+ }
104057
+ let output = `# Mission: ${mission.title}
104058
+ `;
104059
+ output += `Type: ${mission.type} | Status: ${mission.status}
104060
+ `;
104061
+ output += `Objective: ${mission.objective}
104062
+ `;
104063
+ output += `Cost: $${mission.spentUsd.toFixed(4)} | Findings: ${Array.isArray(mission.findings) ? mission.findings.length : 0}
104064
+ `;
104065
+ if (mission.evaluation && typeof mission.evaluation === "object") {
104066
+ const eval_ = mission.evaluation;
104067
+ output += `
104068
+ ## Evaluation
104069
+ `;
104070
+ if (typeof eval_.overall === "number") output += `Overall: ${(eval_.overall * 100).toFixed(0)}%
104071
+ `;
104072
+ if (typeof eval_.completeness === "number") output += `Completeness: ${(eval_.completeness * 100).toFixed(0)}%
104073
+ `;
104074
+ if (typeof eval_.accuracy === "number") output += `Accuracy: ${(eval_.accuracy * 100).toFixed(0)}%
104075
+ `;
104076
+ if (typeof eval_.depth === "number") output += `Depth: ${(eval_.depth * 100).toFixed(0)}%
104077
+ `;
104078
+ if (typeof eval_.relevance === "number") output += `Relevance: ${(eval_.relevance * 100).toFixed(0)}%
104079
+ `;
104080
+ }
104081
+ if (mission.report) {
104082
+ output += `
104083
+ ## Report
104084
+ ${mission.report}`;
104085
+ } else {
104086
+ output += `
104087
+ ## Findings Summary
104088
+ `;
104089
+ const findings = Array.isArray(mission.findings) ? mission.findings : [];
104090
+ for (const f of findings.slice(0, 20)) {
104091
+ const finding = f;
104092
+ output += `- [${finding.type || "finding"}] ${finding.title || "Untitled"}: ${String(finding.content || "").substring(0, 300)}
104093
+ `;
104094
+ }
104095
+ if (findings.length > 20) {
104096
+ output += `
104097
+ ... and ${findings.length - 20} more findings.`;
104098
+ }
104099
+ }
104100
+ return { output, isError: false };
104101
+ } catch (err) {
104102
+ const msg = err instanceof Error ? err.message : String(err);
104103
+ return { output: `Error getting mission report: ${msg}`, isError: true };
104104
+ }
104105
+ }
104106
+ });
104107
+ registry.register({
104108
+ name: "nestor_query_memory",
104109
+ description: "Query all memory layers (episodic: past missions, semantic: knowledge graph) for relevant information. Returns merged results sorted by relevance.",
104110
+ inputSchema: queryMemorySchema,
104111
+ handler: async (input, _ctx) => {
104112
+ const args2 = input;
104113
+ try {
104114
+ const memory = new LayeredMemory(store);
104115
+ const results = await memory.query(String(args2.query), 10);
104116
+ const layerStats = memory.stats();
104117
+ return {
104118
+ output: JSON.stringify({ results, layers: layerStats }, null, 2),
104119
+ isError: false
104120
+ };
104121
+ } catch (err) {
104122
+ const msg = err instanceof Error ? err.message : String(err);
104123
+ return { output: `Memory query failed: ${msg}`, isError: true };
104124
+ }
104125
+ }
104126
+ });
103830
104127
  }
103831
- var emptySchema, createAgentSchema, runAgentSchema, deleteAgentSchema, createWorkflowSchema, searchMemorySchema, storeMemorySchema, createGuardrailSchema, createMissionSchema, missionIdSchema;
104128
+ var emptySchema, createAgentSchema, runAgentSchema, deleteAgentSchema, createWorkflowSchema, searchMemorySchema, storeMemorySchema, createGuardrailSchema, createMissionSchema, missionIdSchema, queryMemorySchema;
103832
104129
  var init_system_tools = __esm({
103833
104130
  "../agent/src/tools/system-tools.ts"() {
103834
104131
  "use strict";
104132
+ init_layered_memory();
103835
104133
  emptySchema = z28.object({}).passthrough();
103836
104134
  createAgentSchema = z28.object({
103837
104135
  name: z28.string().describe("Agent name"),
@@ -103879,6 +104177,9 @@ var init_system_tools = __esm({
103879
104177
  missionIdSchema = z28.object({
103880
104178
  missionId: z28.string().describe("The mission ID")
103881
104179
  });
104180
+ queryMemorySchema = z28.object({
104181
+ query: z28.string().describe("Natural language query to search across all memory layers (episodic: past missions, semantic: knowledge graph)")
104182
+ });
103882
104183
  }
103883
104184
  });
103884
104185
 
@@ -107896,6 +108197,7 @@ var init_memory = __esm({
107896
108197
  init_vector_store();
107897
108198
  init_persistent_vector_store();
107898
108199
  init_embedding_provider();
108200
+ init_layered_memory();
107899
108201
  AgentMemory = class {
107900
108202
  constructor(store, agentId) {
107901
108203
  this.store = store;
@@ -108365,6 +108667,217 @@ ${e.reasoning}${ctx}`;
108365
108667
  }
108366
108668
  });
108367
108669
 
108670
+ // ../agent/src/safety/context-rotator.ts
108671
+ var ContextRotator;
108672
+ var init_context_rotator = __esm({
108673
+ "../agent/src/safety/context-rotator.ts"() {
108674
+ "use strict";
108675
+ ContextRotator = class {
108676
+ cfg;
108677
+ messageCount = 0;
108678
+ estimatedTokens = 0;
108679
+ constructor(config2 = {}) {
108680
+ this.cfg = {
108681
+ maxMessages: config2.maxMessages ?? 50,
108682
+ maxTokensEstimate: config2.maxTokensEstimate ?? 1e5,
108683
+ charsPerToken: config2.charsPerToken ?? 4,
108684
+ recentToKeep: config2.recentToKeep ?? 10
108685
+ };
108686
+ }
108687
+ // ── Recording ───────────────────────────────────────────────────────
108688
+ /** Record a message added to context. */
108689
+ recordMessage(content) {
108690
+ this.messageCount++;
108691
+ this.estimatedTokens += Math.ceil(content.length / this.cfg.charsPerToken);
108692
+ }
108693
+ // ── Checking ────────────────────────────────────────────────────────
108694
+ /** Check if context should be rotated. */
108695
+ shouldRotate() {
108696
+ if (this.messageCount >= this.cfg.maxMessages) {
108697
+ return {
108698
+ rotate: true,
108699
+ reason: `${this.messageCount} messages (limit: ${this.cfg.maxMessages})`
108700
+ };
108701
+ }
108702
+ if (this.estimatedTokens >= this.cfg.maxTokensEstimate) {
108703
+ return {
108704
+ rotate: true,
108705
+ reason: `~${this.estimatedTokens} tokens (limit: ${this.cfg.maxTokensEstimate})`
108706
+ };
108707
+ }
108708
+ return { rotate: false };
108709
+ }
108710
+ // ── Rotation helpers ────────────────────────────────────────────────
108711
+ /**
108712
+ * Build a compact summary for context rotation.
108713
+ *
108714
+ * Strategy:
108715
+ * - Preserve the system message verbatim
108716
+ * - Summarize the middle messages into a compact block
108717
+ * - Keep the last N recent messages verbatim
108718
+ *
108719
+ * Returns the summary text to inject as a single assistant message
108720
+ * between the system prompt and the recent messages.
108721
+ */
108722
+ buildRotationSummary(messages) {
108723
+ const keepCount = this.cfg.recentToKeep;
108724
+ const system = messages.find((m) => m.role === "system");
108725
+ const nonSystem = messages.filter((m) => m.role !== "system");
108726
+ const recent = nonSystem.slice(-keepCount);
108727
+ const middle = nonSystem.slice(0, Math.max(0, nonSystem.length - keepCount));
108728
+ const parts = [];
108729
+ if (system) {
108730
+ parts.push("[System prompt preserved]");
108731
+ }
108732
+ if (middle.length > 0) {
108733
+ const toolCalls = middle.filter(
108734
+ (m) => m.content.includes("tool_use") || m.content.includes("function_call") || m.content.includes("tool_result")
108735
+ ).length;
108736
+ const findings = middle.filter(
108737
+ (m) => m.content.includes("finding") || m.content.includes("result") || m.content.includes("discovered")
108738
+ ).length;
108739
+ const errors = middle.filter(
108740
+ (m) => m.content.includes("error") || m.content.includes("failed")
108741
+ ).length;
108742
+ parts.push(
108743
+ `[${middle.length} earlier messages summarized: ${toolCalls} tool interactions, ${findings} results/findings, ${errors} errors]`
108744
+ );
108745
+ }
108746
+ parts.push(`[${recent.length} recent messages preserved below]`);
108747
+ return parts.join("\n");
108748
+ }
108749
+ // ── Lifecycle ───────────────────────────────────────────────────────
108750
+ /** Reset counters after rotation. */
108751
+ reset() {
108752
+ this.messageCount = 0;
108753
+ this.estimatedTokens = 0;
108754
+ }
108755
+ /** Get current stats (useful for logging/telemetry). */
108756
+ getStats() {
108757
+ return {
108758
+ messageCount: this.messageCount,
108759
+ estimatedTokens: this.estimatedTokens
108760
+ };
108761
+ }
108762
+ };
108763
+ }
108764
+ });
108765
+
108766
+ // ../agent/src/safety/stuck-detector.ts
108767
+ var StuckDetector;
108768
+ var init_stuck_detector = __esm({
108769
+ "../agent/src/safety/stuck-detector.ts"() {
108770
+ "use strict";
108771
+ StuckDetector = class {
108772
+ cfg;
108773
+ toolCallHistory = [];
108774
+ findingsCount = 0;
108775
+ iterationsWithoutProgress = 0;
108776
+ consecutiveErrors = 0;
108777
+ startTime = Date.now();
108778
+ constructor(config2 = {}) {
108779
+ this.cfg = {
108780
+ maxRepeatedCalls: config2.maxRepeatedCalls ?? 3,
108781
+ maxIterationsWithoutProgress: config2.maxIterationsWithoutProgress ?? 5,
108782
+ maxConsecutiveErrors: config2.maxConsecutiveErrors ?? 3,
108783
+ maxSubObjectiveTimeMs: config2.maxSubObjectiveTimeMs ?? 3e5
108784
+ };
108785
+ }
108786
+ // ── Recording ───────────────────────────────────────────────────────
108787
+ /** Record a tool call for pattern detection. */
108788
+ recordToolCall(name, args2) {
108789
+ this.toolCallHistory.push({
108790
+ name,
108791
+ args: JSON.stringify(args2),
108792
+ timestamp: Date.now()
108793
+ });
108794
+ }
108795
+ /** Record a tool result (success or error). */
108796
+ recordResult(isError) {
108797
+ if (isError) {
108798
+ this.consecutiveErrors++;
108799
+ } else {
108800
+ this.consecutiveErrors = 0;
108801
+ }
108802
+ }
108803
+ /** Update findings count — call after each iteration of the agent loop. */
108804
+ updateProgress(currentFindings) {
108805
+ if (currentFindings > this.findingsCount) {
108806
+ this.findingsCount = currentFindings;
108807
+ this.iterationsWithoutProgress = 0;
108808
+ } else {
108809
+ this.iterationsWithoutProgress++;
108810
+ }
108811
+ }
108812
+ // ── Checking ────────────────────────────────────────────────────────
108813
+ /** Check if the agent appears stuck. */
108814
+ check() {
108815
+ const elapsed = Date.now() - this.startTime;
108816
+ if (elapsed > this.cfg.maxSubObjectiveTimeMs) {
108817
+ return {
108818
+ isStuck: true,
108819
+ reason: "timeout",
108820
+ details: `Sub-objective exceeded ${this.cfg.maxSubObjectiveTimeMs}ms (elapsed: ${elapsed}ms)`,
108821
+ suggestedAction: "skip"
108822
+ };
108823
+ }
108824
+ const windowSize = this.cfg.maxRepeatedCalls;
108825
+ if (this.toolCallHistory.length >= windowSize) {
108826
+ const recent = this.toolCallHistory.slice(-windowSize);
108827
+ const first2 = recent[0];
108828
+ const allSame = recent.every(
108829
+ (c) => c.name === first2.name && c.args === first2.args
108830
+ );
108831
+ if (allSame) {
108832
+ return {
108833
+ isStuck: true,
108834
+ reason: "repeated_calls",
108835
+ details: `Tool "${first2.name}" called ${windowSize}x with identical args`,
108836
+ suggestedAction: "retry_different"
108837
+ };
108838
+ }
108839
+ }
108840
+ if (this.iterationsWithoutProgress >= this.cfg.maxIterationsWithoutProgress) {
108841
+ return {
108842
+ isStuck: true,
108843
+ reason: "no_progress",
108844
+ details: `${this.iterationsWithoutProgress} iterations without new findings`,
108845
+ suggestedAction: "escalate_model"
108846
+ };
108847
+ }
108848
+ if (this.consecutiveErrors >= this.cfg.maxConsecutiveErrors) {
108849
+ return {
108850
+ isStuck: true,
108851
+ reason: "error_loop",
108852
+ details: `${this.consecutiveErrors} consecutive tool errors`,
108853
+ suggestedAction: "retry_different"
108854
+ };
108855
+ }
108856
+ return { isStuck: false };
108857
+ }
108858
+ // ── Lifecycle ───────────────────────────────────────────────────────
108859
+ /** Reset all state for reuse (e.g., new sub-objective). */
108860
+ reset() {
108861
+ this.toolCallHistory = [];
108862
+ this.findingsCount = 0;
108863
+ this.iterationsWithoutProgress = 0;
108864
+ this.consecutiveErrors = 0;
108865
+ this.startTime = Date.now();
108866
+ }
108867
+ /** Get a diagnostic snapshot (useful for logging/telemetry). */
108868
+ getSnapshot() {
108869
+ return {
108870
+ totalCalls: this.toolCallHistory.length,
108871
+ consecutiveErrors: this.consecutiveErrors,
108872
+ iterationsWithoutProgress: this.iterationsWithoutProgress,
108873
+ findingsCount: this.findingsCount,
108874
+ elapsedMs: Date.now() - this.startTime
108875
+ };
108876
+ }
108877
+ };
108878
+ }
108879
+ });
108880
+
108368
108881
  // ../agent/src/runtime.ts
108369
108882
  function estimateCost(model, inputTokens, outputTokens) {
108370
108883
  let costs = COST_PER_1M_TOKENS[model];
@@ -108432,6 +108945,8 @@ var init_runtime = __esm({
108432
108945
  init_hot_swap();
108433
108946
  init_auto_downgrade();
108434
108947
  init_reason_logger();
108948
+ init_context_rotator();
108949
+ init_stuck_detector();
108435
108950
  COST_PER_1M_TOKENS = {
108436
108951
  "claude-sonnet-4-20250514": { input: 3, output: 15 },
108437
108952
  "claude-3-5-sonnet": { input: 3, output: 15 },
@@ -108466,6 +108981,10 @@ var init_runtime = __esm({
108466
108981
  autoDowngrade = null;
108467
108982
  /** Explainability: logs the agent's reasoning at each step. */
108468
108983
  reasonLogger = new ReasonLogger();
108984
+ /** Context rotator for fresh-context pattern (Ralph methodology). */
108985
+ contextRotator;
108986
+ /** Stuck detector for loop/stall detection. */
108987
+ stuckDetector;
108469
108988
  constructor(config2) {
108470
108989
  this.config = config2;
108471
108990
  this.hotSwap = new HotSwapAdapter(config2.adapter);
@@ -108550,6 +109069,8 @@ var init_runtime = __esm({
108550
109069
  workspaceContext,
108551
109070
  errorCorrections
108552
109071
  });
109072
+ this.contextRotator = new ContextRotator(config2.contextRotation);
109073
+ this.stuckDetector = new StuckDetector(config2.stuckDetection);
108553
109074
  }
108554
109075
  /**
108555
109076
  * Run the agent loop for a given task.
@@ -108577,6 +109098,9 @@ var init_runtime = __esm({
108577
109098
  let lastOutput = "";
108578
109099
  let exitReason = "completed";
108579
109100
  let error;
109101
+ let consecutiveStucks = 0;
109102
+ this.contextRotator.reset();
109103
+ this.stuckDetector.reset();
108580
109104
  if (this.config.dryRun) {
108581
109105
  this.dryRunInterceptor = new DryRunInterceptor();
108582
109106
  } else {
@@ -108901,8 +109425,51 @@ ${this.config.orgChartContext}` : this.config.orgChartContext;
108901
109425
  }
108902
109426
  ]);
108903
109427
  messages.push(...toolResultMessages);
109428
+ this.stuckDetector.recordToolCall(toolCall.name, toolCall.arguments);
109429
+ this.stuckDetector.recordResult(result2.isError ?? false);
109430
+ const stuckCheck = this.stuckDetector.check();
109431
+ if (stuckCheck.isStuck) {
109432
+ this.log.warn("Agent stuck detected", {
109433
+ reason: stuckCheck.reason,
109434
+ suggestion: stuckCheck.suggestedAction
109435
+ });
109436
+ messages.push({
109437
+ role: "user",
109438
+ content: `[System] You appear to be stuck (${stuckCheck.reason}). ${stuckCheck.suggestedAction === "retry_different" ? "Try a completely different approach." : stuckCheck.suggestedAction === "skip" ? "Skip this and move to the next task." : stuckCheck.suggestedAction === "escalate_model" ? "This task may require deeper analysis." : "Consider aborting this approach."}`
109439
+ });
109440
+ if (stuckCheck.reason === "timeout" || consecutiveStucks >= 2) {
109441
+ exitReason = "error";
109442
+ error = `Agent stuck: ${stuckCheck.reason} \u2014 ${stuckCheck.details ?? ""}`;
109443
+ break;
109444
+ }
109445
+ consecutiveStucks++;
109446
+ } else {
109447
+ consecutiveStucks = 0;
109448
+ }
108904
109449
  }
108905
109450
  if (exitReason === "aborted") break;
109451
+ if (exitReason === "error") break;
109452
+ this.contextRotator.recordMessage(response.content);
109453
+ const rotationCheck = this.contextRotator.shouldRotate();
109454
+ if (rotationCheck.rotate) {
109455
+ const summary = this.contextRotator.buildRotationSummary(messages);
109456
+ const systemMsg = messages[0];
109457
+ const recent = messages.slice(-3);
109458
+ messages.length = 0;
109459
+ messages.push(
109460
+ systemMsg,
109461
+ {
109462
+ role: "user",
109463
+ content: `[Context rotation \u2014 previous messages summarized]
109464
+ ${summary}
109465
+
109466
+ Continue from where you left off.`
109467
+ },
109468
+ ...recent
109469
+ );
109470
+ this.contextRotator.reset();
109471
+ this.log.info("Context rotated", { reason: rotationCheck.reason });
109472
+ }
108906
109473
  }
108907
109474
  if (iterations >= maxIterations && exitReason === "completed") {
108908
109475
  exitReason = "max_iterations";
@@ -109144,6 +109711,9 @@ ${this.config.orgChartContext}` : this.config.orgChartContext;
109144
109711
  let consecutiveFailures = 0;
109145
109712
  let lastFailedToolName = "";
109146
109713
  const MAX_CONSECUTIVE_FAILURES = 3;
109714
+ let consecutiveStucks = 0;
109715
+ this.contextRotator.reset();
109716
+ this.stuckDetector.reset();
109147
109717
  const messages = this.contextBuilder.buildMessages(
109148
109718
  task.prompt,
109149
109719
  this.config.initialMessages
@@ -109421,8 +109991,51 @@ ${this.config.orgChartContext}` : this.config.orgChartContext;
109421
109991
  }
109422
109992
  ]);
109423
109993
  messages.push(...toolResultMessages);
109994
+ this.stuckDetector.recordToolCall(toolCall.name, toolCall.arguments);
109995
+ this.stuckDetector.recordResult(result.isError ?? false);
109996
+ const stuckCheck = this.stuckDetector.check();
109997
+ if (stuckCheck.isStuck) {
109998
+ this.log.warn("Agent stuck detected (streaming)", {
109999
+ reason: stuckCheck.reason,
110000
+ suggestion: stuckCheck.suggestedAction
110001
+ });
110002
+ messages.push({
110003
+ role: "user",
110004
+ content: `[System] You appear to be stuck (${stuckCheck.reason}). ${stuckCheck.suggestedAction === "retry_different" ? "Try a completely different approach." : stuckCheck.suggestedAction === "skip" ? "Skip this and move to the next task." : stuckCheck.suggestedAction === "escalate_model" ? "This task may require deeper analysis." : "Consider aborting this approach."}`
110005
+ });
110006
+ if (stuckCheck.reason === "timeout" || consecutiveStucks >= 2) {
110007
+ exitReason = "error";
110008
+ error = `Agent stuck: ${stuckCheck.reason} \u2014 ${stuckCheck.details ?? ""}`;
110009
+ break;
110010
+ }
110011
+ consecutiveStucks++;
110012
+ } else {
110013
+ consecutiveStucks = 0;
110014
+ }
109424
110015
  }
109425
110016
  if (exitReason === "aborted") break;
110017
+ if (exitReason === "error") break;
110018
+ this.contextRotator.recordMessage(response.content);
110019
+ const rotationCheck = this.contextRotator.shouldRotate();
110020
+ if (rotationCheck.rotate) {
110021
+ const summary = this.contextRotator.buildRotationSummary(messages);
110022
+ const systemMsg = messages[0];
110023
+ const recent = messages.slice(-3);
110024
+ messages.length = 0;
110025
+ messages.push(
110026
+ systemMsg,
110027
+ {
110028
+ role: "user",
110029
+ content: `[Context rotation \u2014 previous messages summarized]
110030
+ ${summary}
110031
+
110032
+ Continue from where you left off.`
110033
+ },
110034
+ ...recent
110035
+ );
110036
+ this.contextRotator.reset();
110037
+ this.log.info("Context rotated (streaming)", { reason: rotationCheck.reason });
110038
+ }
109426
110039
  }
109427
110040
  if (iterations >= maxIterations && exitReason === "completed") {
109428
110041
  exitReason = "max_iterations";
@@ -114276,121 +114889,6 @@ var init_knowledge = __esm({
114276
114889
  }
114277
114890
  });
114278
114891
 
114279
- // ../agent/src/safety/stuck-detector.ts
114280
- var StuckDetector;
114281
- var init_stuck_detector = __esm({
114282
- "../agent/src/safety/stuck-detector.ts"() {
114283
- "use strict";
114284
- StuckDetector = class {
114285
- cfg;
114286
- toolCallHistory = [];
114287
- findingsCount = 0;
114288
- iterationsWithoutProgress = 0;
114289
- consecutiveErrors = 0;
114290
- startTime = Date.now();
114291
- constructor(config2 = {}) {
114292
- this.cfg = {
114293
- maxRepeatedCalls: config2.maxRepeatedCalls ?? 3,
114294
- maxIterationsWithoutProgress: config2.maxIterationsWithoutProgress ?? 5,
114295
- maxConsecutiveErrors: config2.maxConsecutiveErrors ?? 3,
114296
- maxSubObjectiveTimeMs: config2.maxSubObjectiveTimeMs ?? 3e5
114297
- };
114298
- }
114299
- // ── Recording ───────────────────────────────────────────────────────
114300
- /** Record a tool call for pattern detection. */
114301
- recordToolCall(name, args2) {
114302
- this.toolCallHistory.push({
114303
- name,
114304
- args: JSON.stringify(args2),
114305
- timestamp: Date.now()
114306
- });
114307
- }
114308
- /** Record a tool result (success or error). */
114309
- recordResult(isError) {
114310
- if (isError) {
114311
- this.consecutiveErrors++;
114312
- } else {
114313
- this.consecutiveErrors = 0;
114314
- }
114315
- }
114316
- /** Update findings count — call after each iteration of the agent loop. */
114317
- updateProgress(currentFindings) {
114318
- if (currentFindings > this.findingsCount) {
114319
- this.findingsCount = currentFindings;
114320
- this.iterationsWithoutProgress = 0;
114321
- } else {
114322
- this.iterationsWithoutProgress++;
114323
- }
114324
- }
114325
- // ── Checking ────────────────────────────────────────────────────────
114326
- /** Check if the agent appears stuck. */
114327
- check() {
114328
- const elapsed = Date.now() - this.startTime;
114329
- if (elapsed > this.cfg.maxSubObjectiveTimeMs) {
114330
- return {
114331
- isStuck: true,
114332
- reason: "timeout",
114333
- details: `Sub-objective exceeded ${this.cfg.maxSubObjectiveTimeMs}ms (elapsed: ${elapsed}ms)`,
114334
- suggestedAction: "skip"
114335
- };
114336
- }
114337
- const windowSize = this.cfg.maxRepeatedCalls;
114338
- if (this.toolCallHistory.length >= windowSize) {
114339
- const recent = this.toolCallHistory.slice(-windowSize);
114340
- const first2 = recent[0];
114341
- const allSame = recent.every(
114342
- (c) => c.name === first2.name && c.args === first2.args
114343
- );
114344
- if (allSame) {
114345
- return {
114346
- isStuck: true,
114347
- reason: "repeated_calls",
114348
- details: `Tool "${first2.name}" called ${windowSize}x with identical args`,
114349
- suggestedAction: "retry_different"
114350
- };
114351
- }
114352
- }
114353
- if (this.iterationsWithoutProgress >= this.cfg.maxIterationsWithoutProgress) {
114354
- return {
114355
- isStuck: true,
114356
- reason: "no_progress",
114357
- details: `${this.iterationsWithoutProgress} iterations without new findings`,
114358
- suggestedAction: "escalate_model"
114359
- };
114360
- }
114361
- if (this.consecutiveErrors >= this.cfg.maxConsecutiveErrors) {
114362
- return {
114363
- isStuck: true,
114364
- reason: "error_loop",
114365
- details: `${this.consecutiveErrors} consecutive tool errors`,
114366
- suggestedAction: "retry_different"
114367
- };
114368
- }
114369
- return { isStuck: false };
114370
- }
114371
- // ── Lifecycle ───────────────────────────────────────────────────────
114372
- /** Reset all state for reuse (e.g., new sub-objective). */
114373
- reset() {
114374
- this.toolCallHistory = [];
114375
- this.findingsCount = 0;
114376
- this.iterationsWithoutProgress = 0;
114377
- this.consecutiveErrors = 0;
114378
- this.startTime = Date.now();
114379
- }
114380
- /** Get a diagnostic snapshot (useful for logging/telemetry). */
114381
- getSnapshot() {
114382
- return {
114383
- totalCalls: this.toolCallHistory.length,
114384
- consecutiveErrors: this.consecutiveErrors,
114385
- iterationsWithoutProgress: this.iterationsWithoutProgress,
114386
- findingsCount: this.findingsCount,
114387
- elapsedMs: Date.now() - this.startTime
114388
- };
114389
- }
114390
- };
114391
- }
114392
- });
114393
-
114394
114892
  // ../agent/src/safety/circuit-breaker.ts
114395
114893
  var CircuitBreaker;
114396
114894
  var init_circuit_breaker = __esm({
@@ -114512,98 +115010,278 @@ var init_circuit_breaker = __esm({
114512
115010
  }
114513
115011
  });
114514
115012
 
114515
- // ../agent/src/safety/context-rotator.ts
114516
- var ContextRotator;
114517
- var init_context_rotator = __esm({
114518
- "../agent/src/safety/context-rotator.ts"() {
115013
+ // ../agent/src/safety/question-detector.ts
115014
+ var QuestionDetector;
115015
+ var init_question_detector = __esm({
115016
+ "../agent/src/safety/question-detector.ts"() {
114519
115017
  "use strict";
114520
- ContextRotator = class {
114521
- cfg;
114522
- messageCount = 0;
114523
- estimatedTokens = 0;
114524
- constructor(config2 = {}) {
114525
- this.cfg = {
114526
- maxMessages: config2.maxMessages ?? 50,
114527
- maxTokensEstimate: config2.maxTokensEstimate ?? 1e5,
114528
- charsPerToken: config2.charsPerToken ?? 4,
114529
- recentToKeep: config2.recentToKeep ?? 10
114530
- };
114531
- }
114532
- // ── Recording ───────────────────────────────────────────────────────
114533
- /** Record a message added to context. */
114534
- recordMessage(content) {
114535
- this.messageCount++;
114536
- this.estimatedTokens += Math.ceil(content.length / this.cfg.charsPerToken);
115018
+ QuestionDetector = class {
115019
+ questionCount = 0;
115020
+ maxQuestionsBeforeCorrection;
115021
+ constructor(opts) {
115022
+ this.maxQuestionsBeforeCorrection = opts?.maxQuestions ?? 2;
114537
115023
  }
114538
- // ── Checking ────────────────────────────────────────────────────────
114539
- /** Check if context should be rotated. */
114540
- shouldRotate() {
114541
- if (this.messageCount >= this.cfg.maxMessages) {
114542
- return {
114543
- rotate: true,
114544
- reason: `${this.messageCount} messages (limit: ${this.cfg.maxMessages})`
114545
- };
115024
+ /**
115025
+ * Analyze an assistant message for question patterns.
115026
+ * Returns detection result with optional correction prompt.
115027
+ */
115028
+ detect(assistantMessage) {
115029
+ const questions = this.extractQuestions(assistantMessage);
115030
+ if (questions.length === 0) {
115031
+ this.questionCount = 0;
115032
+ return { isQuestion: false, questions: [] };
114546
115033
  }
114547
- if (this.estimatedTokens >= this.cfg.maxTokensEstimate) {
115034
+ this.questionCount++;
115035
+ if (this.questionCount >= this.maxQuestionsBeforeCorrection) {
114548
115036
  return {
114549
- rotate: true,
114550
- reason: `~${this.estimatedTokens} tokens (limit: ${this.cfg.maxTokensEstimate})`
115037
+ isQuestion: true,
115038
+ questions,
115039
+ correctionPrompt: this.buildCorrectionPrompt(questions)
114551
115040
  };
114552
115041
  }
114553
- return { rotate: false };
115042
+ return { isQuestion: true, questions };
115043
+ }
115044
+ extractQuestions(text7) {
115045
+ const questions = [];
115046
+ const questionLines = text7.split("\n").filter((line) => {
115047
+ const trimmed = line.trim();
115048
+ return trimmed.endsWith("?") && trimmed.length > 10;
115049
+ });
115050
+ questions.push(...questionLines.map((q) => q.trim()));
115051
+ const indirectPatterns = [
115052
+ /(?:could you|would you|can you|do you want|should I|shall I|do you prefer|which (?:one|option)|please (?:confirm|specify|clarify|tell me|let me know))/gi,
115053
+ /(?:voulez-vous|souhaitez-vous|préférez-vous|dois-je|faut-il|quel(?:le)? (?:est|sont)|pouvez-vous (?:me dire|confirmer|préciser))/gi
115054
+ ];
115055
+ for (const pattern of indirectPatterns) {
115056
+ const matches = text7.match(pattern);
115057
+ if (matches) {
115058
+ questions.push(...matches.map((m) => m.trim()));
115059
+ }
115060
+ }
115061
+ return questions;
115062
+ }
115063
+ buildCorrectionPrompt(questions) {
115064
+ return `[System \u2014 Autonomous Mode Correction]
115065
+ You are in AUTONOMOUS mode. Do NOT ask questions \u2014 take action instead.
115066
+
115067
+ You asked: ${questions.slice(0, 3).map((q) => `"${q}"`).join(", ")}
115068
+
115069
+ Instead of asking:
115070
+ - Make a reasonable decision based on available information
115071
+ - If multiple options exist, choose the most common/safe one
115072
+ - If information is missing, use your tools to find it (web_search, file_read, etc.)
115073
+ - If truly blocked, document what you need and move on to the next task
115074
+
115075
+ DO NOT ask any more questions. Take action now.`;
115076
+ }
115077
+ /** Reset counter */
115078
+ reset() {
115079
+ this.questionCount = 0;
115080
+ }
115081
+ };
115082
+ }
115083
+ });
115084
+
115085
+ // ../agent/src/safety/completion-detector.ts
115086
+ var CompletionDetector;
115087
+ var init_completion_detector = __esm({
115088
+ "../agent/src/safety/completion-detector.ts"() {
115089
+ "use strict";
115090
+ CompletionDetector = class {
115091
+ noToolCallStreak = 0;
115092
+ minNoToolStreak;
115093
+ constructor(opts) {
115094
+ this.minNoToolStreak = opts?.minNoToolStreak ?? 2;
114554
115095
  }
114555
- // ── Rotation helpers ────────────────────────────────────────────────
114556
115096
  /**
114557
- * Build a compact summary for context rotation.
114558
- *
114559
- * Strategy:
114560
- * - Preserve the system message verbatim
114561
- * - Summarize the middle messages into a compact block
114562
- * - Keep the last N recent messages verbatim
114563
- *
114564
- * Returns the summary text to inject as a single assistant message
114565
- * between the system prompt and the recent messages.
115097
+ * Check if the agent has truly completed its task.
115098
+ * Requires both heuristic indicators and an explicit signal.
114566
115099
  */
114567
- buildRotationSummary(messages) {
114568
- const keepCount = this.cfg.recentToKeep;
114569
- const system = messages.find((m) => m.role === "system");
114570
- const nonSystem = messages.filter((m) => m.role !== "system");
114571
- const recent = nonSystem.slice(-keepCount);
114572
- const middle = nonSystem.slice(0, Math.max(0, nonSystem.length - keepCount));
114573
- const parts = [];
114574
- if (system) {
114575
- parts.push("[System prompt preserved]");
114576
- }
114577
- if (middle.length > 0) {
114578
- const toolCalls = middle.filter(
114579
- (m) => m.content.includes("tool_use") || m.content.includes("function_call") || m.content.includes("tool_result")
114580
- ).length;
114581
- const findings = middle.filter(
114582
- (m) => m.content.includes("finding") || m.content.includes("result") || m.content.includes("discovered")
114583
- ).length;
114584
- const errors = middle.filter(
114585
- (m) => m.content.includes("error") || m.content.includes("failed")
114586
- ).length;
114587
- parts.push(
114588
- `[${middle.length} earlier messages summarized: ${toolCalls} tool interactions, ${findings} results/findings, ${errors} errors]`
114589
- );
115100
+ check(assistantMessage, hadToolCalls) {
115101
+ if (hadToolCalls) {
115102
+ this.noToolCallStreak = 0;
115103
+ } else {
115104
+ this.noToolCallStreak++;
114590
115105
  }
114591
- parts.push(`[${recent.length} recent messages preserved below]`);
114592
- return parts.join("\n");
115106
+ const heuristicScore = this.computeHeuristic(assistantMessage, hadToolCalls);
115107
+ const hasExplicitSignal = this.hasExplicitCompletionSignal(assistantMessage);
115108
+ const isComplete = heuristicScore >= 0.7 && hasExplicitSignal;
115109
+ return {
115110
+ isComplete,
115111
+ heuristicScore,
115112
+ hasExplicitSignal,
115113
+ reason: isComplete ? "Both heuristic and explicit signals indicate completion" : !hasExplicitSignal ? "No explicit completion signal found" : `Heuristic score too low (${heuristicScore.toFixed(2)})`
115114
+ };
115115
+ }
115116
+ computeHeuristic(message, hadToolCalls) {
115117
+ let score = 0;
115118
+ if (!hadToolCalls) score += 0.3;
115119
+ if (this.noToolCallStreak >= this.minNoToolStreak) score += 0.3;
115120
+ const conclusionPatterns = [
115121
+ /(?:in summary|en résumé|to summarize|pour résumer|in conclusion|en conclusion)/i,
115122
+ /(?:here are the (?:results|findings)|voici les (?:résultats|conclusions))/i,
115123
+ /(?:the (?:task|mission|objective|work) is (?:complete|done|finished))/i,
115124
+ /(?:la (?:tâche|mission) est (?:terminée|complète|finie))/i,
115125
+ /(?:I have (?:completed|finished|done)|j'ai (?:terminé|fini|complété))/i
115126
+ ];
115127
+ const conclusionMatches = conclusionPatterns.filter((p8) => p8.test(message)).length;
115128
+ score += Math.min(conclusionMatches * 0.15, 0.4);
115129
+ return Math.min(score, 1);
115130
+ }
115131
+ hasExplicitCompletionSignal(message) {
115132
+ const explicitSignals = [
115133
+ /\[COMPLETE\]/i,
115134
+ /\[DONE\]/i,
115135
+ /\[TASK_COMPLETE\]/i,
115136
+ /\[MISSION_COMPLETE\]/i,
115137
+ /EXIT_SIGNAL:\s*COMPLETE/i,
115138
+ // JSON output with findings (structured output = done)
115139
+ /^\s*\[[\s\S]*"type"[\s\S]*"title"[\s\S]*"confidence"[\s\S]*\]\s*$/m,
115140
+ // Markdown report header (report = done)
115141
+ /^#\s+.*(?:Report|Rapport|Results|Résultats)/m
115142
+ ];
115143
+ return explicitSignals.some((p8) => p8.test(message));
114593
115144
  }
114594
- // ── Lifecycle ───────────────────────────────────────────────────────
114595
- /** Reset counters after rotation. */
115145
+ /** Reset state */
114596
115146
  reset() {
114597
- this.messageCount = 0;
114598
- this.estimatedTokens = 0;
115147
+ this.noToolCallStreak = 0;
114599
115148
  }
114600
- /** Get current stats (useful for logging/telemetry). */
114601
- getStats() {
115149
+ };
115150
+ }
115151
+ });
115152
+
115153
+ // ../agent/src/safety/handoff-generator.ts
115154
+ var HandoffGenerator;
115155
+ var init_handoff_generator = __esm({
115156
+ "../agent/src/safety/handoff-generator.ts"() {
115157
+ "use strict";
115158
+ HandoffGenerator = class {
115159
+ /**
115160
+ * Generate a handoff document from the agent's message history.
115161
+ * This is a heuristic extraction — no LLM call needed.
115162
+ */
115163
+ generate(opts) {
115164
+ const { objective, messages, toolCalls, exitReason, costUsd } = opts;
115165
+ const accomplished = this.extractAccomplished(messages);
115166
+ const remaining = this.extractRemaining(messages, exitReason);
115167
+ const currentState = this.buildCurrentState(toolCalls);
115168
+ const decisions = this.extractDecisions(messages);
115169
+ const errors = toolCalls.filter((tc) => tc.isError).map((tc) => ({ error: `${tc.name}: ${tc.result.substring(0, 200)}`, resolution: "Attempted" }));
115170
+ const toolUsage = {};
115171
+ for (const tc of toolCalls) {
115172
+ toolUsage[tc.name] = (toolUsage[tc.name] || 0) + 1;
115173
+ }
115174
+ const nextSteps = this.suggestNextSteps(remaining, errors, exitReason);
114602
115175
  return {
114603
- messageCount: this.messageCount,
114604
- estimatedTokens: this.estimatedTokens
115176
+ objective,
115177
+ accomplished,
115178
+ remaining,
115179
+ currentState,
115180
+ decisions,
115181
+ errors,
115182
+ nextSteps,
115183
+ toolUsage,
115184
+ costUsd,
115185
+ timestamp: Date.now()
114605
115186
  };
114606
115187
  }
115188
+ /** Convert handoff to Markdown for storage/display */
115189
+ toMarkdown(handoff) {
115190
+ const sections = [
115191
+ `# Session Handoff`,
115192
+ ``,
115193
+ `**Objective:** ${handoff.objective}`,
115194
+ `**Cost:** $${handoff.costUsd.toFixed(4)}`,
115195
+ `**Date:** ${new Date(handoff.timestamp).toISOString()}`,
115196
+ ``,
115197
+ `## Accomplished`,
115198
+ ...handoff.accomplished.map((a) => `- ${a}`),
115199
+ ``,
115200
+ `## Remaining`,
115201
+ ...handoff.remaining.map((r) => `- ${r}`),
115202
+ ``,
115203
+ `## Current State`,
115204
+ handoff.currentState,
115205
+ ``,
115206
+ `## Decisions`,
115207
+ ...handoff.decisions.map((d) => `- **${d.decision}**: ${d.reason}`),
115208
+ ``,
115209
+ `## Errors (${handoff.errors.length})`,
115210
+ ...handoff.errors.map((e) => `- ${e.error}`),
115211
+ ``,
115212
+ `## Next Steps`,
115213
+ ...handoff.nextSteps.map((s) => `- ${s}`),
115214
+ ``,
115215
+ `## Tool Usage`,
115216
+ ...Object.entries(handoff.toolUsage).map(([name, count]) => `- ${name}: ${count}x`)
115217
+ ];
115218
+ return sections.join("\n");
115219
+ }
115220
+ extractAccomplished(messages) {
115221
+ const accomplished = [];
115222
+ for (const msg of messages) {
115223
+ if (msg.role !== "assistant") continue;
115224
+ const lines = msg.content.split("\n");
115225
+ for (const line of lines) {
115226
+ if (/(?:✅|done|completed|finished|created|wrote|found|discovered|built|implemented)/i.test(line) && line.length > 10 && line.length < 200) {
115227
+ accomplished.push(line.replace(/^[-*•]\s*/, "").trim());
115228
+ }
115229
+ }
115230
+ }
115231
+ return accomplished.slice(0, 10);
115232
+ }
115233
+ extractRemaining(messages, exitReason) {
115234
+ const remaining = [];
115235
+ if (exitReason === "max_iterations") remaining.push("Max iterations reached \u2014 work may be incomplete");
115236
+ if (exitReason === "budget_exceeded") remaining.push("Budget exhausted \u2014 work may be incomplete");
115237
+ if (exitReason === "aborted") remaining.push("Execution was aborted");
115238
+ const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
115239
+ if (lastAssistant) {
115240
+ const todoPattern = /(?:todo|remaining|still need|next|à faire|reste)/i;
115241
+ const lines = lastAssistant.content.split("\n");
115242
+ for (const line of lines) {
115243
+ if (todoPattern.test(line) && line.length > 10 && line.length < 200) {
115244
+ remaining.push(line.replace(/^[-*•]\s*/, "").trim());
115245
+ }
115246
+ }
115247
+ }
115248
+ return remaining.slice(0, 10);
115249
+ }
115250
+ buildCurrentState(toolCalls) {
115251
+ const filesWritten = toolCalls.filter((tc) => tc.name === "file_write").map((tc) => String(tc.args.path || "")).filter(Boolean);
115252
+ const commandsRun = toolCalls.filter((tc) => tc.name === "shell_exec").map((tc) => String(tc.args.command || "").substring(0, 100)).filter(Boolean);
115253
+ let state = "";
115254
+ if (filesWritten.length > 0) state += `Files modified: ${filesWritten.join(", ")}
115255
+ `;
115256
+ if (commandsRun.length > 0) state += `Commands run: ${commandsRun.slice(-5).join("; ")}`;
115257
+ return state || "No filesystem changes detected.";
115258
+ }
115259
+ extractDecisions(messages) {
115260
+ const decisions = [];
115261
+ for (const msg of messages) {
115262
+ if (msg.role !== "assistant") continue;
115263
+ const patterns = [
115264
+ /I (?:chose|decided|selected|went with|picked) (.{10,80}) (?:because|since|as|due to) (.{10,120})/gi,
115265
+ /(?:j'ai choisi|j'ai décidé|j'ai opté pour) (.{10,80}) (?:car|parce que|puisque) (.{10,120})/gi
115266
+ ];
115267
+ for (const pattern of patterns) {
115268
+ let match;
115269
+ while ((match = pattern.exec(msg.content)) !== null && decisions.length < 5) {
115270
+ decisions.push({ decision: match[1].trim(), reason: match[2].trim() });
115271
+ }
115272
+ }
115273
+ }
115274
+ return decisions;
115275
+ }
115276
+ suggestNextSteps(remaining, errors, exitReason) {
115277
+ const steps = [];
115278
+ if (remaining.length > 0) steps.push(`Continue with: ${remaining[0]}`);
115279
+ if (errors.length > 0) steps.push(`Investigate ${errors.length} error(s) from previous session`);
115280
+ if (exitReason === "budget_exceeded") steps.push("Consider increasing budget for next run");
115281
+ if (exitReason === "max_iterations") steps.push("Consider increasing max iterations or simplifying the objective");
115282
+ if (steps.length === 0) steps.push("Review results and plan next action");
115283
+ return steps;
115284
+ }
114607
115285
  };
114608
115286
  }
114609
115287
  });
@@ -114615,6 +115293,9 @@ var init_safety = __esm({
114615
115293
  init_stuck_detector();
114616
115294
  init_circuit_breaker();
114617
115295
  init_context_rotator();
115296
+ init_question_detector();
115297
+ init_completion_detector();
115298
+ init_handoff_generator();
114618
115299
  }
114619
115300
  });
114620
115301
 
@@ -116951,7 +117632,7 @@ var init_inventory = __esm({
116951
117632
  /** Find similar past missions for planning. */
116952
117633
  async findSimilarMissions(objective) {
116953
117634
  try {
116954
- return this.store.searchMissions(objective, 3);
117635
+ return this.store.searchMissions(objective, { limit: 3 });
116955
117636
  } catch {
116956
117637
  return [];
116957
117638
  }
@@ -117834,6 +118515,7 @@ __export(src_exports3, {
117834
118515
  CircuitBreaker: () => CircuitBreaker,
117835
118516
  ClaudeAdapter: () => ClaudeAdapter,
117836
118517
  CodeChunker: () => CodeChunker,
118518
+ CompletionDetector: () => CompletionDetector,
117837
118519
  ContextBuilder: () => ContextBuilder,
117838
118520
  ContextRotator: () => ContextRotator,
117839
118521
  DockerDeployer: () => DockerDeployer,
@@ -117847,11 +118529,13 @@ __export(src_exports3, {
117847
118529
  GeminiAdapter: () => GeminiAdapter,
117848
118530
  GeminiEmbeddingProvider: () => GeminiEmbeddingProvider,
117849
118531
  GrokAdapter: () => GrokAdapter,
118532
+ HandoffGenerator: () => HandoffGenerator,
117850
118533
  HotSwapAdapter: () => HotSwapAdapter,
117851
118534
  IntentAnalyzer: () => IntentAnalyzer,
117852
118535
  InventoryManager: () => InventoryManager,
117853
118536
  K8sExecutor: () => K8sExecutor,
117854
118537
  KnowledgeGraph: () => KnowledgeGraph,
118538
+ LayeredMemory: () => LayeredMemory,
117855
118539
  LocalEmbeddings: () => LocalEmbeddings,
117856
118540
  LocalSandbox: () => LocalSandbox,
117857
118541
  McpFactory: () => McpFactory,
@@ -117874,6 +118558,7 @@ __export(src_exports3, {
117874
118558
  PluginManifestSchema: () => PluginManifestSchema,
117875
118559
  PluginSandbox: () => PluginSandbox,
117876
118560
  ProcessSandbox: () => ProcessSandbox,
118561
+ QuestionDetector: () => QuestionDetector,
117877
118562
  RagIndexer: () => RagIndexer,
117878
118563
  RagSearch: () => RagSearch,
117879
118564
  ReasonLogger: () => ReasonLogger,
@@ -149749,7 +150434,7 @@ var init_admin2 = __esm({
149749
150434
  "../server/src/routes/admin.ts"() {
149750
150435
  "use strict";
149751
150436
  init_rate_limit();
149752
- SERVER_VERSION3 = "2.7.1";
150437
+ SERVER_VERSION3 = "2.9.0";
149753
150438
  startTime3 = Date.now();
149754
150439
  }
149755
150440
  });
@@ -151886,6 +152571,36 @@ function missionRoutes(store, logger) {
151886
152571
  next(err);
151887
152572
  }
151888
152573
  });
152574
+ router.get("/search", async (req, res, next) => {
152575
+ try {
152576
+ const q = req.query.q || "";
152577
+ const type = req.query.type;
152578
+ const status = req.query.status;
152579
+ const from2 = req.query.from;
152580
+ const to = req.query.to;
152581
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : 20;
152582
+ const results = store.searchMissions(q, { type, status, from: from2, to, limit });
152583
+ res.json({
152584
+ data: results,
152585
+ meta: {
152586
+ count: results.length,
152587
+ query: q,
152588
+ filters: { type, status, from: from2, to }
152589
+ }
152590
+ });
152591
+ } catch (err) {
152592
+ next(err);
152593
+ }
152594
+ });
152595
+ router.get("/stats", async (req, res, next) => {
152596
+ try {
152597
+ const tenantId = req.tenantId || "default";
152598
+ const stats = store.getMissionStats(tenantId);
152599
+ res.json({ data: stats });
152600
+ } catch (err) {
152601
+ next(err);
152602
+ }
152603
+ });
151889
152604
  router.get("/:id", async (req, res, next) => {
151890
152605
  try {
151891
152606
  const mission = store.getMission(req.params.id);
@@ -153046,7 +153761,7 @@ var VERSION2, DATA_DIR3, TELEMETRY_ID_FILE, TELEMETRY_LOG_FILE, DEFAULT_FLUSH_TH
153046
153761
  var init_telemetry2 = __esm({
153047
153762
  "../server/src/services/telemetry.ts"() {
153048
153763
  "use strict";
153049
- VERSION2 = "2.7.1";
153764
+ VERSION2 = "2.9.0";
153050
153765
  DATA_DIR3 = join23(homedir8(), ".nestor");
153051
153766
  TELEMETRY_ID_FILE = join23(DATA_DIR3, "telemetry-id");
153052
153767
  TELEMETRY_LOG_FILE = join23(DATA_DIR3, "telemetry.jsonl");
@@ -157365,7 +158080,7 @@ var init_src6 = __esm({
157365
158080
  await this._handle.listen();
157366
158081
  const authMode = config2.apiKey ? "API key" : "open (no auth)";
157367
158082
  console.log(`
157368
- Nestor Server v2.7.1`);
158083
+ Nestor Server v2.9.0`);
157369
158084
  console.log(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
157370
158085
  console.log(` HTTP : http://${this._host}:${this._port}`);
157371
158086
  console.log(` WS : ws://${this._host}:${this._port}/ws`);
@@ -167127,7 +167842,7 @@ var init_server = __esm({
167127
167842
  MCP_PROTOCOL_VERSION = "2024-11-05";
167128
167843
  SERVER_INFO = {
167129
167844
  name: "nestor",
167130
- version: "2.7.1"
167845
+ version: "2.9.0"
167131
167846
  };
167132
167847
  SERVER_CAPABILITIES = {
167133
167848
  tools: { listChanged: false },
@@ -167571,7 +168286,7 @@ function printWelcome() {
167571
168286
  console.log(chalk11.cyan(` | .\` | | _| \\__ \\ | | | (_) | | / _ \\__ \\ | __ |`));
167572
168287
  console.log(chalk11.cyan(` |_|\\_| |___| |___/ |_| \\___/ |_|_\\ (_) |___/ |_||_|`));
167573
168288
  console.log("");
167574
- console.log(chalk11.dim(" Interactive Shell \u2014 v2.7.1"));
168289
+ console.log(chalk11.dim(" Interactive Shell \u2014 v2.9.0"));
167575
168290
  console.log(chalk11.dim(" Type /help for commands, /exit to quit."));
167576
168291
  console.log(chalk11.dim(" Multiline: end a line with \\ or use ``` code blocks."));
167577
168292
  console.log("");
@@ -169490,7 +170205,7 @@ var init_shell = __esm({
169490
170205
 
169491
170206
  // src/index.ts
169492
170207
  import { Command } from "commander";
169493
- import { existsSync as existsSync25, readFileSync as readFileSync25 } from "node:fs";
170208
+ import { existsSync as existsSync26, readFileSync as readFileSync26 } from "node:fs";
169494
170209
  import { join as join29 } from "node:path";
169495
170210
  import { homedir as homedir12 } from "node:os";
169496
170211
 
@@ -169511,7 +170226,7 @@ var BANNER = `
169511
170226
  function registerStartCommand(program2) {
169512
170227
  program2.command("start").description("Start the Nestor server").option("-p, --port <port>", "Server port").option("-H, --host <host>", "Server host").option("--no-studio", "Disable the Studio web UI").action(async (options) => {
169513
170228
  console.log(chalk.cyan(BANNER));
169514
- console.log(chalk.dim(` v2.7.1
170229
+ console.log(chalk.dim(` v2.9.0
169515
170230
  `));
169516
170231
  let config2 = readConfigFile();
169517
170232
  if (!config2) {
@@ -171203,7 +171918,7 @@ async function startForeground() {
171203
171918
  console.log(chalk12.cyan(` | .\` | | _| \\__ \\ | | | (_) | | / _ \\__ \\ | __ |`));
171204
171919
  console.log(chalk12.cyan(` |_|\\_| |___| |___/ |_| \\___/ |_|_\\ (_) |___/ |_||_|`));
171205
171920
  console.log("");
171206
- console.log(chalk12.dim(" Daemon Mode \u2014 v2.7.1"));
171921
+ console.log(chalk12.dim(" Daemon Mode \u2014 v2.9.0"));
171207
171922
  console.log(chalk12.dim(` PID: ${process.pid}`));
171208
171923
  console.log(chalk12.dim(` Log: ${LOG_FILE}`));
171209
171924
  console.log("");
@@ -171566,9 +172281,9 @@ var DaemonRunner = class {
171566
172281
  try {
171567
172282
  const store = await getStore();
171568
172283
  try {
171569
- const { randomUUID: randomUUID51 } = await import("node:crypto");
172284
+ const { randomUUID: randomUUID52 } = await import("node:crypto");
171570
172285
  store.createRun({
171571
- id: randomUUID51(),
172286
+ id: randomUUID52(),
171572
172287
  workflowId: `agent:${agentId}`,
171573
172288
  workflowVersion: 1,
171574
172289
  status: result.exitReason === "completed" ? "completed" : "failed",
@@ -171604,9 +172319,9 @@ var DaemonRunner = class {
171604
172319
  try {
171605
172320
  const store = await getStore();
171606
172321
  try {
171607
- const { randomUUID: randomUUID51 } = await import("node:crypto");
172322
+ const { randomUUID: randomUUID52 } = await import("node:crypto");
171608
172323
  store.createRun({
171609
- id: randomUUID51(),
172324
+ id: randomUUID52(),
171610
172325
  workflowId: `agent:${agentId}`,
171611
172326
  workflowVersion: 1,
171612
172327
  status: "failed",
@@ -173349,11 +174064,289 @@ function registerGuardrailCommand(program2) {
173349
174064
  });
173350
174065
  }
173351
174066
 
174067
+ // src/commands/loop.ts
174068
+ init_db();
174069
+ init_config2();
174070
+ import chalk22 from "chalk";
174071
+ import { randomUUID as randomUUID51 } from "node:crypto";
174072
+ import { readFileSync as readFileSync25, existsSync as existsSync25 } from "node:fs";
174073
+ import { execSync as execSync3 } from "node:child_process";
174074
+ function runShellCommand(cmd, cwd) {
174075
+ try {
174076
+ const stdout = execSync3(cmd, {
174077
+ cwd,
174078
+ encoding: "utf-8",
174079
+ timeout: 6e4,
174080
+ stdio: ["pipe", "pipe", "pipe"]
174081
+ });
174082
+ return { stdout: stdout.trim(), exitCode: 0 };
174083
+ } catch (err) {
174084
+ const execErr = err;
174085
+ return {
174086
+ stdout: (execErr.stdout ?? "").toString().trim(),
174087
+ exitCode: execErr.status ?? 1
174088
+ };
174089
+ }
174090
+ }
174091
+ function getGitDiff(cwd) {
174092
+ try {
174093
+ const diff = execSync3("git diff --stat", { cwd, encoding: "utf-8", timeout: 1e4 });
174094
+ return diff.trim() || "(no changes)";
174095
+ } catch {
174096
+ return "(git not available)";
174097
+ }
174098
+ }
174099
+ function getTestOutput(verifyCmd, cwd) {
174100
+ const result = runShellCommand(verifyCmd, cwd);
174101
+ const maxLen = 2e3;
174102
+ const output = result.stdout.length > maxLen ? result.stdout.slice(-maxLen) : result.stdout;
174103
+ return output;
174104
+ }
174105
+ function buildIterationPrompt(opts) {
174106
+ const parts = [];
174107
+ parts.push(`# Objective (iteration ${opts.iteration}/${opts.maxIterations})`);
174108
+ parts.push("");
174109
+ parts.push(opts.spec);
174110
+ parts.push("");
174111
+ if (opts.previousResults.length > 0) {
174112
+ parts.push("## Previous iterations summary");
174113
+ for (const r of opts.previousResults) {
174114
+ const status = r.specMet ? "PASSED" : "FAILED";
174115
+ parts.push(`- Iteration ${r.iteration}: ${status} (${r.exitReason}) \u2014 ${r.output.slice(0, 200)}`);
174116
+ }
174117
+ parts.push("");
174118
+ }
174119
+ if (opts.gitDiff !== "(no changes)" && opts.gitDiff !== "(git not available)") {
174120
+ parts.push("## Current state (git diff --stat)");
174121
+ parts.push("```");
174122
+ parts.push(opts.gitDiff);
174123
+ parts.push("```");
174124
+ parts.push("");
174125
+ }
174126
+ if (opts.verifyOutput) {
174127
+ parts.push("## Verification output (from last check)");
174128
+ parts.push("```");
174129
+ parts.push(opts.verifyOutput);
174130
+ parts.push("```");
174131
+ parts.push("");
174132
+ }
174133
+ parts.push("## Instructions");
174134
+ parts.push("Analyze the current state and work towards achieving the objective.");
174135
+ parts.push("If previous iterations failed, try a DIFFERENT approach.");
174136
+ parts.push("When done, commit your changes with a descriptive message.");
174137
+ return parts.join("\n");
174138
+ }
174139
+ function registerLoopCommand(program2) {
174140
+ program2.command("loop").description("Run an agent in a fresh-context loop until a specification is met (Ralph pattern)").option("--agent <name>", "Agent name to use", "default").option("--spec <text>", "Specification to achieve").option("--spec-file <path>", "File containing the specification").option("--verify <command>", "Shell command to verify spec (exit 0 = done)").option("--max-iterations <n>", "Max loop iterations", "10").option("--budget <usd>", "Max total budget in USD", "5").option("--cwd <path>", "Working directory", process.cwd()).action(async (opts) => {
174141
+ let spec = opts.spec;
174142
+ if (!spec && opts.specFile) {
174143
+ if (!existsSync25(opts.specFile)) {
174144
+ console.error(chalk22.red(`Spec file not found: ${opts.specFile}`));
174145
+ process.exit(1);
174146
+ }
174147
+ spec = readFileSync25(opts.specFile, "utf-8").trim();
174148
+ }
174149
+ if (!spec) {
174150
+ console.error(chalk22.red("Must provide --spec or --spec-file"));
174151
+ process.exit(1);
174152
+ }
174153
+ const maxIterations = parseInt(opts.maxIterations, 10);
174154
+ const totalBudget = parseFloat(opts.budget);
174155
+ const cwd = opts.cwd;
174156
+ console.log(chalk22.cyan("\n Ralph Fresh-Context Loop"));
174157
+ console.log(chalk22.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
174158
+ console.log(chalk22.white(" Spec:"), spec.slice(0, 100) + (spec.length > 100 ? "..." : ""));
174159
+ console.log(chalk22.white(" Agent:"), opts.agent);
174160
+ console.log(chalk22.white(" Max iterations:"), maxIterations);
174161
+ console.log(chalk22.white(" Budget:"), `$${totalBudget}`);
174162
+ if (opts.verify) {
174163
+ console.log(chalk22.white(" Verify cmd:"), opts.verify);
174164
+ }
174165
+ console.log("");
174166
+ let agentModule;
174167
+ try {
174168
+ agentModule = await Promise.resolve().then(() => (init_src5(), src_exports3));
174169
+ } catch {
174170
+ console.error(chalk22.red("Failed to import @nestor/agent. Is it installed?"));
174171
+ process.exit(1);
174172
+ }
174173
+ const store = await getStore();
174174
+ let resolvedAgent;
174175
+ try {
174176
+ const agents = store.listAgents();
174177
+ resolvedAgent = agents.find((a) => a.name === opts.agent);
174178
+ } catch {
174179
+ }
174180
+ const agent = resolvedAgent ?? {
174181
+ id: randomUUID51(),
174182
+ name: opts.agent,
174183
+ adapterType: "claude",
174184
+ adapterConfig: {
174185
+ model: "claude-sonnet-4-20250514",
174186
+ instructions: "You are an expert software engineer. Complete the given specification precisely."
174187
+ },
174188
+ trustLevel: "community",
174189
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
174190
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
174191
+ };
174192
+ let router;
174193
+ try {
174194
+ router = await agentModule.createDefaultRouter();
174195
+ } catch {
174196
+ console.error(chalk22.red("Could not initialize LLM adapter. Is the API key configured?"));
174197
+ process.exit(1);
174198
+ }
174199
+ const summary = {
174200
+ totalIterations: 0,
174201
+ totalCostUsd: 0,
174202
+ totalTokens: 0,
174203
+ totalDurationMs: 0,
174204
+ specMet: false,
174205
+ iterationResults: []
174206
+ };
174207
+ for (let i = 1; i <= maxIterations; i++) {
174208
+ if (summary.totalCostUsd >= totalBudget) {
174209
+ console.log(chalk22.yellow(`
174210
+ Budget exhausted ($${summary.totalCostUsd.toFixed(4)} / $${totalBudget}). Stopping.`));
174211
+ break;
174212
+ }
174213
+ const remainingBudget = totalBudget - summary.totalCostUsd;
174214
+ console.log(chalk22.cyan(`
174215
+ \u2500\u2500 Iteration ${i}/${maxIterations} `) + chalk22.dim(`(budget remaining: $${remainingBudget.toFixed(4)}) \u2500\u2500`));
174216
+ if (opts.verify && i > 1) {
174217
+ const verifyResult = runShellCommand(opts.verify, cwd);
174218
+ if (verifyResult.exitCode === 0) {
174219
+ console.log(chalk22.green(" Specification met! Verify command exited 0."));
174220
+ summary.specMet = true;
174221
+ break;
174222
+ }
174223
+ }
174224
+ const gitDiff = getGitDiff(cwd);
174225
+ const verifyOutput = opts.verify ? getTestOutput(opts.verify, cwd) : "";
174226
+ const prompt = buildIterationPrompt({
174227
+ spec,
174228
+ iteration: i,
174229
+ maxIterations,
174230
+ previousResults: summary.iterationResults,
174231
+ gitDiff,
174232
+ verifyOutput
174233
+ });
174234
+ const modelId = agent.adapterConfig.model ?? "claude-sonnet-4-20250514";
174235
+ const adapter = router.resolve(modelId);
174236
+ const events = new agentModule.RuntimeEventBus();
174237
+ const tools = new agentModule.ToolRegistry();
174238
+ agentModule.registerBuiltinTools(tools);
174239
+ const runtime = new agentModule.AgentRuntime({
174240
+ adapter,
174241
+ tools,
174242
+ toolExecutor: new agentModule.ToolExecutor(),
174243
+ role: {
174244
+ name: agent.name,
174245
+ description: agent.description ?? "Loop agent",
174246
+ instructions: agent.adapterConfig.instructions ?? "You are an expert software engineer.",
174247
+ constraints: []
174248
+ },
174249
+ workingDir: cwd,
174250
+ events,
174251
+ maxCostUsd: remainingBudget,
174252
+ maxIterations: 50,
174253
+ db: store,
174254
+ modelRouter: router,
174255
+ // Enable context rotation within each iteration too
174256
+ contextRotation: { maxMessages: 40, maxTokensEstimate: 8e4 },
174257
+ stuckDetection: { maxRepeatedCalls: 3, maxConsecutiveErrors: 3 }
174258
+ });
174259
+ const taskId = randomUUID51();
174260
+ let iterOutput = "";
174261
+ try {
174262
+ for await (const event of runtime.runStreaming({
174263
+ prompt,
174264
+ agentId: agent.id,
174265
+ taskId
174266
+ })) {
174267
+ switch (event.type) {
174268
+ case "token":
174269
+ process.stdout.write(event.text);
174270
+ iterOutput += event.text;
174271
+ break;
174272
+ case "tool_start":
174273
+ console.log(chalk22.dim(`
174274
+ [tool] ${event.name}`));
174275
+ break;
174276
+ case "tool_result":
174277
+ if (!event.success) {
174278
+ console.log(chalk22.red(` [tool error] ${event.name}: ${event.result.slice(0, 100)}`));
174279
+ }
174280
+ break;
174281
+ case "error":
174282
+ console.log(chalk22.red(`
174283
+ [error] ${event.message}`));
174284
+ break;
174285
+ case "done": {
174286
+ const r = event.result;
174287
+ const iterResult = {
174288
+ iteration: i,
174289
+ output: r.output || iterOutput,
174290
+ exitReason: r.exitReason,
174291
+ costUsd: r.usage.estimatedCostUsd,
174292
+ tokens: r.usage.totalTokens,
174293
+ durationMs: r.durationMs,
174294
+ specMet: false
174295
+ };
174296
+ summary.totalCostUsd += r.usage.estimatedCostUsd;
174297
+ summary.totalTokens += r.usage.totalTokens;
174298
+ summary.totalDurationMs += r.durationMs;
174299
+ summary.totalIterations = i;
174300
+ if (opts.verify) {
174301
+ const postVerify = runShellCommand(opts.verify, cwd);
174302
+ iterResult.specMet = postVerify.exitCode === 0;
174303
+ }
174304
+ summary.iterationResults.push(iterResult);
174305
+ console.log("");
174306
+ console.log(chalk22.dim(` [${r.exitReason}] ${r.usage.totalTokens} tokens, $${r.usage.estimatedCostUsd.toFixed(4)}, ${(r.durationMs / 1e3).toFixed(1)}s`));
174307
+ if (iterResult.specMet) {
174308
+ console.log(chalk22.green(" Specification met!"));
174309
+ summary.specMet = true;
174310
+ }
174311
+ break;
174312
+ }
174313
+ }
174314
+ }
174315
+ } catch (err) {
174316
+ console.error(chalk22.red(`
174317
+ Iteration ${i} crashed: ${err instanceof Error ? err.message : String(err)}`));
174318
+ summary.iterationResults.push({
174319
+ iteration: i,
174320
+ output: `Error: ${err instanceof Error ? err.message : String(err)}`,
174321
+ exitReason: "error",
174322
+ costUsd: 0,
174323
+ tokens: 0,
174324
+ durationMs: 0,
174325
+ specMet: false
174326
+ });
174327
+ }
174328
+ if (summary.specMet) break;
174329
+ }
174330
+ console.log(chalk22.cyan("\n \u2500\u2500 Loop Summary \u2500\u2500"));
174331
+ console.log(chalk22.white(" Iterations:"), summary.totalIterations);
174332
+ console.log(chalk22.white(" Total cost:"), `$${summary.totalCostUsd.toFixed(4)}`);
174333
+ console.log(chalk22.white(" Total tokens:"), summary.totalTokens.toLocaleString());
174334
+ console.log(chalk22.white(" Total time:"), `${(summary.totalDurationMs / 1e3).toFixed(1)}s`);
174335
+ console.log(
174336
+ chalk22.white(" Spec met:"),
174337
+ summary.specMet ? chalk22.green("YES") : chalk22.red("NO")
174338
+ );
174339
+ console.log("");
174340
+ await store.close();
174341
+ process.exit(summary.specMet ? 0 : 1);
174342
+ });
174343
+ }
174344
+
173352
174345
  // src/index.ts
173353
174346
  function checkBetaAccess() {
173354
174347
  const licenseFile = join29(homedir12(), ".nestor", "license.key");
173355
- if (!existsSync25(licenseFile)) return false;
173356
- const key = readFileSync25(licenseFile, "utf-8").trim();
174348
+ if (!existsSync26(licenseFile)) return false;
174349
+ const key = readFileSync26(licenseFile, "utf-8").trim();
173357
174350
  return /^NESTOR-BETA-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(key);
173358
174351
  }
173359
174352
  var command2 = process.argv[2];
@@ -173373,7 +174366,7 @@ if (command2 && !["--help", "-h", "--version", "-V", "install"].includes(command
173373
174366
  }
173374
174367
  }
173375
174368
  var program = new Command();
173376
- program.name("nestor-sh").description("Nestor AI Agent Platform \u2014 orchestrate, secure and monitor AI agents").version("2.7.1");
174369
+ program.name("nestor-sh").description("Nestor AI Agent Platform \u2014 orchestrate, secure and monitor AI agents").version("2.9.0");
173377
174370
  registerStartCommand(program);
173378
174371
  registerInstallCommand(program);
173379
174372
  registerAgentCommand(program);
@@ -173393,6 +174386,7 @@ registerTemplateCommand(program);
173393
174386
  registerRagCommand(program);
173394
174387
  registerTestCommand(program);
173395
174388
  registerGuardrailCommand(program);
174389
+ registerLoopCommand(program);
173396
174390
  var args = process.argv.slice(2);
173397
174391
  if (args.length === 0) {
173398
174392
  Promise.resolve().then(() => (init_shell(), shell_exports)).then(({ registerShellCommand: _ }) => {