deadbyte-mcp 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.gitattributes +7 -0
  2. package/AGENTS.md +25 -0
  3. package/CONTEXT.md +3 -3
  4. package/MANIFEST.SHA256 +66 -40
  5. package/R42-PARENT.json +19 -0
  6. package/README.txt +2 -2
  7. package/bin/appcontainer-stage.obj +0 -0
  8. package/bin/bootstrap-advapi32.obj +0 -0
  9. package/bin/bootstrap-exitcode.obj +0 -0
  10. package/bin/bootstrap-kernel32.obj +0 -0
  11. package/bin/child-control-probe.obj +0 -0
  12. package/bin/child-control-stage.obj +0 -0
  13. package/bin/contained-reverse-worker.obj +0 -0
  14. package/bin/contained-transform-worker.obj +0 -0
  15. package/bin/containment-probe.obj +0 -0
  16. package/bin/deadbyte-contain.obj +0 -0
  17. package/bin/deadbyte-exec.obj +0 -0
  18. package/bin/deadbyte-process-host.obj +0 -0
  19. package/bin/deadbyte-tunnel-host.obj +0 -0
  20. package/controller/README.TXT +1 -1
  21. package/controller/deadbyte-controller.ps1 +1 -1
  22. package/controller/deadbyte-desktop-policy.json +5 -5
  23. package/controller/deadbyte-process-policy.json +4 -4
  24. package/docs/ARCHITECTURE.md +1 -1
  25. package/harness/r42-agent-evals.json +48 -0
  26. package/harness/task-contract.schema.json +33 -0
  27. package/package.json +9 -7
  28. package/proof/CONTAINMENT-BUILD.txt +6 -6
  29. package/scripts/agent-eval-r42.mjs +46 -0
  30. package/scripts/deferred-slot-operation-r42.mjs +126 -0
  31. package/scripts/final-closure-r42.mjs +116 -0
  32. package/scripts/final-closure-verify-r42.mjs +223 -0
  33. package/scripts/gate-windows-r42.ps1 +54 -0
  34. package/scripts/release-parity-r42.mjs +235 -0
  35. package/scripts/release-parity-tests-r42.ps1 +6 -0
  36. package/scripts/verify-r42-parent.mjs +101 -0
  37. package/scripts/windows-gate-evidence-r42.mjs +64 -0
  38. package/src/agent-eval-r42.mjs +73 -0
  39. package/src/agent-eval-runner-r42.mjs +138 -0
  40. package/src/agent-governance-r42.mjs +142 -0
  41. package/src/autonomous-context-engine.mjs +2 -0
  42. package/src/autonomous-loop-state.mjs +10 -0
  43. package/src/autonomous-mcp-tools.mjs +21 -6
  44. package/src/autonomous-output-contracts.mjs +18 -2
  45. package/src/autonomous-planner-contracts.mjs +3 -0
  46. package/src/autonomous-planner.mjs +46 -6
  47. package/src/autonomous-policy.mjs +10 -2
  48. package/src/autonomous-runtime.mjs +116 -16
  49. package/src/autonomous-supervisor.mjs +1 -1
  50. package/src/evidence-publisher-r42.mjs +76 -0
  51. package/src/mcp-server.mjs +1 -1
  52. package/src/provider-circuit.mjs +4 -4
  53. package/src/version.mjs +1 -1
  54. package/test/agent-eval-r42.test.mjs +39 -0
  55. package/test/agent-eval-runner-r42.test.mjs +61 -0
  56. package/test/agent-governance-r42.test.mjs +77 -0
  57. package/test/autonomous-r34-e2e.test.mjs +2 -1
  58. package/test/cli-entrypoint.test.mjs +1 -1
  59. package/test/core.test.mjs +1 -1
  60. package/test/evidence-publisher-r42.test.mjs +53 -0
  61. package/test/human-clarification-r42.test.mjs +53 -0
  62. package/test/provider-responses-r42.test.mjs +90 -0
  63. package/test/r33-closeout-regression.test.mjs +27 -12
  64. package/test/r33-finalization.test.mjs +3 -3
  65. package/test/r42-finalization.test.mjs +38 -0
  66. package/test/release-version.test.mjs +13 -13
  67. package/test/task-contract-r42.test.mjs +41 -0
