kiokuko-dsh 0.2.38 → 0.2.39

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/dist/dsh/core/host.js +4 -4
  2. package/dist/dsh/core/host.js.map +1 -1
  3. package/dist/dsh/decisions/diagnostics.d.ts +4 -3
  4. package/dist/dsh/decisions/diagnostics.d.ts.map +1 -1
  5. package/dist/dsh/decisions/diagnostics.js +2 -2
  6. package/dist/dsh/decisions/diagnostics.js.map +1 -1
  7. package/dist/dsh/decisions/intent-assessment.d.ts +10 -4
  8. package/dist/dsh/decisions/intent-assessment.d.ts.map +1 -1
  9. package/dist/dsh/decisions/intent-assessment.js +26 -10
  10. package/dist/dsh/decisions/intent-assessment.js.map +1 -1
  11. package/dist/dsh/decisions/openai.d.ts.map +1 -1
  12. package/dist/dsh/decisions/openai.js +3 -0
  13. package/dist/dsh/decisions/openai.js.map +1 -1
  14. package/dist/dsh/host-adapter.js +4 -4
  15. package/dist/dsh/host-adapter.js.map +1 -1
  16. package/dist/dsh/lisp/sandbox.d.ts +1 -0
  17. package/dist/dsh/lisp/sandbox.d.ts.map +1 -1
  18. package/dist/dsh/lisp/sandbox.js +2 -2
  19. package/dist/dsh/lisp/sandbox.js.map +1 -1
  20. package/dist/dsh/on-demand-intake.d.ts +19 -4
  21. package/dist/dsh/on-demand-intake.d.ts.map +1 -1
  22. package/dist/dsh/on-demand-intake.js +154 -61
  23. package/dist/dsh/on-demand-intake.js.map +1 -1
  24. package/dist/dsh/task-startup.d.ts +4 -0
  25. package/dist/dsh/task-startup.d.ts.map +1 -1
  26. package/dist/dsh/task-startup.js +8 -2
  27. package/dist/dsh/task-startup.js.map +1 -1
  28. package/dist/locale/en.json +1 -1
  29. package/dist/locale/ja.json +1 -1
  30. package/dist/locale/ko.json +1 -1
  31. package/dist/locale/zh-CN.json +1 -1
  32. package/docs/micro-judge.md +1 -1
  33. package/docs/on-demand-intake.md +2 -0
  34. package/lisp/supervisor.mjs +90 -9
  35. package/package.json +1 -1
@@ -17,6 +17,7 @@ import { KIOKUKO_DSH_SOURCE_KIND, isKiokukoDshSource } from './plugin-source.js'
17
17
  import { taskAllowedOperations, taskCanExecute, } from './task-state.js';
18
18
  import { createRequestIntentContext, unresolvedRequestIntent, validateMainModelRequestIntent, requestIntentReferenceIds, requestIntentRequiredReferenceIds, requestIntentIsUnresolved, requestIntentMessageRefs, requestIntentTaskType, RequestIntentSubmissionSchema, } from './decisions/request-intent.js';
19
19
  import { memoryOwnerKind, } from './memory-owner.js';
20
+ import { assessmentAllowsPreparation, mayConfirmRequestInterpretation } from './decisions/intent-assessment.js';
20
21
  class RequestClarification extends Error {
21
22
  assessment;
22
23
  interpretation;
@@ -195,6 +196,7 @@ export class OnDemandIntake {
195
196
  }
196
197
  }
197
198
  #turns = new Map();
199
+ #nativeConfirmations = new WeakMap();
198
200
  #clarificationContexts = new WeakMap();
199
201
  #clarifications = new Map();
200
202
  #executions = new WeakMap();
