@awak-app/simy-cli 0.7.4 → 0.7.6

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 (35) hide show
  1. package/README.md +51 -0
  2. package/package.json +2 -1
  3. package/src/cli-contract.js +1 -1
  4. package/src/codex-plugins/simy/hooks/consistency-handoff.mjs +2 -2
  5. package/src/codex-plugins/simy/hooks/delivery-loop.mjs +144 -59
  6. package/src/codex-plugins/simy/hooks/perspective-memory.mjs +115 -0
  7. package/src/codex-plugins/simy/hooks/purpose-cycle.mjs +43 -12
  8. package/src/codex-plugins/simy/hooks/purpose-evidence.mjs +31 -8
  9. package/src/codex-plugins/simy/hooks/purpose-search.mjs +2 -1
  10. package/src/codex-plugins/simy/hooks/state-limits.mjs +2 -0
  11. package/src/codex-plugins/simy/skills/delivery-loop/SKILL.md +23 -1
  12. package/src/codex-plugins/simy/skills/sqm-loop/SKILL.md +2 -0
  13. package/src/delivery-hooks/checkpoint-diagnostic.js +1 -0
  14. package/src/delivery-hooks/command.js +10 -2
  15. package/src/delivery-hooks/consistency-monitor.js +71 -22
  16. package/src/delivery-hooks/desktop.js +2 -2
  17. package/src/delivery-hooks/local-runtime.js +2 -2
  18. package/src/delivery-hooks/supervisor.js +15 -4
  19. package/src/desktop-host.js +7 -2
  20. package/src/session-supervision/codex-app-server.js +84 -36
  21. package/src/session-supervision/consistency-auditor.js +87 -34
  22. package/src/session-supervision/index.js +22 -1
  23. package/src/session-supervision/native-delegation.js +86 -0
  24. package/src/session-supervision/purpose-instruction.js +4 -2
  25. package/src/session-supervision/purpose-recovery.js +48 -16
  26. package/src/session-supervision/purpose-verification.js +95 -10
  27. package/src/session-supervision/redelegate-native.js +48 -0
  28. package/src/sqm/claude-plugin.js +2 -0
  29. package/src/sqm/command.js +10 -3
  30. package/src/sqm/hypothesis-knowledge.js +2 -1
  31. package/src/sqm/index.js +11 -5
  32. package/src/sqm/local-tests.js +155 -0
  33. package/src/sqm/proof.js +14 -3
  34. package/src/sqm/report.js +8 -3
  35. package/src/sqm/review-worker.js +16 -2
@@ -2,13 +2,14 @@ import { completionClaimUnbound } from '../codex-plugins/simy/hooks/completion-b
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { automaticContinuationAllowed, queuePurposeVerification, processIdentity, readRecord, updateRecord } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
4
4
  import { purposeDiagnosisPlan } from '../codex-plugins/simy/hooks/purpose-cycle.mjs';
5
- import { reconcileFindings } from '../codex-plugins/simy/hooks/purpose-evidence.mjs';
5
+ import { reconcileFindings, evidenceExcerpt } from '../codex-plugins/simy/hooks/purpose-evidence.mjs';
6
6
  import { requireProviderLink } from '../provider-links.js';
7
7
  import { executeCodexAppServer, openCodexAppServer, readCodexSession } from './codex-app-server.js';
8
8
  import { pauseOrigin } from './autonomous-upgrade.js';
9
9
  import { realpath } from 'node:fs/promises';
10
10
  import { leaseOwnerDead } from './workspace-lease.js';
11
11
 
12
+ const isOpinionOnly = findings => findings.length > 0 && findings.every(row => row.code === 'outcome_alignment');
12
13
  const contract = record => createHash('sha256').update(JSON.stringify([record.run.id, record.run.revision,
13
14
  record.run.objective, record.run.managed.acceptanceCriteria, record.autoContinueEpoch])).digest('hex');
14
15
  export const canAuditOutcome = record => record?.provider === 'codex' && record.autoContinue === true &&
@@ -22,6 +23,33 @@ export const canAuditOutcome = record => record?.provider === 'codex' && record.
22
23
  !(Number.isFinite(record.run.maxContinuations) && record.run.continuations >= record.run.maxContinuations) &&
23
24
  !(record.run.managed.outcomeAudit?.usageReceipts?.filter(row => !row.settled).length >= 128) && !record.run.managed.usageBlocked && !(record.run.managed.maxTokens && record.run.managed.tokensUsed >= record.run.managed.maxTokens);
24
25
 
