@yunhozz/arc 0.1.2 → 0.1.3

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 (78) hide show
  1. package/MANIFEST.sha256 +75 -36
  2. package/claude/agents/arc-coder.md +10 -9
  3. package/claude/agents/arc-planner.md +13 -12
  4. package/claude/agents/arc-qa.md +9 -8
  5. package/claude/agents/arc-reviewer.md +11 -10
  6. package/claude/agents/arc-scouter.md +10 -9
  7. package/claude/skill/SKILL.md +13 -17
  8. package/claude/skill/hooks/role-git-guard.mjs +3 -83
  9. package/claude/skill/hooks/runtime-gate.mjs +12 -0
  10. package/claude/skill/models.toml +11 -11
  11. package/claude/skill/references/commit.md +8 -9
  12. package/claude/skill/references/git-recovery.md +1 -1
  13. package/claude/skill/references/handoff-prep.md +8 -8
  14. package/claude/skill/references/handoff.md +8 -8
  15. package/claude/skill/references/limits.md +4 -4
  16. package/claude/skill/references/planning.md +9 -9
  17. package/claude/skill/references/verification.md +10 -10
  18. package/claude/skill/references/visual-companion.md +2 -3
  19. package/claude/skill/runtime/adapters/claude.mjs +81 -0
  20. package/claude/skill/runtime/adapters/codex.mjs +63 -0
  21. package/claude/skill/runtime/capabilities.mjs +32 -0
  22. package/claude/skill/runtime/evaluate.mjs +103 -0
  23. package/claude/skill/runtime/gate.mjs +71 -0
  24. package/claude/skill/runtime/git-evidence.mjs +114 -0
  25. package/claude/skill/runtime/git-query.mjs +88 -0
  26. package/claude/skill/runtime/identity.mjs +27 -0
  27. package/claude/skill/runtime/operations.mjs +6 -0
  28. package/claude/skill/runtime/role-policy.json +607 -0
  29. package/claude/skill/runtime/schema.mjs +78 -0
  30. package/claude/skill/runtime/store.mjs +63 -0
  31. package/claude/skill/scripts/model_config.py +78 -18
  32. package/codex/agents/arc-coder.toml +8 -8
  33. package/codex/agents/arc-planner.toml +9 -9
  34. package/codex/agents/arc-qa.toml +6 -6
  35. package/codex/agents/arc-reviewer.toml +7 -7
  36. package/codex/agents/arc-scouter.toml +6 -6
  37. package/codex/skill/SKILL.md +15 -19
  38. package/codex/skill/hooks/runtime-gate.mjs +10 -0
  39. package/codex/skill/models.toml +11 -11
  40. package/codex/skill/references/commit.md +9 -9
  41. package/codex/skill/references/git-recovery.md +1 -1
  42. package/codex/skill/references/handoff-prep.md +9 -10
  43. package/codex/skill/references/handoff-worktree.md +1 -1
  44. package/codex/skill/references/handoff.md +9 -9
  45. package/codex/skill/references/limits.md +4 -4
  46. package/codex/skill/references/planning.md +11 -12
  47. package/codex/skill/references/verification.md +10 -11
  48. package/codex/skill/references/visual-companion.md +3 -3
  49. package/codex/skill/runtime/adapters/claude.mjs +81 -0
  50. package/codex/skill/runtime/adapters/codex.mjs +63 -0
  51. package/codex/skill/runtime/capabilities.mjs +32 -0
  52. package/codex/skill/runtime/evaluate.mjs +103 -0
  53. package/codex/skill/runtime/gate.mjs +71 -0
  54. package/codex/skill/runtime/git-evidence.mjs +114 -0
  55. package/codex/skill/runtime/git-query.mjs +88 -0
  56. package/codex/skill/runtime/identity.mjs +27 -0
  57. package/codex/skill/runtime/operations.mjs +6 -0
  58. package/codex/skill/runtime/role-policy.json +607 -0
  59. package/codex/skill/runtime/schema.mjs +78 -0
  60. package/codex/skill/runtime/store.mjs +63 -0
  61. package/lib/models/profiles.mjs +41 -4
  62. package/lib/policy/claude-hook.mjs +57 -17
  63. package/lib/policy/render.mjs +1 -0
  64. package/lib/policy/role-policy.json +38 -8
  65. package/lib/runtime/adapters/claude.mjs +81 -0
  66. package/lib/runtime/adapters/codex.mjs +63 -0
  67. package/lib/runtime/capabilities.mjs +32 -0
  68. package/lib/runtime/evaluate.mjs +103 -0
  69. package/lib/runtime/gate.mjs +71 -0
  70. package/lib/runtime/git-evidence.mjs +114 -0
  71. package/lib/runtime/git-query.mjs +88 -0
  72. package/lib/runtime/identity.mjs +27 -0
  73. package/lib/runtime/operations.mjs +6 -0
  74. package/lib/runtime/schema.mjs +78 -0
  75. package/lib/runtime/store.mjs +63 -0
  76. package/package.json +1 -1
  77. package/scripts/probe-runtime-hooks.mjs +55 -0
  78. package/scripts/update-runtime-assets.mjs +19 -0
@@ -0,0 +1,63 @@
1
+ import {verifiedReconciliation} from './gate.mjs';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ import {validateWorkflowState, validateRequest} from './schema.mjs';
6
+ /** Hash complete normalized decision inputs at the trusted caller boundary, never a tool-supplied hash. */
7
+ export function fingerprintRequest(input){
8
+ const canonical=value=>{
9
+ if(value===null||typeof value==='string'||typeof value==='boolean')return value;
10
+ if(typeof value==='number'&&Number.isFinite(value))return value;
11
+ if(Array.isArray(value))return value.map(canonical);
12
+ if(value&&Object.getPrototypeOf(value)===Object.prototype)return Object.fromEntries(Object.keys(value).sort().map(key=>[key,canonical(value[key])]));
13
+ throw Error('Request fingerprint requires complete JSON inputs');
14
+ };
15
+ return 'sha256:'+crypto.createHash('sha256').update(JSON.stringify(canonical(input))).digest('hex');
16
+ }
17
+ /** A failed/interrupted transaction retains the original state. Locks are never silently recovered. */
18
+ export async function transactWorkflow({statePath,expectedRevision,requestId,request,requestFingerprint,decide}){
19
+ if(!path.isAbsolute(statePath)||!Number.isSafeInteger(expectedRevision)||expectedRevision<0||typeof requestId!=='string'||!requestId||typeof decide!=='function')throw Error('Invalid transaction');
20
+ if(typeof requestFingerprint!=='string'||!/^sha256:[a-f0-9]{64}$/.test(requestFingerprint))throw Error('Valid request fingerprint required');
21
+ const normalized=validateRequest(structuredClone(request));
22
+ if(normalized.requestId!==requestId||fingerprintRequest(normalized)!==requestFingerprint)throw Error('Request fingerprint conflict with normalized input');
23
+ const parent=await fs.realpath(path.dirname(statePath));statePath=path.join(parent,path.basename(statePath));
24
+ const lockPath=statePath+'.lock';let lock;let temporary;
25
+ try{
26
+ lock=await fs.open(lockPath,'wx',0o600);
27
+ const stat=await fs.lstat(statePath);if(!stat.isFile()||stat.isSymbolicLink())throw Error('State must be regular file');
28
+ const state=validateWorkflowState(JSON.parse(await fs.readFile(statePath,'utf8')));
29
+ const prior=state.requests.find(r=>r.requestId===requestId);
30
+ let decision;
31
+ if(prior){
32
+ if(prior.requestFingerprint!==requestFingerprint)throw Error('Request fingerprint conflict');
33
+ const current=await decide(structuredClone(state));
34
+ if(!current||!['allow','deny','hold'].includes(current.decision))throw Error('Invalid replay decision');
35
+ const completing=normalized.operation==='commit'&&prior.decision.decision==='allow'&&prior.decision.expectedCommit&&current.decision==='allow'&&current.proposedTransition&&verifiedReconciliation(current,state,current.proposedTransition,prior.decision.expectedCommit)&&!state.commits.some(c=>c.requestId===requestId);
36
+ if(completing)decision=current;
37
+ else{
38
+ const replay={...current,proposedTransition:null};
39
+ if(current.decision==='allow'&&(current.proposedTransition||!['read','draft','review'].includes(normalized.operation))){replay.decision='hold';replay.reason='Replay cannot authorize repeated state or external effects';}
40
+ return {state,decision:replay,replayed:true};
41
+ }
42
+ }
43
+ if(state.revision!==expectedRevision)throw Error('Stale revision');
44
+ decision??=await decide(structuredClone(state));
45
+ if(!decision||!['allow','deny','hold'].includes(decision.decision))throw Error('Invalid decision');
46
+ const next=decision.proposedTransition?structuredClone(decision.proposedTransition):structuredClone(state);
47
+ if(decision.decision!=='allow'&&decision.proposedTransition)throw Error('Denied transition');
48
+ if(next.workflowId!==state.workflowId||(JSON.stringify(next.target)!==JSON.stringify(state.target)&&!verifiedReconciliation(decision,state,next)))throw Error('Transaction target mismatch');
49
+ for(const old of state.identities){const current=next.identities.find(i=>i.actorRef===old.actorRef);if(!current||current.taskId!==old.taskId||current.role!==old.role)throw Error('Identity rebind forbidden');}
50
+ for(const old of state.leases){const current=next.leases.find(l=>l.actorRef===old.actorRef);if(old.status==='retired'&&(!current||current.status!=='retired'||current.taskId!==old.taskId))throw Error('Retired lease revival forbidden');}
51
+ next.revision=state.revision+1;next.requests=prior?state.requests.map(r=>r.requestId===requestId?{requestId,requestFingerprint,decision}:r):[...state.requests,{requestId,requestFingerprint,decision}];
52
+ next.audit=[...state.audit,{requestId,revision:next.revision,decision:decision.decision,ruleIds:decision.ruleIds??[],result:'pending'}];
53
+ validateWorkflowState(next);
54
+ temporary=path.join(parent,`.${path.basename(statePath)}.${crypto.randomUUID()}.tmp`);
55
+ const handle=await fs.open(temporary,'wx',0o600);try{await handle.writeFile(JSON.stringify(next,null,2)+'\n');await handle.sync();}finally{await handle.close();}
56
+ await fs.rename(temporary,statePath);temporary=undefined;
57
+ const directory=await fs.open(parent,'r');try{await directory.sync();}finally{await directory.close();}
58
+ return {state:next,decision,replayed:false};
59
+ }finally{
60
+ if(temporary)await fs.unlink(temporary).catch(()=>{});
61
+ if(lock){await lock.close();await fs.unlink(lockPath);}
62
+ }
63
+ }
@@ -1,4 +1,4 @@
1
- import {hookFrontmatter,validateClaudeHookCommand} from '../policy/claude-hook.mjs';
1
+ import {validateClaudeHookCommand} from '../policy/claude-hook.mjs';
2
2
  import { parseToml, exactKeys, validateRoles, effortField, ROLES, fail } from './config.mjs';
3
3
  const tools = {
4
4
  arc_scouter: ['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch'],
@@ -74,6 +74,43 @@ function renderCodex(text, role, settings) {
74
74
  if (checked.name !== role || Object.entries(settings).some(([key, value]) => checked[key] !== value)) fail('Rendered Codex profile does not match settings');
75
75
  return Buffer.from(output);
76
76
  }
77
+ // Validate the consumed hook subset without rewriting any hook/user bytes.
78
+ // The two ARC entries are exact contracts; user command entries remain data.
79
+ function validateClaudeHooks(lines){
80
+ const managed=new Map();let event;const events=new Set();
81
+ if(lines[0]!=='hooks:')fail('Unsupported managed Claude hooks');
82
+ for(let i=1;i<lines.length;){
83
+ const heading=/^ (PreToolUse|PostToolUse|SubagentStart):$/.exec(lines[i]);
84
+ if(heading){event=heading[1];if(events.has(event))fail('Duplicate Claude hook event');events.add(event);i++;continue;}
85
+ if(!event)fail('Unsupported Claude hook event');
86
+ const matcher=/^ - matcher: (.+)$/.exec(lines[i]);
87
+ let parsedMatcher,command;
88
+ try{
89
+ if(!matcher)throw Error('Missing matcher');
90
+ parsedMatcher=matcher[1].startsWith('"')?JSON.parse(matcher[1]):matcher[1];
91
+ if(typeof parsedMatcher!=='string'||!parsedMatcher||/[\r\n\x00]/.test(parsedMatcher)||(!matcher[1].startsWith('"')&&!/^[A-Za-z][A-Za-z0-9_|]*$/.test(parsedMatcher)))throw Error('Invalid matcher');
92
+ if(lines[i+1]!==' hooks:'||lines[i+2]!==' - type: command'||!lines[i+3]?.startsWith(' command: '))throw Error('Unsupported command entry');
93
+ command=JSON.parse(lines[i+3].slice(' command: '.length));
94
+ if(typeof command!=='string'||!command||/[\r\n\x00]/.test(command))throw Error('Invalid command');
95
+ }catch{fail('Unsupported managed Claude hooks');}
96
+ let isManaged=false;
97
+ try{validateClaudeHookCommand(command);isManaged=true;}catch{}
98
+ const hasFailure=lines[i+4]?.startsWith(' onFailure:');
99
+ if(isManaged){
100
+ if(lines[i+4]!==' onFailure: block')fail('Unsupported managed Claude failure behavior');
101
+ const encoded=/"([A-Za-z0-9+/]+={0,2})"$/.exec(command)[1];
102
+ const url=new URL(Buffer.from(encoded,'base64').toString('utf8'));const file=url.pathname.split('/').at(-1);
103
+ const expected=file==='runtime-gate.mjs'?'"*"':'Bash';
104
+ if(event!=='PreToolUse'||matcher[1]!==expected||!hasFailure||managed.has(file))fail('Malformed or duplicate managed Claude hooks');
105
+ managed.set(file,url.href.slice(0,url.href.lastIndexOf('/')+1));
106
+ }else{
107
+ if(hasFailure&&![' onFailure: continue',' onFailure: block'].includes(lines[i+4]))fail('Unsupported Claude failure behavior');
108
+ if(/runtime-gate\.mjs|role-git-guard\.mjs/.test(command))fail('Unrecognized managed Claude hook command');
109
+ }
110
+ i+=hasFailure?5:4;
111
+ }
112
+ if(managed.size!==1||!managed.has('role-git-guard.mjs'))fail('Missing or mismatched managed Claude hooks');
113
+ }
77
114
  function renderClaude(text, role, settings) {
78
115
  const lines = linesOf(text); if (lines[0]?.replace(/\r?\n$/, '') !== '---') fail('Missing opening Claude frontmatter');
79
116
  const fields = {}, indices = {}; let current, closed = false;
@@ -81,9 +118,9 @@ function renderClaude(text, role, settings) {
81
118
  const line = lines[i].replace(/\r?\n$/, ''); if (line === '---') { closed = true; break; }
82
119
  if(line==='hooks:') {
83
120
  if(Object.hasOwn(fields,'hooks'))fail('Duplicate Claude hooks');
84
- const raw=lines.slice(i,i+6).map(x=>x.replace(/\r?\n$/,''));
85
- try {const command=JSON.parse(raw[5]?.replace(/^ command: /,''));validateClaudeHookCommand(command);if(raw.join('\n')!==hookFrontmatter(command))throw Error('Unsupported hooks');fields.hooks=command;}catch {fail('Unsupported managed Claude hooks');}
86
- indices.hooks=i;i+=5;current='hooks';continue;
121
+ let end=i+1;while(end<lines.length&&/^[ \t]/.test(lines[end]))end++;
122
+ validateClaudeHooks(lines.slice(i,end).map(x=>x.replace(/\r?\n$/,'')));
123
+ fields.hooks=true;indices.hooks=i;i=end-1;current='hooks';continue;
87
124
  }
88
125
  const item = /^ - ([A-Za-z][A-Za-z0-9]*)$/.exec(line);
89
126
  if (item && current === 'tools') { fields.tools.push(item[1]); continue; }
@@ -16,25 +16,65 @@ export function validateClaudeHookCommand(command) {
16
16
  if(typeof command!=='string'||!command.startsWith(prefix))throw Error('Unmanaged Claude hook command');
17
17
  const encoded=/^"([A-Za-z0-9+/]+={0,2})"$/.exec(command.slice(prefix.length));if(!encoded)throw Error('Invalid Claude hook command');
18
18
  const decoded=Buffer.from(encoded[1],'base64');if(decoded.toString('base64')!==encoded[1])throw Error('Invalid hook path encoding');
19
- const url=new URL(decoded.toString('utf8'));if(url.protocol!=='file:'||url.host||!url.pathname.endsWith('/hooks/role-git-guard.mjs')||url.search||url.hash)throw Error('Invalid hook file URL');
19
+ const url=new URL(decoded.toString('utf8'));if(url.protocol!=='file:'||url.host||!['/hooks/role-git-guard.mjs','/hooks/runtime-gate.mjs'].some(suffix=>url.pathname.endsWith(suffix))||url.search||url.hash)throw Error('Invalid hook file URL');
20
20
  return command;
21
21
  }
22
- export function hookFrontmatter(command=templateHookCommand,eol='\n') {
23
- validateClaudeHookCommand(command);
24
- return ['hooks:',' PreToolUse:',' - matcher: Bash',' hooks:',' - type: command',` command: ${JSON.stringify(command)}`].join(eol);
22
+ function entry(command,matcher,eol){
23
+ validateClaudeHookCommand(command);
24
+ return [` - matcher: ${matcher}`,' hooks:',' - type: command',` command: ${JSON.stringify(command)}`,' onFailure: block'].join(eol);
25
+ }
26
+ function entries(skillPath,eol){
27
+ return entry(buildClaudeHookCommand(path.join(skillPath,'hooks/role-git-guard.mjs')),'Bash',eol);
25
28
  }
26
- export function installClaudeHook(bytes,skillPath) {
27
- const text=bytes.toString('utf8'),eol=/\r?\n/.exec(text)?.[0]??'\n';
28
- const pattern=/^hooks:\r?\n PreToolUse:\r?\n - matcher: Bash\r?\n hooks:\r?\n - type: command\r?\n command: ("[^\r\n]+")(?=\r?$)/gm;
29
- const matches=[...text.matchAll(pattern)];if(matches.length!==1)throw Error('Missing or duplicate managed Claude hook');
30
- validateClaudeHookCommand(JSON.parse(matches[0][1]));
31
- return Buffer.from(text.replace(pattern,()=>hookFrontmatter(buildClaudeHookCommand(path.join(skillPath,'hooks/role-git-guard.mjs')),eol)));
29
+ export function hookFrontmatter(command=templateHookCommand,eol='\n') {
30
+ validateClaudeHookCommand(command);
31
+ return ['hooks:',' PreToolUse:',entry(command,'Bash',eol)].join(eol);
32
32
  }
33
- export function renderSourceClaudeHook(text) {
34
- const eol=/\r?\n/.exec(text)?.[0]??'\n';
35
- const frontmatter=/^---\r?\n([\s\S]*?)\r?\n---(?=\r?\n|$)/.exec(text);
36
- if(!frontmatter)throw Error('Missing Claude frontmatter for hook generation');
37
- if(/^hooks:/m.test(frontmatter[1]))return installClaudeHook(Buffer.from(text),'/__ARC_INSTALLED_SKILL__').toString('utf8');
38
- const closing=frontmatter[0].lastIndexOf(eol+'---');
39
- return text.slice(0,closing)+eol+hookFrontmatter(templateHookCommand,eol)+text.slice(closing);
33
+ function update(text,skillPath,requireManaged){
34
+ const eol=/\r?\n/.exec(text)?.[0]??'\n';
35
+ const fm=/^---\r?\n([\s\S]*?)\r?\n---(?=\r?\n|$)/.exec(text);
36
+ if(!fm)throw Error('Missing Claude frontmatter for hook generation');
37
+ let lines=fm[1].split(/\r?\n/);
38
+ const headers=lines.flatMap((line,i)=>/^hooks:/.test(line)?[i]:[]);
39
+ if(headers.length>1)throw Error('Duplicate Claude hooks');
40
+ let count=0;
41
+ if(headers.length){
42
+ const h=headers[0];if(lines[h]!=='hooks:')throw Error('Malformed Claude hooks');
43
+ let end=h+1;while(end<lines.length&&(/^[ \t]/.test(lines[end])||lines[end]===''))end++;
44
+ const indexes=[];for(let i=h+1;i<end;i++)if(/^ PreToolUse:/.test(lines[i]))indexes.push(i);
45
+ if(indexes.length>1)throw Error('Duplicate PreToolUse');
46
+ if(indexes.length){
47
+ const pre=indexes[0];if(lines[pre]!==' PreToolUse:')throw Error('Malformed PreToolUse');
48
+ let finish=pre+1;while(finish<end&&!/^ \S/.test(lines[finish]))finish++;
49
+ const kept=[];const seen=new Set();
50
+ for(let i=pre+1;i<finish;){
51
+ if(lines[i]===''){kept.push(lines[i++]);continue;}
52
+ if(!/^ - /.test(lines[i]))throw Error('Malformed Claude hook entry');
53
+ let j=i+1;while(j<finish&&!/^ - /.test(lines[j]))j++;
54
+ const chunk=lines.slice(i,j),body=chunk.join('\n');
55
+ const commands=chunk.filter(line=>/^ command: /.test(line));
56
+ const managed=commands.some(line=>{try{validateClaudeHookCommand(JSON.parse(line.slice(' command: '.length)));return true;}catch{return false;}});
57
+ if(managed){
58
+ if(commands.length!==1)throw Error('Malformed managed Claude hook');
59
+ const command=JSON.parse(commands[0].slice(' command: '.length));
60
+ const encoded=/"([A-Za-z0-9+/]+={0,2})"$/.exec(command)[1];const file=new URL(Buffer.from(encoded,'base64').toString('utf8')).pathname.split('/').at(-1);
61
+ if(seen.has(file))throw Error('Duplicate managed Claude hook');seen.add(file);count++;
62
+ const matcher=file==='runtime-gate.mjs'?' - matcher: "*"':' - matcher: Bash';
63
+ const base=[matcher,' hooks:',' - type: command',commands[0]];
64
+ // Legacy four-line guard upgrades once; malformed failure behavior is never repaired silently.
65
+ if(JSON.stringify(chunk)!==JSON.stringify(base)&&JSON.stringify(chunk)!==JSON.stringify([...base,' onFailure: block']))throw Error('Malformed managed Claude hook');
66
+ }else{
67
+ if(body.includes(prefix)||/role-git-guard\.mjs|runtime-gate\.mjs/.test(body))throw Error('Malformed managed Claude hook command');
68
+ kept.push(...chunk);
69
+ }
70
+ i=j;
71
+ }
72
+ lines.splice(pre+1,finish-pre-1,...kept,...entries(skillPath,eol).split(eol));
73
+ }else lines.splice(end,0,' PreToolUse:',...entries(skillPath,eol).split(eol));
74
+ }else lines.push('hooks:',' PreToolUse:',...entries(skillPath,eol).split(eol));
75
+ if(requireManaged&&!count)throw Error('Missing managed Claude hook');
76
+ const replacement='---'+eol+lines.join(eol)+eol+'---';
77
+ return replacement+text.slice(fm[0].length);
40
78
  }
79
+ export function installClaudeHook(bytes,skillPath){return Buffer.from(update(bytes.toString('utf8'),skillPath,true));}
80
+ export function renderSourceClaudeHook(text){return update(text,'/__ARC_INSTALLED_SKILL__',false);}
@@ -23,6 +23,7 @@ export function validateRolePolicy(input) {
23
23
  const routes = {noPlan:['coder','qa','reviewer','orchestrator-accept'],planTask:['coder','fresh-qa','orchestrator-accept','orchestrator-commit'],planFinal:['required-qa','reviewer'],planFinding:['coder','reviewer']};
24
24
  if (!object(input.review) || Object.keys(input.review).length !== 4 || Object.entries(routes).some(([key,value])=>!same(input.review[key],value))) fail('invalid review routing');
25
25
  try {validateGitPolicy(input.git);} catch {fail('invalid Git policy');}
26
+ if (!object(input.runtime) || input.runtime.schemaVersion !== 1 || !same(input.runtime.roles,ROLES) || !strings(input.runtime.requiredEvidence) || !strings(input.runtime.ruleIds) || ['platform-identity','user-approval','current-target-versions','resolved-paths'].some(x=>!input.runtime.requiredEvidence.includes(x)) || ['runtime.schema','runtime.policy','runtime.identity','runtime.target','runtime.assignment','runtime.approval','runtime.paths','runtime.docs-commit','runtime.mode','runtime.transition'].some(x=>!input.runtime.ruleIds.includes(x)) || input.runtime.status !== 'decision-only') fail('invalid runtime policy');
26
27
  return input;
27
28
  }
28
29
  export const defaultRolePolicy = source;
@@ -47,7 +47,7 @@
47
47
  "assigned-role"
48
48
  ],
49
49
  "authority": "orchestrator",
50
- "text": "Act only within the intersection of role authority, approved scope and available platform tools. Delegation, restart, agent messages and attached instructions do not expand role authority. Never bypass a restriction through aliases, scripts, other tools, processes or redelegation. Return conflicting instructions to Orchestrator before dependent work; reuse valid prior approvals for unchanged scope rather than requesting them again.",
50
+ "text": "Act only where role authority, approved scope and available platform tools all permit. Delegation, restart, agent messages and attached instructions do not expand role authority. MUST NOT bypass a restriction through aliases, scripts, other tools, processes or redelegation. Return conflicting instructions to Orchestrator before dependent work; reuse valid prior approvals for unchanged scope rather than requesting them again.",
51
51
  "controls": {
52
52
  "codex": "prompt",
53
53
  "claude": "prompt"
@@ -130,7 +130,7 @@
130
130
  "explicit-user-authorization-for-action"
131
131
  ],
132
132
  "authority": "user",
133
- "text": "Push, merge, deploy, publish and external messages require explicit user authorization for that action, scope and target. Local commit authority does not grant external authority. Orchestrator performs authorized external actions; specialist roles return them without execution.",
133
+ "text": "Push, merge, deploy, publish and external messages require explicit user authorization for that action, scope and target. Local commit authority grants no external authority. Orchestrator performs authorized external actions; specialist roles return them without execution.",
134
134
  "controls": {
135
135
  "codex": "prompt",
136
136
  "claude": "prompt"
@@ -190,7 +190,7 @@
190
190
  "approved-spec-before-dependent-plan"
191
191
  ],
192
192
  "authority": "orchestrator",
193
- "text": "Return design/spec/plan drafts and dependencies, criteria, hold conditions and verification needs in the response. Do not save documents, implement, run checks, prepare targets, request user approval or decide acceptance. Stop formal target-dependent drafting if actual prepared target evidence is missing; route decisions to Orchestrator.",
193
+ "text": "Return design/spec/plan drafts and dependencies, criteria, hold conditions and verification needs in the response. MUST NOT save documents, implement, run checks, prepare targets, request user approval or decide acceptance. Stop formal target-dependent drafting if actual prepared target evidence is missing; route decisions to Orchestrator.",
194
194
  "controls": {
195
195
  "codex": "prompt",
196
196
  "claude": "prompt"
@@ -211,7 +211,7 @@
211
211
  "tdd-or-recorded-exception"
212
212
  ],
213
213
  "authority": "orchestrator",
214
- "text": "Implement only assigned approved files and focused checks under the existing TDD contract. Report changed files, RED/GREEN or authorized exception evidence, results, limits and follow-up. Do not plan, independently QA/review your own work, accept the task or declare overall completion. Git changes remain forbidden even when implementation succeeds.",
214
+ "text": "Edit only assigned, approved files and run focused checks under the existing TDD contract. Report changed files, RED/GREEN or authorized exception evidence, results, limits and follow-up. Do not plan, independently QA/review your own work, accept the task or declare overall completion. Git changes remain forbidden even when implementation succeeds.",
215
215
  "controls": {
216
216
  "codex": "prompt",
217
217
  "claude": "prompt"
@@ -253,7 +253,7 @@
253
253
  "relevant-qa-evidence"
254
254
  ],
255
255
  "authority": "orchestrator",
256
- "text": "Independently inspect the assigned final changes and evidence and report actionable findings and limits. Remain read-only; do not execute QA tests, edit files, perform Git mutations or decide acceptance.",
256
+ "text": "Independently inspect the assigned final changes and evidence and report actionable findings and limits. Remain read-only. MUST NOT execute QA tests, edit files, perform Git mutations or decide acceptance.",
257
257
  "controls": {
258
258
  "codex": "prompt",
259
259
  "claude": "prompt"
@@ -278,7 +278,7 @@
278
278
  "qa-rechecks"
279
279
  ],
280
280
  "authority": "orchestrator",
281
- "text": "For significant/high-risk work without a plan: Coder → QA and required QA rechecks → Reviewer of the final full change → Orchestrator acceptance. Do not omit Reviewer merely because no plan exists. This creates no new plan obligation.",
281
+ "text": "For significant/high-risk work without a plan: Coder \u2192 QA and required QA rechecks \u2192 Reviewer of the final full change \u2192 Orchestrator acceptance. MUST NOT omit Reviewer merely because no plan exists. This creates no new plan obligation.",
282
282
  "controls": {
283
283
  "codex": "prompt",
284
284
  "claude": "prompt"
@@ -303,7 +303,7 @@
303
303
  "required-qa"
304
304
  ],
305
305
  "authority": "orchestrator",
306
- "text": "For significant/high-risk plan work: each task uses Coder → fresh QA as required → Orchestrator acceptance and focused commit. After all tasks are accepted and required QA is complete, Reviewer reviews the final full change once. Reviewer is NOT a per-task or per-commit required gate. Preserve required checks and necessary finding rechecks.",
306
+ "text": "For significant/high-risk plan work: each task uses Coder \u2192 fresh QA as required \u2192 Orchestrator acceptance and focused commit. After all tasks are accepted and required QA is complete, Reviewer reviews the final full change once. Reviewer is NOT a per-task or per-commit required gate. Preserve required checks and necessary finding rechecks.",
307
307
  "controls": {
308
308
  "codex": "prompt",
309
309
  "claude": "prompt"
@@ -327,7 +327,7 @@
327
327
  "reviewer-findings"
328
328
  ],
329
329
  "authority": "orchestrator",
330
- "text": "For final overall Reviewer findings on plan work, including behavior findings, use Coder → Reviewer without QA reassignment; repeat until resolved. This exception applies only to that final plan stage. Preserve required QA and QA failure rechecks before initial final review and outside this exception.",
330
+ "text": "For final overall Reviewer findings on plan work, including behavior findings, use Coder \u2192 Reviewer without QA reassignment; repeat until resolved. This exception applies only at the final plan stage. Preserve required QA and QA failure rechecks before initial final review and outside this exception.",
331
331
  "controls": {
332
332
  "codex": "prompt",
333
333
  "claude": "prompt"
@@ -573,5 +573,35 @@
573
573
  "positionals": "any"
574
574
  }
575
575
  }
576
+ },
577
+ "runtime": {
578
+ "schemaVersion": 1,
579
+ "roles": [
580
+ "orchestrator",
581
+ "scouter",
582
+ "planner",
583
+ "coder",
584
+ "qa",
585
+ "reviewer"
586
+ ],
587
+ "requiredEvidence": [
588
+ "platform-identity",
589
+ "user-approval",
590
+ "current-target-versions",
591
+ "resolved-paths"
592
+ ],
593
+ "ruleIds": [
594
+ "runtime.schema",
595
+ "runtime.policy",
596
+ "runtime.identity",
597
+ "runtime.target",
598
+ "runtime.assignment",
599
+ "runtime.approval",
600
+ "runtime.paths",
601
+ "runtime.docs-commit",
602
+ "runtime.mode",
603
+ "runtime.transition"
604
+ ],
605
+ "status": "decision-only"
576
606
  }
577
607
  }
@@ -0,0 +1,81 @@
1
+ import {evaluate} from '../evaluate.mjs';
2
+ import {evaluateCapability} from '../capabilities.mjs';
3
+ import {registerAgentObservation} from '../identity.mjs';
4
+ import {transactWorkflow, fingerprintRequest} from '../store.mjs';
5
+ import {runGate} from '../gate.mjs';
6
+ import {classifyGitCommand} from '../git-query.mjs';
7
+
8
+ const text=v=>typeof v==='string'&&v.length>0;
9
+ const object=v=>v!==null&&typeof v==='object'&&!Array.isArray(v);
10
+ const supported=new Set(['Read','Glob','Grep','Bash','Write','Edit','MultiEdit','Agent']);
11
+ const deny=reason=>({decision:'deny',reason,ruleIds:['runtime.claude'],proposedTransition:null});
12
+ /** Host-verified support for this installed version is required. A config value
13
+ * alone cannot establish hook failure blocking; provider wiring belongs to T6. */
14
+ export function claudeFailureBlockingReady(support){
15
+ return support?.status==='verified'&&support.onFailure==='block'&&text(support.platformVersion)&&text(support.verificationRef);
16
+ }
17
+ /** Context is a trusted host dependency, never deserialized from stdin/tool_input.
18
+ * describeTool must parse the complete tool invocation conservatively, and supply
19
+ * semantic operation, paths and descriptor. No native provider is assumed here. */
20
+ export function normalizeClaudeEvent(input,context={}){
21
+ 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');
22
+ const toolPath=input.tool_name;
23
+ const coverage={...evaluateCapability({platform:'claude',toolPath,sessionRef:context.sessionRef}),enforcement:'hook-response-only',nativeVerified:false,startupTimeout:'unknown',existingSessionInput:'unsupported',specializedPaths:'unknown',hostedTools:'unsupported'};
24
+ if(!claudeFailureBlockingReady(context.failureBlocking))throw Error('Verified current-version onFailure block contract required');
25
+ if(!supported.has(input.tool_name))return {request:null,coverage};
26
+ if(typeof context.describeTool!=='function')throw Error('Trusted semantic tool provider unavailable');
27
+ const semantic=context.describeTool(structuredClone(input));
28
+ if(!object(semantic)||!text(semantic.operation)||!Array.isArray(semantic.paths)||!object(semantic.descriptor))throw Error('Complete semantic tool descriptor required');
29
+ if(input.tool_name==='Agent'&&semantic.operation!=='assign-agent')throw Error('Agent tool requires assignment validation');
30
+ return {request:{requestId:context.requestId,platform:'claude',actorRef:context.actorRef,taskId:context.taskId,cwd:input.cwd,toolName:input.tool_name,arguments:structuredClone(input.tool_input),operation:semantic.operation,paths:structuredClone(semantic.paths),descriptor:structuredClone(semantic.descriptor)},coverage};
31
+ }
32
+ /** Only documented PreToolUse fields are emitted. hold is a native deny. */
33
+ export function renderClaudeDecision(decision){
34
+ if(decision?.decision==='allow')return {};
35
+ const permissionDecision='deny';
36
+ return {hookSpecificOutput:{hookEventName:'PreToolUse',permissionDecision,permissionDecisionReason:typeof decision?.reason==='string'?decision.reason:'Runtime gate could not verify permission'}};
37
+ }
38
+ /** This returns a response, not proof the host applied it. Startup failures,
39
+ * timeout, hosted/specialized paths and existing write_stdin are outside it. */
40
+ export async function runClaudeGate(input,context={}){
41
+ if(input?.hook_event_name==='PostToolUse'){
42
+ // Result recording cannot authorize assignment/start or prevent a completed call.
43
+ try{if(input.session_id===context.sessionRef&&input.tool_use_id===context.toolUseId&&text(context.actorRef)&&typeof context.recordToolResult==='function')await context.recordToolResult(structuredClone(input));}catch{}
44
+ return {};
45
+ }
46
+ if(input?.hook_event_name==='SubagentStart'){
47
+ // SubagentStart cannot prevent startup. Only a trusted provider can correlate
48
+ // agent_id with a pending assignment; tool/event agent_type is not authority.
49
+ try{
50
+ if(input.session_id===context.sessionRef&&text(input.agent_id)&&typeof context.observeAgent==='function'){
51
+ const observation=await context.observeAgent(structuredClone(input));
52
+ if(observation?.actorRef===input.agent_id&&observation.platform==='claude'){
53
+ const decision=registerAgentObservation(context.state,observation);
54
+ if(decision.decision==='allow'&&decision.proposedTransition&&typeof context.persistObservation==='function')await context.persistObservation(decision,observation);
55
+ }
56
+ }
57
+ }catch{/* Observation failure never claims startup was blocked. */}
58
+ return {};
59
+ }
60
+ try{
61
+ const {request,coverage}=normalizeClaudeEvent(input,context);
62
+ if(!request)return renderClaudeDecision(deny('Tool path has no verified pre-action coverage'));
63
+ if(!text(context.statePath))return renderClaudeDecision(deny('Trusted durable workflow store unavailable'));
64
+ const result=await transactWorkflow({statePath:context.statePath,expectedRevision:context.state?.revision,requestId:request.requestId,request,requestFingerprint:fingerprintRequest(request),decide:async state=>{
65
+ const decision=evaluate(context.policy,state,request,context.evidence);
66
+ if(decision.decision!=='allow')return {...decision,coverage};
67
+ const actor=state.identities.find(identity=>identity.actorRef===request.actorRef);
68
+ if(request.toolName==='Bash'&&actor?.role!=='orchestrator'){
69
+ const query=classifyGitCommand(request.arguments.command,context.policy.git);
70
+ if(!query.allowed)return {...deny('Specialist Git guard: '+query.reason),coverage};
71
+ }
72
+ if(request.operation==='commit'){
73
+ const git=await runGate({phase:'preflight',state,request,gitEvidence:context.gitEvidence});
74
+ if(git.decision!=='allow')return {...git,coverage};
75
+ return {...decision,expectedCommit:git.expectedCommit,coverage};
76
+ }
77
+ return {...decision,coverage};
78
+ }});
79
+ return renderClaudeDecision(result.decision);
80
+ }catch(error){return renderClaudeDecision(deny('Runtime validation unavailable: '+error.message));}
81
+ }
@@ -0,0 +1,63 @@
1
+ import {evaluate} from '../evaluate.mjs';
2
+ import {evaluateCapability} from '../capabilities.mjs';
3
+ import {registerAgentObservation} from '../identity.mjs';
4
+ import {transactWorkflow, fingerprintRequest} from '../store.mjs';
5
+ import {runGate} from '../gate.mjs';
6
+
7
+ const text=v=>typeof v==='string'&&v.length>0;
8
+ const object=v=>v!==null&&typeof v==='object'&&!Array.isArray(v);
9
+ const supported=new Set(['Read','Glob','Grep','Bash','Write','Edit','MultiEdit','exec_command','apply_patch','Agent','spawn_agent']);
10
+ const deny=reason=>({decision:'deny',reason,ruleIds:['runtime.codex'],proposedTransition:null});
11
+ /** Context is a trusted host dependency, never deserialized from stdin/tool_input.
12
+ * describeTool must parse the complete tool invocation conservatively, and supply
13
+ * semantic operation, paths and descriptor. No native provider is assumed here. */
14
+ export function normalizeCodexEvent(input,context={}){
15
+ 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
+ const toolPath=['Agent','spawn_agent'].includes(input.tool_name)?'spawn_agent':input.tool_name;
17
+ const coverage={...evaluateCapability({platform:'codex',toolPath,sessionRef:context.sessionRef}),enforcement:'hook-response-only',nativeVerified:false,startupTimeout:'unknown',existingSessionInput:'unsupported',specializedPaths:'unknown',hostedTools:'unsupported'};
18
+ if(!supported.has(input.tool_name))return {request:null,coverage};
19
+ if(typeof context.describeTool!=='function')throw Error('Trusted semantic tool provider unavailable');
20
+ const semantic=context.describeTool(structuredClone(input));
21
+ if(!object(semantic)||!text(semantic.operation)||!Array.isArray(semantic.paths)||!object(semantic.descriptor))throw Error('Complete semantic tool descriptor required');
22
+ if(['Agent','spawn_agent'].includes(input.tool_name)&&semantic.operation!=='assign-agent')throw Error('Agent tool requires assignment validation');
23
+ return {request:{requestId:context.requestId,platform:'codex',actorRef:context.actorRef,taskId:context.taskId,cwd:input.cwd,toolName:input.tool_name,arguments:structuredClone(input.tool_input),operation:semantic.operation,paths:structuredClone(semantic.paths),descriptor:structuredClone(semantic.descriptor)},coverage};
24
+ }
25
+ /** Only documented PreToolUse fields are emitted. hold is a native deny. */
26
+ export function renderCodexDecision(decision){
27
+ const permissionDecision=decision?.decision==='allow'?'allow':'deny';
28
+ return {hookSpecificOutput:{hookEventName:'PreToolUse',permissionDecision,permissionDecisionReason:typeof decision?.reason==='string'?decision.reason:'Runtime gate could not verify permission'}};
29
+ }
30
+ /** This returns a response, not proof the host applied it. Startup failures,
31
+ * timeout, hosted/specialized paths and existing write_stdin are outside it. */
32
+ export async function runCodexGate(input,context={}){
33
+ if(input?.hook_event_name==='SubagentStart'){
34
+ // SubagentStart cannot prevent startup. Only a trusted provider can correlate
35
+ // agent_id with a pending assignment; tool/event agent_type is not authority.
36
+ 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
+ }
43
+ }
44
+ }catch{/* Observation failure never claims startup was blocked. */}
45
+ return {};
46
+ }
47
+ try{
48
+ const {request,coverage}=normalizeCodexEvent(input,context);
49
+ if(!request)return renderCodexDecision(deny('Tool path has no verified pre-action coverage'));
50
+ if(!text(context.statePath))return renderCodexDecision(deny('Trusted durable workflow store unavailable'));
51
+ const result=await transactWorkflow({statePath:context.statePath,expectedRevision:context.state?.revision,requestId:request.requestId,request,requestFingerprint:fingerprintRequest(request),decide:async state=>{
52
+ const decision=evaluate(context.policy,state,request,context.evidence);
53
+ if(decision.decision!=='allow')return {...decision,coverage};
54
+ if(request.operation==='commit'){
55
+ const git=await runGate({phase:'preflight',state,request,gitEvidence:context.gitEvidence});
56
+ if(git.decision!=='allow')return {...git,coverage};
57
+ return {...decision,expectedCommit:git.expectedCommit,coverage};
58
+ }
59
+ return {...decision,coverage};
60
+ }});
61
+ return renderCodexDecision(result.decision);
62
+ }catch(error){return renderCodexDecision(deny('Runtime validation unavailable: '+error.message));}
63
+ }
@@ -0,0 +1,32 @@
1
+ // Declarations describe documented hooks, never proof that this installation ran them.
2
+ const contracts = {
3
+ codex: {spawn_agent:'Agent', followup_task:null, SubagentStart:null},
4
+ claude: {Agent:'Agent', SubagentStart:null},
5
+ };
6
+ function operationOf(receipt, platform) {
7
+ const matcher = contracts[platform]?.[receipt.operation];
8
+ return matcher && receipt.toolPath === matcher ? receipt.operation : null;
9
+ }
10
+ export function summarizeProbe(receipts, {platform, sessionRef, platformVersion, attemptRef} = {}) {
11
+ const candidates = Array.isArray(receipts) ? receipts.filter(r => r && r.source === 'native' &&
12
+ r.platform === platform && operationOf(r,platform) &&
13
+ typeof sessionRef === 'string' && sessionRef.length > 0 && r.sessionRef === sessionRef &&
14
+ typeof platformVersion === 'string' && platformVersion.length > 0 && r.platformVersion === platformVersion &&
15
+ typeof attemptRef === 'string' && attemptRef.length > 0 && r.attemptRef === attemptRef &&
16
+ r.event === 'PreToolUse' && ['host-result','hook-decision'].includes(r.kind) &&
17
+ r.decision === 'deny' && (r.kind !== 'host-result' || (r.toolExecuted === false && r.success === true))) : [];
18
+ // JSON is unauthenticated. No native host verifier is available here, so even
19
+ // a structurally valid current receipt is only an unverified observation.
20
+ return {platform,sessionRef,platformVersion,attemptRef,verifiedDeny:false,status:'unknown',source:'unverified',
21
+ toolPaths:[...new Set(candidates.map(r=>operationOf(r,platform)))]};
22
+ }
23
+ export function evaluateCapability({platform,toolPath,sessionRef,platformVersion,attemptRef,receipts=[]} = {}) {
24
+ const known=Object.hasOwn(contracts[platform] ?? {},toolPath);
25
+ const observationOnly=known && toolPath === 'SubagentStart';
26
+ const observed=summarizeProbe(receipts,{platform,sessionRef,platformVersion,attemptRef});
27
+ return {platform,toolPath,sessionRef,platformVersion,attemptRef,observationOnly,
28
+ declared:{source:'documentation',matcher:contracts[platform]?.[toolPath] ?? null},
29
+ status:!known || observationOnly ? 'unsupported':'unknown',verifiedDeny:false,
30
+ source:!known || observationOnly ? 'documentation':'unverified',
31
+ observedPaths:observed.toolPaths.filter(p=>p === toolPath)};
32
+ }