skyloom 1.14.6 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/.github/workflows/ci.yml +2 -2
  2. package/.github/workflows/publish.yml +74 -0
  3. package/CONVERSION_PLAN.md +191 -191
  4. package/README.md +523 -220
  5. package/config/default.yaml +46 -43
  6. package/config/models.yaml +928 -155
  7. package/config/providers.yaml +109 -6
  8. package/dist/agents/snow.d.ts +2 -0
  9. package/dist/agents/snow.d.ts.map +1 -1
  10. package/dist/agents/snow.js +36 -5
  11. package/dist/agents/snow.js.map +1 -1
  12. package/dist/cli/loom_chat.d.ts.map +1 -1
  13. package/dist/cli/loom_chat.js +207 -1
  14. package/dist/cli/loom_chat.js.map +1 -1
  15. package/dist/cli/main.js +190 -40
  16. package/dist/cli/main.js.map +1 -1
  17. package/dist/cli/tui.d.ts.map +1 -1
  18. package/dist/cli/tui.js +6 -31
  19. package/dist/cli/tui.js.map +1 -1
  20. package/dist/core/agent.d.ts +6 -4
  21. package/dist/core/agent.d.ts.map +1 -1
  22. package/dist/core/agent.js +61 -20
  23. package/dist/core/agent.js.map +1 -1
  24. package/dist/core/catalog.d.ts.map +1 -1
  25. package/dist/core/catalog.js +30 -9
  26. package/dist/core/catalog.js.map +1 -1
  27. package/dist/core/commands.d.ts +110 -0
  28. package/dist/core/commands.d.ts.map +1 -0
  29. package/dist/core/commands.js +633 -0
  30. package/dist/core/commands.js.map +1 -0
  31. package/dist/core/concurrency.d.ts +38 -0
  32. package/dist/core/concurrency.d.ts.map +1 -0
  33. package/dist/core/concurrency.js +65 -0
  34. package/dist/core/concurrency.js.map +1 -0
  35. package/dist/core/factory.js +16 -16
  36. package/dist/core/file_checkpoint.d.ts +9 -0
  37. package/dist/core/file_checkpoint.d.ts.map +1 -1
  38. package/dist/core/file_checkpoint.js +33 -1
  39. package/dist/core/file_checkpoint.js.map +1 -1
  40. package/dist/core/llm.d.ts.map +1 -1
  41. package/dist/core/llm.js +66 -13
  42. package/dist/core/llm.js.map +1 -1
  43. package/dist/core/memory.js +51 -51
  44. package/dist/core/schemas.d.ts +16 -0
  45. package/dist/core/schemas.d.ts.map +1 -1
  46. package/dist/core/schemas.js +32 -0
  47. package/dist/core/schemas.js.map +1 -1
  48. package/dist/core/security.d.ts.map +1 -1
  49. package/dist/core/security.js +27 -0
  50. package/dist/core/security.js.map +1 -1
  51. package/dist/core/skymd.js +14 -14
  52. package/dist/core/trace.d.ts +105 -0
  53. package/dist/core/trace.d.ts.map +1 -0
  54. package/dist/core/trace.js +213 -0
  55. package/dist/core/trace.js.map +1 -0
  56. package/dist/tools/builtin.d.ts +2 -6
  57. package/dist/tools/builtin.d.ts.map +1 -1
  58. package/dist/tools/builtin.js +180 -125
  59. package/dist/tools/builtin.js.map +1 -1
  60. package/dist/tools/extra.d.ts +13 -0
  61. package/dist/tools/extra.d.ts.map +1 -0
  62. package/dist/tools/extra.js +827 -0
  63. package/dist/tools/extra.js.map +1 -0
  64. package/dist/tools/guards.d.ts +12 -0
  65. package/dist/tools/guards.d.ts.map +1 -0
  66. package/dist/tools/guards.js +143 -0
  67. package/dist/tools/guards.js.map +1 -0
  68. package/dist/tools/model_tool.d.ts.map +1 -1
  69. package/dist/tools/model_tool.js +24 -4
  70. package/dist/tools/model_tool.js.map +1 -1
  71. package/dist/web/markdown.d.ts +32 -0
  72. package/dist/web/markdown.d.ts.map +1 -0
  73. package/dist/web/markdown.js +202 -0
  74. package/dist/web/markdown.js.map +1 -0
  75. package/dist/web/server.d.ts +4 -0
  76. package/dist/web/server.d.ts.map +1 -1
  77. package/dist/web/server.js +14 -582
  78. package/dist/web/server.js.map +1 -1
  79. package/dist/web/ui.d.ts +31 -0
  80. package/dist/web/ui.d.ts.map +1 -0
  81. package/dist/web/ui.js +1009 -0
  82. package/dist/web/ui.js.map +1 -0
  83. package/docs/AESTHETIC_DESIGN.md +152 -152
  84. package/docs/OPTIMIZATION_PLAN.md +178 -178
  85. package/package.json +68 -68
  86. package/src/agents/snow.ts +38 -5
  87. package/src/cli/commands_md.ts +112 -112
  88. package/src/cli/input_macros.ts +83 -83
  89. package/src/cli/loom.ts +1041 -1041
  90. package/src/cli/loom_chat.ts +772 -603
  91. package/src/cli/main.ts +853 -723
  92. package/src/cli/tui.ts +264 -289
  93. package/src/core/agent/guard.ts +133 -133
  94. package/src/core/agent/task.ts +100 -100
  95. package/src/core/agent.ts +1630 -1590
  96. package/src/core/agent_helpers.ts +500 -500
  97. package/src/core/bus.ts +221 -221
  98. package/src/core/cache.ts +153 -153
  99. package/src/core/catalog.ts +199 -178
  100. package/src/core/circuit_breaker.ts +119 -119
  101. package/src/core/commands.ts +704 -0
  102. package/src/core/concurrency.ts +73 -0
  103. package/src/core/config.ts +365 -365
  104. package/src/core/constants.ts +95 -95
  105. package/src/core/factory.ts +656 -656
  106. package/src/core/file_checkpoint.ts +163 -136
  107. package/src/core/hooks.ts +126 -126
  108. package/src/core/llm.ts +972 -915
  109. package/src/core/logger.ts +143 -143
  110. package/src/core/mcp.ts +1001 -1001
  111. package/src/core/memory.ts +1201 -1201
  112. package/src/core/middleware.ts +350 -350
  113. package/src/core/model_config.ts +159 -159
  114. package/src/core/pipelines.ts +424 -424
  115. package/src/core/schemas.ts +319 -282
  116. package/src/core/security.ts +27 -0
  117. package/src/core/semantic.ts +211 -211
  118. package/src/core/skill.ts +384 -384
  119. package/src/core/skymd.ts +143 -143
  120. package/src/core/theme.ts +65 -65
  121. package/src/core/tool.ts +457 -457
  122. package/src/core/trace.ts +236 -0
  123. package/src/core/verify.ts +71 -71
  124. package/src/plugins/loader.ts +91 -91
  125. package/src/skills/loader.ts +75 -75
  126. package/src/tools/builtin.ts +571 -493
  127. package/src/tools/computer.ts +279 -279
  128. package/src/tools/extra.ts +662 -0
  129. package/src/tools/guards.ts +82 -0
  130. package/src/tools/model_tool.ts +93 -74
  131. package/src/tools/todo.ts +76 -76
  132. package/src/web/markdown.ts +193 -0
  133. package/src/web/server.ts +117 -693
  134. package/src/web/ui.ts +949 -0
  135. package/tests/agent.test.ts +211 -159
  136. package/tests/agent_helpers.test.ts +48 -48
  137. package/tests/catalog.test.ts +86 -86
  138. package/tests/checkpoint_commands.test.ts +124 -124
  139. package/tests/claude_compat.test.ts +110 -110
  140. package/tests/commands.test.ts +103 -0
  141. package/tests/concurrency.test.ts +102 -0
  142. package/tests/config.test.ts +41 -41
  143. package/tests/extra_tools.test.ts +212 -0
  144. package/tests/fence_plugin.test.ts +52 -52
  145. package/tests/guard.test.ts +75 -75
  146. package/tests/loom.test.ts +337 -337
  147. package/tests/memory.test.ts +170 -170
  148. package/tests/model_config.test.ts +109 -109
  149. package/tests/skymd.test.ts +146 -146
  150. package/tests/ssrf.test.ts +38 -38
  151. package/tests/structured_retry.test.ts +87 -0
  152. package/tests/task.test.ts +60 -60
  153. package/tests/todo_toolstats.test.ts +94 -94
  154. package/tests/trace.test.ts +128 -0
  155. package/tests/tui.test.ts +67 -67
  156. package/tests/web.test.ts +169 -0
  157. package/tsconfig.json +38 -38
