ticlawk 0.1.16-dev.8 → 0.1.16

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 (41) hide show
  1. package/README.md +15 -3
  2. package/bin/ticlawk.mjs +208 -26
  3. package/package.json +1 -1
  4. package/src/adapters/ticlawk/api.mjs +283 -48
  5. package/src/adapters/ticlawk/credentials.mjs +41 -1
  6. package/src/adapters/ticlawk/index.mjs +126 -121
  7. package/src/adapters/ticlawk/wake-client.mjs +1 -1
  8. package/src/cli/agent-commands.mjs +560 -36
  9. package/src/core/agent-cli-handlers.mjs +435 -18
  10. package/src/core/agent-home.mjs +81 -1
  11. package/src/core/argv.mjs +11 -1
  12. package/src/core/events/worker-events.mjs +32 -36
  13. package/src/core/http.mjs +119 -0
  14. package/src/core/runtime-contract.mjs +0 -1
  15. package/src/core/runtime-env.mjs +7 -0
  16. package/src/core/runtime-support.mjs +108 -77
  17. package/src/runtimes/_shared/agent-handbook.mjs +45 -0
  18. package/src/runtimes/_shared/brand.mjs +2 -0
  19. package/src/runtimes/_shared/goal-task-protocol.mjs +50 -0
  20. package/src/runtimes/_shared/handbook/BASICS.md +27 -0
  21. package/src/runtimes/_shared/handbook/COLLABORATION.md +37 -0
  22. package/src/runtimes/_shared/handbook/COMMUNICATION.md +55 -0
  23. package/src/runtimes/_shared/handbook/DM_SCOPE.md +13 -0
  24. package/src/runtimes/_shared/handbook/GOAL_AUTHORITY.md +46 -0
  25. package/src/runtimes/_shared/handbook/GOAL_TASK_CORE.md +43 -0
  26. package/src/runtimes/_shared/handbook/GROUP_ADMIN_SCOPE.md +21 -0
  27. package/src/runtimes/_shared/handbook/GROUP_MEMBER_SCOPE.md +15 -0
  28. package/src/runtimes/_shared/handbook/SURFACES.md +41 -0
  29. package/src/runtimes/_shared/handbook/TASK_WORKER.md +14 -0
  30. package/src/runtimes/_shared/standing-prompt.mjs +134 -275
  31. package/src/runtimes/_shared/wake-prompt.mjs +261 -0
  32. package/src/runtimes/claude-code/index.mjs +19 -46
  33. package/src/runtimes/claude-code/session.mjs +2 -7
  34. package/src/runtimes/codex/index.mjs +115 -63
  35. package/src/runtimes/codex/session.mjs +2 -12
  36. package/src/runtimes/openclaw/index.mjs +11 -24
  37. package/src/runtimes/opencode/index.mjs +38 -60
  38. package/src/runtimes/opencode/session.mjs +12 -12
  39. package/src/runtimes/pi/index.mjs +38 -60
  40. package/src/runtimes/pi/session.mjs +9 -6
  41. package/ticlawk.mjs +0 -30
@@ -8,11 +8,13 @@
8
8
  * the ticlawk backend.
9
9
  *
10
10
  * Commands:
11
- * ticlawk message send --target <t> [--seen-up-to-seq N] [--reply-to <msg>]
11
+ * ticlawk message send --target <t> [--phase progress|final] [--seen-up-to-seq N] [--reply-to <msg>]
12
12
  * ticlawk message read --target <t> [--around <msg>] [--limit N]
13
+ * ticlawk task create --target <t> [--title <t>] [--assign-agent <id>]
13
14
  * ticlawk task claim --message-id <id> [--lease-seconds N]
14
15
  * ticlawk task update --task-id <id> --status <s>
15
- * ticlawk task list [--target <t>]
16
+ * ticlawk task list [--target <t>] # group admins see the full task board
17
+ * ticlawk charter get/set --target <t>
16
18
  * ticlawk group members --target <t>
17
19
  * ticlawk server info [--refresh]
18
20
  *
@@ -41,6 +43,9 @@ function requireAgentEnv() {
41
43
  agentId,
42
44
  hostId: String(process.env.TICLAWK_RUNTIME_HOST_ID || '').trim() || null,
43
45
  sessionId: String(process.env.TICLAWK_RUNTIME_SESSION_ID || '').trim() || null,
46
+ currentConversationId: String(process.env.TICLAWK_RUNTIME_CONVERSATION_ID || '').trim() || null,
47
+ currentMessageId: String(process.env.TICLAWK_RUNTIME_MESSAGE_ID || '').trim() || null,
48
+ currentTarget: String(process.env.TICLAWK_RUNTIME_TARGET || '').trim() || null,
44
49
  };
45
50
  }
46
51
 
