@howlil/ez-agents 3.4.1 → 3.4.2

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 (102) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +7 -18
  3. package/bin/install.js +52 -10
  4. package/commands/ez/join-discord.md +18 -18
  5. package/ez-agents/bin/lib/assistant-adapter.cjs +264 -264
  6. package/ez-agents/bin/lib/audit-exec.cjs +7 -2
  7. package/ez-agents/bin/lib/circuit-breaker.cjs +118 -118
  8. package/ez-agents/bin/lib/config.cjs +190 -190
  9. package/ez-agents/bin/lib/file-lock.cjs +236 -236
  10. package/ez-agents/bin/lib/frontmatter.cjs +299 -299
  11. package/ez-agents/bin/lib/fs-utils.cjs +153 -153
  12. package/ez-agents/bin/lib/git-utils.cjs +203 -203
  13. package/ez-agents/bin/lib/index.cjs +113 -113
  14. package/ez-agents/bin/lib/init.cjs +757 -757
  15. package/ez-agents/bin/lib/logger.cjs +47 -17
  16. package/ez-agents/bin/lib/milestone.cjs +241 -241
  17. package/ez-agents/bin/lib/model-provider.cjs +241 -241
  18. package/ez-agents/bin/lib/phase.cjs +925 -925
  19. package/ez-agents/bin/lib/planning-write.cjs +107 -107
  20. package/ez-agents/bin/lib/retry.cjs +119 -119
  21. package/ez-agents/bin/lib/roadmap.cjs +306 -306
  22. package/ez-agents/bin/lib/safe-exec.cjs +90 -4
  23. package/ez-agents/bin/lib/safe-path.cjs +130 -130
  24. package/ez-agents/bin/lib/state.cjs +736 -736
  25. package/ez-agents/bin/lib/temp-file.cjs +239 -239
  26. package/ez-agents/bin/lib/template.cjs +223 -223
  27. package/ez-agents/bin/lib/test-file-lock.cjs +112 -112
  28. package/ez-agents/bin/lib/test-graceful.cjs +93 -93
  29. package/ez-agents/bin/lib/test-logger.cjs +60 -60
  30. package/ez-agents/bin/lib/test-safe-exec.cjs +38 -38
  31. package/ez-agents/bin/lib/test-safe-path.cjs +33 -33
  32. package/ez-agents/bin/lib/test-temp-file.cjs +125 -125
  33. package/ez-agents/bin/lib/timeout-exec.cjs +63 -63
  34. package/ez-agents/bin/lib/verify.cjs +15 -1
  35. package/ez-agents/references/checkpoints.md +776 -776
  36. package/ez-agents/references/continuation-format.md +249 -249
  37. package/ez-agents/references/questioning.md +162 -162
  38. package/ez-agents/references/tdd.md +263 -263
  39. package/ez-agents/templates/codebase/concerns.md +310 -310
  40. package/ez-agents/templates/codebase/conventions.md +307 -307
  41. package/ez-agents/templates/codebase/integrations.md +280 -280
  42. package/ez-agents/templates/codebase/stack.md +186 -186
  43. package/ez-agents/templates/codebase/testing.md +480 -480
  44. package/ez-agents/templates/config.json +37 -37
  45. package/ez-agents/templates/continue-here.md +78 -78
  46. package/ez-agents/templates/milestone-archive.md +123 -123
  47. package/ez-agents/templates/milestone.md +115 -115
  48. package/ez-agents/templates/requirements.md +231 -231
  49. package/ez-agents/templates/research-project/ARCHITECTURE.md +204 -204
  50. package/ez-agents/templates/research-project/FEATURES.md +147 -147
  51. package/ez-agents/templates/research-project/PITFALLS.md +200 -200
  52. package/ez-agents/templates/research-project/STACK.md +120 -120
  53. package/ez-agents/templates/research-project/SUMMARY.md +170 -170
  54. package/ez-agents/templates/retrospective.md +54 -54
  55. package/ez-agents/templates/roadmap.md +202 -202
  56. package/ez-agents/templates/summary-minimal.md +41 -41
  57. package/ez-agents/templates/summary-standard.md +48 -48
  58. package/ez-agents/templates/summary.md +248 -248
  59. package/ez-agents/templates/user-setup.md +311 -311
  60. package/ez-agents/templates/verification-report.md +322 -322
  61. package/ez-agents/workflows/add-phase.md +112 -112
  62. package/ez-agents/workflows/add-tests.md +351 -351
  63. package/ez-agents/workflows/add-todo.md +158 -158
  64. package/ez-agents/workflows/audit-milestone.md +332 -332
  65. package/ez-agents/workflows/autonomous.md +743 -743
  66. package/ez-agents/workflows/check-todos.md +177 -177
  67. package/ez-agents/workflows/cleanup.md +152 -152
  68. package/ez-agents/workflows/complete-milestone.md +766 -766
  69. package/ez-agents/workflows/diagnose-issues.md +219 -219
  70. package/ez-agents/workflows/discovery-phase.md +289 -289
  71. package/ez-agents/workflows/discuss-phase.md +762 -762
  72. package/ez-agents/workflows/execute-phase.md +468 -468
  73. package/ez-agents/workflows/execute-plan.md +483 -483
  74. package/ez-agents/workflows/health.md +159 -159
  75. package/ez-agents/workflows/help.md +492 -492
  76. package/ez-agents/workflows/insert-phase.md +130 -130
  77. package/ez-agents/workflows/list-phase-assumptions.md +178 -178
  78. package/ez-agents/workflows/map-codebase.md +316 -316
  79. package/ez-agents/workflows/new-milestone.md +384 -384
  80. package/ez-agents/workflows/new-project.md +1113 -1113
  81. package/ez-agents/workflows/node-repair.md +92 -92
  82. package/ez-agents/workflows/pause-work.md +122 -122
  83. package/ez-agents/workflows/plan-milestone-gaps.md +274 -274
  84. package/ez-agents/workflows/plan-phase.md +651 -651
  85. package/ez-agents/workflows/progress.md +382 -382
  86. package/ez-agents/workflows/quick.md +610 -610
  87. package/ez-agents/workflows/remove-phase.md +155 -155
  88. package/ez-agents/workflows/research-phase.md +74 -74
  89. package/ez-agents/workflows/resume-project.md +307 -307
  90. package/ez-agents/workflows/set-profile.md +81 -81
  91. package/ez-agents/workflows/settings.md +242 -242
  92. package/ez-agents/workflows/stats.md +57 -57
  93. package/ez-agents/workflows/transition.md +544 -544
  94. package/ez-agents/workflows/ui-phase.md +290 -290
  95. package/ez-agents/workflows/ui-review.md +157 -157
  96. package/ez-agents/workflows/update.md +320 -320
  97. package/ez-agents/workflows/validate-phase.md +167 -167
  98. package/ez-agents/workflows/verify-phase.md +243 -243
  99. package/ez-agents/workflows/verify-work.md +584 -584
  100. package/package.json +2 -3
  101. package/scripts/build-hooks.js +43 -43
  102. package/scripts/run-tests.cjs +29 -29