@@ -1,282 +1,319 @@
1
- /**
2
- * Lightweight structured output schemas for LLM response validation.
3
- *
4
- * Why: LLM JSON output is inherently fragile — models emit markdown fences,
5
- * trailing commas, unquoted keys, or hallucinated fields. Rather than layering
6
- * heuristic repair (which silently passes corrupted data), we define typed
7
- * schemas and validate on ingress. Parsing failures surface immediately so the
8
- * caller can retry with a corrected prompt instead of propagating garbage.
9
- *
10
- * Zero external dependencies: uses only JSON and TypeScript types.
11
- */
12
-
13
- /**
14
- * Error raised when an LLM response fails schema validation.
15
- * Carries both a human-readable message and the raw text so callers
16
- * can log / retry with full context.
17
- */
18
- export class SchemaValidationError extends Error {
19
- raw: string;
20
-
21
- constructor(message: string, raw: string = "") {
22
- super(message);
23
- this.name = "SchemaValidationError";
24
- this.raw = raw;
25
- }
26
- }
27
-
28
- /**
29
- * One step in a task plan (mirrors PipelineStep / Task)
30
- */
31
- export interface TaskStepSchema {
32
- id: string | number;
33
- description: string;
34
- agent?: string;
35
- depends_on?: string[];
36
- priority?: "low" | "medium" | "high";
37
- }
38
-
39
- /**
40
- * Full task plan output from Snow's orchestrator
41
- */
42
- export interface TaskPlanSchema {
43
- goal: string;
44
- steps: TaskStepSchema[];
45
- }
46
-
47
- /**
48
- * A single extracted fact for long-term memory
49
- */
50
- export interface FactSchema {
51
- key: string;
52
- value: string;
53
- category?: string;
54
- }
55
-
56
- /**
57
- * Structured fact-extraction output from the LLM
58
- */
59
- export interface ExtractionResultSchema {
60
- facts: FactSchema[];
61
- }
62
-
63
- /**
64
- * Tool call schema for LLM responses
65
- */
66
- export interface ToolCallSchema {
67
- name: string;
68
- arguments: Record<string, unknown>;
69
- }
70
-
71
- /**
72
- * Message schema for agent communication
73
- */
74
- export interface MessageSchema {
75
- role: "user" | "assistant" | "tool";
76
- content: string;
77
- tool_calls?: ToolCallSchema[];
78
- tool_call_id?: string;
79
- }
80
-
81
- /**
82
- * Coerce a value to a target type with best-effort conversion
83
- */
84
- function coerceType(value: unknown, targetType: string): unknown {
85
- if (value === null || value === undefined) {
86
- return value;
87
- }
88
-
89
- switch (targetType) {
90
- case "string":
91
- return String(value);
92
- case "number":
93
- return Number(value);
94
- case "boolean":
95
- return Boolean(value);
96
- case "array":
97
- return Array.isArray(value) ? value : [];
98
- case "object":
99
- return typeof value === "object" ? value : {};
100
- default:
101
- return value;
102
- }
103
- }
104
-
105
- /**
106
- * Extract JSON object/array from a potentially malformed string
107
- */
108
- function extractJSON(text: string): string {
109
- let cleaned = text.trim();
110
-
111
- // Strip markdown code fences
112
- if (cleaned.includes("```")) {
113
- for (const fence of ["```json", "```"]) {
114
- if (cleaned.includes(fence)) {
115
- const after = cleaned.split(fence, 1)[1];
116
- if (after && after.includes("```")) {
117
- cleaned = after.split("```")[0].trim();
118
- break;
119
- }
120
- }
121
- }
122
- }
123
-
124
- // Find first JSON object or array
125
- let objStart = -1;
126
- let depth = 0;
127
- for (let i = 0; i < cleaned.length; i++) {
128
- const ch = cleaned[i];
129
- if (ch === "{") {
130
- if (objStart < 0) objStart = i;
131
- depth++;
132
- } else if (ch === "}") {
133
- depth--;
134
- if (depth === 0 && objStart >= 0) {
135
- return cleaned.substring(objStart, i + 1);
136
- }
137
- }
138
- }
139
-
140
- // If still not closed, close with extra braces
141
- if (objStart >= 0) {
142
- return cleaned.substring(objStart) + "}".repeat(depth);
143
- }
144
-
145
- throw new SchemaValidationError("No valid JSON found in response", text);
146
- }
147
-
148
- /**
149
- * Repair common JSON issues (trailing commas, unquoted keys, etc.)
150
- */
151
- function repairJSON(text: string): string {
152
- let repaired = text;
153
-
154
- // Remove trailing commas
155
- repaired = repaired.replace(/,\s*([}\]])/g, "$1");
156
-
157
- // Quote unquoted keys
158
- repaired = repaired.replace(/(?<!["\'\w])(\w[\w\d_]*)(\s*:)/g, '"$1"$2');
159
-
160
- // Normalize quotes
161
- repaired = repaired.replace(/'/g, '"').replace(/`/g, '"');
162
-
163
- return repaired;
164
- }
165
-
166
- /**
167
- * Parse a raw LLM response string into a typed JSON object.
168
- * Handles markdown fences, leading/trailing text, and minor JSON quirks.
169
- * Raises SchemaValidationError on failure.
170
- */
171
- export function parseSchema<T extends Record<string, unknown>>(
172
- raw: string,
173
- schemaType?: new () => T
174
- ): T {
175
- if (!raw || !raw.trim()) {
176
- throw new SchemaValidationError("empty response", raw);
177
- }
178
-
179
- try {
180
- const extracted = extractJSON(raw);
181
- let data = JSON.parse(extracted);
182
- return data as T;
183
- } catch (error) {
184
- if (error instanceof SchemaValidationError) throw error;
185
-
186
- try {
187
- const repaired = repairJSON(raw);
188
- const data = JSON.parse(repaired);
189
- return data as T;
190
- } catch (repairError) {
191
- throw new SchemaValidationError(
192
- `JSON parse failed: ${(error as Error).message}`,
193
- raw
194
- );
195
- }
196
- }
197
- }
198
-
199
- /**
200
- * Validate task plan schema
201
- */
202
- export function validateTaskPlan(data: unknown): TaskPlanSchema {
203
- if (!data || typeof data !== "object") {
204
- throw new SchemaValidationError("Invalid task plan: must be an object");
205
- }
206
-
207
- const plan = data as Record<string, unknown>;
208
-
209
- if (typeof plan.goal !== "string") {
210
- throw new SchemaValidationError("Invalid task plan: goal must be a string");
211
- }
212
-
213
- if (!Array.isArray(plan.steps)) {
214
- throw new SchemaValidationError("Invalid task plan: steps must be an array");
215
- }
216
-
217
- const steps = (plan.steps as unknown[]).map((step: unknown) => {
218
- if (!step || typeof step !== "object") {
219
- throw new SchemaValidationError("Invalid task plan: step must be an object");
220
- }
221
-
222
- const s = step as Record<string, unknown>;
223
- if (typeof s.id !== "string" && typeof s.id !== "number") {
224
- throw new SchemaValidationError("Invalid task plan: step.id must be string or number");
225
- }
226
-
227
- if (typeof s.description !== "string") {
228
- throw new SchemaValidationError("Invalid task plan: step.description must be a string");
229
- }
230
-
231
- return {
232
- id: s.id,
233
- description: s.description,
234
- agent: typeof s.agent === "string" ? s.agent : "rain",
235
- depends_on: Array.isArray(s.depends_on) ? (s.depends_on as string[]) : [],
236
- priority: (["low", "medium", "high"].includes(s.priority as string)
237
- ? s.priority
238
- : "medium") as "low" | "medium" | "high",
239
- };
240
- });
241
-
242
- return {
243
- goal: plan.goal,
244
- steps,
245
- };
246
- }
247
-
248
- /**
249
- * Validate extraction result schema
250
- */
251
- export function validateExtractionResult(data: unknown): ExtractionResultSchema {
252
- if (!data || typeof data !== "object") {
253
- throw new SchemaValidationError("Invalid extraction result: must be an object");
254
- }
255
-
256
- const result = data as Record<string, unknown>;
257
-
258
- if (!Array.isArray(result.facts)) {
259
- throw new SchemaValidationError("Invalid extraction result: facts must be an array");
260
- }
261
-
262
- const facts = (result.facts as unknown[]).map((fact: unknown) => {
263
- if (!fact || typeof fact !== "object") {
264
- throw new SchemaValidationError("Invalid extraction result: fact must be an object");
265
- }
266
-
267
- const f = fact as Record<string, unknown>;
268
- if (typeof f.key !== "string" || typeof f.value !== "string") {
269
- throw new SchemaValidationError(
270
- "Invalid extraction result: fact must have key and value strings"
271
- );
272
- }
273
-
274
- return {
275
- key: f.key,
276
- value: f.value,
277
- category: typeof f.category === "string" ? f.category : "auto_extracted",
278
- };
279
- });
280
-
281
- return { facts };
282
- }
1
+ /**
2
+ * Lightweight structured output schemas for LLM response validation.
3
+ *
4
+ * Why: LLM JSON output is inherently fragile — models emit markdown fences,
5
+ * trailing commas, unquoted keys, or hallucinated fields. Rather than layering
6
+ * heuristic repair (which silently passes corrupted data), we define typed
7
+ * schemas and validate on ingress. Parsing failures surface immediately so the
8
+ * caller can retry with a corrected prompt instead of propagating garbage.
9
+ *
10
+ * Zero external dependencies: uses only JSON and TypeScript types.
11
+ */
12
+
13
+ /**
14
+ * Error raised when an LLM response fails schema validation.
15
+ * Carries both a human-readable message and the raw text so callers
16
+ * can log / retry with full context.
17
+ */
18
+ export class SchemaValidationError extends Error {
19
+ raw: string;
20
+
21
+ constructor(message: string, raw: string = "") {
22
+ super(message);
23
+ this.name = "SchemaValidationError";
24
+ this.raw = raw;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * One step in a task plan (mirrors PipelineStep / Task)
30
+ */
31
+ export interface TaskStepSchema {
32
+ id: string | number;
33
+ description: string;
34
+ agent?: string;
35
+ depends_on?: string[];
36
+ priority?: "low" | "medium" | "high";
37
+ }
38
+
39
+ /**
40
+ * Full task plan output from Snow's orchestrator
41
+ */
42
+ export interface TaskPlanSchema {
43
+ goal: string;
44
+ steps: TaskStepSchema[];
45
+ }
46
+
47
+ /**
48
+ * A single extracted fact for long-term memory
49
+ */
50
+ export interface FactSchema {
51
+ key: string;
52
+ value: string;
53
+ category?: string;
54
+ }
55
+
56
+ /**
57
+ * Structured fact-extraction output from the LLM
58
+ */
59
+ export interface ExtractionResultSchema {
60
+ facts: FactSchema[];
61
+ }
62
+
63
+ /**
64
+ * Tool call schema for LLM responses
65
+ */
66
+ export interface ToolCallSchema {
67
+ name: string;
68
+ arguments: Record<string, unknown>;
69
+ }
70
+
71
+ /**
72
+ * Message schema for agent communication
73
+ */
74
+ export interface MessageSchema {
75
+ role: "user" | "assistant" | "tool";
76
+ content: string;
77
+ tool_calls?: ToolCallSchema[];
78
+ tool_call_id?: string;
79
+ }
80
+
81
+ /**
82
+ * Coerce a value to a target type with best-effort conversion
83
+ */
84
+ function coerceType(value: unknown, targetType: string): unknown {
85
+ if (value === null || value === undefined) {
86
+ return value;
87
+ }
88
+
89
+ switch (targetType) {
90
+ case "string":
91
+ return String(value);
92
+ case "number":
93
+ return Number(value);
94
+ case "boolean":
95
+ return Boolean(value);
96
+ case "array":
97
+ return Array.isArray(value) ? value : [];
98
+ case "object":
99
+ return typeof value === "object" ? value : {};
100
+ default:
101
+ return value;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Extract JSON object/array from a potentially malformed string
107
+ */
108
+ function extractJSON(text: string): string {
109
+ let cleaned = text.trim();
110
+
111
+ // Strip markdown code fences
112
+ if (cleaned.includes("```")) {
113
+ for (const fence of ["```json", "```"]) {
114
+ if (cleaned.includes(fence)) {
115
+ const after = cleaned.split(fence, 1)[1];
116
+ if (after && after.includes("```")) {
117
+ cleaned = after.split("```")[0].trim();
118
+ break;
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ // Find first JSON object or array
125
+ let objStart = -1;
126
+ let depth = 0;
127
+ for (let i = 0; i < cleaned.length; i++) {
128
+ const ch = cleaned[i];
129
+ if (ch === "{") {
130
+ if (objStart < 0) objStart = i;
131
+ depth++;
132
+ } else if (ch === "}") {
133
+ depth--;
134
+ if (depth === 0 && objStart >= 0) {
135
+ return cleaned.substring(objStart, i + 1);
136
+ }
137
+ }
138
+ }
139
+
140
+ // If still not closed, close with extra braces
141
+ if (objStart >= 0) {
142
+ return cleaned.substring(objStart) + "}".repeat(depth);
143
+ }
144
+
145
+ throw new SchemaValidationError("No valid JSON found in response", text);
146
+ }
147
+
148
+ /**
149
+ * Repair common JSON issues (trailing commas, unquoted keys, etc.)
150
+ */
151
+ function repairJSON(text: string): string {
152
+ let repaired = text;
153
+
154
+ // Remove trailing commas
155
+ repaired = repaired.replace(/,\s*([}\]])/g, "$1");
156
+
157
+ // Quote unquoted keys
158
+ repaired = repaired.replace(/(?<!["\'\w])(\w[\w\d_]*)(\s*:)/g, '"$1"$2');
159
+
160
+ // Normalize quotes
161
+ repaired = repaired.replace(/'/g, '"').replace(/`/g, '"');
162
+
163
+ return repaired;
164
+ }
165
+
166
+ /**
167
+ * Parse a raw LLM response string into a typed JSON object.
168
+ * Handles markdown fences, leading/trailing text, and minor JSON quirks.
169
+ * Raises SchemaValidationError on failure.
170
+ */
171
+ export function parseSchema<T extends Record<string, unknown>>(
172
+ raw: string,
173
+ schemaType?: new () => T
174
+ ): T {
175
+ if (!raw || !raw.trim()) {
176
+ throw new SchemaValidationError("empty response", raw);
177
+ }
178
+
179
+ try {
180
+ const extracted = extractJSON(raw);
181
+ let data = JSON.parse(extracted);
182
+ return data as T;
183
+ } catch (error) {
184
+ if (error instanceof SchemaValidationError) throw error;
185
+
186
+ try {
187
+ const repaired = repairJSON(raw);
188
+ const data = JSON.parse(repaired);
189
+ return data as T;
190
+ } catch (repairError) {
191
+ throw new SchemaValidationError(
192
+ `JSON parse failed: ${(error as Error).message}`,
193
+ raw
194
+ );
195
+ }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Validate task plan schema
201
+ */
202
+ export function validateTaskPlan(data: unknown): TaskPlanSchema {
203
+ if (!data || typeof data !== "object") {
204
+ throw new SchemaValidationError("Invalid task plan: must be an object");
205
+ }
206
+
207
+ const plan = data as Record<string, unknown>;
208
+
209
+ if (typeof plan.goal !== "string") {
210
+ throw new SchemaValidationError("Invalid task plan: goal must be a string");
211
+ }
212
+
213
+ if (!Array.isArray(plan.steps)) {
214
+ throw new SchemaValidationError("Invalid task plan: steps must be an array");
215
+ }
216
+
217
+ const steps = (plan.steps as unknown[]).map((step: unknown) => {
218
+ if (!step || typeof step !== "object") {
219
+ throw new SchemaValidationError("Invalid task plan: step must be an object");
220
+ }
221
+
222
+ const s = step as Record<string, unknown>;
223
+ if (typeof s.id !== "string" && typeof s.id !== "number") {
224
+ throw new SchemaValidationError("Invalid task plan: step.id must be string or number");
225
+ }
226
+
227
+ if (typeof s.description !== "string") {
228
+ throw new SchemaValidationError("Invalid task plan: step.description must be a string");
229
+ }
230
+
231
+ return {
232
+ id: s.id,
233
+ description: s.description,
234
+ agent: typeof s.agent === "string" ? s.agent : "rain",
235
+ depends_on: Array.isArray(s.depends_on) ? (s.depends_on as string[]) : [],
236
+ priority: (["low", "medium", "high"].includes(s.priority as string)
237
+ ? s.priority
238
+ : "medium") as "low" | "medium" | "high",
239
+ };
240
+ });
241
+
242
+ return {
243
+ goal: plan.goal,
244
+ steps,
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Validate extraction result schema
250
+ */
251
+ export function validateExtractionResult(data: unknown): ExtractionResultSchema {
252
+ if (!data || typeof data !== "object") {
253
+ throw new SchemaValidationError("Invalid extraction result: must be an object");
254
+ }
255
+
256
+ const result = data as Record<string, unknown>;
257
+
258
+ if (!Array.isArray(result.facts)) {
259
+ throw new SchemaValidationError("Invalid extraction result: facts must be an array");
260
+ }
261
+
262
+ const facts = (result.facts as unknown[]).map((fact: unknown) => {
263
+ if (!fact || typeof fact !== "object") {
264
+ throw new SchemaValidationError("Invalid extraction result: fact must be an object");
265
+ }
266
+
267
+ const f = fact as Record<string, unknown>;
268
+ if (typeof f.key !== "string" || typeof f.value !== "string") {
269
+ throw new SchemaValidationError(
270
+ "Invalid extraction result: fact must have key and value strings"
271
+ );
272
+ }
273
+
274
+ return {
275
+ key: f.key,
276
+ value: f.value,
277
+ category: typeof f.category === "string" ? f.category : "auto_extracted",
278
+ };
279
+ });
280
+
281
+ return { facts };
282
+ }
283
+
284
+ /* ════════════════════════════════════════
285
+ Structured-output retry
286
+ ════════════════════════════════════════ */
287
+
288
+ /**
289
+ * Ask an LLM for structured output and validate it, retrying with the parse
290
+ * error fed back as a correction when it comes back malformed — the pattern
291
+ * every production agent framework uses (LangGraph structured output, Pydantic
292
+ * AI, OpenAI Agents SDK output types). Without it, a single bad JSON response
293
+ * silently degrades the run (e.g. a multi-step plan collapses to one task).
294
+ *
295
+ * `ask(priorError, attempt)` produces the raw model text; on a retry it receives
296
+ * the previous validation error so the caller can append a correction to the
297
+ * prompt. `parse` must THROW (ideally SchemaValidationError) on invalid input.
298
+ * Returns the first valid value, or throws after exhausting `retries`.
299
+ */
300
+ export async function parseWithRetry<T>(
301
+ ask: (priorError: string | null, attempt: number) => Promise<string>,
302
+ parse: (raw: string) => T,
303
+ opts?: { retries?: number; onRetry?: (attempt: number, error: string) => void },
304
+ ): Promise<T> {
305
+ const retries = Math.max(0, opts?.retries ?? 2);
306
+ let lastError = "";
307
+ for (let attempt = 0; attempt <= retries; attempt++) {
308
+ const raw = await ask(attempt === 0 ? null : lastError, attempt);
309
+ try {
310
+ return parse(raw);
311
+ } catch (e) {
312
+ lastError = e instanceof Error ? e.message : String(e);
313
+ if (attempt < retries) opts?.onRetry?.(attempt + 1, lastError);
314
+ }
315
+ }
316
+ throw new SchemaValidationError(
317
+ `structured output still invalid after ${retries + 1} attempt(s): ${lastError}`,
318
+ );
319
+ }
@@ -59,11 +59,34 @@ const TOOL_DANGER_MAP: Record<string, DangerLevel> = {
59
59
  list_skills: DangerLevel.SAFE,
60
60
  recall_facts: DangerLevel.SAFE,
61
61
  mcp_list_servers: DangerLevel.SAFE,
62
+ // read-only introspection (extra.ts)
63
+ file_info: DangerLevel.SAFE,
64
+ hash: DangerLevel.SAFE,
65
+ base64: DangerLevel.SAFE,
66
+ json_query: DangerLevel.SAFE,
67
+ dns_lookup: DangerLevel.SAFE,
68
+ port_check: DangerLevel.SAFE,
69
+ env_get: DangerLevel.SAFE,
70
+ disk_usage: DangerLevel.SAFE,
71
+ clipboard_read: DangerLevel.SAFE,
72
+ which: DangerLevel.SAFE,
73
+ diff_files: DangerLevel.SAFE,
74
+ uuid: DangerLevel.SAFE,
75
+ random_string: DangerLevel.SAFE,
76
+ current_time: DangerLevel.SAFE,
62
77
 
63
78
  write_file: DangerLevel.LOW,
64
79
  edit_file: DangerLevel.LOW,
65
80
  copy_file: DangerLevel.LOW,
66
81
  move_file: DangerLevel.LOW,
82
+ make_directory: DangerLevel.LOW,
83
+ append_file: DangerLevel.LOW,
84
+ replace_in_file: DangerLevel.LOW,
85
+ sqlite_query: DangerLevel.LOW,
86
+ gzip_file: DangerLevel.LOW,
87
+ gunzip_file: DangerLevel.LOW,
88
+ clipboard_write: DangerLevel.LOW,
89
+ git_branch: DangerLevel.LOW,
67
90
  http_get: DangerLevel.LOW,
68
91
  fetch_page: DangerLevel.LOW,
69
92
  web_search: DangerLevel.LOW,
@@ -76,6 +99,8 @@ const TOOL_DANGER_MAP: Record<string, DangerLevel> = {
76
99
  git_commit: DangerLevel.MEDIUM,
77
100
  git_checkout: DangerLevel.MEDIUM,
78
101
  http_post: DangerLevel.MEDIUM,
102
+ http_request: DangerLevel.MEDIUM,
103
+ download_file: DangerLevel.MEDIUM,
79
104
  mcp_add_server: DangerLevel.MEDIUM,
80
105
  mcp_remove_server: DangerLevel.MEDIUM,
81
106
  launch_app: DangerLevel.MEDIUM,
@@ -88,6 +113,8 @@ const TOOL_DANGER_MAP: Record<string, DangerLevel> = {
88
113
  package_manager: DangerLevel.HIGH,
89
114
  service_control: DangerLevel.HIGH,
90
115
  delegate_to: DangerLevel.HIGH,
116
+ git_push: DangerLevel.HIGH,
117
+ git_pull: DangerLevel.HIGH,
91
118
  mcp_scaffold_server: DangerLevel.HIGH,
92
119
  };
93
120