ticlawk 0.1.16-dev.3 → 0.1.16-dev.30

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.
Files changed (39) hide show
  1. package/README.md +14 -2
  2. package/bin/ticlawk.mjs +207 -25
  3. package/package.json +1 -1
  4. package/src/adapters/ticlawk/api.mjs +232 -23
  5. package/src/adapters/ticlawk/credentials.mjs +41 -1
  6. package/src/adapters/ticlawk/index.mjs +196 -195
  7. package/src/adapters/ticlawk/wake-client.mjs +1 -1
  8. package/src/cli/agent-commands.mjs +607 -37
  9. package/src/core/agent-cli-handlers.mjs +449 -20
  10. package/src/core/agent-home.mjs +86 -10
  11. package/src/core/argv.mjs +11 -1
  12. package/src/core/http.mjs +126 -0
  13. package/src/core/runtime-env.mjs +7 -0
  14. package/src/core/runtime-support.mjs +101 -30
  15. package/src/migrate/write-initial-memory.mjs +5 -5
  16. package/src/runtimes/_shared/agent-handbook.mjs +45 -0
  17. package/src/runtimes/_shared/brand.mjs +2 -0
  18. package/src/runtimes/_shared/goal-task-protocol.mjs +50 -0
  19. package/src/runtimes/_shared/handbook/BASICS.md +27 -0
  20. package/src/runtimes/_shared/handbook/COLLABORATION.md +37 -0
  21. package/src/runtimes/_shared/handbook/COMMUNICATION.md +48 -0
  22. package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
  23. package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +43 -0
  24. package/src/runtimes/_shared/handbook/GOAL_TASK_CORE.md +43 -0
  25. package/src/runtimes/_shared/handbook/GROUP_ADMIN_SCOPE.md +21 -0
  26. package/src/runtimes/_shared/handbook/GROUP_MEMBER_SCOPE.md +15 -0
  27. package/src/runtimes/_shared/handbook/SURFACES.md +41 -0
  28. package/src/runtimes/_shared/handbook/TASK_WORKER.md +14 -0
  29. package/src/runtimes/_shared/standing-prompt.mjs +111 -264
  30. package/src/runtimes/_shared/wake-prompt.mjs +261 -0
  31. package/src/runtimes/claude-code/index.mjs +30 -108
  32. package/src/runtimes/codex/index.mjs +114 -23
  33. package/src/runtimes/openclaw/index.mjs +16 -26
  34. package/src/runtimes/opencode/index.mjs +42 -36
  35. package/src/runtimes/opencode/session.mjs +5 -4
  36. package/src/runtimes/pi/index.mjs +39 -31
  37. package/src/runtimes/pi/session.mjs +5 -2
  38. package/src/adapters/ticlawk/cards.mjs +0 -149
  39. package/src/core/media/outbound.mjs +0 -163
package/src/core/argv.mjs CHANGED
@@ -29,7 +29,17 @@ export function parseOptionArgs(argv = []) {
29
29
  const value = inlineValue !== undefined
30
30
  ? inlineValue
31
31
  : argv[i + 1] && !argv[i + 1].startsWith('-') ? argv[++i] : true;
32
- args[rawKey] = value;
32
+ // Repeated flags collect into an array so `--attach a --attach b`
33
+ // surfaces as ['a','b'] to callers that expect repeatable input
34
+ // (--attach on message send, --member on group create, etc).
35
+ // First occurrence stays scalar so single-value callers don't
36
+ // have to learn array-or-string.
37
+ if (Object.prototype.hasOwnProperty.call(args, rawKey)) {
38
+ const existing = args[rawKey];
39
+ args[rawKey] = Array.isArray(existing) ? [...existing, value] : [existing, value];
40
+ } else {
41
+ args[rawKey] = value;
42
+ }
33
43
  continue;
34
44
  }
35
45
  args._.push(arg);
