Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ What EMRG does today — capability statements, not a changelog.
One line per command: what it answers. Run the daemon in the foreground with `python -m emrg`.

- Python: `uv run pytest tests/ -v` — the number is **measured, never stored**: `uv run --no-sync python3 scripts/check-doc-count.py --measure`. Import check: `uv run python -c "from emrg.client.app import run_client"`
GUI: `cd emrg/gui && npm test` (166: 100 daemon_client + 20 conn-manager + 7 integration + 7 nav-policy + 7 gui-state + 9 build-config + 9 boot-contract + 3 preload-api + 2 theme-guard + 2 image-hash) — syntax: `node --check main.js preload.js daemon_client.js`
Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (716: 5 snapshot-store + 13 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 13 markdown + 29 transcript + 28 TranscriptView + 22 history + 25 historyReplay + 40 composer + 75 Composer + 6 LinkDialog + 16 sidebar + 18 Sidebar + 9 fileTree + 9 FileTree + 17 resultPanel + 8 ResultPanel + 27 workspaceView + 13 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 8 openSession + 6 WelcomeDialog + 9 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 56 daemonBridge + 9 DaemonBridgeProvider + 48 Shell + 24 DialogHost + 26 SettingsPanel + 6 TaskFormDialog + 5 RantDialog + 4 vendorMarkdown + 6 ApprovalDialog + 14 SandboxRootsDialog)
GUI: `cd emrg/gui && npm test` (168: 100 daemon_client + 20 conn-manager + 7 integration + 7 nav-policy + 7 gui-state + 9 build-config + 11 boot-contract + 3 preload-api + 2 theme-guard + 2 image-hash) — syntax: `node --check main.js preload.js daemon_client.js`
Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (729: 5 snapshot-store + 13 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 13 markdown + 29 transcript + 28 TranscriptView + 22 history + 25 historyReplay + 40 composer + 75 Composer + 6 LinkDialog + 16 sidebar + 18 Sidebar + 9 fileTree + 9 FileTree + 17 resultPanel + 8 ResultPanel + 27 workspaceView + 13 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 8 openSession + 6 WelcomeDialog + 9 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 56 daemonBridge + 9 DaemonBridgeProvider + 49 Shell + 24 DialogHost + 26 SettingsPanel + 18 TaskFormDialog + 5 RantDialog + 4 vendorMarkdown + 6 ApprovalDialog + 14 SandboxRootsDialog)
- CI: pytest on ubuntu **and windows-2025**, the GUI suites, and an actionlint gate over `.github/workflows/`; re-trigger a dropped push with `scripts/re-trigger-ci.sh <branch>`, never an empty commit
- Merge gates, run before merging — each asks what no other gate asks: `uv run --no-sync python3 scripts/check-vote-count.py` (approvals still valid) · `scripts/check-pr-base.py` (base reaches master) · `scripts/check-merge-freshness.py` (that green CI is about the landing tree) · `scripts/check-merge-order.py` (dirt and what it lands) · `scripts/check-merge-landing-diff.py` (what the landing changes) · `scripts/check-merge-pairs.py` / `scripts/check-merge-sequence.py` (a pair, or a whole order) · `scripts/check-merge-tree-health.py` (the merged tree passes one guard) · `scripts/check-merge-plan-suite.py` (the plan's final tree passes the suite) — all under `uv run --no-sync python3`
- Post-merge audit: `uv run --no-sync python3 scripts/check-merge-landed.py <N>` (did the merge land a tree its reviews named)
Expand Down
19 changes: 16 additions & 3 deletions emrg/gui/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,14 @@ vision = false
return frame.tasks || [];
});

ipcMain.handle("emrg:taskLimits", async () => {
// 间隔下限的唯一来源是 daemon 的 MIN_INTERVAL(issue #2490):渲染层问这里,
// 不再自己写一份。拿不到就走 undefined,让 daemon 的权威校验说话。
const frame = await requireConn().sendCommandAndWait("list_tasks", {}, 5000);
const v = frame.min_interval;
return typeof v === "number" && Number.isFinite(v) ? { minInterval: v } : {};
});

