@milpa/comal 0.2.2 → 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.
- package/README.md +1 -1
- package/backend/contracts/remote-commands-v1.json +0 -12
- package/backend/contracts/remote-relay-answers-v1.json +209 -0
- package/package.json +3 -1
- package/skills/mv3/SKILL.md +44 -0
- package/skills/mv3/skill.json +4 -0
- package/src/agent.mjs +299 -32
- package/src/auto-guard.mjs +40 -3
- package/src/autocompact.mjs +233 -0
- package/src/bash-timeouts.mjs +29 -0
- package/src/child-env.mjs +23 -0
- package/src/cli.mjs +213 -55
- package/src/code-run-artifact.mjs +30 -0
- package/src/code-run-journal.mjs +135 -0
- package/src/code-run-session-proof.mjs +50 -0
- package/src/code-run-session.mjs +97 -0
- package/src/code-run-worker.mjs +20 -4
- package/src/code-run.mjs +49 -14
- package/src/codex-provider.mjs +4 -4
- package/src/compaction-notice.mjs +22 -0
- package/src/compaction.mjs +162 -23
- package/src/connection-settings.mjs +36 -4
- package/src/context-pressure.mjs +5 -4
- package/src/context.mjs +27 -22
- package/src/execution-metrics.mjs +36 -5
- package/src/file-recovery-proof.mjs +9 -1
- package/src/file-recovery.mjs +36 -3
- package/src/gemini-provider.mjs +5 -3
- package/src/git-integration-operations.mjs +2 -1
- package/src/git-plumbing.mjs +2 -1
- package/src/git-source.mjs +2 -1
- package/src/git-workspaces.mjs +2 -1
- package/src/house.mjs +13 -1
- package/src/i18n/cli-es.mjs +12 -1
- package/src/i18n/es.mjs +38 -1
- package/src/judges.mjs +2 -1
- package/src/mcp.mjs +2 -1
- package/src/model-controls.mjs +16 -2
- package/src/model-deadline.mjs +2 -2
- package/src/openai-provider.mjs +8 -4
- package/src/operations.mjs +17 -2
- package/src/package-identity.mjs +68 -0
- package/src/peer-probe.mjs +16 -0
- package/src/permissions.mjs +97 -4
- package/src/plugin-config.mjs +18 -1
- package/src/plugin-controls.mjs +31 -8
- package/src/plugin-trust.mjs +15 -6
- package/src/plugins/chrome-lab/bridge.mjs +19 -6
- package/src/plugins/chrome-lab/client.mjs +242 -12
- package/src/plugins/chrome-lab/index.mjs +106 -24
- package/src/plugins/chrome-lab/legacy-fill.mjs +25 -0
- package/src/plugins/chrome-lab/native-reply.mjs +31 -0
- package/src/plugins/open-relay/actions.mjs +60 -9
- package/src/plugins/open-relay/client.mjs +3 -3
- package/src/plugins/open-relay/decision.mjs +3 -2
- package/src/plugins/open-relay/index.mjs +17 -7
- package/src/plugins/open-relay/lookup.mjs +41 -4
- package/src/plugins/open-relay/notices.mjs +44 -3
- package/src/plugins/open-relay/panel.mjs +4 -1
- package/src/plugins/open-relay/watch.mjs +27 -8
- package/src/process-lock.mjs +20 -12
- package/src/provider-auth.mjs +6 -3
- package/src/provider-profiles.mjs +13 -3
- package/src/provider.mjs +175 -30
- package/src/read-only-check.mjs +6 -1
- package/src/relay/broker.mjs +160 -0
- package/src/relay/cards.mjs +702 -0
- package/src/relay/flow.mjs +886 -0
- package/src/relay/follow.mjs +119 -0
- package/src/relay/git.mjs +23 -0
- package/src/relay/hub-client.mjs +163 -0
- package/src/relay/state.mjs +55 -0
- package/src/remote-control.mjs +115 -12
- package/src/remote-relay.mjs +468 -0
- package/src/session-bus.mjs +13 -3
- package/src/session-provider-settings.mjs +2 -2
- package/src/skill-runtime.mjs +8 -6
- package/src/source-ownership.mjs +3 -1
- package/src/tool-manifest.mjs +71 -0
- package/src/tool-packs.mjs +22 -6
- package/src/tool-schema.mjs +14 -2
- package/src/tools.mjs +96 -27
- package/src/tui/controller.mjs +4 -1
- package/src/tui/discovery.mjs +11 -7
- package/src/tui/inbound-blocks.mjs +5 -1
- package/src/tui/screen.mjs +53 -14
- package/src/tui/swarm-panel.mjs +19 -8
- package/src/turn-timing.mjs +82 -0
- package/src/usage-accounting.mjs +12 -1
- package/src/vision.mjs +6 -1
- package/src/visual/chrome.mjs +379 -0
- package/src/visual/fingerprint.mjs +30 -0
- package/src/visual/fixtures.mjs +201 -0
- package/src/visual/gate.mjs +92 -0
- package/src/visual/judge.mjs +114 -0
- package/src/visual/ledger.mjs +160 -0
- package/src/visual/measure.mjs +145 -0
- package/src/visual/names.mjs +7 -0
- package/src/visual/operations.mjs +211 -0
- package/src/visual/pixel-diff.mjs +96 -0
- package/src/visual/png.mjs +103 -0
- package/src/visual/registry.mjs +265 -0
- package/src/visual/report.mjs +64 -0
- package/src/visual/verify.mjs +292 -0
- package/src/work-assertions.mjs +15 -4
- package/src/work-plan.mjs +166 -30
- package/src/work-state.mjs +4 -4
- package/src/workflows/chrome-task-adoption.mjs +7 -3
- package/src/workspace-serve.mjs +59 -0
package/src/file-recovery.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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);
|
package/src/gemini-provider.mjs
CHANGED
|
@@ -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:
|
|
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'};
|
package/src/git-plumbing.mjs
CHANGED
|
@@ -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(
|
|
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;
|
package/src/git-source.mjs
CHANGED
|
@@ -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(
|
|
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=[]){
|
package/src/git-workspaces.mjs
CHANGED
|
@@ -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:
|
|
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);
|
package/src/i18n/cli-es.mjs
CHANGED
|
@@ -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| 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| 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.",
|
|
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",
|
|
@@ -323,6 +337,7 @@ export default Object.freeze({
|
|
|
323
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.",
|
|
324
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.",
|
|
325
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.",
|
|
326
341
|
"Attach up to two images per message.": "Adjunta hasta dos imágenes por mensaje.",
|
|
327
342
|
"The session changed; the image was not attached.": "La sesión cambió; no se adjuntó la imagen.",
|
|
328
343
|
"Checking vision support…": "Comprobando soporte de visión…",
|
|
@@ -355,11 +370,31 @@ export default Object.freeze({
|
|
|
355
370
|
"Start a new sign-in only if you choose this option.": "Inicia otra autenticación solo si eliges esta opción.",
|
|
356
371
|
// Panel /swarm (open-relay, fase 4)
|
|
357
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}",
|
|
358
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",
|
|
359
387
|
"Use /swarm or /swarm strip [on|off].": "Usa /swarm o /swarm strip [on|off].",
|
|
360
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.",
|
|
361
389
|
"Swarm strip off.": "Franja del enjambre apagada.",
|
|
362
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}",
|
|
363
398
|
"DONE": "HECHO",
|
|
364
399
|
"FAILED · CANCELLED": "FALLIDO · CANCELADO",
|
|
365
400
|
"OTHER": "OTROS",
|
|
@@ -429,7 +464,9 @@ export default Object.freeze({
|
|
|
429
464
|
"(agent recommends)": "(la recomienda el agente)",
|
|
430
465
|
" Nothing waiting for you in this swarm.": " Nada te espera en este swarm.",
|
|
431
466
|
"{used}/{max} attempts": "{used}/{max} intentos",
|
|
467
|
+
"{used}/{max} attempts (+{excused} excused)": "{used}/{max} intentos (+{excused} excusados)",
|
|
432
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",
|
|
433
470
|
" No tasks in this swarm yet.": " Este swarm aún no tiene tareas.",
|
|
434
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.",
|
|
435
472
|
"last event {seq} · {type}": "último evento {seq} · {type}",
|
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:
|
|
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:
|
|
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));
|
package/src/model-controls.mjs
CHANGED
|
@@ -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')
|
|
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 };
|
package/src/model-deadline.mjs
CHANGED
|
@@ -10,14 +10,14 @@ export function modelTimingSettings(options = {}, env = process.env) {
|
|
|
10
10
|
return { timeoutMs, idleTimeoutMs };
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function modelDeadline({ signal, timeoutMs, idleTimeoutMs, purpose = 'worker' }) {
|
|
13
|
+
export function modelDeadline({ signal, timeoutMs, idleTimeoutMs, purpose = 'worker', provider = 'OpenAI' }) {
|
|
14
14
|
const started = Date.now(), controller = new AbortController(); let idleTimer, expired;
|
|
15
15
|
let lastActivity = started, stage = 'waiting';
|
|
16
16
|
const expire = kind => {
|
|
17
17
|
if (controller.signal.aborted) return;
|
|
18
18
|
const elapsedMs = Date.now() - started;
|
|
19
19
|
const guidance = purpose === 'model-discovery' ? t('Check /v1/models or set --model / COMAL_MODEL to skip discovery.') : t('Adjust {setting} if the model needs more time.', { setting: kind === 'idle' ? '--model-idle-timeout-ms / COMAL_MODEL_IDLE_TIMEOUT_MS' : '--model-timeout-ms / COMAL_MODEL_TIMEOUT_MS' });
|
|
20
|
-
const error = new Error(t('
|
|
20
|
+
const error = new Error(t('{provider} model exceeded {limit} after {seconds} s ({stage}). No tools from this response were executed. {guidance}', { provider, limit: kind === 'idle' ? t('the idle timeout') : t('the total timeout'), seconds: Math.round(elapsedMs / 1000), stage: stage === 'waiting' ? t('waiting for tokens') : t('generating a response'), guidance }));
|
|
21
21
|
error.code = 'MODEL_TIMEOUT'; error.details = { kind, purpose, stage, elapsed_ms: elapsedMs, idle_ms: Date.now() - lastActivity, timeout_ms: timeoutMs, idle_timeout_ms: idleTimeoutMs };
|
|
22
22
|
expired = error; controller.abort(error);
|
|
23
23
|
};
|
package/src/openai-provider.mjs
CHANGED
|
@@ -62,7 +62,7 @@ export function toOpenAIMessages(system, messages, { reasoningField = 'reasoning
|
|
|
62
62
|
|
|
63
63
|
function normalizedMessage({ text = '', reasoning = '', calls = [], finish, usage = {} }) {
|
|
64
64
|
if (typeof text !== 'string' || typeof reasoning !== 'string' || !Array.isArray(calls)) throw new Error('Invalid OpenAI content.');
|
|
65
|
-
if (!finish) throw new Error('OpenAI
|
|
65
|
+
if (!finish) throw new Error('OpenAI response has no finish_reason; no tools from this response were executed.');
|
|
66
66
|
if (finish === 'length') {
|
|
67
67
|
const error = new Error('OpenAI response truncated (length): generation budget or server capacity exhausted; no tools were executed. Use /compact and --max-tokens, or split the script into smaller calls.');
|
|
68
68
|
error.code = 'OUTPUT_LENGTH'; error.details = { text_chars: text.length, reasoning_chars: reasoning.length, tool_argument_chars: calls.reduce((sum, call) => sum + (typeof call?.function?.arguments === 'string' ? call.function.arguments.length : 0), 0), ...(typeof calls.at(-1)?.function?.name === 'string' && calls.at(-1).function.name ? { tool_name: calls.at(-1).function.name } : {}) }; error.usage_accounting = normalizeUsage('openai',usage); error.usage = { input_tokens: usage.prompt_tokens || 0, output_tokens: usage.completion_tokens || 0 }; throw error;
|
|
@@ -173,7 +173,7 @@ export class OpenAIProvider {
|
|
|
173
173
|
this.model = model; return model;
|
|
174
174
|
} catch (error) { throw deadline.error(error); } finally { deadline.close(); }
|
|
175
175
|
}
|
|
176
|
-
async complete({ system, messages, tools = [], schemaCatalog = [], contextIdentity, signal, onText = () => {}, onProgress = () => {}, purpose = 'worker', maxTokens = this.maxTokens, responseSchema }) {
|
|
176
|
+
async complete({ system, messages, tools = [], schemaCatalog = [], contextIdentity, signal, onText = () => {}, onProgress = () => {}, onReasoning = () => {}, onToolCall = () => {}, purpose = 'worker', maxTokens = this.maxTokens, responseSchema }) {
|
|
177
177
|
await this.discoverModel({ signal });
|
|
178
178
|
const accountingRequest={system,messages,tools,maxTokens,...(responseSchema?{responseSchema}:{})}, identity={...accountingIdentity(this),projection_epoch:contextIdentity?.projection_epoch ?? null};
|
|
179
179
|
const schemaFrame=compileToolSchemas(tools,{provider:this.protocol,capabilities:{...(this.getCapabilities?.() || {}),...(this.toolSchemaCapabilities?{tool_schema:this.toolSchemaCapabilities}:{})},catalogue:schemaCatalog}); tools=schemaFrame.tools;
|
|
@@ -219,14 +219,18 @@ export class OpenAIProvider {
|
|
|
219
219
|
let active = false;
|
|
220
220
|
if (typeof delta.content === 'string') { text += delta.content; onText(delta.content); progress.text_chars += delta.content.length; active ||= !!delta.content.length; }
|
|
221
221
|
const reasoningDelta = this.reasoningOf(delta);
|
|
222
|
-
if (reasoningDelta) { reasoning += reasoningDelta; progress.reasoning_chars += reasoningDelta.length; active = true; }
|
|
222
|
+
if (reasoningDelta) { reasoning += reasoningDelta; progress.reasoning_chars += reasoningDelta.length; active = true; onReasoning(); }
|
|
223
223
|
for (const part of delta.tool_calls || []) {
|
|
224
224
|
progress.tool_argument_chars += calls.add(part);
|
|
225
225
|
active ||= !!(part.id || part.function?.name || part.function?.arguments);
|
|
226
|
+
if (part.id || part.function?.name) onToolCall();
|
|
226
227
|
}
|
|
227
228
|
if (active || choice.finish_reason) { deadline.touch(); report(!!choice.finish_reason); }
|
|
228
229
|
}
|
|
229
230
|
deadline.signal.throwIfAborted();
|
|
231
|
+
// HTTP 200 whose SSE body ends without finish_reason (no [DONE], no usage) is a
|
|
232
|
+
// transport cut, not an answer: transient and retryable; nothing from it runs.
|
|
233
|
+
if (!finish) throw Object.assign(new Error('OpenAI streaming was interrupted before finish_reason; the incomplete response was discarded before any tool execution.'), { code: 'MODEL_STREAM_ERROR', details: { ...progress } });
|
|
230
234
|
return account(normalizedMessage({ text, reasoning, finish, usage, calls: calls.calls() }));
|
|
231
235
|
} catch (error) {
|
|
232
236
|
if(responseAccepted&&!deadline.signal.aborted&&(error.name==='SyntaxError'||error.name==='TypeError'&&(error.message==='terminated'||['UND_ERR_SOCKET','ECONNRESET','ERR_STREAM_PREMATURE_CLOSE'].includes(error.cause?.code))))error=Object.assign(new Error('The OpenAI response stream ended or was malformed; the incomplete response was discarded before any tool execution.'),{code:'MODEL_STREAM_ERROR'});
|
|
@@ -250,7 +254,7 @@ export function providerSettings(options = {}, env = process.env, session) {
|
|
|
250
254
|
thinking: options.thinking || env.COMAL_THINKING || 'auto',
|
|
251
255
|
effort: options.effort || env.COMAL_EFFORT || 'auto',
|
|
252
256
|
baseURL: options.baseURL || env.COMAL_BASE_URL || (provider === 'openai' ? env.OPENAI_BASE_URL || DEFAULT_LOCAL_URL : env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com'),
|
|
253
|
-
model: options.model || env.COMAL_MODEL || (provider === 'openai' ? env.OPENAI_MODEL : undefined) || (sameProvider ? session.model : undefined) || (provider === 'anthropic' ? 'claude-sonnet-
|
|
257
|
+
model: options.model || env.COMAL_MODEL || (provider === 'openai' ? env.OPENAI_MODEL : undefined) || (sameProvider ? session.model : undefined) || (provider === 'anthropic' ? 'claude-sonnet-5-5' : undefined),
|
|
254
258
|
apiKey: provider === 'openai' ? env.OPENAI_API_KEY : env.ANTHROPIC_API_KEY,
|
|
255
259
|
};
|
|
256
260
|
}
|