@yunhozz/arc 0.1.10 → 0.2.1

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 (66) hide show
  1. package/MANIFEST.sha256 +59 -50
  2. package/README.md +17 -49
  3. package/claude/agents/arc-implementer.md +4 -2
  4. package/claude/agents/arc-planner.md +2 -2
  5. package/claude/agents/arc-qa.md +3 -3
  6. package/claude/agents/arc-reviewer.md +2 -2
  7. package/claude/agents/arc-scouter.md +2 -2
  8. package/claude/skill/SKILL.md +3 -1
  9. package/claude/skill/references/commit.md +1 -1
  10. package/claude/skill/references/coordination.md +6 -2
  11. package/claude/skill/references/handoff-prep.md +5 -1
  12. package/claude/skill/references/handoff.md +23 -2
  13. package/claude/skill/references/planning.md +11 -2
  14. package/claude/skill/references/verification.md +6 -0
  15. package/claude/skill/references/visual-companion.md +6 -0
  16. package/claude/skill/runtime/bootstrap.mjs +1 -1
  17. package/claude/skill/runtime/cli.mjs +8 -0
  18. package/{lib/cli/workflow.mjs → claude/skill/runtime/command.mjs} +4 -4
  19. package/codex/agents/arc-implementer.toml +5 -2
  20. package/codex/agents/arc-planner.toml +2 -2
  21. package/codex/agents/arc-qa.toml +3 -3
  22. package/codex/agents/arc-reviewer.toml +2 -2
  23. package/codex/agents/arc-scouter.toml +2 -2
  24. package/codex/skill/SKILL.md +3 -1
  25. package/codex/skill/references/commit.md +1 -1
  26. package/codex/skill/references/coordination.md +6 -2
  27. package/codex/skill/references/handoff-prep.md +5 -1
  28. package/codex/skill/references/handoff.md +22 -2
  29. package/codex/skill/references/planning.md +11 -2
  30. package/codex/skill/references/verification.md +6 -0
  31. package/codex/skill/references/visual-companion.md +6 -0
  32. package/codex/skill/runtime/bootstrap.mjs +1 -1
  33. package/codex/skill/runtime/cli.mjs +8 -0
  34. package/codex/skill/runtime/command.mjs +21 -0
  35. package/lib/cli/postinstall.mjs +3 -16
  36. package/lib/cli/root.mjs +8 -19
  37. package/lib/cli/upgrade.mjs +1 -57
  38. package/lib/installer/manifest.mjs +6 -1
  39. package/lib/installer/runtime-assets.mjs +25 -0
  40. package/lib/installer/shell-completion.mjs +15 -20
  41. package/lib/installer/uninstall.mjs +5 -1
  42. package/lib/installer/upgrade-state.mjs +2 -2
  43. package/lib/installer/upgrade.mjs +2 -5
  44. package/lib/management/operations.mjs +39 -0
  45. package/lib/management/status.mjs +18 -0
  46. package/lib/management/update.mjs +58 -0
  47. package/lib/management/workflow.mjs +51 -0
  48. package/lib/policy/role-policy.json +7 -7
  49. package/lib/release/artifact.mjs +1 -1
  50. package/lib/runtime/bootstrap.mjs +1 -1
  51. package/lib/runtime/cli.mjs +8 -0
  52. package/lib/runtime/command.mjs +21 -0
  53. package/lib/tui/app.mjs +96 -0
  54. package/lib/tui/render.mjs +43 -0
  55. package/lib/tui/setup.mjs +21 -0
  56. package/lib/tui/terminal.mjs +81 -0
  57. package/lib/tui/workflow.mjs +12 -0
  58. package/package.json +1 -1
  59. package/scripts/postinstall.mjs +1 -3
  60. package/scripts/update-runtime-assets.mjs +6 -1
  61. package/lib/cli/choices.mjs +0 -47
  62. package/lib/cli/completion.mjs +0 -55
  63. package/lib/cli/index.mjs +0 -55
  64. package/lib/cli/preset.mjs +0 -29
  65. package/lib/cli/terminal.mjs +0 -19
  66. package/lib/cli/uninstall.mjs +0 -17
