@praneeth_54/agentdoctor 2.0.1 → 3.0.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/CHANGELOG.md +123 -1
- package/README.md +510 -260
- package/dist/agent/approvals.d.ts +24 -0
- package/dist/agent/approvals.js +64 -0
- package/dist/agent/chat/deterministic.d.ts +6 -0
- package/dist/agent/chat/deterministic.js +180 -0
- package/dist/agent/chat/index.d.ts +7 -0
- package/dist/agent/chat/index.js +5 -0
- package/dist/agent/chat/memory.d.ts +44 -0
- package/dist/agent/chat/memory.js +103 -0
- package/dist/agent/chat/project-summary.d.ts +16 -0
- package/dist/agent/chat/project-summary.js +70 -0
- package/dist/agent/chat/prompts.d.ts +6 -0
- package/dist/agent/chat/prompts.js +39 -0
- package/dist/agent/chat/response.d.ts +19 -0
- package/dist/agent/chat/response.js +109 -0
- package/dist/agent/chat/service.d.ts +42 -0
- package/dist/agent/chat/service.js +246 -0
- package/dist/agent/chat/types.d.ts +48 -0
- package/dist/agent/chat/types.js +1 -0
- package/dist/agent/context/retrieve.d.ts +21 -0
- package/dist/agent/context/retrieve.js +117 -0
- package/dist/agent/context/truth.d.ts +6 -0
- package/dist/agent/context/truth.js +19 -0
- package/dist/agent/context/types.d.ts +23 -0
- package/dist/agent/context/types.js +4 -0
- package/dist/agent/index.d.ts +28 -0
- package/dist/agent/index.js +15 -0
- package/dist/agent/loop.d.ts +53 -0
- package/dist/agent/loop.js +253 -0
- package/dist/agent/modes.d.ts +18 -0
- package/dist/agent/modes.js +102 -0
- package/dist/agent/plan.d.ts +30 -0
- package/dist/agent/plan.js +121 -0
- package/dist/agent/roles.d.ts +12 -0
- package/dist/agent/roles.js +138 -0
- package/dist/agent/runtime.d.ts +83 -0
- package/dist/agent/runtime.js +293 -0
- package/dist/agent/state.d.ts +30 -0
- package/dist/agent/state.js +95 -0
- package/dist/agent/student.d.ts +56 -0
- package/dist/agent/student.js +230 -0
- package/dist/agent/tools/execute.d.ts +20 -0
- package/dist/agent/tools/execute.js +365 -0
- package/dist/agent/tools/index.d.ts +6 -0
- package/dist/agent/tools/index.js +5 -0
- package/dist/agent/tools/registry.d.ts +7 -0
- package/dist/agent/tools/registry.js +212 -0
- package/dist/agent/tools/run.d.ts +24 -0
- package/dist/agent/tools/run.js +44 -0
- package/dist/agent/tools/types.d.ts +32 -0
- package/dist/agent/tools/types.js +10 -0
- package/dist/agent/tools/write.d.ts +24 -0
- package/dist/agent/tools/write.js +121 -0
- package/dist/agent/verify.d.ts +29 -0
- package/dist/agent/verify.js +210 -0
- package/dist/ai/config.d.ts +22 -0
- package/dist/ai/config.js +69 -0
- package/dist/ai/index.d.ts +17 -0
- package/dist/ai/index.js +52 -0
- package/dist/ai/providers/mock.d.ts +20 -0
- package/dist/ai/providers/mock.js +84 -0
- package/dist/ai/providers/none.d.ts +6 -0
- package/dist/ai/providers/none.js +23 -0
- package/dist/ai/providers/openai-compatible.d.ts +21 -0
- package/dist/ai/providers/openai-compatible.js +151 -0
- package/dist/ai/redact.d.ts +9 -0
- package/dist/ai/redact.js +61 -0
- package/dist/ai/types.d.ts +71 -0
- package/dist/ai/types.js +6 -0
- package/dist/cli/commands/agent.d.ts +29 -0
- package/dist/cli/commands/agent.js +164 -0
- package/dist/cli/commands/chat.d.ts +14 -0
- package/dist/cli/commands/chat.js +153 -0
- package/dist/cli/commands/learn.d.ts +12 -0
- package/dist/cli/commands/learn.js +107 -0
- package/dist/cli/commands/product.d.ts +19 -0
- package/dist/cli/commands/product.js +147 -0
- package/dist/cli/commands/start.d.ts +15 -0
- package/dist/cli/commands/start.js +80 -0
- package/dist/cli/program.js +233 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/dashboard/server.d.ts +6 -0
- package/dist/dashboard/server.js +333 -43
- package/dist/enforcement/runner.d.ts +5 -0
- package/dist/enforcement/runner.js +51 -4
- package/dist/index.d.ts +17 -0
- package/dist/index.js +10 -0
- package/dist/intelligence/graph/build.js +36 -8
- package/dist/intelligence/resolve/imports.js +1 -1
- package/dist/languages/dart.d.ts +10 -0
- package/dist/languages/dart.js +99 -0
- package/dist/languages/go.d.ts +13 -7
- package/dist/languages/go.js +91 -26
- package/dist/languages/index.d.ts +5 -1
- package/dist/languages/index.js +13 -36
- package/dist/languages/java.d.ts +10 -0
- package/dist/languages/java.js +80 -0
- package/dist/languages/kotlin.d.ts +10 -0
- package/dist/languages/kotlin.js +85 -0
- package/dist/languages/php.d.ts +1 -0
- package/dist/languages/php.js +11 -2
- package/dist/languages/python.d.ts +6 -2
- package/dist/languages/python.js +39 -9
- package/dist/languages/rust.d.ts +10 -0
- package/dist/languages/rust.js +93 -0
- package/dist/languages/types.d.ts +4 -3
- package/dist/mcp/agent/registry.d.ts +13 -0
- package/dist/mcp/agent/registry.js +320 -0
- package/dist/mcp/agentdoctor/server.js +8 -1
- package/dist/mcp/intelligence/handlers.d.ts +3 -0
- package/dist/mcp/intelligence/handlers.js +28 -0
- package/dist/mcp/intelligence/registry.d.ts +1 -1
- package/dist/mcp/intelligence/registry.js +30 -1
- package/dist/product/api/doctor.d.ts +14 -0
- package/dist/product/api/doctor.js +185 -0
- package/dist/product/api/openapi.d.ts +7 -0
- package/dist/product/api/openapi.js +122 -0
- package/dist/product/approval/model.d.ts +30 -0
- package/dist/product/approval/model.js +64 -0
- package/dist/product/approval/session.d.ts +49 -0
- package/dist/product/approval/session.js +134 -0
- package/dist/product/database/doctor.d.ts +30 -0
- package/dist/product/database/doctor.js +184 -0
- package/dist/product/decisions/ledger.d.ts +24 -0
- package/dist/product/decisions/ledger.js +110 -0
- package/dist/product/deps/analyze.d.ts +46 -0
- package/dist/product/deps/analyze.js +137 -0
- package/dist/product/deps/lockfiles.d.ts +25 -0
- package/dist/product/deps/lockfiles.js +200 -0
- package/dist/product/discovery/roots.d.ts +42 -0
- package/dist/product/discovery/roots.js +215 -0
- package/dist/product/dna/build.d.ts +50 -0
- package/dist/product/dna/build.js +255 -0
- package/dist/product/eval/lab.d.ts +17 -0
- package/dist/product/eval/lab.js +218 -0
- package/dist/product/events/doctor.d.ts +21 -0
- package/dist/product/events/doctor.js +147 -0
- package/dist/product/evidence-scan.d.ts +12 -0
- package/dist/product/evidence-scan.js +46 -0
- package/dist/product/evolution/timeline.d.ts +29 -0
- package/dist/product/evolution/timeline.js +123 -0
- package/dist/product/features/intelligence.d.ts +23 -0
- package/dist/product/features/intelligence.js +158 -0
- package/dist/product/forensic/mode.d.ts +25 -0
- package/dist/product/forensic/mode.js +70 -0
- package/dist/product/graph/enrich-languages.d.ts +20 -0
- package/dist/product/graph/enrich-languages.js +193 -0
- package/dist/product/health/code-health.d.ts +20 -0
- package/dist/product/health/code-health.js +149 -0
- package/dist/product/index.d.ts +69 -0
- package/dist/product/index.js +35 -0
- package/dist/product/ledger/change-ledger.d.ts +23 -0
- package/dist/product/ledger/change-ledger.js +64 -0
- package/dist/product/map/software-map.d.ts +17 -0
- package/dist/product/map/software-map.js +95 -0
- package/dist/product/memory/institutional.d.ts +16 -0
- package/dist/product/memory/institutional.js +87 -0
- package/dist/product/ops/incident.d.ts +20 -0
- package/dist/product/ops/incident.js +61 -0
- package/dist/product/ops/infra.d.ts +15 -0
- package/dist/product/ops/infra.js +112 -0
- package/dist/product/org/model.d.ts +34 -0
- package/dist/product/org/model.js +195 -0
- package/dist/product/privacy/doctor.d.ts +13 -0
- package/dist/product/privacy/doctor.js +90 -0
- package/dist/product/requirements/trace.d.ts +21 -0
- package/dist/product/requirements/trace.js +166 -0
- package/dist/product/search/index.d.ts +29 -0
- package/dist/product/search/index.js +116 -0
- package/dist/product/search/software-search.d.ts +19 -0
- package/dist/product/search/software-search.js +100 -0
- package/dist/product/security/doctor.d.ts +23 -0
- package/dist/product/security/doctor.js +124 -0
- package/dist/product/self/diagnose.d.ts +15 -0
- package/dist/product/self/diagnose.js +82 -0
- package/dist/product/techdebt/roadmap.d.ts +20 -0
- package/dist/product/techdebt/roadmap.js +118 -0
- package/dist/product/testbrain/analyze.d.ts +29 -0
- package/dist/product/testbrain/analyze.js +129 -0
- package/dist/product/truth.d.ts +12 -0
- package/dist/product/truth.js +30 -0
- package/dist/product/twin/digital-twin.d.ts +23 -0
- package/dist/product/twin/digital-twin.js +52 -0
- package/dist/product/twin/store.d.ts +21 -0
- package/dist/product/twin/store.js +70 -0
- package/dist/product/whatif/engine.d.ts +28 -0
- package/dist/product/whatif/engine.js +70 -0
- package/dist/security/paths.js +43 -15
- package/dist/utils/fs.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { retrieveProjectContext } from "./context/retrieve.js";
|
|
2
|
+
import { summarizeProjectForChat } from "./chat/project-summary.js";
|
|
3
|
+
import { wrapProjectData } from "./chat/prompts.js";
|
|
4
|
+
import { createChatService } from "./chat/service.js";
|
|
5
|
+
import { defaultStudentMode, getModeProfile, modeAllowsMutation } from "./modes.js";
|
|
6
|
+
import { buildAgentPlan, formatAgentPlan } from "./plan.js";
|
|
7
|
+
import { runCodingLoop } from "./loop.js";
|
|
8
|
+
import { resolveRepoRoot } from "../utils/path.js";
|
|
9
|
+
/**
|
|
10
|
+
* Student-oriented surface on the same AgentRuntime/ChatService (no separate engine).
|
|
11
|
+
*/
|
|
12
|
+
export class StudentService {
|
|
13
|
+
root;
|
|
14
|
+
mode;
|
|
15
|
+
chat;
|
|
16
|
+
provider;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.root = resolveRepoRoot(options.root);
|
|
19
|
+
this.mode = options.mode ?? defaultStudentMode();
|
|
20
|
+
this.provider = options.provider;
|
|
21
|
+
const profile = getModeProfile(this.mode);
|
|
22
|
+
this.chat = createChatService({
|
|
23
|
+
root: this.root,
|
|
24
|
+
provider: this.provider,
|
|
25
|
+
systemPromptAddon: profile.systemPromptAddon,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async explainProject() {
|
|
29
|
+
const summary = await summarizeProjectForChat(this.root);
|
|
30
|
+
const context = await retrieveProjectContext({
|
|
31
|
+
root: this.root,
|
|
32
|
+
query: "project architecture modules technologies",
|
|
33
|
+
budgetTokens: 4_000,
|
|
34
|
+
});
|
|
35
|
+
const sections = [
|
|
36
|
+
{
|
|
37
|
+
id: "purpose",
|
|
38
|
+
title: "Project",
|
|
39
|
+
body: `${summary.name} at ${summary.root}`,
|
|
40
|
+
truth: "VERIFIED",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "technologies",
|
|
44
|
+
title: "Technologies",
|
|
45
|
+
body: [
|
|
46
|
+
summary.languages.length ? `Languages: ${summary.languages.join(", ")}` : null,
|
|
47
|
+
summary.frameworks.length ? `Frameworks: ${summary.frameworks.join(", ")}` : null,
|
|
48
|
+
summary.packageManagers.length
|
|
49
|
+
? `Package managers: ${summary.packageManagers.join(", ")}`
|
|
50
|
+
: null,
|
|
51
|
+
]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join("\n") || "UNKNOWN — limited detector signals",
|
|
54
|
+
truth: summary.languages.length ? "VERIFIED" : "UNKNOWN",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "testing",
|
|
58
|
+
title: "Testing",
|
|
59
|
+
body: summary.hasTests
|
|
60
|
+
? "Test-like paths were detected in the repository."
|
|
61
|
+
: "UNKNOWN — no clear test layout detected.",
|
|
62
|
+
truth: summary.hasTests ? "VERIFIED" : "UNKNOWN",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "evidence",
|
|
66
|
+
title: "Evidence files",
|
|
67
|
+
body: context.citations
|
|
68
|
+
.filter((c) => c.path)
|
|
69
|
+
.slice(0, 8)
|
|
70
|
+
.map((c) => `- ${c.path}`)
|
|
71
|
+
.join("\n") || "UNKNOWN — no files retrieved",
|
|
72
|
+
truth: context.citations.some((c) => c.path) ? "VERIFIED" : "UNKNOWN",
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
const text = [
|
|
76
|
+
"",
|
|
77
|
+
"LEARN — Project explanation (evidence-backed)",
|
|
78
|
+
"",
|
|
79
|
+
...sections.flatMap((s) => [`[${s.truth}] ${s.title}`, s.body, ""]),
|
|
80
|
+
wrapProjectData(context.rendered).slice(0, 2_000),
|
|
81
|
+
"",
|
|
82
|
+
"Ask: How does X work? What should I learn next? Switch to Build With Me to implement a feature.",
|
|
83
|
+
"",
|
|
84
|
+
].join("\n");
|
|
85
|
+
return { text, sections };
|
|
86
|
+
}
|
|
87
|
+
async generateVivaQuestions() {
|
|
88
|
+
const summary = await summarizeProjectForChat(this.root);
|
|
89
|
+
const questions = [];
|
|
90
|
+
if (summary.frameworks.length) {
|
|
91
|
+
questions.push(`Why did you choose ${summary.frameworks[0]} for this project?`);
|
|
92
|
+
}
|
|
93
|
+
if (summary.languages.length) {
|
|
94
|
+
questions.push(`Explain how ${summary.languages[0]} is used in your modules.`);
|
|
95
|
+
}
|
|
96
|
+
if (summary.hasTests) {
|
|
97
|
+
questions.push("How do you test the critical paths of this project?");
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
questions.push("What testing strategy would fit this repository? (UNKNOWN today — propose based on stack only if evidenced.)");
|
|
101
|
+
}
|
|
102
|
+
questions.push("Explain the main data flow from user request to response.");
|
|
103
|
+
questions.push("What would break if you removed the authentication/authorization layer (if present)?");
|
|
104
|
+
questions.push("List security controls you implemented and where they live in the repo.");
|
|
105
|
+
// Only keep questions grounded — drop tech invention
|
|
106
|
+
return questions.slice(0, 8);
|
|
107
|
+
}
|
|
108
|
+
async generateProjectDocumentation() {
|
|
109
|
+
const { sections } = await this.explainProject();
|
|
110
|
+
const viva = await this.generateVivaQuestions();
|
|
111
|
+
return [
|
|
112
|
+
...sections,
|
|
113
|
+
{
|
|
114
|
+
id: "abstract",
|
|
115
|
+
title: "Abstract",
|
|
116
|
+
body: `This document summarizes the ${sections[0]?.body ?? "project"} using AgentDoctor repository evidence. Claims without evidence are marked UNKNOWN.`,
|
|
117
|
+
truth: "INFERRED",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "objectives",
|
|
121
|
+
title: "Objectives",
|
|
122
|
+
body: "UNKNOWN — objectives are not stored as structured metadata; derive from README only when present in evidence.",
|
|
123
|
+
truth: "UNKNOWN",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "limitations",
|
|
127
|
+
title: "Limitations",
|
|
128
|
+
body: "Generated documentation may omit undocumented modules. External services are not verified.",
|
|
129
|
+
truth: "VERIFIED",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "viva",
|
|
133
|
+
title: "Viva questions",
|
|
134
|
+
body: viva.map((q, i) => `${i + 1}. ${q}`).join("\n"),
|
|
135
|
+
truth: "INFERRED",
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* BUILD_WITH_ME / BUILD_FOR_ME: explain → plan → teach → approve → runCodingLoop → explain diffs.
|
|
141
|
+
* Reuses the shared coding engine; does not duplicate mutation logic.
|
|
142
|
+
*/
|
|
143
|
+
async buildFeature(options) {
|
|
144
|
+
const mode = this.mode;
|
|
145
|
+
if (!modeAllowsMutation(mode)) {
|
|
146
|
+
return {
|
|
147
|
+
mode,
|
|
148
|
+
explanation: `Mode ${mode} forbids writes (allowWrites=false). Switch to BUILD_WITH_ME.`,
|
|
149
|
+
teachingNotes: [],
|
|
150
|
+
planText: "",
|
|
151
|
+
coding: null,
|
|
152
|
+
evidenceText: "",
|
|
153
|
+
status: "mode_forbidden",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const plan = await buildAgentPlan({ root: this.root, goal: options.goal });
|
|
157
|
+
const planText = formatAgentPlan(plan);
|
|
158
|
+
const teach = mode === "BUILD_WITH_ME"
|
|
159
|
+
? [
|
|
160
|
+
"We will change only the files listed in the plan after your approval.",
|
|
161
|
+
"Each write goes through AgentDoctor tools (path-safe); the model cannot write files directly.",
|
|
162
|
+
"After edits we verify with change/evidence/proof signals — ENGINEERING_CORRECTNESS_NOT_CLAIMED.",
|
|
163
|
+
]
|
|
164
|
+
: ["Efficient build after approval; brief diffs; same safety gates."];
|
|
165
|
+
const explanation = [
|
|
166
|
+
"",
|
|
167
|
+
`${mode} — what will be built`,
|
|
168
|
+
"",
|
|
169
|
+
`Goal: ${options.goal}`,
|
|
170
|
+
"",
|
|
171
|
+
"Relevant concepts:",
|
|
172
|
+
...teach.map((t) => `- ${t}`),
|
|
173
|
+
"",
|
|
174
|
+
planText,
|
|
175
|
+
"",
|
|
176
|
+
].join("\n");
|
|
177
|
+
if (!options.approvedByHuman) {
|
|
178
|
+
return {
|
|
179
|
+
mode,
|
|
180
|
+
explanation,
|
|
181
|
+
teachingNotes: teach,
|
|
182
|
+
planText,
|
|
183
|
+
coding: null,
|
|
184
|
+
evidenceText: "No files modified (approval required).",
|
|
185
|
+
status: "awaiting-approval",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const coding = await runCodingLoop({
|
|
189
|
+
root: this.root,
|
|
190
|
+
goal: options.goal,
|
|
191
|
+
provider: this.provider,
|
|
192
|
+
approvedByHuman: true,
|
|
193
|
+
mode,
|
|
194
|
+
plan,
|
|
195
|
+
...(options.toolCalls ? { toolCalls: options.toolCalls } : {}),
|
|
196
|
+
...(options.useModelLoop !== undefined ? { useModelLoop: options.useModelLoop } : {}),
|
|
197
|
+
verify: options.verify !== false,
|
|
198
|
+
runTests: options.runTests === true,
|
|
199
|
+
});
|
|
200
|
+
const evidenceText = [
|
|
201
|
+
coding.responseText,
|
|
202
|
+
"",
|
|
203
|
+
mode === "BUILD_WITH_ME"
|
|
204
|
+
? "What changed: see files and diffs above. Why: to satisfy the approved plan. Concepts: controlled tools + verification."
|
|
205
|
+
: "Changes applied under approval; see verification summary.",
|
|
206
|
+
"",
|
|
207
|
+
].join("\n");
|
|
208
|
+
const status = coding.stoppedReason === "completed"
|
|
209
|
+
? "completed"
|
|
210
|
+
: coding.stoppedReason === "limit"
|
|
211
|
+
? "limit"
|
|
212
|
+
: coding.stoppedReason === "awaiting-approval"
|
|
213
|
+
? "awaiting-approval"
|
|
214
|
+
: coding.stoppedReason === "mode_forbidden"
|
|
215
|
+
? "mode_forbidden"
|
|
216
|
+
: "failed";
|
|
217
|
+
return {
|
|
218
|
+
mode,
|
|
219
|
+
explanation,
|
|
220
|
+
teachingNotes: [...teach, ...(coding.teachingNotes ?? [])],
|
|
221
|
+
planText,
|
|
222
|
+
coding,
|
|
223
|
+
evidenceText,
|
|
224
|
+
status,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async end() {
|
|
228
|
+
await this.chat.end();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentToolCall, AgentToolName, AgentToolResult } from "./types.js";
|
|
2
|
+
import { type AgentMode } from "../modes.js";
|
|
3
|
+
import { type WorkspaceModel } from "../../workspace/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Execute an agent tool.
|
|
6
|
+
* Write/execute tools require explicit allowWrite/allowExecute (after human approval).
|
|
7
|
+
*/
|
|
8
|
+
export declare function executeAgentTool(rootInput: string, call: AgentToolCall, options?: {
|
|
9
|
+
allowWrite?: boolean;
|
|
10
|
+
allowExecute?: boolean;
|
|
11
|
+
/** Human approval already obtained for this session/action */
|
|
12
|
+
approvedByHuman?: boolean;
|
|
13
|
+
/** Agent mode — LEARN hard-blocks write/execute regardless of approval */
|
|
14
|
+
mode?: AgentMode;
|
|
15
|
+
/** Optional multi-root workspace isolation context */
|
|
16
|
+
workspace?: WorkspaceModel | null;
|
|
17
|
+
/** Forensic mode — refuse all mutations */
|
|
18
|
+
forensicMode?: boolean;
|
|
19
|
+
}): Promise<AgentToolResult>;
|
|
20
|
+
export declare function isReadTool(name: AgentToolName): boolean;
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { handleArchitectureCheckTool, handleCallGraphLookup, handleCodebaseSearch, handleDependencyLookup, handleRefactorImpactTool, handleSymbolLookup, handleTestImpactTool, } from "../../mcp/intelligence/handlers.js";
|
|
4
|
+
import { scan } from "../../core/scanner/scan.js";
|
|
5
|
+
import { analyzeChanges } from "../../core/changes/analyze.js";
|
|
6
|
+
import { discoverFiles } from "../../discovery/files.js";
|
|
7
|
+
import { PathEscapeError, resolveSafeRepoPath } from "../../security/paths.js";
|
|
8
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
9
|
+
import { redactSecrets } from "../../platform/security/redact.js";
|
|
10
|
+
import { summarizeProjectForChat } from "../chat/project-summary.js";
|
|
11
|
+
import { getToolSpec, riskForTool } from "./registry.js";
|
|
12
|
+
import { createFileSafe, deleteFileSafe, editFileSafe } from "./write.js";
|
|
13
|
+
import { inferTestArgv, runAgentCommand } from "./run.js";
|
|
14
|
+
import { evaluateApproval } from "../approvals.js";
|
|
15
|
+
import { modeBlocksToolCategory } from "../modes.js";
|
|
16
|
+
import { assertWorkspacePathAccess } from "../../workspace/index.js";
|
|
17
|
+
import { assertForensicReadOnly } from "../../product/forensic/mode.js";
|
|
18
|
+
function asRecord(args) {
|
|
19
|
+
return args;
|
|
20
|
+
}
|
|
21
|
+
function str(args, key) {
|
|
22
|
+
const v = args[key];
|
|
23
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Execute an agent tool.
|
|
27
|
+
* Write/execute tools require explicit allowWrite/allowExecute (after human approval).
|
|
28
|
+
*/
|
|
29
|
+
export async function executeAgentTool(rootInput, call, options) {
|
|
30
|
+
const started = Date.now();
|
|
31
|
+
const root = resolveRepoRoot(rootInput);
|
|
32
|
+
const risk = riskForTool(call.name);
|
|
33
|
+
const spec = getToolSpec(call.name);
|
|
34
|
+
const fail = (code, message) => ({
|
|
35
|
+
callId: call.id,
|
|
36
|
+
name: call.name,
|
|
37
|
+
ok: false,
|
|
38
|
+
data: null,
|
|
39
|
+
error: { code, message },
|
|
40
|
+
risk,
|
|
41
|
+
durationMs: Date.now() - started,
|
|
42
|
+
});
|
|
43
|
+
const ok = (data) => ({
|
|
44
|
+
callId: call.id,
|
|
45
|
+
name: call.name,
|
|
46
|
+
ok: true,
|
|
47
|
+
data: redactDeep(data),
|
|
48
|
+
risk,
|
|
49
|
+
durationMs: Date.now() - started,
|
|
50
|
+
});
|
|
51
|
+
if (!spec)
|
|
52
|
+
return fail("unknown_tool", `Unknown tool: ${call.name}`);
|
|
53
|
+
const forensic = options?.forensicMode === true || process.env.AGENTDOCTOR_FORENSIC_MODE === "1";
|
|
54
|
+
if (forensic && (spec.category === "write" || spec.category === "execute")) {
|
|
55
|
+
try {
|
|
56
|
+
assertForensicReadOnly(true);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return fail("forensic_read_only", error instanceof Error ? error.message : "Forensic mode refuses write operations");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (modeBlocksToolCategory(options?.mode, spec.category)) {
|
|
63
|
+
return fail("mode_forbidden", `Mode ${options?.mode ?? "LEARN"} does not allow ${spec.category} tools (allowWrites=false). The model cannot override this.`);
|
|
64
|
+
}
|
|
65
|
+
if (spec.category === "write" || spec.category === "execute") {
|
|
66
|
+
const gate = evaluateApproval({ action: `tool:${call.name}`, risk: risk, toolName: call.name }, { approvedByHuman: options?.approvedByHuman === true });
|
|
67
|
+
if (gate.needsHumanApproval) {
|
|
68
|
+
return fail("approval_required", gate.reason);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (spec.category === "write" && !options?.allowWrite) {
|
|
72
|
+
return fail("not_enabled", "Write tools require allowWrite after human approval.");
|
|
73
|
+
}
|
|
74
|
+
if (spec.category === "execute" && !options?.allowExecute) {
|
|
75
|
+
return fail("not_enabled", "Execute tools require allowExecute after human approval.");
|
|
76
|
+
}
|
|
77
|
+
const ensureWorkspace = (absPath) => {
|
|
78
|
+
// Repo-root isolation is already enforced by resolveSafeRepoPath.
|
|
79
|
+
// Workspace gate applies only when a multi-root WorkspaceModel is provided
|
|
80
|
+
// (avoids false denies from /var vs /private/var when workspace is null).
|
|
81
|
+
if (options?.workspace == null)
|
|
82
|
+
return null;
|
|
83
|
+
const access = assertWorkspacePathAccess({
|
|
84
|
+
workspace: options.workspace,
|
|
85
|
+
operationRoot: root,
|
|
86
|
+
targetPath: absPath,
|
|
87
|
+
});
|
|
88
|
+
if (!access.allowed) {
|
|
89
|
+
return fail("workspace_denied", access.reason);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
};
|
|
93
|
+
try {
|
|
94
|
+
switch (call.name) {
|
|
95
|
+
case "read_file": {
|
|
96
|
+
const rel = str(call.arguments, "path");
|
|
97
|
+
if (!rel)
|
|
98
|
+
return fail("invalid_argument", "path required");
|
|
99
|
+
let abs;
|
|
100
|
+
try {
|
|
101
|
+
abs = resolveSafeRepoPath(root, rel);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof PathEscapeError) {
|
|
105
|
+
return fail("path_escape", "path escapes repository root");
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
const denied = ensureWorkspace(abs);
|
|
110
|
+
if (denied)
|
|
111
|
+
return denied;
|
|
112
|
+
const maxBytes = typeof call.arguments.maxBytes === "number" ? call.arguments.maxBytes : 200_000;
|
|
113
|
+
const buf = await fs.readFile(abs);
|
|
114
|
+
if (buf.byteLength > maxBytes) {
|
|
115
|
+
return fail("too_large", `File exceeds ${maxBytes} bytes`);
|
|
116
|
+
}
|
|
117
|
+
const text = buf.toString("utf8");
|
|
118
|
+
return ok({
|
|
119
|
+
path: rel.split(path.sep).join("/"),
|
|
120
|
+
bytes: buf.byteLength,
|
|
121
|
+
content: redactSecrets(text).text,
|
|
122
|
+
channel: "PROJECT_DATA",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
case "list_files": {
|
|
126
|
+
const limit = typeof call.arguments.limit === "number" ? call.arguments.limit : 200;
|
|
127
|
+
const discovery = await discoverFiles({ root });
|
|
128
|
+
const files = discovery.files.slice(0, limit).map((f) => f.relativePath);
|
|
129
|
+
return ok({
|
|
130
|
+
count: discovery.files.length,
|
|
131
|
+
files,
|
|
132
|
+
truncated: discovery.files.length > limit,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
case "search_code":
|
|
136
|
+
return ok(await handleCodebaseSearch(root, asRecord(call.arguments)));
|
|
137
|
+
case "find_symbol":
|
|
138
|
+
return ok(await handleSymbolLookup(root, asRecord(call.arguments)));
|
|
139
|
+
case "find_references": {
|
|
140
|
+
const symbol = str(call.arguments, "symbol");
|
|
141
|
+
if (!symbol)
|
|
142
|
+
return fail("invalid_argument", "symbol required");
|
|
143
|
+
return ok(await handleRefactorImpactTool(root, { symbol }));
|
|
144
|
+
}
|
|
145
|
+
case "find_callers": {
|
|
146
|
+
const target = str(call.arguments, "target");
|
|
147
|
+
if (!target)
|
|
148
|
+
return fail("invalid_argument", "target required");
|
|
149
|
+
const raw = (await handleCallGraphLookup(root, { symbol: target }));
|
|
150
|
+
const edges = (raw.calls ?? []).filter((e) => e.to.includes(target));
|
|
151
|
+
return ok({ ok: raw.ok, direction: "callers", target, edges });
|
|
152
|
+
}
|
|
153
|
+
case "find_callees": {
|
|
154
|
+
const target = str(call.arguments, "target");
|
|
155
|
+
if (!target)
|
|
156
|
+
return fail("invalid_argument", "target required");
|
|
157
|
+
const raw = (await handleCallGraphLookup(root, { symbol: target }));
|
|
158
|
+
const edges = (raw.calls ?? []).filter((e) => e.from.includes(target));
|
|
159
|
+
return ok({ ok: raw.ok, direction: "callees", target, edges });
|
|
160
|
+
}
|
|
161
|
+
case "inspect_project":
|
|
162
|
+
return ok(await summarizeProjectForChat(root));
|
|
163
|
+
case "inspect_architecture":
|
|
164
|
+
return ok(await handleArchitectureCheckTool(root));
|
|
165
|
+
case "inspect_dependencies": {
|
|
166
|
+
const target = str(call.arguments, "target");
|
|
167
|
+
if (!target)
|
|
168
|
+
return fail("invalid_argument", "target required");
|
|
169
|
+
return ok(await handleDependencyLookup(root, { target }));
|
|
170
|
+
}
|
|
171
|
+
case "inspect_tests":
|
|
172
|
+
return ok(await handleTestImpactTool(root));
|
|
173
|
+
case "inspect_findings": {
|
|
174
|
+
const result = await scan({ cwd: root });
|
|
175
|
+
return ok({
|
|
176
|
+
findings: result.findings.slice(0, 50),
|
|
177
|
+
summary: result.summary,
|
|
178
|
+
agents: result.agents.map((a) => ({
|
|
179
|
+
id: a.id,
|
|
180
|
+
detected: a.detected,
|
|
181
|
+
configured: a.configured,
|
|
182
|
+
status: a.status,
|
|
183
|
+
})),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
case "inspect_git_status": {
|
|
187
|
+
const report = await analyzeChanges({ root });
|
|
188
|
+
return ok({
|
|
189
|
+
gitAvailable: report.gitAvailable,
|
|
190
|
+
files: report.files.slice(0, 100),
|
|
191
|
+
summary: report.summary,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
case "inspect_git_diff": {
|
|
195
|
+
const since = str(call.arguments, "since") ?? undefined;
|
|
196
|
+
const report = await analyzeChanges({
|
|
197
|
+
root,
|
|
198
|
+
...(since ? { since } : {}),
|
|
199
|
+
impact: true,
|
|
200
|
+
});
|
|
201
|
+
return ok({
|
|
202
|
+
gitAvailable: report.gitAvailable,
|
|
203
|
+
files: report.files.slice(0, 100),
|
|
204
|
+
summary: report.summary,
|
|
205
|
+
impact: report.impact ?? null,
|
|
206
|
+
note: "Full unified patch text is not dumped; use change analyze for evidence.",
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
case "create_file": {
|
|
210
|
+
const rel = str(call.arguments, "path");
|
|
211
|
+
const content = typeof call.arguments.content === "string" ? call.arguments.content : null;
|
|
212
|
+
if (!rel || content === null)
|
|
213
|
+
return fail("invalid_argument", "path and content required");
|
|
214
|
+
let absCreate;
|
|
215
|
+
try {
|
|
216
|
+
absCreate = resolveSafeRepoPath(root, rel);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (error instanceof PathEscapeError) {
|
|
220
|
+
return fail("path_escape", "path escapes repository root");
|
|
221
|
+
}
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
const denyCreate = ensureWorkspace(absCreate);
|
|
225
|
+
if (denyCreate)
|
|
226
|
+
return denyCreate;
|
|
227
|
+
return ok(await createFileSafe(root, rel, content));
|
|
228
|
+
}
|
|
229
|
+
case "edit_file": {
|
|
230
|
+
const rel = str(call.arguments, "path");
|
|
231
|
+
if (!rel)
|
|
232
|
+
return fail("invalid_argument", "path required");
|
|
233
|
+
let absEdit;
|
|
234
|
+
try {
|
|
235
|
+
absEdit = resolveSafeRepoPath(root, rel);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
if (error instanceof PathEscapeError) {
|
|
239
|
+
return fail("path_escape", "path escapes repository root");
|
|
240
|
+
}
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
const denyEdit = ensureWorkspace(absEdit);
|
|
244
|
+
if (denyEdit)
|
|
245
|
+
return denyEdit;
|
|
246
|
+
const editOpts = {};
|
|
247
|
+
if (typeof call.arguments.content === "string")
|
|
248
|
+
editOpts.content = call.arguments.content;
|
|
249
|
+
if (typeof call.arguments.oldContent === "string") {
|
|
250
|
+
editOpts.oldContent = call.arguments.oldContent;
|
|
251
|
+
}
|
|
252
|
+
if (typeof call.arguments.startLine === "number") {
|
|
253
|
+
editOpts.startLine = call.arguments.startLine;
|
|
254
|
+
}
|
|
255
|
+
if (typeof call.arguments.endLine === "number")
|
|
256
|
+
editOpts.endLine = call.arguments.endLine;
|
|
257
|
+
if (typeof call.arguments.replacement === "string") {
|
|
258
|
+
editOpts.replacement = call.arguments.replacement;
|
|
259
|
+
}
|
|
260
|
+
return ok(await editFileSafe(root, rel, editOpts));
|
|
261
|
+
}
|
|
262
|
+
case "delete_file": {
|
|
263
|
+
const rel = str(call.arguments, "path");
|
|
264
|
+
if (!rel)
|
|
265
|
+
return fail("invalid_argument", "path required");
|
|
266
|
+
let absDel;
|
|
267
|
+
try {
|
|
268
|
+
absDel = resolveSafeRepoPath(root, rel);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
if (error instanceof PathEscapeError) {
|
|
272
|
+
return fail("path_escape", "path escapes repository root");
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
const denyDel = ensureWorkspace(absDel);
|
|
277
|
+
if (denyDel)
|
|
278
|
+
return denyDel;
|
|
279
|
+
return ok(await deleteFileSafe(root, rel));
|
|
280
|
+
}
|
|
281
|
+
case "run_command": {
|
|
282
|
+
const command = str(call.arguments, "command") ?? undefined;
|
|
283
|
+
const argv = Array.isArray(call.arguments.argv)
|
|
284
|
+
? call.arguments.argv.filter((x) => typeof x === "string")
|
|
285
|
+
: undefined;
|
|
286
|
+
if (!command && (!argv || argv.length === 0)) {
|
|
287
|
+
return fail("invalid_argument", "command or argv required");
|
|
288
|
+
}
|
|
289
|
+
const result = await runAgentCommand({
|
|
290
|
+
root,
|
|
291
|
+
...(command ? { command } : {}),
|
|
292
|
+
...(argv ? { argv } : {}),
|
|
293
|
+
execute: true,
|
|
294
|
+
});
|
|
295
|
+
if (result.blocked) {
|
|
296
|
+
return fail("blocked", result.reason);
|
|
297
|
+
}
|
|
298
|
+
if (!result.ok) {
|
|
299
|
+
return fail(result.decision === "require-approval" ? "policy_approval" : "command_failed", result.reason || result.notice);
|
|
300
|
+
}
|
|
301
|
+
return ok(result);
|
|
302
|
+
}
|
|
303
|
+
case "run_tests": {
|
|
304
|
+
const command = str(call.arguments, "command");
|
|
305
|
+
let argv;
|
|
306
|
+
if (command) {
|
|
307
|
+
// fall through to runAgentCommand with command string
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
try {
|
|
311
|
+
const pkgRaw = await fs.readFile(path.join(root, "package.json"), "utf8");
|
|
312
|
+
argv = inferTestArgv(JSON.parse(pkgRaw)) ?? undefined;
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
argv = undefined;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (!command && !argv) {
|
|
319
|
+
return fail("no_test_runner", "Could not infer test command; pass command explicitly (e.g. npm test).");
|
|
320
|
+
}
|
|
321
|
+
const result = await runAgentCommand({
|
|
322
|
+
root,
|
|
323
|
+
...(command ? { command } : {}),
|
|
324
|
+
...(argv ? { argv } : {}),
|
|
325
|
+
execute: true,
|
|
326
|
+
timeoutMs: 120_000,
|
|
327
|
+
});
|
|
328
|
+
if (result.blocked) {
|
|
329
|
+
return fail("blocked", result.reason);
|
|
330
|
+
}
|
|
331
|
+
if (!result.ok) {
|
|
332
|
+
return fail(result.decision === "require-approval" ? "policy_approval" : "command_failed", result.reason || result.notice);
|
|
333
|
+
}
|
|
334
|
+
return ok(result);
|
|
335
|
+
}
|
|
336
|
+
default: {
|
|
337
|
+
const _exhaustive = call.name;
|
|
338
|
+
return fail("unknown_tool", `Unhandled tool: ${_exhaustive}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (error instanceof PathEscapeError) {
|
|
344
|
+
return fail("path_escape", "path escapes repository root");
|
|
345
|
+
}
|
|
346
|
+
return fail("tool_error", error instanceof Error ? error.message : String(error));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function redactDeep(value) {
|
|
350
|
+
if (typeof value === "string")
|
|
351
|
+
return redactSecrets(value).text;
|
|
352
|
+
if (Array.isArray(value))
|
|
353
|
+
return value.map(redactDeep);
|
|
354
|
+
if (value && typeof value === "object") {
|
|
355
|
+
const out = {};
|
|
356
|
+
for (const [k, v] of Object.entries(value)) {
|
|
357
|
+
out[k] = redactDeep(v);
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
return value;
|
|
362
|
+
}
|
|
363
|
+
export function isReadTool(name) {
|
|
364
|
+
return getToolSpec(name)?.category === "read";
|
|
365
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { AgentToolName, AgentToolSpec, AgentToolCall, AgentToolResult, AgentRiskLevel, } from "./types.js";
|
|
2
|
+
export { newToolCall } from "./types.js";
|
|
3
|
+
export { listAgentToolSpecs, getToolSpec, riskForTool } from "./registry.js";
|
|
4
|
+
export { executeAgentTool, isReadTool } from "./execute.js";
|
|
5
|
+
export { createFileSafe, editFileSafe, deleteFileSafe, buildUnifiedDiff } from "./write.js";
|
|
6
|
+
export { runAgentCommand, inferTestArgv } from "./run.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { newToolCall } from "./types.js";
|
|
2
|
+
export { listAgentToolSpecs, getToolSpec, riskForTool } from "./registry.js";
|
|
3
|
+
export { executeAgentTool, isReadTool } from "./execute.js";
|
|
4
|
+
export { createFileSafe, editFileSafe, deleteFileSafe, buildUnifiedDiff } from "./write.js";
|
|
5
|
+
export { runAgentCommand, inferTestArgv } from "./run.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AgentRiskLevel, AgentToolName, AgentToolSpec } from "./types.js";
|
|
2
|
+
export declare function listAgentToolSpecs(options?: {
|
|
3
|
+
includeWrite?: boolean;
|
|
4
|
+
includeExecute?: boolean;
|
|
5
|
+
}): AgentToolSpec[];
|
|
6
|
+
export declare function getToolSpec(name: AgentToolName): AgentToolSpec | undefined;
|
|
7
|
+
export declare function riskForTool(name: AgentToolName): AgentRiskLevel;
|