@awak-app/simy-cli 0.7.36 → 0.7.38
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 +36 -36
- package/package.json +1 -1
- package/src/agent.js +1 -1
- package/src/auto-update.js +25 -13
- package/src/backend-executable.js +1 -1
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +2 -2
- 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 +242 -26
- package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +4 -1
- package/src/codex-plugins/simy/hooks/registered-requirement-checker.mjs +201 -0
- package/src/codex-plugins/simy/hooks/requirement-acceptance.mjs +69 -0
- 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/delivery-hooks/supervisor-watchdog.js +3 -2
- package/src/desktop-host.js +53 -23
- 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/acceptance-reads.js +11 -2
- package/src/session-supervision/analysis-manager.js +9 -2
- package/src/session-supervision/automatic-continuation.js +11 -11
- package/src/session-supervision/catalog-refresh.js +16 -0
- package/src/session-supervision/claude-native-agent-contract.js +62 -0
- package/src/session-supervision/claude-outcome-review.js +7 -6
- package/src/session-supervision/claude-sessions.js +137 -76
- package/src/session-supervision/codex-app-server.js +3 -3
- package/src/session-supervision/codex-children.js +2 -2
- 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-context.js +58 -26
- package/src/session-supervision/conversation-presentation.js +3 -3
- package/src/session-supervision/decision-presentation.js +8 -8
- package/src/session-supervision/discovery-window.js +8 -2
- package/src/session-supervision/index.js +38 -25
- package/src/session-supervision/purpose-verification.js +12 -3
- package/src/session-supervision/remote-errors.js +2 -2
- package/src/session-supervision/work-state.js +3 -3
- package/src/sqm/advisory-guidance.js +1 -0
- package/src/sqm/agent-skill.js +3 -3
- package/src/sqm/checkers.js +35 -1
- package/src/sqm/claude-plugin.js +8 -2
- package/src/sqm/hypothesis-knowledge.js +3 -3
- package/src/sqm/knowledge-recall.js +34 -12
- package/src/sqm/reasoned-assessment.js +22 -9
- package/src/update-detachment/runtime.js +2 -1
package/src/desktop-host.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createDesktopFrameWriter } from './desktop-frame-writer.js';
|
|
2
2
|
import { boundedDiscovery } from './session-supervision/progressive-discovery.js';
|
|
3
|
+
import { createCatalogRefreshPolicy } from './session-supervision/catalog-refresh.js';
|
|
4
|
+
import { supervisedSession } from './session-supervision/discovery-window.js';
|
|
3
5
|
import { createClaudeDesktopArchiveController } from './session-supervision/claude-desktop-archive.js';
|
|
4
6
|
import { createClaudeDesktopNativeAdapter } from './session-supervision/claude-desktop-native.js';
|
|
5
7
|
import { applyControllerExecutionContext } from './session-supervision/execution-context.js';
|
|
@@ -118,9 +120,9 @@ function desktopActionError(action, error) {
|
|
|
118
120
|
const messages = {
|
|
119
121
|
session_workspace_changed: "The conversation workspace changed. Refresh and select it again.",
|
|
120
122
|
session_metadata_unavailable: "Conversation details are unavailable. Refresh this provider and select the conversation again.",
|
|
121
|
-
session_model_unavailable: "This provider did not expose the conversation model;
|
|
123
|
+
session_model_unavailable: "This provider did not expose the conversation model; Delivery Loop is unavailable.",
|
|
122
124
|
existing_loop: "This conversation already has a Delivery Loop. Open its existing supervision instead.",
|
|
123
|
-
task_settled: "This task has finished. Start a new conversation before
|
|
125
|
+
task_settled: "This task has finished. Start a new conversation before turning on Delivery Loop.",
|
|
124
126
|
};
|
|
125
127
|
if (messages[error?.code]) return messages[error.code];
|
|
126
128
|
}
|
|
@@ -203,6 +205,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
203
205
|
status = { ...status, update: { ...status.update,
|
|
204
206
|
auto_update_supported: updateSettings.auto_update_supported === true,
|
|
205
207
|
auto_update_enabled: updateSettings.auto_update_enabled === true,
|
|
208
|
+
auto_update_unavailable_reason: updateSettings.auto_update_unavailable_reason || null,
|
|
206
209
|
auto_update_busy: updateSettings.auto_update_busy === true } };
|
|
207
210
|
if (env.SIMY_REMOTE_CHILD === "1") status = { ...status, remoteSessionProtocol: 1, remoteControl: { version: 2, actions: [...allowedActions] } };
|
|
208
211
|
ownerTransport?.publish({ type: 'status', status: { ...status, remoteSessionProtocol: 1, remoteControl: { version: 2, actions: [...allowedActions] } } });
|
|
@@ -480,6 +483,13 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
480
483
|
})().finally(() => { emailSync = null; });
|
|
481
484
|
await emailSync;
|
|
482
485
|
};
|
|
486
|
+
const saveEmailDeliveryObservation = (controller, completion, observation) => controllerPreferences.change(controller.controllerScope, state => {
|
|
487
|
+
const identity = JSON.stringify([completion.host_id || 'local', completion.provider, completion.provider_session_id]);
|
|
488
|
+
const row = state.conversations[identity] ||= {};
|
|
489
|
+
row.emailDeliveries ||= {};
|
|
490
|
+
row.emailDeliveries[completion.task_id] = { ...observation, checkedAt: Date.now(), provider: completion.provider,
|
|
491
|
+
provider_session_id: completion.provider_session_id, host_id: completion.host_id || 'local' };
|
|
492
|
+
});
|
|
483
493
|
const submitNativeDevelopmentReportCompletions = ({ completions, controller }) => {
|
|
484
494
|
const { scope, deviceSession } = controller;
|
|
485
495
|
if (!deviceSession?.token || !deviceSession?.api_base_url || !sameReportController(controller, nativeDevelopmentReportsController) || nativeDevelopmentReports.state !== "ready" || nativeDevelopmentReportsScope !== scope) return;
|
|
@@ -506,23 +516,31 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
506
516
|
if (receipt && nativeDevelopmentReportsGeneration === generation && accessScope(currentAccess) === scope) {
|
|
507
517
|
if (receipt.status === 'pending') nativeCompletionRetryAt.set(key, Date.now() + 30_000);
|
|
508
518
|
else { nativeCompletionSuccess.add(key); nativeCompletionRetryAt.delete(key); }
|
|
509
|
-
|
|
510
|
-
await controllerPreferences.change(receiptScope, state => {
|
|
511
|
-
const identity = JSON.stringify([hostId, completion.provider, completion.provider_session_id]);
|
|
512
|
-
const row = state.conversations[identity] ||= {};
|
|
513
|
-
row.emailDeliveries ||= {};
|
|
514
|
-
row.emailDeliveries[completion.task_id] = { ...receipt, checkedAt: Date.now(), provider: completion.provider, provider_session_id: completion.provider_session_id, host_id: hostId };
|
|
515
|
-
});
|
|
519
|
+
await saveEmailDeliveryObservation(controller, completion, receipt);
|
|
516
520
|
await requireReportController(controller);
|
|
517
521
|
if (!closed) void publish().catch(() => {});
|
|
518
522
|
}
|
|
519
523
|
return receipt;
|
|
520
|
-
}).catch(error => {
|
|
524
|
+
}).catch(async error => {
|
|
521
525
|
if (nativeDevelopmentReportsGeneration === generation && accessScope(currentAccess) === scope) {
|
|
522
526
|
// These are immutable authorization/contract rejections. A new
|
|
523
527
|
// enrollment revision or scope produces a fresh rejection identity.
|
|
524
|
-
|
|
528
|
+
const rejected = [400, 403, 409].includes(error?.status);
|
|
529
|
+
if (rejected) nativeCompletionRejected.add(rejectionKey);
|
|
525
530
|
else nativeCompletionRetryAt.set(key, Date.now() + 30_000);
|
|
531
|
+
// ON is consent, not a delivery receipt. Surface a failed submission
|
|
532
|
+
// without exposing backend errors, credentials or completion proofs.
|
|
533
|
+
// The next confirmed server receipt replaces this local observation.
|
|
534
|
+
try {
|
|
535
|
+
await requireReportController(controller);
|
|
536
|
+
if (closed || nativeDevelopmentReportsGeneration !== generation || accessScope(currentAccess) !== scope) return null;
|
|
537
|
+
await saveEmailDeliveryObservation(controller, completion, {
|
|
538
|
+
task_id: completion.task_id, enrollment_revision: completion.enrollment_revision,
|
|
539
|
+
status: rejected ? 'rejected' : 'retrying',
|
|
540
|
+
});
|
|
541
|
+
await requireReportController(controller);
|
|
542
|
+
if (!closed) void publish().catch(() => {});
|
|
543
|
+
} catch { /* A changed controller must not receive an old failure. */ }
|
|
526
544
|
}
|
|
527
545
|
return null;
|
|
528
546
|
}).finally(() => nativeCompletionInFlight.delete(key));
|
|
@@ -568,6 +586,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
568
586
|
let commands;
|
|
569
587
|
let sessionStatus = { sessions: [], providers: { codex: { discovery: "loading", reason: null }, claude: { discovery: "loading", reason: null } }, refreshedAt: null, initialLoading: true };
|
|
570
588
|
let sessionRefresh = null, sessionRefreshedAt = 0, sessionRefreshAgain = false, discoveryController = null, discoveryStopped = false;
|
|
589
|
+
const catalogRefresh = createCatalogRefreshPolicy();
|
|
571
590
|
let catalogRevision = 0;
|
|
572
591
|
let enrichmentController = null, sessionEnrichment = null;
|
|
573
592
|
let catalogAnalysisJob = null, catalogAnalysisAgain = false;
|
|
@@ -585,7 +604,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
585
604
|
const records = await recordReader.list();
|
|
586
605
|
if (closed || relay || discoveryStopped || manager !== analysisManager ||
|
|
587
606
|
!desktopAccessReady(currentAccess) || accessScope(currentAccess) !== scope || !coordinator.acceptsNewWork()) return;
|
|
588
|
-
await manager.update(analysisSessions(withCurrentSettings(sessionStatus.sessions, records))
|
|
607
|
+
await manager.update(analysisSessions(withCurrentSettings(sessionStatus.sessions, records)), {
|
|
608
|
+
priorityKeys: new Set(records.filter(supervisedSession).map(record => `${record.provider}:${record.session}`)),
|
|
609
|
+
});
|
|
589
610
|
}
|
|
590
611
|
}).catch(() => {}).finally(() => {
|
|
591
612
|
catalogAnalysisJob = null;
|
|
@@ -640,12 +661,14 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
640
661
|
unfinishedDesktopWork = false;
|
|
641
662
|
})();
|
|
642
663
|
const observePullRequests = dependencies.observePullRequests || createPullRequestObserver();
|
|
643
|
-
const refreshSessions = (force = false) => {
|
|
664
|
+
const refreshSessions = (force = false, { history = false } = {}) => {
|
|
665
|
+
if (history) catalogRefresh.invalidate();
|
|
644
666
|
if (discoveryStopped && !closed && !relay) return sessionStatus;
|
|
645
667
|
if (closed || relay || !desktopAccessReady(currentAccess)) return { sessions: [], providers: {}, refreshedAt: null };
|
|
646
668
|
if (force && sessionRefresh) sessionRefreshAgain = true;
|
|
647
669
|
if (!sessionRefresh && (force || Date.now() - sessionRefreshedAt >= 15_000)) {
|
|
648
670
|
const requestedScope = accessScope(currentAccess);
|
|
671
|
+
const refresh = catalogRefresh.begin(JSON.stringify([requestedScope, providerLinks]));
|
|
649
672
|
const controller = discoveryController = new AbortController();
|
|
650
673
|
const attempt = { id: randomUUID(), requestedAt: Date.now(), state: 'pending' };
|
|
651
674
|
sessionStatus = { ...sessionStatus, discoveryAttempt: attempt };
|
|
@@ -659,7 +682,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
659
682
|
}
|
|
660
683
|
const complete = new Set(value.completeProviders || (value.historyLoading === undefined ? PROVIDER_LINKS : []));
|
|
661
684
|
const merge = (previous, incoming) => {
|
|
662
|
-
const rows = new Map(previous.filter(item => !complete.has(item.provider)).map(item => [`${item.provider}:${item.session}`, item]));
|
|
685
|
+
const rows = new Map(previous.filter(item => refresh.recentOnly || !complete.has(item.provider)).map(item => [`${item.provider}:${item.session}`, item]));
|
|
663
686
|
for (const item of incoming || []) rows.set(`${item.provider}:${item.session}`, item);
|
|
664
687
|
return [...rows.values()];
|
|
665
688
|
};
|
|
@@ -674,7 +697,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
674
697
|
const firstPageTimeoutMs = dependencies.discoveryTimeoutMs ?? 30_000;
|
|
675
698
|
const historyTimeoutMs = dependencies.historyDiscoveryTimeoutMs ?? dependencies.discoveryTimeoutMs ?? 300_000;
|
|
676
699
|
sessionRefresh = boundedDiscovery((signal, startHistory) => (dependencies.discoverSessions || discoverSessions)({
|
|
677
|
-
root: deliveryRoot, signal, onDrain: trackDiscoveryDrain, includeArchived: true, includeSelectionCandidates: true,
|
|
700
|
+
root: deliveryRoot, signal, onDrain: trackDiscoveryDrain, includeArchived: true, includeSelectionCandidates: true, recentOnly: refresh.recentOnly,
|
|
678
701
|
readRecords: () => recordReader.list(),
|
|
679
702
|
connectedProviders: PROVIDER_LINKS.filter(provider => providerLinks[provider].connected),
|
|
680
703
|
timeoutMs: firstPageTimeoutMs, historyTimeoutMs,
|
|
@@ -685,6 +708,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
685
708
|
await applyCatalog(value);
|
|
686
709
|
if (closed || relay || controller.signal.aborted) return;
|
|
687
710
|
const failed = Object.values(value.providers || {}).some(provider => provider.discovery === 'unavailable');
|
|
711
|
+
refresh.complete(!failed);
|
|
688
712
|
sessionStatus = { ...sessionStatus, historyLoading: false,
|
|
689
713
|
discoveryAttempt: { ...attempt, state: failed ? 'failed' : 'succeeded', completedAt: Date.now() } };
|
|
690
714
|
// Publish before PR/network observation and queue maintenance. Neither
|
|
@@ -1507,7 +1531,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1507
1531
|
if(['detach_update','update_reconnect'].includes(action)) {
|
|
1508
1532
|
const access=await accessSnapshot(); requireDesktopAccess(access);
|
|
1509
1533
|
updateRuntime.validate(command,coordinator.snapshot(),updater?.snapshot?.());
|
|
1510
|
-
if(action==='detach_update')await updater.maybeApply({detach:true});
|
|
1534
|
+
if(action==='detach_update')await updater.maybeApply({detach:true,manual:true});
|
|
1511
1535
|
else await reconnectWithHistoryRecovery({root,coordinator,eligible:()=>updateRuntime.historyRecoveryEligible()});
|
|
1512
1536
|
emit({type:'result',id:command.id,ok:coordinator.snapshot().state==='connected' && (action!=='detach_update' || updater.snapshot().state==='updated')});
|
|
1513
1537
|
await publish();return;
|
|
@@ -1578,7 +1602,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1578
1602
|
if (['detach_update', 'update_reconnect'].includes(action)) requireDesktopAccess(access);
|
|
1579
1603
|
else if (!recoveryControl && desktopAccessReady(access) && !desktopActionAllowed({ state: 'unavailable' }, command)) await authorizeWork();
|
|
1580
1604
|
|
|
1581
|
-
|
|
1605
|
+
// Signing back into the same SIMY environment is a prerequisite for
|
|
1606
|
+
// recovery, not permission to resume work. Keep organization changes
|
|
1607
|
+
// and execution fenced until the original checkpoint is reconciled.
|
|
1608
|
+
const recoveryAuthorization = ["login", "authorization_open", "authorization_copy", "organization_cancel"].includes(action);
|
|
1609
|
+
if (!coordinator.acceptsNewWork() && !recoveryControl && !recoveryAuthorization && !["refresh", "reconnect", "task_detail", "check_update", "quit", "delivery_stop", "delivery_pause", "delivery_reject"].includes(action)) {
|
|
1582
1610
|
throw new Error("The update connection requires recovery.");
|
|
1583
1611
|
}
|
|
1584
1612
|
if (['team_threads_refresh','team_thread_open','team_thread_share'].includes(action)) {
|
|
@@ -1890,7 +1918,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1890
1918
|
else if (desktopAccessReady(access)) await agent.refreshCodexStatus?.();
|
|
1891
1919
|
await agent.refreshDesktopStatus();
|
|
1892
1920
|
await refreshPlugins();
|
|
1893
|
-
refreshSessions(true);
|
|
1921
|
+
refreshSessions(true, { history: true });
|
|
1894
1922
|
if (desktopAccessReady(currentAccess) && coordinator.acceptsNewWork()) void actionAssociations.tick().then(publish).catch(() => {});
|
|
1895
1923
|
}
|
|
1896
1924
|
else if (action === "organizations_refresh") await agent.refreshOrganizations();
|
|
@@ -2011,7 +2039,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
2011
2039
|
await authorizeWork(commandScope);
|
|
2012
2040
|
await actionAssociations.correct(command, analysisManager.project(candidate));
|
|
2013
2041
|
} else if (action === "session_set_auto_continue") {
|
|
2014
|
-
if (typeof command.enabled !== 'boolean') throw new Error('Choose whether
|
|
2042
|
+
if (typeof command.enabled !== 'boolean') throw new Error('Choose whether Delivery Loop is on.');
|
|
2015
2043
|
let admission, target, providerEpoch;
|
|
2016
2044
|
if (command.enabled) {
|
|
2017
2045
|
const candidate = await selectedSession(command, { preferObserved: true });
|
|
@@ -2204,7 +2232,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
2204
2232
|
if (fresh.cwd !== candidate.cwd) throw new Error('The conversation workspace changed. Refresh and retry.');
|
|
2205
2233
|
const record = await readRecord(deliveryRoot, 'claude', command.session);
|
|
2206
2234
|
if (fresh.isRunning || fresh.autoContinue || record?.autoContinue || record?.run && !TERMINAL.has(record.run.state))
|
|
2207
|
-
throw Object.assign(new Error('Stop this conversation and turn off
|
|
2235
|
+
throw Object.assign(new Error('Stop this conversation and turn off Delivery Loop before organizing it.'), { code: 'claude_desktop_running' });
|
|
2208
2236
|
};
|
|
2209
2237
|
await verifySelection();
|
|
2210
2238
|
const operation = new AbortController();
|
|
@@ -2227,7 +2255,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
2227
2255
|
} finally { clearInterval(watchdog); }
|
|
2228
2256
|
|
|
2229
2257
|
await recordProviderArchive(deliveryRoot, 'claude', command.session, archived, Date.now());
|
|
2230
|
-
refreshSessions(true);
|
|
2258
|
+
refreshSessions(true, { history: true });
|
|
2231
2259
|
emit({ type: 'result', id: command.id, ok: true, data });
|
|
2232
2260
|
void publish().catch(() => {});
|
|
2233
2261
|
return;
|
|
@@ -2245,7 +2273,9 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
2245
2273
|
listHidden: archived,
|
|
2246
2274
|
}));
|
|
2247
2275
|
}
|
|
2248
|
-
|
|
2276
|
+
// Archive/restore can move an old conversation outside both recent
|
|
2277
|
+
// pages. Explicit organization must be visible immediately.
|
|
2278
|
+
refreshSessions(true, { history: true });
|
|
2249
2279
|
} else if (action === "session_track" || action === "session_untrack") {
|
|
2250
2280
|
const candidate = await selectedSession(command);
|
|
2251
2281
|
if (candidate.capabilities?.canMonitor !== true) throw new Error('Tracking is unavailable for this conversation.');
|
|
@@ -2380,7 +2410,7 @@ function sessionActionError(error, action) {
|
|
|
2380
2410
|
if (error instanceof AutoContinueUnavailableError) return error.message.replace(/[\r\n\x00-\x1f]/g, ' ').slice(0, 600);
|
|
2381
2411
|
const message = String(error?.message || "");
|
|
2382
2412
|
if (/already has a Delivery Loop|budgets cannot be reset/i.test(message)) return "This conversation already has a task. Open its existing supervision instead.";
|
|
2383
|
-
if (/model.*unavailable|did not expose.*model/i.test(message)) return "This provider did not expose the conversation model;
|
|
2413
|
+
if (/model.*unavailable|did not expose.*model/i.test(message)) return "This provider did not expose the conversation model; Delivery Loop is unavailable.";
|
|
2384
2414
|
if (/workspace changed|metadata.*unavailable|session.*changed/i.test(message)) return "The conversation identity or workspace changed. Refresh and select it again.";
|
|
2385
2415
|
if (/objective|acceptance|autonomy policy|budget/i.test(message)) return message.replace(/[\r\n\x00-\x1f]/g, " ").slice(0, 240);
|
|
2386
2416
|
if (/question changed|answer is empty|session understanding|context/i.test(message)) return message.replace(/[\r\n\x00-\x1f]/g, " ").slice(0, 240);
|
package/src/index.js
CHANGED
|
@@ -161,7 +161,7 @@ async function runCommand(installMode) {
|
|
|
161
161
|
Options:
|
|
162
162
|
--version, -v Print the installed CLI version and exit
|
|
163
163
|
--daemon, --deamon Run detached in the background
|
|
164
|
-
--no-tui Disable the interactive
|
|
164
|
+
--no-tui Disable the interactive Delivery Loop console
|
|
165
165
|
--no-open Do not open the browser when authorization is required
|
|
166
166
|
--port <port> Bind a specific localhost port
|
|
167
167
|
--host <url> Connect to a SIMY Web origin (default: ${DEFAULT_WEB_ORIGIN})
|
package/src/local-task.js
CHANGED
|
@@ -1257,7 +1257,7 @@ function localTaskProviderInstruction(task) {
|
|
|
1257
1257
|
? "The managed output directory is reserved for final user-requested deliverables. Keep helper scripts, temporary files, caches, and intermediate files outside it (use the task workspace or an OS temporary directory), and remove those helper files before completion."
|
|
1258
1258
|
: null;
|
|
1259
1259
|
return [
|
|
1260
|
-
"You are the local executor for a SIMY Local Task, not
|
|
1260
|
+
"You are the local executor for a SIMY Local Task, not a Delivery Loop.",
|
|
1261
1261
|
buildWorkflowReportingInstruction(),
|
|
1262
1262
|
"Complete this bounded task once. Do not create Git branches, commits, pull requests, audits, or automatic retries.",
|
|
1263
1263
|
`Permission mode: ${task.permission_mode}.`,
|
|
@@ -117,7 +117,7 @@ export function summarizeCodingLoopEvent(snapshot, event) {
|
|
|
117
117
|
};
|
|
118
118
|
case "stopped":
|
|
119
119
|
return {
|
|
120
|
-
summary: "The local human operator stopped this
|
|
120
|
+
summary: "The local human operator stopped this Delivery Loop run.",
|
|
121
121
|
result: "No further executor handoff will occur for this run.",
|
|
122
122
|
next: "Start a new run or explicitly continue this one when ready.",
|
|
123
123
|
};
|
package/src/runner.js
CHANGED
|
@@ -452,7 +452,7 @@ export async function recoverLocalCodingRun(run, dependencies = {}, now = Date.n
|
|
|
452
452
|
}
|
|
453
453
|
|
|
454
454
|
function assertRunCanContinue(run) {
|
|
455
|
-
if (run.operation || run.child) throw new Error("The selected
|
|
455
|
+
if (run.operation || run.child) throw new Error("The selected Delivery Loop run is still active.");
|
|
456
456
|
if (!RESUMABLE_STATES.has(run.status)) {
|
|
457
457
|
throw new Error("Human guidance is available only when the selected run is waiting.");
|
|
458
458
|
}
|
|
@@ -542,7 +542,7 @@ export async function continueLocalCodingRunAfterRepositoryApproval(
|
|
|
542
542
|
if (!approvedRepository || !approvedPath) {
|
|
543
543
|
throw new Error("An approved local repository and path are required.");
|
|
544
544
|
}
|
|
545
|
-
if (run.operation || run.child) throw new Error("The selected
|
|
545
|
+
if (run.operation || run.child) throw new Error("The selected Delivery Loop run is still active.");
|
|
546
546
|
if (run.status !== "waiting_human") {
|
|
547
547
|
throw new Error("Repository approval is available only while the run is waiting.");
|
|
548
548
|
}
|
|
@@ -6,7 +6,8 @@ import { evidenceExcerpt } from '../codex-plugins/simy/hooks/purpose-evidence.mj
|
|
|
6
6
|
|
|
7
7
|
// Read only original file predicates in the authorized task directory. These
|
|
8
8
|
// are current independent controller reads, not worker claims or authored proof.
|
|
9
|
-
export async function captureAcceptanceReads(cwd, criteria, { signal } = {}) {
|
|
9
|
+
export async function captureAcceptanceReads(cwd, criteria, { signal, excerptLimit = 4000, publicTaskSourcePaths = [] } = {}) {
|
|
10
|
+
if (!Number.isSafeInteger(excerptLimit) || excerptLimit < 1 || excerptLimit > 32768) throw new Error('Acceptance excerpt bound is invalid.');
|
|
10
11
|
const root = await realpath(cwd), rows = [], seen = new Set(); let retained = 0;
|
|
11
12
|
for (const criterion of criteria) {
|
|
12
13
|
signal?.throwIfAborted();
|
|
@@ -22,8 +23,16 @@ export async function captureAcceptanceReads(cwd, criteria, { signal } = {}) {
|
|
|
22
23
|
if (bytesRead > 32768) continue;
|
|
23
24
|
const body = data.subarray(0, bytesRead); retained += bytesRead;
|
|
24
25
|
const digest = createHash('sha256').update(body).digest('hex');
|
|
26
|
+
// Host-only, explicit source-read permission for a native task reviewer.
|
|
27
|
+
// The same original Claude already has Read access to these public-task
|
|
28
|
+
// source paths. Business identifiers such as Token are source, not a
|
|
29
|
+
// credential claim. Generic artifact reads retain their redaction policy.
|
|
30
|
+
const sourceRead = publicTaskSourcePaths.some(file => resolve(root, file) === path);
|
|
31
|
+
const sourceText = bytesRead ? body.toString('utf8') : JSON.stringify({ byteLength: 0, content: '' });
|
|
32
|
+
const observation = sourceRead ? { text: sourceText.slice(0, excerptLimit), truncated: sourceText.length > excerptLimit,
|
|
33
|
+
redacted: false, policy: 'host_authorized_public_task_source' } : evidenceExcerpt(sourceText, excerptLimit);
|
|
25
34
|
rows.push({ id: `file:${target}`, source: 'controller_file_read', status: 'observed', at: Date.now(), digest,
|
|
26
|
-
recipe: { type: 'file_read', path: target }, observation
|
|
35
|
+
recipe: { type: 'file_read', path: target }, observation });
|
|
27
36
|
} catch { signal?.throwIfAborted(); /* Missing/large/unreadable files stay unverified; tool investigation remains available. */ }
|
|
28
37
|
finally { await handle?.close(); }
|
|
29
38
|
}
|
|
@@ -4,6 +4,7 @@ import { lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { analyzeContinuation, CONTINUATION_ANALYSIS_VERSION } from './continuation-analysis.js';
|
|
6
6
|
import { providerGoalEvidence } from './conversation-context.js';
|
|
7
|
+
import { liveSessionWindow, supervisedSession } from './discovery-window.js';
|
|
7
8
|
|
|
8
9
|
const BACKFILL_LIMIT = 10;
|
|
9
10
|
const MAX_CACHE_ENTRIES = 2000;
|
|
@@ -276,11 +277,17 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
|
|
|
276
277
|
});
|
|
277
278
|
startDrain();
|
|
278
279
|
},
|
|
279
|
-
async update(sessions) {
|
|
280
|
+
async update(sessions, { priorityKeys = new Set() } = {}) {
|
|
280
281
|
await exclusive(async () => {
|
|
281
282
|
if (closed || suspended) return;
|
|
283
|
+
// The complete catalog remains navigable. Historical metadata outside
|
|
284
|
+
// this window must not churn the bounded cache or schedule paid work.
|
|
285
|
+
// Preserve queued/manual jobs and explicitly supervised conversations;
|
|
286
|
+
// removal, changed authority and OFF still use the incoming snapshot.
|
|
287
|
+
const window = liveSessionWindow(sessions, { priority: candidate => priorityKeys.has(keyOf(candidate)) || supervisedSession(candidate) ||
|
|
288
|
+
jobs.has(keyOf(candidate)) || ['queued', 'analyzing'].includes(entries.get(keyOf(candidate))?.result?.state) });
|
|
282
289
|
candidates.clear();
|
|
283
|
-
for (const candidate of
|
|
290
|
+
for (const candidate of window) candidates.set(keyOf(candidate), observedCandidate(candidate));
|
|
284
291
|
const sorted = [...candidates.values()].sort((a, b) => activityOf(b) - activityOf(a) || keyOf(a).localeCompare(keyOf(b)));
|
|
285
292
|
const backfill = document.initialized ? new Set() : new Set(sorted.slice(0, BACKFILL_LIMIT).map(keyOf));
|
|
286
293
|
let dirty = !document.initialized;
|
|
@@ -6,15 +6,15 @@ function continuationFailureReason(error) {
|
|
|
6
6
|
// Provider exceptions can contain private paths/output. Only known codes
|
|
7
7
|
// select actionable UI copy; unrecognized failures retain a safe fallback.
|
|
8
8
|
const reasons = {
|
|
9
|
-
desktop_access_required: 'Sign in to an organization with SQM and Developer access before
|
|
10
|
-
provider_sign_in_required: 'Sign in to the original AI provider, then retry
|
|
11
|
-
unavailable: 'The local provider is unavailable. Open or configure it, then retry
|
|
12
|
-
unsupported: 'Update the original AI provider to a compatible version, then retry
|
|
9
|
+
desktop_access_required: 'Sign in to an organization with SQM and Developer access before Delivery Loop can run.',
|
|
10
|
+
provider_sign_in_required: 'Sign in to the original AI provider, then retry Delivery Loop.',
|
|
11
|
+
unavailable: 'The local provider is unavailable. Open or configure it, then retry Delivery Loop.',
|
|
12
|
+
unsupported: 'Update the original AI provider to a compatible version, then retry Delivery Loop.',
|
|
13
13
|
process_exited: 'The original Claude process has exited. Return to the original conversation, then refresh.',
|
|
14
14
|
session_ended: 'This Claude session has ended. Return to the original conversation, then refresh.',
|
|
15
15
|
hook_missing: 'Waiting for the original Claude hook. SIMY recovers automatically when the next verified prompt or response-end hook arrives. If no notifications arrive, check the SIMY plugin in the original Claude window.',
|
|
16
16
|
};
|
|
17
|
-
return Object.hasOwn(reasons, error?.code) ? reasons[error.code] : 'Could not start
|
|
17
|
+
return Object.hasOwn(reasons, error?.code) ? reasons[error.code] : 'Could not start Delivery Loop. Check the original conversation and retry.';
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// Shared by explicit Continue and automatic enrollment. Analysis alone never
|
|
@@ -24,7 +24,7 @@ export async function continueAnalyzedSession({ candidate, manager, root, settin
|
|
|
24
24
|
const link = await requireProviderLink({ root, provider: candidate.provider, expectedEpoch: settings.expectedProviderEpoch });
|
|
25
25
|
const recommendationOptions = { allowDiagnosis: settings.purposeDriven === true && candidate.autoContinue === true };
|
|
26
26
|
const recommendation = manager.requireRecommendation(candidate, recommendationOptions);
|
|
27
|
-
if (candidate.isRunning || candidate.capabilities?.canContinue !== true) throw new Error('
|
|
27
|
+
if (candidate.isRunning || candidate.capabilities?.canContinue !== true) throw new Error('Delivery Loop is unavailable for this conversation.');
|
|
28
28
|
const context = await readContext(candidate, { signal });
|
|
29
29
|
if (context.awaitingTurn || context.incomplete || !recommendation.contextDigest || context.contextDigest !== recommendation.contextDigest) {
|
|
30
30
|
await manager.invalidate(candidate);
|
|
@@ -34,7 +34,7 @@ export async function continueAnalyzedSession({ candidate, manager, root, settin
|
|
|
34
34
|
await beforeEnroll(candidate);
|
|
35
35
|
await requireProviderLink({ root, provider: candidate.provider, expectedEpoch: link.epoch });
|
|
36
36
|
manager.requireRecommendation(candidate, recommendationOptions);
|
|
37
|
-
if (signal?.aborted) throw new Error('
|
|
37
|
+
if (signal?.aborted) throw new Error('Delivery Loop was cancelled before enrollment.');
|
|
38
38
|
return enroll({ root, provider: candidate.provider, session: candidate.session, ...settings, expectedProviderEpoch: link.epoch });
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -46,7 +46,7 @@ function startingPresentation(candidate, status, reason) {
|
|
|
46
46
|
const canPresent = work && !work.needsAttention && !['running', 'stopped', 'archived', 'approval', 'question'].includes(work.state);
|
|
47
47
|
return { ...candidate, autoContinueStatus: status, autoContinueReason: reason,
|
|
48
48
|
...(canPresent ? { workState: { ...work, state: status,
|
|
49
|
-
label: status === 'starting' ? '
|
|
49
|
+
label: status === 'starting' ? 'Delivery Loop を準備中' : '自動復旧中', reason,
|
|
50
50
|
nextAction: null, needsAttention: false } } : {}) };
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -133,7 +133,7 @@ export function createAutomaticContinuation({ root, getManager, selectCandidate,
|
|
|
133
133
|
retries.set(key, { attempt, count, at: now() + Math.min(60_000, 1000 * 2 ** Math.min(count - 1, 6)) });
|
|
134
134
|
} else retries.delete(key);
|
|
135
135
|
errors.set(key, { epoch: candidate.autoContinueEpoch, recoverable,
|
|
136
|
-
reason: recoverable ? 'The AI connection is temporarily unavailable.
|
|
136
|
+
reason: recoverable ? 'The AI connection is temporarily unavailable. Delivery Loop will retry when the connection is available.' : continuationFailureReason(error) });
|
|
137
137
|
}
|
|
138
138
|
}).finally(() => {
|
|
139
139
|
if (controller.signal.aborted) { attempted.delete(key); retries.delete(key); }
|
|
@@ -152,14 +152,14 @@ export function createAutomaticContinuation({ root, getManager, selectCandidate,
|
|
|
152
152
|
if (candidate.autoContinue === true && candidate.capabilities?.canAutoContinue === true &&
|
|
153
153
|
!['managed', 'pending_takeover'].includes(candidate.binding?.mode)) {
|
|
154
154
|
if (pending.has(key)) return startingPresentation(candidate, 'starting',
|
|
155
|
-
'
|
|
155
|
+
'Delivery Loop is on. Preparing Delivery Loop for this conversation in the background.');
|
|
156
156
|
const failure = errors.get(key);
|
|
157
157
|
if (failure?.recoverable && failure.epoch === candidate.autoContinueEpoch)
|
|
158
158
|
return startingPresentation(candidate, 'recovering', failure.reason);
|
|
159
159
|
}
|
|
160
160
|
if (candidate.autoContinue === true && candidate.isRunning !== true &&
|
|
161
161
|
['unmanaged', 'monitor'].includes(candidate.binding?.mode || 'unmanaged') && candidate.analysis?.state === 'needs_user') return {
|
|
162
|
-
...candidate, autoContinueStatus: 'needs_user', autoContinueReason: candidate.analysis.reason || 'Your decision is needed before
|
|
162
|
+
...candidate, autoContinueStatus: 'needs_user', autoContinueReason: candidate.analysis.reason || 'Your decision is needed before Delivery Loop can proceed.',
|
|
163
163
|
};
|
|
164
164
|
const error = errors.get(`${candidate.provider}:${candidate.session}`);
|
|
165
165
|
if (error && candidate.autoContinue === true && candidate.autoContinueEpoch === error.epoch &&
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// History is a navigation index, not a real-time subscription. Keep ordinary
|
|
2
|
+
// status/preference refreshes cheap; an explicit Refresh still reloads history.
|
|
3
|
+
export function createCatalogRefreshPolicy({ now = Date.now, intervalMs = 300_000 } = {}) {
|
|
4
|
+
let completed = null, scope = null, revision = 0;
|
|
5
|
+
return {
|
|
6
|
+
invalidate() { completed = null; revision++; },
|
|
7
|
+
begin(nextScope) {
|
|
8
|
+
if (scope !== nextScope) { scope = nextScope; completed = null; revision++; }
|
|
9
|
+
const generation = revision;
|
|
10
|
+
const recentOnly = completed !== null && now() - completed < intervalMs;
|
|
11
|
+
return { recentOnly, complete(success) {
|
|
12
|
+
if (!recentOnly && success && generation === revision) completed = now();
|
|
13
|
+
} };
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
|
|
3
|
+
// Observed foreground-only Agent contract. Omission is never evidence of
|
|
4
|
+
// foreground execution when the provider exposes background or unknown options.
|
|
5
|
+
const FOREGROUND_SCHEMA = {
|
|
6
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema', type: 'object',
|
|
7
|
+
properties: { description: { type: 'string' }, prompt: { type: 'string' },
|
|
8
|
+
subagent_type: { type: 'string' }, isolation: { type: 'string', enum: ['worktree', 'remote'] } },
|
|
9
|
+
required: ['description', 'prompt'], additionalProperties: false,
|
|
10
|
+
};
|
|
11
|
+
const withoutDescriptions = value => Array.isArray(value) ? value.map(withoutDescriptions) :
|
|
12
|
+
value && typeof value === 'object' ? Object.fromEntries(Object.entries(value)
|
|
13
|
+
.filter(([key, item]) => key !== 'description' || typeof item !== 'string')
|
|
14
|
+
.map(([key, item]) => [key, withoutDescriptions(item)])) : value;
|
|
15
|
+
|
|
16
|
+
export function isForegroundOnlyAgentSchema(schema) {
|
|
17
|
+
return isDeepStrictEqual(withoutDescriptions(schema), FOREGROUND_SCHEMA);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isForegroundNativeReviewInput(toolName, args, observedSchema = null) {
|
|
21
|
+
if (!['Agent', 'Task'].includes(toolName) || !args || typeof args !== 'object' || Array.isArray(args) ||
|
|
22
|
+
typeof args.prompt !== 'string' || typeof args.subagent_type !== 'string' ||
|
|
23
|
+
Object.keys(args).some(key => !['description', 'prompt', 'subagent_type', 'run_in_background'].includes(key)) ||
|
|
24
|
+
Object.hasOwn(args, 'description') && typeof args.description !== 'string') return false;
|
|
25
|
+
if (Object.hasOwn(args, 'run_in_background')) return args.run_in_background === false;
|
|
26
|
+
// The observed Agent schema cannot establish an omitted flag for legacy Task.
|
|
27
|
+
return toolName === 'Agent' && typeof args.description === 'string' && isForegroundOnlyAgentSchema(observedSchema);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Callers supply rows read from the exact owned, stable parent transcript,
|
|
31
|
+
// never a worker-provided schema. A schema first seen after the call proves nothing.
|
|
32
|
+
export function foregroundAgentContract(rows, candidate, beforeIndex = rows.length) {
|
|
33
|
+
const collector = createForegroundAgentContractCollector(candidate);
|
|
34
|
+
for (const row of rows.slice(0, beforeIndex)) collector.add(row);
|
|
35
|
+
return collector.finish();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Keep the prior schema and version agreement without retaining transcript
|
|
39
|
+
// bodies. A snapshot taken before a call cannot be authorized by a later shape.
|
|
40
|
+
export function createForegroundAgentContractCollector(candidate) {
|
|
41
|
+
let shape = null, shapes = 0, version, mixedVersions = false;
|
|
42
|
+
return {
|
|
43
|
+
add(row) {
|
|
44
|
+
if (row.type === 'api-request-shape') { shapes++; if (shapes === 1) shape = row; }
|
|
45
|
+
if (row.version !== undefined) {
|
|
46
|
+
mixedVersions ||= version !== undefined && version !== row.version;
|
|
47
|
+
version = row.version;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
finish() {
|
|
51
|
+
const row = shape;
|
|
52
|
+
if (shapes !== 1 || mixedVersions || version !== row.version || row.sessionId !== candidate.session || row.isSidechain === true ||
|
|
53
|
+
row.cwd !== undefined && row.cwd !== candidate.cwd ||
|
|
54
|
+
!/^[a-f0-9]{64}$/.test(row.shapeHash || '') || !/^\d+\.\d+\.\d+$/.test(row.version || '') ||
|
|
55
|
+
!Array.isArray(row.shape?.tools)) return null;
|
|
56
|
+
const agents = row.shape.tools.filter(tool => tool.name === 'Agent');
|
|
57
|
+
if (agents.length !== 1 || !isForegroundOnlyAgentSchema(agents[0].input_schema)) return null;
|
|
58
|
+
return { schema: agents[0].input_schema, shapeHash: row.shapeHash,
|
|
59
|
+
session: candidate.session, cwd: candidate.cwd, version: row.version ?? null };
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -7,7 +7,7 @@ import { basename, relative, resolve } from 'node:path';
|
|
|
7
7
|
import { runStructuredProviderReview } from '../sqm/review-worker.js';
|
|
8
8
|
import { captureAcceptanceReads } from './acceptance-reads.js';
|
|
9
9
|
import { readClaudeConversation, readClaudeNativeReview } from './claude-sessions.js';
|
|
10
|
-
import {
|
|
10
|
+
import { independentReviewRequestContext, workspaceIdentity } from './purpose-verification.js';
|
|
11
11
|
import { effectiveAcceptanceCriteria } from '../codex-plugins/simy/hooks/acceptance-policy.mjs';
|
|
12
12
|
import { liveOutcomeReviewEligible, settleLiveOutcomeReview, readRecord, taskDirectory, hasPendingUserStop, providerLinkState, processIdentity } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
13
13
|
|
|
@@ -52,7 +52,7 @@ export async function claudeReviewBudgetBoundary(record, { platform = process.pl
|
|
|
52
52
|
// original Claude process waits for its CLI child and retains its writer lease.
|
|
53
53
|
// No second controller/session resumes or writes the task workspace.
|
|
54
54
|
export async function reviewClaudeCheckpoint(record, { root, review = runStructuredProviderReview,
|
|
55
|
-
readContext = readClaudeConversation, readNativeReview = readClaudeNativeReview, onFailure = () => {}, captureReads = captureAcceptanceReads, inspectBudget = claudeReviewBudgetBoundary, signal, executionTimeoutMs = 120000 } = {}) {
|
|
55
|
+
readContext = readClaudeConversation, readNativeReview = readClaudeNativeReview, onFailure = () => {}, captureReads = captureAcceptanceReads, fingerprintWorkspace = workspaceIdentity, inspectBudget = claudeReviewBudgetBoundary, signal, executionTimeoutMs = 120000 } = {}) {
|
|
56
56
|
if (!liveOutcomeReviewEligible(record)) return record;
|
|
57
57
|
const snapshot = structuredClone(record), cwd = taskDirectory(record), criteria = effectiveAcceptanceCriteria(record.run.managed);
|
|
58
58
|
const abort = new AbortController();
|
|
@@ -77,17 +77,18 @@ export async function reviewClaudeCheckpoint(record, { root, review = runStructu
|
|
|
77
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
78
|
findings: [], source: 'controller_review_unavailable', checkedAt: Date.now() } });
|
|
79
79
|
const inventory = [];
|
|
80
|
-
const before = await
|
|
80
|
+
const before = await fingerprintWorkspace(cwd, undefined, inventory, { signal: combined, inventoryLimit: 1000 });
|
|
81
81
|
const reads = await captureReads(cwd, criteria, { signal: combined });
|
|
82
82
|
// This executor cannot independently inspect unnamed artifacts or services.
|
|
83
83
|
// Never turn absent/truncated input into a successful semantic judgment.
|
|
84
84
|
if (!reads.length || reads.some(row => row.observation.truncated || row.observation.redacted)) return await unavailable('Current outcome observations are absent or incomplete.');
|
|
85
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
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 });
|
|
87
|
+
const context = await readContext(record, { signal: combined, userOnly: true, reviewUserContext: true });
|
|
88
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
89
|
if (!await current()) return await readRecord(root, record.provider, record.session);
|
|
90
|
-
const requests =
|
|
90
|
+
const requests = independentReviewRequestContext(context);
|
|
91
|
+
if (requests.incomplete || requests.truncated || requests.redacted) return await unavailable('Original user requests exceed complete review coverage or contain redacted context; acceptance remains unverified.');
|
|
91
92
|
if (budget === 'worker_cap' || budget === 'native_only') {
|
|
92
93
|
const contextDigest = createHash('sha256').update(JSON.stringify(requests.messages.filter(row => row.text?.trim()))).digest('hex');
|
|
93
94
|
const pending = record.run.managed.nativeOutcomeReview;
|
|
@@ -111,7 +112,7 @@ export async function reviewClaudeCheckpoint(record, { root, review = runStructu
|
|
|
111
112
|
}
|
|
112
113
|
const result = await review('claude', {
|
|
113
114
|
system: CLAUDE_REVIEW_SYSTEM,
|
|
114
|
-
prompt: JSON.stringify({ objective: record.run.objective, requests
|
|
115
|
+
prompt: JSON.stringify({ objective: record.run.objective, requests, criteria, workspaceInventory: { paths: inventory.map(row => row.path), truncated: inventory.truncated === true }, observations: reads }),
|
|
115
116
|
output_schema: CLAUDE_REVIEW_SCHEMA
|
|
116
117
|
}, { signal: combined, executionTimeoutMs, subscriptionOnly: true, beforeDispatch: async () => {
|
|
117
118
|
if (!await current()) throw new Error('Review target changed');
|