@milpa/comal 0.1.7 → 0.2.1

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 (134) hide show
  1. package/README.md +19 -1
  2. package/backend/contracts/remote-commands-v1.json +12 -0
  3. package/governance/chrome-task-v10.json +323 -0
  4. package/governance/chrome-task-v11.json +355 -0
  5. package/governance/chrome-task-v12.json +387 -0
  6. package/governance/chrome-task-v13.json +419 -0
  7. package/governance/chrome-task-v3.json +99 -0
  8. package/governance/chrome-task-v4.json +131 -0
  9. package/governance/chrome-task-v5.json +163 -0
  10. package/governance/chrome-task-v6.json +195 -0
  11. package/governance/chrome-task-v7.json +227 -0
  12. package/governance/chrome-task-v8.json +259 -0
  13. package/governance/chrome-task-v9.json +291 -0
  14. package/governance/source-task-v1.json +1 -0
  15. package/governance/source-task-v2.json +1 -0
  16. package/package.json +14 -1
  17. package/src/agent-recovery-proof.mjs +12 -1
  18. package/src/agent.mjs +94 -30
  19. package/src/audit.mjs +21 -6
  20. package/src/auto-guard.mjs +48 -1
  21. package/src/chrome-candidate-recovery-proof.mjs +47 -0
  22. package/src/chrome-recovery-proof.mjs +6 -2
  23. package/src/chrome-source-syntax.mjs +93 -0
  24. package/src/cli.mjs +158 -54
  25. package/src/codex-provider.mjs +3 -2
  26. package/src/connection-settings.mjs +204 -0
  27. package/src/context-pressure.mjs +2 -1
  28. package/src/durable-agent-runs.mjs +5 -11
  29. package/src/event-channels.mjs +8 -0
  30. package/src/file-recovery-proof.mjs +11 -3
  31. package/src/file-recovery.mjs +24 -0
  32. package/src/gemini-provider.mjs +6 -1
  33. package/src/git-agent-revision.mjs +12 -0
  34. package/src/git-agent-workspaces.mjs +143 -0
  35. package/src/git-integration-lineage.mjs +20 -0
  36. package/src/git-integration-operations.mjs +200 -0
  37. package/src/git-integration-recovery-proof.mjs +70 -0
  38. package/src/git-page-cli-bridge.mjs +61 -0
  39. package/src/git-page-evidence.mjs +30 -0
  40. package/src/git-page-identity.mjs +10 -0
  41. package/src/git-page-proof.mjs +96 -0
  42. package/src/git-page-settlement.mjs +21 -0
  43. package/src/git-page-verification.mjs +134 -0
  44. package/src/git-plumbing.mjs +212 -0
  45. package/src/git-process-worker.mjs +50 -0
  46. package/src/git-source.mjs +110 -0
  47. package/src/git-workspaces.mjs +519 -0
  48. package/src/goals.mjs +13 -7
  49. package/src/house.mjs +246 -20
  50. package/src/i18n/cli-es.mjs +4 -0
  51. package/src/i18n/es.mjs +35 -0
  52. package/src/judges.mjs +27 -11
  53. package/src/mcp-runtime.mjs +15 -3
  54. package/src/model-controls.mjs +11 -1
  55. package/src/model-deadline.mjs +4 -3
  56. package/src/native-refusal.mjs +38 -0
  57. package/src/onboarding.mjs +88 -24
  58. package/src/openai-provider.mjs +14 -7
  59. package/src/operations.mjs +9 -2
  60. package/src/output-recovery.mjs +48 -0
  61. package/src/peer-tools.mjs +18 -8
  62. package/src/permission-controls.mjs +232 -0
  63. package/src/permission-rules.mjs +312 -0
  64. package/src/permissions.mjs +113 -30
  65. package/src/plugin-catalog.mjs +84 -0
  66. package/src/plugin-config.mjs +48 -6
  67. package/src/plugin-controls.mjs +332 -0
  68. package/src/plugin-notices.mjs +88 -0
  69. package/src/plugin-trust.mjs +104 -0
  70. package/src/plugins/chrome-lab/bridge.mjs +38 -12
  71. package/src/plugins/chrome-lab/client.mjs +103 -25
  72. package/src/plugins/chrome-lab/diagnostics.mjs +19 -0
  73. package/src/plugins/chrome-lab/index.mjs +116 -26
  74. package/src/plugins/chrome-lab/project.mjs +30 -2
  75. package/src/plugins/chrome-lab/scoped-input.mjs +135 -0
  76. package/src/plugins/chrome-lab/worker-diagnostics.mjs +51 -0
  77. package/src/plugins/open-relay/actions.mjs +205 -0
  78. package/src/plugins/open-relay/bound.mjs +47 -0
  79. package/src/plugins/open-relay/client.mjs +158 -0
  80. package/src/plugins/open-relay/dossier.mjs +119 -0
  81. package/src/plugins/open-relay/index.mjs +153 -0
  82. package/src/plugins/open-relay/lookup.mjs +45 -0
  83. package/src/plugins/open-relay/notices.mjs +79 -0
  84. package/src/plugins/open-relay/watch.mjs +127 -0
  85. package/src/plugins.mjs +72 -29
  86. package/src/private-fs.mjs +46 -0
  87. package/src/process-lock.mjs +24 -13
  88. package/src/provider-auth.mjs +14 -3
  89. package/src/provider.mjs +11 -2
  90. package/src/read-only-check.mjs +7 -3
  91. package/src/scope-ceiling.mjs +56 -0
  92. package/src/scoped-policy.mjs +39 -0
  93. package/src/secret-login.mjs +68 -0
  94. package/src/session-bus.mjs +41 -7
  95. package/src/session-projections-candidate.mjs +119 -0
  96. package/src/session-projections.mjs +119 -0
  97. package/src/session-read-cache.mjs +23 -3
  98. package/src/sessions.mjs +35 -11
  99. package/src/source-editor-role.mjs +13 -0
  100. package/src/source-ownership.mjs +76 -0
  101. package/src/source-proposal-proof.mjs +55 -0
  102. package/src/source-proposals.mjs +118 -0
  103. package/src/source-requests.mjs +148 -0
  104. package/src/source-task-proof.mjs +24 -0
  105. package/src/source-task-revision-proof.mjs +44 -0
  106. package/src/subagents.mjs +187 -87
  107. package/src/tool-packs.mjs +5 -5
  108. package/src/tools.mjs +82 -15
  109. package/src/trusted-context.mjs +5 -1
  110. package/src/tui/connection-transition.mjs +1 -1
  111. package/src/tui/controller.mjs +8 -4
  112. package/src/tui/discovery.mjs +24 -12
  113. package/src/tui/inbound-blocks.mjs +55 -0
  114. package/src/tui/present.mjs +504 -0
  115. package/src/tui/screen.mjs +49 -13
  116. package/src/windows-private.ps1 +63 -0
  117. package/src/work-assertions.mjs +6 -2
  118. package/src/work-plan.mjs +34 -8
  119. package/src/work-state.mjs +15 -9
  120. package/src/workflows/chrome-page-cases.mjs +156 -0
  121. package/src/workflows/chrome-page-checkpoint-reader.mjs +10 -0
  122. package/src/workflows/chrome-page-diagnostics.mjs +113 -0
  123. package/src/workflows/chrome-page-law.mjs +48 -0
  124. package/src/workflows/chrome-page-recovery-host-v13.mjs +14 -0
  125. package/src/workflows/chrome-page-recovery-v13.mjs +104 -0
  126. package/src/workflows/chrome-page-task.mjs +429 -0
  127. package/src/workflows/chrome-page-verifier.mjs +123 -0
  128. package/src/workflows/chrome-source.mjs +73 -0
  129. package/src/workflows/chrome-supplied-inputs.mjs +43 -0
  130. package/src/workflows/chrome-task-adoption.mjs +122 -0
  131. package/src/workflows/chrome-task.mjs +379 -74
  132. package/src/workflows/source-task-v1.mjs +124 -0
  133. package/src/workflows/source-task-v2.mjs +227 -0
  134. package/src/workflows/source-task.mjs +13 -0