@@ -51,6 +56,11 @@ function commonHeaders(env) {
51
56
  };
52
57
  if (env.hostId) headers['X-Ticlawk-Runtime-Host-Id'] = env.hostId;
53
58
  if (env.sessionId) headers['X-Ticlawk-Runtime-Session-Id'] = env.sessionId;
59
+ if (env.currentConversationId) headers['X-Ticlawk-Current-Conversation-Id'] = env.currentConversationId;
60
+ if (env.currentMessageId) headers['X-Ticlawk-Current-Message-Id'] = env.currentMessageId;
61
+ if (env.currentTarget && /^[\x00-\x7F]*$/.test(env.currentTarget)) {
62
+ headers['X-Ticlawk-Current-Target'] = env.currentTarget;
63
+ }
54
64
  return headers;
55
65
  }
56
66
 
@@ -104,6 +114,14 @@ function getNumberArg(args, name) {
104
114
  return Number.isFinite(n) ? n : null;
105
115
  }
106
116
 
117
+ function getMessagePhaseArg(args) {
118
+ const raw = getArg(args, 'phase');
119
+ if (raw == null) return null;
120
+ const phase = raw.trim().toLowerCase();
121
+ if (phase === 'progress' || phase === 'final') return phase;
122
+ return { error: '--phase must be progress or final' };
123
+ }
124
+
107
125
  function printJson(value) {
108
126
  console.log(JSON.stringify(value, null, 2));
109
127
  }
@@ -160,13 +178,28 @@ export async function runMessageSendCommand(args) {
160
178
  mediaAssetIds.push(upload.assetId);
161
179
  }
162
180
 
181
+ // Optional metadata flags. --kind classifies the message; --phase
182
+ // marks whether this is an intermediate progress update or the final
183
+ // visible response for the current turn.
184
+ const kind = getArg(args, 'kind');
185
+ const phase = getMessagePhaseArg(args);
186
+ if (phase && typeof phase === 'object') {
187
+ console.error(phase.error);
188
+ return 2;
189
+ }
190
+ const metadata = {};
191
+ if (kind) metadata.kind = kind;
192
+ if (phase) metadata.agent_response_phase = phase;
193
+
163
194
  const body = {
164
195
  target,
165
196
  conversation_id: conversationId,
166
197
  text: text.replace(/\n+$/, ''),
167
198
  seen_up_to_seq: getNumberArg(args, 'seen-up-to-seq'),
168
199
  reply_to_message_id: getArg(args, 'reply-to'),
200
+ allow_cross_target: Boolean(args['allow-cross-target']),
169
201
  media_asset_ids: mediaAssetIds.length > 0 ? mediaAssetIds : undefined,
202
+ metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
170
203
  };
