@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.
- 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 -6
- package/dist/cli/autonav.d.ts.map +1 -1
- package/dist/cli/autonav.js +32 -53
- 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 +3 -2
- package/dist/cli/nav-memento.d.ts.map +1 -1
- package/dist/cli/nav-memento.js +12 -6
- package/dist/cli/nav-memento.js.map +1 -1
- 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/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 +6 -5
- package/dist/memento/index.d.ts.map +1 -1
- package/dist/memento/index.js +7 -5
- package/dist/memento/index.js.map +1 -1
- package/dist/memento/loop.d.ts +6 -5
- package/dist/memento/loop.d.ts.map +1 -1
- package/dist/memento/loop.js +872 -180
- package/dist/memento/loop.js.map +1 -1
- package/dist/memento/matrix-animation.d.ts +26 -0
- package/dist/memento/matrix-animation.d.ts.map +1 -1
- package/dist/memento/matrix-animation.js +93 -18
- package/dist/memento/matrix-animation.js.map +1 -1
- package/dist/memento/nav-protocol.d.ts +2 -2
- package/dist/memento/nav-protocol.d.ts.map +1 -1
- package/dist/memento/nav-protocol.js +39 -43
- package/dist/memento/nav-protocol.js.map +1 -1
- package/dist/memento/prompts.d.ts +21 -8
- package/dist/memento/prompts.d.ts.map +1 -1
- package/dist/memento/prompts.js +79 -39
- package/dist/memento/prompts.js.map +1 -1
- 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 +6 -6
- package/dist/memento/types.d.ts.map +1 -1
- package/dist/memento/types.js +3 -3
- package/dist/memento/worker-agent.d.ts +4 -1
- package/dist/memento/worker-agent.d.ts.map +1 -1
- package/dist/memento/worker-agent.js +38 -54
- package/dist/memento/worker-agent.js.map +1 -1
- 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/skill-generator/index.d.ts +142 -0
- package/dist/skill-generator/index.d.ts.map +1 -0
- package/dist/skill-generator/index.js +510 -0
- package/dist/skill-generator/index.js.map +1 -0
- 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/templates/.gitignore.template +26 -0
- package/dist/templates/CLAUDE-pack.md.template +114 -0
- package/dist/templates/CLAUDE.md.template +153 -0
- package/dist/templates/README.md.template +174 -0
- package/dist/templates/config-pack.json.template +16 -0
- package/dist/templates/config.json.template +11 -0
- package/dist/templates/index.d.ts +22 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +32 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/plugins.json.template +33 -0
- package/dist/templates/system-configuration.md.template +70 -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
- package/dist/memento/state.d.ts +0 -56
- package/dist/memento/state.d.ts.map +0 -1
- package/dist/memento/state.js +0 -156
- package/dist/memento/state.js.map +0 -1
- package/dist/migrations/versions/v1.4.0-rfc2119-skills.d.ts +0 -18
- package/dist/migrations/versions/v1.4.0-rfc2119-skills.d.ts.map +0 -1
- package/dist/migrations/versions/v1.4.0-rfc2119-skills.js +0 -207
- package/dist/migrations/versions/v1.4.0-rfc2119-skills.js.map +0 -1
package/dist/memento/loop.js
CHANGED
|
@@ -1,43 +1,277 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Memento Loop Core Logic
|
|
3
3
|
*
|
|
4
|
-
* The main loop that coordinates navigator planning and
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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,
|
|
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("\
|
|
80
|
-
console.log("The memento loop
|
|
81
|
-
console.log("Uncommitted changes won't
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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("
|
|
97
|
-
console.log(
|
|
98
|
-
console.log(" [
|
|
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/
|
|
624
|
+
const answer = await promptUser("Choice [c/r/d/q]: ");
|
|
102
625
|
switch (answer) {
|
|
103
626
|
case "c":
|
|
104
627
|
case "commit": {
|
|
105
|
-
|
|
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(
|
|
633
|
+
console.log(`${chalk.green("Committed:")} ${hash} - ${message}\n`);
|
|
113
634
|
}
|
|
114
635
|
break;
|
|
115
636
|
}
|
|
116
|
-
case "
|
|
117
|
-
case "
|
|
118
|
-
|
|
119
|
-
|
|
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("\
|
|
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("\
|
|
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
|
|
151
|
-
const navProtocol =
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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
|
|
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
|
|
217
|
-
//
|
|
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 {
|
|
220
|
-
const prompt =
|
|
221
|
-
const systemPrompt =
|
|
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[
|
|
778
|
+
console.log("\n[Implementer] Starting implementation...");
|
|
779
|
+
console.log(`[Implementer] Model: ${model}`);
|
|
224
780
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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(`[
|
|
826
|
+
console.log(`[Implementer] Tool: ${toolName}`);
|
|
248
827
|
}
|
|
249
|
-
|
|
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
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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 (
|
|
264
|
-
lastAssistantText =
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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:
|
|
289
|
-
summary:
|
|
290
|
-
filesModified,
|
|
291
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
|
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,
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
|
454
|
-
|
|
455
|
-
const commitMessage =
|
|
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
|
-
//
|
|
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:
|
|
1161
|
+
tokensUsed: implementerTokens,
|
|
470
1162
|
lastTool: state.stats.lastTool,
|
|
471
1163
|
isComplete: plan?.isComplete,
|
|
472
1164
|
completionMessage: plan?.completionMessage,
|