@howlil/ez-agents 3.1.0 → 3.4.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 (61) hide show
  1. package/README.md +295 -714
  2. package/bin/install.js +387 -62
  3. package/commands/ez/auth.md +87 -0
  4. package/commands/ez/join-discord.md +1 -1
  5. package/ez-agents/bin/ez-tools.cjs +120 -2
  6. package/ez-agents/bin/lib/assistant-adapter.cjs +62 -3
  7. package/ez-agents/bin/lib/audit-exec.cjs +20 -8
  8. package/ez-agents/bin/lib/auth.cjs +2 -1
  9. package/ez-agents/bin/lib/circuit-breaker.cjs +1 -1
  10. package/ez-agents/bin/lib/commands.cjs +42 -23
  11. package/ez-agents/bin/lib/config.cjs +18 -11
  12. package/ez-agents/bin/lib/core.cjs +42 -25
  13. package/ez-agents/bin/lib/file-lock.cjs +3 -3
  14. package/ez-agents/bin/lib/fs-utils.cjs +1 -1
  15. package/ez-agents/bin/lib/git-utils.cjs +1 -1
  16. package/ez-agents/bin/lib/health-check.cjs +2 -3
  17. package/ez-agents/bin/lib/index.cjs +1 -1
  18. package/ez-agents/bin/lib/init.cjs +70 -23
  19. package/ez-agents/bin/lib/logger.cjs +11 -4
  20. package/ez-agents/bin/lib/model-provider.cjs +124 -29
  21. package/ez-agents/bin/lib/phase.cjs +39 -22
  22. package/ez-agents/bin/lib/planning-write.cjs +107 -0
  23. package/ez-agents/bin/lib/retry.cjs +1 -1
  24. package/ez-agents/bin/lib/roadmap.cjs +3 -2
  25. package/ez-agents/bin/lib/safe-exec.cjs +1 -1
  26. package/ez-agents/bin/lib/safe-path.cjs +1 -1
  27. package/ez-agents/bin/lib/state.cjs +24 -9
  28. package/ez-agents/bin/lib/temp-file.cjs +1 -1
  29. package/ez-agents/bin/lib/template.cjs +2 -1
  30. package/ez-agents/bin/lib/test-file-lock.cjs +1 -1
  31. package/ez-agents/bin/lib/test-graceful.cjs +2 -2
  32. package/ez-agents/bin/lib/test-logger.cjs +2 -2
  33. package/ez-agents/bin/lib/test-temp-file.cjs +1 -1
  34. package/ez-agents/bin/lib/timeout-exec.cjs +4 -3
  35. package/ez-agents/bin/lib/verify.cjs +54 -25
  36. package/ez-agents/references/continuation-format.md +1 -1
  37. package/ez-agents/workflows/add-tests.md +2 -2
  38. package/ez-agents/workflows/add-todo.md +1 -1
  39. package/ez-agents/workflows/autonomous.md +15 -15
  40. package/ez-agents/workflows/diagnose-issues.md +1 -1
  41. package/ez-agents/workflows/discuss-phase.md +3 -3
  42. package/ez-agents/workflows/execute-phase.md +2 -2
  43. package/ez-agents/workflows/health.md +1 -1
  44. package/ez-agents/workflows/help.md +2 -2
  45. package/ez-agents/workflows/map-codebase.md +1 -1
  46. package/ez-agents/workflows/new-milestone.md +5 -5
  47. package/ez-agents/workflows/new-project.md +12 -10
  48. package/ez-agents/workflows/plan-phase.md +8 -8
  49. package/ez-agents/workflows/progress.md +1 -1
  50. package/ez-agents/workflows/set-profile.md +1 -1
  51. package/ez-agents/workflows/settings.md +9 -9
  52. package/ez-agents/workflows/stats.md +1 -1
  53. package/ez-agents/workflows/ui-phase.md +3 -3
  54. package/ez-agents/workflows/ui-review.md +2 -2
  55. package/ez-agents/workflows/update.md +1 -1
  56. package/ez-agents/workflows/validate-phase.md +3 -3
  57. package/ez-agents/workflows/verify-work.md +3 -3
  58. package/package.json +1 -1
  59. package/scripts/build-hooks.js +1 -1
  60. package/scripts/fix-qwen-installation.js +144 -0
  61. package/README.zh-CN.md +0 -702
