@awak-app/simy-cli 0.6.1 → 0.6.4
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 +14 -0
- package/package.json +2 -2
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/README.md +16 -22
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +76 -101
- package/src/codex-plugins/simy/hooks/outcome-progress.mjs +10 -0
- package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +1 -1
- package/src/delivery-hooks/command.js +12 -16
- package/src/delivery-hooks/desktop.js +6 -14
- package/src/delivery-hooks/supervisor.js +7 -31
- package/src/desktop-host.js +22 -16
- package/src/hook-runtime.js +29 -0
- package/src/login-shell-path.js +67 -0
- package/src/session-supervision/action-association-host.js +1 -1
- package/src/session-supervision/action-association.js +2 -2
- package/src/session-supervision/codex-app-server.js +4 -20
- package/src/session-supervision/handoff.js +2 -1
- package/src/session-supervision/index.js +11 -16
- package/src/session-supervision/stop-notifications.js +43 -0
- package/src/sqm/agent-skill.js +2 -12
- package/src/sqm/claude-plugin.js +2 -4
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { deliveryStop } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
3
|
+
|
|
4
|
+
// Auto-continue works while nobody is watching, so a person must learn when it
|
|
5
|
+
// stops. Map a stopped run to a notification kind; the native window owns copy.
|
|
6
|
+
export function stopNotificationKind(run, now = Date.now()) {
|
|
7
|
+
if (!run?.managed?.autoContinueEpoch) return null;
|
|
8
|
+
// The clock keeps aging while paused; the persisted human decision takes
|
|
9
|
+
// precedence over a threshold that was reached only after that decision.
|
|
10
|
+
if (run.state === 'paused') return run.reason === 'Paused by user.' ? null : deliveryStop(run, now) ? 'paused' : null;
|
|
11
|
+
if (run.state === 'blocked' || run.state === 'verification_pending') return 'needs_decision';
|
|
12
|
+
if (run.state === 'recovery_required' && run.managed.autoRecovery !== true) return 'needs_decision';
|
|
13
|
+
if (run.state === 'failed' || run.state === 'budget_exhausted') return 'failed';
|
|
14
|
+
if (run.state === 'completed' || run.state === 'reported_done') return 'finished';
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The first observation is a baseline: restarting SIMY must not replay alerts
|
|
19
|
+
// for runs that stopped earlier. Each distinct stop is announced once.
|
|
20
|
+
export function createStopNotifier({ emit, now = Date.now } = {}) {
|
|
21
|
+
// Retain one stop fingerprint per currently observed task, rather than an
|
|
22
|
+
// event cache whose eviction could replay the entire historical baseline.
|
|
23
|
+
const seen = new Map();
|
|
24
|
+
let baseline = true;
|
|
25
|
+
return {
|
|
26
|
+
observe(records) {
|
|
27
|
+
const present = new Set();
|
|
28
|
+
for (const record of records) {
|
|
29
|
+
const run = record?.run, kind = stopNotificationKind(run, now());
|
|
30
|
+
const key = JSON.stringify([record.provider, record.session, run?.managed?.taskId]);
|
|
31
|
+
present.add(key);
|
|
32
|
+
if (!kind) { seen.delete(key); continue; }
|
|
33
|
+
const id = createHash('sha256').update(JSON.stringify([record.provider, record.session, run.managed.taskId, run.state, run.revision, run.reason])).digest('hex').slice(0, 32);
|
|
34
|
+
if (seen.get(key) === id) continue;
|
|
35
|
+
seen.set(key, id);
|
|
36
|
+
if (!baseline) emit({ type: 'notify', id, kind, provider: record.provider, session: record.session,
|
|
37
|
+
objective: String(run.objective || '').slice(0, 200) });
|
|
38
|
+
}
|
|
39
|
+
for (const key of seen.keys()) if (!present.has(key)) seen.delete(key);
|
|
40
|
+
baseline = false;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
package/src/sqm/agent-skill.js
CHANGED
|
@@ -3,6 +3,7 @@ import { cp, lstat, mkdir, readFile, readdir, rename, rm, writeFile } from "node
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { hookRuntimeCommand } from "../hook-runtime.js";
|
|
6
7
|
|
|
7
8
|
import { inspectClaudePlugin, installClaudePlugin } from "./claude-plugin.js";
|
|
8
9
|
|
|
@@ -328,25 +329,14 @@ async function bundledPluginManifest({ overrides = new Map() } = {}) {
|
|
|
328
329
|
async function installedPluginOverrides() {
|
|
329
330
|
const relative = "hooks/hooks.json";
|
|
330
331
|
const source = JSON.parse(await readFile(path.join(BUNDLED_CODEX_PLUGIN_ROOT, relative), "utf8"));
|
|
331
|
-
const commandPrefix = "node ";
|
|
332
|
-
const runtime = shellQuote(process.execPath);
|
|
333
332
|
for (const groups of Object.values(source.hooks || {})) {
|
|
334
333
|
for (const group of groups || []) {
|
|
335
|
-
for (const hook of group.hooks || [])
|
|
336
|
-
if (typeof hook.command === "string" && hook.command.startsWith(commandPrefix)) {
|
|
337
|
-
hook.command = `${runtime} ${hook.command.slice(commandPrefix.length)}`;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
334
|
+
for (const hook of group.hooks || []) hook.command = hookRuntimeCommand(hook.command);
|
|
340
335
|
}
|
|
341
336
|
}
|
|
342
337
|
return new Map([[relative, `${JSON.stringify(source, null, 2)}\n`]]);
|
|
343
338
|
}
|
|
344
339
|
|
|
345
|
-
function shellQuote(value) {
|
|
346
|
-
if (process.platform === "win32") return `"${value.replaceAll('"', '""')}"`;
|
|
347
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
340
|
async function directoryDigest(root, { missing = "throw", manifest, overrides = new Map() } = {}) {
|
|
351
341
|
let rootStat;
|
|
352
342
|
try {
|
package/src/sqm/claude-plugin.js
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdir, readFile, readdir, rename, rm, lstat, writeFile } from "node:fs/
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { hookRuntimeCommand } from "../hook-runtime.js";
|
|
6
7
|
|
|
7
8
|
const SOURCE = fileURLToPath(new URL("../codex-plugins/simy/", import.meta.url));
|
|
8
9
|
export const CLAUDE_MARKETPLACE = "simy-local";
|
|
@@ -43,7 +44,7 @@ export async function claudePluginFiles() {
|
|
|
43
44
|
} else if (event === "Notification") {
|
|
44
45
|
hook.statusMessage = "SIMY Delivery Loop permission-notification tracking";
|
|
45
46
|
}
|
|
46
|
-
hook.command = hook.command
|
|
47
|
+
hook.command = hookRuntimeCommand(hook.command)
|
|
47
48
|
.replaceAll("process.env.PLUGIN_ROOT", "process.env.CLAUDE_PLUGIN_ROOT")
|
|
48
49
|
.replaceAll("hook.main('codex')", "hook.main('claude')");
|
|
49
50
|
}
|
|
@@ -136,6 +137,3 @@ function json(value) { return `${JSON.stringify(value, null, 2)}\n`; }
|
|
|
136
137
|
async function optionalRead(file) {
|
|
137
138
|
try { return await readFile(file, "utf8"); } catch (error) { if (error.code === "ENOENT") return null; throw error; }
|
|
138
139
|
}
|
|
139
|
-
function quoteRuntime(value) {
|
|
140
|
-
return process.platform === "win32" ? `"${value.replaceAll('"', '""')}"` : `'${value.replaceAll("'", "'\\''")}'`;
|
|
141
|
-
}
|