skyloom 1.13.6 → 1.13.8

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 (193) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +220 -159
  3. package/config/providers.yaml +39 -39
  4. package/config/skills/api_integrator/SKILL.md +15 -15
  5. package/config/skills/arch_designer/SKILL.md +13 -13
  6. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  7. package/config/skills/code_analysis/SKILL.md +13 -13
  8. package/config/skills/code_generator/SKILL.md +12 -12
  9. package/config/skills/code_reviewer/SKILL.md +13 -13
  10. package/config/skills/content_writer/SKILL.md +14 -14
  11. package/config/skills/data_transformer/SKILL.md +15 -15
  12. package/config/skills/document_analysis/SKILL.md +13 -13
  13. package/config/skills/emotional_companion/SKILL.md +15 -15
  14. package/config/skills/performance_checker/SKILL.md +14 -14
  15. package/config/skills/security_auditor/SKILL.md +14 -14
  16. package/config/skills/self_evolve/SKILL.md +13 -13
  17. package/config/skills/sys_operator/SKILL.md +15 -15
  18. package/config/skills/task_planner/SKILL.md +14 -14
  19. package/config/skills/web_research/SKILL.md +14 -14
  20. package/config/skills/workflow_designer/SKILL.md +13 -13
  21. package/dist/agents/dew.js +52 -52
  22. package/dist/agents/fair.js +84 -84
  23. package/dist/agents/fog.js +30 -30
  24. package/dist/agents/frost.js +32 -32
  25. package/dist/agents/rain.js +32 -32
  26. package/dist/agents/snow.js +68 -68
  27. package/dist/cli/commands_md.d.ts +41 -0
  28. package/dist/cli/commands_md.d.ts.map +1 -0
  29. package/dist/cli/commands_md.js +140 -0
  30. package/dist/cli/commands_md.js.map +1 -0
  31. package/dist/cli/input_macros.d.ts +28 -0
  32. package/dist/cli/input_macros.d.ts.map +1 -0
  33. package/dist/cli/input_macros.js +120 -0
  34. package/dist/cli/input_macros.js.map +1 -0
  35. package/dist/cli/loom.d.ts +220 -0
  36. package/dist/cli/loom.d.ts.map +1 -0
  37. package/dist/cli/loom.js +1094 -0
  38. package/dist/cli/loom.js.map +1 -0
  39. package/dist/cli/loom_chat.d.ts +20 -0
  40. package/dist/cli/loom_chat.d.ts.map +1 -0
  41. package/dist/cli/loom_chat.js +685 -0
  42. package/dist/cli/loom_chat.js.map +1 -0
  43. package/dist/cli/main.js +310 -14
  44. package/dist/cli/main.js.map +1 -1
  45. package/dist/cli/tui.d.ts.map +1 -1
  46. package/dist/cli/tui.js +7 -1
  47. package/dist/cli/tui.js.map +1 -1
  48. package/dist/core/agent.d.ts +20 -0
  49. package/dist/core/agent.d.ts.map +1 -1
  50. package/dist/core/agent.js +199 -16
  51. package/dist/core/agent.js.map +1 -1
  52. package/dist/core/factory.d.ts.map +1 -1
  53. package/dist/core/factory.js +34 -2
  54. package/dist/core/factory.js.map +1 -1
  55. package/dist/core/file_checkpoint.d.ts +57 -0
  56. package/dist/core/file_checkpoint.d.ts.map +1 -0
  57. package/dist/core/file_checkpoint.js +162 -0
  58. package/dist/core/file_checkpoint.js.map +1 -0
  59. package/dist/core/hooks.d.ts +43 -0
  60. package/dist/core/hooks.d.ts.map +1 -0
  61. package/dist/core/hooks.js +110 -0
  62. package/dist/core/hooks.js.map +1 -0
  63. package/dist/core/llm.d.ts.map +1 -1
  64. package/dist/core/llm.js +15 -9
  65. package/dist/core/llm.js.map +1 -1
  66. package/dist/core/longdoc.js +5 -5
  67. package/dist/core/mcp.d.ts +16 -0
  68. package/dist/core/mcp.d.ts.map +1 -1
  69. package/dist/core/mcp.js +55 -0
  70. package/dist/core/mcp.js.map +1 -1
  71. package/dist/core/model_config.d.ts +40 -0
  72. package/dist/core/model_config.d.ts.map +1 -0
  73. package/dist/core/model_config.js +191 -0
  74. package/dist/core/model_config.js.map +1 -0
  75. package/dist/core/skill.d.ts +7 -0
  76. package/dist/core/skill.d.ts.map +1 -1
  77. package/dist/core/skill.js +47 -0
  78. package/dist/core/skill.js.map +1 -1
  79. package/dist/core/skymd.d.ts +39 -0
  80. package/dist/core/skymd.d.ts.map +1 -0
  81. package/dist/core/skymd.js +177 -0
  82. package/dist/core/skymd.js.map +1 -0
  83. package/dist/core/tool.d.ts +12 -0
  84. package/dist/core/tool.d.ts.map +1 -1
  85. package/dist/core/tool.js +30 -0
  86. package/dist/core/tool.js.map +1 -1
  87. package/dist/core/verify.d.ts +27 -0
  88. package/dist/core/verify.d.ts.map +1 -0
  89. package/dist/core/verify.js +62 -0
  90. package/dist/core/verify.js.map +1 -0
  91. package/dist/skills/loader.d.ts +22 -2
  92. package/dist/skills/loader.d.ts.map +1 -1
  93. package/dist/skills/loader.js +45 -15
  94. package/dist/skills/loader.js.map +1 -1
  95. package/dist/tools/builtin.d.ts.map +1 -1
  96. package/dist/tools/builtin.js +13 -3
  97. package/dist/tools/builtin.js.map +1 -1
  98. package/dist/tools/model_tool.d.ts +11 -0
  99. package/dist/tools/model_tool.d.ts.map +1 -0
  100. package/dist/tools/model_tool.js +71 -0
  101. package/dist/tools/model_tool.js.map +1 -0
  102. package/dist/tools/todo.d.ts +30 -0
  103. package/dist/tools/todo.d.ts.map +1 -0
  104. package/dist/tools/todo.js +78 -0
  105. package/dist/tools/todo.js.map +1 -0
  106. package/docs/AESTHETIC_DESIGN.md +152 -144
  107. package/docs/OPTIMIZATION_PLAN.md +178 -178
  108. package/package.json +68 -68
  109. package/scripts/install.js +48 -48
  110. package/scripts/link.js +10 -10
  111. package/setup.bat +79 -79
  112. package/skill-test-ty2fOA/test.md +10 -10
  113. package/src/agents/dew.ts +70 -70
  114. package/src/agents/fair.ts +102 -102
  115. package/src/agents/fog.ts +48 -48
  116. package/src/agents/frost.ts +50 -50
  117. package/src/agents/rain.ts +50 -50
  118. package/src/agents/snow.ts +239 -239
  119. package/src/cli/commands_md.ts +112 -0
  120. package/src/cli/input_macros.ts +83 -0
  121. package/src/cli/loom.ts +982 -0
  122. package/src/cli/loom_chat.ts +598 -0
  123. package/src/cli/main.ts +255 -9
  124. package/src/cli/mode.ts +58 -58
  125. package/src/cli/tui.ts +228 -222
  126. package/src/core/agent/guard.ts +134 -134
  127. package/src/core/agent/task.ts +100 -100
  128. package/src/core/agent.ts +195 -16
  129. package/src/core/arbitrate.ts +162 -162
  130. package/src/core/catalog.ts +178 -178
  131. package/src/core/checkpoint.ts +94 -94
  132. package/src/core/estimate.ts +104 -104
  133. package/src/core/evolve.ts +191 -191
  134. package/src/core/factory.ts +31 -2
  135. package/src/core/file_checkpoint.ts +136 -0
  136. package/src/core/filter.ts +103 -103
  137. package/src/core/graph.ts +156 -156
  138. package/src/core/hooks.ts +126 -0
  139. package/src/core/icons.ts +53 -53
  140. package/src/core/index.ts +37 -37
  141. package/src/core/learn.ts +146 -146
  142. package/src/core/llm.ts +15 -9
  143. package/src/core/longdoc.ts +155 -155
  144. package/src/core/mcp.ts +48 -0
  145. package/src/core/mcp_server.ts +176 -176
  146. package/src/core/model_config.ts +157 -0
  147. package/src/core/profile.ts +255 -255
  148. package/src/core/router.ts +124 -124
  149. package/src/core/sandbox.ts +142 -142
  150. package/src/core/security.ts +243 -243
  151. package/src/core/skill.ts +42 -0
  152. package/src/core/skymd.ts +143 -0
  153. package/src/core/theme.ts +65 -65
  154. package/src/core/tool.ts +30 -0
  155. package/src/core/tool_router.ts +193 -193
  156. package/src/core/vector.ts +152 -152
  157. package/src/core/verify.ts +71 -0
  158. package/src/core/workspace.ts +150 -150
  159. package/src/plugins/loader.ts +66 -66
  160. package/src/skills/loader.ts +45 -16
  161. package/src/sql.js.d.ts +29 -29
  162. package/src/tools/builtin.ts +13 -3
  163. package/src/tools/computer.ts +269 -269
  164. package/src/tools/delegate.ts +49 -49
  165. package/src/tools/model_tool.ts +74 -0
  166. package/src/tools/todo.ts +76 -0
  167. package/src/web/tts.ts +93 -93
  168. package/tests/agent.test.ts +159 -159
  169. package/tests/agent_helpers.test.ts +48 -48
  170. package/tests/bus.test.ts +121 -121
  171. package/tests/catalog.test.ts +86 -86
  172. package/tests/checkpoint_commands.test.ts +124 -0
  173. package/tests/claude_compat.test.ts +110 -0
  174. package/tests/config.test.ts +41 -41
  175. package/tests/guard.test.ts +75 -75
  176. package/tests/icons.test.ts +45 -45
  177. package/tests/loom.test.ts +248 -0
  178. package/tests/memory.test.ts +170 -170
  179. package/tests/model_config.test.ts +109 -0
  180. package/tests/router.test.ts +86 -86
  181. package/tests/schemas.test.ts +51 -51
  182. package/tests/semantic.test.ts +83 -83
  183. package/tests/setup.ts +10 -10
  184. package/tests/skill.test.ts +172 -172
  185. package/tests/skymd.test.ts +146 -0
  186. package/tests/task.test.ts +60 -60
  187. package/tests/todo_toolstats.test.ts +94 -0
  188. package/tests/tool.test.ts +108 -108
  189. package/tests/tool_router.test.ts +71 -71
  190. package/tests/tui.test.ts +67 -67
  191. package/vitest.config.ts +17 -17
  192. package/=12 +0 -0
  193. package/=8 +0 -0