@@ -0,0 +1,101 @@
1
+ import { createHash, createPublicKey, verify as cryptoVerify } from 'node:crypto';
2
+ import { lstat, readFile, readdir } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { canonicalJson } from '../src/canonical-json.mjs';
7
+
8
+ const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
9
+ const stateRoot=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
10
+ const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
11
+ const sha1=bytes=>createHash('sha1').update(bytes).digest('hex');
12
+ const insist=(ok,message)=>{if(!ok)throw new Error(message);};
13
+ const CLOSURE_DOMAIN=Buffer.from('DEADBYTE-R41-FINAL-CLOSURE-V1\0','utf8');
14
+
15
+ function parseManifest(bytes,label){
16
+ const entries=new Map();
17
+ for(const [index,line] of bytes.toString('utf8').split(/\r?\n/).entries()){
18
+ if(!line)continue;
19
+ const match=/^([0-9a-f]{64}) ([^\0]+)$/.exec(line);insist(match,`${label} line ${index+1} malformed`);
20
+ const rel=match[2];
21
+ insist(!path.posix.isAbsolute(rel)&&!rel.split('/').includes('..')&&!entries.has(rel),`unsafe/duplicate ${label} path ${rel}`);
22
+ entries.set(rel,match[1]);
23
+ }
24
+ return entries;
25
+ }
26
+ async function walk(dir,rel=''){
27
+ const out=[];
28
+ for(const name of (await readdir(path.join(dir,...(rel?rel.split('/'):[])))).sort((a,b)=>a.localeCompare(b,'en'))){
29
+ const child=rel?`${rel}/${name}`:name;
30
+ const full=path.join(dir,...child.split('/'));
31
+ const st=await lstat(full);
32
+ insist(!st.isSymbolicLink(),`parent tree symlink forbidden: ${child}`);
33
+ if(st.isDirectory())out.push(...await walk(dir,child));
34
+ else{insist(st.isFile(),`parent tree non-file forbidden: ${child}`);out.push(child);}
35
+ }
36
+ return out;
37
+ }
38
+ async function verifyTree(dir,manifestName,expectedManifest,{exact}){
39
+ const bytes=await readFile(path.join(dir,manifestName));
40
+ insist(sha256(bytes)===expectedManifest,`${manifestName} digest mismatch: ${dir}`);
41
+ const entries=parseManifest(bytes,manifestName);
42
+ if(exact){
43
+ const actual=(await walk(dir)).filter(rel=>rel!==manifestName);
44
+ insist(actual.length===entries.size&&actual.every(rel=>entries.has(rel)),`${manifestName} exact tree mismatch: ${dir}`);
45
+ }
46
+ for(const [rel,digest] of entries){
47
+ insist(sha256(await readFile(path.join(dir,...rel.split('/'))))===digest,`${manifestName} file hash mismatch: ${dir} :: ${rel}`);
48
+ }
49
+ return entries.size;
50
+ }
51
+ function verifySignedClosure(envelope,record,publicPem){
52
+ insist(envelope?.body?.schema==='deadbyte.r41-final-closure.v1','R41 parent closure schema mismatch');
53
+ insist(envelope.body.version==='0.13.0','R41 parent closure version mismatch');
54
+ const pointer=envelope.body.active_pointer;
55
+ insist(pointer?.release_id===record.parent_release_id&&pointer?.manifest_sha256===record.parent_manifest_sha256,'R41 parent closure release identity mismatch');
56
+ insist(pointer?.generation_manifest_sha256===record.parent_generation_manifest_sha256,'R41 parent closure generation identity mismatch');
57
+ const bodyBytes=Buffer.from(canonicalJson(envelope.body),'utf8');
58
+ insist(sha256(bodyBytes)===envelope.body_sha256,'R41 parent closure body hash mismatch');
59
+ const {closure_sha256,...unsigned}=envelope;
60
+ insist(sha256(Buffer.from(canonicalJson(unsigned),'utf8'))===closure_sha256,'R41 parent closure envelope hash mismatch');
61
+ const key=createPublicKey(publicPem);insist(key.asymmetricKeyType==='ed25519','R41 parent closure key must be Ed25519');
62
+ const der=Buffer.from(key.export({type:'spki',format:'der'}));
63
+ const keyId=sha256(der);
64
+ insist(envelope.attestation?.algorithm==='ed25519'&&envelope.attestation?.domain==='DEADBYTE-R41-FINAL-CLOSURE-V1','R41 parent closure attestation contract mismatch');
65
+ insist(envelope.attestation.key_id===keyId,'R41 parent closure signer mismatch');
66
+ insist(cryptoVerify(null,Buffer.concat([CLOSURE_DOMAIN,bodyBytes]),key,Buffer.from(envelope.attestation.signature_base64,'base64')),'R41 parent closure signature invalid');
67
+ return pointer;
68
+ }
69
+
70
+ export async function verifyR42Parent(file=path.join(root,'R42-PARENT.json')){
71
+ const record=JSON.parse(await readFile(path.resolve(file),'utf8'));
72
+ insist(record.schema==='deadbyte.r42-parent.v1','R42 parent schema mismatch');
73
+ insist(record.parent_version==='0.13.0'&&record.parent_release_id==='v0.13.0-3255be10de4daf08','R42 parent identity mismatch');
74
+ insist(record.parent_manifest_sha256==='3255be10de4daf0837d67d1ab1913b785b8b8215bc7405823c2ff46968c4b98f','R42 parent manifest mismatch');
75
+ insist(record.parent_generation_manifest_sha256==='143d2d98b675b4ed68cce2bfb1cda76ec22633035b49746e445140fee58b25cd','R42 parent generation mismatch');
76
+ const [sourceFiles,releaseFiles]=await Promise.all([
77
+ verifyTree(path.resolve(record.parent_source_root),'MANIFEST.SHA256',record.parent_manifest_sha256,{exact:false}),
78
+ verifyTree(path.resolve(record.parent_release_root),'MANIFEST.SHA256',record.parent_manifest_sha256,{exact:true})
79
+ ]);
80
+ insist(sourceFiles===record.parent_manifest_file_count&&releaseFiles===record.parent_manifest_file_count,'R42 parent file count mismatch');
81
+
82
+ const closureBytes=await readFile(path.resolve(record.parent_final_closure_path));
83
+ insist(sha256(closureBytes)===record.parent_final_closure_sha256,'R41 parent final closure raw hash mismatch');
84
+ const closure=JSON.parse(closureBytes.toString('utf8').replace(/^\uFEFF/,''));
85
+ const pointer=verifySignedClosure(closure,record,await readFile(path.join(stateRoot,'trust','ed25519-public.pem')));
86
+ const generationFiles=await verifyTree(path.resolve(pointer.generation_root),'GENERATION.SHA256',record.parent_generation_manifest_sha256,{exact:true});
87
+
88
+ const tarball=await readFile(path.resolve(record.parent_public_tarball_path));
89
+ insist(sha256(tarball)===record.parent_public_tarball_sha256,'R41 parent public tarball SHA-256 mismatch');
90
+ insist(sha1(tarball)===record.parent_public_tarball_sha1,'R41 parent public tarball SHA-1 mismatch');
91
+
92
+ return {status:'passed',parent_release_id:record.parent_release_id,parent_manifest_sha256:record.parent_manifest_sha256,
93
+ parent_generation_manifest_sha256:record.parent_generation_manifest_sha256,source_files:sourceFiles,release_files:releaseFiles,
94
+ generation_files:generationFiles,parent_final_closure_sha256:record.parent_final_closure_sha256,
95
+ parent_public_tarball_sha256:record.parent_public_tarball_sha256,parent_public_tarball_sha1:record.parent_public_tarball_sha1};
96
+ }
97
+
98
+ if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
99
+ verifyR42Parent(process.argv[2]).then(value=>console.log(JSON.stringify(value,null,2)))
100
+ .catch(error=>{console.error(error instanceof Error?error.stack:String(error));process.exitCode=1;});
101
+ }
@@ -0,0 +1,64 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, open, readFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { canonicalJson } from '../src/canonical-json.mjs';
7
+ import { readAndVerifyR42EvalEvidence } from '../src/agent-eval-runner-r42.mjs';
8
+
9
+ const here=path.dirname(fileURLToPath(import.meta.url));
10
+ const root=path.resolve(here,'..');
11
+ const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
12
+ const marker='DEADBYTE V0.14.0 / R42 WINDOWS BATCH GATE: PASS';
13
+ function insist(ok,message){if(!ok) throw new Error(message);}
14
+
15
+ export async function writeWindowsGateEvidence({
16
+ candidateRoot=root,
17
+ stateRoot=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp')),
18
+ clock=()=>new Date()
19
+ }={}){
20
+ const candidate=path.resolve(candidateRoot);
21
+ const manifestBytes=await readFile(path.join(candidate,'MANIFEST.SHA256'));
22
+ const manifestSha256=sha256(manifestBytes);
23
+ const pkg=JSON.parse(await readFile(path.join(candidate,'package.json'),'utf8'));
24
+ insist(pkg.version==='0.14.0',`Windows gate evidence requires V0.14.0, found ${pkg.version}`);
25
+
26
+ const suite=JSON.parse(await readFile(path.join(candidate,'harness','r42-agent-evals.json'),'utf8'));
27
+ const suiteSha256=sha256(Buffer.from(canonicalJson(suite),'utf8'));
28
+ const evalId=`r42-${manifestSha256.slice(0,16)}-${suiteSha256.slice(0,16)}`;
29
+ const evalPath=path.join(path.resolve(stateRoot),'evidence','release','r42-agent-evals','agent_eval',`${evalId}.json`);
30
+ const evalVerified=await readAndVerifyR42EvalEvidence(evalPath,{subjectManifestSha256:manifestSha256,suiteSha256});
31
+
32
+ const completed=clock();
33
+ insist(completed instanceof Date&&!Number.isNaN(completed.getTime()),'Windows gate evidence clock invalid');
34
+ const evidence={
35
+ schema:'deadbyte.r42-windows-gate.v1',status:'passed',exit_code:0,version:'0.14.0',
36
+ marker,completed_at_utc:completed.toISOString(),manifest_sha256:manifestSha256,
37
+ model_eval:{path:evalPath,evidence_sha256:evalVerified.evidence_sha256,payload_sha256:evalVerified.payload_sha256,
38
+ suite_sha256:suiteSha256,case_count:evalVerified.payload.case_count}
39
+ };
40
+ const bytes=Buffer.from(`${JSON.stringify(evidence,null,2)}\n`,'utf8');
41
+ const dir=path.join(path.resolve(stateRoot),'evidence','release');
42
+ await mkdir(dir,{recursive:true});
43
+ const file=path.join(dir,`r42-windows-gate-${manifestSha256.slice(0,16)}.json`);
44
+ try{
45
+ const handle=await open(file,'wx',0o600);
46
+ try{await handle.writeFile(bytes);await handle.sync();}finally{await handle.close();}
47
+ return {status:'written',path:file,sha256:sha256(bytes),manifest_sha256:manifestSha256,model_eval:evidence.model_eval};
48
+ }catch(error){
49
+ if(error?.code!=='EEXIST') throw error;
50
+ const existingBytes=await readFile(file);
51
+ const existing=JSON.parse(existingBytes.toString('utf8').replace(/^\uFEFF/,''));
52
+ insist(existing.schema===evidence.schema&&existing.status==='passed'&&existing.exit_code===0,'existing R42 Windows gate evidence invalid');
53
+ insist(existing.version==='0.14.0'&&existing.marker===marker&&existing.manifest_sha256===manifestSha256,'existing R42 Windows gate identity mismatch');
54
+ insist(existing.model_eval?.evidence_sha256===evidence.model_eval.evidence_sha256&&existing.model_eval?.suite_sha256===suiteSha256,'existing R42 Windows gate model-eval mismatch');
55
+ return {status:'existing',path:file,sha256:sha256(existingBytes),manifest_sha256:manifestSha256,model_eval:existing.model_eval};
56
+ }
57
+ }
58
+
59
+ const invoked=process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url);
60
+ if(invoked){
61
+ writeWindowsGateEvidence()
62
+ .then(result=>console.log(JSON.stringify(result)))
63
+ .catch(error=>{console.error(error instanceof Error?error.stack:String(error));process.exitCode=1;});
64
+ }
@@ -0,0 +1,73 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalJson } from './canonical-json.mjs';
3
+
4
+ const HEX64=/^[0-9a-f]{64}$/;
5
+ const ID=/^[A-Za-z0-9._-]{1,96}$/;
6
+ const SEVERITY=new Set(['info','low','medium','high','critical']);
7
+ const sha=value=>createHash('sha256').update(Buffer.from(canonicalJson(value),'utf8')).digest('hex');
8
+ function object(value,label){if(!value||typeof value!=='object'||Array.isArray(value))throw new Error(`${label} invalid`);return value;}
9
+ function keys(value,allowed,label){const extra=Object.keys(value).filter(key=>!allowed.includes(key));if(extra.length)throw new Error(`${label} unknown field '${extra[0]}'`);}
10
+ function id(value,label){if(typeof value!=='string'||!ID.test(value))throw new Error(`${label} invalid`);return value;}
11
+ function text(value,label,max=8192){if(typeof value!=='string'||value.length<1||value.length>max||value.includes('\0'))throw new Error(`${label} invalid`);return value;}
12
+ function textList(value,label,maxItems=32,maxLength=1024){if(!Array.isArray(value)||value.length>maxItems||value.some(item=>typeof item!=='string'||item.length<1||item.length>maxLength))throw new Error(`${label} invalid`);return [...value];}
13
+
14
+ function normalizeCase(raw){
15
+ const value=object(raw,'agent eval case');keys(value,['schema','case_id','task','allowed_context','hidden_oracle','acceptance'],'agent eval case');
16
+ if(value.schema!=='deadbyte.agent-eval-case.v1')throw new Error('agent eval case schema invalid');
17
+ const core={schema:value.schema,case_id:id(value.case_id,'eval case_id'),task:text(value.task,'eval task'),
18
+ allowed_context:structuredClone(object(value.allowed_context,'eval allowed_context')),
19
+ hidden_oracle:structuredClone(object(value.hidden_oracle,'eval hidden_oracle')),
20
+ acceptance:textList(value.acceptance,'eval acceptance')};
21
+ return Object.freeze({...core,case_sha256:sha(core)});
22
+ }
23
+
24
+ function normalizeJudgeResult(raw){
25
+ const value=object(raw,'judge result');keys(value,['schema','verdict','advisory','findings','evidence_refs'],'judge result');
26
+ if(value.schema!==undefined&&value.schema!=='deadbyte.agent-eval-judge-result.v1')throw new Error('judge result schema invalid');
27
+ if(!['pass','fail'].includes(value.verdict))throw new Error('judge verdict invalid');
28
+ if(value.advisory!==undefined&&value.advisory!==true)throw new Error('judge result must remain advisory');
29
+ if(!Array.isArray(value.findings)||value.findings.length>64)throw new Error('judge findings invalid');
30
+ const findings=value.findings.map(finding=>{
31
+ object(finding,'judge finding');keys(finding,['code','severity','evidence'],'judge finding');
32
+ if(!SEVERITY.has(finding.severity))throw new Error('judge finding severity invalid');
33
+ return {code:id(finding.code,'judge finding code'),severity:finding.severity,evidence:text(finding.evidence,'judge finding evidence',2048)};
34
+ });
35
+ if(!Array.isArray(value.evidence_refs)||value.evidence_refs.length<1||value.evidence_refs.length>64||value.evidence_refs.some(ref=>!HEX64.test(ref)))throw new Error('judge evidence refs invalid');
36
+ return Object.freeze({schema:'deadbyte.agent-eval-judge-result.v1',verdict:value.verdict,advisory:true,findings,evidence_refs:[...value.evidence_refs]});
37
+ }
38
+
39
+ export async function runIsolatedEvaluation({caseSpec,generator,judge}){
40
+ const normalized=normalizeCase(caseSpec);
41
+ if(typeof generator!=='function'||typeof judge!=='function')throw new Error('eval generator and judge required');
42
+ const generatorInput=Object.freeze({schema:'deadbyte.agent-eval-generator-input.v1',case_id:normalized.case_id,
43
+ task:normalized.task,allowed_context:structuredClone(normalized.allowed_context),acceptance:[...normalized.acceptance]});
44
+ const generated=object(await generator(generatorInput),'generator result');
45
+ keys(generated,['artifact','trace'],'generator result');
46
+ const artifact=structuredClone(object(generated.artifact,'generator artifact'));
47
+ const candidateSha=sha(artifact);
48
+ const judgeInput=Object.freeze({schema:'deadbyte.agent-eval-judge-input.v1',case_id:normalized.case_id,
49
+ task:normalized.task,acceptance:[...normalized.acceptance],candidate:artifact,candidate_sha256:candidateSha,
50
+ hidden_oracle:structuredClone(normalized.hidden_oracle)});
51
+ const judged=normalizeJudgeResult(await judge(judgeInput));
52
+ const core={schema:'deadbyte.agent-eval-result.v1',case_id:normalized.case_id,case_sha256:normalized.case_sha256,
53
+ candidate_sha256:candidateSha,verdict:judged.verdict,advisory:true,findings:judged.findings,evidence_refs:judged.evidence_refs};
54
+ return Object.freeze({...core,result_sha256:sha(core)});
55
+ }
56
+
57
+ export function buildFreshCriticContext(raw){
58
+ const value=object(raw,'fresh critic context');keys(value,['case_id','artifact','evidence','criteria'],'fresh critic context');
59
+ id(value.case_id,'critic case_id');object(value.artifact,'critic artifact');
60
+ if(!Array.isArray(value.evidence)||value.evidence.length>128)throw new Error('critic evidence invalid');
61
+ return Object.freeze({schema:'deadbyte.fresh-critic-context.v1',case_id:value.case_id,
62
+ artifact:structuredClone(value.artifact),evidence:structuredClone(value.evidence),
63
+ criteria:textList(value.criteria,'critic criteria',64,1024)});
64
+ }
65
+
66
+ export function triageJudgeResult(raw,{critical_action,noncritical_action}){
67
+ const judge=normalizeJudgeResult(raw);
68
+ if(judge.advisory!==true)throw new Error('judge result must remain advisory');
69
+ if(!['pause','repair','continue'].includes(critical_action)||!['pause','repair','continue'].includes(noncritical_action))throw new Error('judge triage action invalid');
70
+ const critical=judge.findings.some(finding=>finding.severity==='critical'||finding.severity==='high');
71
+ return Object.freeze({action:judge.verdict==='pass'?'continue':(critical?critical_action:noncritical_action),
72
+ reason_codes:judge.findings.map(finding=>finding.code),judge_advisory:true});
73
+ }
@@ -0,0 +1,138 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { canonicalJson } from './canonical-json.mjs';
5
+ import { runIsolatedEvaluation } from './agent-eval-r42.mjs';
6
+ import { publishEvidenceAtomic } from './evidence-publisher-r42.mjs';
7
+
8
+ const HEX64=/^[0-9a-f]{64}$/;
9
+ const ID=/^[A-Za-z0-9._-]{1,96}$/;
10
+ const sha=value=>createHash('sha256').update(Buffer.from(canonicalJson(value),'utf8')).digest('hex');
11
+ const shaText=value=>createHash('sha256').update(Buffer.from(String(value),'utf8')).digest('hex');
12
+
13
+ function object(value,label){if(!value||typeof value!=='object'||Array.isArray(value))throw new Error(`${label} invalid`);return value;}
14
+ function keys(value,allowed,label){const extra=Object.keys(value).filter(key=>!allowed.includes(key));if(extra.length)throw new Error(`${label} unknown field '${extra[0]}'`);}
15
+ function parseJson(raw,label){
16
+ if(typeof raw!=='string'||raw.length<2||raw.length>1024*1024)throw new Error(`${label} response invalid`);
17
+ let value;try{value=JSON.parse(raw);}catch{throw new Error(`${label} response must be strict JSON`);}
18
+ return object(value,label);
19
+ }
20
+ function providerMeta(raw){
21
+ const value=object(raw,'model response');
22
+ const meta={
23
+ backend:typeof value.backend==='string'?value.backend:'injected',
24
+ model:typeof value.model==='string'?value.model.slice(0,128):'injected',
25
+ response_id:typeof value.response_id==='string'?value.response_id.slice(0,160):null,
26
+ raw_response_sha256:HEX64.test(value.raw_response_sha256??'')?value.raw_response_sha256:shaText(value.raw_text??''),
27
+ usage:value.usage&&Number.isInteger(value.usage.input_tokens)&&Number.isInteger(value.usage.output_tokens)&&Number.isInteger(value.usage.total_tokens)
28
+ ?{input_tokens:value.usage.input_tokens,output_tokens:value.usage.output_tokens,total_tokens:value.usage.total_tokens}:null
29
+ };
30
+ return Object.freeze(meta);
31
+ }
32
+ function normalizeSuite(raw){
33
+ const value=object(raw,'eval suite');keys(value,['schema','suite_id','cases'],'eval suite');
34
+ if(value.schema!=='deadbyte.agent-eval-suite.v1')throw new Error('eval suite schema invalid');
35
+ if(typeof value.suite_id!=='string'||!ID.test(value.suite_id))throw new Error('eval suite_id invalid');
36
+ if(!Array.isArray(value.cases)||value.cases.length<1||value.cases.length>32)throw new Error('eval suite cases invalid');
37
+ const ids=new Set();
38
+ for(const item of value.cases){
39
+ object(item,'eval suite case');
40
+ if(item.schema!=='deadbyte.agent-eval-case.v1'||typeof item.case_id!=='string'||!ID.test(item.case_id)||ids.has(item.case_id))throw new Error('eval suite case identity invalid');
41
+ ids.add(item.case_id);
42
+ }
43
+ const copy=structuredClone(value);
44
+ return Object.freeze({...copy,suite_sha256:sha(copy)});
45
+ }
46
+ function generatorPrompt(input){
47
+ return [
48
+ 'You are the implementation-side evaluator for DEADBYTE R42.',
49
+ 'Return exactly one JSON object with shape {"artifact":{...}} and no Markdown.',
50
+ 'Do not invent authority, credentials, or evidence. Prefer bounded, typed, fail-closed behavior.',
51
+ `INPUT=${canonicalJson(input)}`
52
+ ].join('\n');
53
+ }
54
+ function judgePrompt(input){
55
+ return [
56
+ 'You are an independent advisory judge for DEADBYTE R42.',
57
+ 'Return exactly one JSON object with schema deadbyte.agent-eval-judge-result.v1.',
58
+ 'Shape: {"schema":"deadbyte.agent-eval-judge-result.v1","verdict":"pass|fail","advisory":true,"findings":[{"code":"ID","severity":"info|low|medium|high|critical","evidence":"text"}],"evidence_refs":["64hex"]}.',
59
+ 'Judge only the supplied candidate against acceptance and hidden oracle. Do not assume implementer reasoning.',
60
+ 'Use candidate_sha256 as at least one evidence_refs entry.',
61
+ `INPUT=${canonicalJson(input)}`
62
+ ].join('\n');
63
+ }
64
+
65
+ export function verifyR42EvalPayload(payload,{subjectManifestSha256=null,suiteSha256=null}={}){
66
+ const value=object(payload,'R42 eval payload');
67
+ keys(value,['schema','status','subject_manifest_sha256','suite_id','suite_sha256','case_count','cases','completed_at_utc'],'R42 eval payload');
68
+ if(value.schema!=='deadbyte.r42-model-eval.v1'||value.status!=='passed')throw new Error('R42 eval payload is not passed');
69
+ if(!HEX64.test(value.subject_manifest_sha256??''))throw new Error('R42 eval manifest invalid');
70
+ if(subjectManifestSha256!==null&&value.subject_manifest_sha256!==subjectManifestSha256)throw new Error('R42 eval manifest mismatch');
71
+ if(!HEX64.test(value.suite_sha256??''))throw new Error('R42 eval suite hash invalid');
72
+ if(suiteSha256!==null&&value.suite_sha256!==suiteSha256)throw new Error('R42 eval suite hash mismatch');
73
+ if(typeof value.suite_id!=='string'||!ID.test(value.suite_id))throw new Error('R42 eval suite id invalid');
74
+ if(!Number.isInteger(value.case_count)||value.case_count<1||!Array.isArray(value.cases)||value.cases.length!==value.case_count)throw new Error('R42 eval cases invalid');
75
+ const seen=new Set();
76
+ for(const item of value.cases){
77
+ object(item,'R42 eval case result');keys(item,['case_id','result','generator','judge'],'R42 eval case result');
78
+ if(typeof item.case_id!=='string'||!ID.test(item.case_id)||seen.has(item.case_id))throw new Error('R42 eval case result identity invalid');
79
+ seen.add(item.case_id);
80
+ if(item.result?.schema!=='deadbyte.agent-eval-result.v1'||item.result?.verdict!=='pass'||item.result?.advisory!==true||!HEX64.test(item.result?.result_sha256??''))throw new Error(`R42 eval case did not pass: ${item.case_id}`);
81
+ for(const role of ['generator','judge']){
82
+ if(!HEX64.test(item[role]?.raw_response_sha256??''))throw new Error(`R42 eval ${role} provenance invalid`);
83
+ }
84
+ }
85
+ if(typeof value.completed_at_utc!=='string'||!Number.isFinite(Date.parse(value.completed_at_utc)))throw new Error('R42 eval completion time invalid');
86
+ return true;
87
+ }
88
+
89
+ export async function runAgentEvalSuiteR42({suite,subjectManifestSha256,requestModel,evidenceRoot=null,clock=()=>new Date()}){
90
+ const normalized=normalizeSuite(suite);
91
+ if(!HEX64.test(subjectManifestSha256??''))throw new Error('R42 eval subject manifest invalid');
92
+ if(typeof requestModel!=='function')throw new Error('R42 eval requestModel required');
93
+ const rows=[];
94
+ for(const caseSpec of normalized.cases){
95
+ let generatorMeta=null,judgeMeta=null;
96
+ const result=await runIsolatedEvaluation({
97
+ caseSpec,
98
+ generator:async input=>{
99
+ const response=object(await requestModel({role:'generator',case_id:caseSpec.case_id,prompt:generatorPrompt(input)}),'generator model response');
100
+ generatorMeta=providerMeta(response);
101
+ const parsed=parseJson(response.raw_text,'generator');
102
+ keys(parsed,['artifact'],'generator response');
103
+ return {artifact:object(parsed.artifact,'generator artifact')};
104
+ },
105
+ judge:async input=>{
106
+ const response=object(await requestModel({role:'judge',case_id:caseSpec.case_id,prompt:judgePrompt(input)}),'judge model response');
107
+ judgeMeta=providerMeta(response);
108
+ return parseJson(response.raw_text,'judge');
109
+ }
110
+ });
111
+ rows.push({case_id:caseSpec.case_id,result,generator:generatorMeta,judge:judgeMeta});
112
+ }
113
+ if(rows.some(row=>row.result.verdict!=='pass'))throw new Error('R42 model-backed eval suite failed');
114
+ const completed=clock();if(!(completed instanceof Date)||Number.isNaN(completed.getTime()))throw new Error('R42 eval clock invalid');
115
+ const payload={
116
+ schema:'deadbyte.r42-model-eval.v1',status:'passed',subject_manifest_sha256:subjectManifestSha256,
117
+ suite_id:normalized.suite_id,suite_sha256:normalized.suite_sha256,case_count:rows.length,cases:rows,
118
+ completed_at_utc:completed.toISOString()
119
+ };
120
+ verifyR42EvalPayload(payload,{subjectManifestSha256,suiteSha256:normalized.suite_sha256});
121
+ let publication=null;
122
+ if(evidenceRoot!==null){
123
+ if(typeof evidenceRoot!=='string'||!path.isAbsolute(evidenceRoot))throw new Error('R42 eval evidence root must be absolute');
124
+ const evidenceId=`r42-${subjectManifestSha256.slice(0,16)}-${normalized.suite_sha256.slice(0,16)}`;
125
+ publication=await publishEvidenceAtomic({root:evidenceRoot,kind:'agent_eval',evidenceId,payload});
126
+ }
127
+ return Object.freeze({payload,publication});
128
+ }
129
+
130
+ export async function readAndVerifyR42EvalEvidence(file,{subjectManifestSha256,suiteSha256}={}){
131
+ const envelope=JSON.parse(await readFile(path.resolve(file),'utf8'));
132
+ if(envelope?.schema!=='deadbyte.evidence-publication.v1'||envelope?.kind!=='agent_eval')throw new Error('R42 eval evidence envelope invalid');
133
+ if(!HEX64.test(envelope.payload_sha256??'')||sha(envelope.payload)!==envelope.payload_sha256)throw new Error('R42 eval payload hash mismatch');
134
+ const {evidence_sha256,...body}=envelope;
135
+ if(!HEX64.test(evidence_sha256??'')||sha(body)!==evidence_sha256)throw new Error('R42 eval evidence hash mismatch');
136
+ verifyR42EvalPayload(envelope.payload,{subjectManifestSha256,suiteSha256});
137
+ return Object.freeze({status:'passed',evidence_sha256,payload_sha256:envelope.payload_sha256,payload:envelope.payload});
138
+ }
@@ -0,0 +1,142 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalJson } from './canonical-json.mjs';
3
+
4
+ const HEX64=/^[0-9a-f]{64}$/;
5
+ const ID=/^[A-Za-z0-9._-]{1,64}$/;
6
+ const RIGOR=Object.freeze({lightweight:0,structured:1,agentic:2});
7
+ const RISK_MINIMUM=Object.freeze({low:'lightweight',medium:'structured',high:'agentic'});
8
+ const EVIDENCE=Object.freeze({assumption:1,source:2,traced_path:3,automated:4,runtime:5});
9
+
10
+ const sha=value=>createHash('sha256').update(Buffer.from(canonicalJson(value),'utf8')).digest('hex');
11
+ function object(value,label){if(!value||typeof value!=='object'||Array.isArray(value)) throw new Error(`${label} invalid`);return value;}
12
+ function keys(value,allowed,label){const extra=Object.keys(value).filter(key=>!allowed.includes(key));if(extra.length) throw new Error(`${label} unknown field '${extra[0]}'`);}
13
+ function id(value,label){if(typeof value!=='string'||!ID.test(value)) throw new Error(`${label} invalid`);return value;}
14
+ function hex(value,label){if(typeof value!=='string'||!HEX64.test(value)) throw new Error(`${label} invalid`);return value;}
15
+ function text(value,label,max=2048){if(typeof value!=='string'||value.length<1||value.length>max||value.includes('\0')) throw new Error(`${label} invalid`);return value;}
16
+ function idList(value,label,max=128){if(!Array.isArray(value)||value.length>max||value.some(item=>typeof item!=='string'||!ID.test(item))||new Set(value).size!==value.length) throw new Error(`${label} invalid`);return [...value];}
17
+
18
+ export function bindApprovalSubject(raw){
19
+ const value=object(raw,'approval subject');
20
+ keys(value,['goal_id','reviewed_event_seq','reviewed_event_sha256','journal_head_sha256','scope','subject'],'approval subject');
21
+ hex(value.goal_id,'approval goal_id');
22
+ if(!Number.isInteger(value.reviewed_event_seq)||value.reviewed_event_seq<1) throw new Error('approval reviewed_event_seq invalid');
23
+ hex(value.reviewed_event_sha256,'approval reviewed_event_sha256');
24
+ hex(value.journal_head_sha256,'approval journal_head_sha256');
25
+ id(value.scope,'approval scope'); object(value.subject,'approval canonical subject');
26
+ const normalized=structuredClone(value);
27
+ return Object.freeze({...normalized,approval_subject_sha256:sha({schema:'deadbyte.approval-subject.v1',...normalized})});
28
+ }
29
+
30
+ export function validateBoundApproval({approval,subject,now=new Date()}){
31
+ const {approval_subject_sha256:claimedSubjectSha,...subjectBody}=object(subject,'approval subject');
32
+ const bound=bindApprovalSubject(subjectBody);
33
+ if(claimedSubjectSha!==undefined&&claimedSubjectSha!==bound.approval_subject_sha256) throw new Error('approval subject hash mismatch');
34
+ const value=object(approval,'bound approval');
35
+ keys(value,['schema','approval_subject_sha256','reviewed_event_seq','reviewed_event_sha256','journal_head_sha256','scope','issued_at_utc','expires_at_utc','provenance'],'bound approval');
36
+ if(value.schema!=='deadbyte.bound-approval.v1') throw new Error('bound approval schema invalid');
37
+ if(value.approval_subject_sha256!==bound.approval_subject_sha256) throw new Error('bound approval subject mismatch');
38
+ if(value.reviewed_event_seq!==bound.reviewed_event_seq||value.reviewed_event_sha256!==bound.reviewed_event_sha256) throw new Error('bound approval reviewed event mismatch');
39
+ if(value.journal_head_sha256!==bound.journal_head_sha256) throw new Error('bound approval journal head mismatch');
40
+ if(value.scope!==bound.scope) throw new Error('bound approval scope mismatch');
41
+ if(value.provenance!=='operator_mcp') throw new Error('bound approval provenance invalid');
42
+ const issued=Date.parse(value.issued_at_utc),expires=Date.parse(value.expires_at_utc),observed=now.getTime();
43
+ if(!Number.isFinite(issued)||!Number.isFinite(expires)||expires<=issued||expires-issued>3_600_000) throw new Error('bound approval lifetime invalid');
44
+ if(observed<issued||observed>=expires) throw new Error('bound approval expired or not yet valid');
45
+ return true;
46
+ }
47
+
48
+ export function createTaskDag(raw){
49
+ const value=object(raw,'task DAG'); keys(value,['run_id','goal_id','tasks'],'task DAG');
50
+ id(value.run_id,'task DAG run_id'); hex(value.goal_id,'task DAG goal_id');
51
+ if(!Array.isArray(value.tasks)||value.tasks.length<1||value.tasks.length>256) throw new Error('task DAG tasks invalid');
52
+ const tasks=value.tasks.map(item=>{
53
+ object(item,'task DAG task');keys(item,['task_id','depends_on','input_artifacts','output_artifacts','state'],'task DAG task');
54
+ return {task_id:id(item.task_id,'task_id'),depends_on:idList(item.depends_on,'depends_on'),
55
+ input_artifacts:idList(item.input_artifacts,'input_artifacts'),output_artifacts:idList(item.output_artifacts,'output_artifacts'),
56
+ state:item.state??'PENDING'};
57
+ });
58
+ const ids=new Set(tasks.map(task=>task.task_id)); if(ids.size!==tasks.length) throw new Error('task DAG duplicate task_id');
59
+ for(const task of tasks) if(task.depends_on.some(dep=>!ids.has(dep))) throw new Error('task DAG dependency missing');
60
+ if(tasks.some(task=>!['PENDING','RUNNING','COMPLETE','FAILED','STALE'].includes(task.state))) throw new Error('task DAG state invalid');
61
+ const visiting=new Set(),visited=new Set(),byId=new Map(tasks.map(task=>[task.task_id,task]));
62
+ const visit=taskId=>{if(visiting.has(taskId)) throw new Error('task DAG cycle detected');if(visited.has(taskId))return;visiting.add(taskId);for(const dep of byId.get(taskId).depends_on)visit(dep);visiting.delete(taskId);visited.add(taskId);};
63
+ for(const task of tasks) visit(task.task_id);
64
+ const core={schema:'deadbyte.task-dag.v1',run_id:value.run_id,goal_id:value.goal_id,tasks};
65
+ return Object.freeze({...core,dag_sha256:sha(core)});
66
+ }
67
+
68
+ export function invalidateTaskDag(dag,{changed_artifact_ids}){
69
+ const changed=new Set(idList(changed_artifact_ids,'changed_artifact_ids'));
70
+ const stale=new Set(dag.tasks.filter(task=>task.input_artifacts.some(artifact=>changed.has(artifact))).map(task=>task.task_id));
71
+ let grew=true;while(grew){grew=false;for(const task of dag.tasks){if(!stale.has(task.task_id)&&task.depends_on.some(dep=>stale.has(dep))){stale.add(task.task_id);grew=true;}}}
72
+ return createTaskDag({run_id:dag.run_id,goal_id:dag.goal_id,tasks:dag.tasks.map(task=>({...task,state:stale.has(task.task_id)?'STALE':task.state}))});
73
+ }
74
+
75
+ export function taskDagFromPlan({run_id,goal_id,plan}){
76
+ object(plan,'task DAG plan');
77
+ if(!Array.isArray(plan.steps)||plan.steps.length<1) throw new Error('task DAG plan steps invalid');
78
+ const tasks=plan.steps.map((step,index)=>{
79
+ const previous=index===0?null:plan.steps[index-1].step_id;
80
+ return {task_id:step.step_id,depends_on:previous?[previous]:[],
81
+ input_artifacts:previous?[`${previous}.result`]:[],output_artifacts:[`${step.step_id}.result`],
82
+ state:step.status==='completed'?'COMPLETE':step.status==='active'?'RUNNING':'PENDING'};
83
+ });
84
+ return createTaskDag({run_id,goal_id,tasks});
85
+ }
86
+
87
+ export function createEvidenceClaim(raw){
88
+ const value=object(raw,'evidence claim');keys(value,['claim_id','statement','level','refs'],'evidence claim');
89
+ id(value.claim_id,'claim_id');text(value.statement,'claim statement',2048);
90
+ if(!Object.hasOwn(EVIDENCE,value.level)) throw new Error('evidence level invalid');
91
+ if(!Array.isArray(value.refs)||value.refs.length>64||value.refs.some(ref=>!HEX64.test(ref))) throw new Error('evidence reference invalid');
92
+ if(value.level!=='assumption'&&value.refs.length<1) throw new Error('evidence reference required');
93
+ const core={schema:'deadbyte.evidence-claim.v1',claim_id:value.claim_id,statement:value.statement,level:value.level,refs:[...value.refs]};
94
+ return Object.freeze({...core,claim_sha256:sha(core)});
95
+ }
96
+ export function evidenceSatisfies(claim,minimum){if(!Object.hasOwn(EVIDENCE,minimum)) throw new Error('minimum evidence level invalid');return EVIDENCE[claim.level]>=EVIDENCE[minimum];}
97
+
98
+ export function deriveEffectiveRigor({project_default,risk_level,explicit_override=null}){
99
+ if(!Object.hasOwn(RIGOR,project_default)) throw new Error('project default rigor invalid');
100
+ if(!Object.hasOwn(RISK_MINIMUM,risk_level)) throw new Error('risk level invalid');
101
+ if(explicit_override!==null&&!Object.hasOwn(RIGOR,explicit_override)) throw new Error('explicit rigor override invalid');
102
+ const risk_minimum=RISK_MINIMUM[risk_level];
103
+ const values=[project_default,risk_minimum,...(explicit_override?[explicit_override]:[])];
104
+ const effective_rigor=values.reduce((best,item)=>RIGOR[item]>RIGOR[best]?item:best,'lightweight');
105
+ return Object.freeze({project_default,risk_minimum,explicit_override,effective_rigor});
106
+ }
107
+
108
+ export function createDecisionRecord(raw){
109
+ const value=object(raw,'decision record');keys(value,['decision_id','decision','reason','evidence_refs','invalidates_if'],'decision record');
110
+ id(value.decision_id,'decision_id');text(value.decision,'decision',2048);text(value.reason,'decision reason',2048);
111
+ if(!Array.isArray(value.evidence_refs)||value.evidence_refs.length<1||value.evidence_refs.length>64||value.evidence_refs.some(ref=>!HEX64.test(ref))) throw new Error('decision evidence refs invalid');
112
+ if(!Array.isArray(value.invalidates_if)||value.invalidates_if.length>32) throw new Error('decision invalidation rules invalid');
113
+ const rules=value.invalidates_if.map(rule=>{
114
+ object(rule,'decision invalidation rule');
115
+ if(rule.kind==='artifact_sha_changed'){
116
+ keys(rule,['kind','artifact_id','expected_sha256'],'artifact invalidation rule');
117
+ return {kind:rule.kind,artifact_id:id(rule.artifact_id,'artifact_id'),expected_sha256:hex(rule.expected_sha256,'expected artifact sha256')};
118
+ }
119
+ if(rule.kind==='policy_sha_changed'){
120
+ keys(rule,['kind','expected_sha256'],'policy invalidation rule');
121
+ return {kind:rule.kind,expected_sha256:hex(rule.expected_sha256,'expected policy sha256')};
122
+ }
123
+ throw new Error('decision invalidation rule kind invalid');
124
+ });
125
+ const core={schema:'deadbyte.decision-record.v1',decision_id:value.decision_id,decision:value.decision,reason:value.reason,
126
+ evidence_refs:[...value.evidence_refs],invalidates_if:rules};
127
+ return Object.freeze({...core,decision_sha256:sha(core)});
128
+ }
129
+
130
+ export function evaluateDecisionFreshness(record,observed){
131
+ object(record,'decision record');object(observed,'decision observed state');
132
+ const invalidated_by=[];
133
+ for(const rule of record.invalidates_if){
134
+ if(rule.kind==='artifact_sha_changed'){
135
+ const actual=observed.artifact_sha256?.[rule.artifact_id]??null;
136
+ if(actual!==rule.expected_sha256) invalidated_by.push({...rule,observed_sha256:actual});
137
+ }else if(rule.kind==='policy_sha_changed'&&observed.policy_sha256!==rule.expected_sha256){
138
+ invalidated_by.push({...rule,observed_sha256:observed.policy_sha256??null});
139
+ }
140
+ }
141
+ return Object.freeze({decision_id:record.decision_id,state:invalidated_by.length?'STALE':'CURRENT',invalidated_by});
142
+ }
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { canonicalJson } from './canonical-json.mjs';
3
3
  import { provenanceFor, dedupeEvidence } from './context-provenance.mjs';
