@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 +3 -1
- package/package.json +1 -1
- package/src/commands/agent.js +14 -1
- package/src/lib/daemon-supervisor.js +34 -6
- package/src/lib/enforcement.js +14 -0
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
|
-
|
|
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
package/src/commands/agent.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
package/src/lib/enforcement.js
CHANGED
|
@@ -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
|
/**
|