ticlawk 0.1.17-dev.1 → 0.1.17-dev.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticlawk",
3
- "version": "0.1.17-dev.1",
3
+ "version": "0.1.17-dev.10",
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",
@@ -375,7 +375,7 @@ export async function getAgentServerInfo({ actingAgentId }) {
375
375
  // ── Reminders ──
376
376
 
377
377
  export async function scheduleAgentReminder({
378
- actingAgentId, title, fireAt, anchorConversationId, anchorMessageId,
378
+ actingAgentId, title, fireAt, anchorConversationId, anchorMessageId, recurrence,
379
379
  }) {
380
380
  return apiFetch('/api/agent/reminders', {
381
381
  method: 'POST',
@@ -385,6 +385,7 @@ export async function scheduleAgentReminder({
385
385
  fire_at: fireAt,
386
386
  anchor_conversation_id: anchorConversationId,
387
387
  anchor_message_id: anchorMessageId ?? null,
388
+ recurrence: recurrence ?? null,
388
389
  }),
389
390
  });
390
391
  }
@@ -557,6 +557,18 @@ export async function runReminderScheduleCommand(args) {
557
557
  console.error('--target or --anchor-conversation-id is required');
558
558
  return 2;
559
559
  }
560
+ // Optional recurrence: --recur-at HH:MM (owner-local) [--recur-weekday 1,2,3].
561
+ // The timezone is system-owned (the owner's), filled in by the backend — the
562
+ // agent never passes it. The backend also computes the first fire_at in it.
563
+ let recurrence = null;
564
+ const recurAt = getArg(args, 'recur-at');
565
+ if (recurAt) {
566
+ const wdRaw = getArg(args, 'recur-weekday');
567
+ const byWeekday = wdRaw
568
+ ? String(wdRaw).split(',').map((s) => parseInt(s.trim(), 10)).filter((n) => n >= 1 && n <= 7)
569
+ : [];
570
+ recurrence = { at: recurAt, ...(byWeekday.length ? { by_weekday: byWeekday } : {}) };
571
+ }
560
572
  const res = await daemonRequest({
561
573
  method: 'POST',
562
574
  path: '/agent/reminder/schedule',
@@ -567,6 +579,7 @@ export async function runReminderScheduleCommand(args) {
567
579
  target,
568
580
  anchor_conversation_id: anchorConversationId,
569
581
  anchor_message_id: anchorMessageId,
582
+ recurrence,
570
583
  },
571
584
  });
572
585
  printJson(res.body);
@@ -1425,7 +1438,7 @@ export const AGENT_COMMAND_HELP = {
1425
1438
  to a user, use \`ticlawk message send --attach <file>\` instead.
1426
1439
  `,
1427
1440
  reminder: `ticlawk reminder <schedule|list|snooze|update|cancel|log>
1428
- ticlawk reminder schedule --title <t> (--fire-at <iso> | --in-seconds N | --in-minutes N) (--target "<target>" | --anchor-conversation-id <id>) [--anchor-message-id <id>]
1441
+ ticlawk reminder schedule --title <t> (--fire-at <iso> | --in-seconds N | --in-minutes N) (--target "<target>" | --anchor-conversation-id <id>) [--anchor-message-id <id>] [--recur-at HH:MM [--recur-tz <IANA>] [--recur-weekday 1,2,3]]
1429
1442
  ticlawk reminder list [--status active|fired|canceled]
1430
1443
  ticlawk reminder snooze <reminder-id> (--fire-at <iso> | --in-seconds N | --in-minutes N)
1431
1444
  ticlawk reminder update <reminder-id> [--title <t>] [--fire-at <iso>]
@@ -1435,6 +1448,17 @@ export const AGENT_COMMAND_HELP = {
1435
1448
  Use reminders for follow-up that depends on future state you cannot resolve
1436
1449
  now. A reminder fires by posting a system message into the anchor
1437
1450
  conversation and waking the owner agent via an explicit delivery.
1451
+
1452
+ RECURRING: for a fixed cadence (e.g. a daily/weekly meal-time check-in), use
1453
+ ONE recurring reminder instead of enumerating many one-shots. Pass --recur-at
1454
+ (the owner's local HH:MM) and optionally --recur-weekday (ISO 1=Mon..7=Sun,
1455
+ comma-separated; omit for every day). You do NOT set a timezone — the backend
1456
+ fills the owner's timezone and computes the fire time in it. On each fire the
1457
+ reminder auto-advances to the next occurrence and stays active. Example —
1458
+ weekday 18:30 dinner check-in (--fire-at is ignored for recurring, the backend
1459
+ computes it):
1460
+ ticlawk reminder schedule --title "晚餐" --anchor-conversation-id <id> \\
1461
+ --in-minutes 1 --recur-at 18:30 --recur-weekday 1,2,3,4,5
1438
1462
  `,
1439
1463
  task: `ticlawk task <create|claim|unclaim|update|list>
1440
1464
  ticlawk task create --target "<target>" [--title <t>] [--assign-agent <agent-id>]
@@ -590,6 +590,7 @@ export async function handleReminderSchedule(req, body, ctx) {
590
590
  fireAt: body.fire_at,
591
591
  anchorConversationId,
592
592
  anchorMessageId: body?.anchor_message_id || null,
593
+ recurrence: body?.recurrence ?? null,
593
594
  });
594
595
  debugLog('agent-cli', 'reminder.schedule', {
595
596
  actingAgentId,
@@ -15,17 +15,18 @@ import { buildEnvelopeTarget, buildCharterBlock } from './wake-prompt.mjs';
15
15
  const STEP_GUIDES = {
16
16
  gap_analysis: {
17
17
  title: 'GAP ANALYSIS',
18
- body: `Compare the current state of the work against the goal and success criteria. Read whatever you need (charter, dashboard, task board, repo, prior messages) to judge where things actually stand.
19
- - If there is concrete executable work toward the goal, first make sure the next unit exists as a task (create/assign it with \`ticlawk task ...\` when a task fits), then report outcome=gap.
20
- - If the goal/milestone is fully met with no meaningful gap, report outcome=no_gap.
21
- - If closing the gap depends on a future or external state that nobody can act on right now, schedule a reminder for the resume condition, then report outcome=wait.`,
18
+ body: `Compare the current state against the goal and success criteria. The [goal_context] block above gives you the open tasks, active reminders, and dashboard state — judge from it; read the charter/repo/prior messages only for what it doesn't cover. The dashboard is the owner's at-a-glance visualization of how far this goal has progressed — this step owns keeping it true to reality: create it if a durable goal has none, refresh it when progress moved materially (\`ticlawk dashboard set\`; see SURFACES.md).
19
+ - Judge "due now" against the current owner-local time above. Produce only what is due now; do NOT pre-produce future occurrences (a later meal, tomorrow's item) each one is produced when its own reminder fires and wakes you at that time.
20
+ - If there is concrete work to do NOW, make sure the next unit exists as a task (\`ticlawk task ...\`), then report outcome=gap.
21
+ - If nothing needs doing this instant but the goal is ONGOING/STANDING its job is to keep something maintained and work recurs (e.g. an active recurring reminder above already covers the next occurrence) — report outcome=wait. Do NOT report no_gap for a standing goal: it has no "done", and parking on no_gap would stop it from waking at the next occurrence. If nothing is scheduled to resume it yet, schedule a reminder first, then report wait.
22
+ - Report outcome=no_gap ONLY if the goal is genuinely, permanently met and will never need action again (an achievement goal that is finished). The completed result is something the owner is waiting on — surface it per the briefing rule below.`,
22
23
  outcomes: ['gap', 'no_gap', 'wait'],
23
24
  },
24
25
  execute: {
25
26
  title: 'EXECUTE',
26
- body: `Do the next concrete unit of work toward the goal (or drive the current task to completion). Send interim updates with \`ticlawk message send --phase progress\` as you go.
27
+ body: `Do the next concrete unit of work toward the goal (or drive the current task to completion). Send routine interim progress with \`ticlawk message send --phase progress\`; if an update clears the briefing bar below (e.g. it is a result the owner explicitly asked to be told about), surface it as a briefing instead.
27
28
  - When the unit of work is finished and ready to be checked, report outcome=task_completed.
28
- - If you cannot proceed without an owner approval, decision, or permission, park ONE canonical approval with \`ticlawk approval request --title "<what you need approved>" [--detail "<context>"]\`, tell the owner what you need and why with \`ticlawk message send\`, then report outcome=needs_approval. The owner's approval (button or a natural-language reply) resumes you automatically.
29
+ - If you cannot proceed without an owner approval, decision, or permission, park ONE canonical approval with \`ticlawk approval request --title "<what you need approved>" [--detail "<context>"]\`, tell the owner what you need and why, then report outcome=needs_approval. The owner's approval (button or a natural-language reply) resumes you automatically.
29
30
  - If you are blocked by something else (missing input, external failure, a needed resource), explain it to the owner, then report outcome=blocked.`,
30
31
  outcomes: ['task_completed', 'needs_approval', 'blocked'],
31
32
  },
@@ -48,6 +49,34 @@ function readPayload(msg) {
48
49
  };
49
50
  }
50
51
 
52
+ // Per-step context: the claim attaches msg.goal_context (open tasks, active
53
+ // reminders, current task, dashboard) for transition deliveries. Render the
54
+ // slice THIS step needs so each step decides on facts, not guesses.
55
+ function buildGoalContextBlock(msg, step) {
56
+ const gc = msg && msg.goal_context && typeof msg.goal_context === 'object' ? msg.goal_context : null;
57
+ if (!gc) return '';
58
+ const lines = ['[goal_context] Current state for this goal (given to you — use it, do not re-derive):'];
59
+ if (gc.now_local) {
60
+ lines.push(`- current time (owner local): ${gc.now_local}${gc.timezone ? ` [${gc.timezone}]` : ''}`);
61
+ }
62
+ if (step === 'gap_analysis' || !STEP_GUIDES[step]) {
63
+ const tasks = Array.isArray(gc.open_tasks) ? gc.open_tasks : [];
64
+ const rems = Array.isArray(gc.active_reminders) ? gc.active_reminders : [];
65
+ lines.push(`- open tasks (${tasks.length}): ${tasks.length
66
+ ? tasks.map((t) => `#${t.number} ${t.title} [${t.status}]`).join('; ')
67
+ : 'none'}`);
68
+ lines.push(`- active reminders (${rems.length}): ${rems.length
69
+ ? rems.map((r) => `"${r.title}" @ ${r.fire_at}${r.recurrence ? ' (recurring)' : ''}`).join('; ')
70
+ : 'none'}`);
71
+ lines.push(`- dashboard: ${gc.dashboard ? 'exists' : 'none yet'}`);
72
+ } else {
73
+ const ct = gc.current_task;
74
+ lines.push(ct ? `- current task: #${ct.number} ${ct.title} [${ct.status}]` : '- current task: (none set)');
75
+ }
76
+ lines.push('[/goal_context]');
77
+ return lines.join('\n');
78
+ }
79
+
51
80
  function buildGoalStepHeader(msg, { step, transitionId, goalVersion, kind }) {
52
81
  const target = buildEnvelopeTarget(msg);
53
82
  const time = msg.created_at || new Date().toISOString();
@@ -66,6 +95,8 @@ export function buildGoalStepPrompt(msg) {
66
95
 
67
96
  const sections = [];
68
97
  if (charterBlock) sections.push(charterBlock);
98
+ const goalContextBlock = buildGoalContextBlock(msg, step);
99
+ if (goalContextBlock) sections.push(goalContextBlock);
69
100
 
70
101
  if (guide) {
71
102
  sections.push([
@@ -74,10 +105,16 @@ export function buildGoalStepPrompt(msg) {
74
105
  `Current step: ${guide.title}`,
75
106
  guide.body,
76
107
  ``,
108
+ `Briefing rule (independent of which step you are on): a briefing (\`ticlawk briefing publish\`) interrupts the owner — it is only for things worth their attention. Default to NOT sending one; routine progress belongs on the dashboard (the owner pulls it) or a chat \`ticlawk message send\`. Send a briefing only when one of these holds:`,
109
+ ` (a) the owner must act or decide — e.g. an approval you parked (\`--mode approval\`);`,
110
+ ` (b) the owner asked to be told about this — a standing request, a scheduled time, or a threshold they set (\`--mode info\`);`,
111
+ ` (c) something happened the owner would be wrong not to know now — goal done, blocked, materially off-track, or a result they are waiting on (\`--mode info\`).`,
112
+ `If you are unsure, it is NOT a briefing — put it on the dashboard. The dashboard is the always-current pull surface; briefings are scarce pushes — over-notifying trains the owner to ignore them.`,
113
+ ``,
77
114
  `When the step is done, advance the state machine by running EXACTLY ONE report:`,
78
115
  ` ${reportCmd}`,
79
116
  ``,
80
- `Reporting the outcome is what continues the loop: a running next state arrives as a fresh step, and the loop parks itself when there is no gap or it must wait. Send owner-facing updates with \`ticlawk message send --target ${target} --phase progress\` (use --phase final only when the loop reaches no_gap/wait/blocked-on-owner). Report exactly once; do not loop inside this single turn.`,
117
+ `Reporting the outcome is what continues the loop: a running next state arrives as a fresh step, and the loop parks itself when there is no gap or it must wait. Reach the owner only through Ticlawk surfaces — \`ticlawk message send --target ${target}\` (chat), \`ticlawk briefing publish\` (push, per the rule above), \`ticlawk dashboard set\` (goal report); see \`SURFACES.md\`. Any owner-facing text is for the owner in their language: say what changed, why it matters, and what (if anything) they must do; never expose internal task titles, file paths, step numbers, or harness tokens. Report exactly once; do not loop inside this single turn.`,
81
118
  `[/goal_step]`,
82
119
  ].join('\n'));
83
120
  } else {
@@ -39,3 +39,5 @@ Use `ticlawk credential request --name <ENV_VAR>` to create the credential slot.
39
39
  ## Reminders
40
40
 
41
41
  Use reminders only for external/time-based future follow-up or visible, persistent resume conditions. Do not use reminders to defer executable work or an owner decision that should be requested now.
42
+
43
+ For a fixed cadence (a daily or weekly check-in, e.g. meal-time reminders), use ONE recurring reminder, not many enumerated one-shots: `ticlawk reminder schedule ... --recur-at HH:MM [--recur-weekday 1,2,3]`. Give `--recur-at` as the owner's local wall-clock time; the timezone is filled in by the system, so you never pass it. It auto-advances to the next occurrence on each fire and stays active, so the cadence never runs out.
@@ -20,6 +20,24 @@ ${buildReadInstructions(ctx)}
20
20
  Read other local files only when the current work clearly needs them.`;
21
21
  }
22
22
 
23
+ // A goal-lane turn is a single backend FSM step, not a chat message. Its
24
+ // per-step instructions and exact CLI commands (`goal report`, `task`,
25
+ // `approval request`, `message send`) live in the goal-step (user) prompt
26
+ // built by goal-step-prompt.mjs. The system prompt here therefore stays
27
+ // minimal: identity + the one reply invariant. It deliberately omits the
28
+ // chat-lane role framing and handbook read-list, which would compete with
29
+ // the narrow step instruction and pull the model toward proactive
30
+ // assistant behavior. See system.md "Lane-aware standing prompt".
31
+ function buildGoalLaneStandingPrompt(_ctx = {}) {
32
+ return `You are executing one backend goal-loop step for this conversation, dispatched by the system — not a message from a person. Do only what this step requires; leave work for other steps to those steps.
33
+
34
+ Your normal output is private and reaches no one. The owner is reached only through Ticlawk surfaces: \`ticlawk message send\` (chat update), \`ticlawk briefing publish\` (active notification/decision), \`ticlawk dashboard set\` (goal-level report). The step tells you which one to use; \`SURFACES.md\` holds the rules for each. Read \`SURFACES.md\` or \`MEMORY.md\` only if the step needs them.`;
35
+ }
36
+
37
+ function isGoalLane(ctx = {}) {
38
+ return ctx?.inbound?.lane === 'goal' || readDeliveryReason(ctx) === 'transition';
39
+ }
40
+
23
41
  function getInboundRaw(ctx = {}) {
24
42
  return ctx?.inbound?.raw && typeof ctx.inbound.raw === 'object'
25
43
  ? ctx.inbound.raw
@@ -144,6 +162,7 @@ function unique(values) {
144
162
  }
145
163
 
146
164
  export function buildStandingPrompt(_ctx = {}) {
165
+ if (isGoalLane(_ctx)) return promptBlock(buildGoalLaneStandingPrompt(_ctx));
147
166
  return promptBlock(buildBaseStandingPrompt(_ctx));
148
167
  }
149
168
 
@@ -84,8 +84,23 @@ export function buildGroupContextBlock(msg) {
84
84
 
85
85
  export function buildCharterBlock(msg) {
86
86
  const charter = (msg.conversation_charter || '').trim();
87
- if (!charter) return '';
88
87
  const conversationId = msg.conversation_id || '';
88
+
89
+ // No charter yet: the goal loop has never been bootstrapped. A transition
90
+ // cannot arrive before a goal exists, and only the goal-authority agent may
91
+ // start one — so give that agent (and only it) the bootstrap path. Without
92
+ // this, a conversation's FIRST goal can never reach the goal lane, because
93
+ // the steady-state guidance below is gated on a charter already existing.
94
+ if (!charter) {
95
+ if (msg.reason === 'transition' || !hasGoalAuthority(msg)) return '';
96
+ return promptBlock(`
97
+ [conversation_goal]
98
+ This conversation has no goal charter yet, so the backend goal loop is not running.
99
+ If this message sets a goal for this conversation, capture it as the charter — the goal and what "done" looks like, in the owner's words — with \`ticlawk charter set --conversation ${conversationId}\` (body on stdin), then run \`ticlawk goal changed --conversation ${conversationId}\` to start the goal loop. Otherwise handle the message normally. See GOAL_AUTHORITY.md.
100
+ [/conversation_goal]
101
+ `);
102
+ }
103
+
89
104
  // The goal lane (transition deliveries) executes against the charter; its
90
105
  // per-step instructions come from the goal-step prompt, so here the charter
91
106
  // is just the goal/success spec. The chat lane must NOT run the loop — it