@@ -0,0 +1,21 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {initializeCooperativeWorkflow,commandResult} from './bootstrap.mjs';
4
+ import {runCooperativeCommand} from './cooperative.mjs';
5
+ import {validateWorkflowId} from './scope.mjs';
6
+ export async function runWorkflowCommand(args,io={}){
7
+ const output=io.output??process.stdout;let workflowId=null;
8
+ try{
9
+ const [command,...rest]=args,options={};
10
+ if(!['init','record','status','reconcile','prepare','result'].includes(command))throw Error('Expected workflow command');
11
+ for(let i=0;i<rest.length;i+=2){const key=rest[i];if(!['--cwd','--workflow','--input'].includes(key)||options[key]!==undefined||typeof rest[i+1]!=='string')throw Error('Invalid workflow option');options[key]=rest[i+1];}
12
+ const cwd=options['--cwd'];workflowId=options['--workflow'];validateWorkflowId(workflowId);
13
+ if(typeof cwd!=='string'||!path.isAbsolute(cwd)||path.normalize(cwd)!==cwd)throw Error('Absolute workspace required');
14
+ if(command==='status'?options['--input']!==undefined:options['--input']!=='-')throw Error('Mutations require --input -; status takes no input');
15
+ // Check/review, commit and external intents are recorded for the operator.
16
+ // Actual Git mutations remain Orchestrator actions; reconciliation observes their results.
17
+ const input=command==='status'?undefined:JSON.parse(io.input??fs.readFileSync(0,'utf8'));
18
+ const result=await(command==='init'?initializeCooperativeWorkflow({cwd,workflowId,input}):runCooperativeCommand({cwd,workflowId,command,input}));
19
+ output.write(JSON.stringify(result)+'\n');return result.decision==='deny'?2:result.decision==='hold'?3:0;
20
+ }catch(error){output.write(JSON.stringify(commandResult({workflowId,decision:'hold',ruleId:'cooperative.error',error:error.message}))+'\n');return 1;}
21
+ }
@@ -1,17 +1,4 @@
1
- import path from 'node:path';
2
- import {inspectNpmContext,resolveInvocationCwd} from '../npm/context.mjs';
3
- import {openInstallerTerminal} from './terminal.mjs';
4
- import {runCli} from './index.mjs';
5
- export async function runPostinstall({packageRoot,env=process.env,cwd=process.cwd(),input=process.stdin,output=process.stdout,error=process.stderr,deps={}}){
6
- let terminal;
7
- try{
8
- if(env.npm_lifecycle_event!=='postinstall'){output.write('Run arc to install the ARC harness.\n');return 0;}
9
- const context=await(deps.inspectNpmContext??inspectNpmContext)({packageRoot,entryPath:path.join(packageRoot,'scripts/postinstall.mjs'),env,cwd});
10
- if(context.kind!=='global'){output.write('Run arc to install the ARC harness.\n');return 0;}
11
- terminal=(deps.openInstallerTerminal??openInstallerTerminal)({env,input,output,error});
12
- if(terminal.kind==='skip'){output.write('Run arc to install the ARC harness.\n');return 0;}
13
- const invocationCwd=resolveInvocationCwd({env,cwd,lifecycle:true});
14
- return await(deps.runCli??runCli)([],{...terminal.io,cwd:invocationCwd,env});
15
- }catch(cause){(terminal?.io.error??error).write(`Error: ${cause.message}\n`);return 1;}
16
- finally{terminal?.dispose?.();}
1
+ export async function runPostinstall({output=process.stdout}={}){
2
+ output.write('Run arc to install or manage the ARC harness.\n');
3
+ return 0;
17
4
  }
package/lib/cli/root.mjs CHANGED
@@ -1,22 +1,11 @@
1
1
  import fs from 'node:fs';
2
- import {renderCompletion} from './completion.mjs';
3
- const usage='Usage: arc [installation options]\n arc preset [economy|balanced|smart] --target <codex|claude|all>\n arc uninstall --target <codex|claude|all>\n arc upgrade [--help]\n arc completion <zsh|bash> (shell setup)\n arc --help | --version';
4
- const completionUsage='Usage: arc completion <zsh|bash>';
2
+ const usage='Usage: arc\n arc --help\n arc --version';
5
3
  export async function runRoot(args=process.argv.slice(2),io={}){
6
- const output=io.output??process.stdout,error=io.error??process.stderr;
7
- const [command,...rest]=args;
8
- if(command==='--help'&&!rest.length){output.write(usage+'\n\n');return(await import('./index.mjs')).runCli(args,io);}
9
- if(command==='--version'&&!rest.length){output.write(JSON.parse(fs.readFileSync(new URL('../../package.json',import.meta.url))).version+'\n');return 0;}
10
- if(command==='workflow')return(await import('./workflow.mjs')).runCli(rest,io);
11
- if(command==='preset')return(await import('./preset.mjs')).runCli(rest,io);
12
- if(command==='upgrade')return(await import('./upgrade.mjs')).runCli(rest,io);
13
- if(command==='install'){error.write('Error: The install subcommand was removed. Run npx --yes @yunhozz/arc [installation options].\n');return 2;}
14
- if(command===undefined||command.startsWith('--'))return(await import('./index.mjs')).runCli(args,io);
15
- if(command==='uninstall')return(await import('./uninstall.mjs')).runCli(rest,io);
16
- if(command==='completion'){
17
- if(rest.length===1&&rest[0]==='--help'){output.write(completionUsage+'\n');return 0;}
18
- if(rest.length===1&&['zsh','bash'].includes(rest[0])){output.write(renderCompletion(rest[0]));return 0;}
19
- error.write('Error: Expected a completion shell (zsh or bash).\n'+completionUsage+'\n');return 2;
20
- }
21
- error.write(`Error: Unknown command: ${command}\n${usage}\n`);return 2;
4
+ const input=io.input??process.stdin,output=io.output??process.stdout,error=io.error??process.stderr;
5
+ if(args.length===1&&args[0]==='--help'){output.write(usage+'\n\nOpen ARC Management in an interactive terminal to install or manage the harness.\n');return 0;}
6
+ if(args.length===1&&args[0]==='--version'){output.write(JSON.parse(fs.readFileSync(new URL('../../package.json',import.meta.url))).version+'\n');return 0;}
7
+ if(args.length){error.write('Error: Unsupported arguments. Open arc to manage the harness.\n'+usage+'\n');return 2;}
8
+ if(!input.isTTY||!output.isTTY){error.write('Error: arc requires an interactive terminal. Run arc directly in a terminal.\n');return 2;}
9
+ const {runTui}=await import('../tui/app.mjs');
10
+ return runTui({...io,input,output,contextOptions:{...io.contextOptions,...(io.cwd?{cwd:io.cwd}:{}),...(io.env?{env:io.env}:{})}});
22
11
  }
@@ -1,57 +1 @@
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 usage='Usage: arc upgrade [--help]';
7
- const inputError=message=>Object.assign(Error(message),{code:'ARC_CLI_INPUT'});
8
- const configurationError=message=>Object.assign(Error(message),{code:'ARC_TARGET_CONFIGURATION'});
9
- 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;}
10
- async function runtime(deps){
11
- 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')]);
12
- 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})};}
13
- return{...context,...state,...upgrade,...npm,...deps,safeFs};
14
- }
15
- function report(result,output,error){
16
- 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');}
17
- if(result.interrupted)return 130;
18
- if(result.results.every(item=>item.status==='installed'&&!item.error)){output.write('ARC version '+result.version+' harness upgraded! Previous harness version: not recorded.\nOpen a new shell session to use updated completion.\nzsh completion: eval "$(arc completion zsh)" for this session\n');return 0;}
19
- error.write('Harness update did not complete. Retry arc upgrade or npx --yes @yunhozz/arc@latest upgrade.\n');return 1;
20
- }
21
- 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;}
22
- export async function runCli(args=process.argv.slice(2),io={},deps={}){
23
- const output=io.output??process.stdout,error=io.error??process.stderr;let controller,onInterrupt,packageStarted=false;
24
- try{
25
- if(args.length===1&&args[0]==='--help'){output.write(usage+'\nUpdates the global CLI and installed harness; npm exec updates the harness only.\n');return 0;}
26
- if(args.length)throw inputError('Expected no arguments or --help');
27
- const root=deps.packageRoot??packageRoot,pkg=identity(root),env=io.env??process.env,cwd=fs.realpathSync(io.cwd??process.cwd()),api=await runtime(deps);
28
- const context=await api.inspectNpmContext({packageRoot:root,entryPath:path.join(root,'bin/arc.mjs'),env,cwd});
29
- if(!['global','npx'].includes(context.kind))throw configurationError('Run upgrade from a verified global ARC installation or npx --yes @yunhozz/arc@latest upgrade');
30
- const state=api.collectInstalledState({safeFs:api.safeFs,userHome:deps.userHome??os.homedir(),env,cwd});
31
- if(!state.targets.length)throw configurationError('No installed ARC harness found. Run npx --yes @yunhozz/arc to install it first');
32
- controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);process.once('SIGTERM',onInterrupt);
33
- if(context.kind==='npx')return report(await api.upgradeInstalledTargets({packageRoot:root,state,safeFs:api.safeFs,signal:controller.signal}),output,error);
34
- const latest=await api.resolveLatest({context,env,cwd,signal:controller.signal});packageStarted=true;
35
- const replaced=await api.upgradeGlobal({context,expectedVersion:latest.version,env,cwd,signal:controller.signal});
36
- if(replaced.packageStatus!=='updated')throw replaced.error??Error('CLI replacement failed');
37
- output.write('CLI: updated to '+latest.version+' (previous '+pkg.version+').\n');
38
- const result=await api.runUpgradeChild({entryPath:path.join(context.packageRoot,'bin/arc-upgrade.mjs'),request:{protocol:1,expectedVersion:latest.version,invocationCwd:cwd,expectedStateToken:state.stateToken},env,cwd,signal:controller.signal});
39
- if(result.stdout)output.write(result.stdout);if(result.stderr)error.write(result.stderr);
40
- if(result.code===0&&!result.signal)return 0;
41
- error.write('CLI was updated but the harness update did not complete. Retry arc upgrade or npx --yes @yunhozz/arc@latest upgrade.\n');return result.code===130?130:1;
42
- }catch(cause){const code=failure(cause,error);if(cause.code==='ARC_CLI_INPUT')error.write(usage+'\n');if(packageStarted)error.write('CLI state may have changed. Retry arc upgrade; if it is unavailable, run npx --yes @yunhozz/arc@latest upgrade.\n');return code;}
43
- finally{if(onInterrupt){process.removeListener('SIGINT',onInterrupt);process.removeListener('SIGTERM',onInterrupt);}}
44
- }
45
- export async function runChild(request,io={},deps={}){
46
- const output=io.output??process.stdout,error=io.error??process.stderr;let onInterrupt;
47
- try{
48
- 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');
49
- 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');
50
- 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});
51
- if(context.kind!=='global'||context.packageRoot!==root)throw configurationError('Cannot verify the replaced global ARC package');
52
- 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');
53
- if(!state.targets.length)throw configurationError('No installed ARC harness found');
54
- const controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);process.once('SIGTERM',onInterrupt);
55
- return report(await api.upgradeInstalledTargets({packageRoot:root,state,safeFs:api.safeFs,signal:controller.signal}),output,error);
56
- }catch(cause){return failure(cause,error);}finally{if(onInterrupt){process.removeListener('SIGINT',onInterrupt);process.removeListener('SIGTERM',onInterrupt);}}
57
- }
1
+ export async function runChild(...args){return(await import('../management/update.mjs')).runChild(...args);}
@@ -1,3 +1,5 @@
1
+ import {COOPERATIVE_RUNTIME_MODULES} from '../runtime/bootstrap.mjs';
2
+ import {RUNTIME_MODULE_PATHS} from './runtime-assets.mjs';
1
3
  import {POLICY_TARGETS,buildPolicyOutputs} from '../policy/render.mjs';