@@ -1,16 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * GSD Model Provider — Unified API for multiple AI providers
4
+ * EZ Model Provider — Unified API for multiple AI providers
5
5
  *
6
6
  * Supports: Anthropic, Moonshot (Kimi), Alibaba (Qwen), OpenAI
7
- *
8
- * Usage:
9
- * const ModelProvider = require('./model-provider.cjs');
10
- * const model = new ModelProvider({ provider: 'anthropic', model: 'sonnet' });
11
- * const response = await model.chat([{ role: 'user', content: 'Hello' }]);
12
7
  */
13
8
 
9
+ const https = require('https');
10
+ const { URL } = require('url');
14
11
  const Logger = require('./logger.cjs');
15
12
  const logger = new Logger();
16
13
 
@@ -24,11 +21,37 @@ class ModelProvider {
24
21
  this.model = config.model || 'sonnet';
25
22
  this.apiKey = config.apiKey || process.env[`${this.provider.toUpperCase()}_API_KEY`];
26
23
 
27
- if (!this.apiKey) {
24
+ if (!this.apiKey && this.provider !== 'anthropic') {
28
25
  logger.warn('API key not configured', { provider: this.provider });
29
26
  }
30
27
  }
31
28
 
29
+ /**
30
+ * Helper for HTTP requests
31
+ */
32
+ _httpRequest(options, data) {
33
+ return new Promise((resolve, reject) => {
34
+ const req = https.request(options, (res) => {
35
+ let body = '';
36
+ res.on('data', (chunk) => body += chunk);
37
+ res.on('end', () => {
38
+ if (res.statusCode >= 200 && res.statusCode < 300) {
39
+ try {
40
+ resolve(JSON.parse(body));
41
+ } catch (e) {
42
+ resolve(body);
43
+ }
44
+ } else {
45
+ reject(new Error(`HTTP ${res.statusCode}: ${body}`));
46
+ }
47
+ });
48
+ });
49
+ req.on('error', reject);
50
+ if (data) req.write(JSON.stringify(data));
51
+ req.end();
52
+ });
53
+ }
54
+
32
55
  /**
33
56
  * Send chat message
34
57
  * @param {Object[]} messages - Chat messages
@@ -48,6 +71,7 @@ class ModelProvider {
48
71
  case 'moonshot':
49
72
  return this._chatMoonshot(messages, options);
50
73
  case 'alibaba':
74
+ case 'qwen':
51
75
  return this._chatQwen(messages, options);
52
76
  case 'openai':
53
77
  return this._chatOpenAI(messages, options);
@@ -60,10 +84,10 @@ class ModelProvider {
60
84
  * Anthropic Claude API
61
85
  */
62
86
  async _chatAnthropic(messages, options) {
63
- // Placeholder - would use @anthropic-ai/sdk in production
87
+ // Anthropic usually requires their SDK or complex headers
64
88
  logger.debug('Anthropic chat', { model: this.model });
65
89
  return {
66
- content: '[Anthropic response placeholder]',
90
+ content: '[Anthropic response placeholder - requires SDK]',
67
91
  provider: 'anthropic',
68
92
  model: this.model
69
93
  };
@@ -73,39 +97,111 @@ class ModelProvider {
73
97
  * Moonshot (Kimi) API
74
98
  */
75
99
  async _chatMoonshot(messages, options) {
76
- // Placeholder - would use moonshot SDK in production
77
- logger.debug('Moonshot chat', { model: this.model });
78
- return {
79
- content: '[Moonshot response placeholder]',
80
- provider: 'moonshot',
81
- model: this.model
100
+ const modelName = this.model === 'sonnet' ? 'moonshot-v1-8k' : this.model;
101
+ const data = {
102
+ model: modelName,
103
+ messages: messages,
104
+ temperature: options.temperature || 0.3
105
+ };
106
+
107
+ const reqOptions = {
108
+ hostname: 'api.moonshot.cn',
109
+ path: '/v1/chat/completions',
110
+ method: 'POST',
111
+ headers: {
112
+ 'Content-Type': 'application/json',
113
+ 'Authorization': `Bearer ${this.apiKey}`
114
+ }
82
115
  };
116
+
117
+ try {
118
+ const response = await this._httpRequest(reqOptions, data);
119
+ return {
120
+ content: response.choices[0].message.content,
121
+ provider: 'moonshot',
122
+ model: modelName
123
+ };
124
+ } catch (error) {
125
+ logger.error('Moonshot API error', { error: error.message });
126
+ throw error;
127
+ }
83
128
  }
84
129
 
85
130
  /**
86
- * Alibaba Qwen API
131
+ * Alibaba Qwen API (DashScope)
87
132
  */
88
133
  async _chatQwen(messages, options) {
89
- // Placeholder - would use DashScope SDK in production
90
- logger.debug('Qwen chat', { model: this.model });
91
- return {
92
- content: '[Qwen response placeholder]',
93
- provider: 'alibaba',
94
- model: this.model
134
+ // Map generic model names to Qwen specific ones
135
+ let modelName = this.model;
136
+ if (modelName === 'sonnet' || modelName === 'gpt-4') modelName = 'qwen-max';
137
+ if (modelName === 'haiku' || modelName === 'gpt-3.5-turbo') modelName = 'qwen-plus';
138
+
139
+ const data = {
140
+ model: modelName,
141
+ input: {
142
+ messages: messages
143
+ },
144
+ parameters: {
145
+ result_format: 'message',
146
+ temperature: options.temperature || 0.3
147
+ }
148
+ };
149
+
150
+ const reqOptions = {
151
+ hostname: 'dashscope.aliyuncs.com',
152
+ path: '/api/v1/services/aigc/text-generation/generation',
153
+ method: 'POST',
154
+ headers: {
155
+ 'Content-Type': 'application/json',
156
+ 'Authorization': `Bearer ${this.apiKey}`
157
+ }
95
158
  };
159
+
160
+ try {
161
+ const response = await this._httpRequest(reqOptions, data);
162
+ return {
163
+ content: response.output.choices[0].message.content,
164
+ provider: 'alibaba',
165
+ model: modelName
166
+ };
167
+ } catch (error) {
168
+ logger.error('Qwen API error', { error: error.message });
169
+ throw error;
170
+ }
96
171
  }
97
172
 
98
173
  /**
99
174
  * OpenAI API
100
175
  */
101
176
  async _chatOpenAI(messages, options) {
102
- // Placeholder - would use openai SDK in production
103
- logger.debug('OpenAI chat', { model: this.model });
104
- return {
105
- content: '[OpenAI response placeholder]',
106
- provider: 'openai',
107
- model: this.model
177
+ const modelName = this.model === 'sonnet' ? 'gpt-4-turbo' : this.model;
178
+ const data = {
179
+ model: modelName,
180
+ messages: messages,
181
+ temperature: options.temperature || 0.3
182
+ };
183
+
184
+ const reqOptions = {
185
+ hostname: 'api.openai.com',
186
+ path: '/v1/chat/completions',
187
+ method: 'POST',
188
+ headers: {
189
+ 'Content-Type': 'application/json',
190
+ 'Authorization': `Bearer ${this.apiKey}`
191
+ }
108
192
  };
193
+
194
+ try {
195
+ const response = await this._httpRequest(reqOptions, data);
196
+ return {
197
+ content: response.choices[0].message.content,
198
+ provider: 'openai',
199
+ model: modelName
200
+ };
201
+ } catch (error) {
202
+ logger.error('OpenAI API error', { error: error.message });
203
+ throw error;
204
+ }
109
205
  }
110
206
 
111
207
  /**
@@ -114,7 +210,6 @@ class ModelProvider {
114
210
  * @returns {number} - Approximate token count
115
211
  */
116
212
  countTokens(text) {
117
- // Rough estimate: 1 token ≈ 4 characters
118
213
  return Math.ceil(text.length / 4);
119
214
  }
120
215
 
@@ -7,6 +7,8 @@ const path = require('path');
7
7
  const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, toPosixPath, output, error } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
9
  const { writeStateMd } = require('./state.cjs');
10
+ const { safePlanningWriteSync } = require('./planning-write.cjs');
11
+ const { defaultLogger: logger } = require('./logger.cjs');
10
12
 
11
13
  function cmdPhasesList(cwd, options, raw) {
12
14
  const phasesDir = path.join(cwd, '.planning', 'phases');
@@ -79,8 +81,9 @@ function cmdPhasesList(cwd, options, raw) {
79
81
 
80
82
  // Default: list directories
81
83
  output({ directories: dirs, count: dirs.length }, raw, dirs.join('\n'));
82
- } catch (e) {
83
- error('Failed to list phases: ' + e.message);
84
+ } catch (err) {
85
+ logger.error('Failed to list phases in cmdPhasesList', { error: err.message });
86
+ error('Failed to list phases: ' + err.message);
84
87
  }
85
88
  }
86
89
 
@@ -144,8 +147,9 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
144
147
  raw,
145
148
  nextDecimal
146
149
  );
147
- } catch (e) {
148
- error('Failed to calculate next decimal phase: ' + e.message);
150
+ } catch (err) {
151
+ logger.error('Failed to calculate next decimal phase in cmdPhaseNextDecimal', { basePhase, error: err.message });
152
+ error('Failed to calculate next decimal phase: ' + err.message);
149
153
  }
150
154
  }
151
155
 
@@ -188,7 +192,8 @@ function cmdFindPhase(cwd, phase, raw) {
188
192
  };
189
193
 
190
194
  output(result, raw, result.directory);
191
- } catch {
195
+ } catch (err) {
196
+ logger.warn('Failed to read phases directory in cmdFindPhase', { phasesDir, error: err.message });
192
197
  output(notFound, raw, '');
193
198
  }
194
199
  }
