skyloom 1.12.0 → 1.13.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.
Files changed (137) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +137 -46
  3. package/config/default.yaml +43 -47
  4. package/config/models.yaml +155 -155
  5. package/config/providers.yaml +39 -39
  6. package/config/skills/api_integrator/SKILL.md +15 -15
  7. package/config/skills/arch_designer/SKILL.md +13 -13
  8. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  9. package/config/skills/code_analysis/SKILL.md +13 -13
  10. package/config/skills/code_generator/SKILL.md +12 -12
  11. package/config/skills/code_reviewer/SKILL.md +13 -13
  12. package/config/skills/content_writer/SKILL.md +14 -14
  13. package/config/skills/data_transformer/SKILL.md +15 -15
  14. package/config/skills/document_analysis/SKILL.md +13 -13
  15. package/config/skills/emotional_companion/SKILL.md +15 -15
  16. package/config/skills/performance_checker/SKILL.md +14 -14
  17. package/config/skills/security_auditor/SKILL.md +14 -14
  18. package/config/skills/self_evolve/SKILL.md +13 -13
  19. package/config/skills/sys_operator/SKILL.md +15 -15
  20. package/config/skills/task_planner/SKILL.md +14 -14
  21. package/config/skills/web_research/SKILL.md +14 -14
  22. package/config/skills/workflow_designer/SKILL.md +13 -13
  23. package/dist/agents/dew.js +52 -52
  24. package/dist/agents/fair.js +84 -84
  25. package/dist/agents/fog.js +30 -30
  26. package/dist/agents/frost.js +32 -32
  27. package/dist/agents/rain.js +32 -32
  28. package/dist/agents/snow.js +68 -68
  29. package/dist/cli/main.js +127 -74
  30. package/dist/cli/main.js.map +1 -1
  31. package/dist/cli/tui.d.ts +52 -19
  32. package/dist/cli/tui.d.ts.map +1 -1
  33. package/dist/cli/tui.js +198 -265
  34. package/dist/cli/tui.js.map +1 -1
  35. package/dist/core/agent/task.d.ts +58 -0
  36. package/dist/core/agent/task.d.ts.map +1 -0
  37. package/dist/core/agent/task.js +83 -0
  38. package/dist/core/agent/task.js.map +1 -0
  39. package/dist/core/agent.d.ts +2 -45
  40. package/dist/core/agent.d.ts.map +1 -1
  41. package/dist/core/agent.js +61 -145
  42. package/dist/core/agent.js.map +1 -1
  43. package/dist/core/agent_helpers.d.ts +10 -0
  44. package/dist/core/agent_helpers.d.ts.map +1 -1
  45. package/dist/core/agent_helpers.js +39 -0
  46. package/dist/core/agent_helpers.js.map +1 -1
  47. package/dist/core/catalog.d.ts +71 -0
  48. package/dist/core/catalog.d.ts.map +1 -0
  49. package/dist/core/catalog.js +176 -0
  50. package/dist/core/catalog.js.map +1 -0
  51. package/dist/core/config.d.ts +8 -0
  52. package/dist/core/config.d.ts.map +1 -1
  53. package/dist/core/config.js +12 -4
  54. package/dist/core/config.js.map +1 -1
  55. package/dist/core/factory.js +16 -16
  56. package/dist/core/llm.d.ts +7 -0
  57. package/dist/core/llm.d.ts.map +1 -1
  58. package/dist/core/llm.js +139 -7
  59. package/dist/core/llm.js.map +1 -1
  60. package/dist/core/longdoc.js +5 -5
  61. package/dist/core/memory.d.ts.map +1 -1
  62. package/dist/core/memory.js +69 -62
  63. package/dist/core/memory.js.map +1 -1
  64. package/dist/core/theme.d.ts +46 -0
  65. package/dist/core/theme.d.ts.map +1 -0
  66. package/dist/core/theme.js +42 -0
  67. package/dist/core/theme.js.map +1 -0
  68. package/dist/web/server.js +542 -519
  69. package/dist/web/server.js.map +1 -1
  70. package/docs/AESTHETIC_DESIGN.md +144 -0
  71. package/docs/OPTIMIZATION_PLAN.md +178 -0
  72. package/package.json +60 -60
  73. package/scripts/install.js +48 -48
  74. package/scripts/link.js +10 -10
  75. package/setup.bat +79 -79
  76. package/skill-test-ty2fOA/test.md +10 -10
  77. package/src/agents/dew.ts +70 -70
  78. package/src/agents/fair.ts +102 -102
  79. package/src/agents/fog.ts +48 -48
  80. package/src/agents/frost.ts +50 -50
  81. package/src/agents/rain.ts +50 -50
  82. package/src/agents/snow.ts +239 -239
  83. package/src/cli/main.ts +417 -372
  84. package/src/cli/mode.ts +58 -58
  85. package/src/cli/tui.ts +174 -223
  86. package/src/core/agent/task.ts +100 -0
  87. package/src/core/agent.ts +1446 -1549
  88. package/src/core/agent_helpers.ts +496 -461
  89. package/src/core/arbitrate.ts +162 -162
  90. package/src/core/catalog.ts +178 -0
  91. package/src/core/checkpoint.ts +94 -94
  92. package/src/core/config.ts +20 -4
  93. package/src/core/estimate.ts +104 -104
  94. package/src/core/evolve.ts +191 -191
  95. package/src/core/factory.ts +627 -627
  96. package/src/core/filter.ts +103 -103
  97. package/src/core/graph.ts +156 -156
  98. package/src/core/icons.ts +53 -53
  99. package/src/core/index.ts +37 -37
  100. package/src/core/learn.ts +146 -146
  101. package/src/core/llm.ts +108 -5
  102. package/src/core/longdoc.ts +155 -155
  103. package/src/core/mcp_server.ts +176 -176
  104. package/src/core/memory.ts +1178 -1171
  105. package/src/core/profile.ts +255 -255
  106. package/src/core/router.ts +124 -124
  107. package/src/core/sandbox.ts +142 -142
  108. package/src/core/security.ts +243 -243
  109. package/src/core/skill.ts +342 -342
  110. package/src/core/theme.ts +65 -0
  111. package/src/core/tool_router.ts +193 -193
  112. package/src/core/vector.ts +152 -152
  113. package/src/core/workspace.ts +150 -150
  114. package/src/plugins/loader.ts +66 -66
  115. package/src/skills/loader.ts +46 -46
  116. package/src/sql.js.d.ts +29 -29
  117. package/src/tools/builtin.ts +380 -380
  118. package/src/tools/computer.ts +269 -269
  119. package/src/tools/delegate.ts +49 -49
  120. package/src/web/server.ts +660 -634
  121. package/src/web/tts.ts +93 -93
  122. package/tests/agent_helpers.test.ts +48 -0
  123. package/tests/bus.test.ts +121 -121
  124. package/tests/catalog.test.ts +86 -0
  125. package/tests/config.test.ts +41 -0
  126. package/tests/icons.test.ts +45 -45
  127. package/tests/memory.test.ts +147 -0
  128. package/tests/router.test.ts +86 -86
  129. package/tests/schemas.test.ts +51 -51
  130. package/tests/semantic.test.ts +83 -83
  131. package/tests/setup.ts +10 -10
  132. package/tests/skill.test.ts +172 -172
  133. package/tests/task.test.ts +60 -0
  134. package/tests/tool.test.ts +108 -108
  135. package/tests/tool_router.test.ts +71 -71
  136. package/tests/tui.test.ts +67 -0
  137. package/vitest.config.ts +17 -17
