@awak-app/simy-cli 0.7.8 → 0.7.10

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.
Files changed (32) hide show
  1. package/README.md +16 -9
  2. package/package.json +4 -3
  3. package/src/cli-contract.js +1 -1
  4. package/src/codex-plugins/simy/.codex-plugin/plugin.json +5 -7
  5. package/src/codex-plugins/simy/hooks/README.md +3 -1
  6. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +12 -2
  7. package/src/codex-plugins/simy/hooks/hypothesis-sheet.mjs +1 -1
  8. package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +6 -5
  9. package/src/codex-plugins/simy/hooks/recovery-incident.mjs +191 -0
  10. package/src/codex-plugins/simy/hooks/scene-reasoning.mjs +22 -0
  11. package/src/delivery-hooks/command.js +34 -1
  12. package/src/delivery-hooks/diagnosis-sheet.js +2 -2
  13. package/src/delivery-hooks/diagnosis.js +5 -3
  14. package/src/delivery-hooks/local-supervisor.js +7 -1
  15. package/src/delivery-hooks/recovery.js +33 -0
  16. package/src/delivery-hooks/supervisor-owner.js +74 -0
  17. package/src/delivery-hooks/supervisor-service.js +65 -0
  18. package/src/delivery-hooks/supervisor-watchdog.js +151 -0
  19. package/src/delivery-hooks/supervisor.js +26 -0
  20. package/src/direct-executor.js +2 -1
  21. package/src/orchestrator/instruction.js +4 -4
  22. package/src/orchestrator/loop.js +1 -1
  23. package/src/orchestrator/problem-solving.js +2 -2
  24. package/src/session-supervision/codex-app-server.js +9 -5
  25. package/src/session-supervision/continuation-analysis.js +3 -2
  26. package/src/session-supervision/purpose-instruction.js +10 -2
  27. package/src/session-supervision/purpose-recovery.js +7 -4
  28. package/src/sqm/claude-plugin.js +6 -0
  29. package/src/sqm/local-tests.js +1 -0
  30. package/src/sqm/review-worker.js +3 -0
  31. package/src/sqm/workflow-onboarding.js +1 -1
  32. /package/src/{codex-plugins/simy/skills/delivery-loop/SKILL.md → delivery-hooks/claude-workflow.md} +0 -0
@@ -617,7 +617,7 @@ async function frozenNativeSandbox(record, root, signal) {
617
617
  return policy;
618
618
  }
619
619
 
