@awak-app/simy-cli 0.7.28 → 0.7.31
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 +88 -3
- package/package.json +4 -4
- package/src/agent.js +1 -0
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +1 -1
- package/src/codex-plugins/simy/.codex-plugin/plugin.json +5 -3
- package/src/codex-plugins/simy/hooks/acceptance-policy.mjs +7 -0
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +570 -65
- package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +6 -3
- package/src/codex-plugins/simy/hooks/purpose-search.mjs +23 -4
- package/src/codex-plugins/simy/hooks/scene-reasoning.mjs +2 -0
- package/src/codex-plugins/simy/hooks/update-dispatch-gate.mjs +126 -4
- package/src/codex-plugins/simy/skills/continue/SKILL.md +69 -0
- package/src/codex-plugins/simy/skills/continue/agents/openai.yaml +6 -0
- package/src/delivery-hooks/claude-workflow.md +3 -3
- package/src/delivery-hooks/command.js +12 -0
- package/src/delivery-hooks/desktop.js +31 -9
- package/src/delivery-hooks/supervisor.js +3 -2
- package/src/desktop-access.js +4 -1
- package/src/desktop-executor.js +6 -0
- package/src/desktop-host.js +494 -72
- package/src/native-development-report-completion.js +26 -0
- package/src/native-development-reports.js +15 -5
- package/src/orchestrator/budget.js +24 -0
- package/src/orchestrator/charter-repair.js +1 -1
- package/src/orchestrator/completion-contract.js +8 -2
- package/src/orchestrator/contract.js +3 -1
- package/src/orchestrator/independent-audit.js +13 -6
- package/src/orchestrator/instruction.js +1 -1
- package/src/orchestrator/loop.js +34 -4
- package/src/provider-stream.js +9 -2
- package/src/runner.js +44 -21
- package/src/session-supervision/action-deadline.js +18 -1
- package/src/session-supervision/autonomous-upgrade.js +4 -2
- package/src/session-supervision/claude-desktop-archive.js +57 -0
- package/src/session-supervision/claude-desktop-native.js +50 -0
- package/src/session-supervision/claude-desktop-native.jxa.js +195 -0
- package/src/session-supervision/claude-desktop-target.js +87 -0
- package/src/session-supervision/claude-outcome-review.js +132 -0
- package/src/session-supervision/claude-owner-renewal.js +197 -0
- package/src/session-supervision/claude-review-definition.js +5 -0
- package/src/session-supervision/claude-sessions.js +210 -40
- package/src/session-supervision/codex-app-server.js +189 -36
- package/src/session-supervision/continuation-analysis.js +7 -7
- package/src/session-supervision/continuation-command.js +45 -0
- package/src/session-supervision/controller-store.js +48 -0
- package/src/session-supervision/conversation-presentation.js +241 -0
- package/src/session-supervision/decision-presentation.js +96 -35
- package/src/session-supervision/execution-context.js +39 -0
- package/src/session-supervision/index.js +38 -9
- package/src/session-supervision/manual-acceptance-command.js +30 -0
- package/src/session-supervision/notification-authority.js +9 -0
- package/src/session-supervision/notification-journal.js +40 -0
- package/src/session-supervision/operation-journal.js +42 -0
- package/src/session-supervision/owner-transport.js +146 -0
- package/src/session-supervision/purpose-recovery.js +10 -2
- package/src/session-supervision/purpose-verification.js +103 -9
- package/src/session-supervision/remote-control.js +1 -1
- package/src/session-supervision/remote-entry.js +2 -0
- package/src/session-supervision/remote-errors.js +15 -0
- package/src/session-supervision/remote-hosts.js +344 -36
- package/src/session-supervision/remote-metadata.js +103 -0
- package/src/session-supervision/remote-session-list.js +2 -4
- package/src/session-supervision/session-archive-command.js +45 -0
- package/src/session-supervision/session-list.js +40 -4
- package/src/session-supervision/sqm-presentation.js +46 -3
- package/src/session-supervision/stop-notifications.js +42 -11
- package/src/session-supervision/work-state.js +106 -6
- package/src/sqm/agent-skill.js +3 -3
- package/src/sqm/claude-plugin.js +7 -2
- package/src/sqm/history-store.js +6 -0
- package/src/team-threads.js +1 -1
- package/src/update-detachment/coordinator.js +7 -1
- package/src/update-detachment/history-recovery.js +18 -0
- package/src/update-detachment/ledger-adapter.js +1 -0
- package/src/update-detachment/retained-paused.js +290 -0
- package/src/update-detachment/runtime.js +78 -15
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
import { createClaudeDesktopNativeAdapter } from './claude-desktop-native.js';
|
|
3
|
+
import { resolveClaudeDesktopTarget } from './claude-desktop-target.js';
|
|
4
|
+
|
|
5
|
+
const failure = (code, message) => Object.assign(new Error(message), { code });
|
|
6
|
+
const sameIdentity = (a, b) => ['desktopId', 'session', 'cwd', 'partition'].every(key => a[key] === b[key]);
|
|
7
|
+
|
|
8
|
+
// One native Desktop window is shared by all conversations. Serialize operations
|
|
9
|
+
// across identities; a per-session queue would allow one operation to navigate
|
|
10
|
+
// away from the other operation's target.
|
|
11
|
+
export function createClaudeDesktopArchiveController({
|
|
12
|
+
readTarget = resolveClaudeDesktopTarget,
|
|
13
|
+
native = createClaudeDesktopNativeAdapter(),
|
|
14
|
+
wait = delay,
|
|
15
|
+
attempts = 60,
|
|
16
|
+
interval = 250,
|
|
17
|
+
} = {}) {
|
|
18
|
+
if (!Number.isInteger(attempts) || attempts < 1 || attempts > 120 || !Number.isFinite(interval) || interval < 0 || interval > 5000)
|
|
19
|
+
throw new TypeError('Invalid Claude Desktop readback bounds.');
|
|
20
|
+
let queue = Promise.resolve();
|
|
21
|
+
return function setArchived(candidate, archived, { signal, beforeDispatch = async () => {} } = {}) {
|
|
22
|
+
// Copy the caller's selection before queueing: later catalog refreshes must
|
|
23
|
+
// not redirect an already requested native operation.
|
|
24
|
+
const selection = { provider: candidate?.provider, session: candidate?.session, cwd: candidate?.cwd };
|
|
25
|
+
const run = async () => {
|
|
26
|
+
if (typeof archived !== 'boolean') throw new TypeError('Expected an explicit archive state.');
|
|
27
|
+
signal?.throwIfAborted();
|
|
28
|
+
const original = await readTarget(selection);
|
|
29
|
+
if (original.archived === archived) return { state: 'verified', archived, changed: false };
|
|
30
|
+
if (!native?.setArchived) throw failure('claude_desktop_unavailable', 'Native Claude Desktop archive control is unavailable on this host.');
|
|
31
|
+
await beforeDispatch();
|
|
32
|
+
signal?.throwIfAborted();
|
|
33
|
+
const current = await readTarget(selection);
|
|
34
|
+
if (!sameIdentity(original, current)) throw failure('claude_desktop_identity_changed', 'Claude Desktop conversation identity changed.');
|
|
35
|
+
if (current.archived === archived) return { state: 'verified', archived, changed: false };
|
|
36
|
+
// Dispatch at most once. A timeout may mean the provider applied the
|
|
37
|
+
// operation; reconcile its own state rather than blindly clicking again.
|
|
38
|
+
let dispatchError;
|
|
39
|
+
try { await native.setArchived(current, archived, { signal }); }
|
|
40
|
+
catch (error) { dispatchError = error; }
|
|
41
|
+
const deadline = Date.now() + 15_000;
|
|
42
|
+
for (let index = 0; index < attempts; index++) {
|
|
43
|
+
const observed = await readTarget(selection);
|
|
44
|
+
if (!sameIdentity(original, observed)) throw failure('claude_desktop_identity_changed', 'Claude Desktop conversation identity changed during native control.');
|
|
45
|
+
if (observed.archived === archived) return { state: 'verified', archived, changed: true };
|
|
46
|
+
signal?.throwIfAborted();
|
|
47
|
+
if (dispatchError?.mutationPossible === false || Date.now() >= deadline) break;
|
|
48
|
+
if (index + 1 < attempts) await wait(interval, undefined, { signal });
|
|
49
|
+
}
|
|
50
|
+
if (dispatchError) throw dispatchError;
|
|
51
|
+
throw failure('claude_desktop_state_unconfirmed', 'Claude Desktop has not confirmed the requested archive state. Refresh before retrying.');
|
|
52
|
+
};
|
|
53
|
+
const result = queue.then(run);
|
|
54
|
+
queue = result.catch(() => {});
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const execute = promisify(execFile);
|
|
5
|
+
const script = fileURLToPath(new URL('./claude-desktop-native.jxa.js', import.meta.url));
|
|
6
|
+
const failure = (code, message) => Object.assign(new Error(message), { code });
|
|
7
|
+
|
|
8
|
+
export function createClaudeDesktopNativeAdapter({ platform = process.platform, run = execute } = {}) {
|
|
9
|
+
async function invoke(request, { signal } = {}) {
|
|
10
|
+
if (platform !== 'darwin') throw failure('claude_desktop_platform_unsupported', 'Claude Desktop native archive control currently requires a macOS conversation host.');
|
|
11
|
+
signal?.throwIfAborted();
|
|
12
|
+
let stdout;
|
|
13
|
+
try {
|
|
14
|
+
({ stdout } = await run('/usr/bin/osascript', ['-l', 'JavaScript', script, JSON.stringify(request)], {
|
|
15
|
+
encoding: 'utf8', timeout: ['inspect', 'preflight'].includes(request.operation) ? 5000 : 20_000, maxBuffer: 32_768, killSignal: 'SIGKILL', signal,
|
|
16
|
+
}));
|
|
17
|
+
} catch (error) {
|
|
18
|
+
signal?.throwIfAborted();
|
|
19
|
+
throw failure('claude_desktop_native_unconfirmed', 'Claude Desktop did not return a result. Refresh its archive state before retrying.');
|
|
20
|
+
}
|
|
21
|
+
let response;
|
|
22
|
+
try { response = JSON.parse(stdout); } catch { throw failure('claude_desktop_native_unconfirmed', 'Claude Desktop returned an unreadable result.'); }
|
|
23
|
+
if (response?.ok !== true) {
|
|
24
|
+
const code = /^claude_desktop_[a-z_]+$/.test(response?.code || '') ? response.code : 'claude_desktop_ui_unavailable';
|
|
25
|
+
throw Object.assign(failure(code, typeof response?.message === 'string' && response.message.length <= 1000 ? response.message : 'Claude Desktop native control is unavailable.'), { mutationPossible: response.mutationPossible !== false });
|
|
26
|
+
}
|
|
27
|
+
return response.data;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
inspect: options => invoke({ operation: 'inspect' }, options),
|
|
31
|
+
setup: options => invoke({ operation: 'setup' }, options),
|
|
32
|
+
async setArchived(target, archived, options) {
|
|
33
|
+
if (typeof archived !== 'boolean') throw new TypeError('An explicit archive state is required.');
|
|
34
|
+
if (!/^local_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(target?.desktopId || '')) throw failure('claude_desktop_identity_unverified', 'An exact Claude Desktop identity is required.');
|
|
35
|
+
const readiness = await invoke({ operation: 'inspect' }, options);
|
|
36
|
+
if (readiness?.available !== true) throw failure('claude_desktop_accessibility_required', 'Enable Accessibility for the app running SIMY on the conversation host, then retry.');
|
|
37
|
+
const preflight = await invoke({ operation: 'preflight' }, options);
|
|
38
|
+
if (preflight?.safeToNavigate !== true) throw Object.assign(failure('claude_desktop_ui_unavailable', 'Claude Desktop navigation safety could not be confirmed.'), { mutationPossible: false });
|
|
39
|
+
try { await run('/usr/bin/open', ['-b', 'com.anthropic.claudefordesktop', 'claude://claude.ai/epitaxy/' + target.desktopId], {
|
|
40
|
+
encoding: 'utf8', timeout: 5000, maxBuffer: 4096, killSignal: 'SIGKILL', signal: options?.signal,
|
|
41
|
+
}); } catch {
|
|
42
|
+
options?.signal?.throwIfAborted();
|
|
43
|
+
throw Object.assign(failure('claude_desktop_ui_unavailable', 'Claude Desktop could not open the selected conversation. Open its Code tab and retry.'), { mutationPossible: false });
|
|
44
|
+
}
|
|
45
|
+
const result = await invoke({ operation: archived ? 'archive' : 'restore', desktopId: target.desktopId, title: target.title }, options);
|
|
46
|
+
if (result?.dispatched !== true) throw failure('claude_desktop_native_unconfirmed', 'Claude Desktop did not confirm dispatch.');
|
|
47
|
+
return result;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// macOS system JavaScript for Automation. No compiler, developer mode, private
|
|
2
|
+
// provider file writes, or embedded credentials are required on the user's Mac.
|
|
3
|
+
ObjC.import('AppKit');
|
|
4
|
+
ObjC.import('ApplicationServices');
|
|
5
|
+
ObjC.bindFunction('AXUIElementCreateApplication', ['id', ['int']]);
|
|
6
|
+
ObjC.bindFunction('AXUIElementCreateSystemWide', ['id', []]);
|
|
7
|
+
ObjC.bindFunction('AXUIElementCopyAttributeValue', ['int', ['id', 'id', 'id *']]);
|
|
8
|
+
ObjC.bindFunction('AXUIElementCopyElementAtPosition', ['int', ['id', 'float', 'float', 'id *']]);
|
|
9
|
+
ObjC.bindFunction('AXValueGetValue', ['bool', ['id', 'int', 'void *']]);
|
|
10
|
+
ObjC.bindFunction('AXIsProcessTrustedWithOptions', ['bool', ['id']]);
|
|
11
|
+
ObjC.bindFunction('AXUIElementPerformAction', ['int', ['id', 'id']]);
|
|
12
|
+
ObjC.bindFunction('AXUIElementSetAttributeValue', ['int', ['id', 'id', 'id']]);
|
|
13
|
+
ObjC.bindFunction('CFEqual', ['bool', ['id', 'id']]);
|
|
14
|
+
|
|
15
|
+
function pause(seconds) { $.NSRunLoop.currentRunLoop.runUntilDate($.NSDate.dateWithTimeIntervalSinceNow(seconds)); }
|
|
16
|
+
function failure(code, message) { var error = new Error(message); error.code = code; throw error; }
|
|
17
|
+
function attribute(element, name, requireReadable) {
|
|
18
|
+
var result = Ref(), status = $.AXUIElementCopyAttributeValue(element, $(name), result);
|
|
19
|
+
// A leaf may omit optional children. Transport/stale-element errors do not
|
|
20
|
+
// prove that a subtree is empty and must not allow navigation past drafts.
|
|
21
|
+
if (requireReadable && status !== 0 && status !== Number($.kAXErrorAttributeUnsupported) && status !== Number($.kAXErrorNoValue))
|
|
22
|
+
failure('claude_desktop_ui_unavailable', 'Claude Desktop accessibility content could not be inspected.');
|
|
23
|
+
return status === 0 ? result[0] : null;
|
|
24
|
+
}
|
|
25
|
+
function textAttribute(element, name) {
|
|
26
|
+
var value = attribute(element, name);
|
|
27
|
+
return value ? (name === 'AXURL' ? String(ObjC.unwrap(value.absoluteString)) : String(ObjC.unwrap(value))) : '';
|
|
28
|
+
}
|
|
29
|
+
function descendants(root, predicate) {
|
|
30
|
+
var result = [], count = 0;
|
|
31
|
+
function visit(element, depth) {
|
|
32
|
+
if (++count > 12000 || depth > 55) failure('claude_desktop_ui_unavailable', 'Claude Desktop accessibility tree exceeded its supported bounds.');
|
|
33
|
+
if (predicate(element)) result.push(element);
|
|
34
|
+
var children = attribute(element, 'AXChildren', true);
|
|
35
|
+
if (children) ObjC.unwrap(children).forEach(function(child) { visit(child, depth + 1); });
|
|
36
|
+
}
|
|
37
|
+
visit(root, 0); return result;
|
|
38
|
+
}
|
|
39
|
+
function hasAncestor(element, predicate) {
|
|
40
|
+
for (var index = 0; index < 60 && element; index++, element = attribute(element, 'AXParent')) if (predicate(element)) return true;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
// AXValue stores two CGFloat doubles on supported 64-bit Macs. Foundation owns
|
|
44
|
+
// the full 16-byte output buffer; do not write a CGPoint into an untyped Ref.
|
|
45
|
+
function geometry(element, name, type) {
|
|
46
|
+
var data = $.NSMutableData.dataWithLength(16), value = attribute(element, name);
|
|
47
|
+
if (!value || !$.AXValueGetValue(value, type, data.mutableBytes)) failure('claude_desktop_ui_unavailable', 'Claude control geometry is unavailable.');
|
|
48
|
+
var encoded = String(ObjC.unwrap(data.base64EncodedStringWithOptions(0)));
|
|
49
|
+
var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', bytes = [], bits = 0, accumulated = 0;
|
|
50
|
+
for (var index = 0; index < encoded.length; index++) {
|
|
51
|
+
var digit = alphabet.indexOf(encoded[index]); if (digit < 0) break;
|
|
52
|
+
accumulated = (accumulated << 6) | digit; bits += 6;
|
|
53
|
+
if (bits >= 8) { bits -= 8; bytes.push((accumulated >> bits) & 255); }
|
|
54
|
+
}
|
|
55
|
+
if (bytes.length !== 16) failure('claude_desktop_ui_unavailable', 'Invalid Claude control geometry.');
|
|
56
|
+
var view = new DataView(new Uint8Array(bytes).buffer);
|
|
57
|
+
return [view.getFloat64(0, true), view.getFloat64(8, true)];
|
|
58
|
+
}
|
|
59
|
+
function ensureNoActiveWork(root) {
|
|
60
|
+
descendants(root, function(element) {
|
|
61
|
+
var role = textAttribute(element, 'AXRole');
|
|
62
|
+
if (!role) failure('claude_desktop_ui_unavailable', 'Claude Desktop element identity could not be inspected.');
|
|
63
|
+
if (['AXSheet', 'AXDialog'].indexOf(role) >= 0 || textAttribute(element, 'AXSubrole') === 'AXDialog')
|
|
64
|
+
failure('claude_desktop_ui_changed', 'Close the Claude Desktop dialog before changing conversations.');
|
|
65
|
+
if (role === 'AXButton' && ['Stop', 'Stop response', 'Stop generating', '停止', '応答を停止', '生成を停止'].indexOf(textAttribute(element, 'AXTitle') || textAttribute(element, 'AXDescription')) >= 0)
|
|
66
|
+
failure('claude_desktop_running', 'Claude Desktop is generating a response. Wait before changing conversations.');
|
|
67
|
+
if (['AXTextArea', 'AXTextField', 'AXComboBox'].indexOf(role) >= 0) {
|
|
68
|
+
var value = attribute(element, 'AXValue');
|
|
69
|
+
if (value === null) failure('claude_desktop_ui_unavailable', 'Claude Desktop input state could not be inspected.');
|
|
70
|
+
if (String(ObjC.unwrap(value)).trim()) failure('claude_desktop_unsaved_input', 'Claude Desktop contains unsent input. Preserve it before changing conversations.');
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function perform(input) {
|
|
76
|
+
if (!input || ['inspect', 'setup', 'preflight', 'archive', 'restore'].indexOf(input.operation) < 0) failure('claude_desktop_invalid_request', 'Invalid Claude Desktop operation.');
|
|
77
|
+
var bundle = 'com.anthropic.claudefordesktop', workspace = $.NSWorkspace.sharedWorkspace;
|
|
78
|
+
var appURL = workspace.URLForApplicationWithBundleIdentifier(bundle);
|
|
79
|
+
if (!appURL || appURL.isNil()) failure('claude_desktop_not_installed', 'Install Claude Desktop on the conversation host and sign in to its Code tab.');
|
|
80
|
+
var version = String(ObjC.unwrap($.NSBundle.bundleWithURL(appURL).objectForInfoDictionaryKey('CFBundleShortVersionString')));
|
|
81
|
+
if (input.operation === 'setup') {
|
|
82
|
+
$.AXIsProcessTrustedWithOptions($({ AXTrustedCheckOptionPrompt: true }));
|
|
83
|
+
// Open the real OS setting. Never grant accessibility or change TCC records.
|
|
84
|
+
workspace.openURL($.NSURL.URLWithString('x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility'));
|
|
85
|
+
}
|
|
86
|
+
var trusted = Boolean($.AXIsProcessTrusted());
|
|
87
|
+
if (['inspect', 'setup'].indexOf(input.operation) >= 0) return { available: trusted, version: version, code: trusted ? 'ready' : 'claude_desktop_accessibility_required' };
|
|
88
|
+
if (!trusted) failure('claude_desktop_accessibility_required', 'Enable Accessibility for the app running SIMY in macOS System Settings, then retry. Run simy sessions provider-setup --provider claude on this host to open the setting.');
|
|
89
|
+
if (input.operation === 'preflight') {
|
|
90
|
+
var existingApps = ObjC.unwrap($.NSRunningApplication.runningApplicationsWithBundleIdentifier(bundle));
|
|
91
|
+
if (existingApps.length === 0) return { safeToNavigate: true };
|
|
92
|
+
if (existingApps.length !== 1) failure('claude_desktop_ui_unavailable', 'Claude Desktop application identity is ambiguous.');
|
|
93
|
+
var existingAX = $.AXUIElementCreateApplication(existingApps[0].processIdentifier);
|
|
94
|
+
// Electron may initially expose only its window chrome. Request its AX
|
|
95
|
+
// tree without navigating or granting any OS permission, then wait briefly
|
|
96
|
+
// for content. Unsupported attributes are harmless; actual reads decide.
|
|
97
|
+
$.AXUIElementSetAttributeValue(existingAX, $('AXManualAccessibility'), $(true));
|
|
98
|
+
for (var attempt = 0; attempt < 11; attempt++) {
|
|
99
|
+
var windows = attribute(existingAX, 'AXWindows', true);
|
|
100
|
+
if (!windows || ObjC.unwrap(windows).length === 0) failure('claude_desktop_ui_unavailable', 'Claude Desktop windows could not be inspected before navigation.');
|
|
101
|
+
var ready = true;
|
|
102
|
+
ObjC.unwrap(windows).forEach(function(window) {
|
|
103
|
+
ensureNoActiveWork(window);
|
|
104
|
+
if (descendants(window, function(element) { return textAttribute(element, 'AXRole') === 'AXWebArea'; }).length === 0) ready = false;
|
|
105
|
+
});
|
|
106
|
+
if (ready) return { safeToNavigate: true };
|
|
107
|
+
if (attempt < 10) pause(0.2);
|
|
108
|
+
}
|
|
109
|
+
failure('claude_desktop_ui_unavailable', 'Claude Desktop conversation content could not be inspected before navigation.');
|
|
110
|
+
}
|
|
111
|
+
if (!/^local_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(input.desktopId || '') || typeof input.title !== 'string' || !input.title.trim() || input.title.length > 512)
|
|
112
|
+
failure('claude_desktop_invalid_request', 'An exact Claude Desktop conversation is required.');
|
|
113
|
+
var apps = ObjC.unwrap($.NSRunningApplication.runningApplicationsWithBundleIdentifier(bundle));
|
|
114
|
+
if (apps.length === 0) {
|
|
115
|
+
workspace.openURL(appURL);
|
|
116
|
+
for (var launch = 0; launch < 30 && apps.length === 0; launch++) { pause(0.2); apps = ObjC.unwrap($.NSRunningApplication.runningApplicationsWithBundleIdentifier(bundle)); }
|
|
117
|
+
}
|
|
118
|
+
if (apps.length !== 1) failure('claude_desktop_ui_unavailable', 'Open a single Claude Desktop application and retry.');
|
|
119
|
+
var running = apps[0], app = $.AXUIElementCreateApplication(running.processIdentifier);
|
|
120
|
+
// The resume/import deep link may change archive state. Use navigation only.
|
|
121
|
+
// The parent opens the navigation URL through LaunchServices before invoking us.
|
|
122
|
+
// LaunchServices already activates the deep-linked window; let its AX tree
|
|
123
|
+
// settle without issuing another application activation.
|
|
124
|
+
pause(0.2);
|
|
125
|
+
function targetArea() {
|
|
126
|
+
var matches = descendants(app, function(element) {
|
|
127
|
+
return textAttribute(element, 'AXRole') === 'AXWebArea' && ['https://claude.ai/epitaxy/' + input.desktopId, 'https://claude.ai/code/' + input.desktopId].indexOf(textAttribute(element, 'AXURL')) >= 0;
|
|
128
|
+
});
|
|
129
|
+
if (matches.length !== 1) failure('claude_desktop_ui_unavailable', 'The exact Claude Desktop conversation is not uniquely visible. Open its Code tab and retry.');
|
|
130
|
+
return matches[0];
|
|
131
|
+
}
|
|
132
|
+
function ensureActionTarget(element) {
|
|
133
|
+
var currentArea = targetArea();
|
|
134
|
+
if (!running.active) failure('claude_desktop_ui_changed', 'Claude Desktop lost focus before the operation. Retry when it is visible.');
|
|
135
|
+
if (!hasAncestor(element, function(parent) { return $.CFEqual(parent, currentArea); }))
|
|
136
|
+
failure('claude_desktop_ui_changed', 'The selected Claude control no longer belongs to the requested conversation. Refresh before retrying.');
|
|
137
|
+
}
|
|
138
|
+
function click(element) {
|
|
139
|
+
ensureActionTarget(element);
|
|
140
|
+
var position = geometry(element, 'AXPosition', 1), size = geometry(element, 'AXSize', 2);
|
|
141
|
+
var x = position[0] + size[0] / 2, y = position[1] + size[1] / 2, hit = Ref();
|
|
142
|
+
if (!isFinite(x) || !isFinite(y) || size[0] <= 0 || size[1] <= 0) failure('claude_desktop_ui_unavailable', 'Claude control is not visible.');
|
|
143
|
+
var status = $.AXUIElementCopyElementAtPosition($.AXUIElementCreateSystemWide(), x, y, hit);
|
|
144
|
+
if (status !== 0 || !hasAncestor(hit[0], function(parent) { return $.CFEqual(parent, element); }))
|
|
145
|
+
failure('claude_desktop_ui_changed', 'Another window or control covers the selected Claude action. Retry when it is visible.');
|
|
146
|
+
var point = $.CGPointMake(x, y);
|
|
147
|
+
$.CGEventPost(0, $.CGEventCreateMouseEvent(null, 1, point, 0));
|
|
148
|
+
$.CGEventPost(0, $.CGEventCreateMouseEvent(null, 2, point, 0));
|
|
149
|
+
}
|
|
150
|
+
var area;
|
|
151
|
+
for (var attempt = 0; attempt < 40; attempt++) {
|
|
152
|
+
try { area = targetArea(); break; } catch (error) { if (attempt === 39) throw error; pause(0.2); }
|
|
153
|
+
}
|
|
154
|
+
// Do not archive an actively generating turn or accept a provider permission,
|
|
155
|
+
// discard, or trust dialog. Those belong to the provider, not to SIMY.
|
|
156
|
+
function ensureIdle() {
|
|
157
|
+
ensureNoActiveWork(app);
|
|
158
|
+
}
|
|
159
|
+
ensureIdle();
|
|
160
|
+
var controls;
|
|
161
|
+
if (input.operation === 'archive') {
|
|
162
|
+
var labels = [input.title + 'のその他のオプション', 'More options for ' + input.title];
|
|
163
|
+
controls = descendants(area, function(element) {
|
|
164
|
+
return textAttribute(element, 'AXRole') === 'AXPopUpButton' && labels.indexOf(textAttribute(element, 'AXDescription')) >= 0 && !hasAncestor(element, function(parent) {
|
|
165
|
+
return ['サイドバー', 'Sidebar'].indexOf(textAttribute(parent, 'AXDescription')) >= 0 || textAttribute(parent, 'AXDOMIdentifier') === 'frame-peek-popover';
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
if (controls.length !== 1) failure('claude_desktop_ui_unavailable', 'The exact Claude conversation menu is unavailable or ambiguous.');
|
|
169
|
+
var popup = controls[0];
|
|
170
|
+
function archiveItems() {
|
|
171
|
+
return descendants(targetArea(), function(element) {
|
|
172
|
+
return textAttribute(element, 'AXRole') === 'AXMenuItem' && ['アーカイブ', 'Archive'].indexOf(textAttribute(element, 'AXTitle')) >= 0 && hasAncestor(element, function(parent) { return labels.indexOf(textAttribute(parent, 'AXTitle')) >= 0; });
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
controls = archiveItems();
|
|
176
|
+
if (controls.length === 0) {
|
|
177
|
+
ensureActionTarget(popup);
|
|
178
|
+
$.AXUIElementPerformAction(popup, $('AXPress')); pause(0.6);
|
|
179
|
+
controls = archiveItems();
|
|
180
|
+
if (controls.length === 0) { click(popup); pause(0.6); controls = archiveItems(); }
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
controls = descendants(area, function(element) { return textAttribute(element, 'AXRole') === 'AXButton' && ['アーカイブ解除', 'Unarchive'].indexOf(textAttribute(element, 'AXTitle')) >= 0; });
|
|
184
|
+
}
|
|
185
|
+
if (controls.length !== 1) failure('claude_desktop_ui_unavailable', 'The native archive or restore action is unavailable or ambiguous.');
|
|
186
|
+
ensureIdle();
|
|
187
|
+
ensureActionTarget(controls[0]);
|
|
188
|
+
if ($.AXUIElementPerformAction(controls[0], $('AXPress')) !== 0) failure('claude_desktop_native_unconfirmed', 'The native Claude action did not return a confirmed result.');
|
|
189
|
+
// Only the caller's independent provider readback can report success.
|
|
190
|
+
return { dispatched: true, version: version };
|
|
191
|
+
}
|
|
192
|
+
function run(argv) {
|
|
193
|
+
try { return JSON.stringify({ ok: true, data: perform(JSON.parse(argv[0])) }); }
|
|
194
|
+
catch (error) { return JSON.stringify({ ok: false, mutationPossible: !error.code || error.code === 'claude_desktop_native_unconfirmed', code: error.code || 'claude_desktop_ui_unavailable', message: error.code ? error.message : 'Claude Desktop native control is unavailable. Open its Code tab and retry.' }); }
|
|
195
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { lstat, open, readdir } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
|
+
import { claudeDesktopSessionRoot } from './claude-desktop-lineage.js';
|
|
7
|
+
|
|
8
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
|
+
const LOCAL = /^local_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
|
+
const fail = message => Object.assign(new Error(message), { code: 'claude_desktop_identity_unverified' });
|
|
11
|
+
const changed = () => Object.assign(fail('Claude Desktop metadata changed.'), { retryable: true });
|
|
12
|
+
const fingerprint = stat => [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(':');
|
|
13
|
+
|
|
14
|
+
async function directory(path) {
|
|
15
|
+
const stat = await lstat(path);
|
|
16
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw fail('Claude Desktop storage is not a regular directory.');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Mutation identity must be resolved from a complete scan, not the bounded,
|
|
20
|
+
// eventually refreshed catalog. Do not expose account settings or transcript text.
|
|
21
|
+
export async function resolveClaudeDesktopTarget(candidate, { home = homedir(), root = claudeDesktopSessionRoot(home) } = {}) {
|
|
22
|
+
if (candidate?.provider !== 'claude' || !UUID.test(candidate.session || '') || !isAbsolute(candidate.cwd || ''))
|
|
23
|
+
throw fail('Select an exact Claude conversation.');
|
|
24
|
+
await directory(root);
|
|
25
|
+
const matches = []; let inspected = 0;
|
|
26
|
+
const accounts = await readdir(root, { withFileTypes: true });
|
|
27
|
+
for (const account of accounts.filter(item => UUID.test(item.name))) {
|
|
28
|
+
const accountPath = join(root, account.name); await directory(accountPath);
|
|
29
|
+
for (const org of (await readdir(accountPath, { withFileTypes: true })).filter(item => UUID.test(item.name))) {
|
|
30
|
+
const partition = join(accountPath, org.name); await directory(partition);
|
|
31
|
+
const entries = (await readdir(partition, { withFileTypes: true })).filter(entry => entry.name.endsWith('.json') && LOCAL.test(entry.name.slice(0, -5)));
|
|
32
|
+
inspected += entries.length;
|
|
33
|
+
if (inspected > 16384) throw fail('Claude Desktop identity scan exceeded its limit.');
|
|
34
|
+
const readEntry = async entry => {
|
|
35
|
+
const path = join(partition, entry.name), before = await lstat(path, { bigint: true });
|
|
36
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > 262144n) throw fail('Claude Desktop metadata cannot be verified.');
|
|
37
|
+
const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW || 0));
|
|
38
|
+
let item;
|
|
39
|
+
try {
|
|
40
|
+
if (fingerprint(await handle.stat({ bigint: true })) !== fingerprint(before)) throw changed();
|
|
41
|
+
// Bound allocation even if the provider replaces/grows metadata while
|
|
42
|
+
// it is being read. Read one extra byte to detect oversized content.
|
|
43
|
+
const bytes = Buffer.alloc(262145);
|
|
44
|
+
let length = 0;
|
|
45
|
+
while (length < bytes.length) {
|
|
46
|
+
const { bytesRead } = await handle.read(bytes, length, bytes.length - length, length);
|
|
47
|
+
if (!bytesRead) break;
|
|
48
|
+
length += bytesRead;
|
|
49
|
+
}
|
|
50
|
+
if (length > 262144) throw fail('Claude Desktop metadata exceeds its size limit.');
|
|
51
|
+
if (fingerprint(await handle.stat({ bigint: true })) !== fingerprint(before) || fingerprint(await lstat(path, { bigint: true })) !== fingerprint(before)) throw changed();
|
|
52
|
+
try { item = JSON.parse(bytes.subarray(0, length).toString('utf8')); }
|
|
53
|
+
catch { throw fail('Claude Desktop metadata is invalid JSON.'); }
|
|
54
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) throw fail('Claude Desktop metadata is invalid.');
|
|
55
|
+
if (fingerprint(await handle.stat({ bigint: true })) !== fingerprint(before) || fingerprint(await lstat(path, { bigint: true })) !== fingerprint(before))
|
|
56
|
+
throw changed();
|
|
57
|
+
} finally { await handle.close(); }
|
|
58
|
+
if (item.cliSessionId !== candidate.session) return;
|
|
59
|
+
// Include competing claims even when their cwd differs. A CLI UUID alone
|
|
60
|
+
// must never select a Desktop account or organization implicitly.
|
|
61
|
+
matches.push({ desktopId: item.sessionId, session: item.cliSessionId, cwd: item.cwd,
|
|
62
|
+
title: item.title, archived: item.isArchived,
|
|
63
|
+
partition: createHash('sha256').update(account.name + '/' + org.name).digest('hex'),
|
|
64
|
+
fileId: entry.name.slice(0, -5) });
|
|
65
|
+
};
|
|
66
|
+
// Bound concurrent descriptors while avoiding a serial full scan for each
|
|
67
|
+
// readback. Wait for every opened descriptor to close before failing.
|
|
68
|
+
for (let offset = 0; offset < entries.length; offset += 8) {
|
|
69
|
+
const results = await Promise.allSettled(entries.slice(offset, offset + 8).map(async entry => {
|
|
70
|
+
for (let attempt = 0; ; attempt++) {
|
|
71
|
+
try { return await readEntry(entry); }
|
|
72
|
+
catch (error) { if (!error.retryable || attempt >= 2) throw error; }
|
|
73
|
+
}
|
|
74
|
+
}));
|
|
75
|
+
const failed = results.find(result => result.status === 'rejected');
|
|
76
|
+
if (failed) throw failed.reason;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (matches.length !== 1) throw fail('Claude conversation is missing or belongs to multiple Desktop records.');
|
|
81
|
+
const target = matches[0];
|
|
82
|
+
if (target.desktopId !== target.fileId || !LOCAL.test(target.desktopId || '') || !isAbsolute(target.cwd || '') || resolve(target.cwd) !== resolve(candidate.cwd) ||
|
|
83
|
+
typeof target.archived !== 'boolean' || typeof target.title !== 'string' || !target.title.trim() || target.title.length > 512)
|
|
84
|
+
throw fail('Claude Desktop identity or archive state does not match the selected conversation.');
|
|
85
|
+
const { fileId, ...verified } = target;
|
|
86
|
+
return verified;
|
|
87
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { CLAUDE_REVIEW_SYSTEM, CLAUDE_REVIEW_SCHEMA, CLAUDE_NATIVE_REVIEW_TYPE } from './claude-review-definition.js';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { basename, relative, resolve } from 'node:path';
|
|
7
|
+
import { runStructuredProviderReview } from '../sqm/review-worker.js';
|
|
8
|
+
import { captureAcceptanceReads } from './acceptance-reads.js';
|
|
9
|
+
import { readClaudeConversation, readClaudeNativeReview } from './claude-sessions.js';
|
|
10
|
+
import { independentRequestContext, workspaceIdentity } from './purpose-verification.js';
|
|
11
|
+
import { effectiveAcceptanceCriteria } from '../codex-plugins/simy/hooks/acceptance-policy.mjs';
|
|
12
|
+
import { liveOutcomeReviewEligible, settleLiveOutcomeReview, readRecord, taskDirectory, hasPendingUserStop, providerLinkState, processIdentity } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
13
|
+
|
|
14
|
+
// Native --max-budget-usd belongs to the original process. A separate reviewer
|
|
15
|
+
// cannot debit that allowance, and copying it would permit twice the spend.
|
|
16
|
+
// Observe only the presence of a cap; never retain or expose the command line.
|
|
17
|
+
export async function claudeReviewBudgetBoundary(record, { platform = process.platform, inspectProcess = processIdentity,
|
|
18
|
+
readCommand = async pid => platform === 'linux' ? readFile(`/proc/${pid}/cmdline`, 'utf8')
|
|
19
|
+
: (await promisify(execFile)('/bin/ps', ['-ww', '-p', String(pid), '-o', 'args='], { timeout: 500, maxBuffer: 256 * 1024 })).stdout } = {}) {
|
|
20
|
+
const owner = record.run?.managed?.liveHookOwner || record.hookSessionProcess;
|
|
21
|
+
if (!owner?.identity || await inspectProcess(owner.pid) !== owner.identity) return 'unknown';
|
|
22
|
+
try {
|
|
23
|
+
if (!['linux', 'darwin'].includes(platform)) return 'unknown';
|
|
24
|
+
const command = await readCommand(owner.pid);
|
|
25
|
+
if (await inspectProcess(owner.pid) !== owner.identity) return 'unknown';
|
|
26
|
+
if (platform === 'darwin') {
|
|
27
|
+
// Do not mistake a rewritten process title for CLI arguments.
|
|
28
|
+
const executable = owner.identity.match(/^\S+\s+\S+\s+\d+\s+\S+\s+\d+\s+(.+)$/)?.[1];
|
|
29
|
+
if (!executable || !/(?:^|\/)(?:claude|node)(?:$|\/)/.test(executable)) return 'unknown';
|
|
30
|
+
const prefix = [executable, basename(executable)].find(path => command.trimStart().startsWith(`${path} `));
|
|
31
|
+
if (!prefix || !/(?:^|\s)--?[a-z]/i.test(command.trimStart().slice(prefix.length))) return 'unknown';
|
|
32
|
+
// ps cannot distinguish a quoted prompt from a cap option. Both cases
|
|
33
|
+
// may use the original process's native Agent, never a separate reviewer.
|
|
34
|
+
if (command.includes('--max-budget-usd')) return 'native_only';
|
|
35
|
+
} else {
|
|
36
|
+
const argv = command.split('\0');
|
|
37
|
+
if (argv.at(-1) === '') argv.pop();
|
|
38
|
+
// A process title can replace argv; padding NULs are not arguments.
|
|
39
|
+
if (argv.filter(Boolean).length < 2) return 'unknown';
|
|
40
|
+
const end = argv.indexOf('--');
|
|
41
|
+
// A Node runtime separator may precede the CLI script. Do not mistake
|
|
42
|
+
// that ambiguous launcher form for the end of Claude's own options.
|
|
43
|
+
if (end >= 0 && basename(argv[0]) === 'node' && argv[1]?.startsWith('-')) return 'unknown';
|
|
44
|
+
const options = end < 0 ? argv : argv.slice(0, end);
|
|
45
|
+
if (options.some(arg => arg === '--max-budget-usd' || arg.startsWith('--max-budget-usd='))) return 'worker_cap';
|
|
46
|
+
}
|
|
47
|
+
return 'no_native_cap_observed';
|
|
48
|
+
} catch { return 'unknown'; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A checkpoint is already committed before this bounded, tool-free review. The
|
|
52
|
+
// original Claude process waits for its CLI child and retains its writer lease.
|
|
53
|
+
// No second controller/session resumes or writes the task workspace.
|
|
54
|
+
export async function reviewClaudeCheckpoint(record, { root, review = runStructuredProviderReview,
|
|
55
|
+
readContext = readClaudeConversation, readNativeReview = readClaudeNativeReview, onFailure = () => {}, captureReads = captureAcceptanceReads, inspectBudget = claudeReviewBudgetBoundary, signal, executionTimeoutMs = 120000 } = {}) {
|
|
56
|
+
if (!liveOutcomeReviewEligible(record)) return record;
|
|
57
|
+
const snapshot = structuredClone(record), cwd = taskDirectory(record), criteria = effectiveAcceptanceCriteria(record.run.managed);
|
|
58
|
+
const abort = new AbortController();
|
|
59
|
+
const combined = signal ? AbortSignal.any([signal, abort.signal]) : abort.signal;
|
|
60
|
+
const current = async () => {
|
|
61
|
+
const live = await readRecord(root, record.provider, record.session);
|
|
62
|
+
const owner = live?.run?.managed?.liveHookOwner;
|
|
63
|
+
return (!owner || await processIdentity(owner.pid) === owner.identity) && liveOutcomeReviewEligible(live) && live.run.id === snapshot.run.id && live.run.revision === snapshot.run.revision && live.run.controlRevision === snapshot.run.controlRevision &&
|
|
64
|
+
!hasPendingUserStop(root, live) && (await providerLinkState(root, record.provider)).connected;
|
|
65
|
+
};
|
|
66
|
+
const unavailable = async summary => settleLiveOutcomeReview({ root, provider: record.provider, session: record.session, snapshot,
|
|
67
|
+
review: { passed: false, inspectionAvailable: false, summary, findings: [], source: 'controller_review_unavailable', checkedAt: Date.now() } });
|
|
68
|
+
let checking = false;
|
|
69
|
+
const timer = setInterval(async () => {
|
|
70
|
+
if (checking) return; checking = true;
|
|
71
|
+
try { if (!await current()) abort.abort(); } catch { abort.abort(); } finally { checking = false; }
|
|
72
|
+
}, 500);
|
|
73
|
+
try {
|
|
74
|
+
const budget = await inspectBudget(record);
|
|
75
|
+
if (budget === 'unknown') return await settleLiveOutcomeReview({ root, provider: record.provider, session: record.session, snapshot,
|
|
76
|
+
review: { passed: false, inspectionAvailable: false, budgetBoundary: 'unknown',
|
|
77
|
+
summary: 'Independent review was not dispatched: the original Claude process budget arguments could not be verified. Acceptance remains unverified; inspect the original launch configuration without changing its budget.',
|
|
78
|
+
findings: [], source: 'controller_review_unavailable', checkedAt: Date.now() } });
|
|
79
|
+
const inventory = [];
|
|
80
|
+
const before = await workspaceIdentity(cwd, undefined, inventory, { signal: combined, inventoryLimit: 1000 });
|
|
81
|
+
const reads = await captureReads(cwd, criteria, { signal: combined });
|
|
82
|
+
// This executor cannot independently inspect unnamed artifacts or services.
|
|
83
|
+
// Never turn absent/truncated input into a successful semantic judgment.
|
|
84
|
+
if (!reads.length || reads.some(row => row.observation.truncated || row.observation.redacted)) return await unavailable('Current outcome observations are absent or incomplete.');
|
|
85
|
+
if (reads.some(row => !inventory.some(file => file.type === 'file' && file.path === relative(cwd, resolve(cwd, row.recipe.path)) && file.digest === row.digest)))
|
|
86
|
+
return await unavailable('Controller observations do not match the reviewed workspace revision; reread the actual outcome.');
|
|
87
|
+
const context = await readContext(record, { signal: combined, userOnly: true });
|
|
88
|
+
if (!context?.messages?.some(row => row.role === 'user') || context.incomplete || context.truncated) return await unavailable('Original user context could not be read completely; inspect the original request before retrying review.');
|
|
89
|
+
if (!await current()) return await readRecord(root, record.provider, record.session);
|
|
90
|
+
const requests = independentRequestContext(context);
|
|
91
|
+
if (budget === 'worker_cap' || budget === 'native_only') {
|
|
92
|
+
const contextDigest = createHash('sha256').update(JSON.stringify(requests.messages.filter(row => row.text?.trim()))).digest('hex');
|
|
93
|
+
const pending = record.run.managed.nativeOutcomeReview;
|
|
94
|
+
if (pending && pending.runId === record.run.id && pending.controlRevision === record.run.controlRevision && pending.criteriaDigest === createHash('sha256').update(JSON.stringify(criteria)).digest('hex') && JSON.stringify(pending.owner) === JSON.stringify(record.run.managed.liveHookOwner || record.hookSessionProcess) && pending.workspaceDigest === before && pending.contextDigest === contextDigest) {
|
|
95
|
+
const native = await readNativeReview(record, pending, { signal: combined });
|
|
96
|
+
if (!native) return await settleLiveOutcomeReview({ root, provider: record.provider, session: record.session, snapshot,
|
|
97
|
+
review: { passed: false, inspectionAvailable: false, budgetBoundary: budget, source: 'controller_review_unavailable', checkedAt: Date.now(), findings: [],
|
|
98
|
+
summary: 'The native independent review has no complete, exact original-session receipt. Keep the original Claude budget and acceptance pending; do not launch a separate reviewer or repeat the native review automatically.' } });
|
|
99
|
+
if (!await current()) return await readRecord(root, record.provider, record.session);
|
|
100
|
+
return await settleLiveOutcomeReview({ root, provider: record.provider, session: record.session, snapshot,
|
|
101
|
+
review: { ...native.output, passed: native.output.passed && native.output.inspectionAvailable, workspaceDigest: before,
|
|
102
|
+
source: 'independent_claude_review', executionSource: 'native_budgeted_subagent', nativeAgentId: native.agentId,
|
|
103
|
+
nativeToolUseId: native.toolUseId, contextPolicy: 'user_requests_and_controller_file_reads', checkedAt: Date.now() } });
|
|
104
|
+
}
|
|
105
|
+
const prompt = JSON.stringify({ objective: record.run.objective, requests, criteria,
|
|
106
|
+
workspaceInventory: { paths: inventory.map(row => row.path), truncated: inventory.truncated === true }, observations: reads });
|
|
107
|
+
return await settleLiveOutcomeReview({ root, provider: record.provider, session: record.session, snapshot,
|
|
108
|
+
review: { passed: false, inspectionAvailable: false, nativeRequest: { prompt, runId: record.run.id, controlRevision: record.run.controlRevision, criteriaDigest: createHash('sha256').update(JSON.stringify(criteria)).digest('hex'), agentType: CLAUDE_NATIVE_REVIEW_TYPE,
|
|
109
|
+
workspaceDigest: before, contextDigest, owner: record.run.managed.liveHookOwner || record.hookSessionProcess, requestedAt: Date.now() },
|
|
110
|
+
summary: 'Independent review will run in a fresh native subagent under the original Claude process budget.', findings: [] } });
|
|
111
|
+
}
|
|
112
|
+
const result = await review('claude', {
|
|
113
|
+
system: CLAUDE_REVIEW_SYSTEM,
|
|
114
|
+
prompt: JSON.stringify({ objective: record.run.objective, requests: independentRequestContext(context), criteria, workspaceInventory: { paths: inventory.map(row => row.path), truncated: inventory.truncated === true }, observations: reads }),
|
|
115
|
+
output_schema: CLAUDE_REVIEW_SCHEMA
|
|
116
|
+
}, { signal: combined, executionTimeoutMs, subscriptionOnly: true, beforeDispatch: async () => {
|
|
117
|
+
if (!await current()) throw new Error('Review target changed');
|
|
118
|
+
} });
|
|
119
|
+
const value = result.output;
|
|
120
|
+
if (typeof value?.passed !== 'boolean' || typeof value.inspectionAvailable !== 'boolean' || typeof value.summary !== 'string' || !Array.isArray(value.findings) ||
|
|
121
|
+
value.findings.some(row => typeof row !== 'string')) return await unavailable('Independent provider returned an unusable assessment; inspect provider availability before retrying.');
|
|
122
|
+
if (!await current()) return await readRecord(root, record.provider, record.session);
|
|
123
|
+
return await settleLiveOutcomeReview({ root, provider: record.provider, session: record.session, snapshot,
|
|
124
|
+
review: { passed: value.passed && value.inspectionAvailable, inspectionAvailable: value.inspectionAvailable, summary: value.summary.slice(0, 4000), findings: value.findings.slice(0, 20),
|
|
125
|
+
workspaceDigest: before, source: 'independent_claude_review', contextPolicy: 'user_requests_and_controller_file_reads', checkedAt: Date.now() } });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
onFailure(error);
|
|
128
|
+
// Failure to inspect is not a product defect or successful acceptance.
|
|
129
|
+
if (!await current()) return await readRecord(root, record.provider, record.session);
|
|
130
|
+
return await unavailable('Independent provider inspection failed; inspect the existing provider connection and retry the original outcome check.');
|
|
131
|
+
} finally { clearInterval(timer); }
|
|
132
|
+
}
|