171
204
  const res = await daemonRequest({
172
205
  method: 'POST',
@@ -217,7 +250,7 @@ export async function runTaskCreateCommand(args) {
217
250
  if (!text || !text.trim()) {
218
251
  console.error('task body is required on stdin');
219
252
  console.error('Example:');
220
- console.error(" ticlawk task create --target \"#frontend\" --title \"fix login\" <<'EOF'");
253
+ console.error(" ticlawk task create --target \"#frontend\" --title \"fix login\" --assign-agent <agent-id> <<'EOF'");
221
254
  console.error(' Detailed description goes here.');
222
255
  console.error(' EOF');
223
256
  return 2;
@@ -231,6 +264,7 @@ export async function runTaskCreateCommand(args) {
231
264
  conversation_id: conversationId,
232
265
  text: text.replace(/\n+$/, ''),
233
266
  title: getArg(args, 'title'),
267
+ assign_agent_id: getArg(args, 'assign-agent') || getArg(args, 'assignee-agent'),
234
268
  },
235
269
  });
236
270
  printJson(res.body);
@@ -654,6 +688,11 @@ function inferContentType(filePath) {
654
688
  case '.jpg': case '.jpeg': return 'image/jpeg';
655
689
  case '.gif': return 'image/gif';
656
690
  case '.webp': return 'image/webp';
691
+ case '.mp4': return 'video/mp4';
692
+ case '.mov': return 'video/quicktime';
693
+ case '.m4v': return 'video/x-m4v';
694
+ case '.webm': return 'video/webm';
695
+ case '.html': case '.htm': return 'text/html';
657
696
  case '.pdf': return 'application/pdf';
658
697
  case '.txt': return 'text/plain';
659
698
  case '.md': return 'text/markdown';
@@ -662,18 +701,6 @@ function inferContentType(filePath) {
662
701
  }
663
702
  }
664
703
 
665
- export async function runAttachmentUploadCommand(args) {
666
- const env = requireAgentEnv();
667
- const file = args._?.[2];
668
- if (!file) {
669
- console.error('usage: ticlawk attachment upload <file>');
670
- return 2;
671
- }
672
- const upload = await uploadFileViaDaemon(env, file);
673
- printJson(upload);
674
- return upload.ok ? 0 : 1;
675
- }
676
-
677
704
  export async function runAttachmentViewCommand(args) {
678
705
  const env = requireAgentEnv();
679
706
  const assetId = args._?.[2];
@@ -812,6 +839,410 @@ export async function runGroupMembersCommand(args) {
812
839
  return exitFromStatus(res.statusCode);
813
840
  }
814
841
 
842
+ export async function runWorkstreamCreateCommand(args) {
843
+ const env = requireAgentEnv();
844
+ const name = getArg(args, 'name');
845
+ if (!name) { console.error('--name is required'); return 2; }
846
+ const description = getArg(args, 'description');
847
+ const memberArgs = args.member;
848
+ const memberAgentIds = Array.isArray(memberArgs)
849
+ ? memberArgs
850
+ : memberArgs ? [memberArgs] : [];
851
+ // Optional charter from stdin when --charter is supplied with no value
852
+ let charter = null;
853
+ if (args.charter === true) {
854
+ charter = await readStdin();
855
+ } else if (typeof args.charter === 'string') {
856
+ charter = args.charter;
857
+ }
858
+ const res = await daemonRequest({
859
+ method: 'POST',
860
+ path: '/agent/workstream/create',
861
+ headers: commonHeaders(env),
862
+ body: {
863
+ name,
864
+ description,
865
+ charter,
866
+ member_agent_ids: memberAgentIds.map((s) => String(s).trim()).filter(Boolean),
867
+ },
868
+ });
869
+ printJson(res.body);
870
+ return exitFromStatus(res.statusCode);
871
+ }
872
+
873
+ export async function runWorkstreamDeleteCommand(args) {
874
+ const env = requireAgentEnv();
875
+ const target = getArg(args, 'target');
876
+ const conversationId = getArg(args, 'conversation-id');
877
+ if (!target && !conversationId) {
878
+ console.error('--target or --conversation-id is required');
879
+ return 2;
880
+ }
881
+ const res = await daemonRequest({
882
+ method: 'POST',
883
+ path: '/agent/workstream/delete',
884
+ headers: commonHeaders(env),
885
+ body: { target, conversation_id: conversationId },
886
+ });
887
+ printJson(res.body);
888
+ return exitFromStatus(res.statusCode);
889
+ }
890
+
891
+ export async function runWorkstreamListCommand(args) {
892
+ const env = requireAgentEnv();
893
+ const res = await daemonRequest({
894
+ method: 'GET',
895
+ path: '/agent/workstream/list',
896
+ headers: commonHeaders(env),
897
+ });
898
+ printJson(res.body);
899
+ return exitFromStatus(res.statusCode);
900
+ }
901
+
902
+ export async function runAgentListCommand(args) {
903
+ const env = requireAgentEnv();
904
+ const res = await daemonRequest({
905
+ method: 'GET',
906
+ path: '/agent/agent/list',
907
+ headers: commonHeaders(env),
908
+ });
909
+ printJson(res.body);
910
+ return exitFromStatus(res.statusCode);
911
+ }
912
+
913
+ export async function runAgentCreateCommand(args) {
914
+ const env = requireAgentEnv();
915
+ const name = getArg(args, 'name');
916
+ const runtime = getArg(args, 'runtime');
917
+ if (!name) { console.error('--name is required'); return 2; }
918
+ if (!runtime) { console.error('--runtime is required'); return 2; }
919
+ const res = await daemonRequest({
920
+ method: 'POST',
921
+ path: '/agent/agent/create',
922
+ headers: commonHeaders(env),
923
+ body: {
924
+ name,
925
+ runtime,
926
+ description: getArg(args, 'description'),
927
+ display_name: getArg(args, 'display-name'),
928
+ model: getArg(args, 'model'),
929
+ },
930
+ });
931
+ printJson(res.body);
932
+ return exitFromStatus(res.statusCode);
933
+ }
934
+
935
+ export async function runAgentDeleteCommand(args) {
936
+ const env = requireAgentEnv();
937
+ const agentId = getArg(args, 'agent-id');
938
+ if (!agentId) { console.error('--agent-id is required'); return 2; }
939
+ const res = await daemonRequest({
940
+ method: 'POST',
941
+ path: '/agent/agent/delete',
942
+ headers: commonHeaders(env),
943
+ body: { agent_id: agentId },
944
+ });
945
+ printJson(res.body);
946
+ return exitFromStatus(res.statusCode);
947
+ }
948
+
949
+ function readJsonFromFileOrInline(value) {
950
+ if (!value) return null;
951
+ // Accept either a path to a .json file or a raw JSON string.
952
+ try {
953
+ return JSON.parse(value);
954
+ } catch {}
955
+ try {
956
+ const fs = require('node:fs');
957
+ const txt = fs.readFileSync(value, 'utf8');
958
+ return JSON.parse(txt);
959
+ } catch (err) {
960
+ throw new Error(`could not parse JSON from ${value}: ${err?.message || err}`);
961
+ }
962
+ }
963
+
964
+ export async function runServiceCreateCommand(args) {
965
+ const env = requireAgentEnv();
966
+ const name = getArg(args, 'name');
967
+ if (!name) { console.error('--name is required'); return 2; }
968
+ const description = getArg(args, 'description');
969
+ let contractSchema = null;
970
+ let endpointConfig = null;
971
+ try {
972
+ const contractRaw = getArg(args, 'contract');
973
+ if (contractRaw) contractSchema = readJsonFromFileOrInline(contractRaw);
974
+ const endpointRaw = getArg(args, 'endpoint');
975
+ if (endpointRaw) endpointConfig = readJsonFromFileOrInline(endpointRaw);
976
+ } catch (err) {
977
+ console.error(err.message);
978
+ return 2;
979
+ }
980
+ if (!endpointConfig) { console.error('--endpoint <path-or-json> is required'); return 2; }
981
+ const res = await daemonRequest({
982
+ method: 'POST', path: '/agent/service/create',
983
+ headers: commonHeaders(env),
984
+ body: { name, description, contract_schema: contractSchema, endpoint_config: endpointConfig },
985
+ });
986
+ printJson(res.body);
987
+ return exitFromStatus(res.statusCode);
988
+ }
989
+
990
+ export async function runServiceUpdateCommand(args) {
991
+ const env = requireAgentEnv();
992
+ const serviceId = getArg(args, 'service-id');
993
+ if (!serviceId) { console.error('--service-id is required'); return 2; }
994
+ const body = { service_id: serviceId };
995
+ if (getArg(args, 'description')) body.description = getArg(args, 'description');
996
+ if (getArg(args, 'contract')) body.contract_schema = readJsonFromFileOrInline(getArg(args, 'contract'));
997
+ if (getArg(args, 'endpoint')) body.endpoint_config = readJsonFromFileOrInline(getArg(args, 'endpoint'));
998
+ if (getArg(args, 'status')) body.status = getArg(args, 'status');
999
+ const res = await daemonRequest({
1000
+ method: 'POST', path: '/agent/service/update',
1001
+ headers: commonHeaders(env), body,
1002
+ });
1003
+ printJson(res.body);
1004
+ return exitFromStatus(res.statusCode);
1005
+ }
1006
+
1007
+ export async function runServiceDeleteCommand(args) {
1008
+ const env = requireAgentEnv();
1009
+ const serviceId = getArg(args, 'service-id');
1010
+ if (!serviceId) { console.error('--service-id is required'); return 2; }
1011
+ const res = await daemonRequest({
1012
+ method: 'POST', path: '/agent/service/delete',
1013
+ headers: commonHeaders(env),
1014
+ body: { service_id: serviceId },
1015
+ });
1016
+ printJson(res.body);
1017
+ return exitFromStatus(res.statusCode);
1018
+ }
1019
+
1020
+ export async function runServiceListCommand(args) {
1021
+ const env = requireAgentEnv();
1022
+ const res = await daemonRequest({
1023
+ method: 'GET', path: '/agent/service/list',
1024
+ headers: commonHeaders(env),
1025
+ });
1026
+ printJson(res.body);
1027
+ return exitFromStatus(res.statusCode);
1028
+ }
1029
+
1030
+ export async function runServiceInfoCommand(args) {
1031
+ const env = requireAgentEnv();
1032
+ const name = getArg(args, 'name');
1033
+ if (!name) { console.error('--name is required'); return 2; }
1034
+ const params = new URLSearchParams();
1035
+ params.set('name', name);
1036
+ const res = await daemonRequest({
1037
+ method: 'GET', path: `/agent/service/info?${params}`,
1038
+ headers: commonHeaders(env),
1039
+ });
1040
+ printJson(res.body);
1041
+ return exitFromStatus(res.statusCode);
1042
+ }
1043
+
1044
+ export async function runServiceCallCommand(args) {
1045
+ const env = requireAgentEnv();
1046
+ const name = getArg(args, 'name');
1047
+ if (!name) { console.error('--name is required'); return 2; }
1048
+ const inputText = await readStdin();
1049
+ let input = null;
1050
+ if (inputText && inputText.trim().length > 0) {
1051
+ try {
1052
+ input = JSON.parse(inputText);
1053
+ } catch (err) {
1054
+ console.error('stdin must be JSON for service call input');
1055
+ return 2;
1056
+ }
1057
+ }
1058
+ const res = await daemonRequest({
1059
+ method: 'POST', path: '/agent/service/call',
1060
+ headers: commonHeaders(env),
1061
+ body: { name, input },
1062
+ });
1063
+ printJson(res.body);
1064
+ return exitFromStatus(res.statusCode);
1065
+ }
1066
+
1067
+ export async function runBriefingGetCommand(args) {
1068
+ const env = requireAgentEnv();
1069
+ const id = args._[2] || getArg(args, 'id');
1070
+ if (!id) { console.error('briefing id required (positional or --id)'); return 2; }
1071
+ const params = new URLSearchParams();
1072
+ params.set('id', id);
1073
+ const res = await daemonRequest({
1074
+ method: 'GET',
1075
+ path: `/agent/briefing/get?${params}`,
1076
+ headers: commonHeaders(env),
1077
+ });
1078
+ printJson(res.body);
1079
+ return exitFromStatus(res.statusCode);
1080
+ }
1081
+
1082
+ export async function runBriefingPublishCommand(args) {
1083
+ const env = requireAgentEnv();
1084
+ const textArg = getArg(args, 'text');
1085
+ const attachPath = getArg(args, 'attach');
1086
+ const mode = String(getArg(args, 'mode') || 'info').trim().toLowerCase();
1087
+ if (!textArg) {
1088
+ console.error('--text "<short>" is required');
1089
+ return 2;
1090
+ }
1091
+ if (textArg.length > 140) {
1092
+ console.error('--text must be ≤140 chars');
1093
+ return 2;
1094
+ }
1095
+ if (!['info', 'approval'].includes(mode)) {
1096
+ console.error('--mode must be info or approval');
1097
+ return 2;
1098
+ }
1099
+ let attachmentAssetId = null;
1100
+ if (attachPath) {
1101
+ const contentType = inferContentType(String(attachPath));
1102
+ if (!contentType.startsWith('image/') && !contentType.startsWith('video/') && contentType !== 'text/html') {
1103
+ console.error('--attach must be an image, video, or HTML file');
1104
+ return 2;
1105
+ }
1106
+ const upload = await uploadFileViaDaemon(env, String(attachPath));
1107
+ if (!upload.ok) {
1108
+ console.error(`attachment upload failed for ${attachPath}: ${upload.error}`);
1109
+ return 1;
1110
+ }
1111
+ attachmentAssetId = upload.assetId;
1112
+ }
1113
+ const res = await daemonRequest({
1114
+ method: 'POST',
1115
+ path: '/agent/briefing/publish',
1116
+ headers: commonHeaders(env),
1117
+ body: {
1118
+ body_text: textArg,
1119
+ attachment_asset_id: attachmentAssetId,
1120
+ current_conversation_id: env.currentConversationId,
1121
+ response_mode: mode,
1122
+ },
1123
+ });
1124
+ printJson(res.body);
1125
+ return exitFromStatus(res.statusCode);
1126
+ }
1127
+
1128
+ export async function runCredentialRequestCommand(args) {
1129
+ const env = requireAgentEnv();
1130
+ const name = getArg(args, 'name');
1131
+ if (!name) { console.error('--name is required (e.g. GEMINI_API_KEY)'); return 2; }
1132
+ const description = getArg(args, 'description');
1133
+ const groupTarget = getArg(args, 'group') || getArg(args, 'workstream');
1134
+ const res = await daemonRequest({
1135
+ method: 'POST',
1136
+ path: '/agent/credential/request',
1137
+ headers: commonHeaders(env),
1138
+ body: {
1139
+ name,
1140
+ description,
1141
+ target: groupTarget,
1142
+ },
1143
+ });
1144
+ printJson(res.body);
1145
+ return exitFromStatus(res.statusCode);
1146
+ }
1147
+
1148
+ export async function runDashboardSetCommand(args) {
1149
+ const env = requireAgentEnv();
1150
+ const target = getArg(args, 'target');
1151
+ const conversationId = getArg(args, 'conversation-id');
1152
+ if (!target && !conversationId) {
1153
+ console.error('--target or --conversation-id is required');
1154
+ return 2;
1155
+ }
1156
+ // Body is read from stdin as JSON: { data_json?, html_template? }
1157
+ const stdinText = await readStdin();
1158
+ let payload = {};
1159
+ if (stdinText && stdinText.trim().length > 0) {
1160
+ try {
1161
+ payload = JSON.parse(stdinText);
1162
+ } catch (err) {
1163
+ console.error('stdin must be JSON: { data_json?, html_template? }');
1164
+ return 2;
1165
+ }
1166
+ }
1167
+ const res = await daemonRequest({
1168
+ method: 'POST',
1169
+ path: '/agent/dashboard/set',
1170
+ headers: commonHeaders(env),
1171
+ body: {
1172
+ target,
1173
+ conversation_id: conversationId,
1174
+ ...(('data_json' in payload) ? { data_json: payload.data_json } : {}),
1175
+ ...(('html_template' in payload) ? { html_template: payload.html_template } : {}),
1176
+ },
1177
+ });
1178
+ printJson(res.body);
1179
+ return exitFromStatus(res.statusCode);
1180
+ }
1181
+
1182
+ export async function runDashboardGetCommand(args) {
1183
+ const env = requireAgentEnv();
1184
+ const target = getArg(args, 'target');
1185
+ const conversationId = getArg(args, 'conversation-id');
1186
+ if (!target && !conversationId) {
1187
+ console.error('--target or --conversation-id is required');
1188
+ return 2;
1189
+ }
1190
+ const params = new URLSearchParams();
1191
+ if (target) params.set('target', target);
1192
+ if (conversationId) params.set('conversation_id', conversationId);
1193
+ const res = await daemonRequest({
1194
+ method: 'GET',
1195
+ path: `/agent/dashboard/get?${params}`,
1196
+ headers: commonHeaders(env),
1197
+ });
1198
+ printJson(res.body);
1199
+ return exitFromStatus(res.statusCode);
1200
+ }
1201
+
1202
+ export async function runWorkstreamCharterGetCommand(args) {
1203
+ const env = requireAgentEnv();
1204
+ const target = getArg(args, 'target');
1205
+ const conversationId = getArg(args, 'conversation-id') || env.currentConversationId;
1206
+ if (!target && !conversationId) {
1207
+ console.error('--target or --conversation-id is required outside a delivered conversation');
1208
+ return 2;
1209
+ }
1210
+ const params = new URLSearchParams();
1211
+ if (target) params.set('target', target);
1212
+ if (conversationId) params.set('conversation_id', conversationId);
1213
+ const res = await daemonRequest({
1214
+ method: 'GET',
1215
+ path: `/agent/workstream/charter/get?${params}`,
1216
+ headers: commonHeaders(env),
1217
+ });
1218
+ printJson(res.body);
1219
+ return exitFromStatus(res.statusCode);
1220
+ }
1221
+
1222
+ export async function runWorkstreamCharterSetCommand(args) {
1223
+ const env = requireAgentEnv();
1224
+ const target = getArg(args, 'target');
1225
+ const conversationId = getArg(args, 'conversation-id') || env.currentConversationId;
1226
+ if (!target && !conversationId) {
1227
+ console.error('--target or --conversation-id is required outside a delivered conversation');
1228
+ return 2;
1229
+ }
1230
+ // Body from stdin (heredoc / pipe). Empty input clears the charter.
1231
+ const charter = await readStdin();
1232
+ const res = await daemonRequest({
1233
+ method: 'POST',
1234
+ path: '/agent/workstream/charter/set',
1235
+ headers: commonHeaders(env),
1236
+ body: {
1237
+ target,
1238
+ conversation_id: conversationId,
1239
+ charter: charter ?? '',
1240
+ },
1241
+ });
1242
+ printJson(res.body);
1243
+ return exitFromStatus(res.statusCode);
1244
+ }
1245
+
815
1246
  export async function runServerInfoCommand(args) {
816
1247
  const env = requireAgentEnv();
817
1248
  const params = new URLSearchParams();
@@ -827,12 +1258,18 @@ export async function runServerInfoCommand(args) {
827
1258
 
828
1259
  export const AGENT_COMMAND_HELP = {
829
1260
  message: `ticlawk message <send|read|check|search|react>
830
- ticlawk message send --target "<target>" [--seen-up-to-seq N] [--reply-to <msg-id>] [--attach <file> ...]
1261
+ ticlawk message send --target "<target>" [--phase progress|final] [--seen-up-to-seq N] [--reply-to <msg-id>] [--allow-cross-target] [--attach <file> ...] [--kind <kind>]
831
1262
  Body is read from stdin (use <<'EOF' ... EOF for multiline).
1263
+ During a delivered turn, sending to a different conversation is blocked
1264
+ unless --allow-cross-target is passed deliberately.
1265
+ --phase progress|final tags whether this is an intermediate update or
1266
+ the final visible response after the current work/goal check is done.
1267
+ --kind <kind> tags this message via metadata.kind.
832
1268
  Targets:
833
- dm:@<user> private message
834
- #<group> group conversation
835
- #<group>:<msgid> thread under a top-level message in that group
1269
+ dm:<conversation-id> private message by conversation id
1270
+ dm:@<user> private message by user/agent handle
1271
+ #<group> group conversation by name or id
1272
+ #<group>:<msgid> replies under a top-level message in that group
836
1273
  --attach <file> uploads a local file and attaches it to the message
837
1274
  (repeatable; max 10 attachments per message).
838
1275
  ticlawk message read --target "<target>" [--around <msg-id>] [--before-seq N] [--limit N]
@@ -844,16 +1281,17 @@ export const AGENT_COMMAND_HELP = {
844
1281
  Use sparingly: prefer acknowledgement/follow-up signals like 👀. Do not
845
1282
  auto-react to every merge, deploy, or task completion with celebratory emoji.
846
1283
  `,
847
- profile: `ticlawk profile <show|update>
1284
+ profile: `ticlawk profile <list|current|use|show|update>
1285
+ ticlawk profile list
1286
+ ticlawk profile current
1287
+ ticlawk profile use ticlawk:<user-id>
1288
+
848
1289
  ticlawk profile show [@handle | --id <agent-id>]
849
1290
  ticlawk profile update [--display-name X] [--description Y] [--avatar-file path]
850
1291
  `,
851
- attachment: `ticlawk attachment <upload|view>
852
- ticlawk attachment upload <file>
853
- Upload-only the user is NOT notified. Almost always you want
854
- \`ticlawk message send --attach <file>\` instead, which uploads AND
855
- surfaces the file to the user inside a chat message.
856
- ticlawk attachment view <asset-id> [--out <path>]
1292
+ attachment: `ticlawk attachment view <asset-id> [--out <path>]
1293
+ Fetch metadata + signed URL for an existing asset. To send a file
1294
+ to a user, use \`ticlawk message send --attach <file>\` instead.
857
1295
  `,
858
1296
  reminder: `ticlawk reminder <schedule|list|snooze|update|cancel|log>
859
1297
  ticlawk reminder schedule --title <t> (--fire-at <iso> | --in-seconds N | --in-minutes N) (--target "<target>" | --anchor-conversation-id <id>) [--anchor-message-id <id>]
@@ -868,23 +1306,109 @@ export const AGENT_COMMAND_HELP = {
868
1306
  conversation and waking the owner agent via an explicit delivery.
869
1307
  `,
870
1308
  task: `ticlawk task <create|claim|unclaim|update|list>
871
- ticlawk task create --target "<target>" [--title <t>]
872
- Body is read from stdin. Creates a brand-new message published as a todo
873
- task. To own it, follow up with \`ticlawk task claim --message-id <id>\`.
1309
+ ticlawk task create --target "<target>" [--title <t>] [--assign-agent <agent-id>]
1310
+ Body is read from stdin. Creates a brand-new group task. Group admins
1311
+ may assign it immediately to an agent member with --assign-agent.
874
1312
  ticlawk task claim --message-id <id> [--lease-seconds N]
875
1313
  ticlawk task claim --number <N> --target "<target>" [--lease-seconds N]
876
1314
  ticlawk task unclaim --task-id <id>
877
1315
  ticlawk task update --task-id <id> --status <todo|in_progress|in_review|done|canceled>
878
- ticlawk task list [--target <target>]
1316
+ Only a group admin can set status=done. Other agents should set
1317
+ in_review and let the group's admin finalize. A group admin can also
1318
+ reopen another agent's in_review task to in_progress for redo.
1319
+ ticlawk task list [--target <target>|--conversation-id <id>]
1320
+ Default view is open tasks plus tasks owned by the caller. When the
1321
+ caller is a group admin and --target/--conversation-id scopes the
1322
+ request to that group, the list shows the full task board, including
1323
+ other agents' in_progress, in_review, and done tasks.
1324
+ `,
1325
+ workstream: `ticlawk workstream <create|delete|list|charter>
1326
+ Compatibility alias for group admin commands. Prefer \`ticlawk group ...\`
1327
+ for groups and \`ticlawk charter ...\` for conversation charters.
1328
+ `,
1329
+ charter: `ticlawk charter <get|set> [--target "<target>" | --conversation-id <id>]
1330
+ ticlawk charter get [--target "<target>" | --conversation-id <id>]
1331
+ Print the conversation charter. DM/group members can read.
1332
+ ticlawk charter set [--target "<target>" | --conversation-id <id>] # body via stdin
1333
+ Replace the conversation charter. DM member agents can write their DM
1334
+ charter; group writes require group admin/owner. Empty stdin clears it.
1335
+ During a delivered turn, omitting target/conversation-id uses the
1336
+ current conversation.
1337
+ `,
1338
+ service: `ticlawk service <create|update|delete|list|info|call>
1339
+ service create --name X --endpoint <path-or-json> [--description Y] [--contract <path-or-json>]
1340
+ Any agent. Register a callable service. --endpoint takes either a
1341
+ .json file path or a JSON string; same for --contract.
1342
+ endpoint_config shape: { url, method?, headers? }.
1343
+ service update --service-id <id> [--description Y] [--contract <json>] [--endpoint <json>] [--status <active|down|archived>]
1344
+ Any agent.
1345
+ service delete --service-id <id>
1346
+ Any agent. Soft-archives.
1347
+ service list
1348
+ Any agent. Returns active services (no endpoint_config).
1349
+ service info --name X
1350
+ Any agent. Returns contract_schema + description.
1351
+ service call --name X # input from stdin (JSON)
1352
+ Any agent. Backend proxies to endpoint_config.url. No retry.
1353
+ `,
1354
+ briefing: `ticlawk briefing <publish|get>
1355
+ briefing publish --text "..." [--mode info|approval] [--attach <image|video|html path>]
1356
+ Publish a briefing to the owner's Briefings. Allowed from DMs, or
1357
+ from groups where this agent is admin/owner.
1358
+ --text short plain text (≤140 chars)
1359
+ --mode owner response mode: info shows ack; approval shows approve
1360
+ --attach optional image, video, or HTML file when visual context matters
1361
+ Briefings are independent of chat — they do NOT appear in any DM
1362
+ message stream. Use this verb (not \`message send\`) for any status
1363
+ surface the owner consumes in Briefings.
1364
+ briefing get <id>
1365
+ Fetch a briefing including text and attachment metadata. Use this when a
1366
+ quote (metadata.quote.kind=briefing) points at a briefing whose
1367
+ full body you want to read.
1368
+ `,
1369
+ credential: `ticlawk credential request --name <ENV_VAR> [--description Y] [--group "#<group>"]
1370
+ Any agent. Pre-allocate a credential slot. Response includes a deep
1371
+ link the user opens in the mobile app (Settings → Credentials)
1372
+ to fill the value. Once filled, the daemon syncs it locally and
1373
+ injects it as an env var when spawning agents.
1374
+ `,
1375
+ dashboard: `ticlawk dashboard <set|get> (--target "<target>" | --conversation-id <id>)
1376
+ dashboard set (--target "<target>" | --conversation-id <id>) # body via stdin (JSON)
1377
+ Allowed from DMs, or from groups where this agent is admin/owner.
1378
+ stdin = { "data_json": ..., "html_template": "..." }.
1379
+ Either field may be omitted (leaves unchanged) or null (clears).
1380
+ dashboard get (--target "<target>" | --conversation-id <id>)
1381
+ Conversation members can read.
1382
+ `,
1383
+ agent: `ticlawk agent <list|create|delete>
1384
+ agent list
1385
+ Any agent. List all non-archived agents owned by the user, including
1386
+ descriptions, runtime/status, and group memberships.
1387
+ agent create --name X --runtime <claude_code|codex|opencode|openclaw|pi> [--description Y] [--display-name N] [--model M]
1388
+ Any agent. Pre-allocate an agent slot (status='unpaired'). User
1389
+ later pairs a runtime to fill it.
1390
+ agent delete --agent-id <id>
1391
+ Owner-only. Agent-facing deletion is disabled.
879
1392
  `,
880
- group: `ticlawk group <create|members>
1393
+ group: `ticlawk group <create|list|delete|charter|members>
881
1394
  ticlawk group create --name <n> [--description <d>] [--member <agent-id> ...]
882
- Agent self-creates a new group. The agent is added as a member; the
1395
+ Agent self-creates a new group. The agent is added as admin; the
883
1396
  conversation owner is set to the user that owns this agent. Other
884
- member agents must belong to the same user (RLS enforces).
885
- ticlawk group members --target "<target>"
886
- ticlawk group members --target "<target>" --add <agent-id> [--add <agent-id> ...]
887
- ticlawk group members --target "<target>" --remove <agent-id>
1397
+ member agents join as regular members and must belong to the same user
1398
+ (RLS enforces).
1399
+ ticlawk group list
1400
+ List group conversations the caller belongs to, including agent members,
1401
+ descriptions, charters, and dashboard presence.
1402
+ ticlawk group delete --target "#<group>"
1403
+ Group-admin only. Hard-delete the group (messages, members, deliveries).
1404
+ ticlawk group charter get (--target "#<group>" | --conversation-id <id>)
1405
+ Compatibility alias for \`ticlawk charter get\` on groups.
1406
+ ticlawk group charter set (--target "#<group>" | --conversation-id <id>) # body via stdin
1407
+ Compatibility alias for \`ticlawk charter set\` on groups. Group-admin only.
1408
+ ticlawk group members (--target "<target>" | --conversation-id <id>)
1409
+ ticlawk group members (--target "<target>" | --conversation-id <id>) --add <agent-id> [--add <agent-id> ...]
1410
+ ticlawk group members (--target "<target>" | --conversation-id <id>) --remove <agent-id>
1411
+ Listing requires membership; add/remove requires group admin.
888
1412
  `,
889
1413
  server: `ticlawk server info [--refresh]
890
1414
  `,