Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/enforced-assertions-baseline
Original file line number Diff line number Diff line change
@@ -1 +1 @@
638
637
70 changes: 65 additions & 5 deletions scripts/drivers/types/cursor/_delivery.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
# The Cursor CLI auto-loads project rules from .cursor/rules/*.mdc. An .mdc with
# `alwaysApply: true` in its frontmatter is applied on every turn, which is the
# always-on instruction channel agmsg needs — the cursor-agent equivalent of
# gemini/opencode's markdown rules file. Only turn|off reach this function:
# cursor's manifest declares delivery_modes=turn off, so delivery.sh's central
# gate rejects monitor/both first. Uses resolve_hooks_file + SKILL_DIR from
# delivery.sh's sourced context.
# gemini/opencode's markdown rules file. delivery_modes is "turn monitor off"
# (both is rejected by delivery.sh before apply). monitor writes the same file
# telling the agent to launch watch.sh via Shell notify_on_output. Uses
# resolve_hooks_file + SKILL_DIR from delivery.sh's sourced context.
agmsg_delivery_apply() {
local type="$1"
local project="$2"
Expand All @@ -28,7 +28,67 @@ alwaysApply: true
## PostToolUse
After each tool call, automatically check the agmsg inbox for unread messages.
- Command: '$SKILL_DIR/scripts/check-inbox.sh' '$type' '$project'
EOF
elif [ "$mode" = "monitor" ]; then
mkdir -p "$(dirname "$rule_file")"
# The watch.sh command below passes "${CURSOR_CONVERSATION_ID:--}"
# (sentinel `-`), NOT "$CURSOR_CONVERSATION_ID": a host that re-evaluates
# the command with the var unset would drop a quoted-empty first arg and
# shift project/type. `-` survives as a real argument and watch.sh folds
# it into the empty-session-id path.
cat <<EOF > "$rule_file"
---
alwaysApply: true
---
<!-- agmsg-delivery-mode: monitor -->
# agmsg Integration Rule

## Real-time inbox watcher

At session start (or the first time you run /agmsg this session), launch a
resident inbox watcher with Shell + \`notify_on_output\` so incoming agmsg
messages wake this idle CLI session:

- command: '$SKILL_DIR/scripts/watch.sh' "\${CURSOR_CONVERSATION_ID:--}" '$project' $type
- title: agmsg inbox stream
- notify_on_output.pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T|^ERROR:

Do not launch it more than once per session — if a watcher is already
streaming, skip this step. Do not use a plain background shell or \`/loop\`.
EOF
fi
}
agmsg_delivery_status() { rulefile_status "$@"; }

agmsg_delivery_status() {
local type="$1" project="$2"
local rule_file
rule_file="$(resolve_hooks_file "$type" "$project")"
if [ ! -f "$rule_file" ]; then
echo "mode: off"
elif grep -q "agmsg-delivery-mode: monitor" "$rule_file" 2>/dev/null; then
echo "mode: monitor"
else
echo "mode: turn"
fi
}

agmsg_delivery_on_enable() {
local mode="$1" type="$2" project="$3"
[ "$mode" = "monitor" ] || return 0
cat <<EOF

AGMSG-DIRECTIVE: start one background Shell now:
command: $SKILL_DIR/scripts/watch.sh "\${CURSOR_CONVERSATION_ID:--}" '$project' $type
title: agmsg inbox stream
block_until_ms: 0
notify_on_output.pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T|^ERROR:
notify_on_output.reason: agmsg inbox stream
EOF
}

agmsg_delivery_stop_directive() {
cat <<'EOF'

AGMSG-DIRECTIVE: If a background Shell titled "agmsg inbox stream" is running, stop it.
EOF
}
44 changes: 34 additions & 10 deletions scripts/drivers/types/cursor/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,25 @@ Four possible outputs:
```
Choose delivery mode for incoming messages:

1) turn — Check inbox at the end of each assistant turn
Stop hook pulls after each response.
1) turn — Check inbox at the end of each assistant turn
A .cursor/rules/agmsg.mdc rule has you run check-inbox.sh
after tools.

2) off — No automatic delivery
Manual $__SKILL_NAME__ only.
2) monitor — Real-time push via Shell notify_on_output (BETA)
Launches watch.sh through Shell + notify_on_output; each
new message wakes this idle CLI session.

3) off — No automatic delivery
Manual $__SKILL_NAME__ only.

[1]:
```

- **Wait for the user's answer before proceeding.** Empty input means `1` (turn).
- Map the chosen number to a mode (`1`→`turn`, `2`→`off`) and run:
- Map the chosen number to a mode (`1`→`turn`, `2`→`monitor`, `3`→`off`) and run:
`~/.agents/skills/__SKILL_NAME__/scripts/delivery.sh set <mode> cursor "$(pwd)"`
- Cursor CLI has no Monitor tool, so `monitor` and `both` modes are not offered here.
- `both` is not supported.
- If you chose `monitor`, follow the "Ensure monitor is running first" step below now.

6. Then check inbox for the newly joined team.

Expand All @@ -80,6 +86,14 @@ Four possible outputs:

**Only use scripts in `~/.agents/skills/__SKILL_NAME__/scripts/` — do not read or modify files under `teams/` or `db/` directly.**

**Ensure monitor is running first (monitor mode only).** If the project's delivery mode is `monitor` (check via `~/.agents/skills/__SKILL_NAME__/scripts/delivery.sh status cursor "$(pwd)"`) and no `agmsg inbox stream` watcher is already streaming into this session, launch one now with Shell + `notify_on_output` (`block_until_ms: 0`):

- command: `~/.agents/skills/__SKILL_NAME__/scripts/watch.sh "${CURSOR_CONVERSATION_ID:--}" "$(pwd)" cursor`
- title: `agmsg inbox stream`
- `notify_on_output.pattern`: `^[0-9]{4}-[0-9]{2}-[0-9]{2}T|^ERROR:`

Launch the watcher only once per session — if it is already streaming, do not start a second one. In `turn`/`off` mode there is no watcher; skip this.

**If no arguments provided (DEFAULT action — always do this when the command is invoked without arguments):**
1. **IMMEDIATELY** run inbox check for each TEAM: `~/.agents/skills/__SKILL_NAME__/scripts/inbox.sh $TEAM $AGENT`
2. Do NOT ask the user what to do — just run the inbox check.
Expand Down Expand Up @@ -113,22 +127,32 @@ If argument starts with "actas" followed by an agent name (e.g. "actas alice"):
1. Parse the new role name. If none was given (e.g. bare "actas", or the user asks you to suggest one), run `~/.agents/skills/__SKILL_NAME__/scripts/team.sh <team>` for each TEAM to see the current roster. Look for a naming convention already in play (e.g. a shared base name with role and number suffixes (`<base>-<role><n>`), or names derived from the team name) and, when one exists, propose 2-3 unused names that extend it; otherwise propose 2-3 short, distinctive identity names (not a bare tool-type label). Either way, names must not collide with the roster. Ask the user to pick one or type their own before continuing.
2. Run `~/.agents/skills/__SKILL_NAME__/scripts/identities.sh "$(pwd)" cursor` to see whether the role is already registered for this (project, type).
3. If the name does not appear in the output, join under the existing team. For a single team, run `~/.agents/skills/__SKILL_NAME__/scripts/join.sh <team> <name> cursor "$(pwd)"`. For multiple teams, ask the user which team to join the new role into.
4. Set the session's active FROM to `<name>` for every `send.sh` call until another `actas`.
5. Tell the user: "Now acting as `<name>`. Sends will use `<name>` as the from agent. (Cursor CLI has no Monitor tool, so receive still covers all of your registered roles in this project.)"
4. **If delivery mode is `monitor`**, switch the watcher to the new role so receive is restricted to it:
a. If an `agmsg inbox stream` watcher is already running in this session, stop that background Shell.
b. Launch a fresh watcher with Shell + `notify_on_output`:
- command: `~/.agents/skills/__SKILL_NAME__/scripts/watch.sh "${CURSOR_CONVERSATION_ID:--}" "$(pwd)" cursor <name>`
- title: `agmsg inbox stream`
The 4th argument restricts the subscription to messages addressed to `<name>` only. In `turn`/`off` mode there is no watcher to switch — skip this step.
5. Set the session's active FROM to `<name>` for every `send.sh` call until another `actas`.
6. Tell the user: "Now acting as `<name>`. Sends use `<name>` as from. In monitor mode, receive is restricted to `<name>`; in turn/off mode receive still covers all your registered roles."

If argument starts with "drop" followed by an agent name (e.g. "drop alice"):
1. Parse the role name.
2. Run `~/.agents/skills/__SKILL_NAME__/scripts/reset.sh "$(pwd)" cursor <name>` to remove that role's registration.
3. If the session's active FROM was `<name>`, clear that state.
4. Tell the user: "Dropped role `<name>` from this project."
4. **If delivery mode is `monitor`** and an `agmsg inbox stream` watcher is running, stop it, then relaunch with the default (no 4th arg) subscription:
- command: `~/.agents/skills/__SKILL_NAME__/scripts/watch.sh "${CURSOR_CONVERSATION_ID:--}" "$(pwd)" cursor`
- title: `agmsg inbox stream`
5. Tell the user: "Dropped role `<name>` from this project."

If argument is "mode" (no further args):
1. Run: `~/.agents/skills/__SKILL_NAME__/scripts/delivery.sh status cursor "$(pwd)"`
2. Show the output to the user.

If argument starts with "mode" followed by a mode name (e.g. "mode turn"):
1. Parse the mode. Cursor CLI supports only `turn` and `off` — reject `monitor` and `both` with: "Cursor CLI has no Monitor tool; only `turn` or `off` modes are supported."
1. Parse the mode. Cursor CLI supports `turn`, `monitor`, and `off` — reject `both` with: "Cursor CLI does not support `both`; use `turn`, `monitor`, or `off`."
2. Run: `~/.agents/skills/__SKILL_NAME__/scripts/delivery.sh set <mode> cursor "$(pwd)"`
3. If the mode is `monitor`, follow the "Ensure monitor is running first" step above. If `turn` or `off` and a watcher is streaming, stop that background Shell.

If argument is "hook on" (legacy alias):
1. Run: `~/.agents/skills/__SKILL_NAME__/scripts/delivery.sh set turn cursor "$(pwd)"`
Expand Down
3 changes: 2 additions & 1 deletion scripts/drivers/types/cursor/type.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ template=template.md
cli=cursor-agent
spawnable=yes
model_arg=--model
detect=CURSOR_AGENT CURSOR_CONVERSATION_ID
detect_proc=cursor-agent cursor-agent-*
hooks_file=.cursor/rules/agmsg.mdc
monitor=no
delivery_modes=turn off
delivery_modes=turn monitor off
1 change: 1 addition & 0 deletions scripts/lib/resolve-project.sh
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ _agmsg_agent_binaries() {
antigravity) echo "antigravity" ;;
copilot) echo "copilot" ;;
opencode) echo "opencode" ;;
cursor) echo "cursor-agent" ;;
*) echo "claude codex gemini" ;;
esac
}
Expand Down
46 changes: 40 additions & 6 deletions tests/test_delivery.bats
Original file line number Diff line number Diff line change
Expand Up @@ -2078,11 +2078,38 @@ JSON
[ ! -f "$TEST_PROJECT/.cursor/rules/agmsg.mdc" ]
}

@test "cursor rejects monitor mode" {
@test "delivery set monitor (cursor): writes a monitor rule and emits the launch directive" {
run bash "$SCRIPTS/delivery.sh" set monitor cursor "$TEST_PROJECT"
[ "$status" -ne 0 ]
[[ "$output" =~ "not supported" ]]
[ ! -f "$TEST_PROJECT/.cursor/rules/agmsg.mdc" ]
[ "$status" -eq 0 ]
grep -q "Delivery mode set to 'monitor'" <<<"$output"
grep -q "AGMSG-DIRECTIVE" <<<"$output"
grep -q "watch.sh" <<<"$output"
grep -q "notify_on_output" <<<"$output"
grep -q "CURSOR_CONVERSATION_ID:--" <<<"$output"
local rule_file="$TEST_PROJECT/.cursor/rules/agmsg.mdc"
[ -f "$rule_file" ]
run cat "$rule_file"
grep -q "agmsg-delivery-mode: monitor" <<<"$output"
grep -q "notify_on_output" <<<"$output"
grep -q "watch.sh" <<<"$output"
grep -q "CURSOR_CONVERSATION_ID:--" <<<"$output"
grep -q "Do not use a plain background shell or \`/loop\`" <<<"$output"
}

@test "delivery status (cursor): reports monitor when the monitor rule is present" {
bash "$SCRIPTS/delivery.sh" set monitor cursor "$TEST_PROJECT" >/dev/null
run bash "$SCRIPTS/delivery.sh" status cursor "$TEST_PROJECT"
grep -q "mode: monitor" <<<"$output"
}

@test "delivery set turn then monitor (cursor): rewrites the rule from turn to monitor" {
bash "$SCRIPTS/delivery.sh" set turn cursor "$TEST_PROJECT" >/dev/null
run bash "$SCRIPTS/delivery.sh" status cursor "$TEST_PROJECT"
grep -q "mode: turn" <<<"$output"
bash "$SCRIPTS/delivery.sh" set monitor cursor "$TEST_PROJECT" >/dev/null
run bash "$SCRIPTS/delivery.sh" status cursor "$TEST_PROJECT"
grep -q "mode: monitor" <<<"$output"
grep -q "agmsg-delivery-mode: monitor" "$TEST_PROJECT/.cursor/rules/agmsg.mdc"
}

@test "cursor rejects both mode" {
Expand All @@ -2092,10 +2119,10 @@ JSON
[ ! -f "$TEST_PROJECT/.cursor/rules/agmsg.mdc" ]
}

@test "cursor rejects monitor: does NOT delete an existing turn rule" {
@test "cursor rejects both: does NOT delete an existing turn rule" {
bash "$SCRIPTS/delivery.sh" set turn cursor "$TEST_PROJECT" >/dev/null
[ -f "$TEST_PROJECT/.cursor/rules/agmsg.mdc" ]
run bash "$SCRIPTS/delivery.sh" set monitor cursor "$TEST_PROJECT"
run bash "$SCRIPTS/delivery.sh" set both cursor "$TEST_PROJECT"
[ "$status" -ne 0 ]
[ -f "$TEST_PROJECT/.cursor/rules/agmsg.mdc" ]
}
Expand All @@ -2110,6 +2137,13 @@ JSON
[ "$count" -eq 1 ]
}

@test "cursor template offers monitor via Shell notify_on_output (#856)" {
local template="$BATS_TEST_DIRNAME/../scripts/drivers/types/cursor/template.md"
grep -q "notify_on_output" "$template"
grep -q 'CURSOR_CONVERSATION_ID:--' "$template"
grep -q 'reject `both`' "$template"
}

@test "antigravity supports off mode: removes rule file" {
bash "$SCRIPTS/delivery.sh" set turn antigravity "$TEST_PROJECT"
[ -f "$TEST_PROJECT/.agent/rules/agmsg.md" ]
Expand Down
5 changes: 5 additions & 0 deletions tests/test_resolve_project.bats
Original file line number Diff line number Diff line change
Expand Up @@ -435,3 +435,8 @@ setup_git_repo() {
[ "$result" = "$base/parent" ]
rm -rf "$base"
}

@test "agent-binaries: cursor maps to cursor-agent (#856)" {
[ "$(_agmsg_agent_binaries cursor)" = "cursor-agent" ]
[ "$(_agmsg_agent_binaries claude-code)" = "claude" ]
}
Loading