openmatrix 0.1.92 → 0.1.93

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.
@@ -166,9 +166,9 @@ ${agentPrompt.instructions}
166
166
 
167
167
  ## 完成要求
168
168
 
169
- 1. 完成任务后,更新任务状态文件: \`.openmatrix/tasks/${task.id}/task.json\`
170
- 2. 将执行结果写入: \`.openmatrix/tasks/${task.id}/artifacts/result.md\`
171
- 3. 如需审批,创建审批请求: \`.openmatrix/approvals/\` 目录
169
+ 1. 将执行结果写入: \`.openmatrix/tasks/${task.id}/artifacts/result.md\`
170
+ (任务状态由 openmatrix complete 命令管理,请勿直接修改 task.json)
171
+ 2. 如需审批,创建审批请求: \`.openmatrix/approvals/\` 目录
172
172
 
173
173
  注意: 任务完成后,由 Skill 调用 \`openmatrix complete\` 并传入 --summary 参数,
174
174
  该摘要会自动追加到全局 \`.openmatrix/context.md\` 供后续 Agent 参考。
@@ -115,15 +115,8 @@ exports.completeCommand = new commander_1.Command('complete')
115
115
 
116
116
  `;
117
117
  try {
118
- // 追加写入全局 context.md
119
- let existingContent = '';
120
- try {
121
- existingContent = await fs.readFile(contextFile, 'utf-8');
122
- }
123
- catch {
124
- // 文件不存在,创建新文件
125
- }
126
- await fs.writeFile(contextFile, existingContent + contextEntry, 'utf-8');
118
+ // 原子追加写入全局 context.md(O_APPEND flag 保证并发安全)
119
+ await fs.appendFile(contextFile, contextEntry, 'utf-8');
127
120
  }
