@jaimevalasek/aioson 1.37.0 → 1.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/agent-execution/adapters/base.js +15 -0
- package/src/agent-execution/adapters/claude.js +3 -0
- package/src/agent-execution/adapters/codex.js +3 -0
- package/src/agent-execution/adapters/opencode.js +3 -0
- package/src/agent-execution/capabilities.js +9 -0
- package/src/agent-execution/dispatcher.js +72 -0
- package/src/agent-execution/executable-resolver.js +7 -0
- package/src/agent-execution/manifest.js +62 -0
- package/src/agent-execution/model-catalog.js +80 -0
- package/src/agent-execution/model-resolver.js +132 -0
- package/src/agent-execution/reports.js +9 -0
- package/src/agent-execution/schema.js +48 -0
- package/src/agent-execution/telemetry-bridge.js +15 -0
- package/src/cli.js +22 -4
- package/src/commands/agent-execution.js +36 -0
- package/src/commands/op-capture.js +33 -3
- package/src/commands/op-reinforce.js +10 -22
- package/src/commands/verification-plan.js +28 -11
- package/src/commands/workflow-execute.js +20 -6
- package/src/harness/criteria-runner.js +4 -1
- package/src/operator-memory/decision.js +41 -0
- package/src/runtime-store.js +167 -8
- package/template/.aioson/agents/dev.md +1 -1
- package/template/.aioson/agents/product.md +2 -1
- package/template/.aioson/docs/autopilot-handoff.md +7 -1
- package/template/.aioson/docs/dev/phase-loop.md +2 -3
- package/template/.aioson/schemas/agent-execution.schema.json +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [1.37.1] - 2026-07-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Intelligent, bounded resolution of human-readable and approximate Codex model names.
|
|
11
|
+
- Separate reasoning-effort propagation and model-resolution metadata across execution, reports, telemetry, and verification.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **Operator-memory promotion threshold is now per signal type.** `authorization`, `exclusion`, and `correction` are single explicit standing decisions and promote to a decision on **first** detection; `confirmation` still needs **2×** (it must repeat to distinguish a pattern from a one-off acceptance). Previously all four required 2× (`op-capture.js`), so a firmly-stated one-shot correction/authorization never persisted unless the agent emitted `op:capture` twice. Aligns the storage engine with the signal taxonomy already documented in `agents/_shared/memory-capture-directive.md`. New `promotionThresholdFor(signalType)` helper.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Re-detecting an already-promoted decision no longer duplicates its FTS row or resets `promoted_at`.** `op:capture` now reinforces in place — bumps `reinforcement_count` + refreshes `last_reinforced` (PMD-11) via the new `decision.reinforceDecision` primitive — instead of re-running promotion when a decision for the slug already exists (the stray proposal from re-detection is dropped). This latent double-insert also affected the old uniform-2× flow on the 3rd+ detection of the same signal.
|
|
18
|
+
|
|
5
19
|
## [1.37.0] - 2026-07-06
|
|
6
20
|
|
|
7
21
|
**Full-feature autopilot + CLI-owned briefing refinement.** Two independent arcs land together. A feature built the normal way (`@product` → `@sheldon`/`@orchestrator` → `@dev`) can now run unattended through `@qa`/`@tester`/`@pentester`/`@validator` to `feature:close` — stopping only at genuine human decisions; `@product` asks the run mode once at kickoff (no hidden flag), settable inline (`--auto`/`--step`) or disarmed per feature. Separately, `@briefing-refiner`'s review surface stops being hand-written every round: the CLI now owns the schema, the render, and the apply, closing an explicit iterate-until-clean loop.
|
package/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { spawn } = require('node:child_process');
|
|
3
|
+
const { capabilities, requiredCapability } = require('../capabilities');
|
|
4
|
+
const { resolveExecutable } = require('../executable-resolver');
|
|
5
|
+
function redact(text) { return String(text||'').replace(/(authorization\s*[:=]\s*(?:bearer\s+)?|api[_-]?key\s*[:=]\s*|token\s*[:=]\s*|password\s*[:=]\s*)[^\s,;]+/gi,'$1[REDACTED]'); }
|
|
6
|
+
function normalizeError(error) { const text=String(error?.message||error||'').toLowerCase(); if(/unexpected argument|invalid (?:argument|option)|unknown option/.test(text))return'invalid_arguments';if(/capacity|overloaded|rate limit/.test(text)) return 'capacity'; if(/model.*invalid|unknown model/.test(text)) return 'invalid_model'; if(/unauthorized|authentication failed|not authenticated|forbidden/.test(text)) return 'auth'; if(/timeout|timed out/.test(text)) return 'timeout'; return 'crash'; }
|
|
7
|
+
function createAdapter(host, buildArgs) {
|
|
8
|
+
return {
|
|
9
|
+
host,
|
|
10
|
+
probe() { return capabilities(host); },
|
|
11
|
+
build(input) { const cap=this.probe(); const required=requiredCapability(input.mode); if(required && !cap[required]) return {ok:false,reason:'unsupported_capability',capability:required,host};if(input.reasoning_effort&&!cap.reasoning_effort)return{ok:false,reason:'unsupported_reasoning_effort',capability:'reasoning_effort',host};if(input.writable_roots?.length&&!cap.additional_workspaces)return{ok:false,reason:'host_capability_missing',capability:'additional_workspaces',host};const command=buildArgs(input);const args=Array.isArray(command)?command:command.args;return {ok:true,executable:cap.executable,args,stdin:Boolean(command.stdin),options:{cwd:input.cwd,shell:false,stdio:'pipe'}}; },
|
|
12
|
+
async execute(input) { const built=this.build(input); if(!built.ok) return built;let resolved;try{resolved=await resolveExecutable(built.executable,input.resolverOptions)}catch(error){return{ok:false,reason:'executable_not_found',error:redact(error.message)}}return new Promise(resolve=>{ const child=spawn(resolved.executable,[...resolved.prefixArgs,...built.args],built.options); input.onSpawn?.(child.pid,new Date().toISOString());let stderr='';let timedOut=false;let settled=false;child.stdout.on('data',d=>input.onStdout?.(d));child.stderr.on('data',d=>{input.onStderr?.(d);stderr+=d;if(stderr.length>4000)stderr=stderr.slice(-4000)});if(built.stdin)child.stdin.end(String(input.prompt_text||''));else child.stdin.end();const finish=result=>{if(settled)return;settled=true;if(timer)clearTimeout(timer);child.stdout.destroy();child.stderr.destroy();input.onExit?.(result);resolve(result)};const timer=input.timeout?setTimeout(()=>{timedOut=true;child.kill()},input.timeout):null;child.on('error',e=>finish({ok:false,reason:normalizeError(e),error:redact(e.message)}));child.on('exit',code=>finish(code===0&&!timedOut?{ok:true,code,resolver_source:resolved.source}:{ok:false,code,reason:timedOut?'timeout':normalizeError(stderr),error:redact(stderr).slice(0,1000),resolver_source:resolved.source})); }); }
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
module.exports={createAdapter,normalizeError,redact};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { createAdapter } = require('./base');
|
|
3
|
+
module.exports=createAdapter('codex',i=>({args:['exec',...(i.model==='configured-default'?[]:['--model',i.model]),...(i.reasoning_effort?['-c',`model_reasoning_effort="${i.reasoning_effort}"`]:[]),...(i.writable_roots||[]).flatMap(root=>['--add-dir',root]),'-'],stdin:true}));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const MATRIX = {
|
|
3
|
+
claude: { native_subagent: false, fresh_session: false, external_process: true, additional_workspaces: true, model_catalog: false, reasoning_effort: false, executable: 'claude' },
|
|
4
|
+
codex: { native_subagent: false, fresh_session: false, external_process: true, additional_workspaces: true, model_catalog: true, reasoning_effort: true, executable: 'codex' },
|
|
5
|
+
opencode: { native_subagent: false, fresh_session: false, external_process: true, additional_workspaces: false, model_catalog: false, reasoning_effort: false, executable: 'opencode' }
|
|
6
|
+
};
|
|
7
|
+
function capabilities(host) { return { ...(MATRIX[host] || {}), source: 'registered_adapter' }; }
|
|
8
|
+
function requiredCapability(mode) { return mode === 'subagent' ? 'native_subagent' : mode === 'fresh-session' ? 'fresh_session' : mode === 'external' ? 'external_process' : null; }
|
|
9
|
+
module.exports = { MATRIX, capabilities, requiredCapability };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs=require('node:fs/promises');const path=require('node:path');const crypto=require('node:crypto');
|
|
3
|
+
const adapters={claude:require('./adapters/claude'),codex:require('./adapters/codex'),opencode:require('./adapters/opencode')};
|
|
4
|
+
const {loadManifest,resolveAgentExecution,resolveExecutionEntry}=require('./manifest');
|
|
5
|
+
const {safeReportPath,validateReport}=require('./reports');
|
|
6
|
+
const {createTelemetryBridge}=require('./telemetry-bridge');
|
|
7
|
+
const TERMINAL=['approved','failed','cancelled'];const LEASE_MS=30000;
|
|
8
|
+
function statePath(projectDir,feature){return path.join(projectDir,'.aioson','context',`agent-execution-state-${feature}.json`)}
|
|
9
|
+
function leasePath(projectDir,feature){return `${statePath(projectDir,feature)}.lock`}
|
|
10
|
+
async function atomic(file,value){await fs.mkdir(path.dirname(file),{recursive:true});const tmp=`${file}.${process.pid}.${crypto.randomUUID()}.tmp`;await fs.writeFile(tmp,`${JSON.stringify(value,null,2)}\n`);await fs.rename(tmp,file)}
|
|
11
|
+
async function readState(projectDir,feature){try{return JSON.parse(await fs.readFile(statePath(projectDir,feature),'utf8'))}catch(e){if(e.code==='ENOENT')return null;throw e}}
|
|
12
|
+
async function acquireLease(projectDir,feature){const file=leasePath(projectDir,feature);await fs.mkdir(path.dirname(file),{recursive:true});const owner=crypto.randomUUID();for(let pass=0;pass<2;pass++){try{const h=await fs.open(file,'wx');await h.writeFile(JSON.stringify({owner,expires_at:Date.now()+LEASE_MS}));await h.close();return{file,owner}}catch(e){if(e.code!=='EEXIST')throw e;try{const current=JSON.parse(await fs.readFile(file,'utf8'));if(current.expires_at>Date.now())return null;await fs.unlink(file)}catch(readError){if(readError.code!=='ENOENT'&&readError.name!=='SyntaxError')throw readError}}}return null}
|
|
13
|
+
async function releaseLease(lease){if(!lease)return;try{const current=JSON.parse(await fs.readFile(lease.file,'utf8'));if(current.owner===lease.owner)await fs.unlink(lease.file)}catch(e){if(e.code!=='ENOENT')throw e}}
|
|
14
|
+
async function safePromptPath(projectDir,promptPath){const root=await fs.realpath(projectDir);const candidate=path.resolve(root,promptPath||'.aioson/context/dev-state.md');const real=await fs.realpath(candidate);if(real!==root&&!real.startsWith(root+path.sep))throw new Error('prompt_path escapes project workspace');const stat=await fs.stat(real);if(!stat.isFile())throw new Error('prompt_path must be a regular file');return real}
|
|
15
|
+
async function resolveWritableRoots(projectDir,roots=[]){const resolved=[];for(const configured of roots){if(typeof configured!=='string'||!configured.trim())throw new Error('writable_root must be a non-empty path');if(configured.split(/[\\/]+/).includes('..'))throw new Error(`writable_root traversal is forbidden: ${configured}`);const candidate=path.resolve(projectDir,configured);let real;try{real=await fs.realpath(candidate)}catch{throw new Error(`writable_root does not exist: ${configured}`)}if(!(await fs.stat(real)).isDirectory())throw new Error(`writable_root is not a directory: ${configured}`);if(!resolved.includes(real))resolved.push(real)}return resolved}
|
|
16
|
+
function capacitySequence(manifest,resolved){const policy=manifest.capacity_policy;if(policy.strategy!=='fallback')return[];return resolved.fallbacks.filter(item=>policy.allow_cross_host===true||item.host===resolved.host)}
|
|
17
|
+
function applyCapacityPolicy(manifest,resolved,attemptCount=0){const p=manifest.capacity_policy;if(attemptCount>=p.max_attempts)return{action:'pause',reason:'capacity_limit'};if(p.strategy==='fallback'){const sequence=capacitySequence(manifest,resolved);return sequence.length?{action:'fallback',sequence}:{action:'pause',reason:'no_authorized_fallback'}}if(p.strategy==='retry'||p.strategy==='wait')return{action:p.strategy,backoff_ms:p.backoff_ms};return{action:'pause'}}
|
|
18
|
+
async function executeWithCapacityPolicy({manifest,resolved,input,adapterRegistry=adapters,catalogLoader,wait=(ms)=>new Promise(resolve=>setTimeout(resolve,ms))}) {
|
|
19
|
+
const history=[];
|
|
20
|
+
const primary=resolved.ok?resolved:await resolveExecutionEntry(resolved,{catalogLoader});
|
|
21
|
+
if(!primary.ok)return{ok:false,reason:primary.reason,candidates:primary.candidates||[],history};
|
|
22
|
+
const candidates=[primary,...capacitySequence(manifest,resolved).map(item=>({...resolved,ok:false,host:item.host,model:item.model,model_requested:undefined}))];
|
|
23
|
+
let index=0;
|
|
24
|
+
for(let count=0;count<manifest.capacity_policy.max_attempts;count++){
|
|
25
|
+
let candidate=candidates[index];
|
|
26
|
+
if(!candidate.ok){
|
|
27
|
+
candidate=await resolveExecutionEntry(candidate,{catalogLoader});
|
|
28
|
+
candidates[index]=candidate;
|
|
29
|
+
if(!candidate.ok){
|
|
30
|
+
history.push({attempt:count+1,host:candidate.host,model_requested:candidate.model_requested||candidate.model,model:null,reason:candidate.reason});
|
|
31
|
+
return{ok:false,reason:candidate.reason,candidates:candidate.candidates||[],history};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const adapter=adapterRegistry[candidate.host];
|
|
35
|
+
if(!adapter)return{ok:false,reason:'unsupported_host',history};
|
|
36
|
+
const prompt_text=String(input.prompt_text||'')
|
|
37
|
+
.replace(/host=[^,\n]+/,`host=${candidate.host}`)
|
|
38
|
+
.replace(/model_resolved=[^,\n]+/,`model_resolved=${candidate.model}`)
|
|
39
|
+
.replace(/model_resolution_strategy=[^,\n]+/,`model_resolution_strategy=${candidate.model_resolution_strategy}`);
|
|
40
|
+
const result=await adapter.execute({...input,prompt_text,mode:candidate.mode,model:candidate.model,reasoning_effort:candidate.reasoning_effort});
|
|
41
|
+
history.push({attempt:count+1,host:candidate.host,model_requested:candidate.model_requested,model:candidate.model,model_resolution_strategy:candidate.model_resolution_strategy,reasoning_effort:candidate.reasoning_effort,reason:result.reason||null});
|
|
42
|
+
if(result.ok)return{...result,host:candidate.host,model:candidate.model,model_requested:candidate.model_requested,model_resolution_strategy:candidate.model_resolution_strategy,reasoning_effort:candidate.reasoning_effort,history};
|
|
43
|
+
if(result.reason!=='capacity')return{...result,history};
|
|
44
|
+
const decision=applyCapacityPolicy(manifest,resolved,count);
|
|
45
|
+
if(decision.action==='pause')return{ok:false,reason:decision.reason||'capacity',history};
|
|
46
|
+
if(decision.action==='fallback'){
|
|
47
|
+
if(index+1>=candidates.length)return{ok:false,reason:'fallback_exhausted',history};
|
|
48
|
+
index+=1;
|
|
49
|
+
}else if(decision.action==='wait'||decision.action==='retry'){
|
|
50
|
+
if(decision.backoff_ms)await wait(decision.backoff_ms);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return{ok:false,reason:'capacity_limit',history};
|
|
54
|
+
}
|
|
55
|
+
function buildExecutionPrompt(promptText,expected,reportPath){return `${promptText}\n\nAIOSON EXECUTION CONTRACT\nComplete the requested agent work, then write exactly one JSON report to: ${reportPath}\nThe report must include: version=1, feature=${expected.feature}, run_id=${expected.run_id}, attempt_id=${expected.attempt_id}, agent=${expected.agent}, host=${expected.host}, model_requested=${expected.model_requested}, model_resolved=${expected.model_resolved}, model_resolution_strategy=${expected.model_resolution_strategy}, reasoning_effort=${expected.reasoning_effort||'null'}, manifest_digest=${expected.manifest_digest}, writable_roots=${JSON.stringify(expected.writable_roots)}, started_at, finished_at, verdict (PASS|FAIL|BLOCKED), findings[], evidence[]. Do not report PASS unless the work and verification completed.`}
|
|
56
|
+
async function readBoundReport(projectDir,relative,expected){const file=safeReportPath(projectDir,relative);let report;try{report=JSON.parse(await fs.readFile(file,'utf8'))}catch(error){return{ok:false,reason:error.code==='ENOENT'?'report_missing':'report_invalid_json',error:error.message}}const validation=validateReport(report,expected);return validation.ok?{ok:true,file,report}:{ok:false,reason:'report_binding_invalid',errors:validation.errors}}
|
|
57
|
+
async function dispatch({projectDir,feature,agent='dev',runId,promptPath,adapterRegistry=adapters,catalogLoader,timeout=600000}){
|
|
58
|
+
const lease=await acquireLease(projectDir,feature);if(!lease)return{ok:false,status:'paused',reason:'lease_held',resume_command:`aioson agent:execution:resume . --feature=${feature}`};
|
|
59
|
+
try{const loaded=await loadManifest(projectDir,feature);if(!loaded.exists)return{ok:false,status:'paused',reason:'manifest_missing',resume_command:`aioson agent:execution:resume . --feature=${feature}`};if(!loaded.ok)return{ok:false,status:'paused',reason:'manifest_invalid',errors:loaded.errors};
|
|
60
|
+
let state=await readState(projectDir,feature);if(state&&state.manifest_digest!==loaded.digest&&!TERMINAL.includes(state.status))return{ok:false,status:'paused',reason:'manifest_digest_changed',expected:state.manifest_digest,actual:loaded.digest};if(state?.completed_agents?.includes(agent))return{ok:true,status:state.status,idempotent:true,state};
|
|
61
|
+
let safePrompt;try{safePrompt=await safePromptPath(projectDir,promptPath)}catch(error){return{ok:false,status:'paused',reason:'invalid_prompt_path',error:error.message}}
|
|
62
|
+
const resolved=await resolveAgentExecution(loaded.manifest,agent,{}, {catalogLoader});if(!resolved.ok)return{ok:false,status:'paused',reason:resolved.reason,candidates:resolved.candidates||[],supported:resolved.supported||[]};const adapter=adapterRegistry[resolved.host];const run_id=runId||state?.run_id||crypto.randomUUID();const attempt_id=crypto.randomUUID();if(!adapter)return{ok:false,status:'paused',reason:'unsupported_host'};let writableRoots;try{writableRoots=await resolveWritableRoots(projectDir,resolved.writable_roots)}catch(error){return{ok:false,status:'paused',reason:'invalid_writable_root',error:error.message}}const reportRelative=resolved.report.replace(/\{run_id\}/g,run_id);const reportFile=safeReportPath(projectDir,reportRelative);const expected={feature,run_id,attempt_id,agent,host:resolved.host,model_requested:resolved.model_requested,model_resolved:resolved.model,model_resolution_strategy:resolved.model_resolution_strategy,reasoning_effort:resolved.reasoning_effort,manifest_digest:loaded.digest,writable_roots:writableRoots,status:'running'};const promptText=await fs.readFile(safePrompt,'utf8');const executionInput={mode:resolved.mode,model:resolved.model,reasoning_effort:resolved.reasoning_effort,writable_roots:writableRoots,cwd:projectDir,prompt_text:buildExecutionPrompt(promptText,expected,path.relative(projectDir,reportFile)),timeout};const built=adapter.build(executionInput);
|
|
63
|
+
const correlation={feature,agent,dispatcher_run_id:run_id,attempt_id,host:resolved.host,model:resolved.model,model_requested:resolved.model_requested,model_resolved:resolved.model,reasoning_effort:resolved.reasoning_effort,model_resolution_strategy:resolved.model_resolution_strategy,catalog_source:resolved.catalog_source};const telemetry=await createTelemetryBridge(projectDir,correlation);
|
|
64
|
+
const attempt={...expected,telemetry_run_id:telemetry.run.telemetry_run_id,status:built.ok?'running':'blocked',reason:built.reason||null,report:reportRelative};state={version:1,feature,run_id,manifest_path:path.relative(projectDir,loaded.path),manifest_digest:loaded.digest,status:built.ok?'running':'paused',reason:built.reason||null,attempts:[...(state?.attempts||[]),attempt],completed_agents:state?.completed_agents||[],updated_at:new Date().toISOString()};await atomic(statePath(projectDir,feature),state);if(!built.ok){telemetry.transition('paused',built.reason);telemetry.close();return{ok:false,status:'paused',reason:built.reason,capability:built.capability,resume_command:`aioson agent:execution:resume . --feature=${feature}`,state}};
|
|
65
|
+
Object.assign(executionInput,{onSpawn:(pid,at)=>telemetry.onSpawn(pid,at),onStdout:d=>telemetry.output('stdout',d),onStderr:d=>telemetry.output('stderr',d)});
|
|
66
|
+
const execution=await executeWithCapacityPolicy({manifest:loaded.manifest,resolved,input:executionInput,adapterRegistry,catalogLoader});attempt.execution_history=execution.history||[];for(const item of attempt.execution_history){if(item.attempt>1)telemetry.event(item.host===resolved.host?'retry':'fallback',`${item.host}/${item.model}`,{attempt:item.attempt,reason:item.reason})}attempt.model_resolved=execution.model||resolved.model;attempt.model_resolution_strategy=execution.model_resolution_strategy||resolved.model_resolution_strategy;attempt.reasoning_effort=execution.reasoning_effort||resolved.reasoning_effort;attempt.host_resolved=execution.host||resolved.host;if(!execution.ok){attempt.status='blocked';attempt.reason=execution.reason;state.status='paused';state.reason=execution.reason;state.updated_at=new Date().toISOString();await atomic(statePath(projectDir,feature),state);if(execution.reason==='timeout')telemetry.event('timeout','Execution timeout');telemetry.transition('paused',execution.reason);telemetry.close();return{ok:false,status:'paused',reason:execution.reason,candidates:execution.candidates||[],execution,state,resume_command:`aioson agent:execution:resume . --feature=${feature}`}}
|
|
67
|
+
telemetry.transition('waiting_report');
|
|
68
|
+
const boundExpected={...expected,host:attempt.host_resolved,model_resolved:attempt.model_resolved,model_resolution_strategy:attempt.model_resolution_strategy,reasoning_effort:attempt.reasoning_effort,status:'running'};const reportResult=await readBoundReport(projectDir,reportRelative,boundExpected);if(!reportResult.ok){attempt.status='blocked';attempt.reason=reportResult.reason;state.status='paused';state.reason=reportResult.reason;state.updated_at=new Date().toISOString();await atomic(statePath(projectDir,feature),state);telemetry.transition('paused',reportResult.reason);telemetry.close();return{ok:false,status:'paused',reason:reportResult.reason,report:reportResult,state,resume_command:`aioson agent:execution:resume . --feature=${feature}`}}
|
|
69
|
+
const digest=crypto.createHash('sha256').update(JSON.stringify(reportResult.report)).digest('hex');telemetry.report(reportResult.report,reportRelative,digest);attempt.status='completed';attempt.verdict=reportResult.report.verdict;state.status=reportResult.report.verdict==='PASS'?'verification_planned':'correcting';telemetry.transition(reportResult.report.verdict==='PASS'?'passed':reportResult.report.verdict==='FAIL'?'failed':'correcting');telemetry.close();if(reportResult.report.verdict==='PASS'&&!state.completed_agents.includes(agent))state.completed_agents.push(agent);state.updated_at=new Date().toISOString();await atomic(statePath(projectDir,feature),state);return{ok:true,status:state.status,run_id,attempt_id,execution,report:reportResult.report,state};
|
|
70
|
+
}finally{await releaseLease(lease)}
|
|
71
|
+
}
|
|
72
|
+
module.exports={acquireLease,applyCapacityPolicy,buildExecutionPrompt,capacitySequence,dispatch,executeWithCapacityPolicy,leasePath,readBoundReport,readState,releaseLease,resolveWritableRoots,safePromptPath,statePath};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs=require('node:fs/promises');const path=require('node:path');
|
|
3
|
+
async function isFile(file){try{return(await fs.stat(file)).isFile()}catch{return false}}
|
|
4
|
+
async function resolveNpmCmdShim(cmdPath){const text=await fs.readFile(cmdPath,'utf8');const dir=path.dirname(cmdPath);const matches=[...text.matchAll(/["'](%~dp0|%dp0%)[\\/]?([^"']+\.(?:js|exe))["']/gi)];const targets=matches.map(match=>path.resolve(dir,match[2].replace(/[\\/]+/g,path.sep))).filter(file=>!/[\\/]node\.exe$/i.test(file));for(const target of targets.reverse()){if(!(await isFile(target)))continue;const real=await fs.realpath(target);return path.extname(real).toLowerCase()==='.js'?{executable:process.execPath,prefixArgs:[real],source:'npm_cmd_js'}:{executable:real,prefixArgs:[],source:'npm_cmd_exe'}}throw new Error(`Unsupported or invalid npm command shim: ${cmdPath}`)}
|
|
5
|
+
async function resolveExecutable(command,{env=process.env,platform=process.platform}={}){if(typeof command!=='string'||!command.trim())throw new Error('Executable is required');const raw=command.trim();if(path.isAbsolute(raw)){if(!(await isFile(raw)))throw new Error(`Executable not found: ${raw}`);return platform==='win32'&&path.extname(raw).toLowerCase()==='.cmd'?resolveNpmCmdShim(raw):{executable:await fs.realpath(raw),prefixArgs:[],source:'absolute'}}if(platform!=='win32')return{executable:raw,prefixArgs:[],source:'path'};const dirs=String(env.PATH||env.Path||'').split(';').filter(Boolean);for(const dir of dirs){for(const ext of ['.exe','.cmd','']){const candidate=path.join(dir,`${raw}${ext}`);if(!(await isFile(candidate)))continue;if(ext==='.cmd')return resolveNpmCmdShim(candidate);if(ext==='.exe')return{executable:await fs.realpath(candidate),prefixArgs:[],source:'windows_exe'};}}
|
|
6
|
+
throw new Error(`Executable not found on PATH: ${raw}`)}
|
|
7
|
+
module.exports={resolveExecutable,resolveNpmCmdShim};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
const { AGENTS, validateManifest } = require('./schema');
|
|
7
|
+
const { capabilities } = require('./capabilities');
|
|
8
|
+
const { loadModelCatalog } = require('./model-catalog');
|
|
9
|
+
const { resolveModel, validateReasoningEffort } = require('./model-resolver');
|
|
10
|
+
|
|
11
|
+
function manifestPath(projectDir, feature) { return path.join(projectDir, '.aioson', 'context', `agent-execution-${feature}.json`); }
|
|
12
|
+
function defaults(feature, host = 'codex') {
|
|
13
|
+
const agents = {};
|
|
14
|
+
for (const id of AGENTS) agents[id] = { enabled: true, host, mode: 'external', model: 'configured-default', writable_roots: [], fallbacks: [], report: `.aioson/context/reports/${feature}/{run_id}/${id}.json` };
|
|
15
|
+
return { version: 1, feature, host, generated_at: new Date().toISOString(), agents, capacity_policy: { strategy: 'pause', max_attempts: 1, backoff_ms: 0 }, cycle_limits: { dev_qa: 3, tester: 3, pentester: 3 }, reporting: { format: 'json', markdown: true } };
|
|
16
|
+
}
|
|
17
|
+
function stable(value) { if (Array.isArray(value)) return value.map(stable); if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(k => [k, stable(value[k])])); return value; }
|
|
18
|
+
function digest(value) { return crypto.createHash('sha256').update(JSON.stringify(stable(value))).digest('hex'); }
|
|
19
|
+
function mergeAdditive(existing, base) {
|
|
20
|
+
if (Array.isArray(existing)) return existing;
|
|
21
|
+
if (!existing || typeof existing !== 'object') return existing === undefined ? base : existing;
|
|
22
|
+
const out = { ...base };
|
|
23
|
+
for (const [key, value] of Object.entries(existing)) out[key] = value && typeof value === 'object' && !Array.isArray(value) ? mergeAdditive(value, base && base[key]) : value;
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
async function initManifest(projectDir, feature, host, { overwrite = false } = {}) {
|
|
27
|
+
const file = manifestPath(projectDir, feature); const base = defaults(feature, host);
|
|
28
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
29
|
+
let value = base;
|
|
30
|
+
try { const old = JSON.parse(await fs.readFile(file, 'utf8')); if (!overwrite) value = mergeAdditive(old, base); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
31
|
+
await fs.writeFile(file, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
32
|
+
return { path: file, manifest: value, digest: digest(value) };
|
|
33
|
+
}
|
|
34
|
+
async function loadManifest(projectDir, feature) {
|
|
35
|
+
const file = manifestPath(projectDir, feature);
|
|
36
|
+
try { const value = JSON.parse(await fs.readFile(file, 'utf8')); const validation = validateManifest(value, feature); return { exists: true, path: file, manifest: value, digest: digest(value), ...validation }; }
|
|
37
|
+
catch (error) { if (error.code === 'ENOENT') return { exists: false, legacy: true, path: file, ok: true }; return { exists: true, path: file, ok: false, errors: [{ path: '$', message: error.message }] }; }
|
|
38
|
+
}
|
|
39
|
+
function resolveAgent(manifest, agent, overrides = {}) { const entry = manifest.agents[agent]; return { ...entry, host: overrides.host || entry.host || manifest.host, model: overrides.model || entry.model, source: 'manifest' }; }
|
|
40
|
+
async function resolveExecutionEntry(entry, { catalogLoader = loadModelCatalog } = {}) {
|
|
41
|
+
const cap = capabilities(entry.host);
|
|
42
|
+
if (entry.reasoning_effort && !cap.reasoning_effort) return { ok: false, reason: 'unsupported_reasoning_effort', host: entry.host, model_requested: entry.model, candidates: [] };
|
|
43
|
+
const catalog = cap.model_catalog ? await catalogLoader(entry.host) : { available: false, reason: 'unsupported_model_catalog', models: [] };
|
|
44
|
+
const model = resolveModel(entry.model, catalog);
|
|
45
|
+
if (!model.ok) return { ...entry, ...model, model_requested: entry.model };
|
|
46
|
+
const effort = validateReasoningEffort(model, entry.reasoning_effort);
|
|
47
|
+
if (!effort.ok) return { ...entry, ok: false, reason: effort.reason, supported: effort.supported, candidates: [], model_requested: entry.model, model_resolved: model.resolved };
|
|
48
|
+
return {
|
|
49
|
+
...entry,
|
|
50
|
+
ok: true,
|
|
51
|
+
model_requested: entry.model,
|
|
52
|
+
model: model.resolved,
|
|
53
|
+
model_resolved: model.resolved,
|
|
54
|
+
model_resolution_strategy: model.strategy,
|
|
55
|
+
catalog_source: model.catalog_source,
|
|
56
|
+
catalog_fetched_at: model.catalog_fetched_at,
|
|
57
|
+
reasoning_effort: effort.reasoning_effort,
|
|
58
|
+
reasoning_effort_verification: effort.verification
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function resolveAgentExecution(manifest, agent, overrides = {}, options = {}) { return resolveExecutionEntry(resolveAgent(manifest, agent, overrides), options); }
|
|
62
|
+
module.exports = { defaults, digest, initManifest, loadManifest, manifestPath, mergeAdditive, resolveAgent, resolveAgentExecution, resolveExecutionEntry };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { REASONING_EFFORTS } = require('./schema');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
9
|
+
const DEFAULT_MAX_MODELS = 1000;
|
|
10
|
+
const SAFE_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/;
|
|
11
|
+
|
|
12
|
+
function unavailable(reason) {
|
|
13
|
+
return { available: false, reason, source: null, fetched_at: null, client_version: null, models: [] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function codexHome(env, home) {
|
|
17
|
+
const configured = typeof env.CODEX_HOME === 'string' ? env.CODEX_HOME.trim() : '';
|
|
18
|
+
return configured || path.join(home, '.codex');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeEfforts(levels) {
|
|
22
|
+
if (!Array.isArray(levels)) return [];
|
|
23
|
+
const supported = new Set(REASONING_EFFORTS);
|
|
24
|
+
return [...new Set(levels.map(level => typeof level === 'string' ? level : level?.effort)
|
|
25
|
+
.filter(effort => typeof effort === 'string' && supported.has(effort)))];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sanitizeModels(models) {
|
|
29
|
+
if (!Array.isArray(models)) return [];
|
|
30
|
+
const bySlug = new Map();
|
|
31
|
+
for (const model of models) {
|
|
32
|
+
const slug = typeof model?.slug === 'string' ? model.slug.trim() : '';
|
|
33
|
+
if (!SAFE_MODEL_ID.test(slug) || bySlug.has(slug)) continue;
|
|
34
|
+
const display = typeof model.display_name === 'string' && model.display_name.trim()
|
|
35
|
+
? model.display_name.trim().slice(0, 200)
|
|
36
|
+
: slug;
|
|
37
|
+
bySlug.set(slug, {
|
|
38
|
+
slug,
|
|
39
|
+
display_name: display,
|
|
40
|
+
supported_efforts: normalizeEfforts(model.supported_reasoning_levels)
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return [...bySlug.values()];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function loadModelCatalog(host, options = {}) {
|
|
47
|
+
if (host !== 'codex') return unavailable('unsupported_model_catalog');
|
|
48
|
+
const env = options.env || process.env;
|
|
49
|
+
const home = options.home || os.homedir();
|
|
50
|
+
const file = path.join(codexHome(env, home), 'models_cache.json');
|
|
51
|
+
const maxBytes = Math.max(1, Number(options.maxBytes) || DEFAULT_MAX_BYTES);
|
|
52
|
+
const maxModels = Math.max(1, Number(options.maxModels) || DEFAULT_MAX_MODELS);
|
|
53
|
+
let stat;
|
|
54
|
+
try {
|
|
55
|
+
stat = await fs.stat(file);
|
|
56
|
+
} catch {
|
|
57
|
+
return unavailable('catalog_unavailable');
|
|
58
|
+
}
|
|
59
|
+
if (!stat.isFile()) return unavailable('catalog_unavailable');
|
|
60
|
+
if (stat.size > maxBytes) return unavailable('catalog_too_large');
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
64
|
+
} catch {
|
|
65
|
+
return unavailable('catalog_invalid');
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(parsed?.models) && parsed.models.length > maxModels) return unavailable('catalog_too_many_models');
|
|
68
|
+
const models = sanitizeModels(parsed?.models);
|
|
69
|
+
if (!models.length) return unavailable('catalog_incompatible');
|
|
70
|
+
return {
|
|
71
|
+
available: true,
|
|
72
|
+
reason: null,
|
|
73
|
+
source: 'codex_local_cache',
|
|
74
|
+
fetched_at: typeof parsed.fetched_at === 'string' ? parsed.fetched_at.slice(0, 100) : null,
|
|
75
|
+
client_version: typeof parsed.client_version === 'string' ? parsed.client_version.slice(0, 50) : null,
|
|
76
|
+
models
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { DEFAULT_MAX_BYTES, DEFAULT_MAX_MODELS, loadModelCatalog, sanitizeModels };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { MAX_MODEL_NAME_LENGTH, REASONING_EFFORTS } = require('./schema');
|
|
4
|
+
|
|
5
|
+
const LITERAL_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/;
|
|
6
|
+
const GENERIC_ALIASES = new Set(['gpt', 'model', 'openai', 'codex']);
|
|
7
|
+
|
|
8
|
+
function tokens(value) {
|
|
9
|
+
return String(value || '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
|
|
10
|
+
.toLowerCase().match(/[a-z]+|\d+/g) || [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeModelName(value) { return tokens(value).join('-'); }
|
|
14
|
+
function numericTokens(value) { return tokens(value).filter(token => /^\d+$/.test(token)); }
|
|
15
|
+
function alphaTokens(value) { return tokens(value).filter(token => /^[a-z]+$/.test(token)); }
|
|
16
|
+
|
|
17
|
+
function distance(left, right) {
|
|
18
|
+
const a = String(left); const b = String(right);
|
|
19
|
+
if (a.length > MAX_MODEL_NAME_LENGTH || b.length > MAX_MODEL_NAME_LENGTH) return Number.POSITIVE_INFINITY;
|
|
20
|
+
const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
21
|
+
for (let i = 0; i <= a.length; i++) matrix[i][0] = i;
|
|
22
|
+
for (let j = 0; j <= b.length; j++) matrix[0][j] = j;
|
|
23
|
+
for (let i = 1; i <= a.length; i++) {
|
|
24
|
+
for (let j = 1; j <= b.length; j++) {
|
|
25
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
26
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
27
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
28
|
+
matrix[i][j] = Math.min(matrix[i][j], matrix[i - 2][j - 2] + cost);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return matrix[a.length][b.length];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function candidates(models) {
|
|
36
|
+
return (Array.isArray(models) ? models : []).filter(model => model && typeof model.slug === 'string')
|
|
37
|
+
.map(model => ({
|
|
38
|
+
slug: model.slug,
|
|
39
|
+
display_name: typeof model.display_name === 'string' ? model.display_name : model.slug,
|
|
40
|
+
supported_efforts: Array.isArray(model.supported_efforts) ? model.supported_efforts : []
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function uniqueBySlug(models) { return [...new Map(models.map(model => [model.slug, model])).values()]; }
|
|
45
|
+
function diagnosticSlugs(models) { return uniqueBySlug(models).map(model => model.slug).sort().slice(0, 5); }
|
|
46
|
+
|
|
47
|
+
function success(requested, model, strategy, catalog) {
|
|
48
|
+
return {
|
|
49
|
+
ok: true,
|
|
50
|
+
requested,
|
|
51
|
+
resolved: model.slug,
|
|
52
|
+
strategy,
|
|
53
|
+
catalog_source: catalog?.source || null,
|
|
54
|
+
catalog_fetched_at: catalog?.fetched_at || null,
|
|
55
|
+
supported_efforts: model.supported_efforts || []
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function failure(requested, reason, models = [], catalog = null) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
requested,
|
|
63
|
+
resolved: null,
|
|
64
|
+
reason,
|
|
65
|
+
candidates: diagnosticSlugs(models),
|
|
66
|
+
catalog_source: catalog?.source || null,
|
|
67
|
+
catalog_fetched_at: catalog?.fetched_at || null
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveModel(value, catalog) {
|
|
72
|
+
const requested = typeof value === 'string' ? value.trim() : '';
|
|
73
|
+
if (!requested) return failure(requested, 'invalid_model');
|
|
74
|
+
if (requested.length > MAX_MODEL_NAME_LENGTH) return failure(requested.slice(0, MAX_MODEL_NAME_LENGTH), 'invalid_model');
|
|
75
|
+
if (requested === 'configured-default') {
|
|
76
|
+
return success(requested, { slug: requested, supported_efforts: [] }, 'configured_default', catalog);
|
|
77
|
+
}
|
|
78
|
+
if (!catalog?.available) {
|
|
79
|
+
if (!LITERAL_MODEL_ID.test(requested)) return failure(requested, catalog?.reason || 'catalog_unavailable');
|
|
80
|
+
return success(requested, { slug: requested, supported_efforts: [] }, 'unverified_literal', null);
|
|
81
|
+
}
|
|
82
|
+
const models = candidates(catalog.models);
|
|
83
|
+
const exact = models.find(model => model.slug === requested);
|
|
84
|
+
if (exact) return success(requested, exact, 'exact_slug', catalog);
|
|
85
|
+
|
|
86
|
+
const requestedKey = normalizeModelName(requested);
|
|
87
|
+
const normalized = uniqueBySlug(models.filter(model => normalizeModelName(model.slug) === requestedKey
|
|
88
|
+
|| normalizeModelName(model.display_name) === requestedKey));
|
|
89
|
+
if (normalized.length === 1) return success(requested, normalized[0], 'normalized_name', catalog);
|
|
90
|
+
if (normalized.length > 1) return failure(requested, 'ambiguous_model', normalized, catalog);
|
|
91
|
+
|
|
92
|
+
const requestedTokens = tokens(requested);
|
|
93
|
+
const isGeneric = requestedTokens.length === 1 && GENERIC_ALIASES.has(requestedTokens[0]);
|
|
94
|
+
const isUsableAlias = requestedKey.length >= 4 && !isGeneric && requestedTokens.some(token => /^[a-z]+$/.test(token));
|
|
95
|
+
if (isUsableAlias) {
|
|
96
|
+
const alias = uniqueBySlug(models.filter(model => {
|
|
97
|
+
const slugTokens = tokens(model.slug); const displayTokens = tokens(model.display_name);
|
|
98
|
+
const suffix = list => list.length >= requestedTokens.length
|
|
99
|
+
&& requestedTokens.every((token, index) => token === list[list.length - requestedTokens.length + index]);
|
|
100
|
+
return suffix(slugTokens) || suffix(displayTokens);
|
|
101
|
+
}));
|
|
102
|
+
if (alias.length === 1) return success(requested, alias[0], 'unique_alias', catalog);
|
|
103
|
+
if (alias.length > 1) return failure(requested, 'ambiguous_model', alias, catalog);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const requestedNumbers = numericTokens(requested);
|
|
107
|
+
const requestedAlpha = alphaTokens(requested);
|
|
108
|
+
const eligible = models.filter(model => {
|
|
109
|
+
const modelNumbers = numericTokens(model.slug);
|
|
110
|
+
if (requestedNumbers.length && requestedNumbers.join('.') !== modelNumbers.join('.')) return false;
|
|
111
|
+
const modelAlpha = alphaTokens(model.slug);
|
|
112
|
+
return !requestedAlpha.length || requestedAlpha[0] === modelAlpha[0];
|
|
113
|
+
});
|
|
114
|
+
const ranked = eligible.map(model => ({ model, score: distance(requestedKey, normalizeModelName(model.slug)) }))
|
|
115
|
+
.sort((a, b) => a.score - b.score || a.model.slug.localeCompare(b.model.slug));
|
|
116
|
+
const maxDistance = Math.min(3, Math.max(1, Math.floor(requestedKey.length * 0.1)));
|
|
117
|
+
if (!ranked.length || ranked[0].score > maxDistance) return failure(requested, 'model_not_found', [], catalog);
|
|
118
|
+
const best = ranked.filter(item => item.score === ranked[0].score);
|
|
119
|
+
if (best.length !== 1) return failure(requested, 'ambiguous_model', best.map(item => item.model), catalog);
|
|
120
|
+
return success(requested, best[0].model, 'fuzzy_unique', catalog);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function validateReasoningEffort(resolution, effort) {
|
|
124
|
+
if (effort === undefined || effort === null || effort === '') return { ok: true, reasoning_effort: null, verification: 'inherited' };
|
|
125
|
+
if (!REASONING_EFFORTS.includes(effort)) return { ok: false, reason: 'invalid_reasoning_effort', supported: REASONING_EFFORTS };
|
|
126
|
+
if (!resolution?.ok) return { ok: false, reason: resolution?.reason || 'model_resolution_failed', supported: [] };
|
|
127
|
+
const supported = Array.isArray(resolution.supported_efforts) ? resolution.supported_efforts : [];
|
|
128
|
+
if (supported.length && !supported.includes(effort)) return { ok: false, reason: 'unsupported_reasoning_effort', supported };
|
|
129
|
+
return { ok: true, reasoning_effort: effort, verification: supported.length ? 'catalog' : 'unverified' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { distance, normalizeModelName, resolveModel, validateReasoningEffort };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs/promises');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const VERDICTS = ['PASS', 'FAIL', 'BLOCKED'];
|
|
5
|
+
const IDENTITY_FIELDS = ['feature','run_id','attempt_id','agent','host','model_resolved','manifest_digest'];
|
|
6
|
+
function validateReport(r, expected = null) { const errors=[]; const req=['version','feature','run_id','attempt_id','agent','host','model_requested','model_resolved','manifest_digest','writable_roots','started_at','finished_at','verdict','findings','evidence'];if(expected?.model_resolution_strategy!==undefined)req.push('model_resolution_strategy'); for(const k of req) if(r?.[k]===undefined) errors.push({path:`$.${k}`,message:'is required'}); if(r && !VERDICTS.includes(r.verdict)) errors.push({path:'$.verdict',message:'must be PASS, FAIL, or BLOCKED'}); if(r && !Array.isArray(r.findings)) errors.push({path:'$.findings',message:'must be an array'});if(r&&!Array.isArray(r.writable_roots))errors.push({path:'$.writable_roots',message:'must be an array'}); if(expected) for(const key of IDENTITY_FIELDS) if(r?.[key]!==expected[key]) errors.push({path:`$.${key}`,message:'does not match the registered attempt'});for(const key of ['model_requested','model_resolution_strategy'])if(expected?.[key]!==undefined&&r?.[key]!==expected[key])errors.push({path:`$.${key}`,message:'does not match the registered attempt'});if(expected&&expected.reasoning_effort&&r?.reasoning_effort!==expected.reasoning_effort)errors.push({path:'$.reasoning_effort',message:'does not match the registered attempt'});if(expected&&JSON.stringify(r?.writable_roots)!==JSON.stringify(expected.writable_roots))errors.push({path:'$.writable_roots',message:'does not match the registered attempt'}); if(expected && !['ready','running'].includes(expected.status)) errors.push({path:'$.attempt_id',message:'attempt is not eligible to report'}); return {ok:errors.length===0,errors}; }
|
|
7
|
+
function safeReportPath(projectDir, relative) { const root=path.resolve(projectDir,'.aioson','context','reports'); const target=path.resolve(projectDir,relative); if(target!==root && !target.startsWith(root+path.sep)) throw new Error('report path escapes report root'); return target; }
|
|
8
|
+
async function writeReport(projectDir, relative, report, expected) { const check=validateReport(report,expected); if(!check.ok){const e=new Error('invalid report');e.errors=check.errors;throw e;} const file=safeReportPath(projectDir,relative); await fs.mkdir(path.dirname(file),{recursive:true}); const handle=await fs.open(file,'wx'); try{await handle.writeFile(`${JSON.stringify(report,null,2)}\n`,'utf8')}finally{await handle.close()} return file; }
|
|
9
|
+
module.exports={validateReport,safeReportPath,writeReport};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const HOSTS = ['claude', 'codex', 'opencode'];
|
|
4
|
+
const MODES = ['fresh-session', 'subagent', 'external', 'current-session'];
|
|
5
|
+
const AGENTS = ['dev', 'qa', 'tester', 'pentester', 'validator'];
|
|
6
|
+
const REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
|
|
7
|
+
const MAX_MODEL_NAME_LENGTH = 200;
|
|
8
|
+
const ROOT_KEYS = ['version','feature','host','generated_at','agents','capacity_policy','cycle_limits','reporting'];
|
|
9
|
+
const AGENT_KEYS = ['enabled','host','mode','model','reasoning_effort','writable_roots','fallbacks','report'];
|
|
10
|
+
const SECRET_KEY = /token|secret|password|authorization|api[_-]?key/i;
|
|
11
|
+
|
|
12
|
+
function validateManifest(value, expectedFeature) {
|
|
13
|
+
const errors = [];
|
|
14
|
+
const add = (path, message) => errors.push({ path, message });
|
|
15
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return { ok: false, errors: [{ path: '$', message: 'must be an object' }] };
|
|
16
|
+
const scanSecrets=(node,p='$')=>{if(!node||typeof node!=='object')return;for(const [key,child] of Object.entries(node)){if(SECRET_KEY.test(key))add(`${p}.${key}`,'secret fields are forbidden; use environment configuration');scanSecrets(child,`${p}.${key}`)}};scanSecrets(value);
|
|
17
|
+
for (const key of Object.keys(value)) if (!ROOT_KEYS.includes(key)) add(`$.${key}`, SECRET_KEY.test(key) ? 'secret fields are forbidden; use environment configuration' : 'unknown field');
|
|
18
|
+
if (value.version !== 1) add('$.version', 'must equal 1');
|
|
19
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value.feature || '')) add('$.feature', 'must be a kebab-case slug');
|
|
20
|
+
if (expectedFeature && value.feature !== expectedFeature) add('$.feature', `must equal ${expectedFeature}`);
|
|
21
|
+
if (!HOSTS.includes(value.host)) add('$.host', `must be one of ${HOSTS.join(', ')}`);
|
|
22
|
+
if (!value.agents || typeof value.agents !== 'object') add('$.agents', 'must be an object');
|
|
23
|
+
else for(const key of Object.keys(value.agents)) if(!AGENTS.includes(key)) add(`$.agents.${key}`,'unknown agent');
|
|
24
|
+
for (const id of AGENTS) {
|
|
25
|
+
const agent = value.agents && value.agents[id];
|
|
26
|
+
if (!agent || typeof agent !== 'object') { add(`$.agents.${id}`, 'is required'); continue; }
|
|
27
|
+
for (const key of Object.keys(agent)) if (!AGENT_KEYS.includes(key)) add(`$.agents.${id}.${key}`, SECRET_KEY.test(key) ? 'secret fields are forbidden; use environment configuration' : 'unknown field');
|
|
28
|
+
if (typeof agent.enabled !== 'boolean') add(`$.agents.${id}.enabled`, 'must be boolean');
|
|
29
|
+
if (!HOSTS.includes(agent.host)) add(`$.agents.${id}.host`, `must be one of ${HOSTS.join(', ')}`);
|
|
30
|
+
if (!MODES.includes(agent.mode)) add(`$.agents.${id}.mode`, `must be one of ${MODES.join(', ')}`);
|
|
31
|
+
if (typeof agent.model !== 'string' || !agent.model.trim()) add(`$.agents.${id}.model`, 'must be a non-empty model id');
|
|
32
|
+
else if (agent.model.length > MAX_MODEL_NAME_LENGTH) add(`$.agents.${id}.model`, `must be at most ${MAX_MODEL_NAME_LENGTH} characters`);
|
|
33
|
+
if (agent.reasoning_effort !== undefined && !REASONING_EFFORTS.includes(agent.reasoning_effort)) add(`$.agents.${id}.reasoning_effort`, `must be one of ${REASONING_EFFORTS.join(', ')}`);
|
|
34
|
+
if(!Array.isArray(agent.writable_roots))add(`$.agents.${id}.writable_roots`,'must be an array');else agent.writable_roots.forEach((root,index)=>{if(typeof root!=='string'||!root.trim())add(`$.agents.${id}.writable_roots[${index}]`,'must be a non-empty path')});
|
|
35
|
+
if (!Array.isArray(agent.fallbacks)) add(`$.agents.${id}.fallbacks`, 'must be an array');
|
|
36
|
+
else agent.fallbacks.forEach((fallback,index)=>{const p=`$.agents.${id}.fallbacks[${index}]`;if(!fallback||typeof fallback!=='object'||Array.isArray(fallback)){add(p,'must be {host, model}');return}for(const key of Object.keys(fallback))if(!['host','model'].includes(key))add(`${p}.${key}`,'unknown field');if(!HOSTS.includes(fallback.host))add(`${p}.host`,`must be one of ${HOSTS.join(', ')}`);if(typeof fallback.model!=='string'||!fallback.model.trim())add(`${p}.model`,'must be a non-empty model id');else if(fallback.model.length>MAX_MODEL_NAME_LENGTH)add(`${p}.model`,`must be at most ${MAX_MODEL_NAME_LENGTH} characters`)});
|
|
37
|
+
if (typeof agent.report !== 'string' || !agent.report.includes('{run_id}')) add(`$.agents.${id}.report`, 'must include {run_id}');
|
|
38
|
+
}
|
|
39
|
+
const limits = value.cycle_limits;
|
|
40
|
+
if (!limits || typeof limits !== 'object') add('$.cycle_limits', 'is required');
|
|
41
|
+
else for (const [key, number] of Object.entries(limits)) if (!Number.isInteger(number) || number < 0) add(`$.cycle_limits.${key}`, 'must be a non-negative integer');
|
|
42
|
+
if (!value.capacity_policy || !['pause', 'retry', 'wait', 'fallback'].includes(value.capacity_policy.strategy)) add('$.capacity_policy.strategy', 'must be pause, retry, wait, or fallback');
|
|
43
|
+
else { for(const key of Object.keys(value.capacity_policy)) if(!['strategy','max_attempts','backoff_ms','allow_cross_host'].includes(key)) add(`$.capacity_policy.${key}`,'unknown field'); if(!Number.isInteger(value.capacity_policy.max_attempts)||value.capacity_policy.max_attempts<1)add('$.capacity_policy.max_attempts','must be a positive integer'); if(!Number.isInteger(value.capacity_policy.backoff_ms)||value.capacity_policy.backoff_ms<0)add('$.capacity_policy.backoff_ms','must be a non-negative integer'); }
|
|
44
|
+
if(!value.reporting||typeof value.reporting!=='object')add('$.reporting','is required');else for(const key of Object.keys(value.reporting))if(!['format','markdown'].includes(key))add(`$.reporting.${key}`,'unknown field');
|
|
45
|
+
return { ok: errors.length === 0, errors };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { AGENTS, HOSTS, MAX_MODEL_NAME_LENGTH, MODES, REASONING_EFFORTS, validateManifest };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const crypto=require('node:crypto');
|
|
3
|
+
const {openRuntimeDb,createExecutionRun,attachExecutionProcess,transitionExecutionRun,appendExecutionEvent,attachExecutionReport,pruneExecutionTelemetry}=require('../runtime-store');
|
|
4
|
+
const DEFAULTS={maxQueueBytes:256*1024,maxEventBytes:16*1024,maxPersistedBytes:1024*1024,maxPendingBytes:64*1024,flushMs:100};
|
|
5
|
+
function redact(value){return String(value||'').replace(/(authorization\s*[:=]\s*(?:bearer\s+)?|api[_-]?key\s*[:=]\s*|token\s*[:=]\s*|password\s*[:=]\s*)[^\s,;]+/gi,'$1[REDACTED]').replace(/\b(sk-[A-Za-z0-9_-]{8,})\b/g,'[REDACTED]');}
|
|
6
|
+
function fingerprint(pid,startedAt){return crypto.createHash('sha256').update(`${pid}:${startedAt}`).digest('hex').slice(0,24);}
|
|
7
|
+
async function createTelemetryBridge(projectDir,correlation,options={}){
|
|
8
|
+
const cfg={...DEFAULTS,...options};const opened=await openRuntimeDb(projectDir);const db=opened.db;pruneExecutionTelemetry(db,{retentionDays:options.retentionDays||14,batch:100});const run=createExecutionRun(db,correlation);let queue=[],queued=0,persisted=0,dropped=0,closed=false,timer=null;const pendingByStream=new Map();
|
|
9
|
+
const flush=()=>{if(!queue.length&&!dropped)return;const items=queue;queue=[];queued=0;for(const item of items){appendExecutionEvent(db,run.telemetry_run_id,item);persisted+=item.bytes;}if(dropped){appendExecutionEvent(db,run.telemetry_run_id,{type:'output_truncated',safe_summary:`${dropped} output bytes dropped`,bytes:Math.min(dropped,cfg.maxEventBytes)});db.prepare('UPDATE agent_execution_runs SET truncated_bytes=truncated_bytes+?,dropped_events=dropped_events+1 WHERE telemetry_run_id=?').run(dropped,run.telemetry_run_id);dropped=0;}};
|
|
10
|
+
const schedule=()=>{if(!timer)timer=setTimeout(()=>{timer=null;try{flush();}catch{}},cfg.flushMs)};
|
|
11
|
+
const enqueue=(stream,text)=>{let safe=redact(text);let bytes=Buffer.byteLength(safe);if(bytes>cfg.maxEventBytes){dropped+=bytes-cfg.maxEventBytes;safe=Buffer.from(safe).subarray(0,cfg.maxEventBytes).toString();bytes=Buffer.byteLength(safe)}if(persisted+queued+bytes>cfg.maxPersistedBytes||queued+bytes>cfg.maxQueueBytes){dropped+=bytes;return}queue.push({type:'output',stream,safe_summary:safe,bytes});queued+=bytes};
|
|
12
|
+
const output=(stream,chunk)=>{if(closed)return;let pending=(pendingByStream.get(stream)||'')+String(chunk||'');if(Buffer.byteLength(pending)>cfg.maxPendingBytes){dropped+=Buffer.byteLength(pending);pendingByStream.set(stream,'');return schedule()}let newline;while((newline=pending.indexOf('\n'))>=0){enqueue(stream,pending.slice(0,newline+1));pending=pending.slice(newline+1)}pendingByStream.set(stream,pending);schedule();};
|
|
13
|
+
return {run,output,onSpawn(pid,startedAt=new Date().toISOString()){return attachExecutionProcess(db,correlation,{pid,fingerprint:fingerprint(pid,startedAt)})},event(type,summary,payload){appendExecutionEvent(db,correlation,{type,safe_summary:redact(summary),payload})},transition(next,reason){flush();return transitionExecutionRun(db,correlation,next,reason)},report(report,path,digest){flush();return attachExecutionReport(db,correlation,{...report,path,digest})},close(){if(closed)return;closed=true;if(timer)clearTimeout(timer);for(const [stream,pending] of pendingByStream)if(pending)enqueue(stream,pending);flush();db.close();}};
|
|
14
|
+
}
|
|
15
|
+
module.exports={createTelemetryBridge,redact,fingerprint};
|
package/src/cli.js
CHANGED
|
@@ -188,7 +188,8 @@ const { runPreflight } = require('./commands/preflight');
|
|
|
188
188
|
const { runClassify } = require('./commands/classify');
|
|
189
189
|
const { runPrototypeCheck } = require('./commands/prototype-check');
|
|
190
190
|
const { runVerifyImplementation } = require('./commands/verify-implementation');
|
|
191
|
-
const { runVerificationPlan } = require('./commands/verification-plan');
|
|
191
|
+
const { runVerificationPlan } = require('./commands/verification-plan');
|
|
192
|
+
const { runAgentExecution } = require('./commands/agent-execution');
|
|
192
193
|
const { runSizing } = require('./commands/sizing');
|
|
193
194
|
const { runDetectTestRunner } = require('./commands/detect-test-runner');
|
|
194
195
|
const { runPulseUpdate } = require('./commands/pulse-update');
|
|
@@ -673,7 +674,21 @@ const JSON_SUPPORTED_COMMANDS = new Set([
|
|
|
673
674
|
'verify:implementation',
|
|
674
675
|
'verify-implementation',
|
|
675
676
|
'verification:plan',
|
|
676
|
-
'verification-plan',
|
|
677
|
+
'verification-plan',
|
|
678
|
+
'agent:execution:init',
|
|
679
|
+
'agent-execution-init',
|
|
680
|
+
'agent:execution:validate',
|
|
681
|
+
'agent-execution-validate',
|
|
682
|
+
'agent:execution:show',
|
|
683
|
+
'agent-execution-show',
|
|
684
|
+
'agent:execution:dispatch',
|
|
685
|
+
'agent-execution-dispatch',
|
|
686
|
+
'agent:execution:resume',
|
|
687
|
+
'agent-execution-resume',
|
|
688
|
+
'agent:execution:status',
|
|
689
|
+
'agent-execution-status',
|
|
690
|
+
'agent:execution:events',
|
|
691
|
+
'agent-execution-events',
|
|
677
692
|
'sizing',
|
|
678
693
|
'detect:test-runner',
|
|
679
694
|
'detect-test-runner',
|
|
@@ -1626,8 +1641,11 @@ async function main() {
|
|
|
1626
1641
|
result = await runPreflight({ args, options, logger: commandLogger });
|
|
1627
1642
|
} else if (command === 'classify') {
|
|
1628
1643
|
result = await runClassify({ args, options, logger: commandLogger });
|
|
1629
|
-
} else if (command === 'verification:plan' || command === 'verification-plan') {
|
|
1630
|
-
result = await runVerificationPlan({ args, options, logger: commandLogger });
|
|
1644
|
+
} else if (command === 'verification:plan' || command === 'verification-plan') {
|
|
1645
|
+
result = await runVerificationPlan({ args, options, logger: commandLogger });
|
|
1646
|
+
} else if (command.startsWith('agent:execution:') || command.startsWith('agent-execution-')) {
|
|
1647
|
+
const sub = command.replace(/^agent:execution:|^agent-execution-/, '');
|
|
1648
|
+
result = await runAgentExecution({ args, options: { ...options, sub }, logger: commandLogger });
|
|
1631
1649
|
} else if (command === 'prototype:check' || command === 'prototype-check') {
|
|
1632
1650
|
result = await runPrototypeCheck({ args, options, logger: commandLogger });
|
|
1633
1651
|
} else if (command === 'verify:implementation' || command === 'verify-implementation') {
|