@yunhozz/arc 0.1.7 → 0.1.10
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/MANIFEST.sha256 +86 -61
- package/README.md +1 -1
- package/claude/agents/{arc-coder.md → arc-implementer.md} +7 -7
- package/claude/agents/arc-planner.md +4 -4
- package/claude/agents/arc-qa.md +4 -4
- package/claude/agents/arc-reviewer.md +5 -5
- package/claude/agents/arc-scouter.md +3 -3
- package/claude/skill/SKILL.md +7 -7
- package/claude/skill/models.toml +3 -3
- package/claude/skill/references/commit.md +22 -7
- package/claude/skill/references/coordination.md +25 -18
- package/claude/skill/references/git-recovery.md +1 -1
- package/claude/skill/references/handoff-prep.md +25 -2
- package/claude/skill/references/handoff.md +106 -24
- package/claude/skill/references/planning.md +73 -22
- package/claude/skill/references/verification.md +155 -17
- package/claude/skill/runtime/acceptance.mjs +68 -0
- package/claude/skill/runtime/assignment.mjs +42 -0
- package/claude/skill/runtime/bootstrap.mjs +35 -0
- package/claude/skill/runtime/cooperative-schema.mjs +55 -0
- package/claude/skill/runtime/cooperative.mjs +136 -0
- package/claude/skill/runtime/evaluate.mjs +40 -4
- package/claude/skill/runtime/external.mjs +51 -0
- package/claude/skill/runtime/gate.mjs +7 -7
- package/claude/skill/runtime/git-evidence.mjs +46 -0
- package/claude/skill/runtime/operations.mjs +2 -2
- package/claude/skill/runtime/role-policy.json +64 -19
- package/claude/skill/runtime/schema.mjs +1 -0
- package/claude/skill/runtime/scope.mjs +36 -0
- package/claude/skill/runtime/store.mjs +25 -4
- package/claude/skill/runtime/verification.mjs +123 -0
- package/claude/skill/scripts/model_config.py +2 -2
- package/codex/agents/{arc-coder.toml → arc-implementer.toml} +8 -8
- package/codex/agents/arc-planner.toml +6 -6
- package/codex/agents/arc-qa.toml +6 -6
- package/codex/agents/arc-reviewer.toml +6 -6
- package/codex/agents/arc-scouter.toml +5 -5
- package/codex/skill/SKILL.md +9 -9
- package/codex/skill/models.toml +3 -3
- package/codex/skill/references/commit.md +23 -8
- package/codex/skill/references/coordination.md +25 -18
- package/codex/skill/references/git-recovery.md +2 -2
- package/codex/skill/references/handoff-prep.md +25 -2
- package/codex/skill/references/handoff-worktree.md +4 -4
- package/codex/skill/references/handoff.md +116 -34
- package/codex/skill/references/planning.md +76 -23
- package/codex/skill/references/verification.md +160 -22
- package/codex/skill/runtime/acceptance.mjs +68 -0
- package/codex/skill/runtime/assignment.mjs +42 -0
- package/codex/skill/runtime/bootstrap.mjs +35 -0
- package/codex/skill/runtime/cooperative-schema.mjs +55 -0
- package/codex/skill/runtime/cooperative.mjs +136 -0
- package/codex/skill/runtime/evaluate.mjs +40 -4
- package/codex/skill/runtime/external.mjs +51 -0
- package/codex/skill/runtime/gate.mjs +7 -7
- package/codex/skill/runtime/git-evidence.mjs +46 -0
- package/codex/skill/runtime/operations.mjs +2 -2
- package/codex/skill/runtime/role-policy.json +64 -19
- package/codex/skill/runtime/schema.mjs +1 -0
- package/codex/skill/runtime/scope.mjs +36 -0
- package/codex/skill/runtime/store.mjs +25 -4
- package/codex/skill/runtime/verification.mjs +123 -0
- package/codex/skill/scripts/model_config.py +3 -3
- package/lib/cli/root.mjs +1 -0
- package/lib/cli/workflow.mjs +21 -0
- package/lib/installer/manifest.mjs +1 -1
- package/lib/installer/plan.mjs +27 -13
- package/lib/installer/targets.mjs +1 -1
- package/lib/models/config.mjs +1 -1
- package/lib/models/profiles.mjs +1 -1
- package/lib/policy/placement.mjs +6 -6
- package/lib/policy/render.mjs +3 -3
- package/lib/policy/role-policy.json +64 -19
- package/lib/release/artifact.mjs +1 -1
- package/lib/runtime/acceptance.mjs +68 -0
- package/lib/runtime/assignment.mjs +42 -0
- package/lib/runtime/bootstrap.mjs +35 -0
- package/lib/runtime/cooperative-schema.mjs +55 -0
- package/lib/runtime/cooperative.mjs +136 -0
- package/lib/runtime/evaluate.mjs +40 -4
- package/lib/runtime/external.mjs +51 -0
- package/lib/runtime/gate.mjs +7 -7
- package/lib/runtime/git-evidence.mjs +46 -0
- package/lib/runtime/operations.mjs +2 -2
- package/lib/runtime/schema.mjs +1 -0
- package/lib/runtime/scope.mjs +36 -0
- package/lib/runtime/store.mjs +25 -4
- package/lib/runtime/verification.mjs +123 -0
- package/package.json +4 -2
- package/claude/skill/scripts/__pycache__/model_config.cpython-314.pyc +0 -0
- package/codex/skill/scripts/__pycache__/apply-models.cpython-314.pyc +0 -0
- package/codex/skill/scripts/__pycache__/model_config.cpython-314.pyc +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {fingerprintRequest} from './store.mjs';
|
|
2
|
+
import {absolute,within} from './scope.mjs';
|
|
3
|
+
const completed=(state,id)=>state.tasks?.some(t=>t.id===id&&t.status==='committed'&&!t.invalidated)&&state.commits?.some(c=>c.taskId===id&&c.observationSource==='local-git'&&state.intents.some(i=>i.operation==='commit'&&i.requestId===c.requestId&&i.taskId===id&&i.status==='succeeded'));
|
|
4
|
+
const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
5
|
+
const text=value=>typeof value==='string'&&value.length>0;
|
|
6
|
+
const version=value=>Number.isSafeInteger(value)&&value>0;
|
|
7
|
+
const setEqual=(a,b)=>Array.isArray(a)&&Array.isArray(b)&&new Set(a).size===a.length&&new Set(b).size===b.length&&same([...a].sort(),[...b].sort());
|
|
8
|
+
const base=state=>({expectedRevision:state?.revision??0,coverage:{enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false},requiredEvidence:[],nextActions:[],proposedTransition:null});
|
|
9
|
+
const hold=(state,id,reason)=>({...base(state),decision:'hold',ruleIds:['cooperative.'+id],reason});
|
|
10
|
+
/** Pure gate. observations are supplied by trusted local readers, never JSON input. */
|
|
11
|
+
export function prepareCooperativeAssignment({policy,state,request,observations}={}){
|
|
12
|
+
if(policy!==undefined&&policy.workflowKind!=='cooperative')return hold(state,'contract','Explicit cooperative contract required');
|
|
13
|
+
const args=request?.arguments;
|
|
14
|
+
if(state?.mode!=='plan'||request?.operation!=='assign-agent'||!args)return hold(state,'unsupported','Only explicit plan assignment is supported');
|
|
15
|
+
const allowed=['role','paths','operations','documents','approvalRef','approvalVersion','documentVersion','correlation'];
|
|
16
|
+
if(Object.keys(args).some(k=>!allowed.includes(k))||args.role!=='implementer'||!text(request.taskId)||!text(request.requestId)||!text(args.correlation)||!text(args.approvalRef)||!version(args.approvalVersion)||!version(args.documentVersion)||!Array.isArray(args.paths)||!args.paths.length||new Set(args.paths).size!==args.paths.length||args.paths.some(p=>!absolute(p)||!within(state.target.realpath,p))||!Array.isArray(args.operations)||!args.operations.length||args.operations.some(o=>o!=='edit'))return hold(state,'assignment','Invalid assignment or unsupported historical agent import');
|
|
17
|
+
if(!same(observations?.target,state.target)||!same(observations?.git?.target,state.target))return hold(state,'target','Re-observe target and renew approval after drift');
|
|
18
|
+
if(request.expectedRevision!==state.revision)return hold(state,'revision','Stale assignment request');
|
|
19
|
+
if(state.records.some(r=>r.data?.priorExecution===true)||state.assignments.some(a=>a.source!=='operator-attested'))return hold(state,'history','Earlier execution remains unverified; a late commit cannot authorize it');
|
|
20
|
+
if(state.intents.some(i=>i.operation==='assign-agent'&&['prepared','unknown','mismatch','succeeded'].includes(i.status)&&(i.taskId===request.taskId||i.correlation===args.correlation||!(i.status==='succeeded'&&completed(state,i.taskId))&&i.paths?.some(p=>args.paths.some(q=>within(p,q)||within(q,p))))))return hold(state,'assignment-active','Resolve the existing dispatch before preparing another');
|
|
21
|
+
const task=state.tasks?.find(t=>t.id===request.taskId);
|
|
22
|
+
if(task&&(task.invalidated||task.status==='held'||task.dependencies?.some(id=>{const predecessor=state.tasks.find(t=>t.id===id);return !predecessor||predecessor.invalidated||!['accepted','committed'].includes(predecessor.status);})))return hold(state,'task-stale','Affected task or predecessor requires renewed readiness and approval');
|
|
23
|
+
const scope=state.scopes?.findLast(s=>s.scopeVersion===state.scopeVersion&&(!s.data.affectedTaskIds||s.data.affectedTaskIds.includes(request.taskId)));
|
|
24
|
+
if(!task||!task.paths?.length||args.paths.some(p=>!task.paths.some(base=>within(base,p)))||scope&&args.paths.some(p=>!scope.data.paths?.some(base=>within(base,p))))return hold(state,'assignment-scope','Assignment must stay inside approved task and recorded scope');
|
|
25
|
+
const documents=args.documents,git=observations.git;
|
|
26
|
+
if(!Array.isArray(documents)||documents.length!==2||!setEqual(documents.map(d=>d.kind),['spec','plan'])||!git.docsCommit||!setEqual(git.docsCommit.paths,documents.map(d=>d.gitPath))||!same(git.docsCommit.documents,documents)||documents.some(d=>d.commit!==git.docsCommit.commit)||!Array.isArray(git.documents)||git.documents.length!==2||documents.some(d=>!git.documents.some(o=>o.path===d.path&&o.gitPath===d.gitPath&&o.kind===d.kind&&o.observedHash===d.contentHash&&o.currentHash===d.contentHash)))return hold(state,'documents','Exact committed spec and plan plus current bytes must match approval');
|
|
27
|
+
const approval=state.records.filter(r=>r.kind==='approval'&&(!r.data.affectedTaskIds||r.data.affectedTaskIds.includes(request.taskId))).at(-1);
|
|
28
|
+
if(task.approvalRef!==args.approvalRef||task.approvalVersion!==args.approvalVersion||task.documentVersion!==args.documentVersion||task.scopeVersion!==state.scopeVersion||task.targetVersion!==state.targetVersion||!approval||approval.source!=='operator-attested'||approval.messageRef!==args.approvalRef||approval.targetVersion!==state.targetVersion||approval.scopeVersion!==state.scopeVersion||approval.data.approvalVersion!==args.approvalVersion||approval.data.documentVersion!==args.documentVersion||!same(approval.data.documents,documents)||documents.some(d=>d.approvalRef!==args.approvalRef))return hold(state,'approval','Current document and approval versions must match');
|
|
29
|
+
const intent={requestId:request.requestId,taskId:request.taskId,operation:request.operation,status:'prepared',role:args.role,paths:[...args.paths],operations:[...args.operations],correlation:args.correlation,documents:structuredClone(documents),docsCommit:git.docsCommit.commit,approvalRef:args.approvalRef,approvalVersion:args.approvalVersion,documentVersion:args.documentVersion,target:structuredClone(state.target),targetVersion:state.targetVersion,scopeVersion:state.scopeVersion,revision:state.revision+1,fingerprint:fingerprintRequest(request),source:'operator-attested'};
|
|
30
|
+
return {...base(state),decision:'allow',ruleIds:['cooperative.assignment-prepared'],proposedTransition:{intent}};
|
|
31
|
+
}
|
|
32
|
+
/** A native response is an operator statement, not authenticated identity or compliance. */
|
|
33
|
+
export function recordCooperativeAssignmentResult({state,intent,result,observations}={}){
|
|
34
|
+
if(!intent||intent.operation!=='assign-agent')return hold(state,'intent','No matching assignment intent');
|
|
35
|
+
if(!['prepared','unknown','mismatch'].includes(intent.status))return hold(state,'assignment-terminal','Preserve the prior terminal native response');
|
|
36
|
+
if(!result||!['succeeded','failed','unknown','mismatch'].includes(result.status)||!text(result.messageRef)||!result.data)return hold(state,'assignment-result','Invalid native result statement');
|
|
37
|
+
if(result.status==='succeeded'&&(state.assignments.some(a=>a.agentId===result.data.agentId&&a.taskId!==intent.taskId)||state.intents?.some(i=>i.verificationActor?.id===result.data.agentId)||state.checks?.some(c=>c.verificationActor?.id===result.data.agentId)))return hold(state,'assignment-freshness','Implementer actor statements cannot cross tasks');
|
|
38
|
+
let status=result.status;
|
|
39
|
+
if(!same(intent.target,observations?.target)||result.data.correlation!==intent.correlation||result.data.taskId!==intent.taskId||status==='succeeded'&&!text(result.data.agentId))status='mismatch';
|
|
40
|
+
const entry={...structuredClone(result),status,requestId:intent.requestId,source:'operator-attested'};
|
|
41
|
+
return {...base(state),decision:['succeeded','failed'].includes(status)?'allow':'hold',ruleIds:['cooperative.assignment-'+status],proposedTransition:{result:entry}};
|
|
42
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import {validateCooperativeCommand,validateCooperativeState} from './cooperative-schema.mjs';
|
|
5
|
+
import {observeCooperativeTarget,validateObservedTarget,validateWorkflowId,workflowStatePath,inspectPath} from './scope.mjs';
|
|
6
|
+
import {saveDurableState} from './store.mjs';
|
|
7
|
+
export const COOPERATIVE_RUNTIME_MODULES=Object.freeze([
|
|
8
|
+
'bootstrap.mjs','cooperative-schema.mjs','cooperative.mjs','evaluate.mjs',
|
|
9
|
+
'scope.mjs','store.mjs','git-evidence.mjs','assignment.mjs','verification.mjs','acceptance.mjs','external.mjs'
|
|
10
|
+
]);
|
|
11
|
+
export const cooperativeCoverage=()=>({enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false});
|
|
12
|
+
export function commandResult({workflowId,state,input,decision='allow',ruleId='cooperative.recorded',persisted=false,...extra}){
|
|
13
|
+
return {decision,ruleIds:[ruleId],workflowId,taskId:input?.taskId??null,requestId:input?.requestId??null,revision:state?.revision??0,coverage:cooperativeCoverage(),requiredEvidence:[],nextActions:[],persisted,...extra};
|
|
14
|
+
}
|
|
15
|
+
export async function cooperativeVersions(){
|
|
16
|
+
const hash=bytes=>'sha256:'+crypto.createHash('sha256').update(bytes).digest('hex');
|
|
17
|
+
let policy;try{policy=await fs.readFile(new URL('../policy/role-policy.json',import.meta.url));}catch(error){if(error.code!=='ENOENT')throw error;policy=await fs.readFile(new URL('./role-policy.json',import.meta.url));}
|
|
18
|
+
const runtime=await Promise.all(COOPERATIVE_RUNTIME_MODULES.map(file=>fs.readFile(new URL(file,import.meta.url))));
|
|
19
|
+
return {policy:'1',runtime:'cooperative-1',policyHash:hash(policy),runtimeHash:hash(Buffer.concat(runtime))};
|
|
20
|
+
}
|
|
21
|
+
export async function initializeCooperativeWorkflow({cwd,workflowId,input,observe=observeCooperativeTarget,storage={}}){
|
|
22
|
+
validateWorkflowId(workflowId);validateCooperativeCommand({command:'init',input});
|
|
23
|
+
if(input.mode!=='plan')return commandResult({workflowId,input,decision:'hold',ruleId:'cooperative.unsupported'});
|
|
24
|
+
const target=await validateObservedTarget(cwd,await observe(cwd));const statePath=await workflowStatePath(target,workflowId);
|
|
25
|
+
const parent=path.dirname(statePath);await fs.mkdir(parent,{recursive:true,mode:0o700});await inspectPath(parent,{directory:true});
|
|
26
|
+
const lockPath=statePath+'.lock';let lock;
|
|
27
|
+
try{
|
|
28
|
+
lock=await fs.open(lockPath,'wx',0o600);await inspectPath(statePath,{missing:true});
|
|
29
|
+
const state={schemaVersion:1,workflowKind:'cooperative',contractVersion:1,workflowId,revision:0,mode:'plan',coverage:cooperativeCoverage(),versions:await cooperativeVersions(),target,targetVersion:1,scopeVersion:0};
|
|
30
|
+
for(const key of ['actors','selections','assignments','identities','leases','scopes','approvals','documents','tasks','requests','intents','results','checks','findings','acceptances','commits','audit','unresolved','records','targetHistory'])state[key]=[];
|
|
31
|
+
state.actors.push({actor:input.actor,source:'operator-attested'});state.selections.push({...input.selection,actor:input.actor,source:'operator-attested',targetVersion:1});
|
|
32
|
+
state.audit.push({command:'init',revision:0,source:'operator-attested',actor:input.actor});validateCooperativeState(state);
|
|
33
|
+
await saveDurableState({statePath,state,create:true,storage});return commandResult({workflowId,state,input,persisted:true,ruleId:'cooperative.initialized'});
|
|
34
|
+
}finally{if(lock){await lock.close();await fs.unlink(lockPath);}}
|
|
35
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {ROLES} from './operations.mjs';
|
|
2
|
+
import {assertSecretFree} from './external.mjs';
|
|
3
|
+
import {object,text} from './schema.mjs';
|
|
4
|
+
import {absolute,validateWorkflowId} from './scope.mjs';
|
|
5
|
+
const fail=message=>{throw Error('Cooperative schema: '+message);};
|
|
6
|
+
const hash=value=>typeof value==='string'&&/^sha256:[a-f0-9]{64}$/.test(value);
|
|
7
|
+
const revision=value=>Number.isSafeInteger(value)&&value>=0;
|
|
8
|
+
const actor=value=>object(value)&&text(value.id)&&Object.keys(value).every(k=>['id','role'].includes(k))&&(value.role===undefined||ROLES.includes(value.role));
|
|
9
|
+
const statement=value=>object(value)&&text(value.messageRef)&&object(value.data);
|
|
10
|
+
function fields(value,allowed){if(!object(value)||Object.keys(value).some(k=>!allowed.includes(k)))fail('unexpected input field');}
|
|
11
|
+
function json(value){try{const encoded=JSON.stringify(value);if(encoded===undefined)fail('JSON input');}catch{fail('JSON input');}}
|
|
12
|
+
export function validateCooperativeCommand(command){
|
|
13
|
+
assertSecretFree(command);fields(command,['command','input']);const {input,command:name}=command;
|
|
14
|
+
if(!['init','record','status','reconcile','prepare','result'].includes(name))fail('command');
|
|
15
|
+
if(name==='status'){if(input!==undefined)fail('status takes no input');return command;}
|
|
16
|
+
const resultStatement=name==='reconcile'||name==='result'&&input?.operation===undefined;
|
|
17
|
+
const allowed=name==='init'?['mode','actor','selection']:name==='record'?['requestId','expectedRevision','actor','evidence']:resultStatement?['requestId','expectedRevision','actor','intentId','result']:['requestId','expectedRevision','actor','operation','taskId','arguments'];
|
|
18
|
+
fields(input,allowed);json(input);
|
|
19
|
+
if(!actor(input.actor))fail('actor attestation');
|
|
20
|
+
if(name==='init'){
|
|
21
|
+
if(!['plan','no-plan','small-task'].includes(input.mode)||!object(input.selection)||!text(input.selection.messageRef)||!hash(input.selection.contentHash)||Object.keys(input.selection).some(k=>!['messageRef','contentHash'].includes(k)))fail('target selection');
|
|
22
|
+
return command;
|
|
23
|
+
}
|
|
24
|
+
if(!text(input.requestId)||input.requestId.length>128||!revision(input.expectedRevision))fail('request identity or revision');
|
|
25
|
+
if(name==='record'){
|
|
26
|
+
fields(input.evidence,['kind','messageRef','data']);
|
|
27
|
+
if(input.evidence?.data?.affectedTaskIds!==undefined&&(!Array.isArray(input.evidence.data.affectedTaskIds)||!input.evidence.data.affectedTaskIds.every(text)||new Set(input.evidence.data.affectedTaskIds).size!==input.evidence.data.affectedTaskIds.length))fail('affected task IDs');
|
|
28
|
+
if(!statement(input.evidence)||!['note','approval','external-approval','document','scope','check','finding','review','acceptance-evidence'].includes(input.evidence.kind))fail('evidence');
|
|
29
|
+
}
|
|
30
|
+
if(resultStatement){
|
|
31
|
+
fields(input.result,['status','messageRef','data','notExecutedEvidence']);
|
|
32
|
+
if(!text(input.intentId)||!statement(input.result)||!['succeeded','failed','unknown','mismatch','cancelled'].includes(input.result.status))fail('result evidence');
|
|
33
|
+
if(input.result.status==='cancelled'&&(!object(input.result.notExecutedEvidence)||!text(input.result.notExecutedEvidence.messageRef)))fail('cancel requires not-executed evidence');
|
|
34
|
+
}
|
|
35
|
+
if((name==='prepare'||name==='result'&&!resultStatement)&&!text(input.operation))fail('operation');
|
|
36
|
+
return command;
|
|
37
|
+
}
|
|
38
|
+
export function validateCooperativeState(state){
|
|
39
|
+
if(!object(state)||state.workflowKind!=='cooperative'||state.contractVersion!==1||state.schemaVersion!==1||!revision(state.revision)||state.mode!=='plan')fail('explicit cooperative contract');
|
|
40
|
+
validateWorkflowId(state.workflowId);
|
|
41
|
+
if(!object(state.target)||!absolute(state.target.realpath)||!absolute(state.target.gitCommonDir)||!absolute(state.target.gitDir)||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(state.target.head)||!(state.target.branch===null||text(state.target.branch)))fail('target');
|
|
42
|
+
if(!object(state.versions)||!text(state.versions.policy)||!text(state.versions.runtime)||!hash(state.versions.policyHash)||!hash(state.versions.runtimeHash))fail('versions');
|
|
43
|
+
if(!object(state.coverage)||state.coverage.provenance!=='operator-attested'||state.coverage.nativeVerified!==false||state.coverage.enforcement!=='cooperative')fail('coverage');
|
|
44
|
+
for(const key of ['actors','selections','assignments','identities','leases','scopes','approvals','documents','tasks','requests','intents','results','checks','findings','acceptances','commits','audit','unresolved','records','targetHistory'])if(!Array.isArray(state[key]))fail(key);
|
|
45
|
+
if(state.tasks.some(t=>!object(t)||!text(t.id)||!Array.isArray(t.paths)||!Array.isArray(t.criteria)||!Array.isArray(t.requiredChecks)||!Array.isArray(t.dependencies))||new Set(state.tasks.map(t=>t.id)).size!==state.tasks.length)fail('task contracts');
|
|
46
|
+
if(state.commitObservations!==undefined&&(!Array.isArray(state.commitObservations)||state.commitObservations.some(o=>!object(o)||o.source!=='local-git'||!text(o.intentId)||!revision(o.revision))))fail('commit observations');
|
|
47
|
+
if(!revision(state.scopeVersion)||!revision(state.targetVersion))fail('scope/target version');
|
|
48
|
+
if(state.actors.some(a=>!actor(a.actor)||a.source!=='operator-attested')||state.selections.some(s=>s.source!=='operator-attested'||!text(s.messageRef)))fail('attestations');
|
|
49
|
+
for(const key of ['scopes','approvals','documents','checks','findings','acceptances','commits'])if(state[key].some(entry=>!object(entry)||entry.source!=='operator-attested'))fail('attestation source');
|
|
50
|
+
if(state.records.some(r=>!actor(r.actor)||r.source!=='operator-attested'||!text(r.kind)||!statement(r)))fail('records');
|
|
51
|
+
if(state.requests.some(r=>!text(r.requestId)||!hash(r.requestFingerprint)||!object(r.decision))||new Set(state.requests.map(r=>r.requestId)).size!==state.requests.length)fail('request replay');
|
|
52
|
+
if(state.intents.some(i=>!text(i.requestId)||!['prepared','succeeded','failed','unknown','mismatch','cancelled'].includes(i.status)||!hash(i.fingerprint))||new Set(state.intents.map(i=>i.requestId)).size!==state.intents.length)fail('intents');
|
|
53
|
+
if(state.results.some(r=>!text(r.requestId)||!['succeeded','failed','unknown','mismatch','cancelled'].includes(r.status)||r.source!=='operator-attested'||!object(r.data)))fail('results');
|
|
54
|
+
assertSecretFree(state);json(state);return state;
|
|
55
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {EXTERNAL_ACTIONS,reconcileCooperativeExternal,cooperativeSupport} from './external.mjs';
|
|
2
|
+
import {reconcileCooperativeCommit} from './acceptance.mjs';
|
|
3
|
+
import {evaluateCooperativeVerification,invalidateAffectedEvidence,readCooperativeChanges} from './verification.mjs';
|
|
4
|
+
import {readAssignmentGitEvidence,readCooperativeCommitGitEvidence} from './git-evidence.mjs';
|
|
5
|
+
import {recordCooperativeAssignmentResult} from './assignment.mjs';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import {validateCooperativeCommand,validateCooperativeState} from './cooperative-schema.mjs';
|
|
8
|
+
import {observeCooperativeTarget,validateObservedTarget,validateWorkflowId,workflowStatePath,inspectPath,absolute,within} from './scope.mjs';
|
|
9
|
+
import {fingerprintRequest,saveDurableState,applyCooperativeCommitTransition} from './store.mjs';
|
|
10
|
+
import {commandResult,cooperativeVersions} from './bootstrap.mjs';
|
|
11
|
+
import {evaluateCooperative,reconcileCooperativeResult} from './evaluate.mjs';
|
|
12
|
+
const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
13
|
+
const sameCheckout=(a,b)=>['realpath','gitCommonDir','gitDir'].every(k=>a[k]===b[k]);
|
|
14
|
+
/** Only trusted code may inject observation/storage. CLI JSON cannot set either. */
|
|
15
|
+
export async function runCooperativeCommand({cwd,workflowId,command,input,observe=observeCooperativeTarget,storage={},readGit=readAssignmentGitEvidence,readChanges=readCooperativeChanges,readCommitGit=readCooperativeCommitGitEvidence}){
|
|
16
|
+
validateWorkflowId(workflowId);validateCooperativeCommand({command,input});
|
|
17
|
+
if(command==='init')throw Error('Use initializeCooperativeWorkflow for init');
|
|
18
|
+
const target=await validateObservedTarget(cwd,await observe(cwd)),statePath=await workflowStatePath(target,workflowId);
|
|
19
|
+
const read=async()=>{await inspectPath(statePath);const state=validateCooperativeState(JSON.parse(await fs.readFile(statePath,'utf8')));if(state.workflowId!==workflowId)throw Error('Workflow identity mismatch');return state;};
|
|
20
|
+
const result=(state,decision,extra={})=>commandResult({workflowId,state,input,...decision,...extra});
|
|
21
|
+
if(command==='status'){
|
|
22
|
+
const state=await read();
|
|
23
|
+
if(!sameCheckout(state.target,target))return result(undefined,{decision:'hold',ruleIds:['cooperative.target']});
|
|
24
|
+
const changes=state.tasks.length?await readChanges({state}):undefined;
|
|
25
|
+
const evaluation=evaluateCooperative({state,request:{command,input},evidence:{target,versions:await cooperativeVersions(),changes}});
|
|
26
|
+
const unresolved=state.unresolved.some(entry=>!entry.resolvedBy)||state.intents.some(i=>['prepared','unknown','mismatch'].includes(i.status)&&!(['check','review'].includes(i.operation)&&i.stale));
|
|
27
|
+
return {...result(state,evaluation.decision==='allow'&&unresolved?{decision:'hold',ruleIds:['cooperative.unresolved']}:evaluation,{persisted:true}),state,support:cooperativeSupport(),...(changes?{changeFingerprints:changes}:{})};
|
|
28
|
+
}
|
|
29
|
+
let lock;
|
|
30
|
+
try{
|
|
31
|
+
lock=await fs.open(statePath+'.lock','wx',0o600);let state=await read();
|
|
32
|
+
if(!sameCheckout(state.target,target))return result(state,{decision:'hold',ruleIds:['cooperative.target']});
|
|
33
|
+
const requestFingerprint=fingerprintRequest({command,input});const prior=state.requests.find(r=>r.requestId===input.requestId);
|
|
34
|
+
if(prior){if(prior.requestFingerprint!==requestFingerprint)throw Error('Request fingerprint conflict');return result(state,{decision:'hold',ruleIds:['cooperative.replay']},{replayed:true});}
|
|
35
|
+
if(!['reconcile','result'].includes(command)&&input.expectedRevision!==state.revision)return result(state,{decision:'hold',ruleIds:[['prepare','result'].includes(command)?'cooperative.unsupported':'cooperative.revision']});
|
|
36
|
+
const currentTarget=await validateObservedTarget(cwd,await observe(cwd));
|
|
37
|
+
if(!sameCheckout(state.target,currentTarget))return result(state,{decision:'hold',ruleIds:['cooperative.target']});
|
|
38
|
+
const versions=await cooperativeVersions();
|
|
39
|
+
let git,gitError;
|
|
40
|
+
if(command==='prepare'&&input.operation==='assign-agent'&&input.arguments&&same(currentTarget,state.target)){
|
|
41
|
+
try{git=await readGit({target:currentTarget,documents:input.arguments?.documents??[]});}catch(error){gitError=error.message;}
|
|
42
|
+
}
|
|
43
|
+
const commitIntent=['result','reconcile'].includes(command)?state.intents.find(i=>i.requestId===input.intentId&&i.operation==='commit'):undefined;
|
|
44
|
+
if(command==='prepare'&&['accept','commit'].includes(input.operation)||commitIntent){
|
|
45
|
+
try{git=await readCommitGit({target:currentTarget,...(commitIntent&&input.result?.data?.commit?{commit:input.result.data.commit}:{})});}catch(error){gitError=error.message;}
|
|
46
|
+
}
|
|
47
|
+
const changes=state.tasks.length?await readChanges({state}):undefined;
|
|
48
|
+
let decision=evaluateCooperative({state,request:{command,input},evidence:{target:currentTarget,versions,git,changes}});
|
|
49
|
+
if(gitError)decision={decision:'hold',ruleIds:[input.operation==='assign-agent'?'cooperative.documents':'cooperative.git-evidence'],reason:gitError};
|
|
50
|
+
if(command==='prepare'&&decision.decision==='allow'){
|
|
51
|
+
if(decision.proposedTransition?.intent)state.intents.push(decision.proposedTransition.intent);
|
|
52
|
+
if(decision.proposedTransition?.acceptance){state.acceptances.push(decision.proposedTransition.acceptance);state.tasks.find(t=>t.id===input.taskId).status='accepted';}
|
|
53
|
+
}
|
|
54
|
+
if(['reconcile','result'].includes(command)&&input.result){
|
|
55
|
+
if(input.expectedRevision!==state.revision)state.unresolved.push({kind:'stale-result',requestId:input.requestId,intentId:input.intentId,expectedRevision:input.expectedRevision,revision:state.revision+1});
|
|
56
|
+
let intent=state.intents.find(i=>i.requestId===input.intentId);
|
|
57
|
+
decision=EXTERNAL_ACTIONS.includes(intent?.operation)?reconcileCooperativeExternal({state,intent,result:input.result,observations:{target:currentTarget}}):intent?.operation==='commit'?reconcileCooperativeCommit({state,intent,result:input.result,gitEvidence:git}):['check','review'].includes(intent?.operation)?evaluateCooperativeVerification({state,request:{...input,operation:'verification-result'},evidence:{changes}}):intent?.operation==='assign-agent'?recordCooperativeAssignmentResult({state,intent,result:input.result,observations:{target:currentTarget}}):reconcileCooperativeResult({state,intent,result:input.result,observations:{target:currentTarget}});
|
|
58
|
+
if(['assign-agent','check','review','commit',...EXTERNAL_ACTIONS].includes(intent?.operation)&&!same(versions,state.versions))decision={...decision,decision:'hold',ruleIds:['cooperative.versions']};
|
|
59
|
+
if(input.expectedRevision!==state.revision)decision={...decision,decision:'hold',ruleIds:['cooperative.revision']};
|
|
60
|
+
if((intent?.operation==='commit'||EXTERNAL_ACTIONS.includes(intent?.operation))&&input.actor.role!=='orchestrator')decision={...decision,decision:'hold',ruleIds:['cooperative.acceptance-actor']};
|
|
61
|
+
if(['check','review'].includes(intent?.operation)&&!same(currentTarget,state.target))decision={...decision,decision:'hold',ruleIds:['cooperative.target']};
|
|
62
|
+
if(intent?.operation==='commit'){
|
|
63
|
+
const verified=decision.proposedTransition;
|
|
64
|
+
// Failed reconciliation records what actually happened without changing target.
|
|
65
|
+
const observation={intentId:input.intentId,revision:state.revision+1,observation:verified?.observation??git??null,source:'local-git',...(gitError?{error:gitError}:{})};
|
|
66
|
+
if(decision.decision==='allow'){
|
|
67
|
+
state=applyCooperativeCommitTransition({state,decision});
|
|
68
|
+
intent=state.intents.find(i=>i.requestId===input.intentId);
|
|
69
|
+
const committedChanges=await readChanges({state}),commit=state.commits.at(-1);commit.postCommitFingerprint=committedChanges.tasks[intent.taskId];
|
|
70
|
+
if(committedChanges.taskSnapshots){
|
|
71
|
+
commit.snapshot=structuredClone(committedChanges.taskSnapshots[intent.taskId]);
|
|
72
|
+
const changedPaths=commit.paths.map(p=>state.target.realpath+'/'+p);
|
|
73
|
+
for(const prior of state.commits.slice(0,-1)){
|
|
74
|
+
const snapshot=prior.snapshotConnections?.at(-1)?.snapshot??prior.snapshot,current=committedChanges.taskSnapshots[prior.taskId];
|
|
75
|
+
if(!snapshot||!current||!changedPaths.some(p=>snapshot.paths.some(base=>within(base,p))))continue;
|
|
76
|
+
const projected=structuredClone(snapshot);for(const key of ['index','rows'])projected[key]=[...snapshot[key].filter(row=>!changedPaths.includes(row.path)),...current[key].filter(row=>changedPaths.includes(row.path))].sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
|
|
77
|
+
prior.snapshotConnections??=[];prior.snapshotConnections.push({commit:commit.commit,requestId:input.requestId,snapshot:projected,fingerprint:fingerprintRequest(projected)});prior.postCommitFingerprint=fingerprintRequest(projected);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for(const entry of state.unresolved)if(entry.kind==='target-changed'&&entry.intentId===input.intentId)entry.resolvedBy=input.requestId;
|
|
81
|
+
}
|
|
82
|
+
state.commitObservations??=[];state.commitObservations.push(observation);
|
|
83
|
+
if(decision.decision!=='allow'&&verified?.result?.status==='succeeded')verified.result.status='mismatch';
|
|
84
|
+
}
|
|
85
|
+
const verification=decision.proposedTransition;
|
|
86
|
+
if(verification?.check){
|
|
87
|
+
if(decision.decision!=='allow'&&verification.check.status==='passed'){verification.check.status='unverified';verification.check.stale=true;verification.findings=[];verification.result.status='mismatch';}
|
|
88
|
+
state.checks.push(verification.check);for(const finding of verification.findings){const index=state.findings.findIndex(f=>f.id===finding.id);if(index<0)state.findings.push(finding);else state.findings[index]=finding;}
|
|
89
|
+
}
|
|
90
|
+
if(intent?.operation==='assign-agent'&&decision.decision!=='allow'&&decision.proposedTransition?.result&&['succeeded','failed'].includes(decision.proposedTransition.result.status))decision.proposedTransition.result.status='mismatch';
|
|
91
|
+
if(EXTERNAL_ACTIONS.includes(intent?.operation)&&decision.decision!=='allow'&&decision.proposedTransition?.result&&(decision.proposedTransition.result.status==='succeeded'||decision.ruleIds[0]!=='cooperative.external'))decision.proposedTransition.result.status='mismatch';
|
|
92
|
+
if(decision.decision==='allow'&&input.expectedRevision===state.revision&&['succeeded','failed','cancelled'].includes(decision.proposedTransition?.result?.status))for(const entry of state.unresolved)if(entry.kind==='stale-result'&&entry.intentId===input.intentId&&!entry.resolvedBy){entry.resolvedBy=input.requestId;entry.resolutionEvidence={intentId:input.intentId,resultRef:input.result.messageRef,revision:state.revision};}
|
|
93
|
+
const actualResult=decision.proposedTransition?.result??input.result;
|
|
94
|
+
// Preserve actual statements even when runtime/HEAD changed, without rebinding target.
|
|
95
|
+
if(!same(currentTarget,state.target)){state.targetHistory.push({target:currentTarget,revision:state.revision+1,source:'local-observation'});state.unresolved.push({kind:'target-changed',...(intent?.operation==='commit'?{intentId:input.intentId}:{}),revision:state.revision+1});}
|
|
96
|
+
if(!same(versions,state.versions))state.unresolved.push({kind:'versions-changed',revision:state.revision+1});
|
|
97
|
+
state.results.push({...actualResult,requestId:input.intentId,recordRequestId:input.requestId,actor:input.actor,source:'operator-attested'});
|
|
98
|
+
if(intent&&(['assign-agent','check','review','commit',...EXTERNAL_ACTIONS].includes(intent.operation)?decision.proposedTransition?.result&&['prepared','unknown','mismatch'].includes(intent.status):true)){
|
|
99
|
+
intent.status=actualResult.status;
|
|
100
|
+
if(decision.decision==='allow'&&actualResult.status==='succeeded'&&intent.operation==='assign-agent')state.assignments.push({intentId:intent.requestId,taskId:intent.taskId,agentId:actualResult.data.agentId,status:'active',source:'operator-attested',identityVerified:false});
|
|
101
|
+
}
|
|
102
|
+
else state.unresolved.push({kind:'unmatched-result',requestId:input.intentId,revision:state.revision+1});
|
|
103
|
+
}else if(command==='record'&&decision.decision==='allow'){
|
|
104
|
+
const entry={...input.evidence,actor:input.actor,source:'operator-attested',requestId:input.requestId,revision:state.revision+1,scopeVersion:state.scopeVersion,targetVersion:state.targetVersion};
|
|
105
|
+
if(entry.kind==='scope'){
|
|
106
|
+
if(!Array.isArray(entry.data.paths)||entry.data.paths.some(p=>!absolute(p)||!within(state.target.realpath,p)))throw Error('Scope paths must stay inside workspace');
|
|
107
|
+
state=invalidateAffectedEvidence({state,changedVersions:{scopeVersion:state.scopeVersion+1},affectedTaskIds:entry.data.affectedTaskIds});entry.scopeVersion=state.scopeVersion;state.scopes.push(entry);
|
|
108
|
+
state.unresolved.push({kind:'scope-changed',revision:state.revision+1,scopeVersion:state.scopeVersion,affectedTaskIds:state.tasks.filter(t=>t.invalidatedBy?.scopeVersion===state.scopeVersion).map(t=>t.id)});
|
|
109
|
+
}
|
|
110
|
+
if(['document','approval'].includes(entry.kind)&&state.tasks.length)state=invalidateAffectedEvidence({state,changedVersions:{[entry.kind+'Version']:entry.revision},affectedTaskIds:entry.data.affectedTaskIds});
|
|
111
|
+
if(decision.proposedTransition?.task){const task=decision.proposedTransition.task,index=state.tasks.findIndex(t=>t.id===task.id);if(index>=0){state=invalidateAffectedEvidence({state,changedVersions:{taskVersion:entry.revision},affectedTaskIds:[task.id]});state.tasks[index]=task;}else state.tasks.push(task);}
|
|
112
|
+
if(decision.proposedTransition?.finding){const finding=decision.proposedTransition.finding,index=state.findings.findIndex(f=>f.id===finding.id);if(index<0)state.findings.push(finding);else state.findings[index]=finding;}
|
|
113
|
+
state.records.push(entry);
|
|
114
|
+
if(decision.proposedTransition?.task)for(const marker of state.unresolved.filter(u=>u.kind==='scope-changed'&&!u.resolvedBy&&u.scopeVersion<=state.scopeVersion)){
|
|
115
|
+
const ids=marker.affectedTaskIds?.length?marker.affectedTaskIds:state.tasks.map(t=>t.id),proof=[];let ready=ids.length>0;
|
|
116
|
+
for(const id of ids){
|
|
117
|
+
const task=state.tasks.find(t=>t.id===id),approval=state.records.findLast(r=>r.kind==='approval'&&(!r.data.affectedTaskIds||r.data.affectedTaskIds.includes(id)));
|
|
118
|
+
if(!task||task.invalidated||task.scopeVersion!==state.scopeVersion||!approval||approval.revision<=marker.revision||approval.scopeVersion!==state.scopeVersion||approval.targetVersion!==state.targetVersion||task.approvalRef!==approval.messageRef||task.approvalVersion!==approval.data.approvalVersion||task.documentVersion!==approval.data.documentVersion){ready=false;break;}
|
|
119
|
+
const documents=approval.data.documents;
|
|
120
|
+
if(!Array.isArray(documents)||documents.length!==2||!['spec','plan'].every(kind=>documents.some(d=>d.kind===kind))){ready=false;break;}
|
|
121
|
+
try{const observed=await readGit({target:currentTarget,documents});if(!same(observed.target,currentTarget)||!observed.docsCommit||!same(observed.docsCommit.documents,documents)||documents.some(d=>d.commit!==observed.docsCommit.commit||d.approvalRef!==approval.messageRef||!observed.documents?.some(o=>o.path===d.path&&o.gitPath===d.gitPath&&o.observedHash===d.contentHash&&o.currentHash===d.contentHash))){ready=false;break;}}
|
|
122
|
+
catch{ready=false;break;}
|
|
123
|
+
const scope=state.scopes.find(s=>s.scopeVersion===state.scopeVersion);if(!scope||task.paths.some(p=>!scope.data.paths.some(base=>within(base,p)))){ready=false;break;}
|
|
124
|
+
proof.push({taskId:id,taskRef:task.messageRef,approvalRef:approval.messageRef,documents:structuredClone(documents)});
|
|
125
|
+
}
|
|
126
|
+
if(ready){marker.resolvedBy=input.requestId;marker.resolutionEvidence={taskIds:ids,contracts:proof,scopeVersion:state.scopeVersion};}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// A held request is durable audit, never an executable intent.
|
|
130
|
+
state.revision++;
|
|
131
|
+
const output=result(state,decision,{persisted:true});
|
|
132
|
+
state.requests.push({requestId:input.requestId,requestFingerprint,request:structuredClone({command,input}),decision:output});
|
|
133
|
+
state.audit.push({command,requestId:input.requestId,revision:state.revision,decision:output.decision,ruleIds:output.ruleIds,source:'operator-attested',actor:input.actor,...(input.intentId?{intentId:input.intentId,resolution:state.results.at(-1)?.status}:{}),...(input.operation?{operation:input.operation}:{})});
|
|
134
|
+
validateCooperativeState(state);await inspectPath(statePath);await saveDurableState({statePath,state,storage});return output;
|
|
135
|
+
}finally{if(lock){await lock.close();await fs.unlink(statePath+'.lock');}}
|
|
136
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {evaluateCooperativeExternal,reconcileCooperativeExternal,EXTERNAL_ACTIONS,unresolvedExternal} from './external.mjs';
|
|
2
|
+
export {evaluateCooperativeExternal,reconcileCooperativeExternal};
|
|
3
|
+
import {evaluateCooperativeAcceptance,prepareCooperativeCommit} from './acceptance.mjs';
|
|
4
|
+
import {evaluateCooperativeVerification} from './verification.mjs';
|
|
5
|
+
import {prepareCooperativeAssignment} from './assignment.mjs';
|
|
1
6
|
/**
|
|
2
7
|
* @typedef {object} GateDecision
|
|
3
8
|
* @property {'allow'|'deny'|'hold'} decision
|
|
@@ -9,6 +14,7 @@
|
|
|
9
14
|
* @property {object} coverage
|
|
10
15
|
*/
|
|
11
16
|
import path from 'node:path';
|
|
17
|
+
import {validateCooperativeState,validateCooperativeCommand} from './cooperative-schema.mjs';
|
|
12
18
|
import {validateWorkflowState, validateRequest, validateEvidence} from './schema.mjs';
|
|
13
19
|
import {ROLES, ROLE_OPERATIONS, GIT_OPERATIONS, EXTERNAL_OPERATIONS} from './operations.mjs';
|
|
14
20
|
const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
@@ -21,7 +27,7 @@ export function evaluate(policy, state, request, evidence){
|
|
|
21
27
|
const actor=state.identities.find(i=>i.actorRef===request.actorRef);
|
|
22
28
|
if(!actor)return result('hold','runtime.identity','platform identity unresolved',['platform-identity']);
|
|
23
29
|
const role=actor.role;
|
|
24
|
-
const roleRule={orchestrator:'role.orchestrator-boundary',scouter:'role.scouter-read-only',planner:'role.planner-draft-only',
|
|
30
|
+
const roleRule={orchestrator:'role.orchestrator-boundary',scouter:'role.scouter-read-only',planner:'role.planner-draft-only',implementer:'role.implementer-implementation',qa:'role.qa-verification',reviewer:'role.reviewer-independent'}[role];
|
|
25
31
|
if(!ROLE_OPERATIONS[role]?.includes(request.operation))return result('deny',GIT_OPERATIONS.includes(request.operation)?'git.specialists-deny':roleRule,'operation forbidden for assigned role');
|
|
26
32
|
const lease=state.leases.find(l=>l.actorRef===actor.actorRef);
|
|
27
33
|
if(lease?.status==='retired')return result('deny','runtime.identity','retired identity');
|
|
@@ -44,7 +50,7 @@ export function evaluate(policy, state, request, evidence){
|
|
|
44
50
|
const fact=kind=>evidence.facts?.find(f=>f.kind===kind&&f.workflowId===state.workflowId&&f.revision===state.revision&&same(f.target,state.target)&&f.policyHash===state.versions.policyHash&&evidence.provenance.some(p=>p.id===f.sourceRef));
|
|
45
51
|
const task=state.tasks.find(t=>t.id===request.taskId);
|
|
46
52
|
if(!task)return result('hold','runtime.assignment','task unavailable',['task']);
|
|
47
|
-
if(role==='
|
|
53
|
+
if(role==='implementer'&&['accepted','committed'].includes(task.status))return result('deny','runtime.identity','task execution ended');
|
|
48
54
|
if(request.operation==='temporary-write'){
|
|
49
55
|
const temporary=assignment.temporary;
|
|
50
56
|
if(!temporary||temporary.cleanupOwner!==request.actorRef||!Number.isFinite(Date.parse(temporary.expiresAt))||!Number.isFinite(Date.parse(evidence.observedAt))||Date.parse(temporary.expiresAt)<=Date.parse(evidence.observedAt)||!request.paths.every(p=>temporary.paths?.some(base=>within(base,p))))return result('hold','role.qa-verification','temporary scope lifetime and cleanup ownership required',['temporary-assignment']);
|
|
@@ -58,7 +64,7 @@ export function evaluate(policy, state, request, evidence){
|
|
|
58
64
|
if(!ROLES.includes(d.role)||d.taskId!==request.taskId)return result('deny','runtime.identity','invalid assignment');
|
|
59
65
|
const previous=state.identities.find(i=>i.actorRef===d.actorRef);
|
|
60
66
|
if(previous&&(previous.taskId!==d.taskId||state.leases.some(l=>l.actorRef===d.actorRef&&l.status==='retired')))return result('deny','runtime.identity','identity cannot cross tasks or revive');
|
|
61
|
-
if(d.role==='
|
|
67
|
+
if(d.role==='implementer'){
|
|
62
68
|
if(state.mode==='plan'){
|
|
63
69
|
// T3 supplies verified Git object evidence. Local assertions never satisfy this gate.
|
|
64
70
|
if(!state.documents.some(d=>d.kind==='spec')||!state.documents.some(d=>d.kind==='plan'))return result('hold','runtime.docs-commit','approved complete spec and plan required',['approved-documents','docs-only-commit']);
|
|
@@ -72,7 +78,7 @@ export function evaluate(policy, state, request, evidence){
|
|
|
72
78
|
const expected={requestId:request.requestId,platform:request.platform,...d};
|
|
73
79
|
const correlationProof=evidence.provenance.find(p=>p.id===correlation?.sourceRef&&p.source==='platform-event'&&p.verification.sourceRef===p.eventRef&&p.platform===request.platform&&p.actorRef===request.actorRef&&p.correlationRef===d.correlationRef&&p.requestId===request.requestId);
|
|
74
80
|
if(d.actorRef!==undefined||!d.correlationRef||!correlationProof||!same(correlation?.data,expected))return result('hold','runtime.identity','trusted platform request correlation required',['assignment-correlation']);
|
|
75
|
-
if(!Array.isArray(d.paths)||!d.paths.length||!same([...d.paths].sort(),[...request.paths].sort())||!Array.isArray(d.operations)||!d.operations.length||d.approvalRef!==approval.id||!approval.scope.roles?.
|
|
81
|
+
if(!Array.isArray(d.paths)||!d.paths.length||!same([...d.paths].sort(),[...request.paths].sort())||!Array.isArray(d.operations)||!d.operations.length||d.approvalRef!==approval.id||!approval.scope.roles?.some(role=>role===d.role)||d.paths.some(p=>!task.paths.some(base=>within(base,p)))||d.operations.some(op=>!ROLE_OPERATIONS[d.role].includes(op)||!approval.scope.operations.includes(op)))return result('hold','runtime.assignment','delegated role paths operations and approval must match',['delegated-scope']);
|
|
76
82
|
if(d.checks&&(!Array.isArray(approval.scope.checks)||d.checks.some(c=>!approval.scope.checks.some(approved=>same(approved,c)))))return result('hold','runtime.assignment','delegated checks not approved',['delegated-checks']);
|
|
77
83
|
if(d.temporary&&!same(d.temporary,approval.scope.temporary))return result('hold','runtime.assignment','delegated temporary scope not approved',['delegated-temporary-scope']);
|
|
78
84
|
if(state.assignments.some(a=>a.assignmentId===request.requestId||a.correlationRef===d.correlationRef))return result('hold','runtime.identity','assignment correlation already registered',['new-assignment-correlation']);
|
|
@@ -101,3 +107,33 @@ export function evaluate(policy, state, request, evidence){
|
|
|
101
107
|
if(request.operation==='transition')return result('hold','runtime.transition','result confirmation required',['observed-result']);
|
|
102
108
|
return result('allow',roleRule,'assigned operation with verified provenance');
|
|
103
109
|
}
|
|
110
|
+
|
|
111
|
+
/** Cooperative statements never enter provider validation or identity resolution. */
|
|
112
|
+
export function evaluateCooperative({policy,state,request,evidence}={}){
|
|
113
|
+
const base={expectedRevision:state?.revision??0,coverage:{enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false}};
|
|
114
|
+
const hold=(id,reason)=>({...base,decision:'hold',ruleIds:[id],reason,requiredEvidence:[],nextActions:[],proposedTransition:null});
|
|
115
|
+
try{validateCooperativeState(state);validateCooperativeCommand(request);}catch(error){return hold('cooperative.schema',error.message);}
|
|
116
|
+
if(policy!==undefined&&policy.workflowKind!=='cooperative')return hold('cooperative.contract','Explicit cooperative policy required');
|
|
117
|
+
if(state.mode!=='plan'||request.command==='result')return hold('cooperative.unsupported','Operation is not supported in this CLI phase');
|
|
118
|
+
if(!evidence||!same(evidence.target,state.target))return hold('cooperative.target','Observed target changed');
|
|
119
|
+
if(!same(evidence.versions,state.versions))return hold('cooperative.versions','Policy or runtime changed');
|
|
120
|
+
if(request.input&&request.input.expectedRevision!==state.revision)return hold('cooperative.revision','Stale revision');
|
|
121
|
+
if(request.command==='record'&&request.input.evidence.kind==='external-approval'&&request.input.actor.role!=='orchestrator')return hold('cooperative.external-approval','Orchestrator records explicit external approval');
|
|
122
|
+
if(request.command==='prepare'){
|
|
123
|
+
if(unresolvedExternal(state))return hold('cooperative.external-unresolved','Investigate unresolved external effects before follow-up');
|
|
124
|
+
if(EXTERNAL_ACTIONS.includes(request.input.operation))return evaluateCooperativeExternal({state,request:request.input,approvalEvidence:state.records.findLast(r=>r.kind==='external-approval'&&r.messageRef===request.input.arguments?.approvalRef)});
|
|
125
|
+
if(request.input.operation==='accept')return evaluateCooperativeAcceptance({state,request:request.input,evidence});
|
|
126
|
+
if(request.input.operation==='commit')return prepareCooperativeCommit({state,request:request.input,evidence,gitEvidence:evidence.git});
|
|
127
|
+
return request.input.operation==='assign-agent'?prepareCooperativeAssignment({policy,state,request:request.input,observations:{target:evidence.target,git:evidence.git}}):evaluateCooperativeVerification({state,request:request.input,evidence});
|
|
128
|
+
}
|
|
129
|
+
if(request.command==='record'&&(['check','review','finding'].includes(request.input.evidence.kind)||request.input.evidence.kind==='acceptance-evidence'&&request.input.evidence.data.kind==='task'))return evaluateCooperativeVerification({state,request:{...request.input,operation:'verification-record'},evidence});
|
|
130
|
+
if(request.command==='status'&&(state.tasks.some(t=>t.invalidated||t.status==='committed'&&!state.commits.some(c=>c.taskId===t.id&&c.postCommitFingerprint===evidence.changes?.tasks?.[t.id]))||state.findings.some(f=>f.status!=='closed')||state.checks.some((c,index)=>!(c.scope==='task'&&state.tasks.some(t=>t.id===c.taskId&&t.status==='committed'))&&!state.checks.slice(index+1).some(next=>next.scope===c.scope&&next.taskId===c.taskId&&same(next.descriptor,c.descriptor))&&(c.stale||c.status!=='passed'||c.changeFingerprint!==(c.scope==='final'?evidence.changes?.final:evidence.changes?.tasks?.[c.taskId])))))return hold('cooperative.verification-stale','Verification or findings require current evidence');
|
|
131
|
+
return {...base,decision:'allow',ruleIds:['cooperative.recorded'],requiredEvidence:[],nextActions:[],proposedTransition:null};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Result statements preserve uncertainty. They do not authenticate native events. */
|
|
135
|
+
export function reconcileCooperativeResult({state,intent,result,observations}={}){
|
|
136
|
+
const base={expectedRevision:state?.revision??0,coverage:{enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false},requiredEvidence:[],nextActions:[],proposedTransition:null};
|
|
137
|
+
try{validateCooperativeState(state);}catch(error){return {...base,decision:'hold',ruleIds:['cooperative.schema'],reason:error.message};}
|
|
138
|
+
return {...base,decision:'hold',ruleIds:[!intent?'cooperative.intent':result?.status==='unknown'?'cooperative.unknown':'cooperative.reconciliation'],requiredEvidence:[],nextActions:[],proposedTransition:null,reason:!intent?'No matching durable intent':'Recorded operator statement requires reconciliation; no execution authorized'};
|
|
139
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {fingerprintRequest} from './store.mjs';
|
|
2
|
+
import {absolute,within} from './scope.mjs';
|
|
3
|
+
export const EXTERNAL_ACTIONS=Object.freeze(['push','merge','deploy','publish']);
|
|
4
|
+
const keys={push:['repository','destination','refspec','sourceOid','expectedRemoteOid'],merge:['repository','changeRef','targetRef','expectedTargetOid'],deploy:['service','project','environment','artifactId','destination'],publish:['service','namespace','name','version','artifactHash','destination']};
|
|
5
|
+
const text=v=>typeof v==='string'&&v.length>0;
|
|
6
|
+
const same=(a,b)=>a===undefined||b===undefined?a===b:fingerprintRequest(a)===fingerprintRequest(b);
|
|
7
|
+
const fields=(v,allowed)=>v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).length===allowed.length&&allowed.every(k=>Object.hasOwn(v,k));
|
|
8
|
+
/** Reject credentials before audit persistence; errors never contain input values. */
|
|
9
|
+
export function assertSecretFree(value){
|
|
10
|
+
if(typeof value==='string'){
|
|
11
|
+
if(/(?:[a-z][a-z0-9+.-]*:\/\/|^)[^\s/]+:[^\s/]+@|[?&#](?:access[_-]?token|token|key|password|secret|signature|credential|auth)=|\b(?:Bearer|Basic)\s+\S+/i.test(value))throw Error('Secret-free identifiers required');
|
|
12
|
+
}else if(value&&typeof value==='object')for(const [key,item] of Object.entries(value)){
|
|
13
|
+
if(/^(?:password|passwd|token|accessToken|access_token|secret|credentials?|authorization|apiKey|api_key|privateKey)$/i.test(key))throw Error('Secret-free identifiers required');assertSecretFree(item);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function descriptorValid(operation,d){
|
|
17
|
+
if(!keys[operation]||!fields(d,keys[operation])||!Object.values(d).every(v=>text(v)&&!/[\r\n\x00]/.test(v)))return false;
|
|
18
|
+
if(d.destination&&/[?#]/.test(d.destination))return false;
|
|
19
|
+
const oid=v=>/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(v);
|
|
20
|
+
if(operation==='push'&&(!oid(d.sourceOid)||!oid(d.expectedRemoteOid)))return false;
|
|
21
|
+
if(operation==='merge'&&!oid(d.expectedTargetOid))return false;
|
|
22
|
+
return operation!=='publish'||/^sha256:[a-f0-9]{64}$/.test(d.artifactHash);
|
|
23
|
+
}
|
|
24
|
+
const coverage={enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false,remoteVerified:false};
|
|
25
|
+
const decision=(state,reason,transition=null,allow=false)=>({expectedRevision:state?.revision??0,decision:allow?'allow':'hold',ruleIds:['cooperative.external'],reason,coverage:{...coverage},requiredEvidence:[],nextActions:[],proposedTransition:transition});
|
|
26
|
+
export const unresolvedExternal=state=>state.intents.some(i=>EXTERNAL_ACTIONS.includes(i.operation)&&['prepared','unknown','mismatch'].includes(i.status));
|
|
27
|
+
export function evaluateCooperativeExternal({state,request,approvalEvidence}={}){
|
|
28
|
+
const a=request?.arguments;
|
|
29
|
+
try{assertSecretFree(request);assertSecretFree(approvalEvidence);}catch{return decision(state,'Secret-free identifiers required');}
|
|
30
|
+
if(state?.mode!=='plan'||request?.expectedRevision!==state.revision||request.actor?.role!=='orchestrator'||!fields(a,['approvalRef','scope','descriptor'])||!descriptorValid(request.operation,a.descriptor)||!Array.isArray(a.scope)||!a.scope.length||new Set(a.scope).size!==a.scope.length||a.scope.some(p=>!absolute(p)||!within(state.target.realpath,p)))return decision(state,'Exact external action, scope and secret-free target required');
|
|
31
|
+
const approval=state.records.findLast(r=>r.kind==='external-approval'&&r.messageRef===a.approvalRef);
|
|
32
|
+
if(!approval||approval.source!=='operator-attested'||approval.scopeVersion!==state.scopeVersion||approval.targetVersion!==state.targetVersion||!same(approval,approvalEvidence)||!same(approval.data,{operation:request.operation,scope:a.scope,descriptor:a.descriptor}))return decision(state,'Exact explicit external approval required; local approval is insufficient');
|
|
33
|
+
if(unresolvedExternal(state)||state.unresolved.some(u=>!u.resolvedBy)||state.intents.some(i=>i.requestId===request.requestId||EXTERNAL_ACTIONS.includes(i.operation)&&i.approvalRef===a.approvalRef))return decision(state,'Unresolved effects or consumed approval require investigation; never retry');
|
|
34
|
+
return decision(state,'External intent only; operator execution is separate',{intent:{requestId:request.requestId,operation:request.operation,status:'prepared',scope:structuredClone(a.scope),descriptor:structuredClone(a.descriptor),approvalRef:a.approvalRef,approvalFingerprint:fingerprintRequest(approval),scopeVersion:state.scopeVersion,targetVersion:state.targetVersion,target:structuredClone(state.target),versions:structuredClone(state.versions),fingerprint:fingerprintRequest(request),source:'operator-attested'}},true);
|
|
35
|
+
}
|
|
36
|
+
export function reconcileCooperativeExternal({state,intent,result,observations}={}){
|
|
37
|
+
if(!intent||!EXTERNAL_ACTIONS.includes(intent.operation)||!['prepared','unknown','mismatch'].includes(intent.status))return decision(state,'Matching unresolved external intent required; replay cannot authorize another effect');
|
|
38
|
+
try{assertSecretFree(result);}catch{return decision(state,'Secret-free result required');}
|
|
39
|
+
const transition={result:{...structuredClone(result),status:'mismatch',messageRef:result?.messageRef??'invalid-result',data:{...structuredClone(result?.data??{}),remoteVerified:false},source:'operator-attested'}};
|
|
40
|
+
const reject=reason=>decision(state,reason,transition);
|
|
41
|
+
if(!result||!text(result.messageRef)||!['succeeded','failed','unknown','mismatch','cancelled'].includes(result.status))return reject('Exact external result statement required');
|
|
42
|
+
if(!fields(result.data,['operation','scope','descriptor'])||result.data.operation!==intent.operation||!same(result.data.scope,intent.scope)||!same(result.data.descriptor,intent.descriptor))return reject('External result action, scope or target mismatch');
|
|
43
|
+
transition.result={...structuredClone(result),data:{...structuredClone(result.data),remoteVerified:false},source:'operator-attested'};
|
|
44
|
+
const approval=state.records.findLast(r=>r.kind==='external-approval'&&r.messageRef===intent.approvalRef);
|
|
45
|
+
if(!same(observations?.target,intent.target)||!approval||fingerprintRequest(approval)!==intent.approvalFingerprint||state.mode!=='plan'||!same(intent.target,state.target)||!same(intent.versions,state.versions)||intent.scopeVersion!==state.scopeVersion||intent.targetVersion!==state.targetVersion){transition.result.status='mismatch';return reject('Stale external approval, target or versions');}
|
|
46
|
+
if(result.status==='succeeded')return decision(state,'Operator result recorded; no independent remote verification',transition,true);
|
|
47
|
+
if(result.status==='failed')return decision(state,'Operator confirms failure; approval remains consumed and no retry authorized',transition,true);
|
|
48
|
+
if(result.status==='cancelled'&&text(result.notExecutedEvidence?.messageRef))return decision(state,'Operator attests no execution; approval remains consumed',transition,true);
|
|
49
|
+
return reject('External effect unresolved or unsuccessful; investigate without retry');
|
|
50
|
+
}
|
|
51
|
+
export function cooperativeSupport(){return {phases:[{phase:1,status:'supported',areas:['durable-state','documents','assignment']},{phase:2,status:'supported',areas:['scope','verification','acceptance-commit']},{phase:3,status:'supported',areas:['external-approval','external-result','audit-recovery']}],modes:{plan:'supported','no-plan':'unsupported',omission:'unsupported'},limits:['operator-attested identity and approval','direct calls outside CLI are not blocked','no network, native or provider execution','audit is local mutable evidence','unknown effects require investigation, not retry'],remoteVerified:false};}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {validateGitPreflight,validateGitCommitResult} from './git-evidence.mjs';
|
|
2
|
+
export {validateGitPreflight,validateGitCommitResult} from './git-evidence.mjs';
|
|
1
3
|
import path from 'node:path';
|
|
2
4
|
|
|
3
5
|
const reconciliations=new WeakMap();
|
|
@@ -44,10 +46,10 @@ export async function runGate({phase,state,request,gitEvidence,expectedCommit,re
|
|
|
44
46
|
}
|
|
45
47
|
if(phase==='preflight'){
|
|
46
48
|
const g=gitEvidence;
|
|
47
|
-
|
|
48
|
-
const dirty=[...g.workingPaths,...g.untrackedPaths];
|
|
49
|
+
const dirty=[...g?.workingPaths??[],...g?.untrackedPaths??[]];
|
|
49
50
|
const scope=d.kind==='documents'?state.documents.map(x=>x.path):task.paths;
|
|
50
|
-
|
|
51
|
+
const invalid=validateGitPreflight({target:state.target,descriptor:d,scope,gitEvidence:g});
|
|
52
|
+
if(invalid)return hold(invalid);
|
|
51
53
|
if(d.kind==='documents'){
|
|
52
54
|
if(!Array.isArray(g.documents)||!samePaths(g.documents.map(x=>x.gitPath),d.paths)||state.documents.some(document=>!g.documents.some(observed=>observed.gitPath===document.gitPath&&observed.path===document.path&&observed.contentHash===document.contentHash&&observed.observedHash===document.contentHash)))return hold('Observed document bytes differ from approved hashes','runtime.docs-commit');
|
|
53
55
|
}
|
|
@@ -56,10 +58,8 @@ export async function runGate({phase,state,request,gitEvidence,expectedCommit,re
|
|
|
56
58
|
if(phase!=='reconcile')return hold('Unknown Git gate phase');
|
|
57
59
|
const expected=expectedCommit;
|
|
58
60
|
if(!expected||expected.requestId!==request.requestId||expected.taskId!==task.id||expected.parent!==d.parent||expected.tree!==d.indexTree||expected.fingerprint!==d.fingerprint||!samePaths(expected.paths,d.paths)||expected.kind!==undefined&&expected.kind!==d.kind||expected.target!==undefined&&!same(expected.target,state.target))return hold('Preflight correlation unavailable');
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if(result.remainingPaths.some(file=>scopedPaths.some(base=>within(base,path.join(state.target.realpath,...file.split('/'))))))return hold('Committed task still has remaining changes');
|
|
62
|
-
if(expected.remainingPaths!==undefined&&!samePaths(result.remainingPaths,expected.remainingPaths))return hold('Remaining changes differ from preflight observation');
|
|
61
|
+
const invalid=validateGitCommitResult({target:state.target,expected,result,scope:d.kind==='documents'?state.documents.map(x=>x.path):task.paths});
|
|
62
|
+
if(invalid)return hold(invalid);
|
|
63
63
|
const next=structuredClone(state);next.target.head=result.commit;
|
|
64
64
|
next.commits=[...state.commits??[],{requestId:request.requestId,taskId:task.id,kind:d.kind,purpose:d.kind,expectedParent:expected.parent,expectedTree:expected.tree,commit:result.commit,parent:result.parent,tree:result.tree,paths:[...result.paths],fingerprint:d.fingerprint,actorRef:request.actorRef}];
|
|
65
65
|
if(d.kind==='task')next.tasks.find(t=>t.id===task.id).status='committed';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import {inspectPath} from './scope.mjs';
|
|
1
3
|
import crypto from 'node:crypto';
|
|
2
4
|
import path from 'node:path';
|
|
3
5
|
import {spawnSync} from 'node:child_process';
|
|
@@ -112,3 +114,47 @@ function pathsForTree(bytes){
|
|
|
112
114
|
const output=text(bytes);if(!output||!output.endsWith('\0'))throw Error('Invalid document tree');
|
|
113
115
|
return output.slice(0,-1).split('\0').map(entry=>{const match=/^(\d+) (blob|tree|commit) ([a-f0-9]+)\t([\s\S]+)$/.exec(entry);if(!match||!oid(match[3])||!gitPath(match[4]))throw Error('Invalid document tree entry');return {mode:match[1],type:match[2],path:match[4]};});
|
|
114
116
|
}
|
|
117
|
+
|
|
118
|
+
/** Assignment observation additionally compares regular current files to Git objects.
|
|
119
|
+
* No Git facts or transport override are accepted from workflow command JSON. */
|
|
120
|
+
export async function readAssignmentGitEvidence({target,documents}){
|
|
121
|
+
const evidence=await readGitEvidence({target,documents});
|
|
122
|
+
for(const document of evidence.documents){
|
|
123
|
+
await inspectPath(document.path);
|
|
124
|
+
document.currentHash=hash(await fs.readFile(document.path));
|
|
125
|
+
if(document.currentHash!==document.contentHash)throw Error('Current approved document hash mismatch');
|
|
126
|
+
}
|
|
127
|
+
return evidence;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Cooperative observations come only from local read-only Git queries. */
|
|
131
|
+
export async function readCooperativeCommitGitEvidence({target,commit,gitRunner=defaultRunner}){
|
|
132
|
+
if(commit!==undefined&&!oid(commit))throw Error('Invalid commit object ID');
|
|
133
|
+
const evidence=await readGitEvidence({target,gitRunner});
|
|
134
|
+
if(commit===undefined)return evidence;
|
|
135
|
+
const run=async args=>{const r=await gitRunner({args,cwd:target.realpath});if(r?.status!==0||r.error||!(Buffer.isBuffer(r.stdout)||typeof r.stdout==='string'))throw Error('Commit evidence query failed');return Buffer.from(r.stdout);};
|
|
136
|
+
const metadata=text(await run(['show','--no-patch','--format=%H%n%P%n%T',commit])).trimEnd().split('\n');
|
|
137
|
+
if(metadata.length!==3||metadata[0]!==commit||!metadata.every(oid))throw Error('Expected exact single-parent commit');
|
|
138
|
+
const committedPaths=paths(await run(['diff-tree','--no-commit-id','--name-only','-r','-z',commit,'--']));
|
|
139
|
+
// Reobserve both HEAD and index after metadata so a concurrent mutation holds.
|
|
140
|
+
const after=await readGitEvidence({target,gitRunner});
|
|
141
|
+
if(!equal(evidence,after))throw Error('Git changed during commit observation');
|
|
142
|
+
return {...evidence,commit:{commit,parent:metadata[1],tree:metadata[2],paths:committedPaths,head:target.head}};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const validGitPaths=values=>Array.isArray(values)&&values.every(gitPath)&&unique(values);
|
|
146
|
+
const within=(base,file)=>{const relative=path.relative(base,file);return relative===''||!path.isAbsolute(relative)&&relative!=='..'&&!relative.startsWith('..'+path.sep);};
|
|
147
|
+
/** Provenance-neutral Git checks shared by trusted and cooperative callers.
|
|
148
|
+
* The caller must establish role, approval and acceptance separately. */
|
|
149
|
+
export function validateGitPreflight({target,descriptor:d,scope,gitEvidence:g}){
|
|
150
|
+
if(!g||!equal(g.target,target)||!oid(d?.parent)||d.parent!==target.head||!oid(d.indexTree)||!/^sha256:[a-f0-9]{64}$/.test(d.fingerprint)||!validGitPaths(d.paths)||!d.paths.length||g.indexTree!==d.indexTree||g.stagedFingerprint!==d.fingerprint||!samePaths(g.stagedPaths,d.paths)||!validGitPaths(g.workingPaths)||!validGitPaths(g.untrackedPaths))return 'Actual Git target index diff and exact paths required';
|
|
151
|
+
if(d.paths.some(file=>!scope.some(base=>within(base,path.join(target.realpath,...file.split('/'))))))return 'Commit contains another task or unrelated changes';
|
|
152
|
+
if([...g.workingPaths,...g.untrackedPaths].some(file=>scope.some(base=>within(base,path.join(target.realpath,...file.split('/'))))))return 'Unstaged or untracked task changes invalidate acceptance';
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
export function validateGitCommitResult({target,expected,result,scope}){
|
|
156
|
+
if(!result||!oid(result.commit)||result.commit===expected.parent||result.commit.length!==expected.parent.length||result.parent!==expected.parent||result.tree!==expected.tree||!samePaths(result.paths,expected.paths)||result.head!==result.commit||!equal(result.target,{...target,head:result.commit})||!validGitPaths(result.remainingPaths))return 'Commit result unavailable or differs from expected parent tree paths and HEAD';
|
|
157
|
+
if(result.remainingPaths.some(file=>scope.some(base=>within(base,path.join(target.realpath,...file.split('/'))))))return 'Committed task still has remaining changes';
|
|
158
|
+
if(expected.remainingPaths!==undefined&&!samePaths(result.remainingPaths,expected.remainingPaths))return 'Remaining changes differ from preflight observation';
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Canonical semantic operations, independent of platform tool aliases. */
|
|
2
|
-
export const ROLES = Object.freeze(['orchestrator','scouter','planner','
|
|
2
|
+
export const ROLES = Object.freeze(['orchestrator','scouter','planner','implementer','qa','reviewer']);
|
|
3
3
|
export const GIT_OPERATIONS = Object.freeze(['stage','commit','push','merge','rebase','reset','branch-create','branch-switch','worktree-create','worktree-remove']);
|
|
4
4
|
export const EXTERNAL_OPERATIONS = Object.freeze(['push','merge','deploy','publish','external-message']);
|
|
5
5
|
export const OPERATIONS = Object.freeze(['read','draft','write','check','qa','review','temporary-write','operational-query','install','prepare-target','request-approval','assign-agent','accept','transition','scope-expansion','indirect-bypass',...new Set([...GIT_OPERATIONS,...EXTERNAL_OPERATIONS])]);
|
|
6
|
-
export const ROLE_OPERATIONS = Object.freeze({orchestrator:['read','write','check','prepare-target','request-approval','assign-agent','accept','transition','install',...GIT_OPERATIONS,...EXTERNAL_OPERATIONS],scouter:['read'],planner:['read','draft'],
|
|
6
|
+
export const ROLE_OPERATIONS = Object.freeze({orchestrator:['read','write','check','prepare-target','request-approval','assign-agent','accept','transition','install',...GIT_OPERATIONS,...EXTERNAL_OPERATIONS],scouter:['read'],planner:['read','draft'],implementer:['read','write','check'],qa:['read','check','temporary-write'],reviewer:['read','review']});
|