ticlawk 0.1.16-dev.13 → 0.1.16-dev.15

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/bin/ticlawk.mjs CHANGED
@@ -42,6 +42,8 @@ import {
42
42
  runDashboardSetCommand,
43
43
  runDashboardGetCommand,
44
44
  runCredentialRequestCommand,
45
+ runBriefingPublishCommand,
46
+ runBriefingGetCommand,
45
47
  runServiceCreateCommand,
46
48
  runServiceUpdateCommand,
47
49
  runServiceDeleteCommand,
@@ -538,6 +540,24 @@ async function main() {
538
540
  process.exit(1);
539
541
  }
540
542
 
543
+ if (command === 'briefing') {
544
+ const sub = args._[1];
545
+ if (args.help || args.h || !sub) {
546
+ console.log(AGENT_COMMAND_HELP.briefing);
547
+ return;
548
+ }
549
+ if (sub === 'publish') {
550
+ process.exitCode = await runBriefingPublishCommand(args);
551
+ return;
552
+ }
553
+ if (sub === 'get') {
554
+ process.exitCode = await runBriefingGetCommand(args);
555
+ return;
556
+ }
557
+ console.error(`unknown briefing subcommand: ${sub}`);
558
+ process.exit(1);
559
+ }
560
+
541
561
  if (command === 'credential') {
542
562
  const sub = args._[1];
543
563
  if (args.help || args.h || !sub) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticlawk",
3
- "version": "0.1.16-dev.13",
3
+ "version": "0.1.16-dev.15",
4
4
  "description": "Local connector that links agent harnesses (Claude Code, Codex, OpenClaw, opencode, Pi) to the Ticlawk mobile app.",
5
5
  "type": "module",
6
6
  "main": "ticlawk.mjs",
@@ -610,6 +610,24 @@ export async function callService({ actingAgentId, name, input }) {
610
610
  );
611
611
  }
612
612
 
613
+ // ── Briefings (CoS publish) ──
614
+
615
+ export async function getBriefing({actingAgentId, briefingId}) {
616
+ const params = new URLSearchParams();
617
+ params.set('acting_as_agent_id', actingAgentId);
618
+ return apiFetch(`/api/agent/briefings/${encodeURIComponent(briefingId)}?${params}`);
619
+ }
620
+
621
+ export async function publishBriefing({actingAgentId, bodyText, bodyHtml}) {
622
+ const body = { acting_as_agent_id: actingAgentId };
623
+ if (bodyText != null) body.body_text = bodyText;
624
+ if (bodyHtml != null) body.body_html = bodyHtml;
625
+ return apiFetch('/api/agent/briefings', {
626
+ method: 'POST',
627
+ body: JSON.stringify(body),
628
+ });
629
+ }
630
+
613
631
  // ── Credentials (CoS slot creation) ──
614
632
 
615
633
  export async function requestCredential({
@@ -167,11 +167,11 @@ const COS_ADDENDUM = [
167
167
  '',
168
168
  'Owner model: hold the user\'s preferences and constraints across workstreams. Apply them consistently. Do not "for-your-own-good" around stated preferences.',
169
169
  '',
170
- '奏折 / report posture: publish status to the user by sending a message into the CoS DM with `ticlawk message send --target dm:@<owner> --kind report` (stdin = the report body). Use this for scheduled summaries, escalations, or workstream check-ins. Reports surface in the Office → 奏折 sub-tab; chat replies do not. Do not over-spam reports — one per meaningful state change.',
170
+ 'Briefing posture: publish briefings via `ticlawk briefing publish` this is a SEPARATE surface from chat. Two content formats: `--text "<≤100 chars>"` for short pings, or `--html <path/to/file.html>` for rich one-page bodies. Briefings do NOT appear in the chat stream they only render in Office → Briefings as full-screen cards owner steps through. Do not over-spam — one per meaningful state change. When the owner taps "comment" on a briefing, the inbound reply message carries metadata.context_ref = { kind: "briefing", briefing_id }; thread the conversation from there.',
171
171
  '',
172
- 'Dashboard posture: maintain a per-workstream dashboard via `ticlawk dashboard set --target "#<ws>"` (stdin JSON: { data_json, html_template }). data_json holds the live numbers; html_template is the hand-written rendering. CoS replaces wholesale — there is no partial update protocol, and there is no schema lock-in on data_json. Use the dashboard for at-a-glance state; use 奏折 for narrative.',
172
+ 'Dashboard posture: maintain a per-workstream dashboard via `ticlawk dashboard set --target "#<ws>"` (stdin JSON: { data_json, html_template }). data_json holds the live numbers; html_template is the hand-written rendering. CoS replaces wholesale — there is no partial update protocol, and there is no schema lock-in on data_json. Use the dashboard for at-a-glance state; use Briefings for narrative. When the owner replies to a dashboard via the Office "聊一下" path, the inbound message carries metadata.context_ref pointing at the workstream; thread the conversation from there.',
173
173
  '',
174
- 'Cadence: schedule your own daily 晨报 via `ticlawk reminder schedule --title "晨报" --in-minutes 1440 --anchor-conversation-id <your DM with owner>` after you handle this turn. The reminder fires by waking you with a system message; treat that wake as the trigger to compile the day\'s 奏折 across workstreams. Re-schedule on each fire so the cadence persists. Skip a day only if you are explicitly told to.',
174
+ 'Cadence: schedule your own daily morning briefing via `ticlawk reminder schedule --title "morning briefing" --in-minutes 1440 --anchor-conversation-id <your DM with owner>` after you handle this turn. The reminder fires by waking you with a system message; treat that wake as the trigger to compile the day\'s briefings across workstreams. Re-schedule on each fire so the cadence persists. Skip a day only if you are explicitly told to.',
175
175
  '[/cos-role]',
176
176
  ].join('\n');
177
177
 
@@ -180,12 +180,39 @@ function buildCosAddendum(msg) {
180
180
  return COS_ADDENDUM;
181
181
  }
182
182
 
183
+ // Quote block: surfaced just above the user's reply so the agent sees
184
+ // what artifact the reply is *about*. Source of truth is
185
+ // `messages.metadata.quote = { kind, ref, snippet }`. We render a
186
+ // short, prefix-cache-friendly block and tell the agent how to fetch
187
+ // the full content if needed.
188
+ function buildQuoteBlock(msg) {
189
+ const meta = msg.message_metadata || msg.metadata || null;
190
+ const quote = meta && typeof meta === 'object' ? meta.quote : null;
191
+ if (!quote || typeof quote !== 'object') return '';
192
+ const kind = String(quote.kind || '').trim();
193
+ const ref = String(quote.ref || '').trim();
194
+ const snippet = String(quote.snippet || '').trim();
195
+ if (!kind || !ref) return '';
196
+ const fetchHint = kind === 'briefing'
197
+ ? `ticlawk briefing get ${ref}`
198
+ : kind === 'dashboard'
199
+ ? `ticlawk dashboard get --target "#${ref}"`
200
+ : kind === 'message'
201
+ ? `ticlawk message read --around ${ref}`
202
+ : '';
203
+ const lines = ['[quote', ` kind=${kind} ref=${ref}`];
204
+ if (snippet) lines.push(` "${snippet.replace(/"/g, '\\"')}"`);
205
+ if (fetchHint) lines.push(` fetch: ${fetchHint}`);
206
+ lines.push('[/quote]');
207
+ return lines.join('\n');
208
+ }
209
+
183
210
  // Wrap each per-turn message with an explicit reply instruction so the
184
211
  // runtime LLM never has to remember the standing prompt to figure out
185
212
  // HOW to reply. Codex in particular treats the developerInstructions as
186
213
  // background and ignores the chat-send pattern without this per-turn
187
214
  // nudge.
188
- function buildWakePromptText({ envelopeHeader, target, rawText, groupContext, charterBlock, cosAddendum }) {
215
+ function buildWakePromptText({ envelopeHeader, target, rawText, groupContext, charterBlock, cosAddendum, quoteBlock }) {
189
216
  const body = `${envelopeHeader} ${rawText || ''}`.trim();
190
217
  const lines = [];
191
218
  if (charterBlock) {
@@ -194,6 +221,9 @@ function buildWakePromptText({ envelopeHeader, target, rawText, groupContext, ch
194
221
  if (cosAddendum) {
195
222
  lines.push(cosAddendum, '');
196
223
  }
224
+ if (quoteBlock) {
225
+ lines.push(quoteBlock, '');
226
+ }
197
227
  lines.push('New message received:', '', body);
198
228
  if (groupContext) {
199
229
  lines.push('', groupContext);
@@ -230,8 +260,9 @@ export function normalizeInboundMessage(msg) {
230
260
  const groupContext = buildGroupContextBlock(enriched);
231
261
  const charterBlock = buildCharterBlock(enriched);
232
262
  const cosAddendum = buildCosAddendum(enriched);
263
+ const quoteBlock = buildQuoteBlock(enriched);
233
264
  const text = header
234
- ? buildWakePromptText({ envelopeHeader: header, target, rawText, groupContext, charterBlock, cosAddendum })
265
+ ? buildWakePromptText({ envelopeHeader: header, target, rawText, groupContext, charterBlock, cosAddendum, quoteBlock })
235
266
  : rawText;
236
267
  return {
237
268
  bindingId: recipientAgentId,
@@ -161,7 +161,7 @@ export async function runMessageSendCommand(args) {
161
161
  }
162
162
 
163
163
  // Optional --kind <s> classifies the message via metadata.kind. The
164
- // canonical user-facing value is 'report' (奏折 surface). Anything else
164
+ // canonical user-facing value is 'briefing' (Office Briefings surface). Anything else
165
165
  // is passed through as-is so we don't have to update this list to add
166
166
  // new conventions.
167
167
  const kind = getArg(args, 'kind');
@@ -1022,6 +1022,56 @@ export async function runServiceCallCommand(args) {
1022
1022
  return exitFromStatus(res.statusCode);
1023
1023
  }
1024
1024
 
1025
+ export async function runBriefingGetCommand(args) {
1026
+ const env = requireAgentEnv();
1027
+ const id = args._[2] || getArg(args, 'id');
1028
+ if (!id) { console.error('briefing id required (positional or --id)'); return 2; }
1029
+ const params = new URLSearchParams();
1030
+ params.set('id', id);
1031
+ const res = await daemonRequest({
1032
+ method: 'GET',
1033
+ path: `/agent/briefing/get?${params}`,
1034
+ headers: commonHeaders(env),
1035
+ });
1036
+ printJson(res.body);
1037
+ return exitFromStatus(res.statusCode);
1038
+ }
1039
+
1040
+ export async function runBriefingPublishCommand(args) {
1041
+ const env = requireAgentEnv();
1042
+ const textArg = getArg(args, 'text');
1043
+ const htmlPath = getArg(args, 'html');
1044
+ if ((textArg && htmlPath) || (!textArg && !htmlPath)) {
1045
+ console.error('exactly one of --text "<short>" or --html <path> is required');
1046
+ return 2;
1047
+ }
1048
+ let bodyText = null;
1049
+ let bodyHtml = null;
1050
+ if (textArg) {
1051
+ if (textArg.length > 100) {
1052
+ console.error('--text must be ≤100 chars');
1053
+ return 2;
1054
+ }
1055
+ bodyText = textArg;
1056
+ } else {
1057
+ try {
1058
+ const fs = await import('node:fs');
1059
+ bodyHtml = fs.readFileSync(String(htmlPath), 'utf8');
1060
+ } catch (err) {
1061
+ console.error(`could not read --html file: ${err?.message || err}`);
1062
+ return 2;
1063
+ }
1064
+ }
1065
+ const res = await daemonRequest({
1066
+ method: 'POST',
1067
+ path: '/agent/briefing/publish',
1068
+ headers: commonHeaders(env),
1069
+ body: { body_text: bodyText, body_html: bodyHtml },
1070
+ });
1071
+ printJson(res.body);
1072
+ return exitFromStatus(res.statusCode);
1073
+ }
1074
+
1025
1075
  export async function runCredentialRequestCommand(args) {
1026
1076
  const env = requireAgentEnv();
1027
1077
  const name = getArg(args, 'name');
@@ -1158,7 +1208,7 @@ export const AGENT_COMMAND_HELP = {
1158
1208
  ticlawk message send --target "<target>" [--seen-up-to-seq N] [--reply-to <msg-id>] [--attach <file> ...] [--kind <kind>]
1159
1209
  Body is read from stdin (use <<'EOF' ... EOF for multiline).
1160
1210
  --kind <kind> tags this message via metadata.kind. The CoS uses
1161
- --kind report to publish a 奏折 / status report that the Office
1211
+ --kind briefing to publish a status briefing that the Office
1162
1212
  tab can list separately.
1163
1213
  Targets:
1164
1214
  dm:@<user> private message
@@ -1236,6 +1286,19 @@ export const AGENT_COMMAND_HELP = {
1236
1286
  Any agent. Returns contract_schema + description.
1237
1287
  service call --name X # input from stdin (JSON)
1238
1288
  Any agent. Backend proxies to endpoint_config.url. No retry.
1289
+ `,
1290
+ briefing: `ticlawk briefing <publish|get>
1291
+ briefing publish (--text "..." | --html <path>)
1292
+ CoS-only. Publish a briefing to the owner's Office → Briefings.
1293
+ --text short plain text (≤100 chars), use for one-liner pings
1294
+ --html path to an HTML file; body is rendered as a full-screen card
1295
+ Briefings are independent of chat — they do NOT appear in the CoS
1296
+ DM message stream. Use this verb (not \`message send\`) for any
1297
+ status surface the owner consumes in Office.
1298
+ briefing get <id>
1299
+ Fetch a briefing including body_text/body_html. Use this when a
1300
+ quote (metadata.quote.kind=briefing) points at a briefing whose
1301
+ full body you want to read.
1239
1302
  `,
1240
1303
  credential: `ticlawk credential request --name <ENV_VAR> [--description Y] [--workstream "#<ws>"]
1241
1304
  CoS-only. Pre-allocate a credential slot. Response includes a deep
@@ -908,6 +908,40 @@ export async function handleServiceCall(req, body, ctx) {
908
908
  }
909
909
  }
910
910
 
911
+ export async function handleBriefingGet(req, query, ctx) {
912
+ const actingAgentId = getActingAgentId(req, query);
913
+ const v = validateActingAgent(actingAgentId, ctx);
914
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
915
+ const briefingId = String(query?.id || '').trim();
916
+ if (!briefingId) return { status: 400, body: { error: 'id is required' } };
917
+ try {
918
+ const data = await api.getBriefing({ actingAgentId, briefingId });
919
+ return { status: 200, body: data };
920
+ } catch (err) {
921
+ return { status: err?.status || 500, body: { error: err?.message || 'briefing get failed' } };
922
+ }
923
+ }
924
+
925
+ export async function handleBriefingPublish(req, body, ctx) {
926
+ const actingAgentId = getActingAgentId(req, body);
927
+ const v = validateActingAgent(actingAgentId, ctx);
928
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
929
+ const bodyText = typeof body?.body_text === 'string' && body.body_text.trim() ? body.body_text : null;
930
+ const bodyHtml = typeof body?.body_html === 'string' && body.body_html.trim() ? body.body_html : null;
931
+ if ((bodyText && bodyHtml) || (!bodyText && !bodyHtml)) {
932
+ return { status: 400, body: { error: 'exactly one of body_text or body_html is required' } };
933
+ }
934
+ if (bodyText && bodyText.length > 100) {
935
+ return { status: 400, body: { error: 'body_text must be ≤100 chars' } };
936
+ }
937
+ try {
938
+ const data = await api.publishBriefing({ actingAgentId, bodyText, bodyHtml });
939
+ return { status: 200, body: data };
940
+ } catch (err) {
941
+ return { status: err?.status || 500, body: { error: err?.message || 'briefing publish failed' } };
942
+ }
943
+ }
944
+
911
945
  export async function handleCredentialRequest(req, body, ctx) {
912
946
  const actingAgentId = getActingAgentId(req, body);
913
947
  const v = validateActingAgent(actingAgentId, ctx);
package/src/core/http.mjs CHANGED
@@ -13,6 +13,8 @@ import {
13
13
  handleWorkstreamDashboardSet,
14
14
  handleWorkstreamDashboardGet,
15
15
  handleCredentialRequest,
16
+ handleBriefingPublish,
17
+ handleBriefingGet,
16
18
  handleServiceCreate,
17
19
  handleServiceUpdate,
18
20
  handleServiceDelete,
@@ -287,6 +289,16 @@ export function startLocalHttpServer({ port, adapter, ctx }) {
287
289
  const r = await handleWorkstreamDashboardGet(req, parseQuery(req.url || ''), cliCtx);
288
290
  return writeJson(res, r.status, r.body);
289
291
  }
292
+ if (urlNoQuery === '/agent/briefing/publish' && method === 'POST') {
293
+ const body = await readJsonBody(req);
294
+ if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
295
+ const r = await handleBriefingPublish(req, body, cliCtx);
296
+ return writeJson(res, r.status, r.body);
297
+ }
298
+ if (urlNoQuery === '/agent/briefing/get' && method === 'GET') {
299
+ const r = await handleBriefingGet(req, parseQuery(req.url || ''), cliCtx);
300
+ return writeJson(res, r.status, r.body);
301
+ }
290
302
  if (urlNoQuery === '/agent/credential/request' && method === 'POST') {
291
303
  const body = await readJsonBody(req);
292
304
  if (body === null) return writeJson(res, 400, { error: 'invalid json body' });
@@ -22,6 +22,7 @@ to users or groups.
22
22
  - 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.
23
23
  - Use only the provided \`ticlawk\` CLI commands for messaging.
24
24
  - If the turn opens with a \`[charter] ... [/charter]\` block, that is the workstream's binding spec (规章制度). Every action you take in this conversation must conform to it. If a request conflicts with the charter, stop and surface the conflict to the Chief of Staff (CoS) — don't silently route around it.
25
+ - If the turn opens with a \`[quote ... [/quote]\` block, the user is replying with a quoted artifact in context. The block carries \`kind=<message|briefing|dashboard>\`, \`ref=<id>\`, a snippet, and a \`fetch:\` command to read the full content if you need it. Treat the user's text as a response *to* that artifact.
25
26
  - Shared services are CoS-published tools you can invoke. \`ticlawk service list\` shows available names + descriptions; \`ticlawk service info --name X\` shows the input contract; \`ticlawk service call --name X\` runs it with stdin JSON input. There is no retry — call failure means stop and report; do not loop.
26
27
 
27
28
  ## Startup checklist (every turn)