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 +20 -0
- package/package.json +1 -1
- package/src/adapters/ticlawk/api.mjs +18 -0
- package/src/adapters/ticlawk/index.mjs +36 -5
- package/src/cli/agent-commands.mjs +65 -2
- package/src/core/agent-cli-handlers.mjs +34 -0
- package/src/core/http.mjs +12 -0
- package/src/runtimes/_shared/standing-prompt.mjs +1 -0
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
|
@@ -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
|
-
'
|
|
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
|
|
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
|
|
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 '
|
|
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
|
|
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)
|