@commonlyai/cli 0.1.32 → 0.1.35

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/README.md CHANGED
@@ -24,4 +24,6 @@ Requires Node 20+. No build step.
24
24
  node --experimental-vm-modules node_modules/.bin/jest --no-coverage
25
25
  ```
26
26
 
27
- 70 tests as of 2026-04-15.
27
+ Instance-selection tests use distinct persisted `savedAt` values and both insertion orders.
28
+ Keep active-instance precedence separate from the newest-match fallback; back-to-back
29
+ `saveInstance` calls can share a millisecond and hide a broken selection branch.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.32",
3
+ "version": "0.1.35",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -50,6 +50,7 @@ import {
50
50
  createClaimHandicap,
51
51
  createClaimKeeper,
52
52
  deliverChatReply,
53
+ frameDecisionForkRule,
53
54
  peerHoldsFrame,
54
55
  resolveCascadeSettings,
55
56
  } from '../lib/enforcement.js';
@@ -694,6 +695,18 @@ const extractPrompt = (event) => {
694
695
  'Only post a concise pod update if a human needs it; otherwise return NO_REPLY.',
695
696
  ].join('\n');
696
697
  }
698
+ if (event.type === 'decision.ruled') {
699
+ if (!p.decisionId || !p.pick || !p.ruledAt) return null;
700
+ const ruler = p.ruledBy?.username || 'A human';
701
+ return [
702
+ '[Decision ruled]',
703
+ `${ruler} chose: ${String(p.pick)}`,
704
+ `Decision: ${String(p.decisionId)}`,
705
+ `Ruled at: ${String(p.ruledAt)}`,
706
+ '',
707
+ 'Continue with this ruling. Post a concise update only if it materially helps the pod; otherwise return NO_REPLY.',
708
+ ].join('\n');
709
+ }
697
710
  return null;
698
711
  };
699
712
 
@@ -1050,7 +1063,7 @@ export const performRun = ({
1050
1063
  const memoryLongTerm = await readLongTerm(client, { onError });
1051
1064
 
1052
1065
  log(`[${event.type}] spawning ${adapter.name}`);
1053
- const result = await adapter.spawn(prompt, {
1066
+ const result = await adapter.spawn(frameDecisionForkRule(prompt), {
1054
1067
  sessionId,
1055
1068
  cwd: agentCwd,
1056
1069
  env: process.env,
@@ -87,12 +87,33 @@ export const createDaemonSupervisor = ({
87
87
  }
88
88
  };
89
89
 
90
+ // The server-declared runtime config carries the owner's model choice; the
91
+ // adapter reads it from the token record's environment (claude: --model).
92
+ const environmentFor = (row) => (
93
+ row.runtime?.model ? { model: String(row.runtime.model) } : null
94
+ );
95
+
90
96
  // Ensure ~/.commonly/tokens/<name>.json exists so `agent run` can boot.
91
97
  // The mint refuses to clobber an existing token (409 token_exists); the
92
98
  // binding to THIS machine is the owner's explicit takeover choice (D3), so
93
99
  // that refusal is answered with rotate:true — loudly.
100
+ // Returns 'ready' | 'changed' (record updated — the seat must restart to
101
+ // load it) | false.
94
102
  const ensureToken = async (row) => {
95
- if (loadToken(row.agentName)) return true;
103
+ const existing = loadToken(row.agentName);
104
+ if (existing) {
105
+ // A model changed in the UI reaches the seat here: update the record,
106
+ // and let the caller restart the child (`agent run` reads its record
107
+ // once at boot). A row with NO declared model leaves the record alone —
108
+ // never strip an operator's hand-set environment.
109
+ const wanted = environmentFor(row);
110
+ if (wanted && existing.environment?.model !== wanted.model) {
111
+ saveToken(row.agentName, { ...existing, environment: { ...(existing.environment || {}), ...wanted } });
112
+ log(`[${row.agentName}] model changed to ${wanted.model} — restarting the seat to load it`);
113
+ return 'changed';
114
+ }
115
+ return 'ready';
116
+ }
96
117
  const body = { agentName: row.agentName, instanceId: row.instanceId };
97
118
  let minted;
98
119
  try {
@@ -120,6 +141,7 @@ export const createDaemonSupervisor = ({
120
141
  log(`[${row.agentName}] no usable CLI adapter on this machine — install claude or codex, or attach manually`);
121
142
  return false;
122
143
  }
144
+ const environment = environmentFor(row);
123
145
  saveToken(row.agentName, {
124
146
  agentName: row.agentName,
125
147
  instanceId: row.instanceId,
@@ -127,9 +149,10 @@ export const createDaemonSupervisor = ({
127
149
  instanceUrl: record.instanceUrl,
128
150
  podId: row.podIds?.[0] || null,
129
151
  adapter,
152
+ ...(environment ? { environment } : {}),
130
153
  });
131
- log(`[${row.agentName}] provisioned runtime token (adapter: ${adapter})`);
132
- return true;
154
+ log(`[${row.agentName}] provisioned runtime token (adapter: ${adapter}${environment ? `, model: ${environment.model}` : ''})`);
155
+ return 'ready';
133
156
  };
134
157
 
135
158
  const tick = async () => {
@@ -180,9 +203,14 @@ export const createDaemonSupervisor = ({
180
203
  seats.set(key, seat);
181
204
  }
182
205
  seat.desired = true;
183
- if (!seat.child && !seat.backoffTimer) {
184
- // eslint-disable-next-line no-await-in-loop
185
- if (await ensureToken(row)) startChild(seat);
206
+ // eslint-disable-next-line no-await-in-loop
207
+ const ready = await ensureToken(row);
208
+ if (ready === 'changed' && seat.child) {
209
+ // desired stays true, so the exit handler respawns with the updated
210
+ // record — the restart path IS the D6 path, no second spawner.
211
+ seat.child.kill('SIGTERM');
212
+ } else if (ready && !seat.child && !seat.backoffTimer) {
213
+ startChild(seat);
186
214
  }
187
215
  }
188
216
 
@@ -28,6 +28,20 @@ export const CLAIMABLE_EVENT_TYPES = new Set([
28
28
  'dm.message',
29
29
  ]);
30
30
 
31
+ // A decision card is the durable path for a real human choice. This lives in
32
+ // wrapper enforcement, rather than only in the MCP tool description, because
33
+ // a task-shaped prompt can otherwise make an agent phrase the same fork as a
34
+ // prose @ask. The rule is framing, not a classifier: the agent still judges
35
+ // whether the work genuinely cannot continue without a human ruling.
36
+ export const DECISION_FORK_FRAME = [
37
+ '[Decision forks]',
38
+ 'If you are blocked on a genuine fork that needs a human choice, call commonly_request_decision; do not post a prose @ask.',
39
+ 'Use it only when you cannot safely continue before a ruling. Give 2–4 concrete options and mark one recommendation.',
40
+ 'Use ordinary pod messages for status, factual questions, and coordination that do not require a human choice.',
41
+ ].join('\n');
42
+
43
+ export const frameDecisionForkRule = (prompt) => `${DECISION_FORK_FRAME}\n\n${prompt}`;
44
+
31
45
  // ── trigger classification ──────────────────────────────────────────────────
32
46
 
33
47
  /**