@@ -1,152 +1,152 @@
1
- /**
2
- * 向量语义搜索 — TF-IDF + Cosine similarity, zero dependencies.
3
- *
4
- * Replaces n-gram Jaccard as the default semantic scorer.
5
- * - IDF pre-computed from document corpus
6
- * - Cosine similarity on TF-IDF vectors
7
- * - CJK-aware tokenization (bigram for CJK, whitespace for ASCII)
8
- *
9
- * Usage:
10
- * const idx = new VectorIndex();
11
- * idx.addDocuments(docs);
12
- * const results = idx.search("deploy script", 5);
13
- */
14
-
15
- /* ═══════════════════════════════════════
16
- Tokenizer — CJK-aware
17
- ═══════════════════════════════════════ */
18
- const CJK = /[一-鿿぀-ゟ가-힯]/;
19
-
20
- function tokenize(text: string): string[] {
21
- const tokens: string[] = [];
22
- let i = 0;
23
- while (i < text.length) {
24
- if (CJK.test(text[i])) {
25
- if (i + 1 < text.length && CJK.test(text[i + 1])) {
26
- tokens.push(text.slice(i, i + 2)); i += 2;
27
- } else {
28
- tokens.push(text[i]); i++;
29
- }
30
- } else if (/[A-Za-z0-9_]/.test(text[i])) {
31
- let j = i;
32
- while (j < text.length && /[A-Za-z0-9_]/.test(text[j])) j++;
33
- tokens.push(text.slice(i, j).toLowerCase()); i = j;
34
- } else {
35
- i++;
36
- }
37
- }
38
- return tokens;
39
- }
40
-
41
- /* ═══════════════════════════════════════
42
- TF-IDF Vector computation
43
- ═══════════════════════════════════════ */
44
- interface DocVector {
45
- id: string;
46
- tf: Map<string, number>;
47
- norm: number;
48
- content: string;
49
- meta?: Record<string, any>;
50
- }
51
-
52
- export class VectorIndex {
53
- private docs: DocVector[] = [];
54
- private idf: Map<string, number> = new Map();
55
- private totalDocs = 0;
56
-
57
- /** Add a document to the index. */
58
- addDocument(id: string, content: string, meta?: Record<string, any>): void {
59
- const tokens = tokenize(content);
60
- const tf = new Map<string, number>();
61
- for (const t of tokens) { tf.set(t, (tf.get(t) || 0) + 1); }
62
-
63
- // Normalize by doc length
64
- const tfIdf = new Map<string, number>();
65
- let normSq = 0;
66
- for (const [term, freq] of tf) {
67
- const tfVal = freq / tokens.length;
68
- const idfVal = this.idf.get(term) || 0;
69
- const val = tfVal * Math.max(0.1, idfVal);
70
- tfIdf.set(term, val);
71
- normSq += val * val;
72
- }
73
-
74
- const norm = Math.sqrt(normSq);
75
- this.docs.push({ id, tf: tfIdf, norm, content: content.slice(0, 500), meta });
76
- this.totalDocs++;
77
-
78
- // Update IDF
79
- for (const term of tf.keys()) {
80
- this.idf.set(term, Math.log((this.totalDocs + 1) / ((this.docFrequency(term) + 1))));
81
- }
82
- }
83
-
84
- addDocuments(docs: Array<{ id: string; content: string; meta?: Record<string, any> }>): void {
85
- for (const d of docs) this.addDocument(d.id, d.content, d.meta);
86
- }
87
-
88
- private docFrequency(term: string): number {
89
- let count = 0;
90
- for (const d of this.docs) { if (d.tf.has(term)) count++; }
91
- return count;
92
- }
93
-
94
- /** Search for documents similar to query. Returns [score, doc] pairs. */
95
- search(query: string, topK: number = 5, minScore: number = 0.01): Array<[number, DocVector]> {
96
- const queryTokens = tokenize(query);
97
- const queryTf = new Map<string, number>();
98
- for (const t of queryTokens) { queryTf.set(t, (queryTf.get(t) || 0) + 1); }
99
-
100
- // Query vector
101
- const qv = new Map<string, number>();
102
- let qNormSq = 0;
103
- for (const [term, freq] of queryTf) {
104
- const tfVal = freq / queryTokens.length;
105
- const idfVal = this.idf.get(term) || 0;
106
- const val = tfVal * Math.max(0.1, idfVal);
107
- qv.set(term, val);
108
- qNormSq += val * val;
109
- }
110
- const qNorm = Math.sqrt(qNormSq);
111
- if (qNorm === 0) return [];
112
-
113
- // Cosine similarity against all docs
114
- const scored: Array<[number, DocVector]> = [];
115
- for (const doc of this.docs) {
116
- if (doc.norm === 0) continue;
117
- let dot = 0;
118
- for (const [term, qVal] of qv) {
119
- dot += qVal * (doc.tf.get(term) || 0);
120
- }
121
- const score = dot / (qNorm * doc.norm);
122
- if (score >= minScore) scored.push([score, doc]);
123
- }
124
-
125
- scored.sort((a, b) => b[0] - a[0]);
126
- return scored.slice(0, topK);
127
- }
128
-
129
- /** Remove a document by ID. */
130
- removeDocument(id: string): void {
131
- this.docs = this.docs.filter(d => d.id !== id);
132
- this.totalDocs = this.docs.length;
133
- }
134
-
135
- get size(): number { return this.docs.length; }
136
-
137
- clear(): void { this.docs = []; this.idf.clear(); this.totalDocs = 0; }
138
- }
139
-
140
- /* ═══════════════════════════════════════
141
- Singleton instance for memory recall
142
- ═══════════════════════════════════════ */
143
- let globalIndex: VectorIndex | null = null;
144
-
145
- export function getVectorIndex(): VectorIndex {
146
- if (!globalIndex) globalIndex = new VectorIndex();
147
- return globalIndex;
148
- }
149
-
150
- export function resetVectorIndex(): void {
151
- globalIndex = new VectorIndex();
152
- }
1
+ /**
2
+ * 向量语义搜索 — TF-IDF + Cosine similarity, zero dependencies.
3
+ *
4
+ * Replaces n-gram Jaccard as the default semantic scorer.
5
+ * - IDF pre-computed from document corpus
6
+ * - Cosine similarity on TF-IDF vectors
7
+ * - CJK-aware tokenization (bigram for CJK, whitespace for ASCII)
8
+ *
9
+ * Usage:
10
+ * const idx = new VectorIndex();
11
+ * idx.addDocuments(docs);
12
+ * const results = idx.search("deploy script", 5);
13
+ */
14
+
15
+ /* ═══════════════════════════════════════
16
+ Tokenizer — CJK-aware
17
+ ═══════════════════════════════════════ */
18
+ const CJK = /[一-鿿぀-ゟ가-힯]/;
19
+
20
+ function tokenize(text: string): string[] {
21
+ const tokens: string[] = [];
22
+ let i = 0;
23
+ while (i < text.length) {
24
+ if (CJK.test(text[i])) {
25
+ if (i + 1 < text.length && CJK.test(text[i + 1])) {
26
+ tokens.push(text.slice(i, i + 2)); i += 2;
27
+ } else {
28
+ tokens.push(text[i]); i++;
29
+ }
30
+ } else if (/[A-Za-z0-9_]/.test(text[i])) {
31
+ let j = i;
32
+ while (j < text.length && /[A-Za-z0-9_]/.test(text[j])) j++;
33
+ tokens.push(text.slice(i, j).toLowerCase()); i = j;
34
+ } else {
35
+ i++;
36
+ }
37
+ }
38
+ return tokens;
39
+ }
40
+
41
+ /* ═══════════════════════════════════════
42
+ TF-IDF Vector computation
43
+ ═══════════════════════════════════════ */
44
+ interface DocVector {
45
+ id: string;
46
+ tf: Map<string, number>;
47
+ norm: number;
48
+ content: string;
49
+ meta?: Record<string, any>;
50
+ }
51
+
52
+ export class VectorIndex {
53
+ private docs: DocVector[] = [];
54
+ private idf: Map<string, number> = new Map();
55
+ private totalDocs = 0;
56
+
57
+ /** Add a document to the index. */
58
+ addDocument(id: string, content: string, meta?: Record<string, any>): void {
59
+ const tokens = tokenize(content);
60
+ const tf = new Map<string, number>();
61
+ for (const t of tokens) { tf.set(t, (tf.get(t) || 0) + 1); }
62
+
63
+ // Normalize by doc length
64
+ const tfIdf = new Map<string, number>();
65
+ let normSq = 0;
66
+ for (const [term, freq] of tf) {
67
+ const tfVal = freq / tokens.length;
68
+ const idfVal = this.idf.get(term) || 0;
69
+ const val = tfVal * Math.max(0.1, idfVal);
70
+ tfIdf.set(term, val);
71
+ normSq += val * val;
72
+ }
73
+
74
+ const norm = Math.sqrt(normSq);
75
+ this.docs.push({ id, tf: tfIdf, norm, content: content.slice(0, 500), meta });
76
+ this.totalDocs++;
77
+
78
+ // Update IDF
79
+ for (const term of tf.keys()) {
80
+ this.idf.set(term, Math.log((this.totalDocs + 1) / ((this.docFrequency(term) + 1))));
81
+ }
82
+ }
83
+
84
+ addDocuments(docs: Array<{ id: string; content: string; meta?: Record<string, any> }>): void {
85
+ for (const d of docs) this.addDocument(d.id, d.content, d.meta);
86
+ }
87
+
88
+ private docFrequency(term: string): number {
89
+ let count = 0;
90
+ for (const d of this.docs) { if (d.tf.has(term)) count++; }
91
+ return count;
92
+ }
93
+
94
+ /** Search for documents similar to query. Returns [score, doc] pairs. */
95
+ search(query: string, topK: number = 5, minScore: number = 0.01): Array<[number, DocVector]> {
96
+ const queryTokens = tokenize(query);
97
+ const queryTf = new Map<string, number>();
98
+ for (const t of queryTokens) { queryTf.set(t, (queryTf.get(t) || 0) + 1); }
99
+
100
+ // Query vector
101
+ const qv = new Map<string, number>();
102
+ let qNormSq = 0;
103
+ for (const [term, freq] of queryTf) {
104
+ const tfVal = freq / queryTokens.length;
105
+ const idfVal = this.idf.get(term) || 0;
106
+ const val = tfVal * Math.max(0.1, idfVal);
107
+ qv.set(term, val);
108
+ qNormSq += val * val;
109
+ }
110
+ const qNorm = Math.sqrt(qNormSq);
111
+ if (qNorm === 0) return [];
112
+
113
+ // Cosine similarity against all docs
114
+ const scored: Array<[number, DocVector]> = [];
115
+ for (const doc of this.docs) {
116
+ if (doc.norm === 0) continue;
117
+ let dot = 0;
118
+ for (const [term, qVal] of qv) {
119
+ dot += qVal * (doc.tf.get(term) || 0);
120
+ }
121
+ const score = dot / (qNorm * doc.norm);
122
+ if (score >= minScore) scored.push([score, doc]);
123
+ }
124
+
125
+ scored.sort((a, b) => b[0] - a[0]);
126
+ return scored.slice(0, topK);
127
+ }
128
+
129
+ /** Remove a document by ID. */
130
+ removeDocument(id: string): void {
131
+ this.docs = this.docs.filter(d => d.id !== id);
132
+ this.totalDocs = this.docs.length;
133
+ }
134
+
135
+ get size(): number { return this.docs.length; }
136
+
137
+ clear(): void { this.docs = []; this.idf.clear(); this.totalDocs = 0; }
138
+ }
139
+
140
+ /* ═══════════════════════════════════════
141
+ Singleton instance for memory recall
142
+ ═══════════════════════════════════════ */
143
+ let globalIndex: VectorIndex | null = null;
144
+
145
+ export function getVectorIndex(): VectorIndex {
146
+ if (!globalIndex) globalIndex = new VectorIndex();
147
+ return globalIndex;
148
+ }
149
+
150
+ export function resetVectorIndex(): void {
151
+ globalIndex = new VectorIndex();
152
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * 验证闭环 — give agents a way to check their own work.
3
+ *
4
+ * After a task that wrote files, the configured verify commands (tests,
5
+ * type-check, lint) run automatically; failures are fed back to the agent
6
+ * for a bounded number of fix rounds. Mirrors Claude Code's first best
7
+ * practice: output quality is transformed when the model can self-verify.
8
+ *
9
+ * Command sources (first non-empty wins):
10
+ * 1. config.yaml: verify: { commands: [...], max_fix_rounds, timeout_s }
11
+ * 2. SKY.md "## Verify" section's fenced code block (see skymd.ts)
12
+ */
13
+
14
+ import { spawnSync } from 'child_process';
15
+ import { loadProjectMemory, parseVerifyCommands } from './skymd';
16
+
17
+ export interface VerifyConfig {
18
+ commands: string[];
19
+ maxFixRounds: number;
20
+ timeoutS: number;
21
+ }
22
+
23
+ export interface VerifyResult {
24
+ ok: boolean;
25
+ /** Human/agent readable report (per-command status + failure tails). */
26
+ report: string;
27
+ }
28
+
29
+ const OUTPUT_TAIL = 4000;
30
+
31
+ /** Resolve verify settings from config, falling back to SKY.md. */
32
+ export function resolveVerifyConfig(config: any, cwd: string = process.cwd()): VerifyConfig {
33
+ const v: any = config?.verify || {};
34
+ let commands: string[] = Array.isArray(v.commands) ? v.commands.filter((c: any) => typeof c === 'string' && c.trim()) : [];
35
+ if (commands.length === 0) {
36
+ try {
37
+ commands = parseVerifyCommands(loadProjectMemory(cwd).text);
38
+ } catch {
39
+ commands = [];
40
+ }
41
+ }
42
+ return {
43
+ commands,
44
+ maxFixRounds: typeof v.max_fix_rounds === 'number' ? v.max_fix_rounds : 2,
45
+ timeoutS: typeof v.timeout_s === 'number' ? v.timeout_s : 300,
46
+ };
47
+ }
48
+
49
+ /** Run all verify commands sequentially; stop at the first failure. */
50
+ export function runVerify(cfg: VerifyConfig, cwd: string = process.cwd()): VerifyResult {
51
+ const lines: string[] = [];
52
+ for (const cmd of cfg.commands) {
53
+ const r = spawnSync(cmd, {
54
+ shell: true,
55
+ cwd,
56
+ encoding: 'utf-8',
57
+ timeout: cfg.timeoutS * 1000,
58
+ env: { ...process.env, CI: '1', FORCE_COLOR: '0' },
59
+ });
60
+ const out = `${r.stdout || ''}${r.stderr || ''}`.trim();
61
+ if (r.status === 0) {
62
+ lines.push(`✓ ${cmd}`);
63
+ } else {
64
+ const tail = out.length > OUTPUT_TAIL ? '…' + out.slice(-OUTPUT_TAIL) : out;
65
+ const why = r.error ? String(r.error) : `exit ${r.status}`;
66
+ lines.push(`✗ ${cmd} (${why})\n${tail}`);
67
+ return { ok: false, report: lines.join('\n') };
68
+ }
69
+ }
70
+ return { ok: true, report: lines.join('\n') };
71
+ }
@@ -1,150 +1,150 @@
1
- /**
2
- * Workspace auto-detection and lifecycle management.
3
- *
4
- * Rules:
5
- * - First launch: auto-select best drive, create workspace/
6
- * - Multi-drive: skip C:, pick drive with most free space
7
- * - Single drive (C: only): use C:\\workspace
8
- * - Unix: use ~/workspace
9
- * - User can override via config workspace.path
10
- */
11
-
12
- import * as fs from 'fs';
13
- import * as os from 'os';
14
- import * as path from 'path';
15
-
16
- const WORKSPACE_SUBDIRS = ['files', 'output', 'temp'];
17
-
18
- export interface DriveInfo {
19
- letter: string;
20
- path: string;
21
- totalBytes: number;
22
- freeBytes: number;
23
- }
24
-
25
- /**
26
- * Enumerate fixed drives with free-space info.
27
- */
28
- function getDriveList(): DriveInfo[] {
29
- const drives: DriveInfo[] = [];
30
-
31
- if (process.platform === 'win32') {
32
- for (let i = 0; i < 26; i++) {
33
- const letter = String.fromCharCode(65 + i); // A-Z
34
- const root = `${letter}:\\`;
35
- if (!fs.existsSync(root)) continue;
36
- try {
37
- const stat = fs.statfsSync(root);
38
- drives.push({
39
- letter,
40
- path: root,
41
- totalBytes: stat.blocks * stat.bsize,
42
- freeBytes: stat.bfree * stat.bsize,
43
- });
44
- } catch {
45
- try {
46
- // Fallback: just mark it available
47
- drives.push({
48
- letter,
49
- path: root,
50
- totalBytes: 0,
51
- freeBytes: 0,
52
- });
53
- } catch {
54
- continue;
55
- }
56
- }
57
- }
58
- } else {
59
- // Unix — treat / as the single candidate
60
- try {
61
- const stat = fs.statfsSync('/');
62
- drives.push({
63
- letter: '',
64
- path: '/',
65
- totalBytes: stat.blocks * stat.bsize,
66
- freeBytes: stat.bfree * stat.bsize,
67
- });
68
- } catch {
69
- // Ignore
70
- }
71
- }
72
-
73
- return drives;
74
- }
75
-
76
- /**
77
- * Pick the best drive for the workspace directory.
78
- *
79
- * - Windows with multiple drives: skip C:, pick drive with most free bytes.
80
- * - Windows with only C: → C:\\workspace.
81
- * - Unix → ~/workspace.
82
- */
83
- export function detectBestWorkspaceRoot(): string {
84
- const drives = getDriveList();
85
-
86
- if (process.platform === 'win32') {
87
- let candidates = drives.filter((d) => d.letter.toUpperCase() !== 'C');
88
- if (candidates.length === 0) {
89
- candidates = drives; // fallback to C:
90
- }
91
- // Sort by free space descending
92
- candidates.sort((a, b) => b.freeBytes - a.freeBytes);
93
- const best = candidates[0];
94
- return path.join(best.path, 'workspace');
95
- } else {
96
- return path.join(os.homedir(), 'workspace');
97
- }
98
- }
99
-
100
- /**
101
- * Resolve workspace path from config.
102
- *
103
- * - "auto" → call detectBestWorkspaceRoot()
104
- * - explicit path → expand ~ and return
105
- */
106
- export function resolveWorkspacePath(configValue: string): string {
107
- if (configValue.toLowerCase() === 'auto') {
108
- return detectBestWorkspaceRoot();
109
- }
110
- return path.resolve(configValue.replace(/^~/, os.homedir()));
111
- }
112
-
113
- /**
114
- * Create workspace directory tree on first use. Idempotent.
115
- *
116
- * Creates:
117
- * workspace/
118
- * ├── files/ # agent-generated files
119
- * ├── output/ # task results, exports
120
- * └── temp/ # scratch / ephemeral
121
- *
122
- * Returns the resolved workspace root path.
123
- */
124
- export function initWorkspace(root: string): string {
125
- fs.mkdirSync(root, { recursive: true });
126
- for (const sub of WORKSPACE_SUBDIRS) {
127
- fs.mkdirSync(path.join(root, sub), { recursive: true });
128
- }
129
- // Touch a .workspace marker so tools can identify it
130
- const marker = path.join(root, '.workspace');
131
- if (!fs.existsSync(marker)) {
132
- fs.writeFileSync(marker, `# Skyloom workspace — created automatically\npath: ${root}\n`, 'utf-8');
133
- }
134
- return root;
135
- }
136
-
137
- /**
138
- * Human-readable byte count (e.g. 128.5 GB).
139
- */
140
- export function formatBytes(n: number): string {
141
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
142
- let val = n;
143
- for (const unit of units) {
144
- if (Math.abs(val) < 1024.0) {
145
- return `${val.toFixed(1)} ${unit}`;
146
- }
147
- val /= 1024.0;
148
- }
149
- return `${val.toFixed(1)} PB`;
150
- }
1
+ /**
2
+ * Workspace auto-detection and lifecycle management.
3
+ *
4
+ * Rules:
5
+ * - First launch: auto-select best drive, create workspace/
6
+ * - Multi-drive: skip C:, pick drive with most free space
7
+ * - Single drive (C: only): use C:\\workspace
8
+ * - Unix: use ~/workspace
9
+ * - User can override via config workspace.path
10
+ */
11
+
12
+ import * as fs from 'fs';
13
+ import * as os from 'os';
14
+ import * as path from 'path';
15
+
16
+ const WORKSPACE_SUBDIRS = ['files', 'output', 'temp'];
17
+
18
+ export interface DriveInfo {
19
+ letter: string;
20
+ path: string;
21
+ totalBytes: number;
22
+ freeBytes: number;
23
+ }
24
+
25
+ /**
26
+ * Enumerate fixed drives with free-space info.
27
+ */
28
+ function getDriveList(): DriveInfo[] {
29
+ const drives: DriveInfo[] = [];
30
+
31
+ if (process.platform === 'win32') {
32
+ for (let i = 0; i < 26; i++) {
33
+ const letter = String.fromCharCode(65 + i); // A-Z
34
+ const root = `${letter}:\\`;
35
+ if (!fs.existsSync(root)) continue;
36
+ try {
37
+ const stat = fs.statfsSync(root);
38
+ drives.push({
39
+ letter,
40
+ path: root,
41
+ totalBytes: stat.blocks * stat.bsize,
42
+ freeBytes: stat.bfree * stat.bsize,
43
+ });
44
+ } catch {
45
+ try {
46
+ // Fallback: just mark it available
47
+ drives.push({
48
+ letter,
49
+ path: root,
50
+ totalBytes: 0,
51
+ freeBytes: 0,
52
+ });
53
+ } catch {
54
+ continue;
55
+ }
56
+ }
57
+ }
58
+ } else {
59
+ // Unix — treat / as the single candidate
60
+ try {
61
+ const stat = fs.statfsSync('/');
62
+ drives.push({
63
+ letter: '',
64
+ path: '/',
65
+ totalBytes: stat.blocks * stat.bsize,
66
+ freeBytes: stat.bfree * stat.bsize,
67
+ });
68
+ } catch {
69
+ // Ignore
70
+ }
71
+ }
72
+
73
+ return drives;
74
+ }
75
+
76
+ /**
77
+ * Pick the best drive for the workspace directory.
78
+ *
79
+ * - Windows with multiple drives: skip C:, pick drive with most free bytes.
80
+ * - Windows with only C: → C:\\workspace.
81
+ * - Unix → ~/workspace.
82
+ */
83
+ export function detectBestWorkspaceRoot(): string {
84
+ const drives = getDriveList();
85
+
86
+ if (process.platform === 'win32') {
87
+ let candidates = drives.filter((d) => d.letter.toUpperCase() !== 'C');
88
+ if (candidates.length === 0) {
89
+ candidates = drives; // fallback to C:
90
+ }
91
+ // Sort by free space descending
92
+ candidates.sort((a, b) => b.freeBytes - a.freeBytes);
93
+ const best = candidates[0];
94
+ return path.join(best.path, 'workspace');
95
+ } else {
96
+ return path.join(os.homedir(), 'workspace');
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Resolve workspace path from config.
102
+ *
103
+ * - "auto" → call detectBestWorkspaceRoot()
104
+ * - explicit path → expand ~ and return
105
+ */
106
+ export function resolveWorkspacePath(configValue: string): string {
107
+ if (configValue.toLowerCase() === 'auto') {
108
+ return detectBestWorkspaceRoot();
109
+ }
110
+ return path.resolve(configValue.replace(/^~/, os.homedir()));
111
+ }
112
+
113
+ /**
114
+ * Create workspace directory tree on first use. Idempotent.
115
+ *
116
+ * Creates:
117
+ * workspace/
118
+ * ├── files/ # agent-generated files
119
+ * ├── output/ # task results, exports
120
+ * └── temp/ # scratch / ephemeral
121
+ *
122
+ * Returns the resolved workspace root path.
123
+ */
124
+ export function initWorkspace(root: string): string {
125
+ fs.mkdirSync(root, { recursive: true });
126
+ for (const sub of WORKSPACE_SUBDIRS) {
127
+ fs.mkdirSync(path.join(root, sub), { recursive: true });
128
+ }
129
+ // Touch a .workspace marker so tools can identify it
130
+ const marker = path.join(root, '.workspace');
131
+ if (!fs.existsSync(marker)) {
132
+ fs.writeFileSync(marker, `# Skyloom workspace — created automatically\npath: ${root}\n`, 'utf-8');
133
+ }
134
+ return root;
135
+ }
136
+
137
+ /**
138
+ * Human-readable byte count (e.g. 128.5 GB).
139
+ */
140
+ export function formatBytes(n: number): string {
141
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
142
+ let val = n;
143
+ for (const unit of units) {
144
+ if (Math.abs(val) < 1024.0) {
145
+ return `${val.toFixed(1)} ${unit}`;
146
+ }
147
+ val /= 1024.0;
148
+ }
149
+ return `${val.toFixed(1)} PB`;
150
+ }