micro-models-agent 0.41.0 → 0.41.2
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/bin/mma.mjs +41 -41
- package/dist/cli/commands.js +9 -19
- package/dist/cli/completer.js +36 -37
- package/dist/cli/index.js +2 -2
- package/dist/cli/main.js +48 -23
- package/dist/cli/plugin-commands.js +36 -0
- package/dist/cli/repl-commands.js +40 -12
- package/dist/cli/repl.js +217 -87
- package/dist/cli/run-result.js +22 -0
- package/dist/cli/security-commands.js +5 -7
- package/dist/cli/setup.js +8 -26
- package/dist/config/config.js +52 -5
- package/dist/config/defaults.js +29 -5
- package/dist/config/experts.js +1 -1
- package/dist/config/index.js +3 -3
- package/dist/config/security.js +3 -10
- package/dist/core/agent-moe.js +2 -10
- package/dist/core/agent.js +273 -82
- package/dist/core/bootstrap.js +80 -13
- package/dist/core/index.js +2 -2
- package/dist/core/prompt-builder.js +23 -2
- package/dist/core/session-logger.js +46 -4
- package/dist/core/version.js +24 -0
- package/dist/i18n/en.json +75 -2
- package/dist/i18n/ru.json +74 -1
- package/dist/index.js +1 -1
- package/dist/llm/image-utils.js +4 -5
- package/dist/llm/index.js +4 -4
- package/dist/llm/model-loader.js +6 -6
- package/dist/llm/openai-compat.js +40 -34
- package/dist/llm/orchestrator.js +33 -29
- package/dist/llm/response.js +9 -9
- package/dist/logger/app-logger.js +1 -1
- package/dist/logger/index.js +1 -1
- package/dist/main.js +1111 -954
- package/dist/migration/backup.js +13 -13
- package/dist/migration/detect.js +11 -11
- package/dist/migration/index.js +2 -2
- package/dist/modules/artifacts/store.js +61 -0
- package/dist/modules/browser/actions.js +34 -4
- package/dist/modules/browser/bridge-client.js +199 -0
- package/dist/modules/browser/bridge-path.js +10 -0
- package/dist/modules/browser/bridge-server.mjs +202 -202
- package/dist/modules/browser/cookie-store.js +6 -6
- package/dist/modules/browser/driver.js +136 -0
- package/dist/modules/browser/index.js +7 -5
- package/dist/modules/browser/module.js +8 -7
- package/dist/modules/browser/session.js +87 -84
- package/dist/modules/browser/snapshot.js +92 -58
- package/dist/modules/browser/types.js +4 -1
- package/dist/modules/certification/cli.js +2 -4
- package/dist/modules/certification/fact-checker.js +1 -3
- package/dist/modules/certification/loader.js +3 -9
- package/dist/modules/certification/runner.js +1 -4
- package/dist/modules/context/chunk-query.js +100 -0
- package/dist/modules/context/fact-extractor.js +162 -0
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/index.js +1 -1
- package/dist/modules/context/manager.js +160 -86
- package/dist/modules/execution/audit-runners.js +152 -0
- package/dist/modules/execution/auditor.js +177 -25
- package/dist/modules/execution/execution-plugin.js +272 -0
- package/dist/modules/execution/module.js +201 -544
- package/dist/modules/execution/moe-executor.js +25 -0
- package/dist/modules/execution/plan-store.js +1 -3
- package/dist/modules/execution/plan-tool.js +508 -0
- package/dist/modules/execution/plan-validator.js +10 -10
- package/dist/modules/execution/planner.js +6 -1
- package/dist/modules/execution/stuck-detector.js +173 -10
- package/dist/modules/execution/verifier.js +86 -42
- package/dist/modules/execution/windows-commands.js +41 -0
- package/dist/modules/hallucination/confidence.js +8 -1
- package/dist/modules/hallucination/detector.js +2 -5
- package/dist/modules/hallucination/factual.js +3 -64
- package/dist/modules/hallucination/index.js +1 -1
- package/dist/modules/hallucination/js-identifiers.js +190 -0
- package/dist/modules/hallucination/llm-judge.js +1 -3
- package/dist/modules/indexer/cache.js +9 -7
- package/dist/modules/indexer/index.js +3 -3
- package/dist/modules/indexer/module.js +95 -42
- package/dist/modules/indexer/project-profile.js +183 -0
- package/dist/modules/indexer/walker.js +17 -17
- package/dist/modules/lsp/check-tool.js +58 -0
- package/dist/modules/lsp/client.js +74 -31
- package/dist/modules/lsp/command.js +60 -0
- package/dist/modules/lsp/config.js +87 -33
- package/dist/modules/lsp/index.js +3 -3
- package/dist/modules/lsp/module.js +185 -21
- package/dist/modules/lsp/probe.js +76 -0
- package/dist/modules/lsp/project-root.js +32 -0
- package/dist/modules/lsp/startup-check.js +141 -0
- package/dist/modules/mcp/module.js +2 -6
- package/dist/modules/memory/index.js +1 -1
- package/dist/modules/memory/module.js +71 -23
- package/dist/modules/memory/search.js +11 -9
- package/dist/modules/memory/store.js +13 -13
- package/dist/modules/pipelines/engine.js +10 -10
- package/dist/modules/pipelines/index.js +3 -3
- package/dist/modules/pipelines/parser.js +17 -14
- package/dist/modules/pipelines/template.js +1 -1
- package/dist/modules/plugins/builtin/lint-on-write.js +21 -16
- package/dist/modules/plugins/builtin/notify.js +3 -2
- package/dist/modules/plugins/index.js +1 -1
- package/dist/modules/plugins/loader.js +59 -17
- package/dist/modules/plugins/manager.js +73 -17
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +1 -1
- package/dist/modules/processes/registry.js +135 -46
- package/dist/modules/registry.js +4 -2
- package/dist/modules/security/audit-notifier.js +39 -39
- package/dist/modules/security/command-validator.js +2 -8
- package/dist/modules/security/data-sanitizer.js +1 -9
- package/dist/modules/security/encryption.js +58 -56
- package/dist/modules/security/network-validator.js +1 -9
- package/dist/modules/security/path-validator.js +1 -3
- package/dist/modules/security/security-policies.js +3 -19
- package/dist/modules/security/session-encryption.js +1 -1
- package/dist/modules/security/session-isolation.js +8 -8
- package/dist/modules/session/index.js +3 -3
- package/dist/modules/session/module.js +5 -5
- package/dist/modules/session/store.js +3 -9
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +1 -2
- package/dist/modules/updater/checker.js +70 -6
- package/dist/modules/updater/index.js +2 -1
- package/dist/modules/updater/module.js +116 -0
- package/dist/modules/user-profile/compressor.js +2 -2
- package/dist/modules/user-profile/index.js +1 -1
- package/dist/modules/user-profile/profile.js +9 -9
- package/dist/tools/attach-image.js +1 -1
- package/dist/tools/bash.js +178 -19
- package/dist/tools/browser.js +46 -29
- package/dist/tools/chunk-query.js +99 -0
- package/dist/tools/download-file.js +116 -0
- package/dist/tools/enable-tools.js +58 -0
- package/dist/tools/executor.js +4 -5
- package/dist/tools/file-info.js +13 -12
- package/dist/tools/filter-tools.js +9 -2
- package/dist/tools/glob-tool.js +11 -11
- package/dist/tools/grep-tool.js +1 -3
- package/dist/tools/hidden-tools-block.js +37 -0
- package/dist/tools/index.js +13 -2
- package/dist/tools/list-dir.js +18 -17
- package/dist/tools/load-skill.js +1 -3
- package/dist/tools/path-utils.js +4 -4
- package/dist/tools/pipeline-run.js +25 -25
- package/dist/tools/process-kill.js +11 -11
- package/dist/tools/process-list.js +20 -22
- package/dist/tools/process-log.js +22 -18
- package/dist/tools/question.js +1 -3
- package/dist/tools/read-file.js +10 -2
- package/dist/tools/recall.js +44 -37
- package/dist/tools/registry.js +15 -4
- package/dist/tools/remember.js +29 -29
- package/dist/tools/scope-check.js +9 -9
- package/dist/tools/subagent.js +54 -9
- package/dist/tools/user-input.js +1 -1
- package/dist/tools/web-browse.js +3 -3
- package/dist/tools/web-fetch.js +3 -3
- package/dist/tools/web-search.js +3 -3
- package/dist/tools/write-file.js +1 -3
- package/dist/ui/box.js +1 -5
- package/dist/ui/index.js +6 -6
- package/dist/ui/line-editor.js +703 -0
- package/dist/ui/line-math.js +69 -0
- package/dist/ui/md-formatter.js +33 -33
- package/dist/ui/output.js +5 -5
- package/dist/ui/plan-view.js +103 -0
- package/dist/ui/renderer.js +15 -10
- package/dist/ui/table.js +1 -1
- package/package.json +48 -48
|
@@ -2,6 +2,7 @@ import { getExpertConfig } from "../../config/experts";
|
|
|
2
2
|
import { filterToolsByTags } from "../../tools/filter-tools";
|
|
3
3
|
import { StuckDetector } from "./stuck-detector";
|
|
4
4
|
import { t } from "../../i18n/index";
|
|
5
|
+
import { ArtifactStore } from "../artifacts/store";
|
|
5
6
|
const TRANSIENT_ERROR_PATTERNS = [
|
|
6
7
|
"timeout",
|
|
7
8
|
"Timeout",
|
|
@@ -102,14 +103,38 @@ async function executeSubtask(subtask, deps, _sharedContext) {
|
|
|
102
103
|
read_only_files: readOnlyFiles,
|
|
103
104
|
tool_tags: expertConfig.tool_tags,
|
|
104
105
|
};
|
|
106
|
+
if (deps.config.subagent?.resultMode === "file") {
|
|
107
|
+
subagentArgs.result_mode = "file";
|
|
108
|
+
subagentArgs.artifact_name = subtask.id;
|
|
109
|
+
}
|
|
105
110
|
const result = await subagentTool.handler({
|
|
106
111
|
config: deps.config,
|
|
107
112
|
baseDir: deps.baseDir,
|
|
108
113
|
logger: deps.logger,
|
|
109
114
|
llmProvider: deps.llmProvider,
|
|
110
115
|
toolExecutor: deps.toolExecutor,
|
|
116
|
+
sessionId: "moe",
|
|
111
117
|
}, subagentArgs);
|
|
112
118
|
if (result.success) {
|
|
119
|
+
if (deps.config.subagent?.resultMode === "file") {
|
|
120
|
+
const pathMatch = result.output.match(/saved to artifact: (.+)/);
|
|
121
|
+
const abs = pathMatch ? pathMatch[1].trim() : "";
|
|
122
|
+
if (abs) {
|
|
123
|
+
const store = new ArtifactStore({
|
|
124
|
+
baseDir: deps.baseDir,
|
|
125
|
+
sessionId: "moe",
|
|
126
|
+
});
|
|
127
|
+
const full = store.read(abs.split(/[\\/]/).pop() || ""); // file name only, scoped to root
|
|
128
|
+
deps.logger.debug(`MoE subtask "${subtask.id}" attached artifact ${abs} (${full?.length ?? 0} chars) to Router`);
|
|
129
|
+
return {
|
|
130
|
+
subtaskId: subtask.id,
|
|
131
|
+
success: true,
|
|
132
|
+
summary: `Artifact: ${abs}`,
|
|
133
|
+
result: full ?? result.output,
|
|
134
|
+
durationMs: Date.now() - startTime,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
113
138
|
return {
|
|
114
139
|
subtaskId: subtask.id,
|
|
115
140
|
success: true,
|
|
@@ -29,9 +29,7 @@ function listDir(dir, baseDir) {
|
|
|
29
29
|
if (!existsSync(dir))
|
|
30
30
|
return [];
|
|
31
31
|
const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
32
|
-
return files
|
|
33
|
-
.map((f) => readPlanFile(join(dir, f), baseDir))
|
|
34
|
-
.filter((p) => p !== null);
|
|
32
|
+
return files.map((f) => readPlanFile(join(dir, f), baseDir)).filter((p) => p !== null);
|
|
35
33
|
}
|
|
36
34
|
function toMeta(plan, status) {
|
|
37
35
|
return {
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { t } from "../../i18n/index";
|
|
2
|
+
import { PlanCreator } from "./planner";
|
|
3
|
+
import { checkPlanCoverage } from "./plan-coverage";
|
|
4
|
+
/**
|
|
5
|
+
* The plan / todo / verify tool definitions for the execution module. The
|
|
6
|
+
* handlers were extracted verbatim from ExecutionModule.getToolDefinitions() —
|
|
7
|
+
* all state access goes through the `deps` closures (the module owns the
|
|
8
|
+
* tracker and the plan store).
|
|
9
|
+
*/
|
|
10
|
+
export function createPlanToolDefinitions(deps) {
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
name: "plan",
|
|
14
|
+
alwaysOn: true,
|
|
15
|
+
description: 'Create, update, show, abort, list, switch, or re-plan multi-step plans.\n\nActions:\n- create: Start a new plan. Previous active plan is auto-preserved: incomplete → draft, complete → archive.\n- update: Mark step status (done/failed/skipped), or rebuild plan with new steps.\n- show: Print current plan checklist.\n- abort: Archive current plan and clear active slot.\n- list: Show all plans (active, drafts, archived) with progress.\n- switch: Make a different plan active (by plan id).\n- re-plan: Iterative replanning: keep completed steps, replace remaining with new steps.\n\nWrite CONCRETE steps with exact file paths and commands:\n- Specify WHICH files to create with exact paths (e.g. "create src/components/Header.tsx with navigation and logo")\n- Specify WHICH packages to install (e.g. "run npm install react react-dom")\n- Specify WHICH CLI commands to run with exact arguments\n- Each step should include at least one file extension (.ts, .tsx, .json, etc.) or a command verb (install, create, build, run, add, init)\n- Good: "Создать src/components/Header.tsx с навигацией и логотипом"\n- Bad: "Настройка проекта" (too vague — what exactly needs to be configured?)\n- For steps that REMOVE files, pass kinds: ["delete"] aligned with steps (the done-gate verifies those files are gone instead of demanding they exist).',
|
|
16
|
+
parameters: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
action: {
|
|
20
|
+
type: "string",
|
|
21
|
+
enum: ["create", "update", "show", "abort", "list", "switch", "re-plan"],
|
|
22
|
+
},
|
|
23
|
+
title: { type: "string" },
|
|
24
|
+
steps: { type: "array", items: { type: "string" } },
|
|
25
|
+
kinds: {
|
|
26
|
+
type: "array",
|
|
27
|
+
items: { type: "string", enum: ["create", "delete"] },
|
|
28
|
+
description: 'Optional per-step intent, aligned 1:1 with steps. Pass "delete" for steps whose purpose is REMOVING files — the done-gate then requires those files to be GONE (not present). Defaults to "create".',
|
|
29
|
+
},
|
|
30
|
+
step: { type: "number" },
|
|
31
|
+
status: { type: "string", enum: ["done", "failed", "skipped"] },
|
|
32
|
+
note: { type: "string" },
|
|
33
|
+
id: { type: "string", description: "Plan id (for switch action)" },
|
|
34
|
+
},
|
|
35
|
+
required: ["action"],
|
|
36
|
+
},
|
|
37
|
+
handler: async (_ctx, args) => {
|
|
38
|
+
const action = String(args.action);
|
|
39
|
+
if (action === "list") {
|
|
40
|
+
const metas = deps.store.listAll();
|
|
41
|
+
if (metas.length === 0) {
|
|
42
|
+
return { success: true, output: t("plan.list_empty") };
|
|
43
|
+
}
|
|
44
|
+
const lines = metas.map((m) => {
|
|
45
|
+
const icon = m.status === "active" ? "[*]" : m.status === "draft" ? "[ ]" : "[-]";
|
|
46
|
+
const namePart = m.name ? ` (${m.name})` : "";
|
|
47
|
+
return `${icon} ${m.id}${namePart} — ${m.title} ${m.doneCount}/${m.stepCount}`;
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
output: `${t("plan.list_header")}\n${lines.join("\n")}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (action === "switch") {
|
|
55
|
+
const planId = String(args.id || "");
|
|
56
|
+
if (!planId) {
|
|
57
|
+
return { success: false, output: t("plan.switch_no_id") };
|
|
58
|
+
}
|
|
59
|
+
const found = deps.store.find(planId);
|
|
60
|
+
if (!found) {
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
output: t("plan.not_found", { id: planId }),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
deps.preserveActive();
|
|
67
|
+
if (found.status === "draft") {
|
|
68
|
+
deps.store.removeDraft(found.plan.id);
|
|
69
|
+
}
|
|
70
|
+
else if (found.status === "archived") {
|
|
71
|
+
deps.store.removeArchived(found.plan.id);
|
|
72
|
+
}
|
|
73
|
+
deps.setPlan(found.plan);
|
|
74
|
+
const display = PlanCreator.toPromptBlock(found.plan, 0);
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
output: t("plan.switched", {
|
|
78
|
+
id: found.plan.id,
|
|
79
|
+
title: found.plan.title,
|
|
80
|
+
}),
|
|
81
|
+
display,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (action === "re-plan") {
|
|
85
|
+
if (!deps.trackerRef.current) {
|
|
86
|
+
return { success: false, output: t("plan.no_active") };
|
|
87
|
+
}
|
|
88
|
+
const newSteps = Array.isArray(args.steps) ? args.steps.map(String) : [];
|
|
89
|
+
if (newSteps.length === 0) {
|
|
90
|
+
return { success: false, output: t("plan.replan_no_steps") };
|
|
91
|
+
}
|
|
92
|
+
const tracker = deps.trackerRef.current;
|
|
93
|
+
const oldPlan = tracker.getPlan();
|
|
94
|
+
const replanned = PlanCreator.replan(oldPlan, newSteps, args.title ? String(args.title) : undefined);
|
|
95
|
+
const keptCount = replanned.steps.length - newSteps.length;
|
|
96
|
+
deps.setPlan(replanned);
|
|
97
|
+
const display = PlanCreator.toPromptBlock(replanned, keptCount);
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
output: t("plan.replanned", {
|
|
101
|
+
kept: String(keptCount),
|
|
102
|
+
steps: String(newSteps.length),
|
|
103
|
+
}),
|
|
104
|
+
display,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (action === "create") {
|
|
108
|
+
const title = String(args.title || "Task Plan");
|
|
109
|
+
const steps = Array.isArray(args.steps) ? args.steps.map(String) : [];
|
|
110
|
+
if (steps.length === 0) {
|
|
111
|
+
return { success: false, output: t("plan.no_steps") };
|
|
112
|
+
}
|
|
113
|
+
const parsed = deps.parseKinds(args, steps);
|
|
114
|
+
if (parsed.error) {
|
|
115
|
+
return { success: false, output: parsed.error };
|
|
116
|
+
}
|
|
117
|
+
// Never silently discard an in-progress plan (observed:
|
|
118
|
+
// ses_msvuao0h — right after compaction the model called
|
|
119
|
+
// `plan create` again, dumping the 1/6 plan into drafts and
|
|
120
|
+
// starting from scratch). An active plan WITH progress blocks
|
|
121
|
+
// creation until the model consciously aborts it. A fresh plan
|
|
122
|
+
// (all steps pending) is safe to replace — the model may have
|
|
123
|
+
// just changed its mind about the approach.
|
|
124
|
+
const existing = deps.trackerRef.current;
|
|
125
|
+
if (existing) {
|
|
126
|
+
const activePlan = existing.getPlan();
|
|
127
|
+
const progressed = activePlan.steps.some((s) => s.status === "done" || s.status === "failed" || s.status === "skipped");
|
|
128
|
+
if (progressed) {
|
|
129
|
+
const done = activePlan.steps.filter((s) => s.status === "done").length;
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
output: t("plan.active_in_progress", {
|
|
133
|
+
id: activePlan.id,
|
|
134
|
+
done: String(done),
|
|
135
|
+
total: String(activePlan.steps.length),
|
|
136
|
+
current: String(existing.getCurrentStepIndex() + 1),
|
|
137
|
+
}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
deps.preserveActive();
|
|
142
|
+
const plan = PlanCreator.createPlan(title, steps, deps.baseDir, parsed.kinds ?? []);
|
|
143
|
+
deps.setPlan(plan);
|
|
144
|
+
const display = PlanCreator.toPromptBlock(plan, 0);
|
|
145
|
+
let output = t("plan.created", {
|
|
146
|
+
title,
|
|
147
|
+
steps: String(steps.length),
|
|
148
|
+
});
|
|
149
|
+
// The model sometimes passes an id expecting the plan to be
|
|
150
|
+
// created under it (observed: id "plan_error_check_e07pb2" was
|
|
151
|
+
// ignored, a new id appeared). Be explicit so it doesn't look
|
|
152
|
+
// for a plan by the wrong id afterwards.
|
|
153
|
+
if (args.id) {
|
|
154
|
+
output += `\n${t("plan.id_ignored")}`;
|
|
155
|
+
}
|
|
156
|
+
if (existing) {
|
|
157
|
+
output += `\n${t("plan.existing_fresh")}`;
|
|
158
|
+
}
|
|
159
|
+
let displayOut = display;
|
|
160
|
+
const taskText = deps.getTaskText(_ctx);
|
|
161
|
+
if (taskText) {
|
|
162
|
+
const { missing } = checkPlanCoverage(taskText, steps);
|
|
163
|
+
if (missing.length > 0) {
|
|
164
|
+
const warn = t("plan.coverage_warning", {
|
|
165
|
+
missing: missing.join(", "),
|
|
166
|
+
});
|
|
167
|
+
output += `\n⚠️ ${warn}`;
|
|
168
|
+
displayOut = `${display}\n⚠️ ${warn}`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
success: true,
|
|
173
|
+
output,
|
|
174
|
+
display: displayOut,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (action === "show") {
|
|
178
|
+
const planId = args.id ? String(args.id) : "";
|
|
179
|
+
if (planId && planId !== deps.trackerRef.current?.getPlan()?.id) {
|
|
180
|
+
// Show a specific plan (active, draft, or archived) instead
|
|
181
|
+
// of ignoring the id and always printing the active one
|
|
182
|
+
// (observed: the model asked for an archived plan three
|
|
183
|
+
// times and got the active plan back every time).
|
|
184
|
+
const found = deps.store.find(planId);
|
|
185
|
+
if (!found) {
|
|
186
|
+
return {
|
|
187
|
+
success: false,
|
|
188
|
+
output: t("plan.not_found", { id: planId }),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const display = PlanCreator.toPromptBlock(found.plan, 0);
|
|
192
|
+
return {
|
|
193
|
+
success: true,
|
|
194
|
+
output: `${t("plan.show_header")}\n${display}`,
|
|
195
|
+
display,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (!deps.trackerRef.current) {
|
|
199
|
+
return { success: false, output: t("plan.no_active") };
|
|
200
|
+
}
|
|
201
|
+
const display = deps.trackerRef.current.toPromptBlock();
|
|
202
|
+
return {
|
|
203
|
+
success: true,
|
|
204
|
+
output: `${t("plan.show_header")}\n${display}`,
|
|
205
|
+
display,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (action === "update" && args.step && deps.trackerRef.current) {
|
|
209
|
+
const tracker = deps.trackerRef.current;
|
|
210
|
+
const stepId = Number(args.step);
|
|
211
|
+
const target = tracker.getStep(stepId);
|
|
212
|
+
if (!target) {
|
|
213
|
+
return { success: false, output: t("plan.step_not_found") };
|
|
214
|
+
}
|
|
215
|
+
const status = String(args.status || "done");
|
|
216
|
+
// Completed plan: re-marking a done step of a finished plan is the
|
|
217
|
+
// loop anchor (observed: 3× "plan update step=4 done" on a 4/4 plan
|
|
218
|
+
// while a NEW task waited). Archive + clear so the model can create a
|
|
219
|
+
// fresh plan for the new task instead of spinning.
|
|
220
|
+
if (tracker.isComplete()) {
|
|
221
|
+
const plan = tracker.getPlan();
|
|
222
|
+
deps.store.archivePlan(plan);
|
|
223
|
+
deps.trackerRef.current = null;
|
|
224
|
+
const done = plan.steps.filter((s) => s.status === "done").length;
|
|
225
|
+
return {
|
|
226
|
+
success: true,
|
|
227
|
+
output: t("plan.completed_archived", {
|
|
228
|
+
id: plan.id,
|
|
229
|
+
done: String(done),
|
|
230
|
+
total: String(plan.steps.length),
|
|
231
|
+
}),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// Idempotency: re-marking a done step as done usually means the
|
|
235
|
+
// plan was already auto-advanced (all step files exist) and the
|
|
236
|
+
// model is confirming its own work. Return SUCCESS so the loop
|
|
237
|
+
// isn't confused — a small model misreads success:false as a
|
|
238
|
+
// failed action and retries/spins. Genuine spinning (repeated
|
|
239
|
+
// identical calls without new work) is still caught by the
|
|
240
|
+
// stuck-detector's consecutive-identical-call check.
|
|
241
|
+
if (status === "done" && target.status === "done") {
|
|
242
|
+
const progress = tracker.getProgressString();
|
|
243
|
+
const display = tracker.toPromptBlock();
|
|
244
|
+
return {
|
|
245
|
+
success: true,
|
|
246
|
+
output: `${t("plan.step_already_done", {
|
|
247
|
+
step: String(stepId),
|
|
248
|
+
})}\n${progress}`,
|
|
249
|
+
display,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// Order enforcement: only the CURRENT step may be marked done.
|
|
253
|
+
// Prevents jumping ahead (step 5 marked done before steps 1-4,
|
|
254
|
+
// observed in the post-mortem session) and the follow-on audit
|
|
255
|
+
// failures ("5/5 steps done, 5 files missing").
|
|
256
|
+
if (status === "done") {
|
|
257
|
+
const blockers = tracker
|
|
258
|
+
.getPlan()
|
|
259
|
+
.steps.filter((s) => s.id < stepId && s.status !== "done" && s.status !== "skipped");
|
|
260
|
+
if (blockers.length > 0) {
|
|
261
|
+
return {
|
|
262
|
+
success: false,
|
|
263
|
+
output: t("plan.order_blocked", {
|
|
264
|
+
step: String(stepId),
|
|
265
|
+
first: String(blockers[0].id),
|
|
266
|
+
desc: blockers[0].description,
|
|
267
|
+
}),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Deliverable gate: marking a step done must match its declared
|
|
272
|
+
// intent. "create" (default) → named files must exist; "delete"
|
|
273
|
+
// → named files must be GONE. Intent is model-declared (kind),
|
|
274
|
+
// never keyword-inferred (rule #10).
|
|
275
|
+
if (status === "done") {
|
|
276
|
+
if (target.kind === "delete") {
|
|
277
|
+
const leftovers = deps.stillExistingDeliverables(target);
|
|
278
|
+
if (leftovers.length > 0) {
|
|
279
|
+
return {
|
|
280
|
+
success: false,
|
|
281
|
+
output: t("plan.deliverables_remain", {
|
|
282
|
+
step: String(stepId),
|
|
283
|
+
files: leftovers.join(", "),
|
|
284
|
+
}),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
const missing = deps.missingStepDeliverables(target);
|
|
290
|
+
if (missing.length > 0) {
|
|
291
|
+
return {
|
|
292
|
+
success: false,
|
|
293
|
+
output: t("plan.deliverables_missing", {
|
|
294
|
+
step: String(stepId),
|
|
295
|
+
files: missing.join(", "),
|
|
296
|
+
}),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
tracker.updateStepStatus(stepId, status);
|
|
302
|
+
if (args.note)
|
|
303
|
+
tracker.addNote(Number(args.step), String(args.note));
|
|
304
|
+
tracker.syncCurrentStep();
|
|
305
|
+
deps.store.saveActive(tracker.getPlan());
|
|
306
|
+
// If this update completed the last step → auto-archive. This breaks
|
|
307
|
+
// the loop where a finished plan stays active and anchors the model
|
|
308
|
+
// to "continue step N, do NOT create a new plan".
|
|
309
|
+
if (tracker.isComplete()) {
|
|
310
|
+
const plan = tracker.getPlan();
|
|
311
|
+
deps.store.archivePlan(plan);
|
|
312
|
+
deps.trackerRef.current = null;
|
|
313
|
+
const done = plan.steps.filter((s) => s.status === "done").length;
|
|
314
|
+
return {
|
|
315
|
+
success: true,
|
|
316
|
+
output: `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })}\n${t("plan.completed_archived", {
|
|
317
|
+
id: plan.id,
|
|
318
|
+
done: String(done),
|
|
319
|
+
total: String(plan.steps.length),
|
|
320
|
+
})}`,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const progress = tracker.getProgressString();
|
|
324
|
+
const display = tracker.toPromptBlock();
|
|
325
|
+
_ctx.sessionLog?.plan("step-update", `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })} | ${progress} | current: step ${tracker.getCurrentStepIndex() + 1}`);
|
|
326
|
+
return {
|
|
327
|
+
success: true,
|
|
328
|
+
output: `${t("plan.step_status", { step: String(args.step), status: String(args.status || "done") })}\n${progress}`,
|
|
329
|
+
display,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (action === "update" &&
|
|
333
|
+
Array.isArray(args.steps) &&
|
|
334
|
+
args.steps.length > 0 &&
|
|
335
|
+
deps.trackerRef.current) {
|
|
336
|
+
const tracker = deps.trackerRef.current;
|
|
337
|
+
const title = String(args.title || tracker.getPlan().title);
|
|
338
|
+
const steps = args.steps.map(String);
|
|
339
|
+
const parsed = deps.parseKinds(args, steps);
|
|
340
|
+
if (parsed.error) {
|
|
341
|
+
return { success: false, output: parsed.error };
|
|
342
|
+
}
|
|
343
|
+
const plan = PlanCreator.createPlan(title, steps, deps.baseDir, parsed.kinds ?? []);
|
|
344
|
+
deps.setPlan(plan);
|
|
345
|
+
const display = PlanCreator.toPromptBlock(plan, 0);
|
|
346
|
+
const output = t("plan.updated", {
|
|
347
|
+
title,
|
|
348
|
+
steps: String(steps.length),
|
|
349
|
+
});
|
|
350
|
+
return {
|
|
351
|
+
success: true,
|
|
352
|
+
output,
|
|
353
|
+
display,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (action === "abort") {
|
|
357
|
+
const tracker = deps.trackerRef.current;
|
|
358
|
+
if (tracker) {
|
|
359
|
+
deps.store.archivePlan(tracker.getPlan());
|
|
360
|
+
}
|
|
361
|
+
deps.trackerRef.current = null;
|
|
362
|
+
deps.store.clearActive();
|
|
363
|
+
return { success: true, output: t("plan.aborted") };
|
|
364
|
+
}
|
|
365
|
+
if (!deps.trackerRef.current) {
|
|
366
|
+
return { success: false, output: t("plan.no_active") };
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
success: false,
|
|
370
|
+
output: t("plan.unknown_action", { action }),
|
|
371
|
+
};
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: "todo",
|
|
376
|
+
alwaysOn: true,
|
|
377
|
+
description: "Manage sub-tasks within current plan step. Use to break down complex steps into smaller tasks.",
|
|
378
|
+
parameters: {
|
|
379
|
+
type: "object",
|
|
380
|
+
properties: {
|
|
381
|
+
action: { type: "string", enum: ["add", "done", "list"] },
|
|
382
|
+
items: { type: "array", items: { type: "string" } },
|
|
383
|
+
},
|
|
384
|
+
required: ["action"],
|
|
385
|
+
},
|
|
386
|
+
handler: async (_ctx, args) => {
|
|
387
|
+
if (!deps.trackerRef.current) {
|
|
388
|
+
return { success: false, output: t("plan.no_active") };
|
|
389
|
+
}
|
|
390
|
+
const tracker = deps.trackerRef.current;
|
|
391
|
+
const action = String(args.action);
|
|
392
|
+
const currentStep = tracker.getCurrentStep();
|
|
393
|
+
if (!currentStep) {
|
|
394
|
+
return { success: false, output: t("plan.step_not_found") };
|
|
395
|
+
}
|
|
396
|
+
if (action === "add" && Array.isArray(args.items)) {
|
|
397
|
+
const items = args.items.map(String);
|
|
398
|
+
const existing = currentStep.subtasks ?? [];
|
|
399
|
+
let nextId = existing.reduce((m, s) => Math.max(m, s.id), 0) + 1;
|
|
400
|
+
for (const item of items) {
|
|
401
|
+
existing.push({ id: nextId++, text: item, done: false });
|
|
402
|
+
}
|
|
403
|
+
currentStep.subtasks = existing;
|
|
404
|
+
deps.store.saveActive(tracker.getPlan());
|
|
405
|
+
const display = tracker.toPromptBlock();
|
|
406
|
+
return {
|
|
407
|
+
success: true,
|
|
408
|
+
output: t("todo.added", {
|
|
409
|
+
count: String(items.length),
|
|
410
|
+
items: items.join(", "),
|
|
411
|
+
}),
|
|
412
|
+
display,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (action === "done") {
|
|
416
|
+
const items = Array.isArray(args.items) ? args.items.map(String) : [];
|
|
417
|
+
if (items.length === 0) {
|
|
418
|
+
return {
|
|
419
|
+
success: false,
|
|
420
|
+
output: t("todo.no_items"),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const subs = currentStep.subtasks ?? [];
|
|
424
|
+
let marked = 0;
|
|
425
|
+
for (const item of items) {
|
|
426
|
+
const sub = subs.find((s) => !s.done && s.text.toLowerCase() === item.toLowerCase());
|
|
427
|
+
if (sub) {
|
|
428
|
+
sub.done = true;
|
|
429
|
+
marked++;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (marked === 0) {
|
|
433
|
+
return {
|
|
434
|
+
success: false,
|
|
435
|
+
output: t("todo.subtask_not_found", {
|
|
436
|
+
items: items.join(", "),
|
|
437
|
+
}),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
currentStep.subtasks = subs;
|
|
441
|
+
deps.store.saveActive(tracker.getPlan());
|
|
442
|
+
const doneCount = subs.filter((s) => s.done).length;
|
|
443
|
+
const display = tracker.toPromptBlock();
|
|
444
|
+
// Marking a sub-task done must NOT auto-complete the plan
|
|
445
|
+
// step — the step is only done when the model confirms the
|
|
446
|
+
// whole step via plan update (observed: 3 × "todo done"
|
|
447
|
+
// returned "Step 3: done" and marked the step done early).
|
|
448
|
+
let output = t("todo.marked_done", {
|
|
449
|
+
count: String(marked),
|
|
450
|
+
});
|
|
451
|
+
output += ` (${doneCount}/${subs.length})`;
|
|
452
|
+
if (doneCount === subs.length) {
|
|
453
|
+
output += `\nAll sub-tasks done — call plan update step=${currentStep.id} status=done to complete this step.`;
|
|
454
|
+
}
|
|
455
|
+
return { success: true, output, display };
|
|
456
|
+
}
|
|
457
|
+
if (action === "list") {
|
|
458
|
+
const subs = currentStep.subtasks ?? [];
|
|
459
|
+
const lines = subs.length
|
|
460
|
+
? subs.map((s) => `${s.done ? "[x]" : "[ ]"} ${s.text}`)
|
|
461
|
+
: ["(no sub-tasks)"];
|
|
462
|
+
return {
|
|
463
|
+
success: true,
|
|
464
|
+
output: `Step ${currentStep.id}: ${currentStep.description}\n${lines.join("\n")}`,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
success: false,
|
|
469
|
+
output: t("todo.unknown_action", { action }),
|
|
470
|
+
};
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
name: "verify",
|
|
475
|
+
alwaysOn: true,
|
|
476
|
+
description: "Run verification for the current step. Checks files mentioned in the step description.",
|
|
477
|
+
parameters: {
|
|
478
|
+
type: "object",
|
|
479
|
+
properties: {
|
|
480
|
+
step: { type: "number", description: "Step number to verify" },
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
handler: async (_ctx, args) => {
|
|
484
|
+
if (!deps.trackerRef.current)
|
|
485
|
+
return { success: false, output: t("plan.no_active") };
|
|
486
|
+
const tracker = deps.trackerRef.current;
|
|
487
|
+
const step = args.step ? tracker.getStep(Number(args.step)) : tracker.getCurrentStep();
|
|
488
|
+
if (!step)
|
|
489
|
+
return { success: false, output: t("plan.step_not_found") };
|
|
490
|
+
const result = await deps.verifier.verifyStep(step.description);
|
|
491
|
+
if (result.noFiles) {
|
|
492
|
+
return {
|
|
493
|
+
success: true,
|
|
494
|
+
output: t("verify.no_files", { step: String(step.id) }),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
success: result.passed,
|
|
499
|
+
output: result.passed
|
|
500
|
+
? t("verify.passed")
|
|
501
|
+
: t("verify.failed", {
|
|
502
|
+
details: result.failed.map((f) => f.message).join("; "),
|
|
503
|
+
}),
|
|
504
|
+
};
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
];
|
|
508
|
+
}
|
|
@@ -3,7 +3,7 @@ export function validatePlan(plan, config) {
|
|
|
3
3
|
const warnings = [];
|
|
4
4
|
const autoFixes = [];
|
|
5
5
|
if (!plan.subtasks || plan.subtasks.length === 0) {
|
|
6
|
-
return { valid: false, errors: [
|
|
6
|
+
return { valid: false, errors: ["Plan has no subtasks"], warnings: [], autoFixes: [] };
|
|
7
7
|
}
|
|
8
8
|
errors.push(...expertTagsExist(plan.subtasks, config));
|
|
9
9
|
errors.push(...hasCycle(plan.subtasks));
|
|
@@ -30,7 +30,7 @@ export function expertTagsExist(subtasks, config) {
|
|
|
30
30
|
const knownExperts = new Set(Object.keys(config.experts || {}));
|
|
31
31
|
for (const subtask of subtasks) {
|
|
32
32
|
if (!knownExperts.has(subtask.expert_tag)) {
|
|
33
|
-
errors.push(`Subtask "${subtask.id}" references unknown expert_tag "${subtask.expert_tag}". Known experts: ${Array.from(knownExperts).join(
|
|
33
|
+
errors.push(`Subtask "${subtask.id}" references unknown expert_tag "${subtask.expert_tag}". Known experts: ${Array.from(knownExperts).join(", ")}`);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
return errors;
|
|
@@ -81,12 +81,12 @@ export function hasAllowedFilesOverlap(subtasks) {
|
|
|
81
81
|
}
|
|
82
82
|
for (const [file, writers] of writeFiles) {
|
|
83
83
|
if (writers.length > 1) {
|
|
84
|
-
const noDep = writers.filter(w => {
|
|
85
|
-
const sub = subtasks.find(s => s.id === w);
|
|
86
|
-
return sub && !sub.depends_on?.some(d => writers.includes(d));
|
|
84
|
+
const noDep = writers.filter((w) => {
|
|
85
|
+
const sub = subtasks.find((s) => s.id === w);
|
|
86
|
+
return sub && !sub.depends_on?.some((d) => writers.includes(d));
|
|
87
87
|
});
|
|
88
88
|
if (noDep.length > 1) {
|
|
89
|
-
errors.push(`Write-write overlap on "${file}" between subtasks: ${writers.join(
|
|
89
|
+
errors.push(`Write-write overlap on "${file}" between subtasks: ${writers.join(", ")} without dependency chain`);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -104,7 +104,7 @@ export function deleteReadOverlap(subtasks) {
|
|
|
104
104
|
continue;
|
|
105
105
|
const readFiles = new Set(reader.read_only_files);
|
|
106
106
|
const writeFiles = writer.allowed_files;
|
|
107
|
-
const overlapping = writeFiles.filter(f => {
|
|
107
|
+
const overlapping = writeFiles.filter((f) => {
|
|
108
108
|
for (const rf of readFiles) {
|
|
109
109
|
if (f.startsWith(rf) || rf.startsWith(f))
|
|
110
110
|
return true;
|
|
@@ -125,7 +125,7 @@ export function deleteReadOverlap(subtasks) {
|
|
|
125
125
|
subtaskId: reader.id,
|
|
126
126
|
dependsOn: writer.id,
|
|
127
127
|
canAutoFix: false,
|
|
128
|
-
error: `Read-after-write overlap between "${writer.id}" (writer) and "${reader.id}" (reader) on "${overlapping.join(
|
|
128
|
+
error: `Read-after-write overlap between "${writer.id}" (writer) and "${reader.id}" (reader) on "${overlapping.join(", ")}" — add depends_on: [${writer.id}]`,
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
}
|
|
@@ -134,9 +134,9 @@ export function deleteReadOverlap(subtasks) {
|
|
|
134
134
|
return results;
|
|
135
135
|
}
|
|
136
136
|
export function applyAutoFixes(plan, autoFixes) {
|
|
137
|
-
const fixed = { ...plan, subtasks: plan.subtasks.map(s => ({ ...s })) };
|
|
137
|
+
const fixed = { ...plan, subtasks: plan.subtasks.map((s) => ({ ...s })) };
|
|
138
138
|
for (const fix of autoFixes) {
|
|
139
|
-
const sub = fixed.subtasks.find(s => s.id === fix.subtaskId);
|
|
139
|
+
const sub = fixed.subtasks.find((s) => s.id === fix.subtaskId);
|
|
140
140
|
if (!sub)
|
|
141
141
|
continue;
|
|
142
142
|
const dependsOnMatch = fix.fix.match(/depends_on:\s*(\S+)/);
|
|
@@ -26,7 +26,7 @@ export class PlanCreator {
|
|
|
26
26
|
const hasActionWord = actionWords.some((w) => task.toLowerCase().includes(w));
|
|
27
27
|
return hasActionWord && words.length > 8;
|
|
28
28
|
}
|
|
29
|
-
static createPlan(title, stepDescriptions, baseDir) {
|
|
29
|
+
static createPlan(title, stepDescriptions, baseDir, kinds) {
|
|
30
30
|
const stepCount = stepDescriptions.length;
|
|
31
31
|
return {
|
|
32
32
|
id: generatePlanId(),
|
|
@@ -35,6 +35,7 @@ export class PlanCreator {
|
|
|
35
35
|
id: i + 1,
|
|
36
36
|
description: desc,
|
|
37
37
|
status: "pending",
|
|
38
|
+
kind: kinds?.[i] ?? "create",
|
|
38
39
|
})),
|
|
39
40
|
createdAt: new Date().toISOString(),
|
|
40
41
|
baseDir,
|
|
@@ -49,6 +50,7 @@ export class PlanCreator {
|
|
|
49
50
|
id: kept.length + i + 1,
|
|
50
51
|
description: desc,
|
|
51
52
|
status: "pending",
|
|
53
|
+
kind: "create",
|
|
52
54
|
}));
|
|
53
55
|
return {
|
|
54
56
|
id: plan.id,
|
|
@@ -79,6 +81,9 @@ export class PlanCreator {
|
|
|
79
81
|
: "[ ]";
|
|
80
82
|
const note = step.note ? ` — ${step.note}` : "";
|
|
81
83
|
lines.push(`${icon} ${step.id}. ${step.description}${note}`);
|
|
84
|
+
for (const sub of step.subtasks ?? []) {
|
|
85
|
+
lines.push(` ${sub.done ? "[x]" : "[ ]"} ${sub.text}`);
|
|
86
|
+
}
|
|
82
87
|
}
|
|
83
88
|
return lines.join("\n");
|
|
84
89
|
}
|