26
+ // Lease accounting is recovery bookkeeping, not permission to start an audit.
27
+ // Reach it even when a legacy worker lease coexists with the audit lease.
28
+ export async function recoverOutcomeAuditLease(record, { root, writeRecord = updateRecord, inspectProcess = processIdentity } = {}) {
29
+ const oldLease = record?.run?.managed?.outcomeAudit?.lease;
30
+ if (!oldLease) return record;
31
+ const oldOwnerDead = await leaseOwnerDead(oldLease.owner, inspectProcess);
32
+ if (!(Date.now() >= oldLease.expiresAt || oldOwnerDead)) return record;
33
+ const boundWrite = (root, provider, session, mutate) => writeRecord(root, provider, session, live => {
34
+ if (live?.run?.id !== record.run.id || live.run.managed?.taskId !== record.run.managed.taskId ||
35
+ live.run.managed.outcomeAudit?.lease?.id !== oldLease.id) return live;
36
+ return mutate(live);
37
+ });
38
+ // Keep unknown consumption and hard-budget blocks before releasing the lease.
39
+ await accountAuditUsage(root, record, oldLease.id, null, boundWrite);
40
+ return boundWrite(root, record.provider, record.session, live => {
41
+ const audit = live.run.managed.outcomeAudit;
42
+ delete audit.lease;
43
+ // Expiry fences late results; only confirmed death settles late-usage slots.
44
+ if (oldOwnerDead) {
45
+ const receipt = audit.usageReceipts?.find(row => row.id === oldLease.id);
46
+ if (receipt) receipt.settled = true;
47
+ }
48
+ audit.lastFailure = { at: Date.now(), code: 'audit_owner_exited', requestId: oldLease.requestId, attemptId: oldLease.id };
49
+ return live;
50
+ });
51
+ }
52
+
25
53
  // A distinct provider context reads original instructions and primary evidence.
26
54
  // It has its own read-only lease; original turns are never resumed or edited.
