@milpa/comal 0.2.1 → 0.2.3

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 (121) hide show
  1. package/README.md +1 -1
  2. package/backend/contracts/remote-commands-v1.json +0 -12
  3. package/backend/contracts/remote-relay-answers-v1.json +209 -0
  4. package/package.json +3 -1
  5. package/skills/mv3/SKILL.md +44 -0
  6. package/skills/mv3/skill.json +4 -0
  7. package/src/agent.mjs +299 -32
  8. package/src/auto-guard.mjs +40 -3
  9. package/src/autocompact.mjs +233 -0
  10. package/src/bash-timeouts.mjs +29 -0
  11. package/src/child-env.mjs +23 -0
  12. package/src/cli.mjs +232 -63
  13. package/src/code-run-artifact.mjs +30 -0
  14. package/src/code-run-journal.mjs +135 -0
  15. package/src/code-run-session-proof.mjs +50 -0
  16. package/src/code-run-session.mjs +97 -0
  17. package/src/code-run-worker.mjs +20 -4
  18. package/src/code-run.mjs +49 -14
  19. package/src/codex-provider.mjs +4 -4
  20. package/src/compaction-notice.mjs +22 -0
  21. package/src/compaction.mjs +162 -23
  22. package/src/connection-settings.mjs +36 -4
  23. package/src/context-pressure.mjs +5 -4
  24. package/src/context.mjs +27 -22
  25. package/src/execution-metrics.mjs +36 -5
  26. package/src/file-recovery-proof.mjs +9 -1
  27. package/src/file-recovery.mjs +36 -3
  28. package/src/gemini-provider.mjs +5 -3
  29. package/src/git-integration-operations.mjs +2 -1
  30. package/src/git-plumbing.mjs +2 -1
  31. package/src/git-source.mjs +2 -1
  32. package/src/git-workspaces.mjs +2 -1
  33. package/src/house.mjs +13 -1
  34. package/src/i18n/cli-es.mjs +12 -1
  35. package/src/i18n/es.mjs +133 -2
  36. package/src/judges.mjs +2 -1
  37. package/src/mcp.mjs +2 -1
  38. package/src/model-controls.mjs +16 -2
  39. package/src/model-deadline.mjs +2 -2
  40. package/src/openai-provider.mjs +8 -4
  41. package/src/operations.mjs +17 -2
  42. package/src/package-identity.mjs +68 -0
  43. package/src/peer-probe.mjs +16 -0
  44. package/src/permissions.mjs +99 -6
  45. package/src/plugin-catalog.mjs +19 -4
  46. package/src/plugin-config.mjs +18 -1
  47. package/src/plugin-controls.mjs +31 -8
  48. package/src/plugin-trust.mjs +15 -6
  49. package/src/plugins/chrome-lab/bridge.mjs +19 -6
  50. package/src/plugins/chrome-lab/client.mjs +242 -12
  51. package/src/plugins/chrome-lab/index.mjs +106 -24
  52. package/src/plugins/chrome-lab/legacy-fill.mjs +25 -0
  53. package/src/plugins/chrome-lab/native-reply.mjs +31 -0
  54. package/src/plugins/omarchy/catalog.mjs +96 -0
  55. package/src/plugins/omarchy/environment.mjs +78 -0
  56. package/src/plugins/omarchy/guide.mjs +54 -0
  57. package/src/plugins/omarchy/index.mjs +213 -0
  58. package/src/plugins/omarchy/runner.mjs +62 -0
  59. package/src/plugins/omarchy/text.mjs +26 -0
  60. package/src/plugins/omarchy/tiers.mjs +157 -0
  61. package/src/plugins/open-relay/actions.mjs +212 -16
  62. package/src/plugins/open-relay/client.mjs +93 -35
  63. package/src/plugins/open-relay/decision.mjs +89 -0
  64. package/src/plugins/open-relay/dossier.mjs +23 -5
  65. package/src/plugins/open-relay/index.mjs +68 -28
  66. package/src/plugins/open-relay/lookup.mjs +41 -4
  67. package/src/plugins/open-relay/notices.mjs +48 -4
  68. package/src/plugins/open-relay/panel.mjs +59 -0
  69. package/src/plugins/open-relay/watch.mjs +198 -85
  70. package/src/plugins.mjs +12 -1
  71. package/src/process-lock.mjs +20 -12
  72. package/src/provider-auth.mjs +6 -3
  73. package/src/provider-profiles.mjs +13 -3
  74. package/src/provider.mjs +175 -30
  75. package/src/read-only-check.mjs +12 -4
  76. package/src/relay/broker.mjs +160 -0
  77. package/src/relay/cards.mjs +702 -0
  78. package/src/relay/flow.mjs +886 -0
  79. package/src/relay/follow.mjs +119 -0
  80. package/src/relay/git.mjs +23 -0
  81. package/src/relay/hub-client.mjs +163 -0
  82. package/src/relay/state.mjs +55 -0
  83. package/src/remote-control.mjs +115 -12
  84. package/src/remote-relay.mjs +468 -0
  85. package/src/session-bus.mjs +13 -3
  86. package/src/session-provider-settings.mjs +2 -2
  87. package/src/skill-runtime.mjs +8 -6
  88. package/src/source-ownership.mjs +3 -1
  89. package/src/swarm-view.mjs +53 -0
  90. package/src/tool-manifest.mjs +71 -0
  91. package/src/tool-packs.mjs +22 -6
  92. package/src/tool-schema.mjs +14 -2
  93. package/src/tools.mjs +96 -27
  94. package/src/tui/connection-transition.mjs +10 -0
  95. package/src/tui/controller.mjs +35 -9
  96. package/src/tui/discovery.mjs +13 -8
  97. package/src/tui/inbound-blocks.mjs +5 -1
  98. package/src/tui/screen.mjs +78 -15
  99. package/src/tui/swarm-panel.mjs +217 -0
  100. package/src/turn-timing.mjs +82 -0
  101. package/src/usage-accounting.mjs +12 -1
  102. package/src/vision.mjs +6 -1
  103. package/src/visual/chrome.mjs +379 -0
  104. package/src/visual/fingerprint.mjs +30 -0
  105. package/src/visual/fixtures.mjs +201 -0
  106. package/src/visual/gate.mjs +92 -0
  107. package/src/visual/judge.mjs +114 -0
  108. package/src/visual/ledger.mjs +160 -0
  109. package/src/visual/measure.mjs +145 -0
  110. package/src/visual/names.mjs +7 -0
  111. package/src/visual/operations.mjs +211 -0
  112. package/src/visual/pixel-diff.mjs +96 -0
  113. package/src/visual/png.mjs +103 -0
  114. package/src/visual/registry.mjs +265 -0
  115. package/src/visual/report.mjs +64 -0
  116. package/src/visual/verify.mjs +292 -0
  117. package/src/work-assertions.mjs +15 -4
  118. package/src/work-plan.mjs +166 -30
  119. package/src/work-state.mjs +4 -4
  120. package/src/workflows/chrome-task-adoption.mjs +7 -3
  121. package/src/workspace-serve.mjs +59 -0
@@ -11,6 +11,10 @@ import { chromeRecoveryMessage,chromeObservationMessage } from './chrome-recover
11
11
  import { nativeDelegation, agentRecoveryPlan } from './agent-recovery-proof.mjs';
12
12
  import { nativeChromeCandidate, chromeCandidateRecoveryPlan } from './chrome-candidate-recovery-proof.mjs';
13
13
 
