feat: PR 审核流程 + 自动合并 + macOS 通知 + 订阅额度透传 + 归档详情
PR 流程(开发→提PR→CodeReview→安全审计→审核→通过即合并): - reviewer 拆分为 code review 与安全审计两个独立 CC run(kind=reviewer/security), TaskResult 四字段(summary/verdict + securitySummary/securityVerdict),闸上两节报告两枚结论徽章 - executor/merge.ts: 通过即合并——临时 worktree 内 merge --no-ff,绝不碰用户工作区/不 push; 冲突安全拒绝(报冲突文件);重复合并幂等;合并后回收执行 worktree + 删分支 - decide 路由 merge:false 逃生口 + 看板「仅通过」按钮(目标分支被工作区检出时用) 通知(daemon/notify.ts): - macOS 原生通知: 进审核闸(复审建议拒绝标⚠)/连续失败需人工/合并完成 - 同任务同类型 60s 抑制、osascript 转义截断、MAESTRO_NOTIFY=0 关闭 订阅额度透传(daemon/usage.ts): - OAuth usage API(与 Claude Code/claude-hud 同源),凭证 keychain→内存零泄漏 - 60s 成败双缓存+并发去重+5s 超时,失败降级 null - GET /api/agents 顶层 usage 字段;Agent 面板显示 5h/周用量条+重置倒计时(>80%琥珀/>95%红) 看板与生命周期: - 归档区: 深度1整树完成沉底,时间倒序分页(尺寸 chip 10/20/50/100 置底) - 归档详情对话框: 全属性/执行历史与时长/审批记录/状态流转时间线(GET /api/tasks/:id/events) - 容器收口: 已拆解 Hard 子任务全 done 自动 done(afterDone 逐级向上) - 同步按钮收进配置面板;执行白名单扩测试命令(npm/go/shellcheck/make/pytest) - Agent 面板显示调度模式与各复杂度模型;被依赖阻塞→被阻塞 测试: 74/74(新增 merge 6/notify 10/usage 7/容器收口/双复审适配) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+42
-4
@@ -7,6 +7,9 @@ import type { TaskStatus } from '../model/status.js';
|
||||
import type { Project, Autonomy } from '../model/types.js';
|
||||
import { syncProject, hasTodoJson } from '../sync/todo-sync.js';
|
||||
import { resolvedExecutorModels } from '../executor/models.js';
|
||||
import { mergeBranch } from '../executor/merge.js';
|
||||
import { git, removeWorktree } from '../executor/worktree.js';
|
||||
import { createUsageFetcher, type UsageInfo } from '../daemon/usage.js';
|
||||
|
||||
/** Project 出参:附加 hasTodoJson(<repoPath>/todo/todo.json 是否存在,每次序列化时算) */
|
||||
function projectOut(p: Project): Project & { hasTodoJson: boolean } {
|
||||
@@ -16,6 +19,8 @@ function projectOut(p: Project): Project & { hasTodoJson: boolean } {
|
||||
export interface ApiOptions {
|
||||
store: Store;
|
||||
logger?: boolean;
|
||||
/** Claude 订阅额度查询(测试注入;默认直连 OAuth usage API,60s 缓存) */
|
||||
getUsage?: () => Promise<UsageInfo | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,7 +80,9 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
|
||||
});
|
||||
|
||||
// ---------- Agents(每项目一条;active 来自 runs 表 status='started',执行器 Phase 2 前通常为空) ----------
|
||||
app.get('/api/agents', () => {
|
||||
// usage = Claude 订阅额度(执行 agent 烧的就是这个池子);查询失败 → null,前端降级显示
|
||||
const getUsage = opts.getUsage ?? createUsageFetcher({ log: app.log });
|
||||
app.get('/api/agents', async () => {
|
||||
const runs = store.activeRuns();
|
||||
const byProject = new Map<string, ActiveRun[]>();
|
||||
for (const r of runs) {
|
||||
@@ -95,7 +102,7 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
|
||||
runId: r.runId, taskId: r.taskId, taskTitle: r.taskTitle, kind: r.kind, startedAt: r.startedAt,
|
||||
})),
|
||||
}));
|
||||
return { totalActive: runs.length, agents };
|
||||
return { totalActive: runs.length, agents, usage: await getUsage() };
|
||||
});
|
||||
|
||||
app.get('/api/projects/:id/tasks', (req) => {
|
||||
@@ -197,10 +204,41 @@ export function buildServer(opts: ApiOptions): FastifyInstance {
|
||||
});
|
||||
|
||||
// 审批闸:accept / reject(reject 必带 reason)
|
||||
app.post('/api/tasks/:id/decide', (req) => {
|
||||
// exec 闸的 accept = 通过并合并(PR 闭环):先 merge 再 decide;merge 失败 → 400,任务保留在审核闸。
|
||||
app.post('/api/tasks/:id/decide', async (req) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const b = req.body as { action?: string; actor?: string; reason?: string | null };
|
||||
const b = req.body as { action?: string; actor?: string; reason?: string | null; merge?: boolean };
|
||||
if (b?.action !== 'accept' && b?.action !== 'reject') throw new StoreError('action 必须是 accept|reject');
|
||||
|
||||
const task = store.getTask(id);
|
||||
// merge:false = 仅通过不合并(逃生口:如目标分支正被用户工作区检出导致自动合并不可用)
|
||||
if (task && task.status === 'exec_review' && b.action === 'accept' && task.result?.branch && b.merge !== false) {
|
||||
const project = store.getProject(task.projectId);
|
||||
if (!project) throw new StoreError(`项目不存在: ${task.projectId}`);
|
||||
const { branch, worktree } = task.result;
|
||||
const mr = await mergeBranch(project.repoPath, branch, project.defaultBranch, task.id);
|
||||
if (!mr.ok) throw new StoreError(`合并失败:${mr.error}(任务保留在审核闸)`);
|
||||
|
||||
store.decide(id, 'accept', b.actor ?? 'user', b.reason ?? null);
|
||||
// 合并产物记录:复用 prUrl 字段写 merged:<mergeCommit>
|
||||
store.setResult(id, { ...task.result, prUrl: `merged:${mr.mergeCommit}` });
|
||||
// 异步回收:执行 worktree + 已合并的任务分支(失败只记日志,不影响响应)
|
||||
void (async () => {
|
||||
try {
|
||||
if (worktree) await removeWorktree(project.repoPath, worktree);
|
||||
} catch (e) {
|
||||
app.log.error(`任务 ${id} 合并后清理 worktree 失败(不影响结果):${(e as Error).message}`);
|
||||
}
|
||||
try {
|
||||
await git(project.repoPath, ['branch', '-d', branch]);
|
||||
} catch (e) {
|
||||
app.log.error(`任务 ${id} 合并后删除分支 ${branch} 失败(不影响结果):${(e as Error).message}`);
|
||||
}
|
||||
})();
|
||||
return store.getTask(id);
|
||||
}
|
||||
|
||||
// 非 exec 闸(或 reject / 无分支结果):行为完全不变
|
||||
return store.decide(id, b.action, b.actor ?? 'user', b.reason ?? null);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user