@yunhozz/arc 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MANIFEST.sha256 +109 -75
- package/README.md +19 -31
- package/claude/agents/{arc-coder.md → arc-implementer.md} +7 -7
- package/claude/agents/arc-planner.md +4 -4
- package/claude/agents/arc-qa.md +4 -4
- package/claude/agents/arc-reviewer.md +5 -5
- package/claude/agents/arc-scouter.md +3 -3
- package/claude/skill/SKILL.md +7 -7
- package/claude/skill/models.toml +3 -3
- package/claude/skill/references/commit.md +22 -7
- package/claude/skill/references/coordination.md +25 -18
- package/claude/skill/references/git-recovery.md +1 -1
- package/claude/skill/references/handoff-prep.md +25 -2
- package/claude/skill/references/handoff.md +116 -24
- package/claude/skill/references/planning.md +78 -22
- package/claude/skill/references/verification.md +155 -17
- package/claude/skill/runtime/acceptance.mjs +68 -0
- package/claude/skill/runtime/assignment.mjs +42 -0
- package/claude/skill/runtime/bootstrap.mjs +35 -0
- package/claude/skill/runtime/cli.mjs +8 -0
- package/claude/skill/runtime/command.mjs +21 -0
- package/claude/skill/runtime/cooperative-schema.mjs +55 -0
- package/claude/skill/runtime/cooperative.mjs +136 -0
- package/claude/skill/runtime/evaluate.mjs +40 -4
- package/claude/skill/runtime/external.mjs +51 -0
- package/claude/skill/runtime/gate.mjs +7 -7
- package/claude/skill/runtime/git-evidence.mjs +46 -0
- package/claude/skill/runtime/operations.mjs +2 -2
- package/claude/skill/runtime/role-policy.json +64 -19
- package/claude/skill/runtime/schema.mjs +1 -0
- package/claude/skill/runtime/scope.mjs +36 -0
- package/claude/skill/runtime/store.mjs +25 -4
- package/claude/skill/runtime/verification.mjs +123 -0
- package/claude/skill/scripts/model_config.py +2 -2
- package/codex/agents/{arc-coder.toml → arc-implementer.toml} +8 -8
- package/codex/agents/arc-planner.toml +6 -6
- package/codex/agents/arc-qa.toml +6 -6
- package/codex/agents/arc-reviewer.toml +6 -6
- package/codex/agents/arc-scouter.toml +5 -5
- package/codex/skill/SKILL.md +9 -9
- package/codex/skill/models.toml +3 -3
- package/codex/skill/references/commit.md +23 -8
- package/codex/skill/references/coordination.md +25 -18
- package/codex/skill/references/git-recovery.md +2 -2
- package/codex/skill/references/handoff-prep.md +25 -2
- package/codex/skill/references/handoff-worktree.md +4 -4
- package/codex/skill/references/handoff.md +126 -34
- package/codex/skill/references/planning.md +81 -23
- package/codex/skill/references/verification.md +160 -22
- package/codex/skill/runtime/acceptance.mjs +68 -0
- package/codex/skill/runtime/assignment.mjs +42 -0
- package/codex/skill/runtime/bootstrap.mjs +35 -0
- package/codex/skill/runtime/cli.mjs +8 -0
- package/codex/skill/runtime/command.mjs +21 -0
- package/codex/skill/runtime/cooperative-schema.mjs +55 -0
- package/codex/skill/runtime/cooperative.mjs +136 -0
- package/codex/skill/runtime/evaluate.mjs +40 -4
- package/codex/skill/runtime/external.mjs +51 -0
- package/codex/skill/runtime/gate.mjs +7 -7
- package/codex/skill/runtime/git-evidence.mjs +46 -0
- package/codex/skill/runtime/operations.mjs +2 -2
- package/codex/skill/runtime/role-policy.json +64 -19
- package/codex/skill/runtime/schema.mjs +1 -0
- package/codex/skill/runtime/scope.mjs +36 -0
- package/codex/skill/runtime/store.mjs +25 -4
- package/codex/skill/runtime/verification.mjs +123 -0
- package/codex/skill/scripts/model_config.py +3 -3
- package/lib/cli/postinstall.mjs +3 -16
- package/lib/cli/root.mjs +8 -18
- package/lib/cli/upgrade.mjs +1 -57
- package/lib/installer/manifest.mjs +7 -2
- package/lib/installer/plan.mjs +27 -13
- package/lib/installer/runtime-assets.mjs +25 -0
- package/lib/installer/shell-completion.mjs +15 -20
- package/lib/installer/targets.mjs +1 -1
- package/lib/installer/uninstall.mjs +5 -1
- package/lib/installer/upgrade-state.mjs +2 -2
- package/lib/installer/upgrade.mjs +2 -5
- package/lib/management/operations.mjs +39 -0
- package/lib/management/status.mjs +18 -0
- package/lib/management/update.mjs +58 -0
- package/lib/management/workflow.mjs +51 -0
- package/lib/models/config.mjs +1 -1
- package/lib/models/profiles.mjs +1 -1
- package/lib/policy/placement.mjs +6 -6
- package/lib/policy/render.mjs +3 -3
- package/lib/policy/role-policy.json +64 -19
- package/lib/release/artifact.mjs +1 -1
- package/lib/runtime/acceptance.mjs +68 -0
- package/lib/runtime/assignment.mjs +42 -0
- package/lib/runtime/bootstrap.mjs +35 -0
- package/lib/runtime/cli.mjs +8 -0
- package/lib/runtime/command.mjs +21 -0
- package/lib/runtime/cooperative-schema.mjs +55 -0
- package/lib/runtime/cooperative.mjs +136 -0
- package/lib/runtime/evaluate.mjs +40 -4
- package/lib/runtime/external.mjs +51 -0
- package/lib/runtime/gate.mjs +7 -7
- package/lib/runtime/git-evidence.mjs +46 -0
- package/lib/runtime/operations.mjs +2 -2
- package/lib/runtime/schema.mjs +1 -0
- package/lib/runtime/scope.mjs +36 -0
- package/lib/runtime/store.mjs +25 -4
- package/lib/runtime/verification.mjs +123 -0
- package/lib/tui/app.mjs +41 -0
- package/lib/tui/render.mjs +21 -0
- package/lib/tui/setup.mjs +20 -0
- package/lib/tui/terminal.mjs +71 -0
- package/lib/tui/workflow.mjs +12 -0
- package/package.json +1 -1
- package/scripts/postinstall.mjs +1 -3
- package/scripts/update-runtime-assets.mjs +6 -1
- package/lib/cli/choices.mjs +0 -47
- package/lib/cli/completion.mjs +0 -55
- package/lib/cli/index.mjs +0 -55
- package/lib/cli/preset.mjs +0 -29
- package/lib/cli/terminal.mjs +0 -19
- package/lib/cli/uninstall.mjs +0 -17
|
@@ -5,7 +5,7 @@ import {loadModelConfig} from '../models/index.mjs';
|
|
|
5
5
|
import {parseLanguage} from './language.mjs';
|
|
6
6
|
import {parseLimits} from './limits.mjs';
|
|
7
7
|
import {removeManagedBlock} from './global-config.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import {removeShellCompletion} from './shell-completion.mjs';
|
|
9
9
|
import {readRuntimeConcurrency} from './runtime-config.mjs';
|
|
10
10
|
const fail=message=>{throw new TargetConfigurationError(message);};
|
|
11
11
|
export function collectInstalledState({safeFs,userHome,env={},cwd}){
|
|
@@ -21,7 +21,7 @@ export function collectInstalledState({safeFs,userHome,env={},cwd}){
|
|
|
21
21
|
mandatory('SKILL.md');if(profiles.some(item=>!item.exists||!item.bytes.length))fail(`Incomplete ARC installation: missing profile at ${target.home}`);
|
|
22
22
|
let language,modelConfig,limits;try{language=parseLanguage(mandatory('settings.toml')).language;modelConfig=loadModelConfig(mandatory('models.toml'),platform);limits=parseLimits(mandatory('limits.toml'));}catch(cause){throw new TargetConfigurationError(`Invalid installed ARC settings at ${target.home}: ${cause.message}`,{cause});}
|
|
23
23
|
const native=inspectPath(safeFs,target.runtimeConfigPath,{kind:'file',read:true,snapshot});if(native.exists)try{readRuntimeConcurrency(platform,native.bytes);}catch(cause){throw new TargetConfigurationError(`Invalid native configuration at ${target.runtimeConfigPath}: ${cause.message}`,{cause});}
|
|
24
|
-
const shell=inspectPath(safeFs,path.join(userHome,'.zshrc'),{kind:'file',read:true,snapshot});if(shell.exists)
|
|
24
|
+
const shell=inspectPath(safeFs,path.join(userHome,'.zshrc'),{kind:'file',read:true,snapshot});if(shell.exists)removeShellCompletion(shell.bytes);
|
|
25
25
|
targets.push({target,language,modelConfig,limits,snapshot});
|
|
26
26
|
}
|
|
27
27
|
const hash=createHash('sha256');for(const entry of targets){hash.update(entry.target.platform+'\0'+entry.target.home+'\0');for(const [name,value]of [...entry.snapshot].sort(([a],[b])=>a.localeCompare(b)))hash.update(JSON.stringify([name,value],(key,value)=>key==='atimeNs'?undefined:typeof value==='bigint'?value.toString():value));}
|
|
@@ -5,7 +5,7 @@ import {buildInstallPlan,validatePlanSet} from './plan.mjs';
|
|
|
5
5
|
import {assertPlanSnapshot} from './transaction.mjs';
|
|
6
6
|
import {inspectPath} from './targets.mjs';
|
|
7
7
|
import {renderManagedBlock,removeManagedBlock,agentsBlockBody} from './global-config.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import {attachShellCompletionCleanup} from './shell-completion.mjs';
|
|
9
9
|
import {verifyPayload} from './manifest.mjs';
|
|
10
10
|
import {installTargets} from './install.mjs';
|
|
11
11
|
import {acquireExternalSkills} from './acquisition.mjs';
|
|
@@ -19,10 +19,7 @@ export function buildUpgradePlans({payload,state,safeFs}){
|
|
|
19
19
|
if(current.exists&&removeManagedBlock(current.bytes).changed){const rendered=renderManagedBlock(current.bytes,agentsBlockBody(target.home,target.userHome,language,target.platform));plan.managedPaths.push(target.instructionPath);if(!rendered.content.equals(current.bytes))plan.globalChanges.push({path:target.instructionPath,original:current.bytes,...rendered});}
|
|
20
20
|
return plan;
|
|
21
21
|
});
|
|
22
|
-
|
|
23
|
-
if(current.exists){const content=renderShellCompletion(current.bytes);if(current.bytes.includes(Buffer.from('# >>> arc completion >>>'))){first.managedPaths.push(filename);if(!content.equals(current.bytes))first.globalChanges.push({path:filename,original:current.bytes,content,action:'update completion'});}}
|
|
24
|
-
for(const plan of plans.slice(1))plan.snapshot.delete(filename);
|
|
25
|
-
}
|
|
22
|
+
attachShellCompletionCleanup(plans,{safeFs});
|
|
26
23
|
validatePlanSet(plans);return plans;
|
|
27
24
|
}
|
|
28
25
|
export async function upgradeInstalledTargets({packageRoot,state,safeFs,signal,acquisition}){
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
|
+
import {resolveTarget} from '../installer/targets.mjs';
|
|
6
|
+
import {buildInstallPlan,validatePlanSet} from '../installer/plan.mjs';
|
|
7
|
+
import {verifyPayload} from '../installer/manifest.mjs';
|
|
8
|
+
import {buildPresetPlan,switchPresetTargets} from '../installer/preset.mjs';
|
|
9
|
+
import {buildUninstallPlan,uninstallTargets} from '../installer/uninstall.mjs';
|
|
10
|
+
import {installTargets} from '../installer/install.mjs';
|
|
11
|
+
import {attachShellCompletionCleanup} from '../installer/shell-completion.mjs';
|
|
12
|
+
import {acquireExternalSkills} from '../installer/acquisition.mjs';
|
|
13
|
+
/** @typedef {{packageRoot:string,userHome:string,cwd:string,env:object,safeFs:object,signal?:AbortSignal,acquisition?:Function}} ManagementContext */
|
|
14
|
+
/** @typedef {{kind:string,plans:object[],options:object}} PreparedOperation */
|
|
15
|
+
/** @typedef {{code:number,exitSession:boolean,results:object[],interrupted:boolean}} OperationResult */
|
|
16
|
+
export async function createManagementContext(options={}){
|
|
17
|
+
let safeFs=options.safeFs;if(!safeFs){const[{loadNative},{openRoot}]=await Promise.all([import('../safe-fs/native.mjs'),import('../safe-fs/index.mjs')]);const native=loadNative();safeFs={openRoot:p=>openRoot(p,{native})};}
|
|
18
|
+
return{packageRoot:fileURLToPath(new URL('../../',import.meta.url)),userHome:os.homedir(),cwd:process.cwd(),env:process.env,...options,safeFs};
|
|
19
|
+
}
|
|
20
|
+
export async function prepareOperation(context,options,{selections=[]}={}){
|
|
21
|
+
if(options.kind==='update'){const{prepareUpdate}=await import('./update.mjs');return prepareUpdate(context);}
|
|
22
|
+
if(!['install','preset','uninstall'].includes(options.kind))throw Object.assign(Error('Unknown management operation'),{code:'ARC_CLI_INPUT'});
|
|
23
|
+
const platforms=options.target==='all'?['codex','claude']:[options.target],payload=options.kind==='install'?verifyPayload(context):null;
|
|
24
|
+
const plans=platforms.map(platform=>{const target=resolveTarget(platform,context),selection=selections.find(item=>item.target.platform===platform&&item.target.home===target.home);if(options.kind==='preset')return buildPresetPlan({target,preset:options.preset,safeFs:context.safeFs});if(options.kind==='uninstall')return buildUninstallPlan({target,safeFs:context.safeFs,skipGlobal:options.skipGlobal});return buildInstallPlan({payload,target,limitsPreview:selection?.preview,languagePreview:selection?.languagePreview,preset:options.preset,language:options.language,skipGlobal:options.skipGlobal,safeFs:context.safeFs,limits:{...(options.maxTotalSubagents===undefined?{}:{max_total_subagents:options.maxTotalSubagents}),...(options.maxConcurrentSubagents===undefined?{}:{max_concurrent_subagents:options.maxConcurrentSubagents})},concurrentExplicit:options.maxConcurrentSubagents!==undefined||Boolean(options.concurrentSelected)});});
|
|
25
|
+
if(options.kind==='install')attachShellCompletionCleanup(plans,context);
|
|
26
|
+
// Uninstall plans initially share the completion snapshot; its executor assigns one owner.
|
|
27
|
+
if(options.kind!=='uninstall')validatePlanSet(plans);
|
|
28
|
+
return{kind:options.kind,plans,options:{...options}};
|
|
29
|
+
}
|
|
30
|
+
export function operationResult(result,successStatuses=['installed']){return{...result,code:result.interrupted?130:result.results.every(item=>successStatuses.includes(item.status)&&!item.error)?0:1,exitSession:false};}
|
|
31
|
+
export function acquisitionFor(context){return context.acquisition??(async()=>{const work=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'arc-acquire-'));let acquired;try{acquired=await acquireExternalSkills({work,safeFs:context.safeFs,signal:context.signal});}catch(error){if(!error.residualPaths?.length)try{fs.rmdirSync(work);}catch(cleanup){throw Object.assign(new AggregateError([error,cleanup],'Cannot clean private acquisition work'),{code:'ARC_ACQUISITION_CLEANUP',residualPaths:[work]});}throw error;}return{...acquired,dispose:async()=>{await acquired.dispose();fs.rmdirSync(work);}};});}
|
|
32
|
+
export async function executeOperation(context,prepared){
|
|
33
|
+
if(prepared.kind==='update'){const{executeUpdate}=await import('./update.mjs');return executeUpdate(context,prepared);}
|
|
34
|
+
const options={safeFs:context.safeFs,signal:context.signal,hooks:context.hooks};
|
|
35
|
+
if(prepared.kind==='install')return operationResult(await installTargets(prepared.plans,{...options,acquisition:acquisitionFor(context)}));
|
|
36
|
+
if(prepared.kind==='preset')return operationResult(await switchPresetTargets(prepared.plans,options),['updated','unchanged']);
|
|
37
|
+
if(prepared.kind==='uninstall')return operationResult(await uninstallTargets(prepared.plans,options),['uninstalled']);
|
|
38
|
+
throw Object.assign(Error('Unknown prepared operation'),{code:'ARC_CLI_INPUT'});
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {resolveTarget,inspectPath} from '../installer/targets.mjs';
|
|
3
|
+
import {parseLanguage} from '../installer/language.mjs';
|
|
4
|
+
import {parseLimits} from '../installer/limits.mjs';
|
|
5
|
+
import {loadModelConfig} from '../models/index.mjs';
|
|
6
|
+
import {removeManagedBlock} from '../installer/global-config.mjs';
|
|
7
|
+
import {readRuntimeConcurrency} from '../installer/runtime-config.mjs';
|
|
8
|
+
import {removeShellCompletion} from '../installer/shell-completion.mjs';
|
|
9
|
+
/** @typedef {{platform:string,target?:object,state:'absent'|'installed'|'incomplete',settings?:object,diagnostics:Error[]}} TargetStatus */
|
|
10
|
+
export async function collectStatus(context){return ['codex','claude'].map(platform=>{
|
|
11
|
+
let target;try{target=resolveTarget(platform,context);const snapshot=new Map(),read=(name,kind='file')=>inspectPath(context.safeFs,name,{kind,read:true,snapshot,tree:kind==='directory',allowLinks:kind==='directory'}),arc=path.join(target.home,'skills/arc'),skill=read(arc,'directory'),profiles=target.profileNames.map(name=>read(path.join(target.home,'agents',name))),instruction=read(target.instructionPath),managed=instruction.exists&&removeManagedBlock(instruction.bytes).changed;
|
|
12
|
+
if(!skill.exists&&!profiles.some(x=>x.exists)&&!managed)return{platform,target,state:'absent',diagnostics:[]};
|
|
13
|
+
const mandatory=name=>{const found=read(path.join(arc,name));if(!found.exists||!found.bytes.length)throw Error(`Incomplete ARC installation: missing ${name} at ${target.home}`);return found.bytes;};
|
|
14
|
+
mandatory('SKILL.md');if(profiles.some(x=>!x.exists||!x.bytes.length))throw Error(`Incomplete ARC installation: missing profile at ${target.home}`);
|
|
15
|
+
const settings={language:parseLanguage(mandatory('settings.toml')).language,modelConfig:loadModelConfig(mandatory('models.toml'),platform),limits:parseLimits(mandatory('limits.toml'))};const native=read(target.runtimeConfigPath);if(native.exists)readRuntimeConcurrency(platform,native.bytes);const shell=read(path.join(context.userHome,'.zshrc'));if(shell.exists)removeShellCompletion(shell.bytes);
|
|
16
|
+
return{platform,target,state:'installed',settings,snapshot,diagnostics:[]};
|
|
17
|
+
}catch(error){return{platform,target,state:'incomplete',diagnostics:[error]};}
|
|
18
|
+
});}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {fileURLToPath} from 'node:url';
|
|
5
|
+
const packageRoot=path.resolve(fileURLToPath(new URL('../../',import.meta.url)));
|
|
6
|
+
const inputError=message=>Object.assign(Error(message),{code:'ARC_CLI_INPUT'});
|
|
7
|
+
const configurationError=message=>Object.assign(Error(message),{code:'ARC_TARGET_CONFIGURATION'});
|
|
8
|
+
function identity(root){const pkg=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));if(pkg.name!=='@yunhozz/arc'||!/^\d+\.\d+\.\d+$/.test(pkg.version))throw configurationError('Invalid ARC package identity');return pkg;}
|
|
9
|
+
export async function runtime(deps){
|
|
10
|
+
const [context,state,upgrade,npm]=await Promise.all([import('../npm/context.mjs'),import('../installer/upgrade-state.mjs'),import('../installer/upgrade.mjs'),import('../npm/upgrade.mjs')]);
|
|
11
|
+
let safeFs=deps.safeFs;if(!safeFs){const[{loadNative},{openRoot}]=await Promise.all([import('../safe-fs/native.mjs'),import('../safe-fs/index.mjs')]);const native=loadNative();safeFs={openRoot:root=>openRoot(root,{native})};}
|
|
12
|
+
return{...context,...state,...upgrade,...npm,...deps,safeFs};
|
|
13
|
+
}
|
|
14
|
+
export function report(result,output,error){
|
|
15
|
+
for(const item of result.results){output.write(item.platform+': '+item.status+'; rollback: '+item.rollback+'\n');for(const filename of item.residualPaths??[])output.write('Residual path: '+filename+'\n');if(item.error)error.write('Error: '+item.error.message+'\n');}
|
|
16
|
+
if(result.interrupted)return 130;
|
|
17
|
+
if(result.results.every(item=>item.status==='installed'&&!item.error)){output.write('ARC version '+result.version+' harness upgraded! Previous harness version: not recorded.\n');return 0;}
|
|
18
|
+
error.write('Harness update did not complete. Run arc again and choose Update Harness, or run npx --yes @yunhozz/arc@latest and choose Update Harness.\n');return 1;
|
|
19
|
+
}
|
|
20
|
+
export function failure(cause,error){error.write('Error: '+cause.message+'\n');return ['ARC_CANCELLED','ARC_NPM_CANCELLED'].includes(cause.code)?130:['ARC_CLI_INPUT','ARC_TARGET_CONFIGURATION','ARC_UNSUPPORTED_RUNTIME','ARC_NATIVE_UNAVAILABLE'].includes(cause.code)?2:1;}
|
|
21
|
+
export async function runChild(request,io={},deps={}){
|
|
22
|
+
const output=io.output??process.stdout,error=io.error??process.stderr;let onInterrupt;
|
|
23
|
+
try{
|
|
24
|
+
if(!request||Object.keys(request).sort().join(',')!=='expectedStateToken,expectedVersion,invocationCwd,protocol'||request.protocol!==1||typeof request.expectedVersion!=='string'||!/^\d+\.\d+\.\d+$/.test(request.expectedVersion)||typeof request.expectedStateToken!=='string'||!/^[a-f0-9]{64}$/.test(request.expectedStateToken)||typeof request.invocationCwd!=='string'||!path.isAbsolute(request.invocationCwd)||fs.realpathSync(request.invocationCwd)!==request.invocationCwd)throw inputError('Invalid private upgrade request');
|
|
25
|
+
const root=deps.packageRoot??packageRoot,pkg=identity(root);if(pkg.version!==request.expectedVersion)throw configurationError('The new ARC package version does not match the requested upgrade');
|
|
26
|
+
const api=await runtime(deps),env=io.env??process.env,cwd=request.invocationCwd,context=await api.inspectNpmContext({packageRoot:root,entryPath:path.join(root,'bin/arc-upgrade.mjs'),env,cwd});
|
|
27
|
+
if(context.kind!=='global'||context.packageRoot!==root)throw configurationError('Cannot verify the replaced global ARC package');
|
|
28
|
+
const state=api.collectInstalledState({safeFs:api.safeFs,userHome:deps.userHome??os.homedir(),env,cwd});if(state.stateToken!==request.expectedStateToken)throw configurationError('Installed ARC settings changed during CLI replacement; retry upgrade');
|
|
29
|
+
if(!state.targets.length)throw configurationError('No installed ARC harness found');
|
|
30
|
+
const controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);process.once('SIGTERM',onInterrupt);
|
|
31
|
+
return report(await api.upgradeInstalledTargets({packageRoot:root,state,safeFs:api.safeFs,signal:controller.signal}),output,error);
|
|
32
|
+
}catch(cause){return failure(cause,error);}finally{if(onInterrupt){process.removeListener('SIGINT',onInterrupt);process.removeListener('SIGTERM',onInterrupt);}}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function prepareUpdate(context){
|
|
36
|
+
const root=context.packageRoot??packageRoot,pkg=identity(root),api=await runtime(context),cwd=fs.realpathSync(context.cwd??process.cwd()),env=context.env??process.env;
|
|
37
|
+
const state=api.collectInstalledState({safeFs:api.safeFs,userHome:context.userHome??os.homedir(),env,cwd});
|
|
38
|
+
if(!state.targets.length)throw configurationError('No installed ARC harness found. Run npx --yes @yunhozz/arc to install it first');
|
|
39
|
+
let plans,version;
|
|
40
|
+
if(!context.upgradeInstalledTargets){const[{verifyPayload},{buildUpgradePlans}]=await Promise.all([import('../installer/manifest.mjs'),import('../installer/upgrade.mjs')]);const payload=verifyPayload({packageRoot:root,safeFs:api.safeFs});plans=buildUpgradePlans({payload,state,safeFs:api.safeFs});version=JSON.parse(payload.files.get('package.json')).version;}
|
|
41
|
+
return{kind:'update',root,pkg,cwd,env,state,plans,version};
|
|
42
|
+
}
|
|
43
|
+
export async function executeUpdate(context,prepared){
|
|
44
|
+
let packageStarted=false;const messages=[];try{
|
|
45
|
+
const api=await runtime(context),{root,pkg,cwd,env,state}=prepared;
|
|
46
|
+
const{assertPlanSnapshot}=await import('../installer/transaction.mjs');for(const entry of state.targets)assertPlanSnapshot(entry,{safeFs:api.safeFs});
|
|
47
|
+
const npmContext=await api.inspectNpmContext({packageRoot:root,entryPath:path.join(root,'bin/arc.mjs'),env,cwd});
|
|
48
|
+
if(!['global','npx'].includes(npmContext.kind))throw configurationError('Run arc from a verified global ARC installation or npx --yes @yunhozz/arc@latest, then choose Update Harness');
|
|
49
|
+
if(npmContext.kind==='npx'){
|
|
50
|
+
let result;if(prepared.plans){const[{installTargets},{acquisitionFor,operationResult}]=await Promise.all([import('../installer/install.mjs'),import('./operations.mjs')]);result=operationResult({version:prepared.version,...await installTargets(prepared.plans,{safeFs:api.safeFs,signal:context.signal,acquisition:acquisitionFor({...context,safeFs:api.safeFs})})});}else{const{operationResult}=await import('./operations.mjs');result=operationResult(await api.upgradeInstalledTargets({packageRoot:root,state,safeFs:api.safeFs,signal:context.signal}));}return result;
|
|
51
|
+
}
|
|
52
|
+
const latest=await api.resolveLatest({context:npmContext,env,cwd,signal:context.signal});packageStarted=true;
|
|
53
|
+
const replaced=await api.upgradeGlobal({context:npmContext,expectedVersion:latest.version,env,cwd,signal:context.signal});if(replaced.packageStatus!=='updated')throw replaced.error??Error('CLI replacement failed');
|
|
54
|
+
messages.push('CLI: updated to '+latest.version+' (previous '+pkg.version+').\n');
|
|
55
|
+
const child=await api.runUpgradeChild({entryPath:path.join(npmContext.packageRoot,'bin/arc-upgrade.mjs'),request:{protocol:1,expectedVersion:latest.version,invocationCwd:cwd,expectedStateToken:state.stateToken},env,cwd,signal:context.signal});
|
|
56
|
+
return{...child,code:child.code===0&&!child.signal?0:child.code===130?130:1,exitSession:true,results:[],messages};
|
|
57
|
+
}catch(error){return{code:['ARC_CANCELLED','ARC_NPM_CANCELLED'].includes(error.code)?130:['ARC_CLI_INPUT','ARC_TARGET_CONFIGURATION','ARC_UNSUPPORTED_RUNTIME','ARC_NATIVE_UNAVAILABLE'].includes(error.code)?2:1,exitSession:packageStarted,results:[],error,messages};}
|
|
58
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {spawnSync} from 'node:child_process';
|
|
4
|
+
import {absolute,inspectPath,observeCooperativeTarget,workflowStatePath,validateWorkflowId} from '../runtime/scope.mjs';
|
|
5
|
+
import {validateCooperativeCommand} from '../runtime/cooperative-schema.mjs';
|
|
6
|
+
import {runWorkflowCommand} from '../runtime/command.mjs';
|
|
7
|
+
const invalid=message=>Object.assign(Error(message),{code:'ARC_CLI_INPUT'});
|
|
8
|
+
export async function observeWorkspace(cwd){
|
|
9
|
+
await inspectPath(cwd,{directory:true});
|
|
10
|
+
const env=Object.fromEntries(Object.entries(process.env).filter(([key])=>!key.toUpperCase().startsWith('GIT_')));
|
|
11
|
+
Object.assign(env,{GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:process.platform==='win32'?'NUL':'/dev/null',GIT_OPTIONAL_LOCKS:'0',GIT_NO_LAZY_FETCH:'1',GIT_TERMINAL_PROMPT:'0'});
|
|
12
|
+
const result=spawnSync('git',['--no-pager','-c','core.fsmonitor=false','rev-parse','--show-toplevel'],{cwd,env,encoding:'utf8',timeout:10000,maxBuffer:1024*1024});
|
|
13
|
+
if(result.status!==0||result.error||!result.stdout.endsWith('\n')||result.stdout.slice(0,-1).includes('\n'))throw invalid('Cannot observe repository root.');
|
|
14
|
+
return observeCooperativeTarget(result.stdout.slice(0,-1));
|
|
15
|
+
}
|
|
16
|
+
export async function readWorkflowJson(filename){
|
|
17
|
+
if(!absolute(filename))throw invalid('An absolute JSON file path is required.');
|
|
18
|
+
await inspectPath(filename);let input;try{input=JSON.parse(await fs.readFile(filename,'utf8'));}catch{throw invalid('Cannot parse workflow JSON file.');}
|
|
19
|
+
return input;
|
|
20
|
+
}
|
|
21
|
+
export async function listWorkflows(cwd){
|
|
22
|
+
const target=await observeWorkspace(cwd),directory=path.join(target.gitCommonDir,'arc','workflows');
|
|
23
|
+
try{await inspectPath(directory,{directory:true});}catch(error){if(error.code==='ENOENT')return[];throw error;}
|
|
24
|
+
const entries=await fs.readdir(directory,{withFileTypes:true}),items=[];
|
|
25
|
+
for(const entry of entries){if(!entry.isDirectory())continue;try{validateWorkflowId(entry.name);}catch{continue;}
|
|
26
|
+
const filename=await workflowStatePath(target,entry.name);try{const state=JSON.parse(await fs.readFile(filename,'utf8'));items.push({id:entry.name,revision:state.revision,target:state.target,foreign:!sameTarget(target,state.target)});}catch(error){if(error.code==='ENOENT')continue;items.push({id:entry.name,invalid:true});}
|
|
27
|
+
}
|
|
28
|
+
return items.sort((a,b)=>a.id.localeCompare(b.id));
|
|
29
|
+
}
|
|
30
|
+
function sameTarget(a,b){return b&&['realpath','gitCommonDir','gitDir','branch','head'].every(key=>a[key]===b[key]);}
|
|
31
|
+
export async function prepareWorkflow({cwd,workflowId,command,input,jsonPath}){
|
|
32
|
+
validateWorkflowId(workflowId);const target=await observeWorkspace(cwd);cwd=target.realpath;const filename=await workflowStatePath(target,workflowId);
|
|
33
|
+
if(jsonPath!==undefined)input=await readWorkflowJson(jsonPath);
|
|
34
|
+
if(command!=='status'){try{validateCooperativeCommand({command,input});}catch{throw invalid('Invalid workflow input. Check the command schema; payload is not displayed.');}}
|
|
35
|
+
else if(input!==undefined)throw invalid('Status takes no input.');
|
|
36
|
+
if(!['init','status','record','prepare','result','reconcile'].includes(command))throw invalid('Invalid workflow command.');
|
|
37
|
+
if(!['status','init'].includes(command)){const state=JSON.parse(await fs.readFile(filename,'utf8'));if(!sameTarget(target,state.target))throw invalid('Workflow target differs from this checkout or HEAD. Use the matching target before mutation.');}
|
|
38
|
+
return{cwd,workflowId,command,input:input===undefined?undefined:structuredClone(input),target};
|
|
39
|
+
}
|
|
40
|
+
// Only expose structural status fields. Evidence payloads, operator statements,
|
|
41
|
+
// request arguments and runtime error text may contain secrets and stay private.
|
|
42
|
+
export function workflowView(body,code){
|
|
43
|
+
const state=body.state;
|
|
44
|
+
return{code,decision:body.decision,workflowId:body.workflowId,revision:body.revision,persisted:body.persisted,ruleIds:body.ruleIds??[],requiredEvidence:(body.requiredEvidence??[]).map(value=>typeof value==='string'?value:'Additional runtime evidence required'),nextActions:(body.nextActions??[]).map(value=>typeof value==='string'?value:'Consult installed runtime instructions'),...(body.error?{error:'Workflow command rejected. Check runtime inputs and target.'}:{}),...(state?{target:state.target,tasks:(state.tasks??[]).map(t=>({id:t.id,status:t.status})),unresolved:(state.unresolved??[]).map(()=> 'Unresolved runtime condition')}: {})};
|
|
45
|
+
}
|
|
46
|
+
export async function executeWorkflow(prepared){
|
|
47
|
+
const current=await observeWorkspace(prepared.cwd);if(!sameTarget(current,prepared.target))return{code:3,decision:'hold',workflowId:prepared.workflowId,ruleIds:['cooperative.target'],error:'Workspace target changed. Review again.',nextActions:[]};
|
|
48
|
+
let text='';const args=[prepared.command,'--cwd',prepared.cwd,'--workflow',prepared.workflowId];if(prepared.command!=='status')args.push('--input','-');
|
|
49
|
+
const code=await runWorkflowCommand(args,{input:JSON.stringify(prepared.input),output:{write(chunk){text+=chunk;}}});
|
|
50
|
+
return workflowView(JSON.parse(text),code);
|
|
51
|
+
}
|
package/lib/models/config.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parse, stringify } from '../../vendor/toml/dist/index.js';
|
|
2
|
-
export const ROLES = Object.freeze(['arc_scouter', 'arc_planner', '
|
|
2
|
+
export const ROLES = Object.freeze(['arc_scouter', 'arc_planner', 'arc_implementer', 'arc_qa', 'arc_reviewer']);
|
|
3
3
|
export class ModelValidationError extends Error {
|
|
4
4
|
constructor(message, options) { super(message, options); this.name = 'ModelValidationError'; this.code = 'ARC_MODEL_VALIDATION'; }
|
|
5
5
|
}
|
package/lib/models/profiles.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { parseToml, exactKeys, validateRoles, effortField, ROLES, fail } from '.
|
|
|
3
3
|
const tools = {
|
|
4
4
|
arc_scouter: ['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch'],
|
|
5
5
|
arc_planner: ['Read', 'Grep', 'Glob'],
|
|
6
|
-
|
|
6
|
+
arc_implementer: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit'],
|
|
7
7
|
arc_qa: ['Read', 'Grep', 'Glob', 'Bash'],
|
|
8
8
|
arc_reviewer: ['Read', 'Grep', 'Glob'],
|
|
9
9
|
};
|
package/lib/policy/placement.mjs
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
const reviews = ['review.significant-no-plan', 'review.significant-plan', 'review.final-plan-recheck'];
|
|
3
3
|
export const POLICY_PROJECTIONS = Object.freeze(Object.fromEntries(Object.entries({
|
|
4
4
|
core: ['scope.non-escalation', 'external.explicit-user-authority', 'role.orchestrator-boundary'],
|
|
5
|
-
assignment: ['git.specialists-deny', 'role.scouter-read-only', 'role.planner-draft-only', 'role.
|
|
6
|
-
planning: ['git.orchestrator-accept-commit', ...reviews],
|
|
7
|
-
handoff: ['git.specialists-deny', 'role.
|
|
8
|
-
preparation: ['scope.non-escalation', 'git.specialists-deny', 'git.orchestrator-accept-commit', 'external.explicit-user-authority'],
|
|
9
|
-
verification: ['git.orchestrator-accept-commit', ...reviews],
|
|
10
|
-
commit: ['scope.non-escalation', 'git.orchestrator-accept-commit', 'external.explicit-user-authority'],
|
|
5
|
+
assignment: ['git.specialists-deny', 'role.scouter-read-only', 'role.planner-draft-only', 'role.implementer-implementation', 'role.qa-verification', 'role.reviewer-independent'],
|
|
6
|
+
planning: ['git.orchestrator-accept-commit', ...reviews, 'planning.plan-amend', 'planning.planner-brief'],
|
|
7
|
+
handoff: ['git.specialists-deny', 'role.implementer-implementation', ...reviews],
|
|
8
|
+
preparation: ['scope.non-escalation', 'git.specialists-deny', 'git.orchestrator-accept-commit', 'external.explicit-user-authority', 'planning.plan-amend'],
|
|
9
|
+
verification: ['git.orchestrator-accept-commit', ...reviews, 'planning.plan-amend'],
|
|
10
|
+
commit: ['scope.non-escalation', 'git.orchestrator-accept-commit', 'external.explicit-user-authority', 'planning.plan-amend'],
|
|
11
11
|
}).map(([name, ids]) => [name, Object.freeze(ids)])));
|
|
12
12
|
|
|
13
13
|
export function validatePolicyProjections(policy, projections = POLICY_PROJECTIONS) {
|
package/lib/policy/render.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import {renderSourceClaudeHook} from './claude-hook.mjs';
|
|
|
2
2
|
import {POLICY_PROJECTIONS, validatePolicyProjections} from './placement.mjs';
|
|
3
3
|
import {validateGitPolicy} from '../../claude/skill/hooks/role-git-guard.mjs';
|
|
4
4
|
import source from './role-policy.json' with {type: 'json'};
|
|
5
|
-
const ROLES = ['orchestrator','scouter','planner','
|
|
5
|
+
const ROLES = ['orchestrator','scouter','planner','implementer','qa','reviewer'];
|
|
6
6
|
const CONTROLS = ['prompt','capability','hook'];
|
|
7
7
|
const fail = message => { throw new Error(`Role policy: ${message}`); };
|
|
8
8
|
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -18,10 +18,10 @@ export function validateRolePolicy(input) {
|
|
|
18
18
|
if (!strings(rule.roles) || rule.roles.some(x=>!ROLES.includes(x)) || !['allow','deny','require'].includes(rule.effect) || !strings(rule.operations) || !strings(rule.evidence) || !['user','orchestrator'].includes(rule.authority) || typeof rule.text !== 'string' || !rule.text || /[\r\n]/.test(rule.text)) fail(`invalid rule ${rule.id}`);
|
|
19
19
|
if (!object(rule.controls) || !same(Object.keys(rule.controls).sort(),['claude','codex']) || Object.values(rule.controls).some(x=>!CONTROLS.includes(x))) fail(`invalid control level for ${rule.id}`);
|
|
20
20
|
}
|
|
21
|
-
for (const id of ['scope.non-escalation','git.specialists-deny','git.orchestrator-accept-commit','external.explicit-user-authority',...ROLES.map(x=>({orchestrator:'role.orchestrator-boundary',scouter:'role.scouter-read-only',planner:'role.planner-draft-only',
|
|
21
|
+
for (const id of ['scope.non-escalation','git.specialists-deny','git.orchestrator-accept-commit','external.explicit-user-authority',...ROLES.map(x=>({orchestrator:'role.orchestrator-boundary',scouter:'role.scouter-read-only',planner:'role.planner-draft-only',implementer:'role.implementer-implementation',qa:'role.qa-verification',reviewer:'role.reviewer-independent'})[x]),'review.significant-no-plan','review.significant-plan','review.final-plan-recheck']) if(!ids.has(id)) fail(`missing required rule ${id}`);
|
|
22
22
|
for (const platform of ['codex','claude']) if (!object(input.platforms?.[platform]) || !CONTROLS.includes(input.platforms[platform].level) || typeof input.platforms[platform].notice !== 'string' || !input.platforms[platform].notice) fail(`invalid ${platform} controls`);
|
|
23
23
|
// These are workflow invariants, not customizable scheduling options.
|
|
24
|
-
const routes = {noPlan:['
|
|
24
|
+
const routes = {noPlan:['implementer','qa','reviewer','orchestrator-accept'],planTask:['implementer','fresh-qa','orchestrator-accept','orchestrator-commit'],planFinal:['required-qa','reviewer'],planFinding:['implementer','reviewer']};
|
|
25
25
|
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');
|
|
26
26
|
try {validateGitPolicy(input.git);} catch {fail('invalid Git policy');}
|
|
27
27
|
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');
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"description": "Return design, spec and plan drafts to Orchestrator."
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
|
-
"id": "
|
|
17
|
+
"id": "implementer",
|
|
18
18
|
"description": "Implement assigned approved changes and focused checks."
|
|
19
19
|
},
|
|
20
20
|
{
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"orchestrator",
|
|
34
34
|
"scouter",
|
|
35
35
|
"planner",
|
|
36
|
-
"
|
|
36
|
+
"implementer",
|
|
37
37
|
"qa",
|
|
38
38
|
"reviewer"
|
|
39
39
|
],
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"roles": [
|
|
59
59
|
"scouter",
|
|
60
60
|
"planner",
|
|
61
|
-
"
|
|
61
|
+
"implementer",
|
|
62
62
|
"qa",
|
|
63
63
|
"reviewer"
|
|
64
64
|
],
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"orchestrator",
|
|
115
115
|
"scouter",
|
|
116
116
|
"planner",
|
|
117
|
-
"
|
|
117
|
+
"implementer",
|
|
118
118
|
"qa",
|
|
119
119
|
"reviewer"
|
|
120
120
|
],
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
"role-ownership"
|
|
151
151
|
],
|
|
152
152
|
"authority": "orchestrator",
|
|
153
|
-
"text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to
|
|
153
|
+
"text": "Assign design/spec/plan/brainstorming drafts ONLY to Planner and implementation/configuration/test edits to Implementer. Orchestrator saves complete drafts and owns approval requests, preparation, scheduling, evidence, acceptance and completion. Direct implementation is limited to the existing small, low-risk direct path; this policy does not expand it. Use a fresh Implementer per top-level task and a fresh QA per top-level task that needs QA; QA findings alone do not create a new task. While approved scope, workspace, ownership and relevant assignment context remain valid, default same-task QA findings to the current Implementer and post-fix rechecks to the current QA. Replace or restart a role only when its context is invalid or unavailable, or the task boundary changes; an approved plan or acceptance-criteria amendment requires a fresh Implementer even within the task. Before reusing Implementer, refresh actual workspace, branch/HEAD and change state; QA reruns the reproduction path and invalidated related checks and reports actual actor, target state, outcomes and limitations. Preserve task-boundary acceptance and the final plan Reviewer correction exception.",
|
|
154
154
|
"controls": {
|
|
155
155
|
"codex": "prompt",
|
|
156
156
|
"claude": "prompt"
|
|
@@ -197,9 +197,9 @@
|
|
|
197
197
|
}
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
|
-
"id": "role.
|
|
200
|
+
"id": "role.implementer-implementation",
|
|
201
201
|
"roles": [
|
|
202
|
-
"
|
|
202
|
+
"implementer"
|
|
203
203
|
],
|
|
204
204
|
"effect": "allow",
|
|
205
205
|
"operations": [
|
|
@@ -233,7 +233,7 @@
|
|
|
233
233
|
"criterion-evidence"
|
|
234
234
|
],
|
|
235
235
|
"authority": "orchestrator",
|
|
236
|
-
"text": "Run approved checks and report commands, observations, reproduction, evidence and limits. Create only essential temporary data/logs/reports in the brief-authorized location and lifetime. Do not edit product source, tests, fixtures or configuration to pass checks, deploy or accept work. Persistent product-state checks require explicit scope and cleanup ownership. Return failures for
|
|
236
|
+
"text": "Run approved checks and report commands, observations, reproduction, evidence and limits. Create only essential temporary data/logs/reports in the brief-authorized location and lifetime. Do not edit product source, tests, fixtures or configuration to pass checks, deploy or accept work. Persistent product-state checks require explicit scope and cleanup ownership. Return failures for Implementer correction and required QA recheck.",
|
|
237
237
|
"controls": {
|
|
238
238
|
"codex": "prompt",
|
|
239
239
|
"claude": "prompt"
|
|
@@ -265,7 +265,7 @@
|
|
|
265
265
|
"orchestrator",
|
|
266
266
|
"scouter",
|
|
267
267
|
"planner",
|
|
268
|
-
"
|
|
268
|
+
"implementer",
|
|
269
269
|
"qa",
|
|
270
270
|
"reviewer"
|
|
271
271
|
],
|
|
@@ -278,7 +278,7 @@
|
|
|
278
278
|
"qa-rechecks"
|
|
279
279
|
],
|
|
280
280
|
"authority": "orchestrator",
|
|
281
|
-
"text": "For significant/high-risk work without a plan:
|
|
281
|
+
"text": "For significant/high-risk work without a plan: Implementer \u2192 QA and required QA rechecks \u2192 Reviewer of the final full change \u2192 Orchestrator acceptance. MUST NOT omit Reviewer merely because no plan exists. This creates no new plan obligation.",
|
|
282
282
|
"controls": {
|
|
283
283
|
"codex": "prompt",
|
|
284
284
|
"claude": "prompt"
|
|
@@ -290,7 +290,7 @@
|
|
|
290
290
|
"orchestrator",
|
|
291
291
|
"scouter",
|
|
292
292
|
"planner",
|
|
293
|
-
"
|
|
293
|
+
"implementer",
|
|
294
294
|
"qa",
|
|
295
295
|
"reviewer"
|
|
296
296
|
],
|
|
@@ -303,7 +303,7 @@
|
|
|
303
303
|
"required-qa"
|
|
304
304
|
],
|
|
305
305
|
"authority": "orchestrator",
|
|
306
|
-
"text": "For significant/high-risk plan work: each task uses
|
|
306
|
+
"text": "For significant/high-risk plan work: each task uses Implementer \u2192 fresh QA as required \u2192 Orchestrator acceptance and focused commit. After all tasks are accepted and required QA is complete, Reviewer reviews the final full change once. Reviewer is NOT a per-task or per-commit required gate. Preserve required checks and necessary finding rechecks.",
|
|
307
307
|
"controls": {
|
|
308
308
|
"codex": "prompt",
|
|
309
309
|
"claude": "prompt"
|
|
@@ -315,7 +315,7 @@
|
|
|
315
315
|
"orchestrator",
|
|
316
316
|
"scouter",
|
|
317
317
|
"planner",
|
|
318
|
-
"
|
|
318
|
+
"implementer",
|
|
319
319
|
"qa",
|
|
320
320
|
"reviewer"
|
|
321
321
|
],
|
|
@@ -327,7 +327,52 @@
|
|
|
327
327
|
"reviewer-findings"
|
|
328
328
|
],
|
|
329
329
|
"authority": "orchestrator",
|
|
330
|
-
"text": "For final overall Reviewer findings on plan work, including behavior findings, use
|
|
330
|
+
"text": "For final overall Reviewer findings on plan work, including behavior findings, use Implementer \u2192 Reviewer without QA reassignment; repeat until resolved. This exception applies only at the final plan stage. Preserve required QA and QA failure rechecks before initial final review and outside this exception.",
|
|
331
|
+
"controls": {
|
|
332
|
+
"codex": "prompt",
|
|
333
|
+
"claude": "prompt"
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
"id": "planning.plan-amend",
|
|
338
|
+
"roles": [
|
|
339
|
+
"orchestrator"
|
|
340
|
+
],
|
|
341
|
+
"effect": "require",
|
|
342
|
+
"operations": [
|
|
343
|
+
"plan-update",
|
|
344
|
+
"plan-commit-amend"
|
|
345
|
+
],
|
|
346
|
+
"evidence": [
|
|
347
|
+
"change-classification",
|
|
348
|
+
"actual-approval-chronology",
|
|
349
|
+
"rewrite-safety",
|
|
350
|
+
"sha-mapping"
|
|
351
|
+
],
|
|
352
|
+
"authority": "orchestrator",
|
|
353
|
+
"text": "During approved task execution, mechanical plan progress records need no new user approval; changed goals, scope, criteria, method, risk, dependencies or required verification require revised-version approval and preparation before affected work. In both cases Orchestrator amends the original planning-document commit at a safe boundary, without a separate document or fixup commit. Preserve actual approval chronology, recoverable old/new contents, hashes, versions and SHA mappings; never retroactively authorize earlier work. Rewrite only unpublished, unshared local linear history with clear ownership and no merges; stop on unknown publication, ownership, unexpected state or replay conflicts, without force push or a fallback document commit. Preserve implementation task boundaries and revalidate affected evidence. See planning, handoff-prep, commit and verification for the full contract.",
|
|
354
|
+
"controls": {
|
|
355
|
+
"codex": "prompt",
|
|
356
|
+
"claude": "prompt"
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"id": "planning.planner-brief",
|
|
361
|
+
"roles": [
|
|
362
|
+
"orchestrator"
|
|
363
|
+
],
|
|
364
|
+
"effect": "require",
|
|
365
|
+
"operations": [
|
|
366
|
+
"planner-brief",
|
|
367
|
+
"planner-feedback"
|
|
368
|
+
],
|
|
369
|
+
"evidence": [
|
|
370
|
+
"complete-stage-brief",
|
|
371
|
+
"prepared-target",
|
|
372
|
+
"preceding-approval"
|
|
373
|
+
],
|
|
374
|
+
"authority": "orchestrator",
|
|
375
|
+
"text": "Before formal drafting, Orchestrator supplies one complete stage-specific brief with original requirements and criteria IDs, approved/open decisions, verified target and ownership, document paths and preceding approved versions, current behavior and connected source/generated/runtime/installer/documentation surfaces, policy conflicts, depth, dependencies, checks, holds and exact deliverable. Preserve shared brief field order. Review the complete draft once and consolidate substantive feedback as changed fields with other fields marked unchanged. Preserve sequential design, spec and plan approvals; no fixed call cap or incomplete drafts, and no calls for already resolved questions.",
|
|
331
376
|
"controls": {
|
|
332
377
|
"codex": "prompt",
|
|
333
378
|
"claude": "prompt"
|
|
@@ -337,7 +382,7 @@
|
|
|
337
382
|
"platforms": {
|
|
338
383
|
"codex": {
|
|
339
384
|
"level": "prompt",
|
|
340
|
-
"notice": "Role restrictions are prompt-level contracts, not a filesystem sandbox. No documented role-specific Git hook or generic per-agent tool allowlist is assumed. Existing shell capabilities are retained because static read access after shell reduction has not been verified;
|
|
385
|
+
"notice": "Role restrictions are prompt-level contracts, not a filesystem sandbox. No documented role-specific Git hook or generic per-agent tool allowlist is assumed. Existing shell capabilities are retained because static read access after shell reduction has not been verified; Implementer and QA need shell for approved checks."
|
|
341
386
|
},
|
|
342
387
|
"claude": {
|
|
343
388
|
"level": "capability",
|
|
@@ -346,13 +391,13 @@
|
|
|
346
391
|
},
|
|
347
392
|
"review": {
|
|
348
393
|
"noPlan": [
|
|
349
|
-
"
|
|
394
|
+
"implementer",
|
|
350
395
|
"qa",
|
|
351
396
|
"reviewer",
|
|
352
397
|
"orchestrator-accept"
|
|
353
398
|
],
|
|
354
399
|
"planTask": [
|
|
355
|
-
"
|
|
400
|
+
"implementer",
|
|
356
401
|
"fresh-qa",
|
|
357
402
|
"orchestrator-accept",
|
|
358
403
|
"orchestrator-commit"
|
|
@@ -362,7 +407,7 @@
|
|
|
362
407
|
"reviewer"
|
|
363
408
|
],
|
|
364
409
|
"planFinding": [
|
|
365
|
-
"
|
|
410
|
+
"implementer",
|
|
366
411
|
"reviewer"
|
|
367
412
|
]
|
|
368
413
|
},
|
|
@@ -580,7 +625,7 @@
|
|
|
580
625
|
"orchestrator",
|
|
581
626
|
"scouter",
|
|
582
627
|
"planner",
|
|
583
|
-
"
|
|
628
|
+
"implementer",
|
|
584
629
|
"qa",
|
|
585
630
|
"reviewer"
|
|
586
631
|
],
|
package/lib/release/artifact.mjs
CHANGED
|
@@ -99,7 +99,7 @@ function validateTarball(bytes,{expectedName,expectedVersion}){
|
|
|
99
99
|
const files=archive(bytes),pkgFile=files.get('package.json');if(!pkgFile)throw new Error('Artifact package metadata is missing');
|
|
100
100
|
const pkg=JSON.parse(pkgFile.bytes);metadata(pkg,undefined,{artifact:true});
|
|
101
101
|
if(pkg.name!==expectedName||pkg.version!==expectedVersion)throw new Error('Artifact package identity mismatch');
|
|
102
|
-
for(const name of ['scripts/postinstall.mjs','lib/cli/postinstall.mjs','lib/
|
|
102
|
+
for(const name of ['scripts/postinstall.mjs','lib/cli/postinstall.mjs','lib/tui/app.mjs','lib/tui/terminal.mjs','lib/cli/root.mjs','lib/cli/upgrade.mjs','bin/arc-upgrade.mjs'])if(!files.has(name))throw new Error('Artifact runtime asset missing: '+name);
|
|
103
103
|
if(files.has('package-lock.json')||[...files.keys()].some(name=>name.split('/').some(part=>part==='.git'||part.startsWith('.arc-release'))))throw new Error('Private staging data in artifact');
|
|
104
104
|
const root=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'arc-release-verify-'));
|
|
105
105
|
try{
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {fingerprintRequest} from './store.mjs';
|
|
3
|
+
import {validateGitPreflight,validateGitCommitResult} from './git-evidence.mjs';
|
|
4
|
+
import {within} from './scope.mjs';
|
|
5
|
+
const reconciliations=new WeakMap();
|
|
6
|
+
export function verifiedCooperativeReconciliation(decision,state){const proof=reconciliations.get(decision);return !!proof&&proof.before===fingerprintRequest(state)&&proof.after===fingerprintRequest(decision.proposedTransition);}
|
|
7
|
+
const completed=(state,id)=>state.tasks?.some(t=>t.id===id&&t.status==='committed'&&!t.invalidated)&&state.commits?.some(c=>c.taskId===id&&c.observationSource==='local-git'&&state.intents.some(i=>i.operation==='commit'&&i.requestId===c.requestId&&i.taskId===id&&i.status==='succeeded'));
|
|
8
|
+
const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b);
|
|
9
|
+
const text=v=>typeof v==='string'&&v.length>0;
|
|
10
|
+
const list=v=>Array.isArray(v)&&v.every(text)&&new Set(v).size===v.length;
|
|
11
|
+
const exact=(a,b)=>list(a)&&list(b)&&same([...a].sort(),[...b].sort());
|
|
12
|
+
const fields=(v,keys)=>v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).every(k=>keys.includes(k));
|
|
13
|
+
const semanticLimit='Semantically unrelated hunks inside the same file require Orchestrator inspection; path and byte evidence cannot distinguish them automatically.';
|
|
14
|
+
const base=state=>({expectedRevision:state?.revision??0,coverage:{enforcement:'cooperative',provenance:'operator-attested',nativeVerified:false},semanticLimit,requiredEvidence:[],nextActions:[],proposedTransition:null});
|
|
15
|
+
const hold=(state,reason)=>({...base(state),decision:'hold',ruleIds:['cooperative.acceptance'],reason});
|
|
16
|
+
const allow=(state,transition)=>({...base(state),decision:'allow',ruleIds:['cooperative.acceptance'],proposedTransition:transition});
|
|
17
|
+
const approval=(state,id)=>state.records.filter(r=>r.kind==='approval'&&(!r.data.affectedTaskIds||r.data.affectedTaskIds.includes(id))).at(-1);
|
|
18
|
+
function authorized(state,task){const a=approval(state,task?.id);return state.mode==='plan'&&task&&!task.invalidated&&a?.source==='operator-attested'&&a.targetVersion===state.targetVersion&&task.targetVersion===state.targetVersion&&task.scopeVersion===a.scopeVersion&&task.approvalRef===a.messageRef&&task.approvalVersion===a.data.approvalVersion&&task.documentVersion===a.data.documentVersion;}
|
|
19
|
+
function checkEvidence(state,task,a,changes){
|
|
20
|
+
if(!same(a.criteria,task.criteria)||!task.criteria.length||!list(a.checkIds)||!list(a.findingIds)||a.changeFingerprint!==changes?.tasks?.[task.id])return false;
|
|
21
|
+
const checks=state.checks.filter(c=>c.scope==='task'&&c.taskId===task.id);
|
|
22
|
+
const latest=task.requiredChecks.map(d=>checks.filter(c=>same(c.descriptor,d)).at(-1));
|
|
23
|
+
if(!task.requiredChecks.length||latest.some(c=>!c||c.stale||c.status!=='passed'||c.verdict!=='proven'||!text(c.resultRef)||c.source!=='operator-attested'||c.verificationActor?.role!=='qa'||!text(c.verificationActor.id)||state.assignments.some(a=>a.agentId===c.verificationActor.id)||c.changeFingerprint!==a.changeFingerprint)||!exact(a.checkIds,latest.map(c=>c?.id)))return false;
|
|
24
|
+
const findings=state.findings.filter(f=>f.scope==='task'&&f.taskId===task.id);
|
|
25
|
+
return exact(a.findingIds,findings.map(f=>f.id))&&findings.every(f=>f.status==='closed'&&text(f.resolutionRef)&&text(f.recheckRef)&&f.resolutionActor?.role==='implementer'&&f.recheckActor?.role==='qa'&&f.changeFingerprint===a.changeFingerprint);
|
|
26
|
+
}
|
|
27
|
+
function waitingFor(state,task,predecessor){
|
|
28
|
+
if(task.status!=='pending'||state.intents.some(i=>i.taskId===task.id&&i.operation==='assign-agent'&&['prepared','unknown','mismatch','succeeded'].includes(i.status)))return false;
|
|
29
|
+
const seen=new Set(),queue=[...(task.dependencies??[])];while(queue.length){const id=queue.pop();if(id===predecessor)return true;if(seen.has(id))continue;seen.add(id);queue.push(...(state.tasks.find(t=>t.id===id)?.dependencies??[]));}return false;
|
|
30
|
+
}
|
|
31
|
+
function dirtyOtherTask(state,task,git){return git.stagedPaths?.some(name=>state.tasks.some(t=>t.id!==task.id&&!completed(state,t.id)&&!waitingFor(state,t,task.id)&&t.paths.some(p=>within(p,path.join(state.target.realpath,...name.split('/'))))));}
|
|
32
|
+
function freshAcceptance(state,task,acceptance,changes){return acceptance&&!acceptance.stale&&authorized(state,task)&&acceptance.taskFingerprint===fingerprintRequest({...task,status:'pending'})&&same(acceptance.approval,approval(state,task.id))&&same(acceptance.target,state.target)&&same(acceptance.versions,state.versions)&&checkEvidence(state,task,acceptance,changes);}
|
|
33
|
+
/** Operator attests semantic sufficiency; code binds exact current evidence only. */
|
|
34
|
+
export function evaluateCooperativeAcceptance({state,request,evidence}={}){
|
|
35
|
+
const task=state?.tasks?.find(t=>t.id===request?.taskId),a=request?.arguments,g=evidence?.git;
|
|
36
|
+
if(request?.expectedRevision!==state?.revision||request.actor?.role!=='orchestrator'||!authorized(state,task)||task.status!=='pending'||state.acceptances.some(v=>v.taskId===task.id&&!v.stale)||state.commits.some(v=>v.taskId===task.id))return hold(state,'Current pending approved task and Orchestrator acceptance required');
|
|
37
|
+
if(!fields(a,['messageRef','criteria','checkIds','findingIds','changeFingerprint'])||!text(a.messageRef)||!checkEvidence(state,task,a,evidence?.changes)||state.checks.some(c=>a.checkIds?.includes(c.id)&&c.verificationActor?.id===request.actor.id))return hold(state,'Exact criteria, current required QA checks and closed findings required');
|
|
38
|
+
const descriptor={kind:'task',parent:state.target.head,indexTree:g?.indexTree,fingerprint:g?.stagedFingerprint,paths:g?.stagedPaths};
|
|
39
|
+
const invalid=validateGitPreflight({target:state.target,descriptor,scope:task.paths,gitEvidence:g});
|
|
40
|
+
if(invalid||dirtyOtherTask(state,task,g))return hold(state,invalid??'Index includes another task changes');
|
|
41
|
+
return allow(state,{acceptance:{...structuredClone(a),id:request.requestId,taskId:task.id,actor:structuredClone(request.actor),source:'operator-attested',descriptor:structuredClone(descriptor),taskFingerprint:fingerprintRequest({...task,status:'pending'}),approval:structuredClone(approval(state,task.id)),target:structuredClone(state.target),versions:structuredClone(state.versions)}});
|
|
42
|
+
}
|
|
43
|
+
/** Prepares a durable intent; never stages or executes a commit. */
|
|
44
|
+
export function prepareCooperativeCommit({state,request,gitEvidence,evidence}={}){
|
|
45
|
+
const task=state?.tasks?.find(t=>t.id===request?.taskId),a=state?.acceptances?.find(a=>a.id===request?.arguments?.acceptanceId&&a.taskId===request.taskId);
|
|
46
|
+
if(request?.expectedRevision!==state?.revision||request.actor?.role!=='orchestrator'||!fields(request.arguments,['acceptanceId'])||task?.status!=='accepted'||!freshAcceptance(state,task,a,evidence?.changes)||state.commits.some(c=>c.taskId===task.id)||state.intents.some(i=>i.operation==='commit'&&i.taskId===task.id&&['prepared','unknown','mismatch'].includes(i.status)))return hold(state,'Exact current acceptance and no prior task commit or unresolved commit required');
|
|
47
|
+
const invalid=validateGitPreflight({target:state.target,descriptor:a.descriptor,scope:task.paths,gitEvidence});
|
|
48
|
+
if(invalid||dirtyOtherTask(state,task,gitEvidence))return hold(state,invalid??'Index includes another task changes');
|
|
49
|
+
const expected={parent:a.descriptor.parent,tree:a.descriptor.indexTree,paths:[...a.descriptor.paths],fingerprint:a.descriptor.fingerprint,remainingPaths:[...new Set([...gitEvidence.workingPaths,...gitEvidence.untrackedPaths])].sort()};
|
|
50
|
+
return allow(state,{intent:{requestId:request.requestId,operation:'commit',taskId:task.id,status:'prepared',acceptanceId:a.id,acceptanceFingerprint:fingerprintRequest(a),expected,target:structuredClone(state.target),versions:structuredClone(state.versions),fingerprint:fingerprintRequest(request),source:'operator-attested'}});
|
|
51
|
+
}
|
|
52
|
+
/** Results are local Git observations, correlated with an operator statement.
|
|
53
|
+
* Mismatch observations remain historical evidence and never rebind the target. */
|
|
54
|
+
export function reconcileCooperativeCommit({state,intent,result,gitEvidence}={}){
|
|
55
|
+
if(!intent||intent.operation!=='commit'||!['prepared','unknown','mismatch'].includes(intent.status)||state.commits.some(c=>c.taskId===intent.taskId))return hold(state,'Matching unresolved commit intent required; replay cannot authorize another effect');
|
|
56
|
+
const task=state.tasks.find(t=>t.id===intent.taskId),a=state.acceptances.find(a=>a.id===intent.acceptanceId),g=gitEvidence;
|
|
57
|
+
const observation=g?structuredClone(g):null;
|
|
58
|
+
const transition={observation,result:{...structuredClone(result),status:'mismatch',source:'operator-attested'}};
|
|
59
|
+
const reject=reason=>({...hold(state,reason),proposedTransition:transition});
|
|
60
|
+
if(!result||!text(result.messageRef)||!fields(result.data,['commit'])||!['succeeded','failed','unknown','mismatch'].includes(result.status))return reject('Exact commit result statement required');
|
|
61
|
+
if(result.status!=='succeeded'){transition.result.status=result.status;return reject('Execution did not establish a successful commit');}
|
|
62
|
+
if(task?.status!=='accepted'||!authorized(state,task)||a?.stale||!a||intent.acceptanceFingerprint!==fingerprintRequest(a)||a.taskFingerprint!==fingerprintRequest({...task,status:'pending'})||!same(a.approval,approval(state,task.id))||!checkEvidence(state,task,a,{tasks:{[task.id]:a.changeFingerprint}})||!same(intent.target,state.target)||!same(intent.versions,state.versions)||!g?.commit||result.data.commit!==g.commit.commit||g.stagedPaths?.length||g.indexTree!==g.commit.tree)return reject('Acceptance, observed commit or clean resulting index mismatch');
|
|
63
|
+
const actual={...g.commit,target:g.target,remainingPaths:[...new Set([...g.workingPaths??[],...g.untrackedPaths??[]])].sort()};
|
|
64
|
+
const invalid=validateGitCommitResult({target:state.target,expected:intent.expected,result:actual,scope:task.paths});
|
|
65
|
+
if(invalid)return reject(invalid);
|
|
66
|
+
transition.result.status='succeeded';transition.commit={...actual,taskId:task.id,requestId:intent.requestId,acceptanceId:a.id,source:'operator-attested',observationSource:'local-git',fingerprint:a.changeFingerprint};transition.target=structuredClone(g.target);
|
|
67
|
+
const decision=allow(state,transition);reconciliations.set(decision,{before:fingerprintRequest(state),after:fingerprintRequest(transition)});return decision;
|
|
68
|
+
}
|