2
4
  import {isDynamicReferencePath,readReferenceBundle} from './superpowers.mjs';
3
5
  import path from 'node:path';import crypto from 'node:crypto';import{loadModelConfig,resolveModelSettings,renderProfiles}from '../models/index.mjs';import{readCheckedFile,typeOf,sameIdentity,metadataOf}from './targets.mjs';
@@ -23,8 +25,11 @@ export function verifyPayload({packageRoot,safeFs}){
23
25
  for(const item of FIXED)select(item,false);for(const item of ROOTS)select(item,true);
24
26
  for(const platform of ['codex','claude'])if(!referenceTrees[platform])throw Error('Required superpowers reference tree is missing');
25
27
  for(const name of listed.keys())if(!files.has(name))throw Error(`Manifest entry is missing: ${name}`);
26
- const required=['claude/skill/hooks/role-git-guard.mjs','claude/skill/hooks/git-policy.json','lib/policy/claude-hook.mjs','lib/policy/role-policy.json','lib/policy/render.mjs','scripts/update-role-policy.mjs','lib/limits.mjs','lib/installer/limits.mjs','lib/installer/runtime-config.mjs','README.md','install.command','package.json','bin/arc.mjs','lib/cli/root.mjs','lib/cli/completion.mjs','lib/installer/shell-completion.mjs','lib/installer/manifest.mjs','lib/installer/targets.mjs','lib/installer/global-config.mjs','lib/installer/plan.mjs','lib/safe-fs/index.mjs','lib/safe-fs/native.mjs','lib/models/index.mjs','lib/models/config.mjs','lib/models/profiles.mjs','vendor/toml/LICENSE','vendor/toml/package.json','vendor/toml/provenance.json','vendor/toml/dist/index.js','prebuilds/index.json'];
28
+ const required=['claude/skill/hooks/role-git-guard.mjs','claude/skill/hooks/git-policy.json','lib/policy/claude-hook.mjs','lib/policy/role-policy.json','lib/policy/render.mjs','scripts/update-role-policy.mjs','lib/limits.mjs','lib/installer/limits.mjs','lib/installer/runtime-config.mjs','README.md','install.command','package.json','bin/arc.mjs','lib/cli/root.mjs','lib/tui/app.mjs','lib/tui/terminal.mjs','lib/tui/render.mjs','lib/tui/setup.mjs','lib/tui/workflow.mjs','lib/management/operations.mjs','lib/management/status.mjs','lib/management/update.mjs','lib/management/workflow.mjs','lib/cli/postinstall.mjs','scripts/postinstall.mjs','lib/cli/upgrade.mjs','bin/arc-upgrade.mjs','lib/installer/shell-completion.mjs','lib/installer/manifest.mjs','lib/installer/targets.mjs','lib/installer/global-config.mjs','lib/installer/plan.mjs','lib/safe-fs/index.mjs','lib/safe-fs/native.mjs','lib/models/index.mjs','lib/models/config.mjs','lib/models/profiles.mjs','vendor/toml/LICENSE','vendor/toml/package.json','vendor/toml/provenance.json','vendor/toml/dist/index.js','prebuilds/index.json'];
29
+ required.push('lib/installer/runtime-assets.mjs',...RUNTIME_MODULE_PATHS);
30
+ for(const platform of ['codex','claude'])required.push(`${platform}/skill/runtime/role-policy.json`,...RUNTIME_MODULE_PATHS.map(name=>`${platform}/skill/runtime/${name.slice('lib/runtime/'.length)}`));
27
31
  for(const tuple of ['darwin-arm64','darwin-x64','linux-arm64-gnu','linux-x64-gnu'])required.push(`prebuilds/${tuple}/arc_safe_fs.node`);
32
+ for(const platform of ['codex','claude'])for(const name of [...COOPERATIVE_RUNTIME_MODULES,'role-policy.json'])required.push(`${platform}/skill/runtime/${name}`);
28
33
  const modelInputs={};for(const platform of ['codex','claude']){const base=`${platform}/skill`;required.push(`${base}/SKILL.md`,`${base}/models.toml`,`${base}/references/limits.md`);const profiles=new Map();for(const role of ['scouter','planner','implementer','qa','reviewer']){const name=`arc-${role}.${platform==='codex'?'toml':'md'}`,key=`${platform}/agents/${name}`;required.push(key);profiles.set(name,files.get(key));}modelInputs[platform]={models:files.get(`${base}/models.toml`),profiles};}
29
34
  for(const name of required)if(!files.has(name))throw Error(`Required runtime asset is missing: ${name}`);
30
35
  const policyInputs=new Map(POLICY_TARGETS.map(({path:name})=>[name,files.get(name)]));
@@ -0,0 +1,25 @@
1
+ // Installation inventory uses repository-root-relative POSIX paths.
2
+ // Keep install-only additions separate from cooperative runtime hash inputs.
3
+ export const RUNTIME_MODULE_PATHS = Object.freeze([
4
+ 'lib/runtime/acceptance.mjs',
5
+ 'lib/runtime/adapters/claude.mjs',
6
+ 'lib/runtime/adapters/codex.mjs',
7
+ 'lib/runtime/assignment.mjs',
8
+ 'lib/runtime/bootstrap.mjs',
9
+ 'lib/runtime/capabilities.mjs',
10
+ 'lib/runtime/cli.mjs',
11
+ 'lib/runtime/command.mjs',
12
+ 'lib/runtime/cooperative-schema.mjs',
13
+ 'lib/runtime/cooperative.mjs',
14
+ 'lib/runtime/evaluate.mjs',
15
+ 'lib/runtime/external.mjs',
16
+ 'lib/runtime/gate.mjs',
17
+ 'lib/runtime/git-evidence.mjs',
18
+ 'lib/runtime/git-query.mjs',
19
+ 'lib/runtime/identity.mjs',
20
+ 'lib/runtime/operations.mjs',
21
+ 'lib/runtime/schema.mjs',
22
+ 'lib/runtime/scope.mjs',
23
+ 'lib/runtime/store.mjs',
24
+ 'lib/runtime/verification.mjs'
25
+ ]);
@@ -1,29 +1,24 @@
1
1
  import path from 'node:path';
2
- import {applyTransaction} from './transaction.mjs';
3
2
  import {inspectPath,TargetConfigurationError} from './targets.mjs';
4
3
  const start='# >>> arc completion >>>',end='# <<< arc completion <<<';
5
- const body='if command -v arc >/dev/null 2>&1; then\n eval "$(arc completion zsh)"\nfi';
6
- export function renderShellCompletion(original){
4
+ export function removeShellCompletion(original){
7
5
  let text;try{text=original===null?'':new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(original);}catch{throw new TargetConfigurationError('.zshrc must be valid UTF-8');}
8
6
  const starts=[...text.matchAll(/# >>> arc completion >>>/g)],ends=[...text.matchAll(/# <<< arc completion <<</g)];
9
- if(starts.length!==ends.length||starts.length>1)throw new TargetConfigurationError('Malformed ARC completion markers');
10
- const newline=text.includes('\r\n')?'\r\n':'\n',block=start+newline+body.replaceAll('\n',newline)+newline+end;
11
- if(starts.length){const first=starts[0].index,last=ends[0].index;if(first>=last||(first>0&&text[first-1]!=='\n')||(last>0&&text[last-1]!=='\n')||!['','\n','\r'].includes(text[last+end.length]??''))throw new TargetConfigurationError('Malformed ARC completion markers');return Buffer.from(text.slice(0,first)+block+text.slice(last+end.length));}
12
- return Buffer.concat([original??Buffer.alloc(0),Buffer.from((text&&!text.endsWith('\n')?newline:'')+block+newline)]);
7
+ const malformed=()=>{throw new TargetConfigurationError('Malformed ARC completion markers');};
8
+ if(starts.length!==ends.length||starts.length>1)malformed();
9
+ if(!starts.length)return{changed:false,content:original};
10
+ const first=starts[0].index,last=ends[0].index;let stop=last+end.length;
11
+ if(first>=last||(first>0&&text[first-1]!=='\n')||(last>0&&text[last-1]!=='\n')||!['','\n','\r\n'].includes(text.slice(stop,stop+2).startsWith('\r')?text.slice(stop,stop+2):text.slice(stop,stop+1)))malformed();
12
+ if(text.slice(stop,stop+2)==='\r\n')stop+=2;else if(text[stop]==='\n')stop++;
13
+ return{changed:true,content:Buffer.from(text.slice(0,first)+text.slice(stop))};
13
14
  }
14
- export function attachShellCompletion(plans,{safeFs}){
15
- const plan=plans[0];if(!plan||plan.skipGlobal)return;
15
+ export function attachShellCompletionCleanup(plans,{safeFs}){
16
+ const plan=plans[0];if(!plan)return;
16
17
  const filename=path.join(plan.target.userHome,'.zshrc'),current=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot:plan.snapshot}),original=current.bytes??null;
18
+ const removed=removeShellCompletion(original);
19
+ for(const item of plans){item.managedPaths=item.managedPaths.filter(name=>name!==filename);item.globalChanges=item.globalChanges.filter(change=>change.path!==filename);if(item!==plan)item.snapshot.delete(filename);}
20
+ if(!removed.changed)return;
17
21
  const metadata=plan.snapshot.get(filename);if(metadata&&(metadata.mode&0o222n)===0n)throw new TargetConfigurationError('.zshrc is not writable');
18
- const content=renderShellCompletion(original);plan.managedPaths.push(filename);
19
- if(original===null||!content.equals(original))plan.globalChanges.push({path:filename,original,content,action:'register completion'});
20
- plan.summary.push(`${filename}: zsh completion; opens automatically in a new zsh session. For this session: eval "$(arc completion zsh)"`);
21
- }
22
-
23
- export function registerShellCompletion(userHome,{safeFs,hooks={}}){
24
- const plan={target:{userHome},globalChanges:[],managedPaths:[],externalPaths:[],snapshot:new Map(),summary:[]};
25
- attachShellCompletion([plan],{safeFs});
26
- if(!plan.globalChanges.length)return{status:'unchanged',residualPaths:[]};
27
- const result=applyTransaction(plan,{safeFs,hooks,specs:plan.globalChanges.map(change=>({path:change.path,bytes:change.content}))});
28
- return{...result,status:result.cleanupError?'failed':'registered'};
22
+ plan.managedPaths.push(filename);plan.globalChanges.push({path:filename,original,content:removed.content,action:'remove managed completion'});
23
+ plan.summary.push(`${filename}: remove managed ARC completion block`);
29
24
  }
@@ -3,6 +3,7 @@ import {inspectPath,sameIdentity,TargetConfigurationError} from './targets.mjs';
3
3
  import {SKILLS,snapshotSkill} from './superpowers.mjs';
4
4
  import {validatePlanSet} from './plan.mjs';
5
5
  import {removeManagedBlock} from './global-config.mjs';
6
+ import {attachShellCompletionCleanup} from './shell-completion.mjs';
6
7
  export function buildUninstallPlan({target,safeFs,skipGlobal=false}){
7
8
  const snapshot=new Map(),removals=[],managedPaths=[path.join(target.home,'skills/arc'),...target.profileNames.map(name=>path.join(target.home,'agents',name))],globalChanges=[];
8
9
  for(const filename of managedPaths){const directory=filename===managedPaths[0];const found=inspectPath(safeFs,filename,{kind:directory?'directory':'file',tree:directory,allowLinks:directory,snapshot});if(found.exists)removals.push(filename);}
@@ -14,11 +15,14 @@ export function buildUninstallPlan({target,safeFs,skipGlobal=false}){
14
15
  }
15
16
  for(const skill of linkedSkills){const root=arc+'/references/superpowers/'+skill,directory=safeFs.openRoot(root);try{if(!sameIdentity(snapshot.get(root),directory.stat()))throw new TargetConfigurationError('Removal reference root changed');snapshotSkill(directory,root,[]);}finally{directory.close();}}
16
17
  if(!skipGlobal){const filename=target.instructionPath??target.globalPaths[0],found=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot});managedPaths.push(filename);if(found.exists){const removed=removeManagedBlock(found.bytes);if(removed.changed)globalChanges.push({path:filename,content:removed.content});}}
17
- return{target,skipGlobal,snapshot,removals,managedPaths,externalPaths:[],globalChanges,summary:[target.platform==='codex'?'Codex config.toml wait settings and native concurrency key are preserved.':'Claude settings.json native concurrency key is preserved.']};
18
+ const plan={target,skipGlobal,snapshot,removals,managedPaths,externalPaths:[],globalChanges,summary:[target.platform==='codex'?'Codex config.toml wait settings and native concurrency key are preserved.':'Claude settings.json native concurrency key is preserved.']};
19
+ attachShellCompletionCleanup([plan],{safeFs});return plan;
18
20
  }
19
21
  import {assertPlanSnapshot,applyUninstallTransaction} from './transaction.mjs';
20
22
  export async function uninstallTargets(plans,{safeFs,signal,hooks={}}){
21
23
  const results=plans.map(plan=>({platform:plan.target.platform,status:'not-started',rollback:'not-needed',residualPaths:[],error:null}));let invalid=false;
24
+ // Each standalone preview retains its snapshot; only the first plan owns the shared shell write.
25
+ for(const plan of plans.slice(1)){const filename=path.join(plans[0].target.userHome,'.zshrc');plan.managedPaths=plan.managedPaths.filter(name=>name!==filename);plan.globalChanges=plan.globalChanges.filter(change=>change.path!==filename);plan.snapshot.delete(filename);}
22
26
  try{validatePlanSet(plans);}catch(error){return{results:results.map(item=>({...item,status:'failed',error})),interrupted:false};}
23
27
  for(let i=0;i<plans.length;i++)try{assertPlanSnapshot(plans[i],{safeFs});}catch(error){results[i]={...results[i],status:'failed',error};invalid=true;}
24
28
  if(invalid)return{results,interrupted:false};
@@ -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 {renderShellCompletion} from './shell-completion.mjs';
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)renderShellCompletion(shell.bytes);
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 {renderShellCompletion} from './shell-completion.mjs';
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
- const first=plans[0];if(first){const filename=path.join(first.target.userHome,'.zshrc'),current=inspectPath(safeFs,filename,{kind:'file',read:true,snapshot:first.snapshot});
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
+ }
@@ -47,7 +47,7 @@
47
47
  "assigned-role"
48
48
  ],
49
49
  "authority": "orchestrator",
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.",
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. For an optional read method failure, use only a pre-authorized, side-effect-free read alternative against the same positively identified material, approved version, purpose and role authority, with equivalent evidence conditions. A different locator requires the exact locator in the brief in advance and confirmed material identity/version; discovery is not authorization. Missing required input, authority or target mismatch, ownership conflict, partial mutation, unknown state, or an alternative that is unapproved, ambiguous or fails requires immediate return to Orchestrator before dependent work. Permit no chained discovery, guessed paths, mutation retries, dependency/environment/authority changes, or repetition of completed work. Report the first failure, alternative, evidence, limits, dependent work held and independent work remaining. First, record the exact failed check, its result and evidence, baseline revision and current revision/change state. Second, classify each failure as baseline or current, recording affected scope and dependency relationships. Third, hold only affected or dependent tasks and checks; when classification is unknown, hold only work whose impact is unknown. Fourth, Orchestrator must release already-approved independent work without waiting for unrelated failures, after confirming valid authority, target, inputs, ownership and gates for that work. Finally, keep acceptance held while any required check remains unresolved. Specialists report evidence and remaining work and must not self-authorize release; an independent-work release never suppresses a failed check, expands scope, changes criteria or waives required verification. Distinguish user-provided instructions, discovered workspace instruction files, installed role references and task inputs by source, applicability, version/approval and access state (available, absent, inaccessible, conflicting or unverified). A root AGENTS.md file absence is not evidence of absence of instructions and alone is not a blocker when applicable instructions were user-provided. Missing required inputs or unresolved instruction conflicts still hold dependent work; retrieved content does not grant authority. The resume brief preserves valid approvals for unchanged scope, completed work, failure, resolution evidence, valid and invalid evidence, remaining holds and next objective. Orchestrator revalidates affected gates after resolution and records changed facts without repeating completed work or treating a restart as new authority. Preserve all role, Git, preservation, approval, QA/recheck, final Reviewer, acceptance and commit boundaries.",
51
51
  "controls": {
52
52
  "codex": "prompt",
53
53
  "claude": "prompt"
@@ -211,7 +211,7 @@
211
211
  "tdd-or-recorded-exception"
212
212
  ],