@@ -217,8 +222,8 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
217
222
  phaseDir = path.join(phasesDir, match);
218
223
  phaseDirName = match;
219
224
  }
220
- } catch {
221
- // phases dir doesn't exist
225
+ } catch (err) {
226
+ logger.warn('Failed to enumerate phase directories in cmdPhasePlanIndex', { phasesDir, error: err.message });
222
227
  }
223
228
 
224
229
  if (!phaseDir) {
@@ -337,10 +342,10 @@ function cmdPhaseAdd(cwd, description, raw) {
337
342
 
338
343
  // Create directory with .gitkeep so git tracks empty folders
339
344
  fs.mkdirSync(dirPath, { recursive: true });
340
- fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
345
+ safePlanningWriteSync(path.join(dirPath, '.gitkeep'), '');
341
346
 
342
347
  // Build phase entry
343
- const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${newPhaseNum} to break down)\n`;
348
+ const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /ez-plan-phase ${newPhaseNum} to break down)\n`;
344
349
 
345
350
  // Find insertion point: before last "---" or at end
346
351
  let updatedContent;
@@ -351,7 +356,7 @@ function cmdPhaseAdd(cwd, description, raw) {
351
356
  updatedContent = content + phaseEntry;
352
357
  }
353
358
 
354
- fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
359
+ safePlanningWriteSync(roadmapPath, updatedContent);
355
360
 
356
361
  const result = {
357
362
  phase_number: newPhaseNum,
@@ -399,7 +404,9 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
399
404
  const dm = dir.match(decimalPattern);
400
405
  if (dm) existingDecimals.push(parseInt(dm[1], 10));
401
406
  }
402
- } catch {}
407
+ } catch (err) {
408
+ logger.warn('Failed to enumerate decimal siblings in cmdPhaseInsert', { phasesDir, normalizedBase, error: err.message });
409
+ }
403
410
 
