@autonav/core 1.5.0 → 1.7.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.
Files changed (238) hide show
  1. package/README.md +2 -2
  2. package/dist/adapter/index.d.ts +3 -3
  3. package/dist/adapter/index.d.ts.map +1 -1
  4. package/dist/adapter/index.js +3 -3
  5. package/dist/adapter/index.js.map +1 -1
  6. package/dist/adapter/navigator-adapter.d.ts +196 -0
  7. package/dist/adapter/navigator-adapter.d.ts.map +1 -0
  8. package/dist/adapter/navigator-adapter.js +579 -0
  9. package/dist/adapter/navigator-adapter.js.map +1 -0
  10. package/dist/cli/autonav.d.ts +2 -5
  11. package/dist/cli/autonav.d.ts.map +1 -1
  12. package/dist/cli/autonav.js +35 -49
  13. package/dist/cli/autonav.js.map +1 -1
  14. package/dist/cli/nav-chat.d.ts +2 -1
  15. package/dist/cli/nav-chat.d.ts.map +1 -1
  16. package/dist/cli/nav-chat.js +32 -3
  17. package/dist/cli/nav-chat.js.map +1 -1
  18. package/dist/cli/nav-init.d.ts +2 -1
  19. package/dist/cli/nav-init.d.ts.map +1 -1
  20. package/dist/cli/nav-init.js +12 -4
  21. package/dist/cli/nav-init.js.map +1 -1
  22. package/dist/cli/nav-install.d.ts +2 -1
  23. package/dist/cli/nav-install.d.ts.map +1 -1
  24. package/dist/cli/nav-install.js +4 -2
  25. package/dist/cli/nav-install.js.map +1 -1
  26. package/dist/cli/nav-memento.d.ts +21 -0
  27. package/dist/cli/nav-memento.d.ts.map +1 -0
  28. package/dist/cli/nav-memento.js +185 -0
  29. package/dist/cli/nav-memento.js.map +1 -0
  30. package/dist/cli/nav-mend.d.ts +2 -1
  31. package/dist/cli/nav-mend.d.ts.map +1 -1
  32. package/dist/cli/nav-mend.js +4 -1
  33. package/dist/cli/nav-mend.js.map +1 -1
  34. package/dist/cli/nav-migrate.d.ts +2 -1
  35. package/dist/cli/nav-migrate.d.ts.map +1 -1
  36. package/dist/cli/nav-migrate.js +2 -6
  37. package/dist/cli/nav-migrate.js.map +1 -1
  38. package/dist/cli/nav-query.d.ts +2 -1
  39. package/dist/cli/nav-query.d.ts.map +1 -1
  40. package/dist/cli/nav-query.js +12 -6
  41. package/dist/cli/nav-query.js.map +1 -1
  42. package/dist/cli/nav-standup.d.ts +18 -0
  43. package/dist/cli/nav-standup.d.ts.map +1 -0
  44. package/dist/cli/nav-standup.js +151 -0
  45. package/dist/cli/nav-standup.js.map +1 -0
  46. package/dist/cli/nav-uninstall.d.ts +2 -1
  47. package/dist/cli/nav-uninstall.d.ts.map +1 -1
  48. package/dist/cli/nav-uninstall.js +4 -2
  49. package/dist/cli/nav-uninstall.js.map +1 -1
  50. package/dist/cli/nav-update.d.ts +2 -1
  51. package/dist/cli/nav-update.d.ts.map +1 -1
  52. package/dist/cli/nav-update.js +11 -6
  53. package/dist/cli/nav-update.js.map +1 -1
  54. package/dist/conversation/App.d.ts +9 -2
  55. package/dist/conversation/App.d.ts.map +1 -1
  56. package/dist/conversation/App.js +304 -111
  57. package/dist/conversation/App.js.map +1 -1
  58. package/dist/conversation/index.d.ts +5 -0
  59. package/dist/conversation/index.d.ts.map +1 -1
  60. package/dist/conversation/index.js +17 -2
  61. package/dist/conversation/index.js.map +1 -1
  62. package/dist/harness/chibi-harness.d.ts +36 -0
  63. package/dist/harness/chibi-harness.d.ts.map +1 -0
  64. package/dist/harness/chibi-harness.js +383 -0
  65. package/dist/harness/chibi-harness.js.map +1 -0
  66. package/dist/harness/chibi-plugins/get_plugin_config +64 -0
  67. package/dist/harness/chibi-plugins/query_navigator +114 -0
  68. package/dist/harness/chibi-plugins/submit_answer +38 -0
  69. package/dist/harness/chibi-plugins/update_plugin_config +91 -0
  70. package/dist/harness/claude-code-harness.d.ts +24 -0
  71. package/dist/harness/claude-code-harness.d.ts.map +1 -0
  72. package/dist/harness/claude-code-harness.js +242 -0
  73. package/dist/harness/claude-code-harness.js.map +1 -0
  74. package/dist/harness/ephemeral-home.d.ts +34 -0
  75. package/dist/harness/ephemeral-home.d.ts.map +1 -0
  76. package/dist/harness/ephemeral-home.js +56 -0
  77. package/dist/harness/ephemeral-home.js.map +1 -0
  78. package/dist/harness/factory.d.ts +47 -0
  79. package/dist/harness/factory.d.ts.map +1 -0
  80. package/dist/harness/factory.js +84 -0
  81. package/dist/harness/factory.js.map +1 -0
  82. package/dist/harness/helpers.d.ts +58 -0
  83. package/dist/harness/helpers.d.ts.map +1 -0
  84. package/dist/harness/helpers.js +78 -0
  85. package/dist/harness/helpers.js.map +1 -0
  86. package/dist/harness/index.d.ts +14 -0
  87. package/dist/harness/index.d.ts.map +1 -0
  88. package/dist/harness/index.js +13 -0
  89. package/dist/harness/index.js.map +1 -0
  90. package/dist/harness/opencode-harness.d.ts +79 -0
  91. package/dist/harness/opencode-harness.d.ts.map +1 -0
  92. package/dist/harness/opencode-harness.js +537 -0
  93. package/dist/harness/opencode-harness.js.map +1 -0
  94. package/dist/harness/opencode-tools/get_plugin_config.ts +72 -0
  95. package/dist/harness/opencode-tools/query_navigator.ts +126 -0
  96. package/dist/harness/opencode-tools/submit_answer.ts +40 -0
  97. package/dist/harness/opencode-tools/update_plugin_config.ts +105 -0
  98. package/dist/harness/sandbox.d.ts +59 -0
  99. package/dist/harness/sandbox.d.ts.map +1 -0
  100. package/dist/harness/sandbox.js +152 -0
  101. package/dist/harness/sandbox.js.map +1 -0
  102. package/dist/harness/tool-server.d.ts +50 -0
  103. package/dist/harness/tool-server.d.ts.map +1 -0
  104. package/dist/harness/tool-server.js +27 -0
  105. package/dist/harness/tool-server.js.map +1 -0
  106. package/dist/harness/types.d.ts +168 -0
  107. package/dist/harness/types.d.ts.map +1 -0
  108. package/dist/harness/types.js +12 -0
  109. package/dist/harness/types.js.map +1 -0
  110. package/dist/index.d.ts +3 -2
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +4 -2
  113. package/dist/index.js.map +1 -1
  114. package/dist/interview/App.d.ts +4 -2
  115. package/dist/interview/App.d.ts.map +1 -1
  116. package/dist/interview/App.js +36 -56
  117. package/dist/interview/App.js.map +1 -1
  118. package/dist/interview/index.d.ts +3 -0
  119. package/dist/interview/index.d.ts.map +1 -1
  120. package/dist/interview/index.js +1 -0
  121. package/dist/interview/index.js.map +1 -1
  122. package/dist/interview/prompts.d.ts +3 -1
  123. package/dist/interview/prompts.d.ts.map +1 -1
  124. package/dist/interview/prompts.js +5 -1
  125. package/dist/interview/prompts.js.map +1 -1
  126. package/dist/interview/ui/ChatBanner.d.ts +17 -0
  127. package/dist/interview/ui/ChatBanner.d.ts.map +1 -0
  128. package/dist/interview/ui/ChatBanner.js +30 -0
  129. package/dist/interview/ui/ChatBanner.js.map +1 -0
  130. package/dist/interview/ui/ChatInput.d.ts +36 -0
  131. package/dist/interview/ui/ChatInput.d.ts.map +1 -0
  132. package/dist/interview/ui/ChatInput.js +304 -0
  133. package/dist/interview/ui/ChatInput.js.map +1 -0
  134. package/dist/interview/ui/MarkdownText.d.ts +13 -0
  135. package/dist/interview/ui/MarkdownText.d.ts.map +1 -0
  136. package/dist/interview/ui/MarkdownText.js +107 -0
  137. package/dist/interview/ui/MarkdownText.js.map +1 -0
  138. package/dist/interview/ui/index.d.ts +3 -0
  139. package/dist/interview/ui/index.d.ts.map +1 -1
  140. package/dist/interview/ui/index.js +3 -0
  141. package/dist/interview/ui/index.js.map +1 -1
  142. package/dist/memento/git-operations.d.ts +95 -0
  143. package/dist/memento/git-operations.d.ts.map +1 -0
  144. package/dist/memento/git-operations.js +256 -0
  145. package/dist/memento/git-operations.js.map +1 -0
  146. package/dist/memento/implementer-agent.d.ts +31 -0
  147. package/dist/memento/implementer-agent.d.ts.map +1 -0
  148. package/dist/memento/implementer-agent.js +95 -0
  149. package/dist/memento/implementer-agent.js.map +1 -0
  150. package/dist/memento/index.d.ts +32 -0
  151. package/dist/memento/index.d.ts.map +1 -0
  152. package/dist/memento/index.js +40 -0
  153. package/dist/memento/index.js.map +1 -0
  154. package/dist/memento/loop.d.ts +34 -0
  155. package/dist/memento/loop.d.ts.map +1 -0
  156. package/dist/memento/loop.js +1227 -0
  157. package/dist/memento/loop.js.map +1 -0
  158. package/dist/memento/matrix-animation.d.ts +103 -0
  159. package/dist/memento/matrix-animation.d.ts.map +1 -0
  160. package/dist/memento/matrix-animation.js +303 -0
  161. package/dist/memento/matrix-animation.js.map +1 -0
  162. package/dist/memento/nav-protocol.d.ts +30 -0
  163. package/dist/memento/nav-protocol.d.ts.map +1 -0
  164. package/dist/memento/nav-protocol.js +75 -0
  165. package/dist/memento/nav-protocol.js.map +1 -0
  166. package/dist/memento/prompts.d.ts +57 -0
  167. package/dist/memento/prompts.d.ts.map +1 -0
  168. package/dist/memento/prompts.js +208 -0
  169. package/dist/memento/prompts.js.map +1 -0
  170. package/dist/memento/rate-limit.d.ts +85 -0
  171. package/dist/memento/rate-limit.d.ts.map +1 -0
  172. package/dist/memento/rate-limit.js +221 -0
  173. package/dist/memento/rate-limit.js.map +1 -0
  174. package/dist/memento/types.d.ts +123 -0
  175. package/dist/memento/types.d.ts.map +1 -0
  176. package/dist/memento/types.js +30 -0
  177. package/dist/memento/types.js.map +1 -0
  178. package/dist/memento/worker-agent.d.ts +33 -0
  179. package/dist/memento/worker-agent.d.ts.map +1 -0
  180. package/dist/memento/worker-agent.js +93 -0
  181. package/dist/memento/worker-agent.js.map +1 -0
  182. package/dist/registry.d.ts +35 -0
  183. package/dist/registry.d.ts.map +1 -0
  184. package/dist/registry.js +87 -0
  185. package/dist/registry.js.map +1 -0
  186. package/dist/repo-analyzer/index.d.ts +2 -1
  187. package/dist/repo-analyzer/index.d.ts.map +1 -1
  188. package/dist/repo-analyzer/index.js +6 -17
  189. package/dist/repo-analyzer/index.js.map +1 -1
  190. package/dist/standup/config.d.ts +19 -0
  191. package/dist/standup/config.d.ts.map +1 -0
  192. package/dist/standup/config.js +42 -0
  193. package/dist/standup/config.js.map +1 -0
  194. package/dist/standup/index.d.ts +24 -0
  195. package/dist/standup/index.d.ts.map +1 -0
  196. package/dist/standup/index.js +29 -0
  197. package/dist/standup/index.js.map +1 -0
  198. package/dist/standup/loop.d.ts +36 -0
  199. package/dist/standup/loop.d.ts.map +1 -0
  200. package/dist/standup/loop.js +508 -0
  201. package/dist/standup/loop.js.map +1 -0
  202. package/dist/standup/prompts.d.ts +62 -0
  203. package/dist/standup/prompts.d.ts.map +1 -0
  204. package/dist/standup/prompts.js +211 -0
  205. package/dist/standup/prompts.js.map +1 -0
  206. package/dist/standup/standup-protocol.d.ts +33 -0
  207. package/dist/standup/standup-protocol.d.ts.map +1 -0
  208. package/dist/standup/standup-protocol.js +189 -0
  209. package/dist/standup/standup-protocol.js.map +1 -0
  210. package/dist/standup/types.d.ts +185 -0
  211. package/dist/standup/types.d.ts.map +1 -0
  212. package/dist/standup/types.js +67 -0
  213. package/dist/standup/types.js.map +1 -0
  214. package/dist/tools/cross-nav.d.ts +21 -0
  215. package/dist/tools/cross-nav.d.ts.map +1 -0
  216. package/dist/tools/cross-nav.js +93 -0
  217. package/dist/tools/cross-nav.js.map +1 -0
  218. package/dist/tools/index.d.ts +1 -0
  219. package/dist/tools/index.d.ts.map +1 -1
  220. package/dist/tools/index.js +1 -0
  221. package/dist/tools/index.js.map +1 -1
  222. package/dist/tools/related-navs-config.d.ts +15 -0
  223. package/dist/tools/related-navs-config.d.ts.map +1 -0
  224. package/dist/tools/related-navs-config.js +132 -0
  225. package/dist/tools/related-navs-config.js.map +1 -0
  226. package/dist/tools/related-navs.d.ts +23 -0
  227. package/dist/tools/related-navs.d.ts.map +1 -0
  228. package/dist/tools/related-navs.js +107 -0
  229. package/dist/tools/related-navs.js.map +1 -0
  230. package/dist/tools/response.d.ts +3 -2
  231. package/dist/tools/response.d.ts.map +1 -1
  232. package/dist/tools/response.js +7 -11
  233. package/dist/tools/response.js.map +1 -1
  234. package/dist/tools/self-config.d.ts +2 -1
  235. package/dist/tools/self-config.d.ts.map +1 -1
  236. package/dist/tools/self-config.js +5 -9
  237. package/dist/tools/self-config.js.map +1 -1
  238. package/package.json +4 -1
