@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
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: ez:auth
3
+ description: Manage API credentials securely (save, list, delete)
4
+ argument-hint: "<save|list|delete|test> [provider] [secret]"
5
+ agent: ez-helper
6
+ allowed-tools:
7
+ - Read
8
+ - Bash
9
+ ---
10
+ <objective>
11
+ Manage API credentials securely using system keychain or fallback file storage
12
+
13
+ **Subcommands:**
14
+ - `ez:auth save <provider> <secret>` — Save credential for provider
15
+ - `ez:auth list` — List all stored providers
16
+ - `ez:auth delete <provider>` — Delete credential for provider
17
+ - `ez:auth test` — Test credential system and show keychain status
18
+
19
+ **Providers:** anthropic, moonshot, alibaba, qwen, openai
20
+ </objective>
21
+
22
+ <execution_context>
23
+ @~/.claude/ez-agents/workflows/ez-helper.md
24
+ </execution_context>
25
+
26
+ <context>
27
+ @.planning/PROJECT.md
28
+ @.planning/STATE.md
29
+
30
+ # Credential storage module:
31
+ @ez-agents/bin/lib/auth.cjs
32
+
33
+ # EZ tools CLI:
34
+ @ez-agents/bin/ez-tools.cjs
35
+ </context>
36
+
37
+ <process>
38
+ ## Subcommand Routing
39
+
40
+ 1. **Parse arguments** — First arg is subcommand (save/list/delete/test), remaining are parameters
41
+ 2. **Route to handler** — Call appropriate auth function from ez-tools.cjs
42
+ 3. **Output result** — Show success/failure with user-friendly messages
43
+
44
+ ## Handlers
45
+
46
+ ### save <provider> <secret>
47
+ 1. Validate provider name against known providers
48
+ 2. Call saveCredential(provider, secret)
49
+ 3. Output: "✓ Credential saved for {provider}" or error message
50
+
51
+ ### list
52
+ 1. Call listProviders()
53
+ 2. Check isKeychainAvailable()
54
+ 3. Output table:
55
+ ```
56
+ Provider Storage
57
+ ─────────────────────────
58
+ anthropic Keychain ✓
59
+ qwen File (fallback)
60
+ ```
61
+ 4. If using file storage, show warning: "⚠ Using file storage — consider installing keytar for better security"
62
+
63
+ ### delete <provider>
64
+ 1. Validate provider exists
65
+ 2. Confirm deletion (ask user or require --force)
66
+ 3. Call deleteCredential(provider)
67
+ 4. Output: "✓ Credential deleted for {provider}"
68
+
69
+ ### test
70
+ 1. Check isKeychainAvailable()
71
+ 2. Output:
72
+ ```
73
+ Credential System Test
74
+
75
+ Keychain (keytar): {Available | Unavailable}
76
+ Storage mode: {System keychain | Fallback file}
77
+
78
+ Stored providers: {count}
79
+ ```
80
+ 3. If keytar unavailable, show install hint: "npm install keytar"
81
+
82
+ ## Error Handling
83
+
84
+ - **Invalid provider:** "Unknown provider '{name}'. Valid: anthropic, moonshot, alibaba, qwen, openai"
85
+ - **Credential not found:** "No credential found for {provider}. Use 'save' to add one."
86
+ - **Keychain error:** "Failed to access keychain: {error}. Using fallback storage."
87
+ </process>
@@ -12,7 +12,7 @@ Display the Discord invite link for the EZ Agents community server.
12
12
 
13
13
  Connect with other EZ Agents users, get help, share what you're building, and stay updated.
14
14
 
15
- **Invite link:** https://discord.gg/gsd
15
+ **Invite link:** https://discord.gg/ez-agents
16
16
 
17
17
  Click the link or paste it into your browser to join.
18
18
  </output>
@@ -139,6 +139,8 @@ const milestone = require('./lib/milestone.cjs');
139
139
  const commands = require('./lib/commands.cjs');
140
140
  const init = require('./lib/init.cjs');
141
141
  const frontmatter = require('./lib/frontmatter.cjs');
142
+ const HealthCheck = require('./lib/health-check.cjs');
143
+ const auth = require('./lib/auth.cjs');
142
144
 
143
145
  // ─── CLI Router ───────────────────────────────────────────────────────────────
144
146
 
@@ -172,7 +174,7 @@ async function main() {
172
174
  const command = args[0];
173
175
 
174
176
  if (!command) {
175
- error('Usage: ez-tools <command> [args] [--raw] [--cwd <path>]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init');
177
+ error('Usage: ez-tools <command> [args] [--raw] [--cwd <path>]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, init, health');
176
178
  }
177
179
 
178
180
  switch (command) {
@@ -247,6 +249,122 @@ async function main() {
247
249
  break;
248
250
  }
249
251
 
252
+ case 'health': {
253
+ const health = new HealthCheck();
254
+ const result = health.runAll();
255
+ console.log(JSON.stringify(result, null, 2));
256
+ break;
257
+ }
258
+
259
+ case 'auth': {
260
+ const subcommand = args[1];
261
+ if (!subcommand) {
262
+ error('Usage: ez-tools auth <save|list|delete|test> [provider] [secret]\nSubcommands: save, list, delete, test');
263
+ }
264
+
265
+ switch (subcommand) {
266
+ case 'save': {
267
+ const provider = args[2];
268
+ const secret = args[3];
269
+ if (!provider || !secret) {
270
+ error('Usage: ez-tools auth save <provider> <secret>');
271
+ }
272
+ const success = await auth.saveCredential(provider, secret);
273
+ if (success) {
274
+ console.log(`✓ Credential saved for ${provider}`);
275
+ } else {
276
+ console.error('Failed to save credential');
277
+ process.exit(1);
278
+ }
279
+ break;
280
+ }
281
+
282
+ case 'list': {
283
+ const providers = await auth.listProviders();
284
+ const usingKeychain = auth.isKeychainAvailable();
285
+
286
+ console.log('');
287
+ console.log('Provider Storage');
288
+ console.log('─────────────────────────');
289
+ if (providers.length === 0) {
290
+ console.log('No credentials stored');
291
+ } else {
292
+ for (const p of providers) {
293
+ const storage = usingKeychain ? 'Keychain ✓' : 'File (fallback)';
294
+ console.log(`${p.padEnd(14)} ${storage}`);
295
+ }
296
+ }
297
+ if (!usingKeychain) {
298
+ console.log('');
299
+ console.log('⚠ Using file storage — consider installing keytar for better security');
300
+ }
301
+ break;
302
+ }
303
+
304
+ case 'delete': {
305
+ const provider = args[2];
306
+ const force = args.includes('--force');
307
+ if (!provider) {
308
+ error('Usage: ez-tools auth delete <provider> [--force]');
309
+ }
310
+
311
+ if (!force) {
312
+ const readline = require('readline');
313
+ const rl = readline.createInterface({
314
+ input: process.stdin,
315
+ output: process.stdout
316
+ });
317
+
318
+ const answer = await new Promise(resolve => {
319
+ rl.question(`Delete credential for ${provider}? [y/N] `, resolve);
320
+ rl.close();
321
+ });
322
+
323
+ if (answer.toLowerCase() !== 'y') {
324
+ console.log('Delete cancelled');
325
+ break;
326
+ }
327
+ }
328
+
329
+ const success = await auth.deleteCredential(provider);
330
+ if (success) {
331
+ console.log(`✓ Credential deleted for ${provider}`);
332
+ } else {
333
+ console.error(`No credential found for ${provider}`);
334
+ process.exit(1);
335
+ }
336
+ break;
337
+ }
338
+
339
+ case 'test': {
340
+ const usingKeychain = auth.isKeychainAvailable();
341
+ const providers = await auth.listProviders();
342
+
343
+ console.log('');
344
+ console.log('Credential System Test');
345
+ console.log('══════════════════════');
346
+ console.log(`Keychain (keytar): ${usingKeychain ? 'Available ✓' : 'Unavailable'}`);
347
+ console.log(`Storage mode: ${usingKeychain ? 'System keychain' : 'Fallback file'}`);
348
+ console.log('');
349
+ console.log(`Stored providers: ${providers.length}`);
350
+ if (providers.length > 0) {
351
+ console.log(` ${providers.join(', ')}`);
352
+ }
353
+ if (!usingKeychain) {
354
+ console.log('');
355
+ console.log('Tip: Install keytar for better security:');
356
+ console.log(' npm install keytar');
357
+ }
358
+ break;
359
+ }
360
+
361
+ default: {
362
+ error('Unknown auth subcommand: ' + subcommand + '\nValid: save, list, delete, test');
363
+ }
364
+ }
365
+ break;
366
+ }
367
+
250
368
  case 'resolve-model': {
251
369
  commands.cmdResolveModel(cwd, args[1], raw);
252
370
  break;
@@ -526,7 +644,7 @@ async function main() {
526
644
  init.cmdInitPlanPhase(cwd, args[2], raw);
527
645
  break;
528
646
  case 'new-project':
529
- init.cmdInitNewProject(cwd, raw);
647
+ await init.cmdInitNewProject(cwd, raw);
530
648
  break;
531
649
  case 'new-milestone':
532
650
  init.cmdInitNewMilestone(cwd, raw);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * GSD Assistant Adapters — Unified interface for AI coding assistants
4
+ * EZ Assistant Adapters — Unified interface for AI coding assistants
5
5
  *
6
6
  * Adapters for: Claude Code, OpenCode, Gemini CLI, Codex
7
7
  *
@@ -165,6 +165,63 @@ class CodexAdapter extends AssistantAdapter {
165
165
  }
166
166
  }
167
167
 
168
+ /**
169
+ * Qwen Code adapter
170
+ */
171
+ class QwenAdapter extends AssistantAdapter {
172
+ constructor() {
173
+ super('qwen');
174
+ }
175
+
176
+ async spawnAgent(type, options) {
177
+ logger.info('Qwen Code: spawning agent', { type });
178
+ // Qwen Code uses its own agent system
179
+ return { type, status: 'completed', result: '[Qwen Code agent result]' };
180
+ }
181
+
182
+ async callTool(tool, params) {
183
+ logger.info('Qwen Code: calling tool', { tool });
184
+ return { tool, status: 'success' };
185
+ }
186
+
187
+ selectModel(taskType) {
188
+ const models = {
189
+ planning: 'qwen-max',
190
+ execution: 'qwen-plus',
191
+ verification: 'qwen-plus'
192
+ };
193
+ return models[taskType] || models.execution;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Kimi Code adapter
199
+ */
200
+ class KimiAdapter extends AssistantAdapter {
201
+ constructor() {
202
+ super('kimi');
203
+ }
204
+
205
+ async spawnAgent(type, options) {
206
+ logger.info('Kimi Code: spawning agent', { type });
207
+ return { type, status: 'completed', result: '[Kimi Code agent result]' };
208
+ }
209
+
210
+ async callTool(tool, params) {
211
+ logger.info('Kimi Code: calling tool', { tool });
212
+ return { tool, status: 'success' };
213
+ }
214
+
215
+ selectModel(taskType) {
216
+ const models = {
217
+ planning: 'moonshot-v1-32k',
218
+ execution: 'moonshot-v1-8k',
219
+ verification: 'moonshot-v1-8k'
220
+ };
221
+ return models[taskType] || models.execution;
222
+ }
223
+ }
224
+
168
225
  /**
169
226
  * Factory function to create adapter
170
227
  * @param {string} type - Adapter type
@@ -175,7 +232,9 @@ function createAdapter(type) {
175
232
  'claude-code': ClaudeCodeAdapter,
176
233
  'opencode': OpenCodeAdapter,
177
234
  'gemini': GeminiAdapter,
178
- 'codex': CodexAdapter
235
+ 'codex': CodexAdapter,
236
+ 'qwen': QwenAdapter,
237
+ 'kimi': KimiAdapter
179
238
  };
180
239
 
181
240
  const AdapterClass = adapters[type];
@@ -191,7 +250,7 @@ function createAdapter(type) {
191
250
  * @returns {string[]} - List of adapter names
192
251
  */
193
252
  function getAvailableAdapters() {
194
- return ['claude-code', 'opencode', 'gemini', 'codex'];
253
+ return ['claude-code', 'opencode', 'gemini', 'codex', 'qwen', 'kimi'];
195
254
  }
196
255
 
197
256
  module.exports = {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * GSD Audit Exec — Command execution with full audit logging
4
+ * EZ Audit Exec — Command execution with full audit logging
5
5
  *
6
6
  * Logs all command executions to audit file for security review:
7
7
  * - Timestamp, command, arguments, context
@@ -22,13 +22,25 @@ const logger = new Logger();
22
22
 
23
23
  const execFileAsync = promisify(execFile);
24
24
 
25
- // Audit log file path
26
- const AUDIT_DIR = join('.planning', 'logs');
27
- const AUDIT_FILE = join(AUDIT_DIR, `audit-${new Date().toISOString().split('T')[0]}.jsonl`);
25
+ // Audit log file path - lazy init
26
+ let _AUDIT_DIR;
27
+ let _AUDIT_FILE;
28
28
 
29
- // Ensure audit directory exists
30
- if (!existsSync(AUDIT_DIR)) {
31
- mkdirSync(AUDIT_DIR, { recursive: true });
29
+ function getAuditDir() {
30
+ if (!_AUDIT_DIR) {
31
+ _AUDIT_DIR = join('.planning', 'logs');
32
+ if (!existsSync(_AUDIT_DIR)) {
33
+ mkdirSync(_AUDIT_DIR, { recursive: true });
34
+ }
35
+ }
36
+ return _AUDIT_DIR;
37
+ }
38
+
39
+ function getAuditFile() {
40
+ if (!_AUDIT_FILE) {
41
+ _AUDIT_FILE = join(getAuditDir(), `audit-${new Date().toISOString().split('T')[0]}.jsonl`);
42
+ }
43
+ return _AUDIT_FILE;
32
44
  }
33
45
 
34
46
  /**
@@ -37,7 +49,7 @@ if (!existsSync(AUDIT_DIR)) {
37
49
  */
38
50
  function writeAudit(entry) {
39
51
  const line = JSON.stringify(entry) + '\n';
40
- appendFileSync(AUDIT_FILE, line, 'utf-8');
52
+ appendFileSync(getAuditFile(), line, 'utf-8');
41
53
  }
42
54
 
43
55
  /**
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * GSD Auth — Secure credential storage using system keychain
4
+ * EZ Auth — Secure credential storage using system keychain
5
5
  *
6
6
  * Stores API keys securely using:
7
7
  * - keytar for system keychain (Windows Credential Manager, macOS Keychain, libsecret)
@@ -25,6 +25,7 @@ const PROVIDERS = {
25
25
  ANTHROPIC: 'anthropic',
26
26
  MOONSHOT: 'moonshot',
27
27
  ALIBABA: 'alibaba',
28
+ QWEN: 'qwen',
28
29
  OPENAI: 'openai'
29
30
  };
30
31
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * GSD Circuit Breaker — Prevent cascading failures
4
+ * EZ Circuit Breaker — Prevent cascading failures
5
5
  *
6
6
  * Implements circuit breaker pattern:
7
7
  * - CLOSED: Normal operation
@@ -3,9 +3,9 @@
3
3
  */
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
- const { execSync } = require('child_process');
7
6
  const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, resolveModelInternal, MODEL_PROFILES, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
8
7
  const { extractFrontmatter } = require('./frontmatter.cjs');
8
+ const { defaultLogger: logger } = require('./logger.cjs');
9
9
 
10
10
  function cmdGenerateSlug(text, raw) {
11
11
  if (!text) {
@@ -70,9 +70,13 @@ function cmdListTodos(cwd, area, raw) {
70
70
  area: todoArea,
71
71
  path: toPosixPath(path.join('.planning', 'todos', 'pending', file)),
72
72
  });
73
- } catch {}
73
+ } catch (err) {
74
+ logger.warn('Failed to parse todo file in cmdListTodos', { file, error: err.message });
75
+ }
74
76
  }
75
- } catch {}
77
+ } catch (err) {
78
+ logger.warn('Failed to list pending todos in cmdListTodos', { pendingDir, error: err.message });
79
+ }
76
80
 
77
81
  const result = { count, todos };
78
82
  output(result, raw, count.toString());
@@ -90,7 +94,8 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
90
94
  const type = stats.isDirectory() ? 'directory' : stats.isFile() ? 'file' : 'other';
91
95
  const result = { exists: true, type };
92
96
  output(result, raw, 'true');
93
- } catch {
97
+ } catch (err) {
98
+ logger.warn('Path verification failed in cmdVerifyPathExists', { fullPath, error: err.message });
94
99
  const result = { exists: false, type: null };
95
100
  output(result, raw, 'false');
96
101
  }
@@ -119,7 +124,9 @@ function cmdHistoryDigest(cwd, raw) {
119
124
  for (const dir of currentDirs) {
120
125
  allPhaseDirs.push({ name: dir, fullPath: path.join(phasesDir, dir), milestone: null });
121
126
  }
122
- } catch {}
127
+ } catch (err) {
128
+ logger.warn('Failed to enumerate current phase directories in cmdHistoryDigest', { phasesDir, error: err.message });
129
+ }
123
130
  }
124
131
 
125
132
  if (allPhaseDirs.length === 0) {
@@ -177,8 +184,8 @@ function cmdHistoryDigest(cwd, raw) {
177
184
  fm['tech-stack'].added.forEach(t => digest.tech_stack.add(typeof t === 'string' ? t : t.name));
178
185
  }
179
186
 
180
- } catch (e) {
181
- // Skip malformed summaries
187
+ } catch (err) {
188
+ logger.warn('Skipping malformed summary in cmdHistoryDigest', { summary, dirPath, error: err.message });
182
189
  }
183
190
  }
184
191
  }
@@ -192,8 +199,9 @@ function cmdHistoryDigest(cwd, raw) {
192
199
  digest.tech_stack = [...digest.tech_stack];
193
200
 
194
201
  output(digest, raw);
195
- } catch (e) {
196
- error('Failed to generate history digest: ' + e.message);
202
+ } catch (err) {
203
+ logger.error('Failed to generate history digest', { error: err.message });
204
+ error('Failed to generate history digest: ' + err.message);
197
205
  }
198
206
  }
199
207
 
@@ -213,7 +221,7 @@ function cmdResolveModel(cwd, agentType, raw) {
213
221
  output(result, raw, model);
214
222
  }
215
223
 
216
- function cmdCommit(cwd, message, files, raw, amend) {
224
+ async function cmdCommit(cwd, message, files, raw, amend) {
217
225
  if (!message && !amend) {
218
226
  error('commit message required');
219
227
  }
@@ -228,7 +236,7 @@ function cmdCommit(cwd, message, files, raw, amend) {
228
236
  }
229
237
 
230
238
  // Check if .planning is gitignored
231
- if (isGitIgnored(cwd, '.planning')) {
239
+ if (await isGitIgnored(cwd, '.planning')) {
232
240
  const result = { committed: false, hash: null, reason: 'skipped_gitignored' };
233
241
  output(result, raw, 'skipped');
234
242
  return;
@@ -237,12 +245,12 @@ function cmdCommit(cwd, message, files, raw, amend) {
237
245
  // Stage files
238
246
  const filesToStage = files && files.length > 0 ? files : ['.planning/'];
239
247
  for (const file of filesToStage) {
240
- execGit(cwd, ['add', file]);
248
+ await execGit(cwd, ['add', file]);
241
249
  }
242
250
 
243
251
  // Commit
244
252
  const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', message];
245
- const commitResult = execGit(cwd, commitArgs);
253
+ const commitResult = await execGit(cwd, commitArgs);
246
254
  if (commitResult.exitCode !== 0) {
247
255
  if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
248
256
  const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
@@ -255,7 +263,7 @@ function cmdCommit(cwd, message, files, raw, amend) {
255
263
  }
256
264
 
257
265
  // Get short hash
258
- const hashResult = execGit(cwd, ['rev-parse', '--short', 'HEAD']);
266
+ const hashResult = await execGit(cwd, ['rev-parse', '--short', 'HEAD']);
259
267
  const hash = hashResult.exitCode === 0 ? hashResult.stdout : null;
260
268
  const result = { committed: true, hash, reason: 'committed' };
261
269
  output(result, raw, hash || 'committed');
@@ -375,6 +383,7 @@ async function cmdWebsearch(query, options, raw) {
375
383
  results
376
384
  }, raw, results.map(r => `${r.title}\n${r.url}\n${r.description}`).join('\n\n'));
377
385
  } catch (err) {
386
+ logger.warn('Websearch request failed in cmdWebsearch', { query, error: err.message });
378
387
  output({ available: false, error: err.message }, raw, '');
379
388
  }
380
389
  }
@@ -411,7 +420,9 @@ function cmdProgressRender(cwd, format, raw) {
411
420
 
412
421
  phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
413
422
  }
414
- } catch {}
423
+ } catch (err) {
424
+ logger.warn('Failed to enumerate phase directories in cmdProgressRender', { phasesDir, error: err.message });
425
+ }
415
426
 
416
427
  const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
417
428
 
@@ -492,7 +503,7 @@ function cmdScaffold(cwd, type, options, raw) {
492
503
  switch (type) {
493
504
  case 'context': {
494
505
  filePath = path.join(phaseDir, `${padded}-CONTEXT.md`);
495
- content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — Context\n\n## Decisions\n\n_Decisions will be captured during /gsd:discuss-phase ${phase}_\n\n## Discretion Areas\n\n_Areas where the executor can use judgment_\n\n## Deferred Ideas\n\n_Ideas to consider later_\n`;
506
+ content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — Context\n\n## Decisions\n\n_Decisions will be captured during /ez-discuss-phase ${phase}_\n\n## Discretion Areas\n\n_Areas where the executor can use judgment_\n\n## Deferred Ideas\n\n_Ideas to consider later_\n`;
496
507
  break;
497
508
  }
498
509
  case 'uat': {
@@ -532,7 +543,7 @@ function cmdScaffold(cwd, type, options, raw) {
532
543
  output({ created: true, path: relPath }, raw, relPath);
533
544
  }
534
545
 
535
- function cmdStats(cwd, format, raw) {
546
+ async function cmdStats(cwd, format, raw) {
536
547
  const phasesDir = path.join(cwd, '.planning', 'phases');
537
548
  const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
538
549
  const statePath = path.join(cwd, '.planning', 'STATE.md');
@@ -566,7 +577,9 @@ function cmdStats(cwd, format, raw) {
566
577
 
567
578
  phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
568
579
  }
569
- } catch {}
580
+ } catch (err) {
581
+ logger.warn('Failed to enumerate phase directories in cmdStats', { phasesDir, error: err.message });
582
+ }
570
583
 
571
584
  const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
572
585
 
@@ -581,7 +594,9 @@ function cmdStats(cwd, format, raw) {
581
594
  requirementsComplete = checked ? checked.length : 0;
582
595
  requirementsTotal = requirementsComplete + (unchecked ? unchecked.length : 0);
583
596
  }
584
- } catch {}
597
+ } catch (err) {
598
+ logger.warn('Failed to parse REQUIREMENTS.md in cmdStats', { reqPath, error: err.message });
599
+ }
585
600
 
586
601
  // Last activity from STATE.md
587
602
  let lastActivity = null;
@@ -591,17 +606,21 @@ function cmdStats(cwd, format, raw) {
591
606
  const activityMatch = stateContent.match(/\*\*Last Activity:\*\*\s*(.+)/);
592
607
  if (activityMatch) lastActivity = activityMatch[1].trim();
593
608
  }
594
- } catch {}
609
+ } catch (err) {
610
+ logger.warn('Failed to read STATE.md in cmdStats', { statePath, error: err.message });
611
+ }
595
612
 
596
613
  // Git stats
597
614
  let gitCommits = 0;
598
615
  let gitFirstCommitDate = null;
599
616
  try {
600
- const commitCount = execGit(cwd, ['rev-list', '--count', 'HEAD']);
617
+ const commitCount = await execGit(cwd, ['rev-list', '--count', 'HEAD']);
601
618
  gitCommits = parseInt(commitCount.trim(), 10) || 0;
602
- const firstDate = execGit(cwd, ['log', '--reverse', '--format=%as', '--max-count=1']);
619
+ const firstDate = await execGit(cwd, ['log', '--reverse', '--format=%as', '--max-count=1']);
603
620
  gitFirstCommitDate = firstDate.trim() || null;
604
- } catch {}
621
+ } catch (err) {
622
+ logger.warn('Failed to compute git stats in cmdStats', { cwd, error: err.message });
623
+ }
605
624
 
606
625
  const completedPhases = phases.filter(p => p.status === 'Complete').length;
607
626
 
@@ -5,6 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { output, error } = require('./core.cjs');
8
+ const { safePlanningWriteSync } = require('./planning-write.cjs');
8
9
 
9
10
  const VALID_CONFIG_KEYS = new Set([
10
11
  'mode', 'granularity', 'parallelization', 'commit_docs', 'model_profile',
@@ -36,16 +37,22 @@ function cmdConfigEnsureSection(cwd, raw) {
36
37
  return;
37
38
  }
38
39
 
39
- // Detect Brave Search API key availability
40
+ // Detect Brave Search API key availability (prefer ~/.ez)
40
41
  const homedir = require('os').homedir();
41
- const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
42
- const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
43
-
44
- // Load user-level defaults from ~/.gsd/defaults.json if available
45
- const globalDefaultsPath = path.join(homedir, '.gsd', 'defaults.json');
42
+ const braveKeyCandidates = [
43
+ path.join(homedir, '.ez', 'brave_api_key'),
44
+ ];
45
+ const hasBraveSearch = !!(process.env.BRAVE_API_KEY || braveKeyCandidates.some(p => fs.existsSync(p)));
46
+
47
+ // Load user-level defaults from ~/.ez/defaults.json
48
+ const defaultsCandidates = [
49
+ path.join(homedir, '.ez', 'defaults.json'),
50
+ ];
51
+ const existingDefaultsPath = defaultsCandidates.find(p => fs.existsSync(p));
52
+ const globalDefaultsPath = existingDefaultsPath || defaultsCandidates[0];
46
53
  let userDefaults = {};
47
54
  try {
48
- if (fs.existsSync(globalDefaultsPath)) {
55
+ if (existingDefaultsPath) {
49
56
  userDefaults = JSON.parse(fs.readFileSync(globalDefaultsPath, 'utf-8'));
50
57
  // Migrate deprecated "depth" key to "granularity"
51
58
  if ('depth' in userDefaults && !('granularity' in userDefaults)) {
@@ -65,8 +72,8 @@ function cmdConfigEnsureSection(cwd, raw) {
65
72
  commit_docs: true,
66
73
  search_gitignored: false,
67
74
  branching_strategy: 'none',
68
- phase_branch_template: 'gsd/phase-{phase}-{slug}',
69
- milestone_branch_template: 'gsd/{milestone}-{slug}',
75
+ phase_branch_template: 'ez/phase-{phase}-{slug}',
76
+ milestone_branch_template: 'ez/{milestone}-{slug}',
70
77
  workflow: {
71
78
  research: true,
72
79
  plan_check: true,
@@ -83,7 +90,7 @@ function cmdConfigEnsureSection(cwd, raw) {
83
90
  };
84
91
 
85
92
  try {
86
- fs.writeFileSync(configPath, JSON.stringify(defaults, null, 2), 'utf-8');
93
+ safePlanningWriteSync(configPath, JSON.stringify(defaults, null, 2));
87
94
  const result = { created: true, path: '.planning/config.json' };
88
95
  output(result, raw, 'created');
89
96
  } catch (err) {
@@ -132,7 +139,7 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
132
139
 
133
140
  // Write back
134
141
  try {
135
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
142
+ safePlanningWriteSync(configPath, JSON.stringify(config, null, 2));
136
143
  const result = { updated: true, key: keyPath, value: parsedValue };
137
144
  output(result, raw, `${keyPath}=${parsedValue}`);
138
145
  } catch (err) {