404
411
  const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1;
405
412
  const decimalPhase = `${normalizedBase}.${nextDecimal}`;
@@ -408,10 +415,10 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
408
415
 
409
416
  // Create directory with .gitkeep so git tracks empty folders
410
417
  fs.mkdirSync(dirPath, { recursive: true });
411
- fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
418
+ safePlanningWriteSync(path.join(dirPath, '.gitkeep'), '');
412
419
 
413
420
  // Build phase entry
414
- const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${decimalPhase} to break down)\n`;
421
+ const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /ez-plan-phase ${decimalPhase} to break down)\n`;
415
422
 
416
423
  // Insert after the target phase section
417
424
  const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
@@ -432,7 +439,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
432
439
  }
433
440
 
434
441
  const updatedContent = content.slice(0, insertIdx) + phaseEntry + content.slice(insertIdx);
435
- fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
442
+ safePlanningWriteSync(roadmapPath, updatedContent);
436
443
 
437
444
  const result = {
438
445
  phase_number: decimalPhase,
@@ -468,7 +475,9 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
468
475
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
469
476
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
470
477
  targetDir = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
471
- } catch {}
478
+ } catch (err) {
479
+ logger.warn('Failed to locate target phase directory in cmdPhaseRemove', { phasesDir, normalized, error: err.message });
480
+ }
472
481
 
