@autonav/core 1.6.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 (257) 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 -6
  11. package/dist/cli/autonav.d.ts.map +1 -1
  12. package/dist/cli/autonav.js +32 -53
  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 +3 -2
  27. package/dist/cli/nav-memento.d.ts.map +1 -1
  28. package/dist/cli/nav-memento.js +12 -6
  29. package/dist/cli/nav-memento.js.map +1 -1
  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/implementer-agent.d.ts +31 -0
  143. package/dist/memento/implementer-agent.d.ts.map +1 -0
  144. package/dist/memento/implementer-agent.js +95 -0
  145. package/dist/memento/implementer-agent.js.map +1 -0
  146. package/dist/memento/index.d.ts +6 -5
  147. package/dist/memento/index.d.ts.map +1 -1
  148. package/dist/memento/index.js +7 -5
  149. package/dist/memento/index.js.map +1 -1
  150. package/dist/memento/loop.d.ts +6 -5
  151. package/dist/memento/loop.d.ts.map +1 -1
  152. package/dist/memento/loop.js +872 -180
  153. package/dist/memento/loop.js.map +1 -1
  154. package/dist/memento/matrix-animation.d.ts +26 -0
  155. package/dist/memento/matrix-animation.d.ts.map +1 -1
  156. package/dist/memento/matrix-animation.js +93 -18
  157. package/dist/memento/matrix-animation.js.map +1 -1
  158. package/dist/memento/nav-protocol.d.ts +2 -2
  159. package/dist/memento/nav-protocol.d.ts.map +1 -1
  160. package/dist/memento/nav-protocol.js +39 -43
  161. package/dist/memento/nav-protocol.js.map +1 -1
  162. package/dist/memento/prompts.d.ts +21 -8
  163. package/dist/memento/prompts.d.ts.map +1 -1
  164. package/dist/memento/prompts.js +79 -39
  165. package/dist/memento/prompts.js.map +1 -1
  166. package/dist/memento/rate-limit.d.ts +85 -0
  167. package/dist/memento/rate-limit.d.ts.map +1 -0
  168. package/dist/memento/rate-limit.js +221 -0
  169. package/dist/memento/rate-limit.js.map +1 -0
  170. package/dist/memento/types.d.ts +6 -6
  171. package/dist/memento/types.d.ts.map +1 -1
  172. package/dist/memento/types.js +3 -3
  173. package/dist/memento/worker-agent.d.ts +4 -1
  174. package/dist/memento/worker-agent.d.ts.map +1 -1
  175. package/dist/memento/worker-agent.js +38 -54
  176. package/dist/memento/worker-agent.js.map +1 -1
  177. package/dist/registry.d.ts +35 -0
  178. package/dist/registry.d.ts.map +1 -0
  179. package/dist/registry.js +87 -0
  180. package/dist/registry.js.map +1 -0
  181. package/dist/repo-analyzer/index.d.ts +2 -1
  182. package/dist/repo-analyzer/index.d.ts.map +1 -1
  183. package/dist/repo-analyzer/index.js +6 -17
  184. package/dist/repo-analyzer/index.js.map +1 -1
  185. package/dist/skill-generator/index.d.ts +142 -0
  186. package/dist/skill-generator/index.d.ts.map +1 -0
  187. package/dist/skill-generator/index.js +510 -0
  188. package/dist/skill-generator/index.js.map +1 -0
  189. package/dist/standup/config.d.ts +19 -0
  190. package/dist/standup/config.d.ts.map +1 -0
  191. package/dist/standup/config.js +42 -0
  192. package/dist/standup/config.js.map +1 -0
  193. package/dist/standup/index.d.ts +24 -0
  194. package/dist/standup/index.d.ts.map +1 -0
  195. package/dist/standup/index.js +29 -0
  196. package/dist/standup/index.js.map +1 -0
  197. package/dist/standup/loop.d.ts +36 -0
  198. package/dist/standup/loop.d.ts.map +1 -0
  199. package/dist/standup/loop.js +508 -0
  200. package/dist/standup/loop.js.map +1 -0
  201. package/dist/standup/prompts.d.ts +62 -0
  202. package/dist/standup/prompts.d.ts.map +1 -0
  203. package/dist/standup/prompts.js +211 -0
  204. package/dist/standup/prompts.js.map +1 -0
  205. package/dist/standup/standup-protocol.d.ts +33 -0
  206. package/dist/standup/standup-protocol.d.ts.map +1 -0
  207. package/dist/standup/standup-protocol.js +189 -0
  208. package/dist/standup/standup-protocol.js.map +1 -0
  209. package/dist/standup/types.d.ts +185 -0
  210. package/dist/standup/types.d.ts.map +1 -0
  211. package/dist/standup/types.js +67 -0
  212. package/dist/standup/types.js.map +1 -0
  213. package/dist/templates/.gitignore.template +26 -0
  214. package/dist/templates/CLAUDE-pack.md.template +114 -0
  215. package/dist/templates/CLAUDE.md.template +153 -0
  216. package/dist/templates/README.md.template +174 -0
  217. package/dist/templates/config-pack.json.template +16 -0
  218. package/dist/templates/config.json.template +11 -0
  219. package/dist/templates/index.d.ts +22 -0
  220. package/dist/templates/index.d.ts.map +1 -0
  221. package/dist/templates/index.js +32 -0
  222. package/dist/templates/index.js.map +1 -0
  223. package/dist/templates/plugins.json.template +33 -0
  224. package/dist/templates/system-configuration.md.template +70 -0
  225. package/dist/tools/cross-nav.d.ts +21 -0
  226. package/dist/tools/cross-nav.d.ts.map +1 -0
  227. package/dist/tools/cross-nav.js +93 -0
  228. package/dist/tools/cross-nav.js.map +1 -0
  229. package/dist/tools/index.d.ts +1 -0
  230. package/dist/tools/index.d.ts.map +1 -1
  231. package/dist/tools/index.js +1 -0
  232. package/dist/tools/index.js.map +1 -1
  233. package/dist/tools/related-navs-config.d.ts +15 -0
  234. package/dist/tools/related-navs-config.d.ts.map +1 -0
  235. package/dist/tools/related-navs-config.js +132 -0
  236. package/dist/tools/related-navs-config.js.map +1 -0
  237. package/dist/tools/related-navs.d.ts +23 -0
  238. package/dist/tools/related-navs.d.ts.map +1 -0
  239. package/dist/tools/related-navs.js +107 -0
  240. package/dist/tools/related-navs.js.map +1 -0
  241. package/dist/tools/response.d.ts +3 -2
  242. package/dist/tools/response.d.ts.map +1 -1
  243. package/dist/tools/response.js +7 -11
  244. package/dist/tools/response.js.map +1 -1
  245. package/dist/tools/self-config.d.ts +2 -1
  246. package/dist/tools/self-config.d.ts.map +1 -1
  247. package/dist/tools/self-config.js +5 -9
  248. package/dist/tools/self-config.js.map +1 -1
  249. package/package.json +4 -1
  250. package/dist/memento/state.d.ts +0 -56
  251. package/dist/memento/state.d.ts.map +0 -1
  252. package/dist/memento/state.js +0 -156
  253. package/dist/memento/state.js.map +0 -1
  254. package/dist/migrations/versions/v1.4.0-rfc2119-skills.d.ts +0 -18
  255. package/dist/migrations/versions/v1.4.0-rfc2119-skills.d.ts.map +0 -1
  256. package/dist/migrations/versions/v1.4.0-rfc2119-skills.js +0 -207
  257. package/dist/migrations/versions/v1.4.0-rfc2119-skills.js.map +0 -1