14
+ // Violations that void recovery. A file changed outside the agent shows as state_chain_broken:<file>:
15
+ // an audit fact about disk drift, not a damaged or forged record, so it never blocks recovery.
16
+ export const recoveryBlockers = violations => violations.filter(v => !String(v.reason).startsWith('state_chain_broken:'));
17
+
14
18
  function pendingFromDeadProcess(session,actor,active=new Set()){
15
19
  const closed=new Set(session.rows.filter(r=>r.type==='house_receipt').map(r=>r.invocation_id)), starts=new Set(session.rows.filter(r=>r.type==='house_started').map(r=>r.invocation_id)), owners=new Map();
16
20
  return session.rows.flatMap((p,index)=>{
@@ -20,16 +24,38 @@ function pendingFromDeadProcess(session,actor,active=new Set()){
20
24
  return owners.get(key)?[{p,index}]:[];
21
25
  });
22
26
  }
27
+ // A person may acknowledge a call whose recovery the house already blocked as uncertain (the process
28
+ // died mid-call and no adapter can observe its effects). The acknowledgement is a new human fact: the
29
+ // call is closed as "effects unknown", never replayed, history is not edited and no permission is granted.
30
+ export function recoveryAcknowledgeOperation({store}){
31
+ return {name:'RecoveryAcknowledge',description:'Human acknowledgement that an interrupted call recovery blocked as uncertain has unknown effects, after inspecting the workspace. The call is closed as unknown effects on the next resume; it is never replayed and grants no permission.',
32
+ audience:'human',surfaces:['cli','tui'],permission:null,effects:{workspace:'none',external:'none',process:'none',authority:'recovery_acknowledgement'},
33
+ input_schema:{type:'object',properties:{invocation_id:{type:'string',format:'uuid'},reason:{type:'string',minLength:20,maxLength:1000}},required:['invocation_id','reason'],additionalProperties:false},
34
+ run:async(input,options)=>{
35
+ if(options.actor?.kind!=='human')throw Object.assign(new Error('Sólo una persona puede reconocer una recuperación incierta; usa /recover acknowledge.'),{code:'HOUSE_REFUSED'});
36
+ if(typeof input.reason!=='string'||input.reason.trim().length<20)throw Object.assign(new Error('Escribe qué revisaste (al menos 20 caracteres).'),{code:'RECOVERY_ACK_INVALID'});
37
+ const session=await store.load(options.sessionId), rows=session.rows;
38
+ const proposal=rows.find(r=>r.type==='house_proposal'&&r.invocation_id===input.invocation_id);
39
+ if(!proposal||!rows.some(r=>r.type==='process_recovery_blocked'&&r.invocation_id===input.invocation_id))throw Object.assign(new Error(`No hay una recuperación bloqueada para ${input.invocation_id}.`),{code:'RECOVERY_ACK_INVALID'});
40
+ if(rows.some(r=>r.type==='house_receipt'&&r.invocation_id===input.invocation_id))throw Object.assign(new Error('Esa llamada ya tiene recibo.'),{code:'RECOVERY_ACK_INVALID'});
41
+ if(rows.some(r=>r.type==='process_recovery_acknowledged'&&r.invocation_id===input.invocation_id))return JSON.stringify({acknowledged:true,invocation_id:input.invocation_id,already:true});
42
+ await store.append(options.sessionId,{type:'process_recovery_acknowledged',source:'human_control',invocation_id:input.invocation_id,operation:proposal.operation,actor:options.actor,reason:input.reason.trim()});
43
+ return JSON.stringify({acknowledged:true,invocation_id:input.invocation_id,operation:proposal.operation,next_action:'Reanuda la sesión: la llamada se cierra como efectos desconocidos y el modelo continúa.'});
44
+ }};
45
+ }
23
46
  export async function recoverFileEffects({store,house,executor,sessionId,actor,signal,session}) {
24
47
  if(!sessionId)return;
25
48
  const snapshot=session?.id===sessionId?session:await store.load(sessionId);
26
- if(snapshot.rows.some(row=>row.recovery) && auditExecution(snapshot,house.stateContext()).violations.length)throw Object.assign(new Error('Recibo de recuperación inválido; no se entregó al modelo.'),{code:'AUDIT_INVALID'});
49
+ // Only a violation in a recovery receipt voids recovery. Others (a file changed outside the agent,
50
+ // seen as state_chain_broken) stay audit facts and do not block resuming.
51
+ const recoveries=new Set(snapshot.rows.filter(row=>row.recovery).map(row=>row.invocation_id));
52
+ if(recoveries.size && recoveryBlockers(auditExecution(snapshot,house.stateContext()).violations).some(v=>recoveries.has(v.invocation_id)||String(v.reason).startsWith('recovery_')))throw Object.assign(new Error('Recibo de recuperación inválido; no se entregó al modelo.'),{code:'AUDIT_INVALID'});
27
53
  if(!pendingFromDeadProcess(snapshot,actor,house.stateContext().active).length)return;
28
54
  return store.workTransaction(sessionId,()=>reconcile({store,house,executor,sessionId,actor,signal}));
29
55
  }
30
56
  async function reconcile({store,house,executor,sessionId,actor,signal}) {
31
57
  let session=await store.load(sessionId);const audit=auditExecution(session,house.stateContext());
32
- if(!audit.journal.consistent || audit.violations.length)throw Object.assign(new Error('Libro inválido; recuperación suspendida.'),{code:'AUDIT_INVALID'});
58
+ if(!audit.journal.consistent || recoveryBlockers(audit.violations).length)throw Object.assign(new Error('Libro inválido; recuperación suspendida.'),{code:'AUDIT_INVALID'});
33
59
  const candidates=pendingFromDeadProcess(session,actor,house.stateContext().active).sort((a,b)=>Number(Boolean(house.recoveryAdapter(sessionId,b.p.operation)))-Number(Boolean(house.recoveryAdapter(sessionId,a.p.operation))) || b.index-a.index);
34
60
  for(const {p,index,unknownOwner} of candidates){
35
61
  signal?.throwIfAborted();
@@ -42,8 +68,15 @@ async function reconcile({store,house,executor,sessionId,actor,signal}) {
42
68
  const event={type:'process_recovery_blocked',source:'recovery_host',invocation_id:p.invocation_id,operation:p.operation,reason};
43
69
  const loaded=await store.load(sessionId);
44
70
  if(!loaded.rows.some(r=>r.type===event.type && r.invocation_id===p.invocation_id && r.reason===reason))await store.append(sessionId,event);
45
- throw Object.assign(new Error(`Recuperación incierta de ${p.operation} (${p.invocation_id}): ${reason}. No se repitió el efecto ni se llamó al modelo.`),{code:'HOUSE_RECOVERY_UNCERTAIN'});
71
+ throw Object.assign(new Error(`Recuperación incierta de ${p.operation} (${p.invocation_id}): ${reason}. No se repitió el efecto ni se llamó al modelo. Revisa el estado y, si decides continuar, ejecuta: /recover acknowledge ${p.invocation_id} <qué revisaste>`),{code:'HOUSE_RECOVERY_UNCERTAIN'});
46
72
  };
73
+ const acknowledgementRef=session.rows.findIndex(r=>r.type==='process_recovery_acknowledged'&&r.invocation_id===p.invocation_id&&r.actor?.kind==='human');
74
+ if(acknowledgementRef>=0){
75
+ const receipt={...base,type:'house_receipt',source:'recovery_host',status:'refused',code:'HOUSE_RECOVERY_ACKNOWLEDGED_UNCERTAIN',error:`El proceso murió durante ${p.operation} y sus efectos son desconocidos; una persona lo reconoció para continuar. No se repitió. Comprueba el estado antes de depender de él.`,effects_observed:'unknown',observations:[],asked:'unjudged',recovery:{...recovery,kind:'acknowledged_uncertain',acknowledgement_ref:acknowledgementRef}};
76
+ const current=await store.load(sessionId);
77
+ if(!recoveryReceiptValid(current.rows,current.rows.length,receipt))return blocked('El reconocimiento humano no valida');
78
+ await store.append(sessionId,receipt);continue;
79
+ }
47
80
  if(unknownOwner)return blocked('El inicio legacy no identifica un proceso verificable; se conserva incierto');
48
81
  let receipt;
49
82
  const adapter=house.recoveryAdapter(sessionId,p.operation);
@@ -101,7 +101,7 @@ export class GeminiProvider {
101
101
  }
102
102
  getCapabilities() { return { ...this.capabilities, ...(this.modelCapabilities && this.modelCapabilities.model === this.model ? Object.fromEntries(Object.entries(this.modelCapabilities.capabilities).filter(([,value]) => value !== null)) : {}) }; }
103
103
  getVisionCapability() { return { supported: this.getCapabilities().vision, source: 'explicit_profile_configuration', model: this.model, imageTokens: this.imageTokens }; }
104
- async complete({ system, messages, tools = [], schemaCatalog = [], contextIdentity, signal, onText = () => {}, onProgress = () => {}, maxTokens = this.maxTokens, purpose = 'worker', nativeSearch = false, stream = true }) {
104
+ async complete({ system, messages, tools = [], schemaCatalog = [], contextIdentity, signal, onText = () => {}, onProgress = () => {}, onReasoning = () => {}, onToolCall = () => {}, maxTokens = this.maxTokens, purpose = 'worker', nativeSearch = false, stream = true }) {
105
105
  const accountingRequest={system,messages,tools,maxTokens},identity={...accountingIdentity(this),projection_epoch:contextIdentity?.projection_epoch ?? null};
106
106
  if (tools.length && !this.getCapabilities().tools) throw unsupported('tools');
107
107
  if (nativeSearch && !this.getCapabilities().web_search) throw unsupported('web_search');
@@ -120,7 +120,7 @@ export class GeminiProvider {
120
120
  const response = await this.fetch(`${this.baseURL}/models/${this.model}:${stream ? 'streamGenerateContent?alt=sse' : 'generateContent'}`, { method: 'POST', headers: { 'content-type': 'application/json', 'x-goog-api-key': this.apiKey }, body, signal: deadline.signal, redirect: 'error' });
121
121
  if (!response.ok) { await response.body?.cancel(); throw new Error(`Gemini HTTP ${response.status}. Revisa modelo, endpoint y credenciales del perfil.`); }
122
122
  const parts = []; let finish, grounding, usageRaw, usage = { input_tokens: 0, output_tokens: 0 };
123
- const consume = event => {
123
+ const consume = (event, live = false) => {
124
124
  if (event.error) throw new Error('Gemini devolvió un error; no se ejecutaron herramientas.');
125
125
  const candidate = event.candidates?.find(item => item.index === 0) || event.candidates?.[0];
126
126
  if (candidate?.finishReason) finish = candidate.finishReason;
@@ -129,10 +129,12 @@ export class GeminiProvider {
129
129
  for (const part of candidate?.content?.parts || []) {
130
130
  parts.push(part); deadline.touch();
131
131
  if (typeof part.text === 'string' && !part.thought) onText(part.text);
132
+ else if (live && part.thought && part.text) onReasoning();
133
+ if (live && part.functionCall) onToolCall();
132
134
  }
133
135
  onProgress({ elapsed_ms: deadline.elapsedMs(), purpose });
134
136
  };
135
- if (response.headers.get('content-type')?.includes('text/event-stream')) { for await (const event of sseEvents(response.body)) { if (event.type !== 'stream_done') consume(event); } }
137
+ if (response.headers.get('content-type')?.includes('text/event-stream')) { for await (const event of sseEvents(response.body)) { if (event.type !== 'stream_done') consume(event, true); } deadline.signal.throwIfAborted(); if (!finish) throw Object.assign(new Error('Respuesta Gemini incompleta (stream interrumpido); no se ejecutaron herramientas.'), { code: 'MODEL_STREAM_ERROR' }); }
136
138
  else consume(await response.json());
137
139
  deadline.signal.throwIfAborted();
138
140
  return {...normalize(parts, finish, usage, grounding,bindUsageAccounting(normalizeUsage('gemini',usageRaw),accountingRequest,identity,body)),schema_diagnostics:schemaFrame.diagnostics,schema_manifest:schemaFrame.manifest};
@@ -14,6 +14,7 @@ import {integrationLineage} from './git-integration-lineage.mjs';
14
14
  import {readPageEvidence,pageEvidenceReferences,publicPageResult,publicPageError} from './git-page-evidence.mjs';
15
15
  import {hostCodeIdentity} from './git-page-identity.mjs';
16
16
  import {isPageVerification,pageVerificationSchema} from './git-page-proof.mjs';
17
+ import {bashTimeoutSchema} from './bash-timeouts.mjs';
17
18
 
18
19
  export const GIT_INTEGRATION_MECHANISM='comal-git-integration-v1';
19
20
  const source='git_integration_host',fail=(message,code='GIT_INTEGRATION_BINDING')=>Object.assign(new Error(message),{code});
@@ -22,7 +23,7 @@ const id={type:'string',pattern:'^[a-f0-9-]{36}$'},token={type:'string',minLengt
22
23
  // contract. Factoring avoids the local GBNF compiler's 2000-repetition boundary;
23
24
  // native min/max lengths and every other check constraint remain in place.
24
25
  const commandPattern=String.raw`^(?:[^\n]|\n){1,1000}(?:[^\n]|\n){0,1000}$`;
25
- const commandVerification={type:'object',additionalProperties:false,required:['criterion','checks'],properties:{criterion:{type:'string',minLength:1,maxLength:4000},checks:{type:'array',minItems:1,maxItems:8,items:{type:'object',additionalProperties:false,required:['id','command'],properties:{id:{type:'string',pattern:'^[a-zA-Z0-9_.:-]{1,80}$'},command:{type:'string',minLength:1,maxLength:2000,pattern:commandPattern},timeout:{type:'integer',minimum:1,maximum:120000},assertions:{type:'array',maxItems:8,items:assertionSchema}}}}}};
26
+ const commandVerification={type:'object',additionalProperties:false,required:['criterion','checks'],properties:{criterion:{type:'string',minLength:1,maxLength:4000},checks:{type:'array',minItems:1,maxItems:8,items:{type:'object',additionalProperties:false,required:['id','command'],properties:{id:{type:'string',pattern:'^[a-zA-Z0-9_.:-]{1,80}$'},command:{type:'string',minLength:1,maxLength:2000,pattern:commandPattern},timeout:bashTimeoutSchema(),assertions:{type:'array',maxItems:8,items:assertionSchema}}}}}};
26
27
  const verification=commandVerification;
27
28
  const schema=(properties,required=[])=>({type:'object',additionalProperties:false,properties,required});
28
29
  const effects={workspace:'write',external:'none',process:'write',authority:'none'};
@@ -1,3 +1,4 @@
1
+ import {childEnvironment} from './child-env.mjs';
1
2
  import {spawn,execFile} from 'node:child_process';
2
3
  import {promisify} from 'node:util';
3
4
  import {mkdir,lstat,stat,realpath,readFile,readlink,access} from 'node:fs/promises';
@@ -19,7 +20,7 @@ export async function privateGitDirectory(directory){
19
20
  }
20
21
  const baseArgs=['--no-pager','--no-optional-locks','--literal-pathspecs','-c','core.fsmonitor=false','-c','core.hooksPath=/dev/null','-c','core.autocrlf=false','-c','commit.gpgsign=false'];
21
22
  export function gitHostEnvironment(extra={}){
22
- return{...Object.fromEntries(Object.entries(process.env).filter(([key])=>!key.startsWith('GIT_'))),GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:'/dev/null',GIT_TERMINAL_PROMPT:'0',GIT_OPTIONAL_LOCKS:'0',LC_ALL:'C',...extra};
23
+ return{...Object.fromEntries(Object.entries(childEnvironment()).filter(([key])=>!key.startsWith('GIT_'))),GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:'/dev/null',GIT_TERMINAL_PROMPT:'0',GIT_OPTIONAL_LOCKS:'0',LC_ALL:'C',...extra};
23
24
  }
24
25
  export function gitWorkerGone(fact){
25
26
  if(!fact?.identity||!processDead(fact.identity))return false;
@@ -1,3 +1,4 @@
1
+ import {childEnvironment} from './child-env.mjs';
1
2
  import {execFile} from 'node:child_process';
2
3
  import {promisify} from 'node:util';
3
4
  import {lstat,open,readlink,realpath} from 'node:fs/promises';
@@ -13,7 +14,7 @@ function text(bytes){const value=bytes.toString('utf8');if(!Buffer.from(value).e
13
14
  const line=bytes=>text(bytes).replace(/\n$/,'');
14
15
  const blobHash=(bytes,format)=>createHash(format).update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
15
16
  function environment(){
16
- const env=Object.fromEntries(Object.entries(process.env).filter(([key])=>!key.startsWith('GIT_')));
17
+ const env=Object.fromEntries(Object.entries(childEnvironment()).filter(([key])=>!key.startsWith('GIT_')));
17
18
  return{...env,GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:'/dev/null',GIT_TERMINAL_PROMPT:'0',GIT_OPTIONAL_LOCKS:'0',LC_ALL:'C'};
18
19
  }
19
20
  async function git(cwd,args,signal,allowed=[]){
@@ -14,6 +14,7 @@ import {GitPlumbing,mutateGit,mutateGitWithChildWitness,GIT_INPUT_GATE_PROTOCOL,
14
14
  import {isPageVerification,pageVerificationSchema,assertPageChain,assertNativePrefix,nativePrefix} from './git-page-proof.mjs';
15
15
  import {hostCodeIdentity} from './git-page-identity.mjs';
16
16
  import {reservePageCheck} from './git-page-verification.mjs';
17
+ import {bashTimeoutSchema} from './bash-timeouts.mjs';
17
18
 
18
19
  const sha=bytes=>createHash('sha256').update(bytes).digest('hex'),line=result=>result.stdout.toString('utf8').replace(/\n$/,'');
19
20
  const sourcePath=name=>{
@@ -25,7 +26,7 @@ const ownerDigest=owner=>{
25
26
  export function gitWorkspaceResourceId(cwd,owner,request){const hex=digest({mechanism:'comal-git-workspace-v1',cwd,owner:ownerDigest(owner),request}).slice(0,32);return`${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;}
26
27
  const resourceId=gitWorkspaceResourceId;
27
28
  const publicWork=work=>({...work,scope:'isolated_source_not_verified_delivery',asked:'unjudged'});
28
- const checkContract=new Ajv({strict:false}).compile({type:'object',additionalProperties:false,required:['criterion','checks'],properties:{criterion:{type:'string',minLength:1,maxLength:4000},checks:{type:'array',minItems:1,maxItems:8,items:{type:'object',additionalProperties:false,required:['id','command'],properties:{id:{type:'string',pattern:'^[a-zA-Z0-9_.:-]{1,80}$'},command:{type:'string',minLength:1,maxLength:2000},timeout:{type:'integer',minimum:1,maximum:120000},assertions:{type:'array',maxItems:8,items:assertionSchema}}}}}});
29
+ const checkContract=new Ajv({strict:false}).compile({type:'object',additionalProperties:false,required:['criterion','checks'],properties:{criterion:{type:'string',minLength:1,maxLength:4000},checks:{type:'array',minItems:1,maxItems:8,items:{type:'object',additionalProperties:false,required:['id','command'],properties:{id:{type:'string',pattern:'^[a-zA-Z0-9_.:-]{1,80}$'},command:{type:'string',minLength:1,maxLength:2000},timeout:bashTimeoutSchema(),assertions:{type:'array',maxItems:8,items:assertionSchema}}}}}});
29
30
  const checkPageContract=new Ajv({strict:false}).compile(pageVerificationSchema);
30
31
 
31
32
  async function readCaptured(root,file,budget,signal){
package/src/house.mjs CHANGED
@@ -8,6 +8,14 @@ import { auditExecution } from './audit.mjs';
8
8
  import { hostRefusalFailure, governanceRefusalFailure, settleNativeRefusal } from './native-refusal.mjs';
9
9
  import { prepareNativeSourceRequest } from './source-requests.mjs';
10
10
  import { t, registerMessages } from './i18n/index.mjs';
11
+ import { parseRule } from './permission-rules.mjs';
12
+ // A saved human allow rule for the whole tool (Bash or Bash(*)) counts like --allowed-tools. Rules live in a
13
+ // private file outside the workspace, never where the model writes.
14
+ const savedBroadAllow = (rules, name) => !rules?.decide?.(name, {})?.list?.match?.(/^(?:deny|ask)$/) && (rules?.applicable?.() || []).some(rule => {
15
+ if (rule.list !== 'allow') return false;
16
+ try { const parsed = parseRule(rule.rule); return (parsed.kind === 'tool' && parsed.name === name) || (name === 'Bash' && parsed.kind === 'bash-any'); } catch { return false; }
17
+ });
18
+
11
19
 
12
20
  // Textos de la tarjeta de confirmación: sólo los ve el humano.
13
21
  registerMessages({
@@ -234,6 +242,10 @@ export class House {
234
242
  return {
235
243
  identity: () => ({actor:structuredClone(context.actor),session_id:context.sessionId,surface:context.surface}),
236
244
  setContext: value => policy.setContext?.(value), getMode: () => policy.mode,
245
+ // Directories the human opened besides the workspace: --add-dir (read) and /add-dir (read+write).
246
+ getDirectories: () => ({ read: [...(policy.readRoots || [])], work: [...(policy.workRoots || [])] }),
247
+ // Decisión del humano al arrancar (--allowed-tools o saltar permisos); el modelo no la escribe.
248
+ grantedByHuman: name => policy.mode === 'bypassPermissions' || Boolean(policy.allowed?.has?.(name)) || !policy.denied?.has?.(name) && savedBroadAllow(policy.rules, name),
237
249
  execute: (name, input, options) => execute(snapshot(), name, input, options),
238
250
  snapshot: () => { const catalog = snapshot(), captured = frame(); return { setContext: value => policy.setContext?.(value), execute: (name, input, options) => execute(catalog, name, input, options, captured), definitions: () => [...catalog.values()].map(op => op.tool()), catalogue: () => [...catalog.values()].map(op => ({ ...op.contract, contract_digest: op.digest })) }; },
239
251
  definitions: () => [...snapshot().values()].map(op => op.tool()),
@@ -260,7 +272,7 @@ export class House {
260
272
  }
261
273
  }
262
274
  }
263
- authority(context) { return digest({ mode: context.policy.mode, allowed: [...(context.policy.allowed || [])].sort(), denied: [...(context.policy.denied || [])].sort(), intentions: context.policy.context?.intentions, governance: this.governance(context.sessionId), scope: this.#scopeStamp(context.sessionId), guards: [...this.#effectiveGuards().values()].map(({ id, token }) => ({ id, token })) }); }
275
+ authority(context) { return digest({ mode: context.policy.mode, allowed: [...(context.policy.allowed || [])].sort(), denied: [...(context.policy.denied || [])].sort(), ...(context.policy.workRoots?.length ? { work_roots: [...context.policy.workRoots] } : {}), intentions: context.policy.context?.intentions, governance: this.governance(context.sessionId), scope: this.#scopeStamp(context.sessionId), guards: [...this.#effectiveGuards().values()].map(({ id, token }) => ({ id, token })) }); }
264
276
  async judge(context, name, input, { signal } = {}) {
265
277
  for (let attempt = 0; attempt < 4; attempt++) {
266
278
  signal?.throwIfAborted(); const before = this.authority(context);
@@ -72,6 +72,17 @@ export default Object.freeze({
72
72
  "Use /model ID, without spaces.": "Usa /model ID, sin espacios.",
73
73
  "Model: {v0} · effort {v1} · same session": "Modelo: {v0} · effort {v1} · misma sesión",
74
74
  "Use /rename NAME.": "Usa /rename NOMBRE.",
75
+ "Added: {v0} · read and write; writes follow the permission mode ({v1}).": "Agregada: {v0} · lectura y escritura; escribir sigue el modo de permisos ({v1}).",
76
+ "{v0} is already inside the workspace.": "{v0} ya está dentro del workspace.",
77
+ "{v0} was already added.": "{v0} ya estaba agregada.",
78
+ "Directories Comal can work in:": "Carpetas donde Comal puede trabajar:",
79
+ "workspace": "workspace",
80
+ "/add-dir: read and write": "/add-dir: lectura y escritura",
81
+ "--add-dir: read only": "--add-dir: solo lectura",
82
+ "Use /add-dir PATH to add one.": "Usa /add-dir RUTA para agregar otra.",
83
+ "/add-dir not restored: {v0}": "/add-dir no se restauró: {v0}",
84
+ "/add-dir not restored: {v0} (the session journal is not consistent).": "/add-dir no se restauró: {v0} (el diario de la sesión no es consistente).",
85
+ "/add-dir restored: {v0} (read and write).": "/add-dir restaurada: {v0} (lectura y escritura).",
75
86
  "Session: {v0}": "Sesión: {v0}",
76
87
  "The demo does not allow changing provider.": "La demo no permite cambiar proveedor.",
77
88
  "Provider: {v0} · {v1} · {v2}": "Proveedor: {v0} · {v1} · {v2}",
@@ -97,5 +108,5 @@ export default Object.freeze({
97
108
  "Use /permissions default|plan|acceptEdits|auto.": "Usa /permissions default|plan|acceptEdits|auto.",
98
109
  "Model: {v0} · effort {v1}\nUse /models, /model ID, or /effort LEVEL.": "Modelo: {v0} · effort {v1}\nUsa /models, /model ID o /effort NIVEL.",
99
110
  "There are no saved sessions.\n": "No hay sesiones guardadas.\n",
100
- "Comal · standalone CLI programming assistant\n\nUsage: node bin/comal.mjs [options] [prompt]\n node bin/comal.mjs sessions | evidence | audit --resume UUID\n\n -p, --print Runs a turn and exits (also accepts stdin)\n --demo Deterministic demo without credentials; allows its edits\n --cwd <path> Workspace (default: current directory)\n --provider <protocol> openai (default) | anthropic | gemini\n --profile <name> Profile from .comal/providers.json (endpoint/model/capabilities)\n --providers-config <file> Profiles file; keys only via apiKeyEnv\n --name <name> Name visible to other sessions\n --listen Keeps the CLI listening for messages and responds when idle\n --inbound <mode> accept (default) | hold | refuse\n --auth-provider <id> Comal login: codex | kimi | minimax | grok\n --effort <level> auto | off | minimal | low | medium | high | xhigh | max (if the model supports it)\n --model <id> Model; by default discovers the local one at /v1/models\n --base-url <url> Endpoint; default http://llama.local:11438/\n --permission-mode <mode> default | acceptEdits | plan | auto\n --allowed-tools <names> Authorizes comma-separated names\n --disallowed-tools <names> Denies names; overrides authorization\n --resume <uuid> Resumes session; with -p and no text continues pending work\n -c, --continue Resumes the most recent session\n --output-format <format> text | json | stream-json (with --print)\n --max-turns <n> Calls before extending work with progress (12)\n --work-max-calls <n> Total limit with task continuation (80; 0 disables)\n --context-window <n> Explicit limit; automatic based on model, fallback 45000\n --max-tokens <n> Initial maximum output tokens (8192)\n --thinking <mode> auto | on | off | low | medium | high (llama.cpp)\n --vision <mode> auto | on | off (auto queries /props when receiving images)\n --image-tokens <n> Estimated reservation per image (4096, maximum 2 images)\n --model-timeout-ms <n> Total time for each OpenAI response (600000)\n --model-idle-timeout-ms <n> Time without OpenAI token progress (180000)\n --system-prompt <text> Additional instructions\n --state-dir <path> State location; default .comal/ (reuses existing .harness/)\n --mcp-config <file> Connects MCP stdio, Streamable HTTP, or SSE from a JSON\n --trust-mcp Authorizes initial connections; changes are reviewed again\n --plugin-config <file> Local Node plugins (JSON); default .comal/plugins.json\n --lang <en|es|auto> UI language (default: en; auto opts into locale detection)\n --codemode Read-only JavaScript tool composition (default: on)\n --no-codemode Disable CodeRun for this process\n --trust-plugins Authorizes loading local code from the initial config\n --chrome-task <file.json> Explicit Chrome task; the house coordinates publication, checks, and closure\n --goal <condition> Runs a goal until verification or limit\n --goal-max-turns <n> Maximum goal turns (20)\n --goal-max-calls <n> Total calls: agent + verifiers (80)\n --goal-max-tokens <n> Goal token budget (100000)\n -h, --help Shows this help\n -v, --version Shows version\n\nVariables (HARNESS_* remains as alias): COMAL_PROVIDER, COMAL_BASE_URL, COMAL_MODEL,\n COMAL_CONTEXT_WINDOW, COMAL_MAX_TOKENS, COMAL_WORK_MAX_CALLS,\n COMAL_MODEL_TIMEOUT_MS, COMAL_MODEL_IDLE_TIMEOUT_MS,\n OPENAI_BASE_URL, OPENAI_MODEL, OPENAI_API_KEY (optional for local),\n ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL (with --provider anthropic).\nRead authorized by default; write and Bash require permission.\nIn --print without a terminal, unauthorized tools are denied.\nShift+Tab: default → acceptEdits → plan → auto → default.\nInteractive commands: /help /status /tools /house /operations /sessions /resume UUID /clear\n /compact /context /state /audit /board /work resume|recover\n /model ID /permissions MODE /goal [condition] /exit\n /queue [pause|resume|clear|edit N] /rename NAME /table FILE\nTUI: F1 help · F2 sessions · F4 queue · F5 or /select selection · F6 discover commands/tools.\n / and Tab complete; ↑/↓ chooses; Enter on suggestions only completes. Alt+C or /copy copies.\n Ctrl+G editor · Alt+Enter multiline.\nMCP: /mcp · /mcp reload [file] · /mcp add NAME {JSON} · /mcp remove NAME\n /mcp reconnect NAME. Default file: .mcp.json in the workspace.\nWeb: /search QUERY · WebSearch/WebFetch · COMAL_SEARCH_PROVIDER, SEARXNG_URL, BRAVE_SEARCH_API_KEY.\nSubagents: /agent [explore|general] TASK · /agents [UUID] · Agent/AgentBatch.\nPlugins: /plugins [list|enable ID|disable ID|reload [ID]|run ID OPERATION {JSON}].\nLive sessions: /list-agents · /send ID_OR_NAME TEXT · /inbox [accept|hold|refuse|release ID|all]\nComal account: comal cloud login|status|logout [--cloud-url ORIGIN] (separate from the model provider)\nArtifacts: comal artifacts list|inspect ID|verify ID|publish DIR --manifest JSON|release ID VERSION JOB\nLogin: comal login PROVIDER [--api-key-env VARIABLE] · comal auth status [PROVIDER] · comal logout PROVIDER\nModels: /model [ID] · /models · /effort [LEVEL] · /capabilities [refresh] · /login PROVIDER · /logout PROVIDER\nProviders: /providers · /provider PROFILE · ProviderSearch/AnalyzeMedia according to capabilities.\nCLI: comal peers | providers | message DESTINATION TEXT (use --listen to receive in CLI).\n/goal status|pause|resume|clear controls the goal; Ctrl+C pauses it.\nLocal skills: .comal/skills/<name>/SKILL.md → /<name> [task]\n\nRemote Control: /rc [start|status|stop] · TUI account: /cloud [login|status|logout] · CLI: comal cloud login\n": "Comal · asistente de programación CLI independiente\n\nUso: node bin/comal.mjs [opciones] [prompt]\n node bin/comal.mjs sessions | evidence | audit --resume UUID\n\n -p, --print Ejecuta un turno y termina (también acepta stdin)\n --demo Demo determinista sin credenciales; permite sus ediciones\n --cwd <ruta> Workspace (por defecto: directorio actual)\n --provider <protocolo> openai (predeterminado) | anthropic | gemini\n --profile <nombre> Perfil de .comal/providers.json (endpoint/modelo/capacidades)\n --providers-config <archivo> Archivo de perfiles; claves sólo vía apiKeyEnv\n --name <nombre> Nombre visible a otras sesiones\n --listen Mantiene el CLI escuchando mensajes y responde al quedar idle\n --inbound <modo> accept (predeterminado) | hold | refuse\n --auth-provider <id> Login de Comal: codex | kimi | minimax | grok\n --effort <nivel> auto | off | minimal | low | medium | high | xhigh | max (si el modelo lo admite)\n --model <id> Modelo; por defecto descubre el local en /v1/models\n --base-url <url> Endpoint; por defecto http://llama.local:11438/\n --permission-mode <modo> default | acceptEdits | plan | auto\n --allowed-tools <nombres> Autoriza nombres separados por comas\n --disallowed-tools <nombres> Deniega nombres; prevalece sobre autorización\n --resume <uuid> Reanuda sesión; con -p sin texto continúa trabajo pendiente\n -c, --continue Reanuda la sesión más reciente\n --output-format <formato> text | json | stream-json (con --print)\n --max-turns <n> Llamadas antes de extender trabajo con progreso (12)\n --work-max-calls <n> Límite total con continuación de tareas (80; 0 desactiva)\n --context-window <n> Límite explícito; automático según modelo, fallback 45000\n --max-tokens <n> Máximo inicial de tokens de salida (8192)\n --thinking <modo> auto | on | off | low | medium | high (llama.cpp)\n --vision <modo> auto | on | off (auto consulta /props al recibir imágenes)\n --image-tokens <n> Reserva estimada por imagen (4096, máximo 2 imágenes)\n --model-timeout-ms <n> Tiempo total de cada respuesta OpenAI (600000)\n --model-idle-timeout-ms <n> Tiempo sin progreso de tokens OpenAI (180000)\n --system-prompt <texto> Instrucciones adicionales\n --state-dir <ruta> Ubicación del estado; por defecto .comal/ (reutiliza .harness/ existente)\n --mcp-config <archivo> Conecta MCP stdio, Streamable HTTP o SSE de un JSON\n --trust-mcp Autoriza conexiones iniciales; cambios se revisan de nuevo\n --plugin-config <archivo> Plugins Node locales (JSON); por defecto .comal/plugins.json\n --trust-plugins Autoriza cargar código local de la config inicial\n --codemode Experimental read-only JavaScript tool composition\n --chrome-task <archivo.json> Tarea Chrome explícita; la casa coordina publicación, checks y cierre\n --goal <condición> Ejecuta un objetivo hasta verificación o límite\n --goal-max-turns <n> Máximo de turnos del objetivo (20)\n --goal-max-calls <n> Llamadas totales: agente + verificadores (80)\n --goal-max-tokens <n> Presupuesto de tokens del objetivo (100000)\n -h, --help Muestra esta ayuda\n -v, --version Muestra versión\n\nVariables (HARNESS_* sigue como alias): COMAL_PROVIDER, COMAL_BASE_URL, COMAL_MODEL,\n COMAL_CONTEXT_WINDOW, COMAL_MAX_TOKENS, COMAL_WORK_MAX_CALLS,\n COMAL_MODEL_TIMEOUT_MS, COMAL_MODEL_IDLE_TIMEOUT_MS,\n OPENAI_BASE_URL, OPENAI_MODEL, OPENAI_API_KEY (opcional para local),\n ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL (con --provider anthropic).\nLectura autorizada por defecto; escritura y Bash requieren permiso.\nEn --print sin terminal, las herramientas no autorizadas se deniegan.\nShift+Tab: default → acceptEdits → plan → auto → default.\nRemote Control: /rc [start|status|stop] · TUI account: /cloud [login|status|logout] · CLI: comal cloud login\nComandos interactivos: /help /status /tools /house /operations /sessions /resume UUID /clear\n /compact /context /state /audit /board /work resume|recover\n /model ID /permissions MODO /goal [condición] /exit\n /queue [pause|resume|clear|edit N] /rename NOMBRE /table ARCHIVO\nTUI: F1 ayuda · F2 sesiones · F4 cola · F5 o /select selección · F6 descubrir comandos/herramientas.\n / y Tab completan; ↑/↓ elige; Enter en sugerencias sólo completa. Alt+C o /copy copiar.\n Ctrl+G editor · Alt+Enter multilínea.\nMCP: /mcp · /mcp reload [archivo] · /mcp add NOMBRE {JSON} · /mcp remove NOMBRE\n /mcp reconnect NOMBRE. Archivo predeterminado: .mcp.json del workspace.\nWeb: /search CONSULTA · WebSearch/WebFetch · COMAL_SEARCH_PROVIDER, SEARXNG_URL, BRAVE_SEARCH_API_KEY.\nSubagentes: /agent [explore|general] TAREA · /agents [UUID] · Agent/AgentBatch.\nPlugins: /plugins [list|enable ID|disable ID|reload [ID]|run ID OPERACIÓN {JSON}].\nSesiones vivas: /list-agents · /send ID_O_NOMBRE TEXTO · /inbox [accept|hold|refuse|release ID|all]\nCuenta Comal: comal cloud login|status|logout [--cloud-url ORIGEN] (separada del proveedor de modelos)\nArtifacts: comal artifacts list|inspect ID|verify ID|publish DIR --manifest JSON|release ID VERSION JOB\nLogin: comal login PROVEEDOR [--api-key-env VARIABLE] · comal auth status [PROVEEDOR] · comal logout PROVEEDOR\nModelos: /model [ID] · /models · /effort [NIVEL] · /capabilities [refresh] · /login PROVEEDOR · /logout PROVEEDOR\nProveedores: /providers · /provider PERFIL · ProviderSearch/AnalyzeMedia según capacidades.\nCLI: comal peers | providers | message DESTINO TEXTO (usa --listen para recibir en CLI).\n/goal status|pause|resume|clear controla el objetivo; Ctrl+C lo pausa.\nSkills locales: .comal/skills/<nombre>/SKILL.md → /<nombre> [tarea]\n\n --lang <en|es|auto> Idioma de interfaz (predeterminado: en)\n\n --no-codemode Desactiva CodeRun para este proceso\n"
111
+ "Comal · standalone CLI programming assistant\n\nUsage: node bin/comal.mjs [options] [prompt]\n node bin/comal.mjs sessions | evidence | audit --resume UUID\n\n -p, --print Runs a turn and exits (also accepts stdin)\n --demo Deterministic demo without credentials; allows its edits\n --cwd <path> Workspace (default: current directory)\n --provider <protocol> openai (default) | anthropic | gemini\n --profile <name> Profile from .comal/providers.json (endpoint/model/capabilities)\n --providers-config <file> Profiles file; keys only via apiKeyEnv\n --name <name> Name visible to other sessions\n --listen Keeps the CLI listening for messages and responds when idle\n --inbound <mode> accept (default) | hold | refuse\n --no-peers No cross-session bus (also COMAL_PEERS=off)\n --auth-provider <id> Comal login: codex | kimi | minimax | grok\n --effort <level> auto | off | minimal | low | medium | high | xhigh | max (if the model supports it)\n --model <id> Model; by default discovers the local one at /v1/models\n --base-url <url> Endpoint; default http://llama.local:11438/\n --permission-mode <mode> default | acceptEdits | plan | auto\n --allowed-tools <names> Authorizes comma-separated names\n --disallowed-tools <names> Denies names; overrides authorization\n --resume <uuid> Resumes session; with -p and no text continues pending work\n -c, --continue Resumes the most recent session\n --output-format <format> text | json | stream-json (with --print)\n --max-turns <n> Calls before extending work with progress (12)\n --work-max-calls <n> Total limit with task continuation (80; 0 disables)\n --context-window <n> Explicit limit; automatic based on model, fallback 45000\n --max-tokens <n> Initial maximum output tokens (8192; up to 32000 when the model allows)\n --autocompact <n|auto|off> Compaction window: N or Nk tokens, auto or off (also COMAL_AUTOCOMPACT)\n --thinking <mode> auto | on | off | low | medium | high (llama.cpp)\n --vision <mode> auto | on | off (auto queries /props when receiving images)\n --image-tokens <n> Estimated reservation per image (4096, maximum 2 images)\n --model-timeout-ms <n> Total time for each OpenAI response (600000)\n --model-idle-timeout-ms <n> Time without model token progress (180000)\n --system-prompt <text> Additional instructions\n --state-dir <path> State location; default .comal/ (reuses existing .harness/)\n --mcp-config <file> Connects MCP stdio, Streamable HTTP, or SSE from a JSON\n --trust-mcp Authorizes initial connections; changes are reviewed again\n --plugin-config <file> Local Node plugins (JSON); default .comal/plugins.json\n --lang <en|es|auto> UI language (default: en; auto opts into locale detection)\n --codemode Read-only JavaScript tool composition (default: on)\n --no-codemode Disable CodeRun for this process\n --trust-plugins Authorizes loading local code from the initial config\n --chrome-task <file.json> Explicit Chrome task; the house coordinates publication, checks, and closure\n --goal <condition> Runs a goal until verification or limit\n --goal-max-turns <n> Maximum goal turns (20)\n --goal-max-calls <n> Total calls: agent + verifiers (80)\n --goal-max-tokens <n> Goal token budget (100000)\n -h, --help Shows this help\n -v, --version Shows version\n\nVariables (HARNESS_* remains as alias): COMAL_PROVIDER, COMAL_BASE_URL, COMAL_MODEL,\n COMAL_CONTEXT_WINDOW, COMAL_MAX_TOKENS, COMAL_WORK_MAX_CALLS,\n COMAL_MODEL_TIMEOUT_MS, COMAL_MODEL_IDLE_TIMEOUT_MS,\n OPENAI_BASE_URL, OPENAI_MODEL, OPENAI_API_KEY (optional for local),\n ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL (with --provider anthropic).\nRead authorized by default; write and Bash require permission.\nIn --print without a terminal, unauthorized tools are denied.\nShift+Tab: default → acceptEdits → plan → auto → default.\nInteractive commands: /help /status /tools /house /operations /sessions /resume UUID /clear\n /compact /context /state /audit /board /work resume|recover\n /model ID /permissions MODE /goal [condition] /exit\n /queue [pause|resume|clear|edit N] /rename NAME /table FILE\nTUI: F1 help · F2 sessions · F4 queue · F5 or /select selection · F6 discover commands/tools.\n / and Tab complete; ↑/↓ chooses; Enter on suggestions only completes. Alt+C or /copy copies.\n Ctrl+G editor · Alt+Enter multiline.\nMCP: /mcp · /mcp reload [file] · /mcp add NAME {JSON} · /mcp remove NAME\n /mcp reconnect NAME. Default file: .mcp.json in the workspace.\nWeb: /search QUERY · WebSearch/WebFetch · COMAL_SEARCH_PROVIDER, SEARXNG_URL, BRAVE_SEARCH_API_KEY.\nSubagents: /agent [explore|general] TASK · /agents [UUID] · Agent/AgentBatch.\nPlugins: /plugins [list|enable ID|disable ID|reload [ID]|run ID OPERATION {JSON}].\nLive sessions: /list-agents · /send ID_OR_NAME TEXT · /inbox [accept|hold|refuse|release ID|all]\nComal account: comal cloud login|status|logout [--cloud-url ORIGIN] (separate from the model provider)\nArtifacts: comal artifacts list|inspect ID|verify ID|publish DIR --manifest JSON|release ID VERSION JOB\nLogin: comal login PROVIDER [--api-key-env VARIABLE] · comal auth status [PROVIDER] · comal logout PROVIDER\nModels: /model [ID] · /models · /effort [LEVEL] · /capabilities [refresh] · /login PROVIDER · /logout PROVIDER\nProviders: /providers · /provider PROFILE · ProviderSearch/AnalyzeMedia according to capabilities.\nCLI: comal peers | providers | message DESTINATION TEXT (use --listen to receive in CLI).\n/goal status|pause|resume|clear controls the goal; Ctrl+C pauses it.\nLocal skills: .comal/skills/<name>/SKILL.md → /<name> [task]\n\nRemote Control: /rc [start|status|stop] · TUI account: /cloud [login|status|logout] · CLI: comal cloud login\n": "Comal · asistente de programación CLI independiente\n\nUso: node bin/comal.mjs [opciones] [prompt]\n node bin/comal.mjs sessions | evidence | audit --resume UUID\n\n -p, --print Ejecuta un turno y termina (también acepta stdin)\n --demo Demo determinista sin credenciales; permite sus ediciones\n --cwd <ruta> Workspace (por defecto: directorio actual)\n --provider <protocolo> openai (predeterminado) | anthropic | gemini\n --profile <nombre> Perfil de .comal/providers.json (endpoint/modelo/capacidades)\n --providers-config <archivo> Archivo de perfiles; claves sólo vía apiKeyEnv\n --name <nombre> Nombre visible a otras sesiones\n --listen Mantiene el CLI escuchando mensajes y responde al quedar idle\n --inbound <modo> accept (predeterminado) | hold | refuse\n --no-peers Sin bus entre sesiones (también COMAL_PEERS=off)\n --auth-provider <id> Login de Comal: codex | kimi | minimax | grok\n --effort <nivel> auto | off | minimal | low | medium | high | xhigh | max (si el modelo lo admite)\n --model <id> Modelo; por defecto descubre el local en /v1/models\n --base-url <url> Endpoint; por defecto http://llama.local:11438/\n --permission-mode <modo> default | acceptEdits | plan | auto\n --allowed-tools <nombres> Autoriza nombres separados por comas\n --disallowed-tools <nombres> Deniega nombres; prevalece sobre autorización\n --resume <uuid> Reanuda sesión; con -p sin texto continúa trabajo pendiente\n -c, --continue Reanuda la sesión más reciente\n --output-format <formato> text | json | stream-json (con --print)\n --max-turns <n> Llamadas antes de extender trabajo con progreso (12)\n --work-max-calls <n> Límite total con continuación de tareas (80; 0 desactiva)\n --context-window <n> Límite explícito; automático según modelo, fallback 45000\n --max-tokens <n> Máximo inicial de tokens de salida (8192; hasta 32000 si el modelo lo admite)\n --autocompact <n|auto|off> Ventana de compactación: N o Nk tokens, auto u off (también COMAL_AUTOCOMPACT)\n --thinking <modo> auto | on | off | low | medium | high (llama.cpp)\n --vision <modo> auto | on | off (auto consulta /props al recibir imágenes)\n --image-tokens <n> Reserva estimada por imagen (4096, máximo 2 imágenes)\n --model-timeout-ms <n> Tiempo total de cada respuesta OpenAI (600000)\n --model-idle-timeout-ms <n> Tiempo sin progreso de tokens del modelo (180000)\n --system-prompt <texto> Instrucciones adicionales\n --state-dir <ruta> Ubicación del estado; por defecto .comal/ (reutiliza .harness/ existente)\n --mcp-config <archivo> Conecta MCP stdio, Streamable HTTP o SSE de un JSON\n --trust-mcp Autoriza conexiones iniciales; cambios se revisan de nuevo\n --plugin-config <archivo> Plugins Node locales (JSON); por defecto .comal/plugins.json\n --trust-plugins Autoriza cargar código local de la config inicial\n --codemode Experimental read-only JavaScript tool composition\n --chrome-task <archivo.json> Tarea Chrome explícita; la casa coordina publicación, checks y cierre\n --goal <condición> Ejecuta un objetivo hasta verificación o límite\n --goal-max-turns <n> Máximo de turnos del objetivo (20)\n --goal-max-calls <n> Llamadas totales: agente + verificadores (80)\n --goal-max-tokens <n> Presupuesto de tokens del objetivo (100000)\n -h, --help Muestra esta ayuda\n -v, --version Muestra versión\n\nVariables (HARNESS_* sigue como alias): COMAL_PROVIDER, COMAL_BASE_URL, COMAL_MODEL,\n COMAL_CONTEXT_WINDOW, COMAL_MAX_TOKENS, COMAL_WORK_MAX_CALLS,\n COMAL_MODEL_TIMEOUT_MS, COMAL_MODEL_IDLE_TIMEOUT_MS,\n OPENAI_BASE_URL, OPENAI_MODEL, OPENAI_API_KEY (opcional para local),\n ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL (con --provider anthropic).\nLectura autorizada por defecto; escritura y Bash requieren permiso.\nEn --print sin terminal, las herramientas no autorizadas se deniegan.\nShift+Tab: default → acceptEdits → plan → auto → default.\nRemote Control: /rc [start|status|stop] · TUI account: /cloud [login|status|logout] · CLI: comal cloud login\nComandos interactivos: /help /status /tools /house /operations /sessions /resume UUID /clear\n /compact /context /state /audit /board /work resume|recover\n /model ID /permissions MODO /goal [condición] /exit\n /queue [pause|resume|clear|edit N] /rename NOMBRE /table ARCHIVO\nTUI: F1 ayuda · F2 sesiones · F4 cola · F5 o /select selección · F6 descubrir comandos/herramientas.\n / y Tab completan; ↑/↓ elige; Enter en sugerencias sólo completa. Alt+C o /copy copiar.\n Ctrl+G editor · Alt+Enter multilínea.\nMCP: /mcp · /mcp reload [archivo] · /mcp add NOMBRE {JSON} · /mcp remove NOMBRE\n /mcp reconnect NOMBRE. Archivo predeterminado: .mcp.json del workspace.\nWeb: /search CONSULTA · WebSearch/WebFetch · COMAL_SEARCH_PROVIDER, SEARXNG_URL, BRAVE_SEARCH_API_KEY.\nSubagentes: /agent [explore|general] TAREA · /agents [UUID] · Agent/AgentBatch.\nPlugins: /plugins [list|enable ID|disable ID|reload [ID]|run ID OPERACIÓN {JSON}].\nSesiones vivas: /list-agents · /send ID_O_NOMBRE TEXTO · /inbox [accept|hold|refuse|release ID|all]\nCuenta Comal: comal cloud login|status|logout [--cloud-url ORIGEN] (separada del proveedor de modelos)\nArtifacts: comal artifacts list|inspect ID|verify ID|publish DIR --manifest JSON|release ID VERSION JOB\nLogin: comal login PROVEEDOR [--api-key-env VARIABLE] · comal auth status [PROVEEDOR] · comal logout PROVEEDOR\nModelos: /model [ID] · /models · /effort [NIVEL] · /capabilities [refresh] · /login PROVEEDOR · /logout PROVEEDOR\nProveedores: /providers · /provider PERFIL · ProviderSearch/AnalyzeMedia según capacidades.\nCLI: comal peers | providers | message DESTINO TEXTO (usa --listen para recibir en CLI).\n/goal status|pause|resume|clear controla el objetivo; Ctrl+C lo pausa.\nSkills locales: .comal/skills/<nombre>/SKILL.md → /<nombre> [tarea]\n\n --lang <en|es|auto> Idioma de interfaz (predeterminado: en)\n\n --no-codemode Desactiva CodeRun para este proceso\n"
101
112
  });
package/src/i18n/es.mjs CHANGED
@@ -1,5 +1,18 @@
1
1
  // Host-authored interface text only. User/model/evidence content is never translated.
2
2
  export default Object.freeze({
3
+ "{provider} model exceeded {limit} after {seconds} s ({stage}). No tools from this response were executed. {guidance}": "Modelo {provider} agotó {limit} tras {seconds} s ({stage}). No se ejecutaron herramientas de esta respuesta. {guidance}",
4
+ "Anthropic declined this request (category: {category}).": "Anthropic declinó esta petición (categoría: {category}).",
5
+ "A server-side fallback was attempted and also declined.": "Se intentó un fallback del lado del servidor y también fue declinado.",
6
+ "Server-side fallback is off or unavailable for this model.": "El fallback del lado del servidor está desactivado o no disponible para este modelo.",
7
+ "Next: switch models with /model, or for legitimate security work apply to the Cyber Verification Program: https://support.claude.com/en/articles/14604842-real-time-cyber-safeguards-on-claude-opus-and-sonnet": "Siguiente paso: cambia de modelo con /model, o para trabajo de seguridad legítimo solicita el Cyber Verification Program: https://support.claude.com/en/articles/14604842-real-time-cyber-safeguards-on-claude-opus-and-sonnet",
8
+ "unspecified": "sin especificar",
9
+ "--anthropic-fallback only applies to the Anthropic provider.": "--anthropic-fallback sólo aplica al proveedor Anthropic.",
10
+ "Anthropic fallback: {v0}": "Fallback de Anthropic: {v0}",
11
+ " · fallback {v0}": " · fallback {v0}",
12
+ "Anthropic declined (category: {v0}).{v1}": "Anthropic declinó (categoría: {v0}).{v1}",
13
+ " Fallback also declined.": " El fallback también declinó.",
14
+ "Served by fallback model {v0}.": "Respondió el modelo de fallback {v0}.",
15
+ "Anthropic fallback": "Fallback de Anthropic",
3
16
  "Invalid model timing: {name} must be between 1 and 3600000 ms.": "Tiempo de modelo inválido: {name} debe estar entre 1 y 3600000 ms.",
4
17
  "Check /v1/models or set --model / COMAL_MODEL to skip discovery.": "Revisa /v1/models o fija --model / COMAL_MODEL para omitir el descubrimiento.",
5
18
  "Adjust {setting} if the model needs more time.": "Ajusta {setting} si el modelo necesita más tiempo.",
@@ -86,6 +99,7 @@ export default Object.freeze({
86
99
  "Display a CSV, TSV, JSON, or Markdown table": "Mostrar una tabla CSV, TSV, JSON o Markdown",
87
100
  "FILE": "ARCHIVO",
88
101
  "Change the visible name of this session": "Cambiar el nombre visible de esta sesión",
102
+ "Open another directory for reading and writing, or list the open ones": "Abrir otra carpeta para leer y escribir, o listar las abiertas",
89
103
  "NAME": "NOMBRE",
90
104
  "Manage queued messages": "Gestionar los mensajes en cola",
91
105
  "Define or control a persistent goal": "Definir o controlar un objetivo persistente",
@@ -251,7 +265,7 @@ export default Object.freeze({
251
265
  "Ready": "Listo",
252
266
  "Preparing turn": "Preparando turno",
253
267
  "Connecting to the model": "Conectando con el modelo",
254
- "# Comal · keyboard\n\n| Action | Key |\n| --- | --- |\n| Discover commands and tools | F6 |\n| Complete command or argument | / and Tab; ↑/↓ chooses; Esc closes |\n| Send / enqueue | Enter (suggestions only complete) |\n| New line | Alt+Enter or Ctrl+J |\n| Switch mode | Shift+Tab |\n| Edit with $VISUAL/$EDITOR | Ctrl+G |\n| Work board | F3 or /board |\n| Continue from checkpoint | /work resume |\n| Sessions / resume | F2 or Ctrl+S |\n| Message queue | F4 or Ctrl+Q |\n| Expand tool results | Ctrl+O |\n| Scroll history | Mouse wheel / PgUp / PgDn |\n| Select text with mouse | F5 or /select; F5 or Esc returns |\n| Copy last full response | Alt+C or /copy |\n| Go to end | Ctrl+End |\n| Composer history | ↑/↓ on first/last line |\n| Start / end of line | Home / End or Ctrl+A / Ctrl+E |\n| Delete word / start / end | Ctrl+W / Ctrl+U / Ctrl+K |\n| Cancel turn / pause goal | Ctrl+C or Esc while working |\n| Exit | Ctrl+D with empty composer or /exit |\n\nYou can keep typing during streaming. Sent messages run in order when the current turn ends. F4 lets you pause, edit, remove, and reorder the queue.\n\n/context shows the context budget. /compact derives state without LLM and waits its turn if there is active work. /state shows request and state; /audit verifies the ledger. The original history is preserved.\n/house shows execution receipts and interrupted operations; /operations shows contracts and declared effects. Execution success does not certify the human goal.\n/plugins shows plugins, dependencies, and operations; enable/disable/reload wait their turn and only the user can change their lifecycle.\n/model ID and /effort LEVEL change the next call; during work they stay queued. /models lists models and /capabilities shows support, provenance, and levels. E indicates effort; Ctx shows the effective window (k/M tokens); ~ indicates a configured limit, ? unknown/fallback capability. /context shows exact values, reserve, and source. /login PROVIDER authenticates; /logout PROVIDER removes credentials from Comal.\n\n**Help keeps the composer active.** You can type while viewing it; PgUp/PgDn scrolls it and Esc/F1 closes it. Sending returns to the conversation. The wheel scrolls the conversation; the arrows edit the draft. F5 releases the mouse and freezes the view: drag to select and use your terminal's copy action (usually Ctrl+Shift+C). Work continues and the view updates when you return. /copy preserves Markdown and code blocks, without the screen line wrapping. Multi-line paste is not sent automatically.": "# Comal · teclado\n\n| Acción | Tecla |\n| --- | --- |\n| Descubrir comandos y herramientas | F6 |\n| Completar comando o argumento | / y Tab; ↑/↓ elige; Esc cierra |\n| Enviar / encolar | Enter (las sugerencias sólo completan) |\n| Nueva línea | Alt+Enter o Ctrl+J |\n| Cambiar modo | Shift+Tab |\n| Editar con $VISUAL/$EDITOR | Ctrl+G |\n| Tablero de trabajo | F3 o /board |\n| Continuar desde checkpoint | /work resume |\n| Sesiones / reanudar | F2 o Ctrl+S |\n| Cola de mensajes | F4 o Ctrl+Q |\n| Desplegar resultados de herramientas | Ctrl+O |\n| Scroll del historial | Rueda del mouse / PgUp / PgDn |\n| Seleccionar texto con el mouse | F5 o /select; F5 o Esc vuelve |\n| Copiar última respuesta completa | Alt+C o /copy |\n| Ir al final | Ctrl+End |\n| Historial del composer | ↑/↓ en primera/última línea |\n| Inicio / final de línea | Home / End o Ctrl+A / Ctrl+E |\n| Borrar palabra / inicio / final | Ctrl+W / Ctrl+U / Ctrl+K |\n| Cancelar turno / pausar goal | Ctrl+C o Esc mientras trabaja |\n| Salir | Ctrl+D con composer vacío o /exit |\n\nPuedes seguir escribiendo durante el streaming. Los mensajes enviados se ejecutan en orden al terminar el turno actual. F4 permite pausar, editar, quitar y reordenar la cola.\n\n/context muestra el presupuesto de contexto. /compact deriva el estado sin LLM y espera su turno si hay trabajo activo. /state muestra petición y estado; /audit verifica el libro. El historial original se conserva.\n/house muestra recibos de ejecución y operaciones interrumpidas; /operations muestra contratos y efectos declarados. El éxito de ejecución no certifica el objetivo humano.\n/plugins muestra plugins, dependencias y operaciones; enable/disable/reload esperan su turno y sólo el usuario puede cambiar su ciclo de vida.\n/model ID y /effort NIVEL cambian la siguiente llamada; durante trabajo quedan en cola. /models lista modelos y /capabilities muestra soporte, procedencia y niveles. E indica esfuerzo; Ctx muestra la ventana efectiva (k/M tokens); ~ indica un límite configurado, ? capacidad desconocida/fallback. /context muestra valores exactos, reserva y fuente. /login PROVEEDOR autentica; /logout PROVEEDOR retira credenciales de Comal.\n\n**La ayuda mantiene activo el composer.** Puedes escribir mientras la consultas; PgUp/PgDn la desplaza y Esc/F1 la cierra. Al enviar vuelve la conversación. La rueda desplaza la conversación; las flechas editan el borrador. F5 libera el mouse y congela la vista: arrastra para seleccionar y usa la acción de copiar de tu terminal (normalmente Ctrl+Shift+C). El trabajo continúa y la vista se actualiza al volver. /copy conserva Markdown y bloques de código, sin el ajuste de líneas de la pantalla. El pegado multilínea no se envía automáticamente.",
268
+ "# Comal · keyboard\n\n| Action | Key |\n| --- | --- |\n| Discover commands and tools | F6 |\n| Complete command or argument | / and Tab; ↑/↓ chooses; Esc closes |\n| Send / enqueue | Enter (suggestions only complete) |\n| New line | Alt+Enter or Ctrl+J |\n| Switch mode | Shift+Tab |\n| Edit with $VISUAL/$EDITOR | Ctrl+G |\n| Work board | F3 or /board |\n| Swarm of this repo (open-relay) | /relay |\n| Swarm panel (open-relay) | /swarm · /swarm strip |\n| Continue from checkpoint | /work resume |\n| Sessions / resume | F2 or Ctrl+S |\n| Message queue | F4 or Ctrl+Q |\n| Expand tool results | Ctrl+O |\n| Scroll history | Mouse wheel / PgUp / PgDn |\n| Select text with mouse | F5 or /select; F5 or Esc returns |\n| Copy last full response | Alt+C or /copy |\n| Go to end | Ctrl+End |\n| Composer history | ↑/↓ on first/last line |\n| Start / end of line | Home / End or Ctrl+A / Ctrl+E |\n| Delete word / start / end | Ctrl+W / Ctrl+U / Ctrl+K |\n| Cancel turn / pause goal | Ctrl+C or Esc while working |\n| Exit | Ctrl+D with empty composer or /exit |\n\nYou can keep typing during streaming. Sent messages run in order when the current turn ends. F4 lets you pause, edit, remove, and reorder the queue.\n\n/context shows the context budget. /compact derives state without LLM and waits its turn if there is active work. /state shows request and state; /audit verifies the ledger. The original history is preserved.\n/house shows execution receipts and interrupted operations; /operations shows contracts and declared effects. Execution success does not certify the human goal.\n/plugins shows plugins, dependencies, and operations; enable/disable/reload wait their turn and only the user can change their lifecycle.\n/model ID and /effort LEVEL change the next call; during work they stay queued. /models lists models and /capabilities shows support, provenance, and levels. E indicates effort; Ctx shows the effective window (k/M tokens); ~ indicates a configured limit, ? unknown/fallback capability. /context shows exact values, reserve, and source. /login PROVIDER authenticates; /logout PROVIDER removes credentials from Comal.\n\n**Help keeps the composer active.** You can type while viewing it; PgUp/PgDn scrolls it and Esc/F1 closes it. Sending returns to the conversation. The wheel scrolls the conversation; the arrows edit the draft. F5 releases the mouse and freezes the view: drag to select and use your terminal's copy action (usually Ctrl+Shift+C). Work continues and the view updates when you return. /copy preserves Markdown and code blocks, without the screen line wrapping. Multi-line paste is not sent automatically.": "# Comal · teclado\n\n| Acción | Tecla |\n| --- | --- |\n| Descubrir comandos y herramientas | F6 |\n| Completar comando o argumento | / y Tab; ↑/↓ elige; Esc cierra |\n| Enviar / encolar | Enter (las sugerencias sólo completan) |\n| Nueva línea | Alt+Enter o Ctrl+J |\n| Cambiar modo | Shift+Tab |\n| Editar con $VISUAL/$EDITOR | Ctrl+G |\n| Tablero de trabajo | F3 o /board |\n| Enjambre de este repo (open-relay) | /relay |\n| Panel del enjambre (open-relay) | /swarm · /swarm strip |\n| Continuar desde checkpoint | /work resume |\n| Sesiones / reanudar | F2 o Ctrl+S |\n| Cola de mensajes | F4 o Ctrl+Q |\n| Desplegar resultados de herramientas | Ctrl+O |\n| Scroll del historial | Rueda del mouse / PgUp / PgDn |\n| Seleccionar texto con el mouse | F5 o /select; F5 o Esc vuelve |\n| Copiar última respuesta completa | Alt+C o /copy |\n| Ir al final | Ctrl+End |\n| Historial del composer | ↑/↓ en primera/última línea |\n| Inicio / final de línea | Home / End o Ctrl+A / Ctrl+E |\n| Borrar palabra / inicio / final | Ctrl+W / Ctrl+U / Ctrl+K |\n| Cancelar turno / pausar goal | Ctrl+C o Esc mientras trabaja |\n| Salir | Ctrl+D con composer vacío o /exit |\n\nPuedes seguir escribiendo durante el streaming. Los mensajes enviados se ejecutan en orden al terminar el turno actual. F4 permite pausar, editar, quitar y reordenar la cola.\n\n/context muestra el presupuesto de contexto. /compact deriva el estado sin LLM y espera su turno si hay trabajo activo. /state muestra petición y estado; /audit verifica el libro. El historial original se conserva.\n/house muestra recibos de ejecución y operaciones interrumpidas; /operations muestra contratos y efectos declarados. El éxito de ejecución no certifica el objetivo humano.\n/plugins muestra plugins, dependencias y operaciones; enable/disable/reload esperan su turno y sólo el usuario puede cambiar su ciclo de vida.\n/model ID y /effort NIVEL cambian la siguiente llamada; durante trabajo quedan en cola. /models lista modelos y /capabilities muestra soporte, procedencia y niveles. E indica esfuerzo; Ctx muestra la ventana efectiva (k/M tokens); ~ indica un límite configurado, ? capacidad desconocida/fallback. /context muestra valores exactos, reserva y fuente. /login PROVEEDOR autentica; /logout PROVEEDOR retira credenciales de Comal.\n\n**La ayuda mantiene activo el composer.** Puedes escribir mientras la consultas; PgUp/PgDn la desplaza y Esc/F1 la cierra. Al enviar vuelve la conversación. La rueda desplaza la conversación; las flechas editan el borrador. F5 libera el mouse y congela la vista: arrastra para seleccionar y usa la acción de copiar de tu terminal (normalmente Ctrl+Shift+C). El trabajo continúa y la vista se actualiza al volver. /copy conserva Markdown y bloques de código, sin el ajuste de líneas de la pantalla. El pegado multilínea no se envía automáticamente.",
255
269
  "View or configure MCP servers": "Ver o configurar servidores MCP",
256
270
  "View plugins and control their lifecycle": "Ver plugins y controlar su ciclo de vida",
257
271
  "View and control child runs": "Ver y controlar ejecuciones hijas",
@@ -317,11 +331,13 @@ export default Object.freeze({
317
331
  "The model server could not be reached.": "No se pudo contactar al servidor del modelo.",
318
332
  "The active model does not support this setting.": "El modelo activo no admite esta configuración.",
319
333
  "The change could not be verified.": "No se pudo verificar el cambio.",
334
+ "Sign in first with /login {v0}.": "Primero inicia sesión con /login {v0}.",
320
335
  "Checking /{command} {target} · active: {model}": "Comprobando /{command} {target} · activo: {model}",
321
336
  "Queued /{command} {target} · active: {model}": "En cola /{command} {target} · activo: {model}",
322
337
  "Connection applied; live-session presence could not be refreshed.": "Conexión aplicada; no se pudo actualizar la presencia de la sesión en vivo.",
323
338
  "Model catalog unavailable; the active connection is unverified. No inference was sent.": "Catálogo de modelos no disponible; la conexión activa no está verificada. No se envió ninguna inferencia.",
324
339
  "Cross-session messaging is unavailable on this platform. Local work remains available.": "Los mensajes entre sesiones no están disponibles en esta plataforma. El trabajo local sigue disponible.",
340
+ "Cross-session messaging unavailable ({v0}); continuing without peers. Use --no-peers or COMAL_PEERS=off to skip it.": "Mensajes entre sesiones no disponibles ({v0}); se continúa sin peers. Usa --no-peers o COMAL_PEERS=off para omitirlos.",
325
341
  "Attach up to two images per message.": "Adjunta hasta dos imágenes por mensaje.",
326
342
  "The session changed; the image was not attached.": "La sesión cambió; no se adjuntó la imagen.",
327
343
  "Checking vision support…": "Comprobando soporte de visión…",
@@ -351,6 +367,121 @@ export default Object.freeze({
351
367
  "Connecting {provider}": "Conectando {provider}",
352
368
  "Authorize {provider}": "Autoriza {provider}",
353
369
  "Sign in again to {provider}": "Vuelve a iniciar sesión con {provider}",
354
- "Start a new sign-in only if you choose this option.": "Inicia otra autenticación solo si eliges esta opción."
370
+ "Start a new sign-in only if you choose this option.": "Inicia otra autenticación solo si eliges esta opción.",
371
+ // Panel /swarm (open-relay, fase 4)
372
+ "Open the open-relay swarm panel": "Abrir el panel del enjambre open-relay",
373
+ "Create or open the swarm project of this repo and start a swarm (human cards)": "Crear o abrir el proyecto de enjambre de este repo y arrancar un enjambre (tarjetas humanas)",
374
+ "Swarm project of this repo; every step is a card you confirm": "Proyecto de enjambre de este repo; cada paso es una tarjeta que confirmas",
375
+ "PgUp/PgDn scrolls · lines {from}–{to} of {count}": "PgUp/PgDn desplaza · líneas {from}–{to} de {count}",
376
+ "Toggle the compact swarm summary next to the conversation": "Mostrar u ocultar el resumen compacto del enjambre junto a la conversación",
377
+ "reasoned {v0}": "razonó {v0}",
378
+ "first response {v0}": "primera respuesta {v0}",
379
+ "tools {v0}": "herramientas {v0}",
380
+ "waiting for you {v0}": "esperándote {v0}",
381
+ "Reasoning…": "Razonando…",
382
+ "Responding…": "Respondiendo…",
383
+ "Response timing on: a dim line after each answer. /timing off hides it.": "Tiempos de respuesta activos: una línea tenue después de cada respuesta. /timing off los oculta.",
384
+ "Response timing off. /timing on shows it again.": "Tiempos de respuesta ocultos. /timing on los vuelve a mostrar.",
385
+ "Use /timing [on|off].": "Usa /timing [on|off].",
386
+ "Show or hide response timing after each answer": "Mostrar u ocultar los tiempos de respuesta después de cada respuesta",
387
+ "Use /swarm or /swarm strip [on|off].": "Usa /swarm o /swarm strip [on|off].",
388
+ "Swarm strip on: a compact summary next to the conversation (terminals 110+ columns wide). /swarm strip turns it off.": "Franja del enjambre activa: un resumen compacto junto a la conversación (terminales de 110+ columnas). /swarm strip la apaga.",
389
+ "Swarm strip off.": "Franja del enjambre apagada.",
390
+ "VERIFYING": "VERIFICANDO",
391
+ "IN REVIEW": "EN REVISIÓN",
392
+ "waiting for the success criteria": "espera los criterios de éxito",
393
+ "the reviewer is correcting it": "el revisor la está corrigiendo",
394
+ "task review": "revisión de tarea",
395
+ "integration review": "revisión de integración",
396
+ "final review": "revisión final",
397
+ "reviewer objections: {n}": "objeciones del revisor: {n}",
398
+ "DONE": "HECHO",
399
+ "FAILED · CANCELLED": "FALLIDO · CANCELADO",
400
+ "OTHER": "OTROS",
401
+ "needs attention": "necesita atención",
402
+ "waiting for your answer": "espera tu respuesta",
403
+ "waiting for approval": "espera aprobación",
404
+ "waiting for a dependency": "espera una dependencia",
405
+ "no free role": "sin rol libre",
406
+ "budget exhausted": "presupuesto agotado",
407
+ "waiting for subtasks": "espera sus subtareas",
408
+ "dispatch configuration": "configuración de despacho",
409
+ "environment": "entorno",
410
+ "Dossier, then Decide: you choose the option and confirm the card": "Expediente y luego Decide: tú eliges la opción y confirmas la tarjeta",
411
+ "Dossier, then Reply: you confirm the card": "Expediente y luego Reply: tú confirmas la tarjeta",
412
+ "Message, then Reply or Ack": "Mensaje y luego Reply o Ack",
413
+ "Open the decision dossier for the open-relay structured question {id} (plugin__open-relay__Dossier {\"message_id\":\"{id}\"}) and present it to me with its options. I will choose the option myself; then propose Decide so I can confirm it on the card.": "Abre el expediente de decisión de la pregunta estructurada {id} de open-relay (plugin__open-relay__Dossier {\"message_id\":\"{id}\"}) y preséntamelo con sus opciones. Yo elijo la opción; después propón Decide para que lo confirme en la tarjeta.",
414
+ "Open the decision dossier for the open-relay question {id} (plugin__open-relay__Dossier {\"message_id\":\"{id}\"}) and present it to me. Do not reply yet: I will decide the answer and confirm it on the card.": "Abre el expediente de decisión de la pregunta {id} de open-relay (plugin__open-relay__Dossier {\"message_id\":\"{id}\"}) y preséntamelo. No respondas todavía: yo decido la respuesta y la confirmo en la tarjeta.",
415
+ "Show me the open-relay message {id} (plugin__open-relay__Message {\"message_id\":\"{id}\"}). I will decide whether to reply or acknowledge it.": "Muéstrame el mensaje {id} de open-relay (plugin__open-relay__Message {\"message_id\":\"{id}\"}). Yo decido si se responde o se da por leído.",
416
+ "Plan, then ApprovePlan or RejectPlan: you confirm the card": "Plan y luego ApprovePlan o RejectPlan: tú confirmas la tarjeta",
417
+ "Show me the open-relay plan proposal {id} (plugin__open-relay__Plan {\"proposal_id\":\"{id}\"}) with its tasks, roles and digest. I will decide whether to approve or reject it and confirm it on the card.": "Muéstrame la propuesta de plan {id} de open-relay (plugin__open-relay__Plan {\"proposal_id\":\"{id}\"}) con sus tareas, roles y digest. Yo decido si se aprueba o se rechaza y lo confirmo en la tarjeta.",
418
+ "Live watch is off (notify=off): /plugins config open-relay notify=on": "La vigilancia en vivo está apagada (notify=off): /plugins config open-relay notify=on",
419
+ "delegate {principal} via {client} · caps {caps}": "delegación de {principal} vía {client} · caps {caps}",
420
+ "delegate not confirmed yet": "delegación aún sin confirmar",
421
+ "cursor {cursor} · {events} events": "cursor {cursor} · {events} eventos",
422
+ "● connected · live (events.wait)": "● conectado · en vivo (events.wait)",
423
+ "◌ catching up with relayd": "◌ poniéndose al día con relayd",
424
+ "◌ connecting": "◌ conectando",
425
+ "◌ relayd unavailable · reconnecting": "◌ relayd no disponible · reconectando",
426
+ "✗ needs you: the delegate session is missing, revoked or invalid": "✗ te necesita: la sesión delegada falta, fue revocada o no es válida",
427
+ "✗ watch stopped": "✗ vigilancia detenida",
428
+ "◌ starting": "◌ arrancando",
429
+ "{n} reconnections": "{n} reconexiones",
430
+ "Missing capabilities: {caps} (the matching actions will be refused)": "Faltan capacidades: {caps} (relayd rechazará las acciones que las necesitan)",
431
+ "Issue it once (it never prints the token):": "Emítela una vez (nunca imprime el token):",
432
+ "The open-relay plugin is not configured in this project.": "El plugin open-relay no está configurado en este proyecto.",
433
+ "1. Issue Comal's delegate session once: relay --control-dir DIR session issue-delegate --principal YOU --client comal --caps read,converse,plans,decide --ttl 30d --out DIR/comal.token": "1. Emite una vez la sesión delegada de Comal: relay --control-dir DIR session issue-delegate --principal TU_USUARIO --client comal --caps read,converse,plans,decide --ttl 30d --out DIR/comal.token",
434
+ "2. /plugins add open-relay controlDir=DIR": "2. /plugins add open-relay controlDir=DIR",
435
+ "Guide: docs/open-relay-operator.md": "Guía: docs/open-relay-operator.md",
436
+ "The open-relay plugin is disabled.": "El plugin open-relay está desactivado.",
437
+ "/plugins enable open-relay turns it on (you review and trust it).": "/plugins enable open-relay lo enciende (tú lo revisas y le das confianza).",
438
+ "The open-relay plugin is waiting for your trust.": "El plugin open-relay espera tu confianza.",
439
+ "/plugins enable open-relay reviews it.": "/plugins enable open-relay lo revisa.",
440
+ "The open-relay plugin is starting…": "El plugin open-relay está arrancando…",
441
+ "The open-relay plugin could not start.": "El plugin open-relay no pudo arrancar.",
442
+ "This open-relay version does not publish the swarm panel. Reload it: /plugins reload open-relay": "Esta versión de open-relay no publica el panel del enjambre. Recárgalo: /plugins reload open-relay",
443
+ "The plugin sent a view that is not valid; nothing is shown.": "El plugin envió una vista no válida; no se muestra nada.",
444
+ "The swarm panel has no data yet.": "El panel del enjambre aún no tiene datos.",
445
+ "↑/↓ choose · Enter prepares the request (does not send) · Tab swarm · PgUp/PgDn · Esc returns": "↑/↓ elige · Enter prepara el pedido (no lo envía) · Tab swarm · PgUp/PgDn · Esc vuelve",
446
+ " Swarm · open-relay": " Enjambre · open-relay",
447
+ "Waiting for the open-relay plugin…": "Esperando al plugin open-relay…",
448
+ "Esc returns": "Esc vuelve",
449
+ " Swarm · {id} ({n}/{total})": " Enjambre · {id} ({n}/{total})",
450
+ "Without the live watch there is no panel data.": "Sin la vigilancia en vivo el panel no tiene datos.",
451
+ "relayd has no swarms visible to this delegation yet.": "relayd aún no tiene swarms visibles para esta delegación.",
452
+ "No swarm data yet.": "Todavía no hay datos del enjambre.",
453
+ "Teams: {teams}": "Equipos: {teams}",
454
+ "Roles: {roles}": "Roles: {roles}",
455
+ "vacant": "vacante",
456
+ " FOR YOU ({n}) · agent text, untrusted": " PARA TI ({n}) · texto de agentes, no confiable",
457
+ "Plan {id} · by {role}{age} · {tasks} tasks, {roles} roles": "Plan {id} · de {role}{age} · {tasks} tareas, {roles} roles",
458
+ "under {task}": "bajo {task}",
459
+ "Structured question": "Pregunta estructurada",
460
+ "Question": "Pregunta",
461
+ "Message ({kind})": "Mensaje ({kind})",
462
+ "blocks {task}": "bloquea {task}",
463
+ "«{question}» · options: {options}": "«{question}» · opciones: {options}",
464
+ "(agent recommends)": "(la recomienda el agente)",
465
+ " Nothing waiting for you in this swarm.": " Nada te espera en este swarm.",
466
+ "{used}/{max} attempts": "{used}/{max} intentos",
467
+ "{used}/{max} attempts (+{excused} excused)": "{used}/{max} intentos (+{excused} excusados)",
468
+ "infrastructure streak exhausted": "racha de infraestructura agotada",
469
+ "wake cap reached: only you can resume it": "tope de despertares agotado: sólo tú la reanudas",
470
+ " No tasks in this swarm yet.": " Este swarm aún no tiene tareas.",
471
+ " Some tasks or roles are not shown (panel limit). Ask for plugin__open-relay__Swarm for the full list.": " Hay tareas o roles sin mostrar (tope del panel). Pide plugin__open-relay__Swarm para la lista completa.",
472
+ "last event {seq} · {type}": "último evento {seq} · {type}",
473
+ "updated {time}": "actualizado {time}",
474
+ "ages: since Comal saw the state (≥ before the watch)": "antigüedad: desde que Comal vio el estado (≥ antes de la vigilancia)",
475
+ "Swarm": "Enjambre",
476
+ "unavailable · /swarm": "no disponible · /swarm",
477
+ "watch off": "vigilancia apagada",
478
+ "● live": "● en vivo",
479
+ "✗ needs you": "✗ te necesita",
480
+ "◌ reconnecting": "◌ reconectando",
481
+ "✗ stopped": "✗ detenida",
482
+ "{run} run · {blocked} blocked · {done} done": "{run} en curso · {blocked} bloq. · {done} hechas",
483
+ "{q} questions · {p} plans": "{q} preguntas · {p} planes",
484
+ "/swarm opens": "/swarm lo abre",
485
+ "Comal is working · Esc to see the conversation": "Comal está trabajando · Esc para ver la conversación"
355
486
 
356
487
  });
package/src/judges.mjs CHANGED
@@ -3,8 +3,9 @@ import { digest } from './operations.mjs';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { performance } from 'node:perf_hooks';
5
5
 
6
+ // Reasoning models spend part of this budget thinking before the JSON verdict.
6
7
  export async function judgeJSON(provider, system, data, signal, purpose = 'goal-evaluation', responseSchema) {
7
- const answer = await provider.complete({ system, messages: [{ role: 'user', content: JSON.stringify(data) }], tools: [], maxTokens: 2048, signal, purpose, ...(responseSchema?{responseSchema}:{}) });
8
+ const answer = await provider.complete({ system, messages: [{ role: 'user', content: JSON.stringify(data) }], tools: [], maxTokens: 8192, signal, purpose, ...(responseSchema?{responseSchema}:{}) });
8
9
  const text = answer.content.filter(b => b.type === 'text').map(b => b.text).join('').trim().replace(/^```(?:json)?\s*|\s*```$/g, '');
9
10
  const result = JSON.parse(text);
10
11
  if (!result || Array.isArray(result) || typeof result !== 'object') throw new Error('El verificador no devolvió un objeto JSON.');
package/src/mcp.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { childEnvironment } from './child-env.mjs';
1
2
  import { spawn } from 'node:child_process';
2
3
  import { createHash } from 'node:crypto';
3
4
 
@@ -12,7 +13,7 @@ export class McpClient extends McpRpcClient {
12
13
  if (!config || typeof config.command !== 'string' || !config.command || config.args && (!Array.isArray(config.args) || config.args.some(v => typeof v !== 'string'))) throw new Error(`Configuración MCP inválida: ${name}`);
13
14
  if (config.env && (typeof config.env !== 'object' || Array.isArray(config.env) || Object.values(config.env).some(v => typeof v !== 'string'))) throw new Error(`env MCP inválido: ${name}`);
14
15
  this.transport = 'stdio';
15
- this.child = spawn(config.command, config.args || [], { cwd, env: { ...process.env, ...config.env }, stdio: ['pipe', 'pipe', 'pipe'], detached: true });
16
+ this.child = spawn(config.command, config.args || [], { cwd, env: childEnvironment(process.env, config.env), stdio: ['pipe', 'pipe', 'pipe'], detached: true });
16
17
  this.child.on('error', err => this.fail(err));
17
18
  this.child.on('exit', () => this.fail(new Error(`Servidor MCP ${name} terminó.`)));
18
19
  this.child.stdin.on('error', err => this.fail(err));
@@ -10,6 +10,7 @@ export function normalizeEffort(value = 'auto') {
10
10
  if (typeof value !== 'string' || !validLevel(value)) throw fail(`Invalid effort. Use ${EFFORT_LEVELS.join(', ')}.`);
11
11
  return value;
12
12
  }
13
+ const claude5 = provider => protocol(provider) === 'anthropic' && /^claude-(?:opus|sonnet|haiku)-5(?:[-.]|$)/.test(provider.model || '');
13
14
  function protocol(provider) { return provider.protocol || (provider.url ? 'anthropic' : provider.baseURL?.includes('generativelanguage') ? 'gemini' : 'openai'); }
14
15
  function headers(provider) {
15
16
  return typeof provider.headers === 'function' ? provider.headers() : protocol(provider) === 'anthropic'
@@ -83,6 +84,11 @@ export async function discoverModelCapabilities(provider, { signal, force = fals
83
84
  const field = ['context_window','context_length','inputTokenLimit','max_input_tokens'].find(key=>Number.isSafeInteger(metadata[key]) && metadata[key]>0);
84
85
  const context = field ? metadata[field] : metadata.meta?.n_ctx;
85
86
  if (Number.isSafeInteger(context) && context > 0) { result.contextWindow = context; result.contextWindowProvenance = { source, field: field || 'meta.n_ctx', scope: ['inputTokenLimit','max_input_tokens'].includes(field) ? 'input_limit' : 'context', kind: 'detected' }; }
87
+ // Output ceiling when the catalogue states one (Gemini outputTokenLimit,
88
+ // Anthropic max_tokens, OpenAI-style max_output_tokens). It only caps the
89
+ // model-aware default output budget; an explicit --max-tokens still wins.
90
+ const outputField = ['max_output_tokens', 'outputTokenLimit', 'max_completion_tokens', 'max_tokens'].find(key => Number.isSafeInteger(metadata[key]) && metadata[key] > 0);
91
+ if (outputField) { result.outputTokenLimit = metadata[outputField]; result.outputTokenLimitProvenance = { source, field: outputField, kind: 'detected' }; }
86
92
  let levels = effortValues(metadata.supported_reasoning_efforts || metadata.reasoning_effort?.values || metadata.thinking?.values || caps?.effort?.levels);
87
93
  if (caps?.effort && typeof caps.effort === 'object') levels = [...new Set([...levels, ...EFFORT_LEVELS.filter(level => bool(caps.effort[level]) === true)])];
88
94
  if (levels.length || bool(caps?.effort) === false) result.effort = { ...result.effort, supported: levels.length > 0, levels: ['auto', ...levels], source };
@@ -156,6 +162,8 @@ export async function discoverModelCapabilities(provider, { signal, force = fals
156
162
  announced(await optional(() => json(provider, url, { signal })), `${protocol(provider)}_model`);
157
163
  }
158
164
  }
165
+ // The Anthropic Messages adapter implements tool use; catalogues do not announce it.
166
+ if (protocol(provider) === 'anthropic' && result.capabilities.tools === null) put('tools', true, 'adapter_default', 'adapter');
159
167
  // Discovery cannot add a wire feature this adapter has not implemented.
160
168
  if (protocol(provider) !== 'gemini') { put('video', false, 'adapter_unsupported', 'adapter'); put('web_search', false, 'adapter_unsupported', 'adapter'); }
161
169
  const configured = provider.capabilitiesConfigured || {};
@@ -173,7 +181,7 @@ export async function setProviderEffort(provider, value, { signal } = {}) {
173
181
  value = normalizeEffort(value);
174
182
  if (value !== 'auto') {
175
183
  const state = await discoverModelCapabilities(provider, { signal });
176
- if (!state.effort.levels.includes(value)) throw fail(`Model ${provider.model || '(no model)'} does not advertise effort ${value}. Available: ${state.effort.levels.join(', ')}.`);
184
+ if (!state.effort.levels.includes(value) && !(value === 'off' && claude5(provider) && state.effort.levels.includes('low'))) throw fail(`Model ${provider.model || '(no model)'} does not advertise effort ${value}. Available: ${state.effort.levels.join(', ')}.`);
177
185
  }
178
186
  provider.effort = value;
179
187
  if (Object.hasOwn(provider, 'thinking')) provider.thinking = 'auto';
@@ -183,7 +191,13 @@ export async function providerEffortWire(provider, { signal } = {}) {
183
191
  const value = normalizeEffort(provider.effort || 'auto');
184
192
  if (value === 'auto') return {};
185
193
  await setProviderEffort(provider, value, { signal });
186
- if (protocol(provider) === 'anthropic') return value === 'off' ? { thinking: { type: 'disabled' } } : { output_config: { effort: value } };
194
+ if (protocol(provider) === 'anthropic') {
195
+ // Claude 5.x rejects thinking.disabled and budget_tokens (HTTP 400); thinking is
196
+ // adaptive by default there, so Comal only sends output_config.effort.
197
+ // effort off there means the lowest effort, never thinking.disabled.
198
+ if (claude5(provider)) return { output_config: { effort: value === 'off' ? 'low' : value } };
199
+ return value === 'off' ? { thinking: { type: 'disabled' } } : { output_config: { effort: value } };
200
+ }
187
201
  if (protocol(provider) === 'gemini') return { thinkingConfig: { thinkingLevel: value.toUpperCase() } };
188
202
  if (['server_template_literal', 'server_props'].includes(provider.modelCapabilities?.effort.source)) return { chat_template_kwargs: value === 'off' ? { enable_thinking: false } : { enable_thinking: true, reasoning_effort: value }, ...(value === 'off' ? { reasoning_effort: 'none' } : {}) };
189
203
  return { reasoning_effort: value === 'off' ? 'none' : value };