473
482
  // Check for executed work (SUMMARY.md files)
474
483
  if (targetDir && !force) {
@@ -536,7 +545,9 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
536
545
  }
537
546
  }
538
547
  }
539
- } catch {}
548
+ } catch (err) {
549
+ logger.warn('Failed to renumber decimal phase directories in cmdPhaseRemove', { normalized, error: err.message });
550
+ }
540
551
 
541
552
  } else {
542
553
  // Integer removal: renumber all subsequent integer phases
@@ -596,7 +607,9 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
596
607
  }
597
608
  }
598
609
  }
599
- } catch {}
610
+ } catch (err) {
611
+ logger.warn('Failed to renumber integer phase directories in cmdPhaseRemove', { normalized, error: err.message });
612
+ }
600
613
  }
601
614
 
602
615
  // Update ROADMAP.md
@@ -663,7 +676,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
663
676
  }
664
677
  }
665
678
 
666
- fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
679
+ safePlanningWriteSync(roadmapPath, roadmapContent);
667
680
 
668
681
  // Update STATE.md phase count
669
682
  const statePath = path.join(cwd, '.planning', 'STATE.md');
@@ -751,7 +764,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
751
764
  `$1${summaryCount}/${planCount} plans complete`
752
765
  );
753
766
 
754
- fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
767
+ safePlanningWriteSync(roadmapPath, roadmapContent);
755
768
 
756
769
  // Update REQUIREMENTS.md traceability for this phase's requirements
757
770
  const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
@@ -783,7 +796,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
783
796
  );
784
797
  }
785
798
 
786
- fs.writeFileSync(reqPath, reqContent, 'utf-8');
799
+ safePlanningWriteSync(reqPath, reqContent);
787
800
  requirementsUpdated = true;
788
801
  }
789
802
  }
@@ -815,7 +828,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
815
828
  }
816
829
  }
817
830
  }
818
- } catch {}
831
+ } catch (err) {
832
+ logger.warn('Failed to locate next phase from disk in cmdPhaseComplete', { phasesDir, phaseNum, error: err.message });
833
+ }
819
834
 