27
55
  async function auditConsistencyAttempt(record, findings, request, { root, signal,
@@ -36,34 +64,18 @@ async function auditConsistencyAttempt(record, findings, request, { root, signal
36
64
  signal?.throwIfAborted();
37
65
  const key = contract(record), id = request.attemptId;
38
66
  const owner = { pid: process.pid, identity: await processIdentity(process.pid) };
39
- const oldLease = record.run.managed.outcomeAudit?.lease;
40
- const oldOwnerDead = oldLease && await leaseOwnerDead(oldLease.owner, processIdentity);
41
- if (oldLease && (Date.now() >= oldLease.expiresAt || oldOwnerDead)) {
42
- // The final provider total cannot be recovered from a dead owner's memory.
43
- await accountAuditUsage(root, record, oldLease.id, null, writeRecord);
44
- await writeRecord(root, record.provider, record.session, live => {
45
- if (live?.run?.managed?.outcomeAudit?.lease?.id === oldLease.id) {
46
- delete live.run.managed.outcomeAudit.lease;
47
- // A confirmed dead process cannot deliver late usage. Settle its dedupe
48
- // slot, while retaining unknown consumption and every hard-budget block.
49
- // Expiry alone does not prove death, so live-owner receipts stay pending.
50
- if (oldOwnerDead) {
51
- const receipt = live.run.managed.outcomeAudit.usageReceipts?.find(row => row.id === oldLease.id);
52
- if (receipt) receipt.settled = true;
53
- }
54
- live.run.managed.outcomeAudit.lastFailure = { at: Date.now(), code: 'audit_owner_exited' };
55
- }
56
- return live;
57
- });
58
- }
59
67
  const claimed = await writeRecord(root, record.provider, record.session, live => {
60
68
  if (signal?.aborted || !canAuditOutcome(live) || contract(live) !== key || live.run.managed.outcomeAudit?.lease) return live;
61
69
  live.run.managed.outcomeAudit = { ...live.run.managed.outcomeAudit,
70
+ lastAttempt: { requestId: request.id, attemptId: id, stage: 'preparing', session: null },
62
71
  lease: { id, owner, requestId: request.id, contract: key, startedAt: Date.now(), expiresAt: request.deadlineAt } };
63
72
  return live;
64
73
  });
65
74
  if (claimed?.run?.managed?.outcomeAudit?.lease?.id !== id) return { received: false, reason: 'audit_lease_busy' };
66
75
  const view = structuredClone(claimed), m = view.run.managed;
76
+ // Routine purpose alignment asks for a colleague's opinion. Existing outcome
77
+ // gaps and completion verification retain the canonical acceptance path.
78
+ const opinionOnly = isOpinionOnly(findings);
67
79
  const plan = purposeDiagnosisPlan(m, 'Independently compare the original user request and later corrections with actual outcomes. Inspect omitted targets, runtime adoption, existing state, evidence and remaining work; do not accept an implementer plan as the original goal.');
68
80
  m.autonomyPolicy = 'autonomous'; m.resumeModel = metadata.model;
69
81
  m.purpose = { ...m.purpose, enabled: true, version: 2, decisionPolicy: 'llm-first-hc102' };
@@ -82,38 +94,72 @@ async function auditConsistencyAttempt(record, findings, request, { root, signal
82
94
  signal?.addEventListener('abort', abort, { once: true });
83
95
  if (signal?.aborted) abort();
84
96
  let result;
97
+ let diagnosticClosed = false, activityObserved = false, diagnosticWrites = Promise.resolve();
98
+ const retainDiagnostic = (stage, sessionId) => {
99
+ if (diagnosticClosed || controller.signal.aborted) return Promise.resolve();
100
+ diagnosticWrites = diagnosticWrites.then(() => writeRecord(root, record.provider, record.session, live => {
101
+ const audit = live?.run?.managed?.outcomeAudit;
102
+ if (diagnosticClosed || controller.signal.aborted || Date.now() >= request.deadlineAt ||
103
+ !canAuditOutcome(live) || contract(live) !== key || audit?.lease?.id !== id ||
104
+ audit.lease.requestId !== request.id || audit.lastAttempt?.attemptId !== id ||
105
+ audit.lastAttempt.requestId !== request.id) return live;
106
+ if (sessionId && audit.lastAttempt.session && audit.lastAttempt.session !== sessionId) return live;
107
+ audit.lastAttempt.stage = stage;
108
+ if (sessionId) audit.lastAttempt.session = sessionId;
109
+ return live;
110
+ }));
111
+ diagnosticWrites.catch(() => controller.abort());
112
+ return diagnosticWrites;
113
+ };
85
114
  let usage = null, usageWrites = Promise.resolve();
86
115
  try {
87
- result = await execute(view, { root, signal: controller.signal, openClient, auditOnly: true, authorizePreparation:beforeDispatch, beforeDispatch,
116
+ result = await execute(view, { root, signal: controller.signal, openClient, auditOnly: true, opinionOnly, peerAction: record.run.managed.decision?.action, authorizePreparation:beforeDispatch,
117
+ beforeDispatch: async () => { await beforeDispatch(); await retainDiagnostic('dispatching'); },
118
+ onEvidence: evidence => {
119
+ const context = evidence?.workerContext;
120
+ if (evidence?.source !== 'adapter_capture' || evidence.actionId !== id || context?.originalSession !== record.session ||
121
+ typeof context.session !== 'string' || !/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(context.session) || context.session === record.session) return;
122
+ return retainDiagnostic('session_bound', context.session);
123
+ },
124
+ onActivity: () => { if (!activityObserved) { activityObserved = true; retainDiagnostic('provider_active'); } },
88
125
  onUsage: value => { usage = value; usageWrites = usageWrites.then(() => accountAuditUsage(root, record, id, value, writeRecord)); usageWrites.catch(() => controller.abort()); if (m.maxTokens && Number.isFinite(value?.tokens) && (m.tokensUsed || 0) + value.tokens >= m.maxTokens) controller.abort(); } });
126
+ await retainDiagnostic('result_received');
89
127
  await beforeDispatch();
90
- const review = result.verification;
128
+ const review = opinionOnly ? result.opinion : result.verification;
91
129
  if (!review || review.source !== 'independent_codex_session' || !review.session || review.session === record.session) throw new Error('Independent audit provenance unavailable.');
130
+ if (opinionOnly && (typeof review.text !== 'string' || !review.text.trim())) throw new Error('Independent opinion was empty.');
92
131
  let persisted = false;
93
132
  await writeRecord(root, record.provider, record.session, live => {
94
133
  if (signal?.aborted || Date.now() >= request.deadlineAt || !live?.run?.managed || contract(live) !== key || live.run.managed.outcomeAudit?.lease?.id !== id || !canAuditOutcome(live)) return live;
95
134
  const audit = live.run.managed.outcomeAudit;
96
- audit.lastReport = review; audit.lastSuccessAt = Date.now(); audit.requestId = request.id;
135
+ if (opinionOnly) audit.lastOpinion = { source: review.source, session: review.session, ...(review.model ? { model: review.model } : {}), text: evidenceExcerpt(review.text, 2000).text, at: Date.now() };
136
+ else audit.lastReport = review;
137
+ audit.lastSuccessAt = Date.now(); audit.requestId = request.id;
97
138
  audit.targetContract = key; audit.lastFailure = null;
98
139
  // Existing finding reconciliation enforces stable IDs, counterevidence
99
140
  // and reopen reasons. A delivered audit never certifies task completion.
100
- if (completionClaimUnbound(live)) {
141
+ if (!opinionOnly && completionClaimUnbound(live)) {
101
142
  audit.completionRecovery = { at: Date.now(), priorState: live.run.state, priorRevision: live.run.revision };
102
143
  live.run.revision++;
103
144
  queuePurposeVerification(live.run, 'Prior terminal claim has no current controller completion receipt. Independently recheck the original frozen acceptance and remaining defects; an audit report alone cannot certify completion.', Date.now());
104
- } else if (live.run.managed.purpose?.enabled) reconcileFindings(live.run.managed, review, Date.now());
145
+ } else if (!opinionOnly && live.run.managed.purpose?.enabled) reconcileFindings(live.run.managed, review, Date.now());
105
146
  persisted = true;
106
147
  return live;
107
148
  });
108
- return { received: true, audited: persisted, ...(persisted ? { passed: review.passed } : { reason: 'target_changed' }) };
149
+ return opinionOnly && persisted ? { received: true, advised: true } : { received: true, audited: persisted, ...(persisted ? { passed: review.passed } : { reason: 'target_changed' }) };
109
150
  } catch (error) {
110
- const safeCode = ['cancelled', 'closed', 'identity', 'unsupported', 'approval', 'permission_restore', 'manual_provider_action', 'timeout'].includes(error?.code) ? error.code : 'independent_audit_failed';
151
+ // Persist only fixed classifications, never provider exception text or metadata.
152
+ const safeCode = ['cancelled', 'closed', 'identity', 'unsupported', 'approval', 'permission_restore', 'manual_provider_action', 'timeout',
153
+ 'unavailable', 'authentication_required', 'permission_narrowed', 'provider_approval', 'archived', 'context_unavailable',
154
+ 'checkpoint', 'decision', 'response_too_large', 'revision_changed', 'turn_failed', 'turn_settling'].includes(error?.code) ? error.code : 'independent_audit_failed';
111
155
  await writeRecord(root, record.provider, record.session, live => {
112
- if (live?.run?.managed?.outcomeAudit?.lease?.id === id) live.run.managed.outcomeAudit.lastFailure = { at: Date.now(), code: safeCode };
156
+ if (live?.run?.managed?.outcomeAudit?.lease?.id === id) live.run.managed.outcomeAudit.lastFailure = { at: Date.now(), code: safeCode, requestId: request.id, attemptId: id };
113
157
  return live;
114
158
  });
115
159
  return { received: true, audited: false, reason: 'independent_audit_failed' };
116
160
  } finally {
161
+ diagnosticClosed = true;
162
+ try { await diagnosticWrites; } catch { /* The attempt already failed closed. */ }
117
163
  signal?.removeEventListener('abort', abort);
118
164
  let usageWriteFailed = false;
119
165
  try { await usageWrites; } catch { usageWriteFailed = true; }
@@ -132,7 +178,7 @@ async function accountAuditUsage(root, record, id, observed, writeRecord = updat
132
178
  await writeRecord(root, record.provider, record.session, live => {
133
179
  if (!live?.run?.managed || live.run.id !== record.run.id || live.run.managed.taskId !== record.run.managed.taskId) return live;
134
180
  const audit = live.run.managed.outcomeAudit ||= {};
135
- const measured = observed?.source === 'codex_app_server' && Number.isSafeInteger(observed.tokens) && observed.tokens >= 0;
181
+ const measured = ['codex_app_server', 'codex_exec'].includes(observed?.source) && Number.isSafeInteger(observed.tokens) && observed.tokens >= 0;
136
182
  audit.usageReceipts ||= [];
137
183
  let receipt = audit.usageReceipts.find(row => row.id === id);
138
184
  if (!receipt) { receipt = { id, tokens: 0, source: measured ? 'measured' : 'unknown' }; audit.usageReceipts.push(receipt); }
@@ -155,17 +201,25 @@ async function accountAuditUsage(root, record, id, observed, writeRecord = updat
155
201
  // An inspection deadline bounds one read-only audit, not the user's task.
156
202
  // Abort fences all late report adoption. Original effect leases are untouched.
157
203
  export async function auditConsistencyTask(record, findings, request, options = {}) {
204
+ record = await recoverOutcomeAuditLease(record, options);
158
205
  if (!canAuditOutcome(record)) return { received: false, reason: 'audit_boundary' };
159
206
  const controller = new AbortController();
160
207
  const abort = () => controller.abort();
161
208
  options.signal?.addEventListener('abort', abort, { once: true });
162
209
  if (options.signal?.aborted) abort();
163
210
  const attemptId = randomUUID();
164
- const timeoutMs = options.auditTimeoutMs ?? 120_000;
165
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error('Invalid audit inspection deadline.');
211
+ // Full outcome inspection reads artifacts and checks the original contract.
212
+ // Keep it finite, but do not give it the tools-disabled opinion's deadline.
213
+ const requestedTimeout = options.auditTimeoutMs ?? (isOpinionOnly(findings) ? 120_000 : 300_000);
214
+ if (!Number.isFinite(requestedTimeout) || requestedTimeout <= 0) throw new Error('Invalid audit inspection deadline.');
215
+ const deadlineAt = Math.min(Date.now() + requestedTimeout,
216
+ Number.isFinite(record.run.deadline) ? record.run.deadline : Infinity);
217
+ const timeoutMs = Math.max(1, deadlineAt - Date.now());
166
218
  let timer, writes = Promise.resolve();
167
219
  const writeRecord = (...args) => {
168
220
  const pending = writes.catch(() => {}).then(async () => {
221
+ // The standard writer already bounds acquisition retries before mutation.
222
+ if (!options.writeRecord) return updateRecord(...args);
169
223
  for (let retry = 0; ; retry++) {
170
224
  try { return await (options.writeRecord || updateRecord)(...args); }
171
225
  catch (error) {
@@ -176,7 +230,6 @@ export async function auditConsistencyTask(record, findings, request, options =
176
230
  });
177
231
  writes = pending; return pending;
178
232
  };
179
- const deadlineAt = Date.now() + timeoutMs;
180
233
  let attemptFinished = false, outerFinished = false;
181
234
  const settleReceipt = async () => {
182
235
  await writeRecord(options.root, record.provider, record.session, live => {
@@ -201,7 +254,7 @@ export async function auditConsistencyTask(record, findings, request, options =
201
254
  await writeRecord(options.root, record.provider, record.session, live => {
202
255
  if (live?.run?.managed?.outcomeAudit?.lease?.id === attemptId) {
203
256
  delete live.run.managed.outcomeAudit.lease;
204
- live.run.managed.outcomeAudit.lastFailure = { at: Date.now(), code: 'audit_timeout' };
257
+ live.run.managed.outcomeAudit.lastFailure = { at: Date.now(), code: 'audit_timeout', requestId: request.id, attemptId };
205
258
  }
206
259
  return live;
207
260
  });
@@ -4,7 +4,7 @@ import { effectiveAcceptanceCriteria } from '../codex-plugins/simy/hooks/accepta
4
4
  import { purposeFailureMode } from '../codex-plugins/simy/hooks/purpose-search.mjs';
5
5
  import { realpath } from 'node:fs/promises';
6
6
  import { randomUUID } from 'node:crypto';
7
- import { TERMINAL as TERMINAL_RUNS, advanceDeliveryControlRevision, canUpgradeDelivery, enablePurposeDelivery, claudeHookChannel, controlDelivery, listRecords, readRecord, processIdentity, startDelivery, updateRecord, recordProviderArchive } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
7
+ import { TERMINAL as TERMINAL_RUNS, advanceDeliveryControlRevision, deliveryDecisionRevision, canUpgradeDelivery, enablePurposeDelivery, claudeHookChannel, controlDelivery, listRecords, readRecord, processIdentity, startDelivery, updateRecord, recordProviderArchive } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
8
8
  import { listCodexSessions, codexThreadMetadata, openCodexAppServer, readCodexSession, readCodexArchiveStates, CodexAppServerError } from './codex-app-server.js';
9
9
  import { listClaudeSessions, readClaudeSession } from './claude-sessions.js';
10
10
  import { loadPurposeRuleSet } from './purpose-instruction.js';
@@ -554,3 +554,24 @@ export async function continueSession(options) {
554
554
  export async function answerSession({ root, provider, session, taskId, expectedRunId, expectedRevision, questionId, answer, now = Date.now() }) {
555
555
  return controlDelivery({ root, provider, session, taskId, expectedRunId, expectedRevision, questionId, answer, action: 'session_answer', now });
556
556
  }
557
+
558
+ // Explicit user resume observes the existing native conversation. A model
559
+ // selection changed in Codex is configuration, not a different task or authority.
560
+ export async function resumeSession({ openClient = openCodexAppServer, ...options }) {
561
+ const record = await readRecord(options.root, options.provider, options.session);
562
+ if (options.provider !== 'codex' || record?.run?.managed?.resumeChannel !== 'codex_app_server')
563
+ return controlDelivery({ ...options, action: 'resume' });
564
+ if (record.run.managed.taskId !== options.taskId) throw new Error('Managed task identity changed; refresh its status.');
565
+ const link = await requireProviderLink(options);
566
+ const client = await openClient({ isolateConnectors: true });
567
+ let metadata;
568
+ try { metadata = await readCodexSession(client, options.session); }
569
+ finally { await client.close(); }
570
+ const cwd = await realpath(metadata.cwd);
571
+ if (cwd !== record.cwd || !metadata.model) throw new Error('Original Codex workspace or model is unavailable; inspect before resuming.');
572
+ await requireProviderLink({ ...options, expectedEpoch: link.epoch });
573
+ return controlDelivery({ ...options, action: 'resume',
574
+ expectedRunId: options.expectedRunId ?? record.run.id,
575
+ expectedRevision: options.expectedRevision ?? deliveryDecisionRevision(record.run),
576
+ resumeObservation: { session: metadata.session, cwd, model: metadata.model, source: 'codex_app_server', providerEpoch: link.epoch } });
577
+ }
@@ -0,0 +1,86 @@
1
+ import { isAbsolute, relative, join } from 'node:path';
2
+ import { requireProviderLink } from '../provider-links.js';
3
+ import { createHash } from 'node:crypto';
4
+ import { realpath, open } from 'node:fs/promises';
5
+ import { taskDirectory } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
6
+
7
+ import { execFile } from 'node:child_process';
8
+ import { constants } from 'node:fs';
9
+ import { promisify } from 'node:util';
10
+ const exec = promisify(execFile);
11
+
12
+ export const authorityDigest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
13
+
14
+ // New explicit delegation is separate from historical enrollment. It never
15
+ // authorizes connectors or overrides a subsequently narrowed native policy.
16
+ export async function delegatedNativeSandbox(record, kind, root, { signal } = {}) {
17
+ if (!['read', 'local_edit', 'local_test'].includes(kind)) return null;
18
+ const grant = record.run?.managed?.nativeDelegation;
19
+ if (!grant) return null;
20
+ const { digest, ...body } = grant;
21
+ if (digest !== authorityDigest(body) || body.schema !== 1 || body.source !== 'explicit_native_redelegation' ||
22
+ body.taskId !== record.run.managed.taskId || body.runId !== record.run.id || body.session !== record.session ||
23
+ body.cwd !== record.cwd || body.executionCwd !== taskDirectory(record) ||
24
+ body.consentEpoch !== (record.providerConsentEpoch ?? null) || !body.consentRef ||
25
+ body.observation?.type !== 'sdk_rollout_turn_context' || body.observation.session !== record.session ||
26
+ body.observation.policyDigest !== authorityDigest(body.policy)) return null;
27
+ try { if (await realpath(body.observation.cwd) !== record.cwd) return null; } catch { return null; }
28
+ try { await requireProviderLink({ root, provider: record.provider, expectedEpoch: body.providerEpoch }); } catch { return null; }
29
+ // A local-only renewal is narrower than the observed native ceiling.
30
+ // Network/connector use needs separate existing authority.
31
+ const gitRoots = await authorizedWorktreeGitRoots(body.executionCwd, body.cwd, [body.policy], { signal });
32
+ return { type: 'workspaceWrite', writableRoots: [body.executionCwd, ...gitRoots], networkAccess: false,
33
+ excludeTmpdirEnvVar: true, excludeSlashTmp: true };
34
+ }
35
+
36
+ export function canWriteWorkspace(sandbox, cwd, sessionCwd) {
37
+ if (sandbox?.type === 'dangerFullAccess') return true;
38
+ if (sandbox?.type !== 'workspaceWrite' || !Array.isArray(sandbox.writableRoots)) return false;
39
+ // Codex workspaceWrite always includes its verified thread cwd. The roots
40
+ // array lists *additional* writable directories and is usually empty.
41
+ return [sessionCwd, ...sandbox.writableRoots].some(root => {
42
+ if (typeof root !== 'string' || !isAbsolute(root)) return false;
43
+ const path = relative(root, cwd);
44
+ return !isAbsolute(path) && path !== '..' && !path.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`);
45
+ });
46
+ }
47
+
48
+
49
+ // Linked worktrees keep their index and object/ref store outside their checkout.
50
+ // Only the original repository's registered link can supply additional roots,
51
+ // and every native ceiling must already cover them. This is not a new grant.
52
+ export async function authorizedWorktreeGitRoots(cwd, sessionCwd, policies, { signal } = {}) {
53
+ if (!policies?.length) return [];
54
+ try {
55
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('GIT_')));
56
+ const git = async (where, args) => (await exec('git', ['-C', where, ...args], {
57
+ env, timeout: 5000, maxBuffer: 64 * 1024, windowsHide: true, signal,
58
+ })).stdout.trim();
59
+ const checkout = await realpath(cwd), original = await realpath(sessionCwd);
60
+ const top = await realpath(await git(checkout, ['rev-parse', '--show-toplevel']));
61
+ if (top !== checkout) return [];
62
+ const gitDir = await realpath(await git(checkout, ['rev-parse', '--absolute-git-dir']));
63
+ const common = await realpath(await git(checkout, ['rev-parse', '--path-format=absolute', '--git-common-dir']));
64
+ // Ordinary checkouts need no extra worktree administration outside cwd.
65
+ if (gitDir === common) return [];
66
+ const originalCommon = await realpath(await git(original, ['rev-parse', '--path-format=absolute', '--git-common-dir']));
67
+ if (common !== originalCommon || !canWriteWorkspace({ type: 'workspaceWrite', writableRoots: [] }, gitDir, join(common, 'worktrees'))) return [];
68
+ // A forged .git pointer must not expose another checkout's administration.
69
+ const file = await open(join(gitDir, 'gitdir'), constants.O_RDONLY | (constants.O_NOFOLLOW || 0) | (constants.O_NONBLOCK || 0));
70
+ let backlink;
71
+ try {
72
+ const stat = await file.stat();
73
+ if (!stat.isFile() || stat.size > 8192 || signal?.aborted) return [];
74
+ const buffer = Buffer.alloc(8193);
75
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
76
+ if (bytesRead > 8192 || signal?.aborted) return [];
77
+ backlink = buffer.subarray(0, bytesRead).toString('utf8').trim();
78
+ } finally { await file.close(); }
79
+ if (await realpath(backlink) !== await realpath(join(checkout, '.git'))) return [];
80
+ const roots = [...new Set([gitDir, common])];
81
+ // workspace-write protects .git implicitly. An enclosing cwd/root does not
82
+ // authorize making Git metadata writable; require an exact explicit root.
83
+ if (!roots.every(path => policies.every(policy => policy?.type === 'dangerFullAccess' || policy?.type === 'workspaceWrite' && policy.writableRoots?.some(root => root === path)))) return [];
84
+ return roots;
85
+ } catch { return []; }
86
+ }
@@ -1,3 +1,4 @@
1
+ import { recallPerspectives, perspectiveInstruction } from '../codex-plugins/simy/hooks/perspective-memory.mjs';
1
2
  import { loadHypothesisKnowledge, hypothesisKnowledgeInstruction } from '../sqm/hypothesis-knowledge.js';
2
3
  import { selectiveRuleReview, applicableRuleIds } from '../codex-plugins/simy/hooks/purpose-rule-policy.mjs';
3
4
  import { searchInstruction } from '../codex-plugins/simy/hooks/purpose-search.mjs';
@@ -38,12 +39,13 @@ export async function loadPurposeRuleSet() {
38
39
  if (!rules.length || new Set(rules.map(rule => rule.id)).size !== rules.length) throw new Error('Purpose rule index is empty or ambiguous.');
39
40
  return { digest: createHash('sha256').update(JSON.stringify(sources)).digest('hex'), rules };
40
41
  }
41
- export async function buildPurposeInstruction(managed, { reviewer = false, cwd, webOrigin, allowNetwork = false, signal, knowledgeLoader = loadHypothesisKnowledge, authorizeKnowledgeNetwork } = {}) {
42
+ export async function buildPurposeInstruction(managed, { reviewer = false, cwd, webOrigin, allowNetwork = false, signal, knowledgeLoader = loadHypothesisKnowledge, authorizeKnowledgeNetwork, root: memoryRoot, record, memoryLoader = recallPerspectives } = {}) {
42
43
  if (!managed?.purpose?.enabled) return '';
43
44
  const ruleSet = await loadPurposeRuleSet();
44
45
  if (ruleSet.digest !== managed.purpose.ruleDigest) throw Object.assign(new Error('Purpose workflow changed; refresh its rule inventory before dispatch.'), { code: 'purpose_rules_changed' });
45
46
  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)) })))}`;
46
47
  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)) })))}`;
47
48
  const knowledge = await knowledgeLoader({ cwd, webOrigin, allowNetwork, signal, ...(authorizeKnowledgeNetwork ? { authorizeNetwork: authorizeKnowledgeNetwork } : {}) });
48
- return `${purposeContract(managed)}\n${hypothesisKnowledgeInstruction(knowledge)}\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)}`;
49
+ 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)}`;
49
51
  }
@@ -23,15 +23,17 @@ function auditReinspectionEligible(run, now) {
23
23
  export async function observePurposeTurn(record, { openClient = openCodexAppServer } = {}) {
24
24
  const client = await openClient();
25
25
  try {
26
- const session=record.run.managed.nativeWait?.session || record.session;
27
- const cwd=session===record.session?record.cwd:record.run.managed.nativeWait?.cwd;
26
+ const wait=record.run.managed.nativeWait || record.run.managed.purpose?.nativeBoundary;
27
+ if (wait && !wait.session) return {state:'unknown'};
28
+ const session=wait?.session || record.session;
29
+ const cwd=session===record.session?record.cwd:wait?.cwd;
28
30
  if(!cwd)return {state:'unknown'};
29
31
  const metadata = await readCodexSession(client, session);
30
32
  if (await realpath(metadata.cwd) !== cwd || metadata.model !== record.run.managed.resumeModel) return { state: 'unknown' };
31
33
  const result = await client.request('thread/turns/list', { threadId: session, limit: 1, sortDirection: 'desc', itemsView: 'summary' });
32
34
  const turn = result?.data?.[0];
33
35
  if (!turn?.id || !['completed', 'failed', 'interrupted', 'inProgress'].includes(turn.status)) return { state: 'unknown' };
34
- return { state: turn.status === 'inProgress' ? 'running' : 'idle', turnId: turn.id, status: turn.status };
36
+ return { state: turn.status === 'inProgress' ? 'running' : 'idle', turnId: turn.id, status: turn.status, session, cwd };
35
37
  } finally { await client.close(); }
36
38
  }
37
39
 
@@ -50,7 +52,7 @@ export async function observeUpgradeMetadata(record, { openClient = openCodexApp
50
52
  finally { await client.close(); }
51
53
  }
52
54
 
53
- export async function reconcilePurposeTasks({ root, signal, prepareCandidate = preparePurposeCandidate, now = Date.now(), observeTurn = observePurposeTurn, observeAuthentication = observeCodexAuthentication, readRecords = () => listRecords(root), observeMetadata = observeUpgradeMetadata }) {
55
+ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = preparePurposeCandidate, now = Date.now(), observeTurn = observePurposeTurn, observeAuthentication = observeCodexAuthentication, readRecords = () => listRecords(root), observeMetadata = observeUpgradeMetadata, writeRecord = updateRecord }) {
54
56
  const started = Date.now();
55
57
  let rules;
56
58
  const deferred = [];
@@ -101,11 +103,30 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
101
103
  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.');
102
104
  };
103
105
  const observe = async read => { await authorized(); const result = await read(); await authorized(); return result; };
104
- const persist = mutate => updateRecord(root, record.provider, record.session, async current => {
106
+ const persist = mutate => writeRecord(root, record.provider, record.session, async current => {
105
107
  try { await requireProviderLink({ root, provider: record.provider, expectedEpoch: link.epoch }); } catch { return current; }
106
108
  if (!current || !automaticContinuationAllowed(current) || current.autoContinueEpoch !== record.autoContinueEpoch) return current;
107
109
  return mutate(current);
108
110
  });
111
+ // Recheck the current budget and boundaries under the existing record lock.
112
+ // Audit usage can change without changing the run revision or updatedAt.
113
+ const nativeCurrent = current => {
114
+ const latest = current?.run, managed = latest?.managed;
115
+ return latest?.id === run.id && latest.revision === run.revision && latest.state === run.state && latest.updatedAt === run.updatedAt &&
116
+ !['paused', 'stopped', 'completed', 'budget_exhausted'].includes(latest.state) && !managed.resumeLease &&
117
+ !managed.pendingApproval && !managed.liveWorkspaceLease && !managed.outcomeAudit?.lease &&
118
+ !['pending', 'transferred', 'pending_received'].includes(managed.handoff?.status) && !managed.rejectedActionOverflow &&
119
+ !managed.rejectedActionDigests?.includes(createHash('sha256').update(JSON.stringify(managed.decision?.action ?? null)).digest('hex')) &&
120
+ !managedBudgetReason(latest, Date.now()) &&
121
+ JSON.stringify(managed.nativeWait) === JSON.stringify(run.managed.nativeWait) &&
122
+ JSON.stringify(managed.purpose.nativeBoundary) === JSON.stringify(run.managed.purpose.nativeBoundary);
123
+ };
124
+ const nativeSessionMatches = observed => {
125
+ const wait = run.managed.nativeWait || run.managed.purpose.nativeBoundary;
126
+ const boundary = run.managed.purpose.nativeBoundary;
127
+ return Boolean(observed.session && observed.session === (wait ? wait.session : record.session) &&
128
+ (!boundary || boundary.session === observed.session));
129
+ };
109
130
  rules ||= await loadPurposeRuleSet();
110
131
  if (run.managed.purpose.ruleDigest !== rules.digest && run.state !== 'auto_resuming') {
111
132
  record = await persist(current => {
@@ -122,15 +143,19 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
122
143
  }
123
144
  if (run.managed.purpose.nativeBoundary && !run.managed.resumeLease && now >= (run.managed.purpose.nativeObservedAt || 0) + 60_000) {
124
145
  let native;try {native=await observe(()=>observeTurn(record));} catch {native={state:'unknown'};}
146
+ if (!nativeSessionMatches(native)) continue;
147
+ let persisted = false;
125
148
  await persist(current=>{
126
- if(current?.run?.id !== run.id || current.run.revision !== run.revision) return current;
149
+ if(!nativeCurrent(current)) return current;
150
+ persisted = true;
127
151
  const purpose=current.run.managed.purpose;purpose.nativeObservedAt=now;purpose.nativeObservation=native;
128
152
  if(native.status==='completed' && native.turnId===purpose.nativeBoundary?.turnId && purpose.nativeBoundary.state!=='writer_completed_requires_verification') {
129
153
  purpose.nativeBoundary={...purpose.nativeBoundary,state:'writer_completed_requires_verification',writer:'completed',completedTurnId:native.turnId,observedAt:now};
130
- queuePurposeVerification(current.run,'Original pending turn completed; independently inspect its actual result and reconcile effects without replaying the operation.',now);
131
- } else if (native.status==='completed' && native.turnId && native.turnId!==purpose.nativeBoundary?.turnId && native.turnId!==purpose.lastOwnedTurnId) {delete purpose.nativeBoundary;current.run.managed.nativeWait=null;purpose.nextSearchAt=0;}
154
+ queuePurposeVerification(current.run,'Original pending turn completed; independently inspect its actual result and reconcile effects without replaying the operation.',Date.now());
155
+ } else if (native.session===record.session && native.status==='completed' && native.turnId && native.turnId!==purpose.nativeBoundary?.turnId && native.turnId!==purpose.lastOwnedTurnId) {delete purpose.nativeBoundary;current.run.managed.nativeWait=null;purpose.nextSearchAt=0;}
132
156
  return current;
133
157
  });
158
+ if (!persisted) continue;
134
159
  record=await readRecord(root,record.provider,record.session);run=record.run;
135
160
  }
136
161
  if (searchEnabled(run.managed) && run.managed.purpose.nativeBoundary && run.state==='blocked' && !run.managed.resumeLease &&
@@ -200,25 +225,31 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
200
225
  if (now < (run.managed.purpose.lastObservedAt || 0) + 60_000) continue;
201
226
  let observed;
202
227
  try { observed = await observe(() => observeTurn(record)); } catch { observed = { state: 'unavailable' }; }
228
+ if (!nativeSessionMatches(observed)) continue;
229
+ let persisted = false;
203
230
  await persist(current => {
204
- if (current?.run?.id !== run.id || current.run.state !== 'blocked' || current.run.revision !== run.revision) return current;
231
+ if (!nativeCurrent(current)) return current;
232
+ persisted = true;
205
233
  current.run.managed.purpose.lastObservedAt = now;
206
234
  current.run.managed.purpose.observationState = observed.state;
207
- if (!current.run.managed.nativeWait?.turnId && observed.turnId) current.run.managed.nativeWait = { turnId: observed.turnId, observedAt: now };
235
+ if (!current.run.managed.nativeWait?.turnId && observed.turnId) current.run.managed.nativeWait = { turnId: observed.turnId, session: observed.session, cwd: observed.cwd, observedAt: now };
208
236
  return current;
209
237
  });
238
+ if (!persisted) continue;
210
239
  // A failed/interrupted owned turn or merely elapsed time is not a user
211
240
  // answer. A subsequent completed original turn permits diagnosis only.
212
- if (run.managed.nativeWait?.turnId && observed.status === 'completed' && observed.turnId !== run.managed.nativeWait.turnId) {
213
- await persist(current => {
214
- if (current?.run?.id === run.id && current.run.revision === run.revision) {
241
+ if (observed.session === record.session && run.managed.nativeWait?.turnId && observed.status === 'completed' && observed.turnId !== run.managed.nativeWait.turnId && observed.turnId !== run.managed.purpose.lastOwnedTurnId) {
242
+ let cleared = false;
243
+ const saved = await persist(current => {
244
+ if (nativeCurrent(current)) {
215
245
  current.run.managed.nativeWait = null;
216
246
  delete current.run.managed.purpose.nativeBoundary;
247
+ cleared = true;
217
248
  }
218
249
  return current;
219
250
  });
220
- run.managed.nativeWait = null;
221
- delete run.managed.purpose.nativeBoundary;
251
+ if (!cleared) continue;
252
+ record = saved; run = saved.run;
222
253
  obstacle = { type: 'native_wait_updated', detail: observed.turnId };
223
254
  } else if (searchEnabled(run.managed)) {
224
255
  obstacle = { type: 'native_wait_diagnosis', detail: run.managed.nativeWait?.turnId || 'writer_unknown' };
@@ -275,9 +306,10 @@ export async function reconcilePurposeTasks({ root, signal, prepareCandidate = p
275
306
  const key = createHash('sha256').update(JSON.stringify([run.managed.verifiedRevision, obstacle, obstacle.type === 'external_dependency' ? run.managed.purpose.dependencyChecks || 0 : null])).digest('hex');
276
307
  await persist(current => {
277
308
  if (current?.run?.id !== run.id || current.run.revision !== run.revision || current.run.state !== run.state || current.run.updatedAt !== run.updatedAt) return current;
309
+ if (['native_wait_updated', 'native_wait_diagnosis'].includes(obstacle.type) && !nativeCurrent(current)) return current;
278
310
  if (searchEnabled(current.run.managed) && ['original_context_updated', 'native_wait_updated'].includes(obstacle.type)) current.run.managed.purpose.nextSearchAt = 0;
279
311
  const queued = queuePurposeDiagnosis(current.run,
280
- `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, now);
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());
281
313
  if (queued && obstacle.type === 'external_dependency') {
282
314
  const purpose = current.run.managed.purpose;
283
315
  purpose.dependencyChecks = (purpose.dependencyChecks || 0) + 1;