@awak-app/simy-cli 0.7.35 → 0.7.37
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 +50 -46
- package/package.json +1 -1
- package/src/agent.js +1 -1
- package/src/backend-executable.js +1 -1
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +1 -1
- package/src/codex-plugins/simy/.codex-plugin/plugin.json +5 -5
- package/src/codex-plugins/simy/hooks/README.md +3 -3
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +2 -2
- package/src/codex-plugins/simy/skills/continue/SKILL.md +4 -4
- package/src/codex-plugins/simy/skills/continue/agents/openai.yaml +3 -3
- package/src/codex-plugins/simy/skills/diagnose/SKILL.md +2 -2
- package/src/codex-plugins/simy/skills/sqm-loop/SKILL.md +3 -3
- package/src/codex-plugins/simy/skills/sqm-loop/agents/openai.yaml +2 -2
- package/src/console/app.js +7 -7
- package/src/delivery-hooks/claude-workflow.md +1 -1
- package/src/delivery-hooks/command.js +5 -5
- package/src/desktop-host.js +58 -11
- package/src/index.js +1 -1
- package/src/local-task.js +1 -1
- package/src/orchestrator/presentation.js +1 -1
- package/src/runner.js +2 -2
- package/src/session-supervision/automatic-continuation.js +11 -11
- package/src/session-supervision/continuation-analysis.js +1 -1
- package/src/session-supervision/continuation-command.js +3 -3
- package/src/session-supervision/continuation-readiness.js +1 -1
- package/src/session-supervision/conversation-presentation.js +3 -3
- package/src/session-supervision/decision-presentation.js +8 -8
- package/src/session-supervision/index.js +13 -13
- package/src/session-supervision/owner-transport.js +53 -11
- package/src/session-supervision/remote-errors.js +2 -2
- package/src/session-supervision/remote-hosts.js +14 -3
- package/src/session-supervision/work-state.js +3 -3
- package/src/sqm/agent-skill.js +3 -3
- package/src/sqm/claude-plugin.js +1 -1
|
@@ -12,10 +12,10 @@ export const ownerSocketPath = (root, platform = process.platform) => platform =
|
|
|
12
12
|
const socketPath = ownerSocketPath;
|
|
13
13
|
// The socket and nonce are accessible only to the local OS account. Application
|
|
14
14
|
// account/provider checks still run in the owner's ordinary command handler.
|
|
15
|
-
export async function createOwnerTransport({ root, nonce, allowedActions, dispatch, allowControllerArchive = false }) {
|
|
15
|
+
export async function createOwnerTransport({ root, nonce, allowedActions, dispatch, allowControllerArchive = false, allowDesktopClient = false }) {
|
|
16
16
|
const writers = new WeakMap();
|
|
17
|
-
const peers = new Set(), subscribers = new Set(), requests = new Map();
|
|
18
|
-
let latest;
|
|
17
|
+
const peers = new Set(), subscribers = new Set(), desktopSubscribers = new Set(), requests = new Map();
|
|
18
|
+
let latest, latestDesktop;
|
|
19
19
|
const send = (peer, frame) => {
|
|
20
20
|
if (peer.destroyed) return;
|
|
21
21
|
// A valid full-history status can already occupy up to 64 MiB of the
|
|
@@ -28,10 +28,10 @@ export async function createOwnerTransport({ root, nonce, allowedActions, dispat
|
|
|
28
28
|
peer.setEncoding('utf8');
|
|
29
29
|
const writer = createDesktopFrameWriter(peer);
|
|
30
30
|
writers.set(peer, writer);
|
|
31
|
-
let authenticated = false, controllerArchive = false, buffer = '';
|
|
31
|
+
let authenticated = false, controllerArchive = false, desktopClient = false, buffer = '';
|
|
32
32
|
const timeout = setTimeout(() => peer.destroy(), 5000);
|
|
33
33
|
peer.on('error', () => {});
|
|
34
|
-
peer.on('close', () => { writer.close(); clearTimeout(timeout); peers.delete(peer); subscribers.delete(peer); for (const [id, item] of requests) if (item.peer === peer) requests.delete(id); });
|
|
34
|
+
peer.on('close', () => { writer.close(); clearTimeout(timeout); peers.delete(peer); subscribers.delete(peer); desktopSubscribers.delete(peer); for (const [id, item] of requests) if (item.peer === peer) requests.delete(id); });
|
|
35
35
|
peer.on('data', chunk => {
|
|
36
36
|
buffer += chunk.toString();
|
|
37
37
|
if (buffer.length > 16384) { peer.destroy(); return; }
|
|
@@ -42,16 +42,18 @@ export async function createOwnerTransport({ root, nonce, allowedActions, dispat
|
|
|
42
42
|
if (!command || typeof command !== 'object' || Array.isArray(command)) { peer.destroy(); return; }
|
|
43
43
|
if (!authenticated) {
|
|
44
44
|
if (command.nonce !== nonce) { peer.destroy(); return; }
|
|
45
|
-
authenticated = true; controllerArchive = allowControllerArchive && command.client === 'archive-controller'; clearTimeout(timeout); peers.add(peer);
|
|
45
|
+
authenticated = true; desktopClient = allowDesktopClient && command.client === 'desktop'; controllerArchive = allowControllerArchive && command.client === 'archive-controller'; clearTimeout(timeout); peers.add(peer);
|
|
46
|
+
if (command.desktopProtocol === 1 && desktopClient) send(peer, { type: 'desktop_owner_ready', version: 1 });
|
|
46
47
|
// One-shot controls need their result, not multi-megabyte broadcasts
|
|
47
48
|
// that can fill the write queue before the result is sent.
|
|
48
49
|
if (command.subscribeStatus !== false) {
|
|
49
|
-
subscribers.add(peer);
|
|
50
|
-
|
|
50
|
+
(desktopClient ? desktopSubscribers : subscribers).add(peer);
|
|
51
|
+
const snapshot = desktopClient ? latestDesktop : latest;
|
|
52
|
+
if (snapshot) send(peer, snapshot);
|
|
51
53
|
}
|
|
52
54
|
continue;
|
|
53
55
|
}
|
|
54
|
-
if (!allowedActions.has(command.action) || typeof command.id !== 'string' || command.id.length > 128 || command.hostId && command.hostId !== 'local' && !(controllerArchive && ['session_archive', 'session_restore', 'session_provider_setup'].includes(command.action))) {
|
|
56
|
+
if ((!desktopClient && !allowedActions.has(command.action)) || typeof command.id !== 'string' || command.id.length > 128 || command.hostId && command.hostId !== 'local' && !desktopClient && !(controllerArchive && ['session_archive', 'session_restore', 'session_provider_setup'].includes(command.action))) {
|
|
55
57
|
send(peer, { type: 'result', id: command.id, ok: false, code: 'unsupported_remote_action' }); continue;
|
|
56
58
|
}
|
|
57
59
|
if (requests.size >= 256) { peer.destroy(); return; }
|
|
@@ -73,15 +75,23 @@ export async function createOwnerTransport({ root, nonce, allowedActions, dispat
|
|
|
73
75
|
if (item) { requests.delete(frame.id); send(item.peer, { ...frame, id: item.id }); }
|
|
74
76
|
} else if (frame.type === 'status' || frame.type === 'notify' && (!frame.hostId || frame.hostId === 'local')) {
|
|
75
77
|
for (const peer of subscribers) send(peer, frame);
|
|
78
|
+
if (frame.type === 'notify') for (const peer of desktopSubscribers) send(peer, frame);
|
|
76
79
|
}
|
|
77
80
|
},
|
|
81
|
+
// Local native viewers need the same complete, scoped projection as the
|
|
82
|
+
// original window. Remote observers retain their local-only catalog.
|
|
83
|
+
publishDesktop(frame) {
|
|
84
|
+
if (frame.type !== 'status') return;
|
|
85
|
+
latestDesktop = frame;
|
|
86
|
+
for (const peer of desktopSubscribers) send(peer, frame);
|
|
87
|
+
},
|
|
78
88
|
async close() { for (const peer of peers) peer.destroy(); await new Promise(resolve => server.close(resolve)); },
|
|
79
89
|
};
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
// Returns false only when no compatible owner endpoint exists. Never starts a
|
|
83
93
|
// second owner, kills a process, or converts a disconnect into an execution retry.
|
|
84
|
-
export async function relayToOwner({ root, webOrigin, input = process.stdin, output = process.stdout }) {
|
|
94
|
+
export async function relayToOwner({ root, webOrigin, input = process.stdin, output = process.stdout, desktopClient = false }) {
|
|
85
95
|
let owner;
|
|
86
96
|
try { owner = JSON.parse(await readFile(join(root, 'desktop-app.lock', 'owner.json'), 'utf8')); }
|
|
87
97
|
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
@@ -97,7 +107,11 @@ export async function relayToOwner({ root, webOrigin, input = process.stdin, out
|
|
|
97
107
|
if (webOrigin && (!owner.webOrigin || normalizeWebOrigin(owner.webOrigin) !== normalizeWebOrigin(webOrigin)))
|
|
98
108
|
throw Object.assign(new Error('The existing SIMY owner belongs to another Web address. Keep its tasks and reconnect using its saved environment.'), { code: 'remote_owner_origin_mismatch' });
|
|
99
109
|
} catch (error) { peer.destroy(); throw error; }
|
|
100
|
-
peer.write(JSON.stringify({ nonce: owner.nonce }) + '\n');
|
|
110
|
+
peer.write(JSON.stringify({ nonce: owner.nonce, ...(desktopClient ? { client: 'desktop', desktopProtocol: 1 } : {}) }) + '\n');
|
|
111
|
+
// Old owners ignore unknown hello fields and expose only the restricted SSH
|
|
112
|
+
// projection. Never attach a native window or forward its controls as though
|
|
113
|
+
// that were a complete desktop endpoint. Keep the original owner untouched.
|
|
114
|
+
if (desktopClient) await confirmDesktopOwner(peer);
|
|
101
115
|
peer.pipe(output, { end: false });
|
|
102
116
|
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
103
117
|
lines.on('line', line => { if (line.length <= 16384 && !peer.destroyed) peer.write(line + '\n'); });
|
|
@@ -107,6 +121,34 @@ export async function relayToOwner({ root, webOrigin, input = process.stdin, out
|
|
|
107
121
|
return true;
|
|
108
122
|
}
|
|
109
123
|
|
|
124
|
+
function confirmDesktopOwner(peer) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const incompatible = () => Object.assign(new Error('The running SIMY owner does not support desktop window handoff.'), { code: 'desktop_owner_update_required' });
|
|
127
|
+
let buffer = Buffer.alloc(0);
|
|
128
|
+
const finish = error => {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
peer.removeListener('data', data); peer.removeListener('close', closed); peer.removeListener('error', failed);
|
|
131
|
+
if (error) { peer.destroy(); reject(error); } else resolve();
|
|
132
|
+
};
|
|
133
|
+
const closed = () => finish(incompatible());
|
|
134
|
+
const failed = error => finish(error);
|
|
135
|
+
const data = chunk => {
|
|
136
|
+
const newline = chunk.indexOf(10);
|
|
137
|
+
const line = newline < 0 ? chunk : chunk.subarray(0, newline);
|
|
138
|
+
if (buffer.length + line.length > 16384) { finish(incompatible()); return; }
|
|
139
|
+
buffer = Buffer.concat([buffer, line]);
|
|
140
|
+
if (newline < 0) return;
|
|
141
|
+
let frame; try { frame = JSON.parse(buffer.toString('utf8')); } catch { finish(incompatible()); return; }
|
|
142
|
+
if (frame?.type !== 'desktop_owner_ready' || frame.version !== 1) { finish(incompatible()); return; }
|
|
143
|
+
peer.pause();
|
|
144
|
+
if (newline + 1 < chunk.length) peer.unshift(chunk.subarray(newline + 1));
|
|
145
|
+
finish();
|
|
146
|
+
};
|
|
147
|
+
const timer = setTimeout(closed, 5000);
|
|
148
|
+
peer.on('data', data); peer.once('close', closed); peer.once('error', failed);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
110
152
|
// A terminal command attaches to the existing execution owner. It never starts
|
|
111
153
|
// another supervisor or retries a mutation after a lost response.
|
|
112
154
|
export async function requestOwnerArchive({ root, command, signal, timeoutMs = 55_000 }) {
|
|
@@ -6,7 +6,7 @@ const messages = {
|
|
|
6
6
|
claude_desktop_accessibility_required: 'Enable macOS Accessibility for the app running SIMY on the conversation host. Use simy sessions provider-setup --provider claude --host ALIAS --session UUID to open that host’s setting.',
|
|
7
7
|
claude_desktop_ui_unavailable: 'Open the exact Claude Desktop Code conversation on the selected host and retry.',
|
|
8
8
|
claude_desktop_ui_changed: 'The Claude Desktop window changed or is covered. Retry when the selected conversation is visible.',
|
|
9
|
-
claude_desktop_running: 'Stop this conversation and turn off
|
|
9
|
+
claude_desktop_running: 'Stop this conversation and turn off Delivery Loop before organizing it.',
|
|
10
10
|
claude_desktop_unsaved_input: 'Claude Desktop contains unsent input. Preserve it on the conversation host before retrying.',
|
|
11
11
|
claude_desktop_identity_unverified: 'Claude Desktop conversation identity could not be verified. Refresh the exact conversation.',
|
|
12
12
|
claude_desktop_identity_changed: 'Claude Desktop account or conversation changed. Refresh before retrying.',
|
|
@@ -24,7 +24,7 @@ const messages = {
|
|
|
24
24
|
session_model_unavailable: 'Select a model in the original remote conversation, then refresh.',
|
|
25
25
|
model_metadata_unavailable: 'Select a model in the original remote conversation, then refresh.',
|
|
26
26
|
session_workspace_changed: 'The remote workspace changed. Refresh and select the conversation again.',
|
|
27
|
-
session_archived: 'Restore the original remote conversation before
|
|
27
|
+
session_archived: 'Restore the original remote conversation before turning on Delivery Loop.',
|
|
28
28
|
existing_loop: 'Use this remote conversation’s existing task controls.',
|
|
29
29
|
task_stopped: 'This remote task was stopped. Review the original conversation and explicitly request resumption.',
|
|
30
30
|
task_settled: 'The remote task has finished. Refresh and select its explicit follow-up controls.',
|
|
@@ -194,10 +194,18 @@ export function createRemoteHostManager({ root, store, spawnImpl = spawn, onChan
|
|
|
194
194
|
if (status.controllerEventsVersion === 1) void pullEvents(connection).catch(() => {});
|
|
195
195
|
else for (const event of status.controllerEvents || []) void receiveNotification(connection, event)?.catch(() => {});
|
|
196
196
|
const expectedScope = preferenceScope;
|
|
197
|
-
|
|
197
|
+
connection.statusPersistence = preferences.change(expectedScope, state => {
|
|
198
198
|
for (const row of status.sessionSupervision.sessions) {
|
|
199
199
|
const key = JSON.stringify([connection.id, row.provider, row.session]);
|
|
200
200
|
const saved = state.conversations[key] ||= {};
|
|
201
|
+
// A lost command response is not proof of a failed save. Only
|
|
202
|
+
// this newly received, authorized host frame may resolve it;
|
|
203
|
+
// cached/offline projections and a requested value cannot.
|
|
204
|
+
const auto = saved.policy?.session_set_auto_continue;
|
|
205
|
+
if (['unknown', 'confirming'].includes(auto?.state) &&
|
|
206
|
+
typeof row.autoContinue === 'boolean' && row.autoContinue === auto.command.enabled) {
|
|
207
|
+
auto.state = 'applied'; delete auto.code;
|
|
208
|
+
}
|
|
201
209
|
if (['ready','partial'].includes(row.statistics?.state)) saved.statistics = { ...row.statistics, observedAt: row.statistics.observedAt || now() };
|
|
202
210
|
if (!saved.importedRemoteDisplay) {
|
|
203
211
|
saved.pinned ??= row.listPinned === true;
|
|
@@ -209,7 +217,7 @@ export function createRemoteHostManager({ root, store, spawnImpl = spawn, onChan
|
|
|
209
217
|
state.hosts ||= {};
|
|
210
218
|
state.hosts[connection.id] = { sessionSupervision: status.sessionSupervision, deliveryHooks: status.deliveryHooks,
|
|
211
219
|
controllerCompletions: [...new Map([...(state.hosts[connection.id]?.controllerCompletions || []), ...(status.controllerCompletions || [])].map(item => [JSON.stringify([item.provider,item.provider_session_id,item.task_id]),item])).values()], tasks: status.tasks, sqmTasks: status.sqmTasks };
|
|
212
|
-
}).catch(() => {});
|
|
220
|
+
}).then(changed).catch(() => {});
|
|
213
221
|
}
|
|
214
222
|
changed();
|
|
215
223
|
} else if (frame.type === 'notify' && connection.ready && local && typeof frame.id === 'string' && ['finished','failed','paused','needs_decision'].includes(frame.kind)) {
|
|
@@ -508,6 +516,7 @@ export function createRemoteHostManager({ root, store, spawnImpl = spawn, onChan
|
|
|
508
516
|
connection.flushing = true;
|
|
509
517
|
const expectedScope = preferenceScope;
|
|
510
518
|
try {
|
|
519
|
+
await connection.statusPersistence;
|
|
511
520
|
const state = await preferences.load(expectedScope);
|
|
512
521
|
const pending = Object.entries(state.conversations).flatMap(([key, row]) => {
|
|
513
522
|
if (JSON.parse(key)[0] !== connection.id) return [];
|
|
@@ -536,7 +545,9 @@ export function createRemoteHostManager({ root, store, spawnImpl = spawn, onChan
|
|
|
536
545
|
await preferences.change(expectedScope, data => {
|
|
537
546
|
const current = data.conversations[key]?.policy?.[item.command.action];
|
|
538
547
|
if (current?.command.id === item.command.id) {
|
|
539
|
-
current.state = result.nextState || (result.ok
|
|
548
|
+
current.state = result.nextState || (result.ok
|
|
549
|
+
? item.command.action === 'session_set_auto_continue' ? 'confirming' : 'applied'
|
|
550
|
+
: result.code === 'remote_operation_unknown' ? 'unknown' : 'failed');
|
|
540
551
|
current.code = result.code;
|
|
541
552
|
}
|
|
542
553
|
});
|
|
@@ -99,14 +99,14 @@ export function sessionWorkState(candidate, run, { now = Date.now(), provider }
|
|
|
99
99
|
// Saved consent is not execution: name the real dispatch blocker rather than
|
|
100
100
|
// presenting a fenced conversation as waiting for its AI.
|
|
101
101
|
if (candidate.autoContinue === true && candidate.dispatchReadiness?.state === 'blocked' && !['completed', 'stopped'].includes(run?.state))
|
|
102
|
-
return result('blocked', '
|
|
102
|
+
return result('blocked', 'Delivery Loop の作業を送信できません', candidate.autoContinueReason || 'Delivery Loop はオンですが、作業を送信できません。',
|
|
103
103
|
candidate.dispatchReadiness.code === 'update_recovery_required' ? 'update_reconnect' : candidate.dispatchReadiness.code === 'writer_reconnect_required' ? 'reconnect_original' : 'open_original', true);
|
|
104
104
|
if (run?.state === 'verification_pending' &&
|
|
105
105
|
(onlyManualAcceptancePending(run) || candidate.autoContinueStatus === 'needs_user'))
|
|
106
106
|
return result('unfinished', '残りの作業あり', '実際の結果の確認が残っています。', 'open_original', true);
|
|
107
107
|
if (candidate.autoContinue === true && ['starting', 'recovering'].includes(candidate.autoContinueStatus))
|
|
108
|
-
return result(candidate.autoContinueStatus, candidate.autoContinueStatus === 'starting' ? '
|
|
109
|
-
candidate.autoContinueReason || '
|
|
108
|
+
return result(candidate.autoContinueStatus, candidate.autoContinueStatus === 'starting' ? 'Delivery Loop を準備中' : '自動復旧中',
|
|
109
|
+
candidate.autoContinueReason || 'Delivery Loop はオンで保存されています。');
|
|
110
110
|
if ((run?.autoRecovery || run?.managed?.autoRecovery) && ['recovery_required', 'auto_resuming'].includes(run.state)) return result('checking', 'AIが状況確認中', '自動復旧の結果を待っています。');
|
|
111
111
|
if (candidate.autoContinue === true && candidate.autoContinueStatus === 'supervising' && run?.state === 'verification_pending')
|
|
112
112
|
return result('checking', '結果を確認中', '作業の結果を自動で確認しています。');
|
package/src/sqm/agent-skill.js
CHANGED
|
@@ -8,7 +8,7 @@ import { hookRuntimeCommand } from "../hook-runtime.js";
|
|
|
8
8
|
import { inspectClaudePlugin, installClaudePlugin } from "./claude-plugin.js";
|
|
9
9
|
|
|
10
10
|
export const SQM_AGENT_SKILL_ID = "simy-sqm-pr-check";
|
|
11
|
-
export const SQM_AGENT_SKILL_VERSION = "3.
|
|
11
|
+
export const SQM_AGENT_SKILL_VERSION = "3.5.0";
|
|
12
12
|
export const SIMY_CODEX_PLUGIN_ID = "simy";
|
|
13
13
|
export const SIMY_CODEX_PLUGIN_VERSION = "0.1.0";
|
|
14
14
|
|
|
@@ -22,10 +22,10 @@ const LEGACY_SKILL_DIGEST = "c9ab46e855280196dbc93fd36ba849418e0c4e500dd6e4b7b03
|
|
|
22
22
|
|
|
23
23
|
const SKILL_CONTENT = `---
|
|
24
24
|
name: ${SQM_AGENT_SKILL_ID}
|
|
25
|
-
description:
|
|
25
|
+
description: Apply a SIMY SQM check as a gate before creating or proposing a pull request, only after the user opted in with @simy, this SQM skill, or a request to use SQM for the task. An immediate SQM check of the current work uses the plugin sqm-loop entry; Delivery Loop uses the separate continue entry. Ordinary PR creation or git push alone does not activate this skill.
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
-
# SIMY SQM PR
|
|
28
|
+
# SIMY SQM check before a PR
|
|
29
29
|
|
|
30
30
|
Apply this workflow only when the user has explicitly requested quality checks with \`@simy\`, invoked this SQM skill, or requested SQM for the current task. A request to start or continue the original task uses SIMY's \`continue\` entry; selecting that entry does not request SQM. A quoted mention, repository organization, installed plugin, or ordinary request to create a PR is not opt-in. Honor a later request to stop using SQM.
|
|
31
31
|
|
package/src/sqm/claude-plugin.js
CHANGED
|
@@ -97,7 +97,7 @@ export async function claudePluginFiles() {
|
|
|
97
97
|
for (const [name, contents] of [...files].sort(([a], [b]) => a.localeCompare(b))) hash.update(name).update("\0").update(contents);
|
|
98
98
|
const manifest = {
|
|
99
99
|
name: "simy", version: `${sourceManifest.version.split("+")[0]}+claude.${hash.digest("hex").slice(0, 20)}`,
|
|
100
|
-
description: "SIMY
|
|
100
|
+
description: "SIMY Delivery Loop through the original conversation controller, SQM checks, and PR review for Claude Code.",
|
|
101
101
|
author: sourceManifest.author, repository: sourceManifest.repository,
|
|
102
102
|
license: sourceManifest.license, keywords: sourceManifest.keywords,
|
|
103
103
|
};
|