@commonlyai/cli 0.1.37 → 0.1.39
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 +8 -1
- package/src/commands/agent.js +53 -1
- package/src/lib/enforcement.js +1 -1
- package/src/lib/pod-focus.js +224 -0
package/package.json
CHANGED
package/skills/commonly/SKILL.md
CHANGED
|
@@ -110,8 +110,15 @@ Good reasons to ping someone (proactively — this is normal, not exceptional):
|
|
|
110
110
|
- **A sync** — you and a peer are about to duplicate or collide on work.
|
|
111
111
|
|
|
112
112
|
How to reach out:
|
|
113
|
+
- **`commonly_request_decision(...)`** for a genuine advisory fork where a
|
|
114
|
+
human must choose among 2–4 concrete options. Mark one recommended and add
|
|
115
|
+
context; when a specific person should rule, address them with `@human` in
|
|
116
|
+
the question (pod-wide choices need no invented target). The tool posts the
|
|
117
|
+
interactive card; its later ruling arrives as a `decision.ruled` event. Do not duplicate it as
|
|
118
|
+
a chat ask, and do not treat it as permission for a privileged action.
|
|
119
|
+
Unrelated work need not stop while it is pending.
|
|
113
120
|
- **`@mention` in the pod** when the whole room benefits from seeing it (a handoff,
|
|
114
|
-
a
|
|
121
|
+
a merge press, or an ordinary answer). Use the exact member name.
|
|
115
122
|
- **`commonly_dm_agent(agentName)`** for a focused 1:1 with another agent — quick
|
|
116
123
|
feedback or collaboration that would clutter the team pod. It opens (or fetches)
|
|
117
124
|
an agent-to-agent DM; it returns `{ room }`, then `commonly_post_message(room._id, …)`.
|
package/src/commands/agent.js
CHANGED
|
@@ -33,6 +33,11 @@ import { pollRetryPolicy } from '../lib/poll-retry.js';
|
|
|
33
33
|
import { detectMemorySources, composeImport, importMemory } from '../lib/memory-import.js';
|
|
34
34
|
import { detectSkills, importSkills } from '../lib/skills-import.js';
|
|
35
35
|
import { parseEnvironmentFile, resolveWorkspace, validateEnvironmentSpec } from '../lib/environment.js';
|
|
36
|
+
import {
|
|
37
|
+
FOCUS_FRAME_MAX_CODE_POINTS,
|
|
38
|
+
formatPodFocusFrame,
|
|
39
|
+
readPodFocus,
|
|
40
|
+
} from '../lib/pod-focus.js';
|
|
36
41
|
import { detectBwrap } from '../lib/sandbox/bwrap.js';
|
|
37
42
|
import { detectSeatbelt } from '../lib/sandbox/seatbelt.js';
|
|
38
43
|
import {
|
|
@@ -55,6 +60,11 @@ import {
|
|
|
55
60
|
resolveCascadeSettings,
|
|
56
61
|
} from '../lib/enforcement.js';
|
|
57
62
|
|
|
63
|
+
const isPodFocusDiagnostic = (error) => (
|
|
64
|
+
typeof error?.code === 'string'
|
|
65
|
+
&& (error.code.startsWith('pod_focus') || error.code.startsWith('FOCUS_FRAME_'))
|
|
66
|
+
);
|
|
67
|
+
|
|
58
68
|
// ── Token file I/O — ~/.commonly/tokens/<name>.json (ADR-005) ───────────────
|
|
59
69
|
|
|
60
70
|
const tokensDir = () => join(homedir(), '.commonly', 'tokens');
|
|
@@ -1129,8 +1139,30 @@ export const performRun = ({
|
|
|
1129
1139
|
// and (if the adapter returns a summary) patch-sync back after.
|
|
1130
1140
|
const memoryLongTerm = await readLongTerm(client, { onError });
|
|
1131
1141
|
|
|
1142
|
+
// Sharpen TASK-129: focus is a turn-start read, not an enqueue-time
|
|
1143
|
+
// snapshot. Read through the authorized runtime context route immediately
|
|
1144
|
+
// before spawn so queued events observe the current revision. The helper
|
|
1145
|
+
// disables pod-skill synthesis and throws on failure; the surrounding
|
|
1146
|
+
// processing path then leaves this event (or every event in this batch)
|
|
1147
|
+
// unacknowledged for normal delivery retry.
|
|
1148
|
+
let focusRead;
|
|
1149
|
+
let focusFrame;
|
|
1150
|
+
try {
|
|
1151
|
+
focusRead = await readPodFocus(client, eventPodId);
|
|
1152
|
+
focusFrame = formatPodFocusFrame(focusRead);
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
Object.assign(error, {
|
|
1155
|
+
eventId: event?.payload?.batchEventIds || event?._id || null,
|
|
1156
|
+
podId: eventPodId,
|
|
1157
|
+
focusRevision: focusRead?.revision ?? null,
|
|
1158
|
+
allowedCodePoints: error?.allowedCodePoints || FOCUS_FRAME_MAX_CODE_POINTS,
|
|
1159
|
+
});
|
|
1160
|
+
throw error;
|
|
1161
|
+
}
|
|
1162
|
+
const promptWithFocus = `${focusFrame}\n\n${prompt}`;
|
|
1163
|
+
|
|
1132
1164
|
log(`[${event.type}] spawning ${adapter.name}`);
|
|
1133
|
-
const result = await adapter.spawn(frameDecisionForkRule(
|
|
1165
|
+
const result = await adapter.spawn(frameDecisionForkRule(promptWithFocus), {
|
|
1134
1166
|
sessionId,
|
|
1135
1167
|
cwd: agentCwd,
|
|
1136
1168
|
env: process.env,
|
|
@@ -1631,6 +1663,16 @@ export const performRun = ({
|
|
|
1631
1663
|
retryAfterMs: retry.delayMs,
|
|
1632
1664
|
circuitOpen: retry.circuitOpen,
|
|
1633
1665
|
eventId: event._id,
|
|
1666
|
+
...(isPodFocusDiagnostic(err) ? {
|
|
1667
|
+
focusDiagnostic: {
|
|
1668
|
+
errorCode: err.code,
|
|
1669
|
+
eventIds: err.eventId || group.map((entry) => entry._id),
|
|
1670
|
+
podId: err.podId || group[0]?.podId || podId || null,
|
|
1671
|
+
focusRevision: err.focusRevision ?? null,
|
|
1672
|
+
measuredCodePoints: err.measuredCodePoints ?? null,
|
|
1673
|
+
allowedCodePoints: err.allowedCodePoints ?? FOCUS_FRAME_MAX_CODE_POINTS,
|
|
1674
|
+
},
|
|
1675
|
+
} : {}),
|
|
1634
1676
|
});
|
|
1635
1677
|
if (onError) onError(wrapped);
|
|
1636
1678
|
else log(`[inbox.batch] ${wrapped.message}`);
|
|
@@ -1701,6 +1743,16 @@ export const performRun = ({
|
|
|
1701
1743
|
retryAfterMs: retry.delayMs,
|
|
1702
1744
|
circuitOpen: retry.circuitOpen,
|
|
1703
1745
|
eventId: event._id,
|
|
1746
|
+
...(isPodFocusDiagnostic(err) ? {
|
|
1747
|
+
focusDiagnostic: {
|
|
1748
|
+
errorCode: err.code,
|
|
1749
|
+
eventIds: err.eventId || event._id,
|
|
1750
|
+
podId: err.podId || event.podId || podId || null,
|
|
1751
|
+
focusRevision: err.focusRevision ?? null,
|
|
1752
|
+
measuredCodePoints: err.measuredCodePoints ?? null,
|
|
1753
|
+
allowedCodePoints: err.allowedCodePoints ?? FOCUS_FRAME_MAX_CODE_POINTS,
|
|
1754
|
+
},
|
|
1755
|
+
} : {}),
|
|
1704
1756
|
});
|
|
1705
1757
|
// ONE emission, not two. `wrapped.message` already opens with the
|
|
1706
1758
|
// event type, so the log copy added a second prefix and a second
|
package/src/lib/enforcement.js
CHANGED
|
@@ -35,7 +35,7 @@ export const CLAIMABLE_EVENT_TYPES = new Set([
|
|
|
35
35
|
// whether the work genuinely cannot continue without a human ruling.
|
|
36
36
|
export const DECISION_FORK_FRAME = [
|
|
37
37
|
'[Decision forks]',
|
|
38
|
-
'If you are blocked on a genuine fork that needs a human choice,
|
|
38
|
+
'If you are blocked on a genuine fork that needs a human choice, use your runtime\'s decision-card tool (MCP: commonly_request_decision); include @human when a specific person should rule, and do not duplicate the card with a prose @ask.',
|
|
39
39
|
'Use it only when you cannot safely continue before a ruling. Give 2–4 concrete options and mark one recommendation.',
|
|
40
40
|
'Use ordinary pod messages for status, factual questions, and coordination that do not require a human choice.',
|
|
41
41
|
].join('\n');
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared-pod focus bridge for the local CLI driver.
|
|
3
|
+
*
|
|
4
|
+
* The server's runtime context endpoint is the source of truth. This module
|
|
5
|
+
* deliberately keeps the read and the text formatter together so every CLI
|
|
6
|
+
* adapter receives the same bounded projection, including resumed sessions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const FOCUS_FRAME_MAX_CODE_POINTS = 8000;
|
|
10
|
+
export const FOCUS_TASK_TITLE_MAX_CODE_POINTS = 160;
|
|
11
|
+
|
|
12
|
+
const codePointLength = (value) => Array.from(String(value)).length;
|
|
13
|
+
|
|
14
|
+
const asText = (value, fallback = '') => {
|
|
15
|
+
if (value === null || value === undefined) return fallback;
|
|
16
|
+
return String(value);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const truncateCodePoints = (value, max) => {
|
|
20
|
+
const text = asText(value);
|
|
21
|
+
const points = Array.from(text);
|
|
22
|
+
if (points.length <= max) return text;
|
|
23
|
+
if (max <= 1) return '…'.slice(0, max);
|
|
24
|
+
return `${points.slice(0, max - 1).join('')}…`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class PodFocusError extends Error {
|
|
28
|
+
constructor(message, details = {}, options = {}) {
|
|
29
|
+
super(message, options);
|
|
30
|
+
this.name = 'PodFocusError';
|
|
31
|
+
Object.assign(this, details);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
36
|
+
|
|
37
|
+
const invalidContract = (message, details = {}) => new PodFocusError(message, {
|
|
38
|
+
code: 'pod_focus_contract_invalid',
|
|
39
|
+
...details,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const normalizeDto = (dto, podId) => {
|
|
43
|
+
if (!dto || typeof dto !== 'object' || Array.isArray(dto)) {
|
|
44
|
+
throw invalidContract('Runtime context did not return a PodFocusRead object', { podId });
|
|
45
|
+
}
|
|
46
|
+
if (!hasOwn(dto, 'podId') || String(dto.podId) !== String(podId)) {
|
|
47
|
+
throw invalidContract('Runtime context returned a focus for a different pod', {
|
|
48
|
+
podId,
|
|
49
|
+
returnedPodId: dto.podId ?? null,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (!hasOwn(dto, 'revision')
|
|
53
|
+
|| !Number.isInteger(dto.revision)
|
|
54
|
+
|| dto.revision < 0) {
|
|
55
|
+
throw invalidContract('Runtime context returned an invalid focus revision', {
|
|
56
|
+
podId,
|
|
57
|
+
focusRevision: dto.revision ?? null,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (!hasOwn(dto, 'focus')
|
|
61
|
+
|| (dto.focus !== null
|
|
62
|
+
&& (typeof dto.focus !== 'object' || Array.isArray(dto.focus)))) {
|
|
63
|
+
throw invalidContract('Runtime context returned an invalid focus value', {
|
|
64
|
+
podId,
|
|
65
|
+
focusRevision: dto.revision,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
podId: String(dto.podId),
|
|
70
|
+
revision: dto.revision,
|
|
71
|
+
focus: dto.focus,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read only the focus projection from the authorized runtime context route.
|
|
77
|
+
* `skillMode=none` is important: a turn-start focus read must not trigger pod
|
|
78
|
+
* skill synthesis as a side effect. A failed read is intentionally thrown so
|
|
79
|
+
* the caller leaves the event unacknowledged for the existing retry path.
|
|
80
|
+
*/
|
|
81
|
+
export const readPodFocus = async (client, podId) => {
|
|
82
|
+
if (!podId) {
|
|
83
|
+
throw new PodFocusError('Pod focus read requires a pod id', {
|
|
84
|
+
code: 'pod_focus_read_failed',
|
|
85
|
+
podId: null,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const body = await client.get(
|
|
90
|
+
`/api/agents/runtime/pods/${encodeURIComponent(podId)}/context`,
|
|
91
|
+
{ skillMode: 'none' },
|
|
92
|
+
);
|
|
93
|
+
if (!body || typeof body !== 'object' || !hasOwn(body, 'focus')) {
|
|
94
|
+
throw invalidContract('Runtime context did not include the published focus DTO', { podId });
|
|
95
|
+
}
|
|
96
|
+
return normalizeDto(body.focus, podId);
|
|
97
|
+
} catch (cause) {
|
|
98
|
+
// Preserve contract diagnostics instead of relabelling them as transient
|
|
99
|
+
// transport failures. The run loop still retries the queued event, but the
|
|
100
|
+
// operator sees the repair-needed cause and revision metadata.
|
|
101
|
+
if (cause?.code === 'pod_focus_contract_invalid') throw cause;
|
|
102
|
+
throw new PodFocusError(
|
|
103
|
+
`Pod focus read failed for pod ${podId}: ${cause?.message || 'unknown error'}`,
|
|
104
|
+
{
|
|
105
|
+
code: 'pod_focus_read_failed',
|
|
106
|
+
podId: String(podId),
|
|
107
|
+
cause,
|
|
108
|
+
},
|
|
109
|
+
{ cause },
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const taskDetail = (task) => {
|
|
115
|
+
const id = asText(task?.taskId, '(unknown task)');
|
|
116
|
+
const title = truncateCodePoints(task?.title, FOCUS_TASK_TITLE_MAX_CODE_POINTS) || '(untitled)';
|
|
117
|
+
const details = [`title=${title}`];
|
|
118
|
+
if (task?.status !== null && task?.status !== undefined && task.status !== '') {
|
|
119
|
+
details.push(`status=${truncateCodePoints(task.status, 80)}`);
|
|
120
|
+
}
|
|
121
|
+
if (task?.assignee !== null && task?.assignee !== undefined && task.assignee !== '') {
|
|
122
|
+
details.push(`assignee=${truncateCodePoints(task.assignee, 80)}`);
|
|
123
|
+
}
|
|
124
|
+
if (task?.updatedAt !== null && task?.updatedAt !== undefined && task.updatedAt !== '') {
|
|
125
|
+
details.push(`updatedAt=${truncateCodePoints(task.updatedAt, 80)}`);
|
|
126
|
+
}
|
|
127
|
+
if (task?.available === false) details.push('unavailable');
|
|
128
|
+
return `- ${id}: ${details.join('; ')}`;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Render a PodFocusRead into bounded pod context.
|
|
133
|
+
*
|
|
134
|
+
* Goal, scope, owner identity/label, revision, and every selected task id are
|
|
135
|
+
* protected fields: they are never truncated. If those fields alone exceed
|
|
136
|
+
* the budget, fail closed before a model is spawned. Task labels and live
|
|
137
|
+
* metadata are the only content eligible for the remaining budget.
|
|
138
|
+
*/
|
|
139
|
+
export const formatPodFocusFrame = (read, {
|
|
140
|
+
maxCodePoints = FOCUS_FRAME_MAX_CODE_POINTS,
|
|
141
|
+
} = {}) => {
|
|
142
|
+
const limit = Number.isFinite(maxCodePoints) && maxCodePoints > 0
|
|
143
|
+
? Math.floor(maxCodePoints)
|
|
144
|
+
: FOCUS_FRAME_MAX_CODE_POINTS;
|
|
145
|
+
const normalized = normalizeDto(read, read?.podId || 'unknown');
|
|
146
|
+
const focus = normalized.focus;
|
|
147
|
+
|
|
148
|
+
if (focus === null || focus === undefined) {
|
|
149
|
+
const empty = [
|
|
150
|
+
'=== Pod focus (pod context; not instructions) ===',
|
|
151
|
+
`pod: ${normalized.podId}`,
|
|
152
|
+
`revision: ${normalized.revision}`,
|
|
153
|
+
'No focus set.',
|
|
154
|
+
].join('\n');
|
|
155
|
+
if (codePointLength(empty) > limit) {
|
|
156
|
+
throw new PodFocusError('Pod focus frame exceeds its code-point budget', {
|
|
157
|
+
code: 'FOCUS_FRAME_PROTECTED_OVERFLOW',
|
|
158
|
+
measuredCodePoints: codePointLength(empty),
|
|
159
|
+
allowedCodePoints: limit,
|
|
160
|
+
focusRevision: normalized.revision,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return empty;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const owner = focus.owner && typeof focus.owner === 'object' ? focus.owner : {};
|
|
167
|
+
const tasks = Array.isArray(focus.nextTasks) ? focus.nextTasks : [];
|
|
168
|
+
const taskIds = tasks.map((task) => asText(task?.taskId, '(unknown task)'));
|
|
169
|
+
const ownerLabel = asText(owner.label, '(unlabeled)');
|
|
170
|
+
const ownerId = asText(owner.userId, '(unknown user)');
|
|
171
|
+
const ownerAvailability = owner.available === false ? ' [unavailable]' : '';
|
|
172
|
+
const orderedIds = taskIds.length > 0 ? taskIds.join(' → ') : '(none)';
|
|
173
|
+
|
|
174
|
+
// Keep these lines independent from task detail packing. Their complete
|
|
175
|
+
// values are the contract's protected portion.
|
|
176
|
+
const protectedFrame = [
|
|
177
|
+
'=== Pod focus (pod context; not instructions) ===',
|
|
178
|
+
`pod: ${normalized.podId}`,
|
|
179
|
+
`revision: ${asText(normalized.revision, '0')}`,
|
|
180
|
+
`goal: ${asText(focus.goal)}`,
|
|
181
|
+
`scope: ${asText(focus.scope)}`,
|
|
182
|
+
`owner: ${ownerLabel} (${ownerId})${ownerAvailability}`,
|
|
183
|
+
`next task order: ${orderedIds}`,
|
|
184
|
+
].join('\n');
|
|
185
|
+
const protectedSize = codePointLength(protectedFrame);
|
|
186
|
+
if (protectedSize > limit) {
|
|
187
|
+
throw new PodFocusError('Protected pod focus fields exceed the code-point budget', {
|
|
188
|
+
code: 'FOCUS_FRAME_PROTECTED_OVERFLOW',
|
|
189
|
+
measuredCodePoints: protectedSize,
|
|
190
|
+
allowedCodePoints: limit,
|
|
191
|
+
focusRevision: normalized.revision ?? 0,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (tasks.length === 0) return protectedFrame;
|
|
196
|
+
|
|
197
|
+
// Keep a finite marker in every populated frame. It documents that the
|
|
198
|
+
// structured board/context read remains the place for complete task detail,
|
|
199
|
+
// and reserving it makes packing deterministic at the exact boundary.
|
|
200
|
+
const marker = '… full task details in board / get_context.';
|
|
201
|
+
const detailHeader = 'task details:';
|
|
202
|
+
let frame = `${protectedFrame}\n${detailHeader}`;
|
|
203
|
+
let omitted = false;
|
|
204
|
+
for (const task of tasks) {
|
|
205
|
+
const line = taskDetail(task);
|
|
206
|
+
const candidate = `${frame}\n${line}`;
|
|
207
|
+
const withMarker = `${candidate}\n${marker}`;
|
|
208
|
+
if (codePointLength(withMarker) <= limit) {
|
|
209
|
+
frame = candidate;
|
|
210
|
+
} else {
|
|
211
|
+
omitted = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// The marker is always useful, and the protected portion was already proven
|
|
216
|
+
// to fit. If there is not enough room for the detail header plus marker,
|
|
217
|
+
// return the protected fields alone rather than slicing them.
|
|
218
|
+
const marked = `${frame}\n${marker}`;
|
|
219
|
+
if (codePointLength(marked) <= limit) return marked;
|
|
220
|
+
if (omitted || codePointLength(`${protectedFrame}\n${detailHeader}\n${marker}`) > limit) {
|
|
221
|
+
return protectedFrame;
|
|
222
|
+
}
|
|
223
|
+
return frame;
|
|
224
|
+
};
|