@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.
- package/README.md +2 -2
- package/dist/adapter/index.d.ts +3 -3
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +3 -3
- package/dist/adapter/index.js.map +1 -1
- package/dist/adapter/navigator-adapter.d.ts +196 -0
- package/dist/adapter/navigator-adapter.d.ts.map +1 -0
- package/dist/adapter/navigator-adapter.js +579 -0
- package/dist/adapter/navigator-adapter.js.map +1 -0
- package/dist/cli/autonav.d.ts +2 -5
- package/dist/cli/autonav.d.ts.map +1 -1
- package/dist/cli/autonav.js +35 -49
- package/dist/cli/autonav.js.map +1 -1
- package/dist/cli/nav-chat.d.ts +2 -1
- package/dist/cli/nav-chat.d.ts.map +1 -1
- package/dist/cli/nav-chat.js +32 -3
- package/dist/cli/nav-chat.js.map +1 -1
- package/dist/cli/nav-init.d.ts +2 -1
- package/dist/cli/nav-init.d.ts.map +1 -1
- package/dist/cli/nav-init.js +12 -4
- package/dist/cli/nav-init.js.map +1 -1
- package/dist/cli/nav-install.d.ts +2 -1
- package/dist/cli/nav-install.d.ts.map +1 -1
- package/dist/cli/nav-install.js +4 -2
- package/dist/cli/nav-install.js.map +1 -1
- package/dist/cli/nav-memento.d.ts +21 -0
- package/dist/cli/nav-memento.d.ts.map +1 -0
- package/dist/cli/nav-memento.js +185 -0
- package/dist/cli/nav-memento.js.map +1 -0
- package/dist/cli/nav-mend.d.ts +2 -1
- package/dist/cli/nav-mend.d.ts.map +1 -1
- package/dist/cli/nav-mend.js +4 -1
- package/dist/cli/nav-mend.js.map +1 -1
- package/dist/cli/nav-migrate.d.ts +2 -1
- package/dist/cli/nav-migrate.d.ts.map +1 -1
- package/dist/cli/nav-migrate.js +2 -6
- package/dist/cli/nav-migrate.js.map +1 -1
- package/dist/cli/nav-query.d.ts +2 -1
- package/dist/cli/nav-query.d.ts.map +1 -1
- package/dist/cli/nav-query.js +12 -6
- package/dist/cli/nav-query.js.map +1 -1
- package/dist/cli/nav-standup.d.ts +18 -0
- package/dist/cli/nav-standup.d.ts.map +1 -0
- package/dist/cli/nav-standup.js +151 -0
- package/dist/cli/nav-standup.js.map +1 -0
- package/dist/cli/nav-uninstall.d.ts +2 -1
- package/dist/cli/nav-uninstall.d.ts.map +1 -1
- package/dist/cli/nav-uninstall.js +4 -2
- package/dist/cli/nav-uninstall.js.map +1 -1
- package/dist/cli/nav-update.d.ts +2 -1
- package/dist/cli/nav-update.d.ts.map +1 -1
- package/dist/cli/nav-update.js +11 -6
- package/dist/cli/nav-update.js.map +1 -1
- package/dist/conversation/App.d.ts +9 -2
- package/dist/conversation/App.d.ts.map +1 -1
- package/dist/conversation/App.js +304 -111
- package/dist/conversation/App.js.map +1 -1
- package/dist/conversation/index.d.ts +5 -0
- package/dist/conversation/index.d.ts.map +1 -1
- package/dist/conversation/index.js +17 -2
- package/dist/conversation/index.js.map +1 -1
- package/dist/harness/chibi-harness.d.ts +36 -0
- package/dist/harness/chibi-harness.d.ts.map +1 -0
- package/dist/harness/chibi-harness.js +383 -0
- package/dist/harness/chibi-harness.js.map +1 -0
- package/dist/harness/chibi-plugins/get_plugin_config +64 -0
- package/dist/harness/chibi-plugins/query_navigator +114 -0
- package/dist/harness/chibi-plugins/submit_answer +38 -0
- package/dist/harness/chibi-plugins/update_plugin_config +91 -0
- package/dist/harness/claude-code-harness.d.ts +24 -0
- package/dist/harness/claude-code-harness.d.ts.map +1 -0
- package/dist/harness/claude-code-harness.js +242 -0
- package/dist/harness/claude-code-harness.js.map +1 -0
- package/dist/harness/ephemeral-home.d.ts +34 -0
- package/dist/harness/ephemeral-home.d.ts.map +1 -0
- package/dist/harness/ephemeral-home.js +56 -0
- package/dist/harness/ephemeral-home.js.map +1 -0
- package/dist/harness/factory.d.ts +47 -0
- package/dist/harness/factory.d.ts.map +1 -0
- package/dist/harness/factory.js +84 -0
- package/dist/harness/factory.js.map +1 -0
- package/dist/harness/helpers.d.ts +58 -0
- package/dist/harness/helpers.d.ts.map +1 -0
- package/dist/harness/helpers.js +78 -0
- package/dist/harness/helpers.js.map +1 -0
- package/dist/harness/index.d.ts +14 -0
- package/dist/harness/index.d.ts.map +1 -0
- package/dist/harness/index.js +13 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/harness/opencode-harness.d.ts +79 -0
- package/dist/harness/opencode-harness.d.ts.map +1 -0
- package/dist/harness/opencode-harness.js +537 -0
- package/dist/harness/opencode-harness.js.map +1 -0
- package/dist/harness/opencode-tools/get_plugin_config.ts +72 -0
- package/dist/harness/opencode-tools/query_navigator.ts +126 -0
- package/dist/harness/opencode-tools/submit_answer.ts +40 -0
- package/dist/harness/opencode-tools/update_plugin_config.ts +105 -0
- package/dist/harness/sandbox.d.ts +59 -0
- package/dist/harness/sandbox.d.ts.map +1 -0
- package/dist/harness/sandbox.js +152 -0
- package/dist/harness/sandbox.js.map +1 -0
- package/dist/harness/tool-server.d.ts +50 -0
- package/dist/harness/tool-server.d.ts.map +1 -0
- package/dist/harness/tool-server.js +27 -0
- package/dist/harness/tool-server.js.map +1 -0
- package/dist/harness/types.d.ts +168 -0
- package/dist/harness/types.d.ts.map +1 -0
- package/dist/harness/types.js +12 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/interview/App.d.ts +4 -2
- package/dist/interview/App.d.ts.map +1 -1
- package/dist/interview/App.js +36 -56
- package/dist/interview/App.js.map +1 -1
- package/dist/interview/index.d.ts +3 -0
- package/dist/interview/index.d.ts.map +1 -1
- package/dist/interview/index.js +1 -0
- package/dist/interview/index.js.map +1 -1
- package/dist/interview/prompts.d.ts +3 -1
- package/dist/interview/prompts.d.ts.map +1 -1
- package/dist/interview/prompts.js +5 -1
- package/dist/interview/prompts.js.map +1 -1
- package/dist/interview/ui/ChatBanner.d.ts +17 -0
- package/dist/interview/ui/ChatBanner.d.ts.map +1 -0
- package/dist/interview/ui/ChatBanner.js +30 -0
- package/dist/interview/ui/ChatBanner.js.map +1 -0
- package/dist/interview/ui/ChatInput.d.ts +36 -0
- package/dist/interview/ui/ChatInput.d.ts.map +1 -0
- package/dist/interview/ui/ChatInput.js +304 -0
- package/dist/interview/ui/ChatInput.js.map +1 -0
- package/dist/interview/ui/MarkdownText.d.ts +13 -0
- package/dist/interview/ui/MarkdownText.d.ts.map +1 -0
- package/dist/interview/ui/MarkdownText.js +107 -0
- package/dist/interview/ui/MarkdownText.js.map +1 -0
- package/dist/interview/ui/index.d.ts +3 -0
- package/dist/interview/ui/index.d.ts.map +1 -1
- package/dist/interview/ui/index.js +3 -0
- package/dist/interview/ui/index.js.map +1 -1
- package/dist/memento/git-operations.d.ts +95 -0
- package/dist/memento/git-operations.d.ts.map +1 -0
- package/dist/memento/git-operations.js +256 -0
- package/dist/memento/git-operations.js.map +1 -0
- package/dist/memento/implementer-agent.d.ts +31 -0
- package/dist/memento/implementer-agent.d.ts.map +1 -0
- package/dist/memento/implementer-agent.js +95 -0
- package/dist/memento/implementer-agent.js.map +1 -0
- package/dist/memento/index.d.ts +32 -0
- package/dist/memento/index.d.ts.map +1 -0
- package/dist/memento/index.js +40 -0
- package/dist/memento/index.js.map +1 -0
- package/dist/memento/loop.d.ts +34 -0
- package/dist/memento/loop.d.ts.map +1 -0
- package/dist/memento/loop.js +1227 -0
- package/dist/memento/loop.js.map +1 -0
- package/dist/memento/matrix-animation.d.ts +103 -0
- package/dist/memento/matrix-animation.d.ts.map +1 -0
- package/dist/memento/matrix-animation.js +303 -0
- package/dist/memento/matrix-animation.js.map +1 -0
- package/dist/memento/nav-protocol.d.ts +30 -0
- package/dist/memento/nav-protocol.d.ts.map +1 -0
- package/dist/memento/nav-protocol.js +75 -0
- package/dist/memento/nav-protocol.js.map +1 -0
- package/dist/memento/prompts.d.ts +57 -0
- package/dist/memento/prompts.d.ts.map +1 -0
- package/dist/memento/prompts.js +208 -0
- package/dist/memento/prompts.js.map +1 -0
- package/dist/memento/rate-limit.d.ts +85 -0
- package/dist/memento/rate-limit.d.ts.map +1 -0
- package/dist/memento/rate-limit.js +221 -0
- package/dist/memento/rate-limit.js.map +1 -0
- package/dist/memento/types.d.ts +123 -0
- package/dist/memento/types.d.ts.map +1 -0
- package/dist/memento/types.js +30 -0
- package/dist/memento/types.js.map +1 -0
- package/dist/memento/worker-agent.d.ts +33 -0
- package/dist/memento/worker-agent.d.ts.map +1 -0
- package/dist/memento/worker-agent.js +93 -0
- package/dist/memento/worker-agent.js.map +1 -0
- package/dist/registry.d.ts +35 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +87 -0
- package/dist/registry.js.map +1 -0
- package/dist/repo-analyzer/index.d.ts +2 -1
- package/dist/repo-analyzer/index.d.ts.map +1 -1
- package/dist/repo-analyzer/index.js +6 -17
- package/dist/repo-analyzer/index.js.map +1 -1
- package/dist/standup/config.d.ts +19 -0
- package/dist/standup/config.d.ts.map +1 -0
- package/dist/standup/config.js +42 -0
- package/dist/standup/config.js.map +1 -0
- package/dist/standup/index.d.ts +24 -0
- package/dist/standup/index.d.ts.map +1 -0
- package/dist/standup/index.js +29 -0
- package/dist/standup/index.js.map +1 -0
- package/dist/standup/loop.d.ts +36 -0
- package/dist/standup/loop.d.ts.map +1 -0
- package/dist/standup/loop.js +508 -0
- package/dist/standup/loop.js.map +1 -0
- package/dist/standup/prompts.d.ts +62 -0
- package/dist/standup/prompts.d.ts.map +1 -0
- package/dist/standup/prompts.js +211 -0
- package/dist/standup/prompts.js.map +1 -0
- package/dist/standup/standup-protocol.d.ts +33 -0
- package/dist/standup/standup-protocol.d.ts.map +1 -0
- package/dist/standup/standup-protocol.js +189 -0
- package/dist/standup/standup-protocol.js.map +1 -0
- package/dist/standup/types.d.ts +185 -0
- package/dist/standup/types.d.ts.map +1 -0
- package/dist/standup/types.js +67 -0
- package/dist/standup/types.js.map +1 -0
- package/dist/tools/cross-nav.d.ts +21 -0
- package/dist/tools/cross-nav.d.ts.map +1 -0
- package/dist/tools/cross-nav.js +93 -0
- package/dist/tools/cross-nav.js.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/related-navs-config.d.ts +15 -0
- package/dist/tools/related-navs-config.d.ts.map +1 -0
- package/dist/tools/related-navs-config.js +132 -0
- package/dist/tools/related-navs-config.js.map +1 -0
- package/dist/tools/related-navs.d.ts +23 -0
- package/dist/tools/related-navs.d.ts.map +1 -0
- package/dist/tools/related-navs.js +107 -0
- package/dist/tools/related-navs.js.map +1 -0
- package/dist/tools/response.d.ts +3 -2
- package/dist/tools/response.d.ts.map +1 -1
- package/dist/tools/response.js +7 -11
- package/dist/tools/response.js.map +1 -1
- package/dist/tools/self-config.d.ts +2 -1
- package/dist/tools/self-config.d.ts.map +1 -1
- package/dist/tools/self-config.js +5 -9
- package/dist/tools/self-config.js.map +1 -1
- 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
|