213
213
  "authority": "orchestrator",
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.",
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. Carry the exact approved visual source, version, approval and access state, criterion IDs, required properties and comparison method, and actual-output identity, revision and comparison basis through Implementer and fresh QA handoffs. Record each visual result as pass, mismatch or unverified with accessible evidence and limits. Implementer self-report does not replace fresh QA and its actual comparison. A missing output-to-revision link is unverified; a visible difference is mismatch separately, without inferring current branch UI status. Missing source/output, inaccessible evidence or an unapproved interpretation holds the affected visual claim for Orchestrator; do not silently substitute or redesign.",
215
215
  "controls": {
216
216
  "codex": "prompt",
217
217
  "claude": "prompt"
@@ -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 Implementer correction and required QA recheck.",
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. Carry the exact approved visual source, version, approval and access state, criterion IDs, required properties and comparison method, and actual-output identity, revision and comparison basis through Implementer and fresh QA handoffs. Record each visual result as pass, mismatch or unverified with accessible evidence and limits. Implementer self-report does not replace fresh QA and its actual comparison. A missing output-to-revision link is unverified; a visible difference is mismatch separately, without inferring current branch UI status. Missing source/output, inaccessible evidence or an unapproved interpretation holds the affected visual claim for Orchestrator; do not silently substitute or redesign.",
237
237
  "controls": {
238
238
  "codex": "prompt",
239
239
  "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: 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.",
281
+ "text": "For significant/high-risk work without a plan: Implementer → QA and required QA rechecks → Reviewer of the final full change → 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 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.",
306
+ "text": "For significant/high-risk plan work: each task uses Implementer → 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.",
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 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.",
330
+ "text": "For final overall Reviewer findings on plan work, including behavior findings, use Implementer → 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"
@@ -372,7 +372,7 @@
372
372
  "preceding-approval"
373
373
  ],
374
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.",
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. Include instruction provenance and input states, required skill/reference paths, bounded read alternatives and return conditions, approved visual sources/versions and criteria, and unresolved interpretation decisions before dependent drafting. Planner returns incomplete required inputs to Orchestrator; optional reading recovery never grants preparation, verification, writing or approval authority.",
376
376
  "controls": {
377
377
  "codex": "prompt",
378
378
  "claude": "prompt"
@@ -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/cli/terminal.mjs'])if(!files.has(name))throw new Error('Artifact runtime asset missing: '+name);
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{