@@ -1,94 +1,94 @@
1
- /**
2
- * Orchestration checkpoint — save/restore task state.
3
- *
4
- * Writes ~/.skyloom/task_checkpoint.json so a long-running orchestration
5
- * interrupted by Ctrl-C can be resumed.
6
- */
7
-
8
- import * as fs from 'fs';
9
- import * as path from 'path';
10
- import { USER_CONFIG_DIR } from './config';
11
-
12
- function checkpointPath(): string {
13
- return path.join(USER_CONFIG_DIR, 'task_checkpoint.json');
14
- }
15
-
16
- /**
17
- * Save current orchestration state so it can be resumed later.
18
- */
19
- export function save(
20
- goal: string,
21
- tasks: any[],
22
- results: any[],
23
- completedIds?: Set<string>
24
- ): void {
25
- const cids = completedIds || new Set(results.map((r: any) => r.id));
26
- const payload = {
27
- goal,
28
- tasks: tasks.map(serializeTask),
29
- results: results.map(serializeResult),
30
- completed_ids: Array.from(cids).sort(),
31
- };
32
-
33
- const p = checkpointPath();
34
- const dir = path.dirname(p);
35
- if (!fs.existsSync(dir)) {
36
- fs.mkdirSync(dir, { recursive: true });
37
- }
38
-
39
- const tmp = p + '.tmp';
40
- fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
41
- fs.renameSync(tmp, p);
42
- }
43
-
44
- /**
45
- * Return the last saved checkpoint dict, or null if none / unreadable.
46
- */
47
- export function load(): Record<string, any> | null {
48
- const p = checkpointPath();
49
- if (!fs.existsSync(p)) {
50
- return null;
51
- }
52
- try {
53
- const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
54
- return typeof data === 'object' && data !== null ? data : null;
55
- } catch {
56
- return null;
57
- }
58
- }
59
-
60
- /**
61
- * Delete the checkpoint file.
62
- */
63
- export function clear(): void {
64
- try {
65
- const p = checkpointPath();
66
- if (fs.existsSync(p)) {
67
- fs.unlinkSync(p);
68
- }
69
- } catch {
70
- // Ignore cleanup errors
71
- }
72
- }
73
-
74
- // ── Serialization helpers ──
75
-
76
- function serializeTask(t: any): Record<string, any> {
77
- return {
78
- id: t.id,
79
- description: t.description,
80
- assigned_to: t.assignedTo ?? t.assigned_to,
81
- all_deps: t.allDeps ?? t.all_deps ?? [],
82
- status: t.status?.value ?? t.status ?? 'unknown',
83
- };
84
- }
85
-
86
- function serializeResult(r: any): Record<string, any> {
87
- return {
88
- id: r.id,
89
- agent: r.agent,
90
- description: r.description,
91
- success: r.success,
92
- content: r.content,
93
- };
94
- }
1
+ /**
2
+ * Orchestration checkpoint — save/restore task state.
3
+ *
4
+ * Writes ~/.skyloom/task_checkpoint.json so a long-running orchestration
5
+ * interrupted by Ctrl-C can be resumed.
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { USER_CONFIG_DIR } from './config';
11
+
12
+ function checkpointPath(): string {
13
+ return path.join(USER_CONFIG_DIR, 'task_checkpoint.json');
14
+ }
15
+
16
+ /**
17
+ * Save current orchestration state so it can be resumed later.
18
+ */
19
+ export function save(
20
+ goal: string,
21
+ tasks: any[],
22
+ results: any[],
23
+ completedIds?: Set<string>
24
+ ): void {
25
+ const cids = completedIds || new Set(results.map((r: any) => r.id));
26
+ const payload = {
27
+ goal,
28
+ tasks: tasks.map(serializeTask),
29
+ results: results.map(serializeResult),
30
+ completed_ids: Array.from(cids).sort(),
31
+ };
32
+
33
+ const p = checkpointPath();
34
+ const dir = path.dirname(p);
35
+ if (!fs.existsSync(dir)) {
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ }
38
+
39
+ const tmp = p + '.tmp';
40
+ fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
41
+ fs.renameSync(tmp, p);
42
+ }
43
+
44
+ /**
45
+ * Return the last saved checkpoint dict, or null if none / unreadable.
46
+ */
47
+ export function load(): Record<string, any> | null {
48
+ const p = checkpointPath();
49
+ if (!fs.existsSync(p)) {
50
+ return null;
51
+ }
52
+ try {
53
+ const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
54
+ return typeof data === 'object' && data !== null ? data : null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Delete the checkpoint file.
62
+ */
63
+ export function clear(): void {
64
+ try {
65
+ const p = checkpointPath();
66
+ if (fs.existsSync(p)) {
67
+ fs.unlinkSync(p);
68
+ }
69
+ } catch {
70
+ // Ignore cleanup errors
71
+ }
72
+ }
73
+
74
+ // ── Serialization helpers ──
75
+
76
+ function serializeTask(t: any): Record<string, any> {
77
+ return {
78
+ id: t.id,
79
+ description: t.description,
80
+ assigned_to: t.assignedTo ?? t.assigned_to,
81
+ all_deps: t.allDeps ?? t.all_deps ?? [],
82
+ status: t.status?.value ?? t.status ?? 'unknown',
83
+ };
84
+ }
85
+
86
+ function serializeResult(r: any): Record<string, any> {
87
+ return {
88
+ id: r.id,
89
+ agent: r.agent,
90
+ description: r.description,
91
+ success: r.success,
92
+ content: r.content,
93
+ };
94
+ }
@@ -223,6 +223,14 @@ export interface SkyloomConfig {
223
223
  agents: Record<string, AgentConfig>;
224
224
  providers?: Record<string, ProviderEntry>;
225
225
  models?: Record<string, ModelEntry[]>;
226
+ /** Top-level default model chosen by the setup wizard (e.g. "deepseek-v4-flash"). */
227
+ default_model?: string;
228
+ /** Top-level default provider chosen by the setup wizard. */
229
+ default_provider?: string;
230
+ /** LLM defaults block from default.yaml / user config (snake_case keys). */
231
+ llm?: Record<string, any>;
232
+ /** Other passthrough top-level config (memory, workspace, cli, mcp, plugins, tts…). */
233
+ [key: string]: any;
226
234
  }
227
235
 
228
236
  /**
@@ -261,18 +269,26 @@ export function mergeConfigs(defaultCfg: SkyloomConfig, userCfg: SkyloomConfig |
261
269
  return defaultCfg;
262
270
  }
263
271
 
272
+ // Preserve all top-level keys (default_model, default_provider, llm, memory,
273
+ // workspace, …) with the user winning, then deep-merge the known sub-objects.
264
274
  return {
275
+ ...defaultCfg,
276
+ ...userCfg,
265
277
  agents: {
266
278
  ...defaultCfg.agents,
267
279
  ...userCfg.agents,
268
280
  },
269
281
  providers: {
270
- ...defaultCfg.providers,
271
- ...userCfg.providers,
282
+ ...(defaultCfg.providers || {}),
283
+ ...(userCfg.providers || {}),
272
284
  },
273
285
  models: {
274
- ...defaultCfg.models,
275
- ...userCfg.models,
286
+ ...(defaultCfg.models || {}),
287
+ ...(userCfg.models || {}),
288
+ },
289
+ llm: {
290
+ ...(defaultCfg.llm || {}),
291
+ ...(userCfg.llm || {}),
276
292
  },
277
293
  };
278
294
  }
@@ -1,104 +1,104 @@
1
- /**
2
- * 资源估算模块 — Token & time budget estimation for task planning.
3
- *
4
- * Helps Snow and other planning agents estimate the cost of
5
- * proposed sub-tasks before committing to execution.
6
- */
7
-
8
- import type { Task } from "./agent";
9
-
10
- /* ═══════════════════════════════════════
11
- Token estimation
12
- ═══════════════════════════════════════ */
13
- const CJK_REGEX = /[一-鿿぀-ゟ가-힯㐀-䶿]/g;
14
-
15
- /** Estimate tokens for a given text (CJK ~2 each, ASCII ~4 chars each). */
16
- export function estimateTokens(text: string): number {
17
- const cjk = (text.match(CJK_REGEX) || []).length;
18
- return cjk * 2 + Math.ceil((text.length - cjk) / 4);
19
- }
20
-
21
- /* ═══════════════════════════════════════
22
- Per-task-type cost estimates
23
- ═══════════════════════════════════════ */
24
- const TASK_TYPE_PATTERNS: Array<[RegExp, number, number]> = [
25
- // [pattern, estimated tokens, estimated tools]
26
- [/read|read_file|grep|search|查|搜索|list/i, 2000, 2],
27
- [/write|write_file|生成|写|create|implement/i, 4000, 5],
28
- [/edit|edit_file|改|修改|fix|修复/i, 3000, 3],
29
- [/delete|delete_file|删|rm/i, 1500, 2],
30
- [/deploy|部署|publish|发布|release/i, 8000, 8],
31
- [/review|审查|audit|审计|scan|扫描/i, 5000, 4],
32
- [/test|测试|run_test|coverage/i, 3000, 3],
33
- [/research|研究|调研|analyze|分析/i, 6000, 4],
34
- [/orchestrate|编排|multi-step|多步/i, 12000, 10],
35
- ];
36
-
37
- /** Estimate cost for a single task description. */
38
- export function estimateTaskCost(description: string): { tokens: number; tools: number; timeSeconds: number } {
39
- let tokens = 2000; // base
40
- let tools = 2; // base
41
- for (const [pattern, t, tc] of TASK_TYPE_PATTERNS) {
42
- if (pattern.test(description)) { tokens = Math.max(tokens, t); tools = Math.max(tools, tc); }
43
- }
44
-
45
- // Time estimate: ~0.5s per tool call + 2s per 1k tokens
46
- const timeSeconds = (tokens / 1000) * 2 + tools * 0.5 + 2;
47
- return { tokens, tools, timeSeconds };
48
- }
49
-
50
- /* ═══════════════════════════════════════
51
- Task plan cost summary
52
- ═══════════════════════════════════════ */
53
- export interface PlanEstimate {
54
- totalTokens: number;
55
- totalTools: number;
56
- totalTimeSeconds: number;
57
- perTask: Array<{ id: string; tokens: number; tools: number; time: number }>;
58
- warnings: string[];
59
- }
60
-
61
- export function estimateTaskPlan(tasks: Task[]): PlanEstimate {
62
- const perTask: PlanEstimate["perTask"] = [];
63
- let totalTokens = 500; // system prompt overhead
64
- let totalTools = 0;
65
- let totalTime = 5; // init overhead
66
- const warnings: string[] = [];
67
-
68
- for (const t of tasks) {
69
- const est = estimateTaskCost(t.description);
70
- perTask.push({ id: t.id, tokens: est.tokens, tools: est.tools, time: est.timeSeconds });
71
- totalTokens += est.tokens;
72
- totalTools += est.tools;
73
- totalTime += est.timeSeconds;
74
-
75
- if (est.timeSeconds > 60) warnings.push(`Task ${t.id} may take >${Math.round(est.timeSeconds)}s`);
76
- if (est.tools > 10) warnings.push(`Task ${t.id} uses many tool calls (${est.tools})`);
77
- }
78
-
79
- if (totalTokens > 64000) warnings.push(`Total token estimate (${totalTokens}) exceeds typical context window`);
80
- if (totalTime > 120) warnings.push(`Estimated total time (${Math.round(totalTime)}s) is significant`);
81
- if (tasks.length > 6) warnings.push(`Large number of sub-tasks (${tasks.length}) — consider merging simpler ones`);
82
-
83
- return { totalTokens, totalTools, totalTimeSeconds: Math.round(totalTime), perTask, warnings };
84
- }
85
-
86
- /* ═══════════════════════════════════════
87
- Format estimate for display
88
- ═══════════════════════════════════════ */
89
- export function formatPlanEstimate(est: PlanEstimate): string {
90
- const lines: string[] = [
91
- `## Plan Estimate`,
92
- `| Task | Tokens | Tools | Time |`,
93
- `|------|--------|-------|------|`,
94
- ...est.perTask.map(t => `| ${t.id} | ${t.tokens} | ${t.tools} | ${t.time.toFixed(0)}s |`),
95
- `| **Total** | **${est.totalTokens}** | **${est.totalTools}** | **${est.totalTimeSeconds}s** |`,
96
- ];
97
-
98
- if (est.warnings.length > 0) {
99
- lines.push("", "### Warnings");
100
- for (const w of est.warnings) lines.push(`- ⚠ ${w}`);
101
- }
102
-
103
- return lines.join("\n");
104
- }
1
+ /**
2
+ * 资源估算模块 — Token & time budget estimation for task planning.
3
+ *
4
+ * Helps Snow and other planning agents estimate the cost of
5
+ * proposed sub-tasks before committing to execution.
6
+ */
7
+
8
+ import type { Task } from "./agent";
9
+
10
+ /* ═══════════════════════════════════════
11
+ Token estimation
12
+ ═══════════════════════════════════════ */
13
+ const CJK_REGEX = /[一-鿿぀-ゟ가-힯㐀-䶿]/g;
14
+
15
+ /** Estimate tokens for a given text (CJK ~2 each, ASCII ~4 chars each). */
16
+ export function estimateTokens(text: string): number {
17
+ const cjk = (text.match(CJK_REGEX) || []).length;
18
+ return cjk * 2 + Math.ceil((text.length - cjk) / 4);
19
+ }
20
+
21
+ /* ═══════════════════════════════════════
22
+ Per-task-type cost estimates
23
+ ═══════════════════════════════════════ */
24
+ const TASK_TYPE_PATTERNS: Array<[RegExp, number, number]> = [
25
+ // [pattern, estimated tokens, estimated tools]
26
+ [/read|read_file|grep|search|查|搜索|list/i, 2000, 2],
27
+ [/write|write_file|生成|写|create|implement/i, 4000, 5],
28
+ [/edit|edit_file|改|修改|fix|修复/i, 3000, 3],
29
+ [/delete|delete_file|删|rm/i, 1500, 2],
30
+ [/deploy|部署|publish|发布|release/i, 8000, 8],
31
+ [/review|审查|audit|审计|scan|扫描/i, 5000, 4],
32
+ [/test|测试|run_test|coverage/i, 3000, 3],
33
+ [/research|研究|调研|analyze|分析/i, 6000, 4],
34
+ [/orchestrate|编排|multi-step|多步/i, 12000, 10],
35
+ ];
36
+
37
+ /** Estimate cost for a single task description. */
38
+ export function estimateTaskCost(description: string): { tokens: number; tools: number; timeSeconds: number } {
39
+ let tokens = 2000; // base
40
+ let tools = 2; // base
41
+ for (const [pattern, t, tc] of TASK_TYPE_PATTERNS) {
42
+ if (pattern.test(description)) { tokens = Math.max(tokens, t); tools = Math.max(tools, tc); }
43
+ }
44
+
45
+ // Time estimate: ~0.5s per tool call + 2s per 1k tokens
46
+ const timeSeconds = (tokens / 1000) * 2 + tools * 0.5 + 2;
47
+ return { tokens, tools, timeSeconds };
48
+ }
49
+
50
+ /* ═══════════════════════════════════════
51
+ Task plan cost summary
52
+ ═══════════════════════════════════════ */
53
+ export interface PlanEstimate {
54
+ totalTokens: number;
55
+ totalTools: number;
56
+ totalTimeSeconds: number;
57
+ perTask: Array<{ id: string; tokens: number; tools: number; time: number }>;
58
+ warnings: string[];
59
+ }
60
+
61
+ export function estimateTaskPlan(tasks: Task[]): PlanEstimate {
62
+ const perTask: PlanEstimate["perTask"] = [];
63
+ let totalTokens = 500; // system prompt overhead
64
+ let totalTools = 0;
65
+ let totalTime = 5; // init overhead
66
+ const warnings: string[] = [];
67
+
68
+ for (const t of tasks) {
69
+ const est = estimateTaskCost(t.description);
70
+ perTask.push({ id: t.id, tokens: est.tokens, tools: est.tools, time: est.timeSeconds });
71
+ totalTokens += est.tokens;
72
+ totalTools += est.tools;
73
+ totalTime += est.timeSeconds;
74
+
75
+ if (est.timeSeconds > 60) warnings.push(`Task ${t.id} may take >${Math.round(est.timeSeconds)}s`);
76
+ if (est.tools > 10) warnings.push(`Task ${t.id} uses many tool calls (${est.tools})`);
77
+ }
78
+
79
+ if (totalTokens > 64000) warnings.push(`Total token estimate (${totalTokens}) exceeds typical context window`);
80
+ if (totalTime > 120) warnings.push(`Estimated total time (${Math.round(totalTime)}s) is significant`);
81
+ if (tasks.length > 6) warnings.push(`Large number of sub-tasks (${tasks.length}) — consider merging simpler ones`);
82
+
83
+ return { totalTokens, totalTools, totalTimeSeconds: Math.round(totalTime), perTask, warnings };
84
+ }
85
+
86
+ /* ═══════════════════════════════════════
87
+ Format estimate for display
88
+ ═══════════════════════════════════════ */
89
+ export function formatPlanEstimate(est: PlanEstimate): string {
90
+ const lines: string[] = [
91
+ `## Plan Estimate`,
92
+ `| Task | Tokens | Tools | Time |`,
93
+ `|------|--------|-------|------|`,
94
+ ...est.perTask.map(t => `| ${t.id} | ${t.tokens} | ${t.tools} | ${t.time.toFixed(0)}s |`),
95
+ `| **Total** | **${est.totalTokens}** | **${est.totalTools}** | **${est.totalTimeSeconds}s** |`,
96
+ ];
97
+
98
+ if (est.warnings.length > 0) {
99
+ lines.push("", "### Warnings");
100
+ for (const w of est.warnings) lines.push(`- ⚠ ${w}`);
101
+ }
102
+
103
+ return lines.join("\n");
104
+ }