@@ -1,43 +1,277 @@
1
1
  /**
2
2
  * Memento Loop Core Logic
3
3
  *
4
- * The main loop that coordinates navigator planning and worker implementation
4
+ * The main loop that coordinates navigator planning and implementer implementation
5
5
  * in a context-clearing iterative development pattern.
6
6
  *
7
- * Design principle: The WORKER forgets between iterations (memento pattern).
7
+ * Design principle: The IMPLEMENTER forgets between iterations (memento pattern).
8
8
  * The NAVIGATOR maintains its own memory and knowledge base. We provide git
9
- * history as context about what the worker has accomplished so far.
9
+ * history as context about what the implementer has accomplished so far.
10
10
  */
11
- import { query } from "@anthropic-ai/claude-agent-sdk";
12
11
  import * as fs from "node:fs";
13
12
  import * as path from "node:path";
14
13
  import * as readline from "node:readline";
14
+ import { NavigatorAdapter } from "../adapter/index.js";
15
+ import { loadNavigator } from "../query-engine/index.js";
15
16
  import chalk from "chalk";
16
17
  import { ensureGitRepo, createBranch, getCurrentBranch, getRecentGitLog, getRecentDiff, getLastCommitDiffStats, hasUncommittedChanges, stageAllChanges, commitChanges, pushBranch, createPullRequest, isGhAvailable, } from "./git-operations.js";
