feat(v0.7): Walk-away Workbench (Band A + B) — multi-agent parallel build

Implements docs/PLAN_WALKAWAY_WORKBENCH.md (27 tasks, waves R0→W0→W1×14→W2→W3→W4)
via module-builder agents. 23 tasks built, 0 blocked.

Band A (finish the walk-away loop): A1 Web Push + lock-screen approve/deny
(web-push dep), A2 voice dictation, A3 quick-reply chips + saved-prompt palette,
A4 activity timeline, A5 stuck/idle alert.
Band B (workbench above the terminal): B1 read-only git diff viewer, B2 statusLine
telemetry → per-tab cost/context/PR gauges, B3 create git worktrees from the UI,
B4 plan-mode / permission-mode relay.

New: src/push/* (subscription store + VAPID push), src/http/{diff,statusline}.ts,
src/session/timeline.ts, public/{diff,timeline,quickreply,push-ui,...}.ts, sw-push,
statusLine script; extends hook intake, manager, server routes (Origin/CSRF guards
+ per-IP rate limits on state-changing ones; loopback-only ingest), terminal-session,
tabs, projects detail, service worker, setup-hooks (statusLine + ntfy bridge).

Orchestrator reconciled a W0 contract gap: added the 21 v0.7 Config fields to the
Config interface in types.ts (T-types had left them only in config.ts's return).

Verified: both tsc clean, full vitest + coverage 91.4/84.1/92.2/93.4 (≥80×4),
build:web OK. W4 review: no CRITICAL/HIGH; all security checks pass. Follow-ups
(non-blocking): move approve.mode validation into parseClientMessage, drop CSP
ws:/wss: wildcard, validate worktree base ref, +2 targeted tests.
This commit is contained in:
Yaojia Wang
2026-06-30 17:42:18 +02:00
parent 4f1d3ebc6b
commit d6809c65c4
54 changed files with 13171 additions and 200 deletions

View File

@@ -32,6 +32,7 @@ import type {
Session,
SessionMeta,
ServerMessage,
TimelineEvent,
WebSocketLike,
} from '../types.js';
import { WS_OPEN } from '../types.js';
@@ -95,6 +96,9 @@ export function createSession(
...process.env,
WEBTERM_SESSION: id,
WEBTERM_HOOK_URL: `http://127.0.0.1:${cfg.port}/hook`,
// B2: statusLine scripts POST telemetry here; WEBTERM_NTFY_* vars forwarded
// via ...process.env spread above (H3 — token set in env, not here).
WEBTERM_STATUSLINE_URL: `http://127.0.0.1:${cfg.port}/hook/status`,
},
});
@@ -116,12 +120,25 @@ export function createSession(
cwd: cwd ?? null,
tmuxName: tName,
pty,
// v0.7 Walk-away Workbench fields (T-spawn-env):
timeline: Object.freeze([] as TimelineEvent[]),
stuckNotified: false, // A5: re-armed to false by each pty output
telemetry: null, // B2: updated by manager.handleStatusLine
};
// onData: persist to scrollback, refresh liveness, broadcast to all clients.
pty.onData((chunk) => {
session.buffer.append(chunk);
session.lastOutputAt = Date.now();
// A5 §3.4: re-arm the stuck flag on every output so each silent round can
// alert at most once. If we were stuck, recover to 'working' and broadcast
// so clients clear the stuck badge immediately — this is the ONLY reliable
// place to do this (same as lastOutputAt; T-manager never sees raw output).
session.stuckNotified = false;
if (session.claudeStatus === 'stuck') {
session.claudeStatus = 'working';
broadcast(session, { type: 'status', status: 'working' });
}
broadcast(session, { type: 'output', data: chunk });
});