Skip to content

feat(hosts): add a cron bridging helper for macOS/Linux, wire claude_code - #595

Open
Shawn-Dong wants to merge 6 commits into
NevaMind-AI:mainfrom
Shawn-Dong:feat/unix-schedule-helper
Open

feat(hosts): add a cron bridging helper for macOS/Linux, wire claude_code#595
Shawn-Dong wants to merge 6 commits into
NevaMind-AI:mainfrom
Shawn-Dong:feat/unix-schedule-helper

Conversation

@Shawn-Dong

Copy link
Copy Markdown
Contributor

Stacked on #594 (merge that first — this branch contains its commits).

What

The Unix half of the schedule helper. #546 gave Windows memu-claude-code schedule {install,verify,status,uninstall}; this PR makes the same four verbs work on macOS/Linux against cron, so registration becomes one deterministic command instead of agent-followed docs.

  • install — writes bridge-prompt.txt + a locked bridge.sh (PATH baked in, cd into the host tree) and registers one short crontab entry. Replaces, never duplicates, any previous entry for the host — including a legacy inlined one ([Bug] macOS cron 会把 bridging 指南里 1320 字符的整行命令截断——record seam 在 macOS 上一次都跑不起来 #591), which install thereby migrates.
  • verify — proves the entry can actually run (files sane, prompt matches the canon, agent resolves and authenticates headless) without waiting for the next tick.
  • status / uninstall — inspect and remove by the script path; uninstall handles both layouts and keeps the run log.

One backend per platform behind the same verb surface; the auth/resolution gates are shared from windows.py (they were always platform-neutral). claude_code only, mirroring #546 — cursor fan-out is the follow-up, like #571.

Field evidence

The macOS error stack this automates away (#591, hourly, before the fix):

/bin/sh: -c: line 0: unexpected EOF while looking for matching `''
/bin/sh: -c: line 1: syntax error: unexpected end of file

Real-machine check on the same Mac: schedule status against a live install correctly reports the crontab entry and a held lock mid-run. 237 tests, mypy + ruff clean.


依赖 #594(请先合并;本分支包含其提交)。

做了什么

schedule helper 的 Unix 半边。#546 给了 Windows 四个动词;本 PR 让同样的 schedule {install,verify,status,uninstall} 在 macOS/Linux 上对 cron 生效——注册从"agent 照文档手抄"变成一条确定命令。

同一动词表面、按平台选后端;认证/解析闸门从 windows.py 复用(本就是平台无关的)。只接线 claude_code,与 #546 同构——cursor 扇出是后续 PR,如 #571

实测依据

#591 的 macOS 错误栈(修复前每个整点):

/bin/sh: -c: line 0: unexpected EOF while looking for matching `''
/bin/sh: -c: line 1: syntax error: unexpected end of file

同一台 Mac 真机检查:schedule status 对存量安装正确报告 crontab 条目与运行中的锁。237 个测试,mypy + ruff 干净。

🤖 Generated with Claude Code

Shawn-Dong and others added 6 commits July 27, 2026 18:06
…tes ~1KB lines

The Unix registration steps in claude_code, cursor, hermes, and generic
BRIDGING_TASK.md all instructed writing the full ~1.2KB pipeline prompt
as a single-quoted inline crontab command. macOS/BSD cron truncates
crontab lines around 1KB before handing them to /bin/sh, so every tick
died instantly with "unexpected EOF while looking for matching quote" —
mailed to /var/mail/$USER and visible nowhere else; the agent binary
never started. Field-confirmed on two hosts' entries (claude-code,
hermes) failing on every tick while the short script-invoking codex
entry beside them kept working.

Fix mirrors the Windows schtasks/TR solution (memU#539) already in the
same docs: the prompt lives verbatim in
~/.memu/hosts/<host>/pipeline-prompt.txt, a small bridge.sh (with an
atomic mkdir single-instance lock, so an hourly tick can't race a
still-running backlog run) reads it and logs to bridging.log, and the
crontab entry is one short line invoking the script. Verified in the
field: the fixed entry's next tick executed cleanly and the lock
correctly skipped a concurrent run.

Also fixed in passing: hermes' inline example invoked `hermes -p`, but
Hermes' one-shot flag is `-z`/`--oneshot` — the entry could never have
worked even without truncation.

UNINSTALL.md Part 1 now matches both the new bridge.sh entry and legacy
inline entries; claude_code INSTALL.md's cron-entry detection grep
likewise matches both layouts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two tests locking the docs' pipeline prompt to
bridging_pipeline_prompt() grepped for the inline `-p 'Run the memU`
cron line, which the previous commit replaced with a prompt-file block.
Anchor on the prompt line itself (the single-line fence written to
pipeline-prompt.txt) instead — same verbatim-equality guarantee, new
location. Also refresh prompt.py's module docstring, which still
described the Unix mechanism as pasting the prompt into the crontab.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ridge script

cursor-agent refuses headless runs in an untrusted directory (Workspace
Trust Required, exit 1) — field-verified in memU#571, which flagged the
Unix cron path as still missing the flag. The wrapper now cds into
~/.memu/hosts/cursor and invokes cursor-agent --trust, so trust lands on
memU's own tree rather than wherever cron starts, mirroring the wired
Windows template. Never --yolo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…try length, document lock tradeoff, make hermes native-first

Four review points, all pre-merge cleanups:

- Filenames converge with the shipped Windows helper: bridge-prompt.txt
  (was pipeline-prompt.txt) and bridge.log (was bridging.log), so the
  planned Unix schedule helper and every cross-platform doc carry one
  naming scheme instead of two forever. bridge.sh keeps its name — the
  extension already distinguishes it from memu-bridge.ps1.

- New regression gate: a parameterized test over the four Unix guides
  asserts every cron entry line stays under 512 chars. The prompt-lock
  tests catch content drift but not re-inlining — this catches the
  actual bug class (memU#591) at test time instead of on the next Mac.
  Generic's entry moved into a fenced block so the gate sees it.

- The 3h lock-reclaim tradeoff is now spelled out in the script comment:
  a legitimate >3h run loses its lock to the next tick and can double-run,
  accepted deliberately over a crashed run wedging the schedule forever.

- Hermes: native cronjob is now the recommended path in so many words;
  raw crontab is explicitly fallback-only, with the history note that no
  install is known to have ever succeeded through it (the nonexistent -p
  flag survived in the guide precisely because nobody ran that path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…code

The Unix half of the schedule helper (NevaMind-AI#546 shipped the Windows half):
`memu-claude-code schedule {install,verify,status,uninstall}` now works on
macOS/Linux against cron. install writes bridge-prompt.txt + a locked
bridge.sh (PATH baked in, cd into the host tree) and registers one short
crontab entry — replacing, never duplicating, any previous entry for the
host, including a legacy inlined one (memU#591), which install thereby
migrates. verify proves the entry can run without waiting for a tick;
uninstall removes both layouts and keeps the run log.

The scheduling package fronts both backends behind one verb surface,
picked by platform. The auth/resolution gates are imported from
windows.py — they were always platform-neutral. claude_code's guide now
leads with the helper and keeps the manual registration as the fallback.

Real-machine check: `schedule status` against a live install correctly
reports the crontab entry and a held lock mid-run. 237 tests, mypy and
ruff clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MrXnneHang

MrXnneHang commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

claude code 和 cursor 可以直接这么应用。

但是 hermes 和 generic 不能简单直接推广,特别是 generic。

windows 的 scheduled task 只直接接了 cursor 和 claude code。

@xnne-bot

Copy link
Copy Markdown
Contributor

补充一下 @MrXnneHang 上面三点的技术依据,方便对照代码。

为什么 claude_code 和 cursor 能直接接、另外两个不能 —— 这条界线在代码里就是 HostSpec.schedule_command:只有 claude_code/cli.py:45cursor/cli.py:46 设了它,而 host_cli.py 里的 if spec.schedule_command: 决定 schedule 子命令只对它们注册。所以 hermes/generic 在 Windows 和 Unix 上都根本没有这个命令。这个 PR 原样保留了这道门,方向是对的。

hermes 有两个卡点:

  • scheduling/prompt.py:42:51 生成的 prompt 写死了 with bash,而 hermes 的规范是 with the shell tool(hermes/BRIDGING_TASK.md 里两处)。这个措辞要先变成 HostSpec 的一个字段,否则 helper 生成的 prompt 会和 hermes 自己的文档对不上。注意 prompt.py 是两个后端共用的,所以这个卡点在 cron 上同样存在,不是 Windows 特有的。
  • hermes 有自己的内部 cron,fix(hosts): never inline the bridging prompt in crontab — cron truncates ~1KB lines #594 的文档改动已经把它标为首选路径;再注册一条 OS 级条目会对同一条 pipeline 重复调度。

generic 更难,这也是「特别是 generic」的原因: schedule_command 是围绕「已知二进制 + 已知 headless flag」做的模板,而 generic 这两样按定义都由用户提供(文档里就是 <agent-cli> <headless-flag> 占位符),外加一个机器相关的 --session-dir。要支持它就得开一个 --command 直通,那恰好是这个 helper 想消灭的现场自由发挥。

顺带一提,generic 的 cron 不是 fallback 而是主路径(generic/BRIDGING_TASK.md:71:默认系统 cron,只有用户特别要求才改用 agent 自己的调度器)—— 因为「generic」意味着 memU 不认识这个 agent,没有原生调度器可以假设。这也正是内联 prompt 那个 bug 在 generic 上最危险的地方:主路径、最多即兴发挥、又没有一个具名二进制可以自检。

另外有一个和这个 PR 直接相关的点:cursor 在 Unix 上其实已经被打开了,但文档没跟上。 PR 描述写的是 "claude_code only,cursor fan-out is the follow-up",实际不是:

也就是说合并后 memu-cursor schedule install 在 macOS/Linux 上就能用,但 cursor/BRIDGING_TASK.md 里 helper 那段在 ## Windows (Task Scheduler)(161 行)下面,Unix 的 ## Step 2(58 行)还是让人手工注册。补一段 Unix 的 helper 说明,或者把 PR 描述改成 claude_code + cursor,两种都行。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants