@yunhozz/arc 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/MANIFEST.sha256 +107 -88
  2. package/README.md +4 -6
  3. package/claude/agents/arc-implementer.md +32 -42
  4. package/claude/agents/arc-planner.md +33 -47
  5. package/claude/agents/arc-qa.md +28 -44
  6. package/claude/agents/arc-reviewer.md +28 -41
  7. package/claude/agents/arc-scouter.md +26 -38
  8. package/claude/skill/SKILL.md +41 -59
  9. package/claude/skill/references/arc-visual-companion.md +28 -0
  10. package/claude/skill/references/commit.md +38 -70
  11. package/claude/skill/references/coordination.md +77 -126
  12. package/claude/skill/references/git-recovery.md +17 -35
  13. package/claude/skill/references/handoff-prep.md +91 -103
  14. package/claude/skill/references/handoff.md +110 -208
  15. package/claude/skill/references/limits.md +19 -31
  16. package/claude/skill/references/planning.md +93 -244
  17. package/claude/skill/references/verification.md +105 -261
  18. package/claude/skill/references/visual-companion.md +293 -15
  19. package/claude/skill/runtime/adapters/claude.mjs +10 -9
  20. package/claude/skill/runtime/adapters/codex.mjs +7 -8
  21. package/claude/skill/runtime/bootstrap.mjs +3 -2
  22. package/claude/skill/runtime/capabilities.mjs +12 -0
  23. package/claude/skill/runtime/cooperative-schema.mjs +3 -2
  24. package/claude/skill/runtime/cooperative.mjs +52 -4
  25. package/claude/skill/runtime/evaluate.mjs +115 -2
  26. package/claude/skill/runtime/identity.mjs +22 -0
  27. package/claude/skill/runtime/role-policy.json +18 -18
  28. package/claude/skill/runtime/schema.mjs +19 -0
  29. package/claude/skill/scripts/model_config.py +3 -79
  30. package/codex/agents/arc-implementer.toml +34 -30
  31. package/codex/agents/arc-planner.toml +27 -23
  32. package/codex/agents/arc-qa.toml +24 -15
  33. package/codex/agents/arc-reviewer.toml +25 -16
  34. package/codex/agents/arc-scouter.toml +20 -26
  35. package/codex/skill/SKILL.md +42 -57
  36. package/codex/skill/references/arc-visual-companion.md +33 -0
  37. package/codex/skill/references/commit.md +35 -81
  38. package/codex/skill/references/coordination.md +76 -104
  39. package/codex/skill/references/git-recovery.md +29 -51
  40. package/codex/skill/references/handoff-prep.md +76 -134
  41. package/codex/skill/references/handoff-worktree.md +32 -66
  42. package/codex/skill/references/handoff.md +111 -271
  43. package/codex/skill/references/limits.md +21 -34
  44. package/codex/skill/references/planning.md +125 -299
  45. package/codex/skill/references/verification.md +90 -272
  46. package/codex/skill/references/visual-companion.md +290 -18
  47. package/codex/skill/runtime/adapters/claude.mjs +10 -9
  48. package/codex/skill/runtime/adapters/codex.mjs +7 -8
  49. package/codex/skill/runtime/bootstrap.mjs +3 -2
  50. package/codex/skill/runtime/capabilities.mjs +12 -0
  51. package/codex/skill/runtime/cooperative-schema.mjs +3 -2
  52. package/codex/skill/runtime/cooperative.mjs +52 -4
  53. package/codex/skill/runtime/evaluate.mjs +115 -2
  54. package/codex/skill/runtime/identity.mjs +22 -0
  55. package/codex/skill/runtime/role-policy.json +18 -18
  56. package/codex/skill/runtime/schema.mjs +19 -0
  57. package/lib/installer/global-config.mjs +1 -0
  58. package/lib/installer/install.mjs +5 -7
  59. package/lib/installer/manifest.mjs +23 -8
  60. package/lib/installer/package-references.mjs +10 -5
  61. package/lib/installer/plan.mjs +1 -4
  62. package/lib/installer/superpowers.mjs +26 -1
  63. package/lib/installer/transaction.mjs +3 -3
  64. package/lib/installer/uninstall.mjs +2 -2
  65. package/lib/installer/upgrade.mjs +2 -9
  66. package/lib/management/operations.mjs +6 -8
  67. package/lib/management/update.mjs +3 -3
  68. package/lib/models/profiles.mjs +1 -40
  69. package/lib/policy/render.mjs +1 -5
  70. package/lib/policy/role-policy.json +18 -18
  71. package/lib/release/artifact.mjs +6 -4
  72. package/lib/runtime/adapters/claude.mjs +10 -9
  73. package/lib/runtime/adapters/codex.mjs +7 -8
  74. package/lib/runtime/bootstrap.mjs +3 -2
  75. package/lib/runtime/capabilities.mjs +12 -0
  76. package/lib/runtime/cooperative-schema.mjs +3 -2
  77. package/lib/runtime/cooperative.mjs +52 -4
  78. package/lib/runtime/evaluate.mjs +115 -2
  79. package/lib/runtime/identity.mjs +22 -0
  80. package/lib/runtime/schema.mjs +19 -0
  81. package/lib/tui/app.mjs +12 -8
  82. package/lib/tui/setup.mjs +5 -8
  83. package/package.json +11 -2
  84. package/scripts/update-manifest.mjs +7 -9
  85. package/claude/skill/hooks/git-policy.json +0 -208
  86. package/claude/skill/hooks/role-git-guard.mjs +0 -18
  87. package/claude/skill/hooks/runtime-gate.mjs +0 -12
  88. package/claude/skill/references/superpowers/brainstorming/visual-companion.md +0 -299
  89. package/codex/skill/hooks/runtime-gate.mjs +0 -10
  90. package/codex/skill/references/superpowers/brainstorming/visual-companion.md +0 -299
  91. package/lib/policy/claude-hook.mjs +0 -80
  92. package/scripts/probe-runtime-hooks.mjs +0 -55
  93. package/scripts/update-superpowers-local.sh +0 -4
  94. /package/claude/skill/references/{superpowers/brainstorming/SKILL.md → brainstorming.md} +0 -0
  95. /package/claude/skill/references/{superpowers/brainstorming/builder-check-prompt.md → builder-check-prompt.md} +0 -0
  96. /package/claude/skill/references/{superpowers/brainstorming/scripts → scripts}/frame-template.html +0 -0
  97. /package/claude/skill/references/{superpowers/brainstorming/scripts → scripts}/helper.js +0 -0
  98. /package/claude/skill/references/{superpowers/brainstorming/scripts → scripts}/server.cjs +0 -0
  99. /package/claude/skill/references/{superpowers/brainstorming/scripts → scripts}/start-server.sh +0 -0
  100. /package/claude/skill/references/{superpowers/brainstorming/scripts → scripts}/stop-server.sh +0 -0
  101. /package/claude/skill/references/{superpowers/LICENSE → superpowers-LICENSE} +0 -0
  102. /package/claude/skill/references/{superpowers/provenance.json → superpowers-provenance.json} +0 -0
  103. /package/claude/skill/references/{superpowers/test-driven-development/SKILL.md → test-driven-development.md} +0 -0
  104. /package/claude/skill/references/{superpowers/test-driven-development/writing-good-tests.md → writing-good-tests.md} +0 -0
  105. /package/claude/skill/references/{superpowers/writing-plans/SKILL.md → writing-plans.md} +0 -0
  106. /package/codex/skill/references/{superpowers/brainstorming/SKILL.md → brainstorming.md} +0 -0
  107. /package/codex/skill/references/{superpowers/brainstorming/builder-check-prompt.md → builder-check-prompt.md} +0 -0
  108. /package/codex/skill/references/{superpowers/brainstorming/scripts → scripts}/frame-template.html +0 -0
  109. /package/codex/skill/references/{superpowers/brainstorming/scripts → scripts}/helper.js +0 -0
  110. /package/codex/skill/references/{superpowers/brainstorming/scripts → scripts}/server.cjs +0 -0
  111. /package/codex/skill/references/{superpowers/brainstorming/scripts → scripts}/start-server.sh +0 -0
  112. /package/codex/skill/references/{superpowers/brainstorming/scripts → scripts}/stop-server.sh +0 -0
  113. /package/codex/skill/references/{superpowers/LICENSE → superpowers-LICENSE} +0 -0
  114. /package/codex/skill/references/{superpowers/provenance.json → superpowers-provenance.json} +0 -0
  115. /package/codex/skill/references/{superpowers/test-driven-development/SKILL.md → test-driven-development.md} +0 -0
  116. /package/codex/skill/references/{superpowers/test-driven-development/writing-good-tests.md → writing-good-tests.md} +0 -0
  117. /package/codex/skill/references/{superpowers/writing-plans/SKILL.md → writing-plans.md} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {evaluate} from '../evaluate.mjs';
