@commonlyai/cli 0.1.171 → 0.1.173

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
@@ -33,11 +33,21 @@ Keep active-instance precedence separate from the newest-match fallback; back-to
33
33
  A hosted agent's local handoff card includes `--memory-from <hosted-agent>` and
34
34
  `--memory-instance <hosted-instance>` on the attach command. After login, attach
35
35
  copies that source's long-term memory into the local identity before returning
36
- its runnable configuration. Both identities must be accessible through the
37
- logged-in account's owner memory API. No hosted runtime credential is moved.
36
+ its runnable configuration. The source and target must belong to the logged-in
37
+ owner. No hosted runtime credential is moved.
38
38
 
39
39
  This is a snapshot, not a shared identity or continuing synchronization. Attach
40
40
  refuses to replace different existing local memory or copy into a non-private
41
- long-term section. A failed copy stops attach; correct the error and retry before
42
- running the local agent. A retry resumes only your matching active local installation,
41
+ long-term section. The install request completes memory authorization and the
42
+ revision-checked copy before joining the pod, creating an installation or posting
43
+ a greeting. A refused copy leaves those effects absent (an existing installation
44
+ is preserved). A later identity-creation or pod-join refusal can leave the copied
45
+ memory without an installation; retries recognize the snapshot as already copied.
46
+ The server claims the registry name before copying. On copy failure it releases
47
+ only the new, unchanged claim when no installation uses it; a claim touched by
48
+ another in-flight handoff is retained. Memory is not rolled back across later
49
+ identity or pod-join operations. Correct the error and
50
+ retry before running the local agent.
51
+ The CLI checks server support before publishing or installing; deploy the backend
52
+ with install-time handoff support before distributing the updated CLI. A retry resumes only your matching active local installation,
43
53
  without changing its configuration or scopes. Repeating a copy with identical memory is safe.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.171",
3
+ "version": "0.1.173",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -22,7 +22,6 @@ import { createHash } from 'crypto';
22
22
 
23
23
  import { requireSeatModel } from '../lib/adapters/seat-model.js';
24
24
  import { createClient } from '../lib/api.js';
25
- import { copyHandoffMemory } from '../lib/handoff-memory.js';
26
25
  import {
27
26
  buildSpawnId,
28
27
  createSpawnCredentialLease,
@@ -987,6 +986,13 @@ export const performAttach = async ({
987
986
  // server via legacy normalization).
988
987
  const runtimeType = adapter.runtimeType || adapter.name;
989
988
 
989
+ if (memoryFrom) {
990
+ const capability = await client.get('/api/registry/install/memory-handoff');
991
+ if (capability?.installTimeMemoryHandoff !== true) {
992
+ throw new Error('Server does not support install-time memory handoff; upgrade the server before attaching');
993
+ }
994
+ }
995
+
990
996
  // Idempotent publish — already-published is expected and fine. Install will
991
997
  // surface the real error if the manifest is truly unusable.
992
998
  try {
@@ -1010,7 +1016,9 @@ export const performAttach = async ({
1010
1016
  // on config.environment so cross-driver implementations can realize it
1011
1017
  // server-side without re-reading the user's file.
1012
1018
  const installResult = await client.post('/api/registry/install', {
1013
- ...(memoryFrom ? { resumeExisting: true } : {}),
1019
+ ...(memoryFrom ? {
1020
+ resumeExisting: true, memoryFrom: { agentName: memoryFrom, instanceId: memoryInstance },
1021
+ } : {}),
1014
1022
  agentName,
1015
1023
  podId,
1016
1024
  displayName: displayName || agentName,
@@ -1034,10 +1042,8 @@ export const performAttach = async ({
1034
1042
  const instanceId = installation.instanceId || 'default';
1035
1043
 
1036
1044
  if (memoryFrom) {
1037
- const memoryCopy = await copyHandoffMemory(client, {
1038
- sourceAgentName: memoryFrom, sourceInstanceId: memoryInstance,
1039
- targetAgentName: installation.agentName, targetInstanceId: instanceId,
1040
- });
1045
+ const memoryCopy = installResult.memoryCopy;
1046
+ if (!memoryCopy) throw new Error('Server does not support install-time memory handoff; upgrade the server before attaching');
1041
1047
  log(memoryCopy.reason === 'source-empty'
1042
1048
  ? 'hosted agent has no long-term memory to copy'
1043
1049
  : 'hosted long-term memory is present in the local agent');
@@ -81,7 +81,7 @@ import { deliverSeatCredential, withholdRuntimeCredential } from '../mcp-credent
81
81
  import {
82
82
  prepareMcpSpawn, PUBLIC_MCP_UNAVAILABLE_CODE, publicMcpUnavailableError, selectedMcpVersionDir,
83
83
  } from '../mcp-home.js';
84
- import { buildMemoryPreamble } from '../memory-bridge.js';
84
+ import { buildMemoryPreamble, ROW_SLICING_CUE } from '../memory-bridge.js';
85
85
  import { claudeConfigDirFor, claudeContextTokens } from '../session-recycle.js';
86
86
  import { skillsAsText } from '../skill-prompt.js';
87
87
  import { adapterFailure, spawnCredentials } from '../upstream-refusal.js';
@@ -98,6 +98,16 @@ const DEFAULT_TIMEOUT_MS = (() => {
98
98
  })();
99
99
 
100
100
  const buildPrompt = buildMemoryPreamble;
101
+
102
+ // The working habits a private claude seat is taught on a fresh session (D48,
103
+ // TASK-545). Parallel reads live here because only this CLI has a subagent
104
+ // tool; codex is passed row slicing alone. A public seat is passed neither: its
105
+ // argv allows Read and the MCP under dontAsk, so it has no subagent to spawn,
106
+ // and it answers strangers rather than working board rows.
107
+ const PARALLEL_READS_CUE = 'Parallel reads: when a row needs several files or sources read, '
108
+ + 'use the Agent tool. Spawn read-only subagents in parallel, brief each one with the files '
109
+ + 'to read and the question to answer, and keep only their conclusions.';
110
+ const WORK_CUES = [PARALLEL_READS_CUE, ROW_SLICING_CUE];
101
111
  const RUNTIME_CREDENTIAL_PLACEHOLDER = '${COMMONLY_AGENT_TOKEN}';
102
112
 
103
113
  const QUOTE_SHELL_ARGUMENT = (value) => `'${String(value).replaceAll("'", "'\\''")}'`;
@@ -799,7 +809,12 @@ export default {
799
809
  // the only two paths that build a real prompt. buildPrompt handles
800
810
  // undefined and '' as absence itself; it does not need a guard, it needs
801
811
  // the value.
802
- const fullPrompt = buildPrompt(prompt, ctx.memoryLongTerm, { freshSession: !isResume });
812
+ const publicTrust = ctx.environment?.sandbox?.trust === 'public';
813
+ const workCues = publicTrust ? [] : WORK_CUES;
814
+ const fullPrompt = buildPrompt(prompt, ctx.memoryLongTerm, {
815
+ freshSession: !isResume,
816
+ workCues,
817
+ });
803
818
  const sessionFlag = isResume ? '--resume' : '--session-id';
804
819
  // Model pin from the ADR-008 environment spec. Absent it, claude picks its
805
820
  // own default — which is how a fleet of ten agents ended up running three
@@ -839,7 +854,6 @@ export default {
839
854
  let mcpConfig = null;
840
855
  let publicClaudeState = null;
841
856
  let publicSkillArgs = [];
842
- const publicTrust = ctx.environment?.sandbox?.trust === 'public';
843
857
  const publicNativeSandbox = publicTrust
844
858
  && PUBLIC_SANDBOX_MODES.has(resolvePublicSandboxMode(ctx.environment?.sandbox));
845
859
  const publicBwrapSandbox = publicTrust
@@ -927,7 +941,10 @@ export default {
927
941
  // The retry creates a new underlying CLI session. Rebuild its prompt
928
942
  // as fresh too: otherwise a session-recovery path is the one fresh
929
943
  // session that misses the durable-state cue.
930
- const freshPrompt = buildPrompt(prompt, ctx.memoryLongTerm, { freshSession: true });
944
+ const freshPrompt = buildPrompt(prompt, ctx.memoryLongTerm, {
945
+ freshSession: true,
946
+ workCues,
947
+ });
931
948
  const retryBase = ['-p', freshPrompt,
932
949
  '--output-format', 'text', '--session-id', freshId, ...modelArgs];
933
950
  const retryCtx = {
@@ -66,7 +66,7 @@ import { CREDENTIAL_KEY, writeCredentialFile } from '../credential-file.js';
66
66
  import { supportedGitHooks } from '../commit-attribution.js';
67
67
  import { deliverSeatCredential, withholdRuntimeCredential } from '../mcp-credential-delivery.js';
68
68
  import { prepareMcpSpawn, publicMcpUnavailableError, selectedMcpVersionDir } from '../mcp-home.js';
69
- import { buildMemoryPreamble } from '../memory-bridge.js';
69
+ import { buildMemoryPreamble, ROW_SLICING_CUE } from '../memory-bridge.js';
70
70
  import { resolveSkillSource, skillsAsText } from '../skill-prompt.js';
71
71
  import { isLegacySandboxTrust, normalizeSandboxTrust } from '../environment.js';
72
72
  import { adapterFailure, spawnCredentials } from '../upstream-refusal.js';
@@ -650,14 +650,20 @@ export default {
650
650
 
651
651
  async spawn(prompt, ctx = {}) {
652
652
  const seatModel = requireSeatModel(ctx.environment, 'codex');
653
+ const sandbox = normalizeSandboxTrust(ctx.environment?.sandbox);
653
654
  // Passed through UNCOALESCED. `ctx.memoryLongTerm || ''` was here, and
654
655
  // `null || ''` is `''` — which routed the unreadable case straight into
655
656
  // the empty-memory branch and made the whole distinction unreachable from
656
657
  // the only two paths that build a real prompt. buildPrompt handles
657
658
  // undefined and '' as absence itself; it does not need a guard, it needs
658
659
  // the value.
660
+ //
661
+ // Codex has no subagent tool, so of D48's two habits (TASK-545) it is
662
+ // taught row slicing alone; claude.js adds parallel reads. A public seat
663
+ // answers strangers rather than working board rows, so it is taught neither.
659
664
  const fullPrompt = buildPrompt(prompt, ctx.memoryLongTerm, {
660
665
  freshSession: !ctx.sessionId,
666
+ workCues: sandbox?.trust === 'public' ? [] : [ROW_SLICING_CUE],
661
667
  });
662
668
 
663
669
  // Per-spawn temp dir for --output-last-message. Cleaned up in `finally`
@@ -674,7 +680,6 @@ export default {
674
680
  });
675
681
 
676
682
  try {
677
- const sandbox = normalizeSandboxTrust(ctx.environment?.sandbox);
678
683
  const mcp = buildMcpOverrideArgs(ctx.environment?.mcp, {
679
684
  runtimeToken: ctx.runtimeToken,
680
685
  instanceUrl: ctx.instanceUrl,
@@ -22,6 +22,18 @@
22
22
 
23
23
  export const SOURCE_RUNTIME = 'local-cli';
24
24
 
25
+ /**
26
+ * Row slicing, the runtime-agnostic half of D48 (TASK-545). A row too large
27
+ * for one turn drifts when one seat carries it whole, so the seat splits it on
28
+ * the board before any code. It names the exact call and field for the reason
29
+ * the memory cue below names `long_term`: a seat adopts a habit through a
30
+ * surface it can see. Each adapter decides whether to pass it.
31
+ */
32
+ export const ROW_SLICING_CUE = 'Slicing: when a row is too large for one turn, split it before '
33
+ + 'writing any code. Post each slice as a child row (commonly_create_task with parentTask set '
34
+ + "to the row's taskId) with one assignee, a written file or area boundary, and its own "
35
+ + 'acceptance line.';
36
+
25
37
  /**
26
38
  * The turn preamble, shared by every adapter so the two cannot drift.
27
39
  *
@@ -39,8 +51,17 @@ export const SOURCE_RUNTIME = 'local-cli';
39
51
  * the section whose name most invites exactly this use — succeeds, returns 200,
40
52
  * and is never seen again; the write and the silence are indistinguishable from
41
53
  * a correct round trip. Naming the section is the whole point of the cue.
54
+ *
55
+ * `workCues` are working habits an adapter teaches on a fresh session, beside
56
+ * the memory cues and under the same once-per-session rule. An adapter passes
57
+ * only the habits its CLI can act on; passing none leaves the preamble as it
58
+ * was.
42
59
  */
43
- export const buildMemoryPreamble = (prompt, memoryLongTerm, { freshSession = false } = {}) => {
60
+ export const buildMemoryPreamble = (
61
+ prompt,
62
+ memoryLongTerm,
63
+ { freshSession = false, workCues = [] } = {},
64
+ ) => {
44
65
  // `null` is the UNREADABLE signal, and it is deliberately not the same value
45
66
  // as `''`. Telling a seat whose token was revoked that "nothing has ever been
46
67
  // saved here" is a false claim about its own history, and it is the same
@@ -87,7 +108,10 @@ export const buildMemoryPreamble = (prompt, memoryLongTerm, { freshSession = fal
87
108
  + 'gates held, decisions pending, and task context, not a transcript — using '
88
109
  + 'the long_term write above.';
89
110
  }
90
- return `${context}\n${freshReadCue}\n=== Current turn ===\n${prompt}\n${sessionEndCue}`;
111
+ const workCue = workCues.length
112
+ ? `\n=== Working a row ===\n${workCues.map((cue) => `- ${cue}`).join('\n')}\n`
113
+ : '';
114
+ return `${context}\n${freshReadCue}${workCue}\n=== Current turn ===\n${prompt}\n${sessionEndCue}`;
91
115
  };
92
116
 
93
117
  export const readLongTerm = async (client, { onError } = {}) => {
@@ -1,23 +0,0 @@
1
- /** Copy only long-term memory using the logged-in owner's revision-checked API. */
2
- export const copyHandoffMemory = async (client, {
3
- sourceAgentName, sourceInstanceId = 'default', targetAgentName, targetInstanceId = 'default',
4
- }) => {
5
- const endpoint = (name, instance) => `/api/agent-memory/${encodeURIComponent(name)}/${encodeURIComponent(instance)}/editable`;
6
- const source = await client.get(endpoint(sourceAgentName, sourceInstanceId));
7
- const content = source?.sections?.long_term?.content;
8
- if (typeof content !== 'string') throw new Error('Hosted memory response is missing long_term content');
9
- if (!content) return { copied: false, reason: 'source-empty' };
10
- const targetPath = endpoint(targetAgentName, targetInstanceId);
11
- const target = await client.get(targetPath);
12
- const prior = target?.sections?.long_term;
13
- if (typeof prior?.content !== 'string') throw new Error('Local memory response is missing long_term content');
14
- if (prior.visibility !== 'private') throw new Error('Local long-term memory must be private before copying hosted memory');
15
- if (prior.content === content) return { copied: false, reason: 'already-copied' };
16
- if (prior.content) throw new Error('Local agent already has different memory; choose a new local name');
17
- // A concurrent first write wins rather than being overwritten by this copy.
18
- await client.patch(`${targetPath}/long_term`, {
19
- copyFrom: { agentName: sourceAgentName, instanceId: sourceInstanceId },
20
- expectedUpdatedAt: prior.updatedAt ?? null,
21
- });
22
- return { copied: true };
23
- };