17
- import { createNavProtocolMcpServer } from "./nav-protocol.js";
18
- import { buildNavPlanPrompt, buildNavSystemPrompt } from "./prompts.js";
18
+ import { createNavProtocolTools } from "./nav-protocol.js";
19
+ import { buildNavPlanPrompt, buildNavSystemPrompt, buildReviewPrompt, buildFixPrompt, buildFixSystemPrompt, } from "./prompts.js";
19
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
+ }
20
253
  /**
21
254
  * Load navigator config from config.json
22
255
  */
23
256
  function loadNavConfig(navDirectory) {
24
257
  const configPath = path.join(navDirectory, "config.json");
25
258
  if (!fs.existsSync(configPath)) {
26
- return null;
259
+ return { identity: null };
27
260
  }
28
261
  try {
29
262
  const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
30
- if (config.name && config.description) {
31
- return {
32
- name: config.name,
33
- description: config.description,
34
- };
35
- }
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
+ };
36
270
  }
37
271
  catch {
38
272
  // Ignore parse errors
273
+ return { identity: null };
39
274
  }
40
- return null;
41
275
  }
42
276
  /**
43
277
  * Load navigator system prompt from CLAUDE.md
@@ -64,6 +298,299 @@ function promptUser(question) {
64
298
  });
65
299
  });
66
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
+ }
67
594
  /**
68
595
  * Handle uncommitted changes in the code directory
69
596
  *
@@ -71,68 +598,60 @@ function promptUser(question) {
71
598
  * what the worker has accomplished. Uncommitted changes won't appear in
72
599
  * that summary, which could cause confusion.
73
600
  */
