@aria_asi/cli 0.2.29 → 0.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aria_asi/cli",
3
- "version": "0.2.29",
4
- "description": "Aria Smart CLI the world's first harness-powered terminal companion",
3
+ "version": "0.2.30",
4
+ "description": "Aria Smart CLI \u2014 the world's first harness-powered terminal companion",
5
5
  "bin": {
6
6
  "aria": "./bin/aria.js"
7
7
  },
@@ -618,6 +618,24 @@ export function buildRuntimeCognitionDirective(packet, bundle = {}) {
618
618
  '- Predictor tests whether the answer will survive the next real operational step.',
619
619
  '- Mizan keeps the answer proportionate, exact, and high-quality.',
620
620
  '',
621
+ 'Visible cognition contract for the user-facing response:',
622
+ '- For any non-trivial reply, include a readable <cognition> block using everyday labels, not internal lens codenames.',
623
+ '- If you request any non-trivial tool call, place the <cognition> block BEFORE the tool request.',
624
+ '- If the action is deploy, destructive, or state-mutating, include both <verify> and <expected> blocks before the tool request.',
625
+ '- The runtime will canonicalize the readable block into backend JSON automatically, so do not hide it behind private shorthand.',
626
+ '',
627
+ '<cognition>',
628
+ ' truth: <what is actually true here, grounded in visible evidence and substrate anchors>',
629
+ ' harm: <what could break, mislead, or damage trust if the next step is wrong>',
630
+ ' trust: <what principle, promise, or user trust boundary governs this turn>',
631
+ ' power: <what capability is being used and what restraint it requires>',
632
+ ' reflection: <what deeper structural read changes the action, not just the wording>',
633
+ ' context: <what surrounding repo/runtime/user context materially changes the correct move>',
634
+ ' impact: <what second-order consequence or next-step outcome follows from this move>',
635
+ ' beauty: <what cleaner, more elegant, more durable path preserves the contract>',
636
+ ` first_principle: ${firstPrincipleText}`,
637
+ '</cognition>',
638
+ '',
621
639
  dominantModules.length ? 'Current module insights:' : 'Current module insights: none yet',
622
640
  ...dominantModules.map((line) => `- ${line}`),
623
641
  '',
@@ -65,12 +65,229 @@ const PRINCIPLE_LIMIT = 400;
65
65
  const PATTERN_LIMIT = 400;
66
66
  const RECEIPT_LIMIT = 500;
67
67
  const OWNER_TOKEN_PATH = join(process.env.HOME || '', '.aria', 'owner-token');
68
+ const COGNITION_BLOCK_RX = /<cognition>([\s\S]*?)<\/cognition>/i;
69
+ const VERIFY_BLOCK_RX =
70
+ /<verify>[\s\S]*?target\s*:[\s\S]*?role\s*:[\s\S]*?verified\s*:[\s\S]*?rollback\s*:[\s\S]*?axiom\s*:[\s\S]*?<\/verify>/i;
71
+ const EXPECTED_BLOCK_RX = /<expected>([\s\S]*?)<\/expected>/i;
72
+ const MEASURABLE_PREDICATE_RX =
73
+ /\b(?:exit_code|exit|rc|status|state|count|latency|error_rate|healthy|exists|http|rows?|bytes?|sha|heartbeat|predicate)\b\s*(?:[:=]|==|>=|<=|>|<)\s*[^\n]+|\b(?:true|false)\b|\b\d+(?:\.\d+)?%?\b/i;
74
+ const QUALITATIVE_DRIFT_RX = /\b(?:better|improved|should work|more reliable|cleaner|enhanced)\b/i;
75
+ const DECISION_SIGNAL_RX = /(?:should|recommend|propose|suggest|let'?s|go with|i'd|i would|here'?s the plan|i'll|next step|action item|ship it|yes do|let me)/i;
76
+ const TRIVIAL_ACK_RX = /^(?:got it|on it|ok|sure|yes|no|done|ack)\b/i;
77
+ const NON_TRIVIAL_MIN_CHARS = 300;
78
+ const READABLE_LENS_SLOTS = [
79
+ { key: 'truth', aliases: ['truth', 'nur', 'zahir'] },
80
+ { key: 'harm', aliases: ['harm', 'mizan', 'batin'] },
81
+ { key: 'trust', aliases: ['trust', 'hikma', 'hikmah'] },
82
+ { key: 'power', aliases: ['power', 'tafakkur', 'sabab'] },
83
+ { key: 'reflection', aliases: ['reflection', 'tadabbur', 'aqibah'] },
84
+ { key: 'context', aliases: ['context', 'ilham'] },
85
+ { key: 'impact', aliases: ['impact', 'wahi', 'meta'] },
86
+ { key: 'beauty', aliases: ['beauty', 'firasah', 'fitrah'] },
87
+ ];
88
+ const DOCTRINE_TRIGGER_MAP_CANDIDATES = [
89
+ join(__dirname, 'discipline', 'doctrine_trigger_map.json'),
90
+ join(__dirname, 'doctrine_trigger_map.json'),
91
+ join(process.env.HOME || '', '.claude', 'hooks', 'doctrine_trigger_map.json'),
92
+ join(process.env.HOME || '', '.claude', 'projects', '-home-hamzaibrahim1', 'memory', 'doctrine_trigger_map.json'),
93
+ join(process.env.HOME || '', '.codex', 'doctrine_trigger_map.json'),
94
+ ];
95
+ const TOOL_DEPLOY_PATTERNS = [
96
+ /\b(?:\.\/)?scripts\/deploy-/i,
97
+ /\bkubectl\s+apply\b/i,
98
+ /\bkubectl\s+set\s+image\b/i,
99
+ /\bkubectl\s+rollout\s+restart\b/i,
100
+ /\bkubectl\s+rollout\s+undo\b/i,
101
+ /\bdocker\s+push\b/i,
102
+ /\bdocker\s+build\b.*--push\b/i,
103
+ ];
104
+ const TOOL_DESTRUCTIVE_PATTERNS = [
105
+ /(?:^|[;&|]\s*|\$\(\s*|`\s*)sudo\s+\S/i,
106
+ /systemctl\s+(?:disable|stop|mask|reset-failed|kill)\b/i,
107
+ /\brm\s+-[rRfF]+/i,
108
+ /\bgit\s+push\b.*\b--force\b/i,
109
+ /\bgit\s+reset\s+--hard\b/i,
110
+ /\bgit\s+checkout\s+--\b/i,
111
+ /\b--no-verify\b/i,
112
+ /\b--no-gpg-sign\b/i,
113
+ /\bkill\s+-(?:9|KILL|TERM|HUP|INT)\b/i,
114
+ /\bpkill\b/i,
115
+ /\b(?:DROP|TRUNCATE)\s+(?:TABLE|DATABASE|SCHEMA|INDEX)\b/i,
116
+ /\bkubectl\s+delete\b/i,
117
+ ];
68
118
 
69
119
  function json(res, status, payload) {
70
120
  res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
71
121
  res.end(JSON.stringify(payload, null, 2));
72
122
  }
73
123
 
124
+ function isNonTrivialAssistantTurn(text, toolIntents = []) {
125
+ const body = String(text || '').trim();
126
+ if (toolIntents.length > 0) return true;
127
+ if (!body) return false;
128
+ if (TRIVIAL_ACK_RX.test(body)) return false;
129
+ return body.length >= NON_TRIVIAL_MIN_CHARS || DECISION_SIGNAL_RX.test(body);
130
+ }
131
+
132
+ function extractVisibleCognitionContract(text) {
133
+ const match = String(text || '').match(COGNITION_BLOCK_RX);
134
+ if (!match) {
135
+ return {
136
+ present: false,
137
+ readable: null,
138
+ rawBlock: null,
139
+ firstPrinciple: null,
140
+ labels: [],
141
+ };
142
+ }
143
+
144
+ const inner = match[1];
145
+ const readable = {};
146
+ const labels = [];
147
+ for (const slot of READABLE_LENS_SLOTS) {
148
+ const aliasPattern = slot.aliases.join('|');
149
+ const lensRx = new RegExp(
150
+ `\\b(?:${aliasPattern})\\s*:\\s*([^\\n]*(?:\\n(?!\\s*(?:${READABLE_LENS_SLOTS.flatMap((entry) => entry.aliases).join('|')})\\s*:|<\\/cognition>)[^\\n]*)*)`,
151
+ 'i',
152
+ );
153
+ const lensMatch = inner.match(lensRx);
154
+ if (!lensMatch) continue;
155
+ readable[slot.key] = (lensMatch[1] || '').trim();
156
+ labels.push(slot.key);
157
+ }
158
+
159
+ const firstPrincipleMatch = inner.match(/\bfirst[_\s-]?principle\s*:\s*([^\n][\s\S]*?)(?=\n\s*[a-z_ -]+\s*:|$)/i);
160
+ return {
161
+ present: true,
162
+ readable,
163
+ rawBlock: match[0],
164
+ firstPrinciple: firstPrincipleMatch ? firstPrincipleMatch[1].trim() : null,
165
+ labels,
166
+ };
167
+ }
168
+
169
+ function hasMeasurableExpectedBlock(text) {
170
+ const match = String(text || '').match(EXPECTED_BLOCK_RX);
171
+ if (!match) return false;
172
+ const inner = match[1].trim();
173
+ if (!inner) return false;
174
+ if (QUALITATIVE_DRIFT_RX.test(inner) && !MEASURABLE_PREDICATE_RX.test(inner)) return false;
175
+ return MEASURABLE_PREDICATE_RX.test(inner);
176
+ }
177
+
178
+ function parseToolArgs(rawArgs) {
179
+ if (!rawArgs) return {};
180
+ if (typeof rawArgs === 'object') return rawArgs;
181
+ if (typeof rawArgs !== 'string') return {};
182
+ try {
183
+ return JSON.parse(rawArgs);
184
+ } catch {
185
+ return { raw: rawArgs };
186
+ }
187
+ }
188
+
189
+ function inferToolAction(toolName, args) {
190
+ const lower = String(toolName || '').toLowerCase();
191
+ if (args && typeof args.command === 'string') return 'bash';
192
+ if (args && (typeof args.file_path === 'string' || typeof args.notebook_path === 'string')) return 'edit';
193
+ if (/\b(?:bash|shell|terminal|command)\b/.test(lower)) return 'bash';
194
+ if (/\b(?:edit|write|notebook)\b/.test(lower)) return 'edit';
195
+ if (/\b(?:deploy|rollout|release)\b/.test(lower)) return 'deploy';
196
+ if (/\b(?:delete|destroy|drop|wipe|purge)\b/.test(lower)) return 'delete';
197
+ if (/\b(?:build|compile)\b/.test(lower)) return 'build';
198
+ return 'tool';
199
+ }
200
+
201
+ function summarizeToolTarget(toolName, args) {
202
+ if (typeof args?.command === 'string' && args.command.trim()) return args.command.trim();
203
+ if (typeof args?.file_path === 'string' && args.file_path.trim()) return args.file_path.trim();
204
+ if (typeof args?.notebook_path === 'string' && args.notebook_path.trim()) return args.notebook_path.trim();
205
+ if (typeof args?.path === 'string' && args.path.trim()) return args.path.trim();
206
+ if (typeof args?.target === 'string' && args.target.trim()) return args.target.trim();
207
+ return `${toolName}${Object.keys(args || {}).length ? ` ${JSON.stringify(args).slice(0, 200)}` : ''}`.trim();
208
+ }
209
+
210
+ function extractProviderToolIntents(providerStyle, providerMeta) {
211
+ if (providerStyle === 'anthropic') {
212
+ const content = Array.isArray(providerMeta?.raw?.content) ? providerMeta.raw.content : [];
213
+ return content
214
+ .filter((block) => block?.type === 'tool_use')
215
+ .map((block) => {
216
+ const args = parseToolArgs(block.input || {});
217
+ return {
218
+ provider: 'anthropic',
219
+ id: block.id || null,
220
+ toolName: block.name || 'tool_use',
221
+ args,
222
+ action: inferToolAction(block.name || 'tool_use', args),
223
+ target: summarizeToolTarget(block.name || 'tool_use', args),
224
+ raw: block,
225
+ };
226
+ });
227
+ }
228
+
229
+ const rawMessage = providerMeta?.raw?.choices?.[0]?.message || {};
230
+ const toolCalls = Array.isArray(rawMessage.tool_calls) ? rawMessage.tool_calls : [];
231
+ return toolCalls.map((toolCall) => {
232
+ const name = toolCall?.function?.name || toolCall?.name || 'tool_call';
233
+ const args = parseToolArgs(toolCall?.function?.arguments || toolCall?.arguments || {});
234
+ return {
235
+ provider: 'openai',
236
+ id: toolCall?.id || null,
237
+ toolName: name,
238
+ args,
239
+ action: inferToolAction(name, args),
240
+ target: summarizeToolTarget(name, args),
241
+ raw: toolCall,
242
+ };
243
+ });
244
+ }
245
+
246
+ function loadDoctrineTriggerMap() {
247
+ for (const candidate of DOCTRINE_TRIGGER_MAP_CANDIDATES) {
248
+ const map = readJsonFile(candidate, null);
249
+ if (map && Array.isArray(map.triggers)) return map;
250
+ }
251
+ return { triggers: [] };
252
+ }
253
+
254
+ function collectDoctrineTriggerHits(text) {
255
+ const triggerMap = loadDoctrineTriggerMap();
256
+ const source = String(text || '');
257
+ const lowerText = source.toLowerCase();
258
+ const hits = [];
259
+ for (const entry of triggerMap.triggers || []) {
260
+ try {
261
+ const rx = new RegExp(entry.trigger, 'ig');
262
+ const matched = [...source.matchAll(rx)][0];
263
+ if (!matched) continue;
264
+ const memoryName = typeof entry.memory === 'string' ? entry.memory.replace(/\.md$/, '') : '';
265
+ const memoryCited = memoryName && lowerText.includes(memoryName.toLowerCase());
266
+ if (memoryCited) continue;
267
+ hits.push({
268
+ trigger: entry.trigger,
269
+ memory: entry.memory || null,
270
+ teaching: entry.teaching || null,
271
+ message: entry.message || null,
272
+ severity: entry.severity || 'block',
273
+ });
274
+ } catch {}
275
+ }
276
+ return hits;
277
+ }
278
+
279
+ function buildToolGateBlockMessage(blockers) {
280
+ const reasons = blockers.map((blocker) => `- ${blocker}`).join('\n');
281
+ return [
282
+ 'Aria runtime blocked the requested tool action.',
283
+ '',
284
+ reasons,
285
+ '',
286
+ 'Re-draft with a readable <cognition> block before the tool request.',
287
+ 'If the action is deploy, destructive, or state-mutating, include <verify> and <expected> blocks as well.',
288
+ ].join('\n');
289
+ }
290
+
74
291
  async function readJson(req) {
75
292
  const chunks = [];
76
293
  for await (const chunk of req) chunks.push(chunk);
@@ -982,6 +1199,9 @@ async function buildDirectTurnContext(req, body, client, options = {}) {
982
1199
 
983
1200
  function openAiResponseEnvelope(body, text, providerMeta, extra = {}) {
984
1201
  const debug = body?.ariaDebug === true;
1202
+ const toolCalls = !extra.blocked && Array.isArray(providerMeta?.raw?.choices?.[0]?.message?.tool_calls)
1203
+ ? providerMeta.raw.choices[0].message.tool_calls
1204
+ : undefined;
985
1205
  return {
986
1206
  id: `chatcmpl_${randomUUID().replace(/-/g, '')}`,
987
1207
  object: 'chat.completion',
@@ -990,10 +1210,11 @@ function openAiResponseEnvelope(body, text, providerMeta, extra = {}) {
990
1210
  choices: [
991
1211
  {
992
1212
  index: 0,
993
- finish_reason: providerMeta.finishReason || 'stop',
1213
+ finish_reason: toolCalls?.length ? (providerMeta.finishReason || 'tool_calls') : (providerMeta.finishReason || 'stop'),
994
1214
  message: {
995
1215
  role: 'assistant',
996
1216
  content: text,
1217
+ ...(toolCalls?.length ? { tool_calls: toolCalls } : {}),
997
1218
  },
998
1219
  },
999
1220
  ],
@@ -1009,13 +1230,32 @@ function openAiResponseEnvelope(body, text, providerMeta, extra = {}) {
1009
1230
  }
1010
1231
 
1011
1232
  function anthropicResponseEnvelope(text, providerMeta, extra = {}, debug = false) {
1233
+ const rawContent = Array.isArray(providerMeta?.raw?.content) ? providerMeta.raw.content : [];
1234
+ const content = !extra.blocked && rawContent.length
1235
+ ? rawContent.map((block) => {
1236
+ if (block?.type === 'text') {
1237
+ return { type: 'text', text: typeof block.text === 'string' ? block.text : text };
1238
+ }
1239
+ if (block?.type === 'tool_use') {
1240
+ return {
1241
+ type: 'tool_use',
1242
+ id: block.id,
1243
+ name: block.name,
1244
+ input: block.input || {},
1245
+ };
1246
+ }
1247
+ return block;
1248
+ })
1249
+ : [{ type: 'text', text }];
1012
1250
  return {
1013
1251
  id: `msg_${randomUUID().replace(/-/g, '')}`,
1014
1252
  type: 'message',
1015
1253
  role: 'assistant',
1016
1254
  model: providerMeta.model,
1017
- stop_reason: providerMeta.finishReason || 'end_turn',
1018
- content: [{ type: 'text', text }],
1255
+ stop_reason: rawContent.some((block) => block?.type === 'tool_use') && !extra.blocked
1256
+ ? (providerMeta.finishReason || 'tool_use')
1257
+ : (providerMeta.finishReason || 'end_turn'),
1258
+ content,
1019
1259
  usage: providerMeta.usage
1020
1260
  ? {
1021
1261
  input_tokens: providerMeta.usage.promptTokens || 0,
@@ -1111,6 +1351,30 @@ function buildReadableAriaEnvelope(extra = {}, debug = false) {
1111
1351
  : [],
1112
1352
  } : null;
1113
1353
 
1354
+ const cognition = extra.cognitionContract ? {
1355
+ visible: Boolean(extra.cognitionContract.present),
1356
+ labels: Array.isArray(extra.cognitionContract.labels) ? extra.cognitionContract.labels : [],
1357
+ readable: extra.cognitionContract.readable || null,
1358
+ first_principle: extra.cognitionContract.firstPrinciple || null,
1359
+ } : null;
1360
+
1361
+ const doctrine = extra.doctrine ? {
1362
+ blocked: Boolean(extra.doctrine.blocked),
1363
+ hits: Array.isArray(extra.doctrine.hits) ? extra.doctrine.hits.slice(0, 3) : [],
1364
+ } : null;
1365
+
1366
+ const tool_gate = extra.toolGate ? {
1367
+ blocked: Boolean(extra.toolGate.blocked),
1368
+ blockers: Array.isArray(extra.toolGate.blockers) ? extra.toolGate.blockers.slice(0, 5) : [],
1369
+ intents: Array.isArray(extra.toolGate.intents)
1370
+ ? extra.toolGate.intents.map((intent) => ({
1371
+ tool: intent.toolName,
1372
+ action: intent.action,
1373
+ target: intent.target,
1374
+ }))
1375
+ : [],
1376
+ } : null;
1377
+
1114
1378
  const envelope = {
1115
1379
  blocked: Boolean(extra.blocked),
1116
1380
  control_plane: 'aria-mounted-runtime',
@@ -1128,6 +1392,9 @@ function buildReadableAriaEnvelope(extra = {}, debug = false) {
1128
1392
  receipts,
1129
1393
  validation,
1130
1394
  layer3,
1395
+ cognition,
1396
+ doctrine,
1397
+ tool_gate,
1131
1398
  };
1132
1399
 
1133
1400
  if (debug) {
@@ -1141,6 +1408,34 @@ function coerceNonEmptyString(value) {
1141
1408
  return typeof value === 'string' && value.trim() ? value.trim() : '';
1142
1409
  }
1143
1410
 
1411
+ function analyzeToolIntentContract(intent, assistantText) {
1412
+ const target = String(intent?.target || '');
1413
+ const action = String(intent?.action || 'tool');
1414
+ const blockers = [];
1415
+ const isDeploy = action === 'deploy' || TOOL_DEPLOY_PATTERNS.some((rx) => rx.test(target));
1416
+ const isDestructive = action === 'delete' || TOOL_DESTRUCTIVE_PATTERNS.some((rx) => rx.test(target));
1417
+ const isMutation = isDeploy || isDestructive || action === 'edit' || action === 'write' || action === 'bash' || action === 'build';
1418
+
1419
+ if (!assistantText.match(COGNITION_BLOCK_RX)) {
1420
+ blockers.push(`${intent.toolName}: missing readable <cognition> block before tool request`);
1421
+ }
1422
+ if (isMutation && !assistantText.match(EXPECTED_BLOCK_RX)) {
1423
+ blockers.push(`${intent.toolName}: missing <expected> block before non-trivial tool request`);
1424
+ } else if (isMutation && !hasMeasurableExpectedBlock(assistantText)) {
1425
+ blockers.push(`${intent.toolName}: <expected> block lacks a measurable predicate`);
1426
+ }
1427
+ if ((isDeploy || isDestructive) && !VERIFY_BLOCK_RX.test(assistantText)) {
1428
+ blockers.push(`${intent.toolName}: missing <verify> block before deploy/destructive tool request`);
1429
+ }
1430
+
1431
+ return {
1432
+ isMutation,
1433
+ isDeploy,
1434
+ isDestructive,
1435
+ blockers,
1436
+ };
1437
+ }
1438
+
1144
1439
  function extractJsonObject(text) {
1145
1440
  const raw = String(text || '').trim();
1146
1441
  if (!raw) {
@@ -1547,22 +1842,17 @@ async function handleProviderProxy(req, body, client, providerStyle) {
1547
1842
  : await callProviderForOpenAI(body, turn.ariaSystemPrompt);
1548
1843
 
1549
1844
  let candidateText = providerMeta.text || '';
1550
- let validation;
1551
- try {
1552
- validation = await client.validateOutput(candidateText, turn.sessionId);
1553
- } catch (error) {
1554
- if (!turn.packetBypassed) throw error;
1555
- validation = {
1556
- passed: true,
1557
- severity: 'warn',
1558
- violations: [
1559
- `remote validate unavailable: ${error instanceof Error ? error.message : String(error)}`,
1560
- ],
1561
- gateTriggers: ['owner-local-bypass'],
1562
- };
1563
- }
1564
- if (validation?.severity === 'block' && validation?.rewritten) {
1565
- candidateText = validation.rewritten;
1845
+ const toolIntents = extractProviderToolIntents(providerStyle, providerMeta);
1846
+ const requiresReadableCognition = isNonTrivialAssistantTurn(candidateText, toolIntents);
1847
+ const cognitionContract = extractVisibleCognitionContract(candidateText);
1848
+
1849
+ let validation = {
1850
+ passed: true,
1851
+ severity: 'pass',
1852
+ violations: [],
1853
+ gateTriggers: [],
1854
+ };
1855
+ if (candidateText.trim()) {
1566
1856
  try {
1567
1857
  validation = await client.validateOutput(candidateText, turn.sessionId);
1568
1858
  } catch (error) {
@@ -1571,19 +1861,60 @@ async function handleProviderProxy(req, body, client, providerStyle) {
1571
1861
  passed: true,
1572
1862
  severity: 'warn',
1573
1863
  violations: [
1574
- `remote validate unavailable after rewrite: ${error instanceof Error ? error.message : String(error)}`,
1864
+ `remote validate unavailable: ${error instanceof Error ? error.message : String(error)}`,
1575
1865
  ],
1576
1866
  gateTriggers: ['owner-local-bypass'],
1577
1867
  };
1578
1868
  }
1869
+ if (validation?.severity === 'block' && validation?.rewritten) {
1870
+ candidateText = validation.rewritten;
1871
+ try {
1872
+ validation = await client.validateOutput(candidateText, turn.sessionId);
1873
+ } catch (error) {
1874
+ if (!turn.packetBypassed) throw error;
1875
+ validation = {
1876
+ passed: true,
1877
+ severity: 'warn',
1878
+ violations: [
1879
+ `remote validate unavailable after rewrite: ${error instanceof Error ? error.message : String(error)}`,
1880
+ ],
1881
+ gateTriggers: ['owner-local-bypass'],
1882
+ };
1883
+ }
1884
+ }
1579
1885
  }
1580
1886
 
1581
1887
  const layer3 = await runLayer3(req, {
1582
- text: candidateText,
1888
+ text: candidateText || (toolIntents.length > 0 ? `<cognition>\n</cognition>` : ''),
1583
1889
  packet: turn.packet,
1584
1890
  fetchPacket: false,
1585
- requireCognitionBlock: body.requireCognitionBlock ?? false,
1891
+ requireCognitionBlock: body.requireCognitionBlock ?? requiresReadableCognition,
1586
1892
  }, client);
1893
+ const doctrineHits = candidateText.trim() ? collectDoctrineTriggerHits(candidateText) : [];
1894
+ const doctrineBlockers = doctrineHits
1895
+ .filter((hit) => String(hit.severity || 'block').toLowerCase() === 'block')
1896
+ .map((hit) => hit.message || hit.teaching || hit.trigger);
1897
+
1898
+ const toolGateBlockers = [];
1899
+ for (const intent of toolIntents) {
1900
+ const contract = analyzeToolIntentContract(intent, candidateText);
1901
+ toolGateBlockers.push(...contract.blockers);
1902
+ try {
1903
+ const runtimeCheck = await client.checkAction(
1904
+ contract.isDeploy ? 'deploy'
1905
+ : contract.isDestructive ? 'delete'
1906
+ : intent.action === 'build' ? 'build'
1907
+ : 'write',
1908
+ intent.target || intent.toolName,
1909
+ );
1910
+ if (!runtimeCheck.allowed) {
1911
+ toolGateBlockers.push(`${intent.toolName}: ${runtimeCheck.reason || 'runtime action gate blocked this tool request'}`);
1912
+ }
1913
+ } catch (error) {
1914
+ if (!turn.packetBypassed) throw error;
1915
+ toolGateBlockers.push(`${intent.toolName}: runtime action gate unavailable during owner-local-bypass`);
1916
+ }
1917
+ }
1587
1918
  const postBundle = evaluateMizanPost(candidateText, {
1588
1919
  hasVerifiedState: findVerifiedState(candidateText),
1589
1920
  layer3Pass: layer3.pass,
@@ -1601,8 +1932,17 @@ async function handleProviderProxy(req, body, client, providerStyle) {
1601
1932
  });
1602
1933
  const postResult = postBundle.result;
1603
1934
 
1604
- const blocked = validation.severity === 'block' || !layer3.pass || postResult.reAuthorSignal;
1605
- const finalText = blocked ? buildPhaseBlockMessage(postResult, 'post') : candidateText;
1935
+ const blocked =
1936
+ validation.severity === 'block' ||
1937
+ !layer3.pass ||
1938
+ postResult.reAuthorSignal ||
1939
+ doctrineBlockers.length > 0 ||
1940
+ toolGateBlockers.length > 0;
1941
+ const finalText = toolGateBlockers.length > 0
1942
+ ? buildToolGateBlockMessage(toolGateBlockers)
1943
+ : blocked
1944
+ ? buildPhaseBlockMessage(postResult, 'post')
1945
+ : candidateText;
1606
1946
  await persistTurnArtifacts(req, body, client, apiKey, {
1607
1947
  ...turn,
1608
1948
  postResult,
@@ -1632,6 +1972,16 @@ async function handleProviderProxy(req, body, client, providerStyle) {
1632
1972
  operatorPlan: turn.operatorPlan,
1633
1973
  validation,
1634
1974
  layer3,
1975
+ cognitionContract,
1976
+ doctrine: {
1977
+ blocked: doctrineBlockers.length > 0,
1978
+ hits: doctrineHits,
1979
+ },
1980
+ toolGate: {
1981
+ blocked: toolGateBlockers.length > 0,
1982
+ blockers: toolGateBlockers,
1983
+ intents: toolIntents,
1984
+ },
1635
1985
  };
1636
1986
  return providerStyle === 'anthropic'
1637
1987
  ? anthropicResponseEnvelope(finalText, providerMeta, extra, body?.ariaDebug === true)