820
835
  // Fallback: if filesystem found no next phase, check ROADMAP.md
821
836
  // for phases that are defined but not yet planned (no directory on disk)
@@ -832,7 +847,9 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
832
847
  break;
833
848
  }
834
849
  }
835
- } catch {}
850
+ } catch (err) {
851
+ logger.warn('Failed to locate next phase from ROADMAP fallback in cmdPhaseComplete', { roadmapPath, phaseNum, error: err.message });
852
+ }
836
853
  }
837
854
 
838
855
  // Update STATE.md
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Planning Write — lock/temp-safe writer for .planning mutations
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const { Worker } = require('worker_threads');
9
+ const { withLock } = require('./file-lock.cjs');
10
+ const { createTempFile, cleanupTemp } = require('./temp-file.cjs');
11
+
12
+ function normalizeTimeoutError(err, filePath) {
13
+ if (!err) return err;
14
+ const message = err.message || '';
15
+ if (message.includes('File locked:')) {
16
+ return err;
17
+ }
18
+ if (message.includes('timed out') || err.code === 'ELOCKED') {
19
+ return new Error(`File locked: ${filePath}`);
20
+ }
21
+ return err;
22
+ }
23
+
24
+ async function safePlanningWrite(filePath, content, options = {}) {
25
+ const {
26
+ timeoutMs = 30000,
27
+ tempPrefix = 'ez-write-',
28
+ } = options;
29
+
30
+ let tempPath = null;
31
+
32
+ try {
33
+ await withLock(filePath, async () => {
34
+ tempPath = await createTempFile(tempPrefix, path.dirname(filePath), content);
35
+ await fs.promises.rename(tempPath, filePath);
36
+ }, { timeout: timeoutMs });
37
+ } catch (err) {
38
+ throw normalizeTimeoutError(err, filePath);
39
+ } finally {
40
+ if (tempPath) {
41
+ try {
42
+ await cleanupTemp(tempPath);
43
+ } catch {
44
+ // best effort cleanup only
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ function safePlanningWriteSync(filePath, content, options = {}) {
51
+ const signalBuffer = new SharedArrayBuffer(4);
52
+ const signal = new Int32Array(signalBuffer);
53
+ const errorPath = path.join(os.tmpdir(), `ez-planning-write-error-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.log`);
54
+ const modulePath = __filename;
55
+
56
+ const workerCode = `
57
+ const fs = require('fs');
58
+ const { workerData } = require('worker_threads');
59
+ const signal = new Int32Array(workerData.signalBuffer);
60
+ const { safePlanningWrite } = require(workerData.modulePath);
61
+ (async () => {
62
+ try {
63
+ await safePlanningWrite(workerData.filePath, workerData.content, workerData.options);
64
+ Atomics.store(signal, 0, 1);
65
+ } catch (err) {
66
+ try {
67
+ fs.writeFileSync(workerData.errorPath, err && (err.stack || err.message) ? (err.stack || err.message) : String(err), 'utf-8');
68
+ } catch {}
69
+ Atomics.store(signal, 0, 2);
70
+ } finally {
71
+ Atomics.notify(signal, 0);
72
+ }
73
+ })();
74
+ `;
75
+
76
+ const worker = new Worker(workerCode, {
77
+ eval: true,
78
+ workerData: { modulePath, filePath, content, options, signalBuffer, errorPath },
79
+ });
80
+
81
+ try {
82
+ while (Atomics.load(signal, 0) === 0) {
83
+ Atomics.wait(signal, 0, 0, 100);
84
+ }
85
+ } finally {
86
+ worker.terminate().catch(() => {});
87
+ }
88
+
89
+ const status = Atomics.load(signal, 0);
90
+ if (status === 2) {
91
+ let message = `safePlanningWriteSync failed for ${filePath}`;
92
+ try {
93
+ if (fs.existsSync(errorPath)) {
94
+ message = fs.readFileSync(errorPath, 'utf-8') || message;
95
+ }
96
+ } catch {}
97
+ try { fs.rmSync(errorPath, { force: true }); } catch {}
98
+ throw new Error(message);
99
+ }
100
+
101
+ try { fs.rmSync(errorPath, { force: true }); } catch {}
102
+ }
103
+
104
+ module.exports = {
105
+ safePlanningWrite,
106
+ safePlanningWriteSync,
107
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * GSD Retry — Retry utility with exponential backoff
4
+ * EZ Retry — Retry utility with exponential backoff
5
5
  *
6
6
  * Features:
7
7
  * - Configurable max retries, base delay, max delay
@@ -5,6 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs');
8
+ const { safePlanningWriteSync } = require('./planning-write.cjs');
8
9
 
9
10
  function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
10
11
  const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
@@ -158,7 +159,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
158
159
  const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
159
160
 
160
161
  // If roadmap marks phase complete, trust that over disk file structure.
161
- // Phases completed before GSD tracking (or via external tools) may lack
162
+ // Phases completed before EZ tracking (or via external tools) may lack
162
163
  // the standard PLAN/SUMMARY pairs but are still done.
163
164
  if (roadmapComplete && diskStatus !== 'complete') {
164
165
  diskStatus = 'complete';
@@ -286,7 +287,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
286
287
  roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
287
288
  }
288
289
 
289
- fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
290
+ safePlanningWriteSync(roadmapPath, roadmapContent);
290
291
 
291
292
  output({
292
293
  updated: true,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * GSD Safe Exec — Secure command execution with allowlist and validation
4
+ * EZ Safe Exec — Secure command execution with allowlist and validation
5
5
  *
6
6
  * Prevents command injection by:
7
7
  * - Using execFile instead of execSync with string concatenation
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * GSD Safe Path — Path traversal prevention utility
4
+ * EZ Safe Path — Path traversal prevention utility
5
5
  *
6
6
  * Prevents path traversal attacks by:
7
7
  * - Resolving and validating paths against base directory
@@ -6,6 +6,8 @@ const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, output, error } = require('./core.cjs');
8
8
  const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
9
+ const { safePlanningWriteSync } = require('./planning-write.cjs');
10
+ const { defaultLogger: logger } = require('./logger.cjs');
9
11
 
10
12
  // Shared helper: extract a field value from STATE.md content.
11
13
  // Supports both **Field:** bold and plain Field: format.
@@ -26,7 +28,9 @@ function cmdStateLoad(cwd, raw) {
26
28
  let stateRaw = '';
27
29
  try {
28
30
  stateRaw = fs.readFileSync(path.join(planningDir, 'STATE.md'), 'utf-8');
29
- } catch {}
31
+ } catch (err) {
32
+ logger.warn('Failed to read STATE.md in cmdStateLoad', { planningDir, error: err.message });
33
+ }
30
34
 
31
35
  const configExists = fs.existsSync(path.join(planningDir, 'config.json'));
32
36
  const roadmapExists = fs.existsSync(path.join(planningDir, 'ROADMAP.md'));
@@ -102,7 +106,8 @@ function cmdStateGet(cwd, section, raw) {
102
106
  }
103
107
 
104
108
  output({ error: `Section or field "${section}" not found` }, raw, '');
105
- } catch {
109
+ } catch (err) {
110
+ logger.error('STATE.md not found in cmdStateGet', { statePath, error: err.message });
106
111
  error('STATE.md not found');
107
112
  }
108
113
  }
@@ -146,7 +151,8 @@ function cmdStatePatch(cwd, patches, raw) {
146
151
  }
147
152
 
148
153
  output(results, raw, results.updated.length > 0 ? 'true' : 'false');
149
- } catch {
154
+ } catch (err) {
155
+ logger.error('Failed to patch STATE.md', { statePath, error: err.message });
150
156
  error('STATE.md not found');
151
157
  }
152
158
  }
@@ -174,7 +180,8 @@ function cmdStateUpdate(cwd, field, value) {
174
180
  } else {
175
181
  output({ updated: false, reason: `Field "${field}" not found in STATE.md` });
176
182
  }
177
- } catch {
183
+ } catch (err) {
184
+ logger.error('Failed to update STATE.md', { statePath, field, error: err.message });
178
185
  output({ updated: false, reason: 'STATE.md not found' });
179
186
  }
180
187
  }