@@ -249,20 +251,20 @@ export class OnDemandIntake {
249
251
  let verified;
250
252
  state.auxiliaryReceipt = await this.host.recovery?.defer({ source: { sessionId: state.input.agent.session.id, turn: state.input.turn,
251
253
  origin: 'intent-assessment', sourceDigest: canonicalContentHash(state.task), contextDigest: digest },
252
- current: () => this.recoveryCurrent(state) && state.assessment?.digest === digest && canonicalContentHash([state.requestIntentContext, decision]) === digest,
254
+ current: () => this.recoveryCurrent(state) && !this.#clarifications.has(state.session.id) && state.assessment?.digest === digest && canonicalContentHash([state.requestIntentContext, decision]) === digest,
253
255
  check: async (signal, attempt) => {
254
256
  await this.host.validate({ ...state.input, signal });
255
- const result = await this.host.assess?.({ ...state.input, signal }, context, decision, attempt);
257
+ const result = await this.host.assess?.({ ...state.input, signal }, context, decision, attempt, this.hasNativeConfirmation(state, context, decision));
256
258
  signal.throwIfAborted();
257
259
  if (!result)
258
260
  return { state: 'rejected', reason: 'assessment_disabled' };
259
- if (result.outcome === 'consistent') {
261
+ if (assessmentAllowsPreparation(result)) {
260
262
  verified = result;
261
- return { state: 'verified', reason: 'semantic_rechecked' };
263
+ return { state: 'verified', reason: result.outcome === 'user_confirmed' ? 'native_user_confirmation_accepted' : 'semantic_rechecked' };
262
264
  }
263
265
  return { state: result.semantic === 'contradicted' ? 'rejected' : 'retry_wait', reason: result.semantic === 'contradicted' ? 'semantic_contradicted' : 'assessment_unresolved' };
264
266
  }, verified: async () => {
265
- if (verified && this.recoveryCurrent(state) && state.assessment?.digest === digest && canonicalContentHash([state.requestIntentContext, decision]) === digest) {
267
+ if (verified && this.recoveryCurrent(state) && !this.#clarifications.has(state.session.id) && state.assessment?.digest === digest && canonicalContentHash([state.requestIntentContext, decision]) === digest) {
266
268
  state.assessment = { digest, result: verified };
267
269
  await this.diagnose(state, { kind: 'assessment', assessment: verified });
268
270
  }
@@ -400,7 +402,7 @@ export class OnDemandIntake {
400
402
  messageRefs: requestIntentMessageRefs(input.messages, task),
401
403
  });
402
404
  try {
403
- await this.host.prepareNativeCoding(input, task, context);
405
+ await this.host.prepareNativeCoding(input, task, context, this.nativeContinuation(input));
404
406
  }
405
407
  catch (error) {
406
408
  input.signal.throwIfAborted();
@@ -495,6 +497,8 @@ export class OnDemandIntake {
495
497
  messages: Object.freeze(structuredClone([...input.messages])),
496
498
  },
497
499
  task: humanTask(input.messages),
500
+ ...(this.isClarificationContinuation(input)
501
+ ? { clarificationPending: true } : {}),
498
502
  humanContents: new Map(humanMessages(input.messages).map((message) => [
499
503
  humanId(message),
500
504
  canonicalContentHash(message),
@@ -538,7 +542,7 @@ export class OnDemandIntake {
538
542
  messageRefs: requestIntentMessageRefs(state.input.messages, state.task),
539
543
  });
540
544
  this.assertCurrent(state);
541
- await this.host.prepareNativeCoding?.(state.input, state.task, context);
545
+ await this.host.prepareNativeCoding?.(state.input, state.task, context, this.nativeContinuation(state.input));
542
546
  this.assertCurrent(state);
543
547
  state.nativeUnowned = true;
544
548
  if (context) {
@@ -555,7 +559,7 @@ export class OnDemandIntake {
555
559
  const context = await this.host.requestContext?.(state.input, state.task)
556
560
  ?? createRequestIntentContext({ task: state.task, messageRefs: requestIntentMessageRefs(state.input.messages, state.task) });
557
561
  this.assertCurrent(state);
558
- await this.host.prepareNativeCoding(state.input, state.task, context);
562
+ await this.host.prepareNativeCoding(state.input, state.task, context, this.nativeContinuation(state.input));
559
563
  this.assertCurrent(state);
560
564
  }
561
565
  try {
@@ -787,6 +791,55 @@ export class OnDemandIntake {
787
791
  current,
788
792
  });
789
793
  }
794
+ isClarificationContinuation(input) {
795
+ const messages = humanMessages(input.messages), known = this.#clarificationContexts.get(input.agent);
796
+ if (!known)
797
+ return false;
798
+ const sources = collectClarificationContext(messages.map(message => ({ id: humanId(message), text: humanTask([message]) })), known).sources;
799
+ return messages.some(message => {
800
+ const reply = known.get(humanId(message));
801
+ return reply && canonicalContentHash(reply.sources) === canonicalContentHash(sources);
802
+ });
803
+ }
804
+ /** Startup preferences are reused only from this exact native confirmation,
805
+ * never by matching a model-written "continue" or quoted answer. */
806
+ nativeContinuation(input) {
807
+ const messages = humanMessages(input.messages);
808
+ const clarification = collectClarificationContext(messages.map(message => ({ id: humanId(message), text: humanTask([message]) })), this.#clarificationContexts.get(input.agent) ?? new Map());
809
+ for (const message of messages) {
810
+ const receipt = this.#nativeConfirmations.get(input.agent)?.get(humanId(message));
811
+ if (receipt && receipt.session === input.agent.session && receipt.originalTurn < input.turn &&
812
+ receipt.contentDigest === canonicalContentHash(humanTask([message])) &&
813
+ receipt.sourcesDigest === canonicalContentHash(clarification.sources))
814
+ return { turn: receipt.originalTurn, task: receipt.originalTask };
815
+ }
816
+ return undefined;
817
+ }
818
+ confirmationScope(context) {
819
+ // Eligible confirmations adopt no proposals. Fresh unselected assistant
820
+ // prose must not invalidate the human answer; selected refs are bound in
821
+ // confirmationCandidate and still cannot enter this path.
822
+ return canonicalContentHash({ activeTask: context.activeTask ?? null,
823
+ activeGoal: context.activeGoal ?? null });
824
+ }
825
+ confirmationCandidate(candidate) {
826
+ return canonicalContentHash([candidate.relation, candidate.action, candidate.changeScope,
827
+ candidate.taskOperation ?? null, candidate.evidence.proposalRefs]);
828
+ }
829
+ hasNativeConfirmation(state, context, candidate) {
830
+ if (!mayConfirmRequestInterpretation(candidate))
831
+ return false;
832
+ const messages = humanMessages(state.input.messages);
833
+ const clarification = collectClarificationContext(messages.map(message => ({ id: humanId(message), text: humanTask([message]) })), this.#clarificationContexts.get(state.input.agent) ?? new Map());
834
+ return messages.some(message => {
835
+ const receipt = this.#nativeConfirmations.get(state.input.agent)?.get(humanId(message));
836
+ return !!receipt && receipt.session === state.input.agent.session &&
837
+ receipt.contentDigest === canonicalContentHash(humanTask([message])) &&
838
+ receipt.sourcesDigest === canonicalContentHash(clarification.sources) &&
839
+ receipt.scopeDigest === this.confirmationScope(context) &&
840
+ receipt.candidateDigest === this.confirmationCandidate(candidate);
841
+ });
842
+ }
790
843
  async assess(state, decision) {
791
844
  if (!this.host.assess ||
792
845
  decision?.action === 'answer' ||
@@ -802,7 +855,7 @@ export class OnDemandIntake {
802
855
  const started = performance.now(), deadline = AbortSignal.timeout(1500);
803
856
  const signal = AbortSignal.any([state.input.signal, deadline]);
804
857
  try {
805
- result = await abortable(this.host.assess({ ...state.input, signal }, context, decision), signal);
858
+ result = await abortable(this.host.assess({ ...state.input, signal }, context, decision, undefined, this.hasNativeConfirmation(state, context, decision)), signal);
806
859
  }
807
860
  catch {
808
861
  state.input.signal.throwIfAborted();
@@ -818,10 +871,10 @@ export class OnDemandIntake {
818
871
  throw new Error('Assessment source changed');
819
872
  state.assessment = { digest, result };
820
873
  await this.diagnose(state, { kind: 'assessment', assessment: result });
821
- if (result.outcome !== 'consistent')
874
+ if (!assessmentAllowsPreparation(result))
822
875
  await this.deferAssessment(state, context, decision, digest);
823
876
  }
824
- return state.assessment.result.outcome === 'consistent';
877
+ return assessmentAllowsPreparation(state.assessment.result);
825
878
  }
826
879
  async prepare(state, taskType, signal, requestIntent, explicitIntent = false) {
827
880
  this.assertCurrent(state);
@@ -909,6 +962,7 @@ export class OnDemandIntake {
909
962
  requestIntent: state.requestIntent ?? null,
910
963
  });
911
964
  state.status = 'prepared';
965
+ delete state.clarificationPending;
912
966
  this.#clarifications.delete(state.input.agent.session.id);
913
967
  delete state.confirmationResume;
914
968
  })();
@@ -996,17 +1050,22 @@ export class OnDemandIntake {
996
1050
  reason.message.startsWith('The requested change scope');
997
1051
  const target = !!executionClarification(state.task, decision, context);
998
1052
  const transition = context?.activeTask && !taskCanExecute(context.activeTask);
999
- const question = reason.assessment
1000
- ? '現在の依頼と実行候補の対象・操作・制約を照合できませんでした。今回の対象と希望する操作、以前の指示を変更する範囲を教えてください。'
1001
- : transition
1002
- ? `現在のタスクは ${context.activeTask.state} です。続けるために、どの操作を希望しますか?(${taskAllowedOperations(context.activeTask).join(' / ')})`
1003
- : target
1004
- ? 'どの具体的なファイル・項目・相手・操作を対象にしますか?'
1005
- : scope
1006
- ? '変更するのは文章だけですか、それともコード・テスト・設定も含みますか?'
1007
- : decision?.action === 'unknown'
1008
- ? 'その対象に、具体的に何をしてほしいですか?'
1009
- : null;
1053
+ const confirmCurrent = reason.assessment && state.assessment?.result.semantic === 'insufficient' &&
1054
+ !!decision && mayConfirmRequestInterpretation(decision) && !(decision.relation === 'same_scope_continuation' && context?.proposals.length) && !target && !scope && !transition;
1055
+ const confirmLabel = 'この依頼の対象・操作・制約のまま進める';
1056
+ const question = confirmCurrent
1057
+ ? `次の依頼について、対象と操作を判定しきれませんでした。\n\n${state.task}\n\n予定している操作: ${{ answer: '回答', research: '調査', verify: '検証', change: decision.changeScope === 'document' ? '文書の編集' : 'コード・テスト・設定の変更', operate: '操作', unknown: '未確定' }[decision.action]}。\nこの依頼に書かれた対象・操作・制約のまま進めてよいですか?違う場合は、具体的な対象と希望する操作を訂正してください。`
1058
+ : reason.assessment
1059
+ ? target ? '依頼の対象を特定できませんでした。変更・確認する具体的なファイルや項目と、希望する操作を教えてください。' : '依頼の対象・操作・制約を判定しきれませんでした。今回の具体的な対象と希望する操作、以前の指示を変更する範囲を教えてください。'
1060
+ : transition
1061
+ ? `現在のタスクは ${context.activeTask.state} です。続けるために、どの操作を希望しますか?(${taskAllowedOperations(context.activeTask).join(' / ')})`
1062
+ : target
1063
+ ? 'どの具体的なファイル・項目・相手・操作を対象にしますか?'
1064
+ : scope
1065
+ ? '変更するのは文章だけですか、それともコード・テスト・設定も含みますか?'
1066
+ : decision?.action === 'unknown'
1067
+ ? 'その対象に、具体的に何をしてほしいですか?'
1068
+ : null;
1010
1069
  // Internal relationship classification belongs to the model. An unresolved
1011
1070
  // classifier is not evidence that the human omitted an action or target.
1012
1071
  if (reason.interpretation || question === null)
@@ -1041,34 +1100,6 @@ export class OnDemandIntake {
1041
1100
  detail: '原文照合または有限領域検査を利用できません。実行は準備されていません。',
1042
1101
  };
1043
1102
  }
1044
- // An inconclusive detector is not evidence that the human omitted a target.
1045
- // Return the submitted interpretation for repair; keep preparation denied.
1046
- if (reason.assessment &&
1047
- state.assessment?.result.semantic === 'insufficient' &&
1048
- reason.candidate) {
1049
- const assessment = state.assessment.result;
1050
- const issues = assessment.semanticIssues ?? [];
1051
- const unresolved = assessment.semanticChecks?.filter(check => issues.includes(check.criterion)) ?? [];
1052
- const providerUncertain = issues.length > 0 && unresolved.length === issues.length && unresolved.every(check => check.status === 'abstained' && (check.reason === 'uncertain' || check.reason === 'tie'));
1053
- return {
1054
- prepared: false,
1055
- status: 'assessment_rejected',
1056
- semantic: state.assessment.result.semantic,
1057
- semanticIssues: state.assessment.result.semanticIssues ?? [],
1058
- semanticChecks: assessment.semanticChecks ?? [],
1059
- originalTask: state.task,
1060
- requestIntent: reason.candidate,
1061
- currentReferences: context
1062
- ? requestIntentRequiredReferenceIds(context)
1063
- : [],
1064
- availableReferences: context ? requestIntentReferenceIds(context) : [],
1065
- permissions: 'unchanged_native_policy',
1066
- nextAction: providerUncertain ? 'report_assessment_uncertainty' : 'revise_request_intent_or_report_conflict',
1067
- detail: providerUncertain
1068
- ? 'The independent provider abstained because of confidence or tied choices. This does not establish ambiguity in the user request or an incorrect candidate. Preserve the request and candidate and report the unresolved provider assessment. Do not resubmit an identical candidate, change its meaning merely to obtain acceptance, ask the human for internal classification fields, or bypass preparation. No work is prepared.'
1069
- : 'The independent comparison did not establish the submitted candidate. Re-read the captured request, answers, active task and same-session plan. Cite an existing proposal only when the request adopts it. Correct the candidate or report the specific unresolved comparison; do not repeat an identical submission or ask the human to supply internal classification fields. Ask a concrete question only if the source itself leaves an action or target ambiguous. No work is prepared.',
1070
- };
1071
- }
1072
1103
  const replies = this.#clarificationContexts.get(state.input.agent) ??
1073
1104
  new Map();
1074
1105
  const clarification = collectClarificationContext(humanMessages(state.input.messages).map((message) => ({
@@ -1156,6 +1187,7 @@ export class OnDemandIntake {
1156
1187
  result: result,
1157
1188
  operation: undefined,
1158
1189
  };
1190
+ state.clarificationPending = true;
1159
1191
  this.#clarifications.set(sessionId, pending);
1160
1192
  await this.diagnose(state, {
1161
1193
  kind: 'question',
@@ -1179,7 +1211,7 @@ export class OnDemandIntake {
1179
1211
  {
1180
1212
  id,
1181
1213
  question,
1182
- ...(scope && !target && !transition
1214
+ ...(confirmCurrent ? { options: [{ label: confirmLabel }, { label: '対象または操作を訂正する' }] } : scope && !target && !transition
1183
1215
  ? {
1184
1216
  options: [
1185
1217
  { label: '文章のみ' },
@@ -1223,24 +1255,62 @@ export class OnDemandIntake {
1223
1255
  };
1224
1256
  return pending.result;
1225
1257
  }
1226
- const answer = response.answers[0];
1258
+ let answer = response.answers[0];
1259
+ let answerQuestionId = id, answerQuestion = question;
1260
+ if (confirmCurrent && answer?.id === id && !answer.custom?.trim() &&
1261
+ answer.selected.length === 1 && answer.selected[0] === '対象または操作を訂正する') {
1262
+ answerQuestionId = `${id}-correction`;
1263
+ answerQuestion = `次の依頼について、訂正後の具体的な対象(ファイル・項目など)と希望する操作を入力してください。変更しない範囲もあれば含めてください。\n\n${state.task}`;
1264
+ await this.diagnose(state, { kind: 'question', questionRef: answerQuestionId, reason: 'clarification_required' });
1265
+ const corrected = await abortable(this.host.questions.ask({ agent: state.input.agent, signal,
1266
+ questions: [{ id: answerQuestionId, question: answerQuestion }] }), signal);
1267
+ signal.throwIfAborted();
1268
+ this.assertCurrent(state);
1269
+ if (this.#clarifications.get(sessionId) !== pending || pending.status !== 'waiting')
1270
+ return result;
1271
+ await this.host.validate(state.input);
1272
+ const afterCorrection = this.host.requestContext
1273
+ ? await this.host.requestContext(state.input, state.task)
1274
+ : (await this.host.interpret?.(state.input, state.task))?.context;
1275
+ signal.throwIfAborted();
1276
+ this.assertCurrent(state);
1277
+ if (context && (!afterCorrection || canonicalContentHash(afterCorrection) !== canonicalContentHash(context))) {
1278
+ if (afterCorrection) {
1279
+ state.requestIntentContext = afterCorrection;
1280
+ state.requestIntent = unresolvedRequestIntent(afterCorrection, 'clarification_context_changed');
1281
+ state.requestIntentContextDigest = canonicalContentHash(afterCorrection);
1282
+ }
1283
+ pending.result = { ...result, status: 'clarification_context_changed',
1284
+ detail: 'The source changed while awaiting the correction. No answer was queued; refresh the current request.' };
1285
+ return pending.result;
1286
+ }
1287
+ answer = corrected.answers[0];
1288
+ }
1227
1289
  const value = answer?.custom?.trim() || answer?.selected[0]?.trim();
1228
- if (answer?.id !== id || !value || value.length > 8192)
1290
+ if (answer?.id !== answerQuestionId || !value || value.length > 8192)
1229
1291
  return result;
1230
- // Preserve actual native sources and answers once; never recursively quote an earlier generated envelope.
1231
- // label the actual UI answer separately, and re-enter ordinary native
1232
- // capture. The reply itself is never mapped to execution authority.
1292
+ // Preserve native sources and answers once, then recapture the reply.
1293
+ // Only the exact affirmative selection below creates a scoped receipt;
1294
+ // a free-text correction is fresh user evidence, never an admission.
1233
1295
  const answerMessageId = randomUUID();
1234
1296
  const reply = clarificationReply(clarification, {
1235
1297
  id: answerMessageId,
1236
- questionId: id,
1237
- question,
1298
+ questionId: answerQuestionId,
1299
+ question: answerQuestion,
1238
1300
  value,
1239
1301
  scope: answerScope,
1240
1302
  });
1241
1303
  const content = reply.content;
1242
1304
  signal.throwIfAborted();
1243
1305
  this.assertCurrent(state);
1306
+ if (confirmCurrent && answerQuestionId === id && decision && context && !answer.custom?.trim() &&
1307
+ answer.selected.length === 1 && answer.selected[0] === confirmLabel) {
1308
+ const receipts = this.#nativeConfirmations.get(state.input.agent) ?? new Map();
1309
+ receipts.set(answerMessageId, { session: state.input.agent.session, originalTurn: state.input.turn, originalTask: state.task,
1310
+ contentDigest: canonicalContentHash(content), sourcesDigest: canonicalContentHash(clarification.sources),
1311
+ scopeDigest: this.confirmationScope(context), candidateDigest: this.confirmationCandidate(decision) });
1312
+ this.#nativeConfirmations.set(state.input.agent, receipts);
1313
+ }
1244
1314
  replies.set(answerMessageId, reply.context);
1245
1315
  this.#clarificationContexts.set(state.input.agent, replies);
1246
1316
  state.input.agent.followup({
@@ -1251,12 +1321,14 @@ export class OnDemandIntake {
1251
1321
  });
1252
1322
  await this.diagnose(state, {
1253
1323
  kind: 'answer',
1254
- questionRef: id,
1324
+ questionRef: answerQuestionId,
1255
1325
  answerRef: answerMessageId,
1256
1326
  });
1257
1327
  pending.status = 'queued';
1258
1328
  pending.result = {
1259
1329
  ...result,
1330
+ questionId: answerQuestionId,
1331
+ question: answerQuestion,
1260
1332
  status: 'clarification_answer_queued',
1261
1333
  answer: value,
1262
1334
  answerRef: answerMessageId,
@@ -1357,7 +1429,10 @@ export class OnDemandIntake {
1357
1429
  if (prior.relation !== bound.relation || prior.action !== bound.action) {
1358
1430
  throw new Error('Main-model interpretation conflicts with the configured decision provider');
1359
1431
  }
1360
- return { ...prior, evidence: bound.evidence };
1432
+ // The explicit submission is a current main-model interpretation, even
1433
+ // when its classification agrees with earlier provider extraction.
1434
+ // Preserve the explicit submission's provenance; this is not user confirmation.
1435
+ return bound;
1361
1436
  }
1362
1437
  return bound;
1363
1438
  }
@@ -1533,6 +1608,7 @@ export class OnDemandIntake {
1533
1608
  return (!this.#stopped &&
1534
1609
  !execution.signal.aborted &&
1535
1610
  UNOWNED_NATIVE_TOOLS.has(execution.name) &&
1611
+ (!state?.clarificationPending || NATIVE_EVIDENCE_READS.has(execution.name) || execution.name === 'skill') &&
1536
1612
  execution.parent === undefined &&
1537
1613
  !!state &&
1538
1614
  state.nativeUnowned === true &&
@@ -1543,9 +1619,11 @@ export class OnDemandIntake {
1543
1619
  proof.execute === this.definition(execution)?.execute &&
1544
1620
  proof.nativeUnownedDigest === canonicalContentHash(execution.arguments));
1545
1621
  }
1546
- /** Direct native Bash follows native permissions, independently of optional task intake. */
1622
+ /** Ordinary native Bash follows native policy; an issued clarification cannot be bypassed by switching tools. */
1547
1623
  nativeCommand(execution) {
1548
1624
  const state = this.bound(execution), actual = this.definition(execution), proof = this.#executions.get(execution);
1625
+ if (state?.clarificationPending)
1626
+ return false;
1549
1627
  if (!this.options.onlyWhenDeferred &&
1550
1628
  !this.#stopped &&
1551
1629
  actual &&
@@ -1576,6 +1654,15 @@ export class OnDemandIntake {
1576
1654
  proof.execute === actual.execute &&
1577
1655
  proof.nativeCommandDigest === canonicalContentHash(execution.arguments)));
1578
1656
  }
1657
+ /** Known observational/cancellation Lisp contracts stay usable while waiting.
1658
+ * Live evaluation is effect-capable; do not inspect or guess its code string. */
1659
+ pendingLispEffect(execution) {
1660
+ if (['lisp_status', 'lisp_hot_status', 'lisp_cancel'].includes(execution.name))
1661
+ return false;
1662
+ const args = execution.arguments;
1663
+ return !(execution.name === 'lisp_inspect' && execution.parent === undefined &&
1664
+ typeof args?.resultOperationId === 'string' && args.ref === undefined);
1665
+ }
1579
1666
  /** Delegate real root Lisp tools to their own effect/lease/approval authority. */
1580
1667
  nativeLisp(execution) {
1581
1668
  const state = this.bound(execution), actual = this.definition(execution), proof = this.#executions.get(execution);
@@ -1587,6 +1674,7 @@ export class OnDemandIntake {
1587
1674
  state.status !== 'answer' ||
1588
1675
  state.input.signal.aborted ||
1589
1676
  !actual ||
1677
+ (state.clarificationPending && this.pendingLispEffect(execution)) ||
1590
1678
  !isLispImplementation(actual.execute) ||
1591
1679
  (execution.agent && this.host.nativeChild?.(execution.agent)))
1592
1680
  return false;
@@ -1861,6 +1949,11 @@ export class OnDemandIntake {
1861
1949
  }
1862
1950
  try {
1863
1951
  this.assertCurrent(state);
1952
+ if (state.clarificationPending && (execution.name === 'bash' || isLispNativeCommand(execution, actual.execute) ||
1953
+ (isLispImplementation(actual.execute) && this.pendingLispEffect(execution)) ||
1954
+ (state.nativeUnowned && UNOWNED_NATIVE_TOOLS.has(execution.name) &&
1955
+ !NATIVE_EVIDENCE_READS.has(execution.name) && execution.name !== 'skill')))
1956
+ return denied('The current request is awaiting clarification. Native commands cannot bypass that question; resolve it and prepare the exact request first.');
1864
1957
  if (state.confirmationResume &&
1865
1958
  execution.name !== TASK_PREPARE_TOOL &&
1866
1959
  this.host.ready(state.input.agent, state.input.turn))