74
- async function handleUncommittedChanges(codeDirectory, verbose) {
601
+ async function handleUncommittedChanges(codeDirectory, navDirectory, navSystemPrompt, navIdentity, options, harness) {
602
+ const { verbose = false } = options;
75
603
  if (!hasUncommittedChanges({ cwd: codeDirectory })) {
76
604
  return;
77
605
  }
606
+ const navName = navIdentity?.name || "navigator";
78
607
  // Show warning
79
- console.log("\n⚠️ WARNING: Uncommitted changes detected!\n");
80
- console.log("The memento loop provides git history as context to the navigator.");
81
- console.log("Uncommitted changes won't appear in that summary.\n");
82
- if (verbose) {
83
- const diff = getRecentDiff({ cwd: codeDirectory });
84
- if (diff) {
85
- console.log("Changes detected:");
86
- console.log("".repeat(40));
87
- // Show truncated diff
88
- const lines = diff.split("\n").slice(0, 20);
89
- console.log(lines.join("\n"));
90
- if (diff.split("\n").length > 20) {
91
- console.log(`... (${diff.split("\n").length - 20} more lines)`);
92
- }
93
- console.log("─".repeat(40) + "\n");
94
- }
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("");
95
619
  }
96
- console.log("How would you like to proceed?\n");
97
- console.log(" [c] Commit changes (recommended)");
98
- console.log(" [i] Add to .gitignore");
99
- console.log(" [d] Discard changes (⚠️ DANGEROUS - cannot be undone!)");
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");
100
623
  console.log(" [q] Quit\n");
101
- const answer = await promptUser("Choice [c/i/d/q]: ");
624
+ const answer = await promptUser("Choice [c/r/d/q]: ");
102
625
  switch (answer) {
103
626
  case "c":
104
627
  case "commit": {
105
- const message = await promptUser("Commit message: ");
106
- if (!message) {
107
- throw new Error("Commit message required");
108
- }
628
+ console.log(chalk.dim("\nGenerating commit message..."));
109
629
  stageAllChanges({ cwd: codeDirectory });
630
+ const message = await generateCommitMessage(codeDirectory, harness);
110
631
  const hash = commitChanges(message, { cwd: codeDirectory, verbose });
111
632
  if (hash) {
112
- console.log(`\n✅ Committed: ${hash}\n`);
633
+ console.log(`${chalk.green("Committed:")} ${hash} - ${message}\n`);
113
634
  }
114
635
  break;
115
636
  }
116
- case "i":
117
- case "ignore": {
118
- console.log("\nTo gitignore specific files, add them to .gitignore manually.");
119
- console.log("Then re-run this command.\n");
120
- throw new Error("Please update .gitignore and re-run");
637
+ case "r":
638
+ case "review": {
639
+ await reviewAndFixChanges(codeDirectory, navDirectory, navSystemPrompt, navIdentity, options, harness);
640
+ break;
121
641
  }
122
642
  case "d":
123
643
  case "discard": {
124
- console.log("\n⚠️ THIS WILL PERMANENTLY DELETE YOUR UNCOMMITTED CHANGES!");
644
+ console.log(chalk.red("\nThis will permanently delete uncommitted changes!"));
125
645
  const confirm = await promptUser("Type 'yes' to confirm: ");
126
646
  if (confirm !== "yes") {
127
647
  throw new Error("Discard cancelled");
128
648
  }
129
- // Discard all changes
130
649
  const { execSync } = await import("node:child_process");
131
650
  execSync("git checkout -- . && git clean -fd", {
132
651
  cwd: codeDirectory,
133
652
  stdio: verbose ? "inherit" : "pipe",
134
653
  });
135
- console.log("\n✅ Changes discarded\n");
654
+ console.log(chalk.green("\nChanges discarded.\n"));
136
655
  break;
137
656
  }
138
657
  case "q":
@@ -144,61 +663,93 @@ async function handleUncommittedChanges(codeDirectory, verbose) {
144
663
  /**
145
664
  * Query the navigator for an implementation plan (with stats tracking)
146
665
  */
147
- async function queryNavForPlanWithStats(codeDirectory, navDirectory, task, iteration, maxIterations, navSystemPrompt, navIdentity, gitLog, options, animation, verbose) {
666
+ async function queryNavForPlanWithStats(codeDirectory, navDirectory, task, iteration, maxIterations, navSystemPrompt, navIdentity, gitLog, options, animation, verbose, harness) {
148
667
  // Navigator uses opus by default for better planning
149
668
  const { navModel: model = "claude-opus-4-5", maxTurns = 50 } = options;
150
- // Create MCP server with plan submission tool
151
- const navProtocol = createNavProtocolMcpServer();
669
+ // Create tool server via harness
670
+ const navProtocol = createNavProtocolTools();
671
+ const { server } = harness.createToolServer("autonav-nav-protocol", navProtocol.tools);
152
672
  const prompt = buildNavPlanPrompt({ codeDirectory, task, iteration, maxIterations, branch: options.branch }, gitLog, navIdentity);
153
673
  const systemPrompt = buildNavSystemPrompt(navSystemPrompt);
154
674
  if (verbose) {
155
675
  console.log("\n[Nav] Querying navigator for plan...");
676
+ console.log(`[Nav] Model: ${model}`);
156
677
  }
157
- const queryIterator = query({
158
- prompt,
159
- options: {
160
- model,
161
- maxTurns,
162
- systemPrompt,
163
- cwd: navDirectory,
164
- mcpServers: {
165
- "autonav-nav-protocol": navProtocol.server,
166
- },
167
- permissionMode: "bypassPermissions",
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,
168
688
  },
169
- });
170
- let resultMessage;
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;
171
699
  let lastTool;
172
- for await (const message of queryIterator) {
173
- if (message.type === "assistant") {
174
- const content = message.message.content;
175
- for (const block of content) {
176
- if (block.type === "tool_use") {
177
- const toolName = block.name.split("__").pop() || block.name;
178
- lastTool = toolName;
179
- if (verbose) {
180
- console.log(`[Nav] Tool: ${toolName}`);
181
- }
182
- // Update animation
183
- animation.setMessage(`Navigator: ${toolName}...`);
184
- animation.setLastTool(toolName);
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;
185
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;
186
731
  }
187
732
  }
188
- if (message.type === "result") {
189
- resultMessage = message;
190
- }
191
733
  }
192
- // Extract token usage from result (input + output = total)
193
- const usage = resultMessage?.usage;
194
- const tokensUsed = (usage?.input_tokens ?? 0) + (usage?.output_tokens ?? 0);
195
- if (!resultMessage || resultMessage.subtype !== "success") {
196
- const errorDetails = resultMessage && "errors" in resultMessage
197
- ? resultMessage.errors.join(", ")
198
- : "Unknown error";
199
- throw new Error(`Navigator query failed: ${errorDetails}`);
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}` : ""}`);
200
751
  }
201
- // Get the captured plan from the MCP server
752
+ // Get the captured plan from the tool server closure
202
753
  const plan = navProtocol.getCapturedPlan();
203
754
  if (!plan) {
204
755
  throw new Error("Navigator did not submit a plan. The navigator must use the submit_implementation_plan tool.");
@@ -211,95 +762,180 @@ async function queryNavForPlanWithStats(codeDirectory, navDirectory, task, itera
211
762
  return { plan, tokensUsed, lastTool };
212
763
  }
213
764
  /**
214
- * Run worker agent with stats tracking
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).
215
770
  */
216
- async function runWorkerAgentWithStats(context, plan, options, animation) {
217
- // Worker uses haiku by default for faster/cheaper implementation
771
+ async function runImplementerAgentWithStats(context, plan, options, animation, harness) {
772
+ // Implementer uses haiku by default for faster/cheaper implementation
218
773
  const { verbose = false, model = "claude-haiku-4-5", maxTurns = 50 } = options;
219
- const { buildWorkerPrompt, buildWorkerSystemPrompt } = await import("./prompts.js");
220
- const prompt = buildWorkerPrompt(context.codeDirectory, plan);
221
- const systemPrompt = buildWorkerSystemPrompt(context.codeDirectory);
774
+ const { buildImplementerPrompt, buildImplementerSystemPrompt } = await import("./prompts.js");
775
+ const prompt = buildImplementerPrompt(context.codeDirectory, plan);
776
+ const systemPrompt = buildImplementerSystemPrompt(context.codeDirectory);
222
777
  if (verbose) {
223
- console.log("\n[Worker] Starting implementation...");
778
+ console.log("\n[Implementer] Starting implementation...");
779
+ console.log(`[Implementer] Model: ${model}`);
224
780
  }
225
- const queryIterator = query({
226
- prompt,
227
- options: {
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({
228
790
  model,
229
791
  maxTurns,
230
792
  systemPrompt,
231
793
  cwd: context.codeDirectory,
232
794
  permissionMode: "bypassPermissions",
233
- },
234
- });
235
- const filesModified = [];
236
- let lastAssistantText = "";
237
- let resultMessage;
238
- let lastTool;
239
- for await (const message of queryIterator) {
240
- if (message.type === "assistant") {
241
- const content = message.message.content;
242
- for (const block of content) {
243
- if (block.type === "tool_use") {
244
- const toolName = block.name;
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;
245
819
  lastTool = toolName;
820
+ mood.toolCount += 1;
821
+ if (!mood.lastError) {
822
+ mood.consecutiveSuccess += 1;
823
+ }
824
+ mood.lastError = false;
246
825
  if (verbose) {
247
- console.log(`[Worker] Tool: ${toolName}`);
826
+ console.log(`[Implementer] Tool: ${toolName}`);
248
827
  }
249
- // Update animation
250
- animation.setMessage(`Worker: ${toolName}...`);
828
+ animation.setMessage(pickMood("impl", toolName, event.input, mood));
251
829
  animation.setLastTool(toolName);
830
+ animation.incrementTurns();
252
831
  // Track file operations
253
- if (toolName === "Write" ||
254
- toolName === "Edit" ||
255
- toolName === "str_replace_based_edit_tool") {
256
- const input = block.input;
257
- const filePath = input.file_path || input.path;
258
- if (typeof filePath === "string" && !filesModified.includes(filePath)) {
259
- filesModified.push(filePath);
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);
260
836
  }
261
837
  }
262
838
  }
263
- else if (block.type === "text") {
264
- lastAssistantText = block.text;
839
+ else if (event.type === "text") {
840
+ lastAssistantText = event.text;
841
+ }
842
+ if (event.type === "result") {
843
+ resultEvent = event;
265
844
  }
266
845
  }
267
846
  }
268
- if (message.type === "result") {
269
- resultMessage = message;
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) || "?"}`);
270
897
  }
271
- }
272
- // Extract token usage (input + output = total)
273
- const usage = resultMessage?.usage;
274
- const tokensUsed = (usage?.input_tokens ?? 0) + (usage?.output_tokens ?? 0);
275
- if (!resultMessage) {
276
- return {
277
- success: false,
278
- summary: "No result message received",
279
- filesModified,
280
- errors: ["No result message received"],
281
- tokensUsed,
282
- lastTool,
283
- };
284
- }
285
- if (resultMessage.subtype !== "success") {
286
- const errorDetails = "errors" in resultMessage ? resultMessage.errors.join(", ") : "Unknown error";
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!
287
929
  return {
288
- success: false,
289
- summary: `Worker failed: ${resultMessage.subtype}`,
290
- filesModified,
291
- errors: [errorDetails],
292
- tokensUsed,
930
+ success: true,
931
+ summary: resultEvent.text || lastAssistantText || "Implementation completed",
932
+ filesModified: allFilesModified,
933
+ tokensUsed: totalTokensUsed,
293
934
  lastTool,
294
935
  };
295
936
  }
296
- return {
297
- success: true,
298
- summary: resultMessage.result || lastAssistantText || "Implementation completed",
299
- filesModified,
300
- tokensUsed,
301
- lastTool,
302
- };
937
+ // Unreachable — infinite loop above always returns
938
+ throw new Error("Unreachable");
303
939
  }
304
940
  /**
305
941
  * Print iteration summary
@@ -333,7 +969,7 @@ function printIterationSummary(iteration, data) {
333
969
  * All state is in-memory. Git history is the only persistent context
334
970
  * visible to the agents.
335
971
  */
336
- export async function runMementoLoop(codeDirectory, navDirectory, task, options) {
972
+ export async function runMementoLoop(codeDirectory, navDirectory, task, options, harness) {
337
973
  const startTime = Date.now();
338
974
  const { verbose = false, pr = false, maxIterations = 0 } = options;
339
975
  // Resolve paths
@@ -347,7 +983,8 @@ export async function runMementoLoop(codeDirectory, navDirectory, task, options)
347
983
  throw new Error(`Navigator directory not found: ${navDirectory}`);
348
984
  }
349
985
  // Load navigator config and system prompt
350
- const navIdentity = loadNavConfig(navDirectory);
986
+ const navConfig = loadNavConfig(navDirectory);
987
+ const navIdentity = navConfig.identity;
351
988
  const navSystemPrompt = loadNavSystemPrompt(navDirectory);
352
989
  // In-memory state
353
990
  const state = {
@@ -368,7 +1005,7 @@ export async function runMementoLoop(codeDirectory, navDirectory, task, options)
368
1005
  // Ensure code directory is a git repo
369
1006
  ensureGitRepo({ cwd: codeDirectory, verbose });
370
1007
  // Check for uncommitted changes - navigator can't see them!
371
- await handleUncommittedChanges(codeDirectory, verbose);
1008
+ await handleUncommittedChanges(codeDirectory, navDirectory, navSystemPrompt, navIdentity, options, harness);
372
1009
  // Create or switch to branch if specified
373
1010
  if (options.branch) {
374
1011
  createBranch(options.branch, { cwd: codeDirectory, verbose });
@@ -386,7 +1023,7 @@ export async function runMementoLoop(codeDirectory, navDirectory, task, options)
386
1023
  else {
387
1024
  console.log(`\nIteration ${state.iteration}...`);
388
1025
  }
389
- // Get git log to show the navigator what the worker has accomplished
1026
+ // Get git log to show the navigator what the implementer has accomplished
390
1027
  const gitLog = getRecentGitLog({ cwd: codeDirectory, count: 20 });
391
1028
  // Create animation with cumulative stats
392
1029
  const animation = new MatrixAnimation({
@@ -394,65 +1031,116 @@ export async function runMementoLoop(codeDirectory, navDirectory, task, options)
394
1031
  width: 50,
395
1032
  lines: 3,
396
1033
  });
397
- // Initialize animation with cumulative stats
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";
398
1038
  animation.setStats({
1039
+ iteration: state.iteration,
1040
+ maxIterations: maxIterations || undefined,
399
1041
  linesAdded: state.stats.linesAdded,
400
1042
  linesRemoved: state.stats.linesRemoved,
401
- tokensUsed: state.stats.tokensUsed,
1043
+ tokensUsed: 0, // Start fresh - will show implementer tokens
402
1044
  lastTool: state.stats.lastTool,
403
1045
  });
1046
+ animation.setModels(implementerModel, navigatorModel);
404
1047
  if (!verbose) {
405
1048
  animation.start();
406
1049
  }
407
1050
  let plan = null;
408
1051
  let iterationTokens = 0;
1052
+ let implementerTokens = 0;
1053
+ let implementerSummary = "";
409
1054
  try {
410
- // Query navigator for plan
411
- const navResult = await queryNavForPlanWithStats(codeDirectory, navDirectory, task, state.iteration, maxIterations, navSystemPrompt, navIdentity, gitLog, options, animation, verbose);
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
+ }
412
1073
  plan = navResult.plan;
413
1074
  iterationTokens += navResult.tokensUsed;
414
1075
  state.stats.tokensUsed += navResult.tokensUsed;
415
1076
  if (navResult.lastTool) {
416
1077
  state.stats.lastTool = navResult.lastTool;
417
1078
  }
418
- animation.setTokens(state.stats.tokensUsed);
419
1079
  if (!plan) {
420
1080
  errors.push(`Iteration ${state.iteration}: Navigator did not provide a plan`);
421
1081
  break;
422
1082
  }
423
1083
  // Record plan in memory (for PR body)
424
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
+ }
425
1098
  // Log if navigator thinks task is complete (advisory only - doesn't stop loop)
426
1099
  if (plan.isComplete) {
427
1100
  state.completionMessage = plan.completionMessage;
428
1101
  }
429
- // Update animation for worker phase
430
- animation.setMessage("Worker implementing...");
431
- // Run worker to implement the plan
432
- const workerResult = await runWorkerAgentWithStats({ codeDirectory, task }, plan, {
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, {
433
1110
  verbose,
434
1111
  model: options.model,
435
1112
  maxTurns: options.maxTurns,
436
- }, animation);
437
- iterationTokens += workerResult.tokensUsed;
438
- state.stats.tokensUsed += workerResult.tokensUsed;
439
- if (workerResult.lastTool) {
440
- state.stats.lastTool = workerResult.lastTool;
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;
441
1120
  }
442
- animation.setTokens(state.stats.tokensUsed);
443
- if (!workerResult.success) {
444
- errors.push(`Iteration ${state.iteration}: Worker failed - ${workerResult.errors?.join(", ") || "Unknown error"}`);
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}`);
445
1130
  // Continue to next iteration - navigator can see the state and adjust
446
1131
  }
1132
+ // Phase 3: Review
1133
+ // Animation is still running — reviewImplementation manages stop/start internally
1134
+ await reviewImplementation(codeDirectory, navDirectory, options, animation, verbose, harness);
447
1135
  }
448
1136
  finally {
449
1137
  if (!verbose) {
450
1138
  animation.stop();
451
1139
  }
452
1140
  }
453
- // Commit changes - nav will see this in git history on next iteration
454
- // Use plan summary directly - navigator should provide conventional commit style
455
- const commitMessage = plan?.summary || `memento: iteration ${state.iteration}`;
1141
+ // Phase 4: Commit with LLM-generated message
1142
+ stageAllChanges({ cwd: codeDirectory });
1143
+ const commitMessage = await generateCommitMessage(codeDirectory, harness);
456
1144
  const commitHash = commitChanges(commitMessage, { cwd: codeDirectory, verbose });
457
1145
  // Get diff stats from the commit
458
1146
  if (commitHash) {
@@ -460,13 +1148,17 @@ export async function runMementoLoop(codeDirectory, navDirectory, task, options)
460
1148
  state.stats.linesAdded += diffStats.linesAdded;
461
1149
  state.stats.linesRemoved += diffStats.linesRemoved;
462
1150
  }
463
- // Print iteration summary
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)
464
1156
  printIterationSummary(state.iteration, {
465
1157
  commitHash,
466
1158
  plan: plan?.summary,
467
1159
  linesAdded: state.stats.linesAdded,
468
1160
  linesRemoved: state.stats.linesRemoved,
469
- tokensUsed: state.stats.tokensUsed,
1161
+ tokensUsed: implementerTokens,
470
1162
  lastTool: state.stats.lastTool,
471
1163
  isComplete: plan?.isComplete,
472
1164
  completionMessage: plan?.completionMessage,