@@ -325,6 +332,7 @@ function cmdStateAddDecision(cwd, options, raw) {
325
332
  summaryText = readTextArgOrFile(cwd, summary, summary_file, 'summary');
326
333
  rationaleText = readTextArgOrFile(cwd, rationale || '', rationale_file, 'rationale');
327
334
  } catch (err) {
335
+ logger.error('Failed to read decision text', { error: err.message });
328
336
  output({ added: false, reason: err.message }, raw, 'false');
329
337
  return;
330
338
  }
@@ -360,6 +368,7 @@ function cmdStateAddBlocker(cwd, text, raw) {
360
368
  try {
361
369
  blockerText = readTextArgOrFile(cwd, blockerOptions.text, blockerOptions.text_file, 'blocker');
362
370
  } catch (err) {
371
+ logger.error('Failed to read blocker text', { error: err.message });
363
372
  output({ added: false, reason: err.message }, raw, 'false');
364
373
  return;
365
374
  }
@@ -574,7 +583,9 @@ function buildStateFrontmatter(bodyContent, cwd) {
574
583
  const info = getMilestoneInfo(cwd);
575
584
  milestone = info.version;
576
585
  milestoneName = info.name;
577
- } catch {}
586
+ } catch (err) {
587
+ logger.warn('Failed to resolve milestone info while building state frontmatter', { error: err.message });
588
+ }
578
589
  }
