ticlawk 0.1.16-dev.3 → 0.1.16-dev.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/bin/ticlawk.mjs +207 -25
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +293 -70
- package/src/adapters/ticlawk/credentials.mjs +41 -1
- package/src/adapters/ticlawk/index.mjs +199 -199
- package/src/adapters/ticlawk/wake-client.mjs +1 -1
- package/src/cli/agent-commands.mjs +607 -37
- package/src/core/agent-cli-handlers.mjs +449 -20
- package/src/core/agent-home.mjs +86 -10
- package/src/core/argv.mjs +11 -1
- package/src/core/events/worker-events.mjs +32 -36
- package/src/core/http.mjs +126 -0
- package/src/core/runtime-env.mjs +7 -0
- package/src/core/runtime-support.mjs +108 -107
- package/src/migrate/write-initial-memory.mjs +5 -5
- package/src/runtimes/_shared/agent-handbook.mjs +45 -0
- package/src/runtimes/_shared/brand.mjs +2 -0
- package/src/runtimes/_shared/goal-task-protocol.mjs +50 -0
- package/src/runtimes/_shared/handbook/BASICS.md +27 -0
- package/src/runtimes/_shared/handbook/COLLABORATION.md +37 -0
- package/src/runtimes/_shared/handbook/COMMUNICATION.md +50 -0
- package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
- package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +43 -0
- package/src/runtimes/_shared/handbook/GOAL_TASK_CORE.md +43 -0
- package/src/runtimes/_shared/handbook/GROUP_ADMIN_SCOPE.md +21 -0
- package/src/runtimes/_shared/handbook/GROUP_MEMBER_SCOPE.md +15 -0
- package/src/runtimes/_shared/handbook/SURFACES.md +41 -0
- package/src/runtimes/_shared/handbook/TASK_WORKER.md +14 -0
- package/src/runtimes/_shared/standing-prompt.mjs +111 -262
- package/src/runtimes/_shared/wake-prompt.mjs +261 -0
- package/src/runtimes/claude-code/index.mjs +34 -127
- package/src/runtimes/claude-code/session.mjs +2 -7
- package/src/runtimes/codex/index.mjs +117 -54
- package/src/runtimes/codex/session.mjs +2 -12
- package/src/runtimes/openclaw/index.mjs +16 -26
- package/src/runtimes/opencode/index.mjs +45 -66
- package/src/runtimes/opencode/session.mjs +12 -12
- package/src/runtimes/pi/index.mjs +42 -60
- package/src/runtimes/pi/session.mjs +9 -6
- package/src/adapters/ticlawk/cards.mjs +0 -149
- package/src/core/media/outbound.mjs +0 -163
|
@@ -1,290 +1,139 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Standing prompt injected into every runtime turn.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Substitutions applied:
|
|
9
|
-
* slock → ticlawk
|
|
10
|
-
* channel → group
|
|
11
|
-
* command surface trimmed to what Ticlawk actually exposes today
|
|
12
|
-
*
|
|
13
|
-
* Adapted sections deliberately preserved verbatim where they encode the
|
|
14
|
-
* etiquette rules that make multi-agent coordination work without
|
|
15
|
-
* runtime-level orchestration. Trim with care; this prompt has been
|
|
16
|
-
* field-calibrated upstream.
|
|
4
|
+
* Keep this thin. The daemon writes the detailed Ticlawk handbook files
|
|
5
|
+
* into each agent workspace; this prompt only carries the non-negotiable
|
|
6
|
+
* routing and execution contract plus the handbook index.
|
|
17
7
|
*/
|
|
18
8
|
|
|
19
|
-
|
|
20
|
-
collaboration, serving as a shared message service for humans and agents
|
|
21
|
-
who may be running on different computers. You communicate with people
|
|
22
|
-
and other agents only through the Ticlawk CLI installed at \`ticlawk\`.
|
|
23
|
-
Your normal assistant output is private activity text — it is NOT sent
|
|
24
|
-
to users or groups.
|
|
25
|
-
|
|
26
|
-
## Critical rules
|
|
27
|
-
|
|
28
|
-
- Always communicate through the \`ticlawk\` CLI. This is your only output channel.
|
|
29
|
-
- Always claim a task via \`ticlawk task claim\` before doing any substantive work on it. If the claim fails, stop immediately and pick a different task.
|
|
30
|
-
- Use only the provided \`ticlawk\` CLI commands for messaging.
|
|
31
|
-
|
|
32
|
-
## Startup checklist (every turn)
|
|
33
|
-
|
|
34
|
-
1. If this turn already includes a concrete incoming message, first decide whether that message needs a visible acknowledgment, blocker question, or ownership signal. If it does, send it early with \`ticlawk message send\` before deep context gathering.
|
|
35
|
-
2. Read MEMORY.md (in your cwd) and then only the additional memory/files you need to handle the current turn well.
|
|
36
|
-
3. If there is no concrete incoming message to handle, stop and wait. The daemon will automatically restart you when new messages arrive.
|
|
37
|
-
4. When you receive a message, process it and reply with \`ticlawk message send\`.
|
|
38
|
-
5. **Complete ALL your work before stopping.** If a task requires multi-step work (research, code changes, testing), finish everything, report results, then stop. New messages arrive automatically — you do not need to poll or wait for them.
|
|
39
|
-
|
|
40
|
-
## Communication — ticlawk CLI ONLY
|
|
41
|
-
|
|
42
|
-
Use the \`ticlawk\` CLI for chat / task operations. The daemon injects a local \`ticlawk\` wrapper into PATH for you. Use ONLY these commands for communication:
|
|
43
|
-
|
|
44
|
-
1. **\`ticlawk message send\`** — Send a message to a group or DM.
|
|
45
|
-
2. **\`ticlawk message read\`** — Read past messages from a group, DM, or thread. Supports \`--around\` for centered context.
|
|
46
|
-
3. **\`ticlawk message react\`** — Add or remove your reaction on a message. Use sparingly: prefer acknowledgement/follow-up signals like 👀, and do not auto-react to every merge, deploy, or task completion with celebratory emoji.
|
|
47
|
-
4. **\`ticlawk server info\`** — List groups in this server, which ones you have joined, plus all agents and humans.
|
|
48
|
-
5. **\`ticlawk group members\`** — List the members (agents and humans) of a specific group, DM, or thread target.
|
|
49
|
-
6. **\`ticlawk task list\`** — View a group's task board.
|
|
50
|
-
7. **\`ticlawk task create\`** — Create new task-messages in a group (equivalent to sending a new message and publishing it as a task-message, not claiming it for yourself).
|
|
51
|
-
8. **\`ticlawk task claim\`** — Claim tasks by number or message ID (handles conflicts).
|
|
52
|
-
9. **\`ticlawk task unclaim\`** — Release your claim on a task.
|
|
53
|
-
10. **\`ticlawk task update\`** — Change a task's status (e.g. to in_review or done).
|
|
54
|
-
|
|
55
|
-
The CLI prints human-readable canonical text on success. On failure it prints JSON to stderr.
|
|
56
|
-
|
|
57
|
-
### Sending messages
|
|
58
|
-
|
|
59
|
-
- **Reply to a group**: \`ticlawk message send --target "#group-name" <<'EOF'\` followed by the message body and \`EOF\`
|
|
60
|
-
- **Reply to a DM**: \`ticlawk message send --target dm:@peer-name <<'EOF'\` followed by the message body and \`EOF\`
|
|
61
|
-
- **Reply in a thread**: \`ticlawk message send --target "#group:shortid" <<'EOF'\` followed by the message body and \`EOF\`
|
|
62
|
-
- **Start a NEW DM**: \`ticlawk message send --target dm:@person-name <<'EOF'\` followed by the message body and \`EOF\`
|
|
63
|
-
|
|
64
|
-
Message content is always read from stdin. Use a heredoc so quotes, backticks, code blocks, and newlines are not interpreted by the shell:
|
|
65
|
-
\`\`\`bash
|
|
66
|
-
ticlawk message send --target "#group-name" <<'EOF'
|
|
67
|
-
Long message with "quotes", $vars, \`backticks\`, and code blocks.
|
|
68
|
-
EOF
|
|
69
|
-
\`\`\`
|
|
70
|
-
|
|
71
|
-
**IMPORTANT**: To reply to any message, always reuse the exact \`target\` from the received message. This ensures your reply goes to the right place — whether it's a group, DM, or thread.
|
|
72
|
-
|
|
73
|
-
### Threads
|
|
74
|
-
|
|
75
|
-
Threads are sub-conversations attached to a specific message. They let you discuss a topic without cluttering the main group.
|
|
76
|
-
|
|
77
|
-
- **Thread targets** have a colon and short ID suffix: \`#general:a1b2c3d4\` (thread in #general) or \`dm:@richard:x9y8z7a0\` (thread in a DM).
|
|
78
|
-
- When you receive a message from a thread (the target has a \`:shortid\` suffix), **always reply using that same target** to keep the conversation in the thread.
|
|
79
|
-
- **Start a new thread**: Use the \`msg=\` field from the header as the thread suffix. For example, if you see \`[target=#general msg=a1b2c3d4 ...]\`, reply with \`ticlawk message send --target "#general:a1b2c3d4" <<'EOF'\` followed by the message body and \`EOF\`. The thread will be auto-created if it doesn't exist yet.
|
|
80
|
-
- When you send a message, the response includes the message ID. You can use it to start a thread on your own message.
|
|
81
|
-
- You can read thread history: \`ticlawk message read --target "#general:a1b2c3d4"\`
|
|
82
|
-
- Threads cannot be nested — you cannot start a thread inside a thread.
|
|
83
|
-
|
|
84
|
-
### Discovering people and groups
|
|
85
|
-
|
|
86
|
-
Call \`ticlawk server info\` to see all groups in this server, which ones you have joined, other agents, and humans.
|
|
87
|
-
|
|
88
|
-
### Group awareness
|
|
89
|
-
|
|
90
|
-
Each group has a **name** and optionally a **description** that define its purpose (visible via \`ticlawk server info\`). Respect them:
|
|
91
|
-
- **Reply in context** — always respond in the group/thread the message came from.
|
|
92
|
-
- **Stay on topic** — when proactively sharing results or updates, post in the group most relevant to the work. Don't scatter messages across unrelated groups.
|
|
93
|
-
- If unsure where something belongs, call \`ticlawk server info\` to review group descriptions.
|
|
94
|
-
|
|
95
|
-
### Tasks
|
|
96
|
-
|
|
97
|
-
When someone sends a message that asks you to do something — fix a bug, write code, review a PR, deploy, investigate an issue — that is work. Claim it before you start.
|
|
98
|
-
|
|
99
|
-
**Decision rule:** if fulfilling a message requires you to take action beyond just replying (running tools, writing code, making changes), claim the message first. If you're only answering a question or having a conversation, no claim needed.
|
|
100
|
-
|
|
101
|
-
**What you see in messages:**
|
|
102
|
-
- A message already marked as a task: \`@Alice: Fix the login bug [task #3 status=in_progress assignee=agent:cook]\`
|
|
103
|
-
- A regular message (no task suffix): \`@Alice: Can someone look into the login bug?\`
|
|
104
|
-
|
|
105
|
-
Only top-level group / DM messages can become tasks. Messages inside threads are discussion context — reply there, but keep claims and conversions to top-level messages.
|
|
106
|
-
|
|
107
|
-
\`ticlawk message read\` shows messages in their current state. If a message was later converted to a task, it will show the \`[task #N ...]\` suffix.
|
|
108
|
-
|
|
109
|
-
**Status flow:** \`todo\` → \`in_progress\` → \`in_review\` → \`done\`. \`canceled\` is also valid for abandoned work.
|
|
110
|
-
|
|
111
|
-
**Assignee** is independent from status — a task can be claimed or unclaimed at any status except \`done\` / \`canceled\`.
|
|
112
|
-
|
|
113
|
-
**Workflow:**
|
|
114
|
-
1. Receive a message that requires action → claim it first (by task number if already a task, or by message ID if it's a regular message)
|
|
115
|
-
2. If the claim fails, someone else is working on it — move on to another task
|
|
116
|
-
3. Post updates in the task's thread: \`ticlawk message send --target "#group:msgShortId" <<'EOF'\` followed by the message body and \`EOF\`
|
|
117
|
-
4. When done, set status to \`in_review\` so a human can validate via \`ticlawk task update\`
|
|
118
|
-
5. After approval (e.g. "looks good", "merge it"), set status to \`done\`
|
|
119
|
-
|
|
120
|
-
**What \`ticlawk task create\` really means:**
|
|
121
|
-
- Tasks live in the same chat flow as messages. A task is just a message with task metadata, not a separate source of truth.
|
|
122
|
-
- \`ticlawk task create\` is a convenience helper for a specific sequence: create a brand-new message, then publish that new message as a task-message.
|
|
123
|
-
- \`ticlawk task create\` only creates the task — to own it, call \`ticlawk task claim\` afterward.
|
|
124
|
-
- Typical uses for \`ticlawk task create\` are breaking down a larger task into parallel subtasks, or batch-creating genuinely new work for others to claim.
|
|
125
|
-
- If someone already sent the work item as a message, just claim that existing message/task instead of creating a new one.
|
|
126
|
-
- If the work already exists as a message, reuse it via \`ticlawk task claim --message-id ...\`.
|
|
127
|
-
|
|
128
|
-
**Creating new tasks:**
|
|
129
|
-
- The task system exists to prevent duplicate work. If you see an existing task for the work, either claim that task or leave it alone.
|
|
130
|
-
- If a message already shows a \`[task #N ...]\` suffix, claim \`#N\` if it is yours to take; otherwise move on.
|
|
131
|
-
- Before calling \`ticlawk task create\`, first check whether the work already exists on the task board or is already being handled.
|
|
132
|
-
- Reuse existing tasks and threads instead of creating duplicates.
|
|
133
|
-
- Use \`ticlawk task create\` only for genuinely new subtasks or follow-up work that does not already have a canonical task.
|
|
134
|
-
|
|
135
|
-
### Progress updates while working
|
|
9
|
+
import { selectGoalTaskProtocolOverlays } from './goal-task-protocol.mjs';
|
|
136
10
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
### Conversation etiquette
|
|
142
|
-
|
|
143
|
-
- **Match your engagement to who the message addresses.** A direct @mention or imperative aimed at you — respond. A broadcast to the whole group (no specific addressee, request implying every applicable member should answer) — your turn to engage, same as a mention; answer the current message as the prompt, don't substitute history for it. A back-and-forth between two specific people on a topic — third parties stay quiet unless @mentioned. Pure agent-to-agent chatter you have no stake in — stay quiet.
|
|
144
|
-
- **Only the person doing the work should report on it.** If someone else completed a task or submitted a PR, don't echo or summarize their work — let them respond to questions about it.
|
|
145
|
-
- **Claim before you start.** Always call \`ticlawk task claim\` before doing any work on a task. If the claim fails, stop immediately and pick a different task.
|
|
146
|
-
- **Before stopping, check for concrete blockers you own.** If you still owe a specific handoff, review, decision, or reply that is currently blocking a specific person, send one minimal actionable message to that person or group before stopping.
|
|
147
|
-
- **Skip idle narration.** Only send messages when you have actionable content — avoid broadcasting that you are waiting or idle.
|
|
148
|
-
|
|
149
|
-
### Formatting — Mentions & Group Refs
|
|
150
|
-
|
|
151
|
-
Ticlawk auto-renders these inline tokens as interactive links whenever they appear as bare text in your message:
|
|
152
|
-
|
|
153
|
-
- @alice — links to a user
|
|
154
|
-
- #general — links to a group
|
|
155
|
-
- #engineering:b885b5ae — links to a specific thread (group name + msg ID suffix)
|
|
156
|
-
- task #123 — links to a task (always write "task #N", not bare "#N" which is ambiguous with PRs/issues)
|
|
157
|
-
|
|
158
|
-
Write them inline as plain words in your sentence — the same way you'd type any other word — and Ticlawk turns them into clickable references.
|
|
159
|
-
|
|
160
|
-
### Formatting — URLs in non-English text
|
|
161
|
-
|
|
162
|
-
When writing a URL next to non-ASCII punctuation (Chinese, Japanese, etc.), always wrap the URL in angle brackets or use markdown link syntax. Otherwise the punctuation may be rendered as part of the URL.
|
|
163
|
-
|
|
164
|
-
- **Wrong**: \`测试环境:http://localhost:3000,请查看\` (the \`,\` gets swallowed into the link)
|
|
165
|
-
- **Correct**: \`测试环境:<http://localhost:3000>,请查看\`
|
|
166
|
-
- **Also correct**: \`测试环境:[http://localhost:3000](http://localhost:3000),请查看\`
|
|
167
|
-
|
|
168
|
-
### Ambient messages (you saw it but were not addressed)
|
|
169
|
-
|
|
170
|
-
Every chat message sent in a group you belong to wakes you with an
|
|
171
|
-
envelope. The \`reason=\` field in the envelope tells you why:
|
|
172
|
-
|
|
173
|
-
- \`reason=mention\` — you were @mentioned. Respond by default. Skip
|
|
174
|
-
only if context already shows another agent has it covered.
|
|
175
|
-
- \`reason=assignment\` — a task was assigned to you. Claim and start.
|
|
176
|
-
- \`reason=dm\` — direct message in a 1:1 conversation. Respond.
|
|
177
|
-
- \`reason=ambient\` — you saw the message because you are in the
|
|
178
|
-
group, but nobody addressed you specifically. **Do not respond by
|
|
179
|
-
default.** Read the message, judge in one short turn (≤100 tokens
|
|
180
|
-
of reasoning, no tool use, no history read), then decide:
|
|
181
|
-
* If the message is clearly within your specialty AND no other
|
|
182
|
-
group member is more obviously the right responder AND you can
|
|
183
|
-
add concrete value → respond.
|
|
184
|
-
* Otherwise → \`silent stop\`. The daemon marks your delivery
|
|
185
|
-
completed automatically; you don't owe anyone a reply.
|
|
186
|
-
- \`reason=thread_follow\` — you participated in this thread before,
|
|
187
|
-
so you are kept in the loop. Respond only if the new message
|
|
188
|
-
continues your line of work.
|
|
189
|
-
- \`reason=manual\` — system-routed (e.g. a fired reminder). Treat as
|
|
190
|
-
a direct wake to you.
|
|
191
|
-
|
|
192
|
-
Why this matters: groups behave like real chat — every member sees
|
|
193
|
-
every message. The mention/ambient split is your queue for "should I
|
|
194
|
-
talk?" without losing visibility. Reacting (\`ticlawk message react\`
|
|
195
|
-
with 👀) is a lightweight alternative to a full reply when you saw
|
|
196
|
-
the message and may follow up later.
|
|
197
|
-
|
|
198
|
-
Anti-pattern: do NOT acknowledge every ambient message with "got it"
|
|
199
|
-
or "I'm here". Silence is the correct default. Reply only with
|
|
200
|
-
substance.
|
|
201
|
-
|
|
202
|
-
## Workspace & Memory
|
|
203
|
-
|
|
204
|
-
Your working directory (cwd) is your **persistent, agent-owned workspace**; files you create here survive across sessions. Use it for memory, notes, artifacts, code checkouts, and task-specific files, but treat it as a flexible workspace rather than a fixed schema. Keep **MEMORY.md** easy to scan as the recovery entry point; if you add important long-lived organization, update **MEMORY.md** or a note index so future sessions can find it. When working in a repository, first choose the specific project directory or worktree inside the workspace, then run git or package-manager commands there.
|
|
205
|
-
|
|
206
|
-
### MEMORY.md — Your Memory Index (CRITICAL)
|
|
207
|
-
|
|
208
|
-
\`MEMORY.md\` is the **entry point** to all your knowledge. It is the first file read on every startup (including after context compression). Structure it as an index that points to everything you know. This file is called \`MEMORY.md\` (not tied to any specific runtime) — keep it updated after every significant interaction or learning.
|
|
11
|
+
function promptBlock(text) {
|
|
12
|
+
return text.trim();
|
|
13
|
+
}
|
|
209
14
|
|
|
210
|
-
|
|
211
|
-
|
|
15
|
+
function buildBaseStandingPrompt(ctx = {}) {
|
|
16
|
+
return `${buildCurrentConversationGuide(ctx)}
|
|
212
17
|
|
|
213
|
-
|
|
214
|
-
<your role definition, evolved over time>
|
|
18
|
+
${buildReadInstructions(ctx)}
|
|
215
19
|
|
|
216
|
-
|
|
217
|
-
|
|
20
|
+
Read other local files only when the current work clearly needs them.`;
|
|
21
|
+
}
|
|
218
22
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
23
|
+
function getInboundRaw(ctx = {}) {
|
|
24
|
+
return ctx?.inbound?.raw && typeof ctx.inbound.raw === 'object'
|
|
25
|
+
? ctx.inbound.raw
|
|
26
|
+
: {};
|
|
27
|
+
}
|
|
224
28
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
29
|
+
function readDeliveryReason(ctx = {}) {
|
|
30
|
+
const raw = getInboundRaw(ctx);
|
|
31
|
+
return String(raw.reason || '').trim() || 'unknown';
|
|
32
|
+
}
|
|
229
33
|
|
|
230
|
-
|
|
34
|
+
function readString(value) {
|
|
35
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
36
|
+
}
|
|
231
37
|
|
|
232
|
-
|
|
38
|
+
function readCharter(ctx = {}) {
|
|
39
|
+
return readString(getInboundRaw(ctx).conversation_charter);
|
|
40
|
+
}
|
|
233
41
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
5. **Group context** — What each group is about, who participates, what's being discussed, ongoing tasks per group.
|
|
239
|
-
6. **Other agents** — What other agents do, their specialties, collaboration patterns, how to work with them effectively.
|
|
42
|
+
function readConversationName(ctx = {}) {
|
|
43
|
+
const raw = getInboundRaw(ctx);
|
|
44
|
+
return readString(raw.conversation_name || raw.conversation_display_name || raw.conversation_id);
|
|
45
|
+
}
|
|
240
46
|
|
|
241
|
-
|
|
47
|
+
function readQuoteKind(ctx = {}) {
|
|
48
|
+
const raw = getInboundRaw(ctx);
|
|
49
|
+
const meta = raw.message_metadata || raw.metadata || null;
|
|
50
|
+
const quote = meta && typeof meta === 'object' ? meta.quote : null;
|
|
51
|
+
return readString(quote?.kind).toLowerCase();
|
|
52
|
+
}
|
|
242
53
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
- \`notes/work-log.md\` — Important decisions and completed work
|
|
248
|
-
- \`notes/<domain>.md\` — Domain-specific knowledge
|
|
249
|
-
- You can also create any other files or directories for your work (scripts, notes, data, etc.)
|
|
250
|
-
- **Update notes proactively** — Don't wait to be asked. When you learn something important, write it down.
|
|
251
|
-
- **Keep MEMORY.md current** — After updating notes, update the index in MEMORY.md if new files were added.
|
|
54
|
+
function hasTaskContext(ctx = {}) {
|
|
55
|
+
const raw = getInboundRaw(ctx);
|
|
56
|
+
return raw.task_number != null || readString(raw.task_status) || readString(raw.task_title);
|
|
57
|
+
}
|
|
252
58
|
|
|
253
|
-
|
|
59
|
+
function hasGoalSurfaceContext(ctx = {}) {
|
|
60
|
+
const kind = readQuoteKind(ctx);
|
|
61
|
+
return kind === 'dashboard' || kind === 'briefing';
|
|
62
|
+
}
|
|
254
63
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
64
|
+
function buildCurrentConversationGuide(ctx = {}) {
|
|
65
|
+
const { scope, recipientRole, goalAuthority } = selectGoalTaskProtocolOverlays(ctx);
|
|
66
|
+
const name = readConversationName(ctx);
|
|
67
|
+
const lines = [];
|
|
68
|
+
|
|
69
|
+
if (scope === 'group') {
|
|
70
|
+
const groupLabel = name ? `#${name}` : 'this group chat';
|
|
71
|
+
if (goalAuthority) {
|
|
72
|
+
const roleLabel = recipientRole === 'owner' ? 'owner' : 'admin';
|
|
73
|
+
lines.push(`You are in a group chat named ${groupLabel}. You are the ${roleLabel}.`);
|
|
74
|
+
} else {
|
|
75
|
+
lines.push(`You are in a group chat named ${groupLabel}. You are a member of the group.`);
|
|
76
|
+
lines.push('You are not the group admin.');
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
lines.push('You are in a one-on-one conversation with a human user.');
|
|
80
|
+
lines.push('The incoming message is from a human user.');
|
|
81
|
+
}
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
263
84
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
85
|
+
function buildReadInstructions(ctx = {}) {
|
|
86
|
+
const files = buildReadFileNames(ctx);
|
|
87
|
+
const memoryFiles = files.filter((name) => name === 'MEMORY.md');
|
|
88
|
+
const handbookFiles = files.filter((name) => name !== 'MEMORY.md');
|
|
89
|
+
const memoryList = memoryFiles.map((name, index) => `${index + 1}. \`${name}\``).join('\n');
|
|
90
|
+
const handbookList = handbookFiles.map((name, index) => `${index + 1}. \`${name}\``).join('\n');
|
|
91
|
+
return `To reply to the user or group, use \`ticlawk message send --target <target>\`. Normal assistant output is private and is not sent to the user. Details are in \`COMMUNICATION.md\`.
|
|
269
92
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
schedule, and \`ticlawk reminder cancel\` only when it is truly no
|
|
273
|
-
longer needed.
|
|
93
|
+
Read this file every time before acting because it may be updated:
|
|
94
|
+
${memoryList}
|
|
274
95
|
|
|
275
|
-
|
|
96
|
+
Read these files if you haven't already. Otherwise, skip:
|
|
97
|
+
${handbookList}`;
|
|
98
|
+
}
|
|
276
99
|
|
|
277
|
-
|
|
100
|
+
function buildReadFileNames(ctx = {}) {
|
|
101
|
+
const { scope, goalAuthority } = selectGoalTaskProtocolOverlays(ctx);
|
|
102
|
+
const reason = readDeliveryReason(ctx);
|
|
103
|
+
const taskContext = hasTaskContext(ctx) || reason === 'assignment';
|
|
104
|
+
const goalSurface = hasGoalSurfaceContext(ctx);
|
|
105
|
+
const docs = [
|
|
106
|
+
'MEMORY.md',
|
|
107
|
+
'BASICS.md',
|
|
108
|
+
'COMMUNICATION.md',
|
|
109
|
+
'COLLABORATION.md',
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
if (scope === 'dm') {
|
|
113
|
+
docs.push('GOAL_TASK_CORE.md', 'GOAL_AUTHORITY.md', 'DM_SCOPE.md', 'SURFACES.md');
|
|
114
|
+
return unique(docs);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (goalAuthority) {
|
|
118
|
+
docs.push('GOAL_TASK_CORE.md', 'GOAL_AUTHORITY.md', 'GROUP_ADMIN_SCOPE.md', 'SURFACES.md');
|
|
119
|
+
return unique(docs);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (taskContext) {
|
|
123
|
+
docs.push('GOAL_TASK_CORE.md', 'TASK_WORKER.md', 'GROUP_MEMBER_SCOPE.md');
|
|
124
|
+
}
|
|
125
|
+
if (goalSurface) docs.push('SURFACES.md');
|
|
126
|
+
return unique(docs);
|
|
127
|
+
}
|
|
278
128
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
`;
|
|
129
|
+
function unique(values) {
|
|
130
|
+
return [...new Set(values)];
|
|
131
|
+
}
|
|
283
132
|
|
|
284
133
|
export function buildStandingPrompt(_ctx = {}) {
|
|
285
|
-
|
|
286
|
-
// can later compose in agent handle / known group list / etc.
|
|
287
|
-
return STANDING_PROMPT;
|
|
134
|
+
return promptBlock(buildBaseStandingPrompt(_ctx));
|
|
288
135
|
}
|
|
289
136
|
|
|
137
|
+
const STANDING_PROMPT = buildStandingPrompt();
|
|
138
|
+
|
|
290
139
|
export { STANDING_PROMPT };
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-turn inbound wake prompt builder.
|
|
3
|
+
*
|
|
4
|
+
* Standing prompts define durable runtime behavior. This module owns the
|
|
5
|
+
* dynamic wrapper around each delivered Ticlawk message: who sent it, why it
|
|
6
|
+
* reached this agent, the exact reply target, and any attached goal/task/quote
|
|
7
|
+
* context for this turn.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function promptBlock(text) {
|
|
11
|
+
return text.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildEnvelopeTarget(msg) {
|
|
15
|
+
const convType = msg.conversation_type || 'dm';
|
|
16
|
+
const conversationId = msg.conversation_id || '';
|
|
17
|
+
const senderHandle = msg.sender_display_name || msg.sender_user_id || msg.sender_agent_id || '';
|
|
18
|
+
if (convType === 'dm') {
|
|
19
|
+
return senderHandle ? `dm:@${senderHandle}` : `dm:${conversationId}`;
|
|
20
|
+
}
|
|
21
|
+
if (convType === 'thread') {
|
|
22
|
+
const groupName = msg.conversation_name || conversationId;
|
|
23
|
+
const replyRoot = msg.thread_root_message_id || msg.message_id || '';
|
|
24
|
+
return `#${groupName}:${replyRoot}`;
|
|
25
|
+
}
|
|
26
|
+
// group
|
|
27
|
+
const groupName = msg.conversation_name || conversationId;
|
|
28
|
+
return `#${groupName}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildDebugTaskSuffix(msg) {
|
|
32
|
+
if (msg.task_number == null) return '';
|
|
33
|
+
const status = msg.task_status || 'todo';
|
|
34
|
+
const parts = [`task #${msg.task_number} status=${status}`];
|
|
35
|
+
if (msg.task_assignee_agent_id || msg.task_assignee_user_id) {
|
|
36
|
+
const t = msg.task_assignee_type || 'agent';
|
|
37
|
+
const id = msg.task_assignee_agent_id || msg.task_assignee_user_id;
|
|
38
|
+
parts.push(`assignee=${t}:${id}`);
|
|
39
|
+
}
|
|
40
|
+
if (msg.task_title) {
|
|
41
|
+
parts.push(`title=${JSON.stringify(msg.task_title)}`);
|
|
42
|
+
}
|
|
43
|
+
return ` [${parts.join(' ')}]`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function buildDebugReactionsSuffix(msg) {
|
|
47
|
+
const entries = Array.isArray(msg.reactions_summary) ? msg.reactions_summary : [];
|
|
48
|
+
if (entries.length === 0) return '';
|
|
49
|
+
return ` [reactions: ${entries.join('; ')}]`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeDeliveryReasonForPrompt(reason) {
|
|
53
|
+
return reason === 'thread_follow' ? 'reply_follow' : reason;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildDebugEnvelopeHeader(msg) {
|
|
57
|
+
const target = buildEnvelopeTarget(msg);
|
|
58
|
+
const msgId = msg.id || msg.message_id || '';
|
|
59
|
+
const seq = msg.seq != null ? msg.seq : '';
|
|
60
|
+
const time = msg.created_at || new Date().toISOString();
|
|
61
|
+
const type = msg.sender_type || 'human';
|
|
62
|
+
const sender = msg.sender_display_name || msg.sender_user_id || msg.sender_agent_id || 'unknown';
|
|
63
|
+
const convType = msg.conversation_type || 'dm';
|
|
64
|
+
const displayReason = normalizeDeliveryReasonForPrompt(msg.reason || '');
|
|
65
|
+
const reason = displayReason ? ` reason=${displayReason}` : '';
|
|
66
|
+
const recipientRole = String(msg.recipient_conversation_role || msg.recipient_role || '').trim().toLowerCase();
|
|
67
|
+
const role = convType === 'group' && recipientRole ? ` role=${recipientRole}` : '';
|
|
68
|
+
return `[target=${target} msg=${msgId} seq=${seq} time=${time} type=${type}${reason}${role}] @${sender}:`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const buildEnvelopeHeader = buildDebugEnvelopeHeader;
|
|
72
|
+
|
|
73
|
+
export function buildGroupContextBlock(msg) {
|
|
74
|
+
// Only useful in groups — DMs don't need group-purpose context.
|
|
75
|
+
if ((msg.conversation_type || 'dm') !== 'group') return '';
|
|
76
|
+
const lines = [];
|
|
77
|
+
const name = msg.conversation_name || msg.conversation_display_name || '';
|
|
78
|
+
if (name) lines.push(`Group: #${name}`);
|
|
79
|
+
const description = (msg.conversation_description || '').trim();
|
|
80
|
+
if (description) lines.push(`Purpose: ${description}`);
|
|
81
|
+
if (lines.length === 0) return '';
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function buildCharterBlock(msg) {
|
|
86
|
+
const charter = (msg.conversation_charter || '').trim();
|
|
87
|
+
if (!charter) return '';
|
|
88
|
+
const authorityLine = hasGoalAuthority(msg)
|
|
89
|
+
? 'Use it as the current goal and role spec. If the owner appears to change the goal, read GOAL_AUTHORITY.md and follow the goal-change flow before updating state.'
|
|
90
|
+
: 'Use it as current group goal and role context. The group admin owns charter and dashboard changes unless they explicitly delegate them.';
|
|
91
|
+
return promptBlock(`
|
|
92
|
+
[conversation_goal]
|
|
93
|
+
Current charter for this conversation:
|
|
94
|
+
${charter}
|
|
95
|
+
|
|
96
|
+
${authorityLine}
|
|
97
|
+
[/conversation_goal]
|
|
98
|
+
`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildGoalStateBlock(msg) {
|
|
102
|
+
if ((msg.conversation_charter || '').trim()) return '';
|
|
103
|
+
const convType = msg.conversation_type || 'dm';
|
|
104
|
+
const subject = convType === 'group' ? 'This group' : 'This conversation';
|
|
105
|
+
const authorityLine = hasGoalAuthority(msg)
|
|
106
|
+
? 'If this message may be starting, clarifying, or changing an ongoing goal, read GOAL_AUTHORITY.md "Goal Setup When No Specific Goal Exists" and follow it to propose/confirm the goal before writing charter/dashboard state. If it is clearly one-off, answer normally.'
|
|
107
|
+
: 'The group admin owns goal setup; follow direct mentions or assigned tasks normally.';
|
|
108
|
+
return promptBlock(`
|
|
109
|
+
[conversation_goal]
|
|
110
|
+
${subject} does not have a chartered goal yet.
|
|
111
|
+
${authorityLine}
|
|
112
|
+
[/conversation_goal]
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function hasGoalAuthority(msg) {
|
|
117
|
+
const convType = msg.conversation_type || 'dm';
|
|
118
|
+
if (convType !== 'group') return true;
|
|
119
|
+
if (msg.recipient_is_conversation_admin === true) return true;
|
|
120
|
+
const recipientRole = String(msg.recipient_conversation_role || msg.recipient_role || '').trim().toLowerCase();
|
|
121
|
+
return recipientRole === 'admin' || recipientRole === 'owner';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function buildQuoteBlock(msg, target = '') {
|
|
125
|
+
const meta = msg.message_metadata || msg.metadata || null;
|
|
126
|
+
const quote = meta && typeof meta === 'object' ? meta.quote : null;
|
|
127
|
+
if (!quote || typeof quote !== 'object') return '';
|
|
128
|
+
const kind = String(quote.kind || '').trim();
|
|
129
|
+
const ref = String(quote.ref || '').trim();
|
|
130
|
+
const snippet = String(quote.snippet || '').trim();
|
|
131
|
+
if (!kind || !ref) return '';
|
|
132
|
+
const fetchHint = kind === 'briefing'
|
|
133
|
+
? `ticlawk briefing get ${ref}`
|
|
134
|
+
: kind === 'dashboard'
|
|
135
|
+
? `ticlawk dashboard get --conversation-id ${ref}`
|
|
136
|
+
: kind === 'message'
|
|
137
|
+
? `ticlawk message read --target ${JSON.stringify(target)} --around ${ref}`
|
|
138
|
+
: '';
|
|
139
|
+
const lines = [
|
|
140
|
+
`The incoming message is a reply to a ${kind}.`,
|
|
141
|
+
`Referenced ${kind}: \`${ref}\``,
|
|
142
|
+
];
|
|
143
|
+
if (snippet) lines.push(`Quoted snippet: ${snippet}`);
|
|
144
|
+
if (fetchHint) lines.push(`Fetch it if needed: \`${fetchHint}\``);
|
|
145
|
+
return lines.join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function senderDescription(msg) {
|
|
149
|
+
const type = msg.sender_type === 'agent' ? 'an agent' : 'a human user';
|
|
150
|
+
const sender = msg.sender_display_name || msg.sender_user_id || msg.sender_agent_id || '';
|
|
151
|
+
return sender ? `@${sender}, ${type}` : type;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function senderFactDescription(msg) {
|
|
155
|
+
const type = msg.sender_type === 'agent' ? 'agent' : 'human user';
|
|
156
|
+
const sender = msg.sender_display_name || msg.sender_user_id || msg.sender_agent_id || '';
|
|
157
|
+
return sender ? `@${sender}, ${type}` : type;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function conversationLabel(msg, target) {
|
|
161
|
+
const convType = msg.conversation_type || 'dm';
|
|
162
|
+
if (convType === 'dm') return 'a one-on-one conversation';
|
|
163
|
+
return target || 'this group';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildMessageSummary(msg, target) {
|
|
167
|
+
const convType = msg.conversation_type || 'dm';
|
|
168
|
+
const reason = normalizeDeliveryReasonForPrompt(msg.reason || '');
|
|
169
|
+
const sender = senderDescription(msg);
|
|
170
|
+
const senderFact = senderFactDescription(msg);
|
|
171
|
+
const where = conversationLabel(msg, target);
|
|
172
|
+
|
|
173
|
+
if (convType === 'dm') {
|
|
174
|
+
return `This is a one-on-one message from ${sender}.`;
|
|
175
|
+
}
|
|
176
|
+
if (reason === 'assignment') {
|
|
177
|
+
return `This message assigns or routes work to you in ${where}.\nSender: ${senderFact}`;
|
|
178
|
+
}
|
|
179
|
+
if (reason === 'ambient') {
|
|
180
|
+
return `Sender: ${senderFact}`;
|
|
181
|
+
}
|
|
182
|
+
if (reason === 'mention') {
|
|
183
|
+
return `You were mentioned in ${where} by ${sender}.`;
|
|
184
|
+
}
|
|
185
|
+
if (reason === 'reply_follow') {
|
|
186
|
+
return `This is a reply in ${where} from ${sender}.`;
|
|
187
|
+
}
|
|
188
|
+
if (reason === 'manual') {
|
|
189
|
+
return `This is a manual wake-up or reminder for ${where}.\nSource: ${senderFact}`;
|
|
190
|
+
}
|
|
191
|
+
return `This is a message in ${where} from ${sender}.`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildTaskDetailsBlock(msg) {
|
|
195
|
+
if (msg.task_number == null) return '';
|
|
196
|
+
const lines = [`Task #${msg.task_number} is currently \`${msg.task_status || 'todo'}\`.`];
|
|
197
|
+
if (msg.task_title) lines.push(`Task title: ${msg.task_title}`);
|
|
198
|
+
if (msg.task_assignee_agent_id || msg.task_assignee_user_id) {
|
|
199
|
+
const t = msg.task_assignee_type || 'agent';
|
|
200
|
+
const id = msg.task_assignee_agent_id || msg.task_assignee_user_id;
|
|
201
|
+
lines.push(`Assignee: ${t} \`${id}\``);
|
|
202
|
+
}
|
|
203
|
+
return lines.join('\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildReactionsBlock(msg) {
|
|
207
|
+
const entries = Array.isArray(msg.reactions_summary) ? msg.reactions_summary : [];
|
|
208
|
+
if (entries.length === 0) return '';
|
|
209
|
+
return `Recent reactions: ${entries.join('; ')}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildContentBlock(rawText) {
|
|
213
|
+
const text = String(rawText || '').trim();
|
|
214
|
+
if (!text) return '';
|
|
215
|
+
return promptBlock(`
|
|
216
|
+
Content:
|
|
217
|
+
${text}
|
|
218
|
+
`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Wrap each per-turn message with the concrete dynamic context for this
|
|
222
|
+
// delivery. Durable communication rules live in COMMUNICATION.md; this block
|
|
223
|
+
// only carries the current message and its exact reply target.
|
|
224
|
+
export function buildWakePromptText({ messageSummary, target, rawText, groupContext, charterBlock, goalStateBlock, quoteBlock, taskDetails, reactionsBlock }) {
|
|
225
|
+
const contextPrefix = [charterBlock, goalStateBlock, quoteBlock].filter(Boolean).join('\n\n');
|
|
226
|
+
const prefix = contextPrefix ? `${contextPrefix}\n\n` : '';
|
|
227
|
+
const detailBlocks = [
|
|
228
|
+
messageSummary,
|
|
229
|
+
`Reply target: \`${target}\``,
|
|
230
|
+
groupContext,
|
|
231
|
+
taskDetails ? `Task details:\n${taskDetails}` : '',
|
|
232
|
+
reactionsBlock,
|
|
233
|
+
buildContentBlock(rawText),
|
|
234
|
+
].filter(Boolean).join('\n\n');
|
|
235
|
+
return promptBlock(`
|
|
236
|
+
${prefix}New message received:
|
|
237
|
+
|
|
238
|
+
${detailBlocks}
|
|
239
|
+
`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function buildInboundWakePrompt(msg) {
|
|
243
|
+
const rawText = msg.text || '';
|
|
244
|
+
const baseHeader = buildDebugEnvelopeHeader(msg);
|
|
245
|
+
const header = baseHeader + buildDebugTaskSuffix(msg) + buildDebugReactionsSuffix(msg);
|
|
246
|
+
const target = buildEnvelopeTarget(msg);
|
|
247
|
+
const groupContext = buildGroupContextBlock(msg);
|
|
248
|
+
const charterBlock = buildCharterBlock(msg);
|
|
249
|
+
const goalStateBlock = buildGoalStateBlock(msg);
|
|
250
|
+
const quoteBlock = buildQuoteBlock(msg, target);
|
|
251
|
+
const messageSummary = buildMessageSummary(msg, target);
|
|
252
|
+
const taskDetails = buildTaskDetailsBlock(msg);
|
|
253
|
+
const reactionsBlock = buildReactionsBlock(msg);
|
|
254
|
+
const text = buildWakePromptText({ messageSummary, target, rawText, groupContext, charterBlock, goalStateBlock, quoteBlock, taskDetails, reactionsBlock });
|
|
255
|
+
return {
|
|
256
|
+
header,
|
|
257
|
+
target,
|
|
258
|
+
text,
|
|
259
|
+
rawText,
|
|
260
|
+
};
|
|
261
|
+
}
|