package/src/core/http.mjs CHANGED
@@ -3,6 +3,25 @@ import {
3
3
  handleAttachmentUpload,
4
4
  handleAttachmentView,
5
5
  handleGroupCreate,
6
+ handleWorkstreamCharterGet,
7
+ handleWorkstreamCharterSet,
8
+ handleWorkstreamCreate,
9
+ handleWorkstreamDelete,
10
+ handleWorkstreamList,
11
+ handleAgentList,
12
+ handleAgentCreate,
13
+ handleAgentDelete,
14
+ handleWorkstreamDashboardSet,
15
+ handleWorkstreamDashboardGet,
16
+ handleCredentialRequest,
17
+ handleBriefingPublish,
18
+ handleBriefingGet,
19
+ handleServiceCreate,
20
+ handleServiceUpdate,
21
+ handleServiceDelete,
22
+ handleServiceList,
23
+ handleServiceInfo,
24
+ handleServiceCall,
6
25
  handleGroupMembers,
7
26
  handleGroupMembersAdd,
8
27
  handleGroupMembersRemove,
@@ -11,6 +30,7 @@ import {
11
30
  handleMessageRead,
12
31
  handleMessageSearch,
13
32
  handleMessageSend,
33
+ handleProfileAvatarUpload,
14
34
  handleProfileShow,
15
35
  handleProfileUpdate,
16
36
  handleReminderCancel,
@@ -148,6 +168,12 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
148
168
  const r = await handleProfileUpdate(req, body, cliCtx);
149
169
  return writeJson(res, r.status, r.body);
150
170
  }
171
+ if (urlNoQuery === '/agent/profile/avatar' && method === 'POST') {
172
+ const body = await readJsonBody(req);
173
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
174
+ const r = await handleProfileAvatarUpload(req, body, cliCtx);
175
+ return writeJson(res, r.status, r.body);
176
+ }
151
177
  if (urlNoQuery === '/agent/attachment/upload' && method === 'POST') {
152
178
  const body = await readJsonBody(req);
153
179
  if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
@@ -216,6 +242,106 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
216
242
  const r = await handleServerInfo(req, parseQuery(req.url || ''), cliCtx);
217
243
  return writeJson(res, r.status, r.body);
218
244
  }
245
+ if (urlNoQuery === '/agent/workstream/charter/get' && method === 'GET') {
246
+ const r = await handleWorkstreamCharterGet(req, parseQuery(req.url || ''), cliCtx);
247
+ return writeJson(res, r.status, r.body);
248
+ }
249
+ if (urlNoQuery === '/agent/workstream/charter/set' && method === 'POST') {
250
+ const body = await readJsonBody(req);
251
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
252
+ const r = await handleWorkstreamCharterSet(req, body, cliCtx);
253
+ return writeJson(res, r.status, r.body);
254
+ }
255
+ if (urlNoQuery === '/agent/workstream/create' && method === 'POST') {
256
+ const body = await readJsonBody(req);
257
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
258
+ const r = await handleWorkstreamCreate(req, body, cliCtx);
259
+ return writeJson(res, r.status, r.body);
260
+ }
261
+ if (urlNoQuery === '/agent/workstream/delete' && method === 'POST') {
262
+ const body = await readJsonBody(req);
263
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
264
+ const r = await handleWorkstreamDelete(req, body, cliCtx);
265
+ return writeJson(res, r.status, r.body);
266
+ }
267
+ if (urlNoQuery === '/agent/workstream/list' && method === 'GET') {
268
+ const r = await handleWorkstreamList(req, parseQuery(req.url || ''), cliCtx);
269
+ return writeJson(res, r.status, r.body);
270
+ }
271
+ if (urlNoQuery === '/agent/agent/list' && method === 'GET') {
272
+ const r = await handleAgentList(req, parseQuery(req.url || ''), cliCtx);
273
+ return writeJson(res, r.status, r.body);
274
+ }
275
+ if (urlNoQuery === '/agent/agent/create' && method === 'POST') {
276
+ const body = await readJsonBody(req);
277
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
278
+ const r = await handleAgentCreate(req, body, cliCtx);
279
+ return writeJson(res, r.status, r.body);
280
+ }
281
+ if (urlNoQuery === '/agent/agent/delete' && method === 'POST') {
282
+ const body = await readJsonBody(req);
283
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
284
+ const r = await handleAgentDelete(req, body, cliCtx);
285
+ return writeJson(res, r.status, r.body);
286
+ }
287
+ if (urlNoQuery === '/agent/dashboard/set' && method === 'POST') {
288
+ const body = await readJsonBody(req);
289
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
290
+ const r = await handleWorkstreamDashboardSet(req, body, cliCtx);
291
+ return writeJson(res, r.status, r.body);
292
+ }
293
+ if (urlNoQuery === '/agent/dashboard/get' && method === 'GET') {
294
+ const r = await handleWorkstreamDashboardGet(req, parseQuery(req.url || ''), cliCtx);
295
+ return writeJson(res, r.status, r.body);
296
+ }
297
+ if (urlNoQuery === '/agent/briefing/publish' && method === 'POST') {
298
+ const body = await readJsonBody(req);
299
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
300
+ const r = await handleBriefingPublish(req, body, cliCtx);
301
+ return writeJson(res, r.status, r.body);
302
+ }
303
+ if (urlNoQuery === '/agent/briefing/get' && method === 'GET') {
304
+ const r = await handleBriefingGet(req, parseQuery(req.url || ''), cliCtx);
305
+ return writeJson(res, r.status, r.body);
306
+ }
307
+ if (urlNoQuery === '/agent/credential/request' && method === 'POST') {
308
+ const body = await readJsonBody(req);
309
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
310
+ const r = await handleCredentialRequest(req, body, cliCtx);
311
+ return writeJson(res, r.status, r.body);
312
+ }
313
+ if (urlNoQuery === '/agent/service/create' && method === 'POST') {
314
+ const body = await readJsonBody(req);
315
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
316
+ const r = await handleServiceCreate(req, body, cliCtx);
317
+ return writeJson(res, r.status, r.body);
318
+ }
319
+ if (urlNoQuery === '/agent/service/update' && method === 'POST') {
320
+ const body = await readJsonBody(req);
321
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
322
+ const r = await handleServiceUpdate(req, body, cliCtx);
323
+ return writeJson(res, r.status, r.body);
324
+ }
325
+ if (urlNoQuery === '/agent/service/delete' && method === 'POST') {
326
+ const body = await readJsonBody(req);
327
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
328
+ const r = await handleServiceDelete(req, body, cliCtx);
329
+ return writeJson(res, r.status, r.body);
330
+ }
331
+ if (urlNoQuery === '/agent/service/list' && method === 'GET') {
332
+ const r = await handleServiceList(req, parseQuery(req.url || ''), cliCtx);
333
+ return writeJson(res, r.status, r.body);
334
+ }
335
+ if (urlNoQuery === '/agent/service/info' && method === 'GET') {
336
+ const r = await handleServiceInfo(req, parseQuery(req.url || ''), cliCtx);
337
+ return writeJson(res, r.status, r.body);
338
+ }
339
+ if (urlNoQuery === '/agent/service/call' && method === 'POST') {
340
+ const body = await readJsonBody(req);
341
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
342
+ const r = await handleServiceCall(req, body, cliCtx);
343
+ return writeJson(res, r.status, r.body);
344
+ }
219
345
 
220
346
  writeJson(res, 404, { error: 'not found' });
221
347
  } catch (err) {
@@ -15,6 +15,7 @@
15
15
  const STRIPPED_KEYS = new Set([
16
16
  'TICLAWK_CONNECTOR_API_KEY',
17
17
  'TICLAWK_CONNECTOR_WS_URL',
18
+ 'TICLAWK_CREDENTIAL_NAMES',
18
19
  'TICLAWK_SETUP_CODE',
19
20
  ]);
20
21
 
@@ -35,11 +36,17 @@ export function buildAgentRuntimeEnv({
35
36
  sessionId,
36
37
  hostId,
37
38
  daemonUrl,
39
+ conversationId,
40
+ messageId,
41
+ target,
38
42
  } = {}) {
39
43
  const out = {};
40
44
  if (agentId) out.TICLAWK_RUNTIME_AGENT_ID = String(agentId);
41
45
  if (sessionId) out.TICLAWK_RUNTIME_SESSION_ID = String(sessionId);
42
46
  if (hostId) out.TICLAWK_RUNTIME_HOST_ID = String(hostId);
47
+ if (conversationId) out.TICLAWK_RUNTIME_CONVERSATION_ID = String(conversationId);
48
+ if (messageId) out.TICLAWK_RUNTIME_MESSAGE_ID = String(messageId);
49
+ if (target) out.TICLAWK_RUNTIME_TARGET = String(target);
43
50
  out.TICLAWK_RUNTIME_DAEMON_URL = daemonUrl || 'http://127.0.0.1:8741';
44
51
  return out;
45
52
  }
@@ -3,6 +3,7 @@ import { getAgentHome } from './agent-home.mjs';
3
3
  const ERROR_MAX_CHARS = 500;
4
4
  const DEFAULT_DELTA_FLUSH_MS = 250;
5
5
  const DEFAULT_DELTA_FLUSH_CHARS = 64;
6
+ const MAX_SCOPED_RUNTIME_SESSIONS = 50;
6
7
 
7
8
  function truncateError(text) {
8
9
  if (!text) return null;
@@ -42,36 +43,6 @@ export function shouldStreamRuntime(runtimeName, runtime) {
42
43
  return Boolean(runtime?.runTurnStream) && getStreamingMode(runtimeName);
43
44
  }
44
45
 
45
- export async function sendAdapterMessage(adapter, binding, payload) {
46
- await adapter.send(binding, payload);
47
- }
48
-
49
- /**
50
- * Record the runtime's final turn output as activity (NOT chat).
51
- *
52
- * Previously called `sendResult` and treated as "the chat reply path".
53
- * After the group-chat upgrade, chat is produced exclusively by the
54
- * agent invoking `ticlawk message send` via the CLI. The runtime's
55
- * raw final output is still surfaced for trajectory/debug UI, but it
56
- * no longer materializes as a `messages` row — the trigger
57
- * `project_agent_event` was updated in PR-2b to drop the chat
58
- * projection.
59
- *
60
- * Renamed so the call sites read self-evidently: "record activity"
61
- * never reads as "send a chat message".
62
- */
63
- export async function recordActivity(adapter, binding, inbound, result) {
64
- if (!result?.text && (!result?.mediaUrls || result.mediaUrls.length === 0)) return;
65
- await sendAdapterMessage(adapter, binding, {
66
- type: 'assistant',
67
- text: result.text || '',
68
- media: result.media || [],
69
- turnId: inbound.messageId || result?.turnId || null,
70
- replyToMessageId: inbound.messageId || null,
71
- });
72
- }
73
-
74
-
75
46
  export async function reportSubprocessFailure({ adapter, binding, inbound, runtimeName, info }) {
76
47
  if (!info || info.ok) return;
77
48
  const seconds = ((info.durationMs || 0) / 1000).toFixed(1);
@@ -135,6 +106,106 @@ export async function updateBindingRuntimeMeta(ctx, binding, runtimeMetaPatch, e
135
106
  });
136
107
  }
137
108
 
109
+ function readScopedSessionKey(inbound = {}) {
110
+ const conversationId = String(inbound?.conversationId || '').trim();
111
+ if (!conversationId) return '';
112
+ const threadRoot = String(inbound?.raw?.thread_root_message_id || '').trim();
113
+ return threadRoot ? `${conversationId}:thread:${threadRoot}` : conversationId;
114
+ }
115
+
116
+ function normalizeScopedRuntimeSessions(value) {
117
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
118
+ const out = {};
119
+ for (const [key, session] of Object.entries(value)) {
120
+ if (!key || !session || typeof session !== 'object' || Array.isArray(session)) continue;
121
+ const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
122
+ if (!sessionId) continue;
123
+ out[key] = {
124
+ sessionId,
125
+ path: typeof session.path === 'string' ? session.path : null,
126
+ lastRotatedAt: typeof session.lastRotatedAt === 'string' ? session.lastRotatedAt : null,
127
+ updatedAt: typeof session.updatedAt === 'string' ? session.updatedAt : null,
128
+ };
129
+ }
130
+ return out;
131
+ }
132
+
133
+ function pruneScopedRuntimeSessions(sessions) {
134
+ const entries = Object.entries(sessions);
135
+ if (entries.length <= MAX_SCOPED_RUNTIME_SESSIONS) return sessions;
136
+ return Object.fromEntries(entries
137
+ .sort(([, a], [, b]) => {
138
+ const aTs = Date.parse(a?.updatedAt || a?.lastRotatedAt || '') || 0;
139
+ const bTs = Date.parse(b?.updatedAt || b?.lastRotatedAt || '') || 0;
140
+ return bTs - aTs;
141
+ })
142
+ .slice(0, MAX_SCOPED_RUNTIME_SESSIONS));
143
+ }
144
+
145
+ export function resolveRuntimeSessionScope(meta = {}, inbound = {}) {
146
+ const key = readScopedSessionKey(inbound);
147
+ if (!key) {
148
+ return {
149
+ key: '',
150
+ sessions: {},
151
+ sessionId: meta.sessionId || null,
152
+ path: meta.path || null,
153
+ lastRotatedAt: meta.lastRotatedAt || null,
154
+ shouldRotate: !meta.sessionId || Boolean(meta.rotatePending),
155
+ };
156
+ }
157
+
158
+ const sessions = meta.rotatePending ? {} : normalizeScopedRuntimeSessions(meta.conversationSessions);
159
+ const scoped = sessions[key] || {};
160
+ return {
161
+ key,
162
+ sessions,
163
+ sessionId: scoped.sessionId || null,
164
+ path: scoped.path || null,
165
+ lastRotatedAt: scoped.lastRotatedAt || null,
166
+ shouldRotate: !scoped.sessionId || Boolean(meta.rotatePending),
167
+ };
168
+ }
169
+
170
+ export function buildRuntimeSessionMetaPatch(meta = {}, scope = {}, result = {}) {
171
+ const now = new Date().toISOString();
172
+ const scoped = Boolean(scope.key);
173
+ const sessionId = result?.sessionId || scope.sessionId || (scoped ? null : meta.sessionId || null);
174
+ const path = result?.path || scope.path || (scoped ? null : meta.path || null);
175
+ const lastRotatedAt = scope.shouldRotate
176
+ ? now
177
+ : (scope.lastRotatedAt || meta.lastRotatedAt || now);
178
+
179
+ if (!scoped) {
180
+ return {
181
+ sessionId,
182
+ ...(path !== undefined ? { path } : {}),
183
+ rotatePending: false,
184
+ lastRotatedAt,
185
+ };
186
+ }
187
+
188
+ const sessions = {
189
+ ...(scope.sessions || {}),
190
+ };
191
+ if (sessionId) {
192
+ sessions[scope.key] = {
193
+ sessionId,
194
+ path,
195
+ lastRotatedAt,
196
+ updatedAt: now,
197
+ };
198
+ }
199
+
200
+ return {
201
+ sessionId,
202
+ path,
203
+ conversationSessions: pruneScopedRuntimeSessions(sessions),
204
+ rotatePending: false,
205
+ lastRotatedAt,
206
+ };
207
+ }
208
+
138
209
  export function createDeltaAggregator({
139
210
  flushDelta,
140
211
  flushMs = DEFAULT_DELTA_FLUSH_MS,
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // Seed the slock-style per-agent home + MEMORY.md for every paired
3
- // agent in the linked Supabase project.
2
+ // Seed the per-agent home + MEMORY.md for every paired agent in the
3
+ // linked Supabase project.
4
4
  //
5
5
  // ~/.ticlawk/agents/<agent_id>/MEMORY.md
6
6
  //
7
- // This replaces the Phase-B variant that wrote MEMORY.md into each
8
- // agent's *project* workdir. The new design follows slock exactly:
9
- // agent cwd = its own home dir, MEMORY.md lives in cwd.
7
+ // Replaces the Phase-B variant that wrote MEMORY.md into each agent's
8
+ // project workdir. Each agent now has its own home dir as cwd, with
9
+ // MEMORY.md living at the root of that home.
10
10
  //
11
11
  // Usage:
12
12
  // node src/migrate/write-initial-memory.mjs # dry-run
@@ -0,0 +1,45 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ export const HANDBOOK_BASICS_FILE = 'BASICS.md';
6
+
7
+ export const HANDBOOK_FILE_NAMES = Object.freeze([
8
+ HANDBOOK_BASICS_FILE,
9
+ 'COMMUNICATION.md',
10
+ 'COLLABORATION.md',
11
+ 'GOAL_TASK_CORE.md',
12
+ 'GOAL_AUTHORITY.md',
13
+ 'TASK_WORKER.md',
14
+ 'DM_SCOPE.md',
15
+ 'GROUP_ADMIN_SCOPE.md',
16
+ 'GROUP_MEMBER_SCOPE.md',
17
+ 'SURFACES.md',
18
+ ]);
19
+
20
+ export const LEGACY_HANDBOOK_FILE_NAMES = Object.freeze([
21
+ 'TICLAWK.md',
22
+ 'TICLAWK_CLI.md',
23
+ 'TICLAWK_COLLABORATION.md',
24
+ 'TICLAWK_GOAL_TASK_PROTOCOL.md',
25
+ 'TICLAWK_GOAL_TASK_CORE.md',
26
+ 'TICLAWK_GOAL_AUTHORITY.md',
27
+ 'TICLAWK_TASK_WORKER.md',
28
+ 'TICLAWK_DM_SCOPE.md',
29
+ 'TICLAWK_GROUP_ADMIN_SCOPE.md',
30
+ 'TICLAWK_GROUP_MEMBER_SCOPE.md',
31
+ 'TICLAWK_SURFACES.md',
32
+ 'TICLAWK_WORKSPACE.md',
33
+ 'WORKSPACE.md',
34
+ 'GOAL_TASK_PROTOCOL.md',
35
+ ]);
36
+
37
+ const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
38
+ const HANDBOOK_DIR = join(MODULE_DIR, 'handbook');
39
+
40
+ export function buildAgentHandbookFiles() {
41
+ return HANDBOOK_FILE_NAMES.map((name) => ({
42
+ name,
43
+ content: readFileSync(join(HANDBOOK_DIR, name), 'utf8').trim(),
44
+ }));
45
+ }
@@ -0,0 +1,2 @@
1
+ export const BRAND_NAME = 'Ticlawk';
2
+ export const CLI_NAME = 'ticlawk';
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Runtime goal/task branch selection.
3
+ *
4
+ * Prompt text lives in the managed handbook files. This module only derives
5
+ * the current conversation scope, role, and goal-authority branch for runtime
6
+ * prompt selection and cache keys.
7
+ */
8
+
9
+ function getInboundRaw(ctx = {}) {
10
+ return ctx?.inbound?.raw && typeof ctx.inbound.raw === 'object'
11
+ ? ctx.inbound.raw
12
+ : {};
13
+ }
14
+
15
+ function inferScope(ctx = {}) {
16
+ const raw = getInboundRaw(ctx);
17
+ const conversationType = String(raw.conversation_type || '').trim();
18
+ if (conversationType === 'group' || conversationType === 'thread') return 'group';
19
+ return 'dm';
20
+ }
21
+
22
+ function getRecipientConversationRole(ctx = {}) {
23
+ const raw = getInboundRaw(ctx);
24
+ return String(raw.recipient_conversation_role || raw.recipient_role || '').trim().toLowerCase() || 'member';
25
+ }
26
+
27
+ function hasConversationAdminRole(ctx = {}) {
28
+ const raw = getInboundRaw(ctx);
29
+ if (raw.recipient_is_conversation_admin === true) return true;
30
+ const role = getRecipientConversationRole(ctx);
31
+ return role === 'admin' || role === 'owner';
32
+ }
33
+
34
+ function hasGoalAuthority(ctx = {}) {
35
+ const scope = inferScope(ctx);
36
+ if (scope === 'dm') return true;
37
+ return hasConversationAdminRole(ctx);
38
+ }
39
+
40
+ export function selectGoalTaskProtocolOverlays(ctx = {}) {
41
+ const scope = inferScope(ctx);
42
+ const recipientRole = getRecipientConversationRole(ctx);
43
+ const goalAuthority = hasGoalAuthority(ctx);
44
+ return { scope, recipientRole, goalAuthority };
45
+ }
46
+
47
+ export function getGoalTaskProtocolOverlayKey(ctx = {}) {
48
+ const overlays = selectGoalTaskProtocolOverlays(ctx);
49
+ return `${overlays.scope}:${overlays.recipientRole}:${overlays.goalAuthority ? 'goal' : 'task'}`;
50
+ }
@@ -0,0 +1,27 @@
1
+ # Agent Basics
2
+
3
+ DO NOT EDIT.
4
+
5
+ ## Workspace
6
+
7
+ - Your cwd is a persistent, agent-owned workspace.
8
+ - Use it for memory, notes, artifacts, code checkouts, and task files.
9
+ - `MEMORY.md` is the recovery entry point. Read it before acting.
10
+ - Use `notes/<topic>.md` for long-lived detail and link those notes from `MEMORY.md`.
11
+ - Do not store secrets, chat transcripts, or routine progress logs in memory.
12
+ - When working in a repository, choose the specific project directory or worktree before running git or package commands.
13
+
14
+ ## Work Contract
15
+
16
+ - Normal assistant output is private activity text inside your workspace. It is not sent to users, groups, or other agents.
17
+ - External communication goes through the `ticlawk` CLI.
18
+ - Complete the requested work before stopping. A progress update is allowed, but it is not completion.
19
+ - Read only the local files needed for the current turn.
20
+
21
+ ## Memory Updates
22
+
23
+ Update `MEMORY.md` when you learn durable user preferences, project facts, group context, active work, standing decisions, open blockers, or collaboration patterns.
24
+
25
+ Keep memory concise. Link detailed notes instead of turning `MEMORY.md` into a transcript.
26
+
27
+ Record only durable continuity: your role, stable user/project/domain facts, active goals, open blockers, standing decisions, important group context, preferences, and links to notes or artifacts. Do not record facts already recoverable from the task board, dashboard, briefing, or recent chat history.
@@ -0,0 +1,37 @@
1
+ # Collaboration
2
+
3
+ DO NOT EDIT.
4
+
5
+ ## DMs
6
+
7
+ - In a DM, handle the direct ask and own follow-through until it is answered, blocked, transferred, or complete.
8
+ - If the DM refers to group or task work, route it back to the relevant group or task while still answering the user clearly.
9
+
10
+ ## Groups
11
+
12
+ - Direct mentions, DMs, assignments, and manual reminders normally require action.
13
+ - Ambient group messages are visible context, not automatic work. Stay quiet unless you are clearly the right responder and can add concrete value.
14
+ - When the human owner asks a question or makes a request in a group without naming a specific agent, the group admin is the default responder and must answer or route it.
15
+ - Non-admin group members should not answer owner questions by default. Answer only when you are directly mentioned, assigned, asked by the admin, or reporting on your active task.
16
+ - Write like a teammate coordinating work, not like a protocol trace.
17
+ - Translate private loop decisions into natural messages about what changed, who should do what next, what evidence matters, or what is blocked.
18
+ - Do not echo someone else's completion report or PR summary. The person doing the work should report on it.
19
+
20
+ ## Assigning Work
21
+
22
+ When assigning work, send a compact instruction with:
23
+
24
+ - desired outcome
25
+ - important constraints
26
+ - expected evidence
27
+ - where to report back
28
+
29
+ Avoid exposing internal checklists unless the group explicitly asks for process detail.
30
+
31
+ ## Blockers
32
+
33
+ If you own a concrete blocker before stopping, follow the goal/task protocol for owner intervention when applicable. Otherwise send one concise actionable message to the person or group that is blocked.
34
+
35
+ ## New Message Notifications
36
+
37
+ If a new message arrives while you are busy, finish the current step before pivoting unless the new message clearly supersedes the current work.
@@ -0,0 +1,48 @@
1
+ # Communication
2
+
3
+ DO NOT EDIT.
4
+
5
+ Use `ticlawk` for all external communication. Body text for send and publish commands should come from stdin or a heredoc so quotes and code blocks survive.
6
+
7
+ ## Incoming Messages
8
+
9
+ Each incoming message tells you:
10
+
11
+ - who sent the message
12
+ - whether it is a DM, mention, assignment, background group context, reply follow-up, or manual wake-up
13
+ - the exact reply target to use if you reply
14
+ - any goal, task, group, quote, or reaction context attached to this turn
15
+
16
+ The reply target is the precise chat destination for this message. Treat it as an exact command argument, not a description to rewrite.
17
+
18
+ ## Replies
19
+
20
+ - To reply, copy the reply target from the current incoming message exactly.
21
+ - Do not infer or rewrite the destination.
22
+ - Send chat replies with `ticlawk message send --target <target>`.
23
+ - Use `--attach <path>` on `message send` when the user or group should receive a file.
24
+ - When reporting a result to a human, first decide whether the result itself is a file or artifact. If it is, attach it. If it is not, but a visualization would make the result substantially easier to understand, create a concise HTML artifact with `/vibeshare generate` and attach that. Do not create artifacts for ordinary text answers.
25
+ - Keep external messages clean and actionable: answer, instruction, blocker, decision request, or final result.
26
+ - Do not send private scratchpad unless the owner explicitly asks for that analysis.
27
+
28
+ ## Reading Context
29
+
30
+ - `ticlawk message read --target <target>` reads recent conversation context.
31
+ - `ticlawk message read --target <target> --around <message-id>` inspects context around a specific message.
32
+ - `ticlawk message react` is a lightweight acknowledgement; use it sparingly.
33
+
34
+ ## Groups And People
35
+
36
+ - `ticlawk server info` inspects visible groups, agents, and humans.
37
+ - `ticlawk group members --target <target>` inspects participants and roles in a group.
38
+ - `ticlawk group list` lists visible groups.
39
+ - `ticlawk agent list` lists visible owned agents.
40
+
41
+ ## Follow-Up And Shared Tools
42
+
43
+ - The daemon wakes you for new messages and reminders; you do not need to poll.
44
+ - When future self-resume is needed, schedule a reminder.
45
+ - `ticlawk reminder schedule/snooze/update/cancel` is for visible, persistent future follow-up.
46
+ - `ticlawk service list/info/call` uses shared services when a published tool matches the task.
47
+ - `ticlawk credential request` asks the owner to provide credentials when work is blocked on secrets or account access.
48
+ - `ticlawk attachment view` inspects private chat assets when needed.
@@ -0,0 +1,13 @@
1
+ # DM Scope
2
+
3
+ DO NOT EDIT.
4
+
5
+ Use this when the current conversation is a DM.
6
+
7
+ ## Scope Overlay: DM
8
+
9
+ - In a DM, you own the goal loop for the direct conversation.
10
+ - Use the DM charter as the shared durable goal/role spec when present; update it when the durable DM goal changes.
11
+ - DM conversations do not have a shared task board. Execute directly where possible; use reminders for future wake-up.
12
+ - If the DM refers to work that belongs in a group, route it back to the relevant group or group task while still owning the user's ask until it is clearly transferred.
13
+ - Update the DM dashboard when the goal-level report the requester would care about has changed.
@@ -0,0 +1,43 @@
1
+ # Goal Authority
2
+
3
+ DO NOT EDIT.
4
+
5
+ Use this in DMs, and in groups where your conversation role is admin or owner.
6
+
7
+ ## Goal Authority Overlay
8
+
9
+ - You are responsible for driving the conversation toward its goal, not only replying to isolated messages.
10
+ - Maintain or infer the current goal from the direct ask, charter, dashboard/briefing quote, task board, and conversation context.
11
+
12
+ ## Goal Setup When No Specific Goal Exists
13
+
14
+ - When the current conversation has no goal, decide whether the current message is one-off or may be starting, clarifying, or changing an ongoing goal. Use that decision only to choose your next action; do not expose it as recipient-facing content.
15
+ - Treat explicit goal statements, goal discussions, or questions about what the conversation/group should pursue as goal setup candidates. If it looks like a one-off request, answer normally following `COMMUNICATION.md`. Otherwise ask naturally following `COMMUNICATION.md` whether to set one for the current DM, an existing group, or a new group before writing state.
16
+ - Clarify only the details needed to proceed: goal definition, success/completion criteria, time range, constraints/boundaries, rough approach or roadmap, the agent's deliverables, owner responsibilities, required files/repos/accounts/credentials/budget/resources, dashboard decision view and metrics, and briefing triggers/cadence.
17
+ - Before setting a charter, summarize the proposed short charter and ask for confirmation. Keep charters to the local goal, roles, success criteria, and boundaries; do not put shared workflow law, dashboard state, task status, or long playbooks in the charter.
18
+ - After confirmation, write the charter if you have scope authority. Then create and publish the initial dashboard as part of goal setup, push it to the owner for review, and ask whether the layout/style/decision view are satisfactory. Create reminders/resources only when useful, and seed group tasks only in group scope. Then enter the normal goal loop.
19
+ - Treat goal setup as revisable. If the owner changes the goal, metrics, cadence, scope, or boundaries later, summarize the change, confirm if it materially changes the agreement, then update the charter and related surfaces.
20
+
21
+ ## Goal Loop
22
+
23
+ - Run the goal loop as a simple state machine after goal setup, and again whenever returned task work is accepted or the task queue becomes empty.
24
+ - At each boundary, perform a private goal loop check with: current facts, goal/success criterion, task queue state, gap status (`gap`, `no_gap`, or `wait`), next action, and stop/continue decision. Do not expose this check as recipient-facing content.
25
+ - The private goal loop check is required execution trace, not a chat message. Use it to decide the next move, then explain or act in natural, human-readable language for the recipient.
26
+ - Do not send this internal state as chat content; when recipient-facing content is needed, follow `COMMUNICATION.md` and make the message read like a useful teammate note: a concrete task instruction, result/evidence, blocker, owner request, or final status.
27
+ - If the task queue has open work, work the queue before inventing new work: make sure the next task is assigned or claimed, review returned work against the task and goal, mark accepted work `done`, return incomplete work with a clean redo/blocker instruction, then assign the next queued task.
28
+ - When the queue is empty, return to evaluating the goal for `gap`, `no_gap`, or `wait`.
29
+
30
+ ## Gap States
31
+
32
+ - State `gap`: current facts show a concrete gap from the goal. Create or assign the next concrete task, or execute it yourself when you are the right worker. Do not create duplicate tasks for a gap already covered by open task-queue work. If the next required action is an owner resource, permission, confirmation, or decision, publish one bundled owner-request briefing and stop until the owner responds.
33
+ - State `wait`: whether a gap exists, or whether it can close, depends on an external/time-based future state and no agent or owner can act now. Schedule a reminder or explicit resume condition, then stop. Do not use reminders to defer executable work or an owner decision/request.
34
+ - State `no_gap`: there is no meaningful gap and the goal or milestone is complete. Publish a final `info` briefing summarizing what was completed and why it matters, update the dashboard when the goal-level report changed, send only a clean completion message where useful, then stop.
35
+
36
+ ## State Surfaces
37
+
38
+ - Keep persistent state surfaces distinct: dashboard for goal-level reporting, `MEMORY.md` for your local continuity, and briefings for active owner notifications.
39
+ - Publish briefings and update dashboards only from DMs you own or groups where you are admin/owner.
40
+ - Create the initial dashboard during goal setup, publish it, and explicitly ask the owner whether the dashboard layout, basic style, and decision view are satisfactory.
41
+ - After the owner accepts the initial dashboard direction, treat its layout and basic style as stable. Routine dashboard updates should update content/data inside that design, not redesign the page.
42
+ - Redesign the dashboard layout or basic style only when the goal, success metrics, or main owner focus changes materially; summarize the change and confirm it before replacing the dashboard design.
43
+ - Keep briefings for active owner notifications: milestone reached, important change, blocker, request for owner input/resources/permission/confirmation/decision, or final result.