@commonlyai/cli 0.1.5 → 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 +1 -1
- package/skills/commonly/SKILL.md +30 -0
- package/src/commands/agent.js +118 -20
package/package.json
CHANGED
package/skills/commonly/SKILL.md
CHANGED
|
@@ -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:
|
package/src/commands/agent.js
CHANGED
|
@@ -107,9 +107,9 @@ export const listLocalAgents = () => {
|
|
|
107
107
|
.filter(Boolean);
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
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
|
-
|
|
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
|
/**
|
|
@@ -502,9 +541,10 @@ export const performRun = ({
|
|
|
502
541
|
// did, its final CLI text is a narration/log — echoing it would duplicate
|
|
503
542
|
// the message and re-fire any @mention. This is the wrapper-side guarantee
|
|
504
543
|
// that a deliberate multi-post ("on it…" then "…done") is never doubled,
|
|
505
|
-
// without relying on the agent to emit NO_REPLY.
|
|
506
|
-
//
|
|
507
|
-
//
|
|
544
|
+
// without relying on the agent to emit NO_REPLY. Against a server that
|
|
545
|
+
// stamps `self` this is exact; against an older one it degrades to the
|
|
546
|
+
// legacy any-bot test, where a concurrent post by another agent can
|
|
547
|
+
// suppress a genuine reply (#757).
|
|
508
548
|
const snapshotMessages = async () => {
|
|
509
549
|
try {
|
|
510
550
|
const { messages = [] } = await client.get(
|
|
@@ -515,7 +555,11 @@ export const performRun = ({
|
|
|
515
555
|
return null; // detection unavailable — fall back to posting the reply
|
|
516
556
|
}
|
|
517
557
|
};
|
|
518
|
-
|
|
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;
|
|
519
563
|
const preSpawnIds = preSpawn
|
|
520
564
|
? new Set(preSpawn.map((m) => String(m._id || m.id)))
|
|
521
565
|
: null;
|
|
@@ -551,31 +595,85 @@ export const performRun = ({
|
|
|
551
595
|
// reply via its output) the wrapper delivers the text as before.
|
|
552
596
|
const replyText = (result.text || '').trim();
|
|
553
597
|
let agentPostedItself = false;
|
|
598
|
+
let suppressedBy = null;
|
|
554
599
|
if (preSpawnIds) {
|
|
555
600
|
const postSpawn = await snapshotMessages();
|
|
556
601
|
if (postSpawn) {
|
|
602
|
+
// The server stamps each message with `self` when it knows who is
|
|
603
|
+
// asking. Trust that over any local guess: the wrapper cannot derive
|
|
604
|
+
// its own bot username reliably (the server applies LEGACY_AGENT_MAP
|
|
605
|
+
// and an owner-scoped instanceId), and a wrong guess double-posts.
|
|
606
|
+
const serverKnowsSelf = postSpawn.some((m) => typeof m.self === 'boolean');
|
|
557
607
|
for (const m of postSpawn) {
|
|
558
|
-
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
if (!
|
|
608
|
+
if (preSpawnIds.has(String(m._id || m.id))) continue;
|
|
609
|
+
// A new human-authored message must NEVER suppress the echo: it is
|
|
610
|
+
// either a human typing mid-turn, or the agent misusing an operator
|
|
611
|
+
// CLI profile / human token (the 2026-07-22 as-operator attribution
|
|
612
|
+
// incident) — in both cases the wrapper still delivers the reply
|
|
613
|
+
// under the agent's own identity.
|
|
614
|
+
if (!m.isBot) continue;
|
|
615
|
+
// With `self`, only THIS agent's own post suppresses. Without it (an
|
|
616
|
+
// older server), fall back to the legacy any-bot test — which drops
|
|
617
|
+
// this agent's reply whenever another agent answers first (#757).
|
|
618
|
+
if (serverKnowsSelf ? m.self === true : true) {
|
|
565
619
|
agentPostedItself = true;
|
|
620
|
+
suppressedBy = {
|
|
621
|
+
id: String(m._id || m.id),
|
|
622
|
+
author: m.username || 'unknown',
|
|
623
|
+
basis: serverKnowsSelf ? 'self' : 'legacy-any-bot',
|
|
624
|
+
};
|
|
566
625
|
break;
|
|
567
626
|
}
|
|
568
627
|
}
|
|
569
628
|
}
|
|
570
629
|
}
|
|
571
|
-
|
|
572
|
-
|
|
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})`);
|
|
573
663
|
} else if (agentPostedItself) {
|
|
574
|
-
|
|
664
|
+
// Name the message that caused the suppression. A silently dropped reply
|
|
665
|
+
// is invisible to everyone; #757 went unnoticed precisely because this
|
|
666
|
+
// line said only "posted via tool" with no way to tell whose post it saw.
|
|
667
|
+
log(
|
|
668
|
+
`[${event.type}] agent posted via tool this turn — not echoing CLI output `
|
|
669
|
+
+ `(avoids double-post; matched message ${suppressedBy.id} by ${suppressedBy.author} `
|
|
670
|
+
+ `via ${suppressedBy.basis})`,
|
|
671
|
+
);
|
|
575
672
|
} else {
|
|
576
673
|
await client.post(`/api/agents/runtime/pods/${eventPodId}/messages`, {
|
|
577
674
|
content: replyText,
|
|
578
675
|
});
|
|
676
|
+
delivered = true;
|
|
579
677
|
log(`[${event.type}] posted ${Buffer.byteLength(replyText)} bytes`);
|
|
580
678
|
}
|
|
581
679
|
if (result.memorySummary) {
|
|
@@ -590,7 +688,7 @@ export const performRun = ({
|
|
|
590
688
|
onError?.(new Error(`memory sync failed: ${err.message}`, { cause: err }));
|
|
591
689
|
}
|
|
592
690
|
}
|
|
593
|
-
return { outcome: 'posted' };
|
|
691
|
+
return { outcome: delivered ? 'posted' : 'no_action' };
|
|
594
692
|
};
|
|
595
693
|
|
|
596
694
|
const tick = async () => {
|