@yunhozz/arc 0.2.4 → 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 +88 -78
- 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 +9 -9
- package/claude/skill/models.toml +11 -11
- package/claude/skill/references/arc-visual-companion.md +3 -3
- package/claude/skill/references/brainstorming.md +153 -62
- package/claude/skill/references/commit.md +6 -6
- package/claude/skill/references/coordination.md +20 -8
- 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 +22 -16
- 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/claude/skill/scripts/model_config.py +17 -5
- 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 +9 -9
- package/codex/skill/models.toml +13 -13
- package/codex/skill/references/arc-visual-companion.md +3 -3
- package/codex/skill/references/brainstorming.md +153 -62
- package/codex/skill/references/commit.md +6 -6
- package/codex/skill/references/coordination.md +20 -8
- 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 +23 -17
- 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/codex/skill/scripts/model_config.py +25 -6
- package/lib/installer/global-config.mjs +1 -1
- package/lib/installer/manifest.mjs +6 -2
- package/lib/installer/plan.mjs +8 -5
- package/lib/installer/preset.mjs +7 -4
- package/lib/installer/runtime-assets.mjs +1 -0
- package/lib/management/operations.mjs +1 -1
- package/lib/models/catalog-anthropic.mjs +46 -0
- package/lib/models/catalog.mjs +62 -0
- package/lib/models/codex-catalog.mjs +103 -0
- package/lib/models/config.mjs +19 -11
- package/lib/models/index.mjs +1 -1
- 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/app.mjs +15 -9
- package/lib/tui/custom-preset.mjs +122 -0
- package/lib/tui/render.mjs +3 -2
- package/lib/tui/setup.mjs +65 -8
- package/package.json +1 -1
|
@@ -114,6 +114,13 @@ umask 077
|
|
|
114
114
|
SESSION_ID="$$-$(date +%s)"
|
|
115
115
|
|
|
116
116
|
if [[ -n "$PROJECT_DIR" ]]; then
|
|
117
|
+
# Make the project dir absolute now: the script cds into its own directory
|
|
118
|
+
# before starting the server, which would re-root a relative path there.
|
|
119
|
+
# mkdir -p keeps the existing behavior of creating a missing project dir.
|
|
120
|
+
if ! PROJECT_DIR="$(mkdir -p -- "$PROJECT_DIR" && CDPATH='' cd -- "$PROJECT_DIR" && pwd)"; then
|
|
121
|
+
echo "{\"error\": \"Cannot create or enter --project-dir\"}"
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
117
124
|
SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
|
|
118
125
|
# Persist the bound port and key per project so a restart reuses them and an
|
|
119
126
|
# already-open browser tab reconnects to the same URL with a valid cookie.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"repository": "https://github.com/obra/superpowers",
|
|
4
|
-
"revision": "
|
|
5
|
-
"fetchedAt": "2026-
|
|
4
|
+
"revision": "22f0b463e7a5e579904dd6211a520321035526f7",
|
|
5
|
+
"fetchedAt": "2026-11-06T16:48:59.704Z",
|
|
6
6
|
"skills": [
|
|
7
7
|
"brainstorming",
|
|
8
8
|
"writing-plans",
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
],
|
|
11
11
|
"files": {
|
|
12
12
|
"LICENSE": "a37e0e9697144819e1d965176ac4ae5bc3fa02d11e7812036bbcadf6dafe2400",
|
|
13
|
-
"brainstorming/SKILL.md": "
|
|
13
|
+
"brainstorming/SKILL.md": "2c9ca71f94886f9ca9f4aef055f7cafb3e1a9bdf3aeebf8f4cb547c69405d207",
|
|
14
14
|
"brainstorming/builder-check-prompt.md": "36d51a4723d6f0a3ce5811a0ea3b3a8424f07533947e1355f5d8cb8cce40c55e",
|
|
15
15
|
"brainstorming/scripts/frame-template.html": "6a8a4e58bd6a44b904e2e3c57de774481d909204597e1498de53f1b2fecc4c4e",
|
|
16
16
|
"brainstorming/scripts/helper.js": "43c6d69954a46ec34a2a262bcc62a9a7e83e839c739f199cb72646d397c686e3",
|
|
17
17
|
"brainstorming/scripts/server.cjs": "763abf96510e0b892a1141397b31aa82ef900cf87ed34dde94bc8782baf4a7ca",
|
|
18
|
-
"brainstorming/scripts/start-server.sh": "
|
|
18
|
+
"brainstorming/scripts/start-server.sh": "314dd68dc00e8dd94c7491aca8a5f004c7fddffc161e2b07482f29f48d13659a",
|
|
19
19
|
"brainstorming/scripts/stop-server.sh": "0b5ccbbd57f62d3ed88993f7940b5ee0e5c0fc9b21c550c623da4f6292e47daf",
|
|
20
20
|
"brainstorming/visual-companion.md": "6ffcb038b53254bd31debaa15e12dcd4ff24bd63d690e0b644ac6396701cf2e2",
|
|
21
21
|
"writing-plans/SKILL.md": "a6c67c1900064347c2a329990dd3c555657c51c3ec53b259a08aa01a2c26139a",
|
|
@@ -10,14 +10,16 @@ Read before verification design, QA/Reviewer assignment, correction rechecks or
|
|
|
10
10
|
<a id="visual-evidence-and-adjudication"></a>
|
|
11
11
|
## Visual evidence and adjudication
|
|
12
12
|
|
|
13
|
-
Each criterion needs exact approved source
|
|
13
|
+
Each criterion needs the exact approved source, version, approval and access state; IDs, properties and comparison method; and a link between output and revision. Implementer reporting does not replace fresh QA. Record pass/mismatch/unverified and connect to criterion verdicts; pass needs accessible supporting evidence. Missing linkage is unverified; visible difference is mismatch, separately, without inferring current branch UI. Hold comparison when materials are inaccessible or interpretation approval is missing.
|
|
14
14
|
|
|
15
|
-
Policy
|
|
15
|
+
Policy, generated-file and manifest checks prove structure, not role behavior, installed builds, Claude Code CLI compliance or fidelity. Report changes, exclusions, checks run and omitted, criterion evidence and limits, and platform availability. Report baseline failures separately; independent progress does not relax checks.
|
|
16
16
|
|
|
17
17
|
<a id="risk-scaled-execution"></a>
|
|
18
18
|
## Risk-scaled execution
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
The [fast gate](handoff.md#orchestrator-direct-path-for-small-tasks) references this risk definition; refresh affected evidence when inputs change and preserve independent verification boundaries.
|
|
21
|
+
|
|
22
|
+
Edits belong to Implementer except the [small direct path](handoff.md#orchestrator-direct-path-for-small-tasks). Orchestrator determines behavior, risk and minimum meaningful checks.
|
|
21
23
|
|
|
22
24
|
| Risk | Minimum flow |
|
|
23
25
|
|---|---|
|
|
@@ -36,7 +38,7 @@ Ordinary/significant tasks need fresh QA before acceptance. Low risk requires na
|
|
|
36
38
|
|
|
37
39
|
Record which risk/instruction/approved obligations each check satisfies. Do not automatically assign QA/Reviewer for clear low-risk scope with sufficient evidence and no requirement. Metadata counts alone prove no significance; one permission/loss rule may. Vague file deletion is not low-risk evidence; reclassify installer/manifest impacts. Confirm cleanup targets unused, required materials intact, ignore patterns scoped and diff approved before focused-check-only acceptance.
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
Distinguish environment failures from code failures and report exact limits and gaps. Use Planner/Scouter only for unresolved planning/discovery. Reviewer covers substantial correctness/factual/security/data/interface risks. Orchestrator assigns support as needed. Claim only verified outputs. Read cited `references/test-driven-development.md`, preserving exceptions.
|
|
40
42
|
|
|
41
43
|
<a id="final-plan-reviewer-correction-loop"></a>
|
|
42
44
|
### Final plan Reviewer correction loop
|
|
@@ -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});
|
|
@@ -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
|
}
|