agent-policy-kit 0.2.0 → 0.4.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.
Files changed (60) hide show
  1. package/AGENT_INSTALL.md +126 -42
  2. package/CHANGELOG.md +93 -3
  3. package/README.md +21 -53
  4. package/bin/agent-policy.mjs +10 -10
  5. package/docs/README.md +46 -0
  6. package/docs/assets/configurator.png +0 -0
  7. package/docs/assets/subagent-limits.svg +44 -0
  8. package/docs/design/2026-11-01-documentation-structure.md +58 -0
  9. package/docs/guide/commands.md +375 -0
  10. package/docs/guide/configurator.md +163 -0
  11. package/docs/guide/getting-started.md +119 -0
  12. package/docs/guide/preferences.md +168 -0
  13. package/docs/guide/privacy.md +76 -0
  14. package/docs/guide/updates-and-removal.md +188 -0
  15. package/docs/maintainers/architecture.md +71 -0
  16. package/docs/maintainers/development.md +93 -0
  17. package/docs/maintainers/model-review.md +101 -0
  18. package/docs/maintainers/release.md +114 -0
  19. package/docs/maintainers/verification.md +95 -0
  20. package/docs/maintainers/website.md +112 -0
  21. package/docs/reference/assistants.md +165 -0
  22. package/docs/reference/model-catalog.md +194 -0
  23. package/docs/reference/ownership.md +177 -0
  24. package/docs/reference/presets.md +128 -0
  25. package/docs/reference/roles.md +157 -0
  26. package/docs/research/2026-11-01-model-review.md +341 -0
  27. package/docs/research/limits-evidence.md +28 -0
  28. package/models/README.md +2 -6
  29. package/models/catalog.json +57 -26
  30. package/package.json +7 -3
  31. package/policies/balanced.json +3 -3
  32. package/policies/fast.json +3 -13
  33. package/policies/quality.json +4 -14
  34. package/schema/policy.schema.json +2 -2
  35. package/site/index.html +108 -60
  36. package/src/detect.mjs +61 -13
  37. package/src/explicit.mjs +4 -4
  38. package/src/inspect.mjs +14 -13
  39. package/src/install.mjs +19 -13
  40. package/src/native.mjs +8 -8
  41. package/src/output.mjs +5 -5
  42. package/src/policy.mjs +2 -2
  43. package/src/release.mjs +1 -1
  44. package/src/setup.mjs +1 -1
  45. package/src/share.mjs +5 -5
  46. package/docs/architecture.md +0 -17
  47. package/docs/compatibility.md +0 -23
  48. package/docs/configuration-page.md +0 -44
  49. package/docs/deployment.md +0 -31
  50. package/docs/development.md +0 -21
  51. package/docs/installation.md +0 -23
  52. package/docs/model-catalog.md +0 -102
  53. package/docs/model-updates.md +0 -22
  54. package/docs/npm-release.md +0 -27
  55. package/docs/policy.md +0 -16
  56. package/docs/privacy.md +0 -9
  57. package/docs/recovery.md +0 -13
  58. package/docs/releases.md +0 -40
  59. package/docs/use-cases.md +0 -25
  60. package/docs/verification.md +0 -45
package/src/detect.mjs CHANGED
@@ -4,17 +4,65 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { spawn } from 'node:child_process';
6
6
 
7
- const CLIENTS = ['codex','claude','kimi','agy','grok'];
7
+ const CLIENTS = ['chatgpt','claude','kimi','gemini','grok'];
8
+ // Each tool's own executable keeps its native name: ChatGPT runs Codex, and Gemini runs Antigravity (agy).
9
+ const EXECUTABLES = {chatgpt:'codex',claude:'claude',kimi:'kimi',gemini:'agy',grok:'grok'};
8
10
  const OUTPUT_LIMIT = 65536;
9
11
  const VERSION = String.raw`(\d{1,4}\.\d{1,4}\.\d{1,4}(?:-[A-Za-z0-9][A-Za-z0-9.-]{0,63})?(?:\+[A-Za-z0-9][A-Za-z0-9.-]{0,63})?)`;
10
12
  function failure(code) { return Object.assign(new Error(code),{code}); }
