@yunhozz/arc 0.2.5 → 0.2.6
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 +75 -69
- package/claude/agents/arc-implementer.md +8 -23
- package/claude/agents/arc-planner.md +14 -23
- package/claude/agents/arc-qa.md +9 -19
- package/claude/agents/arc-reviewer.md +11 -20
- package/claude/agents/arc-scouter.md +10 -17
- package/claude/skill/SKILL.md +8 -8
- package/claude/skill/references/arc-visual-companion.md +3 -3
- package/claude/skill/references/brainstorming.md +60 -38
- package/claude/skill/references/commit.md +6 -6
- package/claude/skill/references/coordination.md +18 -6
- package/claude/skill/references/git-recovery.md +2 -2
- package/claude/skill/references/handoff-prep.md +7 -5
- package/claude/skill/references/handoff.md +62 -9
- package/claude/skill/references/limits.md +5 -5
- package/claude/skill/references/planning.md +18 -12
- package/claude/skill/references/scripts/start-server.sh +7 -0
- package/claude/skill/references/superpowers-provenance.json +4 -4
- package/claude/skill/references/verification.md +6 -4
- package/claude/skill/runtime/bootstrap.mjs +2 -2
- package/claude/skill/runtime/cooperative-schema.mjs +32 -1
- package/claude/skill/runtime/cooperative.mjs +68 -13
- package/claude/skill/runtime/evaluate.mjs +85 -6
- package/claude/skill/runtime/git-evidence.mjs +98 -0
- package/claude/skill/runtime/operations.mjs +2 -2
- package/claude/skill/runtime/role-policy.json +6 -6
- package/claude/skill/runtime/store.mjs +10 -0
- package/claude/skill/runtime/target-preparation.mjs +110 -0
- package/claude/skill/runtime/verification.mjs +6 -4
- package/codex/agents/arc-implementer.toml +10 -25
- package/codex/agents/arc-planner.toml +12 -19
- package/codex/agents/arc-qa.toml +7 -15
- package/codex/agents/arc-reviewer.toml +9 -16
- package/codex/agents/arc-scouter.toml +8 -13
- package/codex/skill/SKILL.md +8 -8
- package/codex/skill/references/arc-visual-companion.md +3 -3
- package/codex/skill/references/brainstorming.md +60 -38
- package/codex/skill/references/commit.md +6 -6
- package/codex/skill/references/coordination.md +18 -6
- package/codex/skill/references/git-recovery.md +2 -2
- package/codex/skill/references/handoff-prep.md +5 -5
- package/codex/skill/references/handoff-worktree.md +4 -4
- package/codex/skill/references/handoff.md +61 -8
- package/codex/skill/references/limits.md +5 -5
- package/codex/skill/references/planning.md +19 -13
- package/codex/skill/references/scripts/start-server.sh +7 -0
- package/codex/skill/references/superpowers-provenance.json +4 -4
- package/codex/skill/references/verification.md +6 -4
- package/codex/skill/runtime/bootstrap.mjs +2 -2
- package/codex/skill/runtime/cooperative-schema.mjs +32 -1
- package/codex/skill/runtime/cooperative.mjs +68 -13
- package/codex/skill/runtime/evaluate.mjs +85 -6
- package/codex/skill/runtime/git-evidence.mjs +98 -0
- package/codex/skill/runtime/operations.mjs +2 -2
- package/codex/skill/runtime/role-policy.json +6 -6
- package/codex/skill/runtime/store.mjs +10 -0
- package/codex/skill/runtime/target-preparation.mjs +110 -0
- package/codex/skill/runtime/verification.mjs +6 -4
- package/lib/installer/manifest.mjs +6 -2
- package/lib/installer/plan.mjs +3 -1
- package/lib/installer/runtime-assets.mjs +1 -0
- package/lib/policy/role-policy.json +6 -6
- package/lib/prompts/agent-fragments.json +254 -0
- package/lib/prompts/render.mjs +199 -0
- package/lib/runtime/bootstrap.mjs +2 -2
- package/lib/runtime/cooperative-schema.mjs +32 -1
- package/lib/runtime/cooperative.mjs +68 -13
- package/lib/runtime/evaluate.mjs +85 -6
- package/lib/runtime/git-evidence.mjs +98 -0
- package/lib/runtime/operations.mjs +2 -2
- package/lib/runtime/store.mjs +10 -0
- package/lib/runtime/target-preparation.mjs +110 -0
- package/lib/runtime/verification.mjs +6 -4
- package/lib/tui/setup.mjs +3 -2
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import {observeCooperativeTarget,validateObservedTarget,validateWorkflowId,workf
|
|
|
6
6
|
import {saveDurableState} from './store.mjs';
|
|
7
7
|
export const COOPERATIVE_RUNTIME_MODULES=Object.freeze([
|
|
8
8
|
'cli.mjs','command.mjs','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','schema.mjs','operations.mjs','gate.mjs','adapters/codex.mjs','adapters/claude.mjs','capabilities.mjs','identity.mjs','git-query.mjs'
|
|
9
|
+
'scope.mjs','store.mjs','git-evidence.mjs','assignment.mjs','verification.mjs','acceptance.mjs','external.mjs','schema.mjs','operations.mjs','gate.mjs','adapters/codex.mjs','adapters/claude.mjs','capabilities.mjs','identity.mjs','git-query.mjs','target-preparation.mjs'
|
|
10
10
|
]);
|
|
11
11
|
export const cooperativeCoverage=()=>({enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false});
|
|
12
12
|
export function commandResult({workflowId,state,input,decision='allow',ruleId='cooperative.recorded',persisted=false,...extra}){
|
|
@@ -16,7 +16,7 @@ export async function cooperativeVersions(){
|
|
|
16
16
|
const hash=bytes=>'sha256:'+crypto.createHash('sha256').update(bytes).digest('hex');
|
|
17
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
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-
|
|
19
|
+
return {policy:'1',runtime:'cooperative-6',policyHash:hash(policy),runtimeHash:hash(Buffer.concat(runtime))};
|
|
20
20
|
}
|
|
21
21
|
export async function initializeCooperativeWorkflow({cwd,workflowId,input,observe=observeCooperativeTarget,storage={}}){
|
|
22
22
|
validateWorkflowId(workflowId);validateCooperativeCommand({command:'init',input});
|
|
@@ -2,6 +2,8 @@ import {ROLES} from './operations.mjs';
|
|
|
2
2
|
import {assertSecretFree} from './external.mjs';
|
|
3
3
|
import {object,text,validatePlannerLedger} from './schema.mjs';
|
|
4
4
|
import {absolute,validateWorkflowId} from './scope.mjs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {validateTargetPreparationRecord} from './target-preparation.mjs';
|
|
5
7
|
const fail=message=>{throw Error('Cooperative schema: '+message);};
|
|
6
8
|
const hash=value=>typeof value==='string'&&/^sha256:[a-f0-9]{64}$/.test(value);
|
|
7
9
|
const revision=value=>Number.isSafeInteger(value)&&value>=0;
|
|
@@ -9,6 +11,24 @@ const actor=value=>object(value)&&text(value.id)&&Object.keys(value).every(k=>['
|
|
|
9
11
|
const statement=value=>object(value)&&text(value.messageRef)&&object(value.data);
|
|
10
12
|
function fields(value,allowed){if(!object(value)||Object.keys(value).some(k=>!allowed.includes(k)))fail('unexpected input field');}
|
|
11
13
|
function json(value){try{const encoded=JSON.stringify(value);if(encoded===undefined)fail('JSON input');}catch{fail('JSON input');}}
|
|
14
|
+
const exact=(value,keys)=>object(value)&&Object.keys(value).length===keys.length&&keys.every(key=>Object.hasOwn(value,key));
|
|
15
|
+
const oid=value=>typeof value==='string'&&/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value);
|
|
16
|
+
const time=value=>typeof value==='string'&&Number.isFinite(Date.parse(value))&&/(?:Z|[+-]\d\d:\d\d)$/.test(value);
|
|
17
|
+
const unique=value=>Array.isArray(value)&&new Set(value).size===value.length;
|
|
18
|
+
const gitPath=value=>text(value)&&!/[\\\x00\r\n:]/.test(value)&&!value.startsWith('/')&&!value.split('/').some(part=>!part||part==='.'||part==='..');
|
|
19
|
+
const ref=value=>text(value)&&/^refs\/[A-Za-z0-9][A-Za-z0-9._\/-]*$/.test(value)&&!value.includes('..')&&!value.includes('//')&&!value.endsWith('/')&&!value.endsWith('.')&&!value.endsWith('.lock');
|
|
20
|
+
const approval=value=>exact(value,['ref','version','hash','approvedAt'])&&text(value.ref)&&Number.isSafeInteger(value.version)&&value.version>0&&hash(value.hash)&&time(value.approvedAt);
|
|
21
|
+
const document=value=>exact(value,['version','hash','content'])&&Number.isSafeInteger(value.version)&&value.version>0&&hash(value.hash)&&typeof value.content==='string'&&Buffer.byteLength(value.content)<=1024*1024;
|
|
22
|
+
export function validatePlanAmendArguments(a){
|
|
23
|
+
if(!exact(a,['classification','reason','changedAt','affectedTaskIds','plan','history','approvals','writers','ownership','publication','remotes','recovery'])||!['contract','mechanical'].includes(a.classification)||!text(a.reason)||!time(a.changedAt)||!unique(a.affectedTaskIds)||!a.affectedTaskIds.length||!a.affectedTaskIds.every(text))fail('plan-amend request');
|
|
24
|
+
if(!exact(a.plan,['path','gitPath','old','new'])||!absolute(a.plan.path)||!gitPath(a.plan.gitPath)||!document(a.plan.old)||!document(a.plan.new)||a.plan.new.version<=a.plan.old.version)fail('plan-amend document');
|
|
25
|
+
if(!exact(a.history,['base','planCommit','head','branch'])||![a.history.base,a.history.planCommit,a.history.head].every(oid)||!text(a.history.branch)||/[\x00\r\n]/.test(a.history.branch))fail('plan-amend history');
|
|
26
|
+
if(!exact(a.approvals,['original','amendment'])||!approval(a.approvals.original)||!(a.classification==='mechanical'&&a.approvals.amendment===null||approval(a.approvals.amendment)))fail('plan-amend approvals');
|
|
27
|
+
if(!Array.isArray(a.writers)||!a.writers.length||a.writers.some(w=>!exact(w,['assignmentId','stoppedAt','evidenceRef'])||!text(w.assignmentId)||!time(w.stoppedAt)||!text(w.evidenceRef))||!unique(a.writers.map(w=>w.assignmentId)))fail('plan-amend writers');
|
|
28
|
+
if(!['exclusive','shared','unknown'].includes(a.ownership)||!['unpublished','published','unknown'].includes(a.publication)||!Array.isArray(a.remotes)||a.remotes.some(r=>!exact(r,['name','identity','complete','refs','observedAt','evidenceRef'])||!text(r.name)||!hash(r.identity)||r.complete!==true||!time(r.observedAt)||!text(r.evidenceRef)||!Array.isArray(r.refs)||r.refs.some(x=>!exact(x,['name','oid'])||!ref(x.name)||!oid(x.oid))||!unique(r.refs.map(x=>x.name)))||!unique(a.remotes.map(r=>r.name)))fail('plan-amend remote snapshot');
|
|
29
|
+
if(!exact(a.recovery,['ref','oid'])||!ref(a.recovery.ref)||!a.recovery.ref.startsWith('refs/arc/recovery/')||!oid(a.recovery.oid)||path.normalize(a.plan.path)!==a.plan.path)fail('plan-amend recovery');
|
|
30
|
+
return a;
|
|
31
|
+
}
|
|
12
32
|
export function validateCooperativeCommand(command){
|
|
13
33
|
assertSecretFree(command);fields(command,['command','input']);const {input,command:name}=command;
|
|
14
34
|
if(!['init','record','status','reconcile','prepare','result'].includes(name))fail('command');
|
|
@@ -25,7 +45,7 @@ export function validateCooperativeCommand(command){
|
|
|
25
45
|
if(name==='record'){
|
|
26
46
|
fields(input.evidence,['kind','messageRef','data']);
|
|
27
47
|
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','planner-stage','planner-evidence','planner-result','planner-review'].includes(input.evidence.kind))fail('evidence');
|
|
48
|
+
if(!statement(input.evidence)||!['note','approval','external-approval','document','scope','check','finding','review','acceptance-evidence','planner-stage','planner-evidence','planner-result','planner-review','target-preparation'].includes(input.evidence.kind))fail('evidence');
|
|
29
49
|
}
|
|
30
50
|
if(resultStatement){
|
|
31
51
|
fields(input.result,['status','messageRef','data','notExecutedEvidence']);
|
|
@@ -33,6 +53,7 @@ export function validateCooperativeCommand(command){
|
|
|
33
53
|
if(input.result.status==='cancelled'&&(!object(input.result.notExecutedEvidence)||!text(input.result.notExecutedEvidence.messageRef)))fail('cancel requires not-executed evidence');
|
|
34
54
|
}
|
|
35
55
|
if((name==='prepare'||name==='result'&&!resultStatement)&&!text(input.operation))fail('operation');
|
|
56
|
+
if(name==='prepare'&&input.operation==='plan-amend')validatePlanAmendArguments(input.arguments);
|
|
36
57
|
return command;
|
|
37
58
|
}
|
|
38
59
|
export function validateCooperativeState(state){
|
|
@@ -44,13 +65,23 @@ export function validateCooperativeState(state){
|
|
|
44
65
|
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
66
|
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
67
|
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');
|
|
68
|
+
if(state.planAmendObservations!==undefined&&(!Array.isArray(state.planAmendObservations)||state.planAmendObservations.some(o=>!object(o)||o.source!=='local-git'||!text(o.intentId)||!revision(o.revision))))fail('plan-amend observations');
|
|
69
|
+
if(state.planAmendments!==undefined&&(!Array.isArray(state.planAmendments)||state.planAmendments.some(a=>!object(a)||a.source!=='operator-attested'||a.observationSource!=='local-git'||!text(a.intentId)||!absolute(a.path)||!hash(a.oldHash)||!hash(a.newHash)||!oid(a.oldCommit)||!oid(a.newCommit)||!Array.isArray(a.mapping)||!a.mapping.length)))fail('plan-amend connections');
|
|
47
70
|
if(!revision(state.scopeVersion)||!revision(state.targetVersion))fail('scope/target version');
|
|
48
71
|
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
72
|
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
73
|
if(state.records.some(r=>!actor(r.actor)||r.source!=='operator-attested'||!text(r.kind)||!statement(r)))fail('records');
|
|
51
74
|
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
75
|
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');
|
|
76
|
+
for(const intent of state.intents.filter(i=>i.operation==='plan-amend')){validatePlanAmendArguments(intent.arguments);if(!object(intent.target)||!object(intent.versions)||!object(intent.observation)||intent.source!=='operator-attested'||intent.checkpoint!==undefined&&!object(intent.checkpoint))fail('plan-amend intent');}
|
|
53
77
|
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
78
|
if(state.plannerLedger!==undefined){validatePlannerLedger(state.plannerLedger);if(state.plannerLedger.stages.some(stage=>stage.workflowId!==state.workflowId||!Array.isArray(stage.artifacts)))fail('Planner workflow binding or artifacts');}
|
|
79
|
+
for(const key of ['targetIntents','targetCheckpoints','targetTransitions','targetDocuments'])if(state[key]!==undefined&&!Array.isArray(state[key]))fail(key);
|
|
80
|
+
const preparationIntents=state.targetIntents??[],checkpoints=state.targetCheckpoints??[],transitions=state.targetTransitions??[],documents=state.targetDocuments??[];
|
|
81
|
+
if(new Set(preparationIntents.map(i=>i.id)).size!==preparationIntents.length)fail('target intent identity');
|
|
82
|
+
for(const intent of preparationIntents){try{validateTargetPreparationRecord('intent',intent);}catch{fail('target intent');}const approved=state.records.find(r=>r.kind==='approval'&&r.messageRef===intent.approvalRef&&r.source==='operator-attested'&&r.actor?.role==='orchestrator'&&r.scopeVersion===intent.scopeVersion&&r.targetVersion===intent.sourceTargetVersion)?.data?.targetPreparation;if(intent.source!=='operator-attested'||intent.workflowId!==state.workflowId||!revision(intent.scopeVersion)||!hash(intent.sourceFingerprint)||!object(intent.sourceSnapshot)||!approved||['depth','selectionRef','kind','candidate','base','workflowId','firstStageId','sourceTargetVersion'].some(k=>approved[k]!==intent[k])||JSON.stringify(approved.sourceTarget)!==JSON.stringify(intent.sourceTarget)||JSON.stringify(approved.documents)!==JSON.stringify(intent.documents))fail('target intent provenance');}
|
|
83
|
+
for(const checkpoint of checkpoints){try{validateTargetPreparationRecord('checkpoint',{type:'checkpoint',...checkpoint});}catch{fail('target checkpoint');}if(checkpoint.source!=='operator-attested'||!text(checkpoint.messageRef)||!preparationIntents.some(i=>i.id===checkpoint.intentId&&i.firstStageId===checkpoint.stageId)||!state.records.some(r=>r.kind==='target-preparation'&&r.messageRef===checkpoint.messageRef&&r.source==='operator-attested'))fail('target checkpoint provenance');}
|
|
84
|
+
for(const transition of transitions){const request=transition.validation?.request;try{validateTargetPreparationRecord('transition',request);}catch{fail('target transition');}if(!object(transition.target)||!text(transition.messageRef)||!Number.isSafeInteger(transition.targetVersion)||transition.targetVersion!==transition.sourceTargetVersion+1||request.intentId!==transition.intentId||request.checkpointRef!==transition.checkpointRef||!preparationIntents.some(i=>i.id===transition.intentId)||!checkpoints.some(c=>c.intentId===transition.intentId&&c.messageRef===transition.checkpointRef)||!state.records.some(r=>r.kind==='target-preparation'&&r.messageRef===transition.messageRef&&r.source==='operator-attested'))fail('target transition provenance');}
|
|
85
|
+
for(const document of documents){try{validateTargetPreparationRecord('destination',document);}catch{fail('target document');}if(document.sourceLink!=='operator-attested'||document.verification?.status!=='verified'||document.verification?.source!=='local-bytes'||!text(document.messageRef)||!transitions.some(t=>t.intentId===document.intentId&&t.messageRef===document.transitionRef&&t.targetVersion===document.targetVersion)||!state.records.some(r=>r.kind==='target-preparation'&&r.messageRef===document.messageRef&&r.source==='operator-attested'))fail('target document provenance');}
|
|
55
86
|
assertSecretFree(state);json(state);return state;
|
|
56
87
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {EXTERNAL_ACTIONS,reconcileCooperativeExternal,cooperativeSupport} from './external.mjs';
|
|
2
2
|
import {reconcileCooperativeCommit} from './acceptance.mjs';
|
|
3
3
|
import {evaluateCooperativeVerification,invalidateAffectedEvidence,readCooperativeChanges} from './verification.mjs';
|
|
4
|
-
import {readAssignmentGitEvidence,readCooperativeCommitGitEvidence} from './git-evidence.mjs';
|
|
4
|
+
import {readAssignmentGitEvidence,readCooperativeCommitGitEvidence,readPlanAmendGitEvidence} from './git-evidence.mjs';
|
|
5
5
|
import {recordCooperativeAssignmentResult} from './assignment.mjs';
|
|
6
6
|
import fs from 'node:fs/promises';
|
|
7
7
|
import {constants} from 'node:fs';
|
|
@@ -9,13 +9,14 @@ import path from 'node:path';
|
|
|
9
9
|
import {createHash} from 'node:crypto';
|
|
10
10
|
import {validateCooperativeCommand,validateCooperativeState} from './cooperative-schema.mjs';
|
|
11
11
|
import {observeCooperativeTarget,validateObservedTarget,validateWorkflowId,workflowStatePath,inspectPath,absolute,within} from './scope.mjs';
|
|
12
|
-
import {fingerprintRequest,saveDurableState,applyCooperativeCommitTransition} from './store.mjs';
|
|
12
|
+
import {fingerprintRequest,saveDurableState,applyCooperativeCommitTransition,applyCooperativeTargetTransition} from './store.mjs';
|
|
13
13
|
import {commandResult,cooperativeVersions} from './bootstrap.mjs';
|
|
14
|
-
import {evaluateCooperative,reconcileCooperativeResult} from './evaluate.mjs';
|
|
14
|
+
import {evaluateCooperative,reconcileCooperativeResult,reconcileCooperativePlanAmend} from './evaluate.mjs';
|
|
15
|
+
import {evaluateTargetPreparation,readTargetPreparationEvidence} from './target-preparation.mjs';
|
|
15
16
|
const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
16
17
|
const sameCheckout=(a,b)=>['realpath','gitCommonDir','gitDir'].every(k=>a[k]===b[k]);
|
|
17
18
|
/** Only trusted code may inject observation/storage. CLI JSON cannot set either. */
|
|
18
|
-
export async function runCooperativeCommand({cwd,workflowId,command,input,observe=observeCooperativeTarget,storage={},readGit=readAssignmentGitEvidence,readChanges=readCooperativeChanges,readCommitGit=readCooperativeCommitGitEvidence}){
|
|
19
|
+
export async function runCooperativeCommand({cwd,workflowId,command,input,observe=observeCooperativeTarget,storage={},readGit=readAssignmentGitEvidence,readChanges=readCooperativeChanges,readCommitGit=readCooperativeCommitGitEvidence,readPlanAmendGit=readPlanAmendGitEvidence,clock=Date.now}){
|
|
19
20
|
validateWorkflowId(workflowId);validateCooperativeCommand({command,input});
|
|
20
21
|
if(command==='init')throw Error('Use initializeCooperativeWorkflow for init');
|
|
21
22
|
const target=await validateObservedTarget(cwd,await observe(cwd)),statePath=await workflowStatePath(target,workflowId);
|
|
@@ -26,7 +27,7 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
|
|
|
26
27
|
if(!sameCheckout(state.target,target))return result(undefined,{decision:'hold',ruleIds:['cooperative.target']});
|
|
27
28
|
const changes=state.tasks.length?await readChanges({state}):undefined;
|
|
28
29
|
const evaluation=evaluateCooperative({state,request:{command,input},evidence:{target,versions:await cooperativeVersions(),changes}});
|
|
29
|
-
const unresolved=state.plannerLedger?.stages.some(s=>s.pending)||state.unresolved.some(entry=>!entry.resolvedBy)||state.intents.some(i=>['prepared','unknown','mismatch'].includes(i.status)&&!(['check','review'].includes(i.operation)&&i.stale));
|
|
30
|
+
const unresolved=state.plannerLedger?.stages.some(s=>s.pending)||state.unresolved.some(entry=>!entry.resolvedBy)||state.intents.some(i=>(['prepared','unknown','mismatch'].includes(i.status)||i.operation==='plan-amend'&&i.status==='failed')&&!(['check','review'].includes(i.operation)&&i.stale));
|
|
30
31
|
return {...result(state,evaluation.decision==='allow'&&unresolved?{decision:'hold',ruleIds:['cooperative.unresolved']}:evaluation,{persisted:true}),state,support:cooperativeSupport(),...(changes?{changeFingerprints:changes}:{})};
|
|
31
32
|
}
|
|
32
33
|
let lock,plannerLock;
|
|
@@ -49,10 +50,43 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
|
|
|
49
50
|
if(command==='prepare'&&['accept','commit'].includes(input.operation)||commitIntent){
|
|
50
51
|
try{git=await readCommitGit({target:currentTarget,...(commitIntent&&input.result?.data?.commit?{commit:input.result.data.commit}:{})});}catch(error){gitError=error.message;}
|
|
51
52
|
}
|
|
53
|
+
const planIntent=state.intents.find(i=>i.operation==='plan-amend'&&i.requestId===((command==='record'&&input.evidence.kind==='note'&&input.evidence.data?.type==='plan-amend-checkpoint')?input.evidence.data.intentId:['result','reconcile'].includes(command)?input.intentId:null));
|
|
54
|
+
const planArguments=command==='prepare'&&input.operation==='plan-amend'?input.arguments:planIntent?.arguments;
|
|
55
|
+
let now;
|
|
56
|
+
if(planArguments){
|
|
57
|
+
try{git=await readPlanAmendGit({target:currentTarget,arguments:planArguments,mapping:command==='reconcile'&&input.result?.status==='succeeded'?input.result.data?.mapping??[]:[]});}catch(error){gitError=error.message;}
|
|
58
|
+
}
|
|
52
59
|
const changes=state.tasks.length?await readChanges({state}):undefined;
|
|
53
60
|
const plannerArtifacts=plannerWork?await readPlannerArtifacts(state,command==='record'&&input.evidence.kind==='planner-evidence'?input.evidence:undefined):undefined;
|
|
54
61
|
const plannerHistory=command==='record'&&input.evidence.kind==='planner-stage'?await readPlannerHistory(state,statePath,input.evidence.data.document?.path):{};
|
|
55
|
-
|
|
62
|
+
if(planArguments&&!gitError){try{now=clock();if(!Number.isFinite(now))throw Error('Invalid observation clock');}catch(error){gitError=error.message;}}
|
|
63
|
+
let destinationReview=false;
|
|
64
|
+
let decision=evaluateCooperative({state,request:{command,input},evidence:{target:currentTarget,versions,git,changes,requestFingerprint,plannerArtifacts,now,...plannerHistory}});
|
|
65
|
+
if(command==='record'&&input.evidence.kind==='planner-review'){
|
|
66
|
+
const data=input.evidence.data,stage=state.plannerLedger?.stages.find(s=>s.id===data.stageId),intent=state.targetIntents?.find(i=>i.id===stage?.targetIntentId),transition=state.targetTransitions?.find(t=>t.intentId===intent?.id&&t.targetVersion===state.targetVersion),document=intent?.documents.find(d=>d.kind===stage?.document.kind);
|
|
67
|
+
if(transition&&document){
|
|
68
|
+
let observed;try{observed=await readTargetPreparationEvidence({state,targetIntent:intent,destination:{realpath:state.target.realpath,path:document.path},observe,readChanges});}catch{}
|
|
69
|
+
const valid=input.actor.role==='orchestrator'&&stage.scopeVersion===state.scopeVersion&&same(versions,state.versions)&&same(currentTarget,state.target)&&!stage.pending&&stage.result?.status==='succeeded'&&stage.result.ref===data.resultRef&&stage.result.chatDraft?.complete===true&&data.full===true&&data.complete===true&&observed?.destination?.document?.hash===data.hash&&Array.isArray(data.criteria)&&data.criteria.length===stage.criteria.length&&new Set(data.criteria.map(c=>c.id)).size===stage.criteria.length&&data.criteria.every(c=>stage.criteria.some(s=>s.id===c.id)&&c.status==='resolved'&&typeof c.judgmentRef==='string'&&c.judgmentRef);
|
|
70
|
+
destinationReview=!!valid;
|
|
71
|
+
decision=valid?{decision:'allow',ruleIds:['cooperative.target-preparation']}:{decision:'hold',ruleIds:['cooperative.target-preparation'],reason:'Current destination bytes and complete source-bound review required'};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if(command==='record'&&['document','approval'].includes(input.evidence.kind)&&state.targetIntents?.some(intent=>!state.targetTransitions?.some(t=>t.intentId===intent.id)&&intent.documents.some(doc=>doc.kind===input.evidence.data.kind&&(input.evidence.data.path===doc.path||input.evidence.data.path===path.join(intent.sourceTarget.realpath,doc.path)))))decision={decision:'hold',ruleIds:['cooperative.target-preparation'],reason:'Prepared target transition required before document records'};
|
|
75
|
+
if(command==='record'&&input.evidence.kind==='approval'){
|
|
76
|
+
const data=input.evidence.data,intent=state.targetIntents?.find(i=>state.targetTransitions?.some(t=>t.intentId===i.id&&t.targetVersion===state.targetVersion)&&i.documents.some(d=>d.kind===data.kind));
|
|
77
|
+
if(intent){
|
|
78
|
+
const document=intent.documents.find(d=>d.kind===data.kind),firstStage=state.plannerLedger?.stages.find(s=>s.id===intent.firstStageId);
|
|
79
|
+
const stage=firstStage?.document.kind===data.kind?firstStage:state.plannerLedger?.stages.find(s=>s.document.kind===data.kind&&s.document.path===path.join(state.target.realpath,document.path)&&s.targetVersion===state.targetVersion);
|
|
80
|
+
const review=state.records.findLast(r=>r.kind==='planner-review'&&r.scopeVersion===state.scopeVersion&&r.targetVersion===state.targetVersion&&r.data?.stageId===stage?.id&&r.data?.resultRef===stage?.result?.ref&&r.data?.full===true&&r.data?.complete===true&&r.data?.hash===data.contentHash);
|
|
81
|
+
let observed;try{observed=await readTargetPreparationEvidence({state,targetIntent:intent,destination:{realpath:state.target.realpath,path:document.path},observe,readChanges});}catch{}
|
|
82
|
+
if(stage?.scopeVersion!==state.scopeVersion||![document.path,path.join(state.target.realpath,document.path)].includes(data.path)||!review||observed?.destination?.document?.hash!==data.contentHash||!same(currentTarget,state.target)||!same(versions,state.versions)||input.actor.role!=='orchestrator')decision={decision:'hold',ruleIds:['cooperative.target-preparation'],reason:'Current reviewed destination bytes required for approval'};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if(command==='record'&&input.evidence.kind==='target-preparation'){
|
|
86
|
+
let observed;
|
|
87
|
+
try{const intent=state.targetIntents?.find(i=>i.id===input.evidence.data.intentId);observed=await readTargetPreparationEvidence({state,targetIntent:intent,destination:input.evidence.data.destination,observe,readChanges});}catch{}
|
|
88
|
+
decision=input.actor.role==='orchestrator'&&same(versions,state.versions)?evaluateTargetPreparation({state,request:input.evidence.data,evidence:{...observed,versions}}):{decision:'hold',ruleIds:['cooperative.target-preparation']};
|
|
89
|
+
}
|
|
56
90
|
if(gitError)decision={decision:'hold',ruleIds:[input.operation==='assign-agent'?'cooperative.documents':'cooperative.git-evidence'],reason:gitError};
|
|
57
91
|
if(command==='prepare'&&decision.decision==='allow'){
|
|
58
92
|
if(decision.proposedTransition?.plannerLedger)state.plannerLedger=decision.proposedTransition.plannerLedger;
|
|
@@ -62,10 +96,10 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
|
|
|
62
96
|
if(['reconcile','result'].includes(command)&&input.result){
|
|
63
97
|
if(input.expectedRevision!==state.revision)state.unresolved.push({kind:'stale-result',requestId:input.requestId,intentId:input.intentId,expectedRevision:input.expectedRevision,revision:state.revision+1});
|
|
64
98
|
let intent=state.intents.find(i=>i.requestId===input.intentId);
|
|
65
|
-
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}});
|
|
66
|
-
if(['assign-agent','check','review','commit',...EXTERNAL_ACTIONS].includes(intent?.operation)&&!same(versions,state.versions))decision={...decision,decision:'hold',ruleIds:['cooperative.versions']};
|
|
99
|
+
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}):intent?.operation==='plan-amend'?reconcileCooperativePlanAmend({state,intent,result:input.result,observations:{target:currentTarget,git},now}):['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}});
|
|
100
|
+
if(['assign-agent','check','review','commit','plan-amend',...EXTERNAL_ACTIONS].includes(intent?.operation)&&!same(versions,state.versions))decision={...decision,decision:'hold',ruleIds:['cooperative.versions']};
|
|
67
101
|
if(input.expectedRevision!==state.revision)decision={...decision,decision:'hold',ruleIds:['cooperative.revision']};
|
|
68
|
-
if((intent?.operation
|
|
102
|
+
if((['commit','plan-amend'].includes(intent?.operation)||EXTERNAL_ACTIONS.includes(intent?.operation))&&input.actor.role!=='orchestrator')decision={...decision,decision:'hold',ruleIds:['cooperative.acceptance-actor']};
|
|
69
103
|
if(['check','review'].includes(intent?.operation)&&!same(currentTarget,state.target))decision={...decision,decision:'hold',ruleIds:['cooperative.target']};
|
|
70
104
|
if(intent?.operation==='commit'){
|
|
71
105
|
const verified=decision.proposedTransition;
|
|
@@ -90,6 +124,20 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
|
|
|
90
124
|
state.commitObservations??=[];state.commitObservations.push(observation);
|
|
91
125
|
if(decision.decision!=='allow'&&verified?.result?.status==='succeeded')verified.result.status='mismatch';
|
|
92
126
|
}
|
|
127
|
+
if(intent?.operation==='plan-amend'){
|
|
128
|
+
if(command!=='reconcile'||gitError||state.unresolved.some(u=>!u.resolvedBy&&!(u.intentId===input.intentId&&['target-changed','stale-result'].includes(u.kind))))decision={...decision,decision:'hold',ruleIds:['cooperative.plan-amend']};
|
|
129
|
+
if(decision.decision!=='allow'&&decision.proposedTransition?.result?.status==='succeeded')decision.proposedTransition.result.status='mismatch';
|
|
130
|
+
const observed={intentId:input.intentId,revision:state.revision+1,source:'local-git',target:currentTarget,observation:git??null,...(gitError?{error:gitError}:{})};
|
|
131
|
+
if(decision.decision==='allow'){
|
|
132
|
+
const previous=structuredClone(state.target),nextVersion=state.targetVersion+1,a=intent.arguments,transition=decision.proposedTransition;
|
|
133
|
+
state=invalidateAffectedEvidence({state,changedVersions:{targetVersion:nextVersion,documentVersion:a.plan.new.version},affectedTaskIds:a.affectedTaskIds});
|
|
134
|
+
state.targetHistory.push({target:previous,revision:state.revision+1,source:'local-observation',kind:'verified-plan-amend-parent',intentId:input.intentId});state.target=structuredClone(transition.target);state.targetVersion=nextVersion;
|
|
135
|
+
state.planAmendments??=[];state.planAmendments.push({intentId:input.intentId,path:a.plan.path,oldVersion:a.plan.old.version,newVersion:a.plan.new.version,oldHash:a.plan.old.hash,newHash:a.plan.new.hash,oldCommit:a.history.planCommit,newCommit:transition.mapping[0].new,mapping:structuredClone(transition.mapping),approvalRefs:structuredClone(a.approvals),source:'operator-attested',observationSource:'local-git'});
|
|
136
|
+
for(const u of state.unresolved)if(!u.resolvedBy&&u.intentId===input.intentId&&u.kind==='target-changed')u.resolvedBy=input.requestId;
|
|
137
|
+
intent=state.intents.find(i=>i.requestId===input.intentId);
|
|
138
|
+
}
|
|
139
|
+
state.planAmendObservations??=[];state.planAmendObservations.push(observed);
|
|
140
|
+
}
|
|
93
141
|
const verification=decision.proposedTransition;
|
|
94
142
|
if(verification?.check){
|
|
95
143
|
if(decision.decision!=='allow'&&verification.check.status==='passed'){verification.check.status='unverified';verification.check.stale=true;verification.findings=[];verification.result.status='mismatch';}
|
|
@@ -100,17 +148,17 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
|
|
|
100
148
|
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};}
|
|
101
149
|
const actualResult=decision.proposedTransition?.result??input.result;
|
|
102
150
|
// Preserve actual statements even when runtime/HEAD changed, without rebinding target.
|
|
103
|
-
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
|
|
151
|
+
if(!same(currentTarget,state.target)){state.targetHistory.push({target:currentTarget,revision:state.revision+1,source:'local-observation'});state.unresolved.push({kind:'target-changed',...(['commit','plan-amend'].includes(intent?.operation)?{intentId:input.intentId}:{}),revision:state.revision+1});}
|
|
104
152
|
if(!same(versions,state.versions))state.unresolved.push({kind:'versions-changed',revision:state.revision+1});
|
|
105
153
|
state.results.push({...actualResult,requestId:input.intentId,recordRequestId:input.requestId,actor:input.actor,source:'operator-attested'});
|
|
106
|
-
if(intent&&(['assign-agent','check','review','commit',...EXTERNAL_ACTIONS].includes(intent.operation)?decision.proposedTransition?.result&&['prepared','unknown','mismatch'].includes(intent.status):true)){
|
|
154
|
+
if(intent&&(['assign-agent','check','review','commit','plan-amend',...EXTERNAL_ACTIONS].includes(intent.operation)?decision.proposedTransition?.result&&['prepared','unknown','mismatch'].includes(intent.status):true)){
|
|
107
155
|
intent.status=actualResult.status;
|
|
108
156
|
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});
|
|
109
157
|
}
|
|
110
158
|
else state.unresolved.push({kind:'unmatched-result',requestId:input.intentId,revision:state.revision+1});
|
|
111
159
|
}else if(command==='record'&&input.evidence.kind.startsWith('planner-')){
|
|
112
|
-
if(decision.proposedTransition?.plannerLedger&&(decision.decision==='allow'||decision.proposedTransition.recordUncertainty)){
|
|
113
|
-
state.plannerLedger=decision.proposedTransition.plannerLedger;
|
|
160
|
+
if(destinationReview||decision.proposedTransition?.plannerLedger&&(decision.decision==='allow'||decision.proposedTransition.recordUncertainty)){
|
|
161
|
+
if(!destinationReview)state.plannerLedger=decision.proposedTransition.plannerLedger;
|
|
114
162
|
state.records.push({...input.evidence,actor:input.actor,source:'operator-attested',requestId:input.requestId,revision:state.revision+1,scopeVersion:state.scopeVersion,targetVersion:state.targetVersion});
|
|
115
163
|
if(input.evidence.kind==='planner-result'){
|
|
116
164
|
const intent=state.intents.find(i=>i.operation==='planner-call'&&i.requestId===input.evidence.data.callRequestId);
|
|
@@ -118,7 +166,12 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
|
|
|
118
166
|
}
|
|
119
167
|
}
|
|
120
168
|
}else if(command==='record'&&decision.decision==='allow'){
|
|
169
|
+
if(input.evidence.kind==='target-preparation'&&input.evidence.data.type==='transition')state=applyCooperativeTargetTransition({state,decision,messageRef:input.evidence.messageRef});
|
|
170
|
+
if(input.evidence.kind==='target-preparation'&&decision.proposedTransition?.intent){state.targetIntents??=[];state.targetIntents.push(decision.proposedTransition.intent);}
|
|
171
|
+
if(input.evidence.kind==='target-preparation'&&decision.proposedTransition?.checkpoint){state.targetCheckpoints??=[];state.targetCheckpoints.push({...decision.proposedTransition.checkpoint,messageRef:input.evidence.messageRef,source:'operator-attested'});}
|
|
172
|
+
if(input.evidence.kind==='target-preparation'&&decision.proposedTransition?.destination){state.targetDocuments??=[];state.targetDocuments.push({...decision.proposedTransition.destination,messageRef:input.evidence.messageRef});}
|
|
121
173
|
const entry={...input.evidence,actor:input.actor,source:'operator-attested',requestId:input.requestId,revision:state.revision+1,scopeVersion:state.scopeVersion,targetVersion:state.targetVersion};
|
|
174
|
+
if(entry.kind==='note'&&decision.proposedTransition?.checkpoint){const intent=state.intents.find(i=>i.operation==='plan-amend'&&i.requestId===decision.proposedTransition.checkpoint.intentId);if(!intent)throw Error('Checkpoint intent unavailable');intent.checkpoint=structuredClone(decision.proposedTransition.checkpoint);}
|
|
122
175
|
if(entry.kind==='scope'){
|
|
123
176
|
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');
|
|
124
177
|
state=invalidateAffectedEvidence({state,changedVersions:{scopeVersion:state.scopeVersion+1},affectedTaskIds:entry.data.affectedTaskIds});entry.scopeVersion=state.scopeVersion;state.scopes.push(entry);
|
|
@@ -143,6 +196,8 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
|
|
|
143
196
|
if(ready){marker.resolvedBy=input.requestId;marker.resolutionEvidence={taskIds:ids,contracts:proof,scopeVersion:state.scopeVersion};}
|
|
144
197
|
}
|
|
145
198
|
}
|
|
199
|
+
// The observed identifiers contain no URLs and do not verify remote state.
|
|
200
|
+
if(planArguments&&Array.isArray(git?.configuredRemotes)&&git.configuredRemotes.every(r=>r&&typeof r.name==='string'&&/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(r.name)&&/^sha256:[a-f0-9]{64}$/.test(r.identity??'')))decision={...decision,observedRemoteIdentities:{source:'local-git',remotes:git.configuredRemotes.map(r=>({name:r.name,identity:r.identity}))}};
|
|
146
201
|
// A held request is durable audit, never an executable intent.
|
|
147
202
|
state.revision++;
|
|
148
203
|
const output=result(state,decision,{persisted:true});
|
package/lib/runtime/evaluate.mjs
CHANGED
|
@@ -14,11 +14,74 @@ import {prepareCooperativeAssignment} from './assignment.mjs';
|
|
|
14
14
|
* @property {object} coverage
|
|
15
15
|
*/
|
|
16
16
|
import path from 'node:path';
|
|
17
|
-
import
|
|
17
|
+
import crypto from 'node:crypto';
|
|
18
|
+
import {fingerprintRequest} from './store.mjs';
|
|
19
|
+
import {validateCooperativeState,validateCooperativeCommand,validatePlanAmendArguments} from './cooperative-schema.mjs';
|
|
18
20
|
import {validateWorkflowState, validateRequest, validateEvidence,validatePlannerStage} from './schema.mjs';
|
|
19
21
|
import {ROLES, ROLE_OPERATIONS, GIT_OPERATIONS, EXTERNAL_OPERATIONS} from './operations.mjs';
|
|
20
22
|
const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
21
23
|
const within=(base,file)=>{const relative=path.relative(base,file);return relative===''||(!relative.startsWith('..'+path.sep)&&relative!=='..'&&!path.isAbsolute(relative));};
|
|
24
|
+
const objectFields=(value,keys)=>value&&typeof value==='object'&&!Array.isArray(value)&&Object.keys(value).length===keys.length&&keys.every(key=>Object.hasOwn(value,key));
|
|
25
|
+
const hashBytes=value=>'sha256:'+crypto.createHash('sha256').update(value).digest('hex');
|
|
26
|
+
const gitBlob=(value,length)=>crypto.createHash(length===64?'sha256':'sha1').update(`blob ${Buffer.byteLength(value)}\0`).update(value).digest('hex');
|
|
27
|
+
const planCoverage={enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false,remoteVerified:false};
|
|
28
|
+
const planDecision=(state,reason,transition=null,allow=false)=>({expectedRevision:state?.revision??0,decision:allow?'allow':'hold',ruleIds:['cooperative.plan-amend'],reason,coverage:{...planCoverage},requiredEvidence:[],nextActions:[],proposedTransition:transition});
|
|
29
|
+
const unresolvedPlan=state=>state.intents.some(i=>i.operation==='plan-amend'&&['prepared','unknown','mismatch','failed'].includes(i.status));
|
|
30
|
+
const remoteIdentities=remotes=>remotes.map(r=>r.name+'\0'+r.identity).sort();
|
|
31
|
+
function remoteSnapshotValid(remotes,configured,at,protectedOids){
|
|
32
|
+
if(!Number.isFinite(at)||!Array.isArray(configured)||!Array.isArray(remotes)||configured.some(r=>!objectFields(r,['name','identity'])||typeof r.name!=='string'||!/^sha256:[a-f0-9]{64}$/.test(r.identity??''))||new Set(configured.map(r=>r.name)).size!==configured.length||!same(remoteIdentities(remotes),remoteIdentities(configured)))return false;
|
|
33
|
+
return remotes.every(r=>r.complete===true&&Number.isFinite(Date.parse(r.observedAt))&&at-Date.parse(r.observedAt)>=0&&at-Date.parse(r.observedAt)<=300000&&r.refs.every(ref=>ref.oid.length===protectedOids[0]?.length&&!protectedOids.includes(ref.oid)));
|
|
34
|
+
}
|
|
35
|
+
function planEntries(commit,length){
|
|
36
|
+
const validOid=value=>typeof value==='string'&&new RegExp(`^[a-f0-9]{${length}}$`).test(value);
|
|
37
|
+
return commit&&validOid(commit.oid)&&validOid(commit.parent)&&typeof commit.author==='string'&&typeof commit.authorDate==='string'&&typeof commit.message==='string'&&Array.isArray(commit.headerTemplate)&&commit.headerTemplate.every(header=>typeof header==='string')&&['tree','parent','committer'].every(name=>commit.headerTemplate.filter(header=>header===name).length===1)&&Array.isArray(commit.tree)&&new Set(commit.tree.map(row=>row.path)).size===commit.tree.length&&commit.tree.every(row=>objectFields(row,['path','mode','oid'])&&typeof row.path==='string'&&row.path.length>0&&!row.path.startsWith('/')&&!/[\\\x00\r\n:]/.test(row.path)&&!row.path.split('/').some(p=>!p||p==='.'||p==='..')&&['100644','100755','120000','160000'].includes(row.mode)&&validOid(row.oid));
|
|
38
|
+
}
|
|
39
|
+
function oldPlanObservation(a,g,target){
|
|
40
|
+
const h=g?.history,commits=h?.commits,length=target.head.length,oldBlob=gitBlob(a.plan.old.content,length);
|
|
41
|
+
if(!same(g?.target,target)||!g.index||!new RegExp(`^[a-f0-9]{${length}}$`).test(g.index.tree??'')||g.index.tree!==g.index.headTree||g.index.stagedPaths?.length!==0||g.worktree?.planClean!==true||!/^sha256:[a-f0-9]{64}$/.test(g.worktree?.nonPlanFingerprint??'')||g.plan?.path!==a.plan.path||g.plan.content!==a.plan.old.content||g.plan.hash!==a.plan.old.hash||g.plan.originalHash!==a.plan.old.hash||!h||h.base!==a.history.base||h.planCommit!==a.history.planCommit||h.head!==a.history.head||!Array.isArray(commits)||!commits.length||commits.length>256||commits.some(c=>!c)||commits[0].oid!==a.history.planCommit||commits.at(-1).oid!==a.history.head||new Set(commits.map(c=>c.oid)).size!==commits.length)return false;
|
|
42
|
+
const originalMode=commits[0].tree?.find(row=>row.path===a.plan.gitPath)?.mode;
|
|
43
|
+
return ['100644','100755'].includes(originalMode)&&g.plan.mode===originalMode&&commits.every((c,index)=>planEntries(c,length)&&c.parent===(index?commits[index-1].oid:a.history.base)&&c.tree.filter(row=>row.path===a.plan.gitPath).length===1&&c.tree.some(row=>row.path===a.plan.gitPath&&row.mode===originalMode&&row.oid===oldBlob));
|
|
44
|
+
}
|
|
45
|
+
function approvalRecord(state,a,approval){
|
|
46
|
+
const record=state.records.findLast(r=>r.kind==='approval'&&r.messageRef===approval.ref);
|
|
47
|
+
return record?.source==='operator-attested'&&record.actor?.role==='orchestrator'&&record.targetVersion===state.targetVersion&&record.scopeVersion===state.scopeVersion&&record.data?.kind==='plan'&&record.data.path===a.plan.path&&record.data.version===approval.version&&record.data.contentHash===approval.hash&&record.data.approvedAt===approval.approvedAt;
|
|
48
|
+
}
|
|
49
|
+
function planAuthorization(state,a,at){
|
|
50
|
+
if(!Number.isFinite(at)||a.ownership!=='exclusive'||a.publication!=='unpublished'||a.history.head!==state.target.head||a.history.branch!==state.target.branch||!state.target.branch||a.recovery.oid!==state.target.head||a.plan.path!==path.join(state.target.realpath,...a.plan.gitPath.split('/'))||a.plan.old.hash!==hashBytes(a.plan.old.content)||a.plan.new.hash!==hashBytes(a.plan.new.content)||a.plan.old.hash===a.plan.new.hash)return false;
|
|
51
|
+
const old=a.approvals.original,next=a.approvals.amendment,changed=Date.parse(a.changedAt);
|
|
52
|
+
if(old.version!==a.plan.old.version||old.hash!==a.plan.old.hash||Date.parse(old.approvedAt)>changed||changed>at||!approvalRecord(state,a,old))return false;
|
|
53
|
+
if(a.classification==='contract'&&!next||next&&(next.version!==a.plan.new.version||next.hash!==a.plan.new.hash||Date.parse(next.approvedAt)<changed||Date.parse(next.approvedAt)>at||!approvalRecord(state,a,next)))return false;
|
|
54
|
+
return a.writers.every(w=>Date.parse(w.stoppedAt)<=at&&state.records.some(r=>r.kind==='note'&&r.messageRef===w.evidenceRef&&r.source==='operator-attested'&&r.actor?.role==='orchestrator'&&r.targetVersion===state.targetVersion&&r.scopeVersion===state.scopeVersion&&r.data?.type==='writer-stopped'&&r.data.assignmentId===w.assignmentId&&r.data.stoppedAt===w.stoppedAt));
|
|
55
|
+
}
|
|
56
|
+
export function prepareCooperativePlanAmend({state,request,evidence}={}){
|
|
57
|
+
const a=request?.arguments,g=evidence?.git,at=evidence?.now;
|
|
58
|
+
try{validatePlanAmendArguments(a);}catch{return planDecision(state,'Exact plan-amend evidence required');}
|
|
59
|
+
if(request.actor?.role!=='orchestrator'||request.expectedRevision!==state.revision||state.unresolved.some(x=>!x.resolvedBy)||state.intents.some(i=>i.operation==='plan-amend'&&['prepared','unknown','mismatch','failed'].includes(i.status))||!planAuthorization(state,a,at)||!oldPlanObservation(a,g,state.target)||g.recoveryRef!==null||!remoteSnapshotValid(a.remotes,g.configuredRemotes,at,g.history.commits.map(c=>c.oid)))return planDecision(state,'Approval, stopped writers, clean index, local history and unpublished remote attestations required');
|
|
60
|
+
return planDecision(state,'Durable intent only; the operator performs any rewrite',{intent:{requestId:request.requestId,operation:'plan-amend',status:'prepared',arguments:structuredClone(a),target:structuredClone(state.target),versions:structuredClone(state.versions),scopeVersion:state.scopeVersion,targetVersion:state.targetVersion,observation:structuredClone(g),fingerprint:fingerprintRequest(request),source:'operator-attested'}},true);
|
|
61
|
+
}
|
|
62
|
+
export function checkpointCooperativePlanAmend({state,request,evidence}={}){
|
|
63
|
+
const d=request?.evidence?.data,intent=state.intents.find(i=>i.requestId===d?.intentId&&i.operation==='plan-amend'),a=intent?.arguments,g=evidence?.git;
|
|
64
|
+
if(request?.actor?.role!=='orchestrator'||!objectFields(d,['type','intentId'])||d.type!=='plan-amend-checkpoint'||!intent||intent.status!=='prepared'||intent.checkpoint||!same(intent.target,state.target)||!same(intent.versions,state.versions)||!planAuthorization(state,a,evidence?.now)||!oldPlanObservation(a,g,state.target)||!same(g.recoveryRef,{name:a.recovery.ref,oid:a.recovery.oid})||g.worktree.nonPlanFingerprint!==intent.observation.worktree.nonPlanFingerprint||g.index.tree!==intent.observation.index.tree||!same(g.history.commits,intent.observation.history.commits)||!remoteSnapshotValid(a.remotes,g.configuredRemotes,evidence.now,g.history.commits.map(c=>c.oid)))return planDecision(state,'Current prepared intent, recovery ref and pre-rewrite observation required');
|
|
65
|
+
return planDecision(state,'Pre-rewrite checkpoint recorded',{checkpoint:{intentId:intent.requestId,messageRef:request.evidence.messageRef,observedAt:new Date(evidence.now).toISOString(),observation:structuredClone(g)}},true);
|
|
66
|
+
}
|
|
67
|
+
export function reconcileCooperativePlanAmend({state,intent,result,observations,now}={}){
|
|
68
|
+
const transition={result:{...structuredClone(result),status:result?.status==='unknown'?'unknown':result?.status==='failed'?'failed':'mismatch',data:{...structuredClone(result?.data??{}),remoteVerified:false},source:'operator-attested'},observation:observations?.git??null};
|
|
69
|
+
const hold=reason=>planDecision(state,reason,transition);
|
|
70
|
+
if(!intent||intent.operation!=='plan-amend'||!['prepared','unknown','mismatch'].includes(intent.status)||!intent.checkpoint||!same(intent.target,state.target)||!same(intent.versions,state.versions)||intent.scopeVersion!==state.scopeVersion||intent.targetVersion!==state.targetVersion)return hold('Matching unresolved plan-amend intent and checkpoint required');
|
|
71
|
+
if(result?.status!=='succeeded')return hold('Rewrite did not establish a successful result');
|
|
72
|
+
const d=result.data,a=intent.arguments,g=observations?.git,target=observations?.target,mapping=d?.mapping,old=intent.observation.history.commits,next=g?.history?.newCommits;
|
|
73
|
+
try{validatePlanAmendArguments({...a,remotes:d?.remotes});}catch{return hold('Exact result and fresh remote attestations required');}
|
|
74
|
+
if(!objectFields(d,['head','mapping','remotes'])||!Array.isArray(mapping)||mapping.length!==old.length||!mapping.length||mapping.some(m=>!objectFields(m,['old','new']))||new Set(mapping.map(m=>m.old)).size!==mapping.length||new Set(mapping.map(m=>m.new)).size!==mapping.length||!Array.isArray(next)||next.length!==mapping.length||!target||target.realpath!==intent.target.realpath||target.gitCommonDir!==intent.target.gitCommonDir||target.gitDir!==intent.target.gitDir||target.branch!==intent.target.branch||target.head!==d.head||mapping.at(-1).new!==target.head||!same(g?.target,target)||g.index?.tree!==g.index?.headTree||g.index?.stagedPaths?.length!==0||g.worktree?.planClean!==true||g.worktree.nonPlanFingerprint!==intent.observation.worktree.nonPlanFingerprint||!same(g.recoveryRef,{name:a.recovery.ref,oid:a.recovery.oid})||g.plan?.path!==a.plan.path||g.plan.content!==a.plan.new.content||g.plan.hash!==a.plan.new.hash||g.plan.originalHash!==a.plan.old.hash||!same(g.history.commits,old)||!same(remoteIdentities(d.remotes),remoteIdentities(a.remotes))||!remoteSnapshotValid(d.remotes,g.configuredRemotes,now,[...old.map(c=>c.oid),...mapping.map(m=>m.new)]))return hold('Observed target, preservation, mapping, document or remote evidence differs');
|
|
75
|
+
const planOid=gitBlob(a.plan.new.content,target.head.length),originalMode=old[0].tree.find(row=>row.path===a.plan.gitPath)?.mode;if(g.plan.mode!==originalMode)return hold('Working plan mode differs');
|
|
76
|
+
for(let i=0;i<mapping.length;i++){
|
|
77
|
+
const before=old[i],after=next[i],m=mapping[i];
|
|
78
|
+
if(!planEntries(after,target.head.length)||m.old!==before.oid||m.new!==after.oid||after.parent!==(i?mapping[i-1].new:a.history.base)||after.author!==before.author||after.authorDate!==before.authorDate||after.message!==before.message||!same(after.headerTemplate,before.headerTemplate))return hold('Commit mapping or metadata differs');
|
|
79
|
+
const plan=after.tree.filter(row=>row.path===a.plan.gitPath),oldPlan=before.tree.find(row=>row.path===a.plan.gitPath);if(plan.length!==1||!oldPlan||plan[0].mode!==oldPlan.mode||plan[0].oid!==planOid)return hold('Unexpected plan bytes or mode in rewritten history');
|
|
80
|
+
const other=rows=>rows.filter(row=>row.path!==a.plan.gitPath).sort((x,y)=>x.path<y.path?-1:x.path>y.path?1:0);
|
|
81
|
+
if(!same(other([...before.tree]),other([...after.tree])))return hold('Non-plan tree changed');
|
|
82
|
+
}
|
|
83
|
+
transition.result.status='succeeded';transition.target=structuredClone(target);transition.mapping=structuredClone(mapping);return planDecision(state,'Locally observed same-intent amendment; remote claims remain operator-attested',transition,true);
|
|
84
|
+
}
|
|
22
85
|
/** Pure policy decision. Evidence is supplied by a trusted adapter/provider, never tool arguments. */
|
|
23
86
|
export function evaluate(policy, state, request, evidence){
|
|
24
87
|
const result=(decision,id,reason,requiredEvidence=[])=>({decision,ruleIds:[id],reason,requiredEvidence,expectedRevision:state?.revision??0,proposedTransition:null,coverage:{platform:request?.platform??'unknown',enforcement:'decision-only'}});
|
|
@@ -113,6 +176,7 @@ export function evaluate(policy, state, request, evidence){
|
|
|
113
176
|
if(d.kind!=='task'||!accepted||task.status!=='accepted'||request.paths.some(p=>!task.paths.some(base=>within(base,p))))return result('hold','git.orchestrator-accept-commit','accepted task verification required',['git-evidence']);
|
|
114
177
|
}
|
|
115
178
|
}
|
|
179
|
+
if(request.operation==='plan-amend')return result('hold','runtime.plan-amend','Cooperative plan-amend requires its durable intent and local observer',['plan-amend-intent']);
|
|
116
180
|
if(EXTERNAL_OPERATIONS.includes(request.operation))return result('hold','external.explicit-user-authority','explicit external target approval must be verified',['external-approval']);
|
|
117
181
|
if(request.operation==='transition')return result('hold','runtime.transition','result confirmation required',['observed-result']);
|
|
118
182
|
return result('allow',roleRule,'assigned operation with verified provenance');
|
|
@@ -128,11 +192,15 @@ export function evaluateCooperative({policy,state,request,evidence}={}){
|
|
|
128
192
|
if(!evidence||!same(evidence.target,state.target))return hold('cooperative.target','Observed target changed');
|
|
129
193
|
if(!same(evidence.versions,state.versions))return hold('cooperative.versions','Policy or runtime changed');
|
|
130
194
|
if(request.input&&request.input.expectedRevision!==state.revision)return hold('cooperative.revision','Stale revision');
|
|
195
|
+
const checkpoint=request.command==='record'&&request.input.evidence.kind==='note'&&request.input.evidence.data?.type==='plan-amend-checkpoint';
|
|
196
|
+
if(unresolvedPlan(state)&&((request.command==='prepare')||(request.command==='record'&&!checkpoint)))return hold('cooperative.plan-amend-unresolved','Resolve the same plan-amend intent before further work');
|
|
197
|
+
if(checkpoint)return checkpointCooperativePlanAmend({state,request:request.input,evidence});
|
|
131
198
|
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');
|
|
132
199
|
if(request.command==='record'&&request.input.evidence.kind.startsWith('planner-')||request.command==='prepare'&&request.input.operation==='planner-call')return evaluateCooperativePlanner({state,request,evidence});
|
|
133
200
|
if(request.command==='prepare'){
|
|
134
201
|
if(unresolvedExternal(state))return hold('cooperative.external-unresolved','Investigate unresolved external effects before follow-up');
|
|
135
202
|
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)});
|
|
203
|
+
if(request.input.operation==='plan-amend')return prepareCooperativePlanAmend({state,request:request.input,evidence});
|
|
136
204
|
if(request.input.operation==='accept')return evaluateCooperativeAcceptance({state,request:request.input,evidence});
|
|
137
205
|
if(request.input.operation==='commit')return prepareCooperativeCommit({state,request:request.input,evidence,gitEvidence:evidence.git});
|
|
138
206
|
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});
|
|
@@ -206,11 +274,19 @@ export function evaluateCooperativePlanner({state,request,evidence}={}){
|
|
|
206
274
|
const kind=input.evidence.kind,ref=input.evidence.messageRef;
|
|
207
275
|
if(kind==='planner-stage'){
|
|
208
276
|
if(ledger.stages.some(s=>s.id===data.id))return result('deny','Planner stage ID already exists');
|
|
209
|
-
const contract={document:data.document,inputVersion:data.inputVersion,criteriaVersion:data.criteriaVersion,scopeVersion:data.scopeVersion};
|
|
277
|
+
const contract={document:data.document,inputVersion:data.inputVersion,criteriaVersion:data.criteriaVersion,scopeVersion:data.scopeVersion,...(data.targetIntentId?{targetIntentId:data.targetIntentId}:{})};
|
|
210
278
|
const approval=state.records.find(r=>r.kind==='approval'&&r.messageRef===data.approvalRef&&r.source==='operator-attested'&&r.scopeVersion===state.scopeVersion&&r.targetVersion===state.targetVersion);
|
|
211
279
|
const approved=approval?.data?.plannerStage;
|
|
212
|
-
if(!approved||!same(contract.document,approved.document)||['inputVersion','criteriaVersion','scopeVersion'].some(k=>contract[k]!==approved[k])||contract.scopeVersion!==state.scopeVersion||!within(state.target.realpath,data.document?.path))return result('hold','Approved stage contract and current scope are required');
|
|
213
|
-
|
|
280
|
+
if(!approved||!same(contract.document,approved.document)||['inputVersion','criteriaVersion','scopeVersion','targetIntentId'].some(k=>contract[k]!==approved[k])||contract.scopeVersion!==state.scopeVersion||!within(state.target.realpath,data.document?.path))return result('hold','Approved stage contract and current scope are required');
|
|
281
|
+
const activeIntent=state.targetIntents?.find(i=>!state.targetTransitions?.some(t=>t.intentId===i.id));
|
|
282
|
+
if(activeIntent&&data.targetIntentId!==activeIntent.id)return result('hold','First stage must bind the active target intent');
|
|
283
|
+
const preparedIntent=state.targetIntents?.find(i=>state.targetTransitions?.some(t=>t.intentId===i.id&&t.targetVersion===state.targetVersion));
|
|
284
|
+
if(preparedIntent&&data.document.kind==='plan'&&!preparedIntent.documents.some(d=>d.kind==='plan'&&path.join(state.target.realpath,d.path)===data.document.path))return result('hold','Plan path must match the prepared target intent');
|
|
285
|
+
if(data.targetIntentId){const intent=state.targetIntents?.find(i=>i.id===data.targetIntentId);if(!intent||intent.firstStageId!==data.id||intent.sourceTargetVersion!==state.targetVersion||intent.documents.some(d=>d.kind==='spec')&&data.document.kind!=='spec'||!intent.documents.some(d=>d.kind===data.document.kind&&path.join(state.target.realpath,d.path)===data.document.path))return result('hold','Current target intent and matching first document required');}
|
|
286
|
+
const targetIntent=state.targetIntents?.find(i=>i.id===data.targetIntentId);
|
|
287
|
+
const approvedOmission=targetIntent?.depth==='Bounded'&&targetIntent.firstStageId===data.id&&targetIntent.documents.length===1&&targetIntent.documents[0].kind==='plan';
|
|
288
|
+
const approvedSpec=approval.data.prerequisiteRefs?.some(ref=>state.records.some(r=>r.kind==='approval'&&r.messageRef===ref&&r.data.kind==='spec'&&r.source==='operator-attested'&&r.scopeVersion===state.scopeVersion&&r.targetVersion===state.targetVersion&&(!preparedIntent||state.targetDocuments?.some(d=>d.intentId===preparedIntent.id&&preparedIntent.documents.some(doc=>doc.kind==='spec'&&doc.path===d.path)&&d.approvalRef===ref&&d.scopeVersion===state.scopeVersion&&d.targetVersion===state.targetVersion&&[d.path,path.join(state.target.realpath,d.path)].includes(r.data.path)&&d.hash===r.data.contentHash))));
|
|
289
|
+
if(data.document.kind==='plan'&&!approvedOmission&&!approvedSpec)return result('hold','Approved spec prerequisite is required for plan drafting');
|
|
214
290
|
const previous=[...ledger.stages,...(evidence.plannerHistory??[]).map(h=>h.stage)].filter(s=>s.document.path===data.document.path&&s.document.kind===data.document.kind&&s.document.phase===data.document.phase);
|
|
215
291
|
if(evidence.plannerHistoryError)return result('hold','Planner history could not be verified: '+evidence.plannerHistoryError);
|
|
216
292
|
if(previous.length){
|
|
@@ -233,8 +309,11 @@ export function evaluateCooperativePlanner({state,request,evidence}={}){
|
|
|
233
309
|
if(kind==='planner-result'){
|
|
234
310
|
const call=stage.calls.find(c=>c.requestId===data.callRequestId);
|
|
235
311
|
if(!call||stage.pending!==call.requestId||!['reserved','unknown'].includes(call.status)||!['succeeded','failed','unknown'].includes(data.status))return result('hold','A pending Planner call and confirmed result identity are required');
|
|
236
|
-
|
|
237
|
-
call.status
|
|
312
|
+
const chat=data.chatDraft,intent=state.targetIntents?.find(i=>i.id===stage.targetIntentId);
|
|
313
|
+
if(chat&&call.status!=='reserved')return result('hold','Unknown Planner call must be reconciled before checkpoint');
|
|
314
|
+
const chatComplete=!!(intent&&chat?.intentId===intent.id&&typeof chat.draftRef==='string'&&chat.draftRef&&typeof chat.selfReviewRef==='string'&&chat.selfReviewRef&&chat.complete===true);
|
|
315
|
+
if(data.status==='succeeded'&&!chatComplete&&(!artifact(data.artifactRef)||artifact(data.artifactRef).purpose!=='draft'||!artifact(data.selfReviewRef)||artifact(data.selfReviewRef).purpose!=='self-review'))return result('hold','Verified full draft and self-review artifacts are required');
|
|
316
|
+
call.status=data.status;call.resultRef=ref;stage.result={ref,requestId:call.requestId,status:data.status,artifactRef:data.artifactRef??null,selfReviewRef:data.selfReviewRef??null,...(chatComplete?{chatDraft:structuredClone(chat)}:{}),source:'operator-attested'};stage.ready=false;
|
|
238
317
|
if(data.status!=='unknown')stage.pending=null;
|
|
239
318
|
return transition(stage,data.status==='unknown'?'Planner execution remains unknown':'Planner result statement recorded',data.status==='unknown'?'hold':'allow',data.status==='unknown');
|
|
240
319
|
}
|
|
@@ -3,6 +3,7 @@ import {inspectPath} from './scope.mjs';
|
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import {spawnSync} from 'node:child_process';
|
|
6
|
+
import {constants} from 'node:fs';
|
|
6
7
|
|
|
7
8
|
const oid=value=>typeof value==='string'&&/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value);
|
|
8
9
|
const hash=bytes=>'sha256:'+crypto.createHash('sha256').update(bytes).digest('hex');
|
|
@@ -142,6 +143,103 @@ export async function readCooperativeCommitGitEvidence({target,commit,gitRunner=
|
|
|
142
143
|
return {...evidence,commit:{commit,parent:metadata[1],tree:metadata[2],paths:committedPaths,head:target.head}};
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
// Capture every working-tree entry, including ignored and empty directories.
|
|
147
|
+
// Never follow a symlink or include Git's own mutable workflow directory.
|
|
148
|
+
async function preservationFingerprint(root,planPath){
|
|
149
|
+
const rows=[];let total=0;
|
|
150
|
+
async function visit(dir,relative=''){
|
|
151
|
+
const before=await fs.lstat(dir);if(!before.isDirectory()||before.isSymbolicLink())throw Error('Unsafe worktree directory');
|
|
152
|
+
const names=(await fs.readdir(dir,{encoding:'buffer'})).map(text).sort((a,b)=>Buffer.compare(Buffer.from(a),Buffer.from(b)));
|
|
153
|
+
for(const name of names){
|
|
154
|
+
if(!relative&&name==='.git')continue;
|
|
155
|
+
const rel=relative?relative+'/'+name:name;if(!gitPath(rel))throw Error('Unsafe worktree path');if(rel===planPath)continue;
|
|
156
|
+
const filename=path.join(dir,name),stat=await fs.lstat(filename);if(++total>50000)throw Error('Worktree snapshot too large');
|
|
157
|
+
const mode=stat.mode&0o7777;
|
|
158
|
+
if(stat.isDirectory()&&!stat.isSymbolicLink()){rows.push({path:rel,type:'directory',mode});await visit(filename,rel);}
|
|
159
|
+
else if(stat.isSymbolicLink())rows.push({path:rel,type:'symlink',mode,target:await fs.readlink(filename)});
|
|
160
|
+
else if(stat.isFile()){
|
|
161
|
+
if(stat.size>64*1024*1024)throw Error('Worktree file too large');
|
|
162
|
+
const handle=await fs.open(filename,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);let bytes;
|
|
163
|
+
try{const first=await handle.stat();if(!first.isFile()||first.dev!==stat.dev||first.ino!==stat.ino)throw Error('Worktree file changed');bytes=await handle.readFile();const last=await handle.stat(),named=await fs.lstat(filename);if(first.dev!==last.dev||first.ino!==last.ino||first.size!==last.size||first.mtimeMs!==last.mtimeMs||last.dev!==named.dev||last.ino!==named.ino||last.mode!==named.mode)throw Error('Worktree file changed');}finally{await handle.close();}
|
|
164
|
+
rows.push({path:rel,type:'file',mode,hash:hash(bytes)});
|
|
165
|
+
}else throw Error('Unsupported worktree entry');
|
|
166
|
+
}
|
|
167
|
+
const after=await fs.lstat(dir);if(before.dev!==after.dev||before.ino!==after.ino||before.mtimeMs!==after.mtimeMs||before.mode!==after.mode)throw Error('Worktree directory changed');
|
|
168
|
+
}
|
|
169
|
+
await visit(root);return hash(Buffer.from(JSON.stringify(rows)));
|
|
170
|
+
}
|
|
171
|
+
async function readPlanBytes(filename){
|
|
172
|
+
await inspectPath(filename);const handle=await fs.open(filename,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
|
|
173
|
+
try{const before=await handle.stat();if(!before.isFile()||before.size>1024*1024)throw Error('Invalid plan file');const bytes=await handle.readFile(),after=await handle.stat(),named=await fs.lstat(filename);if(before.dev!==after.dev||before.ino!==after.ino||before.size!==after.size||before.mtimeMs!==after.mtimeMs||after.dev!==named.dev||after.ino!==named.ino||after.mode!==named.mode)throw Error('Plan file changed');await inspectPath(filename);return {bytes,mode:before.mode&0o111?'100755':'100644'};}finally{await handle.close();}
|
|
174
|
+
}
|
|
175
|
+
// This value is used only as input to an opaque fingerprint and is never
|
|
176
|
+
// persisted or returned. URL credentials and fragment are discarded; a query
|
|
177
|
+
// can affect routing, so its presence requires operator investigation.
|
|
178
|
+
function sanitizedFetchUrl(value){
|
|
179
|
+
if(typeof value!=='string'||!value||/[\x00-\x1f\x7f]/.test(value))throw Error('Invalid remote identity');
|
|
180
|
+
if(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(value)){
|
|
181
|
+
let parsed;try{parsed=new URL(value);}catch{throw Error('Invalid remote identity');}
|
|
182
|
+
if(!['http:','https:','ssh:','git:','file:'].includes(parsed.protocol)||parsed.protocol!=='file:'&&!parsed.hostname||parsed.search)throw Error('Invalid remote identity');
|
|
183
|
+
parsed.username='';parsed.password='';parsed.hash='';return parsed.toString();
|
|
184
|
+
}
|
|
185
|
+
if(!/^[A-Za-z]:[\\/]/.test(value)){
|
|
186
|
+
const scp=/^(?:[^@/:\s]+@)?(\[[0-9A-Fa-f:.]+\]|[^@/:\s]+):(.+)$/.exec(value);
|
|
187
|
+
if(scp)return `scp:${scp[1].toLowerCase()}:${scp[2]}`;
|
|
188
|
+
}
|
|
189
|
+
return `path:${value}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Local, read-only plan-amend observation. Git arguments and object IDs are
|
|
193
|
+
* checked before transport; operator snapshots never enter the Git runner. */
|
|
194
|
+
export async function readPlanAmendGitEvidence({target,arguments:a,mapping=[],gitRunner=defaultRunner}={}){
|
|
195
|
+
if(!target||!a?.plan||!a?.history||!a?.recovery||!gitPath(a.plan.gitPath)||a.plan.path!==path.join(target.realpath,...a.plan.gitPath.split('/'))||![a.history.base,a.history.planCommit,a.history.head].every(oid)||!/^refs\/arc\/recovery\/[A-Za-z0-9._/-]+$/.test(a.recovery.ref)||a.recovery.ref.includes('..')||a.recovery.ref.includes('//')||!Array.isArray(mapping)||mapping.length>256||mapping.some(m=>!m||!oid(m.old)||!oid(m.new)))throw Error('Invalid plan-amend observation request');
|
|
196
|
+
const run=async args=>{const r=await gitRunner({args,cwd:target.realpath});if(!r||r.status!==0||r.error||!(Buffer.isBuffer(r.stdout)||typeof r.stdout==='string'))throw Error('Plan-amend Git query failed: '+args[0]);return Buffer.from(r.stdout);};
|
|
197
|
+
const line=async args=>{const v=text(await run(args));if(!v.endsWith('\n')||v.slice(0,-1).includes('\n'))throw Error('Invalid Git scalar');return v.slice(0,-1);};
|
|
198
|
+
const before=await readGitEvidence({target,gitRunner});
|
|
199
|
+
const headTree=await line(['rev-parse','--verify',`${target.head}^{tree}`]);if(!oid(headTree))throw Error('Invalid HEAD tree');
|
|
200
|
+
const observeRemotes=async()=>{
|
|
201
|
+
const remoteOutput=text(await run(['remote']));if(remoteOutput&&!remoteOutput.endsWith('\n'))throw Error('Incomplete remote list');
|
|
202
|
+
const names=remoteOutput?remoteOutput.slice(0,-1).split('\n'):[];if(!unique(names)||names.some(name=>!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name)))throw Error('Invalid configured remote');
|
|
203
|
+
const remotes=[];for(const name of names.sort()){
|
|
204
|
+
const output=text(await run(['remote','get-url','--all',name]));if(!output||!output.endsWith('\n'))throw Error('Invalid remote identity');
|
|
205
|
+
const urls=output.slice(0,-1).split('\n').map(sanitizedFetchUrl),identities=[...new Set(urls)].sort();if(!identities.length)throw Error('Invalid remote identity');
|
|
206
|
+
remotes.push({name,identity:hash(Buffer.from(JSON.stringify(identities)))});
|
|
207
|
+
}return remotes;
|
|
208
|
+
};
|
|
209
|
+
const observeRecoveryRef=async()=>{
|
|
210
|
+
const result=await gitRunner({args:['show-ref','--verify','--quiet',a.recovery.ref],cwd:target.realpath});
|
|
211
|
+
if(result?.status===0&&!result.error){const value=await line(['rev-parse','--verify',a.recovery.ref]);if(!oid(value))throw Error('Invalid recovery ref');return {name:a.recovery.ref,oid:value};}
|
|
212
|
+
if(result?.status!==1||result.error)throw Error('Recovery ref observation failed');return null;
|
|
213
|
+
};
|
|
214
|
+
const configuredRemotes=await observeRemotes();
|
|
215
|
+
const list=text(await run(['rev-list','--reverse','--topo-order',`${a.history.base}..${a.history.head}`]));
|
|
216
|
+
if(!list.endsWith('\n'))throw Error('Empty or incomplete history');const ids=list.slice(0,-1).split('\n');if(ids.length>256||!ids.every(oid)||!unique(ids))throw Error('Invalid history range');
|
|
217
|
+
const commit=async id=>{
|
|
218
|
+
const raw=await run(['cat-file','commit',id]),separator=raw.indexOf(Buffer.from('\n\n'));if(separator<0)throw Error('Malformed commit');
|
|
219
|
+
const headers=[];for(const header of text(raw.subarray(0,separator)).split('\n')){
|
|
220
|
+
if(header.startsWith(' ')){const previous=headers.at(-1);if(!previous||['tree','parent','committer'].includes(previous.name))throw Error('Malformed commit header');previous.raw+='\n'+header;continue;}
|
|
221
|
+
const parsed=/^([^\s]+) (.*)$/.exec(header);if(!parsed)throw Error('Malformed commit header');headers.push({name:parsed[1],raw:header,value:parsed[2]});
|
|
222
|
+
}
|
|
223
|
+
const fields=name=>headers.filter(header=>header.name===name),trees=fields('tree'),parents=fields('parent'),authors=fields('author'),committers=fields('committer');
|
|
224
|
+
const match=/^author (.*) (\d+) ([+-]\d{4})$/.exec(authors[0]?.raw??'');if(trees.length!==1||!oid(trees[0].value)||parents.length!==1||!oid(parents[0].value)||authors.length!==1||committers.length!==1||!match)throw Error('Expected linear commit metadata');
|
|
225
|
+
const headerTemplate=headers.map(header=>['tree','parent','committer'].includes(header.name)?header.name:header.raw);
|
|
226
|
+
const bytes=await run(['ls-tree','-r','-z','--full-tree',id]);const entries=text(bytes);if(entries&&!entries.endsWith('\0'))throw Error('Incomplete tree');
|
|
227
|
+
const treeEntries=(entries?entries.slice(0,-1).split('\0'):[]).map(row=>{const m=/^(100644|100755|120000|160000) (blob|commit) ([a-f0-9]+)\t([\s\S]+)$/.exec(row);if(!m||!oid(m[3])||!gitPath(m[4]))throw Error('Invalid tree entry');return {path:m[4],mode:m[1],oid:m[3]};});
|
|
228
|
+
if(!unique(treeEntries.map(x=>x.path)))throw Error('Duplicate tree path');
|
|
229
|
+
return {oid:id,parent:parents[0].value,author:match[1],authorDate:match[2]+' '+match[3],message:text(raw.subarray(separator+2)),tree:treeEntries,headerTemplate};
|
|
230
|
+
};
|
|
231
|
+
const commits=[];for(const id of ids)commits.push(await commit(id));
|
|
232
|
+
const newCommits=[];for(const item of mapping)newCommits.push(await commit(item.new));
|
|
233
|
+
const original=await run(['show',`${a.history.planCommit}:${a.plan.gitPath}`]);
|
|
234
|
+
const current=await readPlanBytes(a.plan.path);
|
|
235
|
+
const firstFingerprint=await preservationFingerprint(target.realpath,a.plan.gitPath);
|
|
236
|
+
const recoveryRef=await observeRecoveryRef();
|
|
237
|
+
const after=await readGitEvidence({target,gitRunner}),secondFingerprint=await preservationFingerprint(target.realpath,a.plan.gitPath);
|
|
238
|
+
const latest=await readPlanBytes(a.plan.path),finalRemotes=await observeRemotes(),finalRecoveryRef=await observeRecoveryRef();
|
|
239
|
+
if(!equal(before,after)||firstFingerprint!==secondFingerprint||!latest.bytes.equals(current.bytes)||latest.mode!==current.mode||!equal(configuredRemotes,finalRemotes)||!equal(recoveryRef,finalRecoveryRef))throw Error('Git or worktree changed during observation');
|
|
240
|
+
return {target,index:{tree:before.indexTree,headTree,stagedPaths:before.stagedPaths},worktree:{planClean:!before.workingPaths.includes(a.plan.gitPath)&&!before.untrackedPaths.includes(a.plan.gitPath),nonPlanFingerprint:firstFingerprint},configuredRemotes,history:{base:a.history.base,planCommit:a.history.planCommit,head:a.history.head,commits,newCommits},plan:{path:a.plan.path,content:text(current.bytes),hash:hash(current.bytes),originalHash:hash(original),mode:current.mode},recoveryRef};
|
|
241
|
+
}
|
|
242
|
+
|
|
145
243
|
const validGitPaths=values=>Array.isArray(values)&&values.every(gitPath)&&unique(values);
|
|
146
244
|
const within=(base,file)=>{const relative=path.relative(base,file);return relative===''||!path.isAbsolute(relative)&&relative!=='..'&&!relative.startsWith('..'+path.sep);};
|
|
147
245
|
/** Provenance-neutral Git checks shared by trusted and cooperative callers.
|
|
@@ -2,5 +2,5 @@
|
|
|
2
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
|
-
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'],implementer:['read','write','check'],qa:['read','check','temporary-write'],reviewer:['read','review']});
|
|
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','plan-amend','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','plan-amend','install',...GIT_OPERATIONS,...EXTERNAL_OPERATIONS],scouter:['read'],planner:['read','draft'],implementer:['read','write','check'],qa:['read','check','temporary-write'],reviewer:['read','review']});
|