@@ -0,0 +1,1227 @@
1
+ /**
2
+ * Memento Loop Core Logic
3
+ *
4
+ * The main loop that coordinates navigator planning and implementer implementation
5
+ * in a context-clearing iterative development pattern.
6
+ *
7
+ * Design principle: The IMPLEMENTER forgets between iterations (memento pattern).
8
+ * The NAVIGATOR maintains its own memory and knowledge base. We provide git
9
+ * history as context about what the implementer has accomplished so far.
10
+ */
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import * as readline from "node:readline";
14
+ import { NavigatorAdapter } from "../adapter/index.js";
15
+ import { loadNavigator } from "../query-engine/index.js";
16
+ import chalk from "chalk";
17
+ import { ensureGitRepo, createBranch, getCurrentBranch, getRecentGitLog, getRecentDiff, getLastCommitDiffStats, hasUncommittedChanges, stageAllChanges, commitChanges, pushBranch, createPullRequest, isGhAvailable, } from "./git-operations.js";
18
+ import { createNavProtocolTools } from "./nav-protocol.js";
19
+ import { buildNavPlanPrompt, buildNavSystemPrompt, buildReviewPrompt, buildFixPrompt, buildFixSystemPrompt, } from "./prompts.js";
20
+ import { MatrixAnimation } from "./matrix-animation.js";
21
+ import { parseRateLimitError, formatDuration, getBackoffDelay, getConnectionRetryDelay, isTransientConnectionError, waitWithCountdown, MAX_WAIT_SECONDS, } from "./rate-limit.js";
22
+ const DEBUG = process.env.AUTONAV_DEBUG === "1" || process.env.DEBUG === "1";
23
+ /**
24
+ * Filter stderr lines to extract meaningful error information.
25
+ * The SDK emits a "Spawning Claude Code process" line that includes
26
+ * the entire system prompt and all CLI flags, which is enormous noise.
27
+ * We strip that out and keep only actual error/diagnostic lines.
28
+ */
29
+ function filterStderr(lines) {
30
+ return lines
31
+ .filter((line) => !line.startsWith("Spawning Claude Code process"))
32
+ .join("")
33
+ .trim();
34
+ }
35
+ // ── Mood message pools ──────────────────────────────────────────────────────
36
+ const NAV_START = [
37
+ "Surveying the landscape...",
38
+ "Getting oriented...",
39
+ "Scanning the terrain...",
40
+ ];
41
+ const NAV_EXPLORING = [
42
+ "Deep in thought...",
43
+ "Connecting the dots...",
44
+ "Piecing it together...",
45
+ "Following the thread...",
46
+ ];
47
+ const NAV_THOROUGH = [
48
+ "Leaving no stone unturned...",
49
+ "Thoroughly investigating...",
50
+ "Going deeper...",
51
+ ];
52
+ const NAV_PLANNING = [
53
+ "The plan crystallizes...",
54
+ "Eureka!",
55
+ "I see the path forward...",
56
+ ];
57
+ const NAV_ERROR = [
58
+ "Hmm, that's odd...",
59
+ "Recalibrating...",
60
+ "Unexpected terrain...",
61
+ ];
62
+ const IMPL_START = [
63
+ "Rolling up sleeves...",
64
+ "Let's do this...",
65
+ "Warming up...",
66
+ ];
67
+ const IMPL_READING = [
68
+ "Studying the target...",
69
+ "Reading the blueprints...",
70
+ "Reviewing the plan...",
71
+ ];
72
+ const IMPL_WRITING = [
73
+ "Fingers flying...",
74
+ "In the zone...",
75
+ "Crafting code...",
76
+ "Shaping the solution...",
77
+ ];
78
+ const IMPL_BUILDING = [
79
+ "Moment of truth...",
80
+ "Compiling hopes and dreams...",
81
+ "Building...",
82
+ ];
83
+ const IMPL_TESTING = [
84
+ "Crossing fingers...",
85
+ "Testing fate...",
86
+ "Validating...",
87
+ ];
88
+ const IMPL_FLOWING = [
89
+ "On a roll!",
90
+ "Flow state achieved...",
91
+ "Unstoppable...",
92
+ ];
93
+ const IMPL_ERROR = [
94
+ "Plot twist!",
95
+ "Hmm, let me reconsider...",
96
+ "Not quite...",
97
+ "Adjusting approach...",
98
+ ];
99
+ // Review-fix mood escalation pools (one per round)
100
+ const REVIEW_FIX_MOODS = [
101
+ // Round 1: gracious
102
+ [
103
+ "Addressing feedback...",
104
+ "Fair point, bestie...",
105
+ "Noted with love...",
106
+ "Okay I see you...",
107
+ "Valid, fixing...",
108
+ "The reviewer has a point...",
109
+ "Constructive! We love to see it...",
110
+ "Taking notes...",
111
+ ],
112
+ // Round 2: slightly sassy
113
+ [
114
+ "Alright alright...",
115
+ "Back at it...",
116
+ "More feedback? Cute.",
117
+ "Revising... again...",
118
+ "We're still doing this? Okay.",
119
+ "Serving second draft realness...",
120
+ "This better be the last time...",
121
+ "Slay... I guess...",
122
+ ],
123
+ // Round 3: frustrated fabulous
124
+ [
125
+ "Oh we're STILL going?",
126
+ "Girl, AGAIN?!",
127
+ "This code is my villain arc...",
128
+ "Is this a personal attack?",
129
+ "Mother is not pleased...",
130
+ "Living my revision fantasy...",
131
+ "Not another round...",
132
+ "The drama of it all...",
133
+ ],
134
+ // Round 4: unhinged
135
+ [
136
+ "ARE YOU KIDDING ME?!",
137
+ "I'm literally going to scream...",
138
+ "This is my 13th reason...",
139
+ "I can't even right now...",
140
+ "The audacity...",
141
+ "I did NOT sign up for this...",
142
+ "Gaslight gatekeep girlboss... code review?",
143
+ "Main character syndrome: reviewer edition...",
144
+ ],
145
+ // Round 5: acceptance / chaos
146
+ [
147
+ "FINE. TAKE IT.",
148
+ "Shipping it. Fight me.",
149
+ "Whatever, it's art.",
150
+ "This is camp now.",
151
+ "It's giving... done.",
152
+ "No thoughts, just commits...",
153
+ "Unhinged and merging...",
154
+ "Period. End of discussion.",
155
+ ],
156
+ ];
157
+ function randomFrom(pool) {
158
+ return pool[Math.floor(Math.random() * pool.length)] ?? pool[0];
159
+ }
160
+ function isBuildCommand(toolName, input) {
161
+ if (toolName !== "Bash")
162
+ return false;
163
+ const cmd = input.command || "";
164
+ return /\b(build|compile|tsc|webpack|esbuild)\b/.test(cmd);
165
+ }
166
+ function isTestCommand(toolName, input) {
167
+ if (toolName !== "Bash")
168
+ return false;
169
+ const cmd = input.command || "";
170
+ return /\b(test|jest|vitest|check|lint)\b/.test(cmd);
171
+ }
172
+ function isWriteTool(toolName) {
173
+ return toolName === "Write" || toolName === "Edit" || toolName === "str_replace_based_edit_tool";
174
+ }
175
+ function isReadTool(toolName) {
176
+ return toolName === "Read" || toolName === "Glob" || toolName === "Grep";
177
+ }
178
+ function pickMood(phase, toolName, input, state) {
179
+ if (state.lastError) {
180
+ return randomFrom(phase === "nav" ? NAV_ERROR : IMPL_ERROR);
181
+ }
182
+ if (phase === "nav") {
183
+ if (toolName === "submit_implementation_plan")
184
+ return randomFrom(NAV_PLANNING);
185
+ if (state.toolCount <= 2)
186
+ return randomFrom(NAV_START);
187
+ if (state.toolCount >= 10)
188
+ return randomFrom(NAV_THOROUGH);
189
+ return randomFrom(NAV_EXPLORING);
190
+ }
191
+ // impl phase
192
+ if (state.consecutiveSuccess >= 8)
193
+ return randomFrom(IMPL_FLOWING);
194
+ if (state.toolCount <= 2)
195
+ return randomFrom(IMPL_START);
196
+ if (isBuildCommand(toolName, input))
197
+ return randomFrom(IMPL_BUILDING);
198
+ if (isTestCommand(toolName, input))
199
+ return randomFrom(IMPL_TESTING);
200
+ if (isWriteTool(toolName))
201
+ return randomFrom(IMPL_WRITING);
202
+ if (isReadTool(toolName))
203
+ return randomFrom(IMPL_READING);
204
+ return randomFrom(IMPL_READING);
205
+ }
206
+ // ── Rate limit retry wrapper ────────────────────────────────────────────────
207
+ /**
208
+ * Check if an error message indicates a rate limit
209
+ */
210
+ function isRateLimitError(error) {
211
+ const message = error instanceof Error ? error.message : String(error);
212
+ return parseRateLimitError(message);
213
+ }
214
+ /**
215
+ * Wait for rate limit to reset with countdown display
216
+ */
217
+ async function waitForRateLimit(info, attempt, animation, verbose) {
218
+ // Determine wait time: use parsed reset time or exponential backoff
219
+ // Cap at 5h (session limit window) to avoid waiting for weekly resets
220
+ let waitSeconds;
221
+ if (info.secondsUntilReset && info.secondsUntilReset > 0) {
222
+ // Add 30 second buffer to parsed reset time, cap at 5h
223
+ waitSeconds = Math.min(info.secondsUntilReset + 30, MAX_WAIT_SECONDS);
224
+ }
225
+ else {
226
+ waitSeconds = getBackoffDelay(attempt);
227
+ }
228
+ // Stop animation while waiting
229
+ if (animation) {
230
+ animation.stop();
231
+ }
232
+ // Print rate limit info
233
+ console.log("");
234
+ console.log(chalk.yellow("⏳ Rate limited"));
235
+ if (info.resetTimeRaw) {
236
+ console.log(chalk.dim(` Reset time: ${info.resetTimeRaw}`));
237
+ }
238
+ console.log(chalk.dim(` Waiting ${formatDuration(waitSeconds)} before retry (attempt ${attempt + 1})...`));
239
+ // Wait with countdown
240
+ await waitWithCountdown(waitSeconds, (remaining, formatted) => {
241
+ // Update countdown every 10 seconds or for last 10 seconds
242
+ if (remaining % 10 === 0 || remaining <= 10) {
243
+ process.stdout.write(`\r${chalk.dim(` Resuming in ${formatted}...`)}${" ".repeat(20)}`);
244
+ }
245
+ });
246
+ console.log(`\r${chalk.green(" Resuming...")}${" ".repeat(30)}`);
247
+ console.log("");
248
+ // Restart animation if it was running
249
+ if (animation && !verbose) {
250
+ animation.start();
251
+ }
252
+ }
253
+ /**
254
+ * Load navigator config from config.json
255
+ */
256
+ function loadNavConfig(navDirectory) {
257
+ const configPath = path.join(navDirectory, "config.json");
258
+ if (!fs.existsSync(configPath)) {
259
+ return { identity: null };
260
+ }
261
+ try {
262
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
263
+ const identity = (config.name && config.description)
264
+ ? { name: config.name, description: config.description }
265
+ : null;
266
+ return {
267
+ identity,
268
+ sandbox: config.sandbox,
269
+ };
270
+ }
271
+ catch {
272
+ // Ignore parse errors
273
+ return { identity: null };
274
+ }
275
+ }
276
+ /**
277
+ * Load navigator system prompt from CLAUDE.md
278
+ */
279
+ function loadNavSystemPrompt(navDirectory) {
280
+ const claudeMdPath = path.join(navDirectory, "CLAUDE.md");
281
+ if (!fs.existsSync(claudeMdPath)) {
282
+ throw new Error(`Navigator CLAUDE.md not found at: ${claudeMdPath}`);
283
+ }
284
+ return fs.readFileSync(claudeMdPath, "utf-8");
285
+ }
286
+ /**
287
+ * Prompt user for input
288
+ */
289
+ function promptUser(question) {
290
+ const rl = readline.createInterface({
291
+ input: process.stdin,
292
+ output: process.stdout,
293
+ });
294
+ return new Promise((resolve) => {
295
+ rl.question(question, (answer) => {
296
+ rl.close();
297
+ resolve(answer.trim().toLowerCase());
298
+ });
299
+ });
300
+ }
301
+ /**
302
+ * Generate a commit message from the current diff using a quick LLM call
303
+ */
304
+ async function generateCommitMessage(codeDirectory, harness) {
305
+ const diff = getRecentDiff({ cwd: codeDirectory });
306
+ if (!diff)
307
+ return "chore: commit uncommitted changes";
308
+ // Truncate diff to avoid huge prompts
309
+ const truncatedDiff = diff.length > 4000
310
+ ? diff.substring(0, 4000) + "\n... (truncated)"
311
+ : diff;
312
+ const prompt = `Generate a single-line conventional commit message (e.g. "feat: ...", "fix: ...", "chore: ...") for these changes. Reply with ONLY the commit message, nothing else.\n\n${truncatedDiff}`;
313
+ try {
314
+ const session = harness.run({
315
+ model: "claude-haiku-4-5",
316
+ maxTurns: 1,
317
+ systemPrompt: "You generate concise conventional commit messages. Reply with only the commit message.",
318
+ cwd: codeDirectory,
319
+ permissionMode: "bypassPermissions",
320
+ allowedTools: [],
321
+ }, prompt);
322
+ let message = "";
323
+ for await (const event of session) {
324
+ if (event.type === "text") {
325
+ message += event.text;
326
+ }
327
+ }
328
+ // Clean up: take first line, strip quotes
329
+ const cleaned = message.trim().split("\n")[0]?.replace(/^["']|["']$/g, "").trim();
330
+ return cleaned || "chore: commit uncommitted changes";
331
+ }
332
+ catch {
333
+ return "chore: commit uncommitted changes";
334
+ }
335
+ }
336
+ /**
337
+ * Update the navigator's knowledge base with the implementer's summary.
338
+ * Calls the full `autonav update` pipeline (NavigatorAdapter.update) so the
339
+ * navigator agent can write to its own knowledge/ directory.
340
+ */
341
+ async function updateNavigatorKnowledge(navDirectory, summary, commitHash, verbose) {
342
+ const updateMessage = commitHash
343
+ ? `The implementer just completed work and committed ${commitHash}. Summary of what was implemented:\n\n${summary}`
344
+ : `The implementer just completed work (no commit). Summary:\n\n${summary}`;
345
+ try {
346
+ const navigator = loadNavigator(navDirectory);
347
+ const adapter = new NavigatorAdapter();
348
+ await adapter.update(navigator, updateMessage);
349
+ if (verbose) {
350
+ console.log("[Update] Navigator knowledge base updated");
351
+ }
352
+ }
353
+ catch (err) {
354
+ // Non-fatal — log and continue
355
+ if (verbose) {
356
+ console.log(chalk.yellow(`[Update] Failed to update navigator: ${err instanceof Error ? err.message : err}`));
357
+ }
358
+ }
359
+ }
360
+ /**
361
+ * Ask the navigator to review uncommitted changes, fix issues, then commit
362
+ */
363
+ async function reviewAndFixChanges(codeDirectory, navDirectory, _navSystemPrompt, navIdentity, options, harness) {
364
+ const { verbose = false } = options;
365
+ const diff = getRecentDiff({ cwd: codeDirectory });
366
+ if (!diff)
367
+ return;
368
+ const truncatedDiff = diff.length > 8000
369
+ ? diff.substring(0, 8000) + "\n... (truncated)"
370
+ : diff;
371
+ const navName = navIdentity?.name || "navigator";
372
+ // Step 1: Ask navigator for review (single-turn, no tool use)
373
+ console.log(chalk.dim(`\nAsking ${navName} to review changes...`));
374
+ const reviewPrompt = `Review the following diff for bugs, correctness issues, or missing error handling. Do NOT use any tools — just read the diff and respond.
375
+
376
+ Respond in EXACTLY one of these formats:
377
+
378
+ If no issues: Reply with only "LGTM"
379
+
380
+ If issues found: Reply with a bullet list, one issue per line:
381
+ - [file:line] Issue description. Fix: what to do.
382
+ - [file:line] Issue description. Fix: what to do.
383
+
384
+ Then add a blank line and implementation instructions for an automated agent to fix each issue.
385
+
386
+ Do NOT suggest style improvements, refactors, or nice-to-haves. Only flag things that are bugs or will cause runtime errors.
387
+
388
+ \`\`\`diff
389
+ ${truncatedDiff}
390
+ \`\`\``;
391
+ let reviewResult = "";
392
+ try {
393
+ const reviewSession = harness.run({
394
+ model: options.navModel || "claude-opus-4-5",
395
+ maxTurns: 1,
396
+ systemPrompt: "You are a code reviewer. Be concise and actionable. Never use tools — respond directly.",
397
+ cwd: navDirectory,
398
+ permissionMode: "bypassPermissions",
399
+ allowedTools: [],
400
+ }, reviewPrompt);
401
+ for await (const event of reviewSession) {
402
+ if (event.type === "text") {
403
+ reviewResult += event.text;
404
+ }
405
+ }
406
+ }
407
+ catch (err) {
408
+ console.log(chalk.yellow(`\nReview failed: ${err instanceof Error ? err.message : err}`));
409
+ console.log(chalk.dim("Committing as-is."));
410
+ stageAllChanges({ cwd: codeDirectory });
411
+ const commitMsg = await generateCommitMessage(codeDirectory, harness);
412
+ const hash = commitChanges(commitMsg, { cwd: codeDirectory, verbose });
413
+ if (hash)
414
+ console.log(`\n${chalk.green("Committed:")} ${hash}\n`);
415
+ return;
416
+ }
417
+ // Check if navigator said LGTM
418
+ if (reviewResult.trim().toUpperCase().startsWith("LGTM")) {
419
+ console.log(chalk.green(`\n${navName} says: LGTM`));
420
+ stageAllChanges({ cwd: codeDirectory });
421
+ const commitMsg = await generateCommitMessage(codeDirectory, harness);
422
+ const hash = commitChanges(commitMsg, { cwd: codeDirectory, verbose });
423
+ if (hash)
424
+ console.log(`${chalk.green("Committed:")} ${hash}\n`);
425
+ return;
426
+ }
427
+ // Step 2: Show bullet points of issues found
428
+ const bulletLines = reviewResult.trim().split("\n").filter(l => l.trim().startsWith("- "));
429
+ console.log(chalk.dim(`\n${navName} found ${bulletLines.length} issue${bulletLines.length !== 1 ? "s" : ""}:`));
430
+ for (const bullet of bulletLines) {
431
+ console.log(chalk.yellow(` ${bullet}`));
432
+ }
433
+ if (verbose) {
434
+ // Show full review including implementation instructions
435
+ console.log(chalk.dim("\nFull review:"));
436
+ console.log(reviewResult);
437
+ }
438
+ console.log(chalk.dim("\nFixing issues..."));
439
+ const { buildImplementerPrompt, buildImplementerSystemPrompt } = await import("./prompts.js");
440
+ // Build a plan from the review
441
+ const fixPlan = {
442
+ summary: "Fix issues found in code review",
443
+ steps: [{ description: reviewResult }],
444
+ validationCriteria: ["All review issues addressed"],
445
+ isComplete: false,
446
+ };
447
+ const fixPrompt = buildImplementerPrompt(codeDirectory, fixPlan);
448
+ const fixSystemPrompt = buildImplementerSystemPrompt(codeDirectory);
449
+ try {
450
+ const fixSession = harness.run({
451
+ model: options.model || "claude-haiku-4-5",
452
+ maxTurns: options.maxTurns || 50,
453
+ systemPrompt: fixSystemPrompt,
454
+ cwd: codeDirectory,
455
+ permissionMode: "bypassPermissions",
456
+ }, fixPrompt);
457
+ for await (const event of fixSession) {
458
+ if (verbose && event.type === "tool_use") {
459
+ console.log(`[Fix] Tool: ${event.name}`);
460
+ }
461
+ }
462
+ }
463
+ catch (err) {
464
+ console.log(chalk.yellow(`\nFix failed: ${err instanceof Error ? err.message : err}`));
465
+ console.log(chalk.dim("Committing as-is."));
466
+ }
467
+ // Step 3: Commit everything (original changes + fixes)
468
+ stageAllChanges({ cwd: codeDirectory });
469
+ const commitMsg = await generateCommitMessage(codeDirectory, harness);
470
+ const hash = commitChanges(commitMsg, { cwd: codeDirectory, verbose });
471
+ if (hash)
472
+ console.log(`\n${chalk.green("Committed:")} ${hash}\n`);
473
+ }
474
+ /**
475
+ * Review implementation changes (Phase 3 of the 4-phase iteration loop).
476
+ *
477
+ * Runs up to 5 review-fix cycles:
478
+ * 1. Stage changes and get diff
479
+ * 2. Ask opus to review (single-turn, no tools)
480
+ * 3. If LGTM → done
481
+ * 4. If issues → ask haiku to fix, then re-review
482
+ *
483
+ * Returns whether the review passed and whether fixes were applied.
484
+ */
485
+ async function reviewImplementation(codeDirectory, navDirectory, options, animation, verbose, harness) {
486
+ const MAX_REVIEW_ROUNDS = 5;
487
+ let fixApplied = false;
488
+ for (let round = 1; round <= MAX_REVIEW_ROUNDS; round++) {
489
+ // Stage and get diff
490
+ stageAllChanges({ cwd: codeDirectory });
491
+ const diff = getRecentDiff({ cwd: codeDirectory });
492
+ if (!diff) {
493
+ // No changes to review
494
+ return { lgtm: true, fixApplied };
495
+ }
496
+ // Update animation for review phase (greenBright bold to stand out)
497
+ animation.setMessageColor(chalk.greenBright.bold);
498
+ animation.setMessage(`Reviewing... (round ${round}/${MAX_REVIEW_ROUNDS})`);
499
+ animation.resetTurns();
500
+ if (verbose) {
501
+ console.log(`\n[Review] Round ${round}/${MAX_REVIEW_ROUNDS}`);
502
+ }
503
+ // Ask opus to review the diff (single-turn, no tools)
504
+ let reviewResult = "";
505
+ try {
506
+ const reviewSession = harness.run({
507
+ model: options.navModel || "claude-opus-4-5",
508
+ maxTurns: 1,
509
+ systemPrompt: "You are a code reviewer. Be concise and actionable. Never use tools — respond directly.",
510
+ cwd: navDirectory,
511
+ permissionMode: "bypassPermissions",
512
+ allowedTools: [],
513
+ }, buildReviewPrompt(diff));
514
+ for await (const event of reviewSession) {
515
+ if (event.type === "text") {
516
+ reviewResult += event.text;
517
+ }
518
+ }
519
+ }
520
+ catch (err) {
521
+ if (verbose) {
522
+ console.log(chalk.yellow(`\n[Review] Failed: ${err instanceof Error ? err.message : err}`));
523
+ }
524
+ // Review failed — commit as-is
525
+ return { lgtm: false, fixApplied };
526
+ }
527
+ // Check if LGTM
528
+ if (reviewResult.trim().toUpperCase().startsWith("LGTM")) {
529
+ if (verbose) {
530
+ console.log(chalk.green("[Review] LGTM"));
531
+ }
532
+ return { lgtm: true, fixApplied };
533
+ }
534
+ // Show issues found
535
+ const bulletLines = reviewResult
536
+ .trim()
537
+ .split("\n")
538
+ .filter((l) => l.trim().startsWith("- "));
539
+ if (!verbose) {
540
+ animation.stop();
541
+ console.log(chalk.dim(` └─ Review round ${round}: ${bulletLines.length} issue${bulletLines.length !== 1 ? "s" : ""}`));
542
+ for (const bullet of bulletLines) {
543
+ console.log(chalk.yellow(` ${bullet}`));
544
+ }
545
+ }
546
+ else {
547
+ console.log(`[Review] Found ${bulletLines.length} issue${bulletLines.length !== 1 ? "s" : ""}:`);
548
+ for (const bullet of bulletLines) {
549
+ console.log(chalk.yellow(` ${bullet}`));
550
+ }
551
+ }
552
+ // Pick mood for fix phase based on round
553
+ const moodPool = REVIEW_FIX_MOODS[round - 1] ?? REVIEW_FIX_MOODS[4];
554
+ const fixMood = randomFrom(moodPool);
555
+ if (!verbose) {
556
+ animation.setMessage(fixMood);
557
+ animation.resetTurns();
558
+ animation.start();
559
+ }
560
+ // Ask haiku to fix the issues
561
+ try {
562
+ const fixSession = harness.run({
563
+ model: options.model || "claude-haiku-4-5",
564
+ maxTurns: options.maxTurns || 50,
565
+ systemPrompt: buildFixSystemPrompt(codeDirectory),
566
+ cwd: codeDirectory,
567
+ permissionMode: "bypassPermissions",
568
+ }, buildFixPrompt(codeDirectory, reviewResult));
569
+ for await (const event of fixSession) {
570
+ if (event.type === "tool_use") {
571
+ if (verbose) {
572
+ console.log(`[Fix] Tool: ${event.name}`);
573
+ }
574
+ animation.setLastTool(event.name);
575
+ animation.incrementTurns();
576
+ }
577
+ }
578
+ fixApplied = true;
579
+ }
580
+ catch (err) {
581
+ if (verbose) {
582
+ console.log(chalk.yellow(`\n[Fix] Failed: ${err instanceof Error ? err.message : err}`));
583
+ }
584
+ // Fix failed — commit as-is
585
+ return { lgtm: false, fixApplied };
586
+ }
587
+ }
588
+ // Exhausted all rounds without LGTM
589
+ if (verbose) {
590
+ console.log(chalk.yellow("[Review] Max review rounds reached — committing anyway"));
591
+ }
592
+ return { lgtm: false, fixApplied };
593
+ }
594
+ /**
595
+ * Handle uncommitted changes in the code directory
596
+ *
597
+ * The memento loop provides git history to the navigator as context about
598
+ * what the worker has accomplished. Uncommitted changes won't appear in
599
+ * that summary, which could cause confusion.
600
+ */
601
+ async function handleUncommittedChanges(codeDirectory, navDirectory, navSystemPrompt, navIdentity, options, harness) {
602
+ const { verbose = false } = options;
603
+ if (!hasUncommittedChanges({ cwd: codeDirectory })) {
604
+ return;
605
+ }
606
+ const navName = navIdentity?.name || "navigator";
607
+ // Show warning
608
+ console.log(chalk.yellow("\nUncommitted changes detected.\n"));
609
+ console.log(chalk.dim("The memento loop uses git history as context for the navigator."));
610
+ console.log(chalk.dim("Uncommitted changes won't be visible.\n"));
611
+ // Show brief diff summary
612
+ const diff = getRecentDiff({ cwd: codeDirectory });
613
+ if (diff) {
614
+ const lineCount = diff.split("\n").length;
615
+ const addedCount = diff.split("\n").filter(l => l.startsWith("+") && !l.startsWith("+++")).length;
616
+ const removedCount = diff.split("\n").filter(l => l.startsWith("-") && !l.startsWith("---")).length;
617
+ console.log(chalk.dim(` ${lineCount} lines changed (${chalk.green(`+${addedCount}`)} ${chalk.red(`-${removedCount}`)})`));
618
+ console.log("");
619
+ }
620
+ console.log(" [c] Commit (auto-generate message)");
621
+ console.log(` [r] Ask ${navName} to review, fix issues, then commit`);
622
+ console.log(" [d] Discard changes");
623
+ console.log(" [q] Quit\n");
624
+ const answer = await promptUser("Choice [c/r/d/q]: ");
625
+ switch (answer) {
626
+ case "c":
627
+ case "commit": {
628
+ console.log(chalk.dim("\nGenerating commit message..."));
629
+ stageAllChanges({ cwd: codeDirectory });
630
+ const message = await generateCommitMessage(codeDirectory, harness);
631
+ const hash = commitChanges(message, { cwd: codeDirectory, verbose });
632
+ if (hash) {
633
+ console.log(`${chalk.green("Committed:")} ${hash} - ${message}\n`);
634
+ }
635
+ break;
636
+ }
637
+ case "r":
638
+ case "review": {
639
+ await reviewAndFixChanges(codeDirectory, navDirectory, navSystemPrompt, navIdentity, options, harness);
640
+ break;
641
+ }
642
+ case "d":
643
+ case "discard": {
644
+ console.log(chalk.red("\nThis will permanently delete uncommitted changes!"));
645
+ const confirm = await promptUser("Type 'yes' to confirm: ");
646
+ if (confirm !== "yes") {
647
+ throw new Error("Discard cancelled");
648
+ }
649
+ const { execSync } = await import("node:child_process");
650
+ execSync("git checkout -- . && git clean -fd", {
651
+ cwd: codeDirectory,
652
+ stdio: verbose ? "inherit" : "pipe",
653
+ });
654
+ console.log(chalk.green("\nChanges discarded.\n"));
655
+ break;
656
+ }
657
+ case "q":
658
+ case "quit":
659
+ default:
660
+ throw new Error("Aborted by user");
661
+ }
662
+ }
663
+ /**
664
+ * Query the navigator for an implementation plan (with stats tracking)
665
+ */
666
+ async function queryNavForPlanWithStats(codeDirectory, navDirectory, task, iteration, maxIterations, navSystemPrompt, navIdentity, gitLog, options, animation, verbose, harness) {
667
+ // Navigator uses opus by default for better planning
668
+ const { navModel: model = "claude-opus-4-5", maxTurns = 50 } = options;
669
+ // Create tool server via harness
670
+ const navProtocol = createNavProtocolTools();
671
+ const { server } = harness.createToolServer("autonav-nav-protocol", navProtocol.tools);
672
+ const prompt = buildNavPlanPrompt({ codeDirectory, task, iteration, maxIterations, branch: options.branch }, gitLog, navIdentity);
673
+ const systemPrompt = buildNavSystemPrompt(navSystemPrompt);
674
+ if (verbose) {
675
+ console.log("\n[Nav] Querying navigator for plan...");
676
+ console.log(`[Nav] Model: ${model}`);
677
+ }
678
+ // Capture stderr from Claude Code process for diagnostics
679
+ const stderrLines = [];
680
+ const session = harness.run({
681
+ model,
682
+ maxTurns,
683
+ systemPrompt,
684
+ cwd: navDirectory,
685
+ additionalDirectories: [codeDirectory],
686
+ mcpServers: {
687
+ "autonav-nav-protocol": server,
688
+ },
689
+ permissionMode: "bypassPermissions",
690
+ disallowedTools: ["Write", "Edit", "Bash"],
691
+ stderr: (data) => {
692
+ stderrLines.push(data);
693
+ if (DEBUG) {
694
+ process.stderr.write(`[Nav stderr] ${data}`);
695
+ }
696
+ },
697
+ }, prompt);
698
+ let resultEvent;
699
+ let lastTool;
700
+ const mood = { toolCount: 0, lastError: false, consecutiveSuccess: 0 };
701
+ try {
702
+ for await (const event of session) {
703
+ // Detect errors from tool results
704
+ if (event.type === "tool_result" && event.isError) {
705
+ mood.lastError = true;
706
+ mood.consecutiveSuccess = 0;
707
+ animation.setMessage(pickMood("nav", lastTool || "", {}, mood));
708
+ }
709
+ // Detect rate limit errors in-stream
710
+ if (event.type === "error" && event.retryable) {
711
+ const stderr = filterStderr(stderrLines);
712
+ throw new Error(`Rate limit reached during navigator query${stderr ? `\nStderr:\n${stderr}` : ""}`);
713
+ }
714
+ if (event.type === "tool_use") {
715
+ const toolName = event.name.split("__").pop() || event.name;
716
+ lastTool = toolName;
717
+ mood.toolCount += 1;
718
+ if (!mood.lastError) {
719
+ mood.consecutiveSuccess += 1;
720
+ }
721
+ mood.lastError = false;
722
+ if (verbose) {
723
+ console.log(`[Nav] Tool: ${toolName}`);
724
+ }
725
+ animation.setMessage(pickMood("nav", toolName, event.input, mood));
726
+ animation.setLastTool(toolName);
727
+ animation.incrementTurns();
728
+ }
729
+ if (event.type === "result") {
730
+ resultEvent = event;
731
+ }
732
+ }
733
+ }
734
+ catch (err) {
735
+ // Enrich the error with filtered stderr (strip the noisy spawn command line).
736
+ const stderr = filterStderr(stderrLines);
737
+ const base = err instanceof Error ? err.message : String(err);
738
+ throw new Error(`Navigator query failed: ${base}${stderr ? `\nStderr:\n${stderr}` : ""}`);
739
+ }
740
+ // Extract token usage from result
741
+ const tokensUsed = (resultEvent?.usage?.inputTokens ?? 0) + (resultEvent?.usage?.outputTokens ?? 0);
742
+ if (verbose) {
743
+ console.log(`[Nav] Result: success=${resultEvent?.success}`);
744
+ console.log(`[Nav] Usage: input=${resultEvent?.usage?.inputTokens}, output=${resultEvent?.usage?.outputTokens}, total=${tokensUsed}`);
745
+ console.log(`[Nav] Total cost: $${resultEvent?.costUsd?.toFixed(4) || "?"}`);
746
+ }
747
+ if (!resultEvent || !resultEvent.success) {
748
+ const errorDetails = resultEvent?.text || "Unknown error";
749
+ const stderr = filterStderr(stderrLines);
750
+ throw new Error(`Navigator query failed: ${errorDetails}${stderr ? `\nStderr:\n${stderr}` : ""}`);
751
+ }
752
+ // Get the captured plan from the tool server closure
753
+ const plan = navProtocol.getCapturedPlan();
754
+ if (!plan) {
755
+ throw new Error("Navigator did not submit a plan. The navigator must use the submit_implementation_plan tool.");
756
+ }
757
+ if (verbose) {
758
+ console.log(`[Nav] Plan received: ${plan.summary}`);
759
+ console.log(`[Nav] Steps: ${plan.steps.length}`);
760
+ console.log(`[Nav] Complete: ${plan.isComplete}`);
761
+ }
762
+ return { plan, tokensUsed, lastTool };
763
+ }
764
+ /**
765
+ * Run implementer agent with stats tracking.
766
+ *
767
+ * Retries internally on rate limits and transient errors rather than
768
+ * bailing out. The implementer keeps going until it succeeds or
769
+ * Retries forever with exponential backoff (capped at 4h).
770
+ */
771
+ async function runImplementerAgentWithStats(context, plan, options, animation, harness) {
772
+ // Implementer uses haiku by default for faster/cheaper implementation
773
+ const { verbose = false, model = "claude-haiku-4-5", maxTurns = 50 } = options;
774
+ const { buildImplementerPrompt, buildImplementerSystemPrompt } = await import("./prompts.js");
775
+ const prompt = buildImplementerPrompt(context.codeDirectory, plan);
776
+ const systemPrompt = buildImplementerSystemPrompt(context.codeDirectory);
777
+ if (verbose) {
778
+ console.log("\n[Implementer] Starting implementation...");
779
+ console.log(`[Implementer] Model: ${model}`);
780
+ }
781
+ // Accumulate across retries
782
+ const allFilesModified = [];
783
+ let totalTokensUsed = 0;
784
+ let lastTool;
785
+ let lastAssistantText = "";
786
+ for (let attempt = 0;; attempt++) {
787
+ // Capture stderr from Claude Code process for diagnostics
788
+ const implStderrLines = [];
789
+ const session = harness.run({
790
+ model,
791
+ maxTurns,
792
+ systemPrompt,
793
+ cwd: context.codeDirectory,
794
+ permissionMode: "bypassPermissions",
795
+ stderr: (data) => {
796
+ implStderrLines.push(data);
797
+ if (DEBUG) {
798
+ process.stderr.write(`[Impl stderr] ${data}`);
799
+ }
800
+ },
801
+ }, prompt);
802
+ let resultEvent;
803
+ let rateLimitSeen = false;
804
+ const mood = { toolCount: 0, lastError: false, consecutiveSuccess: 0 };
805
+ try {
806
+ for await (const event of session) {
807
+ // Detect errors from tool results
808
+ if (event.type === "tool_result" && event.isError) {
809
+ mood.lastError = true;
810
+ mood.consecutiveSuccess = 0;
811
+ animation.setMessage(pickMood("impl", lastTool || "", {}, mood));
812
+ }
813
+ // Track rate limit errors in-stream (don't bail, just note it)
814
+ if (event.type === "error" && event.retryable) {
815
+ rateLimitSeen = true;
816
+ }
817
+ if (event.type === "tool_use") {
818
+ const toolName = event.name;
819
+ lastTool = toolName;
820
+ mood.toolCount += 1;
821
+ if (!mood.lastError) {
822
+ mood.consecutiveSuccess += 1;
823
+ }
824
+ mood.lastError = false;
825
+ if (verbose) {
826
+ console.log(`[Implementer] Tool: ${toolName}`);
827
+ }
828
+ animation.setMessage(pickMood("impl", toolName, event.input, mood));
829
+ animation.setLastTool(toolName);
830
+ animation.incrementTurns();
831
+ // Track file operations
832
+ if (isWriteTool(toolName)) {
833
+ const filePath = event.input.file_path || event.input.path;
834
+ if (typeof filePath === "string" && !allFilesModified.includes(filePath)) {
835
+ allFilesModified.push(filePath);
836
+ }
837
+ }
838
+ }
839
+ else if (event.type === "text") {
840
+ lastAssistantText = event.text;
841
+ }
842
+ if (event.type === "result") {
843
+ resultEvent = event;
844
+ }
845
+ }
846
+ }
847
+ catch (err) {
848
+ // Harness throws when the underlying process crashes.
849
+ // Check if it's a rate limit — if so, wait and retry.
850
+ const stderr = filterStderr(implStderrLines);
851
+ const base = err instanceof Error ? err.message : String(err);
852
+ const errorText = `${base}${stderr ? `\n${stderr}` : ""}`;
853
+ const rateLimitInfo = isRateLimitError(errorText);
854
+ if (rateLimitInfo.isRateLimited) {
855
+ await waitForRateLimit(rateLimitInfo, attempt, animation, verbose);
856
+ animation.setMessage("Implementer retrying...");
857
+ animation.setTokens(totalTokensUsed);
858
+ continue;
859
+ }
860
+ // Check for transient connection errors (stale connections, CGNAT, etc.)
861
+ if (isTransientConnectionError(errorText)) {
862
+ const waitSec = getConnectionRetryDelay(attempt);
863
+ animation.stop();
864
+ console.log("");
865
+ console.log(chalk.yellow(`⚡ Connection error (attempt ${attempt + 1})`));
866
+ console.log(chalk.dim(` ${base}`));
867
+ console.log(chalk.dim(` Reconnecting in ${formatDuration(waitSec)}...`));
868
+ await waitWithCountdown(waitSec, (remaining, formatted) => {
869
+ if (remaining % 5 === 0 || remaining <= 5) {
870
+ process.stdout.write(`\r${chalk.dim(` Reconnecting in ${formatted}...`)}${" ".repeat(20)}`);
871
+ }
872
+ });
873
+ console.log(`\r${chalk.green(" Reconnecting...")}${" ".repeat(30)}`);
874
+ if (!verbose)
875
+ animation.start();
876
+ animation.setMessage("Implementer retrying...");
877
+ animation.setTokens(totalTokensUsed);
878
+ continue;
879
+ }
880
+ // Non-retryable error (auth, billing, etc.)
881
+ return {
882
+ success: false,
883
+ summary: `Implementer crashed: ${base}`,
884
+ filesModified: allFilesModified,
885
+ errors: [errorText],
886
+ tokensUsed: totalTokensUsed,
887
+ lastTool,
888
+ };
889
+ }
890
+ // Extract token usage
891
+ const iterTokens = (resultEvent?.usage?.inputTokens ?? 0) + (resultEvent?.usage?.outputTokens ?? 0);
892
+ totalTokensUsed += iterTokens;
893
+ if (verbose) {
894
+ console.log(`[Implementer] Result: success=${resultEvent?.success}`);
895
+ console.log(`[Implementer] Usage: input=${resultEvent?.usage?.inputTokens}, output=${resultEvent?.usage?.outputTokens}, total=${iterTokens}`);
896
+ console.log(`[Implementer] Total cost: $${resultEvent?.costUsd?.toFixed(4) || "?"}`);
897
+ }
898
+ const implStderr = filterStderr(implStderrLines);
899
+ // Check if the run failed due to rate limits — if so, wait and retry
900
+ if (!resultEvent || !resultEvent.success || rateLimitSeen) {
901
+ const errorDetails = resultEvent?.text || "No result message received";
902
+ const fullError = `${errorDetails}${implStderr ? `\nStderr: ${implStderr}` : ""}`;
903
+ // Check if this failure is rate-limit related
904
+ const rateLimitInfo = isRateLimitError(fullError);
905
+ const isRateLimit = rateLimitInfo.isRateLimited || rateLimitSeen;
906
+ if (isRateLimit) {
907
+ // Use parsed info if available, otherwise check stderr
908
+ const stderrInfo = !rateLimitInfo.isRateLimited
909
+ ? parseRateLimitError(implStderr || "rate_limit")
910
+ : rateLimitInfo;
911
+ await waitForRateLimit(stderrInfo, attempt, animation, verbose);
912
+ animation.setMessage("Implementer retrying...");
913
+ animation.setTokens(totalTokensUsed);
914
+ continue;
915
+ }
916
+ // Non-retryable failure or max retries exhausted
917
+ return {
918
+ success: false,
919
+ summary: resultEvent
920
+ ? `Implementer failed`
921
+ : "No result message received",
922
+ filesModified: allFilesModified,
923
+ errors: [fullError],
924
+ tokensUsed: totalTokensUsed,
925
+ lastTool,
926
+ };
927
+ }
928
+ // Success!
929
+ return {
930
+ success: true,
931
+ summary: resultEvent.text || lastAssistantText || "Implementation completed",
932
+ filesModified: allFilesModified,
933
+ tokensUsed: totalTokensUsed,
934
+ lastTool,
935
+ };
936
+ }
937
+ // Unreachable — infinite loop above always returns
938
+ throw new Error("Unreachable");
939
+ }
940
+ /**
941
+ * Print iteration summary
942
+ */
943
+ function printIterationSummary(iteration, data) {
944
+ const added = data.linesAdded > 0 ? chalk.green(`+${data.linesAdded}`) : chalk.dim("+0");
945
+ const removed = data.linesRemoved > 0 ? chalk.red(`-${data.linesRemoved}`) : chalk.dim("-0");
946
+ console.log(chalk.dim("─".repeat(50)));
947
+ console.log(`${chalk.bold(`Iteration ${iteration}`)} │ ` +
948
+ `${chalk.dim("Diff:")} ${added}/${removed} │ ` +
949
+ `${chalk.dim("Tokens:")} ${chalk.cyan(data.tokensUsed.toLocaleString())} │ ` +
950
+ `${chalk.dim("Tool:")} ${chalk.yellow(data.lastTool || "--")}`);
951
+ if (data.commitHash) {
952
+ console.log(`${chalk.dim("Commit:")} ${chalk.green(data.commitHash)} - ${data.plan || ""}`);
953
+ }
954
+ else {
955
+ console.log(`${chalk.dim("No changes committed")}`);
956
+ }
957
+ if (data.isComplete && data.completionMessage) {
958
+ console.log(`\n${chalk.blue("📋 Navigator:")} ${data.completionMessage}`);
959
+ console.log(chalk.dim(" (Loop continues - use --max-iterations to limit)"));
960
+ }
961
+ console.log("");
962
+ }
963
+ /**
964
+ * Run the memento loop
965
+ *
966
+ * Coordinates navigator planning and worker implementation in iterations
967
+ * until the task is complete or max iterations reached.
968
+ *
969
+ * All state is in-memory. Git history is the only persistent context
970
+ * visible to the agents.
971
+ */
972
+ export async function runMementoLoop(codeDirectory, navDirectory, task, options, harness) {
973
+ const startTime = Date.now();
974
+ const { verbose = false, pr = false, maxIterations = 0 } = options;
975
+ // Resolve paths
976
+ codeDirectory = path.resolve(codeDirectory);
977
+ navDirectory = path.resolve(navDirectory);
978
+ // Validate directories exist
979
+ if (!fs.existsSync(codeDirectory)) {
980
+ throw new Error(`Code directory not found: ${codeDirectory}`);
981
+ }
982
+ if (!fs.existsSync(navDirectory)) {
983
+ throw new Error(`Navigator directory not found: ${navDirectory}`);
984
+ }
985
+ // Load navigator config and system prompt
986
+ const navConfig = loadNavConfig(navDirectory);
987
+ const navIdentity = navConfig.identity;
988
+ const navSystemPrompt = loadNavSystemPrompt(navDirectory);
989
+ // In-memory state
990
+ const state = {
991
+ iteration: 0,
992
+ planHistory: [],
993
+ stats: {
994
+ linesAdded: 0,
995
+ linesRemoved: 0,
996
+ tokensUsed: 0,
997
+ },
998
+ };
999
+ if (verbose) {
1000
+ console.log(`\n[Memento] Starting loop`);
1001
+ console.log(`[Memento] Task: ${task.substring(0, 100)}...`);
1002
+ console.log(`[Memento] Code dir: ${codeDirectory}`);
1003
+ console.log(`[Memento] Nav dir: ${navDirectory}`);
1004
+ }
1005
+ // Ensure code directory is a git repo
1006
+ ensureGitRepo({ cwd: codeDirectory, verbose });
1007
+ // Check for uncommitted changes - navigator can't see them!
1008
+ await handleUncommittedChanges(codeDirectory, navDirectory, navSystemPrompt, navIdentity, options, harness);
1009
+ // Create or switch to branch if specified
1010
+ if (options.branch) {
1011
+ createBranch(options.branch, { cwd: codeDirectory, verbose });
1012
+ }
1013
+ const errors = [];
1014
+ try {
1015
+ // Main loop - only max-iterations stops the loop (0 = unlimited = run forever)
1016
+ while (maxIterations === 0 || state.iteration < maxIterations) {
1017
+ state.iteration += 1;
1018
+ if (verbose) {
1019
+ console.log(`\n${"=".repeat(60)}`);
1020
+ console.log(`[Memento] Iteration ${state.iteration}`);
1021
+ console.log(`${"=".repeat(60)}`);
1022
+ }
1023
+ else {
1024
+ console.log(`\nIteration ${state.iteration}...`);
1025
+ }
1026
+ // Get git log to show the navigator what the implementer has accomplished
1027
+ const gitLog = getRecentGitLog({ cwd: codeDirectory, count: 20 });
1028
+ // Create animation with cumulative stats
1029
+ const animation = new MatrixAnimation({
1030
+ message: `Consulting ${navIdentity?.name || "navigator"}...`,
1031
+ width: 50,
1032
+ lines: 3,
1033
+ });
1034
+ // Initialize animation with cumulative diff stats and models
1035
+ // Token counter starts at 0 - will show implementer tokens once that phase begins
1036
+ const implementerModel = options.model || "claude-haiku-4-5";
1037
+ const navigatorModel = options.navModel || "claude-opus-4-5";
1038
+ animation.setStats({
1039
+ iteration: state.iteration,
1040
+ maxIterations: maxIterations || undefined,
1041
+ linesAdded: state.stats.linesAdded,
1042
+ linesRemoved: state.stats.linesRemoved,
1043
+ tokensUsed: 0, // Start fresh - will show implementer tokens
1044
+ lastTool: state.stats.lastTool,
1045
+ });
1046
+ animation.setModels(implementerModel, navigatorModel);
1047
+ if (!verbose) {
1048
+ animation.start();
1049
+ }
1050
+ let plan = null;
1051
+ let iterationTokens = 0;
1052
+ let implementerTokens = 0;
1053
+ let implementerSummary = "";
1054
+ try {
1055
+ // Query navigator for plan (with rate limit retry)
1056
+ let navResult = null;
1057
+ let navRateLimitAttempt = 0;
1058
+ while (navResult === null) {
1059
+ try {
1060
+ navResult = await queryNavForPlanWithStats(codeDirectory, navDirectory, task, state.iteration, maxIterations, navSystemPrompt, navIdentity, gitLog, options, animation, verbose, harness);
1061
+ }
1062
+ catch (navError) {
1063
+ const rateLimitInfo = isRateLimitError(navError);
1064
+ if (rateLimitInfo.isRateLimited) {
1065
+ await waitForRateLimit(rateLimitInfo, navRateLimitAttempt, animation, verbose);
1066
+ navRateLimitAttempt++;
1067
+ continue;
1068
+ }
1069
+ // Not a rate limit or max retries exceeded - re-throw
1070
+ throw navError;
1071
+ }
1072
+ }
1073
+ plan = navResult.plan;
1074
+ iterationTokens += navResult.tokensUsed;
1075
+ state.stats.tokensUsed += navResult.tokensUsed;
1076
+ if (navResult.lastTool) {
1077
+ state.stats.lastTool = navResult.lastTool;
1078
+ }
1079
+ if (!plan) {
1080
+ errors.push(`Iteration ${state.iteration}: Navigator did not provide a plan`);
1081
+ break;
1082
+ }
1083
+ // Record plan in memory (for PR body)
1084
+ state.planHistory.push({ iteration: state.iteration, summary: plan.summary });
1085
+ // Show what the implementer will be working on (truncate to ~60 chars)
1086
+ const shortSummary = plan.summary.length > 60
1087
+ ? plan.summary.substring(0, 57) + "..."
1088
+ : plan.summary;
1089
+ if (!verbose) {
1090
+ // Stop animation, print plan summary, restart below it
1091
+ animation.stop();
1092
+ console.log(chalk.dim(` └─ ${shortSummary}`));
1093
+ animation.start();
1094
+ }
1095
+ else {
1096
+ console.log(`[Plan] ${plan.summary}`);
1097
+ }
1098
+ // Log if navigator thinks task is complete (advisory only - doesn't stop loop)
1099
+ if (plan.isComplete) {
1100
+ state.completionMessage = plan.completionMessage;
1101
+ }
1102
+ // Update animation for implementer phase
1103
+ // Reset counters for fresh implementer session (implementer forgets between iterations)
1104
+ animation.setMessage("Implementer implementing...");
1105
+ animation.setTokens(0);
1106
+ animation.resetTurns();
1107
+ // Run implementer to implement the plan
1108
+ // Rate limit retries are handled internally by runImplementerAgentWithStats
1109
+ const implementerResult = await runImplementerAgentWithStats({ codeDirectory, task }, plan, {
1110
+ verbose,
1111
+ model: options.model,
1112
+ maxTurns: options.maxTurns,
1113
+ }, animation, harness);
1114
+ implementerTokens = implementerResult.tokensUsed;
1115
+ implementerSummary = implementerResult.summary;
1116
+ iterationTokens += implementerResult.tokensUsed;
1117
+ state.stats.tokensUsed += implementerResult.tokensUsed;
1118
+ if (implementerResult.lastTool) {
1119
+ state.stats.lastTool = implementerResult.lastTool;
1120
+ }
1121
+ // Show only implementer tokens (fresh session each iteration)
1122
+ animation.setTokens(implementerTokens);
1123
+ if (!implementerResult.success) {
1124
+ const errDetail = implementerResult.errors?.join("; ") || "Unknown error";
1125
+ // Truncate long error messages (stderr can be very large)
1126
+ const truncated = errDetail.length > 500
1127
+ ? errDetail.substring(0, 500) + "... (truncated)"
1128
+ : errDetail;
1129
+ errors.push(`Iteration ${state.iteration}: Implementer failed - ${truncated}`);
1130
+ // Continue to next iteration - navigator can see the state and adjust
1131
+ }
1132
+ // Phase 3: Review
1133
+ // Animation is still running — reviewImplementation manages stop/start internally
1134
+ await reviewImplementation(codeDirectory, navDirectory, options, animation, verbose, harness);
1135
+ }
1136
+ finally {
1137
+ if (!verbose) {
1138
+ animation.stop();
1139
+ }
1140
+ }
1141
+ // Phase 4: Commit with LLM-generated message
1142
+ stageAllChanges({ cwd: codeDirectory });
1143
+ const commitMessage = await generateCommitMessage(codeDirectory, harness);
1144
+ const commitHash = commitChanges(commitMessage, { cwd: codeDirectory, verbose });
1145
+ // Get diff stats from the commit
1146
+ if (commitHash) {
1147
+ const diffStats = getLastCommitDiffStats({ cwd: codeDirectory });
1148
+ state.stats.linesAdded += diffStats.linesAdded;
1149
+ state.stats.linesRemoved += diffStats.linesRemoved;
1150
+ }
1151
+ // Update navigator knowledge base with what was implemented
1152
+ if (implementerSummary) {
1153
+ await updateNavigatorKnowledge(navDirectory, implementerSummary, commitHash, verbose);
1154
+ }
1155
+ // Print iteration summary (show implementer tokens for this iteration)
1156
+ printIterationSummary(state.iteration, {
1157
+ commitHash,
1158
+ plan: plan?.summary,
1159
+ linesAdded: state.stats.linesAdded,
1160
+ linesRemoved: state.stats.linesRemoved,
1161
+ tokensUsed: implementerTokens,
1162
+ lastTool: state.stats.lastTool,
1163
+ isComplete: plan?.isComplete,
1164
+ completionMessage: plan?.completionMessage,
1165
+ });
1166
+ }
1167
+ // Loop exited - must have hit max iterations
1168
+ console.log(`\nMax iterations (${maxIterations}) reached.`);
1169
+ // Handle PR creation if requested
1170
+ let prUrl;
1171
+ if (pr && options.branch) {
1172
+ if (!isGhAvailable()) {
1173
+ console.warn("\nWarning: gh CLI not available. Cannot create PR. Install and authenticate gh CLI.");
1174
+ }
1175
+ else {
1176
+ console.log("\nCreating pull request...");
1177
+ // Push branch
1178
+ pushBranch(options.branch, {
1179
+ cwd: codeDirectory,
1180
+ verbose,
1181
+ setUpstream: true,
1182
+ });
1183
+ // Create PR
1184
+ const prBody = `## Summary
1185
+
1186
+ ${state.completionMessage || task}
1187
+
1188
+ ## Iterations
1189
+
1190
+ ${state.planHistory.map((h) => `- **${h.iteration}**: ${h.summary}`).join("\n")}
1191
+
1192
+ ---
1193
+ *Created by autonav memento loop*`;
1194
+ prUrl = createPullRequest({
1195
+ cwd: codeDirectory,
1196
+ verbose,
1197
+ title: task.length > 70 ? `${task.substring(0, 67)}...` : task,
1198
+ body: prBody,
1199
+ });
1200
+ console.log(`PR created: ${prUrl}`);
1201
+ }
1202
+ }
1203
+ const durationMs = Date.now() - startTime;
1204
+ return {
1205
+ success: true, // Completed max iterations without fatal error
1206
+ iterations: state.iteration,
1207
+ completionMessage: state.completionMessage,
1208
+ prUrl,
1209
+ branch: options.branch || getCurrentBranch({ cwd: codeDirectory }),
1210
+ durationMs,
1211
+ errors: errors.length > 0 ? errors : undefined,
1212
+ };
1213
+ }
1214
+ catch (error) {
1215
+ const durationMs = Date.now() - startTime;
1216
+ const errorMessage = error instanceof Error ? error.message : String(error);
1217
+ errors.push(errorMessage);
1218
+ return {
1219
+ success: false,
1220
+ iterations: state.iteration,
1221
+ branch: options.branch || getCurrentBranch({ cwd: codeDirectory }),
1222
+ durationMs,
1223
+ errors,
1224
+ };
1225
+ }
1226
+ }
1227
+ //# sourceMappingURL=loop.js.map