micro-models-agent 0.28.17 → 0.29.1
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/dist/cli/commands.js +3 -116
- package/dist/cli/main.js +8 -35
- package/dist/cli/repl.js +611 -110
- package/dist/cli/setup.js +12 -32
- package/dist/config/config.js +30 -46
- package/dist/config/defaults.js +1 -10
- package/dist/config/security.js +8 -15
- package/dist/core/agent-moe.js +12 -24
- package/dist/core/agent.js +47 -281
- package/dist/core/bootstrap.js +36 -52
- package/dist/core/session-logger.js +2 -35
- package/dist/i18n/en.json +15 -79
- package/dist/i18n/index.js +9 -12
- package/dist/i18n/ru.json +15 -79
- package/dist/index.js +13 -13
- package/dist/llm/openai-compat.js +10 -39
- package/dist/logger/app-logger.js +16 -83
- package/dist/main.js +625 -243
- package/dist/modules/browser/session.js +60 -108
- package/dist/modules/context/history.js +15 -0
- package/dist/modules/context/manager.js +10 -119
- package/dist/modules/execution/auditor.js +39 -33
- package/dist/modules/execution/index.js +6 -8
- package/dist/modules/execution/module.js +32 -474
- package/dist/modules/execution/moe-executor.js +40 -97
- package/dist/modules/execution/planner.js +13 -63
- package/dist/modules/execution/stuck-detector.js +39 -252
- package/dist/modules/execution/tracker.js +7 -21
- package/dist/modules/execution/verifier.js +17 -46
- package/dist/modules/hallucination/confidence.js +2 -7
- package/dist/modules/hallucination/consistency.js +42 -8
- package/dist/modules/hallucination/detector.js +21 -26
- package/dist/modules/hallucination/factual.js +150 -170
- package/dist/modules/hallucination/index.js +4 -5
- package/dist/modules/index.js +5 -5
- package/dist/modules/mcp/client.js +2 -8
- package/dist/modules/memory/store.js +0 -4
- package/dist/modules/plugins/builtin/lint-on-write.js +38 -143
- package/dist/modules/processes/detect.js +34 -0
- package/dist/modules/processes/index.js +2 -1
- package/dist/modules/processes/registry.js +35 -125
- package/dist/modules/processes/runner.js +110 -9
- package/dist/modules/security/audit-log.js +10 -30
- package/dist/modules/security/command-validator.js +16 -42
- package/dist/modules/security/content-scanner.js +8 -9
- package/dist/modules/security/network-validator.js +2 -2
- package/dist/modules/security/path-validator.js +10 -64
- package/dist/modules/security/security-policies.js +67 -221
- package/dist/modules/security/session-encryption.js +25 -42
- package/dist/modules/session/manager.js +10 -15
- package/dist/modules/session/store.js +8 -62
- package/dist/modules/skills/index.js +3 -2
- package/dist/modules/skills/matcher.js +27 -0
- package/dist/modules/skills/module.js +23 -10
- package/dist/tools/bash.js +90 -287
- package/dist/tools/create-dir.js +1 -0
- package/dist/tools/delete-file.js +1 -0
- package/dist/tools/edit-file.js +8 -10
- package/dist/tools/executor.js +7 -57
- package/dist/tools/grep-tool.js +29 -51
- package/dist/tools/index.js +40 -55
- package/dist/tools/load-skill.js +18 -14
- package/dist/tools/move-file.js +2 -3
- package/dist/tools/pipeline-run.js +1 -1
- package/dist/tools/read-file.js +5 -15
- package/dist/tools/search-history.js +22 -42
- package/dist/tools/subagent.js +12 -21
- package/dist/tools/web-browse.js +25 -54
- package/dist/tools/web-fetch.js +34 -60
- package/dist/tools/web-search.js +20 -39
- package/dist/tools/write-file.js +10 -13
- package/dist/ui/diff.js +16 -9
- package/dist/ui/renderer.js +6 -69
- package/package.json +1 -1
- package/dist/cli/repl-commands.js +0 -633
- package/dist/core/workspace.js +0 -76
- package/dist/logger/file-log.js +0 -151
- package/dist/modules/certification/cli.js +0 -176
- package/dist/modules/certification/fact-checker.js +0 -84
- package/dist/modules/certification/loader.js +0 -111
- package/dist/modules/certification/manifest.js +0 -50
- package/dist/modules/certification/runner.js +0 -162
- package/dist/modules/certification/scenarios.js +0 -124
- package/dist/modules/certification/types.js +0 -1
- package/dist/modules/execution/plan-coverage.js +0 -68
- package/dist/modules/execution/plan-persister.js +0 -46
- package/dist/modules/execution/plan-store.js +0 -159
- package/dist/modules/hallucination/js-identifiers.js +0 -72
- package/dist/modules/hallucination/llm-judge.js +0 -103
- package/dist/modules/lsp/client.js +0 -235
- package/dist/modules/lsp/config.js +0 -81
- package/dist/modules/lsp/index.js +0 -3
- package/dist/modules/lsp/module.js +0 -68
- package/dist/modules/lsp/types.js +0 -1
|
@@ -1,38 +1,28 @@
|
|
|
1
|
-
import { getExpertConfig } from
|
|
2
|
-
import { filterToolsByTags } from
|
|
3
|
-
import { StuckDetector } from "./stuck-detector";
|
|
4
|
-
import { t } from "../../i18n/index";
|
|
1
|
+
import { getExpertConfig } from '../../config/experts';
|
|
2
|
+
import { filterToolsByTags } from '../../tools/filter-tools';
|
|
5
3
|
const TRANSIENT_ERROR_PATTERNS = [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"503",
|
|
13
|
-
"network",
|
|
14
|
-
"Network",
|
|
15
|
-
"ECONNREFUSED",
|
|
16
|
-
"ECONNRESET",
|
|
17
|
-
"fetch failed",
|
|
18
|
-
"abort",
|
|
19
|
-
"Abort",
|
|
4
|
+
'timeout', 'Timeout', 'TIMEOUT',
|
|
5
|
+
'5xx', '500', '502', '503',
|
|
6
|
+
'network', 'Network',
|
|
7
|
+
'ECONNREFUSED', 'ECONNRESET',
|
|
8
|
+
'fetch failed',
|
|
9
|
+
'abort', 'Abort',
|
|
20
10
|
];
|
|
21
11
|
function isTransientError(error) {
|
|
22
|
-
return TRANSIENT_ERROR_PATTERNS.some(
|
|
12
|
+
return TRANSIENT_ERROR_PATTERNS.some(p => error.includes(p));
|
|
23
13
|
}
|
|
24
14
|
function sleep(ms) {
|
|
25
|
-
return new Promise(
|
|
15
|
+
return new Promise(r => setTimeout(r, ms));
|
|
26
16
|
}
|
|
27
17
|
export function topologicalSort(subtasks) {
|
|
28
18
|
const sorted = [];
|
|
29
|
-
const remaining = new Set(subtasks.map(
|
|
30
|
-
const subtaskMap = new Map(subtasks.map(
|
|
19
|
+
const remaining = new Set(subtasks.map(s => s.id));
|
|
20
|
+
const subtaskMap = new Map(subtasks.map(s => [s.id, s]));
|
|
31
21
|
while (remaining.size > 0) {
|
|
32
22
|
const ready = [];
|
|
33
23
|
for (const id of remaining) {
|
|
34
24
|
const sub = subtaskMap.get(id);
|
|
35
|
-
const depsSatisfied = (sub.depends_on || []).every(
|
|
25
|
+
const depsSatisfied = (sub.depends_on || []).every(d => !remaining.has(d));
|
|
36
26
|
if (depsSatisfied) {
|
|
37
27
|
ready.push(sub);
|
|
38
28
|
}
|
|
@@ -55,19 +45,19 @@ async function executeSubtask(subtask, deps, _sharedContext) {
|
|
|
55
45
|
subtaskId: subtask.id,
|
|
56
46
|
success: false,
|
|
57
47
|
summary: `Unknown expert_tag: ${subtask.expert_tag}`,
|
|
58
|
-
result:
|
|
48
|
+
result: '',
|
|
59
49
|
error: `No expert config found for tag "${subtask.expert_tag}"`,
|
|
60
50
|
durationMs: Date.now() - startTime,
|
|
61
51
|
};
|
|
62
52
|
}
|
|
63
|
-
const subagentTool = deps.toolRegistry.get(
|
|
53
|
+
const subagentTool = deps.toolRegistry.get('subagent');
|
|
64
54
|
if (!subagentTool) {
|
|
65
55
|
return {
|
|
66
56
|
subtaskId: subtask.id,
|
|
67
57
|
success: false,
|
|
68
|
-
summary:
|
|
69
|
-
result:
|
|
70
|
-
error:
|
|
58
|
+
summary: 'Subagent tool not registered',
|
|
59
|
+
result: '',
|
|
60
|
+
error: 'Subagent tool not found in registry',
|
|
71
61
|
durationMs: Date.now() - startTime,
|
|
72
62
|
};
|
|
73
63
|
}
|
|
@@ -77,25 +67,23 @@ async function executeSubtask(subtask, deps, _sharedContext) {
|
|
|
77
67
|
return {
|
|
78
68
|
subtaskId: subtask.id,
|
|
79
69
|
success: false,
|
|
80
|
-
summary: `No tools available for expert "${subtask.expert_tag}" (tags: ${expertConfig.tool_tags.join(
|
|
81
|
-
result:
|
|
82
|
-
error:
|
|
70
|
+
summary: `No tools available for expert "${subtask.expert_tag}" (tags: ${expertConfig.tool_tags.join(', ')})`,
|
|
71
|
+
result: '',
|
|
72
|
+
error: 'No matching tools',
|
|
83
73
|
durationMs: Date.now() - startTime,
|
|
84
74
|
};
|
|
85
75
|
}
|
|
86
76
|
const maxAttempts = expertConfig.max_attempts || 3;
|
|
87
|
-
let lastError =
|
|
77
|
+
let lastError = '';
|
|
88
78
|
let lastResult = null;
|
|
89
|
-
const stuckDetector = new StuckDetector(maxAttempts * 2, maxAttempts);
|
|
90
79
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
91
|
-
stuckDetector.recordIteration(1);
|
|
92
80
|
try {
|
|
93
81
|
const allowedFiles = subtask.allowed_files || [];
|
|
94
82
|
const readOnlyFiles = subtask.read_only_files || [];
|
|
95
83
|
const scopePrompt = allowedFiles.length > 0 || readOnlyFiles.length > 0
|
|
96
|
-
? `\n\nAllowed files to write: ${allowedFiles.join(
|
|
97
|
-
:
|
|
98
|
-
const taskPrompt = `${subtask.description}\n\nExpected output: ${subtask.expected_output}\nSuccess criteria:\n${(subtask.success_criteria || []).map((c) => `- ${c}`).join(
|
|
84
|
+
? `\n\nAllowed files to write: ${allowedFiles.join(', ') || '(none)'}\nRead-only files: ${readOnlyFiles.join(', ') || '(none)'}`
|
|
85
|
+
: '';
|
|
86
|
+
const taskPrompt = `${subtask.description}\n\nExpected output: ${subtask.expected_output}\nSuccess criteria:\n${(subtask.success_criteria || []).map((c) => `- ${c}`).join('\n')}${scopePrompt}`;
|
|
99
87
|
const subagentArgs = {
|
|
100
88
|
task: taskPrompt,
|
|
101
89
|
allowed_files: allowedFiles,
|
|
@@ -120,83 +108,45 @@ async function executeSubtask(subtask, deps, _sharedContext) {
|
|
|
120
108
|
}
|
|
121
109
|
lastError = result.output;
|
|
122
110
|
lastResult = result;
|
|
123
|
-
stuckDetector.recordToolError("subagent", result.output);
|
|
124
|
-
// Inject actionable hints into the next retry's prompt
|
|
125
|
-
if (attempt < maxAttempts) {
|
|
126
|
-
const hints = stuckDetector.getActionableHints();
|
|
127
|
-
const recovery = stuckDetector.getRecoveryMessage();
|
|
128
|
-
if (hints.length > 0) {
|
|
129
|
-
deps.logger.debug(`Hint for subtask "${subtask.id}" retry: ${hints.join("; ")}`);
|
|
130
|
-
}
|
|
131
|
-
if (recovery) {
|
|
132
|
-
deps.logger.debug(`Recovery for subtask "${subtask.id}": ${recovery}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
111
|
if (isTransientError(lastError) && attempt < maxAttempts) {
|
|
136
112
|
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 8000);
|
|
137
113
|
deps.logger.debug(`Retrying subtask "${subtask.id}" (attempt ${attempt}/${maxAttempts}) after ${delay}ms: transient error`);
|
|
138
114
|
await sleep(delay);
|
|
139
115
|
continue;
|
|
140
116
|
}
|
|
141
|
-
// Non-transient failure — build detailed error with hints
|
|
142
|
-
const hints = stuckDetector.getActionableHints();
|
|
143
|
-
let errorDetail = result.output;
|
|
144
|
-
if (hints.length > 0) {
|
|
145
|
-
errorDetail += `\n\n${t("exec.hints", { hints: hints.map((h) => `- ${h}`).join("\n") })}`;
|
|
146
|
-
}
|
|
147
|
-
if (lastError) {
|
|
148
|
-
errorDetail +=
|
|
149
|
-
"\n\nIf relevant skills are available, consider loading one with load_skill for expert guidance.";
|
|
150
|
-
}
|
|
151
117
|
return {
|
|
152
118
|
subtaskId: subtask.id,
|
|
153
119
|
success: false,
|
|
154
120
|
summary: `Failed: ${subtask.id}`,
|
|
155
121
|
result: result.output,
|
|
156
|
-
error:
|
|
122
|
+
error: result.output,
|
|
157
123
|
durationMs: Date.now() - startTime,
|
|
158
124
|
};
|
|
159
125
|
}
|
|
160
126
|
catch (e) {
|
|
161
127
|
lastError = e.message;
|
|
162
|
-
stuckDetector.recordToolError("subagent", e.message);
|
|
163
128
|
if (isTransientError(lastError) && attempt < maxAttempts) {
|
|
164
129
|
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 8000);
|
|
165
130
|
deps.logger.debug(`Retrying subtask "${subtask.id}" (attempt ${attempt}/${maxAttempts}) after ${delay}ms: ${lastError}`);
|
|
166
131
|
await sleep(delay);
|
|
167
132
|
continue;
|
|
168
133
|
}
|
|
169
|
-
const hints = stuckDetector.getActionableHints();
|
|
170
|
-
let errorDetail = e.message;
|
|
171
|
-
if (hints.length > 0) {
|
|
172
|
-
errorDetail += `\n\n${t("exec.hints", { hints: hints.map((h) => `- ${h}`).join("\n") })}`;
|
|
173
|
-
}
|
|
174
|
-
if (lastError) {
|
|
175
|
-
errorDetail +=
|
|
176
|
-
"\n\nIf relevant skills are available, consider loading one with load_skill for expert guidance.";
|
|
177
|
-
}
|
|
178
134
|
return {
|
|
179
135
|
subtaskId: subtask.id,
|
|
180
136
|
success: false,
|
|
181
137
|
summary: `Error: ${subtask.id}`,
|
|
182
|
-
result:
|
|
183
|
-
error:
|
|
138
|
+
result: '',
|
|
139
|
+
error: e.message,
|
|
184
140
|
durationMs: Date.now() - startTime,
|
|
185
141
|
};
|
|
186
142
|
}
|
|
187
143
|
}
|
|
188
|
-
// Exhausted retries — build detailed failure
|
|
189
|
-
const stuckReason = stuckDetector.getStuckReason();
|
|
190
|
-
let finalError = lastError;
|
|
191
|
-
if (stuckReason) {
|
|
192
|
-
finalError += `\n${stuckReason}`;
|
|
193
|
-
}
|
|
194
144
|
return {
|
|
195
145
|
subtaskId: subtask.id,
|
|
196
146
|
success: false,
|
|
197
147
|
summary: `Failed after ${maxAttempts} attempts: ${subtask.id}`,
|
|
198
|
-
result:
|
|
199
|
-
error:
|
|
148
|
+
result: '',
|
|
149
|
+
error: lastError,
|
|
200
150
|
durationMs: Date.now() - startTime,
|
|
201
151
|
};
|
|
202
152
|
}
|
|
@@ -211,41 +161,34 @@ export class MoEExecutor {
|
|
|
211
161
|
const warnings = [];
|
|
212
162
|
const waves = topologicalSort(plan.subtasks);
|
|
213
163
|
if (waves.length === 0) {
|
|
214
|
-
return {
|
|
215
|
-
success: false,
|
|
216
|
-
results: [],
|
|
217
|
-
errors: ["Failed to topologically sort subtasks (possible cycle)"],
|
|
218
|
-
warnings: [],
|
|
219
|
-
};
|
|
164
|
+
return { success: false, results: [], errors: ['Failed to topologically sort subtasks (possible cycle)'], warnings: [] };
|
|
220
165
|
}
|
|
221
166
|
// Verify all subtasks are included — catch silent drops from unresolved dependencies
|
|
222
167
|
const sortedCount = waves.flat().length;
|
|
223
168
|
if (sortedCount < plan.subtasks.length) {
|
|
224
169
|
const missing = plan.subtasks
|
|
225
|
-
.filter(
|
|
226
|
-
.map(
|
|
170
|
+
.filter(s => !waves.flat().some(w => w.id === s.id))
|
|
171
|
+
.map(s => s.id);
|
|
227
172
|
return {
|
|
228
173
|
success: false,
|
|
229
174
|
results: [],
|
|
230
|
-
errors: [
|
|
231
|
-
`Missing subtasks after topological sort: ${missing.join(", ")} (dangling or invalid depends_on)`,
|
|
232
|
-
],
|
|
175
|
+
errors: [`Missing subtasks after topological sort: ${missing.join(', ')} (dangling or invalid depends_on)`],
|
|
233
176
|
warnings: [],
|
|
234
177
|
};
|
|
235
178
|
}
|
|
236
179
|
for (let waveIdx = 0; waveIdx < waves.length; waveIdx++) {
|
|
237
180
|
const wave = waves[waveIdx];
|
|
238
|
-
const wavePromises = wave.map(
|
|
239
|
-
.then(
|
|
181
|
+
const wavePromises = wave.map(subtask => executeSubtask(subtask, this.deps, plan.shared_context)
|
|
182
|
+
.then(result => {
|
|
240
183
|
results.push(result);
|
|
241
184
|
return result;
|
|
242
185
|
})
|
|
243
|
-
.catch(
|
|
186
|
+
.catch(e => {
|
|
244
187
|
const errResult = {
|
|
245
188
|
subtaskId: subtask.id,
|
|
246
189
|
success: false,
|
|
247
190
|
summary: `Unhandled error: ${subtask.id}`,
|
|
248
|
-
result:
|
|
191
|
+
result: '',
|
|
249
192
|
error: e.message,
|
|
250
193
|
durationMs: 0,
|
|
251
194
|
};
|
|
@@ -254,9 +197,9 @@ export class MoEExecutor {
|
|
|
254
197
|
}));
|
|
255
198
|
await Promise.all(wavePromises);
|
|
256
199
|
}
|
|
257
|
-
const failed = results.filter(
|
|
200
|
+
const failed = results.filter(r => !r.success);
|
|
258
201
|
if (failed.length > 0) {
|
|
259
|
-
errors.push(...failed.map(
|
|
202
|
+
errors.push(...failed.map(f => `[${f.subtaskId}] ${f.error || f.summary}`));
|
|
260
203
|
}
|
|
261
204
|
return { success: failed.length === 0, results, errors, warnings };
|
|
262
205
|
}
|
|
@@ -1,85 +1,35 @@
|
|
|
1
|
-
function generatePlanId() {
|
|
2
|
-
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
3
|
-
let id = "";
|
|
4
|
-
for (let i = 0; i < 6; i++) {
|
|
5
|
-
id += chars[Math.floor(Math.random() * chars.length)];
|
|
6
|
-
}
|
|
7
|
-
return `plan_${id}`;
|
|
8
|
-
}
|
|
9
1
|
export class PlanCreator {
|
|
10
2
|
static isMultiStep(task) {
|
|
11
3
|
const fileCount = (task.match(/\b[\w./-]+\.[a-z]+\b/gi) || []).length;
|
|
12
4
|
if (fileCount > 1)
|
|
13
5
|
return true;
|
|
14
|
-
const actionWords = [
|
|
15
|
-
"implement",
|
|
16
|
-
"create",
|
|
17
|
-
"add",
|
|
18
|
-
"build",
|
|
19
|
-
"setup",
|
|
20
|
-
"configure",
|
|
21
|
-
"write",
|
|
22
|
-
"make",
|
|
23
|
-
"develop",
|
|
24
|
-
];
|
|
6
|
+
const actionWords = ['implement', 'create', 'add', 'build', 'setup', 'configure', 'write', 'make', 'develop'];
|
|
25
7
|
const words = task.split(/\s+/);
|
|
26
|
-
const hasActionWord = actionWords.some(
|
|
8
|
+
const hasActionWord = actionWords.some(w => task.toLowerCase().includes(w));
|
|
27
9
|
return hasActionWord && words.length > 8;
|
|
28
10
|
}
|
|
29
|
-
static createPlan(title, stepDescriptions
|
|
30
|
-
const stepCount = stepDescriptions.length;
|
|
11
|
+
static createPlan(title, stepDescriptions) {
|
|
31
12
|
return {
|
|
32
|
-
|
|
33
|
-
title: `[${stepCount}] ${title}`,
|
|
13
|
+
title,
|
|
34
14
|
steps: stepDescriptions.map((desc, i) => ({
|
|
35
15
|
id: i + 1,
|
|
36
16
|
description: desc,
|
|
37
|
-
status:
|
|
17
|
+
status: 'pending',
|
|
38
18
|
})),
|
|
39
19
|
createdAt: new Date().toISOString(),
|
|
40
|
-
baseDir,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
static replan(plan, newSteps, title) {
|
|
44
|
-
const kept = plan.steps.filter((s) => s.status === "done" || s.status === "skipped");
|
|
45
|
-
const baseTitle = plan.title.replace(/^\[\d+[^]]*\]\s*/, "");
|
|
46
|
-
const newTitle = title || baseTitle;
|
|
47
|
-
const totalSteps = kept.length + newSteps.length;
|
|
48
|
-
const added = newSteps.map((desc, i) => ({
|
|
49
|
-
id: kept.length + i + 1,
|
|
50
|
-
description: desc,
|
|
51
|
-
status: "pending",
|
|
52
|
-
}));
|
|
53
|
-
return {
|
|
54
|
-
id: plan.id,
|
|
55
|
-
title: `[${totalSteps}] ${newTitle}`,
|
|
56
|
-
steps: [...kept, ...added],
|
|
57
|
-
createdAt: plan.createdAt,
|
|
58
|
-
baseDir: plan.baseDir,
|
|
59
|
-
name: plan.name,
|
|
60
20
|
};
|
|
61
21
|
}
|
|
62
22
|
static toPromptBlock(plan, currentStepIndex) {
|
|
63
|
-
const
|
|
64
|
-
const lines = [
|
|
65
|
-
`[${plan.id}] ${plan.title}`,
|
|
66
|
-
`Dir: ${plan.baseDir}`,
|
|
67
|
-
`Created: ${date} | Progress: ${plan.steps.filter((s) => s.status === "done").length}/${plan.steps.length} done, current: step ${currentStepIndex + 1}`,
|
|
68
|
-
``,
|
|
69
|
-
];
|
|
23
|
+
const lines = [`[Plan: ${plan.title}] (steps: ${plan.steps.filter(s => s.status === 'done').length}/${plan.steps.length} done, current: step ${currentStepIndex + 1})`];
|
|
70
24
|
for (const step of plan.steps) {
|
|
71
|
-
const icon = step.status ===
|
|
72
|
-
?
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
: step.status === "skipped"
|
|
78
|
-
? "[-]"
|
|
79
|
-
: "[ ]";
|
|
80
|
-
const note = step.note ? ` — ${step.note}` : "";
|
|
25
|
+
const icon = step.status === 'done' ? '[x]' :
|
|
26
|
+
step.status === 'in_progress' ? '[*]' :
|
|
27
|
+
step.status === 'failed' ? '[!]' :
|
|
28
|
+
step.status === 'skipped' ? '[-]' :
|
|
29
|
+
'[ ]';
|
|
30
|
+
const note = step.note ? ` — ${step.note}` : '';
|
|
81
31
|
lines.push(`${icon} ${step.id}. ${step.description}${note}`);
|
|
82
32
|
}
|
|
83
|
-
return lines.join(
|
|
33
|
+
return lines.join('\n');
|
|
84
34
|
}
|
|
85
35
|
}
|