11
- function options({env=process.env,hostHome=os.homedir(),timeoutMs=5000} = {}) {
12
- if (!env || typeof env !== 'object' || typeof hostHome !== 'string' || !path.isAbsolute(hostHome) || hostHome.includes('\0') || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000) throw failure('INVALID_DETECTION_OPTIONS');
13
- return {env,hostHome:path.resolve(hostHome),timeoutMs};
13
+ const absolute = value => typeof value === 'string' && path.isAbsolute(value) && !value.includes('\0');
14
+ function options({env=process.env,hostHome=os.homedir(),timeoutMs=5000,platform=process.platform,systemApplications} = {}) {
15
+ // AGENT_POLICY_APPLICATIONS_DIR replaces /Applications, so hermetic runs never see the host's installed apps.
16
+ systemApplications ??= (typeof env?.AGENT_POLICY_APPLICATIONS_DIR === 'string' && env.AGENT_POLICY_APPLICATIONS_DIR) || '/Applications';
17
+ if (!env || typeof env !== 'object' || !absolute(hostHome) || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 30000 || typeof platform !== 'string' || !absolute(systemApplications)) throw failure('INVALID_DETECTION_OPTIONS');
18
+ return {env,hostHome:path.resolve(hostHome),timeoutMs,platform,systemApplications:path.resolve(systemApplications)};
14
19
  }
15
- function candidates(client,{env,hostHome}) {
20
+ // Desktop apps ship their own copy of a CLI and keep it off PATH. ChatGPT Desktop runs Codex from a
21
+ // fixed bundle path; Claude Desktop keeps one Claude Code per <version>/<build> directory.
22
+ const CHATGPT_CODEX = 'ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex';
23
+ const CLAUDE_DESKTOP = 'Library/Application Support/Claude/claude-code';
24
+ const DESKTOP_VERSION = /^(\d{1,4})\.(\d{1,4})\.(\d{1,4})$/;
25
+ const DESKTOP_BUILD = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
26
+ const DESKTOP_LIMIT = 8;
27
+ async function directories(dir) {
28
+ try { return (await fs.readdir(dir,{withFileTypes:true})).filter(entry=>entry.isDirectory()).map(entry=>entry.name); }
29
+ catch { return []; }
30
+ }
31
+ function newerFirst(a,b) {
32
+ const [x,y] = [a,b].map(name=>name.match(DESKTOP_VERSION).slice(1).map(Number));
33
+ return y[0]-x[0] || y[1]-x[1] || y[2]-x[2];
34
+ }
35
+ async function desktopCandidates(client,{hostHome,platform,systemApplications}) {
36
+ if (platform !== 'darwin') return [];
37
+ if (client === 'chatgpt') return [systemApplications,path.join(hostHome,'Applications')].map(dir=>path.join(dir,CHATGPT_CODEX));
38
+ if (client !== 'claude') return [];
39
+ const base = path.join(hostHome,CLAUDE_DESKTOP), files = [];
40
+ for (const version of (await directories(base)).filter(name=>DESKTOP_VERSION.test(name)).sort(newerFirst)) {
41
+ // Only builds that contain the CLI count toward the limit, so empty or partial folders cannot hide a working one.
42
+ // Within one version the most recently written CLI comes first; the name breaks ties.
43
+ const builds = [];
44
+ for (const build of (await directories(path.join(base,version))).filter(name=>DESKTOP_BUILD.test(name))) {
45
+ const file = path.join(base,version,build,'claude.app/Contents/MacOS/claude');
46
+ try { const stat = await fs.stat(file); if (stat.isFile()) builds.push({file,build,mtime:stat.mtimeMs}); } catch {}
47
+ }
48
+ builds.sort((a,b)=>b.mtime-a.mtime || (a.build < b.build ? -1 : a.build > b.build ? 1 : 0));
49
+ for (const {file} of builds) {
50
+ files.push(file);
51
+ // The limit bounds detection time: each candidate that exists is started once with --version.
52
+ if (files.length === DESKTOP_LIMIT) return files;
53
+ }
54
+ }
55
+ return files;
56
+ }
57
+ async function candidates(client,settings) {
58
+ const result = cliCandidates(client,settings);
59
+ for (const file of await desktopCandidates(client,settings)) result.push({file,origin:'desktop_app'});
60
+ return result.filter((candidate,index) => result.findIndex(c=>c.file === candidate.file) === index);
61
+ }
62
+ function cliCandidates(client,{env,hostHome}) {
16
63
  const dirs = typeof env.PATH === 'string' ? env.PATH.split(path.delimiter) : [];
17
- const names = process.platform === 'win32' ? [client + '.exe',client] : [client];
64
+ const executable = EXECUTABLES[client];
65
+ const names = process.platform === 'win32' ? [executable + '.exe',executable] : [executable];
18
66
  const result = [];
19
67
  for (const dir of dirs) {
20
68
  // Never search the working directory through empty/relative PATH segments.
@@ -23,9 +71,9 @@ function candidates(client,{env,hostHome}) {
23
71
  }
24
72
  const conventional = client === 'kimi' ? ['.kimi-code/bin/kimi']
25
73
  : client === 'claude' ? ['.local/bin/claude','.claude/local/claude']
26
- : [`.local/bin/${client}`];
74
+ : [`.local/bin/${executable}`];
27
75
  for (const relative of conventional) result.push({file:path.join(hostHome,relative),origin:'known_location'});
28
- return result.filter((candidate,index) => result.findIndex(c=>c.file === candidate.file) === index);
76
+ return result;
29
77
  }
30
78
  async function inspect(file) {
31
79
  try {
@@ -78,10 +126,10 @@ function probe(executable,arg,{env,timeoutMs}) {
78
126
  }
79
127
  function identity(client,output) {
80
128
  const patterns = {
81
- codex:[new RegExp(String.raw`^\s*codex(?:-cli)?\s+v?${VERSION}(?:\s|$)`,'im')],
129
+ chatgpt:[new RegExp(String.raw`^\s*codex(?:-cli)?\s+v?${VERSION}(?:\s|$)`,'im')],
82
130
  claude:[new RegExp(String.raw`^\s*v?${VERSION}\s+\(Claude Code\)(?:\s|$)`,'im'),new RegExp(String.raw`^\s*Claude Code\s+v?${VERSION}(?:\s|$)`,'im')],
83
131
  kimi:[new RegExp(String.raw`^\s*kimi(?:\s+code(?:\s+cli)?)?(?:,?\s+version)?\s+v?${VERSION}(?:\s|$)`,'im')],
84
- agy:[new RegExp(String.raw`^\s*(?:Antigravity|agy)\s+v?${VERSION}(?:\s|$)`,'im')],
132
+ gemini:[new RegExp(String.raw`^\s*(?:Antigravity|agy)\s+v?${VERSION}(?:\s|$)`,'im')],
85
133
  grok:[new RegExp(String.raw`^\s*grok\s+v?${VERSION}(?:\s|$)`,'im')],
86
134
  };
87
135
  for (const pattern of patterns[client]) { const match = output.match(pattern); if (match && match[1].length <= 128) return match[1]; }
@@ -92,7 +140,7 @@ async function verify(client,executable,opts) {
92
140
  if (version.status !== 'ok') return {status:version.status};
93
141
  const branded = identity(client,version.output);
94
142
  if (branded) return {status:'detected',version:branded};
95
- if (client !== 'kimi' && client !== 'agy') return {status:'invalid'};
143
+ if (client !== 'kimi' && client !== 'gemini') return {status:'invalid'};
96
144
  const number = version.output.match(new RegExp(String.raw`^\s*v?${VERSION}(?:\s|$)`));
97
145
  if (!number || number[1].length > 128) return {status:'invalid'};
98
146
  const help = await probe(executable,'--help',opts);
@@ -110,7 +158,7 @@ export async function resolveClient(client,opts = {}) {
110
158
  const settings = options(opts);
111
159
  let best = {client,status:'not_found',version:null,origin:null,executable:null};
112
160
  const checked = new Set();
113
- for (const candidate of candidates(client,settings)) {
161
+ for (const candidate of await candidates(client,settings)) {
114
162
  const inspected = await inspect(candidate.file);
115
163
  if (inspected.status === 'not_found') continue;
116
164
  if (inspected.status === 'invalid') {
@@ -132,6 +180,6 @@ export function summarizeDetection(records) {
132
180
  return records.filter(r=>CLIENTS.includes(r?.client)).map(({client,status,version,origin})=>{
133
181
  status = ['detected','not_found','invalid','timeout'].includes(status) ? status : 'invalid';
134
182
  if (status === 'detected' && (typeof version !== 'string' || version.length > 128 || !new RegExp(`^${VERSION}$`).test(version))) status = 'invalid';
135
- return {client,status,version:status === 'detected' ? version : null,origin:['path','known_location'].includes(origin) ? origin : null};
183
+ return {client,status,version:status === 'detected' ? version : null,origin:['path','known_location','desktop_app'].includes(origin) ? origin : null};
136
184
  });
137
185
  }
package/src/explicit.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // Pure command codec, also embedded in the offline configuration page.
2
- export const TOOL_FLAGS=Object.freeze({codex:'codex',claude:'claude',kimi:'kimi',antigravity:'agy',grok:'grok'});
2
+ export const TOOL_FLAGS=Object.freeze({chatgpt:'chatgpt',claude:'claude',kimi:'kimi',gemini:'gemini',grok:'grok'});
3
3
  export const EXPLICIT_FIELDS=Object.freeze(['roles','max-concurrent',...Object.keys(TOOL_FLAGS),...Object.keys(TOOL_FLAGS).map(flag=>'avoid-'+flag),...Object.keys(TOOL_FLAGS).map(flag=>flag+'-max-concurrent')]);
4
4
  const EXPLICIT_MODEL=/^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}$/;
5
5
  const EXPLICIT_EFFORTS=['none','minimal','low','medium','high','xhigh','max'];
@@ -31,7 +31,7 @@ export function parseExplicitOptions(options,roleIds){
31
31
  const at=entry.indexOf('='),role=entry.slice(0,at).trim(),value=entry.slice(at+1).trim();
32
32
  if(at<1||!roles.includes(role)||Object.hasOwn(choices,role))invalidExplicit();
33
33
  let model=value,effort=null;
34
- if(client==='agy'){if(!['pro','flash','flash_lite','inherit'].includes(model))invalidExplicit();}
34
+ if(client==='gemini'){if(!['pro','flash','flash_lite','inherit'].includes(model))invalidExplicit();}
35
35
  else if(client==='grok'){if(grokEffortSuffix(value))invalidExplicit();}
36
36
  else{
37
37
  const colon=value.lastIndexOf(':');if(colon<1)invalidExplicit();
@@ -68,9 +68,9 @@ export function explicitOptions(config,roleCatalog){
68
68
  options[flag]=roles.map(role=>{
69
69
  const selected=preferences.roles?.[role]??preferences[kinds.get(role)];
70
70
  if(!selected||typeof selected.model!=='string'||!EXPLICIT_MODEL.test(selected.model)||(selected.effort!==null&&!EXPLICIT_EFFORTS.includes(selected.effort)))invalidExplicit();
71
- if(['agy','grok'].includes(client)&&selected.effort!==null)invalidExplicit();
71
+ if(['gemini','grok'].includes(client)&&selected.effort!==null)invalidExplicit();
72
72
  if(client==='grok'&&grokEffortSuffix(selected.model))invalidExplicit();
73
- return `${role}=${selected.model}${['agy','grok'].includes(client)?'':':'+(selected.effort??'native')}`;
73
+ return `${role}=${selected.model}${['gemini','grok'].includes(client)?'':':'+(selected.effort??'native')}`;
74
74
  }).join(',');
75
75
  if(preferences.maxConcurrent!==undefined)options[flag+'-max-concurrent']=String(preferences.maxConcurrent);
76
76
  if(!Array.isArray(preferences.avoidModels)||preferences.avoidModels.some(model=>typeof model!=='string'||!EXPLICIT_MODEL.test(model)))invalidExplicit();
package/src/inspect.mjs CHANGED
@@ -5,8 +5,8 @@ import {parseToml} from './toml.mjs';
5
5
  import {CLIENTS} from './policy.mjs';
6
6
  import {inspectConfiguration} from './install.mjs';
7
7
 
8
- const ROOTS={codex:'.codex',claude:'.claude',kimi:'.kimi-code',agy:'.gemini',grok:'.grok'};
9
- const ENV_ROOTS={codex:'CODEX_HOME',claude:'CLAUDE_CONFIG_DIR',kimi:'KIMI_CODE_HOME',grok:'GROK_HOME'};
8
+ const ROOTS={chatgpt:'.codex',claude:'.claude',kimi:'.kimi-code',gemini:'.gemini',grok:'.grok'};
9
+ const ENV_ROOTS={chatgpt:'CODEX_HOME',claude:'CLAUDE_CONFIG_DIR',kimi:'KIMI_CODE_HOME',grok:'GROK_HOME'};
10
10
  const MAX_FILE=1024*1024,MAX_TOTAL=4*MAX_FILE,MAX_ENTRIES=256;
11
11
  const identifier=/^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}$/;
12
12
  const namePattern=/^[A-Za-z0-9][A-Za-z0-9 _.-]{0,79}$/;
@@ -88,7 +88,7 @@ function extractSettings(client,id,native,env,models){
88
88
  if(subagents.max_concurrent!==undefined)client.concurrency.value=cap(subagents.max_concurrent);
89
89
  return;
90
90
  }
91
- if(id==='codex'){
91
+ if(id==='chatgpt'){
92
92
  const agents=native.agents??{};if(!record(agents))throw failure('UNSUPPORTED_NATIVE_SETTING');
93
93
  client.defaults.model=optionalString(agents.default_subagent_model);
94
94
  client.defaults.effort=optionalString(agents.default_subagent_reasoning_effort);
@@ -100,10 +100,11 @@ function extractSettings(client,id,native,env,models){
100
100
  }else if(id==='claude'){
101
101
  const stored=native.env??{};if(!record(stored))throw failure('UNSUPPORTED_NATIVE_SETTING');
102
102
  client.defaults.model=optionalString(stored.CLAUDE_CODE_SUBAGENT_MODEL);
103
+ client.defaults.effort=optionalString(stored.CLAUDE_CODE_SUBAGENT_EFFORT);
103
104
  const count=stored.CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS;
104
105
  if(count!==undefined){if(typeof count!=='string'||!/^\d{1,9}$/.test(count))throw failure('UNSUPPORTED_NATIVE_SETTING');client.concurrency.value=cap(Number(count));}
105
106
  client.overrides={};
106
- for(const[key,field]of [['CLAUDE_CODE_SUBAGENT_MODEL','model'],['CLAUDE_CODE_EFFORT_LEVEL','effort']]){
107
+ for(const[key,field]of [['CLAUDE_CODE_SUBAGENT_MODEL','model'],['CLAUDE_CODE_SUBAGENT_EFFORT','subagentEffort'],['CLAUDE_CODE_EFFORT_LEVEL','effort']]){
107
108
  const value=env[key]??(field==='effort'?stored[key]:undefined);
108
109
  if(value!==undefined)client.overrides[field]=safeString(value);
109
110
  }
@@ -132,14 +133,14 @@ async function rolesIn(directory,client,id,budget,models,depth=0){
132
133
  try{
133
134
  if(entry.isSymbolicLink())throw failure('UNSAFE_FILE');
134
135
  if(entry.isDirectory()){
135
- if(id==='codex')continue;
136
+ if(id==='chatgpt')continue;
136
137
  if(depth>=4)throw failure('INSPECTION_LIMIT');
137
138
  await rolesIn(file,client,id,budget,models,depth+1);continue;
138
139
  }
139
- if(!entry.name.endsWith(id==='codex'?'.toml':'.md'))continue;
140
+ if(!entry.name.endsWith(id==='chatgpt'?'.toml':'.md'))continue;
140
141
  const body=await readFile(file,budget);if(body===null)throw failure('CONFIG_CHANGED_DURING_READ');
141
- const fields=id==='codex'?parseToml(body):frontmatter(body);
142
- const role={name:safeString(fields.name,namePattern),model:optionalString(fields.model),effort:optionalString(id==='codex'?fields.model_reasoning_effort:fields.effort)};
142
+ const fields=id==='chatgpt'?parseToml(body):frontmatter(body);
143
+ const role={name:safeString(fields.name,namePattern),model:optionalString(fields.model),effort:optionalString(id==='chatgpt'?fields.model_reasoning_effort:fields.effort)};
143
144
  if(id==='kimi'){
144
145
  // APK's Markdown role files contain explicit routing guidance, not a
145
146
  // native model binding. Other prose is never interpreted as configuration.
@@ -154,7 +155,7 @@ async function rolesIn(directory,client,id,budget,models,depth=0){
154
155
  role.model=models.get(role.name)??role.model;
155
156
  if(fields.effort!==undefined){role.effort=null;warning(client,failure('UNSUPPORTED_ROLE_EFFORT'));}
156
157
  }
157
- if(id==='agy'){
158
+ if(id==='gemini'){
158
159
  if(role.model!==null&&!['pro','flash','flash_lite','inherit'].includes(role.model))throw failure('UNSUPPORTED_NATIVE_SETTING');
159
160
  role.enabled=fields.subagent??null;
160
161
  if(fields.effort!==undefined){role.effort=null;warning(client,failure('UNSUPPORTED_ROLE_EFFORT'));}
@@ -179,19 +180,19 @@ export async function readConfiguration({client=null,home,env=process.env,explic
179
180
  const report={schemaVersion:1,scope:'user',modelExecution:'not_checked',clients:{}};
180
181
  for(const id of client?[client]:CLIENTS){
181
182
  const root=path.resolve(!explicitHome&&env[ENV_ROOTS[id]]?env[ENV_ROOTS[id]]:path.join(home,ROOTS[id]));
182
- const c={status:'not_configured',defaults:{model:null,effort:null,enabled:null},concurrency:{value:null,kind:['codex','claude','grok'].includes(id)?'native_setting':'guidance'},roles:[],pool:[],warnings:[],apk:{status:'not_checked'}};
183
+ const c={status:'not_configured',defaults:{model:null,effort:null,enabled:null},concurrency:{value:null,kind:['chatgpt','claude','grok'].includes(id)?'native_setting':'guidance'},roles:[],pool:[],warnings:[],apk:{status:'not_checked'}};
183
184
  report.clients[id]=c;const budget={bytes:0,entries:0},models=new Map();
184
185
  try{
185
186
  await safePath(root);
186
- if(id!=='agy'){
187
+ if(id!=='gemini'){
187
188
  try{
188
189
  const text=await readFile(path.join(root,id==='claude'?'settings.json':'config.toml'),budget);
189
190
  if(text!==null){let native;if(id==='claude'){try{native=JSON.parse(text);}catch{throw failure('INVALID_JSON');}}else native=parseToml(text);extractSettings(c,id,native,env,models);}
190
191
  else if(id==='claude')extractSettings(c,id,{},env,models);
191
192
  }catch(error){warning(c,error);}
192
193
  }
193
- await rolesIn(path.join(root,id==='agy'?'config/agents':'agents'),c,id,budget,models);
194
- if(id==='kimi'||id==='agy'){try{await readGuidance(root,c,budget);}catch(error){warning(c,error);}}
194
+ await rolesIn(path.join(root,id==='gemini'?'config/agents':'agents'),c,id,budget,models);
195
+ if(id==='kimi'||id==='gemini'){try{await readGuidance(root,c,budget);}catch(error){warning(c,error);}}
195
196
  if(c.status!=='attention'&&(c.roles.length||c.pool.length||c.defaults.model!==null||c.defaults.effort!==null||c.defaults.enabled!==null||c.concurrency.value!==null||Object.keys(c.overrides??{}).length))c.status='configured';
196
197
  if(root===path.resolve(home,ROOTS[id])){
197
198
  try{const installed=await inspectConfiguration({client:id,home,stateDir});c.apk.status=installed.status;if(installed.status==='verified'){c.apk.releaseVersion=installed.releaseVersion;c.apk.preset=installed.preset;}}
package/src/install.mjs CHANGED
@@ -10,7 +10,7 @@ import catalog from '../models/catalog.json' with {type:'json'};
10
10
  import { parseToml, setTomlValue, restoreTomlValue, removeTomlValue } from './toml.mjs';
11
11
  import { nativeLocations, nativeRoles, instructionBlock, nativeWarnings, BEGIN, END } from './native.mjs';
12
12
 
13
- const CLIENTS = ['codex', 'claude', 'kimi', 'agy', 'grok'];
13
+ const CLIENTS = ['chatgpt', 'claude', 'kimi', 'gemini', 'grok'];
14
14
  const LIMIT = 1024 * 1024;
15
15
  const digest = value => value === null ? null : createHash('sha256').update(value).digest('hex');
16
16
  const json = value => JSON.stringify(value, null, 2) + '\n';
@@ -87,11 +87,15 @@ function ensureObject(parent,key) {
87
87
  if(!object(parent[key]))fail('UNSUPPORTED_CONFIG');
88
88
  return parent[key];
89
89
  }
90
- function jsonInstall(text,maxConcurrent,defaultModel) {
90
+ // Claude Code 2.1.316+ reads CLAUDE_CODE_SUBAGENT_EFFORT for subagents that name no effort; older versions ignore it.
91
+ // It copies the Coder effort, capped at high, so a Coder at xhigh or max does not raise every unnamed subagent.
92
+ const CLAUDE_SUBAGENT_EFFORTS=['low','medium','high'];
93
+ const claudeSubagentEffort=effort=>effort===null?null:CLAUDE_SUBAGENT_EFFORTS.includes(effort)?effort:'high';
94
+ function jsonInstall(text,maxConcurrent,defaultChoice) {
91
95
  const value=configJson(text),ops=[],created=[];
92
96
  if(!own(value,'env'))created.push(['env']);
93
- const env=ensureObject(value,'env');
94
- for(const [key,after] of [['CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS',String(maxConcurrent)],['CLAUDE_CODE_SUBAGENT_MODEL',defaultModel]]) {
97
+ const env=ensureObject(value,'env'),effort=claudeSubagentEffort(defaultChoice.effort);
98
+ for(const [key,after] of [['CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS',String(maxConcurrent)],['CLAUDE_CODE_SUBAGENT_MODEL',defaultChoice.model],...(effort===null?[]:[['CLAUDE_CODE_SUBAGENT_EFFORT',effort]])]) {
95
99
  if(own(env,key)&&typeof env[key]!=='string')fail('UNSUPPORTED_CONFIG');
96
100
  ops.push({kind:'value',path:['env',key],existed:own(env,key),before:env[key]??null,after});env[key]=after;
97
101
  }
@@ -155,7 +159,7 @@ function selectedAliases(preferences,selectedRoles=ROLE_IDS) {
155
159
  }
156
160
  function tomlSettings(client,preferences,maxConcurrent,selectedRoles=ROLE_IDS) {
157
161
  const worker=rolePreference(preferences,selectedRoles[0]);
158
- if(client==='codex')return [[['agents','max_concurrent_threads_per_session'],maxConcurrent],[['agents','default_subagent_model'],worker.model],...(worker.effort===null?[]:[[['agents','default_subagent_reasoning_effort'],worker.effort]])];
162
+ if(client==='chatgpt')return [[['agents','max_concurrent_threads_per_session'],maxConcurrent],[['agents','default_subagent_model'],worker.model],...(worker.effort===null?[]:[[['agents','default_subagent_reasoning_effort'],worker.effort]])];
159
163
  if(client==='grok')return [[['subagents','max_concurrent'],maxConcurrent],...selectedRoles.map(role=>[['subagents','models','apk-'+role],rolePreference(preferences,role).model]),[['subagents','models','general-purpose'],worker.model]];
160
164
  if(client!=='kimi')return [];
161
165
  const selected=selectedAliases(preferences,selectedRoles),roles=effectiveRoles(preferences,selectedRoles);
@@ -185,7 +189,7 @@ function scalar(op,target,type) {
185
189
  }
186
190
  function validateTomlSnapshot(file,client,schemaVersion,selectedRoles=ROLE_IDS) {
187
191
  if(!Array.isArray(file.ops)||!file.ops.every(op=>object(op)&&Array.isArray(op.path)&&op.path.every(key=>typeof key==='string')&&op.path.length<=4))fail('INVALID_RECEIPT');
188
- if(client==='codex') {
192
+ if(client==='chatgpt') {
189
193
  if(!(schemaVersion===2?[3]:[2,3]).includes(file.ops.length))fail('INVALID_RECEIPT');
190
194
  const cap=scalar(file.ops[0],['agents','max_concurrent_threads_per_session'],'number');
191
195
  if(cap<1||cap>32)fail('INVALID_RECEIPT');
@@ -235,12 +239,14 @@ function validateTomlSnapshot(file,client,schemaVersion,selectedRoles=ROLE_IDS)
235
239
  if(!aliases.includes(name)||pinned.has(name))fail('INVALID_RECEIPT');pinned.add(name);scalar(op,['models',name,'overrides','default_effort'],'string');
236
240
  }
237
241
  }
238
- function validateJsonSnapshot(file) {
239
- if(!Array.isArray(file.ops)||file.ops.length!==2||!Array.isArray(file.created)||file.created.length>1||!file.created.every(keys=>equal(keys,['env'])))fail('INVALID_RECEIPT');
240
- for(const[i,key]of ['CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS','CLAUDE_CODE_SUBAGENT_MODEL'].entries()) {
242
+ function validateJsonSnapshot(file,schemaVersion) {
243
+ // Receipts before format v4 never owned CLAUDE_CODE_SUBAGENT_EFFORT; v4 owns it only when the Coder effort was pinned.
244
+ if(!Array.isArray(file.ops)||!(schemaVersion===4?[2,3]:[2]).includes(file.ops.length)||!Array.isArray(file.created)||file.created.length>1||!file.created.every(keys=>equal(keys,['env'])))fail('INVALID_RECEIPT');
245
+ for(const[i,key]of ['CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS','CLAUDE_CODE_SUBAGENT_MODEL','CLAUDE_CODE_SUBAGENT_EFFORT'].slice(0,file.ops.length).entries()) {
241
246
  const op=file.ops[i];fields(op,['kind','path','existed','before','after']);
242
247
  if(op.kind!=='value'||!equal(op.path,['env',key])||typeof op.existed!=='boolean'||typeof op.after!=='string'||op.after.length>4096||(op.existed?typeof op.before!=='string'||op.before.length>4096:op.before!==null))fail('INVALID_RECEIPT');
243
248
  if(i===0&&(!/^[1-9]\d?$/.test(op.after)||Number(op.after)>32))fail('INVALID_RECEIPT');
249
+ if(i===2&&!CLAUDE_SUBAGENT_EFFORTS.includes(op.after))fail('INVALID_RECEIPT');
244
250
  }
245
251
  }
246
252
  function validateReceipt(r,plan) {
@@ -260,7 +266,7 @@ function validateReceipt(r,plan) {
260
266
  fields(file,['path','type','existed','prefix','block']);
261
267
  if(file.type!=='instructions'||!['','\n','\n\n'].includes(file.prefix)||typeof file.block!=='string'||file.block.length>65536||!file.block.startsWith(BEGIN+'\n')||!file.block.endsWith(END+'\n')||file.block.split(BEGIN).length!==2||file.block.split(END).length!==2)fail('INVALID_RECEIPT');
262
268
  } else if(plan.client==='claude') {
263
- fields(file,['path','type','existed','ops','created']);if(file.type!=='json')fail('INVALID_RECEIPT');validateJsonSnapshot(file);
269
+ fields(file,['path','type','existed','ops','created']);if(file.type!=='json')fail('INVALID_RECEIPT');validateJsonSnapshot(file,r.schemaVersion);
264
270
  } else {
265
271
  fields(file,['path','type','existed','ops']);if(file.type!=='toml')fail('INVALID_RECEIPT');validateTomlSnapshot(file,plan.client,r.schemaVersion,r.schemaVersion===4?r.selectedRoles:LEGACY_ROLE_IDS);
266
272
  }
@@ -389,15 +395,15 @@ export async function planInstall(options) {
389
395
  const cap=subagents?.max_concurrent;
390
396
  if(cap!==undefined&&(!Number.isSafeInteger(cap)||cap<1))fail('UNSUPPORTED_CONFIG');
391
397
  }
392
- if(plan.client==='codex') {
393
- if(native.agents&&own(native.agents,'max_threads'))fail('CODEX_CAP_ALIAS_CONFLICT');
398
+ if(plan.client==='chatgpt') {
399
+ if(native.agents&&own(native.agents,'max_threads'))fail('CHATGPT_CAP_ALIAS_CONFLICT');
394
400
  const cap=native.agents?.max_concurrent_threads_per_session;
395
401
  if(cap!==undefined&&(!Number.isSafeInteger(cap)||cap<1))fail('UNSUPPORTED_CONFIG');
396
402
  }
397
403
  }
398
404
  const files=[];plan.warnings=[...nativeWarnings(plan.client),...(resolvedAliases?['kimi-catalog-alias-resolved']:[])];
399
405
  if(plan.client==='claude') {
400
- const edited=jsonInstall(config.text,maxConcurrent,rolePreference(selected,roleIds[0]).model);nativeChange(l.config,config,edited.text);
406
+ const edited=jsonInstall(config.text,maxConcurrent,rolePreference(selected,roleIds[0]));nativeChange(l.config,config,edited.text);
401
407
  files.push({path:l.config,type:'json',existed:config.text!==null,ops:edited.ops,created:edited.created});
402
408
  } else if(config) {
403
409
  let text=config.text??'';const ops=[];
package/src/native.mjs CHANGED
@@ -4,25 +4,25 @@ import {ROLE_DEFINITIONS,ROLE_IDS} from './roles.mjs';
4
4
 
5
5
  export const BEGIN = '<!-- BEGIN agent-policy-kit -->';
6
6
  export const END = '<!-- END agent-policy-kit -->';
7
- const directory = {codex:'.codex',claude:'.claude',kimi:'.kimi-code',agy:'.gemini',grok:'.grok'};
7
+ const directory = {chatgpt:'.codex',claude:'.claude',kimi:'.kimi-code',gemini:'.gemini',grok:'.grok'};
8
8
  export function nativeHomeConflict(client,home,env) {
9
- const variable={codex:'CODEX_HOME',claude:'CLAUDE_CONFIG_DIR',kimi:'KIMI_CODE_HOME',grok:'GROK_HOME'}[client];
9
+ const variable={chatgpt:'CODEX_HOME',claude:'CLAUDE_CONFIG_DIR',kimi:'KIMI_CODE_HOME',grok:'GROK_HOME'}[client];
10
10
  const active=variable&&env[variable];
11
11
  return typeof active==='string'&&active.length>0&&path.resolve(active)!==path.resolve(home,directory[client]);
12
12
  }
13
13
  export function nativeLocations(client,home) {
14
14
  const root=path.join(home,directory[client]);
15
15
  return {
16
- config:client==='agy'?null:path.join(root,client==='claude'?'settings.json':'config.toml'),
16
+ config:client==='gemini'?null:path.join(root,client==='claude'?'settings.json':'config.toml'),
17
17
  instructions:path.join(root,client==='claude'?'CLAUDE.md':'AGENTS.md'),
18
- roles:Object.keys(ROLES).map(role=>path.join(root,client==='agy'?'config/agents':'agents',`apk-${role}.${client==='codex'?'toml':'md'}`))
18
+ roles:Object.keys(ROLES).map(role=>path.join(root,client==='gemini'?'config/agents':'agents',`apk-${role}.${client==='chatgpt'?'toml':'md'}`))
19
19
  };
20
20
  }
21
21
  export function nativeRoles(client,config,selectedRoles=ROLE_IDS) {
22
22
  return selectedRoles.map(role=>{
23
23
  const selected=rolePreference(config,role),name='apk-'+role;
24
24
  const {description,prompt}=ROLE_DEFINITIONS.find(value=>value.id===role);
25
- if(client==='codex') return Object.entries({name,description,model:selected.model,...(selected.effort===null?{}:{model_reasoning_effort:selected.effort}),developer_instructions:prompt}).map(([k,v])=>`${k} = ${JSON.stringify(v)}\n`).join('');
25
+ if(client==='chatgpt') return Object.entries({name,description,model:selected.model,...(selected.effort===null?{}:{model_reasoning_effort:selected.effort}),developer_instructions:prompt}).map(([k,v])=>`${k} = ${JSON.stringify(v)}\n`).join('');
26
26
  const fields=[`name: ${name}`,`description: ${JSON.stringify(description)}`];
27
27
  // Grok selects the model from config.toml [subagents.models]; read-only roles use the bundled plan permission mode.
28
28
  if(client==='grok') {
@@ -47,18 +47,18 @@ export function instructionBlock(client,config,maxConcurrent,selectedRoles=ROLE_
47
47
  for(const role of selectedRoles) {const selected=rolePreference(config,role);text.push(`For apk-${role}, select pool alias ${selected.model} (effort ${selected.effort??'native default'}).`);}
48
48
  text.push('Roles sharing an alias share its native effort default; an explicit effort pin for that alias also affects its native-default roles.');
49
49
  text.push("Use the Agent tool's model selection when spawning; the role file alone does not select a model. The concurrency limit is guidance; no native Agent cap is configured.");
50
- } else if(client==='agy') {
50
+ } else if(client==='gemini') {
51
51
  for(const role of selectedRoles)text.push(`When delegating to apk-${role}, use model tier ${rolePreference(config,role).model}.`);
52
52
  text.push('Per-role effort and a hard concurrency cap are not supported by the documented native agent settings.');
53
53
  }
54
54
  else if(client==='grok')text.push('Use the named role definitions for model selection. Per-role effort is not supported by the documented native agent settings; subagents use the main session effort. Explicit spawn settings and higher-priority instructions can supersede user defaults.');
55
55
  else if(client==='claude')text.push('Use the named role definitions for model and effort selection. Session, project, managed settings, environment overrides and some spawn paths can supersede these user defaults; the native cap applies to ordinary Agent-tool spawns with documented exceptions.');
56
56
  else text.push('Use the named role definitions for model and effort selection. Explicit spawn settings and higher-priority instructions can supersede user defaults.');
57
- if(client==='codex'||client==='claude')text.push('An omitted role effort uses the native client default; it does not pin a level.');
57
+ if(client==='chatgpt'||client==='claude')text.push('An omitted role effort uses the native client default; it does not pin a level.');
58
58
  if(config.avoidModels.length)text.push(`Avoid these models when delegating: ${config.avoidModels.join(', ')}. This is routing guidance.`);
59
59
  text.push(END);
60
60
  return text.join('\n')+'\n';
61
61
  }
62
62
  export function nativeWarnings(client) {
63
- return client==='kimi'?['kimi-concurrency-guidance-only','kimi-model-pool-routing-guidance']:client==='agy'?['agy-concurrency-guidance-only','agy-per-role-effort-unsupported']:client==='grok'?['grok-per-role-effort-unsupported','grok-native-precedence-limits']:client==='claude'?['claude-native-precedence-limits']:['codex-native-precedence-limits'];
63
+ return client==='kimi'?['kimi-concurrency-guidance-only','kimi-model-pool-routing-guidance']:client==='gemini'?['gemini-concurrency-guidance-only','gemini-per-role-effort-unsupported']:client==='grok'?['grok-per-role-effort-unsupported','grok-native-precedence-limits']:client==='claude'?['claude-native-precedence-limits']:['chatgpt-native-precedence-limits'];
64
64
  }
package/src/output.mjs CHANGED
@@ -1,17 +1,17 @@
1
1
  import {PRESETS} from './release.mjs';
2
- const LABELS={codex:'Codex',claude:'Claude Code',kimi:'Kimi',agy:'Antigravity',grok:'Grok Build'};
2
+ const LABELS={chatgpt:'ChatGPT',claude:'Claude',kimi:'Kimi',gemini:'Gemini',grok:'Grok'};
3
3
  const label=client=>LABELS[client]??'Tool';
4
4
  const code=value=>typeof value==='string'&&/^[A-Za-z][A-Za-z0-9_-]{0,127}$/.test(value)?value:'OPERATION_FAILED';
5
5
  const PRESET_LABELS=Object.fromEntries(PRESETS.map(entry=>[entry.id,entry.label]));
6
6
  const preset=value=>Object.hasOwn(PRESET_LABELS,value)?' · '+PRESET_LABELS[value]:'';
7
7
  const NOTES={
8
- 'codex-native-precedence-limits':'Project and explicit client settings can override these defaults.',
8
+ 'chatgpt-native-precedence-limits':'Project and explicit client settings can override these defaults.',
9
9
  'claude-native-precedence-limits':'Project and explicit client settings can override these defaults.',
10
10
  'kimi-concurrency-guidance-only':'Kimi: the concurrency limit is guidance.',
11
11
  'kimi-model-pool-routing-guidance':'Kimi: the parent selects the model alias for each role.',
12
- 'agy-concurrency-guidance-only':'Antigravity: the concurrency limit is guidance.',
13
- 'agy-per-role-effort-unsupported':'Antigravity: native tiers do not support per-role effort.',
14
- 'grok-per-role-effort-unsupported':'Grok Build: native agent settings do not support per-role effort.',
12
+ 'gemini-concurrency-guidance-only':'Gemini: the concurrency limit is guidance.',
13
+ 'gemini-per-role-effort-unsupported':'Gemini: native tiers do not support per-role effort.',
14
+ 'grok-per-role-effort-unsupported':'Grok: native agent settings do not support per-role effort.',
15
15
  'grok-native-precedence-limits':'Project and explicit client settings can override these defaults.',
16
16
  'kimi-catalog-alias-resolved':'Kimi: an existing matching model alias was selected.',
17
17
  };
package/src/policy.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import catalog from '../models/catalog.json' with {type:'json'};
2
2
  import {ROLES,ROLE_IDS,selectedRoleIds} from './roles.mjs';
3
3
  export {ROLES} from './roles.mjs';
4
- export const CLIENTS = Object.freeze(['codex', 'claude', 'kimi', 'agy', 'grok']);
4
+ export const CLIENTS = Object.freeze(['chatgpt', 'claude', 'kimi', 'gemini', 'grok']);
5
5
  export const rolePreference=(config,role)=>config.roles?.[role]??config[ROLES[role]];
6
6
  export const NATIVE_EFFORTS=Object.freeze(Object.fromEntries(CLIENTS.map(client=>[client,Object.freeze([...catalog.clients[client].efforts])])));
7
7
  const identifier = /^[A-Za-z0-9][A-Za-z0-9._:@/\[\]-]{0,127}$/;
@@ -27,7 +27,7 @@ export function validatePolicy(value) {
27
27
  for (const selected of [config.code,config.light,...Object.values(config.roles??{})]) {
28
28
  object(selected,['model','effort']); model(selected.model);
29
29
  if (client === 'kimi' && selected.model === 'primary') invalid();
30
- if(client==='agy'&&!['pro','flash','flash_lite','inherit'].includes(selected.model))invalid();
30
+ if(client==='gemini'&&!['pro','flash','flash_lite','inherit'].includes(selected.model))invalid();
31
31
  if(selected.effort!==null){
32
32
  if(!NATIVE_EFFORTS[client].includes(selected.effort))invalid();
33
33
  const known=catalog.clients[client].models.find(model=>model.id===selected.model);
package/src/release.mjs CHANGED
@@ -4,7 +4,7 @@ import {validatePolicy} from './policy.mjs';
4
4
 
5
5
  export const PACKAGE_VERSION=JSON.parse(fs.readFileSync(new URL('../package.json',import.meta.url),'utf8')).version;
6
6
  const canonical=value=>Array.isArray(value)?value.map(canonical):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(key=>[key,canonical(value[key])])):value;
7
- export const PRESETS=Object.freeze([{id:'fast',label:'Light',description:'Light models for every role. Limits last longest.'},{id:'balanced',label:'Standard',description:'Stronger models for code, review, tests and docs. Fits most plans.'},{id:'quality',label:'Top-tier',description:'High-end models for most roles. Uses limits fastest.',topModels:Object.freeze({codex:'gpt-6-astra',claude:'claude-fable-5-1',kimi:'kimi-code/k3.1-pro'})}].map(Object.freeze));
7
+ export const PRESETS=Object.freeze([{id:'fast',label:'Light',description:'Light models for every role. Limits last longest.'},{id:'balanced',label:'Standard',description:'Stronger models for code, review, tests and docs. Fits most plans.'},{id:'quality',label:'Top-tier',description:'High-end models for most roles. Uses limits fastest.',topModels:Object.freeze({chatgpt:Object.freeze({model:'gpt-6-astra',effort:'high'}),claude:Object.freeze({model:'claude-opus-5-5',effort:'xhigh'}),kimi:Object.freeze({model:'kimi-code/k3.1-pro',effort:'high'})})}].map(Object.freeze));
8
8
  export function presetPreferences(id='balanced'){
9
9
  if(!PRESETS.some(preset=>preset.id===id))throw Object.assign(new Error('Unknown preset.'),{code:'UNKNOWN_PRESET'});
10
10
  return validatePolicy(JSON.parse(fs.readFileSync(new URL(`../policies/${id}.json`,import.meta.url),'utf8')));
package/src/setup.mjs CHANGED
@@ -4,7 +4,7 @@ import { nativeHomeConflict } from './native.mjs';
4
4
  import { PACKAGE_VERSION } from './release.mjs';
5
5
  import { CLIENTS } from './policy.mjs';
6
6
 
7
- const ERRORS = new Set(['INVALID_PRESET','INVALID_RELEASE','LEGACY_RUNTIME_RECEIPT','INVALID_CLIENT','INVALID_POLICY','INVALID_JSON','INVALID_TOML','UNSUPPORTED_CONFIG','UNSUPPORTED_TOML_STRUCTURE','UNSUPPORTED_TOML_VALUE','CODEX_CAP_ALIAS_CONFLICT','OWNERSHIP_CONFLICT','INVALID_RECEIPT','OWNED_DRIFT','INSTALLATION_CONFLICT','POLICY_CONFLICT','UNSAFE_PATH','UNSAFE_FILE','STALE_PLAN','RECOVERY_REQUIRED','ROLLBACK_CONFLICT','INVALID_PLAN','PLATFORM_UNSUPPORTED','INSTALL_LOCKED','INSTALL_FAILED','EACCES','EPERM','ENOENT','ENOSPC','EIO','EROFS']);
7
+ const ERRORS = new Set(['INVALID_PRESET','INVALID_RELEASE','LEGACY_RUNTIME_RECEIPT','INVALID_CLIENT','INVALID_POLICY','INVALID_JSON','INVALID_TOML','UNSUPPORTED_CONFIG','UNSUPPORTED_TOML_STRUCTURE','UNSUPPORTED_TOML_VALUE','CHATGPT_CAP_ALIAS_CONFLICT','OWNERSHIP_CONFLICT','INVALID_RECEIPT','OWNED_DRIFT','INSTALLATION_CONFLICT','POLICY_CONFLICT','UNSAFE_PATH','UNSAFE_FILE','STALE_PLAN','RECOVERY_REQUIRED','ROLLBACK_CONFLICT','INVALID_PLAN','PLATFORM_UNSUPPORTED','INSTALL_LOCKED','INSTALL_FAILED','EACCES','EPERM','ENOENT','ENOSPC','EIO','EROFS']);
8
8
  const safeError = error => ERRORS.has(error?.code) ? error.code : 'SETUP_FAILED';
9
9
  function actions(client,warnings) {
10
10
  return warnings.map(code=>({client,code,automatable:false}));
package/src/share.mjs CHANGED
@@ -47,7 +47,7 @@ function shareRelease(release){
47
47
  for(const preset of release.presets){
48
48
  shareRecord(preset);if(typeof preset.id!=='string'||!preset.id||preset.id==='custom'||SHARE_UNSAFE.has(preset.id)||presets.has(preset.id))invalidShare();
49
49
  shareRecord(shareRecord(preset.preferences).clients);
50
- if(preset.topModels!==undefined&&Object.values(shareRecord(preset.topModels)).some(model=>typeof model!=='string'))invalidShare();
50
+ if(preset.topModels!==undefined)for(const top of Object.values(shareRecord(preset.topModels))){shareRecord(top,['model','effort']);if(typeof top.model!=='string'||(top.effort!==null&&typeof top.effort!=='string'))invalidShare();}
51
51
  presets.set(preset.id,preset);
52
52
  }
53
53
  if(!presets.has(release.preset))invalidShare();
@@ -55,7 +55,7 @@ function shareRelease(release){
55
55
  }
56
56
  function shareChoice(choice,id,metadata){
57
57
  shareRecord(choice,['model','effort']);
58
- if(typeof choice.model!=='string'||choice.model.length>128||(choice.effort!==null&&(['agy','grok'].includes(id)||!metadata.catalog[id].efforts.includes(choice.effort))))invalidShare();
58
+ if(typeof choice.model!=='string'||choice.model.length>128||(choice.effort!==null&&(['gemini','grok'].includes(id)||!metadata.catalog[id].efforts.includes(choice.effort))))invalidShare();
59
59
  return {model:choice.model,effort:choice.effort};
60
60
  }
61
61
  function sharePayload(payload,release,metadata){
@@ -93,12 +93,12 @@ function sharePayload(payload,release,metadata){
93
93
  if(!legacy&&tool.maxConcurrent!==(clients[id]?.maxConcurrent??preset.maxConcurrent))invalidShare();
94
94
  if(!Object.hasOwn(clients,id))invalidShare();
95
95
  const preferences=shareRecord(clients[id]),own=role=>preferences.roles&&Object.hasOwn(preferences.roles,role);
96
- // A preset's optional top model replaces every shared code role that has no role-specific choice, or none of them.
96
+ // A preset's optional top choice (model and effort) replaces every shared code role that has no role-specific choice, or none of them.
97
97
  const top=named.topModels&&Object.hasOwn(named.topModels,id)?named.topModels[id]:null;
98
- const topRoles=roles.filter(role=>metadata.roles.get(role).kind==='code'&&!own(role)),topped=top!==null&&topRoles.some(role=>choices[role].model===top);
98
+ const topRoles=roles.filter(role=>metadata.roles.get(role).kind==='code'&&!own(role)),topped=top!==null&&topRoles.some(role=>choices[role].model===top.model&&choices[role].effort===top.effort);
99
99
  for(const role of roles){
100
100
  const expected=own(role)?preferences.roles[role]:preferences[metadata.roles.get(role).kind];
101
- const choice=shareChoice(expected,id,metadata);if(topped&&topRoles.includes(role))choice.model=top;
101
+ const choice=shareChoice(expected,id,metadata);if(topped&&topRoles.includes(role)){choice.model=top.model;choice.effort=top.effort;}
102
102
  if(choice.model!==choices[role].model||choice.effort!==choices[role].effort)invalidShare();
103
103
  }
104
104
  }
@@ -1,17 +0,0 @@
1
- # Architecture
2
-
3
- APK runs only for configuration commands. It detects installed CLIs, reads release/custom preferences, writes native files and exits. Native agent operation never imports or launches APK.
4
-
5
- One npm version is one configuration release. src/release.mjs exposes its presets, catalog date and notes. policies contains Light/Standard/Top-tier data (ids fast/balanced/quality); models/catalog.json is the dated native model/effort snapshot. The self-contained page is generated from that same version and data. src/explicit.mjs supplies the pure command codec to Node and is embedded in the page at build time. Commands always expand selected role values, even for presets. The CLI converts complete maps to normal validated policy. Required code/light fallbacks copy the first explicit role and are unused by the complete per-role map. No browser/backend/provider request is needed to compose a command.
6
-
7
- src/share.mjs validates and encodes the visible form state into the URL fragment. It never runs native operations or stores clipboard history. The page restores only release-compatible state, then applies normal command validation.
8
-
9
- src/roles.mjs supplies the shared role descriptions/prompts for the page and native generation. src/policy.mjs validates code/light defaults, optional selectedRoles and per-client role overrides. Native generation uses selected roles only. The first selected role in catalog order supplies the native subagent default. Effort null means no explicit pin. Documented levels depend on model and client. Selected Kimi aliases merge explicit effort and resolve only to provisioned entries. Ambiguity fails without writes.
10
-
11
- The fixed native role filenames remain apk-worker/reviewer/explorer/tester/writer/routine. Their presence does not require delegation. The main agent decides whether helpers are useful; main models and disabled features are preserved.
12
-
13
- Receipt v4 stores the selected role set, bounded ownership snapshots and actual release/source/preset provenance. It verifies that the owned file set matches selectedRoles. Inspection/removal checks old hashes, blocks and typed operations without current templates or catalog validation. Historical v3 and v2 native snapshots remain supported. v2 has unknown release provenance. Unsupported layouts/runtime receipts are refused.
14
-
15
- Explicit replacement virtually undoes owned changes to the original user baseline, renders the target, and commits final changes against actual current preimages. The existing transaction protects complete read dependencies, locks, staging, rollback and competing edits. Final uninstall restores original user values and keeps unrelated later edits. Receipts are applied truth; canonical policy is the latest requested snapshot. Per-client partial outcomes are explicit.
16
-
17
- There is one TOML parser dependency and no CLI compilation. The static page's data injection is a deterministic release-preparation step. Generated native files have no package/source/state dependency; npm package removal does not remove saved native settings.
@@ -1,23 +0,0 @@
1
- # Native configuration support
2
-
3
- | Client | Written native configuration | Limits |
4
- |---|---|---|
5
- | Codex | User role TOMLs, native subagent defaults/concurrency, routing instructions | Higher-priority project/explicit settings still follow Codex rules |
6
- | Claude Code | User role Markdown with model/effort, native default/concurrency env, routing instructions | Force settings and native execution-path exceptions can override the intended mapping |
7
- | Kimi Code | User role Markdown, existing secondary-model pool, per-alias effort, routing instructions | Model-role selection and max concurrency are guidance; provisioned aliases/capabilities are required |
8
- | Antigravity | User role Markdown with Pro/Flash/Flash Lite/inherit tiers and routing instructions | No concrete provider pin, documented per-role effort, or universal native max-three cap |
9
- | Grok Build | User role Markdown (read-only roles use permission_mode plan), per-role models and a general-purpose fallback in config.toml, native max_concurrent, routing instructions | No documented per-role effort; subagents use the main session effort. Higher-priority project/explicit settings still follow Grok Build rules |
10
-
11
- These are preferences implemented through native features. APK does not participate in later agent operation and does not enforce policy on every request. avoidModels is guidance. No hook, wrapper or proxy is installed to bypass native limits.
12
-
13
- Global configuration can be superseded by a project's native settings, session flags, environment or plugins. A fresh client session may be needed. Configured files, native loading and actual provider model execution are separate evidence.
14
-
15
- macOS is the local acceptance host. Linux has a source-isolation gate using bubblewrap and a hosted workflow prepared; hosted runs are pending. Windows application is explicitly unsupported.
16
-
17
- Primary references checked for this design:
18
-
19
- - [Codex native subagents](https://learn.chatgpt.com/docs/agent-configuration/subagents).
20
- - [Claude native subagents](https://code.claude.com/docs/en/sub-agents).
21
- - [Kimi configuration](https://github.com/MoonshotAI/kimi-code/blob/main/docs/en/configuration/config-files.md) and [agent definitions](https://github.com/MoonshotAI/kimi-code/blob/main/docs/en/customization/agents.md).
22
- - [Antigravity native subagents](https://antigravity.google/docs/subagents?tab=cli).
23
- - [Grok Build subagents](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/16-subagents.md), observed with Grok Build 1.0.70 and an isolated GROK_HOME.
@@ -1,44 +0,0 @@
1
- # Configuration page
2
-
3
- Open site/index.html in Google Chrome. The page is one self-contained HTML file. It has no backend, telemetry, asset requests or native account access. It embeds this release's model catalog, presets, role definitions, getacc.dev palette and licensed Public Sans font.
4
-
5
- ## Four steps
6
-
7
- 1. Add the subagent roles you need. A green check confirms a nonempty role selection. Each question-mark button explains its description and native prompt. Removing a role also removes its configuration rows. Readding it preserves its choices in the current page session.
8
- 2. Add your coding tools. A green check confirms a nonempty tool selection. New tools append to the list. The first becomes active. From the second onward, All is active by default. A manually selected tool or All stays active during later additions. All configures the selected tools together. The CLI, not the browser, detects which selected tools are installed.
9
- 3. Choose a work mode. No mode is selected initially. Each selected tool needs a chosen mode before the command becomes available. The first choice has no replacement warning. Individual tool details show every selected role's model and supported effort, followed by that tool's parallel limit. The shared All view hides these controls. All offers shared presets. Custom can be selected manually and is also set by model or effort edits. Each tool shows its own mode beneath its name. Individual presets warn before changing that tool. Replacing genuinely edited values, mixed per-tool bases or partially chosen modes from All warns before changing all selected tools. Selecting Custom alone without changing values does not create an overwrite warning. Cancel preserves the choices.
10
- 4. Copy the setup command and run it in a terminal. Incomplete selections identify the missing steps with links. Invalid model settings or per-tool limits disable copying and clear the previous command. Clipboard failure selects the full command for manual copying.
11
-
12
- The page starts with every subagent role selected and no coding tool. A completed third step requires selected modes and valid model settings. A fourth green check means the current command was successfully copied, not installed. Configuration changes clear that check; opening another tool without changing values preserves it. Reset form clears selections, modes, copy status and the URL fragment without changing any installed native files. Light uses 2 parallel subagents, Standard uses 3 and Top-tier uses 2. Selecting a preset restores its models, effort and parallel limit for the targeted tools. Editing a limit marks only that tool Custom. Effort buttons fill their section and show supported explicit levels. A dash means no native effort setting. Unpinned values remain unpinned until changed. Kimi roles sharing a model alias share its effort. Antigravity offers only native tiers. Other tools also accept custom native model IDs, subject to CLI and account support.
13
-
14
- The command is pinned to this page's package version. It always expands every selected tool and role into visible model/effort arguments. Presets fill the form but do not shorten or hide the command. Tools follow their selected order. --roles carries the selected role IDs and --max-concurrent carries the shared fallback. Optional --codex-max-concurrent, --claude-max-concurrent, --kimi-max-concurrent and --antigravity-max-concurrent carry independent overrides. Nonempty --avoid-TOOL lists preserve existing model-avoidance guidance. Omitted tools are not configured.
15
-
16
- Each tool uses role=model:effort entries. :native means no explicit effort pin. Antigravity uses role=tier and Grok Build uses role=model, both without an effort suffix. The parser splits at the last colon, preserving model IDs that contain colons or @. Every selected role must be present exactly once for every selected tool. The generated command reports custom provenance because its values are explicit, even when a preset filled the form.
17
-
18
- The complete command is visible in a read-only field that grows to fit its contents. Copy preserves the visible POSIX shell line continuations. Soft wrapping adds no additional newlines. [Native field sizing](https://developer.chrome.com/docs/css-ui/css-field-sizing) handles content and window-size changes without a JavaScript resize helper. The page never executes the command or grants model access.
19
-
20
- Role/tool changes use native View Transitions with a reduced-motion setting and a synchronous fallback. Section hints are conditional: role guidance after selection, add-more guidance for one tool, individual-configuration guidance for two or more tools, mode guidance when available, and Copied in the fourth heading. Changed hints, step markers, editing-scope labels and per-tool mode captions hold a soft highlight for two seconds, then fade for two seconds. Tool-caption nodes are reused so switching the editing view does not interrupt a pulse. Identical state does not repeat the animation. Labels retain their size during layout changes. No animation dependency is installed.
21
-
22
- ## Share a setup
23
-
24
- The Max at once field appears only in individual coding-tool details. It accepts whole numbers from 1 to 32. Each tool retains its own limit while switching the editing view. Model-preset selection restores that tool's preset limit. Invalid selected-tool limits block the command and copy action. Invalid cached settings on unselected tools do not block selected tools. Kimi and Antigravity use the value as guidance rather than a native hard cap.
25
-
26
- Each form edit replaces the current URL fragment with its visible state. Copy the page address to share it. Version 2 fragments contain roles, tool order, mode/base, selected model/effort pairs, each tool's limit and editing scope. Version 1 scalar-limit links remain readable. Their exact count is preserved; a count that differs from the current preset makes the restored tool Custom. It excludes clipboard completion and unselected cached choices. Opening a link restores the form, including unfinished choices and model drafts. Command validation still blocks incomplete or invalid settings.
27
-
28
- src/share.mjs is the pure browser/Node codec. It uses versioned base64url JSON with strict IDs, fields, duplicate-key, size, UTF-8 and release checks. The fragment stays in the browser and needs no database or config lookup service. Invalid or mismatched-release links show an error and cannot produce an applicable command. Missing-step links scroll/focus without overwriting the setup fragment.
29
-
30
- Public friend links require this static page to be hosted, for example on getacc.dev. A local file URL is machine-local; localhost acceptance proves restoration behavior, not public deployment. Each published release should retain its versioned page so old links can use the matching release.
31
-
32
- ## Build and host
33
-
34
- Run npm run build:site after changing the template, shared CSS, role definitions, command codec, version, presets, catalog or changelog. npm run check rejects a stale generated page. Source assets are in site/template.html, site/base.css and site/fonts/. src/explicit.mjs supplies the same pure codec to the CLI and page. The generated HTML needs no server or build step to open.
35
-
36
- Serve site/index.html from any static host. It can become part of getacc.dev without a service or proxy. Publish immutable pages at versioned release paths. A latest landing URL can point to the current one. Set repository/homepage URLs only after the owner creates them.
37
-
38
- The current version is unreleased. Its page labels that status. Before npm publication, use the verified local tarball as described in the [AI operator guide](../AGENT_INSTALL.md). The checks do not publish or deploy anything.
39
-
40
- The introduction includes a collapsed “How it works and why you need it” explanation aligned with the README. Native details/summary supports keyboard toggling; opening/closing takes 200 ms and honors reduced motion. The hosted versioned URL preserves release-specific shared links.
41
-
42
- The footer credits Mykola Maksymenko and links to his supplied LinkedIn profile, Agents Can Communicate and Automatis Skills. Package author metadata is the source for the visible byline, title, author tags and static JSON-LD Person/project/page relationship. The canonical URL follows the homepage origin and bundled release version. These links load only when followed; no social widgets or runtime metadata requests are added.
43
-
44
- The latest hosted root can update its interface and author metadata from reviewed main independently of npm publication. Published versioned pages remain exact snapshots. Configuration-sensitive changes must match a published package before the host build accepts them. See [deployment](deployment.md).