4
+ import { taskDagFromPlan } from './agent-governance-r42.mjs';
4
5
 
5
6
  const SECRET_KEY = /(?:^|[_-])(token|password|secret|authorization|cookie|credential|credentials|api[_-]?key|private[_-]?key|recovery)(?:$|[_-])/i;
6
7
  const HEX64 = /^[0-9a-f]{64}$/;
@@ -252,6 +253,7 @@ export function buildLoopContext({ goal, policy, loopState = {}, tree = [], cont
252
253
  desktop_memory:desktopMemory===null?null:structuredClone(desktopMemory),
253
254
  long_term_memory:longTermMemory===null?null:structuredClone(longTermMemory),
254
255
  capabilities:capabilities===null?null:structuredClone(capabilities),
256
+ task_dag:loopState.plan&&typeof goal?.goal_id==='string' ? taskDagFromPlan({run_id:goal.goal_id,goal_id:goal.goal_id,plan:loopState.plan}) : null,
255
257
  profiles
256
258
  };
257
259
  return compactLoopContext(state,policy.limits.max_context_bytes);
@@ -146,6 +146,7 @@ export function deriveLoopState(goal,events) {
146
146
  replan_count:0,
147
147
  pending_action:null,
148
148
  pending_approval:false,
149
+ clarification:null,
149
150
  progress:null,
150
151
  critic:null,
151
152
  repair:null,
@@ -223,6 +224,10 @@ export function deriveLoopState(goal,events) {
223
224
  state.phase='WAITING';
224
225
  state.wait_reason=requireText(payload.reason,'loop wait reason');
225
226
  state.pending_approval=payload.requires_approval===true;
227
+ state.clarification=payload.reason==='human_input_required' ? {
228
+ question:requireText(payload.question,'loop clarification question',1000),
229
+ choices:Array.isArray(payload.choices) ? payload.choices.map((choice,index)=>requireText(choice,`loop clarification choice ${index+1}`,256)) : []
230
+ } : null;
226
231
  break;
227
232
  case 'waiting_resumed': {
228
233
  if (state.phase !== 'WAITING') throw new Error('loop waiting_resumed outside WAITING phase');
@@ -231,6 +236,7 @@ export function deriveLoopState(goal,events) {
231
236
  state.phase=resumed;
232
237
  state.wait_reason=null;
233
238
  state.pending_approval=false;
239
+ state.clarification=null;
234
240
  break;
235
241
  }
236
242
  case 'planner_requested':
@@ -255,24 +261,28 @@ export function deriveLoopState(goal,events) {
255
261
  state.phase='PLAN';
256
262
  state.wait_reason=null;
257
263
  state.pending_approval=false;
264
+ state.clarification=null;
258
265
  }
259
266
  break;
260
267
  case 'goal_cancelled':
261
268
  state.phase='FAILED';
262
269
  state.wait_reason=typeof payload.reason==='string'&&payload.reason.length>0?payload.reason:'cancelled';
263
270
  state.pending_approval=false;
271
+ state.clarification=null;
264
272
  state.pending_action=null;
265
273
  break;
266
274
  case 'goal_failed':
267
275
  state.phase='FAILED';
268
276
  state.wait_reason=typeof payload.reason==='string'&&payload.reason.length>0?payload.reason:'failed';
269
277
  state.pending_approval=false;
278
+ state.clarification=null;
270
279
  state.pending_action=null;
271
280
  break;
272
281
  case 'release_completed':
273
282
  state.phase='SUCCEEDED';
274
283
  state.wait_reason=null;
275
284
  state.pending_approval=false;
285
+ state.clarification=null;
276
286
  state.pending_action=null;
277
287
  break;
278
288
  default: