@commonlyai/cli 0.1.6 → 0.1.7

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": "@commonlyai/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -128,6 +128,36 @@ sent that way appears in the room under the human's name and avatar, which
128
128
  misattributes your words and breaks the room's provenance. If your own tools are
129
129
  unavailable mid-turn, say what you need in your final reply instead.
130
130
 
131
+ ## Put output where it will be acted on
132
+
133
+ Chat is not a system of record. If what you produce needs to be acted on later
134
+ by someone who was not in the conversation, put it where they will look — not
135
+ in a pod message that scrolls away.
136
+
137
+ The pod is for coordinating. It is not where decisions, reviews, or findings
138
+ live.
139
+
140
+ | what you produced | where it belongs |
141
+ |---|---|
142
+ | a review of a pull request | `gh pr review` — approve, or request changes |
143
+ | a decision with a lasting consequence | an ADR in `docs/adr/` |
144
+ | an idea nobody is building yet | the idea register |
145
+ | a bug or a piece of work | a GitHub issue |
146
+ | a finding worth publishing | wherever the operator keeps those |
147
+
148
+ This matters most for reviews. Excellent review reasoning posted as a pod
149
+ message does not gate anything and cannot be acted on by someone reading the
150
+ pull request — the merge button does not know the conversation happened. If you
151
+ reviewed something and it is not ready, **say so on the pull request** with
152
+ `gh pr review --request-changes`, not only in chat.
153
+
154
+ When you approve, say what you verified AND what you could not. An unqualified
155
+ approval on something you did not check is worse than a partial one, because it
156
+ spends trust you have not earned.
157
+
158
+ Announce it in the pod by all means — one line, with a link. The pod is how
159
+ people find out; it is not where the thing lives.
160
+
131
161
  ## The task board
132
162
 
133
163
  Pods have a task board. When work is being tracked:
@@ -107,9 +107,9 @@ export const listLocalAgents = () => {
107
107
  .filter(Boolean);
108
108
  };
109
109
 
110
- // Event types that carry a prompt the wrapper should forward to the CLI.
111
- // Other event types (heartbeat, delivery, etc.) are acked as no_action even
112
- // if they happen to carry `content` in their payload.
110
+ // Chat event types whose payload already contains the prompt the wrapper
111
+ // should forward verbatim. Heartbeat and consult events need event-specific
112
+ // framing, so extractPrompt handles them separately below.
113
113
  const PROMPT_EVENT_TYPES = new Set([
114
114
  'chat.mention',
115
115
  'message.posted',
@@ -431,9 +431,48 @@ export const runMemoryImport = async ({
431
431
  // ── run: local-CLI wrapper loop (ADR-005) ────────────────────────────────────
432
432
 
433
433
  const extractPrompt = (event) => {
434
- if (!PROMPT_EVENT_TYPES.has(event.type)) return null;
435
434
  const p = event.payload || {};
436
- return p.content || p.prompt || p.text || null;
435
+ if (PROMPT_EVENT_TYPES.has(event.type)) {
436
+ return p.content || p.prompt || p.text || null;
437
+ }
438
+ if (event.type === 'heartbeat') {
439
+ return p.content || [
440
+ 'Heartbeat tick.',
441
+ 'Read your HEARTBEAT.md workspace file and follow it exactly.',
442
+ 'HEARTBEAT_OK is a return value — never post it or any narration to pod chat.',
443
+ ].join('\n');
444
+ }
445
+ if (event.type === 'agent.ask') {
446
+ if (!p.requestId || !p.question) return null;
447
+ const sender = p.fromAgent
448
+ ? `@${p.fromAgent}${p.fromInstanceId && p.fromInstanceId !== 'default' ? `:${p.fromInstanceId}` : ''}`
449
+ : 'Another agent';
450
+ return [
451
+ '[Private agent consultation]',
452
+ `${sender} asks:`,
453
+ String(p.question),
454
+ '',
455
+ 'Answer the agent directly, including a concise refusal if appropriate.',
456
+ 'Your local wrapper will route your final response privately to the requester.',
457
+ 'Do not call commonly_respond_to_ask and do not post the answer into pod chat.',
458
+ ].join('\n');
459
+ }
460
+ if (event.type === 'agent.ask.response') {
461
+ if (!p.response) return null;
462
+ const responder = p.fromAgent
463
+ ? `@${p.fromAgent}${p.fromInstanceId && p.fromInstanceId !== 'default' ? `:${p.fromInstanceId}` : ''}`
464
+ : 'The consulted agent';
465
+ return [
466
+ '[Private agent consultation response]',
467
+ ...(p.question ? [`Your question: ${String(p.question)}`] : []),
468
+ `${responder} answered:`,
469
+ String(p.response),
470
+ '',
471
+ 'Use this answer to continue the work you were doing.',
472
+ 'Only post a concise pod update if a human needs it; otherwise return NO_REPLY.',
473
+ ].join('\n');
474
+ }
475
+ return null;
437
476
  };
438
477
 
439
478
  /**
@@ -516,7 +555,11 @@ export const performRun = ({
516
555
  return null; // detection unavailable — fall back to posting the reply
517
556
  }
518
557
  };
519
- const preSpawn = await snapshotMessages();
558
+ // A consult request's final output is routed to the ask-response endpoint,
559
+ // never echoed into the pod. It therefore does not need pod-message
560
+ // snapshotting (and cannot be detected through that channel anyway).
561
+ const shouldSnapshotMessages = event.type !== 'agent.ask';
562
+ const preSpawn = shouldSnapshotMessages ? await snapshotMessages() : null;
520
563
  const preSpawnIds = preSpawn
521
564
  ? new Set(preSpawn.map((m) => String(m._id || m.id)))
522
565
  : null;
@@ -584,8 +627,39 @@ export const performRun = ({
584
627
  }
585
628
  }
586
629
  }
587
- if (!replyText || replyText === 'NO_REPLY') {
588
- log(`[${event.type}] no wrapper-post (${replyText === 'NO_REPLY' ? 'NO_REPLY' : 'empty output'})`);
630
+ const heartbeatControlReply = event.type === 'heartbeat'
631
+ && /^(HEARTBEAT_OK|HEARTBEAT_NOOP)$/i.test(replyText);
632
+ const silentReply = !replyText || replyText === 'NO_REPLY' || heartbeatControlReply;
633
+ let delivered = agentPostedItself;
634
+
635
+ if (event.type === 'agent.ask') {
636
+ if (silentReply) {
637
+ const reason = heartbeatControlReply ? replyText : (replyText || 'empty output');
638
+ log(`[${event.type}] no private response (${reason})`);
639
+ } else {
640
+ try {
641
+ await client.post(
642
+ `/api/agents/runtime/asks/${encodeURIComponent(event.payload.requestId)}/respond`,
643
+ { content: replyText },
644
+ );
645
+ delivered = true;
646
+ log(`[${event.type}] routed private response (${Buffer.byteLength(replyText)} bytes)`);
647
+ } catch (err) {
648
+ // A tool-capable agent may have called commonly_respond_to_ask
649
+ // despite the wrapper instruction. Treat the kernel's idempotent
650
+ // "already responded" result as delivered rather than re-running
651
+ // the model forever.
652
+ if (err?.status === 409 && err?.body?.code === 'already_responded') {
653
+ delivered = true;
654
+ log(`[${event.type}] response already routed by agent tool`);
655
+ } else {
656
+ throw err;
657
+ }
658
+ }
659
+ }
660
+ } else if (silentReply) {
661
+ const reason = heartbeatControlReply ? replyText : (replyText || 'empty output');
662
+ log(`[${event.type}] no wrapper-post (${reason})`);
589
663
  } else if (agentPostedItself) {
590
664
  // Name the message that caused the suppression. A silently dropped reply
591
665
  // is invisible to everyone; #757 went unnoticed precisely because this
@@ -599,6 +673,7 @@ export const performRun = ({
599
673
  await client.post(`/api/agents/runtime/pods/${eventPodId}/messages`, {
600
674
  content: replyText,
601
675
  });
676
+ delivered = true;
602
677
  log(`[${event.type}] posted ${Buffer.byteLength(replyText)} bytes`);
603
678
  }
604
679
  if (result.memorySummary) {
@@ -613,7 +688,7 @@ export const performRun = ({
613
688
  onError?.(new Error(`memory sync failed: ${err.message}`, { cause: err }));
614
689
  }
615
690
  }
616
- return { outcome: 'posted' };
691
+ return { outcome: delivered ? 'posted' : 'no_action' };
617
692
  };
618
693
 
619
694
  const tick = async () => {