Authored via multi-agent orchestration (PRD: Band A ∥ Band B → architect review → integrate; Plan: frontend ∥ backend ∥ security → review → integrate). - docs/FEATURE_WALKAWAY_WORKBENCH.md — PRD for 9 features: Band A (push+lock-screen approve, voice, quick-reply chips, activity timeline, stuck/idle alert) and Band B (read-only git diff, statusLine cost/context/PR gauges, UI worktree-create, plan-mode relay). - docs/PLAN_WALKAWAY_WORKBENCH.md — 27 tasks (waves W-1..W4), disjoint Owns, contract resolutions, 30-item security checklist, dispatch schedule, AC map. Planning only — no source changed.
51 KiB
v0.7 Walk-away Workbench — Implementation Plan (Phased)
版本: v0.7 · 来源真相:
FEATURE_WALKAWAY_WORKBENCH.md(PRD is source of truth for scope). 本文把 PRD 的 9 个特性 (A1–A5, B1–B4) 拆成细粒度、单-owner-per-shared-file、可多 agent 并行的任务, 合并了 FE / BE / Security 三份草案并已应用评审修正 (#1–#16)。 约定遵循PLAN.md: 稳定任务 ID +Owns:disjoint 文件 +Depends:+ 建议模型/隔离 + 依赖波次。 进度记录在PROGRESS_LOG.md(orchestrator 独写, subagent 只返回条目, G1)。
0. 多 Agent 并行规则 (开工前必读)
三条铁律 (同 PLAN.md §0, 此处不重复, 以 CLAUDE.md / PLAN.md 为准):
- 文件所有权独占 — 每任务
Owns:列出独占写的文件; 只有该任务可改这些文件。所有新共享类型回src/types.ts(T-types 独占冻结),模块内不本地重声明 (SP9)。 - 只依赖接口不依赖实现 — 任务间经
src/types.ts的 interface / 函数签名解耦; 接口在 W0 就绪后即可与实现方并行编码, 仅在集成/测试时汇合。 - LOG 由 orchestrator 独写 — subagent 不碰
PROGRESS_LOG.md; 把"条目模板"日志作为最终返回结果交回。遇文档未定义的歧义 → 停下返回[!] BLOCKED, 绝不猜 (尤其 B2/B4 的 R0 字段)。
执行 agent: module-builder (TDD 实现单任务)、module-reviewer (只读复查/验收)。每任务测试与实现同属一个 agent, 不要拆分。
1. 依赖关系与并行波次 (Waves)
W-1 研究 spike (阻断 B2/B4 字段冻结)
R0 statusLine schema + --permission-mode 值集 + ExitPlanMode 投递 + decision-JSON 能否带 mode
│
▼
W0 协调点 (串行, 阻塞下游)
T-types (冻结所有新共享类型 + 5 个契约决议见 §3) → T-config · T-spawn-env (可并行起草, 类型就绪即编码)
│
▼
W1 叶子模块 (全部并行, 文件 disjoint —— 并行度最高)
后端新文件: N-push · N-timeline · N-diff-be · N-statusline-be(待R0) · N-worktree · N-statusline-script(待R0) · T-hook-intake
前端新文件: N-push-ui · N-voice(+keybar.ts) · N-quickreply · N-timeline-ui · N-diff-ui · T-sw(+sw-push.js) · T-preview-grid
│
▼
W2 后端汇聚 (单 owner, T-manager → T-server-wire 串行; T-hooks-installer 并行)
T-manager (依赖 N-push/N-timeline) → T-server-wire (依赖全部 BE 任务 + T-manager) · T-hooks-installer
│
▼
W3 前端汇聚 (单 owner, T-termsession → T-tabs; T-projects-ui 并行)
T-termsession (B2/B4 捕获) → T-tabs (依赖全部 N-*-ui + T-preview-grid + T-termsession) · T-projects-ui
│
▼
W4 验证 (报告-only, 不跨 lane 改文件)
V-integration (真起 server + 临时 repo) · V-security (守卫/token/realpath 抽检) · V-fe-pwa (HTTPS push / 跨浏览器)
关键并行事实: W1 的 13 个任务零文件交叉, 全程可并行 (受 ~3–5 agent/批的甜区约束, 见 §6)。前端 N--ui 与后端 N- 在 W1 全程并行; 仅在 W3 T-tabs/T-projects-ui 汇合时才需要 BE 端点就绪。
显式依赖边 (PRD §7.5): A5 → A1(notifyService); B4-FR7 → A1(背景通知); B2/B4 → R0(schema); T-server-wire → {N-push, N-diff-be, N-statusline-be, N-worktree, T-hook-intake, T-manager}; T-tabs → {T-server-wire 契约, T-termsession, T-preview-grid, 全部 N-*-ui}; 所有 → T-types。
2. 协调点 (Shared Files — 触碰顺序)
每个共享文件只有一个 owner 任务 (C3 修正)。下表是触碰顺序; 任何其它任务只读 import, 不得编辑。
| 共享文件 | 唯一 owner | 波次 | 合并的特性改动 |
|---|---|---|---|
src/types.ts |
T-types | W0 | 全部新共享类型 + §3 的 5 个契约决议。必须最先冻结, 否则下游编译失败 |
src/config.ts |
T-config | W0 | 21 个新 env 解析 + L5 --max-time>permTimeoutMs 校验 |
src/session/session.ts |
T-spawn-env | W0 | B2 WEBTERM_STATUSLINE_URL + A1 WEBTERM_NTFY_* 注入; session 字段初始化; A5 stuck 重新武装 (见 §3.5 协调) |
src/http/hook.ts |
T-hook-intake | W1 | A4 扩展摄入 (HookEventFull: 保留 at/tool_input/eventClass); B4 gate='plan' 识别 |
src/session/manager.ts |
T-manager | W2 | A4 timeline append · B2 telemetry 存+广播+晚加入补发(M3) · A5 sweepStuck()(H4) · B4 gate · manager↔notifyService 注入(M5) |
src/server.ts |
T-server-wire | W2 | A1 /push/*+/hook/decision+改 hold 门(C1)+能力token · A4 /…/events · A5 reaper sweep · B1 /projects/diff · B2 /hook/status · B3 /projects/worktree · B4 扩 /hook/permission gate + approve mode + auto 拒绝 · GET /config/ui(#4) |
scripts/setup-hooks.mjs |
T-hooks-installer | W2 | A4 补 PostToolUse(H1) · A1 ntfy/Pushover 桥(env token, H3) · B2 statusLine 段(R0) · L5 --max-time 取自 config |
public/preview-grid.ts |
T-preview-grid | W1 | (#6) renderTelemetryGauge · renderStatusBadge · statusText('stuck'); 冻结导出签名供 W1 importer 安全使用 |
public/terminal-session.ts |
T-termsession | W3 | B2 telemetry 捕获+onTelemetry · B4 pendingGate 捕获 + approve(mode?) · case 'telemetry' + exhaustiveness |
public/tabs.ts |
T-tabs | W3 | B2 仪表 · A5 stuck 徽标 · B4 三按钮审批+mode选择+cmd · A1/A2/A3/A4 面板挂载 · SP10 单一 refreshTab owner(M4) |
public/projects.ts |
T-projects-ui | W3 | B1 diff 入口 · B3 New-worktree 表单 · A4 timeline 区 |
public/keybar.ts |
N-voice (#7) | W1 | A2 mountKeybar(onSend, opts?) 加 onVoiceTrigger?; 仅 N-voice 编辑, 其它任务只 consume mountKeybar |
public/sw.js + public/sw-push.js |
T-sw | W1 | A1 push/notificationclick wiring (sw.js) + 纯可测 helpers (sw-push.js, #11) |
不在任何 Owns: 的共享文件:
docs/PROGRESS_LOG.md(orchestrator 独写)。
3. 契约决议 (Contract Resolutions) — T-types 必须先冻结
评审发现 FE↔BE 草案有 5 处类型契约冲突 (会编译失败或运行期静默错)。这些必须在 W1 派发前于 T-types 一次性定死, 否则前后端各写各的:
3.1 TimelineEvent — 用派生语义 class (评审 #1)
BE 草案用 eventClass(原始 hook 名), FE 用 class(语义集) —— 字段名与语义都冲突。决议: 服务端 src/session/timeline.ts 的 deriveClass() 把原始 hook 名映射为语义类, FE 直接消费, 不自造。
export type TimelineClass = 'tool' | 'waiting' | 'done' | 'stuck' | 'user';
export interface TimelineEvent {
at: number; // 服务端 Date.now()
class: TimelineClass; // 服务端派生的语义类 (FE icon/着色用)
toolName?: string; // sanitize 后的 tool_name (≤200, 去控制字符)
label: string; // 服务端派生人话 ("ran Bash", "edited 3 files")
}
3.2 DiffLine.kind — 一种拼写 + diff 解析只在后端 (评审 #2)
BE 用 '+'|'-'|..., FE 测试用 'added'|'removed'|... —— 冲突。决议: 用语义词拼写; 解析全在 src/http/diff.ts (N-diff-be), public/diff.ts (N-diff-ui) render-only, 删除 FE 的 parseUnifiedDiff/parseNumstat/mergeDiffStats (DRY)。
export type DiffLineKind = 'added' | 'removed' | 'context' | 'hunk' | 'meta';
export interface DiffLine { kind: DiffLineKind; text: string }
export interface DiffHunk { header: string; lines: DiffLine[] }
export type FileStatus = 'modified' | 'added' | 'deleted' | 'renamed' | 'binary' | 'untracked';
export interface DiffFile { oldPath: string; newPath: string; status: FileStatus; added: number; removed: number; binary: boolean; hunks: DiffHunk[] }
export interface DiffResult { files: DiffFile[]; staged: boolean; truncated: boolean }
3.3 Push payload — 一种 shape, FE 读 cls (评审 #3)
BE 发 {cls,...}, FE sw.js 读 payload.class/payload.detail —— 字段名不符且缺 detail。决议 (PRD A1.4 契约):
// push-service 发 / sw-push.js 读 (同一 shape):
interface PushPayload { sessionId: string; toolName?: string; detail?: string; token?: string; cls: NotifyClass }
export type NotifyClass = 'needs-input' | 'done' | 'stuck';
3.4 ClaudeStatus 加 'stuck' + 服务端广播 (评审 #5)
A5 徽标 keys off claudeStatus==='stuck' (AC-A5.5), 但 BE 草案只 notify('stuck'), 既不置 status 也不广播。决议:
- T-types:
ClaudeStatus = 'working' | 'waiting' | 'idle' | 'unknown' | 'stuck'。 - T-manager
sweepStuck(): 置session.claudeStatus='stuck'并broadcast({type:'status',status:'stuck'})(不止 notify)。 - 重新武装 (T-spawn-env, §3.5): 新输出到达时若 status 仍是
'stuck'→ 复位'working'并广播, 否则徽标永久卡住。
3.5 协议 + Session/Config 字段 + 跨任务协调
// 协议 (T-types + T-server protocol via T-types):
ClientMessage |= { type: 'approve'; mode?: PermissionMode } // B4
ServerMessage |= { type: 'telemetry'; telemetry: StatusTelemetry } // B2
// status 消息加 gate:
{ type: 'status'; status: ClaudeStatus; detail?: string; pending?: boolean; gate?: 'tool' | 'plan' }
export type PermissionMode = 'default' | 'acceptEdits' | 'plan' | 'auto'; // B4 (auto 待 R0 确认映射 bypassPermissions)
export interface StatusTelemetry { // B2 — 字段 BLOCKED on R0, 见 §W-1
contextUsedPct?: number; costUsd?: number; linesAdded?: number; linesRemoved?: number;
model?: string; effort?: string; pr?: { number: number; url: string; reviewState?: string };
rate?: { fiveHourPct?: number; sevenDayPct?: number }; at: number;
}
export interface PushSubscriptionRecord { endpoint: string; keys: { p256dh: string; auth: string }; createdAt: number }
export interface NotifyService { isEnabled(): boolean; notify(session: Session, cls: NotifyClass, token?: string): Promise<void> }
export interface CreateWorktreeResult { ok: boolean; path?: string; branch?: string; status?: number; error?: string }
// Session 加可变 runtime 字段 (同 buffer/lastOutputAt 的"不可变 meta 上挂可变 handle"例外):
interface Session { /* ... */ timeline: readonly TimelineEvent[]; stuckNotified: boolean; telemetry: StatusTelemetry | null }
// SessionManager 加方法:
handleStatusLine(id: string, telemetry: StatusTelemetry): void; // B2
sweepStuck(now: number): void; // A5 (用注入的 NotifyService)
handleHookEvent(sessionId, status, detail?, pending?, gate?, eventClass?, toolName?): void; // A4 扩展
// createSessionManager(cfg, notifyService?) — DI, 避免 manager 直 import push-service (循环依赖)
单一遥测真相源 (评审 #15): 遥测只存 TerminalSession.telemetry (getter); TabEntry 不重复存; refreshTab 读 entry.session.telemetry。
/hook/decision 响应 (评审 #14): 统一 204 (SW 不读 body); 契约表对齐 204。
4. 任务清单 (Tasks)
状态图例:
[ ]TODO ·[~]进行中 ·[x]完成 ·[!]受阻。勾选与记录由 orchestrator 写入 PROGRESS_LOG.md。 模型路由: 轻活 Haiku、攻坚 Opus、其余 Sonnet。isolation: worktree仅用于真正并发编辑的 builder。
4.0 任务总表 (Summary)
| ID | 标题 | 特性 | Owns (独占写) | Depends | 模型 | 隔离 | 学科 |
|---|---|---|---|---|---|---|---|
| R0 | 研究 spike (statusLine/permission-mode schema) | B2,B4 | (无源码; 产出 docs/R0_FINDINGS.md) |
— | sonnet | — | research |
| T-types | 冻结新共享类型 + §3 契约 | 全部 | src/types.ts |
R0(B2/B4 字段) | sonnet | — | BE |
| T-config | 21 新 env 解析 + L5 校验 | 全部 | src/config.ts, test/config.test.ts(扩) |
T-types | sonnet | worktree | BE |
| T-spawn-env | spawn env 注入 + session 字段 + stuck 重新武装 | A1,A5,B2 | src/session/session.ts, test/session.test.ts(扩) |
T-types | sonnet | — | BE |
| N-push | push-service + subscription-store | A1 | src/push/push-service.ts, src/push/subscription-store.ts, test/push/*.test.ts |
T-types | opus | worktree | BE/sec |
| N-timeline | 有界事件环 + 标签/类派生 + sanitize | A4 | src/session/timeline.ts, test/session/timeline.test.ts |
T-types | sonnet | worktree | BE |
| N-diff-be | parseUnifiedDiff/parseNumstat/getDiff |
B1 | src/http/diff.ts, test/http/diff.test.ts |
T-types | opus | worktree | BE/sec |
| N-statusline-be | parseStatusLine (tolerant, never throw) |
B2 | src/http/statusline.ts, test/http/statusline.test.ts |
T-types, R0 | sonnet | worktree | BE |
| N-worktree | validateBranchName/computeWorktreeDir/createWorktree |
B3 | src/http/worktrees.ts(扩), test/http/worktrees-create.test.ts |
T-types | opus | worktree | BE/sec |
| N-statusline-script | scripts/statusline.mjs |
B2 | scripts/statusline.mjs |
R0 | haiku | worktree | BE |
| T-hook-intake | parseHookEvent→HookEventFull (at/tool_input/eventClass/gate) |
A4,B4 | src/http/hook.ts, test/hook.test.ts(扩) |
T-types | sonnet | worktree | BE |
| T-preview-grid | 共享仪表/徽标组件 (SP10) | A4,A5,B2 | public/preview-grid.ts, test/telemetry-gauge.test.ts |
T-types | sonnet | worktree | FE |
| N-push-ui | 订阅流 + 🔔 toggle + insecure-context 检测 | A1 | public/push.ts, test/push.test.ts |
T-types | sonnet | worktree | FE/sec |
| N-voice | Web Speech 包装 + keybar 🎤 注入 | A2 | public/voice.ts, public/keybar.ts, test/voice.test.ts |
T-types | sonnet | worktree | FE |
| N-quickreply | 内置 chips + 调色板 CRUD | A3 | public/quick-reply.ts, test/quick-reply.test.ts |
T-types | sonnet | worktree | FE |
| N-timeline-ui | 时间线面板渲染 + 轮询 | A4 | public/timeline.ts, test/timeline.test.ts |
T-types | sonnet | worktree | FE |
| N-diff-ui | diff 查看器 (render-only) | B1 | public/diff.ts, test/diff.test.ts |
T-types | sonnet | worktree | FE/sec |
| T-sw | SW push/notificationclick + 纯 helpers | A1 | public/sw.js, public/sw-push.js, test/sw-push.test.ts |
T-types | sonnet | worktree | FE/sec |
| T-manager | timeline/telemetry/sweepStuck/notifyService DI | A4,A5,B2 | src/session/manager.ts, test/manager.test.ts(扩) |
T-types, N-push, N-timeline | opus | — | BE |
| T-server-wire | 全部新路由 + C1 hold 门 + token + 限流 + auto 拒绝 | 全部 | src/server.ts, test/integration/*.test.ts |
N-push,N-diff-be,N-statusline-be,N-worktree,T-hook-intake,T-manager | opus | — | BE/sec |
| T-hooks-installer | PostToolUse+ntfy 桥+statusLine 段+--max-time |
A1,A4,B2 | scripts/setup-hooks.mjs, test/setup-hooks.test.ts(若有) |
T-config, R0 | sonnet | — | BE/sec |
| T-termsession | telemetry/gate 捕获 + approve(mode?) |
B2,B4 | public/terminal-session.ts, test/terminal-session.test.ts(扩) |
T-types | sonnet | — | FE |
| T-tabs | 仪表+stuck 徽标+3按钮审批+mode+面板挂载 | A1-A5,B2,B4 | public/tabs.ts, test/tabs.test.ts(扩) |
T-preview-grid, T-termsession, N-push-ui, N-voice, N-quickreply, N-timeline-ui | opus | worktree | FE |
| T-projects-ui | diff 入口 + worktree 表单 + timeline 区 | A4,B1,B3 | public/projects.ts, test/worktree-form.test.ts |
N-diff-ui, N-timeline-ui, T-server-wire 契约 | sonnet | worktree | FE/sec |
| V-integration | 端到端集成 (真起 server, 临时 repo) | 全部 | (报告-only) | W2,W3 | sonnet(reviewer) | — | QA |
| V-security | 守卫/token/realpath/XSS 抽检 | 全部 | (报告-only) | W2,W3 | sonnet(reviewer) | — | sec |
| V-fe-pwa | HTTPS push 注册 + 跨浏览器降级 | A1,A2 | (报告-only) | W3 | sonnet(reviewer) | — | QA |
总计: 27 个任务 (1 研究 + 3 协调 + 14 W1 叶子 + 3 W2 后端 + 3 W3 前端 + 3 W4 验证)。
4.1 W-1 · 研究 spike (阻断)
R0 · statusLine / permission-mode schema 确认 [ ]
- Wave: W-1 · Feature: B2, B4 · Owns: 无源码 (产出
docs/R0_FINDINGS.md) · Depends: 无 · Model: sonnet · Isolation: — - Why blocking: B2
StatusTelemetry字段名 + B4permDecision能否携带 mode, 不得猜测 (H5)。先于 T-types 对 B2/B4 字段的冻结。 - Steps:
- 核实真实 Claude Code statusLine stdin JSON schema (字段名/类型:
context_window.used_percentage?cost.total_cost_usd?rate(5h/7d)?pr?) — 用 Context7/官方文档/本机~/.claude实测。 - 核实
--permission-mode接受值集 (default/acceptEdits/plan/bypassPermissions)。 - 核实
ExitPlanMode是否经/hook/permission到达 (tool_name 形态)。 - 核实
permDecision的 decision JSON 能否写回"更新后的 permission mode" (当前src/server.ts:61只发 behavior; 无证据能带 mode — 若不能, 记录替代方案: plan 批准后改走claude输入/重启 with mode)。
- 核实真实 Claude Code statusLine stdin JSON schema (字段名/类型:
- Accept (AC-B2.6/AC-B4.6): 四项均有文档证据或明确"无法"记录; T-types/N-statusline-be/N-statusline-script/B4 路径据此实现; 未确认项 → 相关任务
[!] BLOCKED。
4.2 W0 · 协调点 (串行, 阻塞下游)
T-types · 冻结新共享类型 + §3 契约 [ ]
- Wave: W0 · Feature: 全部 · Owns:
src/types.ts· Depends: R0(仅 B2/B4 字段) · Model: sonnet · Isolation: — - Steps:
- 应用 §3.1–§3.5 全部决议:
TimelineEvent(语义class)、DiffLine.kind(语义词)、PushPayload/NotifyClass/PushSubscriptionRecord/NotifyService、ClaudeStatus += 'stuck'、StatusTelemetry(R0)、PermissionMode、CreateWorktreeResult、DiffFile/DiffHunk/DiffResult。 - 协议扩展:
ClientMessage.approve.mode?、ServerMessage |= telemetry、status.gate?。 Session加timeline/stuckNotified/telemetry;SessionManager加handleStatusLine/sweepStuck/扩handleHookEvent;createSessionManager(cfg, notifyService?)。LiveSessionInfo可选带telemetry?(缩略图墙)。- B2/B4 字段若 R0 未完成 → 标
// TODO(R0)并对该子集返回 BLOCKED, 不冻结。
- 应用 §3.1–§3.5 全部决议:
- Accept:
tsc --noEmit通过; 无实现纯类型; §3 五项契约可被 FE/BE 双侧 import。
T-config · 21 新 env 解析 + L5 校验 [ ]
- Wave: W0 · Feature: 全部 · Owns:
src/config.ts,test/config.test.ts(扩) · Depends: T-types · Model: sonnet · Isolation: worktree - Steps (用既有
parseNonNegativeInt/parseBool帮手; 评审 #16 确认 21 个新解析):- A1:
VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY(秘密)/VAPID_SUBJECT/PUSH_STORE_PATH/PUSH_MAX_SUBS(50)/NOTIFY_DONE(1)/NOTIFY_DND(0)/DECISION_TOKEN_TTL_MS(=permTimeoutMs)。 - A4:
TIMELINE_MAX(200)/TIMELINE_ENABLED(1)。 A5:STUCK_TTL(600s×1000)/STUCK_ALERT(1)。 - B1:
DIFF_TIMEOUT_MS(2000)/DIFF_MAX_BYTES(2MB)/DIFF_MAX_FILES(300)。 B2:STATUSLINE_TTL_MS(30000)。 - B3:
WORKTREE_ENABLED(1)/WORKTREE_ROOT(undef→<repo>-worktrees)/WORKTREE_TIMEOUT_MS(10000)。 - B4:
DEFAULT_PERMISSION_MODE(白名单 4 值, 默认 default)/ALLOW_AUTO_MODE(0)。 - L5 校验:
permTimeoutMs > 0; 记录--max-time须 > permTimeoutMs (实际注入在 T-hooks-installer)。秘密缺失只记 "configured"/"missing", 不入日志。 - 返回对象
Object.freeze。
- A1:
- Security: SEC-C5 (VAPID 私钥不入日志); SEC-L4 (保留
isHttpOrigin拒非 http(s) scheme); SEC-M4 (L5)。 - Accept (AC-A1.6): 默认值/覆盖/非法值抛错; VAPID 未设 → push 标记禁用;
vitest run config全绿。
T-spawn-env · spawn env + session 字段 + stuck 重新武装 [ ]
- Wave: W0 · Feature: A1, A5, B2 · Owns:
src/session/session.ts,test/session.test.ts(扩) · Depends: T-types · Model: sonnet · Isolation: — - Steps:
- spawn env 加
WEBTERM_STATUSLINE_URL: http://127.0.0.1:${cfg.port}/hook/status(B2);WEBTERM_NTFY_*由process.env透传 (不在此设 token, H3)。 - session 初始化:
timeline: Object.freeze([])、stuckNotified: false、telemetry: null。 pty.onData: 既有lastOutputAt=Date.now()旁加session.stuckNotified=false; §3.4 重新武装: 若session.claudeStatus==='stuck'→ 置'working'并 broadcast{type:'status',status:'working'}(复用 session.ts 既有 broadcast util)。
- spawn env 加
- 协调注记: claudeStatus 通常由 T-manager 改; 此处 onData 的复位是 A5 重新武装的唯一可靠点 (同 lastOutputAt 已在此处)。T-manager owner 知悉, 不重复实现。
- Accept (AC-A5.2): 输出恢复后 stuck flag/status 复位; spawn env 含 statusline URL;
vitest run session全绿。
4.3 W1 · 叶子模块 (全部并行)
N-push · push-service + subscription-store [ ]
- Wave: W1 · Feature: A1 · Owns:
src/push/push-service.ts,src/push/subscription-store.ts,test/push/push-service.test.ts,test/push/subscription-store.test.ts· Depends: T-types · Model: opus · Isolation: worktree - Steps:
subscription-store:loadSubscriptionStore(path, maxSubs);list()(返回 frozen)/add(FIFO 封顶)/remove/prune(dead[])/persist()(mode:0o600)。缺文件→空; 坏 JSON→空 (best-effort, 校验 endpoint+keys)。push-service(实现NotifyService):isEnabled()=两 VAPID key 均在;notify(session, cls, token?): DND/notifyDone短路; payload=§3.3PushPayload(无 raw 输出/秘密);tag=sessionId(替换不堆叠, M6/A1-FR7);requireInteraction仅 needs-input; 404/410→prune+persist; 其它错误记日志续发。
- Security: SEC-C5 (私钥不入日志); SEC-M1 (
PUSH_MAX_SUBS封顶); SEC-M2 (600 权限, 绝不经 GET); SEC-L1 (web-push锁版本,npm audit); payload 最小化。 - Accept:
isEnabled真值表; needs-input/done/stuck 分支; 404/410 剪除; DND no-op; mockweb-push; ≥80%。
N-timeline · 有界事件环 + 标签/类派生 [ ]
- Wave: W1 · Feature: A4 · Owns:
src/session/timeline.ts,test/session/timeline.test.ts· Depends: T-types · Model: sonnet · Isolation: worktree - Steps (纯函数, 零 DOM):
sanitizeField(s, max=200): 去\x00-\x1f, 截断。deriveClass(eventName)→TimelineClass(§3.1: PreToolUse/PostToolUse→tool, PermissionRequest/Notification(permission_prompt)→waiting, Stop/SessionEnd→done, UserPromptSubmit→user)。deriveLabel(eventName, toolName?)→ 人话 ("ran Bash"/"edited "/"waiting for approval"/"done")。makeTimelineEvent(eventName, toolName, at)→TimelineEvent | null(whitelist eventName, 否则 null)。appendEvent(events, ev, maxLen): 返回新数组, 超maxLen淘汰最旧 (NEVER 改入参)。
- Security: SEC-H6 (sanitize 工具名/路径); SEC-M8 (环上限 append 时强制)。
- Accept (AC-A4.4, AC-A4.5): 每类一例; sanitize/截断;
appendEvent不可变 + 淘汰 (appendMAX+10→ 长度 ≤ MAX); ≥80%。
N-diff-be · diff 解析 + getDiff [ ]
- Wave: W1 · Feature: B1 · Owns:
src/http/diff.ts,test/http/diff.test.ts· Depends: T-types · Model: opus · Isolation: worktree - Steps:
parseNumstat(out)→Map<path,{added,removed}>(含 binary-\t-、rename)。parseUnifiedDiff(patch, numstat)→DiffFile[](单/多 hunk、rename、new//dev/null、delete、binary、空→[]; never throw; 坏行→context)。getDiff(repoPath, {staged, cfg}):execFileAsync('git', ['diff','--no-color', staged?'--staged':[], '--'], {cwd,timeout:diffTimeoutMs,maxBuffer:diffMaxBytes})+--numstat+status --porcelain(untracked, L3);truncated = files>diffMaxFiles || patch.length>=diffMaxBytes。
- Security: SEC-H7 (路径三连校验在路由层, 此处
--终结选项); SEC-M9 (maxBuffer/maxFiles DoS); SEC-H4 (diff 内容作为纯 text 字段返回, 不渲染)。FR-B1.9?base明确 P2 延后 (评审 #13; 需git rev-parse --verify白名单)。 - Accept (AC-B1.1, AC-B1.5): numstat/unified 各分支; +/- 与
git diff --numstat一致;<script>内容原样入DiffLine.text; ≥80% + 临时 repo 集成。
N-statusline-be · parseStatusLine [ ]
- Wave: W1 · Feature: B2 · Owns:
src/http/statusline.ts,test/http/statusline.test.ts· Depends: T-types, R0 · Model: sonnet · Isolation: worktree - Steps:
parseStatusLine(body: unknown): StatusTelemetry | null—unknown+逐字段收窄 (仿parseHookEvent); 非对象→null; 缺/脏字段→undefined; 数值Number.isFinite校验; 恒设at:Date.now(); never throw。 - Security: SEC-M7 (
unknown+narrowing, 字符串长度上限)。 若 R0 未确认字段名 →[!] BLOCKED(H5)。 - Accept (AC-B2.6): 满/缺/脏/空; never throw; ≥80%。
N-worktree · 分支校验 + worktree 创建 [ ]
- Wave: W1 · Feature: B3 · Owns:
src/http/worktrees.ts(扩, 与listWorktrees同文件),test/http/worktrees-create.test.ts· Depends: T-types · Model: opus · Isolation: worktree - Steps:
validateBranchName(b): 拒 空/>250/前导-/../尾.lock/控制字符/空白/~^:?*[\/@{/首尾或连续/。sanitizeBranchForDir(b):/→-, 去首尾-, 不安全 FS 字符→-。computeWorktreeDir(repoPath, sanitized, root?): base=root ?? <dirname>/<basename>-worktrees; M2:fs.realpath(base)+realpath(candidate)后startsWith(realBase+sep)(防 symlink 绕过); 越界 throw。createWorktree(repoPath, branch, {base?, worktreeRoot?, timeoutMs}): validate → repoPath 三连校验 → computeDir →execFileAsync('git',['worktree','add','-b',branch,'--',dir, base].filter); 分类错 (branch exists→409/path exists→409/其它→500),error安全文案不带 raw stderr。
- Security (本特性最重): SEC-H2 (分支注入/前导
-); SEC-H3/M2 (realpath 包含); SEC-M10 (不泄 git stderr);execFile无 shell +--。 - Accept (AC-B3.4, AC-B3.6): validate 每拒绝模式; symlink root 越界被拒; ≥80% + 端点集成 (真 repo)。
N-statusline-script · scripts/statusline.mjs [ ]
- Wave: W1 · Feature: B2 · Owns:
scripts/statusline.mjs· Depends: R0 · Model: haiku · Isolation: worktree - Steps: 读 stdin statusLine JSON →
POST $WEBTERM_STATUSLINE_URL -H "X-Webterm-Session: $WEBTERM_SESSION" --data-binary @-; echo 一行精简 (cost·ctx%·model) 给 Claude 状态行; web-terminal 外 (WEBTERM_STATUSLINE_URL未设) → no-op。 R0 未定 schema → BLOCKED。 - Accept: 本机 POST 到
/hook/status成功; 非 web-terminal 环境静默。
T-hook-intake · parseHookEvent → HookEventFull [ ]
- Wave: W1 · Feature: A4, B4 · Owns:
src/http/hook.ts,test/hook.test.ts(扩) · Depends: T-types · Model: sonnet · Isolation: worktree - Steps: 新增
HookEventFull {sessionId, status, detail?, at, eventClass, toolInput?, gate?};parseHookEvent改返回该型 (status/detail 逻辑不变); 加at=Date.now()、eventClass(原始 hook 名 whitelist)、toolInput(原样 unknown)、gate='plan'wheneventClass==='PermissionRequest' && tool==='ExitPlanMode'(R0 待核)。 - Security: SEC-M7 (
unknown+narrowing, never throw)。 - Accept: 既有 status 测试不回归; 新字段 (at/eventClass/gate) 有测试; ≥80%。
T-preview-grid · 共享仪表/徽标组件 (SP10) [ ]
- Wave: W1 · Feature: A4, A5, B2 · Owns:
public/preview-grid.ts,test/telemetry-gauge.test.ts· Depends: T-types · Model: sonnet · Isolation: worktree - Why W1 (评审 #6, #8): 单一 owner; W1 即冻结导出签名, 让 W3 的 T-tabs/T-projects-ui 安全 import; 测试与实现同任务同波次 (不跨波)。
- Steps: 加
renderTelemetryGauge(container, telemetry, staleTtlMs)(上下文条+$成本+model chip+PR 徽标,>80%变色,>TTL置灰);renderStatusBadge(container, status)(含'stuck'=⚠); 扩statusText()支持'stuck'。全程el()/textContent, 零innerHTML; PR URL 仅用户点击复制/打开。 - Security: SEC-H5 (telemetry 字段 textContent); SEC-L5 (PR URL
new URL().protocol==='https:'校验后才入 href)。 - Accept: gauge 渲染 (缺字段宽容/陈旧置灰/
<script>作文本/PR URL scheme 校验); ≥80%。
N-push-ui · 订阅流 + 🔔 toggle [ ]
- Wave: W1 · Feature: A1 · Owns:
public/push.ts,test/push.test.ts· Depends: T-types · Model: sonnet · Isolation: worktree - Steps:
checkPushSupport()(unsupported/insecure-context/vapid-missing/permission-denied/available/subscribed);fetchVapidKey()(503→null);subscribePush/unsubscribePush(POST/DELETE/push/subscribe);mountPushToggle(container, opts)(insecure→灰铃+提示 HTTPS/Tailscale; vapid-missing→隐藏)。localStorage['web-terminal:push-muted']仅影响应用内 (评审 #12: A1-FR9 记为 in-app-only; 全局 DND 走NOTIFY_DND)。 - Security: SEC-H8 (
isSecureContext检测); SEC-C4 路由侧 (本任务只发请求)。 - Accept (AC-A1.1, AC-A1.5): 各 support 状态; insecure 隐藏不崩; mock SW/Notification/fetch; ≥80%。 (notificationclick 测试归 T-sw, 评审 #11)。
N-voice · Web Speech + keybar 🎤 [ ]
- Wave: W1 · Feature: A2 · Owns:
public/voice.ts,public/keybar.ts,test/voice.test.ts· Depends: T-types · Model: sonnet · Isolation: worktree - Steps:
isSpeechSupported();createVoiceInput(onTranscript, opts?)→VoiceInput | null(start/stop/dispose/isActive; interim 回调;autoSend→补\r)。keybar.ts:mountKeybar(onSend, opts?)加onVoiceTrigger?, supported 时 append 🎤 (touchstart/touchend 推杆 +preventDefault防软键盘)。纯前端, 零服务端, 无音频抵达后端。 - Security: SEC-L2 (Chrome→Google 音频披露文案; AC-A2.5 验无音频抵达服务端)。
- Accept (AC-A2.x): 不支持隐藏不崩; interim/final; autoSend 补/不补
\r; dispose 后无回调; ≥80%。
N-quickreply · chips + 调色板 [ ]
- Wave: W1 · Feature: A3 · Owns:
public/quick-reply.ts,test/quick-reply.test.ts· Depends: T-types · Model: sonnet · Isolation: worktree - Steps:
BUILT_IN_CHIPS(yes/continue/1/2/3/Esc); 不可变 CRUD (addChip/removeChip/reorderChip/updateChip返回新数组);loadPalette/savePalette(localStorage, 坏数据不抛);mountQuickReply(container,{onSend})(点 chip→onSend(text+(appendEnter?'\r':''));+开内联编辑器; 标签textContent)。 - Security: SEC-L3 (snippet 标签 textContent, 防注入)。
- Accept (AC-A3.x): CRUD 不可变; round-trip 持久; 坏数据不抛; appendEnter 行为;
<script>作文本; ≥80%。
N-timeline-ui · 时间线面板 [ ]
- Wave: W1 · Feature: A4 · Owns:
public/timeline.ts,test/timeline.test.ts· Depends: T-types · Model: sonnet · Isolation: worktree - Steps:
normalizeTimelineEvent(raw)(安全收窄→null);timelineIcon(cls)(按 §3.1TimelineClass);renderTimelineEvent(ev)("HH:MM · icon · label",textContent, 复用relTime);fetchTimeline(id)(err→[]);mountTimeline(container,id,opts?)(newest-first, 可见时轮询refreshMs,maxEvents上限, empty 态)。 - Security: SEC-H6 (label textContent)。
- Accept (AC-A4.3): normalize 防御; icon 映射; textContent; 轮询起停; dispose 清 interval; ≥80%。
N-diff-ui · diff 查看器 (render-only) [ ]
- Wave: W1 · Feature: B1 · Owns:
public/diff.ts,test/diff.test.ts· Depends: T-types · Model: sonnet · Isolation: worktree - Steps (评审 #2: render-only, 无解析):
fetchDiff(repoPath, staged)(GET/projects/diff);normalizeDiffResult(raw)(→null if invalid);renderDiffFile(file, opts)/renderDiff(result)(按文件分组, +/- 徽标, 折叠, truncated 警告, 空态; 全程textContent/el());mountDiffViewer(container, repoPath, {onClose})(working|staged 切换)。 - Security (关键): SEC-H4 (diff 内容全程 textContent, 零 innerHTML)。
- Accept (AC-B1.3, AC-B1.4):
<script>/&/ANSI 作文本; truncated 警告; 空态; staged 切换; ≥80%。
T-sw · SW push/notificationclick + 纯 helpers [ ]
- Wave: W1 · Feature: A1 · Owns:
public/sw.js,public/sw-push.js,test/sw-push.test.ts· Depends: T-types · Model: sonnet · Isolation: worktree - Steps (评审 #11: 抽纯 helper):
sw-push.js(纯, 无 SW 全局):buildPushNotification(payload)→{title, options}(needs-input 带 Allow/Deny +requireInteraction,tag=sessionId);resolveNotificationClick(notification, action)→{kind:'decision', body}|{kind:'focus', url}(Allow/Deny→/hook/decision{sessionId,decision,token}; 正文→/?session=<id>)。sw.js:importScripts('./sw-push.js');addEventListener('push'/'notificationclick')调上面 helper; fetch bypass 扩/push/*(除既有/term//hook)。
- Security: SEC-C1 (decision 带 token, 远程→Origin+token 在路由层校验);
credentials:'same-origin'。 - Accept (AC-A1.2, AC-A1.3):
buildPushNotification各 class;resolveNotificationClickallow/deny/default 路由 (jsdom mock); ≥80% (live addEventListener 验证归 W4)。
4.4 W2 · 后端汇聚 (单 owner)
T-manager · timeline/telemetry/sweepStuck/notifyService [ ]
- Wave: W2 · Feature: A4, A5, B2 · Owns:
src/session/manager.ts,test/manager.test.ts(扩) · Depends: T-types, N-push(NotifyService), N-timeline(appendEvent) · Model: opus · Isolation: — - Steps:
createSessionManager(cfg, notifyService?)DI (避免直 import push-service)。handleStatusLine(id, telemetry): 存session.telemetry+broadcast({type:'telemetry',telemetry})(B2)。handleHookEvent(...)扩:cfg.timelineEnabled && eventClass→session.timeline = appendEvent(...); broadcast status 带gate(A4/B4)。sweepStuck(now)(A5/H4): 跳过 exited/idle/已 notified;now-lastOutputAt > stuckTtlMs→claudeStatus='stuck'+ broadcast status='stuck' (§3.4 评审 #5) +stuckNotified=true+notifyService?.notify(session,'stuck');stuckAlert=0||stuckTtlMs=0禁用。覆盖 attached 与 detached 存活会话。handleAttachCase 2 (M3): 晚加入设备补发telemetry(若有) + 当前 status (pending 部分留 T-server-wire)。list()带telemetry?(LiveSessionInfo)。
- Security: SEC-M3 (每 stuck 回合一告警, 重新武装在 T-spawn-env)。
- Accept (AC-A5.1, AC-A5.3, AC-B2.3): sweepStuck 覆盖 attached、一回合一次; 晚加入补发遥测; ≥80%。
T-server-wire · 全部新路由 + C1 + token + 限流 [ ]
- Wave: W2 · Feature: 全部 · Owns:
src/server.ts,test/integration/push.test.ts,test/integration/timeline-events.test.ts,test/integration/worktree.test.ts· Depends: N-push, N-diff-be, N-statusline-be, N-worktree, T-hook-intake, T-manager · Model: opus · Isolation: — - Steps:
- 初始化:
loadSubscriptionStore→createPushService→createSessionManager(cfg, pushService);pendingApprovals条目扩{res,timer,token,expiresAt}。 - C1 hold 门修正 (
src/server.ts:288, SEC-C2):shouldHold = clients.size>0 || (pushService.isEnabled() && subStore.list().length>0); 仅 hold 路径铸crypto.randomUUID()token +void pushService.notify(session,'needs-input',token)。 - 新路由:
GET /push/vapid-key(公钥, 503 if disabled);POST/DELETE /push/subscribe(requireAllowedOriginSEC-C4 + 限流);POST /hook/decision(远程:requireAllowedOriginSEC-C1 + 限流 + token 校验 {session+resolve/超时失效} →resolvePending(permDecision(...))→ 返回 204);GET /live-sessions/:id/events(只读,timelineEnabledelse []);GET /projects/diff(只读 + 路径三连校验 SEC-H7 →getDiff);POST /hook/status(isLoopbackSEC-H1 →parseStatusLine→handleStatusLine);POST /projects/worktree(requireAllowedOriginSEC-C3 +worktreeEnabled→createWorktree);GET /config/ui(只读, 返回{allowAutoMode}, 评审 #4)。 - 限流 (评审 #9, SEC-H9): 每-IP 滑窗 —
/hook/decision≤10/min、/push/subscribe≤5/min (内存 Map, 无外部包)。 /hook路由: 传eventClass+toolInput给handleHookEvent; Stop/SessionEnd →pushService.notify(session,'done')。/hook/permission:gate='plan'透传; B4 approve 携带 mode。- WS approve mode (评审 #10, SEC-M5): 处理
{type:'approve', mode}→permDecision(behavior, mode);ALLOW_AUTO_MODE=0时拒绝mode:'auto'降级 default (即使越过 FE 隐藏到达)。 - reaper (
src/server.ts:317): 同间隔加manager.sweepStuck(Date.now())(H4, 无新 timer)。
- 初始化:
- Security: SEC-C1/C2/C3/C4/H1/H7/H9/M1。
- Accept (AC-A1.2, AC-A1.8, AC-B2.5, AC-B3.2): C1 零客户端但有订阅→hold; decision 坏/陈旧 token/外来 Origin→403; loopback 校验; 限流 429; auto 拒绝; 集成全绿。
T-hooks-installer · 安装器扩展 [ ]
- Wave: W2 · Feature: A1, A4, B2 · Owns:
scripts/setup-hooks.mjs,test/setup-hooks.test.ts(若有) · Depends: T-config, R0 · Model: sonnet · Isolation: — - Steps: H1
FF_EVENTS补PostToolUse; L5--max-time = ceil(PERM_TIMEOUT_MS/1000)+缓冲(取自 env, 不写裸字面量); H3 ntfy/Pushover 桥 (仅WEBTERM_NTFY_URL+TOPIC存在时追加 curl, token 走$WEBTERM_NTFY_TOKENenv, 不入 settings.json/argv, priority high/low); B2 statusLine 段 (R0 schema, 幂等 MARKER,--remove干净)。 - Security: SEC-C6 (token 不入 settings.json/argv); SEC-M4 (
--max-time>permTimeoutMs)。 - Accept (AC-A1.7, AC-A4.5, AC-B2.1):
FF_EVENTS含 PostToolUse; ntfy 命令正确 priority 且 grep settings.json 无 token 字面量; statusLine 幂等/--remove。
4.5 W3 · 前端汇聚 (单 owner)
T-termsession · telemetry/gate 捕获 + approve(mode?) [ ]
- Wave: W3 · Feature: B2, B4 · Owns:
public/terminal-session.ts,test/terminal-session.test.ts(扩) · Depends: T-types · Model: sonnet · Isolation: — - Steps: 加
onTelemetry?回调 +telemetry/pendingGategetter (单一遥测真相源, 评审 #15);handle()加case 'telemetry'(+ exhaustiveness/assertNever);statuscase 记pendingGate = msg.gate ?? null;approve(mode?)→sendMsg({type:'approve', ...(mode?{mode}:{})})。 - Accept (AC-B2.6): telemetry 捕获+回调; gate 记录; approve(mode) 序列化; ≥80% +
telemetry往返。
T-tabs · 仪表+徽标+审批+面板挂载 (最大 FE 任务) [ ]
- Wave: W3 · Feature: A1–A5, B2, B4 · Owns:
public/tabs.ts,test/tabs.test.ts(扩) · Depends: T-preview-grid, T-termsession, N-push-ui, N-voice, N-quickreply, N-timeline-ui · Model: opus · Isolation: worktree - Steps:
- SP10 单一
refreshTabowner (M4): stuck 徽标 (claudeStatus==='stuck'→⚠) + telemetry gauge (renderTelemetryGauge, 读entry.session.telemetry)。 - B4
updateApprovalBar(tabs.ts:193): plan gate (pendingGate==='plan')→3 按钮 (Approve+Auto→approve('acceptEdits')/ Approve+Review→approve('default')/ Keep Planning→reject()); tool gate 仍 2 按钮 (无回归)。 - B4
openProject(...,mode?): cmd 升级claude --permission-mode <mode>(非 default 时);loadDefaultMode()(localStorage,auto仅当allowAutoMode);loadUiConfig()fetchGET /config/ui。 - A1
mountPushToggle(构造器一次, 铃在 tabBar); A2setupVoice(mic→handleVoiceTrigger→createVoiceInput→sendToActive, interim overlay); A3mountQuickReply(#quickreply行在#keybar上); A4 timeline 面板 toggle (mountTimeline, 懒挂、关 tab dispose)。 TabEntry加timelineHandle(不加 telemetry, 评审 #15)。
- SP10 单一
- Security: SEC-M5 (auto 二次确认+警示,
allowAutoMode门); SEC-M6 (tag 去重已在 SW)。 - Accept (AC-A5.5, AC-B2.2, AC-B4.2, AC-B4.3, AC-B4.4): stuck 徽标; tab 仪表; plan 3 按钮/tool 2 按钮无回归; auto 隐藏 when 0; ≥80%。
T-projects-ui · diff 入口 + worktree 表单 + timeline 区 [ ]
- Wave: W3 · Feature: A4, B1, B3 · Owns:
public/projects.ts,test/worktree-form.test.ts· Depends: N-diff-ui, N-timeline-ui, T-server-wire 契约 · Model: sonnet · Isolation: worktree - Steps: B1
renderProjectDetail加 "View Diff" →mountDiffViewer内联面板 (会话行用 cwd); B3renderNewWorktreeForm(validateBranchNameClient客户端预校验 +createWorktreePOST + 成功hooks.onOpenProject(dir,label,'claude\r'); 错误textContent); A4 "Activity" 区 (running session 各mountTimeline, 重渲/onBack dispose)。 - Security: SEC-H4 (diff textContent); SEC-L3/H6 (错误/标签 textContent); B3 写端点 Origin 在路由层。
- Accept (AC-B1.3, AC-B3.1): 表单校验; diff 内联; timeline 区; server 错误作 textContent; ≥80%。
4.6 W4 · 验证 (报告-only, 不跨 lane 改文件)
同 PLAN.md §5 G4: reviewer 只产 findings (标 severity + owning task); 修复派回 owner builder。
V-integration · 端到端集成 [ ]
- Owns: 无 (报告) · Depends: W2, W3 · Model: sonnet (reviewer)
- Steps: 真起 server + 临时 git repo — C1 hold 门 (零客户端有订阅→hold); timeline 实时 hook 轮询; diff 真 repo; worktree 创建并开 tab; plan-gate 3 按钮解决; stuck 越 TTL; statusline.mjs→遥测仪表;
TIMELINE_ENABLED=0/WORKTREE_ENABLED=0/STUCK_TTL=0禁用路径。 - Accept: PRD §8 全部 AC 对照运行通过。
V-security · 安全抽检 [ ]
- Owns: 无 (报告) · Depends: W2, W3 · Model: sonnet (security-reviewer)
- Steps:
/hook/decision坏 Origin/陈旧 token/loopback IP → 403;/push/subscribe//projects/worktree缺 Origin → 403; 限流 429; symlink 越界 worktree → 拒; diff/telemetry/timeline<script>作文本; grep settings.json + 进程 argv 无 ntfy/VAPID token 字面量 (AC-A1.7); VAPID 私钥不入日志;web-pushnpm audit。 - Accept (合并安全章节 §5): 安全表逐行核; 无 CRITICAL/HIGH 未决。
V-fe-pwa · HTTPS push + 跨浏览器 [ ]
- Owns: 无 (报告) · Depends: W3 · Model: sonnet (reviewer)
- Steps: HTTPS/Tailscale 上下文 push 订阅成功 + 锁屏 Allow/Deny 解决; 不安全上下文隐藏 🔔; Safari/iOS Web Speech 不支持→mic 隐藏; SW push 需加主屏→降级 ntfy 桥。
- Accept (AC-A1.5, AC-A2.3, C2): 安全上下文订阅成功; 不安全/不支持优雅降级。
5. 安全检查清单 (折叠到任务)
完整缓解见 PRD §5 合并安全章节; 下表把每条挂到 owner 任务 (severity 同评审约定: CRITICAL=阻断合并):
| 安全项 | Severity | Owner 任务 |
|---|---|---|
SEC-C1 /hook/decision Origin+能力token |
CRITICAL | T-server-wire (+ T-sw 发 token) |
| SEC-C2 C1 hold 门修正 (零客户端有订阅→hold) | CRITICAL | T-server-wire |
SEC-C3 /projects/worktree requireAllowedOrigin |
CRITICAL | T-server-wire |
SEC-C4 /push/subscribe (POST/DELETE) Origin |
CRITICAL | T-server-wire |
| SEC-C5 VAPID 私钥不日志/不经 GET/缺失降级 | CRITICAL | T-config, N-push |
| SEC-C6 ntfy token 仅 env, 不入 settings.json/argv | CRITICAL | T-hooks-installer |
SEC-H1 /hook/status loopback-only |
HIGH | T-server-wire |
SEC-H2 validateBranchName 前导-/注入 |
HIGH | N-worktree |
| SEC-H3/M2 realpath 两端包含校验 | HIGH | N-worktree |
| SEC-H4 diff 内容 textContent | HIGH | N-diff-ui, T-projects-ui |
| SEC-H5 telemetry 字段 textContent + PR URL scheme | HIGH | T-preview-grid |
| SEC-H6 timeline label sanitize + textContent | HIGH | N-timeline, N-timeline-ui |
SEC-H7 /projects/diff 路径三连 + -- |
HIGH | N-diff-be, T-server-wire |
| SEC-H8 push 需安全上下文检测 | HIGH | N-push-ui |
| SEC-H9 限流 (decision≤10/min, subscribe≤5/min) | HIGH | T-server-wire |
SEC-M1 PUSH_MAX_SUBS 封顶 |
MEDIUM | N-push |
| SEC-M2 订阅库 600 / 不经 GET | MEDIUM | N-push |
| SEC-M3 stuck 每回合一告警 + 重新武装 | MEDIUM | T-manager, T-spawn-env |
SEC-M4 --max-time>permTimeoutMs |
MEDIUM | T-config, T-hooks-installer |
SEC-M5 ALLOW_AUTO_MODE=0 服务端拒 auto |
MEDIUM | T-server-wire (+ T-tabs 隐藏, T-types 白名单) |
| SEC-M6 tag 去重 | MEDIUM | N-push, T-sw |
SEC-M7 unknown+narrowing (statusline/hook) |
MEDIUM | N-statusline-be, T-hook-intake |
| SEC-M8 timeline 环上限 | MEDIUM | N-timeline |
| SEC-M9 diff DoS (maxBuffer/maxFiles) | MEDIUM | N-diff-be |
| SEC-M10 不泄 git stderr | MEDIUM | N-worktree |
SEC-L1 web-push 锁版本/audit |
LOW | N-push, V-security |
| SEC-L2 音频隐私披露 | LOW | N-voice |
| SEC-L3 snippet 标签 textContent | LOW | N-quickreply |
SEC-L4 ALLOWED_ORIGINS scheme 校验保留 |
LOW | T-config |
| SEC-L5 PR URL https scheme 校验 | LOW | T-preview-grid |
6. 建议的多 Agent 分派排程 (Dispatch Schedule)
单批并行控制在 ~3–5 个 agent (官方甜区)。worktree 隔离前提: 先 commit W0 产物再派 W1 隔离 builder。
| 批次 | 波次 | 并行任务 (≤5/批) | 说明 |
|---|---|---|---|
| 1 | W-1 | R0 | 串行研究 spike, 阻断 B2/B4 字段 |
| 2 | W0 | T-types | 串行冻结契约 (§3 五项决议), 全员依赖 |
| 3 | W0 | T-config, T-spawn-env | 2 并行 (类型就绪) |
| 4a | W1 | N-push, N-diff-be, N-worktree | 后端高风险 (opus×2+opus) |
| 4b | W1 | N-timeline, N-statusline-be, N-statusline-script, T-hook-intake | 后端其余 (statusline 待 R0) |
| 5a | W1 | T-preview-grid, N-diff-ui, N-timeline-ui | 前端 (preview-grid 先, 供 W3) |
| 5b | W1 | N-push-ui, N-voice, N-quickreply, T-sw | 前端其余 |
| 6 | W2 | T-manager → 然后 T-server-wire; T-hooks-installer 并行 | manager 先于 server-wire (串行); installer 独立文件并行 |
| 7 | W3 | T-termsession → 然后 T-tabs; T-projects-ui 并行 | termsession 先 (tabs 依赖 onTelemetry); tabs/projects 文件 disjoint 可并行 |
| 8 | W4 | V-integration, V-security, V-fe-pwa | 3 reviewer 并行, 报告-only |
批 4a/4b 与 5a/5b 共 13 个 W1 任务全 disjoint, agent 充足可并发更多; 不足则按批跑。
7. 验收映射 (AC → 任务)
全局门槛: 纯核心 (diff/statusLine/timeline 解析、branch/dir 校验、push payload、label/类派生) 单测 ≥80%; 端点经集成测试 (真起 server); A1 push 在模拟 TLS/安全上下文注册成功。
| 特性 | 关键 AC (PRD §8) | 主验收任务 |
|---|---|---|
| A1 | C1 零 tab 锁屏解决(AC-A1.2)、不安全上下文隐藏铃(AC-A1.5)、VAPID 未设 503(AC-A1.6)、token 不入 settings.json(AC-A1.7)、坏/陈旧 token 403(AC-A1.8) | N-push, N-push-ui, T-sw, T-server-wire, T-hooks-installer, V-integration, V-security, V-fe-pwa |
| A2 | 按住说话打进终端、软键盘不弹、不支持隐藏、无音频抵达(AC-A2.5) | N-voice, T-tabs, V-fe-pwa |
| A3 | 内置+持久调色板同路径发送、标签惰性文本(AC-A3.4) | N-quickreply, T-tabs |
| A4 | 人话时间线、/…/events 受界、PostToolUse(AC-A4.5)、sanitize(AC-A4.4)、TIMELINE_ENABLED=0 空 |
N-timeline, N-timeline-ui, T-hook-intake, T-manager, T-hooks-installer, T-projects-ui/T-tabs |
| A5 | 静默一次告警、恢复重新武装(AC-A5.2)、覆盖 attached 无新 timer(AC-A5.3)、stuck 徽标(AC-A5.5)、0 禁用 | T-manager, T-spawn-env, T-server-wire, T-tabs, T-preview-grid |
| B1 | 结构化 diff+numstat 一致、非 git→404、超大 truncated、<script> 作文本(AC-B1.4)、解析≥80%(AC-B1.5)、untracked(L3) |
N-diff-be, N-diff-ui, T-server-wire, T-projects-ui |
| B2 | tab 仪表、晚加入补发(M3, AC-B2.3)、缺字段宽容/陈旧置灰、非本机 403(AC-B2.5)、telemetry 往返、R0 确认 |
N-statusline-be, N-statusline-script, T-manager, T-server-wire, T-termsession, T-preview-grid, T-tabs |
| B3 | 建出并开 agent、缺 Origin 403、非法分支拒、realpath 拒 symlink(AC-B3.4)、并行不互踩、纯+集成 | N-worktree, T-server-wire, T-projects-ui, V-security |
| B4 | plan 启动、3 按钮 approve+auto 切 acceptEdits(AC-B4.2)、tool 2 按钮无回归、ALLOW_AUTO_MODE=0 禁 auto、非法 mode 拒、R0 确认(AC-B4.6) |
T-types, T-termsession, T-tabs, T-server-wire, R0 |
| 跨特性 | 共享文件单 owner(C3)、env 命名(L4)、--max-time>permTimeoutMs(L5)、共享仪表无 DOM 争用(M4) |
§2 协调点, T-config, T-hooks-installer, T-preview-grid |
8. 评审修正落点速查 (Review Fixes → 本计划)
| # | 修正 | 落点 |
|---|---|---|
| 1 | TimelineEvent 用派生语义 class |
§3.1, T-types, N-timeline deriveClass |
| 2 | DiffLine.kind 一种拼写 + diff 只在后端解析 |
§3.2, N-diff-be(解析), N-diff-ui(render-only) |
| 3 | Push payload 一种 shape, FE 读 cls |
§3.3, N-push, T-sw |
| 4 | 加 GET /config/ui |
T-server-wire, T-tabs loadUiConfig |
| 5 | 'stuck' 入 union + 服务端广播 + 重新武装 |
§3.4, T-types, T-manager, T-spawn-env |
| 6 | preview-grid.ts 单 owner |
§2, T-preview-grid (新增 W1 任务) |
| 7 | keybar.ts 归属明确 |
§2, N-voice owns keybar.ts (非 T-sw) |
| 8 | gauge 测试与实现同任务同波次 | T-preview-grid (test/telemetry-gauge.test.ts 同 W1) |
| 9 | 限流收紧 (decision≤10/min, subscribe≤5/min) | T-server-wire |
| 10 | 服务端 ALLOW_AUTO_MODE 拒 auto |
T-server-wire |
| 11 | SW 抽纯 helper 可测 | T-sw (sw-push.js + test/sw-push.test.ts) |
| 12 | A1-FR9 mute 记为 in-app-only | N-push-ui (注记), 全局 DND 走 NOTIFY_DND |
| 13 | FR-B1.9 ?base 明确延后 P2 |
N-diff-be (注记) |
| 14 | /hook/decision 响应 204 |
§3.5, T-server-wire |
| 15 | 遥测单一真相源 (TerminalSession) | §3.5, T-termsession, T-tabs (TabEntry 不重复) |
| 16 | config 计数确认 21 | T-config |
9. 一句话总结
v0.7 拆成 27 个单-owner-per-shared-file 任务, 4 波 (协调→叶子并行→后端汇聚→前端汇聚) + 1 阻断研究 (R0) + 3 报告-only 验证; 5 处 FE↔BE 类型契约在 §3 于 T-types 一次定死; 安全 30 项折叠到 owner; 服务端仍不解析一个终端字节 (新增仅 git 子进程、带外 JSON、出站推送签名)。