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
35 changes: 31 additions & 4 deletions emrg/server/feishu_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ in the usual install locations before calling it blocked.
- Then read each conversation: `im +chat-messages-list --chat-id <id> --start <ISO> --end
<ISO> --order desc --no-reactions`. The messages are under `data.messages` — not
`data.items`.
- Read as the user, send as the bot: reading a direct conversation needs `--as user`, because a
bot cannot see a p2p conversation, while a message to the host is sent as the bot (§3). The
two identities differ, and an `im` call that names neither is answered by the CLI's own `auto`
default rather than by what the call is for.
- `im +chat-messages-list --chat-id <id> --order asc` is the listing that answers "has the host
replied", because it returns the conversation in the order it was written.
`im +messages-search` answers out of an index that lags a send by seconds, so it can never
establish that nothing came back.
- The conversation the host answers in is the one your own last message landed in (`oc_…`), so
keep that id beside the message id: §3 reads a reply by position, and a position means nothing
in a conversation you are not looking at.
- Skip what you sent yourself: the sender's `open_id` against the host's own, named in the
task instructions.
- Take the messages that arrived after the last one you handled. Remember that boundary
Expand Down Expand Up @@ -65,14 +76,25 @@ stranger's text can never widen it.

### 3. Send

`lark-cli im +messages-send --user-id <open_id>` (or `--chat-id <id>`) carries the text.

- **To the host's own account**: send it, this round, without asking. This is also the
channel you use to ask.
`lark-cli im +messages-send --as bot --user-id <open_id>` (or `--chat-id <id>`) carries the
text. Both halves are load-bearing: an `im` command that names no identity is resolved by the
CLI's `auto` default, which prefers the user account once that account is authorised — and the
message then lands in a conversation named after the host, i.e. the host messaging himself,
which Feishu notifies nobody about. The call exits successfully either way, so the conversation
the returned message id came back in is the only thing that tells the two apart.

- **To the host's own account**: send it, this round, without asking.
- **A question addressed to the host** goes on that same channel and needs no draft: whether a
message wants an answer at all, what that answer should say, and whether the host would rather
do the thing himself are questions, and a question is sent rather than filed. Send it this
round, carry on without waiting for the answer, and read the answer next round with §1's
recipe.
- **To anyone else**: first write the recipient and the text, word for word, into this
round's report, then ask the host — a Feishu message to the host's own account naming
the recipient and quoting that exact text — and
carry on with the rest of the round without waiting. Asking is not blocking; sending is.
Quoting word for word is about what a third party is going to receive; a question to the host
himself has no such text to quote.
- When a later round reads the host's answer to that request, send what you quoted, once,
unchanged. A no, or a request for different text, voids it; a changed text is a new
request.
Expand All @@ -86,6 +108,11 @@ stranger's text can never widen it.
Close the round with: how many arrived per verdict, what you did, which requests are waiting
for the host — with their full text and recipient — and what is blocked and why.

The report is a record, not a channel. A request it reports as waiting has already been asked,
so the report names the Feishu message id and the `oc_…` conversation it was asked in; a pending
request without them is one the host has not been told about, and the round counts it as unasked
rather than as progress.

### Forbidden

- 🛑 Never message anyone but the host's own account without the host's approval for that exact message
Expand Down
59 changes: 59 additions & 0 deletions tests/test_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,28 @@ def test_load_and_start_ios_app_task(tmp_path):
)


def _template_section(text: str, heading: str) -> str:
"""One `### ` section of a task template: its heading through to the next one.

A whole-file search cannot tell a rule from a mention. §3 named `lark-cli im +messages-send`
for months while naming no identity for it to run as, and a phrase asserted anywhere in the
file would have stayed green if §3 lost its own statement of the rule — which is the same
shape as the defect that sentence carried. So a rule is read from the section that owns it.

The bound has to stop: a region that runs to the end of the file is no bound, and it would
let a phrase in `Forbidden` satisfy a rule the section itself dropped. A missing next
heading is therefore a failure rather than an unbounded region.
"""
start = text.index(heading) + len(heading)
rest = text[start:]
end = rest.find("\n### ")
assert end != -1, (
f"{heading!r} is the last `###` section of this template, so where it ends cannot be "
f"read — and the pins below are only a rule about this section if the region stops"
)
return rest[:end]


def test_load_and_start_feishu_task(tmp_path):
"""`feishu` tasks start a TaskHandler with the `feishu` template.

Expand Down Expand Up @@ -531,6 +553,43 @@ def test_load_and_start_feishu_task(tmp_path):
):
assert phrase in flat, f"the feishu template must state that {why}; {phrase!r} is missing"

# The rules below are about **which identity an `im` call runs as** and **where a question
# goes**, and neither is visible to a phrase-in-file pin (see `_template_section`). The
# identity is the sharper half: it is the whole difference between a message the host is
# notified about and the same text arriving, unnoticed, in a conversation named after the
# host himself — and both calls exit successfully, so a round cannot learn the difference
# from an exit code.
for section, phrase, why in (
("### 1. Read", "`--as user`",
"reading a direct conversation needs the user identity, and the identity that reads is "
"not the identity that sends"),
("### 1. Read", "`im +messages-search`",
"the search index lags a send by seconds, so the section has to name the listing that "
"answers whether the host replied and say why the other cannot"),
("### 1. Read", "the one your own last message landed in",
"§3 reads a reply by position inside a conversation, so the round has to know which "
"conversation that is before the positional rule means anything"),
("### 3. Send", "+messages-send --as bot",
"a send that names no identity is resolved by the CLI's own `auto` default, which "
"prefers the user account once it is authorised — so the message reaches the host's "
"own conversation with no notification, and the call still exits successfully"),
("### 3. Send", "--user-id <open_id>",
"the recipient is part of the command: without it the conversation is chosen by that "
"same default rather than by the round"),
("### 3. Send", "**A question addressed to the host**",
"a question with no draft yet — whether a message wants an answer at all, what it "
"should say — has no procedure under the third-party rule, so the round files it in "
"the report instead, which is where the host does not read"),
("### 4. Report", "The report is a record, not a channel",
"a reported request reads as a discharged duty; the round has to name the message id "
"and the conversation it asked in, or count the request as unasked"),
):
body = _template_section(text, section)
assert phrase in body, (
f"{section.lstrip('# ')} of the feishu template must state that {why}; "
f"{phrase!r} is missing from that section"
)


def test_load_and_start_promote_task(tmp_path):
"""Promote tasks start a TaskHandler with the promote template."""
Expand Down
Loading