Step 245: HeadlessEditorState — agent API surface without ImGui/SDL
Adds the headless agent architecture for Sprint 9 Phase 9a: - ASTUtils.h: pure AST utilities extracted from EditorUtils.h - HeadlessEditorState.h: GUI-free state with buffer management - HeadlessAgentRPCHandler.h: full RPC dispatch (20+ methods) - step245_test.cpp: 20 tests all passing Also fixes test compilation errors (NotificationSystem API changes, AgentRole permissions, DependencyPanel missing include) and adds SDL2 system library detection fix in CMakeLists.txt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -764,7 +764,13 @@ target_link_libraries(step125_integration_test PRIVATE
|
||||
unofficial::tree-sitter::tree-sitter
|
||||
tree_sitter_python
|
||||
tree_sitter_cpp
|
||||
tree_sitter_elisp)
|
||||
tree_sitter_elisp
|
||||
tree_sitter_javascript
|
||||
tree_sitter_rust
|
||||
tree_sitter_go
|
||||
tree_sitter_typescript
|
||||
tree_sitter_java
|
||||
tree_sitter_org)
|
||||
|
||||
add_executable(step126_test tests/step126_test.cpp)
|
||||
target_include_directories(step126_test PRIVATE src)
|
||||
@@ -780,7 +786,7 @@ target_link_libraries(step128_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
add_executable(step129_test tests/step129_test.cpp)
|
||||
target_include_directories(step129_test PRIVATE src)
|
||||
target_link_libraries(step129_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
target_link_libraries(step129_test PRIVATE nlohmann_json::nlohmann_json imgui::imgui)
|
||||
|
||||
add_executable(step130_test tests/step130_test.cpp)
|
||||
target_include_directories(step130_test PRIVATE src)
|
||||
@@ -1249,7 +1255,20 @@ target_link_libraries(step206_test PRIVATE
|
||||
tree_sitter_rust
|
||||
tree_sitter_go)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
if(UNIX AND NOT APPLE)
|
||||
# Force system SDL2 on Linux. The vcpkg toolchain overrides PKG_CONFIG_PATH,
|
||||
# causing pkg_check_modules to find vcpkg's headless (offscreen-only) SDL2.
|
||||
# We bypass that by pointing pkg-config directly at the system .pc files.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
set(_saved_pkg_config_path "$ENV{PKG_CONFIG_PATH}")
|
||||
set(ENV{PKG_CONFIG_PATH} "/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig")
|
||||
pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2)
|
||||
set(ENV{PKG_CONFIG_PATH} "${_saved_pkg_config_path}")
|
||||
add_library(SDL2::SDL2 INTERFACE IMPORTED)
|
||||
target_link_libraries(SDL2::SDL2 INTERFACE PkgConfig::SDL2)
|
||||
else()
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
endif()
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
find_package(imgui CONFIG REQUIRED)
|
||||
@@ -1296,7 +1315,7 @@ if(IMGUI_SDL2_BACKEND)
|
||||
endif()
|
||||
target_link_libraries(whetstone_editor PRIVATE
|
||||
SDL2::SDL2
|
||||
SDL2::SDL2main
|
||||
$<TARGET_NAME_IF_EXISTS:SDL2::SDL2main>
|
||||
OpenGL::GL
|
||||
glad::glad
|
||||
imgui::imgui
|
||||
@@ -1335,4 +1354,20 @@ add_executable(whetstone_pipeline src/pipeline_main.cpp)
|
||||
target_include_directories(whetstone_pipeline PRIVATE src)
|
||||
target_link_libraries(whetstone_pipeline PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
# Step 245: Headless EditorState test (no SDL/ImGui dependency)
|
||||
add_executable(step245_test tests/step245_test.cpp)
|
||||
target_include_directories(step245_test PRIVATE src)
|
||||
target_link_libraries(step245_test PRIVATE
|
||||
nlohmann_json::nlohmann_json
|
||||
unofficial::tree-sitter::tree-sitter
|
||||
tree_sitter_python
|
||||
tree_sitter_cpp
|
||||
tree_sitter_elisp
|
||||
tree_sitter_javascript
|
||||
tree_sitter_typescript
|
||||
tree_sitter_java
|
||||
tree_sitter_rust
|
||||
tree_sitter_go
|
||||
tree_sitter_org)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
77
editor/src/ASTUtils.h
Normal file
77
editor/src/ASTUtils.h
Normal file
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
// ASTUtils.h — Pure AST utility functions (no ImGui/SDL dependency).
|
||||
// Extracted for use by both EditorUtils.h and HeadlessEditorState.h.
|
||||
|
||||
#include "ast/ASTNode.h"
|
||||
#include "ast/Serialization.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
// Clone a Module tree (deep copy via JSON roundtrip)
|
||||
static inline std::unique_ptr<Module> cloneModule(const Module* ast) {
|
||||
if (!ast) return nullptr;
|
||||
json j = toJson(ast);
|
||||
ASTNode* node = fromJson(j);
|
||||
if (!node || node->conceptType != "Module") return nullptr;
|
||||
return std::unique_ptr<Module>(static_cast<Module*>(node));
|
||||
}
|
||||
|
||||
// Check if a node is an annotation type
|
||||
static inline bool isAnnotationNode(const ASTNode* node) {
|
||||
if (!node) return false;
|
||||
if (node->conceptType.find("Annotation") != std::string::npos) return true;
|
||||
if (node->conceptType == "DerefStrategy") return true;
|
||||
if (node->conceptType == "OptimizationLock") return true;
|
||||
if (node->conceptType == "LangSpecific") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Count annotation nodes in an AST subtree
|
||||
static inline int countAnnotationNodes(const ASTNode* node) {
|
||||
if (!node) return 0;
|
||||
int count = isAnnotationNode(node) ? 1 : 0;
|
||||
for (const auto* child : node->allChildren())
|
||||
count += countAnnotationNodes(child);
|
||||
return count;
|
||||
}
|
||||
|
||||
// Find a node by ID in an AST tree
|
||||
static inline ASTNode* findNodeById(ASTNode* node,
|
||||
const std::string& id) {
|
||||
if (!node) return nullptr;
|
||||
if (node->id == id) return node;
|
||||
for (auto* child : node->allChildren())
|
||||
if (auto* found = findNodeById(child, id)) return found;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Check if a span contains a position
|
||||
static inline bool spanContains(const ASTNode* node, int line, int col) {
|
||||
if (!node || !node->hasSpan()) return false;
|
||||
if (line < node->spanStartLine || line > node->spanEndLine) return false;
|
||||
if (line == node->spanStartLine && col < node->spanStartCol) return false;
|
||||
if (line == node->spanEndLine && col > node->spanEndCol) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Span score (smaller span = better match)
|
||||
static inline int spanScore(const ASTNode* node) {
|
||||
if (!node || !node->hasSpan()) return 999999;
|
||||
return (node->spanEndLine - node->spanStartLine) * 1000 +
|
||||
(node->spanEndCol - node->spanStartCol);
|
||||
}
|
||||
|
||||
// Find a node at a line/col position
|
||||
static inline ASTNode* findNodeAtPosition(ASTNode* node,
|
||||
int line, int col) {
|
||||
if (!node) return nullptr;
|
||||
ASTNode* best = nullptr;
|
||||
if (spanContains(node, line, col)) best = node;
|
||||
for (auto* child : node->allChildren()) {
|
||||
ASTNode* cand = findNodeAtPosition(child, line, col);
|
||||
if (cand) {
|
||||
if (!best || spanScore(cand) < spanScore(best)) best = cand;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <fstream>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
558
editor/src/HeadlessAgentRPCHandler.h
Normal file
558
editor/src/HeadlessAgentRPCHandler.h
Normal file
@@ -0,0 +1,558 @@
|
||||
#pragma once
|
||||
// Step 245: Headless Agent RPC Handler
|
||||
//
|
||||
// Mirrors AgentRPCHandler.h but operates on HeadlessEditorState.
|
||||
// Uses the same agentRpcError/agentRpcResult helpers and supports
|
||||
// the same JSON-RPC methods for the agent-critical path:
|
||||
// getAST, parseSource, generateFromAST, runPipeline, projectLanguage,
|
||||
// applyMutation, applyBatch, getInScopeSymbols, getCallHierarchy,
|
||||
// getDependencyGraph, generateCode, setAgentRole,
|
||||
// getAnnotationSuggestions, applyAnnotationSuggestion,
|
||||
// recordAnnotationFeedback, startWorkflowRecording,
|
||||
// stopWorkflowRecording, getWorkflowRecording, replayWorkflow, ping
|
||||
//
|
||||
// Deliberately omits GUI-only methods. Stays under 600 lines.
|
||||
|
||||
struct HeadlessEditorState;
|
||||
|
||||
// Re-use the response helpers (same signature as AgentRPCHandler.h)
|
||||
static inline json headlessRpcError(const json& id, int code,
|
||||
const std::string& msg) {
|
||||
return {{"jsonrpc", "2.0"}, {"id", id},
|
||||
{"error", {{"code", code}, {"message", msg}}}};
|
||||
}
|
||||
|
||||
static inline json headlessRpcResult(const json& id, const json& result) {
|
||||
return {{"jsonrpc", "2.0"}, {"id", id}, {"result", result}};
|
||||
}
|
||||
|
||||
static inline json headlessRequireAST(HeadlessEditorState& state,
|
||||
const json& id) {
|
||||
if (!state.active() || !state.isStructured())
|
||||
return headlessRpcError(id, -32000, "No structured buffer");
|
||||
if (!state.activeAST())
|
||||
return headlessRpcError(id, -32001, "AST unavailable");
|
||||
return json();
|
||||
}
|
||||
|
||||
static inline json headlessRequireMutable(HeadlessEditorState& state,
|
||||
const json& id) {
|
||||
if (!state.active() || !state.isStructured())
|
||||
return headlessRpcError(id, -32000, "No structured buffer");
|
||||
if (!state.mutationAST())
|
||||
return headlessRpcError(id, -32001, "AST unavailable");
|
||||
return json();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Main dispatch
|
||||
// -----------------------------------------------------------------------
|
||||
inline json handleHeadlessAgentRequest(HeadlessEditorState& state,
|
||||
const json& request,
|
||||
const std::string& sessionId) {
|
||||
json id = request.contains("id") ? request["id"] : json(nullptr);
|
||||
std::string method = request.value("method", "");
|
||||
AgentRole role = state.getAgentRole(sessionId);
|
||||
|
||||
// --- ping ---
|
||||
if (method == "ping")
|
||||
return headlessRpcResult(id, "pong");
|
||||
|
||||
// --- getAST ---
|
||||
if (method == "getAST") {
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
return headlessRpcResult(id, {
|
||||
{"ast", toJson(state.activeAST())},
|
||||
{"annotationCount", countAnnotationNodes(state.activeAST())},
|
||||
{"diagnostics", state.buildDiagnosticsJson()}
|
||||
});
|
||||
}
|
||||
|
||||
// --- parseSource ---
|
||||
if (method == "parseSource") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string source = params.value("source", "");
|
||||
std::string language = params.value("language", "");
|
||||
if (source.empty() || language.empty())
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing source or language");
|
||||
Pipeline pipeline;
|
||||
std::vector<ParseDiagnostic> diags;
|
||||
auto mod = pipeline.parse(source, language, diags);
|
||||
json diagArr = json::array();
|
||||
for (const auto& d : diags)
|
||||
diagArr.push_back({{"line", d.line}, {"column", d.column},
|
||||
{"message", d.message},
|
||||
{"severity", d.severity}});
|
||||
json result = {{"diagnostics", diagArr}};
|
||||
if (mod) result["ast"] = toJson(mod.get());
|
||||
return headlessRpcResult(id, result);
|
||||
}
|
||||
|
||||
// --- generateFromAST ---
|
||||
if (method == "generateFromAST") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string language = params.value("language",
|
||||
state.active()->language);
|
||||
Pipeline pipeline;
|
||||
std::string code = pipeline.generate(state.activeAST(), language);
|
||||
return headlessRpcResult(id, {{"code", code},
|
||||
{"language", language}});
|
||||
}
|
||||
|
||||
// --- runPipeline ---
|
||||
if (method == "runPipeline") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string source = params.value("source", "");
|
||||
std::string srcLang = params.value("sourceLanguage", "");
|
||||
std::string tgtLang = params.value("targetLanguage", "");
|
||||
if (source.empty() || srcLang.empty() || tgtLang.empty())
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing source, sourceLanguage, or targetLanguage");
|
||||
Pipeline pipeline;
|
||||
auto pr = pipeline.run(source, srcLang, tgtLang);
|
||||
json diagArr = json::array();
|
||||
for (const auto& d : pr.parseDiags)
|
||||
diagArr.push_back({{"line", d.line}, {"column", d.column},
|
||||
{"message", d.message},
|
||||
{"severity", d.severity}});
|
||||
json valArr = json::array();
|
||||
for (const auto& d : pr.validationDiags)
|
||||
valArr.push_back({{"severity", d.severity},
|
||||
{"message", d.message},
|
||||
{"nodeId", d.nodeId}});
|
||||
json violArr = json::array();
|
||||
for (const auto& v : pr.violations)
|
||||
violArr.push_back({{"severity", v.severity},
|
||||
{"category", v.category},
|
||||
{"message", v.message},
|
||||
{"nodeId", v.nodeId}});
|
||||
json suggArr = json::array();
|
||||
for (const auto& s : pr.suggestions)
|
||||
suggArr.push_back({{"nodeId", s.nodeId},
|
||||
{"annotationType", s.annotationType},
|
||||
{"strategy", s.strategy},
|
||||
{"reason", s.reason},
|
||||
{"confidence", s.confidence}});
|
||||
json result = {
|
||||
{"success", pr.success}, {"generatedCode", pr.generatedCode},
|
||||
{"parseDiagnostics", diagArr},
|
||||
{"validationDiagnostics", valArr},
|
||||
{"violations", violArr}, {"suggestions", suggArr},
|
||||
{"foldCount", pr.foldResult.nodesModified},
|
||||
{"dceCount", pr.dceResult.nodesModified}
|
||||
};
|
||||
if (pr.ast) result["ast"] = toJson(pr.ast.get());
|
||||
return headlessRpcResult(id, result);
|
||||
}
|
||||
|
||||
// --- projectLanguage ---
|
||||
if (method == "projectLanguage") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string targetLanguage = params.value("targetLanguage", "");
|
||||
if (targetLanguage.empty())
|
||||
return headlessRpcError(id, -32602, "Missing targetLanguage");
|
||||
CrossLanguageProjector projector;
|
||||
auto projected = projector.project(state.activeAST(),
|
||||
targetLanguage);
|
||||
if (!projected)
|
||||
return headlessRpcError(id, -32020, "Projection failed");
|
||||
Pipeline pipeline;
|
||||
std::string code = pipeline.generate(projected.get(),
|
||||
targetLanguage);
|
||||
return headlessRpcResult(id, {
|
||||
{"ast", toJson(projected.get())}, {"generatedCode", code},
|
||||
{"sourceLanguage", state.active()->language},
|
||||
{"targetLanguage", targetLanguage}
|
||||
});
|
||||
}
|
||||
|
||||
// --- setAgentRole ---
|
||||
if (method == "setAgentRole") {
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string roleName = params.value("role", "linter");
|
||||
AgentRole newRole =
|
||||
AgentPermissionPolicy::roleFromString(roleName);
|
||||
state.setAgentRole(sessionId, newRole);
|
||||
return headlessRpcResult(id,
|
||||
{{"role", AgentPermissionPolicy::roleLabel(newRole)}});
|
||||
}
|
||||
|
||||
// --- generateCode ---
|
||||
if (method == "generateCode") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string spec = params.value("spec", "");
|
||||
bool preferImports = params.value("preferImports", true);
|
||||
state.library.primitives.setRoot(state.activeAST());
|
||||
state.library.primitives.setLanguage(state.active()->language);
|
||||
AgentCodeGen gen;
|
||||
state.library.primitives.setContextTags(
|
||||
state.library.semanticTags.inferTagsFromText(spec));
|
||||
AgentCodeGenResult genRes = gen.generate(
|
||||
spec, state.library.primitives,
|
||||
state.active()->language, preferImports);
|
||||
if (!genRes.node)
|
||||
return headlessRpcError(id, -32020, "Code generation failed");
|
||||
json nodeJson = toJson(genRes.node);
|
||||
deleteTree(genRes.node);
|
||||
return headlessRpcResult(id, {
|
||||
{"node", nodeJson}, {"note", genRes.note},
|
||||
{"usedSymbols", genRes.usedSymbols},
|
||||
{"language", state.active()->language}
|
||||
});
|
||||
}
|
||||
|
||||
// --- applyMutation ---
|
||||
if (method == "applyMutation") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
if (!state.active() || !state.isStructured())
|
||||
return headlessRpcError(id, -32000, "No structured buffer");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string type = params.value("type", "");
|
||||
bool preferImports = params.value("preferImports", false);
|
||||
bool strictMode = params.value("strictMode", false);
|
||||
Module* ast = state.mutationAST();
|
||||
if (!ast) return headlessRpcError(id, -32001, "AST unavailable");
|
||||
ASTMutationAPI mut;
|
||||
mut.setRoot(ast);
|
||||
ASTMutationAPI::MutationResult res;
|
||||
LibraryPolicyResult policy;
|
||||
std::vector<std::string> affectedIds;
|
||||
if (type == "setProperty") {
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (!nodeId.empty()) affectedIds.push_back(nodeId);
|
||||
res = mut.setProperty(params.value("nodeId", ""),
|
||||
params.value("property", ""),
|
||||
params.value("value", ""));
|
||||
} else if (type == "updateNode") {
|
||||
std::map<std::string, std::string> props;
|
||||
if (params.contains("properties")) {
|
||||
for (auto& [k, v] : params["properties"].items())
|
||||
props[k] = v.get<std::string>();
|
||||
}
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (!nodeId.empty()) affectedIds.push_back(nodeId);
|
||||
res = mut.updateNode(params.value("nodeId", ""), props);
|
||||
} else if (type == "deleteNode") {
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (!nodeId.empty()) affectedIds.push_back(nodeId);
|
||||
res = mut.deleteNode(params.value("nodeId", ""));
|
||||
} else if (type == "insertNode") {
|
||||
ASTNode* node = nullptr;
|
||||
std::string insertedId;
|
||||
if (params.contains("node")) {
|
||||
node = fromJson(params["node"]);
|
||||
if (node) insertedId = node->id;
|
||||
}
|
||||
if (node) {
|
||||
policy = checkMutationLibraryPolicy(
|
||||
node, ast, preferImports, strictMode);
|
||||
if (!policy.ok) {
|
||||
deleteTree(node);
|
||||
return headlessRpcError(id, -32011, policy.error);
|
||||
}
|
||||
}
|
||||
res = mut.insertNode(params.value("parentId", ""),
|
||||
params.value("role", ""), node);
|
||||
if (!res.success && node) {
|
||||
deleteTree(node);
|
||||
} else if (res.success && !insertedId.empty()) {
|
||||
affectedIds.push_back(insertedId);
|
||||
}
|
||||
} else {
|
||||
return headlessRpcError(id, -32602, "Unknown mutation type");
|
||||
}
|
||||
if (!res.success)
|
||||
return headlessRpcError(id, -32010, res.error);
|
||||
state.applyOrchestratorToActive();
|
||||
if (state.active()) {
|
||||
state.active()->incrementalOptimizer.setRoot(
|
||||
state.active()->sync.getAST());
|
||||
state.active()->incrementalOptimizer.recordExternalTransform(
|
||||
"agent-mutation:" + type, affectedIds,
|
||||
state.agentActorLabel(sessionId));
|
||||
}
|
||||
return headlessRpcResult(id, {
|
||||
{"success", true}, {"warning", res.warning},
|
||||
{"libraryWarning", policy.warning},
|
||||
{"unknownFunctions", policy.unknownFunctions}
|
||||
});
|
||||
}
|
||||
|
||||
// --- applyBatch ---
|
||||
if (method == "applyBatch") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto errChk = headlessRequireMutable(state, id);
|
||||
if (!errChk.is_null()) return errChk;
|
||||
Module* ast = state.mutationAST();
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
if (!params.contains("mutations") ||
|
||||
!params["mutations"].is_array())
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing mutations array");
|
||||
BatchMutationAPI batch;
|
||||
batch.setRoot(ast);
|
||||
std::vector<BatchMutationAPI::Mutation> mutations;
|
||||
std::vector<ASTNode*> ownedNodes;
|
||||
for (const auto& m : params["mutations"]) {
|
||||
BatchMutationAPI::Mutation mut;
|
||||
mut.type = m.value("type", "");
|
||||
mut.nodeId = m.value("nodeId", "");
|
||||
mut.property = m.value("property", "");
|
||||
mut.value = m.value("value", "");
|
||||
mut.parentId = m.value("parentId", "");
|
||||
mut.role = m.value("role", "");
|
||||
if (m.contains("node")) {
|
||||
mut.newNode = fromJson(m["node"]);
|
||||
if (mut.newNode) ownedNodes.push_back(mut.newNode);
|
||||
}
|
||||
mutations.push_back(mut);
|
||||
}
|
||||
auto batchRes = batch.applySequence(mutations);
|
||||
if (!batchRes.success) {
|
||||
for (auto* n : ownedNodes) {
|
||||
if (n->parent == nullptr) deleteTree(n);
|
||||
}
|
||||
return headlessRpcError(id, -32010, batchRes.error);
|
||||
}
|
||||
state.applyOrchestratorToActive();
|
||||
if (state.active()) {
|
||||
state.active()->incrementalOptimizer.setRoot(
|
||||
state.active()->sync.getAST());
|
||||
state.active()->incrementalOptimizer.recordExternalTransform(
|
||||
"agent-batch", {},
|
||||
state.agentActorLabel(sessionId));
|
||||
}
|
||||
return headlessRpcResult(id,
|
||||
{{"success", true}, {"appliedCount", batchRes.appliedCount}});
|
||||
}
|
||||
|
||||
// --- getInScopeSymbols ---
|
||||
if (method == "getInScopeSymbols") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (nodeId.empty())
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing nodeId parameter");
|
||||
ContextAPI ctx;
|
||||
ctx.setRoot(state.activeAST());
|
||||
auto symbols = ctx.getInScopeSymbols(nodeId);
|
||||
json arr = json::array();
|
||||
for (const auto& s : symbols)
|
||||
arr.push_back({{"name", s.name}, {"kind", s.kind},
|
||||
{"nodeId", s.nodeId}});
|
||||
return headlessRpcResult(id, {{"symbols", arr}});
|
||||
}
|
||||
|
||||
// --- getCallHierarchy ---
|
||||
if (method == "getCallHierarchy") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string functionId = params.value("functionId", "");
|
||||
if (functionId.empty())
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing functionId parameter");
|
||||
ContextAPI ctx;
|
||||
ctx.setRoot(state.activeAST());
|
||||
auto info = ctx.getCallHierarchy(functionId);
|
||||
return headlessRpcResult(id, {
|
||||
{"functionId", info.functionId},
|
||||
{"functionName", info.functionName},
|
||||
{"callerIds", info.callerIds},
|
||||
{"calleeIds", info.calleeIds}
|
||||
});
|
||||
}
|
||||
|
||||
// --- getDependencyGraph ---
|
||||
if (method == "getDependencyGraph") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (nodeId.empty())
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing nodeId parameter");
|
||||
ContextAPI ctx;
|
||||
ctx.setRoot(state.activeAST());
|
||||
auto deps = ctx.getDependencyGraph(nodeId);
|
||||
return headlessRpcResult(id, {{"dependencies", deps}});
|
||||
}
|
||||
|
||||
// --- getAnnotationSuggestions ---
|
||||
if (method == "getAnnotationSuggestions") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireAST(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
Module* ast = state.activeAST();
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string nodeId = params.value("nodeId", "");
|
||||
if (!nodeId.empty()) {
|
||||
if (!findNodeById(ast, nodeId))
|
||||
return headlessRpcError(id, -32002,
|
||||
"Node not found: " + nodeId);
|
||||
}
|
||||
AgentAnnotationAssistant assistant;
|
||||
auto result = assistant.suggest(ast, nodeId);
|
||||
json suggArr = json::array();
|
||||
for (const auto& s : result.suggestions)
|
||||
suggArr.push_back({
|
||||
{"nodeId", s.nodeId},
|
||||
{"annotationType", s.annotationType},
|
||||
{"strategy", s.strategy}, {"reason", s.reason},
|
||||
{"confidence", s.confidence}
|
||||
});
|
||||
json diagArr = json::array();
|
||||
for (const auto& d : result.diagnostics)
|
||||
diagArr.push_back({{"severity", d.severity},
|
||||
{"message", d.message},
|
||||
{"nodeId", d.nodeId}});
|
||||
return headlessRpcResult(id, {{"scopeId", result.scopeNodeId},
|
||||
{"suggestions", suggArr},
|
||||
{"diagnostics", diagArr}});
|
||||
}
|
||||
|
||||
// --- applyAnnotationSuggestion ---
|
||||
if (method == "applyAnnotationSuggestion") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto err = headlessRequireMutable(state, id);
|
||||
if (!err.is_null()) return err;
|
||||
Module* ast = state.mutationAST();
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
MemoryStrategyInference::Suggestion suggestion;
|
||||
suggestion.nodeId = params.value("nodeId", "");
|
||||
suggestion.annotationType = params.value("annotationType", "");
|
||||
suggestion.strategy = params.value("strategy", "");
|
||||
suggestion.reason = params.value("reason", "");
|
||||
suggestion.confidence = params.value("confidence", 0.0);
|
||||
AgentAnnotationAssistant assistant;
|
||||
auto res = assistant.applySuggestion(ast, suggestion);
|
||||
if (!res.success)
|
||||
return headlessRpcError(id, -32010, res.error);
|
||||
if (params.contains("accepted")) {
|
||||
bool accepted = params.value("accepted", true);
|
||||
assistant.recordFeedback(suggestion, accepted);
|
||||
}
|
||||
state.applyOrchestratorToActive();
|
||||
return headlessRpcResult(id,
|
||||
{{"success", true}, {"warning", res.warning}});
|
||||
}
|
||||
|
||||
// --- recordAnnotationFeedback ---
|
||||
if (method == "recordAnnotationFeedback") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
MemoryStrategyInference::Suggestion suggestion;
|
||||
suggestion.nodeId = params.value("nodeId", "");
|
||||
suggestion.annotationType = params.value("annotationType", "");
|
||||
suggestion.strategy = params.value("strategy", "");
|
||||
suggestion.reason = params.value("reason", "");
|
||||
suggestion.confidence = params.value("confidence", 0.0);
|
||||
bool accepted = params.value("accepted", false);
|
||||
AgentAnnotationAssistant assistant;
|
||||
assistant.recordFeedback(suggestion, accepted);
|
||||
return headlessRpcResult(id, true);
|
||||
}
|
||||
|
||||
// --- startWorkflowRecording ---
|
||||
if (method == "startWorkflowRecording") {
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string name = params.value("name", "workflow");
|
||||
state.agent.workflowRecorder.startRecording(
|
||||
name, sessionId, WorkflowRecorder::RecordingConfig{},
|
||||
state.buildSessionMetadata(false));
|
||||
return headlessRpcResult(id,
|
||||
{{"recording", true}, {"name", name}});
|
||||
}
|
||||
|
||||
// --- stopWorkflowRecording ---
|
||||
if (method == "stopWorkflowRecording")
|
||||
return headlessRpcResult(id,
|
||||
state.agent.workflowRecorder.stopRecording());
|
||||
|
||||
// --- getWorkflowRecording ---
|
||||
if (method == "getWorkflowRecording")
|
||||
return headlessRpcResult(id,
|
||||
state.agent.workflowRecorder.exportWorkflow());
|
||||
|
||||
// --- replayWorkflow ---
|
||||
if (method == "replayWorkflow") {
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
if (!params.contains("workflow"))
|
||||
return headlessRpcError(id, -32602,
|
||||
"Missing workflow payload");
|
||||
WorkflowRecorder temp;
|
||||
if (!temp.loadWorkflow(params["workflow"]))
|
||||
return headlessRpcError(id, -32602,
|
||||
"Invalid workflow payload");
|
||||
auto requests = temp.buildReplayRequests();
|
||||
json results = json::array();
|
||||
state.agent.workflowRecorder.setReplaying(true);
|
||||
for (auto& req : requests)
|
||||
results.push_back(
|
||||
handleHeadlessAgentRequest(state, req, sessionId));
|
||||
state.agent.workflowRecorder.setReplaying(false);
|
||||
return headlessRpcResult(id,
|
||||
{{"count", results.size()}, {"responses", results}});
|
||||
}
|
||||
|
||||
// --- getSessionInfo ---
|
||||
if (method == "getSessionInfo") {
|
||||
return headlessRpcResult(id, {
|
||||
{"mode", "headless"},
|
||||
{"workspace", state.workspaceRoot},
|
||||
{"language", state.defaultLanguage},
|
||||
{"bufferCount", (int)state.bufferStates.size()},
|
||||
{"activeBuffer", state.active() ? state.active()->path : ""},
|
||||
{"role", AgentPermissionPolicy::roleLabel(role)}
|
||||
});
|
||||
}
|
||||
|
||||
return headlessRpcError(id, -32601, "Method not found");
|
||||
}
|
||||
239
editor/src/HeadlessEditorState.h
Normal file
239
editor/src/HeadlessEditorState.h
Normal file
@@ -0,0 +1,239 @@
|
||||
#pragma once
|
||||
// Step 245: Headless EditorState — agent API surface without ImGui/SDL.
|
||||
//
|
||||
// Provides the same method interface that AgentRPCHandler.h expects,
|
||||
// but with zero GUI dependencies. Uses std::chrono for timestamps,
|
||||
// stderr for logging, and holds its own lightweight buffer map.
|
||||
|
||||
// --- Core AST and pipeline (no ImGui) ---
|
||||
#include "ast/ASTNode.h"
|
||||
#include "ast/Serialization.h"
|
||||
#include "ast/Generator.h"
|
||||
#include "ast/Annotation.h"
|
||||
#include "ASTUtils.h"
|
||||
#include "Pipeline.h"
|
||||
#include "ASTMutationAPI.h"
|
||||
#include "BatchMutationAPI.h"
|
||||
#include "ContextAPI.h"
|
||||
#include "CrossLanguageProjector.h"
|
||||
#include "MemoryStrategyInference.h"
|
||||
#include "AgentAnnotationAssistant.h"
|
||||
#include "AgentPermissionPolicy.h"
|
||||
#include "AgentCodeGen.h"
|
||||
#include "AgentLibraryPolicy.h"
|
||||
#include "WorkflowRecorder.h"
|
||||
#include "Orchestrator.h"
|
||||
#include "IncrementalOptimizer.h"
|
||||
#include "TextASTSync.h"
|
||||
#include "BufferManager.h"
|
||||
#include "EditorModePolicy.h"
|
||||
#include "PrimitivesRegistry.h"
|
||||
#include "SemanticTags.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Headless agent state — no WebSocket, no AgentMarketplace (ImGui dep)
|
||||
// -----------------------------------------------------------------------
|
||||
struct HeadlessAgentState {
|
||||
std::map<std::string, AgentRole> roles;
|
||||
AgentRole defaultRole = AgentRole::Linter;
|
||||
WorkflowRecorder workflowRecorder;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Headless library state — no ImGui panels
|
||||
// -----------------------------------------------------------------------
|
||||
struct HeadlessLibraryState {
|
||||
PrimitivesRegistry primitives;
|
||||
SemanticTags semanticTags;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// HeadlessBufferState — lightweight buffer without ImGui widgets
|
||||
// -----------------------------------------------------------------------
|
||||
struct HeadlessBufferState {
|
||||
TextASTSync sync;
|
||||
Orchestrator orchestrator;
|
||||
IncrementalOptimizer incrementalOptimizer;
|
||||
std::string language = "python";
|
||||
std::string path = "(untitled)";
|
||||
std::string editBuf;
|
||||
BufferManager::BufferMode bufferMode =
|
||||
BufferManager::BufferMode::Structured;
|
||||
bool orchestratorDirty = true;
|
||||
bool modified = false;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// HeadlessEditorState — agent-facing state, no GUI
|
||||
// -----------------------------------------------------------------------
|
||||
struct HeadlessEditorState {
|
||||
std::map<std::string, std::unique_ptr<HeadlessBufferState>>
|
||||
bufferStates;
|
||||
HeadlessBufferState* activeBuffer = nullptr;
|
||||
HeadlessAgentState agent;
|
||||
HeadlessLibraryState library;
|
||||
std::string workspaceRoot;
|
||||
std::string defaultLanguage = "python";
|
||||
bool verbose = false;
|
||||
|
||||
// --- Buffer access ---
|
||||
|
||||
HeadlessBufferState* active() { return activeBuffer; }
|
||||
|
||||
bool isStructured() const {
|
||||
return activeBuffer &&
|
||||
allowStructuredFeatures(activeBuffer->bufferMode);
|
||||
}
|
||||
|
||||
Module* activeAST() {
|
||||
if (!activeBuffer) return nullptr;
|
||||
if (!allowStructuredFeatures(activeBuffer->bufferMode))
|
||||
return nullptr;
|
||||
return activeBuffer->sync.getAST();
|
||||
}
|
||||
|
||||
Module* mutationAST() {
|
||||
if (!activeBuffer) return nullptr;
|
||||
if (!allowStructuredFeatures(activeBuffer->bufferMode))
|
||||
return nullptr;
|
||||
if (activeBuffer->orchestratorDirty ||
|
||||
!activeBuffer->orchestrator.getAST()) {
|
||||
syncOrchestratorFromActive();
|
||||
}
|
||||
return activeBuffer->orchestrator.getAST();
|
||||
}
|
||||
|
||||
void syncOrchestratorFromActive() {
|
||||
if (!activeBuffer) return;
|
||||
Module* ast = activeBuffer->sync.getAST();
|
||||
if (ast) {
|
||||
activeBuffer->orchestrator.setAST(cloneModule(ast));
|
||||
activeBuffer->orchestratorDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
void applyOrchestratorToActive() {
|
||||
if (!activeBuffer) return;
|
||||
if (!isStructured()) return;
|
||||
Module* ast = activeBuffer->orchestrator.getAST();
|
||||
if (!ast) return;
|
||||
activeBuffer->sync.setAST(cloneModule(ast));
|
||||
activeBuffer->incrementalOptimizer.setRoot(
|
||||
activeBuffer->sync.getAST());
|
||||
Pipeline pipeline;
|
||||
std::string code = pipeline.generate(
|
||||
activeBuffer->sync.getAST(), activeBuffer->language);
|
||||
activeBuffer->editBuf = code;
|
||||
activeBuffer->orchestratorDirty = false;
|
||||
}
|
||||
|
||||
// --- Agent role management ---
|
||||
|
||||
AgentRole getAgentRole(const std::string& sessionId) const {
|
||||
auto it = agent.roles.find(sessionId);
|
||||
if (it != agent.roles.end()) return it->second;
|
||||
return agent.defaultRole;
|
||||
}
|
||||
|
||||
void setAgentRole(const std::string& sessionId, AgentRole role) {
|
||||
agent.roles[sessionId] = role;
|
||||
}
|
||||
|
||||
std::string agentActorLabel(const std::string& sessionId) const {
|
||||
AgentRole role = getAgentRole(sessionId);
|
||||
return std::string(AgentPermissionPolicy::roleLabel(role)) +
|
||||
":" + sessionId;
|
||||
}
|
||||
|
||||
// --- Diagnostics ---
|
||||
|
||||
json buildDiagnosticsJson() const {
|
||||
return json::array();
|
||||
}
|
||||
|
||||
json buildSessionMetadata(bool /*autoRecording*/) const {
|
||||
return {
|
||||
{"mode", "headless"},
|
||||
{"workspace", workspaceRoot},
|
||||
{"language", defaultLanguage},
|
||||
{"timestamp",
|
||||
std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count()}
|
||||
};
|
||||
}
|
||||
|
||||
// --- Buffer operations ---
|
||||
|
||||
HeadlessBufferState* openBuffer(const std::string& filePath,
|
||||
const std::string& content,
|
||||
const std::string& lang) {
|
||||
auto buf = std::make_unique<HeadlessBufferState>();
|
||||
buf->path = filePath;
|
||||
buf->language = lang.empty() ? defaultLanguage : lang;
|
||||
buf->editBuf = content;
|
||||
buf->bufferMode = BufferManager::BufferMode::Structured;
|
||||
if (!content.empty()) {
|
||||
Pipeline pipeline;
|
||||
std::vector<ParseDiagnostic> diags;
|
||||
auto mod = pipeline.parse(content, buf->language, diags);
|
||||
if (mod) {
|
||||
buf->sync.setAST(std::move(mod));
|
||||
buf->orchestratorDirty = true;
|
||||
}
|
||||
} else {
|
||||
auto mod = std::make_unique<Module>(
|
||||
"root", filePath, buf->language);
|
||||
buf->sync.setAST(std::move(mod));
|
||||
buf->orchestratorDirty = true;
|
||||
}
|
||||
HeadlessBufferState* raw = buf.get();
|
||||
bufferStates[filePath] = std::move(buf);
|
||||
if (!activeBuffer) activeBuffer = raw;
|
||||
return raw;
|
||||
}
|
||||
|
||||
bool setActiveBuffer(const std::string& path) {
|
||||
auto it = bufferStates.find(path);
|
||||
if (it == bufferStates.end()) return false;
|
||||
activeBuffer = it->second.get();
|
||||
return true;
|
||||
}
|
||||
|
||||
void closeBuffer(const std::string& path) {
|
||||
auto it = bufferStates.find(path);
|
||||
if (it == bufferStates.end()) return;
|
||||
if (activeBuffer == it->second.get())
|
||||
activeBuffer = nullptr;
|
||||
bufferStates.erase(it);
|
||||
if (!activeBuffer && !bufferStates.empty())
|
||||
activeBuffer = bufferStates.begin()->second.get();
|
||||
}
|
||||
|
||||
// --- RPC entry point ---
|
||||
json processAgentRequest(const json& request,
|
||||
const std::string& sessionId);
|
||||
|
||||
// --- Logging ---
|
||||
void log(const std::string& msg) const {
|
||||
if (verbose)
|
||||
std::cerr << "[whetstone-headless] " << msg << "\n";
|
||||
}
|
||||
};
|
||||
|
||||
// Include the headless RPC handler and wire up processAgentRequest
|
||||
#include "HeadlessAgentRPCHandler.h"
|
||||
|
||||
inline json HeadlessEditorState::processAgentRequest(
|
||||
const json& request, const std::string& sessionId) {
|
||||
return handleHeadlessAgentRequest(*this, request, sessionId);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Step 125 Integration Test: EditorState agent mutation flow
|
||||
#include "EditorUtils.h"
|
||||
#include "AgentPermissionPolicy.h"
|
||||
#include "imgui.h"
|
||||
#include <iostream>
|
||||
|
||||
@@ -32,7 +33,7 @@ int main() {
|
||||
state.bufferStates[buf->path] = std::move(buf);
|
||||
state.activeBuffer = state.bufferStates["(untitled)"].get();
|
||||
|
||||
state.agentMutationPermissions["agent_1"] = false;
|
||||
state.setAgentRole("agent_1", AgentRole::Linter); // Linter cannot mutate
|
||||
json mutReq = {
|
||||
{"jsonrpc","2.0"},
|
||||
{"id",1},
|
||||
@@ -42,7 +43,7 @@ int main() {
|
||||
json mutRes = state.processAgentRequest(mutReq, "agent_1");
|
||||
expect(mutRes.contains("error"), "mutation blocked", passed, failed);
|
||||
|
||||
state.agentMutationPermissions["agent_1"] = true;
|
||||
state.setAgentRole("agent_1", AgentRole::Refactor); // Refactor can mutate
|
||||
json mutRes2 = state.processAgentRequest(mutReq, "agent_1");
|
||||
expect(mutRes2.contains("result"), "mutation allowed", passed, failed);
|
||||
Module* astAfter = state.activeAST();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Step 129 TDD Test: Dependency management UI writeback
|
||||
#include "DependencyPanel.h"
|
||||
#include "NotificationSystem.h"
|
||||
#include "VulnerabilityDatabase.h"
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
@@ -31,6 +33,9 @@ int main() {
|
||||
std::filesystem::remove_all(dir);
|
||||
std::filesystem::create_directories(dir);
|
||||
|
||||
NotificationSystem notifications;
|
||||
VulnerabilityDatabase vulnDb;
|
||||
|
||||
// requirements.txt writeback
|
||||
{
|
||||
std::ofstream out(dir / "requirements.txt");
|
||||
@@ -38,8 +43,7 @@ int main() {
|
||||
out.close();
|
||||
|
||||
DependencyPanelState panel;
|
||||
std::string log;
|
||||
refreshDependencies(panel, dir.string(), log);
|
||||
refreshDependencies(panel, dir.string(), notifications, vulnDb);
|
||||
expect(panel.deps.size() == 1, "requirements parsed", passed, failed);
|
||||
|
||||
DependencySpec dep;
|
||||
@@ -47,7 +51,7 @@ int main() {
|
||||
dep.version = "2.0.0";
|
||||
dep.source = "requirements.txt";
|
||||
panel.deps.push_back(dep);
|
||||
bool ok = writeDependenciesForSource("requirements.txt", dir.string(), panel.deps, log);
|
||||
bool ok = writeDependenciesForSource("requirements.txt", dir.string(), panel.deps, notifications);
|
||||
expect(ok, "requirements writeback ok", passed, failed);
|
||||
std::string content = readFile(dir / "requirements.txt");
|
||||
expect(content.find("numpy==1.26.0") != std::string::npos, "requirements keep numpy", passed, failed);
|
||||
@@ -62,8 +66,7 @@ int main() {
|
||||
out.close();
|
||||
|
||||
DependencyPanelState panel;
|
||||
std::string log;
|
||||
refreshDependencies(panel, dir.string(), log);
|
||||
refreshDependencies(panel, dir.string(), notifications, vulnDb);
|
||||
|
||||
for (auto it = panel.deps.begin(); it != panel.deps.end(); ) {
|
||||
if (it->name == "react") {
|
||||
@@ -75,7 +78,7 @@ int main() {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
bool ok = writeDependenciesForSource("package.json", dir.string(), panel.deps, log);
|
||||
bool ok = writeDependenciesForSource("package.json", dir.string(), panel.deps, notifications);
|
||||
expect(ok, "package.json writeback ok", passed, failed);
|
||||
|
||||
nlohmann::json j;
|
||||
@@ -90,10 +93,9 @@ int main() {
|
||||
|
||||
// edge case: unsupported writeback
|
||||
{
|
||||
std::string log;
|
||||
std::vector<DependencySpec> deps;
|
||||
deps.push_back({"Boost", "", "CMakeLists.txt"});
|
||||
bool ok = writeDependenciesForSource("CMakeLists.txt", dir.string(), deps, log);
|
||||
bool ok = writeDependenciesForSource("CMakeLists.txt", dir.string(), deps, notifications);
|
||||
expect(!ok, "unsupported writeback returns false", passed, failed);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Step 142 TDD Test: Emacs package browser
|
||||
#include "EmacsPackageBrowser.h"
|
||||
#include "EmacsIntegration.h"
|
||||
#include "NotificationSystem.h"
|
||||
#include <iostream>
|
||||
|
||||
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
|
||||
@@ -27,9 +28,9 @@ int main() {
|
||||
|
||||
MockEmacsConnection mock;
|
||||
EmacsPackageBrowserState state;
|
||||
std::string log;
|
||||
NotificationSystem notifications;
|
||||
|
||||
refreshEmacsPackages(state, mock, log);
|
||||
refreshEmacsPackages(state, mock, notifications);
|
||||
expect(!state.packages.empty(), "packages loaded", passed, failed);
|
||||
|
||||
const EmacsPackageEntry* usePkg = findPkg(state.packages, "use-package");
|
||||
@@ -44,7 +45,7 @@ int main() {
|
||||
expect(magitPkg->status == "available", "available status set", passed, failed);
|
||||
}
|
||||
|
||||
bool loadOk = loadEmacsPackage(mock, "magit", log);
|
||||
bool loadOk = loadEmacsPackage(mock, "magit", notifications);
|
||||
expect(loadOk, "load package command", passed, failed);
|
||||
expect(mock.getLastSentCommand().find("(require 'magit)") != std::string::npos,
|
||||
"require command sent", passed, failed);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Step 143 TDD Test: Elisp function discovery and indexing
|
||||
#include "EmacsFunctionDiscovery.h"
|
||||
#include "NotificationSystem.h"
|
||||
#include <iostream>
|
||||
|
||||
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
|
||||
@@ -17,21 +18,21 @@ int main() {
|
||||
int failed = 0;
|
||||
|
||||
MockEmacsConnection mock;
|
||||
std::string log;
|
||||
NotificationSystem notifications;
|
||||
std::string error;
|
||||
|
||||
auto packages = queryEmacsPackageList(mock, false, log, error);
|
||||
auto packages = queryEmacsPackageList(mock, false, notifications, error);
|
||||
expect(!packages.empty(), "loaded packages fetched", passed, failed);
|
||||
|
||||
auto funcs = queryEmacsFunctions(mock, "^use-package", log);
|
||||
auto funcs = queryEmacsFunctions(mock, "^use-package", notifications);
|
||||
expect(!funcs.empty(), "apropos returns functions", passed, failed);
|
||||
|
||||
auto doc = queryEmacsFunctionDoc(mock, "use-package", log);
|
||||
auto doc = queryEmacsFunctionDoc(mock, "use-package", notifications);
|
||||
expect(!doc.signature.empty(), "describe-function signature", passed, failed);
|
||||
expect(!doc.doc.empty(), "describe-function docstring", passed, failed);
|
||||
|
||||
EmacsFunctionIndex index;
|
||||
refreshEmacsFunctionIndex(index, mock, packages, log);
|
||||
refreshEmacsFunctionIndex(index, mock, packages, notifications);
|
||||
expect(index.functionsByPackage.count("use-package") > 0, "index contains use-package", passed, failed);
|
||||
|
||||
Module module("mod1", "test", "elisp");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Step 144 TDD Test: Emacs keybinding deep integration
|
||||
#include "EmacsKeybinding.h"
|
||||
#include "NotificationSystem.h"
|
||||
#include <iostream>
|
||||
|
||||
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
|
||||
@@ -18,29 +19,29 @@ int main() {
|
||||
|
||||
MockEmacsConnection mock;
|
||||
EmacsKeybindingState state;
|
||||
std::string log;
|
||||
NotificationSystem notifications;
|
||||
|
||||
bool prefixOk = emacsHandleKeySequence(state, mock, "C-x", log);
|
||||
bool prefixOk = emacsHandleKeySequence(state, mock, "C-x", notifications);
|
||||
expect(prefixOk, "prefix handled", passed, failed);
|
||||
expect(state.prefix == "C-x", "prefix stored", passed, failed);
|
||||
|
||||
bool seqOk = emacsHandleKeySequence(state, mock, "C-s", log);
|
||||
bool seqOk = emacsHandleKeySequence(state, mock, "C-s", notifications);
|
||||
expect(seqOk, "sequence handled", passed, failed);
|
||||
expect(state.prefix.empty(), "prefix cleared", passed, failed);
|
||||
expect(state.lastCommand == "save-buffer", "command resolved", passed, failed);
|
||||
expect(mock.getLastSentCommand().find("call-interactively") != std::string::npos,
|
||||
"call-interactively sent", passed, failed);
|
||||
|
||||
bool mxOk = emacsHandleKeySequence(state, mock, "M-x", log);
|
||||
bool mxOk = emacsHandleKeySequence(state, mock, "M-x", notifications);
|
||||
expect(mxOk, "M-x handled", passed, failed);
|
||||
expect(state.minibufferActive, "minibuffer active", passed, failed);
|
||||
|
||||
std::snprintf(state.minibufferBuf, sizeof(state.minibufferBuf), "find-file");
|
||||
bool mbOk = emacsExecuteMinibuffer(state, mock, log);
|
||||
bool mbOk = emacsExecuteMinibuffer(state, mock, notifications);
|
||||
expect(mbOk, "minibuffer command executed", passed, failed);
|
||||
expect(!state.minibufferActive, "minibuffer cleared", passed, failed);
|
||||
|
||||
updateEmacsModeLine(state, mock, 2.0, log);
|
||||
updateEmacsModeLine(state, mock, 2.0, notifications);
|
||||
expect(!state.modeLine.empty(), "mode line updated", passed, failed);
|
||||
|
||||
std::cout << "\n=== Step 144 Results: " << passed << " passed, "
|
||||
|
||||
126
editor/tests/step245_test.cpp
Normal file
126
editor/tests/step245_test.cpp
Normal file
@@ -0,0 +1,126 @@
|
||||
// Step 245 TDD Test: HeadlessEditorState — agent API without ImGui/SDL
|
||||
#include "HeadlessEditorState.h"
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
|
||||
static void expect(bool cond, const std::string& name,
|
||||
int& passed, int& failed) {
|
||||
if (cond) {
|
||||
std::cout << "Test " << (passed + failed + 1)
|
||||
<< " PASS: " << name << "\n";
|
||||
++passed;
|
||||
} else {
|
||||
std::cout << "Test " << (passed + failed + 1)
|
||||
<< " FAIL: " << name << "\n";
|
||||
++failed;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
|
||||
// 1. Construct headless state without crash
|
||||
HeadlessEditorState state;
|
||||
state.workspaceRoot = "/tmp/test";
|
||||
state.defaultLanguage = "python";
|
||||
expect(state.active() == nullptr, "no active buffer initially",
|
||||
passed, failed);
|
||||
|
||||
// 2. Open a buffer with Python source
|
||||
std::string src = "def hello(name):\n return 'hi ' + name\n";
|
||||
auto* buf = state.openBuffer("test.py", src, "python");
|
||||
expect(buf != nullptr, "buffer opened", passed, failed);
|
||||
expect(state.active() == buf, "buffer is active", passed, failed);
|
||||
expect(state.isStructured(), "buffer is structured", passed, failed);
|
||||
expect(state.activeAST() != nullptr, "AST parsed", passed, failed);
|
||||
|
||||
// 3. Parse source via RPC
|
||||
json parseReq = {
|
||||
{"jsonrpc", "2.0"}, {"id", 1}, {"method", "parseSource"},
|
||||
{"params", {{"source", "x = 42"}, {"language", "python"}}}
|
||||
};
|
||||
// Set role to Refactor so parseSource is permitted
|
||||
state.setAgentRole("test-agent", AgentRole::Refactor);
|
||||
json parseRes = state.processAgentRequest(parseReq, "test-agent");
|
||||
expect(parseRes.contains("result"), "parseSource returns result",
|
||||
passed, failed);
|
||||
expect(parseRes["result"].contains("ast"), "parseSource has AST",
|
||||
passed, failed);
|
||||
|
||||
// 4. Get AST via RPC
|
||||
json getReq = {
|
||||
{"jsonrpc", "2.0"}, {"id", 2}, {"method", "getAST"}
|
||||
};
|
||||
json getRes = state.processAgentRequest(getReq, "test-agent");
|
||||
expect(getRes.contains("result"), "getAST returns result",
|
||||
passed, failed);
|
||||
expect(getRes["result"].contains("ast"), "getAST has ast field",
|
||||
passed, failed);
|
||||
|
||||
// 5. Run pipeline via RPC
|
||||
json pipeReq = {
|
||||
{"jsonrpc", "2.0"}, {"id", 3}, {"method", "runPipeline"},
|
||||
{"params", {{"source", "def foo(): pass"},
|
||||
{"sourceLanguage", "python"},
|
||||
{"targetLanguage", "cpp"}}}
|
||||
};
|
||||
json pipeRes = state.processAgentRequest(pipeReq, "test-agent");
|
||||
expect(pipeRes.contains("result"), "runPipeline returns result",
|
||||
passed, failed);
|
||||
expect(pipeRes["result"].contains("generatedCode"),
|
||||
"pipeline generates code", passed, failed);
|
||||
|
||||
// 6. Mutation via RPC — rename the module
|
||||
Module* ast = state.activeAST();
|
||||
std::string rootId = ast ? ast->id : "";
|
||||
expect(!rootId.empty(), "root node has id", passed, failed);
|
||||
|
||||
json mutReq = {
|
||||
{"jsonrpc", "2.0"}, {"id", 4}, {"method", "applyMutation"},
|
||||
{"params", {{"type", "setProperty"}, {"nodeId", rootId},
|
||||
{"property", "name"}, {"value", "renamed"}}}
|
||||
};
|
||||
json mutRes = state.processAgentRequest(mutReq, "test-agent");
|
||||
expect(mutRes.contains("result"), "mutation returns result",
|
||||
passed, failed);
|
||||
expect(mutRes["result"].value("success", false),
|
||||
"mutation succeeded", passed, failed);
|
||||
|
||||
// 7. Permission check — Linter cannot mutate
|
||||
state.setAgentRole("linter-agent", AgentRole::Linter);
|
||||
json mutReq2 = {
|
||||
{"jsonrpc", "2.0"}, {"id", 5}, {"method", "applyMutation"},
|
||||
{"params", {{"type", "setProperty"}, {"nodeId", rootId},
|
||||
{"property", "name"}, {"value", "blocked"}}}
|
||||
};
|
||||
json mutRes2 = state.processAgentRequest(mutReq2, "linter-agent");
|
||||
expect(mutRes2.contains("error"), "linter mutation blocked",
|
||||
passed, failed);
|
||||
|
||||
// 8. Ping
|
||||
json pingReq = {{"jsonrpc", "2.0"}, {"id", 6}, {"method", "ping"}};
|
||||
json pingRes = state.processAgentRequest(pingReq, "test-agent");
|
||||
expect(pingRes.contains("result"), "ping returns result",
|
||||
passed, failed);
|
||||
|
||||
// 9. getSessionInfo
|
||||
json infoReq = {
|
||||
{"jsonrpc", "2.0"}, {"id", 7}, {"method", "getSessionInfo"}
|
||||
};
|
||||
json infoRes = state.processAgentRequest(infoReq, "test-agent");
|
||||
expect(infoRes.contains("result"), "getSessionInfo returns result",
|
||||
passed, failed);
|
||||
expect(infoRes["result"].value("mode", "") == "headless",
|
||||
"mode is headless", passed, failed);
|
||||
|
||||
// 10. Buffer management — close and reopen
|
||||
state.closeBuffer("test.py");
|
||||
expect(state.active() == nullptr, "buffer closed", passed, failed);
|
||||
expect(!state.isStructured(), "no structured after close",
|
||||
passed, failed);
|
||||
|
||||
std::cout << "\n=== Step 245 Results: " << passed << " passed, "
|
||||
<< failed << " failed ===\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
25
progress.md
Normal file
25
progress.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Sprint 9 Progress — Agent-First Tooling
|
||||
|
||||
## Phase 9a: Standalone MCP Server
|
||||
|
||||
### Step 245: HeadlessEditorState (no ImGui dependency)
|
||||
**Status:** PASS (20/20 tests)
|
||||
|
||||
Created the headless agent API surface — same method interface as EditorState
|
||||
but with zero GUI dependencies (no ImGui, no SDL, no WebSocket).
|
||||
|
||||
**Files created:**
|
||||
- `editor/src/ASTUtils.h` — Pure AST utility functions extracted from EditorUtils.h
|
||||
(cloneModule, isAnnotationNode, countAnnotationNodes, findNodeById, findNodeAtPosition)
|
||||
- `editor/src/HeadlessEditorState.h` — Lightweight state: HeadlessAgentState,
|
||||
HeadlessLibraryState, HeadlessBufferState, buffer management, orchestrator sync
|
||||
- `editor/src/HeadlessAgentRPCHandler.h` — Full RPC dispatch mirroring
|
||||
AgentRPCHandler.h (20+ methods: getAST, parseSource, runPipeline, applyMutation,
|
||||
applyBatch, projectLanguage, generateCode, workflow recording, etc.)
|
||||
- `editor/tests/step245_test.cpp` — 20 test cases covering construction, buffer
|
||||
management, RPC dispatch, permission enforcement, pipeline execution
|
||||
|
||||
**Key design decisions:**
|
||||
- JSON roundtrip cloning (matching EditorUtils.h) instead of manual deep copy
|
||||
- Separate HeadlessAgentState (no AgentMarketplace/WebSocket dependency chain)
|
||||
- All files under 600-line architecture limit
|
||||
427
sprint9_plan.md
Normal file
427
sprint9_plan.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# Sprint 9: Agent-First Tooling — Plan
|
||||
|
||||
> **Goal:** Make Whetstone a practical tool that AI agents use from a terminal
|
||||
> to write better code with fewer tokens and fewer errors. Strip away the GUI
|
||||
> dependency. Optimize every RPC round-trip for agent consumption. Ship a
|
||||
> standalone MCP server that Claude Code (or any MCP client) can talk to
|
||||
> directly.
|
||||
>
|
||||
> **Prerequisites:** Sprint 8 complete (244 steps). Full agent API (25+ RPC
|
||||
> methods), MCP server/bridge headers, orchestrator, eval harness, and
|
||||
> pipeline tool all in place.
|
||||
>
|
||||
> **Key insight:** Agents don't need squiggly lines, floating windows, or
|
||||
> syntax highlighting. They need error codes, AST node references, structured
|
||||
> diagnostics, and compact diffs. The entire LSP surface can be reduced to
|
||||
> what's actionable in a JSON response.
|
||||
>
|
||||
> **Key themes:**
|
||||
> 1. Headless-first: `whetstone_mcp` as a standalone binary, no SDL required
|
||||
> 2. Agent-optimized RPC: compact responses, token budgets, delta encoding
|
||||
> 3. Lean diagnostics: error codes + node references, not human-readable prose
|
||||
> 4. File-level operations: create, read, write, diff without a GUI
|
||||
> 5. Real integration testing with Claude Code via MCP
|
||||
|
||||
---
|
||||
|
||||
## Phase 9a: Standalone MCP Server (Steps 245–249)
|
||||
|
||||
Ship `whetstone_mcp` as a headless binary that any MCP client can launch.
|
||||
No SDL, no ImGui, no OpenGL. Just JSON-RPC over stdio.
|
||||
|
||||
- [ ] **Step 245: Headless EditorState (no ImGui dependency)**
|
||||
Extract a `HeadlessEditorState` that provides the agent API surface without
|
||||
any ImGui or SDL includes:
|
||||
- Wraps the AST engine, tree-sitter parser, pipeline, and code generators
|
||||
- Provides `processAgentRequest(json, sessionId)` identical to EditorState
|
||||
- No `ImGui::GetTime()` — uses `std::chrono` for timestamps
|
||||
- No notification toasts — logs to stderr or collects in a vector
|
||||
- Shares all headers: ASTNode.h, Pipeline.h, AgentRPCHandler.h (refactored
|
||||
to accept a trait/interface rather than full EditorState)
|
||||
- Test: HeadlessEditorState can parse source, mutate AST, generate code
|
||||
without linking SDL or ImGui
|
||||
*New:* `editor/src/HeadlessEditorState.h`
|
||||
|
||||
- [ ] **Step 246: mcp_main.cpp entry point**
|
||||
Create the standalone MCP server executable:
|
||||
- `main()` creates HeadlessEditorState + MCPBridge
|
||||
- Wires `processAgentRequest` as the RPC callback
|
||||
- Wires resource reader for `whetstone://` URIs
|
||||
- Calls `bridge.runStdio()` — reads Content-Length framed JSON-RPC
|
||||
- CLI flags: `--workspace <dir>` (set working directory),
|
||||
`--language <lang>` (default language), `--verbose` (stderr logging)
|
||||
- Graceful shutdown on SIGINT/SIGTERM
|
||||
- Test: launch process, send `initialize` + `tools/list`, verify 10+ tools
|
||||
*New:* `editor/src/mcp_main.cpp`
|
||||
*Modifies:* `editor/CMakeLists.txt` (add `whetstone_mcp` target, no SDL link)
|
||||
|
||||
- [ ] **Step 247: File operation tools for MCP**
|
||||
Agents need to create, read, write, and diff files without a GUI:
|
||||
- `whetstone_file_read` — read file contents (with optional line range)
|
||||
- `whetstone_file_write` — write content to a file (creates if needed)
|
||||
- `whetstone_file_create` — create a new file with optional template
|
||||
- `whetstone_file_diff` — unified diff between current buffer and disk
|
||||
- `whetstone_workspace_list` — list files in workspace (with glob filter)
|
||||
- All operations respect workspace root from `--workspace`
|
||||
- RPC methods added to AgentRPCHandler, MCP tools registered in MCPServer
|
||||
- Test: create file via RPC, read it back, diff after modification
|
||||
*Modifies:* `AgentRPCHandler.h`, `MCPServer.h`
|
||||
|
||||
- [ ] **Step 248: Compact AST response format**
|
||||
Full AST dumps waste tokens. Add a compact mode:
|
||||
- `getAST` gains `compact: true` parameter — returns abbreviated nodes:
|
||||
`{id, type, name, line, children: [ids]}` instead of full property dump
|
||||
- `getASTSubtree` — new method: returns only the subtree rooted at a nodeId
|
||||
- `getASTDiff` — new method: returns only nodes changed since last query
|
||||
(uses a version counter incremented on each mutation)
|
||||
- Response includes `tokenEstimate` field (rough char count / 4)
|
||||
- Test: compact AST is <30% the size of full AST for a 50-function module
|
||||
*Modifies:* `AgentRPCHandler.h`
|
||||
|
||||
- [ ] **Step 249: MCP server integration tests**
|
||||
End-to-end tests for the standalone MCP server:
|
||||
1. Launch `whetstone_mcp`, complete MCP handshake (initialize/initialized)
|
||||
2. `tools/list` returns all registered tools with valid schemas
|
||||
3. `tools/call whetstone_get_ast` on empty state returns valid response
|
||||
4. `tools/call whetstone_run_pipeline` with Python source → generates code
|
||||
5. `resources/read whetstone://diagnostics` returns structured diagnostics
|
||||
6. `prompts/list` returns all registered prompts
|
||||
7. File operations: create, read, write, diff cycle
|
||||
8. Compact AST vs full AST size comparison
|
||||
*New:* `step249_test.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Phase 9b: Agent-Optimized Diagnostics (Steps 250–253)
|
||||
|
||||
Replace human-readable LSP diagnostics with agent-optimized structured
|
||||
output. Agents need error codes and AST references, not squiggly lines.
|
||||
|
||||
- [ ] **Step 250: Structured diagnostic format**
|
||||
Define a compact diagnostic format for agent consumption:
|
||||
```json
|
||||
{
|
||||
"code": "E0302",
|
||||
"severity": "error",
|
||||
"nodeId": "func_42",
|
||||
"line": 17, "col": 4,
|
||||
"message": "type mismatch: expected int, got str",
|
||||
"fix": {"type": "setProperty", "nodeId": "func_42", "property": "returnType", "value": "str"}
|
||||
}
|
||||
```
|
||||
- `getDiagnostics` RPC method: returns array of structured diagnostics
|
||||
- Each diagnostic includes: error code, severity, node reference, location,
|
||||
short message, and optional machine-applicable fix
|
||||
- Combines tree-sitter parse errors, Whetstone validation errors, and
|
||||
LSP diagnostics into one unified stream
|
||||
- Severity levels: error, warning, info, hint (numeric codes for filtering)
|
||||
- Test: parse invalid Python, verify diagnostics have nodeIds and fix hints
|
||||
*New:* `editor/src/StructuredDiagnostics.h`
|
||||
*Modifies:* `AgentRPCHandler.h`, `MCPServer.h`
|
||||
|
||||
- [ ] **Step 251: Quick-fix actions as RPC mutations**
|
||||
Turn diagnostic fixes into one-shot RPC calls:
|
||||
- `applyQuickFix` method: takes diagnostic code + nodeId, applies the
|
||||
suggested fix as a mutation
|
||||
- `getQuickFixes` method: for a given nodeId, returns all applicable fixes
|
||||
as mutation objects the agent can review before applying
|
||||
- Fix categories: type correction, missing import, unused variable removal,
|
||||
rename suggestion, missing return statement
|
||||
- Each fix is a concrete mutation (or batch) — no human interpretation needed
|
||||
- Test: parse code with type error, get fix, apply fix, verify diagnostic clears
|
||||
*Modifies:* `AgentRPCHandler.h`, `StructuredDiagnostics.h`
|
||||
|
||||
- [ ] **Step 252: Diagnostic delta streaming**
|
||||
After a mutation, agents shouldn't re-fetch all diagnostics:
|
||||
- `getDiagnosticsDelta` method: returns only diagnostics that changed since
|
||||
a given version number
|
||||
- Response: `{added: [...], removed: [...], version: N}`
|
||||
- Version counter increments on every AST mutation or reparse
|
||||
- Removed diagnostics identified by their `(code, nodeId, line)` tuple
|
||||
- Enables efficient "mutate → check if error cleared" loops
|
||||
- Test: introduce error, get diagnostics, fix error, get delta showing removal
|
||||
*Modifies:* `AgentRPCHandler.h`, `StructuredDiagnostics.h`
|
||||
|
||||
- [ ] **Step 253: Diagnostic and delta tests**
|
||||
Comprehensive tests for the diagnostic pipeline:
|
||||
1. Parse valid code → zero diagnostics
|
||||
2. Parse invalid code → structured diagnostics with nodeIds
|
||||
3. Diagnostics include fix suggestions where applicable
|
||||
4. `applyQuickFix` resolves the diagnostic
|
||||
5. Delta after fix shows removal
|
||||
6. Delta after introducing new error shows addition
|
||||
7. Combined tree-sitter + Whetstone diagnostics in one stream
|
||||
8. Severity filtering works
|
||||
*New:* `step253_test.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Phase 9c: Token-Efficient Agent Protocol (Steps 254–257)
|
||||
|
||||
Minimize token cost per interaction. Every byte in a response costs the
|
||||
agent (and the user) money and context window space.
|
||||
|
||||
- [ ] **Step 254: Response budget system**
|
||||
Let agents request only what they need:
|
||||
- All query methods gain optional `budget` parameter (max response chars)
|
||||
- If response exceeds budget, it's truncated with `"truncated": true` and
|
||||
a `"continuation"` token for paginated follow-up
|
||||
- `getAST` with budget: returns top-level nodes first, details on demand
|
||||
- `getDiagnostics` with budget: returns highest-severity first
|
||||
- Default budget: unlimited (backward compatible)
|
||||
- Test: getAST with budget=500 returns truncated response with continuation
|
||||
*Modifies:* `AgentRPCHandler.h`
|
||||
|
||||
- [ ] **Step 255: Symbol-only mode for scope queries**
|
||||
`getInScopeSymbols` currently returns full node data. Add lean mode:
|
||||
- `symbols` parameter (default): returns `[{name, type, kind, nodeId}]`
|
||||
- `detailed` parameter: returns full node JSON (current behavior)
|
||||
- `getCallHierarchy` lean mode: returns `{callers: [name], callees: [name]}`
|
||||
instead of full node trees
|
||||
- `getDependencyGraph` lean mode: adjacency list of nodeIds only
|
||||
- Test: lean scope query is <20% the size of detailed query
|
||||
*Modifies:* `AgentRPCHandler.h`
|
||||
|
||||
- [ ] **Step 256: Batch query endpoint**
|
||||
Agents often need AST + diagnostics + scope in one round-trip:
|
||||
- `batchQuery` method: accepts array of query requests, returns array of
|
||||
results in the same order
|
||||
- Single JSON-RPC call, single response — no protocol overhead per query
|
||||
- Each sub-query is a normal method name + params
|
||||
- Errors in one sub-query don't fail the batch
|
||||
- Test: batch of [getAST, getDiagnostics, getInScopeSymbols] returns 3 results
|
||||
*Modifies:* `AgentRPCHandler.h`, `MCPServer.h`
|
||||
|
||||
- [ ] **Step 257: Token efficiency tests + benchmarks**
|
||||
Measure and verify token savings:
|
||||
1. Compact AST vs full AST: measure ratio for 10/50/200 node modules
|
||||
2. Lean scope vs detailed: measure ratio
|
||||
3. Delta diagnostics vs full: measure ratio
|
||||
4. Budget truncation: verify continuation works across 3 pages
|
||||
5. Batch query vs sequential: measure total bytes
|
||||
6. Benchmark: time for 100 parse→mutate→diagnose cycles in headless mode
|
||||
*New:* `step257_test.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Phase 9d: Multi-File Project Support (Steps 258–262)
|
||||
|
||||
Agents work on projects, not single files. Add project-level operations.
|
||||
|
||||
- [ ] **Step 258: Project model and workspace indexing**
|
||||
HeadlessEditorState gains project awareness:
|
||||
- `ProjectState`: tracks open files, workspace root, language per file
|
||||
- `openFile` / `closeFile` RPC methods manage multiple buffers
|
||||
- `listBuffers` returns all open files with language and dirty status
|
||||
- `setActiveBuffer` switches which buffer `getAST` etc. operate on
|
||||
- Workspace scan on startup: index file paths (not content) for fast lookup
|
||||
- Test: open 3 files, switch active, verify getAST returns correct AST
|
||||
*New:* `editor/src/ProjectState.h`
|
||||
*Modifies:* `HeadlessEditorState.h`, `AgentRPCHandler.h`
|
||||
|
||||
- [ ] **Step 259: Cross-file symbol resolution**
|
||||
When an agent queries scope, include symbols from other open files:
|
||||
- `getInScopeSymbols` gains `crossFile: true` parameter
|
||||
- Scans all open buffers for exported symbols (functions, classes, modules)
|
||||
- Returns source file path with each cross-file symbol
|
||||
- Import graph: track which files import which, update on file change
|
||||
- Test: file A imports from file B, scope query in A includes B's exports
|
||||
*Modifies:* `AgentRPCHandler.h`, `ProjectState.h`
|
||||
|
||||
- [ ] **Step 260: Project-wide diagnostics**
|
||||
Diagnostics across all open files in one call:
|
||||
- `getProjectDiagnostics` method: returns diagnostics grouped by file path
|
||||
- Compact format: `{"/path/foo.py": [{code, line, message}], ...}`
|
||||
- Includes cross-file errors (undefined import, circular dependency)
|
||||
- Optional severity filter and file path glob filter
|
||||
- Test: two files with errors, project diagnostics returns both sets
|
||||
*Modifies:* `AgentRPCHandler.h`, `StructuredDiagnostics.h`
|
||||
|
||||
- [ ] **Step 261: Project-wide search and refactor**
|
||||
Agents need to find and rename across files:
|
||||
- `searchProject` method: find symbol references across all open files
|
||||
by name or nodeId, returns `[{file, line, col, nodeId, context}]`
|
||||
- `renameSymbol` method: rename a symbol across all files that reference it
|
||||
returns a preview of all changes, then applies on confirmation
|
||||
- Uses the import graph to find cross-file references
|
||||
- Test: rename a function, verify all call sites across 2 files are updated
|
||||
*Modifies:* `AgentRPCHandler.h`, `ProjectState.h`
|
||||
|
||||
- [ ] **Step 262: Multi-file project tests**
|
||||
Full integration tests for project operations:
|
||||
1. Open workspace with 5 Python files
|
||||
2. Index finds all files
|
||||
3. Open 3 files, verify buffer list
|
||||
4. Cross-file scope resolution works
|
||||
5. Project-wide diagnostics aggregates correctly
|
||||
6. Search finds references across files
|
||||
7. Rename updates all references atomically
|
||||
8. Close file removes it from scope resolution
|
||||
*New:* `step262_test.cpp`
|
||||
|
||||
---
|
||||
|
||||
## Phase 9e: Claude Code MCP Integration (Steps 263–267)
|
||||
|
||||
Make `whetstone_mcp` work as a real MCP server that Claude Code can use.
|
||||
This is the "install a plugin and activate you in my terminal" goal.
|
||||
|
||||
- [ ] **Step 263: MCP server configuration for Claude Code**
|
||||
Create the config and documentation for Claude Code integration:
|
||||
- `claude_desktop_config.json` snippet for `whetstone_mcp` server
|
||||
- Auto-detection: if `whetstone_mcp` is in PATH, register it
|
||||
- Server args: `--workspace $(pwd)` to set project root
|
||||
- Startup health check: verify MCP handshake completes in <2s
|
||||
- Test: config generation produces valid JSON matching Claude Code schema
|
||||
*New:* `editor/src/MCPConfig.h`
|
||||
*New:* `mcp-config.example.json`
|
||||
|
||||
- [ ] **Step 264: Agent workflow prompts for real coding tasks**
|
||||
Expand MCP prompts beyond annotations to practical coding workflows:
|
||||
- `implement_function` — spec → AST → code → validate → diagnostics loop
|
||||
- `fix_errors` — get diagnostics → apply quick-fixes → verify
|
||||
- `add_tests` — analyze function → generate test cases → write test file
|
||||
- `refactor_extract` — select code region → extract to new function
|
||||
- `explain_code` — get AST subtree → describe structure and behavior
|
||||
- Each prompt guides the agent through the optimal tool sequence
|
||||
- Test: each prompt generates valid multi-step instruction messages
|
||||
*Modifies:* `MCPServer.h`
|
||||
|
||||
- [ ] **Step 265: Tool result formatting for LLM consumption**
|
||||
Optimize tool results so Claude Code can parse them efficiently:
|
||||
- Error results include actionable next steps (not just error messages)
|
||||
- Success results include a one-line summary before detailed JSON
|
||||
- Large results (>2000 chars) auto-paginate with continuation tokens
|
||||
- Code blocks in results are language-tagged for model comprehension
|
||||
- Diagnostics include human-readable explanation alongside error codes
|
||||
- Test: tool results match expected format for 10 common operations
|
||||
*Modifies:* `MCPServer.h`, `AgentRPCHandler.h`
|
||||
|
||||
- [ ] **Step 266: End-to-end MCP workflow test**
|
||||
Simulate a complete coding session through MCP:
|
||||
1. Initialize MCP server with a workspace
|
||||
2. Create a new Python file via `whetstone_file_create`
|
||||
3. Write function stubs via `whetstone_file_write`
|
||||
4. Parse the file via `whetstone_run_pipeline`
|
||||
5. Get AST and verify structure via `whetstone_get_ast`
|
||||
6. Mutate: add a parameter via `whetstone_mutate`
|
||||
7. Get diagnostics: verify type-awareness via `whetstone_get_diagnostics`
|
||||
8. Generate code from spec via `whetstone_generate_code`
|
||||
9. Write generated code to new file
|
||||
10. Project-wide diagnostics show no errors
|
||||
*New:* `step266_test.cpp`
|
||||
|
||||
- [ ] **Step 267: MCP integration documentation**
|
||||
Write the integration guide (in code comments, not standalone docs):
|
||||
- MCPConfig.h: detailed comments explaining setup for Claude Code,
|
||||
Cursor, Continue, and generic MCP clients
|
||||
- Tool descriptions: ensure every MCP tool description is clear enough
|
||||
that an LLM can use it without examples
|
||||
- Example session transcript as a code comment in mcp_main.cpp
|
||||
- Error recovery guide: common errors and how the agent should respond
|
||||
*Modifies:* `mcp_main.cpp`, `MCPServer.h`, `MCPConfig.h`
|
||||
|
||||
---
|
||||
|
||||
## Phase 9f: Performance & Stability (Steps 268–272)
|
||||
|
||||
Agents hit the RPC surface hard and fast. Make sure it can keep up.
|
||||
|
||||
- [ ] **Step 268: Headless performance profiling**
|
||||
Establish baseline performance for agent workloads:
|
||||
- Benchmark harness: measures time for parse, mutate, generate, diagnose
|
||||
- Target: parse 1000-line file in <100ms, single mutation in <5ms,
|
||||
getAST (compact) in <10ms, diagnostics in <20ms
|
||||
- Profile memory: HeadlessEditorState with 10 open files <50MB
|
||||
- Report: JSON output with p50/p95/p99 latencies per operation
|
||||
- Test: all operations meet target latencies
|
||||
*New:* `editor/src/HeadlessBenchmark.h`
|
||||
*New:* `step268_test.cpp`
|
||||
|
||||
- [ ] **Step 269: Incremental reparse optimization**
|
||||
Don't reparse the entire file on every mutation:
|
||||
- Track byte ranges changed by each mutation
|
||||
- Use tree-sitter's incremental parse: `ts_parser_parse` with edit info
|
||||
- Only regenerate diagnostics for affected subtrees
|
||||
- Cache: last parse tree kept in memory, reused for incremental update
|
||||
- Test: editing one function in a 1000-line file is >5x faster than full reparse
|
||||
*Modifies:* `HeadlessEditorState.h`, `Pipeline.h` or relevant parser header
|
||||
|
||||
- [ ] **Step 270: AST mutation batching optimization**
|
||||
`applyBatch` should be faster than N individual mutations:
|
||||
- Defer reparse until batch completes (single reparse at end)
|
||||
- Defer diagnostic regeneration until batch completes
|
||||
- Coalesce adjacent mutations (e.g., two setProperty on same node)
|
||||
- Validation: run structural validation once after batch, not per-mutation
|
||||
- Test: batch of 20 mutations is >3x faster than 20 individual calls
|
||||
*Modifies:* `AgentRPCHandler.h`
|
||||
|
||||
- [ ] **Step 271: Stress test suite**
|
||||
Verify stability under sustained agent load:
|
||||
1. 1000 sequential parse→mutate→diagnose cycles without crash or leak
|
||||
2. 100 batch mutations of 50 operations each
|
||||
3. Rapid buffer switching (10 files, random access pattern)
|
||||
4. Concurrent-style stress: interleaved reads and writes
|
||||
5. Large file handling: 5000-line file parse + mutation
|
||||
6. Memory stable: RSS doesn't grow >2x after 1000 cycles
|
||||
*New:* `step271_test.cpp`
|
||||
|
||||
- [ ] **Step 272: Error recovery and graceful degradation**
|
||||
Agents will send bad requests. Handle them gracefully:
|
||||
- Malformed JSON → clean error, no crash
|
||||
- Invalid nodeId → error with suggestion (nearest valid node)
|
||||
- Mutation on deleted node → error explaining node was removed
|
||||
- Parse failure → partial AST with error markers, not empty response
|
||||
- OOM protection: if AST exceeds size limit, refuse new insertions
|
||||
- Test: 10 different error scenarios all return structured errors, no crashes
|
||||
*Modifies:* `AgentRPCHandler.h`, `HeadlessEditorState.h`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Phase | Steps | Description |
|
||||
|-------|-------|-------------|
|
||||
| 9a | 245–249 | Standalone MCP server (`whetstone_mcp` binary) |
|
||||
| 9b | 250–253 | Agent-optimized diagnostics (error codes, nodeIds, fixes) |
|
||||
| 9c | 254–257 | Token-efficient protocol (budgets, deltas, batching) |
|
||||
| 9d | 258–262 | Multi-file project support |
|
||||
| 9e | 263–267 | Claude Code MCP integration |
|
||||
| 9f | 268–272 | Performance, stability, and stress testing |
|
||||
|
||||
**Total: 28 steps across 6 phases.**
|
||||
|
||||
---
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
- **No SDL/ImGui dependency for agent path:** `whetstone_mcp` links only
|
||||
against tree-sitter, nlohmann-json, and the Whetstone core headers.
|
||||
This means it builds and runs on any Linux box, CI server, or container
|
||||
without display server or GPU.
|
||||
- **HeadlessEditorState vs EditorState:** HeadlessEditorState is not a
|
||||
subclass — it's a parallel implementation that shares the same headers
|
||||
(ASTNode.h, Pipeline.h, CodeGen.h, etc.) but replaces ImGui-dependent
|
||||
code with std::chrono timestamps and stderr logging.
|
||||
- **Agent RPC interface trait:** AgentRPCHandler.h should be refactored to
|
||||
accept an interface (getAST, mutateAST, getDiagnostics) rather than
|
||||
taking `EditorState&` directly. Both EditorState and HeadlessEditorState
|
||||
implement this interface. This avoids code duplication.
|
||||
- **600-line limit enforced:** All new headers must pass file_limits_test.
|
||||
- **Test-first:** Every step has real assertions testing actual behavior.
|
||||
No placeholder tests.
|
||||
- **MCP protocol version:** `2024-11-05` (current stable). Content-Length
|
||||
framing over stdio.
|
||||
- **Token accounting:** Responses include `tokenEstimate` (chars/4) so
|
||||
agents can make informed decisions about what to query.
|
||||
|
||||
---
|
||||
|
||||
## Sprint 10 Preview (for planning)
|
||||
|
||||
**Sprint 10: Language Intelligence & GUI Rehabilitation**
|
||||
1. LSP client for headless mode (clangd, pyright, rust-analyzer)
|
||||
2. Language-specific code actions (Go imports, Rust borrow fixes)
|
||||
3. GUI overhaul: pin floating windows, sane defaults, usable color scheme
|
||||
4. Terminal UI mode (TUI) as alternative to ImGui GUI
|
||||
5. Plugin system: load language support and tools dynamically
|
||||
Reference in New Issue
Block a user