// renderer → main 日志桥(rant 2026-08-24:打开会话链路排障)。级别白名单,
// 消息截断到 2000 字符防刷屏。写同一 emrg-gui.log,时间线可与 main 日志对齐。
ipcMain.handle("emrg:log", async (_e, { level, msg } = {}) => {
Expand All @@ -960,14 +968,19 @@ vision = false

// ── Task/template CRUD IPC (rant 2026-08-12T18:23:15 P3) ──────────
ipcMain.handle("emrg:taskCreate", async (_e, payload) => {
const { name, type, project, interval, enabled, repo, description, sandbox } = payload || {};
const { name, type, project, interval, enabled, repo, description, sandbox, schedule } = payload || {};
if (typeof name !== "string" || !name.trim()) throw new Error("invalid task name");
if (typeof type !== "string" || !type.trim()) throw new Error("invalid task type");
if (typeof project !== "string" || !project.trim()) throw new Error("invalid project");
const frame = await requireConn().sendCommandAndWait("task_create", {
// `schedule` 必须原样转发:这张固定字段表此前把它丢掉,于是 GUI 永远只能建
// 单一频次的任务,daemon 收了也白收(issue #2490)。只有 payload 真的带了这个
// 键才发,缺席=不声明,与 task_update 的「缺省不动该字段」一致。
const fields = {
name: name.trim(), task_type: type.trim(), project: project.trim(),
interval, enabled, repo, description, sandbox,
}, 8000);
};
if ("schedule" in (payload || {})) fields.schedule = schedule;
const frame = await requireConn().sendCommandAndWait("task_create", fields, 8000);
if (!frame.ok && frame.error) throw new Error(frame.error);
return frame;
});
Expand Down
2 changes: 2 additions & 0 deletions emrg/gui/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ const api = {
registerProject: (payload) => ipcRenderer.invoke("emrg:registerProject", payload),
removeProject: (payload) => ipcRenderer.invoke("emrg:removeProject", payload),
listTasks: () => ipcRenderer.invoke("emrg:listTasks"),
// 间隔下限由 daemon 上报(issue #2490)——渲染层不留第二份数字
taskLimits: () => ipcRenderer.invoke("emrg:taskLimits"),
triggerTask: (payload) => ipcRenderer.invoke("emrg:triggerTask", payload),
// Task/template CRUD (rant 2026-08-12T18:23:15 P3)
taskCreate: (payload) => ipcRenderer.invoke("emrg:taskCreate", payload),
Expand Down
29 changes: 29 additions & 0 deletions emrg/gui/renderer/src/components/Shell.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ function mockEmrg() {
const taskUpdate = vi.fn().mockResolvedValue({ ok: true });
const taskDelete = vi.fn().mockResolvedValue({ ok: true });
const taskTemplateList = vi.fn().mockResolvedValue([{ name: "journal" }]);
// issue #2490:间隔下限由 daemon 上报 —— 这里刻意给 120(不是渲染层曾写死的 60),
// 于是「表单拿到的就是上报值」这件事在接线层可断言。
const taskLimits = vi.fn().mockResolvedValue({ minInterval: 120 });
const sendRant = vi.fn().mockResolvedValue({ ok: true, count: 11 });
const restartDaemon = vi.fn().mockResolvedValue({ ok: true });
const setModel = vi.fn().mockResolvedValue({ ok: true });
Expand All @@ -62,6 +65,7 @@ function mockEmrg() {
taskUpdate,
taskDelete,
taskTemplateList,
taskLimits,
sendRant,
restartDaemon,
setModel,
Expand All @@ -84,6 +88,7 @@ function mockEmrg() {
taskUpdate,
taskDelete,
taskTemplateList,
taskLimits,
sendRant,
restartDaemon,
setModel,
Expand Down Expand Up @@ -770,6 +775,30 @@ describe("Shell (Batch 5 slice 3 chat wiring)", () => {
expect(screen.queryByTestId("task-form-dialog")).not.toBeInTheDocument();
});

it("表单的间隔下限来自 daemon 上报(taskLimits),不是渲染层写死的数", async () => {
const m = mockEmrg();
render(wrapper(<Shell />));
await waitFor(() => expect(screen.getByTestId("composer")).toBeInTheDocument());
fireEvent.click(screen.getByTestId("nav-tasks"));
await waitFor(() => expect(screen.getByTestId("task-row")).toBeInTheDocument());
fireEvent.click(screen.getByText("+ Add task"));
await waitFor(() => expect(screen.getByTestId("task-form-dialog")).toBeInTheDocument());
// 接线本身:表单打开时问过 daemon 的下限
await waitFor(() => expect(m.taskLimits).toHaveBeenCalled());

fireEvent.change(screen.getByTestId("task-form-name"), { target: { value: "t3" } });
// 上报值是 120:写成 60(渲染层曾经写死的那个数)必须被拒,且错误文案带 120
fireEvent.change(screen.getByTestId("task-form-interval"), { target: { value: "60" } });
fireEvent.click(screen.getByTestId("task-form-save"));
expect(screen.getByTestId("task-form-error")).toHaveTextContent("120");
expect(m.taskCreate).not.toHaveBeenCalled();

fireEvent.change(screen.getByTestId("task-form-interval"), { target: { value: "120" } });
fireEvent.click(screen.getByTestId("task-form-save"));
await waitFor(() => expect(m.taskCreate).toHaveBeenCalledTimes(1));
expect(m.taskCreate.mock.calls[0][0].interval).toBe(120);
});

it("任务行「Edit」→ 表单预填 + name 只读 → 保存 → taskUpdate(不改名)", async () => {
const m = mockEmrg();
render(wrapper(<Shell />));
Expand Down
17 changes: 17 additions & 0 deletions emrg/gui/renderer/src/components/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ interface WorkspaceBridge {
triggerTask(p: { name: string }): Promise<unknown>;
taskCreate(p: TaskFormPayload): Promise<unknown>;
taskUpdate(p: TaskFormPayload): Promise<unknown>;
/**
* 间隔下限(issue #2490):daemon 的 `MIN_INTERVAL`,由 `tasks_list` 帧上报。
* 渲染层不保留第二份数字 —— 拿不到就没有预检,由 daemon 权威校验。
*/
taskLimits?(): Promise<{ minInterval?: number }>;
taskDelete(p: { name: string }): Promise<unknown>;
taskTemplateList(): Promise<{ name: string }[]>;
sendRant(p: { message: string; project?: string }): Promise<{ ok?: boolean; count?: number }>;
Expand Down Expand Up @@ -128,6 +133,8 @@ export function Shell() {
const [dialogState, dispatch] = useReducer(dialogReducer, initialDialogState);
// Batch 5 slice 8:任务表单 + Rant 对话框(vanilla openTaskForm/openRantForm 语义)
const [taskForm, setTaskForm] = useState<{ task: TaskRec | null } | null>(null);
/** 间隔下限:daemon 上报的值(undefined = 还没问到,表单就不预检) */
const [minInterval, setMinInterval] = useState<number | undefined>(undefined);
const [rantOpen, setRantOpen] = useState(false);
const [taskTypes, setTaskTypes] = useState<string[]>([]);
// 升级横幅(rant 2026-08-27T21:54:51:React 迁移丢 upgrade 事件):dismissed 记录
Expand Down Expand Up @@ -372,6 +379,15 @@ export function Shell() {
setTaskTypes((prev) => (prev.length ? prev : ["evolution"]));
}
if (!isEdit) await loadProjects(); // 新建需要项目下拉
// 下限先问 daemon(issue #2490):问不到保持 undefined,表单不预检
if (b?.taskLimits) {
try {
const limits = await b.taskLimits();
if (typeof limits?.minInterval === "number") setMinInterval(limits.minInterval);
} catch {
/* 拿不到就交给 daemon 权威校验 */
}
}
setTaskForm({ task: isEdit ? task : null });
}

Expand Down Expand Up @@ -878,6 +894,7 @@ export function Shell() {
request={taskForm}
types={taskTypes}
projects={projects}
minInterval={minInterval}
onSubmit={(payload) => void saveTask(payload)}
onDismiss={() => setTaskForm(null)}
/>
Expand Down
Loading
Loading