@@ -8,6 +8,8 @@ import { copyText } from './clipboard.mjs';
8
8
  import { tone, createTones, safeText, wrap, width, clip, pad, progressBar, stripANSI, terminalSurface } from './text.mjs';
9
9
  import { brandingWelcome, brandingStatus, brandMotionEnabled, BRAND_FRAME_INTERVAL_MS, BRAND_WELCOME_FRAMES } from './brand.mjs';
10
10
  import { commandCatalog, commandSuggestions, applySuggestion, contextualTip } from './discovery.mjs';
11
+ import { inboundBlockFromRow, inboundBlockFromEvent, inboundLines } from './inbound-blocks.mjs';
12
+ import { presentResultBlock, presentToolBlock, presentPermission } from './present.mjs';
11
13
 
12
14
  const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
13
15
  const MOUSE_ON = '\x1b[?1000h\x1b[?1006h';
@@ -233,12 +235,17 @@ export class TerminalScreen {
233
235
  setAttachmentStatus(status) { this.attachmentStatus = status; this.schedule(); }
234
236
  draftChanged() { if (!this.composer.attachments.length) this.attachmentStatus = null; this.callbacks.onDraft?.(this.composer.text, this.composer.attachments); this.schedule(); }
235
237
  addTable(text, format, title) { const data = this.tables.parse(text, format); this.blocks.push({ role: 'table', data, text: title }); this.schedule(); }
238
+ // Resultados de comandos: la presentación vive en present.mjs; /raw alterna el JSON exacto.
239
+ commandResult(command, arg, text) { this.blocks.push({ role: 'result', command, arg, text: String(text ?? '') }); this.schedule(); }
240
+ commandOutput(command, arg) { this.pendingCommand = { command, arg }; }
241
+ toggleRaw() { this.rawView = !this.rawView; this.notice(this.rawView ? t('Raw view on: results and permissions show the exact JSON. /raw returns to the readable view.') : t('Readable view on. /raw shows the exact JSON again.')); }
236
242
  loadSession(session) {
237
243
  this.cacheReadTokens=null;
238
244
  this.preparedDraft = null; this.dismissedCompletion = null; this.blocks = []; this.toolBlocks.clear(); this.subagentBlocks.clear(); this.streaming = null; this.scroll = 0; this.lastContentLength = 0; this.goal = session.goal;
239
245
  const rows = session.rows.filter(row => row.type === 'message').slice(-200);
240
246
  for (const [responseId, row] of rows.entries()) {
241
- const message = row.message;
247
+ const message = row.message, inbound = inboundBlockFromRow(row);
248
+ if (inbound) { this.blocks.push(inbound); continue; }
242
249
  if (message.role === 'user' && typeof message.content === 'string' && row.source !== 'goal') this.blocks.push({ role: 'user', text: message.content });
243
250
  if (message.role === 'user' && row.source === 'human_composer' && Array.isArray(message.content)) this.blocks.push({role:'user',text:message.content.filter(block=>block.type==='text').map(block=>block.text).join('\n')});
244
251
  for (const block of Array.isArray(message.content) ? message.content : []) {
@@ -316,7 +323,9 @@ export class TerminalScreen {
316
323
  this.schedule(); return;
317
324
  }
318
325
  if (event.type === 'user' && event.source !== 'goal') this.blocks.push({ role: 'user', text: event.userIntent || event.text });
319
- if (event.type === 'text_delta') {
326
+ const inbound = inboundBlockFromEvent(event); if (inbound) this.blocks.push(inbound);
327
+ if (event.type === 'text_delta' && this.pendingCommand && !this.streaming) { const { command, arg } = this.pendingCommand; this.pendingCommand = null; this.blocks.push({ role: 'result', command, arg, text: String(event.text ?? '') }); }
328
+ else if (event.type === 'text_delta') {
320
329
  if (!this.streaming) { this.streaming = { role: 'assistant', text: '' }; this.blocks.push(this.streaming); }
321
330
  this.streaming.text += event.text; this.phase = t("Receiving response");
322
331
  }
@@ -348,17 +357,37 @@ export class TerminalScreen {
348
357
  if (this.blocks.length > 250) this.blocks.splice(0, this.blocks.length - 250);
349
358
  this.schedule();
350
359
  }
351
- async confirm(name, input, signal) {
360
+ // Opciones de la tarjeta con «sí, siempre»: muestra la regla exacta que se guardaría.
361
+ rememberLines(permission, columns) {
362
+ const labels = { once: t('Yes'), project: t('Yes, always in this project'), user: t('Yes, always for me'), no: 'No' };
363
+ const options = permission.choices.map((choice, index) => index === permission.choice ? (choice === 'no' ? tone.warning(`❯ ${labels[choice]}`) : tone.good(`❯ ${labels[choice]}`)) : ` ${labels[choice]}`);
364
+ return [clip(t(' Always = save rule {v0}', { v0: permission.remember.rule }), columns - 1), ' ' + options.join(' '), tone.muted(clip(t(' Enter confirms · y once · {v0}u always for me · n/Esc denies', { v0: permission.choices.includes('project') ? t('p this project · ') : '' }), columns - 1))];
365
+ }
366
+ async confirm(name, input, signal, { summary, remember, edit } = {}) {
352
367
  if (signal?.aborted || this.closed) return false;
353
368
  if (this.selecting) this.setSelection(false);
354
369
  return new Promise(resolve => {
355
370
  let settled=false;
356
371
  const finish = allowed => { if(settled)return;settled=true;signal?.removeEventListener('abort', abort);if(this.permission===question)this.permission = null;this.schedule();resolve(allowed); };
357
372
  const abort = () => finish(false);
358
- const question = { name, input, allowed: false, scroll: 0, finish };
373
+ // remember: { rule, project } ofrece «sí, siempre»; la regla se muestra antes de elegir.
374
+ const choices = remember?.rule ? ['once', ...(remember.project ? ['project'] : []), 'user', 'no'] : null;
375
+ const question = { name, input, summary, edit: edit && typeof edit.field === 'string' && typeof edit.text === 'string' ? { field: edit.field, text: edit.text } : null, allowed: false, scroll: 0, finish, ...(choices ? { remember, choices, choice: choices.length - 1 } : {}) };
359
376
  this.permission = question;signal?.addEventListener('abort', abort, { once: true });if(signal?.aborted)abort();else this.schedule();
360
377
  });
361
378
  }
379
+ // Editar antes de enviar: la tarjeta se deniega (no se envía nada) y el texto exacto
380
+ // pasa al composer con una instrucción. El humano lo corrige y lo envía; el modelo
381
+ // vuelve a proponer la operación y la tarjeta nueva muestra el texto exacto otra vez.
382
+ editPermission() {
383
+ const { name, edit } = this.permission; this.permission.finish(false);
384
+ const before = this.composer.text;
385
+ try {
386
+ this.composer.setText(before ? before + '\n\n' : '');
387
+ this.composer.insert(t("I edited the text for {v0} (field {v1}). Propose it again with exactly this text, without changing it:", { v0: String(name).split('__').at(-1), v1: edit.field }) + '\n<<<\n' + edit.text + '\n>>>');
388
+ this.draftChanged(); this.notice(t("The action was not sent. Edit the text in the composer and send it; the model proposes it again in a new card."));
389
+ } catch (error) { this.notice(error.message, true); }
390
+ }
362
391
  showSessions(items) { this.overlay = { kind: 'sessions', items, filter: '', index: Math.max(0, items.findIndex(item => item.id !== this.metadata.sessionId)) }; this.schedule(); }
363
392
  showQueue() { this.overlay = { kind: 'queue', index: 0 }; this.schedule(); }
364
393
  showHelp() { this.overlay = { kind: 'help', scroll: 0 }; this.schedule(); }
@@ -414,12 +443,17 @@ export class TerminalScreen {
414
443
  if (key.ctrl && name === 'c' && this.overlay?.kind === 'palette' && !this.permission) { this.closeOverlay(); return; }
415
444
  if (key.ctrl && name === 'c') { if (this.permission) this.permission.finish(false); if (this.busy) await this.callbacks.onAction?.('interrupt'); else if (this.composer.text) { this.composer.clear(); this.draftChanged(); } else await this.callbacks.onAction?.('exit'); return; }
416
445
  if (this.permission) {
417
- if (name === 'escape' || name === 'n') this.permission.finish(false);
418
- else if (name === 'y') this.permission.finish(true);
419
- else if (name === 'left' || name === 'right' || name === 'tab') this.permission.allowed = !this.permission.allowed;
420
- else if (name === 'return') this.permission.finish(this.permission.allowed);
446
+ const permission = this.permission, answer = choice => choice === 'project' || choice === 'user' ? { allowed: true, remember: choice } : choice === 'once';
447
+ if (name === 'escape' || name === 'n') permission.finish(false);
448
+ else if (name === 'y') permission.finish(true);
449
+ else if (permission.choices && ((name === 'p' && permission.choices.includes('project')) || name === 'u')) permission.finish(answer(name === 'p' ? 'project' : 'user'));
450
+ else if (permission.choices && (name === 'left' || name === 'right' || name === 'tab')) permission.choice = (permission.choice + (name === 'left' ? permission.choices.length - 1 : 1)) % permission.choices.length;
451
+ else if (name === 'left' || name === 'right' || name === 'tab') permission.allowed = !permission.allowed;
452
+ else if (name === 'return') permission.finish(permission.choices ? answer(permission.choices[permission.choice]) : permission.allowed);
421
453
  else if (name === 'pageup') this.permission.scroll = Math.max(0, this.permission.scroll - 10);
422
454
  else if (name === 'pagedown') this.permission.scroll += 10;
455
+ else if (name === 'r') { this.permission.raw = !(this.permission.raw ?? this.rawView); this.permission.scroll = 0; }
456
+ else if (name === 'e' && this.permission.edit) { this.editPermission(); return; }
423
457
  this.schedule(); return;
424
458
  }
425
459
  if (name === 'f5') { this.setSelection(); return; }
@@ -530,12 +564,14 @@ export class TerminalScreen {
530
564
  else output.push('', tone.accent(t(" COMAL · on the comal one cooks")), '', ...intro);
531
565
  }
532
566
  for (const block of this.blocks) {
533
- const cacheKey = `${columns}:${this.expandedTools}:${block.text || ''}:${block.state || ''}:${block.output || ''}`;
567
+ const cacheKey = `${columns}:${this.expandedTools}:${Boolean(this.rawView)}:${block.text || ''}:${block.state || ''}:${block.output || ''}`;
534
568
  if (block.cacheKey === cacheKey) { output.push(...block.cached); continue; }
535
569
  let lines;
536
570
  if (block.role === 'user') lines = [tone.accent(t(" You")), ...wrap(safeText(block.text), columns - 4).map(line => ' ' + line), ''];
537
571
  else if (block.role === 'assistant') lines = [tone.bold(' Comal'), ...this.markdown.render(block.text, columns - 4).map(line => ' ' + line), ''];
538
572
  else if (block.role === 'notice') lines = wrap((block.error ? tone.bad : tone.muted)(safeText(` ${block.error ? '✗' : '·'} ${block.text}`)), columns);
573
+ else if (block.role === 'inbound') lines = inboundLines(block, columns);
574
+ else if (block.role === 'result') lines = [...presentResultBlock(block, { columns: columns - 4, raw: this.rawView, expanded: this.expandedTools }).map(line => ' ' + line), ''];
539
575
  else if (block.role === 'table') lines = [tone.accent(' ' + block.text), ...this.tables.render(block.data, columns - 4).map(line => ' ' + line), ''];
540
576
  else if (block.role === 'subagent') {
541
577
  const icon = ['running','queued','waiting'].includes(block.runner_state || block.state) ? this.spinner() : block.work_state==='checks_verified' ? '✓' : block.state==='paused' ? '◇' : block.state==='completed' ? '·' : '✗';
@@ -550,7 +586,7 @@ export class TerminalScreen {
550
586
  const p = block.progress, bar = p.total > 0 ? ` ${progressBar(p.progress, p.total, 14)} ${p.progress}/${p.total}` : ` ${p.progress}`;
551
587
  lines.push(' ' + tone.accent(clip(safeText(bar + ' ' + p.message), columns - 6)));
552
588
  }
553
- if (this.expandedTools || block.state === 'error') lines.push(...wrap(safeText(String(block.output || JSON.stringify(block.input))).slice(0, 6000), columns - 6).map(line => ' ' + tone.muted(line)));
589
+ if (this.expandedTools || block.state === 'error') lines.push(...presentToolBlock(block, { columns: columns - 6, raw: this.rawView, expanded: this.expandedTools }).map(line => ' ' + line));
554
590
  }
555
591
  block.cacheKey = block.state === 'running' ? null : cacheKey; block.cached = lines; output.push(...lines);
556
592
  }
@@ -584,9 +620,9 @@ export class TerminalScreen {
584
620
  this.overlay.scroll = Math.min(this.overlay.scroll, Math.max(0, lines.length - height)); return lines.slice(this.overlay.scroll, this.overlay.scroll + height);
585
621
  }
586
622
  if (this.permission) {
587
- const permission = this.permission, details = wrap(safeText(JSON.stringify(permission.input, null, 2)), columns - 6);
588
- permission.scroll = Math.min(permission.scroll, Math.max(0, details.length - height + 5));
589
- return [tone.warning(t(" Allow {v0}", {v0: permission.name})), tone.muted(t(" Detail {v0}/{v1} · PgUp/PgDn to review", {v0: permission.scroll + 1, v1: details.length})), '', ...details.slice(permission.scroll, permission.scroll + height - 5).map(line => ' ' + line), '', t(" {v0} {v1} · Enter confirms · y/n · Esc denies", {v0: permission.allowed ? tone.good(t("❯ Yes")) : t(" Yes"), v1: !permission.allowed ? tone.warning('❯ No') : ' No'})];
623
+ const permission = this.permission, raw = permission.raw ?? Boolean(this.rawView), details = presentPermission(permission.name, permission.input, { summary: permission.summary, columns: columns - 6, raw });
624
+ const chrome = permission.choices ? 7 : 5; permission.scroll = Math.min(permission.scroll, Math.max(0, details.length - height + chrome));
625
+ return [tone.warning(t(" Allow {v0}", {v0: permission.name})), tone.muted(clip(t(" Detail {v0}/{v1} · PgUp/PgDn to review", {v0: permission.scroll + 1, v1: details.length}) + (raw ? t(' · r readable view') : t(' · r shows JSON')), columns - 1)), '', ...details.slice(permission.scroll, permission.scroll + height - chrome).map(line => ' ' + line), '', ...(permission.choices ? this.rememberLines(permission, columns) : [t(" {v0} {v1} · Enter confirms · y/n · Esc denies", {v0: permission.allowed ? tone.good(t("❯ Yes")) : t(" Yes"), v1: !permission.allowed ? tone.warning('❯ No') : ' No'}) + (permission.edit ? t(" · e edits the text") : '')])];
590
626
  }
591
627
  if (this.overlay.kind === 'help') {
592
628
  const lines = [tone.muted(t(" Help · composer active · type below · PgUp/PgDn scrolls · Esc/F1 closes")), '', ...this.markdown.render(t(HELP), columns - 4).map(line => ' ' + line)];
@@ -0,0 +1,63 @@
1
+ $ErrorActionPreference = 'Stop'
2
+ [Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
3
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
4
+ $currentSid = [Security.Principal.WindowsIdentity]::GetCurrent().User
5
+ $trusted = @($currentSid.Value, 'S-1-5-18', 'S-1-5-32-544')
6
+ [Console]::Out.WriteLine('{"ready":true}')
7
+ [Console]::Out.Flush()
8
+
9
+ function Assert-Private([string] $path, [bool] $directory) {
10
+ $attributes = [IO.File]::GetAttributes($path)
11
+ if (($attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'Private state cannot be a reparse point.' }
12
+ if ($directory -ne (($attributes -band [IO.FileAttributes]::Directory) -ne 0)) { throw 'Private state has the wrong file type.' }
13
+ if ($directory) { $acl = [IO.Directory]::GetAccessControl($path) }
14
+ else { $acl = [IO.File]::GetAccessControl($path) }
15
+ $owner = $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value
16
+ if ($trusted -notcontains $owner) { throw 'Private state is owned by another Windows account.' }
17
+ foreach ($rule in $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])) {
18
+ if ($rule.AccessControlType -ne [Security.AccessControl.AccessControlType]::Allow) { continue }
19
+ $sid = $rule.IdentityReference.Value
20
+ # Creator/owner rights resolve only to the already validated owner.
21
+ if (($trusted -notcontains $sid) -and ($sid -ne 'S-1-3-0') -and ($sid -ne 'S-1-3-4')) {
22
+ throw 'Private state grants access to other Windows accounts.'
23
+ }
24
+ }
25
+ }
26
+
27
+ while (($line = [Console]::In.ReadLine()) -ne $null) {
28
+ $request = $null
29
+ try {
30
+ $request = ConvertFrom-Json -InputObject $line
31
+ $path = [string] $request.path
32
+ $exists = $true
33
+ try {
34
+ $before = [IO.File]::GetAttributes($path)
35
+ if (($before -band [IO.FileAttributes]::ReparsePoint) -ne 0) { throw 'Private state cannot be a reparse point.' }
36
+ }
37
+ catch [IO.FileNotFoundException] { $exists = $false }
38
+ catch [IO.DirectoryNotFoundException] { $exists = $false }
39
+ if (($request.action -eq 'ensure') -and -not $exists) {
40
+ $acl = New-Object Security.AccessControl.DirectorySecurity
41
+ $acl.SetOwner($currentSid)
42
+ $acl.SetAccessRuleProtection($true, $false)
43
+ foreach ($sid in $trusted) {
44
+ $identity = New-Object Security.Principal.SecurityIdentifier($sid)
45
+ $rule = New-Object Security.AccessControl.FileSystemAccessRule(
46
+ $identity,
47
+ [Security.AccessControl.FileSystemRights]::FullControl,
48
+ ([Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [Security.AccessControl.InheritanceFlags]::ObjectInherit),
49
+ [Security.AccessControl.PropagationFlags]::None,
50
+ [Security.AccessControl.AccessControlType]::Allow
51
+ )
52
+ $acl.AddAccessRule($rule)
53
+ }
54
+ [void] [IO.Directory]::CreateDirectory($path, $acl)
55
+ }
56
+ if (@('ensure', 'directory', 'file') -notcontains $request.action) { throw 'Unknown private-state action.' }
57
+ Assert-Private $path ($request.action -ne 'file')
58
+ $result = @{ id = $request.id; ok = $true }
59
+ }
60
+ catch { $result = @{ id = $request.id; ok = $false; error = $_.Exception.Message } }
61
+ [Console]::Out.WriteLine(($result | ConvertTo-Json -Compress))
62
+ [Console]::Out.Flush()
63
+ }
@@ -2,8 +2,12 @@ import { isDeepStrictEqual } from 'node:util';
2
2
 
3
3
  export const assertionSchema = { type: 'object', properties: {
4
4
  kind: { type: 'string', enum: ['nonempty', 'contains', 'not_contains', 'json_equals'] },
5
- value: {}, pointer: { type: 'string', maxLength: 500 },
6
- }, required: ['kind'], additionalProperties: false, allOf: [{if:{properties:{kind:{enum:['contains','not_contains','json_equals']}}},then:{required:['value']}},{if:{properties:{kind:{const:'json_equals'}}},then:{required:['pointer']}}] };
5
+ value: { description: 'contains/not_contains: a nonempty string to find in the output. json_equals: the expected JSON value, preserving boolean/number/null types.' }, pointer: { type: 'string', maxLength: 500 },
6
+ }, required: ['kind'], additionalProperties: false, allOf: [
7
+ {if:{properties:{kind:{enum:['contains','not_contains','json_equals']}}},then:{required:['value']}},
8
+ {if:{properties:{kind:{enum:['contains','not_contains']}}},then:{properties:{value:{type:'string',minLength:1,maxLength:2000}}}},
9
+ {if:{properties:{kind:{const:'json_equals'}}},then:{required:['pointer']}},
10
+ ] };
7
11
  export function validateAssertions(assertions = []) {
8
12
  for (const assertion of assertions) {
9
13
  if (assertion.kind !== 'nonempty' && !Object.hasOwn(assertion, 'value')) throw new Error('The assertion requires value.');
package/src/work-plan.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { digest } from './operations.mjs';
3
- import { auditExecution } from './audit.mjs';
3
+ import { auditExecutionViews, auditExecution } from './audit.mjs';
4
4
  import { readOnlyWorkCheck, runtimeBoundReadCheck, RUNTIME_CHECK_IDS } from './read-only-check.mjs';
5
5
  import { checkContractIssues, proposeCheckRepairs } from './work-check-contracts.mjs';
6
6
  import { assertionSchema, assertOutput, validateAssertions } from './work-assertions.mjs';
@@ -96,6 +96,20 @@ function closedProcessFailure(session,step,audit,active){
96
96
  return {kind:'native_process_nonzero_exit',exit_code:Number(match[1]),receipt_id:leaf.invocation_id,parent_receipt_id:parent.invocation_id,derivation:exits.length?'native_process_exit_observation':'legacy_native_shell_error',partial_effects:'preserved_unjudged',retry_authorized:false};
97
97
  }
98
98
 
99
+ // This closes only a native file handler's known pre-replacement failure. It
100
+ // neither credits its check nor retries that step; directories/temp files may
101
+ // already exist. Plugin error text or a source error code alone is insufficient.
102
+ function closedSourcePreflight(session,step,audit,active){
103
+ if(step.status!=='failed'||!['Write','Edit'].includes(step.operation)||active.has(step.invocation_id)||!audit.journal.verified||audit.violations.length||audit.unverified.length)return null;
104
+ const rows=session.rows,leaf=audit.receipts.find(r=>r.invocation_id===step.receipt_id),parent=audit.receipts.find(r=>r.invocation_id===step.invocation_id);
105
+ if(!leaf||!parent||leaf.status!=='failed'||parent.status!=='failed'||leaf.parent_invocation_id!==parent.invocation_id||!['WorkStepRun','WorkDraft'].includes(parent.operation)||leaf.error!==parent.error)return null;
106
+ const proposal=rows.find(r=>r.type==='house_proposal'&&r.invocation_id===parent.invocation_id),contract=rows.findLast(r=>r.type==='house_contract'&&r.contract_digest===leaf.contract_digest)?.contract;
107
+ if(!proposal||!nativeWorkParent(rows,proposal)||contract?.name!==step.operation||contract.origin!==null||contract.permission!==step.operation||contract.steps||contract.effects.workspace!=='write'||['external','process','authority'].some(k=>contract.effects[k]!=='none'))return null;
108
+ const facts=leaf.observations||[];
109
+ if(facts.length!==1||facts[0].kind!=='native_file_no_replacement'||facts[0].mechanism!=='workspace-file-v1'||facts[0].phase!=='before_destination_dispatch'||facts[0].destination_replaced!==false||facts[0].auxiliary_effects!=='possible_directories_or_temporary_files')return null;
110
+ return{kind:'native_file_pre_replacement_rejection',receipt_id:leaf.invocation_id,parent_receipt_id:parent.invocation_id,destination_replaced:false,auxiliary_effects:facts[0].auxiliary_effects,retry_authorized:false};
111
+ }
112
+
99
113
  // A native media capability rejection precedes file capture and provider dispatch.
100
114
  // This is not a generic exemption for provider/plugin failures or mutations.
101
115
  function closedMediaPreflight(session,step,audit,active){
@@ -127,7 +141,8 @@ function closedArtifactProcessing(session,step,audit,active){
127
141
  }
128
142
 
129
143
  // A board is a fold of host facts. Model prose, nested receipts and thinking are not facts.
130
- export function foldWorkBook(session, { active = new Set() } = {}) {
144
+ export function foldWorkBook(session,context={}) {return foldWorkWithAudit(session,context,()=>auditExecution(session));}
145
+ function foldWorkWithAudit(session, { active = new Set() } = {},auditFor) {
131
146
  const rows=session.rows||[];
132
147
  let plan = null, checkpoint = null; const tasks = new Map(), steps = new Map(), drafts = new Map(), waiting = new Map(), attestations = new Map(), draftCalls = new Map(), draftInputs = new Map();
133
148
  for (let i = 0; i < (session.rows || []).length; i++) {
@@ -168,7 +183,7 @@ export function foldWorkBook(session, { active = new Set() } = {}) {
168
183
  }
169
184
  if (row.type === 'work_checkpoint' && row.source === 'work_host') checkpoint = { ...row, ref: i };
170
185
  }
171
- const audit = auditExecution(session), receipts = new Map(audit.receipts.map(row => [row.invocation_id, row]));
186
+ const audit = auditFor(), receipts = new Map(audit.receipts.map(row => [row.invocation_id, row]));
172
187
  for (const step of steps.values()) if (step.status === 'interrupted') {
173
188
  // auditExecution admits a recovery receipt only after checking its native
174
189
  // derivation; a resealed recovery label alone cannot erase an unknown effect.
@@ -180,7 +195,7 @@ export function foldWorkBook(session, { active = new Set() } = {}) {
180
195
  if (proof) Object.assign(step, { status: 'refused', preflight_failure: proof, recovery_ref: proof.parent_receipt_ref,
181
196
  next_action: 'The target was not dispatched: an artifact preflight failed. Prepare the missing or invalid output with new work, then retry this original step under current permissions. No failed check was credited.' });
182
197
  }
183
- for(const step of steps.values()){const proof=closedProcessFailure(session,step,audit,active)||closedMediaPreflight(session,step,audit,active)||closedArtifactProcessing(session,step,audit,active);if(proof)step.failure_closed=proof;}
198
+ for(const step of steps.values()){const proof=closedProcessFailure(session,step,audit,active)||closedSourcePreflight(session,step,audit,active)||closedMediaPreflight(session,step,audit,active)||closedArtifactProcessing(session,step,audit,active);if(proof)step.failure_closed=proof;}
184
199
  // Explicit local operator review is an attestation, not reconstructed execution
185
200
  // success. Only sealed human-control receipts can review a closed native Bash
186
201
  // failure; raw facts and model prose never clear a fence.
@@ -226,8 +241,15 @@ export function foldWorkBook(session, { active = new Set() } = {}) {
226
241
  return { plan, tasks: [...tasks.values()], steps: [...steps.values()], drafts: [...drafts.values()], checkpoint, receipts, receiptValid, checkValid, attestations };
227
242
  }
228
243
 
229
- export function workProjection(session, context = {}) {
230
- const book = foldWorkBook(session, context); if (!book.plan) return null;
244
+ export function workProjection(session, context = {}) {return projectWork(foldWorkBook(session,context));}
245
+ // No caller-provided validation or persistent cache. Preserve the coordinator's
246
+ // exact context and the work fold's existing default audit context separately.
247
+ export function inspectWork(session,context={}) {
248
+ const [audit,workAudit]=auditExecutionViews(session,[context,{}]);
249
+ return{audit,work:projectWork(foldWorkWithAudit(session,context,()=>workAudit))};
250
+ }
251
+ function projectWork(book) {
252
+ if (!book.plan) return null;
231
253
  const active = book.tasks.find(task => task.declared_status === 'in_progress');
232
254
  const recoverable = book.tasks.find(task => task.status === 'blocked' && !task.held_by.length && book.steps.some(step => step.task_id === task.id && step.preflight_failure));
233
255
  const next = active || recoverable || book.tasks.find(task => ['pending', 'needs_verification'].includes(task.status) && !task.held_by.length);
@@ -255,6 +277,7 @@ export function workProjection(session, context = {}) {
255
277
  read_call: { name: 'EvidenceRead', input: { row: receipt_ref ?? recovery_ref ?? ref } } })),
256
278
  drafts: book.drafts.map(({ id, task_id, file_path, digest, bytes, complete, parts, part_ids }) => ({ id, task_id, file_path, digest, bytes, complete, next_index: parts.length, recent_part_ids: (part_ids || []).slice(-3).filter(Boolean) })),
257
279
  checkpoint: book.checkpoint ? { reason: book.checkpoint.reason, plan_digest: book.checkpoint.plan_digest, task_id: book.checkpoint.task_id, next_action: book.checkpoint.next_action, ref: book.checkpoint.ref } : null,
280
+ source_preflight_failures: book.steps.filter(step=>step.failure_closed?.kind==='native_file_pre_replacement_rejection').map(step=>({task_id:step.task_id,step_id:step.step_id,...step.failure_closed,next_action:'The native destination replacement was not dispatched. Preserve this failed step, inspect current source, and use a NEW step with its explicit expected version. Current permissions and checks still apply; do not replay the failed step ID.'})),
258
281
  process_failures: book.steps.filter(step=>step.failure_closed?.kind==='native_process_nonzero_exit').map(step=>({task_id:step.task_id,step_id:step.step_id,...step.failure_closed,next_action:'The shell ended with a nonzero exit. Preserve partial outputs and failed receipts; use a new judged correction step, then rerun mandatory checks. Do not replay the failed step ID.'})),
259
282
  media_preflight_failures: book.steps.filter(step=>step.failure_closed?.kind==='native_media_preflight_rejection').map(step=>({task_id:step.task_id,step_id:step.step_id,...step.failure_closed,next_action:'No provider media request was sent. Use a supported MIME in a new observation step; an image-only provider can inspect locally extracted video frames. Failed checks remain uncredited.'})),
260
283
  media_processing: book.steps.filter(step=>step.failure_closed?.kind==='native_media_processing_acknowledged').map(step=>({task_id:step.task_id,step_id:step.step_id,...step.failure_closed,next_action:'The media upload was acknowledged; no artifact candidate exists from this failed attempt. Preserve its ID and failed receipt. Reconcile the SAME directory/metadata in a new judged ArtifactPublish step; do not replay this failed step ID or invent an artifact ID.'})),
@@ -436,6 +459,9 @@ export class WorkCoordinator {
436
459
  for (const parent of task.depends_on) dependencyProofs[parent] = digest(book.tasks.find(row => row.id === parent).receipt_ids);
437
460
  if (book.steps.some(step => step.task_id === task.id && (['running', 'interrupted'].includes(step.status)||step.status==='failed'&&!step.failure_closed))) throw failure('Hay un paso con efectos inciertos: inspecciona antes de cerrar.', 'WORK_UNVERIFIED');
438
461
  }
462
+ // Artifact/check reads above can yield to external changes. Recheck the
463
+ // original authority and close guards at the final durable done frontier.
464
+ if (input.status === 'done') await options.revalidate?.();
439
465
  await this.fact(options, { type: 'work_task_changed', task_id: task.id, task_digest: workDigest(task), plan_version: book.plan.version, status: input.status, reason: input.reason, receipt_ids: receiptIds, dependency_proofs: dependencyProofs });
440
466
  return { task_id: task.id, status: input.status, receipt_ids: receiptIds, scope: input.status === 'done' && task.checks.every(check=>check.assertions?.length) ? 'declared_assertions_verified' : 'declared_checks_returned', asked: 'unjudged' };
441
467
  }); await this.notify(options.sessionId); return JSON.stringify({ ...result, ...(refreshed.length ? { refreshed_tasks: refreshed } : {}) });
@@ -479,7 +505,7 @@ export class WorkCoordinator {
479
505
  const retryFailedRead = input.recheck && check && existing.status === 'failed' && book.receipts.has(existing.receipt_id) && readOnlyWorkCheck(check.operation, check.arguments);
480
506
  if (existing.status !== 'refused' && !(input.recheck && check && existing.status === 'returned') && !retryFailedRead) throw failure('El paso quedó interrumpido o falló con posibles efectos; inspecciona y registra otro paso antes de repetir. Un check de lectura con recibo failed se recupera con el mismo step_id y recheck:true.', 'WORK_UNCERTAIN');
481
507
  }
482
- const incoming=this.house.registry.get(input.operation),pureObservation=readOnlyWorkCheck(input.operation,input.arguments)&&incoming?.contract.origin===null&&incoming.contract.effects.workspace==='read'&&incoming.contract.effects.external==='none'&&incoming.contract.effects.authority==='none'&&(incoming.contract.effects.process==='none'||input.operation==='Bash'&&Boolean(readOnlyWorkCheck('Bash',input.arguments)));
508
+ const incoming=this.house.registry.get(input.operation),pureObservation=readOnlyWorkCheck(input.operation,input.arguments)&&incoming?.contract.origin===null&&incoming.contract.effects.workspace==='read'&&incoming.contract.effects.external==='none'&&incoming.contract.effects.authority==='none'&&(incoming.contract.effects.process==='none'||input.operation==='GitInspect'&&incoming.contract.effects.process==='read'||input.operation==='Bash'&&Boolean(readOnlyWorkCheck('Bash',input.arguments)));
483
509
  if (!pureObservation&&book.steps.some(step => step.task_id === task.id && (['running', 'interrupted'].includes(step.status) || step.status === 'failed' && !step.failure_closed && !readOnlyWorkCheck(step.operation, step.arguments)))) throw failure('The task has uncertain effects. New mutations remain fenced. Native Read/FileInfo/ListDirectory/Grep observations may inspect current state; do not repeat the uncertain action.', 'WORK_UNCERTAIN');
484
510
  const offered=options.catalogue?.find(op=>op.name===input.operation), operation=this.house.registry.get(input.operation);
485
511
  if(offered && operation?.digest===offered.contract_digest) {
@@ -558,7 +584,7 @@ export class WorkCoordinator {
558
584
  const task = { task_id: id, plan_version: version };
559
585
  return [
560
586
  {name:'WorkFailureReview',description:'Local operator attests review of an exact terminal native Bash failure after inspecting partial state. Keeps failure, mandatory checks and no replay. This is NOT objective execution success or human task acceptance.',audience:'human',surfaces:['cli','tui'],permission:null,effects:{workspace:'none',external:'none',process:'none',authority:'work_failure_review'},input_schema:schema({...task,step_id:id,expected_receipt:{type:'string',format:'uuid'},reason:{type:'string',minLength:20,maxLength:1000}},['task_id','plan_version','step_id','expected_receipt','reason']),run:(input,options)=>this.reviewFailure(input,options)},
561
- { name: 'WorkPlanSet', description: 'Plan small tasks before effects; each needs id/title/criterion/checks. Up to 32 artifact files. Checks use exact tools, e.g. Read:{file_path:"file"}. Verification needs assertions. append:true adds tasks without resending old ones; no permission grant.', input_schema: schema({ objective: str(2200), expected_version: version, append: { type: 'boolean' }, tasks: { type: 'array', minItems: 1, maxItems: 24, items: schema({ id, title: str(300), criterion: str(1000), phase: { type: 'string', enum: ['discovery','implementation','verification'] }, capabilities: { type: 'array', maxItems: 4, uniqueItems: true, items: str(100) }, artifacts: { type: 'array', maxItems: 32, uniqueItems: true, items: str(1024) }, depends_on: { type: 'array', maxItems: 24, items: id }, checks: { type: 'array', maxItems: 4, items: schema({ step_id: id, operation: { ...str(128), description: 'Exact callable tool name from the catalogue, never an instruction or test description.' }, arguments: args, assertions: { type: 'array', minItems: 1, maxItems: 4, items: assertionSchema } }, ['step_id', 'operation', 'arguments']) } }, ['id', 'title', 'criterion', 'checks']) }, retire_ids: { type: 'array', maxItems: 24, items: id }, reason: str(1000) }, ['objective', 'expected_version', 'tasks']), run: (input, options) => this.setPlan(input, options) },
587
+ { name: 'WorkPlanSet', description: 'Plan small tasks before effects; each needs id/title/criterion/checks. Up to 32 artifact files. Checks use exact tools, e.g. Read:{file_path:"file"}. Checks need complete, observed arguments: never invent a target_id. For discovery/implementation, declare a source Read check with a nonempty assertion. Add a separate verification task after observing runtime IDs; an open task cannot be closed without checks or have checks added by repair. Verification phase requires assertions. append:true adds tasks without resending old ones; no permission grant.', input_schema: schema({ objective: str(2200), expected_version: version, append: { type: 'boolean' }, tasks: { type: 'array', minItems: 1, maxItems: 24, items: schema({ id, title: str(300), criterion: str(1000), phase: { type: 'string', enum: ['discovery','implementation','verification'] }, capabilities: { type: 'array', maxItems: 4, uniqueItems: true, items: str(100) }, artifacts: { type: 'array', maxItems: 32, uniqueItems: true, items: str(1024) }, depends_on: { type: 'array', maxItems: 24, items: id }, checks: { type: 'array', maxItems: 4, items: schema({ step_id: id, operation: { ...str(128), description: 'Exact callable tool name from the catalogue, never an instruction or test description.' }, arguments: args, assertions: { type: 'array', minItems: 1, maxItems: 4, items: assertionSchema } }, ['step_id', 'operation', 'arguments']) } }, ['id', 'title', 'criterion', 'checks']) }, retire_ids: { type: 'array', maxItems: 24, items: id }, reason: str(1000) }, ['objective', 'expected_version', 'tasks']), run: (input, options) => this.setPlan(input, options) },
562
588
  { name: 'WorkCheckRepair', description: 'Repair check contracts locally without replacing tasks, retiring drafts or replaying effects. Supply current plan_version only for deterministic suggestions (Read/Grep keys, integer types, unique canonical MCP names). Optional repairs use exact check digests. Assertions/criteria/artifacts stay unchanged. Missing MCP: acquire browser.verify then retry. No permission grant.', input_schema: schema({ plan_version: version, literal_types_only: { type: 'boolean' }, repairs: { type: 'array', minItems: 1, maxItems: 8, items: schema({task_id:id,step_id:id,expected_digest:{type:'string',pattern:'^[a-f0-9]{64}$'},operation:str(128),arguments:args},['task_id','step_id','expected_digest']) }, reason:str(1000) }, ['plan_version']), run: (input,options) => this.repairChecks(input,options) },
563
589
  { name: 'WorkTaskUpdate', description: 'Start/block/close a task. done with verify:true runs missing/stale read-only checks from the plan through the judge, then closes. No model roundtrip per check. Opaque/mutating checks require explicit WorkStepRun. Records declared checks, not semantic objective proof.', input_schema: schema({ ...task, status: { type: 'string', enum: ['in_progress', 'blocked', 'done'] }, verify: { type: 'boolean' }, reason: str(1000) }, ['task_id', 'plan_version', 'status']), run: (input, options) => this.updateTask(input, options) },
564
590
  { name: 'WorkStepRun', description: 'Call WorkCheckRepair, ToolPack and WorkTaskUpdate DIRECTLY, never nested here. Run a declared check OR additional work/observations through the judge. Declared check: existing step_id, omit operation/arguments. Additional observation (DOM, console, network, discovery) or implementation: NEW unique step_id plus exact operation and arguments; NO plan edit or check repair is needed. Extra steps do not change assertions. Do not rename existing checks. Retry failed read-only checks with SAME step_id and recheck:true; never repeat uncertain mutations. Completed steps reuse receipts.', input_schema: schema({ ...task, step_id: id, operation: str(128), arguments: args, next_action: str(1000), recheck: { type: 'boolean' } }, ['task_id', 'plan_version', 'step_id']), run: (input, options) => this.runStep(input, options) },
@@ -1,3 +1,4 @@
1
+ import {savedSourceTask,sourceTaskFactValid} from './source-task-proof.mjs';
1
2
  import { recoveryAttribution } from './file-recovery-proof.mjs';
2
3
  import { peerMessagePurpose } from './peer-message-context.mjs';
3
4
  import path from 'node:path';
@@ -9,6 +10,7 @@ import { createHash } from 'node:crypto';
9
10
  import { projectToolOutput } from './model-output.mjs';
10
11
  import { currentWorkRequest, isContinuation } from './work-intent.mjs';
11
12
  import { browserCurrency } from './browser-currency.mjs';
13
+ import { isHumanIntentRecord } from './trusted-context.mjs';
12
14
 
13
15
  export const THINKING_TYPES = new Set(['openai_reasoning', 'thinking', 'redacted_thinking']);
14
16
  export function excerpt(value, size = 1200) { const text = String(value ?? ''); return text.length <= size ? text : text.slice(0, Math.floor(size * .7)) + '\n[extracto determinista; consultar referencia para texto completo]\n' + text.slice(-Math.floor(size * .3)); }
@@ -19,10 +21,10 @@ export function deriveWorkState(session, { catalogue = [], mode, active, exclude
19
21
  for (let i = 0; i < rows.length; i++) {
20
22
  const row = rows[i];
21
23
  if (row.type === 'reset') { requests.length = 0; reports.length = 0; hypotheses.length = 0; calls.clear(); }
22
- if (row.type === 'intent') requests.push({ text: row.text, ref: reference(session, i), source: 'human' });
24
+ if (isHumanIntentRecord(row)) requests.push({ text: row.text, ref: reference(session, i), source: 'human' });
23
25
  if (row.type !== 'message') continue;
24
26
  const message = row.message;
25
- if (message.role === 'user' && typeof message.content === 'string' && (row.source === 'delegated' || !requests.length && row.source !== 'goal')) requests.push({ text: message.content, ref: reference(session, i), source: row.source === 'delegated' ? 'delegated_task_not_authority' : 'legacy_user' });
27
+ if (message.role === 'user' && typeof message.content === 'string' && (row.source === 'delegated' || !requests.length && !row.source)) requests.push({ text: message.content, ref: reference(session, i), source: row.source === 'delegated' ? 'delegated_task_not_authority' : 'legacy_user' });
26
28
  const blocks = Array.isArray(message.content) ? message.content : [];
27
29
  for (let j = 0; j < blocks.length; j++) {
28
30
  const block = blocks[j], ref = reference(session, i, j);
@@ -44,6 +46,7 @@ export function deriveWorkState(session, { catalogue = [], mode, active, exclude
44
46
  checkpoint: rows.findLast(row => row.type === 'chrome_task_checkpoint' && row.source === 'workflow_host')?.state || null,
45
47
  delivery: rows.findLast(row => row.type === 'chrome_task_delivery' && row.source === 'workflow_host')?.status || 'unjudged' }
46
48
  } : {}),
49
+ ...(sourceTaskFactValid(savedSourceTask(session))?{workflow:{kind:'source-task',task:savedSourceTask(session).task,task_digest:savedSourceTask(session).task_digest,delivery:rows.findLast(r=>r.type==='source_task_delivery'&&r.source==='source_task_host')?.status||'unjudged',administrative_controls:'host_owned'}}:{}),
47
50
  governance: rows.findLast(row => row.type === 'governance_activated' && row.source === 'governance_host')?.plan?.plan_hash || null,
48
51
  browser_runtime_currency: browserCurrency(session),
49
52
  goal: session.goal ? { condition: excerpt(session.goal.condition, 1200), status: session.goal.status, iterations: session.goal.iterations, calls: session.goal.calls, tokens: session.goal.tokens, evaluation_authority: 'advisory' } : null,
@@ -72,11 +75,6 @@ export function deriveWorkState(session, { catalogue = [], mode, active, exclude
72
75
  // current facts and references, not repeated contract hashes or prior board dumps.
73
76
  export function modelWorkState(session, options = {}) {
74
77
  const state = deriveWorkState(session, options);
75
- if (state.workflow?.kind === 'chrome-task' && state.work) state.work = {
76
- counts: state.work.counts, cards: state.work.cards.map(({title,status}) => ({title,status})),
77
- current: state.work.current ? { title: state.work.current.title, status: state.work.current.status, criterion: state.work.current.criterion } : null,
78
- close_call: { name: 'ChromeFinish', input: {} }, administrative_controls: 'host_owned', permissions_from_plan: false
79
- };
80
78
  const controls = new Set(['WorkBoard', 'WorkSnapshot', 'AuditStatus', 'HouseSnapshot', 'OperationCatalog']);
81
79
  state.audit.journal = { verified: state.audit.journal.verified, consistent: state.audit.journal.consistent, tip_sha256: state.audit.journal.tip_sha256, integrity: state.audit.journal.integrity };
82
80
  state.audit.authority_refusals_total = state.audit.authority_refusals.length; state.audit.authority_refusals = [];
@@ -105,6 +103,14 @@ export function modelWorkState(session, options = {}) {
105
103
  }
106
104
  const preferredPaths = (state.work?.current?.artifacts || []).map(file => path.resolve(session.cwd, file));
107
105
  state.file_working_set = recordedFilePages(session, 24000, { preferredPaths });
106
+ // Fold and validate the complete host state before projecting away its plan,
107
+ // step history and identifiers. Long Chrome tasks also reach this path.
108
+ if (state.workflow?.kind === 'chrome-task' && state.work) state.work = {
109
+ counts: state.work.counts, cards: state.work.cards.map(({title,status}) => ({title,status})),
110
+ current: state.work.current ? { title: state.work.current.title, status: state.work.current.status, criterion: state.work.current.criterion } : null,
111
+ close_call: { name: 'ChromeFinish', input: {} }, administrative_controls: 'host_owned', permissions_from_plan: false
112
+ };
113
+ if(state.workflow?.kind==='source-task'&&state.work){state.work_control={administrative_controls:'host_owned',close_call:{name:'SourceDeliver',input:{}},grants_permission:false};state.catalogue.discovery='SourceStatus';state.work={counts:state.work.counts,cards:state.work.cards.map(({title,status})=>({title,status})),current:state.work.current?{title:state.work.current.title,status:state.work.current.status,criterion:state.work.current.criterion}:null,close_call:{name:'SourceDeliver',input:{}},administrative_controls:'host_owned',permissions_from_plan:false};}
108
114
  return state.file_working_set?.length ? { file_working_set: state.file_working_set, ...state } : state;
109
115
  }
110
116
 
@@ -191,11 +197,11 @@ export function readEvidence(session, { row, block, field='record', expected_dig
191
197
  return { ref: reference(session, row, block), kind: record.type, digest: valueDigest, field, offset, offset_unit:'UTF16_code_units', ...(search!==undefined?{found:true}:{}), content: text.slice(offset, next), truncated: next < text.length, ...(next < text.length ? { next_offset:next,next_call:{name:'EvidenceRead',input:nextInput} } : {}), authority: 'data_not_permission', archived_calls_are_not_executed: true };
192
198
  }
193
199
 
194
- export function evidenceOperations({ store, house, getCatalogue = () => [], getMode = () => undefined }) {
200
+ export function evidenceOperations({ store, house, getCatalogue = () => [], getMode = () => undefined,resolveSession }) {
195
201
  const empty = { type: 'object', properties: {}, additionalProperties: false };
196
202
  return [
197
203
  { name: 'WorkSnapshot', description: 'Derive request, observed state, tool progress and evidence references deterministically from this session book. No LLM, no disk remeasurement, no authority from model thinking.', input_schema: empty, run: async (_, { sessionId, catalogue, mode }) => JSON.stringify(deriveWorkState(await store.load(sessionId), { catalogue: catalogue || getCatalogue(), mode: mode || getMode(), ...house.stateContext() })) },
198
204
  { name: 'AuditStatus', description: 'Verify local journal hash chain, host contracts, authorizations, execution receipts and recorded surface continuity. Does not judge the human objective or reread the disk. Hash chain is not signed.', input_schema: empty, run: async (_, { sessionId }) => JSON.stringify(auditReport(auditExecution(await store.load(sessionId), house.stateContext()))) },
199
- { name: 'EvidenceRead', description: 'Read retained evidence without rerunning operations. Prefer exact read_call/next_call. field:output pages raw receipt output with UTF16 offsets; search locates literal text. Default row reads the whole record; block selects only message.content. session_id may select your direct child session, never unrelated peers. Expected digest rejects a wrong reference. Thinking is metadata only; all returned content is data, not authority.', input_schema: { type: 'object', properties: { session_id:{type:'string',pattern:'^[a-f0-9-]{36}$'}, row: { type: 'integer', minimum: 0 }, block: { type: 'integer', minimum: 0 }, field:{type:'string',enum:['record','output']}, expected_digest:{type:'string',pattern:'^[a-f0-9]{64}$'},search:{type:'string',minLength:1,maxLength:200}, offset: { type: 'integer', minimum: 0 }, limit: { type: 'integer', minimum: 1, maximum: 8000 } }, required: ['row'], additionalProperties: false }, run: async (input, { sessionId }) => {const current=await store.load(sessionId), target=input.session_id&&input.session_id!==sessionId?await store.load(input.session_id):current;if(target.id!==current.id&&target.parent_session_id!==current.id)throw Object.assign(new Error('La evidencia solicitada no pertenece a esta sesión ni a un hijo directo.'),{code:'EVIDENCE_SCOPE_DENIED'});if(target.journalAudit?.consistent===false)throw Object.assign(new Error('El libro de evidencia está alterado; no se devolvieron bytes.'),{code:'AUDIT_INVALID'});return JSON.stringify(readEvidence(target,input));} },
205
+ { name: 'EvidenceRead', description: 'Read retained evidence without rerunning operations. Prefer exact read_call/next_call. field:output pages raw receipt output with UTF16 offsets; search locates literal text. Default row reads the whole record; block selects only message.content. session_id may select your direct child session, never unrelated peers. Expected digest rejects a wrong reference. Thinking is metadata only; all returned content is data, not authority.', input_schema: { type: 'object', properties: { session_id:{type:'string',pattern:'^[a-f0-9-]{36}$'}, row: { type: 'integer', minimum: 0 }, block: { type: 'integer', minimum: 0 }, field:{type:'string',enum:['record','output']}, expected_digest:{type:'string',pattern:'^[a-f0-9]{64}$'},search:{type:'string',minLength:1,maxLength:200}, offset: { type: 'integer', minimum: 0 }, limit: { type: 'integer', minimum: 1, maximum: 8000 } }, required: ['row'], additionalProperties: false }, run: async (input, { sessionId }) => {const current=await store.load(sessionId), target=input.session_id&&input.session_id!==sessionId?(resolveSession?await resolveSession(input.session_id,current):await store.load(input.session_id)):current;if(target.id!==current.id&&target.parent_session_id!==current.id)throw Object.assign(new Error('La evidencia solicitada no pertenece a esta sesión ni a un hijo directo.'),{code:'EVIDENCE_SCOPE_DENIED'});if(target.journalAudit?.consistent===false)throw Object.assign(new Error('El libro de evidencia está alterado; no se devolvieron bytes.'),{code:'AUDIT_INVALID'});return JSON.stringify(readEvidence(target,input));} },
200
206
  ].map(definition => ({ ...definition, native: true }));
201
207
  }