2
- import {evaluateCapability} from '../capabilities.mjs';
3
- import {registerAgentObservation} from '../identity.mjs';
2
+ import {evaluateCapability,plannerHookContextStatus} from '../capabilities.mjs';
3
+ import {recordPlannerAgentObservation} from '../identity.mjs';
4
4
  import {transactWorkflow, fingerprintRequest} from '../store.mjs';
5
5
  import {runGate} from '../gate.mjs';
6
6
 
@@ -12,6 +12,7 @@ const deny=reason=>({decision:'deny',reason,ruleIds:['runtime.codex'],proposedTr
12
12
  * describeTool must parse the complete tool invocation conservatively, and supply
13
13
  * semantic operation, paths and descriptor. No native provider is assumed here. */
14
14
  export function normalizeCodexEvent(input,context={}){
15
+ const status=plannerHookContextStatus({platform:'codex',input,context});if(status.missing.length)throw Error('Missing trusted hook inputs: '+status.missing.join(', '));
15
16
  if(!object(input)||input.hook_event_name!=='PreToolUse'||!text(input.session_id)||input.session_id!==context.sessionRef||!text(context.actorRef)||!text(context.taskId)||!text(context.requestId)||!text(input.tool_use_id)||input.tool_use_id!==context.toolUseId||!text(input.tool_name)||!object(input.tool_input)||!text(input.cwd))throw Error('Verified session identity and complete PreToolUse event required');
16
17
  const toolPath=['Agent','spawn_agent'].includes(input.tool_name)?'spawn_agent':input.tool_name;
17
18
  const coverage={...evaluateCapability({platform:'codex',toolPath,sessionRef:context.sessionRef}),enforcement:'hook-response-only',nativeVerified:false,startupTimeout:'unknown',existingSessionInput:'unsupported',specializedPaths:'unknown',hostedTools:'unsupported'};
@@ -34,12 +35,10 @@ export async function runCodexGate(input,context={}){
34
35
  // SubagentStart cannot prevent startup. Only a trusted provider can correlate
35
36
  // agent_id with a pending assignment; tool/event agent_type is not authority.
36
37
  try{
37
- if(input.session_id===context.sessionRef&&text(input.agent_id)&&typeof context.observeAgent==='function'){
38
- const observation=await context.observeAgent(structuredClone(input));
39
- if(observation?.actorRef===input.agent_id&&observation.platform==='codex'){
40
- const decision=registerAgentObservation(context.state,observation);
41
- if(decision.decision==='allow'&&decision.proposedTransition&&typeof context.persistObservation==='function')await context.persistObservation(decision,observation);
42
- }
38
+ if(input.session_id===context.sessionRef&&text(input.agent_id)&&text(context.statePath)){
39
+ let observation;try{if(typeof context.observeAgent==='function')observation=await context.observeAgent(structuredClone(input));}catch{}
40
+ const decision=await recordPlannerAgentObservation({statePath:context.statePath,expectedRevision:context.state?.revision,input,platform:'codex',sessionRef:context.sessionRef,observation});
41
+ if(decision.decision==='allow'&&decision.proposedTransition&&typeof context.persistObservation==='function')await context.persistObservation(decision,observation);
43
42
  }
44
43
  }catch{/* Observation failure never claims startup was blocked. */}
45
44
  return {};
@@ -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'
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'
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-1',policyHash:hash(policy),runtimeHash:hash(Buffer.concat(runtime))};
19
+ return {policy:'1',runtime:'cooperative-4',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});
@@ -28,6 +28,7 @@ export async function initializeCooperativeWorkflow({cwd,workflowId,input,observ
28
28
  lock=await fs.open(lockPath,'wx',0o600);await inspectPath(statePath,{missing:true});
29
29
  const state={schemaVersion:1,workflowKind:'cooperative',contractVersion:1,workflowId,revision:0,mode:'plan',coverage:cooperativeCoverage(),versions:await cooperativeVersions(),target,targetVersion:1,scopeVersion:0};
30
30
  for(const key of ['actors','selections','assignments','identities','leases','scopes','approvals','documents','tasks','requests','intents','results','checks','findings','acceptances','commits','audit','unresolved','records','targetHistory'])state[key]=[];
31
+ state.plannerLedger={version:1,stages:[]};
31
32
  state.actors.push({actor:input.actor,source:'operator-attested'});state.selections.push({...input.selection,actor:input.actor,source:'operator-attested',targetVersion:1});
32
33
  state.audit.push({command:'init',revision:0,source:'operator-attested',actor:input.actor});validateCooperativeState(state);
33
34
  await saveDurableState({statePath,state,create:true,storage});return commandResult({workflowId,state,input,persisted:true,ruleId:'cooperative.initialized'});
@@ -28,5 +28,17 @@ export function evaluateCapability({platform,toolPath,sessionRef,platformVersion
28
28
  declared:{source:'documentation',matcher:contracts[platform]?.[toolPath] ?? null},
29
29
  status:!known || observationOnly ? 'unsupported':'unknown',verifiedDeny:false,
30
30
  source:!known || observationOnly ? 'documentation':'unverified',
31
+ observation:observationOnly?'documented':toolPath==='followup_task'?'unknown':'not-verified',enforcement:observationOnly?'observation-only':'unverified',
31
32
  observedPaths:observed.toolPaths.filter(p=>p === toolPath)};
32
33
  }
34
+
35
+ /** Raw event fields identify an invocation, not its approved role or semantics. */
36
+ export function plannerHookContextStatus({platform,input,context={}}={}){
37
+ const text=v=>typeof v==='string'&&v.length>0,object=v=>v!==null&&typeof v==='object'&&!Array.isArray(v);
38
+ const fields={
39
+ 'hook.hook_event_name':input?.hook_event_name==='PreToolUse','hook.session_id':text(input?.session_id),'hook.tool_use_id':text(input?.tool_use_id),'hook.tool_name':text(input?.tool_name),'hook.tool_input':object(input?.tool_input),'hook.cwd':text(input?.cwd),
40
+ 'context.sessionRef':text(context.sessionRef),'context.actorRef':text(context.actorRef),'context.taskId':text(context.taskId),'context.requestId':text(context.requestId),'context.toolUseId':text(context.toolUseId),'context.statePath':text(context.statePath),'context.state':object(context.state),'context.policy':object(context.policy),'context.evidence':object(context.evidence),'context.describeTool':typeof context.describeTool==='function',
41
+ };
42
+ const missing=Object.keys(fields).filter(k=>!fields[k]);
43
+ return {platform,available:Object.keys(fields).filter(k=>fields[k]),missing,enforcement:missing.length?'unavailable':'hook-response-only',nativeVerified:false,source:'context-validation'};
44
+ }
@@ -1,6 +1,6 @@
1
1
  import {ROLES} from './operations.mjs';
2
2
  import {assertSecretFree} from './external.mjs';
3
- import {object,text} from './schema.mjs';
3
+ import {object,text,validatePlannerLedger} from './schema.mjs';
4
4
  import {absolute,validateWorkflowId} from './scope.mjs';
5
5
  const fail=message=>{throw Error('Cooperative schema: '+message);};
6
6
  const hash=value=>typeof value==='string'&&/^sha256:[a-f0-9]{64}$/.test(value);
@@ -25,7 +25,7 @@ export function validateCooperativeCommand(command){
25
25
  if(name==='record'){
26
26
  fields(input.evidence,['kind','messageRef','data']);
27
27
  if(input.evidence?.data?.affectedTaskIds!==undefined&&(!Array.isArray(input.evidence.data.affectedTaskIds)||!input.evidence.data.affectedTaskIds.every(text)||new Set(input.evidence.data.affectedTaskIds).size!==input.evidence.data.affectedTaskIds.length))fail('affected task IDs');
28
- if(!statement(input.evidence)||!['note','approval','external-approval','document','scope','check','finding','review','acceptance-evidence'].includes(input.evidence.kind))fail('evidence');
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');
29
29
  }
30
30
  if(resultStatement){
31
31
  fields(input.result,['status','messageRef','data','notExecutedEvidence']);
@@ -51,5 +51,6 @@ export function validateCooperativeState(state){
51
51
  if(state.requests.some(r=>!text(r.requestId)||!hash(r.requestFingerprint)||!object(r.decision))||new Set(state.requests.map(r=>r.requestId)).size!==state.requests.length)fail('request replay');
52
52
  if(state.intents.some(i=>!text(i.requestId)||!['prepared','succeeded','failed','unknown','mismatch','cancelled'].includes(i.status)||!hash(i.fingerprint))||new Set(state.intents.map(i=>i.requestId)).size!==state.intents.length)fail('intents');
53
53
  if(state.results.some(r=>!text(r.requestId)||!['succeeded','failed','unknown','mismatch','cancelled'].includes(r.status)||r.source!=='operator-attested'||!object(r.data)))fail('results');
54
+ 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');}
54
55
  assertSecretFree(state);json(state);return state;
55
56
  }
@@ -4,6 +4,9 @@ import {evaluateCooperativeVerification,invalidateAffectedEvidence,readCooperati
4
4
  import {readAssignmentGitEvidence,readCooperativeCommitGitEvidence} from './git-evidence.mjs';
5
5
  import {recordCooperativeAssignmentResult} from './assignment.mjs';
6
6
  import fs from 'node:fs/promises';
7
+ import {constants} from 'node:fs';
8
+ import path from 'node:path';
9
+ import {createHash} from 'node:crypto';
7
10
  import {validateCooperativeCommand,validateCooperativeState} from './cooperative-schema.mjs';
8
11
  import {observeCooperativeTarget,validateObservedTarget,validateWorkflowId,workflowStatePath,inspectPath,absolute,within} from './scope.mjs';
9
12
  import {fingerprintRequest,saveDurableState,applyCooperativeCommitTransition} from './store.mjs';
@@ -23,12 +26,14 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
23
26
  if(!sameCheckout(state.target,target))return result(undefined,{decision:'hold',ruleIds:['cooperative.target']});
24
27
  const changes=state.tasks.length?await readChanges({state}):undefined;
25
28
  const evaluation=evaluateCooperative({state,request:{command,input},evidence:{target,versions:await cooperativeVersions(),changes}});
26
- const unresolved=state.unresolved.some(entry=>!entry.resolvedBy)||state.intents.some(i=>['prepared','unknown','mismatch'].includes(i.status)&&!(['check','review'].includes(i.operation)&&i.stale));
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));
27
30
  return {...result(state,evaluation.decision==='allow'&&unresolved?{decision:'hold',ruleIds:['cooperative.unresolved']}:evaluation,{persisted:true}),state,support:cooperativeSupport(),...(changes?{changeFingerprints:changes}:{})};
28
31
  }
29
- let lock;
32
+ let lock,plannerLock;
30
33
  try{
31
34
  lock=await fs.open(statePath+'.lock','wx',0o600);let state=await read();
35
+ const plannerWork=command==='prepare'&&input.operation==='planner-call'||command==='record'&&input.evidence.kind.startsWith('planner-');
36
+ if(plannerWork)plannerLock=await fs.open(path.join(target.gitCommonDir,'arc','planner.lock'),'wx',0o600);
32
37
  if(!sameCheckout(state.target,target))return result(state,{decision:'hold',ruleIds:['cooperative.target']});
33
38
  const requestFingerprint=fingerprintRequest({command,input});const prior=state.requests.find(r=>r.requestId===input.requestId);
34
39
  if(prior){if(prior.requestFingerprint!==requestFingerprint)throw Error('Request fingerprint conflict');return result(state,{decision:'hold',ruleIds:['cooperative.replay']},{replayed:true});}
@@ -45,9 +50,12 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
45
50
  try{git=await readCommitGit({target:currentTarget,...(commitIntent&&input.result?.data?.commit?{commit:input.result.data.commit}:{})});}catch(error){gitError=error.message;}
46
51
  }
47
52
  const changes=state.tasks.length?await readChanges({state}):undefined;
48
- let decision=evaluateCooperative({state,request:{command,input},evidence:{target:currentTarget,versions,git,changes}});
53
+ const plannerArtifacts=plannerWork?await readPlannerArtifacts(state,command==='record'&&input.evidence.kind==='planner-evidence'?input.evidence:undefined):undefined;
54
+ const plannerHistory=command==='record'&&input.evidence.kind==='planner-stage'?await readPlannerHistory(state,statePath,input.evidence.data.document?.path):{};
55
+ let decision=evaluateCooperative({state,request:{command,input},evidence:{target:currentTarget,versions,git,changes,requestFingerprint,plannerArtifacts,...plannerHistory}});
49
56
  if(gitError)decision={decision:'hold',ruleIds:[input.operation==='assign-agent'?'cooperative.documents':'cooperative.git-evidence'],reason:gitError};
50
57
  if(command==='prepare'&&decision.decision==='allow'){
58
+ if(decision.proposedTransition?.plannerLedger)state.plannerLedger=decision.proposedTransition.plannerLedger;
51
59
  if(decision.proposedTransition?.intent)state.intents.push(decision.proposedTransition.intent);
52
60
  if(decision.proposedTransition?.acceptance){state.acceptances.push(decision.proposedTransition.acceptance);state.tasks.find(t=>t.id===input.taskId).status='accepted';}
53
61
  }
@@ -100,6 +108,15 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
100
108
  if(decision.decision==='allow'&&actualResult.status==='succeeded'&&intent.operation==='assign-agent')state.assignments.push({intentId:intent.requestId,taskId:intent.taskId,agentId:actualResult.data.agentId,status:'active',source:'operator-attested',identityVerified:false});
101
109
  }
102
110
  else state.unresolved.push({kind:'unmatched-result',requestId:input.intentId,revision:state.revision+1});
111
+ }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;
114
+ 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
+ if(input.evidence.kind==='planner-result'){
116
+ const intent=state.intents.find(i=>i.operation==='planner-call'&&i.requestId===input.evidence.data.callRequestId);
117
+ if(intent)intent.status=input.evidence.data.status;
118
+ }
119
+ }
103
120
  }else if(command==='record'&&decision.decision==='allow'){
104
121
  const entry={...input.evidence,actor:input.actor,source:'operator-attested',requestId:input.requestId,revision:state.revision+1,scopeVersion:state.scopeVersion,targetVersion:state.targetVersion};
105
122
  if(entry.kind==='scope'){
@@ -132,5 +149,36 @@ export async function runCooperativeCommand({cwd,workflowId,command,input,observ
132
149
  state.requests.push({requestId:input.requestId,requestFingerprint,request:structuredClone({command,input}),decision:output});
133
150
  state.audit.push({command,requestId:input.requestId,revision:state.revision,decision:output.decision,ruleIds:output.ruleIds,source:'operator-attested',actor:input.actor,...(input.intentId?{intentId:input.intentId,resolution:state.results.at(-1)?.status}:{}),...(input.operation?{operation:input.operation}:{})});
134
151
  validateCooperativeState(state);await inspectPath(statePath);await saveDurableState({statePath,state,storage});return output;
135
- }finally{if(lock){await lock.close();await fs.unlink(statePath+'.lock');}}
152
+ }finally{if(plannerLock){await plannerLock.close();await fs.unlink(path.join(target.gitCommonDir,'arc','planner.lock'));}if(lock){await lock.close();await fs.unlink(statePath+'.lock');}}
153
+ }
154
+
155
+ async function readPlannerArtifacts(state,newRecord){
156
+ const entries=(state.plannerLedger?.stages??[]).flatMap(s=>(s.artifacts??[]).map(a=>({...a,stageId:s.id})));
157
+ if(newRecord)entries.push({...newRecord.data,ref:newRecord.messageRef});
158
+ return Promise.all(entries.map(async artifact=>{
159
+ try{
160
+ if(!absolute(artifact.path)||!within(state.target.realpath,artifact.path))throw Error('Artifact path escapes workspace');
161
+ await inspectPath(artifact.path);const handle=await fs.open(artifact.path,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);let bytes;
162
+ try{const before=await handle.stat();if(!before.isFile())throw Error('Artifact must be a regular file');bytes=await handle.readFile();const after=await handle.stat(),named=await fs.lstat(artifact.path);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)throw Error('Artifact changed while reading');}finally{await handle.close();}
163
+ await inspectPath(artifact.path);if(!within(state.target.realpath,await fs.realpath(artifact.path)))throw Error('Artifact realpath escapes workspace');
164
+ const hash='sha256:'+createHash('sha256').update(bytes).digest('hex');
165
+ if(artifact.hash&&artifact.hash!==hash||artifact.contentHash&&artifact.contentHash!==hash)throw Error('Artifact hash mismatch');
166
+ return {...artifact,hash,verification:{status:'verified',source:'local-bytes'}};
167
+ }catch(error){return {...artifact,verification:{status:'unverified',source:'local-bytes'},error:error.message};}
168
+ }));
169
+ }
170
+ async function readPlannerHistory(state,statePath,documentPath){
171
+ const plannerHistory=[];
172
+ try{
173
+ const root=path.dirname(path.dirname(statePath));await inspectPath(root,{directory:true});
174
+ for(const entry of await fs.readdir(root,{withFileTypes:true})){
175
+ if(entry.name===state.workflowId)continue;
176
+ if(!entry.isDirectory()||entry.isSymbolicLink())throw Error('Unsafe workflow history path');
177
+ const filename=path.join(root,entry.name,'state.json');await inspectPath(filename);const other=validateCooperativeState(JSON.parse(await fs.readFile(filename,'utf8')));
178
+ if(!sameCheckout(state.target,other.target))continue;
179
+ if(!other.plannerLedger&&other.documents.some(d=>d.path===documentPath||d.data?.path===documentPath))throw Error('Existing document history has no Planner ledger');
180
+ for(const stage of other.plannerLedger?.stages??[])plannerHistory.push({workflowId:other.workflowId,stage});
181
+ }
182
+ return {plannerHistory};
183
+ }catch(error){return {plannerHistory,plannerHistoryError:error.message};}
136
184
  }
@@ -15,7 +15,7 @@ import {prepareCooperativeAssignment} from './assignment.mjs';
15
15
  */
16
16
  import path from 'node:path';
17
17
  import {validateCooperativeState,validateCooperativeCommand} from './cooperative-schema.mjs';
18
- import {validateWorkflowState, validateRequest, validateEvidence} from './schema.mjs';
18
+ import {validateWorkflowState, validateRequest, validateEvidence,validatePlannerStage} from './schema.mjs';
19
19
  import {ROLES, ROLE_OPERATIONS, GIT_OPERATIONS, EXTERNAL_OPERATIONS} from './operations.mjs';
20
20
  const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
21
21
  const within=(base,file)=>{const relative=path.relative(base,file);return relative===''||(!relative.startsWith('..'+path.sep)&&relative!=='..'&&!path.isAbsolute(relative));};
@@ -82,7 +82,17 @@ export function evaluate(policy, state, request, evidence){
82
82
  if(d.checks&&(!Array.isArray(approval.scope.checks)||d.checks.some(c=>!approval.scope.checks.some(approved=>same(approved,c)))))return result('hold','runtime.assignment','delegated checks not approved',['delegated-checks']);
83
83
  if(d.temporary&&!same(d.temporary,approval.scope.temporary))return result('hold','runtime.assignment','delegated temporary scope not approved',['delegated-temporary-scope']);
84
84
  if(state.assignments.some(a=>a.assignmentId===request.requestId||a.correlationRef===d.correlationRef))return result('hold','runtime.identity','assignment correlation already registered',['new-assignment-correlation']);
85
- const next=structuredClone(state);next.assignments.push({assignmentId:request.requestId,requestId:request.requestId,requestedBy:request.actorRef,status:'pending',actorRef:null,platform:request.platform,correlationRef:d.correlationRef,role:d.role,taskId:d.taskId,paths:[...d.paths],operations:[...d.operations],approvalRef:d.approvalRef,...(d.checks?{checks:structuredClone(d.checks)}:{}),...(d.temporary?{temporary:structuredClone(d.temporary)}:{})});
85
+ const next=structuredClone(state);
86
+ if(d.role==='planner'){
87
+ const stage=state.plannerLedger?.stages.find(s=>s.id===d.plannerCall?.stageId),proof=fact('planner-call');
88
+ const contract=stage&&{document:stage.document,inputVersion:stage.inputVersion,criteriaVersion:stage.criteriaVersion,scopeVersion:stage.scopeVersion};
89
+ if(!stage||stage.workflowId!==state.workflowId||stage.approvalRef!==approval.id||!same(approval.scope.plannerStage,contract)||!d.paths.includes(stage.document.path)||proof?.sourceRef!==correlation.sourceRef||!same({stageId:proof?.data.stageId,inputVersion:proof?.data.inputVersion,criteriaVersion:proof?.data.criteriaVersion,scopeVersion:proof?.data.scopeVersion},{stageId:stage.id,inputVersion:stage.inputVersion,criteriaVersion:stage.criteriaVersion,scopeVersion:stage.scopeVersion}))return result('hold','runtime.planner-call','Current approved Planner stage and provider evidence required',['planner-stage-ledger','planner-call-evidence']);
90
+ if(stage.document.kind==='plan'&&!state.documents.some(doc=>doc.kind==='spec'&&state.approvals.some(a=>a.id===doc.approvalRef&&a.contentHash===doc.contentHash&&evidence.provenance.some(p=>p.id===a.id&&p.messageRef===a.messageRef&&p.contentHash===doc.contentHash&&p.verification.sourceRef===a.messageRef))))return result('hold','runtime.planner-call','Approved spec prerequisite required',['approved-spec']);
91
+ const planner=evaluatePlannerCall({stage,request:{...d.plannerCall,requestId:request.requestId},evidence:{artifacts:proof.data.artifacts,semanticProvenance:'operator-attested'}});
92
+ if(planner.decision!=='allow')return planner;
93
+ next.plannerLedger.stages[next.plannerLedger.stages.findIndex(s=>s.id===stage.id)]=planner.proposedTransition;
94
+ }
95
+ next.assignments.push({assignmentId:request.requestId,requestId:request.requestId,requestedBy:request.actorRef,status:'pending',actorRef:null,platform:request.platform,correlationRef:d.correlationRef,role:d.role,taskId:d.taskId,paths:[...d.paths],operations:[...d.operations],approvalRef:d.approvalRef,...(d.role==='planner'?{plannerStageId:d.plannerCall.stageId}:{}),...(d.checks?{checks:structuredClone(d.checks)}:{}),...(d.temporary?{temporary:structuredClone(d.temporary)}:{})});
86
96
  try{validateWorkflowState(next);}catch{return result('hold','runtime.assignment','invalid pending assignment scope',['valid-assignment']);}
87
97
  return {...result('allow','runtime.assignment','pending assignment authorized; observed spawn result still required'),proposedTransition:next};
88
98
  }
@@ -119,6 +129,7 @@ export function evaluateCooperative({policy,state,request,evidence}={}){
119
129
  if(!same(evidence.versions,state.versions))return hold('cooperative.versions','Policy or runtime changed');
120
130
  if(request.input&&request.input.expectedRevision!==state.revision)return hold('cooperative.revision','Stale revision');
121
131
  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
+ if(request.command==='record'&&request.input.evidence.kind.startsWith('planner-')||request.command==='prepare'&&request.input.operation==='planner-call')return evaluateCooperativePlanner({state,request,evidence});
122
133
  if(request.command==='prepare'){
123
134
  if(unresolvedExternal(state))return hold('cooperative.external-unresolved','Investigate unresolved external effects before follow-up');
124
135
  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)});
@@ -137,3 +148,105 @@ export function reconcileCooperativeResult({state,intent,result,observations}={}
137
148
  try{validateCooperativeState(state);}catch(error){return {...base,decision:'hold',ruleIds:['cooperative.schema'],reason:error.message};}
138
149
  return {...base,decision:'hold',ruleIds:[!intent?'cooperative.intent':result?.status==='unknown'?'cooperative.unknown':'cooperative.reconciliation'],requiredEvidence:[],nextActions:[],proposedTransition:null,reason:!intent?'No matching durable intent':'Recorded operator statement requires reconciliation; no execution authorized'};
139
150
  }
151
+
152
+ /** Pure Planner decision. Artifact verification belongs to the trusted reader;
153
+ * semantic judgments retain their recorded source and never become native proof. */
154
+ export function evaluatePlannerCall({stage,request,evidence}={}){
155
+ const base={expectedRevision:stage?.revision??0,proposedTransition:null,coverage:{enforcement:'decision-only',semanticProvenance:evidence?.semanticProvenance==='operator-attested'?'operator-attested':'unverified',nativeVerified:false}};
156
+ const result=(decision,reason,requiredEvidence=[])=>({...base,decision,ruleIds:['runtime.planner-call'],reason,requiredEvidence});
157
+ if(['format-only','re-expression-only','save-error-only','patch-error-only'].includes(request?.intent))return result('deny','Orchestrator handles non-progress delivery and formatting work');
158
+ try{validatePlannerStage(stage);}catch(error){return result('hold',error.message,['planner-stage-ledger']);}
159
+ if(!request||!['initial','correction','further'].includes(request.kind)||request.intent!=='substantive'||typeof request.requestId!=='string'||!request.requestId||request.stageId!==stage.id||!Array.isArray(request.criterionIds)||new Set(request.criterionIds).size!==request.criterionIds.length||!Array.isArray(request.evidenceRefs)||new Set(request.evidenceRefs).size!==request.evidenceRefs.length)return result('hold','Valid stage-bound Planner request required',['planner-request']);
160
+ if(stage.pending)return result('hold','Reconcile the pending Planner call before another request',['confirmed-planner-result']);
161
+ if(stage.calls.some(c=>c.requestId===request.requestId))return result('hold','Planner request already recorded',['new-request-id']);
162
+ const initial=stage.calls.find(c=>c.kind==='initial'),correction=stage.calls.find(c=>c.kind==='correction');
163
+ if(request.kind==='initial'&&stage.calls.length)return result('deny','Initial draft opportunity already used');
164
+ if(request.kind!=='initial'){
165
+ if(!initial||!['succeeded','failed'].includes(initial.status)||!stage.result||!['succeeded','failed'].includes(stage.result.status)||stage.result.requestId!==stage.calls.at(-1).requestId||!['succeeded','failed'].includes(stage.calls.at(-1).status))return result('hold','A confirmed prior result is required',['confirmed-planner-result']);
166
+ if(!stage.review?.full||stage.review.resultRef!==stage.result.ref||stage.review.criteriaVersion!==stage.criteriaVersion||request.reviewRef!==stage.review.ref||stage.review.source!=='operator-attested')return result('hold','A complete review of the recorded result is required',['consolidated-review']);
167
+ if(request.kind==='correction'&&correction)return result('deny','Consolidated correction opportunity already used');
168
+ if(request.kind==='further'&&!correction)return result('hold','Use the consolidated correction before further calls',['consolidated-correction']);
169
+ }
170
+ const used=new Set(stage.calls.flatMap(c=>c.evidenceHashes)),artifacts=request.evidenceRefs.map(ref=>evidence?.artifacts?.find(a=>a.ref===ref));
171
+ if(request.kind==='further'){
172
+ if(!request.criterionIds.length||request.criterionIds.some(id=>!stage.criteria.some(c=>c.id===id&&c.status==='open')))return result('deny','Concrete open criteria are required');
173
+ if(!request.evidenceRefs.length)return result('hold','Unresolved criteria alone are not new progress',['new-criterion-evidence']);
174
+ }
175
+ if(artifacts.some(a=>a&&a.stageId!==stage.id))return result('deny','Evidence belongs to another stage');
176
+ if(artifacts.some(a=>!a||!['input','progress','finding'].includes(a.purpose)||a.verification?.status!=='verified'||a.verification.source!=='local-bytes'||!/^sha256:[a-f0-9]{64}$/.test(a.hash??'')))return result('hold','Accessible locally verified input or progress bytes are required',['verified-artifact-bytes']);
177
+ if(request.kind==='further'){
178
+ if(artifacts.some(a=>used.has(a.hash)))return result('deny','Identical evidence bytes cannot authorize another call');
179
+ if(request.criterionIds.some(id=>!artifacts.some(a=>a.criterionIds?.includes(id))))return result('hold','Evidence must address every requested criterion',['criterion-evidence-link']);
180
+ if(artifacts.some(a=>!a.semantic||a.semantic.meaningful!==true||a.semantic.source!=='operator-attested'||!a.semantic.judgmentRef||a.semantic.targetHash!==a.hash))return result('hold','A hash-bound semantic judgment is required',['semantic-attestation']);
181
+ }
182
+ const next=structuredClone(stage),call={requestId:request.requestId,kind:request.kind,intent:request.intent,criterionIds:[...request.criterionIds],evidenceHashes:artifacts.map(a=>a.hash),evidenceRefs:[...request.evidenceRefs],reviewRef:request.reviewRef??null,status:'reserved',ordinal:stage.calls.length+1};
183
+ next.calls.push(call);next.pending=request.requestId;next.ready=false;
184
+ return {...result('allow','Planner call reserved; dispatch and result remain unverified'),proposedTransition:next};
185
+ }
186
+
187
+ /** Cooperative Planner records are attestations with separately observed bytes. */
188
+ export function evaluateCooperativePlanner({state,request,evidence}={}){
189
+ const base={expectedRevision:state.revision,coverage:{enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false},proposedTransition:null,requiredEvidence:[]};
190
+ const result=(decision,reason)=>({...base,decision,ruleIds:['cooperative.planner'],reason});
191
+ if(request.input.actor.role!=='orchestrator')return result('hold','Orchestrator records and prepares Planner work');
192
+ if(!state.plannerLedger)return result('hold','Existing workflow Planner history has no ledger; reconciliation required');
193
+ const ledger=structuredClone(state.plannerLedger),input=request.input,data=input.evidence?.data;
194
+ const stage=ledger.stages.find(s=>s.id===(data?.stageId??input.arguments?.stageId));
195
+ const observed=evidence.plannerArtifacts??[];
196
+ const artifact=ref=>observed.find(a=>a.ref===ref&&a.stageId===stage?.id&&a.verification?.status==='verified');
197
+ const transition=(s,reason,decision='allow',uncertainty=false)=>({...result(decision,reason),proposedTransition:{plannerLedger:ledger,...(s?{stageId:s.id}:{}),...(uncertainty?{recordUncertainty:true}:{})}});
198
+ if(request.command==='prepare'){
199
+ if(!stage)return result('hold','Planner stage ledger unavailable');
200
+ if(stage.workflowId!==state.workflowId||stage.scopeVersion!==state.scopeVersion||stage.targetVersion!==state.targetVersion)return result('hold','Planner stage contract version changed');
201
+ const d=evaluatePlannerCall({stage,request:{...input.arguments,requestId:input.requestId},evidence:{artifacts:observed,semanticProvenance:'operator-attested'}});
202
+ if(d.decision!=='allow')return {...d,coverage:base.coverage,expectedRevision:state.revision};
203
+ ledger.stages[ledger.stages.indexOf(stage)]=d.proposedTransition;
204
+ return {...transition(stage,d.reason),proposedTransition:{plannerLedger:ledger,intent:{requestId:input.requestId,operation:'planner-call',stageId:stage.id,taskId:input.taskId??null,status:'prepared',fingerprint:evidence.requestFingerprint,source:'operator-attested',identityVerified:false,target:structuredClone(state.target),kind:input.arguments.kind}}};
205
+ }
206
+ const kind=input.evidence.kind,ref=input.evidence.messageRef;
207
+ if(kind==='planner-stage'){
208
+ 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};
210
+ 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
+ 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
+ if(data.document.kind==='plan'&&!approval.data.prerequisiteRefs?.some(ref=>state.records.some(r=>r.kind==='approval'&&r.messageRef===ref&&r.data.kind==='spec'&&r.source==='operator-attested')))return result('hold','Approved spec prerequisite is required for plan drafting');
214
+ 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
+ if(evidence.plannerHistoryError)return result('hold','Planner history could not be verified: '+evidence.plannerHistoryError);
216
+ if(previous.length){
217
+ const prior=previous.find(s=>data.previousStageRef?.workflowId===s.workflowId&&data.previousStageRef?.stageId===s.id);
218
+ if(!prior||approved.newScope!==true||!same(approved.previousStageRef,data.previousStageRef)||data.inputVersion<=prior.inputVersion||data.approvalRef===prior.approvalRef)return result('hold','Existing stage history requires an approved scope transition and predecessor link');
219
+ }
220
+ const created={...structuredClone(data),workflowId:state.workflowId,targetVersion:state.targetVersion,targetIdentity:{realpath:state.target.realpath,gitCommonDir:state.target.gitCommonDir,gitDir:state.target.gitDir},calls:[],pending:null,result:null,review:null,artifacts:[],ready:false};
221
+ try{validatePlannerStage(created);}catch(error){return result('hold',error.message);}
222
+ ledger.stages.push(created);return transition(created,'Planner stage registered without resetting prior history');
223
+ }
224
+ if(!stage)return result('hold','Planner stage ledger unavailable');
225
+ if(stage.scopeVersion!==state.scopeVersion||stage.targetVersion!==state.targetVersion)return result('hold','Planner stage contract version changed');
226
+ if(kind==='planner-evidence'){
227
+ if(stage.artifacts.some(a=>a.ref===ref))return result('deny','Planner evidence reference already recorded');
228
+ const item=observed.find(a=>a.ref===ref&&a.stageId===stage.id);
229
+ if(!item||item.verification?.status!=='verified'||!['draft','self-review','input','progress','finding'].includes(data.purpose)||!Array.isArray(data.criterionIds)||data.criterionIds.some(id=>!stage.criteria.some(c=>c.id===id)))return result('hold','Accessible stage-bound artifact bytes and purpose required');
230
+ if(data.semantic&&(data.semantic.source!==undefined&&data.semantic.source!=='operator-attested'||typeof data.semantic.meaningful!=='boolean'||!data.semantic.judgmentRef||data.semantic.targetHash!==item.hash))return result('hold','Semantic attestation must bind the observed artifact hash');
231
+ stage.artifacts.push({...item,purpose:data.purpose,criterionIds:[...data.criterionIds],...(data.semantic?{semantic:{...data.semantic,source:'operator-attested'}}:{})});return transition(stage,'Artifact bytes recorded separately from semantic attestation');
232
+ }
233
+ if(kind==='planner-result'){
234
+ const call=stage.calls.find(c=>c.requestId===data.callRequestId);
235
+ 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
+ if(data.status==='succeeded'&&(!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');
237
+ call.status=data.status;call.resultRef=ref;stage.result={ref,requestId:call.requestId,status:data.status,artifactRef:data.artifactRef??null,selfReviewRef:data.selfReviewRef??null,source:'operator-attested'};stage.ready=false;
238
+ if(data.status!=='unknown')stage.pending=null;
239
+ return transition(stage,data.status==='unknown'?'Planner execution remains unknown':'Planner result statement recorded',data.status==='unknown'?'hold':'allow',data.status==='unknown');
240
+ }
241
+ if(kind==='planner-review'){
242
+ if(stage.pending||!stage.result||!['succeeded','failed'].includes(stage.result.status)||data.resultRef!==stage.result.ref||data.full!==true||typeof data.complete!=='boolean'||!Array.isArray(data.criteria)||data.criteria.length!==stage.criteria.length||new Set(data.criteria.map(c=>c.id)).size!==stage.criteria.length||data.criteria.some(c=>!stage.criteria.some(old=>old.id===c.id)||!['open','resolved'].includes(c.status)||!c.judgmentRef))return result('hold','Full review must bind the confirmed result and all criteria');
243
+ for(const old of stage.criteria)if(old.status==='resolved'&&data.criteria.find(c=>c.id===old.id).status==='open'){
244
+ const reassessment=data.reassessments?.find(r=>r.criterionId===old.id&&r.previousJudgmentRef===old.judgmentRef&&r.reasonRef),proof=artifact(reassessment?.newEvidenceRef);
245
+ if(!reassessment||!proof||!proof.criterionIds.includes(old.id)||!proof.semantic?.meaningful||stage.calls.some(c=>c.evidenceHashes.includes(proof.hash)))return result('hold','Resolved criteria require a new evidence-bound reassessment');
246
+ }
247
+ if(data.complete&&(data.criteria.some(c=>c.status!=='resolved')||!artifact(stage.result.artifactRef)||!artifact(stage.result.selfReviewRef)))return result('hold','Incomplete draft cannot become ready or accepted');
248
+ stage.criteria=structuredClone(data.criteria);stage.review={ref,resultRef:data.resultRef,criteriaVersion:stage.criteriaVersion,full:true,complete:data.complete,source:'operator-attested'};stage.ready=data.complete;
249
+ return transition(stage,'Semantic completeness and criterion judgments remain operator-attested');
250
+ }
251
+ return result('hold','Unsupported Planner record');
252
+ }
@@ -1,5 +1,8 @@
1
1
  import {validateWorkflowState, text} from './schema.mjs';
2
2
  import {ROLES} from './operations.mjs';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import {fingerprintRequest,saveDurableState} from './store.mjs';
3
6
  const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
4
7
  /** Observation is supplied by the trusted platform provider, never agent/tool arguments. */
5
8
  export function registerAgentObservation(state, observation){
@@ -25,3 +28,22 @@ export function registerAgentObservation(state, observation){
25
28
  validateWorkflowState(next);
26
29
  return {decision:'allow',reason:'matching observed spawn bound to pending assignment',state:next,proposedTransition:next};
27
30
  }
31
+
32
+ /** A raw start is audit only; verified correlation may bind an existing assignment. */
33
+ export async function recordPlannerAgentObservation({statePath,expectedRevision,input,platform,sessionRef,observation}={}){
34
+ if(!path.isAbsolute(statePath??'')||input?.hook_event_name!=='SubagentStart'||input.session_id!==sessionRef||!text(sessionRef)||!text(input.agent_id)||!['codex','claude'].includes(platform))throw Error('Trusted observation target and complete start required');
35
+ const parent=await fs.realpath(path.dirname(statePath));statePath=path.join(parent,path.basename(statePath));let lock;
36
+ try{
37
+ lock=await fs.open(statePath+'.lock','wx',0o600);const stat=await fs.lstat(statePath);if(!stat.isFile()||stat.isSymbolicLink())throw Error('Observation state must be regular file');
38
+ const state=validateWorkflowState(JSON.parse(await fs.readFile(statePath,'utf8'))),eventHash=fingerprintRequest({input,observation:observation??null});
39
+ if(state.plannerObservations?.some(o=>o.eventHash===eventHash))return {decision:'hold',reason:'Observation replay',proposedTransition:null,replayed:true};
40
+ const candidates=state.assignments.filter(a=>a.status==='pending'&&a.role==='planner');
41
+ let decision={decision:'hold',reason:'Raw event lacks verified assignment correlation',proposedTransition:null};
42
+ if(state.revision!==expectedRevision)decision.reason='Stale provider context cannot bind identity';
43
+ else if(observation?.actorRef===input.agent_id&&observation.platform===platform)decision=registerAgentObservation(state,observation);
44
+ const correlated=decision.decision==='allow'&&Boolean(decision.proposedTransition),next=correlated?decision.proposedTransition:structuredClone(state);
45
+ const entry={eventHash,event:'SubagentStart',source:correlated?'verified-provider':'hook-stdin',sessionRef,agentRef:input.agent_id,classification:correlated?'correlated':candidates.length?'uncorrelated':'unmatched',identityVerified:correlated,reason:decision.reason,stageCandidates:candidates.map(a=>({assignmentId:a.assignmentId,stageId:a.plannerStageId??null,requestId:a.requestId})),coverage:{enforcement:'observation-only',nativeVerified:false,prevention:false},deduplication:'content-hash-only'};
46
+ next.plannerObservations=[...(state.plannerObservations??[]),entry];next.revision=state.revision+1;next.audit.push({event:'SubagentStart',eventHash,revision:next.revision,classification:entry.classification,source:entry.source});validateWorkflowState(next);await saveDurableState({statePath,state:next});
47
+ return {...decision,proposedTransition:correlated?next:null,observation:entry,persisted:true};
48
+ }finally{if(lock){await lock.close();await fs.unlink(statePath+'.lock');}}
49
+ }
@@ -59,6 +59,7 @@ export function validateWorkflowState(s){
59
59
  if(s.acceptances.some(a=>!object(a)||!text(a.taskId)||!text(a.actorRef)||!text(a.fingerprint)||!strings(a.criteria)||!strings(a.checkIds)||!strings(a.findingIds)))fail('acceptances');
60
60
  if(s.commits.some(c=>!object(c)||!text(c.purpose)||!text(c.taskId)||!text(c.expectedParent)||!text(c.expectedTree)))fail('commits');
61
61
  if(s.requests.some(r=>!object(r)||!text(r.requestId)||!hash(r.requestFingerprint)||!object(r.decision))||new Set(s.requests.map(r=>r.requestId)).size!==s.requests.length)fail('request replay');
62
+ if(s.plannerLedger!==undefined){validatePlannerLedger(s.plannerLedger);if(s.plannerLedger.stages.some(stage=>stage.workflowId!==s.workflowId))fail('Planner workflow binding');}
62
63
  return s;
63
64
  }
64
65
  export function validateRequest(r){
@@ -77,3 +78,21 @@ export function validateEvidence(e){
77
78
  }
78
79
  return e;
79
80
  }
81
+
82
+ /** Shared stage shape. Missing ledgers remain valid legacy state, never fresh history. */
83
+ export function validatePlannerStage(stage){
84
+ if(stage?.ready!==undefined&&typeof stage.ready!=='boolean')fail('Planner readiness type');
85
+ if(!object(stage)||!text(stage.id)||!text(stage.workflowId)||!object(stage.document)||!absolute(stage.document.path)||!['spec','plan'].includes(stage.document.kind)||!text(stage.document.phase)||!text(stage.approvalRef)||!Number.isSafeInteger(stage.inputVersion)||stage.inputVersion<1||!Number.isSafeInteger(stage.scopeVersion)||stage.scopeVersion<0||!Number.isSafeInteger(stage.criteriaVersion)||stage.criteriaVersion<1)fail('Planner stage identity');
86
+ if(!Array.isArray(stage.criteria)||!stage.criteria.length||!unique(stage.criteria,'id')||stage.criteria.some(c=>!['open','resolved'].includes(c.status)||!text(c.judgmentRef)))fail('Planner criteria');
87
+ if(!Array.isArray(stage.calls)||!unique(stage.calls,'requestId')||stage.calls.some(c=>!['initial','correction','further'].includes(c.kind)||!['reserved','unknown','succeeded','failed'].includes(c.status)||!Array.isArray(c.evidenceHashes)||c.evidenceHashes.some(h=>!hash(h)))||stage.calls.filter(c=>c.kind==='initial').length>1||stage.calls.filter(c=>c.kind==='correction').length>1)fail('Planner call history');
88
+ if(stage.pending!==null&&(!text(stage.pending)||!stage.calls.some(c=>c.requestId===stage.pending&&['reserved','unknown'].includes(c.status))))fail('Planner pending call');
89
+ const pending=stage.calls.filter(c=>['reserved','unknown'].includes(c.status));if(pending.length>1||pending.length!==(stage.pending===null?0:1))fail('Planner orphan reservation');
90
+ if(stage.artifacts!==undefined&&(!Array.isArray(stage.artifacts)||!unique(stage.artifacts,'ref')||stage.artifacts.some(a=>!absolute(a.path)||!hash(a.hash)||!Array.isArray(a.criterionIds)||!['draft','self-review','input','progress','finding'].includes(a.purpose)||a.verification?.status!=='verified'||a.verification.source!=='local-bytes'||a.semantic&&a.semantic.source!=='operator-attested')))fail('Planner artifact evidence');
91
+ if(stage.ready===true&&(stage.pending!==null||stage.criteria.some(c=>c.status!=='resolved')||stage.result?.status!=='succeeded'||stage.review?.complete!==true||stage.review?.full!==true||stage.review?.resultRef!==stage.result.ref||!stage.artifacts?.some(a=>a.ref===stage.result.artifactRef&&a.purpose==='draft')||!stage.artifacts?.some(a=>a.ref===stage.result.selfReviewRef&&a.purpose==='self-review')))fail('Planner incomplete readiness');
92
+ return stage;
93
+ }
94
+ export function validatePlannerLedger(ledger){
95
+ if(!object(ledger)||ledger.version!==1||!Array.isArray(ledger.stages)||!unique(ledger.stages,'id'))fail('Planner ledger');
96
+ for(const stage of ledger.stages)validatePlannerStage(stage);
97
+ return ledger;
98
+ }
package/lib/tui/app.mjs CHANGED
@@ -8,7 +8,7 @@ function goHome(){throw Object.assign(Error('Return home'),{code:'ARC_TUI_HOME'}
8
8
  function interrupted(){return Object.assign(Error('Session interrupted'),{code:'ARC_CANCELLED'});}
9
9
  export function createTuiInterface(session,{input,controller}={}){
10
10
  async function event(){const e=await session.nextEvent();if(e.name==='interrupt')throw interrupted();if(e.name==='eof')throw Object.assign(Error('Session closed'),{code:'ARC_TUI_EOF'});if(e.name==='error')throw e.error;return e;}
11
- async function menu(title,rows,{selected=0,info=[],separatorBefore,backRow=false,backLabel='back',wordmark=false}={}){
11
+ async function menu(title,rows,{selected=0,info=[],separatorBefore,backRow=false,backLabel='back',escapeBack=false,wordmark=false}={}){
12
12
  const actions=backRow?[...rows,'Back']:rows;
13
13
  selected=actions.length?Math.max(0,Math.min(selected,actions.length-1)):0;
14
14
  while(true){
@@ -17,7 +17,7 @@ export function createTuiInterface(session,{input,controller}={}){
17
17
  const capacity=Math.max(1,viewport.rows-(wordmark?6:1)-information.length-4),offset=Math.max(0,Math.min(displayIndex-capacity+1,display.length-capacity)),visible=display.slice(offset,offset+capacity);
18
18
  const screen={title,wordmark,rows:[...information,...visible],selected:actions.length?information.length+displayIndex-offset:undefined,hint:navigationHint('menu',{backLabel}),...(display.length>capacity?{position:(offset+1)+'-'+Math.min(offset+capacity,display.length)+'/'+display.length}:{})};
19
19
  const rendered=session.render(screen),e=await event();
20
- if(e.name==='up'&&actions.length)selected=(selected+actions.length-1)%actions.length;else if(e.name==='down'&&actions.length)selected=(selected+1)%actions.length;else if(e.name==='escape'){if(backLabel==='exit')return null;goHome();}else if(e.name==='enter'&&actions.length&&rendered.selectionEnabled!==false)return backRow&&selected===rows.length?null:selected;
20
+ if(e.name==='up'&&actions.length)selected=(selected+actions.length-1)%actions.length;else if(e.name==='down'&&actions.length)selected=(selected+1)%actions.length;else if(e.name==='escape'){if(backLabel==='exit'||escapeBack)return null;goHome();}else if(e.name==='enter'&&actions.length&&rendered.selectionEnabled!==false)return backRow&&selected===rows.length?null:selected;
21
21
  }
22
22
  }
23
23
  async function text(title,initial=''){let value=initial,selected=2;while(true){const rendered=session.render({title,mode:'text',rows:[value,'','Save','Back',''],selected,hint:navigationHint('text')});const e=await event();if(e.name==='text'||e.name==='paste')value+=e.text;else if(e.name==='backspace')value=[...value].slice(0,-1).join('');else if(e.name==='up'||e.name==='down')selected=selected===2?3:2;else if(e.name==='escape')goHome();else if(e.name==='enter'&&rendered.selectionEnabled!==false)return selected===2?value:null;}}
@@ -32,7 +32,11 @@ export function createTuiInterface(session,{input,controller}={}){
32
32
  async function busy(action){session.render({title:'Working',mode:'busy',rows:['Operation in progress. Please wait.'],hint:navigationHint('busy')});session.setBusy(true);const wasFlowing=input?.readableFlowing;const onData=chunk=>{if(String(chunk).includes('\x03')||String(chunk).includes('\x04'))controller?.abort();};input?.on('data',onData);try{const result=await action();if(controller?.signal.aborted){if(typeof result?.code!=='number')throw interrupted();return{...result,code:130,interrupted:true};}return result;}finally{input?.removeListener('data',onData);if(wasFlowing!==true)input?.pause?.();session.setBusy(false);session.discardInput();}}
33
33
  return{menu,text,page,busy};
34
34
  }
35
- export function resultRows(result){return[...(result.messages??[]).map(s=>s.trim()),...[result.stdout,result.stderr].flatMap(text=>text?text.trimEnd().split(/\r?\n/):[]),...(result.results??[]).flatMap(item=>[`${item.platform??item.target?.platform??'Target'}: ${item.status}; rollback: ${item.rollback??'not needed'}`,...(item.residualPaths??[]).map(p=>'Residual path: '+p),...(item.error?['Error: '+item.error.message]:[])]),...(result.cli?['CLI: '+result.cli.status,...(result.cli.error?['CLI error: '+result.cli.error.message]:[])]:[]),...(result.error?['Error: '+result.error.message]:[]),'Exit code: '+result.code,...(result.code===0?['Open a new agent session to use changed harness settings.']:['Review the failed target and retry from the menu.']),...(result.exitSession?[(result.cli?'CLI removal':'CLI replacement')+' ended this session. Run arc again for further management.']:[])];}
35
+ export function resultRows(result){
36
+ const clean=text=>text.replace(/; rollback: [^\r\n]*/g,''),label=platform=>({codex:'Codex',claude:'Claude'}[platform]??platform);
37
+ const removed=result.code===0&&['removed','absent'].includes(result.cli?.status);
38
+ return[...(result.messages??[]).map(s=>clean(s.trim())),...[result.stdout,result.stderr].flatMap(text=>text?text.trimEnd().split(/\r?\n/).map(clean):[]),...(result.results??[]).flatMap(item=>{const platform=item.platform??item.target?.platform??'Target';return[...(removed&&item.status==='uninstalled'?[]:[item.status==='installed'?`ARC installed for ${label(platform)}.`:item.status==='uninstalled'?`${label(platform)} ARC settings removed.`:`${platform}: ${item.status}`]),...(item.residualPaths??[]).map(p=>'Residual path: '+p),...(item.error?['Error: '+item.error.message]:[])];}),...(removed?[result.cli.status==='removed'?'ARC settings and CLI removed.':'ARC settings removed. CLI is not installed.']:result.cli?['CLI: '+result.cli.status,...(result.cli.error?['CLI error: '+result.cli.error.message]:[])]:[]),...(result.error?['Error: '+result.error.message]:[]),'Exit code: '+result.code,...(result.exitSession?[result.cli?'CLI removal ended this session.':'CLI replacement ended this session.']:[])];
39
+ }
36
40
  export async function runTui(options={}){
37
41
  let session=options.session,code=0,finalReport;const controller=new AbortController(),signals=options.signals??process;const abort=()=>controller.abort();signals.on('SIGINT',abort);signals.on('SIGTERM',abort);
38
42
  try{session??=createTerminalSession(options);const ui=createTuiInterface(session,{input:options.input??process.stdin,controller}),api={collectStatus,prepareOperation,executeOperation,...options.services},context=options.context?{...options.context,signal:controller.signal}:await createManagementContext({...options.contextOptions,signal:controller.signal});const draft={};let selected=0;
@@ -42,11 +46,11 @@ export async function runTui(options={}){
42
46
  await ui.page('Result',resultRows(result));return false;
43
47
  }
44
48
  async function uninstall(){
45
- let target=0,global=0;
46
- while(true){const nextTarget=await ui.menu('Uninstall target',['Codex','Claude','Both targets'],{selected:target,backRow:true});if(nextTarget===null)return null;target=nextTarget;
47
- while(true){const nextGlobal=await ui.menu('Global ARC instructions',['Remove ARC blocks','Preserve ARC blocks'],{selected:global,backRow:true});if(nextGlobal===null)break;global=nextGlobal;
48
- return ui.busy(()=>api.prepareOperation(context,{kind:'uninstall',target:['codex','claude','all'][target],skipGlobal:global===1}));
49
- }
49
+ let selected=0,target=0;
50
+ while(true){const choice=await ui.menu('Uninstall',['Settings','Remove CLI'],{selected,backRow:true});if(choice===null)return null;selected=choice;
51
+ if(choice===0){const next=await ui.menu('Uninstall settings',['Codex','Claude'],{selected:target,backRow:true});if(next===null)continue;target=next;return ui.busy(()=>api.prepareOperation(context,{kind:'uninstall',target:['codex','claude'][target],removeCli:false}));}
52
+ const proceed=await ui.menu('Remove CLI',['Continue','Back'],{selected:1,escapeBack:true,info:['Removes ARC settings for Codex and Claude, then removes the ARC CLI.','']});
53
+ if(proceed===0)return ui.busy(()=>api.prepareOperation(context,{kind:'uninstall',target:'all',removeCli:true}));
50
54
  }
51
55
  }
52
56
  while(true){const statuses=await api.collectStatus(context),ordered=[...statuses].sort((a,b)=>({claude:0,codex:1}[a.platform]??2)-({claude:0,codex:1}[b.platform]??2));
package/lib/tui/setup.mjs CHANGED
@@ -2,21 +2,18 @@ const presetLabel=value=>({economy:'Economy',balanced:'Balanced',smart:'Smart'}[
2
2
  const languageLabel=value=>({en:'EN',ko:'KR'}[value]??value);
3
3
  import {parsePositiveLimit} from '../limits.mjs';
4
4
  export function initialSettings(statuses){return Object.fromEntries(['codex','claude'].map(platform=>{const settings=statuses.find(s=>s.platform===platform)?.settings;return[platform,{preset:settings?.modelConfig?.preset??'balanced',language:settings?.language??'en',maxTotalSubagents:settings?.limits?.max_total_subagents??100,maxConcurrentSubagents:settings?.limits?.max_concurrent_subagents??5,skipGlobal:false}];}));}
5
- export function setupOptions(draft,target){if(target==='all'&&JSON.stringify(draft.codex)!==JSON.stringify(draft.claude))throw Error('Target settings differ. Explicitly select common values before applying to both.');return{...draft[target==='all'?'codex':target],target};}
5
+ export function setupOptions(draft,target){if(!['codex','claude'].includes(target))throw Object.assign(Error('Unsupported setup target'),{code:'ARC_CLI_INPUT'});return{...draft[target],target};}
6
6
  export async function runSetup({ui,statuses,draft,kind,prepare}){
7
7
  for(const[key,value]of Object.entries(initialSettings(statuses)))draft[key]??=value;
8
8
  let selectedTarget=0;
9
- while(true){const targetIndex=await ui.menu(kind==='preset'?'Change Preset':'Install / Update',['Codex','Claude','Both targets'],{selected:selectedTarget,backRow:true});if(targetIndex===null)return null;selectedTarget=targetIndex;const target=['codex','claude','all'][targetIndex];
10
- if(target==='all'&&JSON.stringify(draft.codex)!==JSON.stringify(draft.claude)){
11
- const choice=await ui.menu('Target settings differ',['Use Codex values for both','Use Claude values for both'],{backRow:true,info:['Codex: '+summary(draft.codex),'Claude: '+summary(draft.claude),'Choosing common values replaces the other target values.']});if(choice===null)continue;const source=choice===0?'codex':'claude';draft.codex={...draft[source]};draft.claude={...draft[source]};
12
- }
13
- const settings=draft[target==='all'?'codex':target];let editing=true,selectedField=kind==='preset'?1:0;
9
+ while(true){const targetIndex=await ui.menu(kind==='preset'?'Change Preset':'Install / Update',['Codex','Claude'],{selected:selectedTarget,backRow:true});if(targetIndex===null)return null;selectedTarget=targetIndex;const target=['codex','claude'][targetIndex];
10
+ const settings=draft[target];let editing=true,selectedField=0;
14
11
  while(editing){const labels=kind==='preset'?['Preset: '+presetLabel(settings.preset),'Apply','Back']:['Preset: '+presetLabel(settings.preset),'Harness language: '+languageLabel(settings.language),'Total subagents: '+settings.maxTotalSubagents,'Concurrent subagents: '+settings.maxConcurrentSubagents,'Apply global settings: '+!settings.skipGlobal,'Apply','Back'];const field=await ui.menu('Settings — '+target,labels,{selected:selectedField,separatorBefore:labels.length-2});if(field===null||field===labels.length-1)break;selectedField=field;
15
12
  if(field===0){const value=await ui.menu('Performance preset',['Economy','Balanced','Smart'],{backRow:true,selected:['economy','balanced','smart'].indexOf(settings.preset)});if(value!==null)settings.preset=['economy','balanced','smart'][value];if(kind==='preset')selectedField=1;}
16
13
  else if(kind==='install'&&field===1){const value=await ui.menu('Harness output language',['EN','KR'],{backRow:true,selected:settings.language==='ko'?1:0});if(value!==null)settings.language=['en','ko'][value];}
17
- else if(kind==='install'&&(field===2||field===3)){const key=field===2?'maxTotalSubagents':'maxConcurrentSubagents';let raw=String(settings[key]);while(true){raw=await ui.text(field===2?'Maximum total subagents':'Maximum concurrent subagents',raw);if(raw===null)break;let value;try{value=parsePositiveLimit(raw);}catch{await ui.page('Invalid limit',['Enter a positive safe ASCII decimal integer.']);continue;}settings[key]=value;if(target==='all')draft.claude={...settings};break;}}
14
+ else if(kind==='install'&&(field===2||field===3)){const key=field===2?'maxTotalSubagents':'maxConcurrentSubagents';let raw=String(settings[key]);while(true){raw=await ui.text(field===2?'Maximum total subagents':'Maximum concurrent subagents',raw);if(raw===null)break;let value;try{value=parsePositiveLimit(raw);}catch{await ui.page('Invalid limit',['Enter a positive safe ASCII decimal integer.']);continue;}settings[key]=value;break;}}
18
15
  else if(kind==='install'&&field===4)settings.skipGlobal=!settings.skipGlobal;
19
- else{if(target==='all')draft.claude={...settings};return prepare({kind,...setupOptions(draft,target)});}
16
+ else{return prepare({kind,...setupOptions(draft,target)});}
20
17
  }
21
18
  }
22
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yunhozz/arc",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -27,7 +27,16 @@
27
27
  "files": [
28
28
  "bin/",
29
29
  "lib/",
30
- "scripts/",
30
+ "scripts/build-native.mjs",
31
+ "scripts/check-installer-parity.mjs",
32
+ "scripts/pack-release.mjs",
33
+ "scripts/postinstall.mjs",
34
+ "scripts/update-manifest.mjs",
35
+ "scripts/update-role-policy.mjs",
36
+ "scripts/update-runtime-assets.mjs",
37
+ "scripts/update-superpowers.mjs",
38
+ "scripts/vendor-toml.mjs",
39
+ "scripts/verify-release.mjs",
31
40
  "vendor/",
32
41
  "prebuilds/",
33
42
  "native/binding.c",
@@ -1,28 +1,26 @@
1
- import {isDynamicReferencePath} from '../lib/installer/superpowers.mjs';
1
+ import {renderManifest,LOCAL_UPDATER_PATH} from '../lib/installer/manifest.mjs';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import crypto from 'node:crypto';
5
4
  import { fileURLToPath } from 'node:url';
6
5
  const i = process.argv.indexOf('--root');
7
6
  const root = i < 0 ? fileURLToPath(new URL('../', import.meta.url)) : path.resolve(process.argv[i + 1]);
8
7
  const mutable = new Set(['codex/skill/models.toml', 'claude/skill/models.toml']);
9
8
  const excluded = new Set(['.DS_Store', '__pycache__', 'build']);
10
- const files = new Set();
9
+ const files = new Map();
11
10
  function visit(relative) {
12
- if(isDynamicReferencePath(relative)) return;
13
11
  const filename = path.join(root, relative);
14
- if (!fs.existsSync(filename)) return;
15
- const stat = fs.lstatSync(filename);
12
+ let stat;try { stat = fs.lstatSync(filename); } catch (error) { if(error.code==='ENOENT')return;throw error; }
16
13
  if (stat.isSymbolicLink()) throw new Error(`Protected payload contains symlink: ${relative}`);
14
+ if(relative===LOCAL_UPDATER_PATH){if(!stat.isFile())throw new Error('Local updater wrapper must be a regular file');return;}
17
15
  if (stat.isDirectory()) {
18
16
  for (const name of fs.readdirSync(filename).sort()) if (!excluded.has(name)) visit(`${relative}/${name}`);
19
- } else if (stat.isFile() && !mutable.has(relative)) files.add(relative);
17
+ } else if (stat.isFile() && !mutable.has(relative)) files.set(relative,fs.readFileSync(filename));
20
18
  else if (!stat.isFile()) throw new Error(`Protected payload is not regular: ${relative}`);
21
19
  }
22
20
  for (const entry of ['README.md', 'install.command', 'package.json', 'bin', 'lib', 'native/binding.c', 'native/binding.gyp', 'scripts', 'vendor', 'prebuilds', 'codex/skill', 'codex/agents', 'claude/skill', 'claude/agents']) visit(entry);
23
- const content = [...files].sort().map(name => `${crypto.createHash('sha256').update(fs.readFileSync(path.join(root, name))).digest('hex')} ${name}\n`).join('');
21
+ const content = renderManifest(files);
24
22
  const manifest = path.join(root, 'MANIFEST.sha256');
25
23
  if (process.argv.includes('--check')) {
26
- if (!fs.existsSync(manifest) || fs.readFileSync(manifest, 'utf8') !== content) { console.error('Protected payload manifest is stale'); process.exitCode = 1; }
24
+ if (!fs.existsSync(manifest) || !fs.readFileSync(manifest).equals(content)) { console.error('Protected payload manifest is stale'); process.exitCode = 1; }
27
25
  else console.log(`Protected payload manifest verified (${files.size} files)`);
28
26
  } else { fs.writeFileSync(manifest, content); console.log(`Updated manifest (${files.size} files)`); }