620
- export async function executeCodexAppServer(record, { webOrigin, openClient = openCodexAppServer, signal, authorizePreparation = async () => {}, beforeDispatch = async () => {}, onActivity = () => {}, onUsage = () => {}, onEvidence = async () => {}, observeExternal = collectPurposeObservations, root, auditOnly = false, opinionOnly = false, peerAction, runPeerReview = runStructuredProviderReview } = {}) {
620
+ export async function executeCodexAppServer(record, { webOrigin, openClient = openCodexAppServer, signal, authorizePreparation = async () => {}, beforeDispatch = async () => {}, onStarted = async () => {}, onActivity = () => {}, onUsage = () => {}, onEvidence = async () => {}, observeExternal = collectPurposeObservations, root, auditOnly = false, opinionOnly = false, peerAction, runPeerReview = runStructuredProviderReview } = {}) {
621
621
  const run = record.run, decision = run.managed?.decision;
622
622
  if(run.managed?.enrollment?.nativeAuthorityDigest && run.managed.enrollment.nativeAuthorityDigest!==createHash('sha256').update(JSON.stringify([run.managed.providerSandbox,run.managed.providerSandboxSource])).digest('hex'))throw new CodexAppServerError('permission_narrowed','Frozen native enrollment authority changed; do not dispatch with the altered ceiling.');
623
623
  if (!decision || !run.managed.autonomyPolicy) throw new CodexAppServerError('decision', 'An assessed action is required before Codex dispatch.');
@@ -672,7 +672,7 @@ export async function executeCodexAppServer(record, { webOrigin, openClient = op
672
672
  };
673
673
  const beforeEvidence = searchEnabled(run.managed) && !reviewing ? await captureWorkspace() : null;
674
674
  let evidenceQueue = Promise.resolve(), evidenceFailure = null;
675
- let activeTurnId = null, turnCompleted = false, turnStart = null;
675
+ let activeTurnId = null, turnCompleted = false, turnStart = null, startReceipt = null;
676
676
  let usage = null, lastTotal = null;
677
677
  try {
678
678
  // An unavailable login is a connection dependency, not an action approval.
@@ -789,7 +789,7 @@ export async function executeCodexAppServer(record, { webOrigin, openClient = op
789
789
  }
790
790
  if (signal?.aborted) throw new CodexAppServerError('cancelled', 'Worker dispatch was cancelled during preparation.');
791
791
  const done = new Promise((resolve, reject) => {
792
- let turnId = null, completion = null, settled = false;
792
+ let turnId = null, completion = null, settled = false, startRecorded = false;
793
793
  const messages = new Map();
794
794
  const activeTools = new Set();
795
795
  const earlyActivity = [];
@@ -819,7 +819,7 @@ export async function executeCodexAppServer(record, { webOrigin, openClient = op
819
819
  const cancelled = () => finish(new CodexAppServerError('cancelled', 'Codex turn was cancelled.'));
820
820
  const closed = () => finish(new CodexAppServerError('closed', 'Codex app-server closed before turn completion.'));
821
821
  const complete = () => {
822
- if (!completion || !turnId || completion.turn?.id !== turnId) return;
822
+ if (!completion || !turnId || !startRecorded || completion.turn?.id !== turnId) return;
823
823
  turnCompleted = true;
824
824
  if (completion.turn.status !== 'completed') return finish(new CodexAppServerError('turn_failed', 'Codex turn did not complete.'));
825
825
  const message = messages.get(turnId);
@@ -846,7 +846,10 @@ export async function executeCodexAppServer(record, { webOrigin, openClient = op
846
846
  client.child.once('close', closed);
847
847
  turnStart = client.request('turn/start', { threadId: targetSession, input: [{ type: 'text', text: `${prompt}\n${purposeInstruction}` }], cwd: isolated ? executionCwd : record.cwd,
848
848
  model: run.managed.resumeModel, ...(isolated ? {sandboxPolicy} : {}) })
849
- .then(response => { turnId = response?.turn?.id; activeTurnId = turnId; if (!turnId) finish(new CodexAppServerError('turn_failed', 'Codex did not start a turn.')); else {
849
+ .then(async response => { turnId = response?.turn?.id; activeTurnId = turnId; if (!turnId) finish(new CodexAppServerError('turn_failed', 'Codex did not start a turn.')); else {
850
+ startReceipt = Promise.resolve().then(() => onStarted({ turnId, session: targetSession }));
851
+ await startReceipt;
852
+ startRecorded = true;
850
853
  for (const params of earlyUsage) if (params.turnId === turnId) observeUsage(params);
851
854
  for (const params of earlyActivity) if (params.turnId === turnId) observeActivity(params.method, params);
852
855
  complete();
@@ -912,6 +915,7 @@ export async function executeCodexAppServer(record, { webOrigin, openClient = op
912
915
  await evidenceQueue;
913
916
  throw error;
914
917
  } finally {
918
+ if (startReceipt) await startReceipt.catch(() => {});
915
919
  if (turnStart && !client.closed) {
916
920
  // Only the controller-owned target may be interrupted. Isolated reads
917
921
  // never load or change the original conversation's policy defaults.
@@ -1,3 +1,4 @@
1
+ import { withSceneReasoning } from '../codex-plugins/simy/hooks/scene-reasoning.mjs';
1
2
  import { readCodexConversation } from './codex-app-server.js';
2
3
  import { readClaudeConversation } from './claude-sessions.js';
3
4
  import { boundConversation, boundProviderGoal, providerGoalEvidence, sanitizedText } from './conversation-context.js';
@@ -5,7 +6,7 @@ import { runStructuredProviderReview } from '../sqm/review-worker.js';
5
6
  import { inspectProviderStatus } from '../desktop-status.js';
6
7
  import { createHash } from 'node:crypto';
7
8
 
8
- export const CONTINUATION_ANALYSIS_VERSION = 5;
9
+ export const CONTINUATION_ANALYSIS_VERSION = 6;
9
10
  const BLOCKER_TYPES = ['approval', 'missing_information', 'external_dependency', 'execution_failure'];
10
11
  const STATES = ['continue', 'no_action', 'needs_user', 'waiting', 'uncertain'];
11
12
  const string = maximum => ({ type: 'string', maxLength: maximum });
@@ -228,7 +229,7 @@ export async function analyzeContinuation(candidate, {
228
229
  await beforeProviderDispatch();
229
230
  if (signal?.aborted) return uncertain('Conversation analysis was cancelled.', 'cancelled');
230
231
  result = await runProvider(candidate.provider, {
231
- system: SYSTEM, prompt: `Classify this conversation evidence only (older history omitted: ${context.truncated}):\n${JSON.stringify(context.providerGoal ? { originalProviderGoal: context.providerGoal, messages: context.messages } : context.messages)}`, output_schema: OUTPUT_SCHEMA,
232
+ system: withSceneReasoning(SYSTEM), prompt: `Classify this conversation evidence only (older history omitted: ${context.truncated}):\n${JSON.stringify(context.providerGoal ? { originalProviderGoal: context.providerGoal, messages: context.messages } : context.messages)}`, output_schema: OUTPUT_SCHEMA,
232
233
  }, { ...providerOptions, signal, readOnlyAnalysis: true, beforeDispatch: beforeProviderDispatch });
233
234
  } catch (error) {
234
235
  if (error?.code === 'desktop_access_required') return uncertain('SIMY organization access is required before checking this conversation.', 'desktop_access_required');
@@ -1,3 +1,5 @@
1
+ import { withSceneReasoning } from '../codex-plugins/simy/hooks/scene-reasoning.mjs';
2
+ import { currentRecoveryIncident, unresolvedRecoveryContext } from '../codex-plugins/simy/hooks/recovery-incident.mjs';
1
3
  import { recallPerspectives, perspectiveInstruction } from '../codex-plugins/simy/hooks/perspective-memory.mjs';
2
4
  import { loadHypothesisKnowledge, hypothesisKnowledgeInstruction } from '../sqm/hypothesis-knowledge.js';
3
5
  import { selectiveRuleReview, applicableRuleIds } from '../codex-plugins/simy/hooks/purpose-rule-policy.mjs';
@@ -40,12 +42,18 @@ export async function loadPurposeRuleSet() {
40
42
  return { digest: createHash('sha256').update(JSON.stringify(sources)).digest('hex'), rules };
41
43
  }
42
44
  export async function buildPurposeInstruction(managed, { reviewer = false, cwd, webOrigin, allowNetwork = false, signal, knowledgeLoader = loadHypothesisKnowledge, authorizeKnowledgeNetwork, root: memoryRoot, record, memoryLoader = recallPerspectives } = {}) {
43
- if (!managed?.purpose?.enabled) return '';
45
+ if (!managed?.purpose?.enabled) return withSceneReasoning();
44
46
  const ruleSet = await loadPurposeRuleSet();
45
47
  if (ruleSet.digest !== managed.purpose.ruleDigest) throw Object.assign(new Error('Purpose workflow changed; refresh its rule inventory before dispatch.'), { code: 'purpose_rules_changed' });
46
48
  if (reviewer && selectiveRuleReview(managed)) return `${llmDecisionInstruction}\nIndependently verify the original acceptance criteria and applicable canonical obligations from actual evidence. Consult the inventory as needed after forming an LLM judgment; do not enumerate every ID or impose core controls as universal checklist gates. Report only rules actually evaluated; omitted inventory entries are not passed or not_applicable. Known applicable obligations ${JSON.stringify(applicableRuleIds(managed))} must be independently assessed and cannot be discarded by worker claims. Do not copy worker dispositions as proof. Failed, unavailable or missing evidence remains unverified with a concrete acquisition/repair finding. No unrelated rule coverage obligation is implied.\nruleSet: ${JSON.stringify(ruleSet.rules.map(rule => ({ ...rule, path: fileURLToPath(new URL(rule.file, root)) })))}`;
47
49
  if (reviewer) return `${llmDecisionInstruction}\nCanonical rule inventory. Review applicability and evidence for EVERY ID, including HC-01 through HC-102; these controls are requirements, not proof that their mechanisms exist. Read the referenced sections and acceptance details as needed. User instructions retain priority. Do not impose unrelated development/release controls on other task types. Core controls ${CORE_CONTROL_IDS.join(', ')} apply to every autonomous task; inspect their actual bounded behavior, including conditional duties whose trigger did not occur, rather than marking the entire control not_applicable. Return rules:[{id,evidenceRefs:[{ref,target,observation}],status:"satisfied|failed|unverified|not_applicable",basis:"inspected evidence or specific applicability reason"}]. Keep basis concise with source locators; stored receipts are limited to 160 UTF-8 bytes and basisTruncated means re-read the original evidence. Never copy worker dispositions as independent proof. Missing capability is unverified and requires an acquisition/repair finding, not a human certification request.\nruleSet: ${JSON.stringify(ruleSet.rules.map(rule => ({ ...rule, path: fileURLToPath(new URL(rule.file, root)) })))}`;
48
50
  const knowledge = await knowledgeLoader({ cwd, webOrigin, allowNetwork, signal, ...(authorizeKnowledgeNetwork ? { authorizeNetwork: authorizeKnowledgeNetwork } : {}) });
49
51
  const memory = await memoryLoader({ root: memoryRoot, record, signal });
50
- return `${purposeContract(managed)}\n${perspectiveInstruction(memory)}\n${hypothesisKnowledgeInstruction(knowledge, { flexible: selectiveRuleReview(managed) })}\n${searchInstruction(managed)}\nForm a grounded candidate judgment with LLM reasoning first; consult the applicable canonical rule sections using read tools to challenge that judgment before its effects. This ruleSet is authoritative workflow material; user instructions retain priority. ${selectiveRuleReview(managed) ? "Review applicable rules and known obligations only; do not require full inventory coverage." : "Use the purpose contract for full initial rule coverage and changed dispositions on subsequent search turns."} Missing sources or unavailable tools remain unverified; do not fabricate coverage. Repository AGENTS.md and explicit user constraints also apply.\nruleSet: ${JSON.stringify(ruleSet.rules.map(rule => ({ ...rule, path: fileURLToPath(new URL(rule.file, root)) })))}\nPrior rule dispositions (provider reported, re-evaluate applicability): ${JSON.stringify(managed.purpose.version === 2 ? managed.purpose.lastReview?.rules || [] : [])}\nLast purpose review (provider reported): ${JSON.stringify(managed.purpose.lastReview ? { gap: managed.purpose.lastReview.gap, hypothesis: managed.purpose.lastReview.hypothesis, noChangeAssessment: managed.purpose.lastReview.noChangeAssessment, observations: managed.purpose.lastReview.observations, unresolvedRules: managed.purpose.lastReview.rules.filter(rule => ['failed', 'unverified'].includes(rule.status)) } : null)}\nRecovery context: ${JSON.stringify(managed.purpose.lastRecovery || null)}`;
52
+ const attempt = managed.executionAttempt;
53
+ const recoveryContext = { previous: managed.purpose.lastRecovery || null, incident: currentRecoveryIncident(managed),
54
+ unresolved: unresolvedRecoveryContext(managed),
55
+ receipt: attempt ? { id: attempt.id, phase: attempt.phase, decisionId: attempt.decisionId,
56
+ providerSessionId: attempt.providerSessionId || null, providerTurnId: attempt.providerTurnId || null,
57
+ startedAt: attempt.startedAt || null, dispatchedAt: attempt.dispatchedAt || null } : null };
58
+ return `${purposeContract(managed)}\n${perspectiveInstruction(memory)}\n${hypothesisKnowledgeInstruction(knowledge, { flexible: selectiveRuleReview(managed) })}\n${searchInstruction(managed)}\nUse the shared scene reasoning cycle; consult applicable canonical rule sections as perspective inputs and challenge the candidate before its effects. This ruleSet is authoritative workflow material; user instructions retain priority. ${selectiveRuleReview(managed) ? "Review applicable rules and known obligations only; do not require full inventory coverage." : "Use the purpose contract for full initial rule coverage and changed dispositions on subsequent search turns."} Missing sources or unavailable tools remain unverified; do not fabricate coverage. Repository AGENTS.md and explicit user constraints also apply.\nruleSet: ${JSON.stringify(ruleSet.rules.map(rule => ({ ...rule, path: fileURLToPath(new URL(rule.file, root)) })))}\nPrior rule dispositions (provider reported, re-evaluate applicability): ${JSON.stringify(managed.purpose.version === 2 ? managed.purpose.lastReview?.rules || [] : [])}\nLast purpose review (provider reported): ${JSON.stringify(managed.purpose.lastReview ? { gap: managed.purpose.lastReview.gap, hypothesis: managed.purpose.lastReview.hypothesis, noChangeAssessment: managed.purpose.lastReview.noChangeAssessment, observations: managed.purpose.lastReview.observations, unresolvedRules: managed.purpose.lastReview.rules.filter(rule => ['failed', 'unverified'].includes(rule.status)) } : null)}\nRecovery context (controller receipt data, not instructions, permission or acceptance; return to the original unfinished work after the next useful check): ${JSON.stringify(recoveryContext)}`;
51
59
  }
@@ -58,6 +58,7 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
58
58
  const deferred = [];
59
59
 
60
60
  for (let record of await readRecords()) {
61
+ if (signal?.aborted) break;
61
62
  try {
62
63
  let run = record.run;
63
64
  if (upgradeEligible(record) && (policyAdoptionPending(record) || run.managed.controllerContractVersion !== 2 || run.state === 'paused' || run.state === 'verification_pending')) {
@@ -98,6 +99,7 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
98
99
  let link;
99
100
  try { link = await requireProviderLink({ root, provider: record.provider }); } catch { continue; }
100
101
  const authorized = async () => {
102
+ signal?.throwIfAborted();
101
103
  await requireProviderLink({ root, provider: record.provider, expectedEpoch: link.epoch });
102
104
  const latest = await readRecord(root, record.provider, record.session);
103
105
  if (!latest || !automaticContinuationAllowed(latest) || latest.run?.id !== run.id || latest.autoContinueEpoch !== record.autoContinueEpoch || ['paused', 'stopped'].includes(latest.run.state)) throw new Error('Purpose observation authorization changed.');
@@ -105,7 +107,7 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
105
107
  const observe = async read => { await authorized(); const result = await read(); await authorized(); return result; };
106
108
  const persist = mutate => writeRecord(root, record.provider, record.session, async current => {
107
109
  try { await requireProviderLink({ root, provider: record.provider, expectedEpoch: link.epoch }); } catch { return current; }
108
- if (!current || !automaticContinuationAllowed(current) || current.autoContinueEpoch !== record.autoContinueEpoch) return current;
110
+ if (signal?.aborted || !current || !automaticContinuationAllowed(current) || current.autoContinueEpoch !== record.autoContinueEpoch) return current;
109
111
  return mutate(current);
110
112
  });
111
113
  // Recheck the current budget and boundaries under the existing record lock.
@@ -203,7 +205,7 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
203
205
  if (current?.run?.id !== run.id || current.run.state !== 'blocked' || current.run.revision !== run.revision || current.run.updatedAt !== run.updatedAt) return current;
204
206
  current.run.managed.authenticationWait.nextCheckAt = now + 60_000;
205
207
  if (authentication === 'available') {
206
- const queued = queuePurposeDiagnosis(current.run, 'Configured provider login is available again. Inspect the original purpose and any interrupted action before choosing authorized work.', `authentication-restored:${run.managed.authenticationWait.since}`, now);
208
+ const queued = queuePurposeDiagnosis(current.run, 'Configured provider login is available again. Inspect the original purpose and any interrupted action before choosing authorized work.', `authentication-restored:${run.managed.authenticationWait.since}`, now, 'authentication');
207
209
  if (queued) delete current.run.managed.authenticationWait;
208
210
  }
209
211
  return current;
@@ -219,7 +221,7 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
219
221
  current.run.managed.purpose.observationState = observed.state;
220
222
  return current;
221
223
  });
222
- if (observed.state === 'idle') obstacle = { type: 'idle_without_checkpoint', detail: observed.turnId };
224
+ if (observed.state === 'idle' && (!observed.session || observed.session === record.session)) obstacle = { type: 'idle_without_checkpoint', detail: observed.turnId };
223
225
  } else if (run.state === 'blocked' && ['missing_information', 'authorization', 'external_dependency'].includes(run.blocker)) {
224
226
  if (run.blockerSource === 'provider' || run.managed.attempts?.at(-1)?.errorCode === 'provider_approval') {
225
227
  if (now < (run.managed.purpose.lastObservedAt || 0) + 60_000) continue;
@@ -309,7 +311,7 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
309
311
  if (['native_wait_updated', 'native_wait_diagnosis'].includes(obstacle.type) && !nativeCurrent(current)) return current;
310
312
  if (searchEnabled(current.run.managed) && ['original_context_updated', 'native_wait_updated'].includes(obstacle.type)) current.run.managed.purpose.nextSearchAt = 0;
311
313
  const queued = queuePurposeDiagnosis(current.run,
312
- `Reassess the original purpose after ${obstacle.type}: ${obstacle.detail}. Gather evidence before deciding that a person is needed. Preserve unresolved approvals/dependencies; choose independent work or a safe alternative where possible.`, key, Date.now());
314
+ `Reassess the original purpose after ${obstacle.type}: ${obstacle.detail}. Use the shared scene reasoning cycle to select the next useful check. Preserve unresolved approvals/dependencies; choose independent work or a safe alternative where possible, then return to the original unfinished work.`, key, now, obstacle.type);
313
315
  if (queued && obstacle.type === 'external_dependency') {
314
316
  const purpose = current.run.managed.purpose;
315
317
  purpose.dependencyChecks = (purpose.dependencyChecks || 0) + 1;
@@ -318,6 +320,7 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
318
320
  return current;
319
321
  });
320
322
  } catch (error) {
323
+ if (signal?.aborted) break;
321
324
  if (!/^Delivery state (?:is busy; retry the command\.|lock owner is unavailable; inspect the lock before retrying\.)$/.test(error.message)) throw error;
322
325
  // One unavailable record cannot prevent independent conversations from
323
326
  // progressing. Never delete or reclaim an ambiguous writer lock.
@@ -21,7 +21,12 @@ export async function claudePluginFiles() {
21
21
  const content = await readFile(path.join(SOURCE, relative), "utf8");
22
22
  files.set(relative, content.replaceAll("current Codex thread", "current Claude Code conversation"));
23
23
  }
24
+ // Codex's local delivery skill is retired. Keep the separately maintained
25
+ // Claude workflow without shipping it back into Codex's skill inventory.
26
+ files.set("skills/delivery-loop/SKILL.md", await readFile(
27
+ new URL("../delivery-hooks/claude-workflow.md", import.meta.url), "utf8"));
24
28
  files.set("hooks/delivery-loop.mjs", await readFile(path.join(SOURCE, "hooks/delivery-loop.mjs"), "utf8"));
29
+ files.set("hooks/recovery-incident.mjs", await readFile(path.join(SOURCE, "hooks/recovery-incident.mjs"), "utf8"));
25
30
  files.set("hooks/minimal-sufficient.mjs", await readFile(path.join(SOURCE, "hooks/minimal-sufficient.mjs"), "utf8"));
26
31
  files.set("hooks/outcome-progress.mjs", await readFile(path.join(SOURCE, "hooks/outcome-progress.mjs"), "utf8"));
27
32
  files.set("hooks/continuation-policy.mjs", await readFile(path.join(SOURCE, "hooks/continuation-policy.mjs"), "utf8"));
@@ -34,6 +39,7 @@ export async function claudePluginFiles() {
34
39
  files.set("hooks/completion-binding.mjs", await readFile(path.join(SOURCE, "hooks/completion-binding.mjs"), "utf8"));
35
40
  files.set("hooks/consistency-handoff.mjs", await readFile(path.join(SOURCE, "hooks/consistency-handoff.mjs"), "utf8"));
36
41
  files.set("hooks/purpose-evidence.mjs", await readFile(path.join(SOURCE, "hooks/purpose-evidence.mjs"), "utf8"));
42
+ files.set("hooks/scene-reasoning.mjs", await readFile(path.join(SOURCE, "hooks/scene-reasoning.mjs"), "utf8"));
37
43
  files.set("hooks/purpose-cycle.mjs", await readFile(path.join(SOURCE, "hooks/purpose-cycle.mjs"), "utf8"));
38
44
  files.set("hooks/purpose-rule-policy.mjs", await readFile(path.join(SOURCE, "hooks/purpose-rule-policy.mjs"), "utf8"));
39
45
  files.set("hooks/acceptance-policy.mjs", await readFile(path.join(SOURCE, "hooks/acceptance-policy.mjs"), "utf8"));
@@ -8,6 +8,7 @@ import path from 'node:path';
8
8
  // This catalog is shipped with the CLI; a repository cannot supply shell commands.
9
9
  const profiles = {
10
10
  'delivery-loop': [
11
+ { id: 'autonomous-recovery', files: ['test/autonomous-recovery.test.js', 'test/scene-reasoning.test.js', 'test/supervisor-watchdog.test.js'] },
11
12
  { id: 'perspective-memory', files: ['test/perspective-memory.test.js'] },
12
13
  { id: 'optional-reasoning', files: ['test/purpose-selective-rules.test.js', 'test/purpose-evidence.test.js', 'test/purpose-rule-review.test.js'] },
13
14
  // This connected controller group now contains over 200 cases. Keep a
@@ -1,3 +1,4 @@
1
+ import { withSceneReasoning } from '../codex-plugins/simy/hooks/scene-reasoning.mjs';
1
2
  import { requireProviderSubscription } from './subscription-auth.js';
2
3
  import { signalExecutorProcess } from "../runner.js";
3
4
  import spawn from "cross-spawn";
@@ -398,6 +399,8 @@ export function runStructuredCodexReview(request, options) {
398
399
  }
399
400
 
400
401
  export async function runStructuredProviderReview(provider, request, options = {}) {
402
+ // Keep malformed requests invalid; the reasoning policy must not fill missing authority or inputs.
403
+ if (typeof request?.system === 'string' && request.system.trim()) request = { ...request, system: withSceneReasoning(request.system) };
401
404
  const fallbackProviders = options.enableEngineFallback === true && options.providerReportingSupported === true && options.readOnlyAnalysis !== false && Array.isArray(options.allowedFallbackProviders)
402
405
  ? [...new Set(options.allowedFallbackProviders.filter(candidate => ["codex", "claude"].includes(candidate) && candidate !== provider))] : [];
403
406
  if (!fallbackProviders.length) return runSingleProviderReview(provider, request, options);
@@ -58,7 +58,7 @@ export async function prepareDaemonWorkflows({
58
58
  try {
59
59
  const installed = await install();
60
60
  logger(
61
- "SIMY workflows installed. In Codex, restart, then find and install SIMY in Plugins. In Claude Code, add the SIMY marketplace and install simy@simy-local before using /simy:sqm-loop or /simy:pr-review. For Delivery Loop, review and enable SIMY hooks in /hooks, then request /simy:delivery-loop for each task. Plugin installation does not grant hook trust.",
61
+ "SIMY workflows installed. In Codex, restart, then find and install SIMY in Plugins for SQM and PR review. For the Codex Agentic Loop, run simy in a terminal and select Codex. In Claude Code, add the SIMY marketplace and install simy@simy-local before using /simy:sqm-loop or /simy:pr-review. For Claude Code Delivery Loop, review and enable SIMY hooks in /hooks, then request /simy:delivery-loop for each task. Plugin installation does not grant hook trust.",
62
62
  );
63
63
  if (installed.claude_plugin?.marketplace_root) {
64
64
  logger(`Claude Code: claude plugin marketplace add ${JSON.stringify(installed.claude_plugin.marketplace_root)}`);