579
590
 
580
591
  let totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
@@ -609,7 +620,9 @@ function buildStateFrontmatter(bodyContent, cwd) {
609
620
  totalPlans = diskTotalPlans;
610
621
  completedPlans = diskTotalSummaries;
611
622
  }
612
- } catch {}
623
+ } catch (err) {
624
+ logger.warn('Failed to compute phase progress from disk while building state frontmatter', { error: err.message });
625
+ }
613
626
  }
614
627
 
615
628
  let progressPercent = null;
@@ -637,7 +650,9 @@ function buildStateFrontmatter(bodyContent, cwd) {
637
650
  normalizedStatus = 'executing';
638
651
  }
639
652
 
640
- const fm = { gsd_state_version: '1.0' };
653
+ const fm = {
654
+ ez_state_version: '1.0',
655
+ };
641
656
 
642
657
  if (milestone) fm.milestone = milestone;
643
658
  if (milestoneName) fm.milestone_name = milestoneName;
@@ -676,9 +691,9 @@ function syncStateFrontmatter(content, cwd) {
676
691
  * Write STATE.md with synchronized YAML frontmatter.
677
692
  * All STATE.md writes should use this instead of raw writeFileSync.
678
693
  */
679
- function writeStateMd(statePath, content, cwd) {
694
+ function writeStateMd(statePath, content, cwd, options = {}) {
680
695
  const synced = syncStateFrontmatter(content, cwd);
681
- fs.writeFileSync(statePath, synced, 'utf-8');
696
+ safePlanningWriteSync(statePath, synced, options);
682
697
  }
683
698
 
684
699
  function cmdStateJson(cwd, raw) {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * GSD Temp File — Secure temporary file handler
4
+ * EZ Temp File — Secure temporary file handler
5
5
  *
6
6
  * Creates secure temp files with fs.mkdtemp(), automatic cleanup on exit,
7
7
  * and path validation to prevent traversal attacks.