@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,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']});
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"description": "Return design, spec and plan drafts to Orchestrator."
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
|
-
"id": "
|
|
17
|
+
"id": "implementer",
|
|
18
18
|
"description": "Implement assigned approved changes and focused checks."
|
|
19
19
|
},
|
|
20
20
|
{
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"orchestrator",
|
|
34
34
|
"scouter",
|
|
35
35
|
"planner",
|
|
36
|
-
"
|
|
36
|
+
"implementer",
|
|
37
37
|
"qa",
|
|
38
38
|
"reviewer"
|
|
39
39
|
],
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"roles": [
|
|
59
59
|
"scouter",
|
|
60
60
|
"planner",
|
|
61
|
-
"
|
|
61
|
+
"implementer",
|
|
62
62
|
"qa",
|
|
63
63
|
"reviewer"
|
|
64
64
|
],
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"orchestrator",
|
|
115
115
|
"scouter",
|
|
116
116
|
"planner",
|
|
117
|
-
"
|
|
117
|
+
"implementer",
|
|
118
118
|
"qa",
|
|
119
119
|
"reviewer"
|
|
120
120
|
],
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"role-ownership"
|
|
151
151
|
],
|
|
152
152
|
"authority": "orchestrator",
|
|
153
|
-
"text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to
|
|
153
|
+
"text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Implementer. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it. Use a fresh Implementer per top-level task and a fresh QA per top-level task that needs QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and relevant assignment context remain valid, default same-task QA findings to the current Implementer and post-fix rechecks to the current QA. Replace or restart a role only when its context is invalid or unavailable, or the task boundary changes; an approved plan or acceptance-criteria amendment requires a fresh Implementer even within the task. Before reusing Implementer, refresh actual workspace, branch/HEAD and change state; QA reruns the reproduction path and invalidated related checks and reports actual actor, target state, outcomes and limitations. Preserve task-boundary acceptance and the final plan Reviewer correction exception.",
|
|
154
154
|
"controls": {
|
|
155
155
|
"codex": "prompt",
|
|
156
156
|
"claude": "prompt"
|
|
@@ -197,9 +197,9 @@
|
|
|
197
197
|
}
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
|
-
"id": "role.
|
|
200
|
+
"id": "role.implementer-implementation",
|
|
201
201
|
"roles": [
|
|
202
|
-
"
|
|
202
|
+
"implementer"
|
|
203
203
|
],
|
|
204
204
|
"effect": "allow",
|
|
205
205
|
"operations": [
|
|
@@ -233,7 +233,7 @@
|
|
|
233
233
|
"criterion-evidence"
|
|
234
234
|
],
|
|
235
235
|
"authority": "orchestrator",
|
|
236
|
-
"text": "Run approved checks and report commands, observations, reproduction, evidence and limits. Create only essential temporary data/logs/reports in the brief-authorized location and lifetime. Do not edit product source, tests, fixtures or configuration to pass checks, deploy or accept work. Persistent product-state checks require explicit scope and cleanup ownership. Return failures for
|
|
236
|
+
"text": "Run approved checks and report commands, observations, reproduction, evidence and limits. Create only essential temporary data/logs/reports in the brief-authorized location and lifetime. Do not edit product source, tests, fixtures or configuration to pass checks, deploy or accept work. Persistent product-state checks require explicit scope and cleanup ownership. Return failures for Implementer correction and required QA recheck.",
|
|
237
237
|
"controls": {
|
|
238
238
|
"codex": "prompt",
|
|
239
239
|
"claude": "prompt"
|
|
@@ -265,7 +265,7 @@
|
|
|
265
265
|
"orchestrator",
|
|
266
266
|
"scouter",
|
|
267
267
|
"planner",
|
|
268
|
-
"
|
|
268
|
+
"implementer",
|
|
269
269
|
"qa",
|
|
270
270
|
"reviewer"
|
|
271
271
|
],
|
|
@@ -278,7 +278,7 @@
|
|
|
278
278
|
"qa-rechecks"
|
|
279
279
|
],
|
|
280
280
|
"authority": "orchestrator",
|
|
281
|
-
"text": "For significant/high-risk work without a plan:
|
|
281
|
+
"text": "For significant/high-risk work without a plan: Implementer \u2192 QA and required QA rechecks \u2192 Reviewer of the final full change \u2192 Orchestrator acceptance. MUST NOT omit Reviewer merely because no plan exists. This creates no new plan obligation.",
|
|
282
282
|
"controls": {
|
|
283
283
|
"codex": "prompt",
|
|
284
284
|
"claude": "prompt"
|
|
@@ -290,7 +290,7 @@
|
|
|
290
290
|
"orchestrator",
|
|
291
291
|
"scouter",
|
|
292
292
|
"planner",
|
|
293
|
-
"
|
|
293
|
+
"implementer",
|
|
294
294
|
"qa",
|
|
295
295
|
"reviewer"
|
|
296
296
|
],
|
|
@@ -303,7 +303,7 @@
|
|
|
303
303
|
"required-qa"
|
|
304
304
|
],
|
|
305
305
|
"authority": "orchestrator",
|
|
306
|
-
"text": "For significant/high-risk plan work: each task uses
|
|
306
|
+
"text": "For significant/high-risk plan work: each task uses Implementer \u2192 fresh QA as required \u2192 Orchestrator acceptance and focused commit. After all tasks are accepted and required QA is complete, Reviewer reviews the final full change once. Reviewer is NOT a per-task or per-commit required gate. Preserve required checks and necessary finding rechecks.",
|
|
307
307
|
"controls": {
|
|
308
308
|
"codex": "prompt",
|
|
309
309
|
"claude": "prompt"
|
|
@@ -315,7 +315,7 @@
|
|
|
315
315
|
"orchestrator",
|
|
316
316
|
"scouter",
|
|
317
317
|
"planner",
|
|
318
|
-
"
|
|
318
|
+
"implementer",
|
|
319
319
|
"qa",
|
|
320
320
|
"reviewer"
|
|
321
321
|
],
|
|
@@ -327,7 +327,52 @@
|
|
|
327
327
|
"reviewer-findings"
|
|
328
328
|
],
|
|
329
329
|
"authority": "orchestrator",
|
|
330
|
-
"text": "For final overall Reviewer findings on plan work, including behavior findings, use
|
|
330
|
+
"text": "For final overall Reviewer findings on plan work, including behavior findings, use Implementer \u2192 Reviewer without QA reassignment; repeat until resolved. This exception applies only at the final plan stage. Preserve required QA and QA failure rechecks before initial final review and outside this exception.",
|
|
331
|
+
"controls": {
|
|
332
|
+
"codex": "prompt",
|
|
333
|
+
"claude": "prompt"
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"id": "planning.plan-amend",
|
|
338
|
+
"roles": [
|
|
339
|
+
"orchestrator"
|
|
340
|
+
],
|
|
341
|
+
"effect": "require",
|
|
342
|
+
"operations": [
|
|
343
|
+
"plan-update",
|
|
344
|
+
"plan-commit-amend"
|
|
345
|
+
],
|
|
346
|
+
"evidence": [
|
|
347
|
+
"change-classification",
|
|
348
|
+
"actual-approval-chronology",
|
|
349
|
+
"rewrite-safety",
|
|
350
|
+
"sha-mapping"
|
|
351
|
+
],
|
|
352
|
+
"authority": "orchestrator",
|
|
353
|
+
"text": "During approved task execution, mechanical plan progress records need no new user approval; changed goals, scope, criteria, method, risk, dependencies or required verification require revised-version approval and preparation before affected work. In both cases Orchestrator amends the original planning-document commit at a safe boundary, without a separate document or fixup commit. Preserve actual approval chronology, recoverable old/new contents, hashes, versions and SHA mappings; never retroactively authorize earlier work. Rewrite only unpublished, unshared local linear history with clear ownership and no merges; stop on unknown publication, ownership, unexpected state or replay conflicts, without force push or a fallback document commit. Preserve implementation task boundaries and revalidate affected evidence. See planning, handoff-prep, commit and verification for the full contract.",
|
|
354
|
+
"controls": {
|
|
355
|
+
"codex": "prompt",
|
|
356
|
+
"claude": "prompt"
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"id": "planning.planner-brief",
|
|
361
|
+
"roles": [
|
|
362
|
+
"orchestrator"
|
|
363
|
+
],
|
|
364
|
+
"effect": "require",
|
|
365
|
+
"operations": [
|
|
366
|
+
"planner-brief",
|
|
367
|
+
"planner-feedback"
|
|
368
|
+
],
|
|
369
|
+
"evidence": [
|
|
370
|
+
"complete-stage-brief",
|
|
371
|
+
"prepared-target",
|
|
372
|
+
"preceding-approval"
|
|
373
|
+
],
|
|
374
|
+
"authority": "orchestrator",
|
|
375
|
+
"text": "Before formal drafting, Orchestrator supplies one complete stage-specific brief with original requirements and criteria IDs, approved/open decisions, verified target and ownership, document paths and preceding approved versions, current behavior and connected source/generated/runtime/installer/documentation surfaces, policy conflicts, depth, dependencies, checks, holds and exact deliverable. Preserve shared brief field order. Review the complete draft once and consolidate substantive feedback as changed fields with other fields marked unchanged. Preserve sequential design, spec and plan approvals; no fixed call cap or incomplete drafts, and no calls for already resolved questions.",
|
|
331
376
|
"controls": {
|
|
332
377
|
"codex": "prompt",
|
|
333
378
|
"claude": "prompt"
|
|
@@ -337,7 +382,7 @@
|
|
|
337
382
|
"platforms": {
|
|
338
383
|
"codex": {
|
|
339
384
|
"level": "prompt",
|
|
340
|
-
"notice": "Role restrictions are prompt-level contracts, not a filesystem sandbox. No documented role-specific Git hook or generic per-agent tool allowlist is assumed. Existing shell capabilities are retained because static read access after shell reduction has not been verified;
|
|
385
|
+
"notice": "Role restrictions are prompt-level contracts, not a filesystem sandbox. No documented role-specific Git hook or generic per-agent tool allowlist is assumed. Existing shell capabilities are retained because static read access after shell reduction has not been verified; Implementer and QA need shell for approved checks."
|
|
341
386
|
},
|
|
342
387
|
"claude": {
|
|
343
388
|
"level": "capability",
|
|
@@ -346,13 +391,13 @@
|
|
|
346
391
|
},
|
|
347
392
|
"review": {
|
|
348
393
|
"noPlan": [
|
|
349
|
-
"
|
|
394
|
+
"implementer",
|
|
350
395
|
"qa",
|
|
351
396
|
"reviewer",
|
|
352
397
|
"orchestrator-accept"
|
|
353
398
|
],
|
|
354
399
|
"planTask": [
|
|
355
|
-
"
|
|
400
|
+
"implementer",
|
|
356
401
|
"fresh-qa",
|
|
357
402
|
"orchestrator-accept",
|
|
358
403
|
"orchestrator-commit"
|
|
@@ -362,7 +407,7 @@
|
|
|
362
407
|
"reviewer"
|
|
363
408
|
],
|
|
364
409
|
"planFinding": [
|
|
365
|
-
"
|
|
410
|
+
"implementer",
|
|
366
411
|
"reviewer"
|
|
367
412
|
]
|
|
368
413
|
},
|
|
@@ -580,7 +625,7 @@
|
|
|
580
625
|
"orchestrator",
|
|
581
626
|
"scouter",
|
|
582
627
|
"planner",
|
|
583
|
-
"
|
|
628
|
+
"implementer",
|
|
584
629
|
"qa",
|
|
585
630
|
"reviewer"
|
|
586
631
|
],
|