@@ -1,236 +1,236 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * EZ File Lock — File locking utility for concurrent write protection
5
- *
6
- * Uses proper-lockfile to prevent concurrent writes to planning files
7
- * Includes deadlock detection (30s timeout) and automatic lock cleanup
8
- *
9
- * Fallback: Uses simple lock file mechanism if proper-lockfile is not available
10
- *
11
- * Usage:
12
- * const { withLock, isLocked, ifUnlocked } = require('./file-lock.cjs');
13
- * await withLock('.planning/STATE.md', async () => {
14
- * // Safe to write - no other process can modify this file
15
- * });
16
- */
17
-
18
- const fs = require('fs');
19
- const path = require('path');
20
- const Logger = require('./logger.cjs');
21
- const logger = new Logger();
22
-
23
- // Try to load proper-lockfile, fallback to simple implementation
24
- let properLockfile = null;
25
- try {
26
- properLockfile = require('proper-lockfile');
27
- } catch (err) {
28
- logger.warn('proper-lockfile not available, using fallback implementation');
29
- }
30
-
31
- const DEFAULT_OPTIONS = {
32
- staleTime: 30000, // Lock considered stale after 30s
33
- update: 10000, // Update lock every 10s to prevent stale
34
- retries: {
35
- retries: 10,
36
- factor: 1.1,
37
- minTimeout: 100,
38
- maxTimeout: 1000,
39
- randomize: true
40
- }
41
- };
42
-
43
- // Simple lock file implementation (fallback)
44
- const LOCK_SUFFIX = '.lock';
45
- const lockHolders = new Map(); // Track locks held by this process
46
-
47
- /**
48
- * Get lock file path
49
- * @param {string} filePath - Original file path
50
- * @returns {string} - Lock file path
51
- */
52
- function getLockPath(filePath) {
53
- return filePath + LOCK_SUFFIX;
54
- }
55
-
56
- /**
57
- * Simple lock acquisition (fallback when proper-lockfile unavailable)
58
- */
59
- async function simpleLock(filePath, options = {}) {
60
- const lockPath = getLockPath(filePath);
61
- const startTime = Date.now();
62
- const timeout = options.timeout || 30000;
63
- const staleTime = options.staleTime || 30000;
64
-
65
- while (Date.now() - startTime < timeout) {
66
- try {
67
- // Check if lock file exists and is not stale
68
- if (fs.existsSync(lockPath)) {
69
- const stats = fs.statSync(lockPath);
70
- const age = Date.now() - stats.mtimeMs;
71
-
72
- if (age < staleTime) {
73
- // Lock is held by another process, wait and retry
74
- await new Promise(resolve => setTimeout(resolve, 100));
75
- continue;
76
- }
77
- // Lock is stale, remove it
78
- try {
79
- fs.unlinkSync(lockPath);
80
- } catch (err) {
81
- // Ignore - another process might have removed it
82
- }
83
- }
84
-
85
- // Try to create lock file (using 'wx' flag to fail if it exists)
86
- fs.writeFileSync(lockPath, JSON.stringify({
87
- pid: process.pid,
88
- timestamp: Date.now()
89
- }), { encoding: 'utf-8', flag: 'wx' });
90
-
91
- lockHolders.set(filePath, lockPath);
92
-
93
- return {
94
- release: async () => {
95
- try {
96
- if (fs.existsSync(lockPath)) {
97
- fs.unlinkSync(lockPath);
98
- }
99
- lockHolders.delete(filePath);
100
- } catch (err) {
101
- logger.warn(`Failed to release lock: ${lockPath}`, { error: err.message });
102
- }
103
- }
104
- };
105
- } catch (err) {
106
- // Lock acquisition failed, wait and retry
107
- await new Promise(resolve => setTimeout(resolve, 100));
108
- }
109
- }
110
-
111
- throw new Error(`Lock acquisition timed out after ${timeout}ms`);
112
- }
113
-
114
- /**
115
- * Execute an operation with file lock
116
- * @param {string} filePath - Path to file to lock
117
- * @param {Function} operation - Async function to execute while locked
118
- * @param {Object} options - Lock options
119
- * @returns {Promise<any>} - Result of operation
120
- */
121
- async function withLock(filePath, operation, options = {}) {
122
- const lockOptions = { ...DEFAULT_OPTIONS, ...options };
123
- const absolutePath = path.resolve(filePath);
124
-
125
- // Ensure file exists
126
- if (!fs.existsSync(absolutePath)) {
127
- const dir = path.dirname(absolutePath);
128
- if (!fs.existsSync(dir)) {
129
- fs.mkdirSync(dir, { recursive: true });
130
- }
131
- fs.writeFileSync(absolutePath, '', 'utf-8');
132
- }
133
-
134
- let release = null;
135
- const startTime = Date.now();
136
-
137
- try {
138
- logger.debug(`Attempting to acquire lock: ${absolutePath}`);
139
-
140
- if (properLockfile) {
141
- // Use proper-lockfile
142
- release = await properLockfile.lock(absolutePath, lockOptions);
143
- } else {
144
- // Use simple fallback
145
- const lockResult = await simpleLock(absolutePath, lockOptions);
146
- release = lockResult.release;
147
- }
148
-
149
- const acquireTime = Date.now() - startTime;
150
- logger.debug(`Lock acquired: ${absolutePath} (${acquireTime}ms)`);
151
-
152
- const result = await operation();
153
- return result;
154
- } catch (err) {
155
- const waitTime = Date.now() - startTime;
156
- if (err.message.includes('timed out') || err.code === 'ELOCKED') {
157
- logger.error(`File locked by another process: ${absolutePath}`, {
158
- waited: waitTime,
159
- timeout: lockOptions.timeout || 30000
160
- });
161
- throw new Error(`File locked: ${filePath} (waited ${waitTime}ms)`);
162
- }
163
- logger.error(`Lock acquisition failed: ${absolutePath}`, {
164
- error: err.message,
165
- waited: waitTime
166
- });
167
- throw err;
168
- } finally {
169
- if (release) {
170
- try {
171
- await release();
172
- logger.debug(`Lock released: ${absolutePath}`);
173
- } catch (err) {
174
- logger.warn(`Failed to release lock: ${absolutePath}`, {
175
- error: err.message
176
- });
177
- }
178
- }
179
- }
180
- }
181
-
182
- /**
183
- * Check if a file is currently locked
184
- * @param {string} filePath - Path to file
185
- * @returns {Promise<boolean>} - True if locked
186
- */
187
- async function isLocked(filePath) {
188
- const absolutePath = path.resolve(filePath);
189
-
190
- if (properLockfile) {
191
- if (!fs.existsSync(absolutePath)) {
192
- return false;
193
- }
194
- return properLockfile.check(absolutePath);
195
- }
196
-
197
- // Simple fallback
198
- const lockPath = getLockPath(filePath);
199
- if (!fs.existsSync(lockPath)) {
200
- return false;
201
- }
202
-
203
- // Check if lock is stale
204
- try {
205
- const stats = fs.statSync(lockPath);
206
- const age = Date.now() - stats.mtimeMs;
207
- const staleTime = 30000;
208
-
209
- if (age >= staleTime) {
210
- // Lock is stale, remove it
211
- fs.unlinkSync(lockPath);
212
- return false;
213
- }
214
- return true;
215
- } catch (err) {
216
- return false;
217
- }
218
- }
219
-
220
- /**
221
- * Execute operation only if file is not locked
222
- * @param {string} filePath - Path to file
223
- * @param {Function} operation - Async function to execute
224
- * @param {any} fallback - Value to return if file is locked
225
- * @returns {Promise<any>} - Result of operation or fallback
226
- */
227
- async function ifUnlocked(filePath, operation, fallback = null) {
228
- const locked = await isLocked(filePath);
229
- if (locked) {
230
- logger.debug(`File locked, using fallback: ${filePath}`);
231
- return fallback;
232
- }
233
- return withLock(filePath, operation);
234
- }
235
-
236
- module.exports = { withLock, isLocked, ifUnlocked };
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * EZ File Lock — File locking utility for concurrent write protection
5
+ *
6
+ * Uses proper-lockfile to prevent concurrent writes to planning files
7
+ * Includes deadlock detection (30s timeout) and automatic lock cleanup
8
+ *
9
+ * Fallback: Uses simple lock file mechanism if proper-lockfile is not available
10
+ *
11
+ * Usage:
12
+ * const { withLock, isLocked, ifUnlocked } = require('./file-lock.cjs');
13
+ * await withLock('.planning/STATE.md', async () => {
14
+ * // Safe to write - no other process can modify this file
15
+ * });
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const Logger = require('./logger.cjs');
21
+ const logger = new Logger();
22
+
23
+ // Try to load proper-lockfile, fallback to simple implementation
24
+ let properLockfile = null;
25
+ try {
26
+ properLockfile = require('proper-lockfile');
27
+ } catch (err) {
28
+ logger.warn('proper-lockfile not available, using fallback implementation');
29
+ }
30
+
31
+ const DEFAULT_OPTIONS = {
32
+ staleTime: 30000, // Lock considered stale after 30s
33
+ update: 10000, // Update lock every 10s to prevent stale
34
+ retries: {
35
+ retries: 10,
36
+ factor: 1.1,
37
+ minTimeout: 100,
38
+ maxTimeout: 1000,
39
+ randomize: true
40
+ }
41
+ };
42
+
43
+ // Simple lock file implementation (fallback)
44
+ const LOCK_SUFFIX = '.lock';
45
+ const lockHolders = new Map(); // Track locks held by this process
46
+
47
+ /**
48
+ * Get lock file path
49
+ * @param {string} filePath - Original file path
50
+ * @returns {string} - Lock file path
51
+ */
52
+ function getLockPath(filePath) {
53
+ return filePath + LOCK_SUFFIX;
54
+ }
55
+
56
+ /**
57
+ * Simple lock acquisition (fallback when proper-lockfile unavailable)
58
+ */
59
+ async function simpleLock(filePath, options = {}) {
60
+ const lockPath = getLockPath(filePath);
61
+ const startTime = Date.now();
62
+ const timeout = options.timeout || 30000;
63
+ const staleTime = options.staleTime || 30000;
64
+
65
+ while (Date.now() - startTime < timeout) {
66
+ try {
67
+ // Check if lock file exists and is not stale
68
+ if (fs.existsSync(lockPath)) {
69
+ const stats = fs.statSync(lockPath);
70
+ const age = Date.now() - stats.mtimeMs;
71
+
72
+ if (age < staleTime) {
73
+ // Lock is held by another process, wait and retry
74
+ await new Promise(resolve => setTimeout(resolve, 100));
75
+ continue;
76
+ }
77
+ // Lock is stale, remove it
78
+ try {
79
+ fs.unlinkSync(lockPath);
80
+ } catch (err) {
81
+ // Ignore - another process might have removed it
82
+ }
83
+ }
84
+
85
+ // Try to create lock file (using 'wx' flag to fail if it exists)
86
+ fs.writeFileSync(lockPath, JSON.stringify({
87
+ pid: process.pid,
88
+ timestamp: Date.now()
89
+ }), { encoding: 'utf-8', flag: 'wx' });
90
+
91
+ lockHolders.set(filePath, lockPath);
92
+
93
+ return {
94
+ release: async () => {
95
+ try {
96
+ if (fs.existsSync(lockPath)) {
97
+ fs.unlinkSync(lockPath);
98
+ }
99
+ lockHolders.delete(filePath);
100
+ } catch (err) {
101
+ logger.warn(`Failed to release lock: ${lockPath}`, { error: err.message });
102
+ }
103
+ }
104
+ };
105
+ } catch (err) {
106
+ // Lock acquisition failed, wait and retry
107
+ await new Promise(resolve => setTimeout(resolve, 100));
108
+ }
109
+ }
110
+
111
+ throw new Error(`Lock acquisition timed out after ${timeout}ms`);
112
+ }
113
+
114
+ /**
115
+ * Execute an operation with file lock
116
+ * @param {string} filePath - Path to file to lock
117
+ * @param {Function} operation - Async function to execute while locked
118
+ * @param {Object} options - Lock options
119
+ * @returns {Promise<any>} - Result of operation
120
+ */
121
+ async function withLock(filePath, operation, options = {}) {
122
+ const lockOptions = { ...DEFAULT_OPTIONS, ...options };
123
+ const absolutePath = path.resolve(filePath);
124
+
125
+ // Ensure file exists
126
+ if (!fs.existsSync(absolutePath)) {
127
+ const dir = path.dirname(absolutePath);
128
+ if (!fs.existsSync(dir)) {
129
+ fs.mkdirSync(dir, { recursive: true });
130
+ }
131
+ fs.writeFileSync(absolutePath, '', 'utf-8');
132
+ }
133
+
134
+ let release = null;
135
+ const startTime = Date.now();
136
+
137
+ try {
138
+ logger.debug(`Attempting to acquire lock: ${absolutePath}`);
139
+
140
+ if (properLockfile) {
141
+ // Use proper-lockfile
142
+ release = await properLockfile.lock(absolutePath, lockOptions);
143
+ } else {
144
+ // Use simple fallback
145
+ const lockResult = await simpleLock(absolutePath, lockOptions);
146
+ release = lockResult.release;
147
+ }
148
+
149
+ const acquireTime = Date.now() - startTime;
150
+ logger.debug(`Lock acquired: ${absolutePath} (${acquireTime}ms)`);
151
+
152
+ const result = await operation();
153
+ return result;
154
+ } catch (err) {
155
+ const waitTime = Date.now() - startTime;
156
+ if (err.message.includes('timed out') || err.code === 'ELOCKED') {
157
+ logger.error(`File locked by another process: ${absolutePath}`, {
158
+ waited: waitTime,
159
+ timeout: lockOptions.timeout || 30000
160
+ });
161
+ throw new Error(`File locked: ${filePath} (waited ${waitTime}ms)`);
162
+ }
163
+ logger.error(`Lock acquisition failed: ${absolutePath}`, {
164
+ error: err.message,
165
+ waited: waitTime
166
+ });
167
+ throw err;
168
+ } finally {
169
+ if (release) {
170
+ try {
171
+ await release();
172
+ logger.debug(`Lock released: ${absolutePath}`);
173
+ } catch (err) {
174
+ logger.warn(`Failed to release lock: ${absolutePath}`, {
175
+ error: err.message
176
+ });
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Check if a file is currently locked
184
+ * @param {string} filePath - Path to file
185
+ * @returns {Promise<boolean>} - True if locked
186
+ */
187
+ async function isLocked(filePath) {
188
+ const absolutePath = path.resolve(filePath);
189
+
190
+ if (properLockfile) {
191
+ if (!fs.existsSync(absolutePath)) {
192
+ return false;
193
+ }
194
+ return properLockfile.check(absolutePath);
195
+ }
196
+
197
+ // Simple fallback
198
+ const lockPath = getLockPath(filePath);
199
+ if (!fs.existsSync(lockPath)) {
200
+ return false;
201
+ }
202
+
203
+ // Check if lock is stale
204
+ try {
205
+ const stats = fs.statSync(lockPath);
206
+ const age = Date.now() - stats.mtimeMs;
207
+ const staleTime = 30000;
208
+
209
+ if (age >= staleTime) {
210
+ // Lock is stale, remove it
211
+ fs.unlinkSync(lockPath);
212
+ return false;
213
+ }
214
+ return true;
215
+ } catch (err) {
216
+ return false;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Execute operation only if file is not locked
222
+ * @param {string} filePath - Path to file
223
+ * @param {Function} operation - Async function to execute
224
+ * @param {any} fallback - Value to return if file is locked
225
+ * @returns {Promise<any>} - Result of operation or fallback
226
+ */
227
+ async function ifUnlocked(filePath, operation, fallback = null) {
228
+ const locked = await isLocked(filePath);
229
+ if (locked) {
230
+ logger.debug(`File locked, using fallback: ${filePath}`);
231
+ return fallback;
232
+ }
233
+ return withLock(filePath, operation);
234
+ }
235
+
236
+ module.exports = { withLock, isLocked, ifUnlocked };