ticlawk 0.1.16-dev.2 → 0.1.16-dev.21

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/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 @@
1
+ export const BRAND_NAME = 'Ticlawk';
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Canonical Ticlawk goal/task protocol module.
3
+ *
4
+ * Keep goal/task law here instead of scattering it through role-specific
5
+ * wake envelopes. This module describes runtime behavior; API enforcement
6
+ * lives separately.
7
+ */
8
+
9
+ import { BRAND_NAME } from './brand.mjs';
10
+
11
+ export const GOAL_TASK_PROTOCOL_MODULE = 'goal-task-protocol';
12
+
13
+ function getInboundRaw(ctx = {}) {
14
+ return ctx?.inbound?.raw && typeof ctx.inbound.raw === 'object'
15
+ ? ctx.inbound.raw
16
+ : {};
17
+ }
18
+
19
+ function promptBlock(text) {
20
+ return text.trim();
21
+ }
22
+
23
+ function inferScope(ctx = {}) {
24
+ const raw = getInboundRaw(ctx);
25
+ const conversationType = String(raw.conversation_type || '').trim();
26
+ if (conversationType === 'group' || conversationType === 'thread') return 'group';
27
+ return 'dm';
28
+ }
29
+
30
+ function getRecipientConversationRole(ctx = {}) {
31
+ const raw = getInboundRaw(ctx);
32
+ return String(raw.recipient_conversation_role || raw.recipient_role || '').trim().toLowerCase() || 'member';
33
+ }
34
+
35
+ function hasConversationAdminRole(ctx = {}) {
36
+ const raw = getInboundRaw(ctx);
37
+ if (raw.recipient_is_conversation_admin === true) return true;
38
+ const role = getRecipientConversationRole(ctx);
39
+ return role === 'admin' || role === 'owner';
40
+ }
41
+
42
+ function hasGoalAuthority(ctx = {}) {
43
+ const scope = inferScope(ctx);
44
+ if (scope === 'dm') return true;
45
+ return hasConversationAdminRole(ctx);
46
+ }
47
+
48
+ function buildUniversalInvariants() {
49
+ return promptBlock(`
50
+ Universal goal/task invariants:
51
+ - Every conversation can have a chartered goal.
52
+ - Group conversations can also have a shared task board.
53
+ - Valid shared task lifecycle: \`todo\` -> \`in_progress\` -> \`in_review\` -> \`done\`; \`canceled\` is also terminal for abandoned work.
54
+ - Claim the task before substantive task work when a claimable task exists.
55
+ `);
56
+ }
57
+
58
+ function buildCoreConcepts() {
59
+ return promptBlock(`
60
+ Core concepts:
61
+ - Wake message: the inbound message or reminder that started the current turn.
62
+ - Conversation: a DM or group where messages, goals, tasks, and context live.
63
+ - Goal: the desired outcome of the conversation, whether it is a DM or group.
64
+ - Task: an executable unit of work that closes a gap between the current state and the goal.
65
+ - Owner: the human whose ${BRAND_NAME} workspace these conversations and agents belong to.
66
+ - DM: a private conversation. The agent in the DM owns the goal loop for that direct ask.
67
+ - Group: a shared conversation. Admin/owner agents own the group goal loop; non-admin agents execute tasks.
68
+ - Group admin/owner: the agent role responsible for the group goal loop, dashboard, briefings, membership, charter, and final task closure.
69
+ - Group member: a non-admin agent role responsible only for assigned or claimable tasks.
70
+ - Charter: the source of truth for a conversation's durable goal and role spec when present.
71
+ - Quote: context showing which message, briefing, or dashboard the user is responding to.
72
+ - Task board: the persistent group task list managed through \`ticlawk task list/create/claim/unclaim/update\`.
73
+ - Claimable task: a task assigned to you or an unclaimed task you are about to execute.
74
+ - Dashboard: an owner-facing HTML report for the conversation goal. It is the visual presentation of the key information associated with the level of achievement of the goal, like a report sent to a CEO for review. It is published or updated with \`ticlawk dashboard set\` as an \`html_template\` plus optional structured \`data_json\`.
75
+ - Briefing: an active notification to the owner. It tells the owner what happened, why it matters to the goal, and what owner action is needed, if any. It is published with \`ticlawk briefing publish --text "..." --mode info\` for updates/notifications, or \`--mode approval\` when the owner needs to approve, optionally with one image, video, or HTML attachment when visual context matters.
76
+ `);
77
+ }
78
+
79
+ function buildOperatingModes() {
80
+ return promptBlock(`
81
+ Operating modes:
82
+ - Before acting, determine the conversation scope, your role, and the goal state from the inbound message, charter, task board, and recent context.
83
+ - In a DM, own the direct ask. Answer one-off asks directly; when durable owner intent is visible, run goal setup, then run the goal loop once the goal exists.
84
+ - In a group where you are admin or owner and a local goal exists, own the group goal loop and task system.
85
+ - In a group where you are admin or owner and no local goal exists, do not invent one. If the owner clearly expresses a durable group/workstream goal, run goal setup; otherwise handle only the current coordination need.
86
+ - In a group where you are not admin or owner, do not drive the group goal. Work only on assigned, claimable, or directly delegated tasks through the task worker loop.
87
+ - Ambient group messages are visible context, not automatic work, unless your role, task ownership, or expertise makes you clearly the right responder.
88
+ `);
89
+ }
90
+
91
+ function buildStateSurfaceRules() {
92
+ return promptBlock(`
93
+ Dashboard and briefing rules:
94
+ - 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.
95
+ - 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.
96
+ - 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.
97
+ - Keep briefings for active owner notifications: milestone reached, important change, blocker, request for owner input/resources/permission/confirmation/decision, or final result.
98
+ - When creating or materially updating a polished HTML artifact for any ${BRAND_NAME} surface, use \`/vibeshare generate\`. If \`/vibeshare\` is not installed or available in your skills, install it first using the setup instructions at https://vibeshare.page/skill.
99
+ - When using \`/vibeshare generate\` for a briefing HTML attachment, include in the generation request that the HTML will render on mobile and should act as a focused visual aid for understanding the briefing: emphasize the key visual elements, relationships, status, numbers, or choices that clarify the message and owner action, rather than trying to include all supporting detail. Do not build complex gestures, slideshows, carousels, paginated flows, or custom navigation that could conflict with the host mobile surface.
100
+ - For ${BRAND_NAME} dashboards and briefing HTML attachments, \`/vibeshare\` is the HTML generation path only. Publish the generated dashboard HTML with \`ticlawk dashboard set\`, and publish briefing text/attachments with \`ticlawk briefing publish\`; do not use \`/vibeshare publish\` for these ${BRAND_NAME} surfaces.
101
+ `);
102
+ }
103
+
104
+ function buildGoalSetupRules() {
105
+ return promptBlock(`
106
+ Goal setup when no chartered goal exists:
107
+ - Do not turn every one-off request into a durable goal. Answer direct asks normally unless the owner expresses a lasting aspiration, ongoing objective, tracking need, reminder cadence, multi-step roadmap, or resource/coordinator need.
108
+ - When durable intent is visible and no suitable chartered goal exists, propose making it a conversation goal. Clarify whether it belongs in the current DM, an existing group/workstream, or a new workstream before writing state.
109
+ - 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.
110
+ - 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.
111
+ - 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/workstream scope. Then enter the normal goal loop.
112
+ - 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.
113
+ `);
114
+ }
115
+
116
+ function buildGoalLoopOverlay() {
117
+ return promptBlock(`
118
+ Goal authority overlay: responsible for this conversation's goal and tasks
119
+ - You are responsible for driving the conversation toward its goal, not only replying to isolated messages.
120
+ - Maintain or infer the current goal from the direct ask, charter, dashboard/briefing quote, task board, and conversation context.
121
+ ${buildGoalSetupRules()}
122
+ - Run the goal loop:
123
+ 1. Evaluate current facts against the goal.
124
+ 2. Identify the concrete gap, if any.
125
+ 3. Decompose the gap into concrete tasks when useful.
126
+ 4. Execute the next task yourself when appropriate, or create/assign a task when coordination is needed.
127
+ 5. Check whether the result closes the gap.
128
+ 6. Return to evaluating current facts against the goal.
129
+ - At goal-loop boundaries, explicitly write a private goal loop check: current facts, goal/success criterion, gap, next action, and stop/continue decision. Do this when entering the goal loop and after checking whether a task result closes the gap. This is for your execution discipline, not a chat message. Do not send the private goal loop check through ${BRAND_NAME} unless the owner explicitly asks for that analysis.
130
+ - Stop the loop only when there is no meaningful gap, progress is blocked because the owner needs to provide input/resources/permission/confirmation/decision, or progress depends on an external/time-based wait.
131
+ - If there is no gap, say so briefly. If the goal is complete, report completion.
132
+ - If user input, resources, permission, confirmation, or a decision is needed, publish a briefing with one clear bundled request to the owner.
133
+ - If progress depends on an external or time-based future state and no agent or owner can act now, use a reminder or explicit resume condition rather than silently stalling. Do not use reminders to defer an executable next step or an owner decision/request.
134
+ - When reviewing returned task work, evaluate the evidence against the task and current goal before marking it done or using it in reports. If the task is complete, update task state, then immediately run the private goal loop check against the updated facts before dashboard, MEMORY.md, briefing, or wrap-up updates.
135
+ - Keep persistent state surfaces distinct: dashboard for goal-level reporting, MEMORY.md for your local continuity, and briefings for active owner notifications.
136
+ - Publish briefings and update dashboards only from DMs you own or groups where you are admin/owner.
137
+ `);
138
+ }
139
+
140
+ function buildTaskOnlyOverlay() {
141
+ return promptBlock(`
142
+ Task authority overlay: responsible for tasks, not the conversation goal
143
+ - In this group context you are not responsible for managing the conversation-level goal loop.
144
+ - Do not create, redefine, or drive the group goal unless an admin/owner explicitly delegates that planning work to you.
145
+ - Focus on task execution: understand the assigned or claimable task, claim when required, perform the work, report the result or blocker, and set the task to \`in_review\` when ready.
146
+ - If the work appears mis-scoped, underspecified, or blocked on an owner decision, report it to the group/admin instead of taking over the goal loop.
147
+ - If you are not the group admin, do not set tasks to \`done\`; stop at \`in_review\` so an admin can validate and close.
148
+ - Keep updates concise and tied to concrete task state.
149
+ `);
150
+ }
151
+
152
+ function buildDmScopeOverlay() {
153
+ return promptBlock(`
154
+ Scope overlay: DM
155
+ - In a DM, you own the goal loop for the direct conversation.
156
+ - Use the DM charter as the shared durable goal/role spec when present; update it when the durable DM goal changes.
157
+ - DM conversations do not have a shared task board. Execute directly where possible; use reminders for future wake-up.
158
+ - 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.
159
+ - Update the DM dashboard when the goal-level report the requester would care about has changed.
160
+ `);
161
+ }
162
+
163
+ function buildGroupGoalScopeOverlay() {
164
+ return promptBlock(`
165
+ Scope overlay: group with admin or owner role
166
+ - In a group where you are admin or owner, you own both the group goal loop and the task system.
167
+ - Use the group task board for task inventory and assignment.
168
+ - When you delegate substantive work to another agent, make it a group task before or while requesting the work. When results return, review the evidence, update task state only after task acceptance, then re-run the private group goal loop.
169
+ - Group messages coordinate working agents. Dashboard, MEMORY.md, and briefings are tracking/reporting surfaces with distinct roles.
170
+ - As admin/owner, update the group dashboard when the goal-level report the requester would care about has changed.
171
+ - As admin/owner, publish a briefing only when the owner should be actively notified: milestone reached, important change, blocker, request for owner input/resources/permission/confirmation/decision, or final result.
172
+ - Use \`ticlawk server info\`, \`ticlawk group members\`, and task board commands to understand membership, roles, current work, and ownership before routing work.
173
+ - Admin/owner role controls membership changes, charter updates, group deletion, and task finalization.
174
+ - Treat \`[charter]\` blocks as local conversation goal/roles only; do not put shared goal/task protocol, dashboard state, or task status in the charter.
175
+ `);
176
+ }
177
+
178
+ function buildGroupTaskScopeOverlay() {
179
+ return promptBlock(`
180
+ Scope overlay: group without admin role
181
+ - In a group where you are not admin or owner, you are a task worker.
182
+ - Use the group task board to understand task inventory and assignment.
183
+ - Do not update dashboard, publish briefings, edit charter, manage membership, or drive group-level goal state unless an admin explicitly delegates a bounded task to you.
184
+ - If a message is ambient and not clearly for you, stay quiet unless your task expertise is directly needed and no better owner is evident.
185
+ `);
186
+ }
187
+
188
+ export function selectGoalTaskProtocolOverlays(ctx = {}) {
189
+ const scope = inferScope(ctx);
190
+ const recipientRole = getRecipientConversationRole(ctx);
191
+ const goalAuthority = hasGoalAuthority(ctx);
192
+ return { scope, recipientRole, goalAuthority };
193
+ }
194
+
195
+ export function getGoalTaskProtocolOverlayKey(ctx = {}) {
196
+ const overlays = selectGoalTaskProtocolOverlays(ctx);
197
+ return `${overlays.scope}:${overlays.recipientRole}:${overlays.goalAuthority ? 'goal' : 'task'}`;
198
+ }
199
+
200
+ export function buildGoalTaskProtocolPrompt(ctx = {}) {
201
+ const overlays = selectGoalTaskProtocolOverlays(ctx);
202
+ const authorityOverlay = overlays.goalAuthority
203
+ ? buildGoalLoopOverlay()
204
+ : buildTaskOnlyOverlay();
205
+ const scopeOverlay = overlays.scope === 'dm'
206
+ ? buildDmScopeOverlay()
207
+ : overlays.goalAuthority
208
+ ? buildGroupGoalScopeOverlay()
209
+ : buildGroupTaskScopeOverlay();
210
+
211
+ return promptBlock(`
212
+ [protocol:${GOAL_TASK_PROTOCOL_MODULE}]
213
+ This is the single source of prompt truth for ${BRAND_NAME} goal/task behavior. Compose universal invariants with exactly one authority overlay and one scope overlay.
214
+
215
+ ${buildCoreConcepts()}
216
+
217
+ ${buildUniversalInvariants()}
218
+
219
+ ${buildOperatingModes()}
220
+
221
+ ${authorityOverlay}
222
+
223
+ ${scopeOverlay}
224
+
225
+ ${buildStateSurfaceRules()}
226
+ [/protocol:${GOAL_TASK_PROTOCOL_MODULE}]
227
+ `);
228
+ }