128
121
  catch {
129
122
  // 忽略写入错误
@@ -46,6 +46,13 @@ export declare class GitCommitManager {
46
46
  * 获取未提交的文件列表
47
47
  */
48
48
  getUncommittedFiles(): Promise<string[]>;
49
+ /**
50
+ * 获取未提交的文件列表(带状态信息)
51
+ */
52
+ getUncommittedFilesWithStatus(): Promise<{
53
+ status: 'new' | 'modified' | 'deleted';
54
+ path: string;
55
+ }[]>;
49
56
  /**
50
57
  * 获取已修改的文件差异统计
51
58
  */
@@ -61,17 +68,29 @@ export declare class GitCommitManager {
61
68
  * 生成提交信息
62
69
  *
63
70
  * 格式规范:
64
- * <type>: 简短描述
71
+ * feat(TASK-001): 简短描述
72
+ *
73
+ * 实现内容:
74
+ * 模块A: 功能描述
75
+ * 模块B: 功能描述
65
76
  *
66
- * 改动点1
67
- * 改动点2
77
+ * 新增文件:
78
+ * model/xxx.go
79
+ * service/xxx.go
68
80
  *
69
- * 影响范围: 模块/功能
70
- * 文件改动: 文件1, 文件2
81
+ * 修改文件:
82
+ * main.go: 路由注册和handler初始化
71
83
  *
72
84
  * Co-Authored-By: OpenMatrix https://github.com/bigfish1913/openmatrix
73
85
  */
74
- generateCommitMessage(info: CommitInfo): string;
86
+ generateCommitMessage(info: CommitInfo, filesWithStatus?: {
87
+ status: 'new' | 'modified' | 'deleted';
88
+ path: string;
89
+ }[]): string;
90
+ /**
91
+ * 按目录分组文件
92
+ */
93
+ private groupFilesByDirectory;
75
94
  /**
76
95
  * 执行提交
77
96
  */
@@ -112,6 +112,32 @@ class GitCommitManager {
112
112
  return [];
113
113
  }
114
114
  }
115
+ /**
116
+ * 获取未提交的文件列表(带状态信息)
117
+ */
118
+ async getUncommittedFilesWithStatus() {
119
+ try {
120
+ const { stdout } = await execAsync('git status --porcelain', { cwd: this.repoPath });
121
+ return stdout
122
+ .split('\n')
123
+ .filter(line => line.trim())
124
+ .map(line => {
125
+ const statusCode = line.slice(0, 2);
126
+ const filePath = line.slice(3).trim();
127
+ let status = 'modified';
128
+ if (statusCode.includes('?') || statusCode.includes('A')) {
129
+ status = 'new';
130
+ }
131
+ else if (statusCode.includes('D')) {
132
+ status = 'deleted';
133
+ }
134
+ return { status, path: filePath };
135
+ });
136
+ }
137
+ catch {
138
+ return [];
139
+ }
140
+ }
115
141
  /**
116
142
  * 获取已修改的文件差异统计
117
143
  */
@@ -173,17 +199,22 @@ class GitCommitManager {
173
199
  * 生成提交信息
174
200
  *
175
201
  * 格式规范:
176
- * <type>: 简短描述
202
+ * feat(TASK-001): 简短描述
177
203
  *
178
- * 改动点1
179
- * 改动点2
204
+ * 实现内容:
205
+ * 模块A: 功能描述
206
+ * 模块B: 功能描述
180
207
  *
181
- * 影响范围: 模块/功能
182
- * 文件改动: 文件1, 文件2
208
+ * 新增文件:
209
+ * model/xxx.go
210
+ * service/xxx.go
211
+ *
212
+ * 修改文件:
213
+ * main.go: 路由注册和handler初始化
183
214
  *
184
215
  * Co-Authored-By: OpenMatrix https://github.com/bigfish1913/openmatrix
185
216
  */
186
- generateCommitMessage(info) {
217
+ generateCommitMessage(info, filesWithStatus) {
187
218
  const lines = [];
188
219
  // 类型映射:根据 phase 确定提交类型
189
220
  const phaseToType = {
@@ -201,7 +232,7 @@ class GitCommitManager {
201
232
  // 格式: feat(TASK-001): 简短描述
202
233
  lines.push(`${commitType}(${info.taskId}): ${title}`);
203
234
  lines.push('');
204
- // Phase 描述作为改动点
235
+ // Phase 描述
205
236
  const phaseDescriptions = {
206
237
  tdd: '编写测试用例',
207
238
  develop: '实现功能代码',
@@ -214,21 +245,69 @@ class GitCommitManager {
214
245
  lines.push('');
215
246
  lines.push(`影响范围: ${info.impactScope.join('、')}`);
216
247
  }
217
- // 文件改动
218
- const changedFiles = info.changes.slice(0, 5).map(f => {
219
- const parts = f.split('/');
220
- return parts.length > 2 ? parts.slice(-2).join('/') : f;
221
- });
222
- if (changedFiles.length > 0) {
223
- const fileSummary = changedFiles.join(', ');
224
- const suffix = info.changes.length > 5 ? ` 等 ${info.changes.length} 个文件` : '';
225
- lines.push(`文件改动: ${fileSummary}${suffix}`);
248
+ // 按新增/修改分类列出文件
249
+ if (filesWithStatus && filesWithStatus.length > 0) {
250
+ const newFiles = filesWithStatus.filter(f => f.status === 'new').map(f => f.path);
251
+ const modifiedFiles = filesWithStatus.filter(f => f.status === 'modified').map(f => f.path);
252
+ const deletedFiles = filesWithStatus.filter(f => f.status === 'deleted').map(f => f.path);
253
+ if (newFiles.length > 0) {
254
+ lines.push('');
255
+ lines.push('新增文件:');
256
+ // 按目录分组
257
+ const grouped = this.groupFilesByDirectory(newFiles);
258
+ for (const [dir, files] of grouped) {
259
+ lines.push(`${dir ? dir + '/' : ''}${files.join(', ')}`);
260
+ }
261
+ }
262
+ if (modifiedFiles.length > 0) {
263
+ lines.push('');
264
+ lines.push('修改文件:');
265
+ const grouped = this.groupFilesByDirectory(modifiedFiles);
266
+ for (const [dir, files] of grouped) {
267
+ lines.push(`${dir ? dir + '/' : ''}${files.join(', ')}`);
268
+ }
269
+ }
270
+ if (deletedFiles.length > 0) {
271
+ lines.push('');
272
+ lines.push('删除文件:');
273
+ for (const f of deletedFiles) {
274
+ lines.push(f);
275
+ }
276
+ }
277
+ }
278
+ else if (info.changes.length > 0) {
279
+ // 回退:无状态信息时使用 changes 列表
280
+ const changedFiles = info.changes.slice(0, 10).map(f => {
281
+ const parts = f.split('/');
282
+ return parts.length > 2 ? parts.slice(-2).join('/') : f;
283
+ });
284
+ lines.push('');
285
+ lines.push(`文件改动: ${changedFiles.join(', ')}`);
286
+ if (info.changes.length > 10) {
287
+ lines.push(`...等 ${info.changes.length} 个文件`);
288
+ }
226
289
  }
227
290
  lines.push('');
228
291
  // Co-Author
229
292
  lines.push(`Co-Authored-By: OpenMatrix https://github.com/bigfish1913/openmatrix`);
230
293
  return lines.join('\n');
231
294
  }
295
+ /**
296
+ * 按目录分组文件
297
+ */
298
+ groupFilesByDirectory(files) {
299
+ const groups = new Map();
300
+ for (const file of files) {
301
+ const parts = file.split('/');
302
+ const dir = parts.length > 1 ? parts.slice(0, -1).join('/') : '';
303
+ const fileName = parts[parts.length - 1];
304
+ if (!groups.has(dir)) {
305
+ groups.set(dir, []);
306
+ }
307
+ groups.get(dir).push(fileName);
308
+ }
309
+ return groups;
310
+ }
232
311
  /**
233
312
  * 执行提交
234
313
  */
@@ -251,6 +330,8 @@ class GitCommitManager {
251
330
  }
252
331
  // 分析影响范围
253
332
  const impactScope = await this.analyzeImpactScope(files);
333
+ // 获取文件状态信息(新增/修改/删除)
334
+ const filesWithStatus = await this.getUncommittedFilesWithStatus();
254
335
  // 更新 commit info
255
336
  const fullInfo = {
256
337
  ...info,
@@ -258,7 +339,7 @@ class GitCommitManager {
258
339
  impactScope: info.impactScope.length > 0 ? info.impactScope : impactScope
259
340
  };
260
341
  // 生成提交信息
261
- const commitMessage = this.generateCommitMessage(fullInfo);
342
+ const commitMessage = this.generateCommitMessage(fullInfo, filesWithStatus);
262
343
  // 添加文件 - 使用 git add . 而不是 git add -A
263
344
  // git add . 只添加当前目录及子目录的文件,不会添加上级目录的文件
264
345
  // 同时通过 .gitignore 排除不需要的文件
@@ -1,9 +1,14 @@
1
1
  export declare class FileStore {
2
2
  private basePath;
3
3
  constructor(basePath: string);
4
+ getBasePath(): string;
4
5
  ensureDir(path: string): Promise<void>;
5
6
  writeJson<T>(path: string, data: T): Promise<void>;
6
7
  writeMarkdown(path: string, content: string): Promise<void>;
8
+ /**
9
+ * 原子追加写入(使用 O_APPEND flag,内核保证追加原子性)
10
+ */
11
+ appendFile(filePath: string, content: string): Promise<void>;
7
12
  /**
8
13
  * 读取 JSON 文件
9
14
  * @returns 文件内容,如果文件不存在则返回 null;其他错误会抛出异常
@@ -9,6 +9,9 @@ class FileStore {
9
9
  constructor(basePath) {
10
10
  this.basePath = basePath;
11
11
  }
12
+ getBasePath() {
13
+ return this.basePath;
14
+ }
12
15
  async ensureDir(path) {
13
16
  const fullPath = (0, path_1.join)(this.basePath, path);
14
17
  await (0, promises_1.mkdir)(fullPath, { recursive: true });
@@ -23,6 +26,14 @@ class FileStore {
23
26
  await (0, promises_1.mkdir)((0, path_1.dirname)(fullPath), { recursive: true });
24
27
  await (0, promises_1.writeFile)(fullPath, content, 'utf-8');
25
28
  }
29
+ /**
30
+ * 原子追加写入(使用 O_APPEND flag,内核保证追加原子性)
31
+ */
32
+ async appendFile(filePath, content) {
33
+ const fullPath = (0, path_1.join)(this.basePath, filePath);
34
+ await (0, promises_1.mkdir)((0, path_1.dirname)(fullPath), { recursive: true });
35
+ await (0, promises_1.appendFile)(fullPath, content, 'utf-8');
36
+ }
26
37
  /**
27
38
  * 读取 JSON 文件
28
39
  * @returns 文件内容,如果文件不存在则返回 null;其他错误会抛出异常
@@ -2,12 +2,15 @@ import type { GlobalState, Task, Approval, ApprovalStatus } from '../types/index
2
2
  export declare class StateManager {
3
3
  private store;
4
4
  private stateCache;
5
- private writeLock;
5
+ private lockDepth;
6
6
  constructor(basePath: string);
7
7
  /**
8
- * 串行化异步写操作,防止并发 read-modify-write 竞态
8
+ * 跨进程文件锁 防止多个 openmatrix CLI 进程同时读写 state.json
9
+ *
10
+ * 使用 O_EXCL | O_CREAT 原子创建锁文件,Windows/Linux/macOS 均支持
11
+ * 支持可重入:同进程内嵌套调用(如 updateTask → updateTaskStatistics)直接执行
9
12
  */
10
- private withLock;
13
+ private withFileLock;
11
14
  initialize(): Promise<void>;
12
15
  getState(): Promise<GlobalState>;
13
16
  updateState(updates: Partial<GlobalState>): Promise<void>;
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.StateManager = void 0;
4
4
  const file_store_js_1 = require("./file-store.js");
5
+ const promises_1 = require("fs/promises");
6
+ const path_1 = require("path");
5
7
  const DEFAULT_CONFIG = {
6
8
  timeout: 120,
7
9
  maxRetries: 3,
@@ -12,23 +14,44 @@ const DEFAULT_CONFIG = {
12
14
  class StateManager {
13
15
  store;
14
16
  stateCache = null;
15
- writeLock = Promise.resolve();
17
+ lockDepth = 0; // 可重入:同一进程内嵌套调用不阻塞
16
18
  constructor(basePath) {
17
19
  this.store = new file_store_js_1.FileStore(basePath);
18
20
  }
19
21
  /**
20
- * 串行化异步写操作,防止并发 read-modify-write 竞态
22
+ * 跨进程文件锁 防止多个 openmatrix CLI 进程同时读写 state.json
23
+ *
24
+ * 使用 O_EXCL | O_CREAT 原子创建锁文件,Windows/Linux/macOS 均支持
25
+ * 支持可重入:同进程内嵌套调用(如 updateTask → updateTaskStatistics)直接执行
21
26
  */
22
- async withLock(fn) {
23
- const prev = this.writeLock;
24
- let resolve;
25
- this.writeLock = new Promise(r => { resolve = r; });
26
- await prev;
27
+ async withFileLock(fn) {
28
+ // 可重入:同一进程内嵌套调用直接执行
29
+ if (this.lockDepth > 0) {
30
+ return fn();
31
+ }
32
+ const lockPath = (0, path_1.join)(this.store.getBasePath(), '.lock');
33
+ const maxRetries = 50;
34
+ const retryDelay = 100;
35
+ for (let i = 0; i < maxRetries; i++) {
36
+ try {
37
+ const fd = await (0, promises_1.open)(lockPath, 'wx');
38
+ await fd.write(`${process.pid}\n`);
39
+ await fd.close();
40
+ break;
41
+ }
42
+ catch {
43
+ if (i === maxRetries - 1)
44
+ throw new Error('Cannot acquire state lock');
45
+ await new Promise(r => setTimeout(r, retryDelay));
46
+ }
47
+ }
48
+ this.lockDepth++;
27
49
  try {
28
50
  return await fn();
29
51
  }
30
52
  finally {
31
- resolve();
53
+ this.lockDepth--;
54
+ await (0, promises_1.unlink)(lockPath).catch(() => { });
32
55
  }
33
56
  }
34
57
  async initialize() {
@@ -83,7 +106,7 @@ class StateManager {
83
106
  return this.stateCache;
84
107
  }
85
108
  async updateState(updates) {
86
- await this.withLock(async () => {
109
+ await this.withFileLock(async () => {
87
110
  const state = await this.getState();
88
111
  // 确保 statistics 存在(兼容旧版本)
89
112
  if (!state.statistics) {
@@ -112,7 +135,7 @@ class StateManager {
112
135
  });
113
136
  }
114
137
  async createTask(input) {
115
- return this.withLock(async () => {
138
+ return this.withFileLock(async () => {
116
139
  const taskId = this.generateTaskId();
117
140
  const now = new Date().toISOString();
118
141
  const task = {
@@ -161,7 +184,7 @@ class StateManager {
161
184
  return task;
162
185
  }
163
186
  async updateTask(taskId, updates) {
164
- await this.withLock(async () => {
187
+ await this.withFileLock(async () => {
165
188
  const task = await this.getTask(taskId);
166
189
  if (!task)
167
190
  throw new Error(`Task ${taskId} not found`);
@@ -234,67 +257,69 @@ class StateManager {
234
257
  return await this.store.readMarkdown(`tasks/${taskId}/context.md`);
235
258
  }
236
259
  async updateTaskStatistics(oldStatus, newStatus) {
237
- // 直接写入状态,不经过 updateState(避免重入锁死锁)
238
- const state = await this.getState();
239
- const stats = { ...state.statistics };
240
- // 扩展统计字段(如果不存在则初始化)
241
- if (!('scheduled' in stats))
242
- stats.scheduled = 0;
243
- if (!('blocked' in stats))
244
- stats.blocked = 0;
245
- if (!('waiting' in stats))
246
- stats.waiting = 0;
247
- if (!('verify' in stats))
248
- stats.verify = 0;
249
- if (!('accept' in stats))
250
- stats.accept = 0;
251
- if (!('retry_queue' in stats))
252
- stats.retry_queue = 0;
253
- // Decrement old status count
254
- if (oldStatus === 'pending')
255
- stats.pending--;
256
- else if (oldStatus === 'scheduled')
257
- stats.scheduled--;
258
- else if (oldStatus === 'in_progress')
259
- stats.inProgress--;
260
- else if (oldStatus === 'blocked')
261
- stats.blocked--;
262
- else if (oldStatus === 'waiting')
263
- stats.waiting--;
264
- else if (oldStatus === 'verify')
265
- stats.verify--;
266
- else if (oldStatus === 'accept')
267
- stats.accept--;
268
- else if (oldStatus === 'completed')
269
- stats.completed--;
270
- else if (oldStatus === 'failed')
271
- stats.failed--;
272
- else if (oldStatus === 'retry_queue')
273
- stats.retry_queue--;
274
- // Increment new status count
275
- if (newStatus === 'pending')
276
- stats.pending++;
277
- else if (newStatus === 'scheduled')
278
- stats.scheduled++;
279
- else if (newStatus === 'in_progress')
280
- stats.inProgress++;
281
- else if (newStatus === 'blocked')
282
- stats.blocked++;
283
- else if (newStatus === 'waiting')
284
- stats.waiting++;
285
- else if (newStatus === 'verify')
286
- stats.verify++;
287
- else if (newStatus === 'accept')
288
- stats.accept++;
289
- else if (newStatus === 'completed')
290
- stats.completed++;
291
- else if (newStatus === 'failed')
292
- stats.failed++;
293
- else if (newStatus === 'retry_queue')
294
- stats.retry_queue++;
295
- const newState = { ...state, statistics: stats };
296
- await this.store.writeJson('state.json', newState);
297
- this.stateCache = newState;
260
+ await this.withFileLock(async () => {
261
+ // 从文件重新读取最新状态(不用缓存,避免 stale)
262
+ const state = await this.store.readJson('state.json') ?? await this.getState();
263
+ const stats = { ...state.statistics };
264
+ // 扩展统计字段(如果不存在则初始化)
265
+ if (!('scheduled' in stats))
266
+ stats.scheduled = 0;
267
+ if (!('blocked' in stats))
268
+ stats.blocked = 0;
269
+ if (!('waiting' in stats))
270
+ stats.waiting = 0;
271
+ if (!('verify' in stats))
272
+ stats.verify = 0;
273
+ if (!('accept' in stats))
274
+ stats.accept = 0;
275
+ if (!('retry_queue' in stats))
276
+ stats.retry_queue = 0;
277
+ // Decrement old status count
278
+ if (oldStatus === 'pending')
279
+ stats.pending--;
280
+ else if (oldStatus === 'scheduled')
281
+ stats.scheduled--;
282
+ else if (oldStatus === 'in_progress')
283
+ stats.inProgress--;
284
+ else if (oldStatus === 'blocked')
285
+ stats.blocked--;
286
+ else if (oldStatus === 'waiting')
287
+ stats.waiting--;
288
+ else if (oldStatus === 'verify')
289
+ stats.verify--;
290
+ else if (oldStatus === 'accept')
291
+ stats.accept--;
292
+ else if (oldStatus === 'completed')
293
+ stats.completed--;
294
+ else if (oldStatus === 'failed')
295
+ stats.failed--;
296
+ else if (oldStatus === 'retry_queue')
297
+ stats.retry_queue--;
298
+ // Increment new status count
299
+ if (newStatus === 'pending')
300
+ stats.pending++;
301
+ else if (newStatus === 'scheduled')
302
+ stats.scheduled++;
303
+ else if (newStatus === 'in_progress')
304
+ stats.inProgress++;
305
+ else if (newStatus === 'blocked')
306
+ stats.blocked++;
307
+ else if (newStatus === 'waiting')
308
+ stats.waiting++;
309
+ else if (newStatus === 'verify')
310
+ stats.verify++;
311
+ else if (newStatus === 'accept')
312
+ stats.accept++;
313
+ else if (newStatus === 'completed')
314
+ stats.completed++;
315
+ else if (newStatus === 'failed')
316
+ stats.failed++;
317
+ else if (newStatus === 'retry_queue')
318
+ stats.retry_queue++;
319
+ const newState = { ...state, statistics: stats };
320
+ await this.store.writeJson('state.json', newState);
321
+ this.stateCache = newState;
322
+ });
298
323
  }
299
324
  generateRunId() {
300
325
  const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmatrix",
3
- "version": "0.1.92",
3
+ "version": "0.1.93",
4
4
  "description": "AI Agent task orchestration system with Claude Code Skills integration",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",